NoCheatPlus/NCPCore/src/main/java/fr/neatmonster/nocheatplus/players/PlayerDataManager.java

1165 lines
45 KiB
Java

/*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.neatmonster.nocheatplus.players;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.checks.CheckType;
import fr.neatmonster.nocheatplus.checks.ViolationHistory;
import fr.neatmonster.nocheatplus.checks.combined.CombinedData;
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
import fr.neatmonster.nocheatplus.compat.versions.BukkitVersion;
import fr.neatmonster.nocheatplus.compat.versions.GenericVersion;
import fr.neatmonster.nocheatplus.compat.versions.ServerVersion;
import fr.neatmonster.nocheatplus.components.NoCheatPlusAPI;
import fr.neatmonster.nocheatplus.components.config.ICheckConfig;
import fr.neatmonster.nocheatplus.components.config.IConfig;
import fr.neatmonster.nocheatplus.components.data.ICanHandleTimeRunningBackwards;
import fr.neatmonster.nocheatplus.components.data.ICheckData;
import fr.neatmonster.nocheatplus.components.data.IData;
import fr.neatmonster.nocheatplus.components.data.IDataOnJoin;
import fr.neatmonster.nocheatplus.components.data.IDataOnLeave;
import fr.neatmonster.nocheatplus.components.data.IDataOnReload;
import fr.neatmonster.nocheatplus.components.data.IDataOnRemoveSubCheckData;
import fr.neatmonster.nocheatplus.components.data.IDataOnWorldChange;
import fr.neatmonster.nocheatplus.components.data.IDataOnWorldUnload;
import fr.neatmonster.nocheatplus.components.registry.factory.IFactoryOne;
import fr.neatmonster.nocheatplus.components.registry.factory.RichFactoryRegistry;
import fr.neatmonster.nocheatplus.components.registry.factory.RichFactoryRegistry.CheckRemovalSpec;
import fr.neatmonster.nocheatplus.components.registry.feature.ComponentWithName;
import fr.neatmonster.nocheatplus.components.registry.feature.ConsistencyChecker;
import fr.neatmonster.nocheatplus.components.registry.feature.IDisableListener;
import fr.neatmonster.nocheatplus.components.registry.feature.IHaveCheckType;
import fr.neatmonster.nocheatplus.components.registry.feature.INeedConfig;
import fr.neatmonster.nocheatplus.components.registry.feature.IRemoveData;
import fr.neatmonster.nocheatplus.components.registry.feature.TickListener;
import fr.neatmonster.nocheatplus.components.registry.order.RegistrationOrder.RegisterMethodWithOrder;
import fr.neatmonster.nocheatplus.components.registry.order.RegistrationOrder.RegisterWithOrder;
import fr.neatmonster.nocheatplus.config.ConfPaths;
import fr.neatmonster.nocheatplus.config.ConfigFile;
import fr.neatmonster.nocheatplus.config.ConfigManager;
import fr.neatmonster.nocheatplus.event.mini.MiniListener;
import fr.neatmonster.nocheatplus.logging.StaticLog;
import fr.neatmonster.nocheatplus.logging.Streams;
import fr.neatmonster.nocheatplus.permissions.PermissionPolicy;
import fr.neatmonster.nocheatplus.permissions.PermissionRegistry;
import fr.neatmonster.nocheatplus.permissions.PermissionSettings;
import fr.neatmonster.nocheatplus.permissions.RegisteredPermission;
import fr.neatmonster.nocheatplus.players.PlayerMap.PlayerInfo;
import fr.neatmonster.nocheatplus.utilities.CheckTypeUtil;
import fr.neatmonster.nocheatplus.utilities.CheckUtils;
import fr.neatmonster.nocheatplus.utilities.IdUtil;
import fr.neatmonster.nocheatplus.utilities.StringUtil;
import fr.neatmonster.nocheatplus.utilities.TickTask;
import fr.neatmonster.nocheatplus.utilities.ds.corw.DualSet;
import fr.neatmonster.nocheatplus.utilities.ds.map.HashMapLOW;
import fr.neatmonster.nocheatplus.worlds.IWorldData;
import fr.neatmonster.nocheatplus.worlds.WorldDataManager;
/**
* Player data storage. May contain functionality of the DataManager, which
* isn't intended to be in a PlayerDataManager (refactoring stage).
*
* @author asofold
*
*/
// TODO: RegisterWithOrder still relevant ?
// TODO: Tag utility (common stuff).
@RegisterWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = "(^feature.*)", basePriority = "-80")
public class PlayerDataManager implements IPlayerDataManager, ComponentWithName, INeedConfig, ConsistencyChecker, IDisableListener {
/////////////////////
// Instance
/////////////////////
private int foundInconsistencies = 0;
/** PlayerData storage. */
private final HashMapLOW<UUID, PlayerData> playerData = new HashMapLOW<UUID, PlayerData>(100);
/** Primary thread only (no lock for this field): UUIDs to remove upon next bulk removal. */
private final Set<UUID> bulkPlayerDataRemoval = new LinkedHashSet<UUID>();
private final DualSet<UUID> frequentPlayerTasks = new DualSet<UUID>();
/**
* Access order for playerName (exact) -> ms time of logout.
* <hr>
* Later this might hold central player data objects instead of the long
* only.
*/
private final Map<UUID, Long> lastLogout = new LinkedHashMap<UUID, Long>(50, 0.75f, true);
/**
* Keeping track of online players. Currently id/name mappings are not kept
* on logout, but might be later.
*/
// TODO: Switch to UUIDs as keys, get data by uuid when possible, use PlayerMap for getting the UUID.
private final PlayerMap playerMap;
/**
* IRemoveData instances.
*/
// TODO: might use a map for those later (extra or not).
private final ArrayList<IRemoveData> iRemoveData = new ArrayList<IRemoveData>();
/**
* Execution histories of the checks.
*/
// TODO: Move to PlayerData / CheckTypeTree (NodeS).
private final Map<CheckType, Map<String, ExecutionHistory>> executionHistories = new HashMap<CheckType, Map<String,ExecutionHistory>>();
/** Flag if data expiration is active at all. */
private boolean doExpireData = false;
/**
* Duration in milliseconds for expiration of logged off players data. In
* the config minutes are used as unit.
*/
private long durExpireData = 0;
/** Data and execution history. */
private boolean deleteData = true;
/** Violation history and execution history. */
private boolean deleteHistory = false;
/**
* Reference for passing to PlayerData for handling permission caching and
* policies.
*/
/*
* TODO: Per world (rule/proxy) registries, with one central registry for
* ids (per-world registries would proxy id registration, but have their own
* rule settings).
*/
private final PermissionRegistry permissionRegistry;
private WorldDataManager worldDataManager;
private final Lock lock = new ReentrantLock();
// TODO: Consider same lock for some registry parts (deadlocking possibilities with exposed API).
private final RichFactoryRegistry<PlayerFactoryArgument> factoryRegistry = new RichFactoryRegistry<PlayerFactoryArgument>(lock);
private final TickListener tickListener = new TickListener() {
private int delayRareTasks = 0;
@Override
public void onTick(final int tick, final long timeLast) {
if (rareTasks(tick, timeLast)) {
delayRareTasks = 10;
}
else {
if (delayRareTasks == 0) {
}
else {
delayRareTasks --;
}
}
frequentTasks(tick, timeLast);
}
};
private final MiniListener<?>[] miniListeners = new MiniListener<?>[] {
/*
* TODO: Constants in a class 'ListenerTags', plus a plan
* (system.data.player.nocheatplus, system.nocheatplus.data ??,
* nocheatplus.system.data.player...). (RegistryTags for other?).
*/
new MiniListener<PlayerQuitEvent>() {
@EventHandler(priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", afterTag = ".*")
@Override
public void onEvent(final PlayerQuitEvent event) {
playerLeaves(event.getPlayer());
}
},
new MiniListener<PlayerKickEvent>() {
// TODO: ignoreCancelled !?
// TODO: afterTag !?
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", afterTag = "feature.*")
@Override
public void onEvent(final PlayerKickEvent event) {
playerLeaves(event.getPlayer());
}
},
new MiniListener<AsyncPlayerPreLoginEvent>() {
@EventHandler(priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = ".*")
@Override
public void onEvent(final AsyncPlayerPreLoginEvent event) {
// TODO: Maintain a flag for precondition (e.g. ProtocolLib present).
if (event.getLoginResult() == AsyncPlayerPreLoginEvent.Result.ALLOWED) {
onAsyncPlayerPreLogin(event);
}
}
},
new MiniListener<PlayerLoginEvent>() {
@EventHandler(priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = ".*")
@Override
public void onEvent(final PlayerLoginEvent event) {
// TODO: Maintain a flag for precondition (e.g. ProtocolLib present).
if (event.getResult() == PlayerLoginEvent.Result.ALLOWED) {
onPlayerLogin(event);
}
else {
onPlayerLoginDenied(event);
}
}
},
new MiniListener<PlayerJoinEvent>() {
@EventHandler(priority = EventPriority.LOWEST)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = ".*")
@Override
public void onEvent(final PlayerJoinEvent event) {
playerJoins(event);
}
},
new MiniListener<PlayerChangedWorldEvent>() {
@EventHandler(priority = EventPriority.LOWEST)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = ".*")
@Override
public void onEvent(final PlayerChangedWorldEvent event) {
playerChangedWorld(event);
}
},
new MiniListener<WorldUnloadEvent>() {
@EventHandler(priority = EventPriority.LOWEST)
@RegisterMethodWithOrder(tag = "system.nocheatplus.datamanager", beforeTag = ".*")
@Override
public void onEvent(final WorldUnloadEvent event) {
onWorldUnload(event);
}
},
};
/**
*
* @param worldDataManager
* @param permissionRegistry
*/
public PlayerDataManager(final WorldDataManager worldDataManager, final PermissionRegistry permissionRegistry) {
DataManager.instance = this; // TODO: Let NoCheatPlus do this, DataManager returns an ILockable.
if (ServerVersion.isMinecraftVersionUnknown()) {
// True hacks.
BukkitVersion.init();
}
final String version = ServerVersion.getMinecraftVersion();
if (GenericVersion.compareVersions(version, "1.8") >= 0 || version.equals("1.7.10") && Bukkit.getServer().getVersion().toLowerCase().indexOf("spigot") != -1) {
// Safe to assume Spigot, don't store Player instances.
playerMap = new PlayerMap(false);
}
else {
// Likely an older version without efficient mapping.
playerMap = new PlayerMap(true);
}
this.permissionRegistry = permissionRegistry; // TODO: World specific.
this.worldDataManager = worldDataManager;
// (Call support.)
factoryRegistry.createAutoGroup(IDataOnReload.class);
factoryRegistry.createAutoGroup(IDataOnWorldUnload.class);
factoryRegistry.createAutoGroup(IDataOnJoin.class);
factoryRegistry.createAutoGroup(IDataOnLeave.class);
factoryRegistry.createAutoGroup(IDataOnWorldChange.class);
factoryRegistry.createAutoGroup(IDataOnRemoveSubCheckData.class);
// Data/config removal.
factoryRegistry.createAutoGroup(IData.class);
factoryRegistry.createAutoGroup(IConfig.class);
factoryRegistry.createAutoGroup(ICheckData.class);
factoryRegistry.createAutoGroup(ICheckConfig.class);
}
/**
* Check the logged out players for if any data can be removed.<br>
* Currently only "dumb" full removal is performed. Later it is thinkable to
* remove "as much as reasonable".
*/
public void checkExpiration() {
if (!doExpireData || durExpireData <= 0) {
return;
}
final long now = System.currentTimeMillis();
final Set<Entry<UUID, Long>> entries = lastLogout.entrySet();
final Iterator<Entry<UUID, Long>> iterator = entries.iterator();
while (iterator.hasNext()) {
final Entry<UUID, Long> entry = iterator.next();
// TODO: Multi stage expiration.
final long ts = entry.getValue();
if (now - ts <= durExpireData) {
break;
}
final UUID playerId = entry.getKey();
// TODO: LEGACY handling: switch to UUIDs here for sure.
legacyPlayerDataExpirationRemovalByName(playerId, deleteData);
bulkPlayerDataRemoval.add(playerId); // For bulk removal.
iterator.remove();
}
// Bulk removal of PlayerData.
if (!bulkPlayerDataRemoval.isEmpty()) {
doBulkPlayerDataRemoval(); // Using this method allows checking for delayed removal etc.
}
}
private final void legacyPlayerDataExpirationRemovalByName(final UUID playerId,
final boolean deleteData) {
final String playerName = DataManager.getPlayerName(playerId);
if (playerName == null) {
// TODO: WARN
return;
}
// TODO: Validity of name?
if (deleteData) {
final PlayerData pData = playerData.get(playerId);
if (pData != null) {
pData.removeData(false); // TODO: staged ...
}
clearComponentData(CheckType.ALL, playerName);
}
if (deleteData || deleteHistory) {
removeExecutionHistory(CheckType.ALL, playerName);
}
if (deleteHistory) {
ViolationHistory.removeHistory(playerName);
}
}
/**
* Called by the rareTasksListener (OnDemandTickListener).
* @return "Did something" - true if data was removed or similar, i.e. reset the removal delay counter. False if nothing relevant had been done.
*/
private final boolean rareTasks(final int tick, final long timeLast) {
boolean something = false;
if (!bulkPlayerDataRemoval.isEmpty()) {
doBulkPlayerDataRemoval();
something = true;
}
// TODO: Process rarePlayerTasks
return something;
}
/**
* On tick.
*/
private final void frequentTasks(final int tick, final long timeLast) {
frequentPlayerTasks.mergePrimaryThread();
final Iterator<UUID> it = frequentPlayerTasks.iteratorPrimaryThread();
while (it.hasNext()) {
final PlayerData pData = getPlayerData(it.next(), null, false, null);
if (pData == null || pData.processTickFrequent(tick, timeLast)) {
it.remove();
}
}
}
/**
* Primary thread only. This checks for if players are/should be online.
*/
private final void doBulkPlayerDataRemoval() {
int size = bulkPlayerDataRemoval.size();
if (size > 0) {
// Test for online players.
final Iterator<UUID> it = bulkPlayerDataRemoval.iterator();
while (it.hasNext()) {
final UUID playerId = it.next();
boolean skip = !lastLogout.containsKey(playerId);
// TODO: Also remove fake players, thus test for logged in too.
/*
* TODO: Multi stage removal: (1) non essential like permission
* cache, (2) essential like set-back location, (3) all. In
* addition things will get shifty, once we use PlayerData
* during asynchronous login - possibly we'll need parked data
* then, also considering offline servers.
*/
if (skip) {
it.remove();
size --;
final PlayerData data = playerData.get(playerId);
if (data != null) {
// Should be online, keep essential data.
data.removeData(true);
}
continue;
}
}
// Actually remove data.
if (size > 0) {
playerData.remove(bulkPlayerDataRemoval);
bulkPlayerDataRemoval.clear();
if (ConfigManager.getConfigFile().getBoolean(ConfPaths.LOGGING_EXTENDED_STATUS)) {
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().debug(Streams.STATUS, "Bulk PlayerData removal: " + size);
}
}
}
}
/**
* Remove data instances from the cache for a given player and a given check
* type. CheckType.ALL and null will be interpreted as removing all data.
* <hr/>
* Does not touch the execution history.
* <hr/>
*
* @param playerName
* Exact player name.
* @param checkType
* Check type to remove data for, null is regarded as ALL.
* @return If any data was present (not strict).
*/
public boolean removeData(final String playerName, CheckType checkType) {
PlayerData pData = getPlayerData(playerName);
// TODO: Once working, use the most correct name from PlayerData.
final UUID playerId = pData == null ? getUUID(playerName) : pData.getPlayerId();
if (pData == null && playerId != null) {
pData = playerData.get(playerId);
}
boolean somethingFound = pData != null || playerId != null;
// TODO: Method signature with UUID / (I)PlayerData ?
if (checkType == null) {
checkType = CheckType.ALL;
}
// Check extended registered components.
/*
* TODO: "System data" might not be wise to erase for online players.
* ICheckData vs IData (...), except if registered for per check
* type removal.
*/
somethingFound |= clearComponentData(checkType, playerName);
if (pData != null) {
final CheckRemovalSpec removalSpec = new CheckRemovalSpec(checkType, true, this);
final boolean hasComplete = !removalSpec.completeRemoval.isEmpty();
final boolean hasSub = !removalSpec.subCheckRemoval.isEmpty();
if (hasComplete || hasSub) {
if (hasComplete) {
pData.removeAllGenericInstances(removalSpec.completeRemoval);
}
if (hasSub) {
pData.removeSubCheckData(removalSpec.subCheckRemoval, removalSpec.checkTypes);
}
// TODO: Remove the PlayerData instance, if necessary?
}
// TODO: Maintain a shouldBeOnline flag for fast skipping?
if (checkType == CheckType.ALL) {
// TODO: Fetch/use UUID early, and check validity of name.
if (playerId != null) {
bulkPlayerDataRemoval.add(playerId);
}
}
if (pData.isDebugActive(checkType)) {
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().debug(
Streams.TRACE_FILE,
CheckUtils.getLogMessagePrefix(playerName, checkType)
+ "Removed data.");
}
}
return somethingFound;
}
/**
* Check if player instances are stored for efficiency (legacy).
*
* @return
*/
public boolean storesPlayerInstances() {
return playerMap.storesPlayerInstances();
}
@Override
public boolean addComponent(IRemoveData obj) {
if (iRemoveData.contains(obj)) {
return false;
}
else {
iRemoveData.add((IRemoveData) obj);
return true;
}
}
@Override
public void removeComponent(IRemoveData obj) {
iRemoveData.remove((IRemoveData) obj);
}
/**
* Initializing with online players.
*/
public void onEnable() {
TickTask.addTickListener(tickListener);
final NoCheatPlusAPI api = NCPAPIProvider.getNoCheatPlusAPI();
for (final MiniListener<?> listener : miniListeners) {
api.addComponent(listener, false);
}
for (final Player player : BridgeMisc.getOnlinePlayers()) {
addOnlinePlayer(player);
}
}
/**
* Cleanup method, removes all data and config, but does not call
* ConfigManager.cleanup.
*/
@Override
public void onDisable() {
// TODO: Process pending set backs etc. -> iterate playerData -> onDisable.
clearData(CheckType.ALL);
playerData.clear(); // Also clear for online players.
iRemoveData.clear();
lastLogout.clear();
executionHistories.clear();
playerMap.clear();
// Finally alert (summary) if inconsistencies found.
if (foundInconsistencies > 0) {
StaticLog.logWarning("DataMan found " + foundInconsistencies + " inconsistencies (warnings suppressed).");
foundInconsistencies = 0;
}
}
public void onWorldUnload(final WorldUnloadEvent event) {
final Collection<Class<? extends IDataOnWorldUnload>> types = factoryRegistry.getGroupedTypes(IDataOnWorldUnload.class);
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().onWorldUnload(event.getWorld(), types);
}
}
/**
* Add mappings for player names variations.
* @param player
*/
private void addOnlinePlayer(final Player player) {
playerMap.updatePlayer(player);
}
/**
* Remove mappings for player names variations.
* @param player
*/
private void removeOnlinePlayer(final Player player) {
// TODO: Consider to only remove the Player instance? Yes do so... and remove the mapping if the full data expires only.
playerMap.remove(player);
}
/**
* Ensure a PlayerData instance exists for later use.
*
* @param event
*/
private void onAsyncPlayerPreLogin(final AsyncPlayerPreLoginEvent event) {
final UUID playerId = event.getUniqueId(); // Treat carefully :).
if (playerData.containsKey(playerId)) {
// Skip if a PlayerData instance already exists.
return;
}
else {
// Create with default world data.
getPlayerData(playerId, event.getName(), true, worldDataManager.getDefaultWorldData()).addTag(PlayerData.TAG_OPTIMISTIC_CREATE);
}
}
private void onPlayerLoginDenied(final PlayerLoginEvent event) {
// Consistency check existing data.
final UUID playerId = event.getPlayer().getUniqueId();
final PlayerData pData = getPlayerData(playerId);
if (pData != null && pData.hasTag(PlayerData.TAG_OPTIMISTIC_CREATE)) {
bulkPlayerDataRemoval.add(playerId);
}
}
/**
* Just update the world data for later use.
*
* @param event
*/
private void onPlayerLogin(final PlayerLoginEvent event) {
// Consistency check existing data.
final Player player = event.getPlayer();
final UUID playerId = player.getUniqueId();
final PlayerData pData = getPlayerData(playerId);
if (pData == null) {
// Create an instance.
// TODO: Legacy server compatibility with world getting?
getPlayerData(player);
}
else {
// Consistency check.
final String playerName = pData.getPlayerName();
if (!playerName.equals(player.getName())) {
updatePlayerName(playerId, playerName, pData, "login");
}
// Update world.
pData.updateCurrentWorld(worldDataManager.getWorldData(player.getWorld()));
}
pData.removeTag(PlayerData.TAG_OPTIMISTIC_CREATE);
}
private void updatePlayerName(final UUID playerId, final String playerName,
final PlayerData pData, String tag) {
// Name change.
pData.updatePlayerName(playerName);
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.STATUS,
CheckUtils.getLogMessagePrefix(playerName, null)
+ " Update player name for id " + playerId + ": " + playerName
+ "(" + tag + (pData.hasTag(PlayerData.TAG_OPTIMISTIC_CREATE) ?
", optimistically created data" : "") + ")");
}
private void playerJoins(final PlayerJoinEvent event) {
final long timeNow = System.currentTimeMillis();
final Player player = event.getPlayer();
final UUID playerId = player.getUniqueId();
//
lastLogout.remove(playerId);
addOnlinePlayer(player);
//
final PlayerData pData = getPlayerData(player, true);
// Consistency check.
final String playerName = pData.getPlayerName();
if (!playerName.equals(player.getName())) {
updatePlayerName(playerId, playerName, pData, "login");
}
// Data stuff.
final Collection<Class<? extends IDataOnJoin>> types = factoryRegistry.getGroupedTypes(IDataOnJoin.class);
pData.onPlayerJoin(player, player.getWorld(), timeNow, worldDataManager, types);
pData.getGenericInstance(CombinedData.class).lastJoinTime = timeNow;
}
/**
* Quit or kick.
* @param player
*/
private void playerLeaves(final Player player) {
final long timeNow = System.currentTimeMillis();
final UUID playerId = player.getUniqueId();
lastLogout.put(playerId, timeNow);
final PlayerData pData = playerData.get(playerId);
if (pData != null) {
final Collection<Class<? extends IDataOnLeave>> types = factoryRegistry.getGroupedTypes(IDataOnLeave.class);
pData.onPlayerLeave(player, timeNow, types);
pData.getGenericInstance(CombinedData.class).lastLogoutTime = timeNow;
}
else {
// TODO: put lastLogoutTime to OfflinePlayerData ?
}
removeOnlinePlayer(player);
}
private void playerChangedWorld(final PlayerChangedWorldEvent event) {
final Player player = event.getPlayer();
final PlayerData pData = getPlayerData(player, true);
final Collection<Class<? extends IDataOnWorldChange>> types = factoryRegistry.getGroupedTypes(IDataOnWorldChange.class);
pData.onPlayerChangedWorld(player, event.getFrom(), player.getWorld(),
worldDataManager, types);
}
/**
* Fetch settings from the current default config.
*/
private void adjustSettings() {
final ConfigFile config = ConfigManager.getConfigFile();
doExpireData = config.getBoolean(ConfPaths.DATA_EXPIRATION_ACTIVE);
durExpireData = config.getLong(ConfPaths.DATA_EXPIRATION_DURATION, 1, 1000000, 60) * 60000L; // in minutes
deleteData = config.getBoolean(ConfPaths.DATA_EXPIRATION_DATA, true); // hidden.
deleteHistory = config.getBoolean(ConfPaths.DATA_EXPIRATION_HISTORY);
// TODO: Per world permission registries: need world configs (...).
Set<RegisteredPermission> changedPermissions = null;
try {
// TODO: Only update if changes are there - should have a config-path hash+size thing (+ setting).
changedPermissions = permissionRegistry.updateSettings(PermissionSettings.fromConfig(config,
ConfPaths.PERMISSIONS_POLICY_DEFAULT, ConfPaths.PERMISSIONS_POLICY_RULES));
}
catch (Exception e) {
StaticLog.logSevere("Failed to read the permissions setup. Relay to ALWAYS policy.");
StaticLog.logSevere(e);
permissionRegistry.updateSettings(new PermissionSettings(null, null, new PermissionPolicy()));
}
// Invalidate all already fetched permissions.
for(final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().adjustSettings(changedPermissions);
}
}
@Override
public void onReload() {
// present.
adjustSettings();
final Collection<Class<? extends IDataOnReload>> types = factoryRegistry.getGroupedTypes(IDataOnReload.class);
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().onReload(types);
}
}
@Override
public String getComponentName() {
return "NoCheatPlus_DataManager";
}
@Override
public void checkConsistency(final Player[] onlinePlayers) {
// Check online player tracking consistency.
int missing = 0;
int changed = 0;
for (int i = 0; i < onlinePlayers.length; i++) {
final Player player = onlinePlayers[i];
final UUID id = player.getUniqueId();
// if (player.isOnline()) {
// TODO: Add a consistency check method !?
if (!playerMap.hasPlayerInfo(id)) {
missing ++;
// TODO: Add the player [problem: messy NPC plugins?]?
}
if (playerMap.storesPlayerInstances() && player != playerMap.getPlayer(id)) {
changed ++;
// Update the reference.
addOnlinePlayer(player);
// }
}
}
// TODO: Consider checking lastLogout for too long gone players.
// TODO: Later the map size will not work, if we keep name/id mappings after logout. Other checking methods are possible.
final int storedSize = this.playerMap.size();
if (missing != 0 || changed != 0 || onlinePlayers.length != storedSize) {
foundInconsistencies ++;
if (!ConfigManager.getConfigFile().getBoolean(ConfPaths.DATA_CONSISTENCYCHECKS_SUPPRESSWARNINGS)) {
final List<String> details = new LinkedList<String>();
if (missing != 0) {
details.add("missing online players (" + missing + ")");
}
if (onlinePlayers.length != storedSize) {
// TODO: Consider checking for not online players and remove them.
details.add("wrong number of online players (" + storedSize + " instead of " + onlinePlayers.length + ")");
}
if (changed != 0) {
details.add("changed player instances (" + changed + ")");
}
StaticLog.logWarning("DataMan inconsistencies: " + StringUtil.join(details, " | "));
}
}
}
void registerFrequentPlayerTaskPrimaryThread(final UUID playerId) {
frequentPlayerTasks.addPrimaryThread(playerId);
}
void registerFrequentPlayerTaskAsynchronous(final UUID playerId) {
frequentPlayerTasks.addAsynchronous(playerId);
}
/**
* Might yield false negatives, should be reasonable on performance.
*
* @param playerId
* @return
*/
boolean isFrequentPlayerTaskScheduled(final UUID playerId) {
// TODO : Efficient impl / optimized methods?
if (Bukkit.isPrimaryThread()) {
return frequentPlayerTasks.containsPrimaryThread(playerId);
}
else {
return frequentPlayerTasks.containsAsynchronous(playerId);
}
}
/**
* Clear player related data, only for registered components (not execution
* history, violation history, normal check data).<br>
* That should at least go for chat engine data.
*
* @param CheckType
* @param PlayerName
* @return If something was removed.
*/
public boolean clearComponentData(final CheckType checkType, final String PlayerName) {
// TODO: UUID.
boolean removed = false;
for (final IRemoveData rmd : iRemoveData) {
if (checkType == CheckType.ALL) {
// Not sure this is really good, though.
if (rmd.removeData(PlayerName) != null) {
removed = true;
}
}
else if (rmd instanceof IHaveCheckType) {
final CheckType refType = ((IHaveCheckType) rmd).getCheckType();
if (refType == checkType || CheckTypeUtil.isAncestor(checkType, refType)) {
if (rmd.removeData(PlayerName) != null) {
removed = true;
}
}
}
}
return removed;
}
/**
* Adjust to the system time having run backwards. This is much like
* clearData(CheckType.ALL), with the exception of calling
* ICanHandleTimeRunningBackwards.handleTimeRanBackwards for data instances
* which implement this.
*/
public void handleSystemTimeRanBackwards() {
// TODO: WorldDataManager should have an extra method and be called before this.
// Collect data factories and clear execution history.
for (final CheckType type : CheckTypeUtil.getWithDescendants(CheckType.ALL)) {
final Map<String, ExecutionHistory> map = executionHistories.get(type);
if (map != null) {
map.clear();
}
}
for (final IRemoveData rmd : iRemoveData) {
if (rmd instanceof ICanHandleTimeRunningBackwards) {
((ICanHandleTimeRunningBackwards) rmd).handleTimeRanBackwards();
}
else {
rmd.removeAllData();
}
}
ViolationHistory.clear(CheckType.ALL);
// PlayerData
// TODO: Register explicitly (adding IDataOnTimeRanBackwards)?
Collection<Class<? extends IData>> dataTypes = factoryRegistry.getGroupedTypes(IData.class);
for (final Entry<UUID, PlayerData> entry : playerData.iterable()){
entry.getValue().handleTimeRanBackwards(dataTypes);
}
}
/**
* Fetch a PlayerData instance. If none is present and create is set, a new
* instance will be created.
*
* @param playerId
* @param playerName
* Exact player name (rather).
* @param create
* @param worldData
* WorldData is only used for creating new instances, in which
* case it must not be null.
* @return
*/
PlayerData getPlayerData(final UUID playerId,
final String playerName, final boolean create,
final IWorldData worldData) {
final PlayerData data = playerData.get(playerId);
if (!create || data != null) {
return data;
}
else {
// Creating this should be mostly harmless.
// TODO: Might want to lock still (same lock as used within the
// playerData map).
final PlayerData newData = new PlayerData(playerId, playerName,
permissionRegistry);
final PlayerData oldData = playerData.putIfAbsent(playerId,
newData);
final PlayerData usedData = oldData == null ? newData : oldData;
usedData.updateCurrentWorld(
worldData == null ? worldDataManager.getDefaultWorldData()
: worldData);
return usedData;
}
}
@Override
public PlayerData getPlayerData(final Player player, boolean create) {
return getPlayerData(player.getUniqueId(), player.getName(),
create, create ? worldDataManager.getWorldDataSafe(player) : null);
}
@Override
public PlayerData getPlayerData(final Player player) {
return getPlayerData(player, true);
}
@Override
public PlayerData getPlayerData(final String playerName) {
final UUID playerId = DataManager.getUUID(playerName);
return playerId == null ? null : playerData.get(playerId);
}
@Override
public PlayerData getPlayerData(final UUID playerId) {
return playerData.get(playerId);
}
@Override
public Player getPlayerExact(final String playerName) {
return playerMap.getPlayerExact(playerName);
}
@Override
public UUID getUUID(final String input) {
// TODO: Use player map.
final Player player = getPlayer(input);
if (player != null) {
return player.getUniqueId();
}
return IdUtil.UUIDFromStringSafe(input);
}
@Override
public String getPlayerName(final UUID playerId) {
final PlayerInfo info = playerMap.getPlayerInfo(playerId);
if (info != null && info.exactName != null) {
return info.exactName;
}
final PlayerData data = playerData.get(playerId);
if (data != null) {
return data.getPlayerName();
}
return null;
}
@Override
public Player getPlayer(final UUID id) {
return playerMap.getPlayer(id);
}
@Override
public Player getPlayer(final String playerName) {
return playerMap.getPlayer(playerName);
}
@Override
public void restoreDefaultDebugFlags() {
// (Note that WorldData is resetting differently, and before this.)
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().resetDebug();
}
}
@Override
public void clearAllExemptions() {
final Iterator<Entry<UUID, PlayerData>> it = playerData.iterator();
while (it.hasNext()) {
it.next().getValue().clearAllExemptions();
}
}
@Override
public <T> void removeGenericInstance(Class<T> registeredFor) {
// TODO: Really needs OfflinePlayerData for more frequent data removal.
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().removeGenericInstance(registeredFor);
}
}
@Override
public void clearData(final CheckType checkType) {
final CheckRemovalSpec removalSpec = new CheckRemovalSpec(checkType, true, this);
final boolean hasComplete = !removalSpec.completeRemoval.isEmpty();
final boolean hasSub = !removalSpec.subCheckRemoval.isEmpty();
if (hasComplete || hasSub) {
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
final IPlayerData pData = entry.getValue();
if (hasComplete) {
pData.removeAllGenericInstances(removalSpec.completeRemoval);
}
if (hasSub) {
pData.removeSubCheckData(removalSpec.subCheckRemoval, removalSpec.checkTypes);
}
// TODO: Remove the PlayerData instance, if suitable?
}
}
// TODO: IRemoveData - why register here at all ?
for (final IRemoveData rmd : iRemoveData) {
if (checkType == CheckType.ALL) {
// Not sure this is really good, though.
rmd.removeAllData();
}
else if (rmd instanceof IHaveCheckType) {
final CheckType refType = ((IHaveCheckType) rmd).getCheckType();
if (refType == checkType || CheckTypeUtil.isAncestor(checkType, refType)) {
rmd.removeAllData();
}
}
}
for (final CheckType type : removalSpec.checkTypes) {
final Map<String, ExecutionHistory> map = executionHistories.get(type);
if (map != null) {
map.clear();
}
}
ViolationHistory.clear(checkType);
// TODO: PlayerData removal should have other mechanisms (stages).
if (checkType == CheckType.ALL) {
bulkPlayerDataRemoval.addAll(playerData.getKeys());
doBulkPlayerDataRemoval(); // Only removes offline player data.
}
}
/**
* Used by checks to register the history for external access.<br>
* NOTE: This method is not really meant to be used from outside NCP.
*
* @param type
* @param histories
* @deprecated New implementation pending.
*/
public void registerExecutionHistory(CheckType type, Map<String, ExecutionHistory> histories) {
executionHistories.put(type, histories);
}
/**
* Access method to the the execution history for check type for a player.
*
* @param type
* @param playerName
* Exact case for player name.
* @return null if not present.
* @deprecated New implementation pending.
*/
public ExecutionHistory getExecutionHistory(final CheckType type, final String playerName) {
final Map<String, ExecutionHistory> map = executionHistories.get(type);
if (map != null) {
return map.get(playerName);
}
return null;
}
/**
* Remove the execution history for a player for the given check type.
*
* @param type
* @param playerName
* @return
* @deprecated New implementation pending.
*/
public boolean removeExecutionHistory(final CheckType type, final String playerName) {
boolean removed = false;
// TODO: design ...
for (final CheckType refType : CheckTypeUtil.getWithDescendants(type)) {
final Map<String, ExecutionHistory> map = executionHistories.get(refType);
if (map != null && map.remove(playerName) != null) {
removed = true;
}
}
return removed;
}
@Override
public <T> void registerFactory(final Class<T> registerFor,
final IFactoryOne<PlayerFactoryArgument, T> factory) {
factoryRegistry.registerFactory(registerFor, factory);
}
@Override
public <T> Collection<Class<? extends T>> getGroupedTypes(final Class<T> groupType) {
return factoryRegistry.getGroupedTypes(groupType);
}
@Override
public <T> Collection<Class<? extends T>> getGroupedTypes(final Class<T> groupType,
final CheckType checkType) {
return factoryRegistry.getGroupedTypes(groupType, checkType);
}
@Override
public <I> void addToGroups(final Class<I> itemType,
final Class<? super I>... groupTypes) {
factoryRegistry.addToGroups(itemType, groupTypes);
}
@Override
public <I> void addToGroups(CheckType checkType, Class<I> itemType,
Class<? super I>... groupTypes) {
factoryRegistry.addToGroups(checkType, itemType, groupTypes);
}
@Override
public void addToExistingGroups(Class<?> itemType) {
factoryRegistry.addToExistingGroups(itemType);
}
@Override
public <I> void addToExistingGroups(final CheckType checkType,
final Class<I> itemType) {
factoryRegistry.addToExistingGroups(checkType, itemType);
}
@Override
public <G> void createGroup(final Class<G> groupType) {
factoryRegistry.createGroup(groupType);
}
@Override
public <G> void createAutoGroup(final Class<G> groupType) {
factoryRegistry.createAutoGroup(groupType);
}
@Override
public <I> void addToGroups(final Collection<CheckType> checkTypes,
final Class<I> itemType, final Class<? super I>... groupTypes) {
factoryRegistry.addToGroups(checkTypes, itemType, groupTypes);
}
@Override
public <I> void addToExistingGroups(final Collection<CheckType> checkTypes,
final Class<I> itemType) {
factoryRegistry.addToExistingGroups(checkTypes, itemType);
}
@Override
public <T> T getNewInstance(final Class<T> registeredFor,
final PlayerFactoryArgument arg) {
return factoryRegistry.getNewInstance(registeredFor, arg);
}
@Override
public void removeCachedConfigs() {
final Collection<Class<?>> types = new LinkedHashSet<Class<?>>(
factoryRegistry.getGroupedTypes(IConfig.class));
if (!types.isEmpty()) {
for (final Entry<UUID, PlayerData> entry : playerData.iterable()) {
entry.getValue().removeAllGenericInstances(types);
}
}
}
}