1 /***************************************************************************
2  *                     Copyright © 2020 - Arianne                          *
3  ***************************************************************************
4  ***************************************************************************
5  *                                                                         *
6  *   This program is free software; you can redistribute it and/or modify  *
7  *   it under the terms of the GNU General Public License as published by  *
8  *   the Free Software Foundation; either version 2 of the License, or     *
9  *   (at your option) any later version.                                   *
10  *                                                                         *
11  ***************************************************************************/
12 package games.stendhal.server.core.scripting.lua;
13 
14 import static games.stendhal.common.constants.Actions.SUMMON;
15 
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.LinkedList;
19 import java.util.List;
20 
21 import org.luaj.vm2.LuaFunction;
22 import org.luaj.vm2.LuaTable;
23 import org.luaj.vm2.LuaValue;
24 
25 import games.stendhal.server.core.engine.GameEvent;
26 import games.stendhal.server.core.engine.SingletonRepository;
27 import games.stendhal.server.core.engine.StendhalRPZone;
28 import games.stendhal.server.core.pathfinder.FixedPath;
29 import games.stendhal.server.core.pathfinder.Node;
30 import games.stendhal.server.core.rp.StendhalRPAction;
31 import games.stendhal.server.core.rule.EntityManager;
32 import games.stendhal.server.core.scripting.ScriptInLua.LuaLogger;
33 import games.stendhal.server.entity.RPEntity;
34 import games.stendhal.server.entity.creature.Creature;
35 import games.stendhal.server.entity.creature.RaidCreature;
36 import games.stendhal.server.entity.item.Item;
37 import games.stendhal.server.entity.item.StackableItem;
38 import games.stendhal.server.entity.mapstuff.sign.Reader;
39 import games.stendhal.server.entity.mapstuff.sign.ShopSign;
40 import games.stendhal.server.entity.mapstuff.sign.Sign;
41 import games.stendhal.server.entity.npc.ChatAction;
42 import games.stendhal.server.entity.npc.ChatCondition;
43 import games.stendhal.server.entity.npc.ConversationStates;
44 import games.stendhal.server.entity.npc.SilentNPC;
45 import games.stendhal.server.entity.npc.SpeakerNPC;
46 import games.stendhal.server.entity.player.Player;
47 
48 
49 /**
50  * Exposes some entity classes & functions to Lua.
51  */
52 public class LuaEntityHelper {
53 
54 	private static LuaLogger logger = LuaLogger.get();
55 
56 	public static final EntityManager manager = SingletonRepository.getEntityManager();
57 
58 	private static final LuaConditionHelper conditionHelper = LuaConditionHelper.get();
59 	private static final LuaActionHelper actionHelper = LuaActionHelper.get();
60 
61 	private static LuaEntityHelper instance;
62 
63 
64 	/**
65 	 * Retrieves the static instance.
66 	 *
67 	 * @return
68 	 * 		Static EntityHelper instance.
69 	 */
get()70 	public static LuaEntityHelper get() {
71 		if (instance == null) {
72 			instance = new LuaEntityHelper();
73 		}
74 
75 		return instance;
76 	}
77 
78 	/**
79 	 * Converts a table of coordinates to a FixedPath instance.
80 	 *
81 	 * @param table
82 	 * 		Table containing coordinates.
83 	 * @param loop
84 	 * 		If <code>true</code>, the path should loop.
85 	 * @return
86 	 * 		New FixedPath instance.
87 	 */
tableToPath(final LuaTable table, final boolean loop)88 	private static FixedPath tableToPath(final LuaTable table, final boolean loop) {
89 		if (!table.istable()) {
90 			logger.error("Entity path must be a table");
91 			return null;
92 		}
93 
94 		List<Node> nodes = new LinkedList<Node>();
95 
96 		// Lua table indexing begins at 1
97 		int index;
98 		for (index = 1; index <= table.length(); index++) {
99 			LuaValue point = table.get(index);
100 			if (point.istable()) {
101 				LuaValue luaX = ((LuaTable) point).get(1);
102 				LuaValue luaY = ((LuaTable) point).get(2);
103 
104 				if (luaX.isinttype() && luaY.isinttype()) {
105 					Integer X = luaX.toint();
106 					Integer Y = luaY.toint();
107 
108 					nodes.add(new Node(X, Y));
109 				} else {
110 					logger.error("Path nodes must be integers");
111 					return null;
112 				}
113 			} else {
114 				logger.error("Invalid table data in entity path");
115 				return null;
116 			}
117 		}
118 
119 		return new FixedPath(nodes, loop);
120 	}
121 
122 	/**
123 	 * Retrieves a logged in Player.
124 	 *
125 	 * @param name
126 	 * 		Name of player.
127 	 * @return
128 	 * 		Logged in player or <code>null</code>.
129 	 */
getPlayer(final String name)130 	public Player getPlayer(final String name) {
131 		return SingletonRepository.getRuleProcessor().getPlayer(name);
132 	}
133 
134 	/**
135 	 * Retrieves an existing SpeakerNPC.
136 	 *
137 	 * FIXME: cannot cast to LuaSpeakerNPC, so specialized methods will not work
138 	 * 			with entities retrieved from this method that are not instances
139 	 * 			of LuaSpeakerNPC.
140 	 *
141 	 * @param name
142 	 * 		Name of NPC.
143 	 * @return
144 	 * 		SpeakerNPC instance or <code>null</code>.
145 	 */
getNPC(final String name)146 	public SpeakerNPC getNPC(final String name) {
147 		final SpeakerNPC npc = SingletonRepository.getNPCList().get(name);
148 
149 		if (npc == null) {
150 			logger.warn("NPC \"" + name + "\" not found");
151 			return null;
152 		}
153 		if (!(npc instanceof LuaSpeakerNPC)) {
154 			logger.warn("Lua call to entities:getNPC did not return LuaSpeakerNPC instance, specialized methods will fail with NPC \"" + npc.getName() + "\"");
155 		}
156 
157 		return npc;
158 	}
159 
160 	/**
161 	 * Retrieves a registered Item.
162 	 *
163 	 * @param name
164 	 * 		Name of the item.
165 	 * @return
166 	 * 		Item instance or <code>null</code> if not a registered item.
167 	 */
getItem(final String name)168 	public Item getItem(final String name) {
169 		return manager.getItem(name);
170 	}
171 
172 	/**
173 	 * Retrieves a registered StackableItem.
174 	 *
175 	 * @param name
176 	 * 		Name of the item.
177 	 * @return
178 	 * 		StackableItem instance or <code>null</code> if not a registered stackable item.
179 	 */
getStackableItem(final String name)180 	public StackableItem getStackableItem(final String name) {
181 		final Item item = getItem(name);
182 		if (item instanceof StackableItem) {
183 			return (StackableItem) item;
184 		}
185 
186 		return null;
187 	}
188 
189 	/**
190 	 * Creates a new SpeakerNPC instance.
191 	 *
192 	 * @param name
193 	 * 		Name of new NPC.
194 	 * @return
195 	 * 		New SpeakerNPC instance.
196 	 */
createSpeakerNPC(final String name)197 	public LuaSpeakerNPC createSpeakerNPC(final String name) {
198 		return new LuaSpeakerNPC(name);
199 	}
200 
201 	/**
202 	 * Creates a new SilentNPC instance.
203 	 *
204 	 * @return
205 	 * 		New SilentNPC instance.
206 	 */
createSilentNPC()207 	public LuaSilentNPC createSilentNPC() {
208 		return new LuaSilentNPC();
209 	}
210 
211 	/**
212 	 * Helper function for setting an NPCs path.
213 	 *
214 	 * @param entity
215 	 * 		The NPC instance of which path is being set.
216 	 * @param table
217 	 * 		Lua table with list of coordinates representing nodes.
218 	 */
219 	@Deprecated
setPath(final RPEntity entity, final LuaTable table, Boolean loop)220 	public void setPath(final RPEntity entity, final LuaTable table, Boolean loop) {
221 		logger.warn("entities:setPath is deprecated. Call \"setPath\" directly from the entity instance.");
222 
223 		if (loop == null) {
224 			loop = false;
225 		}
226 
227 		entity.setPath(tableToPath(table, loop));
228 	}
229 
230 	/**
231 	 * Helper function for setting an NPCs path & starting position.
232 	 *
233 	 * @param entity
234 	 * 		The NPC instance of which path is being set.
235 	 * @param table
236 	 * 		Lua table with list of coordinates representing nodes.
237 	 */
238 	@Deprecated
setPathAndPosition(final RPEntity entity, final LuaTable table, Boolean loop)239 	public void setPathAndPosition(final RPEntity entity, final LuaTable table, Boolean loop) {
240 		logger.warn("entities:setPathAndPosition is deprecated. Call \"setPathAndPosition\" directly from the entity instance.");
241 
242 		if (loop == null) {
243 			loop = false;
244 		}
245 
246 		entity.setPathAndPosition(tableToPath(table, loop));
247 	}
248 
249 	/**
250 	 * Sets a LuaGuidedEntity's path using a table.
251 	 *
252 	 * @param entity
253 	 * 		The NPC instance of which path is being set.
254 	 * @param table
255 	 * 		Lua table with list of coordinates representing nodes.
256 	 */
setEntityPath(final LuaGuidedEntity entity, final LuaTable table, Boolean loop)257 	private static void setEntityPath(final LuaGuidedEntity entity, final LuaTable table, Boolean loop) {
258 		if (loop == null) {
259 			loop = false;
260 		}
261 
262 		entity.setPath(tableToPath(table, loop));
263 	}
264 
265 	/**
266 	 * Sets a LuaGuidedEntity's path & starting position using a table.
267 	 *
268 	 * @param entity
269 	 * 		The NPC instance of which path is being set.
270 	 * @param table
271 	 * 		Lua table with list of coordinates representing nodes.
272 	 */
setEntityPathAndPosition(final LuaGuidedEntity entity, final LuaTable table, Boolean loop)273 	private static void setEntityPathAndPosition(final LuaGuidedEntity entity, final LuaTable table, Boolean loop) {
274 		if (loop == null) {
275 			loop = false;
276 		}
277 
278 		entity.setPathAndPosition(tableToPath(table, loop));
279 	}
280 
281 	/**
282 	 * Creates a new Sign entity.
283 	 *
284 	 * @return
285 	 * 		New Sign instance.
286 	 */
createSign()287 	public Sign createSign() {
288 		return createSign(true);
289 	}
290 
291 	/**
292 	 * Creates a new Sign entity.
293 	 *
294 	 * @param visible
295 	 * 		If <code>false</code>, sign does not have a visual representation.
296 	 * @return
297 	 * 		New Sign instance.
298 	 */
createSign(final boolean visible)299 	public Sign createSign(final boolean visible) {
300 		if (visible) {
301 			return new Sign();
302 		}
303 
304 		return new Reader();
305 	}
306 
307 	/**
308 	 * Creates a new ShopSign entity.
309 	 *
310 	 * @param name
311 	 * 		The shop name.
312 	 * @param title
313 	 * 		The sign title.
314 	 * @param caption
315 	 * 		The caption above the table.
316 	 * @param seller
317 	 * 		<code>true</code>, if this sign is for items sold by an NPC (defaults to <code>true</code> if <code>null</code>).
318 	 * @return
319 	 * 		New ShopSign instance.
320 	 */
createShopSign(final String name, final String title, final String caption, Boolean seller)321 	public ShopSign createShopSign(final String name, final String title, final String caption, Boolean seller) {
322 		// default to seller
323 		if (seller == null) {
324 			seller = true;
325 		}
326 
327 		return new ShopSign(name, title, caption, seller);
328 	}
329 
330 	/**
331 	 * Summons a creature into the world.
332 	 *
333 	 * FIXME: "coercion error java.lang.IllegalArgumentException: argument type mismatch" occurs if "raid" is LuaBoolean or LuaValue type
334 	 *
335 	 * @param name
336 	 * 		Name of creature to be summoned.
337 	 * @param zone
338 	 * 		Name of zone where creature should be summoned.
339 	 * @param x
340 	 * 		Horizontal position of summon location.
341 	 * @param y
342 	 * 		Vertical position of summon location.
343 	 * @param summoner
344 	 * 		Name of entity doing the summoning.
345 	 * @param raid
346 	 * 		(boolean) Whether or not the creature should be a RaidCreature instance (default: true)
347 	 * @return
348 	 * 		0 = success
349 	 * 		1 = creature not found
350 	 * 		2 = zone not found
351 	 */
summonCreature(final String name, final String zoneName, final int x, final int y, String summoner, final boolean raid)352 	private int summonCreature(final String name, final String zoneName, final int x, final int y, String summoner, final boolean raid) {
353 		if (summoner == null) {
354 			logger.warn("Unknown summoner");
355 			summoner = getClass().getName();
356 		}
357 
358 		if (!manager.isCreature(name)) {
359 			return 1;
360 		}
361 
362 		final StendhalRPZone zone = SingletonRepository.getRPWorld().getZone(zoneName);
363 		if (zone == null) {
364 			return 2;
365 		}
366 
367 		if (raid) {
368 			final RaidCreature creature = new RaidCreature(manager.getCreature(name));
369 			StendhalRPAction.placeat(zone, creature, x, y);
370 		} else {
371 			final Creature creature = manager.getCreature(name); // use standard creatures
372 			StendhalRPAction.placeat(zone, creature, x, y);
373 		}
374 
375 		new GameEvent(summoner, SUMMON, name).raise();
376 
377 		return 0;
378 	}
379 
summonCreature(final LuaTable table)380 	public int summonCreature(final LuaTable table) {
381 		final String name = table.get("name").tojstring();
382 		final String zoneName = table.get("zone").tojstring();
383 		final Integer x = table.get("x").toint();
384 		final Integer y = table.get("y").toint();
385 		String summoner = null;
386 		boolean raid = true;
387 
388 		final LuaValue checksummoner = table.get("summoner");
389 		if (checksummoner != null && !checksummoner.isnil() && checksummoner.isstring()) {
390 			summoner = checksummoner.tojstring();
391 		}
392 
393 		final LuaValue checkraid = table.get("raid");
394 		if (checkraid != null && !checkraid.isnil() && checkraid.isboolean()) {
395 			raid = checkraid.toboolean();
396 		}
397 
398 		return summonCreature(name, zoneName, x, y, summoner, raid);
399 	}
400 
401 
402 	/**
403 	 * A special interface that overloads setPath & setPathAndPosition
404 	 * methods to accept a Lua table as parameter argument.
405 	 */
406 	private interface LuaGuidedEntity {
407 
setPath(final FixedPath path)408 		public void setPath(final FixedPath path);
409 
setPath(final LuaTable table, Boolean loop)410 		public void setPath(final LuaTable table, Boolean loop);
411 
setPathAndPosition(final FixedPath path)412 		public void setPathAndPosition(final FixedPath path);
413 
setPathAndPosition(final LuaTable table, Boolean loop)414 		public void setPathAndPosition(final LuaTable table, Boolean loop);
415 	}
416 
417 	private class LuaSpeakerNPC extends SpeakerNPC implements LuaGuidedEntity {
418 
LuaSpeakerNPC(final String name)419 		public LuaSpeakerNPC(final String name) {
420 			super(name);
421 		}
422 
423 		/**
424 		 * Additional method to support transitions using Lua tables.
425 		 *
426 		 * @param states
427 		 * 		The conversation state(s) the entity should be in to trigger response.
428 		 * 		Can be ConversationStates enum value or LuaTable of ConversationStates.
429 		 * @param triggers
430 		 * 		String or LuaTable of strings to trigger response.
431 		 * @param conditions
432 		 * 		ChatCondition instance or LuaTable of ChatCondition instances.
433 		 * @param nextState
434 		 * 		Conversation state to set entity to after response.
435 		 * @param reply
436 		 * 		The NPC's response or <code>null</code>
437 		 * @param actions
438 		 * 		ChatAction instance or LuaTable of ChatAction instances.
439 		 */
440 		@SuppressWarnings({ "unused", "unchecked" })
add(final Object states, final Object triggers, final Object conditions, final ConversationStates nextState, final String reply, final Object actions)441 		public void add(final Object states, final Object triggers, final Object conditions,
442 				final ConversationStates nextState, final String reply, final Object actions) {
443 
444 			ConversationStates[] listenStates = null;
445 			List<String> listenTriggers = null;
446 			ChatCondition listenConditions = null;
447 			ChatAction listenActions = null;
448 
449 			if (states != null) {
450 				if (states instanceof ConversationStates) {
451 					listenStates = Arrays.asList((ConversationStates) states).toArray(new ConversationStates[] {});
452 				} else {
453 					final List<ConversationStates> tmp = new LinkedList<>();
454 					final LuaTable table = (LuaTable) states;
455 					for (final LuaValue idx: table.keys()) {
456 						final ConversationStates state = (ConversationStates) table.get(idx).touserdata(ConversationStates.class);
457 
458 						if (state == null) {
459 							logger.error("Invalid ConversationStates data");
460 							continue;
461 						}
462 
463 						tmp.add(state);
464 					}
465 
466 					listenStates = tmp.toArray(new ConversationStates[] {});
467 				}
468 			}
469 
470 			if (triggers != null) {
471 				listenTriggers = new ArrayList<>();
472 				if (triggers instanceof String) {
473 					listenTriggers.add((String) triggers);
474 				} else if (triggers instanceof List) {
475 					listenTriggers.addAll((List<String>) triggers);
476 				} else {
477 					final LuaTable table = (LuaTable) triggers;
478 					for (final LuaValue idx: table.keys()) {
479 						listenTriggers.add(table.get(idx).tojstring());
480 					}
481 				}
482 			}
483 
484 			if (conditions != null) {
485 				if (conditions instanceof ChatCondition) {
486 					listenConditions = (ChatCondition) conditions;
487 				} else if (conditions instanceof LuaFunction) {
488 					listenConditions = conditionHelper.create((LuaFunction) conditions);
489 				} else {
490 					listenConditions = conditionHelper.andCondition((LuaTable) conditions);
491 				}
492 			}
493 
494 			if (actions != null) {
495 				if (actions instanceof ChatAction) {
496 					listenActions = (ChatAction) actions;
497 				} else if (actions instanceof LuaFunction) {
498 					listenActions = actionHelper.create((LuaFunction) actions);
499 				} else {
500 					listenActions = actionHelper.multiple((LuaTable) actions);
501 				}
502 			}
503 
504 			add(listenStates, listenTriggers, listenConditions, nextState, reply, listenActions);
505 		}
506 
507 		@Override
setPath(LuaTable table, Boolean loop)508 		public void setPath(LuaTable table, Boolean loop) {
509 			LuaEntityHelper.setEntityPath(this, table, loop);
510 		}
511 
512 		@Override
setPathAndPosition(LuaTable table, Boolean loop)513 		public void setPathAndPosition(LuaTable table, Boolean loop) {
514 			LuaEntityHelper.setEntityPathAndPosition(this, table, loop);
515 		}
516 	}
517 
518 	private class LuaSilentNPC extends SilentNPC implements LuaGuidedEntity {
519 
520 		@Override
setPath(LuaTable table, Boolean loop)521 		public void setPath(LuaTable table, Boolean loop) {
522 			LuaEntityHelper.setEntityPath(this, table, loop);
523 		}
524 
525 		@Override
setPathAndPosition(LuaTable table, Boolean loop)526 		public void setPathAndPosition(LuaTable table, Boolean loop) {
527 			LuaEntityHelper.setEntityPathAndPosition(this, table, loop);
528 		}
529 
530 	}
531 }
532