Refactor the join/leave process.

Note that this commit makes breaking changes to the Arena interface and completely changes the responsibility of the InventoryManager.

The join/leave process is replaced with an implementation of the Command Pattern, where every step of the process (e.g. teleport to lobby, change gamemode to survival, reset health, etc.) is realized as an implementation of the new Step interface, which has "run" and "undo" operations. The "run" operation takes a snapshot of a specific part of the player's state and then resets it. The "undo" operation restores the snapshot.

Step groupings are arranged in two factory classes, PlayerJoinArena and PlayerSpecArena, which realize, respectively, the join and spectate processes. Each instance of ArenaImpl instantiates groupings for its own context, and a grouping is invoked as one unit when a player joins or spectates an arena, and rolled back when a player leaves.

As a result of a more stringent process, some things are now a little different than before:

- Setting spectate-on-death to true effectively results in /ma leave followed by /ma spec. This makes the player/arena state a little more predictable and well-defined.
- Using exit warps will result in the player leaving to their entry point and then being teleported to the exit warp. This means the exit warp doesn't have any effect on the rest of the restoration process, as it effectively happens "post leave".

This finally fixes #423.
This commit is contained in:
Andreas Troelsen 2018-04-29 14:42:57 +02:00
parent 208a43262e
commit b1c6b61827
33 changed files with 818 additions and 501 deletions

View File

