Java tutorial
package com.jabyftw.lobstercraft.player; import com.jabyftw.lobstercraft.ConfigurationValues; import com.jabyftw.lobstercraft.LobsterCraft; import com.jabyftw.lobstercraft.Permissions; import com.jabyftw.lobstercraft.services.Service; import com.jabyftw.lobstercraft.services.services_event.*; import com.jabyftw.lobstercraft.util.DatabaseState; import com.jabyftw.lobstercraft.util.Util; import com.jabyftw.lobstercraft.world.CityOccupation; import com.sun.istack.internal.NotNull; import com.sun.istack.internal.Nullable; import net.milkbowl.vault.permission.Permission; import org.apache.commons.lang.builder.HashCodeBuilder; import org.bukkit.Bukkit; import org.bukkit.Server; 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.PlayerJoinEvent; import org.bukkit.event.player.PlayerKickEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.util.NumberConversions; import java.lang.reflect.Constructor; import java.sql.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; /** * Copyright (C) 2016 Rafael Sartori for LobsterCraft Plugin * <p/> * 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. * <p/> * 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. * <p/> * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * <p/> * Email address: rafael.sartori96@gmail.com */ public class PlayerHandlerService extends Service { // Lock used when multiple maps are used on the same space private final Object playerMapsLock = new Object(); /* * Online players */ private final ConcurrentHashMap<Player, OnlinePlayer> onlinePlayers_player = new ConcurrentHashMap<>(); private final ConcurrentHashMap<OfflinePlayer, OnlinePlayer> onlinePlayers_offlinePlayer = new ConcurrentHashMap<>(); /* * Offline players */ private final ConcurrentHashMap<Integer, HashSet<OfflinePlayer>> registeredOfflinePlayers_cityId = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, OfflinePlayer> registeredOfflinePlayers_name = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Integer, OfflinePlayer> registeredOfflinePlayers_id = new ConcurrentHashMap<>(); // Unregistered players that may be going to rejoin the server before it closes private final ConcurrentHashMap<String, OfflinePlayer> unregisteredOfflinePlayers_name = new ConcurrentHashMap<>(); /* * Profile handling */ private final ConcurrentHashMap<Integer, ProfileStorage> playerProfiles = new ConcurrentHashMap<>(); private final ProfileUnloader profileUnloader; private static final long TIME_PROFILE_IS_STORED_MILLISECONDS = TimeUnit.SECONDS.toMillis( LobsterCraft.configuration.getLong(ConfigurationValues.PLAYER_TIME_PROFILE_KEPT_SECONDS.toString())), PROFILE_SAVING_PERIOD_TICKS = LobsterCraft.configuration .getLong(ConfigurationValues.PLAYER_TIME_BETWEEN_PROFILE_SAVES_TICKS.toString()); private Connection connection; /* * Player limiter for AsyncPlayerPreLoginEvent * * Number of ticks per player join, this must be the time difference between 2 "joins" * Unit: (players per second / ticks per second)^(-1) => (ticks per player) */ private final static int TICKS_NEEDED_BETWEEN_JOINS = NumberConversions.ceil(LobsterCraft.configuration .getInt(ConfigurationValues.LOGIN_LIMITER_PLAYERS_FOR_PERIOD.toString()) / (LobsterCraft.configuration .getInt(ConfigurationValues.LOGIN_LIMITER_PLAYERS_PERIOD_OF_TIME_SECONDS.toString()) * 20.0D)); private final Object playerJoinedLock = new Object(); private long lastPlayerJoinedTick = -1; // We will lock with playerJoinedLock /* * Player name changes */ private final static long REQUIRED_TIME_TO_ALLOW_NAME = TimeUnit.DAYS.toMillis(LobsterCraft.configuration .getLong(ConfigurationValues.LOGIN_NAME_CHANGE_USERNAME_AVAILABLE_DAYS.toString())), PLAYER_CAN_CHANGE_NAME_AGAIN = TimeUnit.DAYS.toMillis(LobsterCraft.configuration .getLong(ConfigurationValues.LOGIN_NAME_CHANGE_PLAYER_ALLOWED_TO_CHANGE_DAYS.toString())); private final List<String> blacklistedNames = LobsterCraft.configuration .getStringList(ConfigurationValues.PLAYER_NAME_BLACKLIST.toString()); private final ConcurrentHashMap<Integer, NameChangeEntry> nameChangeEntries = new ConcurrentHashMap<>(); /* * Ban entries */ private final ConcurrentHashMap<Integer, HashSet<BannedPlayerEntry>> playerBanEntries = new ConcurrentHashMap<>(); /* * Mute entries: for administrator mutes => this player can't say anything */ private final ConcurrentHashMap<Integer, HashSet<MutePlayerEntry>> playerMuteEntries = new ConcurrentHashMap<>(); public PlayerHandlerService() throws SQLException { // Register service super(); // Create database cache connection = LobsterCraft.dataSource.getConnection(); // connection used on profile unloader cacheOfflinePlayers(connection); cachePlayerNameChanges(connection); cachePlayerBanRecords(connection); cacheModeratorMuteRecords(connection); // Register our listeners Bukkit.getServer().getPluginManager().registerEvents(new CustomEventsListener(), LobsterCraft.plugin); Bukkit.getServer().getPluginManager().registerEvents(new PreLoginListener(), LobsterCraft.plugin); Bukkit.getServer().getPluginManager().registerEvents(new TeleportListener(), LobsterCraft.plugin); Bukkit.getServer().getPluginManager().registerEvents(new SafePlayerActionsListener(), LobsterCraft.plugin); Bukkit.getServer().getPluginManager().registerEvents(new ChatListener(), LobsterCraft.plugin); Bukkit.getServer().getPluginManager().registerEvents(new XrayListener(), LobsterCraft.plugin); // Register profile unloader Bukkit.getServer().getScheduler().runTaskTimerAsynchronously(LobsterCraft.plugin, (profileUnloader = new ProfileUnloader()), PROFILE_SAVING_PERIOD_TICKS, PROFILE_SAVING_PERIOD_TICKS); } @Override public void onDisable() { try { checkConnection(); // Make sure all profiles are saved synchronized (playerProfiles) { while (!playerProfiles.isEmpty()) profileUnloader.run(); } // Save our cache saveChangedPlayers(connection); saveChangedPlayerNames(connection); // Finally, close our connection connection.close(); } catch (SQLException exception) { exception.printStackTrace(); } } /* * Player management */ /** * Get offline player from cache, if registered * Note: this will retrieve an OfflinePlayer even if the player is online * * @param playerName valid player name (doesn't need to be lower cased) * @return offline player from database, if registered; default offline player, otherwise * @throws IllegalArgumentException if player name isn't valid */ public OfflinePlayer getOfflinePlayer(@NotNull final String playerName) throws IllegalArgumentException { OfflinePlayer offlinePlayer; final String loweredPlayerName = playerName.toLowerCase(); synchronized (playerMapsLock) { // Retrieve from registered names if ((offlinePlayer = registeredOfflinePlayers_name.get(loweredPlayerName)) != null) { return offlinePlayer; } else if ((offlinePlayer = unregisteredOfflinePlayers_name.get(loweredPlayerName)) != null) { // unregistered players that might be going to rejoin the // server return offlinePlayer; } else { return new OfflinePlayer(loweredPlayerName); } } } /** * Get offline player from cache using player's id * Note: this will retrieve an OfflinePlayer even if the player is online * * @param playerId <b>EXISTING</b> player id * @return offline player for that id; null, if none found * @see PlayerHandlerService#getOfflinePlayer(String) that method needs locking because it'll check 2 lists simultaneously, while this method will rely on Map's * synchronization (it's thread safe). The player doesn't need to be on the "string"-key Map to get a not-null object returned here. */ public OfflinePlayer getOfflinePlayer(int playerId) { return registeredOfflinePlayers_id.get(playerId); } /** * Retrieve OnlinePlayer for Bukkit's player instance * <p> * OnlinePlayer will be available between PlayerJoinEvent with priority set to LOW and PlayerQuitEvent with priority set to HIGHEST * * @param player Bukkit's player instance * @param onlineState player's online state, null will search for any player * @return online player; null, if none found * @see PlayerHandlerService#getOfflinePlayer(String) that method needs locking because it'll check 2 lists simultaneously, while this method will rely on Map's * synchronization (it's thread safe). The player doesn't need to be on the "string"-key Map to get a not-null object returned here. */ public OnlinePlayer getOnlinePlayer(@NotNull Player player, @Nullable OnlinePlayer.OnlineState onlineState) { OnlinePlayer onlinePlayer = onlinePlayers_player.get(player); if (onlinePlayer == null) return null; else return onlineState == null || onlinePlayer.getOnlineState() == onlineState ? onlinePlayer : null; } /** * Retrieve OnlinePlayer for OfflinePlayer's instance * <p> * OnlinePlayer will be available between PlayerJoinEvent with priority set to LOW and PlayerQuitEvent with priority set to HIGHEST * * @param offlinePlayer OfflinePlayer instance to search * @param onlineState player's online state, null will search for any player * @return online player; null, if none found * @see PlayerHandlerService#getOfflinePlayer(String) that method needs locking because it'll check 2 lists simultaneously, while this method will rely on Map's * synchronization (it's thread safe). The player doesn't need to be on the "string"-key Map to get a not-null object returned here. */ public OnlinePlayer getOnlinePlayer(@NotNull OfflinePlayer offlinePlayer, @Nullable OnlinePlayer.OnlineState onlineState) { OnlinePlayer onlinePlayer = onlinePlayers_offlinePlayer.get(offlinePlayer); if (onlinePlayer == null) return null; else return onlineState == null || onlinePlayer.getOnlineState() == onlineState ? onlinePlayer : null; } /** * Retrieve OnlinePlayer for player's name * <p> * OnlinePlayer will be available between PlayerJoinEvent with priority set to LOW and PlayerQuitEvent with priority set to HIGHEST * * @param playerName player's exact name * @param onlineState player's online state, null will search for any player * @return online player; null, if none found * @see PlayerHandlerService#getOfflinePlayer(String) that method needs locking because it'll check 2 lists simultaneously, while this method will rely on Map's * synchronization (it's thread safe). The player doesn't need to be on the "string"-key Map to get a not-null object returned here. */ public OnlinePlayer getOnlinePlayer(@NotNull String playerName, @Nullable OnlinePlayer.OnlineState onlineState) { OnlinePlayer onlinePlayer = onlinePlayers_offlinePlayer.get(getOfflinePlayer(playerName)); if (onlinePlayer == null) return null; else return onlineState == null || onlinePlayer.getOnlineState() == onlineState ? onlinePlayer : null; } /** * Retrieve OnlinePlayer for player name (will use the most "perfect" name) * <p> * OnlinePlayer will be available between PlayerJoinEvent with priority set to LOW and PlayerQuitEvent with priority set to HIGHEST * * @param string player name to search * @param onlineState player's online state, null will search for any player * @return matched online player; null, if none found */ public OnlinePlayer matchOnlinePlayer(@NotNull String string, @Nullable OnlinePlayer.OnlineState onlineState) { if (string.length() < 3) return null; OnlinePlayer mostEqual = getOnlinePlayer(string, onlineState); int equalSize = 3; // Check if name is exact if (mostEqual != null) return mostEqual; synchronized (playerMapsLock) { for (OnlinePlayer onlinePlayer : onlinePlayers_offlinePlayer.values()) { int thisSize = Util.getEqualityOfNames(string.toCharArray(), onlinePlayer.getOfflinePlayer().getPlayerName().toCharArray()); if (thisSize >= equalSize) { mostEqual = onlinePlayer; equalSize = thisSize; } } } return mostEqual != null && (onlineState == null || mostEqual.getOnlineState() == onlineState) ? mostEqual : null; } /** * Retrieve registered OfflinePlayer by player name (will use the most "perfect" name) * * @param string player name to search * @return matched online player; null, if none found */ public OfflinePlayer matchOfflinePlayer(@NotNull String string) { if (string.length() < 3) return null; OfflinePlayer mostEqual = registeredOfflinePlayers_name.get(string.toLowerCase()); int equalSize = 3; // Check if name is exact if (mostEqual != null) return mostEqual; synchronized (playerMapsLock) { for (OfflinePlayer offlinePlayer : registeredOfflinePlayers_name.values()) { int thisSize = Util.getEqualityOfNames(string.toCharArray(), offlinePlayer.getPlayerName().toCharArray()); if (thisSize >= equalSize) { mostEqual = offlinePlayer; equalSize = thisSize; } } } return mostEqual; } /** * Retrieve a list of online players * * @param onlineState null means no filtering at all * @return a Set from HashSet with all online players filtered by given onlineState */ public Set<OnlinePlayer> getOnlinePlayers(@Nullable OnlinePlayer.OnlineState onlineState) { HashSet<OnlinePlayer> playerSet = new HashSet<>(); // Filter logged players synchronized (playerMapsLock) { for (OnlinePlayer onlinePlayer : onlinePlayers_player.values()) if (onlineState == null || onlinePlayer.getOnlineState() == onlineState) playerSet.add(onlinePlayer); } return playerSet; } /** * Retrieve a list of offline players for given cityId * * @param cityId null means no filtering at all * @return a Set from HashSet with all offline players filtered by given city id */ public Set<OfflinePlayer> getOfflinePlayersPlayersForCity(int cityId) { // Filter logged players synchronized (playerMapsLock) { return registeredOfflinePlayers_cityId.get(cityId); } } /** * Retrieve a list of online players * * @param cityId city to search for * @param onlineState null means no filtering at all * @return a Set from HashSet with all online players filtered by given onlineState and cityId */ public Set<OnlinePlayer> getOnlinePlayersForCity(int cityId, @Nullable OnlinePlayer.OnlineState onlineState) { HashSet<OnlinePlayer> playerSet = new HashSet<>(); // Filter logged players synchronized (playerMapsLock) { for (OfflinePlayer offlinePlayer : registeredOfflinePlayers_cityId.get(cityId)) { OnlinePlayer onlinePlayer; if ((onlinePlayer = offlinePlayer.getOnlinePlayer(onlineState)) != null) playerSet.add(onlinePlayer); } } return playerSet; } /** * Register player * * @param encryptedPassword player's encrypted password * @return a LoginResponse to send the CommandSender ("LOGIN_WENT_ASYNCHRONOUS_SUCCESSFULLY" is a success) * @see Util#encryptString(String) for password encrypting */ public OnlinePlayer.LoginResponse registerPlayer(@NotNull final OnlinePlayer onlinePlayer, @NotNull final String encryptedPassword) { final OfflinePlayer offlinePlayer = onlinePlayer.getOfflinePlayer(); // Check if player is registered if (offlinePlayer.isRegistered()) return OnlinePlayer.LoginResponse.ALREADY_REGISTERED; Bukkit.getScheduler().runTaskAsynchronously(LobsterCraft.plugin, () -> { try { // Set offline player's attributes (lastIp is just set on login) offlinePlayer.lastIp = onlinePlayer.getPlayer().getAddress().getAddress().getHostAddress(); offlinePlayer.encryptedPassword = encryptedPassword; offlinePlayer.databaseState = DatabaseState.INSERT_TO_DATABASE; // Register player on database Connection connection = LobsterCraft.dataSource.getConnection(); // Prepare statement PreparedStatement preparedStatement = connection .prepareStatement("INSERT INTO `minecraft`.`user_profiles`" + "(`playerName`, `password`, `moneyAmount`, `city_cityId`, `cityOccupation`, `lastTimeOnline`, `timePlayed`, `lastIp`)" + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);", Statement.RETURN_GENERATED_KEYS); // Set variables preparedStatement.setString(1, offlinePlayer.getPlayerName().toLowerCase()); // Lower case it just to make sure preparedStatement.setString(2, offlinePlayer.getEncryptedPassword()); preparedStatement.setDouble(3, offlinePlayer.getMoneyAmount()); preparedStatement.setObject(4, offlinePlayer.getCityId(), Types.SMALLINT); // Will write null if is null preparedStatement.setObject(5, offlinePlayer.getCityOccupation() != null ? offlinePlayer.getCityOccupation().getOccupationId() : null, Types.TINYINT); preparedStatement.setLong(6, offlinePlayer.getLastTimeOnline()); preparedStatement.setLong(7, offlinePlayer.getTimePlayed()); preparedStatement.setString(8, offlinePlayer.getLastIp()); // Execute statement preparedStatement.execute(); // Retrieve generated keys ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); // Check if key exists if (!generatedKeys.next()) throw new SQLException("Query didn't return any generated key"); offlinePlayer.playerId = generatedKeys.getInt("playerId"); // Close everything generatedKeys.close(); preparedStatement.close(); connection.close(); // Check if was successful if (offlinePlayer.getPlayerId() == null || offlinePlayer.getPlayerId() <= 0) throw new IllegalStateException(Util.appendStrings("Failed to register player: playerId is ", offlinePlayer.getPlayerId())); // Change player's instance location synchronized (playerMapsLock) { unregisteredOfflinePlayers_name.remove(offlinePlayer.getPlayerName(), offlinePlayer); registeredOfflinePlayers_name.put(offlinePlayer.getPlayerName(), offlinePlayer); registeredOfflinePlayers_id.put(offlinePlayer.getPlayerId(), offlinePlayer); // Check if player has a city (even though he just registered...) if (offlinePlayer.getCityId() != null) { if (!registeredOfflinePlayers_cityId.containsKey(offlinePlayer.getCityId())) registeredOfflinePlayers_cityId.put(offlinePlayer.getCityId(), new HashSet<>()); registeredOfflinePlayers_cityId.get(offlinePlayer.getCityId()).add(offlinePlayer); } } // Update database state offlinePlayer.databaseState = DatabaseState.ON_DATABASE; onlinePlayer.onlineState = OnlinePlayer.OnlineState.PRE_LOGIN; // Force login forceLoginPlayer(onlinePlayer); } catch (Exception exception) { exception.printStackTrace(); onlinePlayer.getPlayer().kickPlayer("4Um erro ocorreu ao registrar!"); } }); return OnlinePlayer.LoginResponse.LOGIN_WENT_ASYNCHRONOUS_SUCCESSFULLY; } /** * Force player to login * * @param onlinePlayer player to force login * @return a response to the possible CommandSender */ public OnlinePlayer.LoginResponse forceLoginPlayer(@NotNull OnlinePlayer onlinePlayer) { return onlinePlayer.loginPlayer(); } /** * Change player's password (the player <b>MUST</b> be registered) * Note: you should check if the player knows old password * * @param offlinePlayer player to get the password changed * @param newEncryptedPassword new encrypted password * @return true if the password was changed * @see Util#encryptString(String) for password encrypting */ public boolean changePlayerPassword(@NotNull final OfflinePlayer offlinePlayer, @NotNull final String newEncryptedPassword) { // Check if player is registered if (!offlinePlayer.isRegistered()) return false; // Update password offlinePlayer.encryptedPassword = newEncryptedPassword; offlinePlayer.databaseState = DatabaseState.UPDATE_DATABASE; return true; } /** * Change player's name (the player <b>MUST</b> be registered) * Note: you should check if player knows the password before * * @param offlinePlayer player that will receive the new name * @param newPlayerName player's new name, will be lower cased * @return result of change */ public ChangeNameResponse changePlayerName(@NotNull final OfflinePlayer offlinePlayer, @NotNull final String newPlayerName) { final String newPlayerNameLowered = newPlayerName.toLowerCase(), oldPlayerName = offlinePlayer.playerName; OnlinePlayer onlinePlayer = offlinePlayer.getOnlinePlayer(null); OfflinePlayer hypotheticalOfflinePlayer; // Check if name is valid if (!Util.checkStringCharactersAndLength(newPlayerNameLowered, 3, 16)) return ChangeNameResponse.NAME_INVALID; synchronized (nameChangeEntries) { for (NameChangeEntry entry : nameChangeEntries.values()) // Check if name is a recent change that isn't offlinePlayer's if (entry.getPlayerId() != offlinePlayer.getPlayerId() && !entry.isNameAvailable() && entry.getOldPlayerName().equalsIgnoreCase(newPlayerNameLowered)) return ChangeNameResponse.NAME_PROTECTED; // Check player name is valid synchronized (playerMapsLock) { // Don't need to catch IllegalArgumentException, name is already checked above hypotheticalOfflinePlayer = getOfflinePlayer(newPlayerNameLowered); // Check if player is registered if (hypotheticalOfflinePlayer.isRegistered()) return ChangeNameResponse.NAME_UNAVAILABLE; // Insert name change record NameChangeEntry oldNameEntry = nameChangeEntries.putIfAbsent(offlinePlayer.getPlayerId(), new NameChangeEntry(offlinePlayer)); // If old existed, the new NameChangeEntry (created above) wasn't inserted and, because of this, we should update the old one if (oldNameEntry != null) // Check if player can change its name again if (oldNameEntry.canPlayerChangeNameAgain()) oldNameEntry.changeNameAgain(oldPlayerName); else return ChangeNameResponse.CANT_CHANGE_NAME_YET; // Remove from registered player's map if (!registeredOfflinePlayers_name.remove(offlinePlayer.getPlayerName(), offlinePlayer)) return ChangeNameResponse.ERROR_OCCURRED; // Store Bukkit's OfflinePlayer for posterior permission transfer org.bukkit.OfflinePlayer oldBukkitOfflinePlayer = offlinePlayer.getBukkitOfflinePlayer(), newBukkitOfflinePlayer = hypotheticalOfflinePlayer.getBukkitOfflinePlayer(); // If there is an online player, kick it before we corrupt any online player map if (onlinePlayer != null) onlinePlayers_offlinePlayer.remove(offlinePlayer, onlinePlayer); // Update player name offlinePlayer.playerName = newPlayerNameLowered; offlinePlayer.databaseState = DatabaseState.UPDATE_DATABASE; // Re-insert even though OnlinePlayer will contain an outdated name if (onlinePlayer != null) onlinePlayers_offlinePlayer.put(offlinePlayer, onlinePlayer); Permission permission = LobsterCraft.permission; String primaryGroup = permission.getPrimaryGroup(null, oldBukkitOfflinePlayer); // Transfer permissions boolean removeGroup = permission.playerRemoveGroup(null, oldBukkitOfflinePlayer, primaryGroup); boolean addGroup = permission.playerAddGroup(null, newBukkitOfflinePlayer, primaryGroup); LobsterCraft.logger .config(Util.appendStrings("Player change name: ", oldPlayerName, " -> ", newPlayerName, "; removed old from group? ", removeGroup, " added new to group? ", addGroup)); // Reinsert player on map registeredOfflinePlayers_name.put(newPlayerNameLowered, offlinePlayer); } } // Kick player if is online if (onlinePlayer != null) onlinePlayer.getPlayer() .kickPlayer(Util.appendStrings("aNome alterado para 6\"", newPlayerName, "\"a!")); return ChangeNameResponse.SUCCESSFULLY_CHANGED; } /** * Retrieves the entire history of bans for player. * * @param playerId player's id * @return an UNMODIFIABLE set of player's records */ public Set<BannedPlayerEntry> getPlayerBanEntries(int playerId) { return Collections.unmodifiableSet(playerBanEntries.getOrDefault(playerId, new HashSet<>())); } /** * Kick or ban player, online or not. This won't announce to the server and will keep a record on MySQL. This method <b>SHOULD</b> run asynchronously. * * @param offlinePlayer player to be kicked * @param banType kick type * @param reason reason to be at record, from 4 to 120 characters * @param moderatorId moderator to be stored, can be null * @param bannedDuration ban duration, can be null * @return a ban response to the CommandSender */ public BanResponse kickPlayer(@NotNull OfflinePlayer offlinePlayer, @NotNull final BanType banType, @NotNull final String reason, @Nullable Integer moderatorId, @Nullable final Long bannedDuration) { // Check if player is registered if (!offlinePlayer.isRegistered()) return BanResponse.PLAYER_NOT_REGISTERED; // Set variables int playerId = offlinePlayer.getPlayerId(); long recordDate = System.currentTimeMillis(); // Check unban date Long unbanDate; if (banType != BanType.PLAYER_TEMPORARILY_BANNED) // Only temporary banned requires this argument unbanDate = null; else if (bannedDuration != null) unbanDate = recordDate + bannedDuration; else return BanResponse.BAN_DURATION_NOT_SET; // Check if reason has right size if (!Util.checkStringLength(reason, 4, 120)) return BanResponse.INVALID_REASON_LENGTH; try { // Retrieve connection Connection connection = LobsterCraft.dataSource.getConnection(); // Prepare statement PreparedStatement preparedStatement = connection.prepareStatement( // 6 arguments "INSERT INTO `minecraft`.`ban_records` (`user_playerId`, `user_moderatorId`, `banType`, `recordDate`, `reason`, `unbanDate`) VALUES (?, ?, ?, ?, ?, ?);", Statement.RETURN_GENERATED_KEYS); // Set variables for query preparedStatement.setInt(1, playerId); preparedStatement.setObject(2, moderatorId, Types.INTEGER); // will write null if is null preparedStatement.setByte(3, banType.getTypeId()); preparedStatement.setLong(4, recordDate); preparedStatement.setString(5, reason); preparedStatement.setObject(6, unbanDate, Types.BIGINT); // Execute statement preparedStatement.execute(); // Retrieve generated keys ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); // Throw error if there is no generated key if (!generatedKeys.next()) throw new SQLException("There is no generated key"); // Create entry BannedPlayerEntry bannedPlayerEntry = new BannedPlayerEntry(generatedKeys.getLong("recordId"), moderatorId, banType, recordDate, reason, unbanDate); // Add entry to storage synchronized (playerBanEntries) { playerBanEntries.putIfAbsent(playerId, new HashSet<>()); playerBanEntries.get(playerId).add(bannedPlayerEntry); } // Close everything generatedKeys.close(); preparedStatement.close(); connection.close(); // Schedule player kick, if he is online OnlinePlayer onlinePlayer = offlinePlayer.getOnlinePlayer(null); if (onlinePlayer != null) Bukkit.getServer().getScheduler().runTask(LobsterCraft.plugin, () -> { if (onlinePlayer.getPlayer().isOnline()) // Kick player if he is online onlinePlayer.getPlayer().kickPlayer(bannedPlayerEntry.getKickMessage()); }); return BanResponse.SUCCESSFULLY_EXECUTED; } catch (SQLException exception) { exception.printStackTrace(); return BanResponse.ERROR_OCCURRED; } } public Set<MutePlayerEntry> getPlayerMutedEntries(int playerId) { return Collections.unmodifiableSet(playerMuteEntries.getOrDefault(playerId, new HashSet<>())); } /** * This method will insert a database entry and <b>SHOULD</b> run asynchronously * * @param offlinePlayer player to be muted * @param moderator the administrator that muted the player, can be null (Console) * @param reason the reason to be muted * @param muteDuration duration to be muted * @return a response to send to the administrator */ public MuteResponse mutePlayer(@NotNull OfflinePlayer offlinePlayer, @Nullable OnlinePlayer moderator, @NotNull final String reason, @NotNull final long muteDuration) { // Check if player is registered if (!offlinePlayer.isRegistered()) return MuteResponse.PLAYER_NOT_REGISTERED; // Set variables int playerId = offlinePlayer.getPlayerId(); long recordDate = System.currentTimeMillis(); long unmuteDate = recordDate + muteDuration; // Check if reason has right size if (!Util.checkStringLength(reason, 4, 128)) return MuteResponse.INVALID_REASON_LENGTH; try { // Retrieve connection Connection connection = LobsterCraft.dataSource.getConnection(); // Prepare statement PreparedStatement preparedStatement = connection.prepareStatement( "INSERT INTO `minecraft`.`mod_muted_players`(`user_mutedId`, `user_moderatorId`, `muteDate`, `unmuteDate`, `reason`) VALUES (?, ?, ?, ?, ?);", Statement.RETURN_GENERATED_KEYS); // Set variables for query preparedStatement.setInt(1, playerId); if (moderator != null) preparedStatement.setInt(2, moderator.getOfflinePlayer().getPlayerId()); // will write null if is null else preparedStatement.setNull(2, Types.INTEGER); preparedStatement.setLong(3, recordDate); preparedStatement.setLong(4, unmuteDate); preparedStatement.setString(5, reason); // Execute statement preparedStatement.execute(); // Retrieve generated keys ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); // Throw error if there is no generated key if (!generatedKeys.next()) throw new SQLException("There is no generated key"); // Create entry MutePlayerEntry mutedPlayerEntry = new MutePlayerEntry(generatedKeys.getLong("mute_index"), moderator != null ? moderator.getOfflinePlayer().getPlayerId() : null, recordDate, unmuteDate, reason); // Add entry to storage synchronized (playerMuteEntries) { playerMuteEntries.putIfAbsent(playerId, new HashSet<>()); playerMuteEntries.get(playerId).add(mutedPlayerEntry); } // Close everything generatedKeys.close(); preparedStatement.close(); connection.close(); // Check if player is online and warn him OnlinePlayer onlinePlayer = offlinePlayer.getOnlinePlayer(null); if (onlinePlayer != null) { StringBuilder stringBuilder = new StringBuilder("cVoc foi silenciado"); if (moderator != null) stringBuilder.append(" por ").append(moderator.getPlayer().getDisplayName()); stringBuilder.append("c at ").append(Util.formatDate(unmuteDate)).append('\n') .append("pela razo: 4\"").append(reason).append('\"'); onlinePlayer.getPlayer().sendMessage(stringBuilder.toString()); } return MuteResponse.SUCCESSFULLY_EXECUTED; } catch (SQLException exception) { exception.printStackTrace(); return MuteResponse.ERROR_OCCURRED; } } /* * Database handling */ /** * This method will cache every registered player. Should run on start, so we don't need to synchronize it. * * @param connection MySQL connection * @throws SQLException in case of something going wrong, should stop the server on start */ private void cacheOfflinePlayers(@NotNull final Connection connection) throws SQLException { long start = System.nanoTime(); // Prepare statement and execute query PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM minecraft.user_profiles;"); ResultSet resultSet = preparedStatement.executeQuery(); // Iterate through all results while (resultSet.next()) { String playerName = resultSet.getString("playerName").toLowerCase(); // Check for the nullity of some variables Integer cityId = resultSet.getInt("city_cityId"); if (resultSet.wasNull()) cityId = null; Byte cityOccupationId = resultSet.getByte("cityOccupation"); if (resultSet.wasNull()) cityOccupationId = null; OfflinePlayer offlinePlayer = new OfflinePlayer(resultSet.getInt("playerId"), playerName, resultSet.getString("password"), resultSet.getDouble("moneyAmount"), cityId, CityOccupation.fromId(cityOccupationId), resultSet.getLong("lastTimeOnline"), resultSet.getLong("timePlayed"), resultSet.getString("lastIp")); registeredOfflinePlayers_name.put(playerName, offlinePlayer); registeredOfflinePlayers_id.put(offlinePlayer.getPlayerId(), offlinePlayer); // Check if player has a city if (offlinePlayer.getCityId() != null) { if (!registeredOfflinePlayers_cityId.containsKey(offlinePlayer.getCityId())) registeredOfflinePlayers_cityId.put(offlinePlayer.getCityId(), new HashSet<>()); registeredOfflinePlayers_cityId.get(offlinePlayer.getCityId()).add(offlinePlayer); } } // Close everything resultSet.close(); preparedStatement.close(); // Announce values LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to retrieve ", registeredOfflinePlayers_id.size(), " players.")); } /** * This method will cache every valid (not old) player name change record. Should run on start, so we don't need to synchronize it. * * @param connection MySQL connection * @throws SQLException in case of something going wrong, should stop the server on start */ private void cachePlayerNameChanges(@NotNull final Connection connection) throws SQLException { long start = System.nanoTime(); // Prepare statement PreparedStatement preparedStatement = connection .prepareStatement("SELECT * FROM minecraft.player_name_changes WHERE changeDate > ?;"); // Set the minimum date for this (we will only consider name changes that actually can deny a player join) preparedStatement.setLong(1, System.currentTimeMillis() - REQUIRED_TIME_TO_ALLOW_NAME); // Execute query ResultSet resultSet = preparedStatement.executeQuery(); // Iterate through all entries while (resultSet.next()) { int playerId = resultSet.getInt("user_playerId"); nameChangeEntries.put(playerId, new NameChangeEntry(playerId, resultSet.getString("oldPlayerName"), resultSet.getLong("changeDate"))); } // Close everything resultSet.close(); preparedStatement.close(); // Announce values LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to retrieve ", registeredOfflinePlayers_id.size(), " player name changes.")); } /** * This method will cache every ban record. Should run on start, so we don't need to synchronize it. * * @param connection MySQL connection * @throws SQLException in case of something going wrong, should stop the server on start */ private void cachePlayerBanRecords(@NotNull final Connection connection) throws SQLException { long start = System.nanoTime(); // Prepare statement PreparedStatement preparedStatement = connection .prepareStatement("SELECT * FROM `minecraft`.`ban_records`;"); // WHERE `banType` = ? OR `unbanDate` > ?;"); // Set variables - do not filter, let's leave the history available for administrator commands //preparedStatement.setByte(1, BanType.PLAYER_PERMANENTLY_BANNED.getTypeId()); // will filter permanent ban //preparedStatement.setLong(2, System.currentTimeMillis()); // will filter unfinished temporary bans // Execute query ResultSet resultSet = preparedStatement.executeQuery(); // Iterate through results while (resultSet.next()) { // Retrieve variables int playerId = resultSet.getInt("user_playerId"); Integer moderatorId = resultSet.getInt("user_moderatorId"); if (resultSet.wasNull()) moderatorId = null; Long unbanDate = resultSet.getLong("unbanDate"); if (resultSet.wasNull()) unbanDate = null; // Insert base set playerBanEntries.putIfAbsent(playerId, new HashSet<>()); // Insert entry playerBanEntries.get(playerId) .add(new BannedPlayerEntry(resultSet.getLong("recordId"), moderatorId, BanType.getBanType(resultSet.getByte("banType")), resultSet.getLong("recordDate"), resultSet.getString("reason"), unbanDate)); } // Close everything resultSet.close(); preparedStatement.close(); // Announce values LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to retrieve ", playerBanEntries.size(), " ban records.")); } /** * This method will cache every mute entry. Should run on start, so we don't need to synchronize it. * * @param connection MySQL connection * @throws SQLException in case of something going wrong, should stop the server on start */ private void cacheModeratorMuteRecords(@NotNull final Connection connection) throws SQLException { long start = System.nanoTime(); // Prepare statement PreparedStatement preparedStatement = connection .prepareStatement("SELECT * FROM minecraft.mod_muted_players;"); // Execute query ResultSet resultSet = preparedStatement.executeQuery(); // Iterate through results while (resultSet.next()) { // Retrieve variables int mutedId = resultSet.getInt("user_mutedId"); Integer moderatorId = resultSet.getInt("user_moderatorId"); if (resultSet.wasNull()) moderatorId = null; // Insert base set playerMuteEntries.putIfAbsent(mutedId, new HashSet<>()); // Insert entry playerMuteEntries.get(mutedId).add(new MutePlayerEntry(resultSet.getLong("mute_index"), moderatorId, resultSet.getLong("muteDate"), resultSet.getLong("unmuteDate"), resultSet.getString("reason"))); } // Close everything resultSet.close(); preparedStatement.close(); // Announce values LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to retrieve ", playerMuteEntries.size(), " mute records.")); } /** * This should run on server close, so we don't need to synchronize as every player join is denied before. * * @param connection MySQL connection * @throws SQLException in case of something going wrong */ private void saveChangedPlayers(@NotNull Connection connection) throws SQLException { long start = System.nanoTime(); int numberOfPlayersUpdated = 0; // Prepare statement PreparedStatement preparedStatement = connection.prepareStatement( "UPDATE `minecraft`.`user_profiles` SET `playerName` = ?, `password` = ?, `moneyAmount` = ?, `city_cityId` = ?," + " `cityOccupation` = ?, `lastTimeOnline` = ?, `timePlayed` = ?, `lastIp` = ? WHERE `playerId` = ?;"); // Iterate through all players for (OfflinePlayer offlinePlayer : registeredOfflinePlayers_id.values()) // Filter the ones needing updates => REGISTERED PLAYERS: they have money amount, passwords, last time online, last IP if (offlinePlayer.getDatabaseState() == DatabaseState.UPDATE_DATABASE) { preparedStatement.setString(1, offlinePlayer.getPlayerName()); preparedStatement.setString(2, offlinePlayer.getEncryptedPassword()); preparedStatement.setDouble(3, offlinePlayer.getMoneyAmount()); preparedStatement.setObject(4, offlinePlayer.getCityId(), Types.INTEGER); // Will write null if is null preparedStatement.setObject(5, offlinePlayer.getCityOccupation() != null ? offlinePlayer.getCityOccupation().getOccupationId() : null, Types.TINYINT); preparedStatement.setLong(6, offlinePlayer.getLastTimeOnline()); preparedStatement.setLong(7, offlinePlayer.getTimePlayed()); preparedStatement.setString(8, offlinePlayer.getLastIp()); preparedStatement.setLong(9, offlinePlayer.getPlayerId()); // Add batch preparedStatement.addBatch(); // Update their database state offlinePlayer.databaseState = DatabaseState.ON_DATABASE; numberOfPlayersUpdated++; } // Execute and announce if (numberOfPlayersUpdated > 0) { preparedStatement.executeBatch(); LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to update ", numberOfPlayersUpdated, " players.")); } // Close statement preparedStatement.close(); } /** * This should run on server close, so we don't need to synchronize as every player join is denied before. * * @param connection MySQL connection * @throws SQLException in case of something going wrong */ private void saveChangedPlayerNames(@NotNull Connection connection) throws SQLException { long start = System.nanoTime(); int numberOfEntriesUpdated = 0, numberOfEntriesInserted = 0; // Prepare statements PreparedStatement updateStatement = connection.prepareStatement( "UPDATE `minecraft`.`player_name_changes` SET `oldPlayerName` = ?, `changeDate` = ? WHERE `user_playerId` = ?;"); PreparedStatement insertStatement = connection.prepareStatement( "INSERT INTO `minecraft`.`player_name_changes` (`user_playerId`, `oldPlayerName`, `changeDate`) VALUES (?, ?, ?);"); // Iterate through all entries for (NameChangeEntry nameChangeEntry : nameChangeEntries.values()) { if (nameChangeEntry.databaseState == DatabaseState.UPDATE_DATABASE) { // Set variables insertStatement.setString(1, nameChangeEntry.getOldPlayerName()); insertStatement.setLong(2, nameChangeEntry.getChangeDate()); insertStatement.setInt(3, nameChangeEntry.getPlayerId()); // Add batch updateStatement.addBatch(); numberOfEntriesUpdated++; } else if (nameChangeEntry.databaseState == DatabaseState.INSERT_TO_DATABASE) { // Set variables insertStatement.setInt(1, nameChangeEntry.getPlayerId()); insertStatement.setString(2, nameChangeEntry.getOldPlayerName()); insertStatement.setLong(3, nameChangeEntry.getChangeDate()); // Add batch insertStatement.addBatch(); numberOfEntriesInserted++; } else { // Lets not change their database state continue; } // Update their database state nameChangeEntry.databaseState = DatabaseState.ON_DATABASE; } // Delete those who wasn't updated PreparedStatement deleteStatement = connection .prepareStatement("DELETE FROM `minecraft`.`player_name_changes` WHERE `changeDate` > ?;"); deleteStatement.setLong(1, System.currentTimeMillis() + REQUIRED_TIME_TO_ALLOW_NAME); deleteStatement.execute(); deleteStatement.close(); // Delete from cache too Iterator<NameChangeEntry> iterator = nameChangeEntries.values().iterator(); while (iterator.hasNext()) { NameChangeEntry next = iterator.next(); if (next.isNameAvailable()) iterator.remove(); } // Execute and announce if needed if (numberOfEntriesUpdated > 0) updateStatement.executeBatch(); if (numberOfEntriesInserted > 0) insertStatement.executeBatch(); if (numberOfEntriesUpdated > 0 || numberOfEntriesInserted > 0) LobsterCraft.logger.info(Util.appendStrings("Took us ", Util.formatDecimal( (double) (System.nanoTime() - start) / (double) TimeUnit.MILLISECONDS.toNanos(1)), "ms to clean old, insert ", numberOfEntriesInserted, " and update ", numberOfEntriesUpdated, " name changes.")); // Close statement updateStatement.close(); insertStatement.close(); } /** * Retrieve profiles from database. This should run asynchronously as it may search on MySQL. * * @param onlinePlayer online player instance to build Profiles * @return a set of profiles, null if any error occurred */ public FutureTask<Set<Profile>> retrieveProfiles(final OnlinePlayer onlinePlayer) { return new FutureTask<>(() -> { synchronized (playerProfiles) { ProfileStorage profileStorage = playerProfiles .remove(onlinePlayer.getOfflinePlayer().getPlayerId()); // Check if profile was queued to be removed if (profileStorage != null) return profileStorage.profiles; } // Prepare for database search HashSet<Profile> profiles = new HashSet<>(); checkConnection(); try { // Get all profiles types for (Profile.ProfileType profileType : Profile.ProfileType.values()) // Note: getConstructor(OnlinePlayer) won't work profiles.add(profileType.getProfileClass().getDeclaredConstructor(OnlinePlayer.class) .newInstance(onlinePlayer).loadProfileFromDatabase(connection)); return profiles; } catch (Exception exception) { exception.printStackTrace(); return null; } }); } private void checkConnection() throws SQLException { boolean connectionInvalid = false; // Create connection if it is dead or something if (connection == null || (connectionInvalid = !connection.isValid(1))) { if (connectionInvalid) connection.close(); connection = LobsterCraft.dataSource.getConnection(); } } /* * Event handling */ /** * Lowest priority event check: will run first * Does NOT ignore cancelled events * This method will check, in this order: * - server is closing => deny login * - server is initializing => deny login * - there's a lot of players joining => deny login * - server is full => deny login if player doesn't have the permission * - player's name has invalid length or characters => deny login * - player's name is on blacklist => deny login * - player's name is already logged in => deny login * - player's name is on recent changed player names list => deny login * - player's (temporary) banned => deny login * <p> * <b>Note:</b> player's name is checked while retrieving OfflinePlayer from PlayerHandlerService (will occur on ban check) but we will check it before anyway * * @param event Spigot/Bukkit given event * @see com.jabyftw.lobstercraft.player.PlayerHandlerService#getOfflinePlayer(String) */ @EventHandler(priority = EventPriority.LOWEST) private void onPlayerAsyncPreLogin(AsyncPlayerPreLoginEvent event) { String playerName = event.getName().toLowerCase(); // Check if server is closing if (LobsterCraft.serverClosing) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "4Servidor fechando...\n6Tente novamente em alguns minutos."); return; } // Check if login is early if (LobsterCraft.tickCounter.getTick() < 10L) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "4Servidor iniciando...\n6Tente novamente em alguns segundos."); return; } // Check if there's a lot of players joining synchronized (playerJoinedLock) { if (lastPlayerJoinedTick > 0 && // a player must already have joined the server (LobsterCraft.tickCounter.getTick() - lastPlayerJoinedTick) < TICKS_NEEDED_BETWEEN_JOINS) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_FULL, "4Muitas pessoas entrando simultaneamente...\n6Tente novamente em alguns segundos."); return; } } // Check if server is full { Server server = Bukkit.getServer(); @SuppressWarnings("deprecation") org.bukkit.OfflinePlayer offlinePlayer = server.getOfflinePlayer(playerName); // There will always be a player for this method // If is full and ((player isn't op) or (player don't have permission)) if (server.getOnlinePlayers().size() >= server.getMaxPlayers() && // Note: 'null' worlds are supported for permissions system that allows global permissions (!offlinePlayer.isOp() || !LobsterCraft.permission.playerHas(null, offlinePlayer, Permissions.JOIN_FULL_SERVER.toString()))) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_FULL, "4Servidor lotado!\ncAguarde alguns segundos e tente novamente."); return; } } // Check if player's name is valid if (!Util.checkStringCharactersAndLength(playerName, 3, 16)) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, "4Nome invlido!\ncEle contm caracteres invlidos ou\nc muito longo/curto"); return; } // After name check done above, OfflinePlayer will always exist // Check if player's name is on blacklist for (String blacklistedName : blacklistedNames) { if (playerName.equalsIgnoreCase(blacklistedName) || playerName.contains(blacklistedName)) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, "4Nome invlido!\ncEntre no servidor com um nome diferente."); return; } } // Check if player is already online //noinspection deprecation OnlinePlayer onlinePlayer = getOnlinePlayer(playerName, null); if (onlinePlayer != null) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "4Jogador j est online."); return; } // Check if player's name is a "recent changed name" synchronized (nameChangeEntries) { for (NameChangeEntry entry : nameChangeEntries.values()) if (!entry.isNameAvailable() && entry.getOldPlayerName().equalsIgnoreCase(playerName)) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, Util.appendStrings( "4Nome indisponvel!\n6O nome ser liberado para todos os jogadores em\n6", Util.formatDate(entry.getChangeDate() + REQUIRED_TIME_TO_ALLOW_NAME))); return; } } // Check if player is (temporary) banned { OfflinePlayer offlinePlayer = getOfflinePlayer(playerName); // Just check if player is registered if (offlinePlayer.isRegistered()) synchronized (playerBanEntries) { Set<BannedPlayerEntry> entries = getPlayerBanEntries(offlinePlayer.getPlayerId()); // Check if player has a record if (entries != null) // Iterate checking for active record for (BannedPlayerEntry bannedPlayerEntry : entries) if (bannedPlayerEntry.isBanned()) { event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, bannedPlayerEntry.getKickMessage()); return; } } } // Call event for services to prepare for player join AsyncPlayerPreJoinEvent asyncPlayerPreJoinEvent = new AsyncPlayerPreJoinEvent(); LobsterCraft.plugin.getServer().getPluginManager().callEvent(asyncPlayerPreJoinEvent); // Check if event was cancelled by any service if (asyncPlayerPreJoinEvent.isCancelled()) event.disallow(asyncPlayerPreJoinEvent.getResult(), asyncPlayerPreJoinEvent.getKickMessage()); } /** * Lowest priority event check: will run first * Does NOT ignore cancelled events * <p> * This event will call PlayerBecameOnlineEvent after assigning a Player to an OfflinePlayer, creating a OnlinePlayer (its OnlineState will be defined by * OfflinePlayer.isRegistered() * <p> * The OnlinePlayer will be created on this event, all services might initialize its variables for the player through an PlayerBecameOnlineEvent * <p> * Note: even if the event (PlayerJoinEvent) is cancelled, the OnlinePlayer will be created * * @param event Spigot/Bukkit given event * @see PlayerBecameOnlineEvent * @see OnlinePlayer#getOnlineState() * @see com.jabyftw.lobstercraft.player.OfflinePlayer#isRegistered() */ @EventHandler(priority = EventPriority.LOWEST) private void onPlayerJoinLowest(PlayerJoinEvent event) { // Remove join message, because it'll announced when logged in event.setJoinMessage(""); // This will create OnlinePlayer instance Player bukkitPlayer = event.getPlayer(); OfflinePlayer offlinePlayer; OnlinePlayer onlinePlayer; // Insert everything (synchronized because every map should be inserted on the same time) synchronized (playerMapsLock) { offlinePlayer = getOfflinePlayer(bukkitPlayer.getName()); onlinePlayer = new OnlinePlayer(offlinePlayer, bukkitPlayer); onlinePlayers_offlinePlayer.put(offlinePlayer, onlinePlayer); onlinePlayers_player.put(bukkitPlayer, onlinePlayer); // Insert unregistered offline player if (!offlinePlayer.isRegistered()) unregisteredOfflinePlayers_name.put(offlinePlayer.getPlayerName(), offlinePlayer); } // Call event that will initialize OnlinePlayer's attributes on each service LobsterCraft.plugin.getServer().getPluginManager().callEvent(new PlayerBecameOnlineEvent(onlinePlayer)); // Set last joined tick if player has successfully joined synchronized (playerJoinedLock) { lastPlayerJoinedTick = LobsterCraft.tickCounter.getTick(); } } /** * Highest check * This event will ignore cancelled * <p> * This event will remove leave message from invisible kicked players * * @param event Bukkit given event */ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) private void onPlayerKickHighest(PlayerKickEvent event) { // Set quit message, none if player is invisible event.setLeaveMessage(LobsterCraft.vanishManager.isVanished(event.getPlayer()) ? "" : "4- c" + event.getPlayer().getName()); } /** * Highest check * This event isn't cancellable * <p> * This event will remove quit message from invisible players * * @param event Bukkit given event */ @EventHandler(priority = EventPriority.HIGHEST) private void onPlayerQuitHighest(PlayerQuitEvent event) { // Set quit message, none if player is invisible event.setQuitMessage(LobsterCraft.vanishManager.isVanished(event.getPlayer()) ? "" : "4- c" + event.getPlayer().getName()); } /** * Monitor check * This event isn't cancellable * <p> * This event will call PlayerLoggedOutEvent and later destroy OnlinePlayer * * @param event Bukkit given event */ @EventHandler(priority = EventPriority.MONITOR) private void onPlayerQuitMonitor(PlayerQuitEvent event) { Player bukkitPlayer = event.getPlayer(); OnlinePlayer onlinePlayer = getOnlinePlayer(bukkitPlayer, null); // Call event for all services to destroy OnlinePlayer LobsterCraft.plugin.getServer().getPluginManager().callEvent(new PlayerLoggedOutEvent(onlinePlayer)); // Log off player before removing its profiles onlinePlayer.logOff(); // Remove and store player's profiles if (onlinePlayer.getOfflinePlayer().isRegistered()) { HashSet<Profile> profileSet = new HashSet<>(); synchronized (onlinePlayer.profiles) { Iterator<Profile> iterator = onlinePlayer.profiles.values().iterator(); // Iterate through all profiles while (iterator.hasNext()) { Profile profile = iterator.next(); // Delete OnlinePlayer from profile profile.applyProfile(null); profileSet.add(profile); iterator.remove(); } } // Store profiles synchronized (playerProfiles) { playerProfiles.put(onlinePlayer.getOfflinePlayer().getPlayerId(), new ProfileStorage(profileSet)); } } boolean removeOfflineKey, removeBukkitKey; // Remove everything synchronized (playerMapsLock) { removeOfflineKey = onlinePlayers_offlinePlayer.remove(getOfflinePlayer(bukkitPlayer.getName()), onlinePlayer); removeBukkitKey = onlinePlayers_player.remove(bukkitPlayer, onlinePlayer); } // Check if everything was successfully removed if (!removeBukkitKey || !removeOfflineKey) { StringBuilder stringBuilder = new StringBuilder("Player ").append(bukkitPlayer.getName()) .append(" wasn't removed properly from "); if (!removeBukkitKey) { stringBuilder.append("Bukkit player's map"); if (!removeOfflineKey) stringBuilder.append(" and from "); } if (!removeOfflineKey) stringBuilder.append("OfflinePlayer's map"); LobsterCraft.logger.warning(stringBuilder.toString()); } } // /** // * This will listen for PlayerJoinsCityEvent. We must check anything if needed // * // * @param event our event // * @see com.jabyftw.lobstercraft.world.CityStructure#joinCity(OfflinePlayer) where this event is called and some other stuff is checked // */ // @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) // private void onPlayerJoinCityHigh(PlayerJoinsCityEvent event) { // // TODO maybe we are forgetting some checks? // } /** * This will listen for PlayerJoinsCityEvent. We must set cityId and occupation for the player. * * @param event our event * @see com.jabyftw.lobstercraft.world.CityStructure#joinCity(OfflinePlayer) where this event is called and some other stuff is checked */ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onPlayerJoinCityMonitor(PlayerJoinsCityEvent event) { // Set variables event.getOfflinePlayer().cityId = event.getCityStructure().getCityId(); event.getOfflinePlayer().cityOccupation = CityOccupation.CITIZEN; event.getOfflinePlayer().databaseState = DatabaseState.UPDATE_DATABASE; synchronized (playerMapsLock) { // Insert player to map if (!registeredOfflinePlayers_cityId.containsKey(event.getCityStructure().getCityId())) registeredOfflinePlayers_cityId.put(event.getCityStructure().getCityId(), new HashSet<>()); registeredOfflinePlayers_cityId.get(event.getCityStructure().getCityId()).add(event.getOfflinePlayer()); } } // /** // * This will listen for PlayerChangesCityOccupationEvent. We must check anything if needed // * // * @param event our event // * @see com.jabyftw.lobstercraft.world.CityStructure#joinCity(OfflinePlayer) where this event is called and some other stuff is checked // */ // @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) // private void onPlayerChangeCityOccupationHigh(PlayerChangesCityOccupationEvent event) { // // TODO maybe we are forgetting some checks? // } /** * This will listen for PlayerChangesCityOccupationEvent. We must set city occupation for the player. * * @param event our event * @see com.jabyftw.lobstercraft.world.CityStructure#joinCity(OfflinePlayer) where this event is called and some other stuff is checked. */ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onPlayerChangeCityOccupationMonitor(PlayerChangesCityOccupationEvent event) { // Set variables event.getOfflinePlayer().cityOccupation = event.getCityOccupation(); event.getOfflinePlayer().databaseState = DatabaseState.UPDATE_DATABASE; } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) private void onPlayerChangesBuildingMode(PlayerChangesBuildingModeEvent event) { event.getOnlinePlayer().buildingMode = event.getBuildingMode(); if (event.shouldWarnPlayer()) event.getOnlinePlayer().getPlayer() .sendMessage(Util.appendStrings("6Voc est construindo no tipo de proteo de c", event.getBuildingMode().getBlockProtectionType().getDisplayName())); } /* * Some classes */ private class ProfileUnloader implements Runnable { @Override public void run() { synchronized (playerProfiles) { Iterator<ProfileStorage> iterator = playerProfiles.values().iterator(); try { checkConnection(); } catch (SQLException exception) { exception.printStackTrace(); LobsterCraft.logger.severe("Couldn't restore connection for profile saving!"); } // Iterate through all storage while (iterator.hasNext()) { ProfileStorage storage = iterator.next(); // Check if we should remove it if (storage.shouldBeRemoved() || LobsterCraft.serverClosing) { for (Profile profile : storage.profiles) // This will filter profile saving if (profile.getDatabaseState().shouldSave() && !profile.saveToDatabase(connection)) LobsterCraft.logger.warning(Util.appendStrings("Couldn't save profile ", profile.profileType.name(), " for playerId=", profile.playerId)); // Remove from queue, it was saved iterator.remove(); } } } } } private class ProfileStorage { protected final HashSet<Profile> profiles = new HashSet<>(); protected final long timeWhenStored = System.currentTimeMillis(); protected ProfileStorage(@NotNull final Collection<Profile> profileCollection) { this.profiles.addAll(profileCollection); } protected boolean shouldBeRemoved() { return System.currentTimeMillis() - timeWhenStored > TIME_PROFILE_IS_STORED_MILLISECONDS; } } private class NameChangeEntry { // Database information private final int playerId; private String oldPlayerName; private long changeDate; // Class variable DatabaseState databaseState = DatabaseState.NOT_ON_DATABASE; /** * This should be created BEFORE the name is changed * * @param offlinePlayer player that is changing name */ private NameChangeEntry(@NotNull final OfflinePlayer offlinePlayer) { this.playerId = offlinePlayer.getPlayerId(); this.oldPlayerName = offlinePlayer.getPlayerName(); this.changeDate = System.currentTimeMillis(); this.databaseState = DatabaseState.INSERT_TO_DATABASE; } private NameChangeEntry(int playerId, @NotNull final String oldPlayerName, long changeDate) { this.playerId = playerId; this.oldPlayerName = oldPlayerName; this.changeDate = changeDate; this.databaseState = DatabaseState.ON_DATABASE; } /** * Change player name of the entry, this will block the old player name for logging in players * This method <b>WON'T</b> check for any condition. * This method <b>WON'T</b> save NameChangeEntry on database either. * * @param oldPlayerName before-change player name * @return this instance to be saved */ private NameChangeEntry changeNameAgain(@NotNull final String oldPlayerName) { this.oldPlayerName = oldPlayerName; this.changeDate = System.currentTimeMillis(); this.databaseState = DatabaseState.UPDATE_DATABASE; return this; } boolean isNameAvailable() { return (System.currentTimeMillis() - changeDate) > REQUIRED_TIME_TO_ALLOW_NAME; } boolean canPlayerChangeNameAgain() { return (System.currentTimeMillis() - changeDate) > PLAYER_CAN_CHANGE_NAME_AGAIN; } int getPlayerId() { return playerId; } long getChangeDate() { return changeDate; } String getOldPlayerName() { return oldPlayerName; } @Override public boolean equals(Object obj) { return obj != null && ((obj instanceof OfflinePlayer && ((OfflinePlayer) obj).getPlayerId() == playerId) || (obj instanceof NameChangeEntry && ((NameChangeEntry) obj).playerId == playerId)); } @Override public int hashCode() { return new HashCodeBuilder(3, 9).append(playerId).toHashCode(); } } public class BannedPlayerEntry { private final long recordId; private final Integer moderatorId; private final BanType banType; private final long recordDate; private final String reason; private final Long unbanDate; private BannedPlayerEntry(long recordId, @Nullable Integer moderatorId, @NotNull BanType banType, long recordDate, @NotNull String reason, @Nullable Long unbanDate) { this.recordId = recordId; this.moderatorId = moderatorId; this.banType = banType; this.recordDate = recordDate; this.reason = reason; this.unbanDate = unbanDate; if (unbanDate == null && banType == BanType.PLAYER_TEMPORARILY_BANNED) throw new IllegalArgumentException( "BanType can't be PLAYER_TEMPORARILY_BANNED if unbanDate is not set!"); } /** * @return true if moderator is a player, false if moderator is the console */ boolean isModeratorAPlayer() { return moderatorId != null; } boolean isBanned() { return (unbanDate == null && banType == BanType.PLAYER_PERMANENTLY_BANNED) || (banType == BanType.PLAYER_TEMPORARILY_BANNED && unbanDate != null && unbanDate > System.currentTimeMillis()); } /** * @return the string that should appear to the player */ String getKickMessage() { return banType.getBaseKickMessage() .replaceAll("%moderator%", isModeratorAPlayer() ? getOfflinePlayer(moderatorId).getPlayerName() : "Console") .replaceAll("%reason%", reason) // Check Util#dateFormat .replaceAll("%unbanDate%", unbanDate != null ? Util.formatDate(unbanDate) : "30/02/2194 25:66") .replaceAll("%recordDate%", Util.formatDate(recordDate)); } /* * Getters */ public Integer getModeratorId() { return moderatorId; } public BanType getBanType() { return banType; } public long getRecordDate() { return recordDate; } public String getReason() { return reason; } public Long getUnbanDate() { return unbanDate; } @Override public boolean equals(Object obj) { return obj instanceof BannedPlayerEntry && ((BannedPlayerEntry) obj).recordId == recordId; } @Override public int hashCode() { return new HashCodeBuilder(5, 63).append(recordId).toHashCode(); } } public enum BanType { PLAYER_KICKED((byte) 1, "expulso", "6Voc foi expulso por c%moderator%\n" + "6Motivo: c\"%reason%\""), PLAYER_PERMANENTLY_BANNED( (byte) 2, "perm.", "6Voc foi banido por c%moderator%\n" + "6Motivo: c\"%reason%\"\n" + "6Banido permanentemente em c%recordDate%"), PLAYER_TEMPORARILY_BANNED( (byte) 3, "temp.", "6Voc foi exilado por c%moderator%\n" + "6Motivo: c\"%reason%\"\n" + "6Exilado c%recordDate% 6at c%unbanDate%"); private final byte typeId; private final String kickMessage, typeName; BanType(byte typeId, @NotNull final String typeName, @NotNull final String kickMessage) { this.typeId = typeId; this.kickMessage = kickMessage; this.typeName = typeName; } public byte getTypeId() { return typeId; } /** * This is used for history command * * @return a portuguese name for the entry */ public String getTypeName() { return typeName; } /** * Get the kick message to be customized:<br> * <b>%moderator%</b>: should be moderator's name, <i>"Console"</i> if null<br> * <b>%reason%</b>: should be the reason of kick * <b>%unbanDate%</b>: should be the date the player will be allowed to rejoin * <b>%recordDate%</b>: should be the date the player was kicked * * @return a string that should be shown to player screen * @see Util#formatDate(long) to use on <b>%unbanDate%</b> * @see BannedPlayerEntry#getKickMessage() */ public String getBaseKickMessage() { return kickMessage; } public static BanType getBanType(byte typeId) { for (BanType banType : values()) if (banType.getTypeId() == typeId) return banType; return null; } } public class MutePlayerEntry { private final long mute_index; private final Integer moderatorId; private final long recordDate, unmuteDate; private final String reason; private MutePlayerEntry(long mute_index, @Nullable Integer moderatorId, long recordDate, long unmuteDate, @NotNull String reason) { this.mute_index = mute_index; this.moderatorId = moderatorId; this.recordDate = recordDate; this.unmuteDate = unmuteDate; this.reason = reason; } public long getRecordDate() { return recordDate; } public long getUnmuteDate() { return unmuteDate; } public String getReason() { return reason; } public boolean isMuted() { return System.currentTimeMillis() <= unmuteDate; } public boolean hasModerator() { return this.moderatorId != null; } /** * @return null if there is no moderator */ public OfflinePlayer getModerator() { return hasModerator() ? getOfflinePlayer(moderatorId) : null; } } public enum ChangeNameResponse { SUCCESSFULLY_CHANGED, NAME_UNAVAILABLE, NAME_PROTECTED, NAME_INVALID, CANT_CHANGE_NAME_YET, INCORRECT_PASSWORD, ERROR_OCCURRED } public enum BanResponse { SUCCESSFULLY_EXECUTED, PLAYER_NOT_REGISTERED, BAN_DURATION_NOT_SET, INVALID_REASON_LENGTH, ERROR_OCCURRED } public enum MuteResponse { SUCCESSFULLY_EXECUTED, PLAYER_NOT_REGISTERED, INVALID_REASON_LENGTH, ERROR_OCCURRED } }