diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 045d1ee974..12d062affb 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -24,6 +24,7 @@ import fr.xephi.authme.service.BackupService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.MigrationService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.service.bungeecord.BungeeReceiver; import fr.xephi.authme.service.yaml.YamlParseException; import fr.xephi.authme.settings.Settings; @@ -69,6 +70,7 @@ public class AuthMe extends JavaPlugin { private Injector injector; private BackupService backupService; private ConsoleLogger logger; + private SpectateLoginService spectateLoginService; /** * Constructor. @@ -246,6 +248,7 @@ void instantiateServices(Injector injector) { bukkitService = injector.getSingleton(BukkitService.class); commandHandler = injector.getSingleton(CommandHandler.class); backupService = injector.getSingleton(BackupService.class); + spectateLoginService = injector.getSingleton(SpectateLoginService.class); // Trigger instantiation (class not used elsewhere) injector.getSingleton(BungeeReceiver.class); @@ -316,6 +319,9 @@ public void onDisable() { // Wait for tasks and close data source new TaskCloser(this, database).run(); + // Remove all armorstands so that the players do not get Spectator gamemode + spectateLoginService.removeArmorstands(); + // Disabled correctly Consumer infoLogMethod = logger == null ? getLogger()::info : logger::info; infoLogMethod.accept("AuthMe " + this.getDescription().getVersion() + " disabled!"); diff --git a/src/main/java/fr/xephi/authme/listener/PlayerListener.java b/src/main/java/fr/xephi/authme/listener/PlayerListener.java index 727f45a76e..6421bac641 100644 --- a/src/main/java/fr/xephi/authme/listener/PlayerListener.java +++ b/src/main/java/fr/xephi/authme/listener/PlayerListener.java @@ -11,6 +11,7 @@ import fr.xephi.authme.service.AntiBotService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.JoinMessageService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.settings.Settings; @@ -19,6 +20,7 @@ import fr.xephi.authme.settings.properties.RegistrationSettings; import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.ChatColor; +import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; @@ -49,6 +51,8 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerShearEntityEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; import org.bukkit.inventory.InventoryView; import javax.inject.Inject; @@ -90,6 +94,8 @@ public class PlayerListener implements Listener { private PermissionsManager permissionsManager; @Inject private QuickCommandsProtectionManager quickCommandsProtectionManager; + @Inject + private SpectateLoginService spectateLoginService; // Lowest priority to apply fast protection checks @EventHandler(priority = EventPriority.LOWEST) @@ -375,6 +381,36 @@ public void onPlayerRespawn(PlayerRespawnEvent event) { if (spawn != null && spawn.getWorld() != null) { event.setRespawnLocation(spawn); } + + if (settings.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN) + || spectateLoginService.hasStand(event.getPlayer())) { + // If a player is dead, no stand will be created for him + // therefore we can be sure that there will not be two stands + bukkitService.runTaskLater(() -> { + spectateLoginService.createStand(event.getPlayer()); + }, 1L); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onToggleSneak(PlayerToggleSneakEvent event) { + if (listenerService.shouldCancelEvent(event.getPlayer()) + && (settings.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN) + || spectateLoginService.hasStand(event.getPlayer()))) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onTeleport(PlayerTeleportEvent event) { + if (listenerService.shouldCancelEvent(event.getPlayer()) + && event.getCause() == PlayerTeleportEvent.TeleportCause.SPECTATE + && event.getPlayer().getGameMode() == GameMode.SPECTATOR + && (settings.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN) + || spectateLoginService.hasStand(event.getPlayer()))) { + spectateLoginService.updateTarget(event.getPlayer()); + event.setCancelled(true); + } } /* @@ -508,4 +544,5 @@ public void onPlayerInventoryClick(InventoryClickEvent event) { event.setCancelled(true); } } + } diff --git a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java index cacfdbd6c6..65a7ab8bb2 100644 --- a/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java +++ b/src/main/java/fr/xephi/authme/process/join/AsynchronousJoin.java @@ -14,6 +14,7 @@ import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.PluginHookService; import fr.xephi.authme.service.SessionService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.service.ValidationService; import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.service.bungeecord.MessageType; @@ -39,7 +40,7 @@ * Asynchronous process for when a player joins. */ public class AsynchronousJoin implements AsynchronousProcess { - + private final ConsoleLogger logger = ConsoleLoggerFactory.get(AsynchronousJoin.class); @Inject @@ -81,6 +82,9 @@ public class AsynchronousJoin implements AsynchronousProcess { @Inject private ProxySessionManager proxySessionManager; + @Inject + private SpectateLoginService spectateLoginService; + AsynchronousJoin() { } @@ -173,7 +177,7 @@ private void handlePlayerWithUnmetNameRestriction(Player player, String ip) { * Performs various operations in sync mode for an unauthenticated player (such as blindness effect and * limbo player creation). * - * @param player the player to process + * @param player the player to process * @param isAuthAvailable true if the player is registered, false otherwise */ private void processJoinSync(Player player, boolean isAuthAvailable) { @@ -191,6 +195,13 @@ private void processJoinSync(Player player, boolean isAuthAvailable) { int blindTimeOut = (registrationTimeout <= 0) ? 99999 : registrationTimeout; player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, blindTimeOut, 2)); } + + if (service.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN)) { + // The delay is necessary in order to make sure that the player is teleported to spawn + // and after authorization appears in the same place + bukkitService.runTaskLater(() -> spectateLoginService.createStand(player), 1L); + } + commandManager.runCommandsOnJoin(player); }); } @@ -201,7 +212,6 @@ private void processJoinSync(Player player, boolean isAuthAvailable) { * * @param player the player to verify * @param ip the ip address of the player - * * @return true if the verification is OK (no infraction), false if player has been kicked */ private boolean validatePlayerCountForIp(final Player player, String ip) { diff --git a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java index 8501e85cbd..25a4abb7da 100644 --- a/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java +++ b/src/main/java/fr/xephi/authme/process/login/ProcessSyncPlayerLogin.java @@ -12,11 +12,13 @@ import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; import fr.xephi.authme.service.JoinMessageService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.service.bungeecord.BungeeSender; import fr.xephi.authme.settings.WelcomeMessageConfiguration; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.RegistrationSettings; +import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffectType; @@ -57,6 +59,9 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess { @Inject private PermissionsManager permissionsManager; + @Inject + private SpectateLoginService spectateLoginService; + ProcessSyncPlayerLogin() { } @@ -98,6 +103,11 @@ public void processPlayerLogin(Player player, boolean isFirstLogin, List player.removePotionEffect(PotionEffectType.BLINDNESS); } + if (commonService.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN) + || spectateLoginService.hasStand(player)) { + spectateLoginService.removeStand(player); + } + // The Login event now fires (as intended) after everything is processed bukkitService.callEvent(new LoginEvent(player)); diff --git a/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java b/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java index 46fd7fefc0..1331975e40 100644 --- a/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java +++ b/src/main/java/fr/xephi/authme/process/logout/ProcessSyncPlayerLogout.java @@ -9,6 +9,7 @@ import fr.xephi.authme.process.SynchronousProcess; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.service.TeleportationService; import fr.xephi.authme.settings.commandconfig.CommandManager; import fr.xephi.authme.settings.properties.RegistrationSettings; @@ -44,6 +45,9 @@ public class ProcessSyncPlayerLogout implements SynchronousProcess { @Inject private CommandManager commandManager; + @Inject + private SpectateLoginService spectateLoginService; + ProcessSyncPlayerLogout() { } @@ -65,6 +69,10 @@ public void processSyncLogout(Player player) { service.send(player, MessageKey.LOGOUT_SUCCESS); logger.info(player.getName() + " logged out"); + + if (service.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN)) { + spectateLoginService.createStand(player); + } } private void applyLogoutEffect(Player player) { diff --git a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java index fad8b74734..81c7669333 100644 --- a/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java +++ b/src/main/java/fr/xephi/authme/process/quit/ProcessSyncPlayerQuit.java @@ -2,7 +2,10 @@ import fr.xephi.authme.data.limbo.LimboService; import fr.xephi.authme.process.SynchronousProcess; +import fr.xephi.authme.service.CommonService; +import fr.xephi.authme.service.SpectateLoginService; import fr.xephi.authme.settings.commandconfig.CommandManager; +import fr.xephi.authme.settings.properties.RestrictionSettings; import org.bukkit.entity.Player; import javax.inject.Inject; @@ -10,12 +13,18 @@ public class ProcessSyncPlayerQuit implements SynchronousProcess { + @Inject + private CommonService service; + @Inject private LimboService limboService; @Inject private CommandManager commandManager; + @Inject + private SpectateLoginService spectateLoginService; + /** * Processes a player having quit. * @@ -26,6 +35,11 @@ public void processSyncQuit(Player player, boolean wasLoggedIn) { if (wasLoggedIn) { commandManager.runCommandsOnLogout(player); } else { + if (service.getProperty(RestrictionSettings.SPECTATE_STAND_LOGIN) + || spectateLoginService.hasStand(player)) { + spectateLoginService.removeStand(player); + } + limboService.restoreData(player); player.saveData(); // #1238: Speed is sometimes not restored properly } diff --git a/src/main/java/fr/xephi/authme/service/SpectateLoginService.java b/src/main/java/fr/xephi/authme/service/SpectateLoginService.java new file mode 100644 index 0000000000..bc1044ef41 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/SpectateLoginService.java @@ -0,0 +1,104 @@ +package fr.xephi.authme.service; + +import fr.xephi.authme.settings.properties.RestrictionSettings; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.Map; + +/** + * Sets the player gamemode to Spectator, puts the player in an invisible armorstand and fixes the direction of the view + */ +public class SpectateLoginService { + + private Map armorStands = new HashMap<>(); + private Map gameModeMap = new HashMap<>(); + + @Inject + private CommonService service; + + /** + * Creates a stand for the player + * + * @param player the player + */ + public void createStand(Player player) { + if (player.isDead()) { + return; + } + Location location = player.getLocation(); + ArmorStand stand = spawnStand(location); + + armorStands.put(player, stand); + gameModeMap.put(player, player.getGameMode()); + + player.setGameMode(GameMode.SPECTATOR); + player.setSpectatorTarget(stand); + } + + /** + * Updates spectator target for the player + * + * @param player the player + */ + public void updateTarget(Player player) { + ArmorStand stand = armorStands.get(player); + if (stand != null) { + player.setSpectatorTarget(stand); + } + } + + /** + * Removes the player's stand and deletes effects + * + * @param player the player + */ + public void removeStand(Player player) { + ArmorStand stand = armorStands.get(player); + if (stand != null) { + + stand.remove(); + player.setSpectatorTarget(null); + player.setGameMode(gameModeMap.get(player)); + + gameModeMap.remove(player); + armorStands.remove(player); + } + } + + /** + * Removes all armorstands and restores player gamemode + */ + public void removeArmorstands() { + for (Player player : armorStands.keySet()) { + removeStand(player); + } + + gameModeMap.clear(); + armorStands.clear(); + } + + public boolean hasStand(Player player) { + return armorStands.containsKey(player); + } + + private ArmorStand spawnStand(Location loc) { + double pitch = service.getProperty(RestrictionSettings.HEAD_PITCH); + double yaw = service.getProperty(RestrictionSettings.HEAD_YAW); + Location location = new Location(loc.getWorld(), loc.getX(), loc.getY(), loc.getBlockZ(), + (float) yaw, (float) pitch); + + ArmorStand stand = location.getWorld().spawn(location, ArmorStand.class); + + stand.setGravity(false); + stand.setAI(false); + stand.setInvisible(true); + + return stand; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java index 8794ba4473..a21b7e5f06 100644 --- a/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java +++ b/src/main/java/fr/xephi/authme/settings/properties/RestrictionSettings.java @@ -193,6 +193,22 @@ public final class RestrictionSettings implements SettingsHolder { public static final Property> UNRESTRICTED_INVENTORIES = newLowercaseStringSetProperty("settings.unrestrictions.UnrestrictedInventories"); + @Comment({ + "While in unregistered/logged out state, should players be set to spectator mode and", + "forced to spectate within a spawn point invisible armor stand, for 0 movement and head", + "pitch + yaw? may be more effective than 'allowMovement' at locking the player in place." + }) + public static final Property SPECTATE_STAND_LOGIN = + newProperty("settings.restrictions.spectateStandLogin.enabled", false); + + @Comment("Head Yaw position for 'spectateStandLogin'.") + public static final Property HEAD_YAW = + newProperty("settings.restrictions.spectateStandLogin.headYaw", 0.0f); + + @Comment("Head Pitch position for 'spectateStandLogin'.") + public static final Property HEAD_PITCH = + newProperty("settings.restrictions.spectateStandLogin.headPitch", 0.0f); + private RestrictionSettings() { }