@ -351,7 +351,7 @@ public class ArenaClass
Arena arena = am.getArenaWithPlayer(p);
if (arena != null) {
try {
arena.getInventoryManager().restoreInv(p);
arena.getInventoryManager().equip(p);
removeBannedItems(p.getInventory());
} catch (Exception e) {
am.getPlugin().getLogger().severe("Failed to give " + p.getName() + " their own items: " + e.getMessage());

View File

@ -4,6 +4,10 @@ import static com.garbagemule.MobArena.util.config.ConfigUtils.makeSection;
import com.garbagemule.MobArena.ArenaClass.ArmorType;
import com.garbagemule.MobArena.ScoreboardManager.NullScoreboardManager;
import com.garbagemule.MobArena.steps.Step;
import com.garbagemule.MobArena.steps.StepFactory;
import com.garbagemule.MobArena.steps.PlayerJoinArena;
import com.garbagemule.MobArena.steps.PlayerSpecArena;
import com.garbagemule.MobArena.events.ArenaEndEvent;
import com.garbagemule.MobArena.events.ArenaPlayerDeathEvent;
import com.garbagemule.MobArena.events.ArenaPlayerJoinEvent;
@ -17,16 +21,8 @@ import com.garbagemule.MobArena.repairable.Repairable;
import com.garbagemule.MobArena.repairable.RepairableComparator;
import com.garbagemule.MobArena.repairable.RepairableContainer;
import com.garbagemule.MobArena.things.Thing;
import com.garbagemule.MobArena.time.Time;
import com.garbagemule.MobArena.time.TimeStrategy;
import com.garbagemule.MobArena.time.TimeStrategyLocked;
import com.garbagemule.MobArena.time.TimeStrategyNull;
import com.garbagemule.MobArena.util.ClassChests;
import com.garbagemule.MobArena.util.Delays;
import com.garbagemule.MobArena.util.Enums;
import com.garbagemule.MobArena.util.ItemParser;
import com.garbagemule.MobArena.util.inventory.InventoryManager;
import com.garbagemule.MobArena.util.inventory.InventoryUtils;
import com.garbagemule.MobArena.util.timer.AutoStartTimer;
import com.garbagemule.MobArena.util.timer.StartDelayTimer;
import com.garbagemule.MobArena.waves.SheepBouncer;
@ -34,7 +30,6 @@ import com.garbagemule.MobArena.waves.WaveManager;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
@ -65,6 +60,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class ArenaImpl implements Arena
@ -94,7 +90,6 @@ public class ArenaImpl implements Arena
private RewardManager rewardManager;
private ClassLimitManager limitManager;
private Map<Player,ArenaPlayer> arenaPlayerMap;
private Map<Player,PlayerData> playerData = new HashMap<>();
private Set<Player> arenaPlayers, lobbyPlayers, readyPlayers, specPlayers, deadPlayers;
private Set<Player> movingPlayers;
@ -123,7 +118,6 @@ public class ArenaImpl implements Arena
// Misc
private ArenaListener eventListener;
private List<Thing> entryFee;
private TimeStrategy timeStrategy;
private AutoStartTimer autoStartTimer;
private StartDelayTimer startDelayTimer;
private boolean isolatedChat;
@ -134,6 +128,11 @@ public class ArenaImpl implements Arena
// Last player standing
private Player lastStanding;
// Actions
private Map<Player, Step> histories;
private StepFactory playerJoinArena;
private StepFactory playerSpecArena;
/**
* Primary constructor. Requires a name and a world.
*/
@ -152,7 +151,7 @@ public class ArenaImpl implements Arena
this.running = false;
this.edit = false;
this.inventoryManager = new InventoryManager(this);
this.inventoryManager = new InventoryManager();
this.rewardManager = new RewardManager(this);
// Warps, points and locations
@ -220,16 +219,17 @@ public class ArenaImpl implements Arena
this.isolatedChat = settings.getBoolean("isolated-chat", false);
String timeString = settings.getString("player-time-in-arena", "world");
Time time = Enums.getEnumFromString(Time.class, timeString);
this.timeStrategy = (time != null ? new TimeStrategyLocked(time) : new TimeStrategyNull());
// Scoreboards
this.scoreboard = (settings.getBoolean("use-scoreboards", true) ? new ScoreboardManager(this) : new NullScoreboardManager(this));
// Messenger
String prefix = settings.getString("prefix", "");
this.messenger = !prefix.isEmpty() ? new Messenger(prefix) : plugin.getGlobalMessenger();
// Actions
this.histories = new HashMap<>();
this.playerJoinArena = PlayerJoinArena.create(this);
this.playerSpecArena = PlayerSpecArena.create(this);
}
@ -354,12 +354,6 @@ public class ArenaImpl implements Arena
return waveManager;
}
@Override
public Location getPlayerEntry(Player p) {
PlayerData mp = playerData.get(p);
return (mp != null ? mp.entry() : null);
}
@Override
public ArenaListener getEventListener() {
return eventListener;
@ -523,15 +517,7 @@ public class ArenaImpl implements Arena
movingPlayers.add(p);
p.teleport(region.getArenaWarp());
movingPlayers.remove(p);
p.setAllowFlight(false);
p.setFlying(false);
//movePlayerToLocation(p, region.getArenaWarp());
setHealth(p, p.getMaxHealth());
p.setFoodLevel(20);
if (settings.getBoolean("display-waves-as-level", false)) {
p.setLevel(0);
p.setExp(0.0f);
}
assignClassPermissions(p);
arenaPlayerMap.get(p).resetStats();
@ -678,6 +664,10 @@ public class ArenaImpl implements Arena
}
movingPlayers.add(p);
rollback(p);
specPlayers.remove(p);
// Announce globally (must happen before moving player)
if (settings.getBoolean("global-join-announce", false)) {
if (lobbyPlayers.isEmpty()) {
@ -687,18 +677,17 @@ public class ArenaImpl implements Arena
}
}
MAUtils.sitPets(p);
movePlayerToLobby(p);
takeFee(p);
storePlayerData(p, loc);
removePotionEffects(p);
setHealth(p, p.getMaxHealth());
p.setFoodLevel(20);
if (settings.getBoolean("display-timer-as-level", false)) {
p.setLevel(0);
p.setExp(0.0f);
Step step = playerJoinArena.create(p);
try {
step.run();
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, () -> "Player " + p.getName() + " couldn't join arena " + name);
return false;
}
p.setGameMode(GameMode.SURVIVAL);
histories.put(p, step);
lobbyPlayers.add(p);
plugin.getArenaMaster().addPlayer(p, this);
arenaPlayerMap.put(p, new ArenaPlayer(p, this, plugin));
@ -775,7 +764,6 @@ public class ArenaImpl implements Arena
removeClassPermissions(p);
removePotionEffects(p);
restoreInvAndExp(p);
if (inLobby(p) || inArena(p)) {
refund(p);
}
@ -792,7 +780,6 @@ public class ArenaImpl implements Arena
}
}
movePlayerToEntry(p);
discardPlayer(p);
endArena();
@ -834,8 +821,9 @@ public class ArenaImpl implements Arena
return;
}
setHealth(p, p.getMaxHealth());
Delays.revivePlayer(plugin, this, p);
p.setHealth(20.0);
plugin.getServer().getScheduler()
.scheduleSyncDelayedTask(plugin, () -> revivePlayer(p));
endArena();
}
@ -864,37 +852,25 @@ public class ArenaImpl implements Arena
}
deadPlayers.remove(p);
revivePlayer(p);
plugin.getServer().getScheduler()
.scheduleSyncDelayedTask(plugin, () -> revivePlayer(p));
}
@Override
@SuppressWarnings("deprecation")
public void revivePlayer(Player p) {
Delays.douse(plugin, p, 1);
removeClassPermissions(p);
removePotionEffects(p);
discardPlayer(p);
if (settings.getBoolean("spectate-on-death", true)) {
movePlayerToSpec(p);
messenger.tell(p, Msg.SPEC_FROM_ARENA);
messenger.tell(p, Msg.MISC_MA_LEAVE_REMINDER);
} else {
restoreInvAndExp(p);
movePlayerToEntry(p);
discardPlayer(p);
playerSpec(p, null);
}
p.updateInventory();
}
@Override
public Location getRespawnLocation(Player p) {
Location l = null;
if (settings.getBoolean("spectate-on-death", true)) {
l = region.getSpecWarp();
} else {
l = playerData.get(p).entry();
}
return l;
return region.getSpecWarp();
}
@Override
@ -904,14 +880,37 @@ public class ArenaImpl implements Arena
}
movingPlayers.add(p);
storePlayerData(p, loc);
MAUtils.sitPets(p);
movePlayerToSpec(p);
rollback(p);
Step step = playerSpecArena.create(p);
try {
step.run();
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, () -> "Player " + p.getName() + " couldn't spec arena " + name);
return;
}
histories.put(p, step);
specPlayers.add(p);
plugin.getArenaMaster().addPlayer(p, this);
messenger.tell(p, Msg.SPEC_PLAYER_SPECTATE);
movingPlayers.remove(p);
}
private void rollback(Player p) {
Step step = histories.remove(p);
if (step == null) {
return;
}
try {
step.undo();
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, () -> "Failed to revert player " + p.getName());
}
}
private void spawnPets() {
for (Player p : arenaPlayers) {
// Skip players who are either null or offline
@ -1078,33 +1077,6 @@ public class ArenaImpl implements Arena
scheduleTask(sheepBouncer, settings.getInt("first-wave-delay", 5) * 20);
}
@Override
public void storePlayerData(Player p, Location loc)
{
plugin.getArenaMaster().addPlayer(p, this);
PlayerData mp = playerData.get(p);
// If there's no player stored, create a new one!
if (mp == null) {
if (region.getExitWarp() != null) loc = region.getExitWarp();
mp = new PlayerData(p, loc);
playerData.put(p, mp);
}
// At any rate, update the data.
mp.update();
// And update the inventory as well.
try {
inventoryManager.storeInv(p);
inventoryManager.clearInventory(p);
} catch (Exception e) {
e.printStackTrace();
plugin.getLogger().severe("Failed to store inventory for player " + p.getName() + "!");
}
}
@Override
public void storeContainerContents()
{
@ -1124,80 +1096,16 @@ public class ArenaImpl implements Arena
}
}
@Override
public void movePlayerToLobby(Player p)
{
specPlayers.remove(p); // If joining from spec area
lobbyPlayers.add(p);
p.teleport(region.getLobbyWarp());
p.setAllowFlight(false);
p.setFlying(false);
timeStrategy.setPlayerTime(p);
}
@Override
public void movePlayerToSpec(Player p)
{
specPlayers.add(p);
p.teleport(region.getSpecWarp());
timeStrategy.setPlayerTime(p);
}
@Override
public void movePlayerToEntry(Player p)
{
Location entry = playerData.get(p).entry();
if (entry == null || p.isDead()) return;
p.teleport(entry);
timeStrategy.resetPlayerTime(p);
p.setGameMode(playerData.get(p).getMode());
p.addPotionEffects(playerData.get(p).getPotionEffects());
}
private void restoreInvAndExp(Player p) {
inventoryManager.clearInventory(p);
try {
inventoryManager.restoreInv(p);
inventoryManager.clearCache(p);
} catch (Exception e) {
e.printStackTrace();
plugin.getLogger().severe("Failed to restore inventory for player " + p.getName() + "!");
}
rewardManager.grantRewards(p);
// Try to prevent XP issues
if (lobbyPlayers.contains(p)
|| !settings.getBoolean("keep-exp")
|| settings.getBoolean("display-waves-as-level", false)
|| settings.getBoolean("display-timer-as-level", false)) {
playerData.get(p).restoreData();
}
else {
p.setFoodLevel(playerData.get(p).food());
}
}
@Override
public void discardPlayer(Player p)
{
rollback(p);
plugin.getArenaMaster().removePlayer(p);
clearPlayer(p);
}
private void clearPlayer(Player p)
{
// Remove the player data completely.
PlayerData mp = playerData.remove(p);
// Health must be handled in a certain way because of Heroes
// Math.min to guard for ItemLoreStats weirdness
setHealth(p, Math.min(p.getMaxHealth(), mp.health()));
// Put out fire.
Delays.douse(plugin, p, 3);
// Remove pets.
monsterManager.removePets(p);
@ -1211,10 +1119,6 @@ public class ArenaImpl implements Arena
scoreboard.removePlayer(p);
}
private void setHealth(Player p, double health) {
p.setHealth(health);
}
@Override
public void repairBlocks()
{
@ -1245,7 +1149,7 @@ public class ArenaImpl implements Arena
return;
}
inventoryManager.clearInventory(p);
InventoryManager.clearInventory(p);
arenaPlayer.setArenaClass(arenaClass);
arenaClass.grantItems(p);
@ -1265,7 +1169,7 @@ public class ArenaImpl implements Arena
return;
}
inventoryManager.clearInventory(p);
InventoryManager.clearInventory(p);
arenaPlayer.setArenaClass(arenaClass);
PlayerInventory inv = p.getInventory();

