diff --git a/src/main/java/me/unurled/sacredrealms/sr/commands/CommandManager.java b/src/main/java/me/unurled/sacredrealms/sr/commands/CommandManager.java index 01894fe..d5eb1f2 100644 --- a/src/main/java/me/unurled/sacredrealms/sr/commands/CommandManager.java +++ b/src/main/java/me/unurled/sacredrealms/sr/commands/CommandManager.java @@ -5,6 +5,7 @@ import static me.unurled.sacredrealms.sr.utils.Logger.error; import java.lang.reflect.InvocationTargetException; import me.unurled.sacredrealms.sr.SR; import me.unurled.sacredrealms.sr.commands.admin.AttributeCommand; +import me.unurled.sacredrealms.sr.commands.admin.ClientBuildCommand; import me.unurled.sacredrealms.sr.commands.player.ResetAdventureCommand; import me.unurled.sacredrealms.sr.managers.Manager; import org.bukkit.command.PluginCommand; diff --git a/src/main/java/me/unurled/sacredrealms/sr/commands/admin/ClientBuildCommand.java b/src/main/java/me/unurled/sacredrealms/sr/commands/admin/ClientBuildCommand.java new file mode 100644 index 0000000..d046ed4 --- /dev/null +++ b/src/main/java/me/unurled/sacredrealms/sr/commands/admin/ClientBuildCommand.java @@ -0,0 +1,241 @@ +package me.unurled.sacredrealms.sr.commands.admin; + +import static me.unurled.sacredrealms.sr.utils.Logger.log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import me.unurled.sacredrealms.sr.components.clientbuild.ClientBuild; +import me.unurled.sacredrealms.sr.components.clientbuild.ClientBuildManager; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// TODO: test this command +public class ClientBuildCommand implements TabExecutor, Listener { + + private List nameCompletions = new ArrayList<>(); + private ClientBuildManager manager = null; + + /** + * Executes the given command, returning its success.
+ * If false is returned, then the "usage" plugin.yml entry for this command (if defined) will be + * sent to the player. + * + * @param sender Source of the command + * @param command Command which was executed + * @param label Alias of the command which was used + * @param args Passed command arguments + * @return true if a valid command, otherwise false + */ + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args) { + if (!sender.hasPermission("sr.clientbuild")) { + sender.sendMessage("You do not have permission to use this command."); + return true; + } + + if (!(sender instanceof Player p)) { + sender.sendMessage("You must be a player to use this command."); + return true; + } + + if (args.length == 0) { + sender.sendMessage("Usage: /clientbuild "); + return true; + } + + ClientBuildManager manager = ClientBuildManager.getInstance(ClientBuildManager.class); + if (args.length == 1) { // list + if (args[0].equalsIgnoreCase("list")) { + + List builds = manager.getBuilds(); + if (builds.isEmpty()) { + sender.sendMessage("There are no client builds."); + return true; + } + + sender.sendMessage("Client Builds:"); + for (ClientBuild build : builds) { + if (build != null) sender.sendMessage(build.getName()); + } + return true; + + } else { + sender.sendMessage("Usage: /clientbuild "); + return true; + } + } else if (args.length == 2) { + // create, delete, modify, save, display + if (args[0].equalsIgnoreCase("create")) { + String name = args[1].toLowerCase(); + if (manager.getBuildNames().contains(name)) { + sender.sendMessage("A client build with that name already exists."); + return true; + } + sender.sendMessage( + "Client build created, place blocks that you want to add to the Client" + + " Build and save after you are finished."); + manager.setTemporaryBlocks(p, new HashMap<>()); + manager.setTemporaryPreviousBlocks(p, new HashMap<>()); + return true; + } else if (args[0].equalsIgnoreCase("delete")) { + String name = args[1].toLowerCase(); + if (!manager.getBuildNames().contains(name)) { + sender.sendMessage("A client build with that name does not exist."); + return true; + } + manager.removeBuild(name); + sender.sendMessage("Client build deleted."); + return true; + } else if (args[0].equalsIgnoreCase("modify")) { + String name = args[1].toLowerCase(); + if (!manager.getBuildNames().contains(name)) { + sender.sendMessage("A client build with that name does not exist."); + return true; + } + sender.sendMessage("Modify the client build and save after you are finished."); + manager.setTemporaryBlocks(p, new HashMap<>()); + manager.setTemporaryPreviousBlocks(p, new HashMap<>()); + return true; + } else if (args[0].equalsIgnoreCase("save")) { + // save the client build to the list of builds so stopping the listener. + ClientBuild build = new ClientBuild(args[1].toLowerCase()); + if (!manager.hasTemporaryBlocks(p)) { + sender.sendMessage("You do not have any temporary blocks to save."); + return true; + } + HashMap blocks = manager.getTemporaryBlocks(p); + log("blocks: " + blocks.size()); + for (Entry block : blocks.entrySet()) { + build.addBlock(block.getKey(), block.getValue()); + } + + manager.addBuild(build); + + // remove blocks from the world + for (Entry block : manager.getTemporaryPreviousBlocks(p).entrySet()) { + block.getKey().getWorld().setBlockData(block.getKey(), block.getValue()); + // p.sendBlockChange(block.getLocation(), block.getBlockData()); + } + + manager.removeTemporaryBlocks(p); + manager.removeTemporaryPreviousBlocks(p); + + sender.sendMessage("Client builds saved."); + return true; + } else if (args[0].equalsIgnoreCase("display")) { + String name = args[1].toLowerCase(); + if (!manager.getBuildNames().contains(name)) { + sender.sendMessage("A client build with that name does not exist."); + return true; + } + ClientBuild build = manager.getBuild(name); + if (build == null) { + sender.sendMessage("An error occurred while displaying the client build."); + return true; + } + if (manager.getPlayerBuilds(p).contains(name)) { + manager.removeDisplayFromPlayer(p, build); + sender.sendMessage("Removed the client build display."); + } else { + manager.displayToPlayer(p, build); + sender.sendMessage("Displayed the client build."); + } + return true; + } else { + sender.sendMessage("Usage: /clientbuild "); + return true; + } + } else { + sender.sendMessage("Usage: /clientbuild "); + return true; + } + } + + @EventHandler + public void onBlockPlace(@NotNull BlockPlaceEvent e) { + Player p = e.getPlayer(); + if (manager == null) { + manager = ClientBuildManager.getInstance(ClientBuildManager.class); + } + if (manager.hasTemporaryBlocks(p)) { + log("block placed " + e.getBlock().getType()); + manager.getTemporaryBlocks(p).put(e.getBlock().getLocation(), e.getBlock().getBlockData()); + } + if (manager.hasTemporaryPreviousBlocks(p)) { + Block block = e.getBlock(); + // set blockstate to block + manager + .getTemporaryPreviousBlocks(p) + .put(block.getLocation(), e.getBlockReplacedState().getBlockData()); + } + } + + @EventHandler + public void onBlockBreak(@NotNull BlockBreakEvent e) { + Player p = e.getPlayer(); + if (manager == null) { + manager = ClientBuildManager.getInstance(ClientBuildManager.class); + } + if (manager.hasTemporaryBlocks(p)) { + manager.getTemporaryBlocks(p).remove(e.getBlock().getLocation()); + } + if (manager.hasTemporaryPreviousBlocks(p)) { + Block block = e.getBlock(); + // set blockstate to block + manager.getTemporaryPreviousBlocks(p).put(block.getLocation(), block.getBlockData()); + } + } + + /** + * Requests a list of possible completions for a command argument. + * + * @param sender Source of the command. For players tab-completing a command inside of a command + * block, this will be the player, not the command block. + * @param command Command which was executed + * @param label Alias of the command which was used + * @param args The arguments passed to the command, including final partial argument to be + * completed + * @return A List of possible completions for the final argument, or null to default to the + * command executor + */ + @Override + public @Nullable List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args) { + if (args.length == 1) { + return List.of("create", "delete", "list", "modify", "save", "display"); + } else if (args.length == 2) { + if (args[0].equalsIgnoreCase("delete") + || args[0].equalsIgnoreCase("modify") + || args[0].equalsIgnoreCase("display")) { + ClientBuildManager manager = ClientBuildManager.getInstance(ClientBuildManager.class); + List buildNames = manager.getBuildNames(); + if (nameCompletions.isEmpty() || !new HashSet<>(nameCompletions).containsAll(buildNames)) { + nameCompletions = buildNames; + } + return nameCompletions; + } + } + return null; + } +} diff --git a/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuild.java b/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuild.java new file mode 100644 index 0000000..0619c52 --- /dev/null +++ b/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuild.java @@ -0,0 +1,39 @@ +package me.unurled.sacredrealms.sr.components.clientbuild; + +import java.util.HashMap; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; +import org.jetbrains.annotations.NotNull; + +public class ClientBuild { + private final String name; + private final HashMap blocks = new HashMap<>(); + + public ClientBuild(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public HashMap getBlocks() { + return this.blocks; + } + + public void addBlock(Location loc, BlockData state) { + this.blocks.put(loc, state); + } + + public void removeBlock(@NotNull Location loc) { + this.blocks.remove(loc); + } + + public void clearBlocks() { + this.blocks.clear(); + } + + public boolean containsBlock(@NotNull Location loc, @NotNull BlockData data) { + return this.blocks.containsKey(loc) && this.blocks.get(loc).matches(data); + } +} diff --git a/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuildManager.java b/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuildManager.java new file mode 100644 index 0000000..5717d6a --- /dev/null +++ b/src/main/java/me/unurled/sacredrealms/sr/components/clientbuild/ClientBuildManager.java @@ -0,0 +1,241 @@ +package me.unurled.sacredrealms.sr.components.clientbuild; + +import static me.unurled.sacredrealms.sr.utils.Logger.error; +import static me.unurled.sacredrealms.sr.utils.Logger.log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.UUID; +import me.unurled.sacredrealms.sr.SR; +import me.unurled.sacredrealms.sr.commands.admin.ClientBuildCommand; +import me.unurled.sacredrealms.sr.data.DataHandler; +import me.unurled.sacredrealms.sr.data.DataManager; +import me.unurled.sacredrealms.sr.data.gson.ClientBuildDeserializer; +import me.unurled.sacredrealms.sr.data.gson.ClientBuildSerializer; +import me.unurled.sacredrealms.sr.managers.Manager; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ClientBuildManager extends Manager { + + private final List builds = new ArrayList<>(); + + /** a map of players that have ClientBuild displayed */ + private final HashMap> playerBlocks = new HashMap<>(); + + private final HashMap> temporaryBlocks = new HashMap<>(); + private final HashMap> temporaryPreviousBlocks = + new HashMap<>(); + + /** Load the manager */ + @Override + public void load() { + super.load(); + Bukkit.getPluginManager().registerEvents(new ClientBuildCommand(), SR.getInstance()); + } + + public List getBuilds() { + return this.builds; + } + + /** Save the data */ + @Override + public void saveData() { + // save client side builds to redis + + DataManager dataManager = DataManager.getInstance(DataManager.class); + DataHandler dh = dataManager.getDataHandler(); + + Gson gson = + new GsonBuilder() + .registerTypeAdapter(ClientBuild.class, new ClientBuildSerializer()) + .create(); + for (ClientBuild build : builds) { + String json = gson.toJson(build); + dh.set("sr.clientbuild." + build.getName(), json); + } + + for (Entry> entry : playerBlocks.entrySet()) { + for (String name : entry.getValue()) { + // save for player + dh.set("sr.players.clientbuild." + entry.getKey().getUniqueId() + "." + name, "true"); + } + } + } + + /** Load the data */ + @Override + public void loadData() { + // load client side builds from redis + DataManager dataManager = DataManager.getInstance(DataManager.class); + DataHandler dh = dataManager.getDataHandler(); + + if (dh == null) { + error("Failed to get DataHandler instance, can't load client builds."); + error("Retrying in 10 seconds."); + Bukkit.getScheduler().runTaskLaterAsynchronously(SR.getInstance(), this::loadData, 200L); + return; + } + List keys = dh.getKeysAll("sr.clientbuild"); + log("Loading " + keys.size() + " client builds."); + + Gson gson = + new GsonBuilder() + .registerTypeAdapter(ClientBuild.class, new ClientBuildDeserializer()) + .create(); + + for (String key : keys) { + String json = dh.get(key); + ClientBuild build = gson.fromJson(json, ClientBuild.class); + if (build == null) { + error("Failed to load client build " + key + " " + json); + } + builds.add(build); + } + } + + @EventHandler + public void onPlayerJoin(@NotNull PlayerJoinEvent e) { + DataHandler dh = DataManager.getInstance(DataManager.class).getDataHandler(); + Player p = e.getPlayer(); + + List names = + new ArrayList<>(dh.getKeysAll("sr.players.clientbuild." + p.getUniqueId())); + + playerBlocks.put(p, names); + + for (String name : names) { + ClientBuild build = getBuild(name); + if (build != null) { + displayToPlayer(p, build); + } + } + } + + @EventHandler + public void onPlayerQuit(@NotNull PlayerQuitEvent e) { + Player p = e.getPlayer(); + List names = playerBlocks.get(p); + if (names == null) { + return; + } + Bukkit.getScheduler() + .runTaskAsynchronously( + SR.getInstance(), + () -> { + for (String name : names) { + ClientBuild build = getBuild(name); + if (build != null) { + removeDisplayFromPlayer(p, build); + } + } + }); + + // save the player's displayed builds + + DataManager dataManager = DataManager.getInstance(DataManager.class); + DataHandler dh = dataManager.getDataHandler(); + + dh.remove("sr.players.clientbuild." + p.getUniqueId()); + + for (String name : names) { + dh.set("sr.players.clientbuild." + p.getUniqueId() + "." + name, "true"); + } + + playerBlocks.get(p).clear(); + + playerBlocks.remove(p); + } + + public List getBuildNames() { + List names = new ArrayList<>(); + for (ClientBuild build : builds) { + names.add(build.getName()); + } + return names; + } + + public void addBuild(ClientBuild build) { + builds.add(build); + } + + public void removeBuild(String name) { + for (ClientBuild build : builds) { + if (build.getName().equals(name)) { + builds.remove(build); + return; + } + } + } + + @Nullable + public ClientBuild getBuild(@NotNull String name) { + for (ClientBuild build : builds) { + if (build.getName().equals(name)) { + return build; + } + } + return null; + } + + public List getPlayerBuilds(Player p) { + return playerBlocks.get(p); + } + + public void displayToPlayer(@NotNull Player p, @NotNull ClientBuild build) { + playerBlocks.get(p).add(build.getName()); + for (Entry entry : build.getBlocks().entrySet()) { + p.sendBlockChange(entry.getKey(), entry.getValue()); + } + } + + public void removeDisplayFromPlayer(@NotNull Player p, @NotNull ClientBuild build) { + playerBlocks.get(p).remove(build.getName()); + for (Entry entry : build.getBlocks().entrySet()) { + p.sendBlockChange(entry.getKey(), entry.getKey().getBlock().getBlockData()); + } + } + + public HashMap getTemporaryBlocks(@NotNull Player p) { + return temporaryBlocks.get(p.getUniqueId()); + } + + public void setTemporaryBlocks(@NotNull Player p, HashMap blocks) { + temporaryBlocks.put(p.getUniqueId(), blocks); + } + + public boolean hasTemporaryBlocks(@NotNull Player p) { + return temporaryBlocks.containsKey(p.getUniqueId()); + } + + public void removeTemporaryBlocks(@NotNull Player p) { + temporaryBlocks.remove(p.getUniqueId()); + } + + public HashMap getTemporaryPreviousBlocks(@NotNull Player p) { + return temporaryPreviousBlocks.get(p.getUniqueId()); + } + + public void setTemporaryPreviousBlocks(@NotNull Player p, HashMap blocks) { + temporaryPreviousBlocks.put(p.getUniqueId(), blocks); + } + + public boolean hasTemporaryPreviousBlocks(@NotNull Player p) { + return temporaryPreviousBlocks.containsKey(p.getUniqueId()); + } + + public void removeTemporaryPreviousBlocks(@NotNull Player p) { + temporaryPreviousBlocks.remove(p.getUniqueId()); + } +} diff --git a/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildDeserializer.java b/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildDeserializer.java new file mode 100644 index 0000000..93ff96b --- /dev/null +++ b/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildDeserializer.java @@ -0,0 +1,65 @@ +package me.unurled.sacredrealms.sr.data.gson; + +import static me.unurled.sacredrealms.sr.utils.Logger.error; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import me.unurled.sacredrealms.sr.components.clientbuild.ClientBuild; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.data.BlockData; + +public class ClientBuildDeserializer implements JsonDeserializer { + + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + * + *

In the implementation of this call-back method, you should consider invoking {@link + * JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects for any + * non-trivial field of the returned object. However, you should never invoke it on the same type + * passing {@code json} since that will cause an infinite loop (Gson will call your call-back + * method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @param context + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * @throws JsonParseException if json is not in the expected format of {@code typeofT} + */ + @Override + public ClientBuild deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String[] data = json.getAsString().split(";"); + ClientBuild build = new ClientBuild(data[0]); + for (String block : data[1].split(",")) { + String[] spl = block.replace("{", "").replace("}", "").split("\\|"); + World world = Bukkit.getWorld(spl[0]); + if (world == null) { + error("World " + spl[0] + " does not exist"); + continue; + } + Location loc; + BlockData bData; + try { + loc = + new Location( + world, + Integer.parseInt(spl[1]), + Integer.parseInt(spl[2]), + Integer.parseInt(spl[3])); + + bData = Bukkit.createBlockData(spl[4]); + } catch (NumberFormatException e) { + error("Invalid block data: " + block); + continue; + } + build.addBlock(loc, bData); + } + return build; + } +} diff --git a/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildSerializer.java b/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildSerializer.java new file mode 100644 index 0000000..d1f72d7 --- /dev/null +++ b/src/main/java/me/unurled/sacredrealms/sr/data/gson/ClientBuildSerializer.java @@ -0,0 +1,50 @@ +package me.unurled.sacredrealms.sr.data.gson; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.util.Map.Entry; +import me.unurled.sacredrealms.sr.components.clientbuild.ClientBuild; +import org.bukkit.Location; +import org.bukkit.block.data.BlockData; + +public class ClientBuildSerializer implements JsonSerializer { + + /** + * Gson invokes this call-back method during serialization when it encounters a field of the + * specified type. + * + *

In the implementation of this call-back method, you should consider invoking {@link + * JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any + * non-trivial field of the {@code src} object. However, you should never invoke it on the {@code + * src} object itself since that will cause an infinite loop (Gson will call your call-back method + * again). + * + * @param src the object that needs to be converted to Json. + * @param typeOfSrc the actual type (fully genericized version) of the source object. + * @param context + * @return a JsonElement corresponding to the specified object. + */ + @Override + public JsonElement serialize(ClientBuild src, Type typeOfSrc, JsonSerializationContext context) { + StringBuilder s = new StringBuilder(); + s.append(src.getName()); + s.append(";"); + for (Entry entry : src.getBlocks().entrySet()) { + s.append("{"); + s.append(entry.getKey().getWorld().getName()); + s.append("|"); + s.append(entry.getKey().getBlockX()); + s.append("|"); + s.append(entry.getKey().getBlockY()); + s.append("|"); + s.append(entry.getKey().getBlockZ()); + s.append("|"); + s.append(entry.getValue().getAsString()); + s.append("},"); + } + return new JsonPrimitive(s.toString()); + } +} diff --git a/src/main/java/me/unurled/sacredrealms/sr/managers/Managers.java b/src/main/java/me/unurled/sacredrealms/sr/managers/Managers.java index 80241ee..bc7b61d 100644 --- a/src/main/java/me/unurled/sacredrealms/sr/managers/Managers.java +++ b/src/main/java/me/unurled/sacredrealms/sr/managers/Managers.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import me.unurled.sacredrealms.sr.SR; import me.unurled.sacredrealms.sr.commands.CommandManager; +import me.unurled.sacredrealms.sr.components.clientbuild.ClientBuildManager; import me.unurled.sacredrealms.sr.components.combat.CombatManager; import me.unurled.sacredrealms.sr.components.entity.EntityManager; import me.unurled.sacredrealms.sr.components.item.ItemManager; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 28b48b2..2ae70cd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -17,10 +17,16 @@ permissions: sr.attributes: default: op description: When the player has permission for the command /attributes set + sr.clientbuild: + description: When the player has permission for the command /clientbuild sr.resetadventure: default: op description: When the player has permission for the command /resetadventure commands: attributes: - description: Set the attributes of the player. \ No newline at end of file + description: Set the attributes of the player. + clientbuild: + description: Set the client build of the player. + resetadventure: + description: Reset the adventure of the player \ No newline at end of file