package tc.oc.commons.bukkit.nick; import java.util.HashMap; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventBus; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import tc.oc.api.bukkit.friends.OnlineFriends; import tc.oc.api.bukkit.users.BukkitUserStore; import tc.oc.api.bukkit.users.OnlinePlayers; import tc.oc.api.docs.PlayerId; import tc.oc.api.docs.Session; import tc.oc.api.docs.UserId; import tc.oc.minecraft.scheduler.SyncExecutor; import tc.oc.api.users.UserSearchResponse; import tc.oc.commons.bukkit.event.UserLoginEvent; import tc.oc.commons.bukkit.util.PlayerStates; import tc.oc.commons.core.plugin.PluginFacet; @Singleton public class IdentityProviderImpl implements IdentityProvider, Listener, PluginFacet { private static final String REVEAL_ALL_PERMISSION = "nick.see-through-all"; private final BukkitUserStore userStore; private final EventBus eventBus; private final OnlinePlayers onlinePlayers; private final PlayerStates playerStates; private final OnlineFriends friendMap; private final SyncExecutor syncExecutor; private final ConsoleIdentity consoleIdentity; private final Map<Player, Identity> identities = new HashMap<>(); private final Map<String, Player> nicknames = new HashMap<>(); @Inject IdentityProviderImpl(BukkitUserStore userStore, EventBus eventBus, OnlinePlayers onlinePlayers, PlayerStates playerStates, OnlineFriends friendMap, SyncExecutor syncExecutor, ConsoleIdentity consoleIdentity) { this.userStore = userStore; this.eventBus = eventBus; this.onlinePlayers = onlinePlayers; this.playerStates = playerStates; this.friendMap = friendMap; this.syncExecutor = syncExecutor; this.consoleIdentity = consoleIdentity; } @Override public boolean revealAll(CommandSender viewer) { return viewer.hasPermission(REVEAL_ALL_PERMISSION); } @Override public boolean reveal(CommandSender viewer, UserId userId) { return revealAll(viewer) || friendMap.areFriends(viewer, userId); } @Override public Identity createIdentity(PlayerId playerId, @Nullable String nickname) { return new IdentityImpl(onlinePlayers, friendMap, playerStates, this, playerId, nickname); } @Override public Identity createIdentity(Player player, @Nullable String nickname) { return createIdentity(userStore.getUser(player), nickname); } @Override public Identity createIdentity(CommandSender player) { return player instanceof Player ? createIdentity((Player) player, null) : consoleIdentity(); } @Override public Identity consoleIdentity() { return consoleIdentity; } @Override public Identity createIdentity(Session session) { return createIdentity(session.user(), session.nickname()); } @Override public Identity createIdentity(UserSearchResponse response) { if(response.last_session != null) { return createIdentity(response.user, response.last_session.nickname()); } else { return createIdentity(response.user, null); } } @Override public Identity currentIdentity(CommandSender player) { if(player instanceof Player) { return currentIdentity(userStore.getUser((Player) player), (Player) player); } else { return consoleIdentity; } } @Override public Identity currentIdentity(PlayerId playerId) { return currentIdentity(playerId, onlinePlayers.find(playerId)); } private Identity currentIdentity(PlayerId playerId, @Nullable Player player) { Identity identity = identities.get(player); if(identity == null) { identity = createIdentity(playerId, null); if(player != null && player.willBeOnline()) { identities.put(player, identity); } } return identity; } @Override public @Nullable Identity onlineIdentity(String name) { Player player = nicknames.get(name); if(player == null) player = onlinePlayers.find(name); return player == null ? null : currentIdentity(player); } @Override public void changeIdentity(Player player, @Nullable String nickname) { final Identity oldIdentity = currentIdentity(player); if(Objects.equals(oldIdentity.getNickname(), nickname)) return; applyNickname(player, oldIdentity.getNickname(), nickname); eventBus.callEvent(new PlayerIdentityChangeEvent(player, oldIdentity, currentIdentity(player))); } protected void validateNickname(@Nullable String nickname) { if(nickname != null && onlinePlayers.find(nickname) != null) { throw new IllegalArgumentException("Nickname '" + nickname + "' is the name of a real online player"); } } protected void applyNickname(Player player, @Nullable String oldNickname, @Nullable String newNickname) { validateNickname(newNickname); if(oldNickname != null) { nicknames.remove(oldNickname); } final Identity identity = createIdentity(userStore.getUser(player), newNickname); if(player.willBeOnline()) { identities.put(player, identity); if(newNickname != null) { nicknames.put(newNickname, player); } } } /** * If a player has a nickname set in their user document on login, apply it. * This needs to run before {@link PlayerAppearanceListener#refreshNamesOnLogin} */ @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void applyNicknameOnLogin(UserLoginEvent event) { if(event.getUser().nickname() != null) { applyNickname(event.getPlayer(), null, event.getUser().nickname()); } } /** * Clean up after quitting players */ @EventHandler(priority = EventPriority.MONITOR) public void deactivateNickOnQuit(PlayerQuitEvent event) { final Identity identity = identities.remove(event.getPlayer()); if(identity != null && identity.getNickname() != null) { nicknames.remove(identity.getNickname()); } } /** * Clear any nickname that collides with the real name of a player logging in. * This ensures that usernames + nicknames together contain no duplicates. * The user who's nickname was cleared is not notified of this, but this * should be an extremely rare situation, so it's not a big problem. */ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false) public void clearConflictingNicks(AsyncPlayerPreLoginEvent event) { final String name = event.getName(); syncExecutor.execute(() -> { final Player player = nicknames.get(name); if(player != null) { changeIdentity(player, null); } }); } }