View File

@ -8,14 +8,12 @@ import com.garbagemule.MobArena.listeners.MagicSpellsListener;
import com.garbagemule.MobArena.things.ThingManager;
import com.garbagemule.MobArena.util.VersionChecker;
import com.garbagemule.MobArena.util.config.ConfigUtils;
import com.garbagemule.MobArena.util.inventory.InventoryManager;
import com.garbagemule.MobArena.waves.ability.AbilityManager;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.ChatColor;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.RegisteredServiceProvider;
@ -26,10 +24,8 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* MobArena
@ -40,9 +36,6 @@ public class MobArena extends JavaPlugin
private ArenaMaster arenaMaster;
private CommandHandler commandHandler;
// Inventories from disconnects
private Set<String> inventoriesToRestore;
// Vault
private Economy economy;
@ -91,9 +84,6 @@ public class MobArena extends JavaPlugin
arenaMaster = new ArenaMasterImpl(this);
arenaMaster.initialize();
// Register any inventories to restore.
registerInventories();
// Register event listeners
registerListeners();
@ -258,32 +248,6 @@ public class MobArena extends JavaPlugin
"Note: You -must- use spaces instead of tabs!";
}
private void registerInventories() {
this.inventoriesToRestore = new HashSet<>();
File dir = new File(getDataFolder(), "inventories");
if (!dir.exists()) {
dir.mkdir();
return;
}
for (File f : dir.listFiles()) {
if (f.getName().endsWith(".inv")) {
inventoriesToRestore.add(f.getName().substring(0, f.getName().indexOf(".")));
}
}
}
public void restoreInventory(Player p) {
if (!inventoriesToRestore.contains(p.getName())) {
return;
}
if (InventoryManager.restoreFromFile(this, p)) {
inventoriesToRestore.remove(p.getName());
}
}
public Economy getEconomy() {
return economy;
}

