From 401490df58f45bd6b4c5965d96a7e00460027cae Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Wed, 26 Apr 2023 13:12:14 +0200 Subject: [PATCH] Greatly improves handling of un-exited sessions #25 --- .../net/knarcraft/minigames/MiniGames.java | 8 +- .../arena/AbstractArenaPlayerRegistry.java | 51 ++++++++- .../arena/AbstractPlayerEntryState.java | 108 ++++++++++++++++-- .../minigames/arena/ArenaPlayerRegistry.java | 8 ++ .../minigames/arena/PlayerEntryState.java | 20 +++- .../dropper/DropperArenaPlayerRegistry.java | 6 + .../dropper/DropperPlayerEntryState.java | 92 +++++++++++++-- .../parkour/ParkourArenaPlayerRegistry.java | 6 + .../parkour/ParkourPlayerEntryState.java | 61 +++++++++- .../listener/PlayerLeaveListener.java | 87 -------------- .../listener/PlayerStateChangeListener.java | 91 +++++++++++++++ .../minigames/util/ArenaStorageHelper.java | 52 ++++++++- 12 files changed, 473 insertions(+), 117 deletions(-) delete mode 100644 src/main/java/net/knarcraft/minigames/listener/PlayerLeaveListener.java create mode 100644 src/main/java/net/knarcraft/minigames/listener/PlayerStateChangeListener.java diff --git a/src/main/java/net/knarcraft/minigames/MiniGames.java b/src/main/java/net/knarcraft/minigames/MiniGames.java index cf7e3f6..c6ca215 100644 --- a/src/main/java/net/knarcraft/minigames/MiniGames.java +++ b/src/main/java/net/knarcraft/minigames/MiniGames.java @@ -9,6 +9,7 @@ import net.knarcraft.minigames.arena.dropper.DropperArenaGroup; import net.knarcraft.minigames.arena.dropper.DropperArenaHandler; import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry; import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry; +import net.knarcraft.minigames.arena.dropper.DropperPlayerEntryState; import net.knarcraft.minigames.arena.parkour.ParkourArena; import net.knarcraft.minigames.arena.parkour.ParkourArenaData; import net.knarcraft.minigames.arena.parkour.ParkourArenaGameMode; @@ -16,6 +17,7 @@ import net.knarcraft.minigames.arena.parkour.ParkourArenaGroup; import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler; import net.knarcraft.minigames.arena.parkour.ParkourArenaPlayerRegistry; import net.knarcraft.minigames.arena.parkour.ParkourArenaRecordsRegistry; +import net.knarcraft.minigames.arena.parkour.ParkourPlayerEntryState; import net.knarcraft.minigames.arena.record.IntegerRecord; import net.knarcraft.minigames.arena.record.LongRecord; import net.knarcraft.minigames.command.LeaveArenaCommand; @@ -50,7 +52,7 @@ import net.knarcraft.minigames.container.SerializableUUID; import net.knarcraft.minigames.listener.CommandListener; import net.knarcraft.minigames.listener.DamageListener; import net.knarcraft.minigames.listener.MoveListener; -import net.knarcraft.minigames.listener.PlayerLeaveListener; +import net.knarcraft.minigames.listener.PlayerStateChangeListener; import net.knarcraft.minigames.placeholder.DropperRecordExpansion; import net.knarcraft.minigames.placeholder.ParkourRecordExpansion; import org.bukkit.Bukkit; @@ -217,6 +219,8 @@ public final class MiniGames extends JavaPlugin { ConfigurationSerialization.registerClass(ParkourArenaData.class); ConfigurationSerialization.registerClass(ParkourArenaGroup.class); ConfigurationSerialization.registerClass(ParkourArenaGameMode.class); + ConfigurationSerialization.registerClass(DropperPlayerEntryState.class); + ConfigurationSerialization.registerClass(ParkourPlayerEntryState.class); } @Override @@ -240,7 +244,7 @@ public final class MiniGames extends JavaPlugin { PluginManager pluginManager = getServer().getPluginManager(); pluginManager.registerEvents(new DamageListener(), this); pluginManager.registerEvents(new MoveListener(this.dropperConfiguration, this.parkourConfiguration), this); - pluginManager.registerEvents(new PlayerLeaveListener(), this); + pluginManager.registerEvents(new PlayerStateChangeListener(), this); pluginManager.registerEvents(new CommandListener(), this); registerCommand("miniGamesReload", new ReloadCommand(), null); diff --git a/src/main/java/net/knarcraft/minigames/arena/AbstractArenaPlayerRegistry.java b/src/main/java/net/knarcraft/minigames/arena/AbstractArenaPlayerRegistry.java index 3ebbce7..0844f12 100644 --- a/src/main/java/net/knarcraft/minigames/arena/AbstractArenaPlayerRegistry.java +++ b/src/main/java/net/knarcraft/minigames/arena/AbstractArenaPlayerRegistry.java @@ -1,11 +1,16 @@ package net.knarcraft.minigames.arena; +import net.knarcraft.minigames.MiniGames; +import net.knarcraft.minigames.util.ArenaStorageHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.logging.Level; /** * A player registry to keep track of currently playing players @@ -17,18 +22,29 @@ public abstract class AbstractArenaPlayerRegistry implements Ar private final Map arenaPlayers = new HashMap<>(); private final Map entryStates = new HashMap<>(); - //TODO: Save all entry states each time the map changes - //TODO: If a player joins, and their entry state exists, restore the state + /** + * Instantiates a new arena player registry + */ + public AbstractArenaPlayerRegistry() { + loadEntryStates(); + } + + @Override + public @Nullable PlayerEntryState getEntryState(@NotNull UUID playerId) { + return this.entryStates.get(playerId); + } @Override public void registerPlayer(@NotNull UUID playerId, @NotNull ArenaSession arenaSession) { this.arenaPlayers.put(playerId, arenaSession); this.entryStates.put(playerId, arenaSession.getEntryState()); + this.saveEntryStates(); } @Override public boolean removePlayer(@NotNull UUID playerId) { this.entryStates.remove(playerId); + this.saveEntryStates(); return this.arenaPlayers.remove(playerId) != null; } @@ -44,9 +60,38 @@ public abstract class AbstractArenaPlayerRegistry implements Ar // Kick the player gracefully entry.getValue().triggerQuit(immediately); this.arenaPlayers.remove(entry.getKey()); - this.entryStates.remove(entry.getKey()); } } } + /** + * Gets a string key unique to this type of player registry + * + * @return

