From edee44630c77bde96335096bd23c0343bfaca1df Mon Sep 17 00:00:00 2001 From: Koopa <115321970+KoopaCode@users.noreply.github.com> Date: Fri, 31 Jan 2025 21:19:49 -0500 Subject: [PATCH] Public Release --- .github/workflows/build.yml | 90 +++++ README.md | 125 +++++++ pom.xml | 83 +++++ .../armorstandstorage/ArmorStandListener.java | 342 ++++++++++++++++++ .../armorstandstorage/ArmorStandStorage.java | 79 ++++ .../armorstandstorage/Database.java | 201 ++++++++++ src/main/resources/config.yml | 25 ++ src/main/resources/plugin.yml | 9 + 8 files changed, 954 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/koopacraft/armorstandstorage/ArmorStandListener.java create mode 100644 src/main/java/com/koopacraft/armorstandstorage/ArmorStandStorage.java create mode 100644 src/main/java/com/koopacraft/armorstandstorage/Database.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e0e0d76 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,90 @@ +name: Build +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Get Project Info + run: | + echo "PLUGIN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_ENV + echo "PLUGIN_NAME=$(mvn help:evaluate -Dexpression=project.name -q -DforceStdout)" >> $GITHUB_ENV + echo "PLUGIN_FILE=$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout)" >> $GITHUB_ENV + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Debug Directory + run: ls -la target/ + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PLUGIN_FILE }}-${{ env.PLUGIN_VERSION }} + path: target/${{ env.PLUGIN_FILE }}-${{ env.PLUGIN_VERSION }}.jar + retention-days: 5 + + - name: Send Build Notification + if: always() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + title: "${{ job.status == 'success' && '✅ Build Success!' || '❌ Build Failed!' }}" + description: | + **${{ env.PLUGIN_NAME }} v${{ env.PLUGIN_VERSION }}** + By ${{ github.actor }} • ${{ github.sha }} + ${{ github.repository }} + color: ${{ job.status == 'success' && '0x00ff00' || '0xff0000' }} + username: "🏺Artifact Build's" + avatar_url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" + + release: + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ env.PLUGIN_FILE }}-${{ env.PLUGIN_VERSION }} + + - name: Upload Release + uses: softprops/action-gh-release@v2 + with: + files: ${{ env.PLUGIN_FILE }}-${{ env.PLUGIN_VERSION }}.jar + fail_on_unmatched_files: true + + - name: Send Release Notification + if: always() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + title: "${{ job.status == 'success' && '🎉 Release Published!' || '❌ Release Failed!' }}" + description: | + **${{ env.PLUGIN_NAME }} v${{ env.PLUGIN_VERSION }}** + ${{ job.status == 'success' && '➜ https://github.com/${{ github.repository }}/releases/latest' || '' }} + color: ${{ job.status == 'success' && '0x00ff00' || '0xff0000' }} + username: "🏺Artifact Build's" + avatar_url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fffdfce --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ + + +![Armor Stand Storage Banner](https://lapislabs.dev/images/ass.png) + +🛡️ Armor Stand Storage v1.0 +============================ +

Storage Image

