/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ package illarion.client.world; import gnu.trove.map.hash.TIntObjectHashMap; import illarion.client.Login; import illarion.client.gui.DialogType; import illarion.client.net.client.RequestAppearanceCmd; import illarion.client.util.ChatLog; import illarion.client.world.items.*; import illarion.client.world.movement.Movement; import illarion.common.graphics.Layer; import illarion.common.types.CharacterId; import illarion.common.types.ServerCoordinate; import illarion.common.util.DirectoryManager; import illarion.common.util.DirectoryManager.Directory; import illarion.common.util.FastMath; import org.illarion.engine.Engine; import org.jetbrains.annotations.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.EnumSet; import java.util.Objects; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Main Class for the player controlled character. * * @author Martin Karing <nitram@illarion.org> * @author Nop */ @ThreadSafe public final class Player { /** * Maximal value for the volume of the sound. */ public static final float MAX_CLIENT_VOL = 100.f; /** * The key in the configuration for the sound on/off flag. */ @Nonnull public static final String CFG_SOUND_ON = "soundOn"; //$NON-NLS-1$ /** * The key in the configuration for the sound volume value. */ @Nonnull public static final String CFG_SOUND_VOL = "soundVolume"; //$NON-NLS-1$ /** * Average value for the perception attribute. */ private static final int PERCEPTION_AVERAGE = 8; /** * Share of one perception point on the coverage of a tile. */ private static final int PERCEPTION_COVER_SHARE = 4; /** * The instance of the logger used by this class. */ @Nonnull private static final Logger LOGGER = LoggerFactory.getLogger(Player.class); /** * The graphical representation of the character. */ @Nonnull private final Char character; /** * The inventory of the player. */ @Nonnull private final Inventory inventory; /** * The player movement handler that takes care that the player character is walking around. */ @Nonnull private final Movement movementHandler; /** * The path to the folder of character specific stuff, like the map or the names table. */ @Nonnull private final Path path; /** * The instance of the combat handler that maintains the attack targets of this player. */ @Nonnull private final CombatHandler combatHandler; /** * This map contains the containers that are known for the player. */ @Nonnull @GuardedBy("containerLock") private final TIntObjectHashMap<ItemContainer> containers; /** * This lock is used to synchronize the access on the containers. */ @Nonnull private final ReadWriteLock containerLock; /** * The chat log instance that takes care for logging the text that is spoken in the game. */ @Nonnull private final ChatLog chatLog; @Nonnull private final CarryLoad carryLoad; /** * The current location of the server map for the player. */ @Nullable private ServerCoordinate playerLocation; /** * The character ID of the player. */ @Nullable private CharacterId playerId; /** * This flag is changed to {@code true} once the location of the player was set once. */ private boolean validLocation; /** * The merchant dialog that is currently open. */ @Nullable private MerchantList merchantDialog; /** * Constructor for the player that receives the character name from the login data automatically. */ public Player(@Nonnull Engine engine) { this(engine, Login.getInstance().getLoginCharacter()); } /** * Default constructor for the player. * * @param charName the character name of the player playing this game */ public Player(@Nonnull Engine engine, @Nonnull String charName) { Path userDir = DirectoryManager.getInstance().getDirectory(Directory.User); path = userDir.resolve(charName); chatLog = new ChatLog(path); carryLoad = new CarryLoad(); character = new Char(); validLocation = false; try { if (Files.notExists(path)) { Files.createDirectory(path); } } catch (IOException e) { throw new IllegalStateException("Failed to create directory for user data.", e); } character.setName(charName); combatHandler = new CombatHandler(); movementHandler = new Movement(this, engine.getInput(), World.getMapDisplay()); inventory = new Inventory(); containers = new TIntObjectHashMap<>(); containerLock = new ReentrantReadWriteLock(); } public void openMerchantDialog(int dialogId, @Nonnull String title, @Nonnull Collection<MerchantItem> items) { MerchantList list = new MerchantList(dialogId); items.forEach(list::addItem); MerchantList oldList = merchantDialog; merchantDialog = list; if (oldList != null) { closeDialog(oldList.getId(), EnumSet.of(DialogType.Merchant)); } if (World.getGameGui().isReady()) { World.getGameGui().getDialogMerchantGui().showMerchantDialog(dialogId, title, items); World.getGameGui().getContainerGui().updateMerchantOverlay(); World.getGameGui().getInventoryGui().updateMerchantOverlay(); } } public void closeDialog(int dialogId, @Nonnull Collection<DialogType> dialogTypes) { if ((merchantDialog != null) && dialogTypes.contains(DialogType.Merchant)) { if (dialogId == merchantDialog.getId()) { MerchantList oldList = merchantDialog; merchantDialog = null; oldList.closeDialog(); if (World.getGameGui().isReady()) { World.getGameGui().getInventoryGui().updateMerchantOverlay(); World.getGameGui().getContainerGui().updateMerchantOverlay(); } } } if (World.getGameGui().isReady()) { World.getGameGui().getDialogGui().closeDialog(dialogId, dialogTypes); } } @Nonnull public ItemContainer getOrCreateContainer(int id, @Nonnull String title, @Nonnull String description, int slotCount) { if (hasContainer(id)) { ItemContainer container = getContainer(id); if (container == null) { throw new IllegalStateException( "Has container with ID but can't receive it. Internal state corrupted."); } if (container.getSlotCount() == slotCount) { return container; } else { LOGGER.error("Received container event for existing container but without fitting slot count!"); removeContainer(id); } } return createNewContainer(id, title, description, slotCount); } /** * Check if there is currently a container with the specified ID open. * * @param id the ID of the container * @return {@code true} in case this container exists */ public boolean hasContainer(int id) { containerLock.readLock().lock(); try { return containers.containsKey(id); } finally { containerLock.readLock().unlock(); } } /** * Get the container that is assigned to the ID. This either creates a new container or opens and existing one. * * @param id the ID of the container * @return the container assigned to this ID or a newly created container */ @Nullable public ItemContainer getContainer(int id) { containerLock.readLock().lock(); try { return containers.get(id); } finally { containerLock.readLock().unlock(); } } /** * Remove a container that is assigned to a specified key. This also removes the container from the GUI. * * @param id the key of the parameter to remove */ public void removeContainer(int id) { synchronized (containers) { if (containers.containsKey(id)) { containers.remove(id); } } World.getGameGui().getContainerGui().closeContainer(id); } /** * Create a new item container instance. * * @param id the ID of the container * @param slotCount the amount of slots the new container is supposed to have * @return the new item container * @throws IllegalArgumentException in case there is already a container with the same ID */ @Nonnull public ItemContainer createNewContainer(int id, @Nonnull String title, @Nonnull String description, int slotCount) { containerLock.writeLock().lock(); try { if (containers.containsKey(id)) { throw new IllegalArgumentException("Can't create item container that already exists."); } ItemContainer newContainer = new ItemContainer(id, title, description, slotCount); containers.put(id, newContainer); return newContainer; } finally { containerLock.writeLock().unlock(); } } /** * Get the graphical representation of the players character. * * @return The character of the player */ @Nonnull @Contract(pure = true) public Char getCharacter() { return character; } /** * Get the chat log that is active for this player. * * @return the chat log of this player */ @Nonnull @Contract(pure = true) public ChatLog getChatLog() { return chatLog; } /** * Get the inventory of the player. * * @return the player inventory */ @Nonnull @Contract(pure = true) public Inventory getInventory() { return inventory; } /** * Get the current location of the character. * * @return The current location of the character */ @Nonnull @Contract(pure = true) public ServerCoordinate getLocation() { if (playerLocation == null) { throw new IllegalStateException("The location of the player is not yet set."); } return playerLocation; } /** * Change the location of the character. This will instantly change the position on the map where the client is * shown. Calling this function is a result of any warp requested by the server. Calling this function will * subsequently cause all other components of the client to be updated according to the new location. * * @param newLoc new location of the character on the map */ public void setLocation(@Nonnull ServerCoordinate newLoc) { validLocation = true; if (Objects.equals(playerLocation, newLoc)) { return; } boolean isLongRange = false; if (playerLocation == null) { isLongRange = true; } else { if (playerLocation.getDistance(newLoc) > 10) { isLongRange = true; } if (FastMath.abs(playerLocation.getZ() - newLoc.getZ()) > 3) { isLongRange = true; } } if (isLongRange) { World.getMapDisplay().setActive(false); World.getMap().clear(); } // set logical location updateLocation(newLoc); character.setLocation(newLoc); character.stopAnimation(); character.resetAnimation(true); World.getPlayer().getCombatHandler().standDown(); World.getMapDisplay().setLocation(newLoc.toDisplayCoordinate(Layer.Chars)); if (isLongRange) { World.getPeople().clear(); } else { World.getMap().checkInside(); World.getPeople().clipCharacters(); } } /** * Update the location that is bound to this player. This does not have any side effects but the location of the * player and the sound listener changed. * * @param newLoc the new location of the player */ public void updateLocation(@Nonnull ServerCoordinate newLoc) { if (Objects.equals(playerLocation, newLoc)) { return; } LOGGER.debug("Setting player location to: {}", newLoc); playerLocation = newLoc; World.getMusicBox().updatePlayerLocation(); World.getMap().getMiniMap().setPlayerLocation(newLoc); World.getMap().updateAllTiles(); World.getMap().checkInside(); } /** * Get the instance of the combat handler. * * @return the combat handler of this player */ @Nonnull @Contract(pure = true) public CombatHandler getCombatHandler() { return combatHandler; } /** * Get the movement handler of the player character that allows controls the movement of the player. * * @return the movement handler */ @Nonnull @Contract(pure = true) public Movement getMovementHandler() { return movementHandler; } /** * Get the path to the player directory. * * @return The path to the player directory */ @Nonnull @Contract(pure = true) public Path getPath() { return path; } /** * Get the merchant list. * * @return the merchant list */ @Nullable @Contract(pure = true) public MerchantList getMerchantList() { return merchantDialog; } /** * Get the current ID of the players character. * * @return The ID of the player character */ @Nullable @Contract(pure = true) public CharacterId getPlayerId() { return playerId; } /** * Set the ID of the players character. This also causes a introducing of the player character to the rest of the * client, so the Chat box for example is able to show the name of the character without additional affords. * * @param newPlayerId the new ID of the player */ public void setPlayerId(@Nonnull CharacterId newPlayerId) { playerId = newPlayerId; character.setCharId(playerId); World.getNet().sendCommand(new RequestAppearanceCmd(newPlayerId)); } /** * Check if a location is at the same level as the player. * * @param checkLoc The location that shall be checked * @return true if the location is at the same level as the player */ @Contract(pure = true) boolean isBaseLevel(@Nonnull ServerCoordinate checkLoc) { return (playerLocation != null) && (playerLocation.getZ() == checkLoc.getZ()); } /** * Check how good the player character is able to see the target character. * * @param chara The character that is checked * @return the visibility of the character in percent */ @Contract(pure = true) public float canSee(@Nonnull Char chara) { if (isPlayer(chara.getCharId())) { return Char.VISIBILITY_MAX; } MapTile tile = World.getMap().getMapAt(chara.getVisibleLocation()); if ((tile == null) || tile.isHidden()) { return 0; } return Char.VISIBILITY_MAX; } /** * Check if a ID is the ID of the player. * * @param checkId the ID to be checked * @return true if it is the player, false if not */ @Contract(value = "null -> false", pure = true) public boolean isPlayer(@Nullable CharacterId checkId) { return (playerId != null) && playerId.equals(checkId); } /** * Check if the player is setup and ready. * <p /> * As long as this returns {@code false}, the player did not receive it's ID yet. * * @return {@code true} in case the player is properly set up. */ @Contract(pure = true) public boolean isPlayerIdSet() { return playerId != null; } /** * Check if the location of the character is set. * * @return {@code true} in case the location is set. */ @Contract(pure = true) public boolean isLocationSet() { return playerLocation != null; } /** * Get the map level the player character is currently standing on. * * @return The level the player character is currently standing on */ @Contract(pure = true) public int getBaseLevel() { return playerLocation == null ? 0 : playerLocation.getZ(); } /** * Check if there is currently a merchant list active. * * @return {@code true} in case a merchant list is active */ @Contract(pure = true) public boolean hasMerchantList() { return merchantDialog != null; } @Contract(pure = true) public boolean hasValidLocation() { return validLocation; } /** * Check of a position in server coordinates is on the screen of the player. * * @param testLoc The location that shall be checked. * @param tolerance an additional tolerance added to the default clipping distance * @return true if the position is within the clipping distance and the tolerance */ @Contract(pure = true) public boolean isOnScreen(@Nonnull ServerCoordinate testLoc, int tolerance) { if (playerLocation == null) { throw new IllegalStateException("The player location is not yet set."); } if (Math.abs(testLoc.getZ() - playerLocation.getZ()) > 2) { return false; } int width = MapDimensions.getInstance().getStripesWidth() >> 1; int height = MapDimensions.getInstance().getStripesHeight() >> 1; int limit = (Math.max(width, height) + tolerance) - 2; return (Math.abs(playerLocation.getX() - testLoc.getX()) + Math.abs(playerLocation.getY() - testLoc.getY())) < limit; } /** * Then this instance of player is removed its content needs to removed correctly as well. */ public void shutdown() { character.markAsRemoved(); movementHandler.shutdown(); } @Nonnull public CarryLoad getCarryLoad() { return carryLoad; } }