A unique key used for entry state storage

+ */ + protected abstract String getEntryStateStorageKey(); + + /** + * Saves all entry states to disk + */ + private void saveEntryStates() { + ArenaStorageHelper.storeArenaPlayerEntryStates(getEntryStateStorageKey(), new HashSet<>(entryStates.values())); + } + + /** + * Loads all entry states from disk + */ + private void loadEntryStates() { + this.entryStates.clear(); + Set entryStates = ArenaStorageHelper.getArenaPlayerEntryStates(getEntryStateStorageKey()); + for (PlayerEntryState entryState : entryStates) { + this.entryStates.put(entryState.getPlayerId(), entryState); + } + if (this.entryStates.size() > 0) { + MiniGames.log(Level.WARNING, entryStates.size() + " un-exited sessions found. This happens if " + + "players are leaving in the middle of a game, or the server crashes. MiniGames will do its best " + + "to fix the players' states."); + } + } + } diff --git a/src/main/java/net/knarcraft/minigames/arena/AbstractPlayerEntryState.java b/src/main/java/net/knarcraft/minigames/arena/AbstractPlayerEntryState.java index c5cdf04..4c29cce 100644 --- a/src/main/java/net/knarcraft/minigames/arena/AbstractPlayerEntryState.java +++ b/src/main/java/net/knarcraft/minigames/arena/AbstractPlayerEntryState.java @@ -1,5 +1,8 @@ package net.knarcraft.minigames.arena; +import net.knarcraft.minigames.MiniGames; +import net.knarcraft.minigames.container.SerializableUUID; +import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -7,12 +10,17 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.jetbrains.annotations.NotNull; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; + /** * An abstract representation of a player's entry state */ public abstract class AbstractPlayerEntryState implements PlayerEntryState { - protected final Player player; + protected final UUID playerId; private final boolean makePlayerInvisible; private final Location entryLocation; private final boolean originalIsFlying; @@ -29,7 +37,7 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState { * @param makePlayerInvisible

Whether players should be made invisible while in the arena

*/ public AbstractPlayerEntryState(@NotNull Player player, boolean makePlayerInvisible) { - this.player = player; + this.playerId = player.getUniqueId(); this.makePlayerInvisible = makePlayerInvisible; this.entryLocation = player.getLocation().clone(); this.originalIsFlying = player.isFlying(); @@ -40,24 +48,74 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState { this.originalCollideAble = player.isCollidable(); } + /** + * Instantiates a new abstract player entry state + * + * @param playerId

The id of the player whose state this should keep track of

+ * @param makePlayerInvisible

Whether players should be made invisible while in the arena

+ * @param entryLocation

The location the player entered from

+ * @param originalIsFlying

Whether the player was flying before entering the arena

+ * @param originalGameMode

The game-mode of the player before entering the arena

+ * @param originalAllowFlight

Whether the player was allowed flight before entering the arena

+ * @param originalInvulnerable

Whether the player was invulnerable before entering the arena

+ * @param originalIsSwimming

Whether the player was swimming before entering the arena

+ * @param originalCollideAble

Whether the player was collide-able before entering the arena

+ */ + public AbstractPlayerEntryState(@NotNull UUID playerId, boolean makePlayerInvisible, Location entryLocation, + boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight, + boolean originalInvulnerable, boolean originalIsSwimming, + boolean originalCollideAble) { + this.playerId = playerId; + this.makePlayerInvisible = makePlayerInvisible; + this.entryLocation = entryLocation; + this.originalIsFlying = originalIsFlying; + this.originalGameMode = originalGameMode; + this.originalAllowFlight = originalAllowFlight; + this.originalInvulnerable = originalInvulnerable; + this.originalIsSwimming = originalIsSwimming; + this.originalCollideAble = originalCollideAble; + } + + @Override + public @NotNull UUID getPlayerId() { + return this.playerId; + } + @Override public void setArenaState() { + Player player = getPlayer(); + if (player == null) { + return; + } if (this.makePlayerInvisible) { - this.player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, + player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY, PotionEffect.INFINITE_DURATION, 3)); } } @Override public void restore() { - this.player.setFlying(this.originalIsFlying); - this.player.setGameMode(this.originalGameMode); - this.player.setAllowFlight(this.originalAllowFlight); - this.player.setInvulnerable(this.originalInvulnerable); - this.player.setSwimming(this.originalIsSwimming); - this.player.setCollidable(this.originalCollideAble); + Player player = getPlayer(); + if (player == null) { + return; + } + restore(player); + } + + /** + * Restores the state of the given player + * + * @param player

The player to restore the state for

+ */ + public void restore(Player player) { + player.setFlying(this.originalIsFlying); + player.setGameMode(this.originalGameMode); + player.setAllowFlight(this.originalAllowFlight); + player.setInvulnerable(this.originalInvulnerable); + player.setSwimming(this.originalIsSwimming); + player.setCollidable(this.originalCollideAble); if (this.makePlayerInvisible) { - this.player.removePotionEffect(PotionEffectType.INVISIBILITY); + player.removePotionEffect(PotionEffectType.INVISIBILITY); } } @@ -66,4 +124,34 @@ public abstract class AbstractPlayerEntryState implements PlayerEntryState { return this.entryLocation; } + /** + * Gets the player this entry state belongs to + * + * @return

The player, or null if not currently online

+ */ + protected Player getPlayer() { + Player player = Bukkit.getOfflinePlayer(this.playerId).getPlayer(); + if (player == null) { + MiniGames.log(Level.WARNING, "Unable to change state for player with id " + this.playerId + + " because the player was not found on the server."); + } + return player; + } + + @NotNull + @Override + public Map serialize() { + Map data = new HashMap<>(); + data.put("playerId", new SerializableUUID(this.playerId)); + data.put("makePlayerInvisible", this.makePlayerInvisible); + data.put("entryLocation", this.entryLocation); + data.put("originalIsFlying", this.originalIsFlying); + data.put("originalGameMode", this.originalGameMode.name()); + data.put("originalAllowFlight", this.originalAllowFlight); + data.put("originalInvulnerable", this.originalInvulnerable); + data.put("originalIsSwimming", this.originalIsSwimming); + data.put("originalCollideAble", this.originalCollideAble); + return data; + } + } diff --git a/src/main/java/net/knarcraft/minigames/arena/ArenaPlayerRegistry.java b/src/main/java/net/knarcraft/minigames/arena/ArenaPlayerRegistry.java index c3ff259..5588433 100644 --- a/src/main/java/net/knarcraft/minigames/arena/ArenaPlayerRegistry.java +++ b/src/main/java/net/knarcraft/minigames/arena/ArenaPlayerRegistry.java @@ -12,6 +12,14 @@ import java.util.UUID; */ public interface ArenaPlayerRegistry { + /** + * Gets the current entry state for the given player + * + * @param playerId

The id of the player to get an entry state for

+ * @return

The entry state of the player, or null if not found

+ */ + @Nullable PlayerEntryState getEntryState(@NotNull UUID playerId); + /** * Registers that the given player has started playing the given dropper arena session * diff --git a/src/main/java/net/knarcraft/minigames/arena/PlayerEntryState.java b/src/main/java/net/knarcraft/minigames/arena/PlayerEntryState.java index cfbc91f..16f8edf 100644 --- a/src/main/java/net/knarcraft/minigames/arena/PlayerEntryState.java +++ b/src/main/java/net/knarcraft/minigames/arena/PlayerEntryState.java @@ -1,11 +1,15 @@ package net.knarcraft.minigames.arena; import org.bukkit.Location; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.entity.Player; + +import java.util.UUID; /** * The stored state of a player */ -public interface PlayerEntryState { +public interface PlayerEntryState extends ConfigurationSerializable { /** * Sets the state of the stored player to the state used by the arena @@ -17,6 +21,20 @@ public interface PlayerEntryState { */ void restore(); + /** + * Restores the stored state for the given player + * + * @param player

A player object that's refers to the same player as the stored player

+ */ + void restore(Player player); + + /** + * Gets the id of the player this state belongs to + * + * @return

The player the state belongs to

+ */ + UUID getPlayerId(); + /** * Gets the location the player entered from * diff --git a/src/main/java/net/knarcraft/minigames/arena/dropper/DropperArenaPlayerRegistry.java b/src/main/java/net/knarcraft/minigames/arena/dropper/DropperArenaPlayerRegistry.java index e3cd768..8e82940 100644 --- a/src/main/java/net/knarcraft/minigames/arena/dropper/DropperArenaPlayerRegistry.java +++ b/src/main/java/net/knarcraft/minigames/arena/dropper/DropperArenaPlayerRegistry.java @@ -6,4 +6,10 @@ import net.knarcraft.minigames.arena.AbstractArenaPlayerRegistry; * A registry to keep track of which players are playing in which arenas */ public class DropperArenaPlayerRegistry extends AbstractArenaPlayerRegistry { + + @Override + protected String getEntryStateStorageKey() { + return "dropper"; + } + } diff --git a/src/main/java/net/knarcraft/minigames/arena/dropper/DropperPlayerEntryState.java b/src/main/java/net/knarcraft/minigames/arena/dropper/DropperPlayerEntryState.java index c72fedb..9385fdd 100644 --- a/src/main/java/net/knarcraft/minigames/arena/dropper/DropperPlayerEntryState.java +++ b/src/main/java/net/knarcraft/minigames/arena/dropper/DropperPlayerEntryState.java @@ -1,10 +1,15 @@ package net.knarcraft.minigames.arena.dropper; import net.knarcraft.minigames.arena.AbstractPlayerEntryState; +import net.knarcraft.minigames.container.SerializableUUID; import org.bukkit.GameMode; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import java.util.Map; +import java.util.UUID; + /** * The state of a player before entering a dropper arena */ @@ -29,29 +34,100 @@ public class DropperPlayerEntryState extends AbstractPlayerEntryState { this.horizontalVelocity = horizontalVelocity; } + /** + * Instantiates a new parkour player entry state + * + * @param playerId

The id of the player whose state this should keep track of

+ * @param makePlayerInvisible

Whether players should be made invisible while in the arena

+ * @param entryLocation

The location the player entered from

+ * @param originalIsFlying

Whether the player was flying before entering the arena

+ * @param originalGameMode

The game-mode of the player before entering the arena

+ * @param originalAllowFlight

Whether the player was allowed flight before entering the arena

+ * @param originalInvulnerable

Whether the player was invulnerable before entering the arena

+ * @param originalIsSwimming

Whether the player was swimming before entering the arena

+ * @param originalCollideAble

Whether the player was collide-able before entering the arena

+ */ + public DropperPlayerEntryState(@NotNull UUID playerId, boolean makePlayerInvisible, Location entryLocation, + boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight, + boolean originalInvulnerable, boolean originalIsSwimming, + boolean originalCollideAble, float originalFlySpeed, boolean disableHitCollision, + float horizontalVelocity, DropperArenaGameMode arenaGameMode) { + super(playerId, makePlayerInvisible, entryLocation, originalIsFlying, originalGameMode, originalAllowFlight, + originalInvulnerable, originalIsSwimming, originalCollideAble); + this.originalFlySpeed = originalFlySpeed; + this.disableHitCollision = disableHitCollision; + this.horizontalVelocity = horizontalVelocity; + this.arenaGameMode = arenaGameMode; + } + @Override public void setArenaState() { super.setArenaState(); - this.player.setAllowFlight(true); - this.player.setFlying(true); - this.player.setGameMode(GameMode.ADVENTURE); - this.player.setSwimming(false); + Player player = getPlayer(); + if (player == null) { + return; + } + player.setAllowFlight(true); + player.setFlying(true); + player.setGameMode(GameMode.ADVENTURE); + player.setSwimming(false); if (this.disableHitCollision) { - this.player.setCollidable(false); + player.setCollidable(false); } // If playing on the inverted game-mode, negate the horizontal velocity to swap the controls if (this.arenaGameMode == DropperArenaGameMode.INVERTED) { - this.player.setFlySpeed(-this.horizontalVelocity); + player.setFlySpeed(-this.horizontalVelocity); } else { - this.player.setFlySpeed(this.horizontalVelocity); + player.setFlySpeed(this.horizontalVelocity); } } @Override public void restore() { super.restore(); - this.player.setFlySpeed(this.originalFlySpeed); + Player player = getPlayer(); + if (player == null) { + return; + } + player.setFlySpeed(this.originalFlySpeed); + } + + @NotNull + @Override + public Map serialize() { + Map data = super.serialize(); + data.put("originalFlySpeed", this.originalFlySpeed); + data.put("disableHitCollision", this.disableHitCollision); + data.put("horizontalVelocity", this.horizontalVelocity); + data.put("arenaGameMode", this.arenaGameMode); + return data; + } + + /** + * Deserializes a ParkourPlayerEntryState from the given data + * + * @return

The data to deserialize

+ */ + @SuppressWarnings("unused") + public static DropperPlayerEntryState deserialize(Map data) { + UUID playerId = ((SerializableUUID) data.get("playerId")).getRawValue(); + boolean makePlayerInvisible = (boolean) data.get("makePlayerInvisible"); + Location entryLocation = (Location) data.get("entryLocation"); + boolean originalIsFlying = (boolean) data.get("originalIsFlying"); + GameMode originalGameMode = GameMode.valueOf((String) data.get("originalGameMode")); + boolean originalAllowFlight = (boolean) data.get("originalAllowFlight"); + boolean originalInvulnerable = (boolean) data.get("originalInvulnerable"); + boolean originalIsSwimming = (boolean) data.get("originalIsSwimming"); + boolean originalCollideAble = (boolean) data.get("originalCollideAble"); + float originalFlySpeed = ((Number) data.get("originalFlySpeed")).floatValue(); + boolean disableHitCollision = (boolean) data.get("disableHitCollision"); + float horizontalVelocity = ((Number) data.get("horizontalVelocity")).floatValue(); + DropperArenaGameMode arenaGameMode = (DropperArenaGameMode) data.get("arenaGameMode"); + + return new DropperPlayerEntryState(playerId, makePlayerInvisible, entryLocation, originalIsFlying, + originalGameMode, originalAllowFlight, originalInvulnerable, originalIsSwimming, originalCollideAble, + originalFlySpeed, disableHitCollision, horizontalVelocity, arenaGameMode); } } diff --git a/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourArenaPlayerRegistry.java b/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourArenaPlayerRegistry.java index 5f86632..f863e18 100644 --- a/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourArenaPlayerRegistry.java +++ b/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourArenaPlayerRegistry.java @@ -6,4 +6,10 @@ import net.knarcraft.minigames.arena.AbstractArenaPlayerRegistry; * A registry to keep track of which players are playing in which arenas */ public class ParkourArenaPlayerRegistry extends AbstractArenaPlayerRegistry { + + @Override + protected String getEntryStateStorageKey() { + return "parkour"; + } + } diff --git a/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourPlayerEntryState.java b/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourPlayerEntryState.java index 74d95f2..10e229b 100644 --- a/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourPlayerEntryState.java +++ b/src/main/java/net/knarcraft/minigames/arena/parkour/ParkourPlayerEntryState.java @@ -1,10 +1,15 @@ package net.knarcraft.minigames.arena.parkour; import net.knarcraft.minigames.arena.AbstractPlayerEntryState; +import net.knarcraft.minigames.container.SerializableUUID; import org.bukkit.GameMode; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import java.util.Map; +import java.util.UUID; + /** * The state of a player before entering a parkour arena */ @@ -19,14 +24,60 @@ public class ParkourPlayerEntryState extends AbstractPlayerEntryState { super(player, makePlayerInvisible); } + /** + * Instantiates a new parkour player entry state + * + * @param playerId

The id of the player whose state this should keep track of

+ * @param makePlayerInvisible

Whether players should be made invisible while in the arena

+ * @param entryLocation

The location the player entered from

+ * @param originalIsFlying

Whether the player was flying before entering the arena

+ * @param originalGameMode

The game-mode of the player before entering the arena

+ * @param originalAllowFlight

Whether the player was allowed flight before entering the arena

+ * @param originalInvulnerable

Whether the player was invulnerable before entering the arena

+ * @param originalIsSwimming

Whether the player was swimming before entering the arena

+ * @param originalCollideAble

Whether the player was collide-able before entering the arena

+ */ + public ParkourPlayerEntryState(@NotNull UUID playerId, boolean makePlayerInvisible, Location entryLocation, + boolean originalIsFlying, GameMode originalGameMode, boolean originalAllowFlight, + boolean originalInvulnerable, boolean originalIsSwimming, + boolean originalCollideAble) { + super(playerId, makePlayerInvisible, entryLocation, originalIsFlying, originalGameMode, originalAllowFlight, + originalInvulnerable, originalIsSwimming, originalCollideAble); + } + @Override public void setArenaState() { super.setArenaState(); - this.player.setAllowFlight(false); - this.player.setFlying(false); - this.player.setGameMode(GameMode.ADVENTURE); - this.player.setSwimming(false); - this.player.setCollidable(false); + Player player = getPlayer(); + if (player == null) { + return; + } + player.setAllowFlight(false); + player.setFlying(false); + player.setGameMode(GameMode.ADVENTURE); + player.setSwimming(false); + player.setCollidable(false); + } + + /** + * Deserializes a ParkourPlayerEntryState from the given data + * + * @return

The data to deserialize

+ */ + @SuppressWarnings("unused") + public static ParkourPlayerEntryState deserialize(Map data) { + UUID playerId = ((SerializableUUID) data.get("playerId")).getRawValue(); + boolean makePlayerInvisible = (boolean) data.get("makePlayerInvisible"); + Location entryLocation = (Location) data.get("entryLocation"); + boolean originalIsFlying = (boolean) data.get("originalIsFlying"); + GameMode originalGameMode = GameMode.valueOf((String) data.get("originalGameMode")); + boolean originalAllowFlight = (boolean) data.get("originalAllowFlight"); + boolean originalInvulnerable = (boolean) data.get("originalInvulnerable"); + boolean originalIsSwimming = (boolean) data.get("originalIsSwimming"); + boolean originalCollideAble = (boolean) data.get("originalCollideAble"); + + return new ParkourPlayerEntryState(playerId, makePlayerInvisible, entryLocation, originalIsFlying, + originalGameMode, originalAllowFlight, originalInvulnerable, originalIsSwimming, originalCollideAble); } } diff --git a/src/main/java/net/knarcraft/minigames/listener/PlayerLeaveListener.java b/src/main/java/net/knarcraft/minigames/listener/PlayerLeaveListener.java deleted file mode 100644 index 0bfaa5c..0000000 --- a/src/main/java/net/knarcraft/minigames/listener/PlayerLeaveListener.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.knarcraft.minigames.listener; - -import net.knarcraft.minigames.MiniGames; -import net.knarcraft.minigames.arena.ArenaSession; -import net.knarcraft.minigames.arena.parkour.ParkourArenaSession; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.player.PlayerTeleportEvent; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.logging.Level; - -/** - * A listener for players leaving the server or the arena - */ -public class PlayerLeaveListener implements Listener { - - private final Map leftSessions = new HashMap<>(); - - @EventHandler - public void onPlayerLeave(PlayerQuitEvent event) { - Player player = event.getPlayer(); - - ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId()); - if (arenaSession == null) { - return; - } - - MiniGames.log(Level.WARNING, "Found player " + player.getUniqueId() + - " leaving in the middle of a session!"); - leftSessions.put(player.getUniqueId(), arenaSession); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - UUID playerId = event.getPlayer().getUniqueId(); - // Force the player to quit from the session once they re-join - if (leftSessions.containsKey(playerId)) { - MiniGames.log(Level.WARNING, "Found un-exited dropper session!"); - Bukkit.getScheduler().runTaskLater(MiniGames.getInstance(), () -> { - leftSessions.get(playerId).triggerQuit(false); - MiniGames.log(Level.WARNING, "Triggered a quit!"); - leftSessions.remove(playerId); - }, 80); - } - } - - /** - * Prevent the player from teleporting away from an arena for any reason - * - * @param event

The triggered teleport event

- */ - @EventHandler(ignoreCancelled = true) - public void onPlayerTeleport(PlayerTeleportEvent event) { - Location targetLocation = event.getTo(); - if (targetLocation == null) { - return; - } - - // Ignore if not in an arena session - ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId()); - if (arenaSession == null) { - return; - } - - // If teleported to the arena's spawn, it's fine - if (targetLocation.equals(arenaSession.getArena().getSpawnLocation())) { - return; - } - - // If teleported to the arena's checkpoint, it's fine - if (arenaSession instanceof ParkourArenaSession parkourArenaSession && - targetLocation.equals(parkourArenaSession.getRegisteredCheckpoint())) { - return; - } - - event.setCancelled(true); - } - -} diff --git a/src/main/java/net/knarcraft/minigames/listener/PlayerStateChangeListener.java b/src/main/java/net/knarcraft/minigames/listener/PlayerStateChangeListener.java new file mode 100644 index 0000000..14987b0 --- /dev/null +++ b/src/main/java/net/knarcraft/minigames/listener/PlayerStateChangeListener.java @@ -0,0 +1,91 @@ +package net.knarcraft.minigames.listener; + +import net.knarcraft.minigames.MiniGames; +import net.knarcraft.minigames.arena.ArenaPlayerRegistry; +import net.knarcraft.minigames.arena.ArenaSession; +import net.knarcraft.minigames.arena.PlayerEntryState; +import net.knarcraft.minigames.arena.parkour.ParkourArenaSession; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.spigotmc.event.player.PlayerSpawnLocationEvent; + +import java.util.logging.Level; + +/** + * A listener for players leaving/joining the server, or leaving the server unexpectedly + */ +public class PlayerStateChangeListener implements Listener { + + @EventHandler + public void onPlayerSpawn(PlayerSpawnLocationEvent event) { + Player player = event.getPlayer(); + + // Restore any lingering arena states + Location restoreLocation; + restoreLocation = restoreStateIfNecessary(player, MiniGames.getInstance().getDropperArenaPlayerRegistry()); + if (restoreLocation != null) { + event.setSpawnLocation(restoreLocation); + } + restoreLocation = restoreStateIfNecessary(player, MiniGames.getInstance().getParkourArenaPlayerRegistry()); + if (restoreLocation != null) { + event.setSpawnLocation(restoreLocation); + } + } + + /** + * Prevent the player from teleporting away from an arena for any reason + * + * @param event

The triggered teleport event

+ */ + @EventHandler(ignoreCancelled = true) + public void onPlayerTeleport(PlayerTeleportEvent event) { + Location targetLocation = event.getTo(); + if (targetLocation == null) { + return; + } + + // Ignore if not in an arena session + ArenaSession arenaSession = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId()); + if (arenaSession == null) { + return; + } + + // If teleported to the arena's spawn, it's fine + if (targetLocation.equals(arenaSession.getArena().getSpawnLocation())) { + return; + } + + // If teleported to the arena's checkpoint, it's fine + if (arenaSession instanceof ParkourArenaSession parkourArenaSession && + targetLocation.equals(parkourArenaSession.getRegisteredCheckpoint())) { + return; + } + + event.setCancelled(true); + } + + /** + * Restores the state of the given player if a lingering session is found in the given player registry + * + * @param player

The player whose state should be checked

+ * @param playerRegistry

The registry to check for a lingering state

+ * @return

The location the player should spawn in, or null if not restored

+ */ + private Location restoreStateIfNecessary(Player player, ArenaPlayerRegistry playerRegistry) { + PlayerEntryState entryState = playerRegistry.getEntryState(player.getUniqueId()); + if (entryState != null) { + MiniGames.log(Level.INFO, "Found existing state for joining player " + player + + ". Attempting to restore the player's state."); + playerRegistry.removePlayer(player.getUniqueId()); + + entryState.restore(player); + return entryState.getEntryLocation(); + } else { + return null; + } + } + +} diff --git a/src/main/java/net/knarcraft/minigames/util/ArenaStorageHelper.java b/src/main/java/net/knarcraft/minigames/util/ArenaStorageHelper.java index c64ad1d..9abfd2d 100644 --- a/src/main/java/net/knarcraft/minigames/util/ArenaStorageHelper.java +++ b/src/main/java/net/knarcraft/minigames/util/ArenaStorageHelper.java @@ -1,18 +1,68 @@ package net.knarcraft.minigames.util; import net.knarcraft.minigames.MiniGames; +import net.knarcraft.minigames.arena.PlayerEntryState; +import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.logging.Level; +/** + * A helper class for dealing with arena storage + */ public final class ArenaStorageHelper { private ArenaStorageHelper() { } + /** + * Stores the given entry states to disk + * + * @param key

The key specifying the correct entry state file

+ * @param entryStates

The entry states to save

+ */ + public static void storeArenaPlayerEntryStates(String key, Set entryStates) { + YamlConfiguration configuration = new YamlConfiguration(); + configuration.set(key, new ArrayList<>(entryStates)); + + try { + configuration.save(new File(MiniGames.getInstance().getDataFolder(), key + "EntryStates.yml")); + } catch (IOException e) { + MiniGames.log(Level.SEVERE, "Unable to save entry states to disk"); + } + } + + /** + * Gets saved entry states from disk + * + * @param key

The key specifying the correct entry state file

+ * @return

The previously saved entry states

+ */ + public static Set getArenaPlayerEntryStates(String key) { + File entryStateFile = new File(MiniGames.getInstance().getDataFolder(), key + "EntryStates.yml"); + if (!entryStateFile.exists()) { + return new HashSet<>(); + } + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(entryStateFile); + Set output = new HashSet<>(); + + List entries = configuration.getList(key, new ArrayList<>()); + for (Object entry : entries) { + if (entry instanceof PlayerEntryState entryState) { + output.add(entryState); + } + } + return output; + } + /** * Gets the file used to store the given arena id's data * @@ -20,7 +70,7 @@ public final class ArenaStorageHelper { * @param arenaId

The id of the arena to get a data file for

* @return

The file the arena's data is/should be stored in

*/ - static @NotNull File getArenaDataFile(File root, @NotNull UUID arenaId) { + public static @NotNull File getArenaDataFile(File root, @NotNull UUID arenaId) { File arenaDataFile = new File(root, arenaId + ".yml"); if (!root.exists() && !root.mkdirs()) { MiniGames.log(Level.SEVERE, "Unable to create the arena data directories");