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