/*
* 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 illarion.client.IllaClient;
import illarion.client.graphics.Avatar;
import illarion.client.net.client.RequestAppearanceCmd;
import illarion.client.world.events.CharRemovedEvent;
import illarion.common.config.ConfigChangedEvent;
import illarion.common.types.CharacterId;
import illarion.common.types.ServerCoordinate;
import javolution.util.FastTable;
import org.bushe.swing.event.EventBus;
import org.bushe.swing.event.annotation.AnnotationProcessor;
import org.bushe.swing.event.annotation.EventTopicSubscriber;
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.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Handles all characters known to the client but the player character.
*
* @author Martin Karing <nitram@illarion.org>
* @author Nop
*/
@ThreadSafe
public final class People {
/**
* The logger instance that takes care for the logging output of this class.
*/
@Nonnull
private static final Logger log = LoggerFactory.getLogger(People.class);
/**
* This is the format string that is displayed in the {@link #toString()} function.
*/
@Nonnull
private static final String TO_STRING_TEXT = "People Manager - %d$1 characters in storage";
/**
* The list of visible characters.
*/
@Nonnull
@GuardedBy("charsLock")
private final Map<CharacterId, Char> chars;
/**
* The lock that is used to secure the chars table properly.
*/
@Nonnull
private final ReentrantReadWriteLock charsLock;
/**
* A list of characters that are going to be removed.
*/
@Nonnull
private final List<Char> removalList;
private int permanentAvatarTagState;
/**
* Default constructor. Sets up all needed base variables to init the class.
*/
public People() {
removalList = new FastTable<>();
chars = new HashMap<>();
charsLock = new ReentrantReadWriteLock();
permanentAvatarTagState = IllaClient.getCfg().getInteger("showAvatarTagPermanently");
AnnotationProcessor.process(this);
}
@EventTopicSubscriber(topic = "showAvatarTagPermanently")
public void onQuestMarkerSettingsChanged(@Nonnull String topic, @Nonnull ConfigChangedEvent event) {
if ("showAvatarTagPermanently".equals(topic)) {
permanentAvatarTagState = event.getConfig().getInteger(topic);
}
}
@Contract(value = "null->false", pure = true)
public boolean isAvatarTagShown(@Nullable CharacterId id) {
if (id == null) {
return false;
}
switch (permanentAvatarTagState % 3) {
case 0:
return false;
case 1:
return id.isHuman() && !World.getPlayer().isPlayer(id);
case 2:
return !World.getPlayer().isPlayer(id);
default:
return false;
}
}
/**
* Get a character on the client screen. If the character is not available, its created and the appearance is
* requested from the server.
*
* @param id the ID of the character
* @return the character that was requested
*/
@Nonnull
public Char accessCharacter(@Nonnull CharacterId id) {
if (World.getPlayer().isPlayer(id)) {
return World.getPlayer().getCharacter();
}
Char chara = getCharacter(id);
if (chara == null) {
return createNewCharacter(id);
}
return chara;
}
@Nullable
public Char getCharOnScreenLoc(int x, int y) {
charsLock.readLock().lock();
try {
@Nullable Char characterOnScreenLocation = null;
for (Char character : chars.values()) {
Avatar avatar = character.getAvatar();
if ((avatar != null) && character.getInteractive().isCharOnScreenLoc(x, y)) {
if (characterOnScreenLocation == null) {
characterOnScreenLocation = character;
} else {
Avatar otherAvatar = characterOnScreenLocation.getAvatar();
assert otherAvatar != null; /* characterOnScreenLocation can't store a char without avatar */
if (avatar.getOrder() < otherAvatar.getOrder()) {
characterOnScreenLocation = character;
}
}
}
}
return characterOnScreenLocation;
} finally {
charsLock.readLock().unlock();
}
}
/**
* Add a character to the list of known characters.
*
* @param chara the character that shall be added
*/
private void addCharacter(@Nonnull Char chara) {
if (chara.getCharId() == null) {
throw new IllegalArgumentException("Adding character without ID is illegal.");
}
if (World.getPlayer().isPlayer(chara.getCharId())) {
throw new IllegalArgumentException("Adding player character to the chars list is not allowed.");
}
charsLock.writeLock().lock();
try {
chars.put(chara.getCharId(), chara);
} finally {
charsLock.writeLock().unlock();
}
}
/**
* Add a character to the list of characters that are going to be removed at the next run of {@link
* #cleanRemovalList()}.
*
* @param removeChar the character that is going to be removed
*/
public void addCharacterToRemoveList(@Nonnull Char removeChar) {
if (removeChar.getCharId() == null) {
throw new IllegalArgumentException("Removing character without ID is illegal.");
}
if (World.getPlayer().isPlayer(removeChar.getCharId())) {
throw new IllegalArgumentException("Removing player character from the chars list is not allowed.");
}
removalList.add(removeChar);
}
/**
* Clean up the removal list. All character from the list of character to remove get removed and recycled. The list
* is cleared after calling this function.
*/
public void cleanRemovalList() {
charsLock.writeLock().lock();
try {
if (!removalList.isEmpty()) {
for (Char removeChar : removalList) {
CharacterId removeId = removeChar.getCharId();
if (removeId == null) {
log.error("Character without ID located in remove list.");
continue;
}
removeCharacter(removeId);
}
removalList.clear();
}
} finally {
charsLock.writeLock().unlock();
}
}
/**
* Clear the list of characters and recycle all of them.
*/
public void clear() {
if (World.getPlayer().getCombatHandler().isAttacking()) {
World.getPlayer().getCombatHandler().standDown();
}
charsLock.writeLock().lock();
try {
cleanRemovalList();
chars.values().forEach(Char::markAsRemoved);
chars.clear();
} finally {
charsLock.writeLock().unlock();
}
}
/**
* Check all known characters if they are outside of the screen and hide them from the screen. Save them still to
* the characters that are known to left the screen.
*/
public void clipCharacters() {
charsLock.writeLock().lock();
try {
@Nonnull Player player = World.getPlayer();
for (Char character : chars.values()) {
ServerCoordinate charLocation = character.getLocation();
if ((charLocation != null) && !player.isOnScreen(charLocation, 0)) {
addCharacterToRemoveList(character);
}
}
cleanRemovalList();
} finally {
charsLock.writeLock().unlock();
}
}
/**
* This function creates a new character and requests the required information from the server.
*
* @param id the ID of the character to be created
* @return the created character
*/
@Nonnull
private Char createNewCharacter(@Nonnull CharacterId id) {
log.debug("Creating new character: {}", id);
Char chara = new Char();
chara.setCharId(id);
addCharacter(chara);
World.getNet().sendCommand(new RequestAppearanceCmd(id));
return chara;
}
/**
* Get a character out of the list of the known characters.
*
* @param id ID of the requested character
* @return the character or {@code null} if it does not exist
*/
@Nullable
@Contract(value = "null -> null", pure = true)
public Char getCharacter(@Nullable CharacterId id) {
if (id == null) {
return null;
}
if (World.getPlayer().isPlayer(id)) {
return World.getPlayer().getCharacter();
}
charsLock.readLock().lock();
try {
return chars.get(id);
} finally {
charsLock.readLock().unlock();
}
}
/**
* Get the character on a special location on the map.
*
* @param coordinate the location the character is searched at
* @return the character or {@code null} if not found
*/
@Nullable
public Char getCharacterAt(@Nonnull ServerCoordinate coordinate) {
Char playerChar = World.getPlayer().getCharacter();
if (coordinate.equals(playerChar.getLocation())) {
return playerChar;
}
charsLock.readLock().lock();
try {
for (Char character : chars.values()) {
if (coordinate.equals(character.getLocation())) {
return character;
}
}
} finally {
charsLock.readLock().unlock();
}
return null;
}
/**
* Remove a character from the game list and recycle the character reference for later usage. Also clean up
* everything related to this character such as the attacking marker.
*
* @param id the ID of the character that shall be removed
*/
public void removeCharacter(@Nonnull CharacterId id) {
if (World.getPlayer().isPlayer(id)) {
throw new IllegalArgumentException("Removing the player character from the people list is not legal.");
}
charsLock.writeLock().lock();
try {
Char chara = chars.get(id);
if (chara != null) {
EventBus.publish(new CharRemovedEvent(id));
// cancel attack when character is removed
if (World.getPlayer().getCombatHandler().isAttacking(chara)) {
World.getPlayer().getCombatHandler().standDown();
}
chars.remove(id);
chara.markAsRemoved();
}
} finally {
charsLock.writeLock().unlock();
}
}
/**
* Get the string representation of this instance.
*/
@Override
@Nonnull
public String toString() {
charsLock.readLock().lock();
try {
return String.format(TO_STRING_TEXT, chars.size());
} finally {
charsLock.readLock().unlock();
}
}
/**
* Force update of light values.
*/
void updateLight() {
World.getPlayer().getCharacter().updateLight(Char.LIGHT_UPDATE);
charsLock.readLock().lock();
try {
for (Char character : chars.values()) {
character.updateLight(Char.LIGHT_UPDATE);
}
} finally {
charsLock.readLock().unlock();
}
}
}