+ +Transform your armor stands into functional storage units while maintaining their display capabilities! Perfect for kits, storage rooms, and displays. + +**Version:** 1.0 + +**Minecraft Versions:** 1.8 - 1.21.x + +**Release Date:** 1/27/25 + +✨ Features +---------- + +* **Smart Storage System** + * Shift + Right-Click any armor stand to access storage + * 27 slots of storage space per armor stand + * Items in armor slots automatically display on the stand + * Regular right-click still works for vanilla interactions +* **Intelligent Armor Detection** + * Automatically detects and displays armor pieces + * Works with any armor type (leather, iron, gold, diamond, netherite) + * Supports modded armor that follows naming conventions + * Compatible with Elytra and player heads +* **Server Friendly** + * Multi-world support with configurable blocked worlds + * Minimal performance impact + * SQLite database for reliable storage + * Works on Minecraft 1.8+ + +🎮 Usage +-------- + +1. Place an armor stand +2. Add armor/items normally OR +3. Shift + Right-Click to open storage +4. Place items anywhere in the inventory: + * Armor pieces will automatically display + * Other items are safely stored + * First 6 slots are prioritized for display + +⚙️ Configuration +---------------- + + # Disable storage in specific worlds + disabled-worlds: + #- spawn_world + #- event_world + + # Debug mode for detailed logging + debug: false + + # Storage settings + storage: + inventory-title: "Armor Stand Storage" + inventory-rows: 3 + +🔒 Permissions +-------------- + +* `armorstandstorage.use` - Access to armor stand storage (default: op) + +📋 Commands +----------- + +No commands - just shift-right-click to use! + +💡 Tips +------- + +* Place armor in any slot - it will automatically display +* Use regular right-click for vanilla armor stand features +* Items are saved even if the armor stand breaks +* Works great for shop displays and storage rooms +* Perfect for RPG servers and town builds + +🔧 Installation +--------------- + +1. Download the plugin +2. Place in your plugins folder +3. Restart server +4. Configure (optional) +5. Start using! + +🎯 Planned Features +------------------- + +* Custom inventory sizes +* Per-world inventory settings +* Hologram support +* Shop integration +* API for developers + +🆕 What's New in 1.0 +-------------------- + +* Initial release +* Full armor stand storage functionality +* Multi-world support +* Automatic armor detection and display +* SQLite database integration +* Legacy version support (1.8+) + +📝 Version History +------------------ + +* **1.0** (1.27.2025) + * Initial release + * Core functionality implemented + * Database storage system + * Multi-version support + +ArmorStandStorage v1.0 - © 2024 Koopa + +Made with ❤️ by Koopa + +[GitHub Issues](https://github.com/KoopaCode/ArmorStand-Storage/issues) | [Discord](https://discord.gg/KmHGjaHWct) + +`storage` `free` `armorstand` `plugin` `1.8-1.21` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d401bf4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.koopacraft + armorstandstorage + 1.0-SNAPSHOT + jar + + ArmorStandStorage + A plugin that allows storing items in armor stands + + + 17 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21.1-R0.1-SNAPSHOT + provided + + + org.xerial + sqlite-jdbc + 3.45.1.0 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + false + + + org.sqlite + com.koopacraft.armorstandstorage.lib.sqlite + + + + + + + + + + src/main/resources + true + + + + \ No newline at end of file diff --git a/src/main/java/com/koopacraft/armorstandstorage/ArmorStandListener.java b/src/main/java/com/koopacraft/armorstandstorage/ArmorStandListener.java new file mode 100644 index 0000000..6cf3a1f --- /dev/null +++ b/src/main/java/com/koopacraft/armorstandstorage/ArmorStandListener.java @@ -0,0 +1,342 @@ +package com.koopacraft.armorstandstorage; + +import org.bukkit.Bukkit; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.event.player.PlayerArmorStandManipulateEvent; + +import java.util.HashMap; +import java.util.UUID; + +public class ArmorStandListener implements Listener { + private final ArmorStandStorage plugin; + private final HashMap openInventories = new HashMap<>(); + private final boolean isLegacyVersion; + + public ArmorStandListener(ArmorStandStorage plugin) { + this.plugin = plugin; + // Check if we're running on a legacy version (pre 1.13) + isLegacyVersion = !isMethodAvailable("org.bukkit.entity.ArmorStand", "getEquipment"); + + // Register all existing armor stands on startup + Bukkit.getScheduler().runTaskLater(plugin, () -> { + for (org.bukkit.World world : Bukkit.getWorlds()) { + for (Entity entity : world.getEntities()) { + if (entity instanceof ArmorStand) { + plugin.getDatabase().registerArmorStand(entity.getLocation()); + } + } + } + plugin.getLogger().info("Registered all existing armor stands!"); + }, 20L); + } + + private boolean isMethodAvailable(String className, String methodName) { + try { + Class clazz = Class.forName(className); + clazz.getMethod(methodName); + return true; + } catch (Exception e) { + return false; + } + } + + private void setArmorStandEquipment(ArmorStand armorStand, ItemStack[] contents) { + EntityEquipment equipment = armorStand.getEquipment(); + if (equipment == null) return; + + equipment.clear(); + + try { + // Scan entire inventory for armor and items + for (int i = 0; i < contents.length; i++) { + ItemStack item = contents[i]; + if (item == null) continue; + + String type = item.getType().name().toUpperCase(); + + // Check for armor pieces + if (type.endsWith("_HELMET") || type.contains("_HEAD") || type.equals("PLAYER_HEAD") || type.equals("SKULL") || type.equals("SKULL_ITEM")) { + equipment.setHelmet(item); + } + else if (type.endsWith("_CHESTPLATE") || type.equals("ELYTRA")) { + equipment.setChestplate(item); + } + else if (type.endsWith("_LEGGINGS")) { + equipment.setLeggings(item); + } + else if (type.endsWith("_BOOTS")) { + equipment.setBoots(item); + } + // Handle weapons/tools/items for hands + else if (i == 4 || (equipment.getHelmet() == null && equipment.getChestplate() == null + && equipment.getLeggings() == null && equipment.getBoots() == null)) { + if (isLegacyVersion) { + equipment.setItemInHand(item); + } else { + equipment.setItemInMainHand(item); + } + } + else if (!isLegacyVersion && i == 5) { + equipment.setItemInOffHand(item); + } + } + } catch (Exception e) { + plugin.getLogger().warning("Error setting armor stand equipment: " + e.getMessage()); + } + } + + private void getArmorStandEquipment(ArmorStand armorStand, Inventory inventory) { + EntityEquipment equipment = armorStand.getEquipment(); + if (equipment == null) return; + + try { + inventory.setItem(0, equipment.getHelmet()); + inventory.setItem(1, equipment.getChestplate()); + inventory.setItem(2, equipment.getLeggings()); + inventory.setItem(3, equipment.getBoots()); + + // Handle main and off hand differently for legacy versions + if (isLegacyVersion) { + inventory.setItem(4, equipment.getItemInHand()); // Legacy method + } else { + inventory.setItem(4, equipment.getItemInMainHand()); + inventory.setItem(5, equipment.getItemInOffHand()); + } + } catch (Exception e) { + plugin.getLogger().warning("Error getting armor stand equipment: " + e.getMessage()); + } + } + + @EventHandler + public void onArmorStandPlace(CreatureSpawnEvent event) { + if (event.getEntityType() == EntityType.ARMOR_STAND) { + plugin.getDatabase().registerArmorStand(event.getLocation()); + plugin.debug("New armor stand registered at: " + formatLocation(event.getLocation())); + } + } + + @EventHandler + public void onArmorStandRemove(EntityDeathEvent event) { + if (event.getEntityType() == EntityType.ARMOR_STAND) { + if (plugin.getDatabase().isRegistered(event.getEntity().getLocation())) { + plugin.getDatabase().removeArmorStand(event.getEntity().getLocation()); + plugin.getLogger().info("Registered armor stand removed at: " + formatLocation(event.getEntity().getLocation())); + } else { + plugin.getLogger().info("Unregistered armor stand removed at: " + formatLocation(event.getEntity().getLocation())); + } + } + } + + private String formatLocation(org.bukkit.Location loc) { + return String.format("World: %s, X: %.2f, Y: %.2f, Z: %.2f", + loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ()); + } + + @EventHandler + public void onArmorStandInteract(PlayerInteractAtEntityEvent event) { + if (!(event.getRightClicked() instanceof ArmorStand)) { + return; + } + + Player player = event.getPlayer(); + ArmorStand armorStand = (ArmorStand) event.getRightClicked(); + + // Check if in disabled world + if (plugin.isWorldDisabled(player.getWorld().getName())) { + player.sendMessage(plugin.getMessage("blocked-world")); + return; + } + + // Check if player is sneaking + if (!player.isSneaking()) { + return; // Allow normal armor stand interaction when not sneaking + } + + // Check permission + if (!player.hasPermission("armorstandstorage.use")) { + player.sendMessage(plugin.getMessage("no-permission")); + return; + } + + event.setCancelled(true); + + // Create inventory with configured size + int size = Math.max(27, plugin.getInventoryRows() * 9); + Inventory inventory = Bukkit.createInventory(null, size, plugin.getInventoryTitle()); + + // First, check if there's saved inventory data + ItemStack[] savedItems = plugin.getDatabase().getArmorStandInventory(armorStand.getLocation()); + + if (savedItems.length > 0 && hasItems(savedItems)) { + // Load saved inventory + for (int i = 0; i < Math.min(savedItems.length, size); i++) { + inventory.setItem(i, savedItems[i]); + } + } else { + // No saved items, get current equipment + EntityEquipment equipment = armorStand.getEquipment(); + if (equipment != null) { + // Store current equipment in first slots + inventory.setItem(0, equipment.getHelmet()); + inventory.setItem(1, equipment.getChestplate()); + inventory.setItem(2, equipment.getLeggings()); + inventory.setItem(3, equipment.getBoots()); + if (isLegacyVersion) { + inventory.setItem(4, equipment.getItemInHand()); + } else { + inventory.setItem(4, equipment.getItemInMainHand()); + inventory.setItem(5, equipment.getItemInOffHand()); + } + + // Save this initial state to database + plugin.getDatabase().saveArmorStand(armorStand.getLocation(), inventory.getContents()); + } + } + + // Open inventory + player.openInventory(inventory); + openInventories.put(player.getUniqueId(), armorStand); + } + + // Helper method to check if an ItemStack array has any non-null items + private boolean hasItems(ItemStack[] items) { + for (ItemStack item : items) { + if (item != null) { + return true; + } + } + return false; + } + + @EventHandler + public void onArmorStandManipulate(PlayerArmorStandManipulateEvent event) { + ArmorStand armorStand = event.getRightClicked(); + + // Don't interfere if player is sneaking (our storage UI handles that) + if (event.getPlayer().isSneaking()) { + return; + } + + // Schedule a task to save the new equipment state after the vanilla interaction + Bukkit.getScheduler().runTask(plugin, () -> { + if (!plugin.getDatabase().isRegistered(armorStand.getLocation())) { + plugin.getDatabase().registerArmorStand(armorStand.getLocation()); + } + + // Get current equipment and save it + EntityEquipment equipment = armorStand.getEquipment(); + if (equipment != null) { + ItemStack[] contents = new ItemStack[27]; + contents[0] = equipment.getHelmet(); + contents[1] = equipment.getChestplate(); + contents[2] = equipment.getLeggings(); + contents[3] = equipment.getBoots(); + if (isLegacyVersion) { + contents[4] = equipment.getItemInHand(); + } else { + contents[4] = equipment.getItemInMainHand(); + contents[5] = equipment.getItemInOffHand(); + } + plugin.getDatabase().saveArmorStand(armorStand.getLocation(), contents); + plugin.debug("Saved armor stand equipment after manual interaction"); + } + }); + } + + @EventHandler + public void onInventoryClick(InventoryClickEvent event) { + Player player = (Player) event.getWhoClicked(); + ArmorStand armorStand = openInventories.get(player.getUniqueId()); + + if (armorStand == null || !event.getView().getTitle().equals(plugin.getInventoryTitle())) { + return; + } + + plugin.debug("Player " + player.getName() + " clicked in armor stand inventory"); + + // Update equipment immediately after click + Bukkit.getScheduler().runTask(plugin, () -> { + Inventory inv = event.getView().getTopInventory(); + ItemStack[] contents = inv.getContents(); + + // Update armor stand equipment + setArmorStandEquipment(armorStand, contents); + + // Save to database + plugin.getDatabase().saveArmorStand(armorStand.getLocation(), contents); + }); + } + + @EventHandler + public void onInventoryDrag(InventoryDragEvent event) { + Player player = (Player) event.getWhoClicked(); + ArmorStand armorStand = openInventories.get(player.getUniqueId()); + + if (armorStand == null || !event.getView().getTitle().equals(plugin.getInventoryTitle())) { + return; + } + + plugin.debug("Player " + player.getName() + " dragged in armor stand inventory"); + + // Update equipment immediately after drag + Bukkit.getScheduler().runTask(plugin, () -> { + Inventory inv = event.getView().getTopInventory(); + ItemStack[] contents = inv.getContents(); + + // Update armor stand equipment + setArmorStandEquipment(armorStand, contents); + + // Save to database + plugin.getDatabase().saveArmorStand(armorStand.getLocation(), contents); + }); + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + Player player = (Player) event.getPlayer(); + ArmorStand armorStand = openInventories.remove(player.getUniqueId()); + + if (armorStand == null || !event.getView().getTitle().equals(plugin.getInventoryTitle())) { + return; + } + + // Save the exact inventory contents, preserving null slots + Inventory inv = event.getView().getTopInventory(); + ItemStack[] contents = new ItemStack[inv.getSize()]; + + // Count non-null items while copying contents + int itemCount = 0; + for (int i = 0; i < inv.getSize(); i++) { + contents[i] = inv.getItem(i); + if (contents[i] != null) { + itemCount++; + } + } + + // Save to database + plugin.getDatabase().saveArmorStand(armorStand.getLocation(), contents); + + // Update armor stand equipment + setArmorStandEquipment(armorStand, contents); + + plugin.debug("Saving inventory with " + contents.length + " slots"); + plugin.debug("Found " + itemCount + " items to save"); + plugin.debug("Saved armor stand inventory for " + player.getName()); + } +} \ No newline at end of file diff --git a/src/main/java/com/koopacraft/armorstandstorage/ArmorStandStorage.java b/src/main/java/com/koopacraft/armorstandstorage/ArmorStandStorage.java new file mode 100644 index 0000000..5df6053 --- /dev/null +++ b/src/main/java/com/koopacraft/armorstandstorage/ArmorStandStorage.java @@ -0,0 +1,79 @@ +package com.koopacraft.armorstandstorage; + +import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; +import java.util.List; +import java.util.ArrayList; + +public class ArmorStandStorage extends JavaPlugin { + private Database database; + private List disabledWorlds; + private boolean debugMode; + + @Override + public void onEnable() { + // Create config if it doesn't exist + saveDefaultConfig(); + + // Load settings + disabledWorlds = getConfig().getStringList("disabled-worlds"); + if (disabledWorlds == null) { + disabledWorlds = new ArrayList<>(); + } + debugMode = getConfig().getBoolean("debug", false); + + // Initialize database + database = new Database(this, new File(getDataFolder(), "armorstands.db")); + + // Register events + getServer().getPluginManager().registerEvents(new ArmorStandListener(this), this); + + // Log startup + getLogger().info("ArmorStandStorage has been enabled!"); + if (!disabledWorlds.isEmpty()) { + getLogger().info("Armor stand storage is disabled in: " + String.join(", ", disabledWorlds)); + } + } + + @Override + public void onDisable() { + if (database != null) { + database.close(); + } + getLogger().info("ArmorStandStorage has been disabled!"); + } + + public Database getDatabase() { + return database; + } + + public String getMessage(String path) { + return getConfig().getString("messages." + path, "Message not found: " + path); + } + + public String getDisabledWorld() { + return getConfig().getString("disabled-world", "world_KC_SPAWN"); + } + + public String getInventoryTitle() { + return getConfig().getString("storage.inventory-title", "Armor Stand Storage"); + } + + public int getInventoryRows() { + return getConfig().getInt("storage.inventory-rows", 3); + } + + public boolean isWorldDisabled(String worldName) { + return disabledWorlds.contains(worldName); + } + + public void debug(String message) { + if (debugMode) { + getLogger().info("[Debug] " + message); + } + } + + public boolean isDebugMode() { + return debugMode; + } +} \ No newline at end of file diff --git a/src/main/java/com/koopacraft/armorstandstorage/Database.java b/src/main/java/com/koopacraft/armorstandstorage/Database.java new file mode 100644 index 0000000..1b617f8 --- /dev/null +++ b/src/main/java/com/koopacraft/armorstandstorage/Database.java @@ -0,0 +1,201 @@ +package com.koopacraft.armorstandstorage; + +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; + +import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.sql.*; +import java.util.Base64; + +public class Database { + private Connection connection; + private final ArmorStandStorage plugin; + + public Database(ArmorStandStorage plugin, File file) { + this.plugin = plugin; + try { + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + file.getAbsolutePath()); + + // Create tables if they don't exist + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS armor_stands (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "world TEXT NOT NULL," + + "x DOUBLE NOT NULL," + + "y DOUBLE NOT NULL," + + "z DOUBLE NOT NULL," + + "inventory TEXT," + + "registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ")"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void registerArmorStand(Location location) { + try { + String sql = "INSERT OR IGNORE INTO armor_stands (world, x, y, z) VALUES (?, ?, ?, ?)"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, location.getWorld().getName()); + pstmt.setDouble(2, location.getX()); + pstmt.setDouble(3, location.getY()); + pstmt.setDouble(4, location.getZ()); + pstmt.executeUpdate(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public boolean isRegistered(Location location) { + try { + String sql = "SELECT id FROM armor_stands WHERE world = ? AND x = ? AND y = ? AND z = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, location.getWorld().getName()); + pstmt.setDouble(2, location.getX()); + pstmt.setDouble(3, location.getY()); + pstmt.setDouble(4, location.getZ()); + + ResultSet rs = pstmt.executeQuery(); + return rs.next(); + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public void removeArmorStand(Location location) { + try { + String sql = "DELETE FROM armor_stands WHERE world = ? AND x = ? AND y = ? AND z = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, location.getWorld().getName()); + pstmt.setDouble(2, location.getX()); + pstmt.setDouble(3, location.getY()); + pstmt.setDouble(4, location.getZ()); + pstmt.executeUpdate(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void saveArmorStand(Location location, ItemStack[] inventory) { + try { + String serializedInventory = serializeItems(inventory); + plugin.debug("Saving inventory: " + serializedInventory); + + String sql = "UPDATE armor_stands SET inventory = ? WHERE world = ? AND x = ? AND y = ? AND z = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, serializedInventory); + pstmt.setString(2, location.getWorld().getName()); + pstmt.setDouble(3, location.getX()); + pstmt.setDouble(4, location.getY()); + pstmt.setDouble(5, location.getZ()); + int updated = pstmt.executeUpdate(); + + // If no rows were updated, the armor stand might not be registered yet + if (updated == 0) { + sql = "INSERT INTO armor_stands (world, x, y, z, inventory) VALUES (?, ?, ?, ?, ?)"; + try (PreparedStatement insertStmt = connection.prepareStatement(sql)) { + insertStmt.setString(1, location.getWorld().getName()); + insertStmt.setDouble(2, location.getX()); + insertStmt.setDouble(3, location.getY()); + insertStmt.setDouble(4, location.getZ()); + insertStmt.setString(5, serializedInventory); + insertStmt.executeUpdate(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public ItemStack[] getArmorStandInventory(Location location) { + try { + String sql = "SELECT inventory FROM armor_stands WHERE world = ? AND x = ? AND y = ? AND z = ?"; + try (PreparedStatement pstmt = connection.prepareStatement(sql)) { + pstmt.setString(1, location.getWorld().getName()); + pstmt.setDouble(2, location.getX()); + pstmt.setDouble(3, location.getY()); + pstmt.setDouble(4, location.getZ()); + + ResultSet rs = pstmt.executeQuery(); + if (rs.next()) { + String inventoryData = rs.getString("inventory"); + plugin.debug("Loading inventory: " + inventoryData); + if (inventoryData != null && !inventoryData.isEmpty()) { + return deserializeItems(inventoryData); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return new ItemStack[27]; + } + + private String serializeItems(ItemStack[] items) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); + + // Count non-null items + int nonNullCount = 0; + for (ItemStack item : items) { + if (item != null) nonNullCount++; + } + dataOutput.writeInt(nonNullCount); + + // Write items with their slot numbers + for (int i = 0; i < items.length; i++) { + if (items[i] != null) { + dataOutput.writeInt(i); // Write the slot number + dataOutput.writeObject(items[i]); + } + } + + dataOutput.close(); + return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + } + + private ItemStack[] deserializeItems(String data) throws Exception { + if (data == null || data.isEmpty()) { + return new ItemStack[27]; // Return empty array with correct size + } + + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); + + int size = dataInput.readInt(); + ItemStack[] items = new ItemStack[27]; // Fixed size for chest inventory + + // Read items into their exact positions + for (int i = 0; i < size; i++) { + int slot = dataInput.readInt(); // Read the slot number + ItemStack item = (ItemStack) dataInput.readObject(); + if (slot >= 0 && slot < items.length) { + items[slot] = item; + } + } + + dataInput.close(); + return items; + } + + public void close() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a9b750d --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,25 @@ +# ArmorStandStorage Configuration + +# Worlds where armor stand storage is disabled +# Leave empty to allow in all worlds +# Example: +disabled-worlds: + #- world_KC_SPAWN + #- world_event + #- minigames_lobby + +# Debug mode - set to true for detailed logging +debug: false + +# Messages +messages: + no-permission: "§cYou don't have permission to use armor stand storage!" + must-sneak: "§cYou must be sneaking to use this!" + blocked-world: "§cArmor stand storage is disabled in this world!" + +# Storage settings +storage: + # Inventory title + inventory-title: "Armor Stand Storage" + # Number of rows in the storage (must be between 1 and 6) + inventory-rows: 3 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..fbe9145 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: ArmorStandStorage +version: 1.0 +main: com.koopacraft.armorstandstorage.ArmorStandStorage +api-version: 1.13 +description: Allows players to store items in armor stands +permissions: + armorstandstorage.use: + description: Allows players to use armor stand storage + default: op \ No newline at end of file