Implements #5 and stuff

Uses a player entry state to store and restore a player's state such as game-mode and fly-mode.
Removes the restriction of fly mode and game-mode, and sets all arena players to adventure mode.
Force-quits all sessions when exiting the plugin.
Prevents usage of any commands other than the leave command when playing in an arena.
This commit is contained in:
Kristian Knarvik 2023-03-27 14:23:55 +02:00
parent 9a56f58f2f
commit 9a7d3ca360
13 changed files with 211 additions and 50 deletions

View File

@ -26,6 +26,7 @@ To modify
| /droppercreate | \<name> | Creates a new dropper arena with the given name |
| /dropperremove | \<arena> | Removes the specified dropper arena |
| [/dropperedit](#dropperedit) | \<arena> \<option> \[value] | Gets or sets a dropper arena option |
| /dropperreload | | Reloads all data from disk |
## Command explanation

View File

@ -3,6 +3,7 @@ package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.command.CreateArenaCommand;
import net.knarcraft.dropper.command.EditArenaCommand;
import net.knarcraft.dropper.command.EditArenaTabCompleter;
@ -15,6 +16,7 @@ import net.knarcraft.dropper.command.RemoveArenaCommand;
import net.knarcraft.dropper.command.RemoveArenaTabCompleter;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.listener.CommandListener;
import net.knarcraft.dropper.listener.DamageListener;
import net.knarcraft.dropper.listener.MoveListener;
import net.knarcraft.dropper.listener.PlayerLeaveListener;
@ -22,6 +24,7 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@ -91,8 +94,6 @@ public final class Dropper extends JavaPlugin {
this.arenaHandler = new DropperArenaHandler();
this.arenaHandler.loadArenas();
//TODO: Add a command for joining a specific arena. Only teleport if the stage check succeeds (The server can
// use something like https://www.spigotmc.org/resources/commandblocks.62720/ for immersion)
//TODO: Store various information about players' performance, and hook into PlaceholderAPI
//TODO: Possibly implement an optional queue mode, which only allows one player inside one dropper arena at any
@ -105,6 +106,7 @@ public final class Dropper extends JavaPlugin {
pluginManager.registerEvents(new DamageListener(), this);
pluginManager.registerEvents(new MoveListener(), this);
pluginManager.registerEvents(new PlayerLeaveListener(), this);
pluginManager.registerEvents(new CommandListener(), this);
registerCommand("dropperreload", new ReloadCommand(), null);
registerCommand("droppercreate", new CreateArenaCommand(), null);
@ -117,7 +119,13 @@ public final class Dropper extends JavaPlugin {
@Override
public void onDisable() {
// Throw out currently playing players before exiting
for (Player player : getServer().getOnlinePlayers()) {
DropperArenaSession session = playerRegistry.getArenaSession(player.getUniqueId());
if (session != null) {
session.triggerQuit(true);
}
}
}
/**

View File

@ -36,7 +36,7 @@ public class DropperArena {
*
* <p>This is technically the fly speed</p>
*/
private final double playerHorizontalVelocity;
private final float playerHorizontalVelocity;
/**
* The stage number of this arena. If not null, the previous stage number must be cleared before access.
@ -53,8 +53,7 @@ public class DropperArena {
*/
private final @NotNull Material winBlockType;
//TODO: Store records for this arena (maps with player->deaths/time). It should be possible to get those in sorted
// order (smallest to largest)
//TODO: It should be possible to get records in sorted order (smallest to largest)
/**
* Instantiates a new dropper arena
@ -63,14 +62,14 @@ public class DropperArena {
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param playerVerticalVelocity <p>The velocity to use for players' vertical velocity</p>
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity</p>
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity (-1 to 1)</p>
* @param stage <p>The stage number of this stage, or null if not limited to stages</p>
* @param winBlockType <p>The material of the block players have to hit to win this dropper arena</p>
* @param recordsRegistry <p>The registry keeping track of all of this arena's records</p>
*/
public DropperArena(@NotNull String arenaName, @NotNull Location spawnLocation, @Nullable Location exitLocation,
double playerVerticalVelocity, double playerHorizontalVelocity, @Nullable Integer stage, @NotNull Material winBlockType,
@NotNull DropperArenaRecordsRegistry recordsRegistry) {
double playerVerticalVelocity, float playerHorizontalVelocity, @Nullable Integer stage,
@NotNull Material winBlockType, @NotNull DropperArenaRecordsRegistry recordsRegistry) {
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
@ -158,7 +157,7 @@ public class DropperArena {
*
* @return <p>Players' velocity in this arena</p>
*/
public double getPlayerHorizontalVelocity() {
public float getPlayerHorizontalVelocity() {
return this.playerHorizontalVelocity;
}

View File

@ -52,7 +52,7 @@ public class DropperArenaPlayerRegistry {
for (Map.Entry<UUID, DropperArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit();
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}

View File

@ -18,10 +18,9 @@ public class DropperArenaSession {
private final @NotNull DropperArena arena;
private final @NotNull Player player;
private final @NotNull ArenaGameMode gameMode;
private final @NotNull Location entryLocation;
private int deaths;
private final long startTime;
private final float playersOriginalFlySpeed;
private final PlayerEntryState entryState;
/**
* Instantiates a new dropper arena session
@ -37,13 +36,19 @@ public class DropperArenaSession {
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
this.entryLocation = player.getLocation();
this.entryState = new PlayerEntryState(player);
// Make the player fly to improve mobility in the air
player.setAllowFlight(true);
player.setFlying(true);
this.playersOriginalFlySpeed = player.getFlySpeed();
player.setFlySpeed((float) this.arena.getPlayerHorizontalVelocity());
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());
}
/**
* Gets the state of the player when they joined the session
*
* @return <p>The player's entry state</p>
*/
public @NotNull PlayerEntryState getEntryState() {
return this.entryState;
}
/**
@ -70,21 +75,21 @@ public class DropperArenaSession {
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit();
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*/
private void teleportToExit() {
private void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryLocation;
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true);
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
@ -125,17 +130,17 @@ public class DropperArenaSession {
this.deaths++;
}
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true);
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
}
/**
* Triggers a quit for the player playing in this session
*/
public void triggerQuit() {
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit();
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
@ -148,9 +153,7 @@ public class DropperArenaSession {
removeSession();
// Remove flight mode
this.player.setFlySpeed(this.playersOriginalFlySpeed);
this.player.setFlying(false);
this.player.setAllowFlight(false);
entryState.restore();
}
/**

View File

@ -0,0 +1,71 @@
package net.knarcraft.dropper.arena;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
/**
* The state of a player before entering a dropper arena
*/
public class PlayerEntryState {
private final Player player;
private final Location entryLocation;
private final boolean originalIsFlying;
private final float originalFlySpeed;
private final GameMode originalGameMode;
private final boolean originalAllowFlight;
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
/**
* Instantiates a new player state
*
* @param player <p>The player whose state should be stored</p>
*/
public PlayerEntryState(Player player) {
this.player = player;
this.entryLocation = player.getLocation();
this.originalFlySpeed = player.getFlySpeed();
this.originalIsFlying = player.isFlying();
this.originalGameMode = player.getGameMode();
this.originalAllowFlight = player.getAllowFlight();
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
}
/**
* Sets the state of the stored player to the state used by arenas
*
* @param horizontalVelocity <p>The horizontal velocity to apply to the player</p>
*/
public void setArenaState(float horizontalVelocity) {
this.player.setAllowFlight(true);
this.player.setFlying(true);
this.player.setFlySpeed(horizontalVelocity);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
}
/**
* Restores the stored state for the stored player
*/
public void restore() {
this.player.setFlying(this.originalIsFlying);
this.player.setGameMode(this.originalGameMode);
this.player.setAllowFlight(this.originalAllowFlight);
this.player.setFlySpeed(this.originalFlySpeed);
this.player.setInvulnerable(this.originalInvulnerable);
this.player.setSwimming(this.originalIsSwimming);
}
/**
* Gets the location the player entered from
*
* @return <p>The location the player entered from</p>
*/
public Location getEntryLocation() {
return this.entryLocation;
}
}

View File

@ -6,7 +6,6 @@ import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.PlayerTeleporter;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -30,12 +29,6 @@ public class JoinArenaCommand implements CommandExecutor {
return false;
}
if (player.isFlying() || player.getGameMode() == GameMode.CREATIVE ||
player.getGameMode() == GameMode.SPECTATOR) {
commandSender.sendMessage("You cannot join a dropper arena while able to fly!");
return false;
}
// Disallow joining if the player is already in a dropper arena
DropperArenaSession existingSession = Dropper.getInstance().getPlayerRegistry().getArenaSession(player.getUniqueId());
if (existingSession != null) {
@ -50,6 +43,24 @@ public class JoinArenaCommand implements CommandExecutor {
return false;
}
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
commandSender.sendMessage("You cannot join an arena while inside a vehicle or carrying a passenger.");
return false;
}
return joinArena(specifiedArena, player, arguments);
}
/**
* Performs the actual arena joining
*
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
ArenaGameMode gameMode;
if (arguments.length > 1) {
@ -66,13 +77,15 @@ public class JoinArenaCommand implements CommandExecutor {
playerRegistry.registerPlayer(player.getUniqueId(), newSession);
// Try to teleport the player to the arena
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false);
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false, false);
if (!teleported) {
commandSender.sendMessage("Unable to teleport you to the dropper arena. Make sure you're not in a vehicle," +
"and is not carrying a passenger!");
newSession.triggerQuit();
player.sendMessage("Unable to teleport you to the dropper arena. Make sure you're not in a vehicle," +
"and not carrying a passenger!");
newSession.triggerQuit(false);
return false;
} else {
// Make sure to update the state again in the air to remove a potential swimming state
newSession.getEntryState().setArenaState(specifiedArena.getPlayerHorizontalVelocity());
return true;
}
}

View File

@ -32,7 +32,7 @@ public class LeaveArenaCommand implements TabExecutor {
return false;
}
existingSession.triggerQuit();
existingSession.triggerQuit(false);
return true;
}

View File

@ -0,0 +1,46 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaSession;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.ArrayList;
import java.util.List;
/**
* A listener for players trying to use commands while inside a dropper arena
*/
public class CommandListener implements Listener {
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
Player player = event.getPlayer();
DropperArenaSession existingSession = Dropper.getInstance().getPlayerRegistry().getArenaSession(
player.getUniqueId());
if (existingSession == null) {
return;
}
List<String> allowedCommands = new ArrayList<>();
allowedCommands.add("/dropperleave");
allowedCommands.add("/dleave");
String message = event.getMessage();
if (!message.startsWith("/")) {
return;
}
for (String command : allowedCommands) {
if (message.equals(command)) {
return;
}
}
player.sendMessage("You cannot use that command while in an arena!");
event.setCancelled(true);
}
}

View File

@ -44,7 +44,7 @@ public class PlayerLeaveListener implements Listener {
if (leftSessions.containsKey(playerId)) {
Dropper.getInstance().getLogger().log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit();
leftSessions.get(playerId).triggerQuit(false);
Dropper.getInstance().getLogger().log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
@ -66,7 +66,7 @@ public class PlayerLeaveListener implements Listener {
return;
}
arenaSession.triggerQuit();
arenaSession.triggerQuit(false);
}
/**

View File

@ -9,7 +9,6 @@ public enum ArenaGameMode {
/**
* The default game-mode. Failing once throws the player out.
* //TODO: Verify if we want the default game-mode to lock the player in the arena until they beat it
*/
DEFAULT,

View File

@ -53,7 +53,6 @@ public final class ArenaStorageHelper {
configSection.set(ArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
configSection.set(ArenaStorageKey.RECORDS.getKey(), arena.getRecordsRegistry());
}
//TODO: Save records belonging to the arena
configuration.save(arenaFile);
}
@ -100,7 +99,8 @@ public final class ArenaStorageHelper {
Location spawnLocation = (Location) configurationSection.get(ArenaStorageKey.SPAWN_LOCATION.getKey());
Location exitLocation = (Location) configurationSection.get(ArenaStorageKey.EXIT_LOCATION.getKey());
double verticalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
double horizontalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey());
float horizontalVelocity = sanitizeHorizontalVelocity((float) configurationSection.getDouble(
ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey()));
Integer stage = (Integer) configurationSection.get(ArenaStorageKey.STAGE.getKey());
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
ArenaStorageKey.WIN_BLOCK_TYPE.getKey());
@ -133,4 +133,20 @@ public final class ArenaStorageHelper {
return arenaName.toLowerCase().trim().replaceAll(" ", "_");
}
/**
* Sanitizes the given horizontal velocity to make sure it doesn't leave its bounds
*
* @param horizontalVelocity <p>The horizontal velocity to sanitize</p>
* @return <p>The sanitized horizontal velocity</p>
*/
private static float sanitizeHorizontalVelocity(float horizontalVelocity) {
if (horizontalVelocity < -1) {
return -1;
} else if (horizontalVelocity > 1) {
return 1;
} else {
return horizontalVelocity;
}
}
}

View File

@ -21,12 +21,13 @@ public final class PlayerTeleporter {
*
* <p>Forcing teleport should only be used inside an arena, to prevent the player from becoming stuck.</p>
*
* @param player <p>The player about to teleport</p>
* @param location <p>The location the player should be teleported to</p>
* @param force <p>Whether to force a player teleport, even in a vehicle or a passenger</p>
* @param player <p>The player about to teleport</p>
* @param location <p>The location the player should be teleported to</p>
* @param force <p>Whether to force a player teleport, even in a vehicle or a passenger</p>
* @param immediately <p>Whether to to the teleportation immediately, not using any timers</p>
* @return <p>True if the player was successfully teleported</p>
*/
public static boolean teleportPlayer(Player player, Location location, boolean force) {
public static boolean teleportPlayer(Player player, Location location, boolean force, boolean immediately) {
if (!player.getPassengers().isEmpty()) {
if (force) {
for (Entity passenger : player.getPassengers()) {
@ -55,7 +56,11 @@ public final class PlayerTeleporter {
player.setVelocity(new Vector(0, 0, 0));
//When teleporting a player out of the arena, sometimes the move listener is slow to react, giving the player
// lethal velocity, and causing damage. That's why the player is given 5 ticks of invulnerability
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> player.setInvulnerable(false), 5);
if (!immediately) {
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> player.setInvulnerable(false), 5);
} else {
player.setInvulnerable(false);
}
return true;
}