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