1 /*************************************************************************** 2 * (C) Copyright 2003-2016 - Stendhal * 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.actions.equip; 13 14 import java.util.Arrays; 15 import java.util.List; 16 17 import org.apache.log4j.Logger; 18 19 import games.stendhal.common.EquipActionConsts; 20 import games.stendhal.server.actions.ItemAccessPermissions; 21 import games.stendhal.server.core.engine.ItemLogger; 22 import games.stendhal.server.core.engine.SingletonRepository; 23 import games.stendhal.server.core.events.EquipListener; 24 import games.stendhal.server.entity.Entity; 25 import games.stendhal.server.entity.item.Corpse; 26 import games.stendhal.server.entity.item.Item; 27 import games.stendhal.server.entity.item.OwnedItem; 28 import games.stendhal.server.entity.item.Stackable; 29 import games.stendhal.server.entity.item.StackableItem; 30 import games.stendhal.server.entity.player.Player; 31 import games.stendhal.server.entity.slot.EntitySlot; 32 import games.stendhal.server.util.EntityHelper; 33 import marauroa.common.game.RPAction; 34 import marauroa.common.game.RPObject; 35 import marauroa.common.game.RPSlot; 36 import marauroa.common.game.SlotOwner; 37 38 /** 39 * this encapsulates the equip/drop source. 40 */ 41 class SourceObject extends MoveableObject { 42 private static InvalidSource invalidSource = new InvalidSource(); 43 private static Logger logger = Logger.getLogger(SourceObject.class); 44 /** the item . */ 45 private Item item; 46 47 private int quantity; 48 49 private boolean isLootingRewardable = false; 50 createSourceObject(final RPAction action, final Player player)51 public static SourceObject createSourceObject(final RPAction action, final Player player) { 52 53 if ((action == null) || (player == null)) { 54 return invalidSource; 55 } 56 57 // source item must be there 58 if (!action.has(EquipActionConsts.SOURCE_PATH) 59 && !action.has(EquipActionConsts.BASE_ITEM) 60 && !action.has(EquipActionConsts.SOURCE_NAME)) { 61 logger.warn("action does not have a base item, path nor name. action: " + action); 62 63 return invalidSource; 64 } 65 66 if (player.getZone() == null) { 67 return invalidSource; 68 } 69 70 SourceObject source; 71 if (!action.has(EquipActionConsts.SOURCE_PATH)) { 72 translateCompatibilityToPaths(action); 73 } 74 source = createSource(action, player); 75 76 if ((source.getEntity() != null) && source.getEntity().hasSlot("content") && (source.getEntity().getSlot("content").size() > 0)) { 77 player.sendPrivateText("Please empty your " + source.getEntityName() + " before moving it around"); 78 return invalidSource; 79 } 80 81 adjustAmountForStackables(action, source); 82 return source; 83 } 84 85 /** 86 * Translate old style object reference to entity path. 87 * @param action action to upgrade to use entity paths 88 */ translateCompatibilityToPaths(RPAction action)89 private static void translateCompatibilityToPaths(RPAction action) { 90 if (action.has(EquipActionConsts.BASE_OBJECT)) { 91 List<String> path = Arrays.asList(action.get(EquipActionConsts.BASE_OBJECT), 92 action.get(EquipActionConsts.BASE_SLOT), action.get(EquipActionConsts.BASE_ITEM)); 93 action.put(EquipActionConsts.SOURCE_PATH, path); 94 } else { 95 List<String> path = Arrays.asList(action.get(EquipActionConsts.BASE_ITEM)); 96 action.put(EquipActionConsts.SOURCE_PATH, path); 97 } 98 } 99 100 @SuppressWarnings("unused") createSourceForContainedItem(final RPAction action, final Player player)101 private static SourceObject createSourceForContainedItem(final RPAction action, final Player player) { 102 SourceObject source; 103 final Entity parent = EquipUtil.getEntityFromId(player, action.getInt(EquipActionConsts.BASE_OBJECT)); 104 105 if (!isValidParent(parent, player)) { 106 return invalidSource; 107 } 108 109 final String slotName = action.get(EquipActionConsts.BASE_SLOT); 110 111 if (!parent.hasSlot(slotName)) { 112 player.sendPrivateText("Source " + slotName + " does not exist"); 113 logger.error(player.getName() + " tried to use non existing slot " + slotName + " of " + parent 114 + " as source. player zone: " + player.getZone() + " object zone: " + parent.getZone()); 115 116 return invalidSource; 117 } 118 final RPSlot baseSlot = parent.getSlot(slotName); 119 120 if (!isValidBaseSlot(player, baseSlot)) { 121 return invalidSource; 122 } 123 final RPObject.ID baseItemId = new RPObject.ID(action.getInt(EquipActionConsts.BASE_ITEM), ""); 124 if (!baseSlot.has(baseItemId)) { 125 logger.debug("Base item(" + parent + ") doesn't contain item(" + baseItemId + ") on given slot(" + slotName 126 + ")"); 127 // Remove message as discussed on #arianne 2010-04-25 128 // player.sendPrivateText("There is no such item in the " + slotName + " of " 129 // + parent.getDescriptionName(true)); 130 return invalidSource; 131 } 132 133 final Entity entity = (Entity) baseSlot.get(baseItemId); 134 if (!(entity instanceof Item)) { 135 player.sendPrivateText("Oh, that " + entity.getDescriptionName(true) 136 + " is not an item and therefore cannot be equipped"); 137 return invalidSource; 138 } 139 140 if (parent instanceof Corpse) { 141 Corpse corpse = (Corpse) parent; 142 if (!corpse.mayUse(player)) { 143 logger.debug(player.getName() + " tried to access eCorpse owned by " + corpse.getCorpseOwner()); 144 player.sendPrivateText("Only " + corpse.getCorpseOwner() + " may access the corpse for now."); 145 return invalidSource; 146 } 147 } 148 149 source = new SourceObject(player, parent, slotName, (Item) entity); 150 151 // handle logging of looting items 152 if (parent instanceof Corpse) { 153 Corpse corpse = (Corpse) parent; 154 checkIfLootingIsRewardable(player, corpse, source, (Item) entity); 155 } 156 157 return source; 158 } 159 160 /** 161 * Create a SourceObject for an item path. 162 * 163 * @param action 164 * @param player 165 * @return source object 166 */ createSource(RPAction action, final Player player)167 private static SourceObject createSource(RPAction action, final Player player) { 168 List<String> path = action.getList(EquipActionConsts.SOURCE_PATH); 169 Entity entity = EntityHelper.getEntityFromPath(player, path); 170 if (entity == null) { 171 entity = EntityHelper.getEntityByName(player, action.get(EquipActionConsts.SOURCE_NAME)); 172 } 173 if (entity == null || !(entity instanceof Item)) { 174 return invalidSource; 175 } 176 Item item = (Item) entity; 177 if (item.isContained() && !ItemAccessPermissions.mayAccessContainedEntity(player, item)) { 178 return invalidSource; 179 } 180 RPObject container = item.getBaseContainer(); 181 182 /* 183 * Top level items need to be checked for players standing on them. 184 */ 185 if (container instanceof Item) { 186 if (isItemBelowOtherPlayer(player, (Item) container)) { 187 return invalidSource; 188 } 189 } 190 191 String slotName = null; 192 RPObject parent = item.getContainer(); 193 if (parent != null) { 194 if (!(parent instanceof Entity)) { 195 logger.error("Non entity container: " + parent); 196 return invalidSource; 197 } 198 slotName = item.getContainerSlot().getName(); 199 } 200 201 SourceObject source = new SourceObject(player, (Entity) parent, slotName, item); 202 203 // handle logging of looting items 204 if (parent instanceof Corpse) { 205 Corpse corpse = (Corpse) parent; 206 checkIfLootingIsRewardable(player, corpse, source, (Item) entity); 207 } 208 209 return source; 210 } 211 isValidBaseSlot(final Player player, final RPSlot baseSlot)212 private static boolean isValidBaseSlot(final Player player, final RPSlot baseSlot) { 213 if (! (baseSlot instanceof EntitySlot)) { 214 return false; 215 } 216 EntitySlot slot = (EntitySlot) baseSlot; 217 slot.clearErrorMessage(); 218 boolean res = slot.isReachableForTakingThingsOutOfBy(player); 219 if (!res) { 220 logger.debug("Unreachable slot"); 221 String error = slot.getErrorMessage(); 222 if (error != null) { 223 player.sendPrivateText(slot.getErrorMessage()); 224 } 225 } 226 return res; 227 } 228 229 @SuppressWarnings("unused") createSourceForNonContainedItem(final RPAction action, final Player player)230 private static SourceObject createSourceForNonContainedItem(final RPAction action, final Player player) { 231 final SourceObject source = new SourceObject(player); 232 final RPObject.ID baseItemId = new RPObject.ID(action.getInt(EquipActionConsts.BASE_ITEM), player.getID().getZoneID()); 233 234 source.item = source.getNonContainedItem(baseItemId); 235 if (source.item == null) { 236 return invalidSource; 237 } 238 return source; 239 } 240 adjustAmountForStackables(final RPAction action, final SourceObject source)241 private static void adjustAmountForStackables(final RPAction action, final SourceObject source) { 242 if ((source.item instanceof Stackable< ? >) && action.has(EquipActionConsts.QUANTITY)) { 243 final int entityQuantity = ((Stackable< ? >) source.item).getQuantity(); 244 245 source.quantity = action.getInt(EquipActionConsts.QUANTITY); 246 if ((entityQuantity < 1) || (source.quantity < 1) || (source.quantity >= entityQuantity)) { 247 // quantity == 0 performs a regular move 248 // of the entire item 249 source.quantity = 0; 250 } 251 } 252 } 253 254 /** 255 * Represents the source of a movement of a contained Item as in Drop or Equip. 256 * 257 * @param player 258 * who want to do action 259 * @param parent 260 * who contains it right now 261 * @param slotName 262 * where to get it from 263 * @param entity 264 * the item to move 265 * 266 */ SourceObject(final Player player, final Entity parent, final String slotName, final Item entity)267 private SourceObject(final Player player, final Entity parent, final String slotName, final Item entity) { 268 super(player); 269 this.parent = parent; 270 this.slot = slotName; 271 this.item = entity; 272 } 273 isValidParent(final Entity parent, final Player player)274 private static boolean isValidParent(final Entity parent, final Player player) { 275 if (parent == null) { 276 // Object doesn't exist. 277 return false; 278 } 279 280 // TODO: Check that this code is not required because this check is 281 // done in PlayerSlot 282 // is the container a player and not the current one? 283 if ((parent instanceof Player) && !parent.getID().equals(player.getID())) { 284 // trying to remove an item from another player 285 return false; 286 } 287 return true; 288 } 289 getNonContainedItem(final RPObject.ID baseItemId)290 private Item getNonContainedItem(final RPObject.ID baseItemId) { 291 Entity entity = null; 292 if (SingletonRepository.getRPWorld().has(baseItemId)) { 293 entity = (Entity) SingletonRepository.getRPWorld().get(baseItemId); 294 if ((entity instanceof Item)) { 295 if (isItemBelowOtherPlayer(player, (Item) entity)) { 296 entity = null; 297 } 298 } else { 299 entity = null; 300 } 301 } 302 return (Item) entity; 303 } 304 SourceObject(final Player player)305 public SourceObject(final Player player) { 306 super(player); 307 } 308 309 /** 310 * moves this entity to the destination. 311 * 312 * @param dest 313 * to move to 314 * @param player 315 * who moves the Source 316 * @return true if successful 317 */ moveTo(final DestinationObject dest, final Player player)318 public boolean moveTo(final DestinationObject dest, final Player player) { 319 final String targetSlot = dest.getContentSlotName(); 320 321 if (!((EquipListener) item).canBeEquippedIn(targetSlot)) { 322 // give some feedback 323 player.sendPrivateText("You can't carry this " + item.getTitle() + " on your " + targetSlot + "."); 324 logger.warn("tried to equip an entity into disallowed slot: " + item.getClass() + "; equip rejected"); 325 return false; 326 } 327 328 if (item instanceof OwnedItem) { 329 final OwnedItem owned = (OwnedItem) item; 330 if (owned.hasOwner() && !owned.canEquipToSlot(player, targetSlot)) { 331 owned.onEquipFail(player, targetSlot); 332 logger.warn("tried to equip an owned entity into disallowed slot: " + item.getClass() + "; equip rejected"); 333 return false; 334 } 335 } 336 337 if (!dest.isValid() || !dest.preCheck(item, player)) { 338 // no extra logger warning needed here as each is inside the methods called above, where necessary 339 return false; 340 } 341 342 final String[] srcInfo = getLogInfo(); 343 final Item entity = removeFromWorld(); 344 logger.debug("item removed"); 345 dest.addToWorld(entity, player); 346 logger.debug("item readded"); 347 348 new ItemLogger().equipAction(player, entity, srcInfo, dest.getLogInfo()); 349 350 return true; 351 } 352 353 /** returns true when this SourceObject is valid. */ 354 @Override isValid()355 public boolean isValid() { 356 return (item != null); 357 } 358 359 /** 360 * returns true when this entity and the other is within the given distance. 361 */ 362 @Override checkDistance(final Entity other, final double distance)363 public boolean checkDistance(final Entity other, final double distance) { 364 final SlotOwner parent = item.getContainerBaseOwner(); 365 if (parent instanceof Entity) { 366 final Entity checker = (Entity) item.getContainerBaseOwner(); 367 if (other.nextTo(checker, distance)) { 368 return true; 369 } else { 370 logger.debug("distance check failed " + other.squaredDistance(checker)); 371 player.sendPrivateText("You cannot reach that far."); 372 } 373 } 374 375 return false; 376 } 377 378 /** 379 * removes the entity from the world and returns it (so it may be added 380 * again). In case of splitted StackableItem the only item is reduced and a 381 * new StackableItem with the splitted off amount is returned. 382 * 383 * @return Entity to place somewhere else in the world 384 */ removeFromWorld()385 public Item removeFromWorld() { 386 if (quantity != 0) { 387 final StackableItem newItem = ((StackableItem) item).splitOff(quantity); 388 new ItemLogger().splitOff(player, item, newItem, quantity); 389 return newItem; 390 } else { 391 item.removeFromWorld(); 392 return item; 393 } 394 } 395 396 /** 397 * Gets the entity that should be equipped. 398 * 399 * @return entity 400 */ getEntity()401 public Entity getEntity() { 402 return item; 403 } 404 405 /** 406 * @return the amount of objects. 407 */ getQuantity()408 public int getQuantity() { 409 410 int temp = quantity; 411 if (quantity == 0) { 412 // everything 413 temp = 1; 414 if (item instanceof StackableItem) { 415 temp = ((StackableItem) item).getQuantity(); 416 } 417 418 } 419 return temp; 420 } 421 422 /** 423 * Sets the quantity. 424 * 425 * @param amount 426 */ setQuantity(final int amount)427 public void setQuantity(final int amount) { 428 quantity = amount; 429 } 430 431 /** 432 * Checks whether the item is below <b>another</b> player. 433 * 434 * @param player Player trying to access the item 435 * @param sourceItem 436 * to check 437 * 438 * @return true, if it cannot be taken; false otherwise 439 */ isItemBelowOtherPlayer(final Player player, final Item sourceItem)440 private static boolean isItemBelowOtherPlayer(final Player player, final Item sourceItem) { 441 final List<Player> players = player.getZone().getPlayers(); 442 for (final Player otherPlayer : players) { 443 if (player.equals(otherPlayer)) { 444 continue; 445 } 446 // Allow players always pick up their own items 447 if (!player.getName().equals(sourceItem.getBoundTo())) { 448 if (otherPlayer.getArea().intersects(sourceItem.getArea())) { 449 player.sendPrivateText("You cannot take items which are below other players"); 450 return true; 451 } 452 } 453 } 454 return false; 455 } 456 457 /** 458 * Checks if looting the item is rewardable as looted item 459 * 460 * @return true iff the player deserves it 461 */ isLootingRewardable()462 public boolean isLootingRewardable() { 463 return isLootingRewardable; 464 } 465 466 /** 467 * Determines if looting should be logged for the player 468 * 469 * @param player 470 * @param corpse 471 * @param source 472 * @param item 473 */ checkIfLootingIsRewardable(Player player, Corpse corpse, SourceObject source, Item item)474 private static void checkIfLootingIsRewardable(Player player, Corpse corpse, SourceObject source, Item item) { 475 if (item.isFromCorpse()) { 476 if (corpse.isItemLootingRewardable()) { 477 source.isLootingRewardable = true; 478 } else { 479 if (player.getName().equals(corpse.getKiller())) { 480 source.isLootingRewardable = true; 481 } 482 } 483 } 484 } 485 486 @Override getLogInfo()487 public String[] getLogInfo() { 488 final String[] res = new String[3]; 489 if (parent != null) { 490 res[0] = "slot"; 491 if (parent.has("name")) { 492 res[1] = parent.get("name"); 493 } else { 494 res[1] = parent.getDescriptionName(false); 495 } 496 res[2] = slot; 497 } else { 498 res[0] = "ground"; 499 res[1] = item.getZone().getName(); 500 res[2] = item.getX() + " " + item.getY(); 501 } 502 return res; 503 } 504 getEntityName()505 String getEntityName() { 506 Entity entity1 = getEntity(); 507 final String itemName; 508 if (entity1.has("name")) { 509 itemName = entity1.get("name"); 510 } else if (entity1 instanceof Item) { 511 itemName = "item"; 512 } else { 513 itemName = "entity"; 514 } 515 return itemName; 516 } 517 518 private static class InvalidSource extends SourceObject { 519 /** 520 * Constructor. 521 */ InvalidSource()522 public InvalidSource() { 523 super(null); 524 525 } 526 527 /** 528 * 529 * @return false 530 */ 531 @Override isValid()532 public boolean isValid() { 533 return false; 534 } 535 } 536 } 537