diff --git a/src/main/java/tk/alex3025/headstones/Headstones.java b/src/main/java/tk/alex3025/headstones/Headstones.java new file mode 100644 index 0000000..73dbb71 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/Headstones.java @@ -0,0 +1,73 @@ +package tk.alex3025.headstones; + +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import tk.alex3025.headstones.commands.HeadstonesCommand; +import tk.alex3025.headstones.commands.subcommands.ClearDatabaseCommand; +import tk.alex3025.headstones.commands.subcommands.ReloadConfigCommand; +import tk.alex3025.headstones.listeners.BlockBreakListener; +import tk.alex3025.headstones.listeners.PlayerDeathListener; +import tk.alex3025.headstones.listeners.RightClickListener; +import tk.alex3025.headstones.utils.ConfigFile; + +public final class Headstones extends JavaPlugin { + + private static Headstones instance; + + private ConfigFile config; + private ConfigFile messages; + private ConfigFile database; + + @Override + public void onEnable() { + instance = this; + + this.loadConfigurationFiles(); + this.registerListeners(); + this.registerCommands(); + } + + @Override + public void onDisable() { + // Plugin shutdown logic + } + + private void loadConfigurationFiles() { + this.config = new ConfigFile(this,"config.yml"); + this.messages = new ConfigFile(this,"messages.yml"); + this.database = new ConfigFile(this,"database.yml"); + } + + private void registerListeners() { + new PlayerDeathListener(); + new BlockBreakListener(); + new RightClickListener(); + } + + private void registerCommands() { + new HeadstonesCommand(); + + // Subcommands + new ClearDatabaseCommand(); + new ReloadConfigCommand(); + } + + public static Headstones getInstance() { + return instance; + } + + // Config getters + @Override + public @NotNull ConfigFile getConfig() { + return config; + } + + public ConfigFile getMessages() { + return messages; + } + + public ConfigFile getDatabase() { + return database; + } + +} diff --git a/src/main/java/tk/alex3025/headstones/commands/HeadstonesCommand.java b/src/main/java/tk/alex3025/headstones/commands/HeadstonesCommand.java new file mode 100644 index 0000000..47dd0d1 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/commands/HeadstonesCommand.java @@ -0,0 +1,74 @@ +package tk.alex3025.headstones.commands; + +import org.bukkit.command.*; +import org.bukkit.entity.Player; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tk.alex3025.headstones.Headstones; +import tk.alex3025.headstones.commands.subcommands.SubcommandBase; +import tk.alex3025.headstones.utils.Message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class HeadstonesCommand implements CommandExecutor, TabCompleter { + + public HeadstonesCommand() { + PluginCommand command = Headstones.getInstance().getCommand("headstones"); + if (command != null) { + command.setExecutor(this); + command.setTabCompleter(this); + } + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) { + if (args.length == 0) { + String prefix = Message.getTranslation("prefix"); + Message.sendMessage(sender, "&8&m+----------+&r " + prefix + " &8&m+----------+"); + sender.sendMessage(""); + Message.sendMessage(sender, " &7Author: &balex3025"); + sender.sendMessage(""); + Message.sendMessage(sender, " &7Version: &b" + Headstones.getInstance().getDescription().getVersion()); + sender.sendMessage(""); + Message.sendMessage(sender, "&8&m+----------+&r " + prefix + " &8&m+----------+"); + } else { + SubcommandBase subcommand = SubcommandBase.getSubcommand(args[0]); + if (subcommand != null) { + // Remove the subcommand name from the args + String[] newArgs = new String[args.length - 1]; + System.arraycopy(args, 1, newArgs, 0, args.length - 1); + + if (subcommand.isPlayersOnly() && !(sender instanceof Player)) { + new Message(sender).translation("player-only").send(); + return true; + } + + if (!subcommand.hasPermission(sender)) { + new Message(sender).translation("no-permissions").send(); + return true; + } + + return subcommand.onCommand(sender, newArgs); + } + new Message(sender).translation("unknown-subcommand").send(); + } + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String @NotNull [] args) { + List matches = new ArrayList<>(); + if (args.length == 1) { + for (SubcommandBase subcommand : SubcommandBase.getRegisteredSubcommands()) + if (subcommand.hasPermission(sender)) + matches.add(subcommand.getName()); + + return StringUtil.copyPartialMatches(args[0], matches, new ArrayList<>()); + } + return Collections.emptyList(); + } + +} diff --git a/src/main/java/tk/alex3025/headstones/commands/subcommands/ClearDatabaseCommand.java b/src/main/java/tk/alex3025/headstones/commands/subcommands/ClearDatabaseCommand.java new file mode 100644 index 0000000..6b749b6 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/commands/subcommands/ClearDatabaseCommand.java @@ -0,0 +1,47 @@ +package tk.alex3025.headstones.commands.subcommands; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tk.alex3025.headstones.Headstones; +import tk.alex3025.headstones.utils.ConfigFile; +import tk.alex3025.headstones.utils.Message; + +import java.util.HashMap; +import java.util.Map; + +public class ClearDatabaseCommand extends SubcommandBase { + + private final Map waitingConfirmPlayers = new HashMap<>();; + + public ClearDatabaseCommand() { + super("cleardb", "headstones.cleardb"); + + // Clear waiting players after 10 seconds + Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(Headstones.getInstance(), () -> { + for (Map.Entry entry : this.waitingConfirmPlayers.entrySet()) + if (System.currentTimeMillis() - entry.getValue() > 10000) + this.waitingConfirmPlayers.remove(entry.getKey()); + }, 0, 40); + } + + @Override + public boolean onCommand(CommandSender sender, String[] args) { + Player player = (Player) sender; + + if (this.waitingConfirmPlayers.containsKey(player.getUniqueId().toString())) { + this.waitingConfirmPlayers.remove(player.getUniqueId().toString()); + + ConfigFile headstonesFile = Headstones.getInstance().getDatabase(); + headstonesFile.set("headstones", new HashMap<>()); + headstonesFile.save(); + + Message.sendPrefixedMessage(player, "&aDatabase cleared!"); + } else { + this.waitingConfirmPlayers.put(player.getUniqueId().toString(), System.currentTimeMillis()); + Message.sendPrefixedMessage(player, "&eAre you sure you want to clear the database? &c&lTHIS WILL MAKE ALL EXISTING HEADSTONES USELESS. &7Type &f/headstones cleardb &7again to confirm."); + } + return true; + } + +} diff --git a/src/main/java/tk/alex3025/headstones/commands/subcommands/ReloadConfigCommand.java b/src/main/java/tk/alex3025/headstones/commands/subcommands/ReloadConfigCommand.java new file mode 100644 index 0000000..987f394 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/commands/subcommands/ReloadConfigCommand.java @@ -0,0 +1,20 @@ +package tk.alex3025.headstones.commands.subcommands; + +import org.bukkit.command.CommandSender; +import tk.alex3025.headstones.utils.ConfigFile; +import tk.alex3025.headstones.utils.Message; + +public class ReloadConfigCommand { + + public ReloadConfigCommand() { + new SubcommandBase("reload", "headstones.reload") { + @Override + public boolean onCommand(CommandSender sender, String[] args) { + ConfigFile.reloadAll(); + Message.sendPrefixedMessage(sender, "&aSuccessfully reloaded all configuration files!"); + return true; + } + }; + } + +} diff --git a/src/main/java/tk/alex3025/headstones/commands/subcommands/SubcommandBase.java b/src/main/java/tk/alex3025/headstones/commands/subcommands/SubcommandBase.java new file mode 100644 index 0000000..5cc641e --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/commands/subcommands/SubcommandBase.java @@ -0,0 +1,66 @@ +package tk.alex3025.headstones.commands.subcommands; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public abstract class SubcommandBase { + + private static final List registeredSubcommands = new ArrayList<>(); + + private final String name; + private String permission = null; + private boolean playersOnly = false; + + public SubcommandBase(@NotNull String name) { + this.name = name; + this.registerSubcommand(); + } + + public SubcommandBase(@NotNull String name, String permission) { + this(name); + this.permission = permission; + } + + public SubcommandBase(@NotNull String name, String permission, boolean playersOnly) { + this(name, permission); + this.playersOnly = playersOnly; + } + + public abstract boolean onCommand(CommandSender sender, String[] args); + + public boolean hasPermission(CommandSender sender) { + return this.getPermission() == null || (this.getPermission() != null && sender.hasPermission(this.getPermission())); + } + + public String getName() { + return this.name; + } + + public String getPermission() { + return this.permission; + } + + public boolean isPlayersOnly() { + return this.playersOnly; + } + + public void registerSubcommand() { + registeredSubcommands.add(this); + } + + public static List getRegisteredSubcommands() { + return registeredSubcommands; + } + + public static @Nullable SubcommandBase getSubcommand(String subcommand) { + for (SubcommandBase registered : registeredSubcommands) + if (registered.getName().equals(subcommand)) + return registered; + return null; + } + +} diff --git a/src/main/java/tk/alex3025/headstones/listeners/BlockBreakListener.java b/src/main/java/tk/alex3025/headstones/listeners/BlockBreakListener.java new file mode 100644 index 0000000..0587713 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/listeners/BlockBreakListener.java @@ -0,0 +1,24 @@ +package tk.alex3025.headstones.listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockBreakEvent; +import org.jetbrains.annotations.NotNull; +import tk.alex3025.headstones.utils.Headstone; +import tk.alex3025.headstones.utils.Message; + +public class BlockBreakListener extends ListenerBase { + + @EventHandler + public void onBlockBreak(@NotNull BlockBreakEvent event) { + Headstone headstone = Headstone.fromBlock(event.getBlock()); + + if (headstone != null) + if (headstone.isOwner(event.getPlayer())) + headstone.onBreak(event); + else { + event.setCancelled(true); + new Message(event.getPlayer()).translation("cannot-break-others").prefixed(false).send(); + } + } + +} diff --git a/src/main/java/tk/alex3025/headstones/listeners/ListenerBase.java b/src/main/java/tk/alex3025/headstones/listeners/ListenerBase.java new file mode 100644 index 0000000..95aeaac --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/listeners/ListenerBase.java @@ -0,0 +1,13 @@ +package tk.alex3025.headstones.listeners; + +import org.bukkit.Bukkit; +import org.bukkit.event.Listener; +import tk.alex3025.headstones.Headstones; + +public abstract class ListenerBase implements Listener { + + public ListenerBase() { + Bukkit.getPluginManager().registerEvents(this, Headstones.getInstance()); + } + +} diff --git a/src/main/java/tk/alex3025/headstones/listeners/PlayerDeathListener.java b/src/main/java/tk/alex3025/headstones/listeners/PlayerDeathListener.java new file mode 100644 index 0000000..363bdd7 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/listeners/PlayerDeathListener.java @@ -0,0 +1,28 @@ +package tk.alex3025.headstones.listeners; + +import com.bgsoftware.wildloaders.api.npc.ChunkLoaderNPC; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.jetbrains.annotations.NotNull; +import tk.alex3025.headstones.utils.ExperienceManager; +import tk.alex3025.headstones.utils.Headstone; + +public class PlayerDeathListener extends ListenerBase { + + @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) + public void onPlayerDeath(@NotNull PlayerDeathEvent event) { + Player player = event.getPlayer(); + + // Check if the player is a chunk loader from the WildLoaders plugin + if (player instanceof ChunkLoaderNPC) return; + + boolean keepExperience = !event.getKeepLevel() && player.hasPermission("headstones.keep-experience"); + boolean keepInventory = !event.getKeepInventory() && player.hasPermission("headstones.keep-inventory"); + + if (!(keepExperience && keepInventory) || !player.getInventory().isEmpty() || ExperienceManager.getExperience(player) != 0) + new Headstone(player).onPlayerDeath(event, keepExperience, keepInventory); + } + +} diff --git a/src/main/java/tk/alex3025/headstones/listeners/RightClickListener.java b/src/main/java/tk/alex3025/headstones/listeners/RightClickListener.java new file mode 100644 index 0000000..a422f3a --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/listeners/RightClickListener.java @@ -0,0 +1,29 @@ +package tk.alex3025.headstones.listeners; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerInteractEvent; +import org.jetbrains.annotations.NotNull; +import tk.alex3025.headstones.Headstones; +import tk.alex3025.headstones.utils.Headstone; +import tk.alex3025.headstones.utils.Message; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; + +public class RightClickListener extends ListenerBase { + + @EventHandler + public void onPlayerInteract(@NotNull PlayerInteractEvent event) { + if (event.getClickedBlock() != null && event.getAction().isRightClick() && event.getHand().name().equals("HAND")) { + Headstone headstone = Headstone.fromBlock(event.getClickedBlock()); + + if (headstone != null) + new Message(event.getPlayer(), new HashMap<>() {{ + put("username", headstone.getOwner().getName()); + put("datetime", new SimpleDateFormat(Headstones.getInstance().getConfig().getString("date-format")).format(new Date(headstone.getTimestamp()))); + }}).translation("headstone-info").send(); + } + } + +} diff --git a/src/main/java/tk/alex3025/headstones/utils/ConfigFile.java b/src/main/java/tk/alex3025/headstones/utils/ConfigFile.java new file mode 100644 index 0000000..32a38db --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/utils/ConfigFile.java @@ -0,0 +1,70 @@ +package tk.alex3025.headstones.utils; + +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import tk.alex3025.headstones.Headstones; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ConfigFile extends YamlConfiguration { + + private final static List CONFIGS = new ArrayList<>(); + + private final Headstones instance; + private File file; + + public ConfigFile(Headstones instance, String filename) { + this.instance = instance; + + try { + this.createOrLoadConfig(filename); + } catch (IOException | InvalidConfigurationException e) { + e.printStackTrace(); + } + + CONFIGS.add(this); + } + + public void createOrLoadConfig(String filename) throws IOException, InvalidConfigurationException { + this.file = new File(this.instance.getDataFolder(), filename); + + if (!this.file.exists()) { + this.file.getParentFile().mkdirs(); + instance.saveResource(filename, false); + } + + this.load(this.file); + } + + public void save() { + try { + this.save(this.file); + this.reload(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void reload() { + try { + this.load(this.file); + } catch (InvalidConfigurationException e) { + e.printStackTrace(); + } catch (IOException ignored) { + try { + this.createOrLoadConfig(this.file.getName()); + } catch (IOException | InvalidConfigurationException e) { + e.printStackTrace(); + } + } + } + + public static void reloadAll() { + for (ConfigFile config : CONFIGS) + config.reload(); + } + +} diff --git a/src/main/java/tk/alex3025/headstones/utils/ExperienceManager.java b/src/main/java/tk/alex3025/headstones/utils/ExperienceManager.java new file mode 100644 index 0000000..b23ac5f --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/utils/ExperienceManager.java @@ -0,0 +1,48 @@ +package tk.alex3025.headstones.utils; + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class ExperienceManager { + + public static int getExperience(int level) { + int xp = 0; + + if (level >= 0 && level <= 15) + xp = (int) Math.round(Math.pow(level, 2) + 6 * level); + else if (level > 15 && level <= 30) + xp = (int) Math.round((2.5 * Math.pow(level, 2) - 40.5 * level + 360)); + else if (level > 30) + xp = (int) Math.round(((4.5 * Math.pow(level, 2) - 162.5 * level + 2220))); + + return xp; + } + + public static int getExperience(@NotNull Player player) { + return Math.round(player.getExp() * player.getExpToLevel()) + getExperience(player.getLevel()); + } + + public static void setExperience(Player player, int amount) { + float a = 0, b = 0, c = -amount; + + if (amount > getExperience(0) && amount <= getExperience(15)) { + a = 1; + b = 6; + } else if (amount > getExperience(15) && amount <= getExperience(30)) { + a = 2.5f; + b = -40.5f; + c += 360; + } else if (amount > getExperience(30)) { + a = 4.5f; + b = -162.5f; + c += 2220; + } + + int level = (int) Math.floor((-b + Math.sqrt(Math.pow(b, 2) - (4 * a * c))) / (2 * a)); + int xp = amount - getExperience(level); + + player.setLevel(level); + player.setExp(0); + player.giveExp(xp); + } +} diff --git a/src/main/java/tk/alex3025/headstones/utils/Headstone.java b/src/main/java/tk/alex3025/headstones/utils/Headstone.java new file mode 100644 index 0000000..cf3d044 --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/utils/Headstone.java @@ -0,0 +1,238 @@ +package tk.alex3025.headstones.utils; + +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Skull; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Rotatable; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tk.alex3025.headstones.Headstones; + +import java.io.IOException; +import java.time.Instant; +import java.util.*; + +public class Headstone { + + private final String uuid; + private final OfflinePlayer owner; + private final Location location; + private final long timestamp; + private final int experience; + private final ItemStack[] inventory; + + public Headstone(@NotNull Player player) { + this.uuid = UUID.randomUUID().toString(); + + this.owner = player; + this.location = player.getLocation(); + this.timestamp = Instant.now().toEpochMilli(); + + this.experience = ExperienceManager.getExperience(player); + this.inventory = player.getInventory().getContents(); + } + + private Headstone(String uuid, OfflinePlayer player, Location location, long timestamp, int experience, ItemStack[] inventory) { + this.uuid = uuid; + + this.owner = player; + this.location = location; + this.timestamp = timestamp; + + this.experience = experience; + this.inventory = inventory; + } + + public static @Nullable Headstone fromUUID(String uuid) { + ConfigurationSection headstones = Headstone.getHeadstonesData().getConfigurationSection("headstones"); + if (headstones != null) { + ConfigurationSection hs = headstones.getConfigurationSection(uuid); + if (hs != null) { + Location location = new Location(Bukkit.getWorld(hs.getString("world")), hs.getDouble("x"), hs.getDouble("y"), hs.getDouble("z")); + + ItemStack[] inventory = null; + if (hs.getString("inventory") != null) + try { + inventory = InventorySerializer.deserialize(hs.getString("inventory")); + } catch (IOException e) { + e.printStackTrace(); + } + + OfflinePlayer owner = Bukkit.getOfflinePlayer(UUID.fromString(hs.getString("owner"))); + return new Headstone(uuid, owner, location, hs.getLong("timestamp"), hs.getInt("experience", 0), inventory); + } + } + return null; + } + + public static @Nullable Headstone fromLocation(Location location) { + ConfigurationSection headstones = Headstone.getHeadstonesData().getConfigurationSection("headstones"); + + if (headstones != null) + for (String uuid : headstones.getKeys(false)) { + Headstone hs = Headstone.fromUUID(uuid); + if (hs != null) + if (location.equals(hs.getLocation())) + return hs; + } + return null; + } + + public static @Nullable Headstone fromBlock(@NotNull Block block) { + return block.getType().equals(Material.PLAYER_HEAD) ? Headstone.fromLocation(block.getLocation()) : null; + } + + public void onPlayerDeath(PlayerDeathEvent event, boolean keepExperience, boolean keepInventory) { + Location skullLocation = this.createPlayerSkull(); + + if (skullLocation != null) { + // Disable drops + event.getDrops().clear(); + event.setShouldDropExperience(false); + + this.savePlayerData(skullLocation, keepExperience, keepInventory); + } + } + + public void onBreak(@NotNull BlockBreakEvent event) { + Player player = event.getPlayer(); + + this.restorePlayerInventory(player); + this.deletePlayerData(); + + event.setDropItems(Headstones.getInstance().getConfig().getBoolean("drop-player-head")); + + player.spawnParticle(Particle.REDSTONE, this.location.add(0.5, 0.2, 0.5), 10, 0.2, 0.1, 0.2, new Particle.DustOptions(Color.fromRGB(255, 255, 255), 1.5F)); + player.playSound(this.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 2.0F, 1.0F); + + new Message(player).translation("headstone-broken").prefixed(false).send(); + } + + private void savePlayerData(@NotNull Location skullLocation, boolean keepExperience, boolean keepInventory) { + ConfigFile headstonesFile = Headstone.getHeadstonesData(); + ConfigurationSection hs = headstonesFile.createSection("headstones." + this.uuid); + + hs.set("owner", this.owner.getUniqueId().toString()); + + hs.set("x", (int) Math.floor(skullLocation.getX())); + hs.set("y", (int) Math.floor(skullLocation.getY())); + hs.set("z", (int) Math.floor(skullLocation.getZ())); + hs.set("world", skullLocation.getWorld().getName()); + + hs.set("timestamp", Instant.now().toEpochMilli()); + + if (keepExperience) + hs.set("experience", this.experience); + + if (keepInventory) + hs.set("inventory", InventorySerializer.serialize(this.inventory)); + + headstonesFile.save(); + } + + private void deletePlayerData() { + ConfigFile headstonesFile = Headstone.getHeadstonesData(); + ConfigurationSection headstones = headstonesFile.getConfigurationSection("headstones"); + + if (headstones != null) + headstones.set(this.uuid, null); + + headstonesFile.save(); + } + + private void restorePlayerInventory(Player player) { + ExperienceManager.setExperience(player, this.experience); + + if (this.inventory != null) + for (int i = 0, size = this.inventory.length; i < size; i++) + if (this.inventory[i] != null) { + PlayerInventory playerInventory = player.getInventory(); + if (playerInventory.getItem(i) == null) + playerInventory.setItem(i, this.inventory[i]); + else { + HashMap drops = playerInventory.addItem(this.inventory[i]); + for (ItemStack item : drops.values()) + player.getWorld().dropItem(this.location, item); + } + } + } + + private @Nullable Block checkForSafeBlock() { + int playerX = this.location.getBlockX(); + int playerY = this.location.getBlockY(); + int playerZ = this.location.getBlockZ(); + + int radius = 0; + + for (int x = playerX - radius; x <= playerX + radius; x++) { + for (int y = playerY - radius; y <= playerY + radius; y++) + for (int z = playerZ - radius; z <= playerZ + radius; z++) { + Block block = this.location.getWorld().getBlockAt(x,y,z); + if (block.getType().isEmpty()) + return block; + } + + if (radius <= 5) + radius++; + } + + return null; + } + + private @Nullable Location createPlayerSkull() { + Block block = this.checkForSafeBlock(); + + if (block != null) { + block.setType(Material.PLAYER_HEAD); + + if (block.getState() instanceof Skull skull) { + skull.setOwningPlayer(this.owner); + + BlockData data = skull.getBlockData(); + + List faces = new ArrayList<>(List.of(BlockFace.values())); + // Remove invalid faces + faces.remove(BlockFace.UP); + faces.remove(BlockFace.DOWN); + faces.remove(BlockFace.SELF); + + ((Rotatable) data).setRotation(faces.get(new Random().nextInt(faces.size()))); + + skull.setBlockData(data); + skull.update(); + + return block.getLocation(); + } + } + return null; + } + + public boolean isOwner(@NotNull Player player) { + return player.getUniqueId().equals(this.owner.getUniqueId()); + } + + public OfflinePlayer getOwner() { + return this.owner; + } + + public Location getLocation() { + return this.location; + } + + public long getTimestamp() { + return this.timestamp; + } + + private static ConfigFile getHeadstonesData() { + return Headstones.getInstance().getDatabase(); + } + +} diff --git a/src/main/java/tk/alex3025/headstones/utils/InventorySerializer.java b/src/main/java/tk/alex3025/headstones/utils/InventorySerializer.java new file mode 100644 index 0000000..35ea32f --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/utils/InventorySerializer.java @@ -0,0 +1,58 @@ +package tk.alex3025.headstones.utils; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.jetbrains.annotations.NotNull; +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class InventorySerializer { + + public static @NotNull String serialize(ItemStack[] inventoryContents) throws IllegalStateException { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); + + dataOutput.writeInt(inventoryContents.length); + + for (ItemStack item : inventoryContents) + if (item != null) + dataOutput.writeObject(item.serializeAsBytes()); + else + dataOutput.writeObject(null); + + dataOutput.close(); + return Base64Coder.encodeLines(outputStream.toByteArray()); + } catch (Exception e) { + throw new IllegalStateException("Unable to save item stacks.", e); + } + } + + public static ItemStack @NotNull [] deserialize(String serializedInventory) throws IOException { + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializedInventory)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); + + ItemStack[] items = new ItemStack[dataInput.readInt()]; + + for (int i = 0; i < items.length; i++) { + byte[] stack = (byte[]) dataInput.readObject(); + + if (stack != null) + items[i] = ItemStack.deserializeBytes(stack); + else + items[i] = null; + } + + dataInput.close(); + return items; + } catch (ClassNotFoundException e) { + throw new IOException("Unable to decode class type.", e); + } + } + +} diff --git a/src/main/java/tk/alex3025/headstones/utils/Message.java b/src/main/java/tk/alex3025/headstones/utils/Message.java new file mode 100644 index 0000000..c1d703a --- /dev/null +++ b/src/main/java/tk/alex3025/headstones/utils/Message.java @@ -0,0 +1,68 @@ +package tk.alex3025.headstones.utils; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import tk.alex3025.headstones.Headstones; + +import java.util.Map; + +public class Message { + + private String rawMessage; + private final CommandSender sender; + private Map placeholders; + + private boolean prefixed = true; + + public Message(CommandSender sender) { + this.sender = sender; + } + + public Message(CommandSender sender, Map placeholders) { + this.sender = sender; + this.placeholders = placeholders; + } + + public Message text(String message) { + this.rawMessage = message; + return this; + } + + public Message translation(String key) { + return this.text(Headstones.getInstance().getMessages().getString(key)); + } + + public Message prefixed(boolean prefixed) { + this.prefixed = prefixed; + return this; + } + + public void send() { + if (this.rawMessage != null && !this.rawMessage.isEmpty()) { + // Format placeholders + if (this.placeholders != null) + for (Map.Entry entry : this.placeholders.entrySet()) + this.rawMessage = this.rawMessage.replace("%" + entry.getKey() + "%", entry.getValue()); + + if (this.prefixed) + Message.sendPrefixedMessage(this.sender, this.rawMessage); + else + Message.sendMessage(this.sender, this.rawMessage); + } + } + + public static void sendMessage(@NotNull CommandSender sender, String message) { + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', message)); + } + + public static void sendPrefixedMessage(CommandSender sender, String message) { + String prefix = Headstones.getInstance().getMessages().getString("prefix"); + Message.sendMessage(sender, prefix + " " + message); + } + + public static String getTranslation(String key) { + return Headstones.getInstance().getMessages().getString(key); + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..9e7e3da --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,12 @@ +# ===================================== # +# # +# Headstones v${project.version} - by alex3025 # +# PLUGIN CONFIGURATION # +# # +# ===================================== # + +# The player head will be dropped on break? +drop-player-head: false + +# The date format to use in the headstone info message. +date-format: 'dd/MM/yyyy HH:mm' diff --git a/src/main/resources/database.yml b/src/main/resources/database.yml new file mode 100644 index 0000000..c4384df --- /dev/null +++ b/src/main/resources/database.yml @@ -0,0 +1,6 @@ +# -------------------------- # +# Headstones Database File # +# /!\ DO NOT EDIT /!\ # +# -------------------------- # + +headstones: {} diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..779451a --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,25 @@ +# ===================================== # +# # +# Headstones v${project.version} - by alex3025 # +# MESSAGES CUSTOMIZATION # +# # +# ===================================== # + +prefix: '&8[&bHeadstones&8]' + +# You can set any message to an empty string to disable it. + +# These messages won't have the prefix. +no-permissions: '&cYou don''t have the permission to use this command!' +cannot-break-others: '&cYou cannot break other players'' headstones!' + +headstone-broken: '&aYou broke your headstone and got all your items back!' + +# These messages will have the prefix. +unknown-subcommand: '&cUnknown subcommand!' +player-only: '&cThis command can be executed by players only!' +headstone-info: '&f%username%&7 died here on &f%datetime%&7' + +# These messages will appear on the action bar. +inventory-full: '&cYour inventory is full!' +some-items-dropped: '&cSome items were dropped on the ground!' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..ae4b0d8 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,13 @@ +name: Headstones +description: Don't lose your precious items when you die! +authors: [ alex3025 ] +website: https://alex3025.tk + +version: '${project.version}' +api-version: 1.18 +main: tk.alex3025.headstones.Headstones + +commands: + headstones: + aliases: [ hs, headstone ] + usage: /headstones