View File

@ -1,109 +0,0 @@
package com.garbagemule.MobArena;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import java.util.Collection;
public class PlayerData
{
private Player player;
private double health;
private int food, level;
private float exp;
private GameMode mode = null;
private Location entry = null;
private Collection<PotionEffect> potions;
public PlayerData(Player player, Location loc) {
this.player = player;
this.mode = player.getGameMode();
this.potions = player.getActivePotionEffects();
this.entry = loc;
update();
}
/**
* Updates the information that is restored, when a player
* dies in the arena, that is, health, food level, and
* experience. Used when a player re-joins an arena while
* already being a spectator.
*/
public void update() {
this.health = player.getHealth();
this.food = player.getFoodLevel();
this.level = player.getLevel();
this.exp = player.getExp();
}
/**
* Restores health, food level, and experience as per the
* currently stored values of this object. Used when a
* player leaves the arena.
*/
public void restoreData() {
player.setFoodLevel(food);
player.setLevel(level);
player.setExp(exp);
}
public Player getPlayer() {
return player;
}
public double health() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
public int food() {
return food;
}
public void setFood(int food) {
this.food = food;
}
public int level() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public float exp() {
return exp;
}
public void setExp(int exp) {
this.exp = exp;
}
public GameMode getMode() {
return mode;
}
public Collection<PotionEffect> getPotionEffects() {
return potions;
}
public void setMode(GameMode mode) {
this.mode = mode;
}
public Location entry() {
return entry;
}
public void setEntry(Location entry) {
this.entry = entry;
}
}

View File

