Greatly improves handling of un-exited sessions #25

This commit is contained in:
Kristian Knarvik 2023-04-26 13:12:14 +02:00
parent b2fbaf0e68
commit 401490df58
12 changed files with 473 additions and 117 deletions

View File

@ -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);

View File

@ -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<K extends Arena> implements Ar
private final Map<UUID, ArenaSession> arenaPlayers = new HashMap<>();
private final Map<UUID, PlayerEntryState> 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<K extends Arena> 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 <p>A unique key used for entry state storage</p>
*/
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<PlayerEntryState> 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.");
}
}
}

View File

@ -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 <p>Whether players should be made invisible while in the arena</p>
*/
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 <p>The id of the player whose state this should keep track of</p>
* @param makePlayerInvisible <p>Whether players should be made invisible while in the arena</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
*/
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 <p>The player to restore the state for</p>
*/
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 <p>The player, or null if not currently online</p>
*/
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<String, Object> serialize() {
Map<String, Object> 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;
}
}

View File

@ -12,6 +12,14 @@ import java.util.UUID;
*/
public interface ArenaPlayerRegistry<K extends Arena> {
/**
* Gets the current entry state for the given player
*
* @param playerId <p>The id of the player to get an entry state for</p>
* @return <p>The entry state of the player, or null if not found</p>
*/
@Nullable PlayerEntryState getEntryState(@NotNull UUID playerId);
/**
* Registers that the given player has started playing the given dropper arena session
*

View File

@ -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 <p>A player object that's refers to the same player as the stored player</p>
*/
void restore(Player player);
/**
* Gets the id of the player this state belongs to
*
* @return <p>The player the state belongs to</p>
*/
UUID getPlayerId();
/**
* Gets the location the player entered from
*

View File

@ -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<DropperArena> {
@Override
protected String getEntryStateStorageKey() {
return "dropper";
}
}

View File

@ -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 <p>The id of the player whose state this should keep track of</p>
* @param makePlayerInvisible <p>Whether players should be made invisible while in the arena</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
*/
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<String, Object> serialize() {
Map<String, Object> 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 <p>The data to deserialize</p>
*/
@SuppressWarnings("unused")
public static DropperPlayerEntryState deserialize(Map<String, Object> 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);
}
}

View File

@ -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<ParkourArena> {
@Override
protected String getEntryStateStorageKey() {
return "parkour";
}
}

View File

@ -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 <p>The id of the player whose state this should keep track of</p>
* @param makePlayerInvisible <p>Whether players should be made invisible while in the arena</p>
* @param entryLocation <p>The location the player entered from</p>
* @param originalIsFlying <p>Whether the player was flying before entering the arena</p>
* @param originalGameMode <p>The game-mode of the player before entering the arena</p>
* @param originalAllowFlight <p>Whether the player was allowed flight before entering the arena</p>
* @param originalInvulnerable <p>Whether the player was invulnerable before entering the arena</p>
* @param originalIsSwimming <p>Whether the player was swimming before entering the arena</p>
* @param originalCollideAble <p>Whether the player was collide-able before entering the arena</p>
*/
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 <p>The data to deserialize</p>
*/
@SuppressWarnings("unused")
public static ParkourPlayerEntryState deserialize(Map<String, Object> 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);
}
}

View File

@ -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<UUID, ArenaSession> 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 <p>The triggered teleport event</p>
*/
@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);
}
}

View File

@ -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 <p>The triggered teleport event</p>
*/
@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 <p>The player whose state should be checked</p>
* @param playerRegistry <p>The registry to check for a lingering state</p>
* @return <p>The location the player should spawn in, or null if not restored</p>
*/
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;
}
}
}

View File

@ -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 <p>The key specifying the correct entry state file</p>
* @param entryStates <p>The entry states to save</p>
*/
public static void storeArenaPlayerEntryStates(String key, Set<PlayerEntryState> 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 <p>The key specifying the correct entry state file</p>
* @return <p>The previously saved entry states</p>
*/
public static Set<PlayerEntryState> 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<PlayerEntryState> 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 <p>The id of the arena to get a data file for</p>
* @return <p>The file the arena's data is/should be stored in</p>
*/
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");