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