@ -5,6 +5,7 @@ import com.garbagemule.MobArena.commands.CommandInfo;
import com.garbagemule.MobArena.framework.ArenaMaster;
import com.garbagemule.MobArena.util.inventory.InventoryManager;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@CommandInfo(
name = "restore",
@ -20,12 +21,17 @@ public class RestoreCommand implements Command
// Require a player name
if (args.length != 1) return false;
if (am.getArenaWithPlayer(args[0]) != null) {
Player player = am.getPlugin().getServer().getPlayer(args[0]);
if (player == null) {
am.getGlobalMessenger().tell(sender, "Player not found.");
return true;
}
if (am.getArenaWithPlayer(player) != null) {
am.getGlobalMessenger().tell(sender, "Player is currently in an arena.");
return true;
}
if (InventoryManager.restoreFromFile(am.getPlugin(), am.getPlugin().getServer().getPlayer(args[0]))) {
if (InventoryManager.restoreFromFile(am.getPlugin(), player)) {
am.getGlobalMessenger().tell(sender, "Restored " + args[0] + "'s inventory!");
} else {
am.getGlobalMessenger().tell(sender, "Failed to restore " + args[0] + "'s inventory.");

View File

@ -81,8 +81,6 @@ public interface Arena
WaveManager getWaveManager();
Location getPlayerEntry(Player p);
ArenaListener getEventListener();
void setLeaderboard(Leaderboard leaderboard);
@ -168,18 +166,10 @@ public interface Arena
void playerSpec(Player p, Location loc);
void storePlayerData(Player p, Location loc);
void storeContainerContents();
void restoreContainerContents();
void movePlayerToLobby(Player p);
void movePlayerToSpec(Player p);
void movePlayerToEntry(Player p);
void discardPlayer(Player p);
void repairBlocks();

View File

@ -322,8 +322,6 @@ public class MAGlobalListener implements Listener
return;
}
}
plugin.restoreInventory(event.getPlayer());
}
public enum TeleportResponse {

View File

@ -0,0 +1,66 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.logging.Level;
class ClearInventory extends PlayerStep {
private final File inventories;
private final Arena arena;
private ItemStack[] contents;
private File backup;
private ClearInventory(Player player, Arena arena) {
super(player);
this.inventories = new File(arena.getPlugin().getDataFolder(), "inventories");
this.arena = arena;
}
@Override
public void run() {
contents = player.getInventory().getContents();
createBackup();
player.getInventory().clear();
}
@Override
public void undo() {
player.getInventory().setContents(contents);
arena.getInventoryManager().remove(player);
deleteBackup();
}
private void createBackup() {
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("contents", contents);
backup = new File(inventories, player.getUniqueId().toString());
try {
yaml.save(backup);
} catch (IOException e) {
throw new RuntimeException("Failed to store inventory for " + player.getName(), e);
}
arena.getInventoryManager().put(player, contents);
}
private void deleteBackup() {
try {
Files.delete(backup.toPath());
} catch (IOException e) {
arena.getPlugin().getLogger().log(Level.WARNING, "Couldn't delete backup inventory file for " + player.getName(), e);
}
}
static StepFactory create(Arena arena) {
return player -> new ClearInventory(player, arena);
}
}

View File

@ -0,0 +1,34 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import java.util.Collection;
import java.util.Collections;
class ClearPotionEffects extends PlayerStep {
private Collection<PotionEffect> effects;
private ClearPotionEffects(Player player) {
super(player);
effects = Collections.emptyList();
}
@Override
public void run() {
effects = player.getActivePotionEffects();
effects.stream()
.map(PotionEffect::getType)
.forEach(player::removePotionEffect);
}
@Override
public void undo() {
player.addPotionEffects(effects);
}
static StepFactory create() {
return ClearPotionEffects::new;
}
}

View File

@ -0,0 +1,36 @@
package com.garbagemule.MobArena.steps;
/**
* This wrapper allows a delegate {@link Step}'s {@link Step#run() run()}
* operation to be deferred until this wrapper's {@link Step#undo() undo()}
* operation is invoked. This is useful when a Step should only be invoked
* during rollback, but not during the initial procedure.
*
* @see GrantRewards
*/
class Defer implements Step {
private Step step;
private Defer(Step step) {
this.step = step;
}
@Override
public void run() {
// OK BOSS
}
@Override
public void undo() {
step.run();
}
@Override
public String toString() {
return "deferred(" + step + ")";
}
static StepFactory it(StepFactory factory) {
return player -> new Defer(factory.create(player));
}
}

View File

@ -0,0 +1,27 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.entity.Player;
class GrantRewards extends PlayerStep {
private final Arena arena;
private GrantRewards(Player player, Arena arena) {
super(player);
this.arena = arena;
}
@Override
public void run() {
arena.getRewardManager().grantRewards(player);
}
@Override
public void undo() {
// OK BOSS
}
static StepFactory create(Arena arena) {
return player -> new GrantRewards(player, arena);
}
}

View File

@ -0,0 +1,29 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.function.Supplier;
abstract class MovePlayerStep extends PlayerStep {
private final Supplier<Location> destination;
private Location location;
MovePlayerStep(Player player, Supplier<Location> destination) {
super(player);
this.destination = destination;
}
@Override
public void run() {
location = player.getLocation();
player.teleport(destination.get());
}
@Override
public void undo() {
player.teleport(location);
}
}

View File

@ -0,0 +1,14 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.entity.Player;
class MoveToExit extends MovePlayerStep {
private MoveToExit(Player player, Arena arena) {
super(player, () -> arena.getRegion().getExitWarp());
}
static StepFactory create(Arena arena) {
return player -> new MoveToExit(player, arena);
}
}

View File

@ -0,0 +1,14 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.entity.Player;
class MoveToLobby extends MovePlayerStep {
private MoveToLobby(Player player, Arena arena) {
super(player, () -> arena.getRegion().getLobbyWarp());
}
static StepFactory create(Arena arena) {
return player -> new MoveToLobby(player, arena);
}
}

View File

@ -0,0 +1,14 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.entity.Player;
class MoveToSpec extends MovePlayerStep {
private MoveToSpec(Player player, Arena arena) {
super(player, () -> arena.getRegion().getSpecWarp());
}
static StepFactory create(Arena arena) {
return player -> new MoveToSpec(player, arena);
}
}

View File

@ -0,0 +1,32 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.List;
public class PlayerJoinArena {
public static StepFactory create(Arena arena) {
ConfigurationSection settings = arena.getSettings();
List<StepFactory> factories = new ArrayList<>();
if (arena.getRegion().getExitWarp() != null) {
factories.add(Defer.it(MoveToExit.create(arena)));
}
factories.add(SitPets.create());
factories.add(MoveToLobby.create(arena));
factories.add(SetGameMode.create());
factories.add(Defer.it(GrantRewards.create(arena)));
factories.add(ClearInventory.create(arena));
factories.add(ClearPotionEffects.create());
factories.add(SetFlying.create());
factories.add(SetHealth.create());
factories.add(SetHunger.create());
factories.add(SetExperience.create());
factories.add(SetPlayerTime.create(settings));
return PlayerMultiStep.create(factories, arena.getPlugin().getLogger());
}
}

View File

@ -0,0 +1,70 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* PlayerMultiSteps group lists of {@link StepFactory StepFactories} into a
* single unit, effectively realizing the Composite Pattern.
* <p>
* Each factory is invoked to create a given {@link Step}, which is executed
* right away. If the Step succeeds, it is pushed to a history stack. The undo
* operation pops the successful Steps off the history stack one by one and
* runs the undo operation on each one in reverse order.
* <p>
* If one Step fails, any previous successful Steps are rolled back by running
* their undo operations.
*/
class PlayerMultiStep extends PlayerStep {
private final List<StepFactory> factories;
private final Logger logger;
private Deque<Step> history;
private PlayerMultiStep(Player player, List<StepFactory> factories, Logger logger) {
super(player);
this.factories = factories;
this.logger = logger;
this.history = new ArrayDeque<>();
}
@Override
public void run() {
history.clear();
factories.forEach(factory -> {
Step step = factory.create(player);
try {
step.run();
history.push(step);
logger.info("Step " + step + " OK");
} catch (RuntimeException up) {
logger.log(Level.SEVERE, up, () -> "Failed to run step " + step);
undo();
throw up;
}
});
}
@Override
public void undo() {
while (!history.isEmpty()) {
Step step = history.pop();
try {
step.undo();
logger.info("Rollback " + step + " OK");
} catch (RuntimeException e) {
logger.log(Level.SEVERE, e, () -> "Failed to undo step " + step);
}
}
}
static StepFactory create(List<StepFactory> factories, Logger logger) {
return player -> new PlayerMultiStep(player, factories, logger);
}
}

View File

@ -0,0 +1,31 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.configuration.ConfigurationSection;
import java.util.ArrayList;
import java.util.List;
public class PlayerSpecArena {
public static StepFactory create(Arena arena) {
ConfigurationSection settings = arena.getSettings();
List<StepFactory> factories = new ArrayList<>();
if (arena.getRegion().getExitWarp() != null) {
factories.add(Defer.it(MoveToExit.create(arena)));
}
factories.add(SitPets.create());
factories.add(MoveToSpec.create(arena));
factories.add(SetGameMode.create());
factories.add(ClearInventory.create(arena));
factories.add(ClearPotionEffects.create());
factories.add(SetFlying.create());
factories.add(SetHealth.create());
factories.add(SetHunger.create());
factories.add(SetExperience.create());
factories.add(SetPlayerTime.create(settings));
return PlayerMultiStep.create(factories, arena.getPlugin().getLogger());
}
}

View File

@ -0,0 +1,18 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
abstract class PlayerStep implements Step {
protected final Player player;
protected PlayerStep(Player player) {
this.player = player;
}
@Override
public String toString() {
String step = getClass().getSimpleName();
String target = player.getName();
return step + "(" + target + ")";
}
}

View File

@ -0,0 +1,31 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
class SetExperience extends PlayerStep {
private int level;
private float exp;
private SetExperience(Player player) {
super(player);
}
@Override
public void run() {
level = player.getLevel();
exp = player.getExp();
player.setExp(0);
player.setLevel(0);
}
@Override
public void undo() {
player.setLevel(level);
player.setExp(exp);
}
static StepFactory create() {
return SetExperience::new;
}
}

View File

@ -0,0 +1,35 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
class SetFlying extends PlayerStep {
private boolean allow;
private boolean flying;
private float speed;
private SetFlying(Player player) {
super(player);
}
@Override
public void run() {
allow = player.getAllowFlight();
flying = player.isFlying();
speed = player.getFlySpeed();
player.setFlySpeed(0);
player.setFlying(false);
player.setAllowFlight(false);
}
@Override
public void undo() {
player.setAllowFlight(allow);
player.setFlying(flying);
player.setFlySpeed(speed);
}
static StepFactory create() {
return SetFlying::new;
}
}

View File

@ -0,0 +1,28 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
class SetGameMode extends PlayerStep {
private GameMode mode;
private SetGameMode(Player player) {
super(player);
}
@Override
public void run() {
mode = player.getGameMode();
player.setGameMode(GameMode.SURVIVAL);
}
@Override
public void undo() {
player.setGameMode(mode);
}
static StepFactory create() {
return SetGameMode::new;
}
}

View File

@ -0,0 +1,39 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
class SetHealth extends PlayerStep {
private static final double FULL_HEALTH = 20.0;
private static final int NORMAL_FIRE = -20;
private static final int NORMAL_AIR = 300;
private double health;
private int fire;
private int air;
private SetHealth(Player player) {
super(player);
}
@Override
public void run() {
health = player.getHealth();
fire = player.getFireTicks();
air = player.getRemainingAir();
player.setRemainingAir(NORMAL_AIR);
player.setFireTicks(NORMAL_FIRE);
player.setHealth(FULL_HEALTH);
}
@Override
public void undo() {
player.setHealth(health);
player.setFireTicks(fire);
player.setRemainingAir(air);
}
static StepFactory create() {
return SetHealth::new;
}
}

View File

@ -0,0 +1,39 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
class SetHunger extends PlayerStep {
private static final int FULL_FOOD = 20;
private static final float NORMAL_SATURATION = 5f;
private static final float NORMAL_EXHAUSTION = 0f;
private int food;
private float saturation;
private float exhaustion;
private SetHunger(Player player) {
super(player);
}
@Override
public void run() {
food = player.getFoodLevel();
saturation = player.getSaturation();
exhaustion = player.getExhaustion();
player.setExhaustion(NORMAL_EXHAUSTION);
player.setSaturation(NORMAL_SATURATION);
player.setFoodLevel(FULL_FOOD);
}
@Override
public void undo() {
player.setFoodLevel(food);
player.setSaturation(saturation);
player.setExhaustion(exhaustion);
}
static StepFactory create() {
return SetHunger::new;
}
}

View File

@ -0,0 +1,42 @@
package com.garbagemule.MobArena.steps;
import com.garbagemule.MobArena.time.Time;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
class SetPlayerTime extends PlayerStep {
private final Time time;
private SetPlayerTime(Player player, Time time) {
super(player);
this.time = time;
}
@Override
public void run() {
if (time != null) {
player.setPlayerTime(time.getTime(), false);
}
}
@Override
public void undo() {
if (time != null) {
player.resetPlayerTime();
}
}
static StepFactory create(ConfigurationSection settings) {
Time time = parseTime(settings);
return player -> new SetPlayerTime(player, time);
}
private static Time parseTime(ConfigurationSection settings) {
String value = settings.getString("player-time-in-arena", "world");
try {
return Time.valueOf(value.toLowerCase());
} catch (IllegalArgumentException e) {
return null;
}
}
}

View File

@ -0,0 +1,79 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Ocelot;
import org.bukkit.entity.Player;
import org.bukkit.entity.Tameable;
import org.bukkit.entity.Wolf;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
class SitPets extends PlayerStep {
private List<Entity> pets;
private SitPets(Player player) {
super(player);
pets = Collections.emptyList();
}
@Override
public void run() {
pets = findNearbyPets(player);
pets.forEach(SitPets.setSitting(true));
}
@Override
public void undo() {
pets.forEach(SitPets.setSitting(false));
}
static StepFactory create() {
return SitPets::new;
}
private static List<Entity> findNearbyPets(Player player) {
return player.getNearbyEntities(80, 40, 80).stream()
.filter(SitPets.isPetOwnedBy(player))
.filter(SitPets::isFollowing)
.collect(Collectors.toList());
}
private static Predicate<Entity> isPetOwnedBy(Player player) {
return entity -> {
switch (entity.getType()) {
case WOLF:
case OCELOT:
return player.equals(((Tameable) entity).getOwner());
}
return false;
};
}
private static boolean isFollowing(Entity entity) {
switch (entity.getType()) {
case WOLF:
return !((Wolf) entity).isSitting();
case OCELOT:
return !((Ocelot) entity).isSitting();
}
return false;
}
private static Consumer<Entity> setSitting(boolean sitting) {
return entity -> {
switch (entity.getType()) {
case WOLF:
((Wolf) entity).setSitting(sitting);
break;
case OCELOT:
((Ocelot) entity).setSitting(sitting);
break;
}
};
}
}

View File

@ -0,0 +1,28 @@
package com.garbagemule.MobArena.steps;
/**
* Steps are small, self-contained pieces of behavior.
* <p>
* The {@link #run()} method of a Step executes its behavior. This could be
* anything, but it usually involves a change in state, <em>somewhere</em>,
* and some Steps support the optional {@link #undo()} operation that attempts
* to revert the state back to what it was before the Step was executed.
* <p>
* Steps realize the Command role of the Command Pattern.
*
* @see StepFactory
*/
public interface Step {
/**
* Execute the behavior of this Step.
*/
void run();
/**
* Undo a previous execution of this Step (optional operation).
* <p>
* This method can be called without first invoking the {@link #run()}
* method. Doing so is silly.
*/
void undo();
}

View File

@ -0,0 +1,11 @@
package com.garbagemule.MobArena.steps;
import org.bukkit.entity.Player;
/**
* StepFactories create closures over {@link Player Players} and return
* {@link Step Steps} that operate on the given Player.
*/
public interface StepFactory {
Step create(Player player);
}

View File

@ -1,24 +0,0 @@
package com.garbagemule.MobArena.time;
import org.bukkit.entity.Player;
public interface TimeStrategy
{
/**
* Set the time enum used by setPlayerTime()
* @param time a Time enum
*/
void setTime(Time time);
/**
* Set the local client time for the player.
* @param p a player
*/
void setPlayerTime(Player p);
/**
* Reset the local client time for the player to the server time
* @param p a player
*/
void resetPlayerTime(Player p);
}

View File

@ -1,27 +0,0 @@
package com.garbagemule.MobArena.time;
import org.bukkit.entity.Player;
public class TimeStrategyLocked implements TimeStrategy
{
private Time time;
public TimeStrategyLocked(Time time) {
setTime(time);
}
@Override
public void setTime(Time time) {
this.time = time;
}
@Override
public void setPlayerTime(Player p) {
p.setPlayerTime(time.getTime(), false);
}
@Override
public void resetPlayerTime(Player p) {
p.resetPlayerTime();
}
}

View File

@ -1,15 +0,0 @@
package com.garbagemule.MobArena.time;
import org.bukkit.entity.Player;
public class TimeStrategyNull implements TimeStrategy
{
@Override
public void setTime(Time time) {}
@Override
public void setPlayerTime(Player p) {}
@Override
public void resetPlayerTime(Player p) {}
}

View File

@ -1,30 +0,0 @@
package com.garbagemule.MobArena.util;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.entity.Player;
public class Delays
{
public static void douse(MobArena plugin, final Player p, long delay) {
if (!plugin.isEnabled()) return;
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {
public void run() {
if (p.isOnline()) {
p.setFireTicks(0);
}
}
}, delay);
}
public static void revivePlayer(MobArena plugin, final Arena arena, final Player p) {
if (!plugin.isEnabled()) return;
plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() {
public void run() {
if (p.isOnline()) {
arena.revivePlayer(p);
}
}
});
}
}

