1 /***************************************************************************
2  *                (C) Copyright 2003-2018 - Faiumoni e.V.                  *
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.client.entity;
13 
14 import static games.stendhal.common.constants.Actions.AUTOWALK;
15 import static games.stendhal.common.constants.Actions.MODE;
16 import static games.stendhal.common.constants.Actions.TYPE;
17 import static games.stendhal.common.constants.Actions.WALK;
18 import static games.stendhal.common.constants.General.PATHSET;
19 
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.Set;
24 
25 import org.apache.log4j.Logger;
26 
27 import games.stendhal.client.ClientSingletonRepository;
28 import games.stendhal.client.GameObjects;
29 import games.stendhal.client.StendhalClient;
30 import games.stendhal.client.gui.chatlog.HeaderLessEventLine;
31 import games.stendhal.common.Direction;
32 import games.stendhal.common.NotificationType;
33 import games.stendhal.common.constants.Testing;
34 import games.stendhal.common.grammar.Grammar;
35 import marauroa.common.game.RPAction;
36 import marauroa.common.game.RPObject;
37 import marauroa.common.game.RPSlot;
38 
39 /**
40  * This class identifies the user of this client.
41  *
42  * @author durkham, hendrik
43  */
44 public class User extends Player {
45 	private static final StaticUserProxy NO_USER = new NoUserProxy();
46 	private static final Logger logger = Logger.getLogger(User.class);
47 	private static final String IGNORE_SLOT = "!ignore";
48 
49 	private static String groupLootmode;
50 	private static Set<String> groupMembers = Collections.emptySet();
51 	private static StaticUserProxy userProxy = NO_USER;
52 
53 	private final Set<String> ignore = new HashSet<String>();
54 	private final SpeedPredictor speedPredictor;
55 
56 	/**
57 	 * creates a User object
58 	 */
User()59 	public User() {
60 		if (isNull()) {
61 			speedPredictor = new SpeedPredictor();
62 		} else {
63 			speedPredictor = new SpeedPredictor(userProxy.getUser().speedPredictor);
64 		}
65 		userProxy = new NormalUserProxy(this);
66 	}
67 
68 	/**
69 	 * gets the User object
70 	 *
71 	 * @return user object
72 	 */
get()73 	public static User get() {
74 		return userProxy.getUser();
75 	}
76 
77 	/**
78 	 * is the user object not set, yet?
79 	 *
80 	 * @return true, if the the user object is unknown; false if it is known
81 	 */
isNull()82 	public static boolean isNull() {
83 		return userProxy == NO_USER;
84 	}
85 
86 	/**
87 	 * Resets the class to uninitialized.
88 	 */
setNull()89 	static void setNull() {
90 		userProxy = NO_USER;
91 	}
92 
93 	/**
94 	 * gets the name of the player's character
95 	 *
96 	 * @return charname or <code>null</code>
97 	 */
getCharacterName()98 	public static String getCharacterName() {
99 		return userProxy.getName();
100 	}
101 
102 	/**
103 	 * gets the level of the current user
104 	 *
105 	 * @return level
106 	 */
getPlayerLevel()107 	public static int getPlayerLevel() {
108 		return userProxy.getPlayerLevel();
109 	}
110 
111 	/**
112 	 * gets the server release version
113 	 *
114 	 * @return server release version or <code>null</code>
115 	 */
getServerRelease()116 	public static String getServerRelease() {
117 		return userProxy.getServerRelease();
118 	}
119 
120 	/**
121 	 * is the specified charname a buddy of us?
122 	 *
123 	 * @param name charname to test
124 	 * @return true, if it is a buddy, false if it is not a buddy or the user object is unknown.
125 	 */
hasBuddy(String name)126 	public static boolean hasBuddy(String name) {
127 		return userProxy.hasBuddy(name);
128 	}
129 
130 	/**
131 	 * is this user an admin with an adminlevel equal or above 600?
132 	 *
133 	 * @return true, if the user is an admin; false otherwise
134 	 */
isAdmin()135 	public static boolean isAdmin() {
136 		return userProxy.isAdmin();
137 	}
138 
139 	/**
140 	 * is the player in a group which shares the loot?
141 	 *
142 	 * @return true if this player is a group and it uses shared looting
143 	 */
isGroupSharingLoot()144 	public static boolean isGroupSharingLoot() {
145 		return "shared".equals(groupLootmode);
146 	}
147 
148 	/**
149 	 * is the named player ignored?
150 	 *
151 	 * @param name name of player
152 	 * @return true, if the player should be ignored; false otherwise
153 	 */
isIgnoring(String name)154 	public static boolean isIgnoring(String name) {
155 		return userProxy.isIgnoring(name);
156 	}
157 
158 	/**
159 	 * checks if the specified player is in the same group as this player
160 	 *
161 	 * @param otherPlayer name of the other player
162 	 * @return true if the other player is in the same group
163 	 */
isPlayerInGroup(String otherPlayer)164 	public static boolean isPlayerInGroup(String otherPlayer) {
165 		return groupMembers.contains(otherPlayer);
166 	}
167 
168 	/**
169 	 * updates the group information
170 	 *
171 	 * @param members members
172 	 * @param lootmode lootmode
173 	 */
updateGroupStatus(Collection<String> members, String lootmode)174 	public static void updateGroupStatus(Collection<String> members, String lootmode) {
175 		Set<String> oldGroupMembers = groupMembers;
176 
177 		if (members == null) {
178 			groupMembers = Collections.emptySet();
179 		} else {
180 			groupMembers = new HashSet<>(members);
181 		}
182 		groupLootmode = lootmode;
183 
184 		// fire change event to color of player object on minimap
185 		for (IEntity entity : GameObjects.getInstance()) {
186 			if ((entity instanceof Player)
187 					&& (oldGroupMembers.contains(entity.getName())
188 							|| groupMembers.contains(entity.getName()))) {
189 				((Player) entity).fireChange(RPEntity.PROP_GROUP_MEMBERSHIP);
190 			}
191 		}
192 	}
193 
194 	/**
195 	 * calculates the squared distance between the user and the specified coordinates
196 	 *
197 	 * @param x x coordinate
198 	 * @param y y coordinate
199 	 * @return the squared distance
200 	 */
squaredDistanceTo(final double x, final double y)201 	static double squaredDistanceTo(final double x, final double y) {
202 		return userProxy.squareDistanceTo(x, y);
203 	}
204 
205 	/**
206 	 * Add players to the set of ignored players.
207 	 * Player names are the attributes prefixed with '_'.
208 	 *
209 	 * @param ignoreObj The container object for player names
210 	 */
addIgnore(RPObject ignoreObj)211 	private void addIgnore(RPObject ignoreObj) {
212 		for (String attr : ignoreObj) {
213 			if (attr.charAt(0) == '_') {
214 				ignore.add(attr.substring(1));
215 			}
216 		}
217 	}
218 
219 	/**
220 	 * Remove players from the set of ignored players.
221 	 * Player names are the attributes prefixed with '_'.
222 	 *
223 	 * @param ignoreObj The container object for player names
224 	 */
removeIgnore(RPObject ignoreObj)225 	private void removeIgnore(RPObject ignoreObj) {
226 		for (String attr : ignoreObj) {
227 			if (attr.charAt(0) == '_') {
228 				ignore.remove(attr.substring(1));
229 			}
230 		}
231 	}
232 
233 	/**
234 	 * Returns the objectid for the named item.
235 	 *
236 	 * @param slotName
237 	 *            name of slot to search
238 	 * @param itemName
239 	 *            name of item
240 	 * @return objectid or <code>-1</code> in case there is no such item
241 	 */
findItem(final String slotName, final String itemName)242 	public int findItem(final String slotName, final String itemName) {
243 		RPSlot slot = getSlot(slotName);
244 		if (slot == null) {
245 			return -1;
246 		}
247 		for (final RPObject item : slot) {
248 			if (item.get("name").equals(itemName)) {
249 				return item.getID().getObjectID();
250 			}
251 		}
252 
253 		return -1;
254 	}
255 
256 	/**
257 	 * checks whether the user owns a pet
258 	 *
259 	 * @return true, if the user owns a pet; false otherwise
260 	 */
hasPet()261 	public boolean hasPet() {
262 		return rpObject.has("pet");
263 	}
264 
265 	/**
266 	 * gets the ID of a pet
267 	 *
268 	 * @return ID of pet
269 	 */
getPetID()270 	public int getPetID() {
271 		return rpObject.getInt("pet");
272 	}
273 
274 	/**
275 	 * checks whether the user owns a sheep
276 	 *
277 	 * @return true, if the user owns a sheep; false otherwise
278 	 */
hasSheep()279 	public boolean hasSheep() {
280 		return rpObject.has("sheep");
281 	}
282 
283 	/**
284 	 * gets the ID of a sheep
285 	 *
286 	 * @return ID of sheep
287 	 */
getSheepID()288 	public int getSheepID() {
289 		return rpObject.getInt("sheep");
290 	}
291 
292 	/**
293 	 * gets the zone name
294 	 *
295 	 * @return zone name
296 	 */
getZoneName()297 	public String getZoneName() {
298 		return getID().getZoneID();
299 	}
300 
301 	/**
302 	 * Is this object the user of this client?
303 	 *
304 	 * @return true
305 	 */
306 	@Override
isUser()307 	public boolean isUser() {
308 		return true;
309 	}
310 
311 	@Override
onAway(final String message)312 	protected void onAway(final String message) {
313 		super.onAway(message);
314 
315 		String text;
316 		if (message == null) {
317 			text = "You are no longer marked as being away.";
318 		} else {
319 			text = "You have been marked as being away.";
320 		}
321 		notifyUser(text, NotificationType.INFORMATION);
322 	}
323 
324 	/**
325 	 * The object added/changed attribute(s).
326 	 *
327 	 * @param object
328 	 *            The base object.
329 	 * @param changes
330 	 *            The changes.
331 	 */
332 	@Override
onChangedAdded(final RPObject object, final RPObject changes)333 	public void onChangedAdded(final RPObject object, final RPObject changes) {
334 		/* TODO: Remove condition when walking bug fix is finished. */
335 		if (false) { // DISABLED
336 			if (!this.stopped()) {
337 				boolean shouldStop = true;
338 				String debugString = "Stopped on:";
339 
340 				if (StendhalClient.get().directionKeyIsPressed()) {
341 					shouldStop = false;
342 				} else {
343 					debugString += " !directionKeyIsPressed()";
344 				}
345 				if (object.has(AUTOWALK)) {
346 					shouldStop = false;
347 				} else {
348 					debugString += " !has(AUTOWALK)";
349 				}
350 				if (object.has(PATHSET)) {
351 					shouldStop = false;
352 				} else {
353 					debugString += " !has(PATHSET)";
354 				}
355 
356 				if (shouldStop) {
357 					/* Stop the character's movement. */
358 					this.stopMovement();
359 
360 					if (logger.isDebugEnabled() || Testing.DEBUG) {
361 						logger.info(debugString);
362 					}
363 				}
364 			}
365 		}
366 
367 		super.onChangedAdded(object, changes);
368 
369 		// The first time we ignore it.
370 		if (object != null) {
371 			notifyUserAboutPlayerOnlineChanges(changes);
372 
373 			if (changes.hasSlot(IGNORE_SLOT)) {
374 				RPObject ign = changes.getSlot(IGNORE_SLOT).getFirst();
375 				if (ign != null) {
376 					addIgnore(ign);
377 				}
378 			}
379 		}
380 	}
381 
382 	@Override
onChangedRemoved(final RPObject base, final RPObject diff)383 	public void onChangedRemoved(final RPObject base, final RPObject diff) {
384 		super.onChangedRemoved(base, diff);
385 		if (diff.hasSlot(IGNORE_SLOT)) {
386 			RPObject ign = diff.getSlot(IGNORE_SLOT).getFirst();
387 			if (ign != null) {
388 				removeIgnore(ign);
389 			}
390 		}
391 	}
392 
393 	@Override
onHealed(final int amount)394 	public void onHealed(final int amount) {
395 		super.onHealed(amount);
396 		String pointDesc = Grammar.quantityplnoun(amount, "health point");
397 		notifyUser(getTitle() + " heals " + pointDesc + ".", NotificationType.HEAL);
398 	}
399 
notifyUser(String message, NotificationType type)400 	private void notifyUser(String message, NotificationType type) {
401 		ClientSingletonRepository.getUserInterface().addEventLine(new HeaderLessEventLine(message, type));
402 	}
403 
notifyUserAboutPlayerOnlineChanges(RPObject changes)404 	private void notifyUserAboutPlayerOnlineChanges(RPObject changes) {
405 		notifyUserAboutPlayerStatus(changes, "offline", " has left Stendhal.");
406 		notifyUserAboutPlayerStatus(changes, "online", " has joined Stendhal.");
407 	}
408 
notifyUserAboutPlayerStatus(RPObject changes, String status, String messageEnd)409 	private void notifyUserAboutPlayerStatus(RPObject changes, String status, String messageEnd) {
410 		if (changes.has(status)) {
411 			String[] players = changes.get(status).split(",");
412 			for (String playername : players) {
413 				notifyUser(playername + messageEnd, NotificationType.INFORMATION);
414 			}
415 		}
416 	}
417 
418 	/**
419 	 * Start movement towards a direction. This is for
420 	 * the client side movement prediction to start moving before the server
421 	 * responds to the move action.
422 	 *
423 	 * @param direction new direction
424 	 * @param facing <code>true</code> if the player should just turn
425 	 */
predictMovement(Direction direction, boolean facing)426 	public void predictMovement(Direction direction, boolean facing) {
427 		// Only handle the case of starting movement. Prediction when already
428 		// moving looks odd.
429 		if (stopped()) {
430 			if (isConfused()) {
431 				direction = direction.oppositeDirection();
432 			}
433 			if (!facing) {
434 				double speed = speedPredictor.getSpeed();
435 				setSpeed(direction.getdx() * speed, direction.getdy() * speed);
436 				fireChange(PROP_SPEED);
437 				speedPredictor.startPrediction();
438 			}
439 			// setDirection fires the appropriate property for itself
440 			setDirection(direction);
441 		}
442 	}
443 
444 	@Override
processPositioning(final RPObject base, final RPObject diff)445 	protected void processPositioning(final RPObject base, final RPObject diff) {
446 		if (speedPredictor.isActive() && (diff.has("direction") || diff.has("x") || diff.has("y"))) {
447 			speedPredictor.onMoved();
448 		}
449 		super.processPositioning(base, diff);
450 	}
451 
452 	/**
453 	 * Stop the user's movement.
454 	 */
stopMovement()455 	public void stopMovement() {
456 		final RPAction stopAction = new RPAction();
457 
458 		stopAction.put(TYPE, WALK);
459 		stopAction.put(MODE, "stop");
460 
461 		ClientSingletonRepository.getClientFramework().send(stopAction);
462 	}
463 
464 	/**
465 	 * Interface to separate the no user special case from the normal
466 	 * situation.
467 	 */
468 	private static interface StaticUserProxy {
getName()469 		String getName();
getPlayerLevel()470 		int getPlayerLevel();
getServerRelease()471 		String getServerRelease();
getUser()472 		User getUser();
hasBuddy(String buddy)473 		boolean hasBuddy(String buddy);
isAdmin()474 		boolean isAdmin();
isIgnoring(String name)475 		boolean isIgnoring(String name);
squareDistanceTo(double x, double y)476 		double squareDistanceTo(double x, double y);
477 	}
478 
479 	private static class NormalUserProxy implements StaticUserProxy {
480 		private final User user;
481 
NormalUserProxy(User user)482 		NormalUserProxy(User user) {
483 			this.user = user;
484 		}
485 
486 		@Override
getName()487 		public String getName() {
488 			return user.getName();
489 		}
490 
491 		@Override
getPlayerLevel()492 		public int getPlayerLevel() {
493 			return user.getLevel();
494 		}
495 
496 		@Override
getServerRelease()497 		public String getServerRelease() {
498 			return user.rpObject.get("release");
499 		}
500 
501 		@Override
getUser()502 		public User getUser() {
503 			return user;
504 		}
505 
506 		@Override
hasBuddy(String buddy)507 		public boolean hasBuddy(String buddy) {
508 			return user.rpObject.has("buddies", buddy);
509 		}
510 
511 		@Override
isAdmin()512 		public boolean isAdmin() {
513 			return ((user.rpObject != null)
514 					&& user.rpObject.has("adminlevel")
515 					&& (user.rpObject.getInt("adminlevel") >= 600));
516 		}
517 
518 		@Override
squareDistanceTo(double x, double y)519 		public double squareDistanceTo(double x, double y) {
520 			double xDiff = user.getX() - x;
521 			double yDiff = user.getY() - y;
522 			return xDiff * xDiff + yDiff * yDiff;
523 		}
524 
525 		@Override
isIgnoring(String name)526 		public boolean isIgnoring(String name) {
527 			return user.ignore.contains(name);
528 		}
529 	}
530 
531 	private static class NoUserProxy implements StaticUserProxy {
532 		@Override
getName()533 		public String getName() {
534 			return null;
535 		}
536 
537 		@Override
getPlayerLevel()538 		public int getPlayerLevel() {
539 			return 0;
540 		}
541 
542 		@Override
getServerRelease()543 		public String getServerRelease() {
544 			return null;
545 		}
546 
547 		@Override
getUser()548 		public User getUser() {
549 			return null;
550 		}
551 
552 		@Override
hasBuddy(String buddy)553 		public boolean hasBuddy(String buddy) {
554 			return false;
555 		}
556 
557 		@Override
isAdmin()558 		public boolean isAdmin() {
559 			return false;
560 		}
561 
562 		@Override
isIgnoring(String name)563 		public boolean isIgnoring(String name) {
564 			return false;
565 		}
566 
567 		@Override
squareDistanceTo(double x, double y)568 		public double squareDistanceTo(double x, double y) {
569 			return Double.POSITIVE_INFINITY;
570 		}
571 	}
572 }
573