View File

@ -1,9 +1,7 @@
package com.garbagemule.MobArena.util.inventory;
import com.garbagemule.MobArena.MobArena;
import com.garbagemule.MobArena.framework.Arena;
import org.bukkit.Material;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
@ -12,88 +10,39 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
public class InventoryManager
{
private File dir;
private Map<Player,ItemStack[]> items, armor;
private Map<Player, ItemStack[]> inventories;
public InventoryManager(Arena arena) {
this.dir = new File(arena.getPlugin().getDataFolder(), "inventories");
this.dir.mkdir();
this.items = new HashMap<>();
this.armor = new HashMap<>();
public InventoryManager() {
this.inventories = new HashMap<>();
}
public void storeInv(Player p) throws IOException {
// Avoid overwrites
if (items.containsKey(p)) return;
// Fetch the player's items and armor
ItemStack[] items = p.getInventory().getContents();
ItemStack[] armor = p.getInventory().getArmorContents();
// Store them in memory
this.items.put(p, items);
this.armor.put(p, armor);
// And on disk
File file = new File(dir, p.getName());
YamlConfiguration config = new YamlConfiguration();
config.set("items", items);
config.set("armor", armor);
config.save(file);
p.updateInventory();
public void put(Player p, ItemStack[] contents) {
inventories.put(p, contents);
}
public void restoreInv(Player p) throws IOException, InvalidConfigurationException {
// Try to grab the items from memory first
ItemStack[] items = this.items.get(p);
ItemStack[] armor = this.armor.get(p);
// If we can't restore from memory, restore from file
if (items == null || armor == null) {
File file = new File(dir, p.getName());
YamlConfiguration config = new YamlConfiguration();
config.load(file);
// Get the items and armor lists
List<?> itemsList = config.getList("items");
List<?> armorList = config.getList("armor");
// Turn the lists into arrays
items = itemsList.toArray(new ItemStack[itemsList.size()]);
armor = armorList.toArray(new ItemStack[armorList.size()]);
public void equip(Player p) {
ItemStack[] contents = inventories.get(p);
if (contents == null) {
return;
}
// Set the player inventory contents
p.getInventory().setContents(items);
p.getInventory().setArmorContents(armor);
p.getInventory().setContents(contents);
}
public void clearCache(Player p) {
items.remove(p);
armor.remove(p);
File file = new File(dir, p.getName());
if (file.exists()) {
file.delete();
}
public void remove(Player p) {
inventories.remove(p);
}
/**
* Clear a player's inventory completely.
* @param p a player
*/
public void clearInventory(Player p) {
public static void clearInventory(Player p) {
PlayerInventory inv = p.getInventory();
inv.clear();
inv.setHelmet(null);
@ -131,29 +80,23 @@ public class InventoryManager
public static boolean restoreFromFile(MobArena plugin, Player p) {
try {
// Grab the file and load the config
File dir = new File(plugin.getDataFolder(), "inventories");
File file = new File(dir, p.getName());
File inventories = new File(plugin.getDataFolder(), "inventories");
File file = new File(inventories, p.getUniqueId().toString());
if (!file.exists()) {
return false;
}
YamlConfiguration config = new YamlConfiguration();
config.load(file);
// Get the items and armor lists
List<?> itemsList = config.getList("items");
List<?> armorList = config.getList("armor");
ItemStack[] contents = config.getList("contents").toArray(new ItemStack[0]);
p.getInventory().setContents(contents);
// Turn the lists into arrays
ItemStack[] items = itemsList.toArray(new ItemStack[itemsList.size()]);
ItemStack[] armor = armorList.toArray(new ItemStack[armorList.size()]);
// Set the player inventory contents
p.getInventory().setContents(items);
p.getInventory().setArmorContents(armor);
// Delete files
file.delete();
return true;
} catch (Exception e) {
plugin.getLogger().log(Level.SEVERE, "Failed to restore inventory for " + p.getName(), e);
return false;
}
}