From 0750a0abd7b75904681e191a1d64fd9245fc4974 Mon Sep 17 00:00:00 2001 From: benrobson Date: Tue, 31 Dec 2024 16:07:16 +1100 Subject: [PATCH 01/28] Updated MD file with new proposed features. --- .gitignore | 3 +- README.md | 51 +++++++++++++++++- pom.xml | 10 ++++ .../me/benrobson/blockvault/BlockVault.java | 22 ++++++++ .../me/benrobson/blockvault/Blockvault.java | 17 ------ src/main/resources/config.yml | 40 ++++++++++++++ src/main/resources/plugin.yml | 17 +++++- src/main/resources/vault.schem | Bin 0 -> 2318 bytes src/main/resources/vault.yml | 0 9 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 src/main/java/me/benrobson/blockvault/BlockVault.java delete mode 100644 src/main/java/me/benrobson/blockvault/Blockvault.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/vault.schem create mode 100644 src/main/resources/vault.yml diff --git a/.gitignore b/.gitignore index 723ef36..ec376bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +target \ No newline at end of file diff --git a/README.md b/README.md index 233f9d4..ee17e35 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ -# BlockVault -BlockVault is a collaborative Minecraft plugin where players work together to collect every block in the game and display them. +## BlockVault + +![img](https://i.imgur.com/DPcMoaq.png) + +BlockVault is an interactive challenge where players collaborate to collect every unique block and item in Minecraft. The centerpiece is a large, dynamic museum building where contributions are tracked, and progress is displayed, turning the collection into an engaging and rewarding experience. + +## Features +### Dynamic Building Generation +* Generates a polished, pre-designed museum building at a designated location using schematics. +* Customizable layouts for server-specific aesthetics or a default design for simplicity. + +### Interactive Block Displays +* Sections dedicated to specific block categories (e.g., wood, ores, decorative blocks). +* Blocks and items can be placed on predefined slots with item frames or display stands, indicating where each item belongs and can be contributed to. + +### Points System +* Players earn points for each block they unlock and contribute to the museum. +* The points awarded vary based on the rarity of the block/item: + * **Common Blocks:** 1 point per block (e.g., Cobblestone, Oak Logs). + * **Uncommon Blocks:** 5 points per block (e.g., Prismarine, Obsidian). + * **Rare Blocks/Items:** 10+ points per block (e.g., Dragon Egg, Netherite Block). +* A running total of points is tracked for each player, contributing to their leaderboard position. + +### Tracking Progress +* A real-time leaderboard displays the top contributors and their collected blocks/items. +* A progress bar or percentage indicator shows how close the server is to completing the collection. + +### Progress Leaderboard & GUI Integration +* Progress can be viewed in real-time using a hologram display, showcasing the top contributors and collection milestones. +* Use the `/vault leaderboard` command to see a detailed leaderboard of top contributors, including points and blocks contributed. +* GUI integration allows players to easily browse and view all collected blocks in an organized interface. +* Add categories to the leaderboard: + * Most blocks contributed today/this week. + * Most rare blocks contributed. + * Category-specific leaders (e.g., wood, ores). + +### Timer and Boss Integration +* A countdown timer can be enabled to add urgency, ticking down the time remaining for the challenge. +* A boss integration shows live updates on: + * The time elapsed. + * How many blocks have been collected. + * How much time is left (if the timer is enabled). + +## Commands +* `/vault start` – Starts the vault challenge (requires `blockvault.start`). +* `/vault generate` – Generates the vault based on the schematic (requires `blockvault.generate`). +* `/vault add ` – Manually adds a block/item to the collection. +* `/vault progress` – Displays the overall progress of the collection and the leaderboard. +* `/vault leaderboard` – Shows the top contributors and their points in a detailed list. \ No newline at end of file diff --git a/pom.xml b/pom.xml index a46b1a0..57cf993 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,10 @@ sonatype https://oss.sonatype.org/content/groups/public/ + + enginehub-maven + https://maven.enginehub.org/repo/ + @@ -68,5 +72,11 @@ 1.21.3-R0.1-SNAPSHOT provided + + com.sk89q.worldedit + worldedit-core + 7.2.0-SNAPSHOT + provided + diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java new file mode 100644 index 0000000..5352d22 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -0,0 +1,22 @@ +package me.benrobson.blockvault; + +import org.bukkit.plugin.java.JavaPlugin; + +import static org.bukkit.Bukkit.getConsoleSender; + +public final class BlockVault extends JavaPlugin { + + @Override + public void onEnable() { + getConsoleSender().sendMessage("§8[§6BlockVault§8] §aPlugin is now enabled!"); + + // Register commands + + // Register events + } + + @Override + public void onDisable() { + getConsoleSender().sendMessage("§8[§6BlockVault§8] §cPlugin is now disabled. Thanks for playing!"); + } +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/Blockvault.java b/src/main/java/me/benrobson/blockvault/Blockvault.java deleted file mode 100644 index 7aeacc8..0000000 --- a/src/main/java/me/benrobson/blockvault/Blockvault.java +++ /dev/null @@ -1,17 +0,0 @@ -package me.benrobson.blockvault; - -import org.bukkit.plugin.java.JavaPlugin; - -public final class Blockvault extends JavaPlugin { - - @Override - public void onEnable() { - // Plugin startup logic - - } - - @Override - public void onDisable() { - // Plugin shutdown logic - } -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..2695b96 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,40 @@ +# BlockVault Configuration +# Configures the BlockVault, collection goals, progress tracking, and rewards system. + +# Museum settings +vault: + location: + # Coordinates for the vault's center point + x: 100 + y: 64 + z: 100 + world: world + + # Schematic settings for the vault design + schematic: + # Path to the schematic file + path: "vault.schem" + # Whether to use the default schematic or a custom one + use_default_schematic: true + + excluded_items: + - minecraft:bedrock + - minecraft:barrier + +# Progress Tracking +progress: + # Whether to display the progress bar on the screen + show_progress_bar: true + +# Leaderboard Settings +leaderboard: + # Whether to show the leaderboard in the server + show_leaderboard_hologram: true + location: + # Coordinates for the leaderboard hologram + x: 100 + y: 64 + z: 100 + world: world + # Update interval for leaderboard display in seconds + update_interval: 60 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bacbd46..8d692f5 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,4 +1,17 @@ -name: blockvault +name: BlockVault version: '1.0.0' -main: me.benrobson.blockvault.Blockvault +main: me.benrobson.blockvault.BlockVault api-version: '1.21' +commands: + blockvault: + description: Start and manage the BlockVault challenge. + usage: / + aliases: + - leaderboard + - lb +permissions: + blockvault.use: + description: Allows usage of BlockVault commands. + default: true + blockvault.generate: + description: Allows usage to generate BlockVault. \ No newline at end of file diff --git a/src/main/resources/vault.schem b/src/main/resources/vault.schem new file mode 100644 index 0000000000000000000000000000000000000000..7c595573a47bc5fe1f5ffbba8defd41897c08c8e GIT binary patch literal 2318 zcmV+p3GwzHiwFP!00000|Lt6Ra~ro2hX7C_36Yd6zhv36qS#SvDGxig<45(6({$3A zX6iO=+{DdrPUMN=h~jDPPL(~Ket>?ley#$G$31u?anfC5tK1pyfw;ZJ;^hq zVQ<%Uf>4Z12wIPnbKW_0yX}+(l%OA`TM&B3xreV1^t$l54Lv_R5ZdPnx)RgwI_-fo zcAQH#K)n30!yJm&|iu*%Yf1(=w2+=51f9MLC)KLu=`HPw+MPR#*~4x zm+@)%jt?(gCca-H=vwUiWB;V%wvRa!fd4Wd zFmU%r^hCfUFVM*1pWCus>eEQEt&GpXmcD+9db7d zK_k;qw+Q-c!9W|`>*q8(BY?FNZxi%rRJB#!+&HJ3Mx$Q4>7zpCo9U)ag8o!w&}tk% zF5dm7Al@k)Xl?b~;sZIZci3(FURTG}35Yx) z=w&P>#Po*zEeMV(&x{VK=f$WU5P9VVa2>?AT;jS%w5tgJzAG+}=u5ts`LBLAeq22D`=P`4+OX8; zP~{u%WEjqI74((UbK&kOq4&DdTPs-arIPGjEXm$E^=IDbt?fD7y`lS-f3_r&XK&!0 ziX|huP!&}=-oW!wWpBXcYN&eT1_4(cRK?8~V*lI}a+}XLH~(e(V-8jOLHpEs>vsA0 z^?lzPhT{yF%VoeN^W!^bFxc?+d0)%){mwaZ)3%_@=qoEqiq@;6x~%9cTJODC^d+N@ zAog1)bbfUGfTv=F8vn*<{ySG>PjrJf|KJXL;eojItE+qT@13`N$6S(|fjz5-{>9zUmtIL>k3~dT+3T^&c zic*SitgYRoboJKSI;Ql-+D$CF)+rX8w{NY%15t)=-QL*vXl-rnHt@|6E?%!=E{vBy z-k46$gbkS9xx2CP$=24^UErT8{uFwiLVM%seQxI*W$ez@);+HN&enZy>o#vwyv^H~ zZmS;gz-Ue(uv7nx0>{hPv?;VT0w9_LR|}s}d+(#1qErqsFF7@w&a7e~v~u zccpA&T$PbUt4pB&i!pqOD6OT?B}=*IrCA`5kO+J^h8qMFt~GFUO)Ma2$2g5}1wYl} zoTYm(1N}BZLy^VnWOjkzfL|2eGiYN(O^=CJOd-!uFbK0K^I*zkTZI4@v;cIv%!f-7 zsmX+y##My@ljx4*3#M_E$ZHQpe!{UHN@cjjJs#(>KOR!~i_uEN=}NjsIlm$@H(qR1 z_%u1^83i~KJs5gKxGaKJ8?Z{uF26oei`l7=szA7K{1_mrF^<}d8C)S;XcI!DDZXa0 zw&FC0_oX3qdk$HSOy;z%5D@95#Zw9AOtvutj-g*K6Omu1f~E57%F4=AQ5%{61vQ&Z zTbUdPzy=C){1|YNOInlx2V5xw2_ZUf<1IW7+UoPOgqKZw7B^zFu4^f z+f=rhXv4HrCbR(y9?VMg{YXSMDZ|ubYXoGp@YDdD^3{l&kWiX{aWfV!*}lp2R!PK_ zw=`$5c8VhtE+a$}-MVh^lt%Qz3Eyj2o$GnQb8rDQwBk=pE&sT`E*nJ+%BWbxG~vd&t!8ZjR0$zQir^%&6< z?@=!xg+z`T6sc;{G_Kjc$@Er9RPZ(j*A)V4O-Rnv!}Q1p5oM7d zF^$HYDXxzPi^zC<^Oj!7u|1@!1yLnKQg*71(LLeV z7>NQDwxd_AY`qF$lc(`eOAin%JD3fGQhLc>h_W5v~@-$Q7QL?)WijiqDI~p#?CMo z`j%`)x$X(8t3+~Y+LXlmamKaBY*TuH$UYHGOBNBIrHmQPQpSvO(JWkMl!IpBGNT+c o3zr$?peNRRMl(WRTl4>SVrX9euYySaJJJ~a7g5YB)2dJa0Gq;}1poj5 literal 0 HcmV?d00001 diff --git a/src/main/resources/vault.yml b/src/main/resources/vault.yml new file mode 100644 index 0000000..e69de29 From 1dae4a645bebabe9c0cc4e69f87d2d9369700d7a Mon Sep 17 00:00:00 2001 From: benrobson Date: Tue, 31 Dec 2024 16:13:55 +1100 Subject: [PATCH 02/28] Added Broadcast Announcements to scope. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ee17e35..448dfa4 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ BlockVault is an interactive challenge where players collaborate to collect ever * Most rare blocks contributed. * Category-specific leaders (e.g., wood, ores). +### Broadcast Announcements +* **Rare Blocks/Items Announcements:** Announce server-wide when a rare block or item is added to the vault, celebrating player contributions and encouraging participation. +* **Milestone Achievements:** Broadcast messages when major milestones are reached, such as completing 50% of the vault or finishing specific block categories. + ### Timer and Boss Integration * A countdown timer can be enabled to add urgency, ticking down the time remaining for the challenge. * A boss integration shows live updates on: From 4a72641448c9de80600e84c99d5d1fc46f47b122 Mon Sep 17 00:00:00 2001 From: benrobson Date: Tue, 31 Dec 2024 21:59:36 +1100 Subject: [PATCH 03/28] Add BV commands. --- .../me/benrobson/blockvault/BlockVault.java | 10 ++++-- .../benrobson/blockvault/commands/bvadd.java | 25 +++++++++++++ .../blockvault/commands/bvgenerate.java | 19 ++++++++++ .../blockvault/commands/bvleaderboard.java | 14 ++++++++ .../blockvault/commands/bvprogress.java | 14 ++++++++ .../blockvault/commands/bvstart.java | 19 ++++++++++ src/main/resources/config.yml | 34 ++++++------------ src/main/resources/plugin.yml | 28 ++++++++------- src/main/resources/vault.schem | Bin 2318 -> 0 bytes 9 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvadd.java create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvgenerate.java create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvprogress.java create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvstart.java delete mode 100644 src/main/resources/vault.schem diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 5352d22..96abe9b 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault; +import me.benrobson.blockvault.commands.*; import org.bukkit.plugin.java.JavaPlugin; import static org.bukkit.Bukkit.getConsoleSender; @@ -8,15 +9,20 @@ public final class BlockVault extends JavaPlugin { @Override public void onEnable() { - getConsoleSender().sendMessage("§8[§6BlockVault§8] §aPlugin is now enabled!"); + getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§aPlugin is now enabled!"); // Register commands + getCommand("bvstart").setExecutor(new bvstart()); + getCommand("bvgenerate").setExecutor(new bvgenerate()); + getCommand("bvadd").setExecutor(new bvadd()); + getCommand("bvprogress").setExecutor(new bvprogress()); + getCommand("bvleaderboard").setExecutor(new bvleaderboard()); // Register events } @Override public void onDisable() { - getConsoleSender().sendMessage("§8[§6BlockVault§8] §cPlugin is now disabled. Thanks for playing!"); + getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§cPlugin is now disabled."); } } \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvadd.java b/src/main/java/me/benrobson/blockvault/commands/bvadd.java new file mode 100644 index 0000000..a45b6bc --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvadd.java @@ -0,0 +1,25 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class bvadd implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("blockvault.add")) { + sender.sendMessage("§cYou don't have permission to use this command!"); + return true; + } + + if (args.length < 1) { + sender.sendMessage("§cUsage: /vault add "); + return true; + } + + // Logic to add the specified item/block + String block = args[0]; + sender.sendMessage("§aAdded " + block + " to the vault!"); + return true; + } +} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java b/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java new file mode 100644 index 0000000..e6210ea --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java @@ -0,0 +1,19 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class bvgenerate implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("blockvault.generate")) { + sender.sendMessage("§cYou don't have permission to use this command!"); + return true; + } + + // Logic to generate the vault + sender.sendMessage("§aVault generated successfully!"); + return true; + } +} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java new file mode 100644 index 0000000..7910c79 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java @@ -0,0 +1,14 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class bvleaderboard implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Logic to display leaderboard + sender.sendMessage("§aVault Leaderboard:\n1. Player1 - 100 points\n2. Player2 - 80 points"); + return true; + } +} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java new file mode 100644 index 0000000..e543963 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -0,0 +1,14 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class bvprogress implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Logic to display progress + sender.sendMessage("§aVault Progress: 75% complete."); + return true; + } +} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java new file mode 100644 index 0000000..a4abf94 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -0,0 +1,19 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +public class bvstart implements CommandExecutor { + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("blockvault.start")) { + sender.sendMessage("§cYou don't have permission to use this command!"); + return true; + } + + // Logic to start the vault challenge + sender.sendMessage("§aVault challenge started!"); + return true; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 2695b96..abcf71a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,7 +1,7 @@ # BlockVault Configuration # Configures the BlockVault, collection goals, progress tracking, and rewards system. -# Museum settings +# Vault settings vault: location: # Coordinates for the vault's center point @@ -14,27 +14,15 @@ vault: schematic: # Path to the schematic file path: "vault.schem" - # Whether to use the default schematic or a custom one - use_default_schematic: true - excluded_items: - - minecraft:bedrock - - minecraft:barrier +# Points system settings +points: + # Base points for each block type + base_points: + common: 1 # Points for common blocks (e.g., Cobblestone, Oak Logs) + uncommon: 5 # Points for uncommon blocks (e.g., Prismarine, Obsidian) + rare: 10 # Points for rare blocks/items (e.g., Dragon Egg, Netherite Block) -# Progress Tracking -progress: - # Whether to display the progress bar on the screen - show_progress_bar: true - -# Leaderboard Settings -leaderboard: - # Whether to show the leaderboard in the server - show_leaderboard_hologram: true - location: - # Coordinates for the leaderboard hologram - x: 100 - y: 64 - z: 100 - world: world - # Update interval for leaderboard display in seconds - update_interval: 60 \ No newline at end of file +# Language settings +lang: + prefix: "&8&l[&3BD&8&l]" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 8d692f5..70c09e9 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,16 +2,20 @@ name: BlockVault version: '1.0.0' main: me.benrobson.blockvault.BlockVault api-version: '1.21' +softdepend: [WorldEdit] commands: - blockvault: - description: Start and manage the BlockVault challenge. - usage: / - aliases: - - leaderboard - - lb -permissions: - blockvault.use: - description: Allows usage of BlockVault commands. - default: true - blockvault.generate: - description: Allows usage to generate BlockVault. \ No newline at end of file + bvadd: + description: Command + usage: /bvadd + bvgenerate: + description: Command + usage: /bvgenerate + bvleaderboard: + description: Command + usage: /bvleaderboard + bvprogress: + description: Command + usage: /bvprogress + bvstart: + description: Command + usage: /bvstart diff --git a/src/main/resources/vault.schem b/src/main/resources/vault.schem deleted file mode 100644 index 7c595573a47bc5fe1f5ffbba8defd41897c08c8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2318 zcmV+p3GwzHiwFP!00000|Lt6Ra~ro2hX7C_36Yd6zhv36qS#SvDGxig<45(6({$3A zX6iO=+{DdrPUMN=h~jDPPL(~Ket>?ley#$G$31u?anfC5tK1pyfw;ZJ;^hq zVQ<%Uf>4Z12wIPnbKW_0yX}+(l%OA`TM&B3xreV1^t$l54Lv_R5ZdPnx)RgwI_-fo zcAQH#K)n30!yJm&|iu*%Yf1(=w2+=51f9MLC)KLu=`HPw+MPR#*~4x zm+@)%jt?(gCca-H=vwUiWB;V%wvRa!fd4Wd zFmU%r^hCfUFVM*1pWCus>eEQEt&GpXmcD+9db7d zK_k;qw+Q-c!9W|`>*q8(BY?FNZxi%rRJB#!+&HJ3Mx$Q4>7zpCo9U)ag8o!w&}tk% zF5dm7Al@k)Xl?b~;sZIZci3(FURTG}35Yx) z=w&P>#Po*zEeMV(&x{VK=f$WU5P9VVa2>?AT;jS%w5tgJzAG+}=u5ts`LBLAeq22D`=P`4+OX8; zP~{u%WEjqI74((UbK&kOq4&DdTPs-arIPGjEXm$E^=IDbt?fD7y`lS-f3_r&XK&!0 ziX|huP!&}=-oW!wWpBXcYN&eT1_4(cRK?8~V*lI}a+}XLH~(e(V-8jOLHpEs>vsA0 z^?lzPhT{yF%VoeN^W!^bFxc?+d0)%){mwaZ)3%_@=qoEqiq@;6x~%9cTJODC^d+N@ zAog1)bbfUGfTv=F8vn*<{ySG>PjrJf|KJXL;eojItE+qT@13`N$6S(|fjz5-{>9zUmtIL>k3~dT+3T^&c zic*SitgYRoboJKSI;Ql-+D$CF)+rX8w{NY%15t)=-QL*vXl-rnHt@|6E?%!=E{vBy z-k46$gbkS9xx2CP$=24^UErT8{uFwiLVM%seQxI*W$ez@);+HN&enZy>o#vwyv^H~ zZmS;gz-Ue(uv7nx0>{hPv?;VT0w9_LR|}s}d+(#1qErqsFF7@w&a7e~v~u zccpA&T$PbUt4pB&i!pqOD6OT?B}=*IrCA`5kO+J^h8qMFt~GFUO)Ma2$2g5}1wYl} zoTYm(1N}BZLy^VnWOjkzfL|2eGiYN(O^=CJOd-!uFbK0K^I*zkTZI4@v;cIv%!f-7 zsmX+y##My@ljx4*3#M_E$ZHQpe!{UHN@cjjJs#(>KOR!~i_uEN=}NjsIlm$@H(qR1 z_%u1^83i~KJs5gKxGaKJ8?Z{uF26oei`l7=szA7K{1_mrF^<}d8C)S;XcI!DDZXa0 zw&FC0_oX3qdk$HSOy;z%5D@95#Zw9AOtvutj-g*K6Omu1f~E57%F4=AQ5%{61vQ&Z zTbUdPzy=C){1|YNOInlx2V5xw2_ZUf<1IW7+UoPOgqKZw7B^zFu4^f z+f=rhXv4HrCbR(y9?VMg{YXSMDZ|ubYXoGp@YDdD^3{l&kWiX{aWfV!*}lp2R!PK_ zw=`$5c8VhtE+a$}-MVh^lt%Qz3Eyj2o$GnQb8rDQwBk=pE&sT`E*nJ+%BWbxG~vd&t!8ZjR0$zQir^%&6< z?@=!xg+z`T6sc;{G_Kjc$@Er9RPZ(j*A)V4O-Rnv!}Q1p5oM7d zF^$HYDXxzPi^zC<^Oj!7u|1@!1yLnKQg*71(LLeV z7>NQDwxd_AY`qF$lc(`eOAin%JD3fGQhLc>h_W5v~@-$Q7QL?)WijiqDI~p#?CMo z`j%`)x$X(8t3+~Y+LXlmamKaBY*TuH$UYHGOBNBIrHmQPQpSvO(JWkMl!IpBGNT+c o3zr$?peNRRMl(WRTl4>SVrX9euYySaJJJ~a7g5YB)2dJa0Gq;}1poj5 From 4da1d50af92a3e43bafcd8624c16d7c5d67c8bdc Mon Sep 17 00:00:00 2001 From: benrobson Date: Tue, 31 Dec 2024 22:19:56 +1100 Subject: [PATCH 04/28] Start on generate and generator util. --- pom.xml | 4 +- .../me/benrobson/blockvault/BlockVault.java | 2 +- .../blockvault/commands/bvgenerate.java | 22 ++++++- .../blockvault/util/GeneratorUtil.java | 66 +++++++++++++++++++ src/main/resources/plugin.yml | 2 +- 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java diff --git a/pom.xml b/pom.xml index 57cf993..7196de4 100644 --- a/pom.xml +++ b/pom.xml @@ -74,8 +74,8 @@ com.sk89q.worldedit - worldedit-core - 7.2.0-SNAPSHOT + worldedit-bukkit + 7.2.14 provided diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 96abe9b..4aaddb2 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -13,7 +13,7 @@ public void onEnable() { // Register commands getCommand("bvstart").setExecutor(new bvstart()); - getCommand("bvgenerate").setExecutor(new bvgenerate()); + getCommand("bvgenerate").setExecutor(new bvgenerate(this)); getCommand("bvadd").setExecutor(new bvadd()); getCommand("bvprogress").setExecutor(new bvprogress()); getCommand("bvleaderboard").setExecutor(new bvleaderboard()); diff --git a/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java b/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java index e6210ea..d07b84f 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java @@ -1,19 +1,35 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.BlockVault; +import me.benrobson.blockvault.util.GeneratorUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; public class bvgenerate implements CommandExecutor { + private final BlockVault plugin; + + public bvgenerate(BlockVault plugin) { + this.plugin = plugin; + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("This command must be run by a player."); + return true; + } + if (!sender.hasPermission("blockvault.generate")) { - sender.sendMessage("§cYou don't have permission to use this command!"); + sender.sendMessage("You do not have permission to use this command."); return true; } - // Logic to generate the vault - sender.sendMessage("§aVault generated successfully!"); + FileConfiguration config = plugin.getConfig(); + GeneratorUtil.loadSchematic(config, plugin); + sender.sendMessage("Schematic is being generated. Check the console for details."); return true; } } diff --git a/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java b/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java new file mode 100644 index 0000000..4eca008 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java @@ -0,0 +1,66 @@ +package me.benrobson.blockvault.util; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.session.ClipboardHolder; +import com.sk89q.worldedit.world.World; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class GeneratorUtil { + + public static void loadSchematic(FileConfiguration config, Plugin plugin) { + // Load vault settings from config.yml + String worldName = config.getString("vault.location.world"); + int x = config.getInt("vault.location.x"); + int y = config.getInt("vault.location.y"); + int z = config.getInt("vault.location.z"); + String schematicPath = config.getString("vault.schematic.path"); + + org.bukkit.World bukkitWorld = Bukkit.getWorld(worldName); + if (bukkitWorld == null) { + plugin.getLogger().severe("World '" + worldName + "' is not loaded or does not exist."); + return; + } + + File schematicFile = new File(plugin.getDataFolder(), schematicPath); + if (!schematicFile.exists()) { + plugin.getLogger().severe("Schematic file '" + schematicPath + "' does not exist."); + return; + } + + // Load and paste the schematic + try (FileInputStream fis = new FileInputStream(schematicFile)) { + ClipboardFormat format = ClipboardFormats.findByFile(schematicFile); + if (format == null) { + plugin.getLogger().severe("Invalid schematic format for file: " + schematicPath); + return; + } + + ClipboardReader reader = format.getReader(fis); + Clipboard clipboard = reader.read(); + World worldEditWorld = BukkitAdapter.adapt(bukkitWorld); + BlockVector3 location = BlockVector3.at(x, y, z); + + try (EditSession editSession = com.sk89q.worldedit.WorldEdit.getInstance() + .newEditSession(worldEditWorld)) { + ClipboardHolder holder = new ClipboardHolder(clipboard); + holder.createPaste(editSession).to(location).build(); + plugin.getLogger().info("Schematic pasted successfully at: " + x + ", " + y + ", " + z); + } + } catch (IOException e) { + plugin.getLogger().severe("Failed to load schematic: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 70c09e9..b76042d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,7 +2,7 @@ name: BlockVault version: '1.0.0' main: me.benrobson.blockvault.BlockVault api-version: '1.21' -softdepend: [WorldEdit] +depend: [WorldEdit] commands: bvadd: description: Command From 387978c2016069bf4c1cc3d227cb1a2834b07f90 Mon Sep 17 00:00:00 2001 From: benrobson Date: Wed, 1 Jan 2025 13:44:45 +1100 Subject: [PATCH 05/28] Add base implementation for item add, submit and points. --- pom.xml | 2 +- .../me/benrobson/blockvault/BlockVault.java | 8 +- .../benrobson/blockvault/commands/bvadd.java | 25 ---- .../blockvault/commands/bvstart.java | 26 ++++- .../blockvault/commands/bvsubmit.java | 107 ++++++++++++++++++ .../blockvault/util/GeneratorUtil.java | 86 ++++++++------ .../benrobson/blockvault/util/VaultUtil.java | 76 +++++++++++++ src/main/resources/config.yml | 10 +- .../resources/{vault.yml => vault_items.yml} | 0 9 files changed, 269 insertions(+), 71 deletions(-) delete mode 100644 src/main/java/me/benrobson/blockvault/commands/bvadd.java create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvsubmit.java create mode 100644 src/main/java/me/benrobson/blockvault/util/VaultUtil.java rename src/main/resources/{vault.yml => vault_items.yml} (100%) diff --git a/pom.xml b/pom.xml index 7196de4..791c697 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ com.sk89q.worldedit worldedit-bukkit - 7.2.14 + 7.2.9 provided diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 4aaddb2..dbd93cb 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -1,6 +1,7 @@ package me.benrobson.blockvault; import me.benrobson.blockvault.commands.*; +import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.plugin.java.JavaPlugin; import static org.bukkit.Bukkit.getConsoleSender; @@ -12,13 +13,16 @@ public void onEnable() { getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§aPlugin is now enabled!"); // Register commands - getCommand("bvstart").setExecutor(new bvstart()); + getCommand("bvstart").setExecutor(new bvstart(this)); getCommand("bvgenerate").setExecutor(new bvgenerate(this)); - getCommand("bvadd").setExecutor(new bvadd()); + getCommand("bvadd").setExecutor(new bvsubmit(this)); getCommand("bvprogress").setExecutor(new bvprogress()); getCommand("bvleaderboard").setExecutor(new bvleaderboard()); // Register events + + saveDefaultConfig(); + VaultUtil.generateVaultItems(this); } @Override diff --git a/src/main/java/me/benrobson/blockvault/commands/bvadd.java b/src/main/java/me/benrobson/blockvault/commands/bvadd.java deleted file mode 100644 index a45b6bc..0000000 --- a/src/main/java/me/benrobson/blockvault/commands/bvadd.java +++ /dev/null @@ -1,25 +0,0 @@ -package me.benrobson.blockvault.commands; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; - -public class bvadd implements CommandExecutor { - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!sender.hasPermission("blockvault.add")) { - sender.sendMessage("§cYou don't have permission to use this command!"); - return true; - } - - if (args.length < 1) { - sender.sendMessage("§cUsage: /vault add "); - return true; - } - - // Logic to add the specified item/block - String block = args[0]; - sender.sendMessage("§aAdded " + block + " to the vault!"); - return true; - } -} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java index a4abf94..2599f96 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvstart.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -1,19 +1,39 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.VaultUtil; +import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; public class bvstart implements CommandExecutor { + private final Plugin plugin; + private final VaultUtil vaultUtil; + + public bvstart(Plugin plugin) { + this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin.getConfig()); + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Check for the necessary permission if (!sender.hasPermission("blockvault.start")) { sender.sendMessage("§cYou don't have permission to use this command!"); return true; } - // Logic to start the vault challenge - sender.sendMessage("§aVault challenge started!"); + // Check if the vault challenge has already been started + if (vaultUtil.hasStarted()) { + sender.sendMessage("§cThe vault challenge has already been started."); + return true; + } + + // Log the start date and time + vaultUtil.logVaultStart(); + plugin.saveConfig(); + sender.sendMessage(ChatColor.GREEN + "The vault challenge has started."); return true; } -} +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java new file mode 100644 index 0000000..f01d289 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java @@ -0,0 +1,107 @@ +package me.benrobson.blockvault.commands; + +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static me.benrobson.blockvault.util.VaultUtil.formatMaterialName; + +public class bvsubmit implements CommandExecutor { + private final Plugin plugin; + + public bvsubmit(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("§cOnly players can use this command!"); + return true; + } + + Player player = (Player) sender; + + if (!player.hasPermission("blockvault.add")) { + player.sendMessage("§cYou don't have permission to use this command!"); + return true; + } + + ItemStack itemInHand = player.getInventory().getItemInMainHand(); + + if (itemInHand == null || itemInHand.getType() == Material.AIR) { + player.sendMessage("§cYou are not holding any item or block!"); + return true; + } + + // Get item display name or fallback to material name + ItemMeta meta = itemInHand.getItemMeta(); + String displayName = (meta != null && meta.hasDisplayName()) ? meta.getDisplayName() : formatMaterialName(itemInHand.getType()); + String itemName = itemInHand.getType().name().toLowerCase(); + + // Load configuration files + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); + File configFile = new File(plugin.getDataFolder(), "config.yml"); + + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Check if item has already been collected globally + String collector = vaultData.getString("vault_data.global_collected_items." + itemName); + if (collector != null) { + player.sendMessage("§cItem " + displayName + " has already been collected by " + collector + "!"); + return true; + } + + // Add item to player's collected items + List playerItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); + if (playerItems.contains(itemName)) { + player.sendMessage("§cYou have already collected " + displayName + "!"); + return true; + } + + // Determine the item's point category and corresponding points + String category = vaultItems.getString("items." + itemName, "COMMON"); // Default to COMMON + int pointsPerCategory = config.getInt("points." + category, 1); // Default to 1 point + + playerItems.add(itemName); + vaultData.set("vault_data." + player.getName() + ".collected_items", playerItems); + + // Update player points + int currentPoints = vaultData.getInt("vault_data." + player.getName() + ".points", 0); + vaultData.set("vault_data." + player.getName() + ".points", currentPoints + pointsPerCategory); + + // Update global collected items + vaultData.set("vault_data.global_collected_items." + itemName, player.getName()); + + try { + vaultData.save(vaultDataFile); + + // Remove one item from the player's hand + if (itemInHand.getAmount() > 1) { + itemInHand.setAmount(itemInHand.getAmount() - 1); + } else { + player.getInventory().setItemInMainHand(null); + } + + player.sendMessage("§aSuccessfully added " + displayName + " to your vault! (§b+" + pointsPerCategory + " points§a)"); + } catch (IOException e) { + player.sendMessage("§cFailed to save collected item. Please try again."); + e.printStackTrace(); + } + + return true; + } +} diff --git a/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java b/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java index 4eca008..cb25085 100644 --- a/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java @@ -20,47 +20,63 @@ public class GeneratorUtil { public static void loadSchematic(FileConfiguration config, Plugin plugin) { - // Load vault settings from config.yml - String worldName = config.getString("vault.location.world"); - int x = config.getInt("vault.location.x"); - int y = config.getInt("vault.location.y"); - int z = config.getInt("vault.location.z"); - String schematicPath = config.getString("vault.schematic.path"); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String worldName = config.getString("vault.location.world"); + int x = config.getInt("vault.location.x"); + int y = config.getInt("vault.location.y"); + int z = config.getInt("vault.location.z"); + String schematicPath = config.getString("vault.schematic.path"); - org.bukkit.World bukkitWorld = Bukkit.getWorld(worldName); - if (bukkitWorld == null) { - plugin.getLogger().severe("World '" + worldName + "' is not loaded or does not exist."); - return; - } + // Validate configuration fields + if (worldName == null || schematicPath == null) { + plugin.getLogger().severe("Configuration error: Missing required fields 'vault.location.world' or 'vault.schematic.path'."); + return; + } - File schematicFile = new File(plugin.getDataFolder(), schematicPath); - if (!schematicFile.exists()) { - plugin.getLogger().severe("Schematic file '" + schematicPath + "' does not exist."); - return; - } + org.bukkit.World bukkitWorld = Bukkit.getWorld(worldName); + if (bukkitWorld == null) { + plugin.getLogger().severe("World '" + worldName + "' is not loaded or does not exist. Check if the world is properly loaded in the server."); + return; + } - // Load and paste the schematic - try (FileInputStream fis = new FileInputStream(schematicFile)) { - ClipboardFormat format = ClipboardFormats.findByFile(schematicFile); - if (format == null) { - plugin.getLogger().severe("Invalid schematic format for file: " + schematicPath); + File schematicFile = new File(plugin.getDataFolder(), schematicPath); + if (!schematicFile.exists()) { + plugin.getLogger().severe("Schematic file '" + schematicFile.getAbsolutePath() + "' does not exist. Ensure the file is in the correct location."); return; } - ClipboardReader reader = format.getReader(fis); - Clipboard clipboard = reader.read(); - World worldEditWorld = BukkitAdapter.adapt(bukkitWorld); - BlockVector3 location = BlockVector3.at(x, y, z); + plugin.getLogger().info("Starting schematic loading..."); + try (FileInputStream fis = new FileInputStream(schematicFile)) { + ClipboardFormat format = ClipboardFormats.findByFile(schematicFile); + if (format == null) { + plugin.getLogger().severe("Invalid schematic format for file: " + schematicPath); + return; + } + + ClipboardReader reader = format.getReader(fis); + Clipboard clipboard = reader.read(); + World worldEditWorld = BukkitAdapter.adapt(bukkitWorld); + BlockVector3 location = BlockVector3.at(x, y, z); - try (EditSession editSession = com.sk89q.worldedit.WorldEdit.getInstance() - .newEditSession(worldEditWorld)) { - ClipboardHolder holder = new ClipboardHolder(clipboard); - holder.createPaste(editSession).to(location).build(); - plugin.getLogger().info("Schematic pasted successfully at: " + x + ", " + y + ", " + z); + Bukkit.getScheduler().runTask(plugin, () -> { + try (EditSession editSession = com.sk89q.worldedit.WorldEdit.getInstance() + .newEditSession(worldEditWorld)) { + ClipboardHolder holder = new ClipboardHolder(clipboard); + holder.createPaste(editSession).to(location).ignoreAirBlocks(true).build(); + plugin.getLogger().info("Schematic pasted successfully at: " + x + ", " + y + ", " + z); + } catch (Exception e) { + plugin.getLogger().severe("Error pasting schematic: " + e.getMessage()); + e.printStackTrace(); + } + }); + } catch (IOException e) { + plugin.getLogger().severe("Failed to load schematic file: " + e.getMessage()); + plugin.getLogger().severe("Ensure the file is not corrupted and is in a valid format."); + e.printStackTrace(); + } catch (Exception e) { + plugin.getLogger().severe("An unexpected error occurred while loading the schematic: " + e.getMessage()); + e.printStackTrace(); } - } catch (IOException e) { - plugin.getLogger().severe("Failed to load schematic: " + e.getMessage()); - e.printStackTrace(); - } + }); } -} \ No newline at end of file +} diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java new file mode 100644 index 0000000..9cbb83c --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -0,0 +1,76 @@ +package me.benrobson.blockvault.util; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class VaultUtil { + + private final FileConfiguration config; + + public VaultUtil(FileConfiguration config) { + this.config = config; + } + + // Method to check if the vault has started + public boolean hasStarted() { + return config.getBoolean("vault.started", false); + } + + // Method to log the start time of the vault challenge + public void logVaultStart() { + String currentDateTime = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + config.set("vault.start-time", currentDateTime); + config.set("vault.started", true); + } + + public static void generateVaultItems(Plugin plugin) { + File file = new File(plugin.getDataFolder(), "vault_items.yml"); + + if (file.exists()) { + plugin.getLogger().info("vault_items.yml already exists. Skipping generation."); + return; + } + + Map itemData = new HashMap<>(); + + for (Material material : Material.values()) { + // Add materials that are either blocks or items + if (material.isBlock() || material.isItem()) { + itemData.put(material.name().toLowerCase(), "COMMON"); // Default category + } + } + + YamlConfiguration yaml = new YamlConfiguration(); + yaml.createSection("items", itemData); + + try { + yaml.save(file); + plugin.getLogger().info("Generated vault_items.yml with all blocks and items set to default category COMMON."); + } catch (IOException e) { + plugin.getLogger().severe("Failed to save vault_items.yml: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Formats a Material name to a user-friendly display format. + * + * @param material The Material to format. + * @return The formatted name. + */ + public static String formatMaterialName(Material material) { + String[] words = material.name().toLowerCase().split("_"); + StringBuilder formattedName = new StringBuilder(); + for (String word : words) { + formattedName.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)).append(" "); + } + return formattedName.toString().trim(); + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index abcf71a..f36f237 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,6 +3,8 @@ # Vault settings vault: + start-time: "2025-01-01 14:30:00" + started: false location: # Coordinates for the vault's center point x: 100 @@ -17,11 +19,9 @@ vault: # Points system settings points: - # Base points for each block type - base_points: - common: 1 # Points for common blocks (e.g., Cobblestone, Oak Logs) - uncommon: 5 # Points for uncommon blocks (e.g., Prismarine, Obsidian) - rare: 10 # Points for rare blocks/items (e.g., Dragon Egg, Netherite Block) + common: 1 # Points for common blocks (e.g., Cobblestone, Oak Logs) + uncommon: 5 # Points for uncommon blocks (e.g., Prismarine, Obsidian) + rare: 10 # Points for rare blocks/items (e.g., Dragon Egg, Netherite Block) # Language settings lang: diff --git a/src/main/resources/vault.yml b/src/main/resources/vault_items.yml similarity index 100% rename from src/main/resources/vault.yml rename to src/main/resources/vault_items.yml From 5c77efc46369baaeef1a9186e3879606efbea75b Mon Sep 17 00:00:00 2001 From: benrobson Date: Wed, 1 Jan 2025 18:35:42 +1100 Subject: [PATCH 06/28] Removed generate and started logic for leaderboard and progress. --- .../me/benrobson/blockvault/BlockVault.java | 7 +- .../blockvault/commands/bvgenerate.java | 35 -------- .../blockvault/commands/bvleaderboard.java | 49 ++++++++++- .../blockvault/commands/bvprogress.java | 74 ++++++++++++++++- .../blockvault/commands/bvstart.java | 5 +- .../blockvault/commands/bvsubmit.java | 9 +- .../blockvault/util/GeneratorUtil.java | 82 ------------------- .../benrobson/blockvault/util/VaultUtil.java | 29 +++++++ src/main/resources/config.yml | 13 ++- src/main/resources/plugin.yml | 7 +- 10 files changed, 167 insertions(+), 143 deletions(-) delete mode 100644 src/main/java/me/benrobson/blockvault/commands/bvgenerate.java delete mode 100644 src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index dbd93cb..1c07624 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -14,10 +14,9 @@ public void onEnable() { // Register commands getCommand("bvstart").setExecutor(new bvstart(this)); - getCommand("bvgenerate").setExecutor(new bvgenerate(this)); - getCommand("bvadd").setExecutor(new bvsubmit(this)); - getCommand("bvprogress").setExecutor(new bvprogress()); - getCommand("bvleaderboard").setExecutor(new bvleaderboard()); + getCommand("bvsubmit").setExecutor(new bvsubmit(this)); + getCommand("bvprogress").setExecutor(new bvprogress(this)); + getCommand("bvleaderboard").setExecutor(new bvleaderboard(this)); // Register events diff --git a/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java b/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java deleted file mode 100644 index d07b84f..0000000 --- a/src/main/java/me/benrobson/blockvault/commands/bvgenerate.java +++ /dev/null @@ -1,35 +0,0 @@ -package me.benrobson.blockvault.commands; - -import me.benrobson.blockvault.BlockVault; -import me.benrobson.blockvault.util.GeneratorUtil; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.Player; - -public class bvgenerate implements CommandExecutor { - private final BlockVault plugin; - - public bvgenerate(BlockVault plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("This command must be run by a player."); - return true; - } - - if (!sender.hasPermission("blockvault.generate")) { - sender.sendMessage("You do not have permission to use this command."); - return true; - } - - FileConfiguration config = plugin.getConfig(); - GeneratorUtil.loadSchematic(config, plugin); - sender.sendMessage("Schematic is being generated. Check the console for details."); - return true; - } -} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java index 7910c79..89ccd02 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java @@ -3,12 +3,55 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.util.*; public class bvleaderboard implements CommandExecutor { + private final Plugin plugin; + + public bvleaderboard(Plugin plugin) { + this.plugin = plugin; + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - // Logic to display leaderboard - sender.sendMessage("§aVault Leaderboard:\n1. Player1 - 100 points\n2. Player2 - 80 points"); + if (!(sender instanceof Player)) { + sender.sendMessage("§cOnly players can view the leaderboard!"); + return true; + } + + // Load the player data file + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + // Get all player points data + Set playerNames = vaultData.getConfigurationSection("vault_data").getKeys(false); + Map playerPoints = new HashMap<>(); + + for (String playerName : playerNames) { + int points = vaultData.getInt("vault_data." + playerName + ".points"); + playerPoints.put(playerName, points); + } + + // Sort players by points in descending order + List> sortedPlayers = new ArrayList<>(playerPoints.entrySet()); + sortedPlayers.sort((entry1, entry2) -> Integer.compare(entry2.getValue(), entry1.getValue())); + + // Display top 10 players + sender.sendMessage("§aVault Leaderboard:"); + + int topPlayersCount = Math.min(10, sortedPlayers.size()); + for (int i = 0; i < topPlayersCount; i++) { + Map.Entry entry = sortedPlayers.get(i); + String playerName = entry.getKey(); + int points = entry.getValue(); + sender.sendMessage("§f" + (i + 1) + ". " + playerName + " - " + points + " points"); + } + return true; } -} +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java index e543963..a8a053d 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -1,14 +1,84 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.File; +import java.util.List; public class bvprogress implements CommandExecutor { + private final Plugin plugin; + private final VaultUtil vaultUtil; + + public bvprogress(Plugin plugin) { + this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin.getConfig()); + } + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - // Logic to display progress - sender.sendMessage("§aVault Progress: 75% complete."); + if (!(sender instanceof Player)) { + sender.sendMessage("§cOnly players can use this command!"); + return true; + } + + if (!vaultUtil.hasStarted()) { + sender.sendMessage("§cYou cannot check progress as the Vault has not been opened yet."); + return true; + } + + Player player = (Player) sender; + + // Load vault items and vault data + File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + + YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + // Get the total number of items in the vault + int totalItems = vaultItems.getConfigurationSection("items").getKeys(false).size(); + + // Get the total number of collected items for this player + List playerCollectedItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); + int collectedItemCount = playerCollectedItems.size(); + + // Calculate the percentage of progress + int progressPercentage = (int) ((double) collectedItemCount / totalItems * 100); + + // Create a progress bar + StringBuilder progressBar = new StringBuilder("§a["); + int barLength = 20; + int progressLength = (int) ((progressPercentage / 100.0) * barLength); + for (int i = 0; i < barLength; i++) { + if (i < progressLength) { + progressBar.append("§a#"); + } else { + progressBar.append("§7#"); + } + } + progressBar.append("§a]"); + + // Display progress + player.sendMessage("§aVault Progress: " + progressPercentage + "% complete."); + player.sendMessage(progressBar.toString()); + + // Display the last 5 contributions + List recentContributions = vaultData.getStringList("vault_data." + player.getName() + ".recent_contributions"); + if (recentContributions.isEmpty()) { + player.sendMessage("§cNo recent contributions."); + } else { + player.sendMessage("§eLast 5 Contributions:"); + for (int i = Math.max(0, recentContributions.size() - 5); i < recentContributions.size(); i++) { + player.sendMessage("§7- " + recentContributions.get(i)); + } + } + return true; } } diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java index 2599f96..958fc7c 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvstart.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -24,16 +24,15 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } - // Check if the vault challenge has already been started if (vaultUtil.hasStarted()) { - sender.sendMessage("§cThe vault challenge has already been started."); + sender.sendMessage("§cYou cannot submit items as the Vault has not been opened yet."); return true; } // Log the start date and time vaultUtil.logVaultStart(); plugin.saveConfig(); - sender.sendMessage(ChatColor.GREEN + "The vault challenge has started."); + sender.sendMessage(ChatColor.GREEN + "The vault challenge has started, the vault is open!."); return true; } } \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java index f01d289..a8b2de0 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -15,12 +16,15 @@ import java.util.List; import static me.benrobson.blockvault.util.VaultUtil.formatMaterialName; +import static me.benrobson.blockvault.util.VaultUtil.updateRecentContributions; public class bvsubmit implements CommandExecutor { private final Plugin plugin; + private final VaultUtil vaultUtil; public bvsubmit(Plugin plugin) { this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin.getConfig()); } @Override @@ -32,8 +36,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St Player player = (Player) sender; - if (!player.hasPermission("blockvault.add")) { - player.sendMessage("§cYou don't have permission to use this command!"); + if (vaultUtil.hasStarted()) { + sender.sendMessage("§cYou cannot submit items as the Vault has not been opened yet."); return true; } @@ -77,6 +81,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St int pointsPerCategory = config.getInt("points." + category, 1); // Default to 1 point playerItems.add(itemName); + updateRecentContributions(player, itemName, plugin); vaultData.set("vault_data." + player.getName() + ".collected_items", playerItems); // Update player points diff --git a/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java b/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java deleted file mode 100644 index cb25085..0000000 --- a/src/main/java/me/benrobson/blockvault/util/GeneratorUtil.java +++ /dev/null @@ -1,82 +0,0 @@ -package me.benrobson.blockvault.util; - -import com.sk89q.worldedit.EditSession; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; -import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; -import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; -import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.session.ClipboardHolder; -import com.sk89q.worldedit.world.World; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.plugin.Plugin; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; - -public class GeneratorUtil { - - public static void loadSchematic(FileConfiguration config, Plugin plugin) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { - String worldName = config.getString("vault.location.world"); - int x = config.getInt("vault.location.x"); - int y = config.getInt("vault.location.y"); - int z = config.getInt("vault.location.z"); - String schematicPath = config.getString("vault.schematic.path"); - - // Validate configuration fields - if (worldName == null || schematicPath == null) { - plugin.getLogger().severe("Configuration error: Missing required fields 'vault.location.world' or 'vault.schematic.path'."); - return; - } - - org.bukkit.World bukkitWorld = Bukkit.getWorld(worldName); - if (bukkitWorld == null) { - plugin.getLogger().severe("World '" + worldName + "' is not loaded or does not exist. Check if the world is properly loaded in the server."); - return; - } - - File schematicFile = new File(plugin.getDataFolder(), schematicPath); - if (!schematicFile.exists()) { - plugin.getLogger().severe("Schematic file '" + schematicFile.getAbsolutePath() + "' does not exist. Ensure the file is in the correct location."); - return; - } - - plugin.getLogger().info("Starting schematic loading..."); - try (FileInputStream fis = new FileInputStream(schematicFile)) { - ClipboardFormat format = ClipboardFormats.findByFile(schematicFile); - if (format == null) { - plugin.getLogger().severe("Invalid schematic format for file: " + schematicPath); - return; - } - - ClipboardReader reader = format.getReader(fis); - Clipboard clipboard = reader.read(); - World worldEditWorld = BukkitAdapter.adapt(bukkitWorld); - BlockVector3 location = BlockVector3.at(x, y, z); - - Bukkit.getScheduler().runTask(plugin, () -> { - try (EditSession editSession = com.sk89q.worldedit.WorldEdit.getInstance() - .newEditSession(worldEditWorld)) { - ClipboardHolder holder = new ClipboardHolder(clipboard); - holder.createPaste(editSession).to(location).ignoreAirBlocks(true).build(); - plugin.getLogger().info("Schematic pasted successfully at: " + x + ", " + y + ", " + z); - } catch (Exception e) { - plugin.getLogger().severe("Error pasting schematic: " + e.getMessage()); - e.printStackTrace(); - } - }); - } catch (IOException e) { - plugin.getLogger().severe("Failed to load schematic file: " + e.getMessage()); - plugin.getLogger().severe("Ensure the file is not corrupted and is in a valid format."); - e.printStackTrace(); - } catch (Exception e) { - plugin.getLogger().severe("An unexpected error occurred while loading the schematic: " + e.getMessage()); - e.printStackTrace(); - } - }); - } -} diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index 9cbb83c..7cbbbd0 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -3,11 +3,13 @@ import org.bukkit.Material; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import java.io.File; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; public class VaultUtil { @@ -73,4 +75,31 @@ public static String formatMaterialName(Material material) { } return formattedName.toString().trim(); } + + public static void updateRecentContributions(Player player, String itemName, Plugin plugin) { + // Load the vault data file + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + // Get the current list of recent contributions for the player + String playerPath = "vault_data." + player.getName() + ".recent_contributions"; + List recentContributions = vaultData.getStringList(playerPath); + + // Ensure the list has no more than 5 entries + if (recentContributions.size() >= 5) { + recentContributions.remove(0); // Remove the oldest contribution + } + + // Add the new contribution to the list + recentContributions.add(player.getName() + " collected " + itemName); + + // Save the updated list back into the YML file + vaultData.set(playerPath, recentContributions); + try { + vaultData.save(vaultDataFile); + } catch (IOException e) { + player.sendMessage("§cFailed to save recent contributions."); + e.printStackTrace(); + } + } } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f36f237..360b42e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -12,16 +12,15 @@ vault: z: 100 world: world - # Schematic settings for the vault design - schematic: - # Path to the schematic file - path: "vault.schem" +timer: + enabled: false + timer-end: "2025-01-01 14:30:00" # Points system settings points: - common: 1 # Points for common blocks (e.g., Cobblestone, Oak Logs) - uncommon: 5 # Points for uncommon blocks (e.g., Prismarine, Obsidian) - rare: 10 # Points for rare blocks/items (e.g., Dragon Egg, Netherite Block) + common: 1 + uncommon: 5 + rare: 10 # Language settings lang: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b76042d..2630d5b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,12 +4,9 @@ main: me.benrobson.blockvault.BlockVault api-version: '1.21' depend: [WorldEdit] commands: - bvadd: + bvsubmit: description: Command - usage: /bvadd - bvgenerate: - description: Command - usage: /bvgenerate + usage: /bvsubmit bvleaderboard: description: Command usage: /bvleaderboard From 2c9bf552fc84423c0a131505d53d17354b6b70b0 Mon Sep 17 00:00:00 2001 From: benrobson Date: Wed, 1 Jan 2025 21:51:19 +1100 Subject: [PATCH 07/28] Added FileUtil, and updated commands to factor. --- README.md | 10 +-- .../me/benrobson/blockvault/BlockVault.java | 5 ++ .../blockvault/commands/bvleaderboard.java | 17 +++- .../blockvault/commands/bvprogress.java | 3 +- .../blockvault/commands/bvstart.java | 3 +- .../blockvault/commands/bvsubmit.java | 3 +- .../benrobson/blockvault/util/FileUtil.java | 23 ++++++ .../benrobson/blockvault/util/VaultUtil.java | 82 +++++++++++++++++-- 8 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/main/java/me/benrobson/blockvault/util/FileUtil.java diff --git a/README.md b/README.md index 448dfa4..565ca23 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ -## BlockVault +# BlockVault ![img](https://i.imgur.com/DPcMoaq.png) BlockVault is an interactive challenge where players collaborate to collect every unique block and item in Minecraft. The centerpiece is a large, dynamic museum building where contributions are tracked, and progress is displayed, turning the collection into an engaging and rewarding experience. ## Features -### Dynamic Building Generation -* Generates a polished, pre-designed museum building at a designated location using schematics. -* Customizable layouts for server-specific aesthetics or a default design for simplicity. - ### Interactive Block Displays * Sections dedicated to specific block categories (e.g., wood, ores, decorative blocks). * Blocks and items can be placed on predefined slots with item frames or display stands, indicating where each item belongs and can be contributed to. @@ -29,10 +25,6 @@ BlockVault is an interactive challenge where players collaborate to collect ever * Progress can be viewed in real-time using a hologram display, showcasing the top contributors and collection milestones. * Use the `/vault leaderboard` command to see a detailed leaderboard of top contributors, including points and blocks contributed. * GUI integration allows players to easily browse and view all collected blocks in an organized interface. -* Add categories to the leaderboard: - * Most blocks contributed today/this week. - * Most rare blocks contributed. - * Category-specific leaders (e.g., wood, ores). ### Broadcast Announcements * **Rare Blocks/Items Announcements:** Announce server-wide when a rare block or item is added to the vault, celebrating player contributions and encouraging participation. diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 1c07624..2453385 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -1,7 +1,9 @@ package me.benrobson.blockvault; import me.benrobson.blockvault.commands.*; +import me.benrobson.blockvault.util.FileUtil; import me.benrobson.blockvault.util.VaultUtil; +import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; import static org.bukkit.Bukkit.getConsoleSender; @@ -12,6 +14,9 @@ public final class BlockVault extends JavaPlugin { public void onEnable() { getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§aPlugin is now enabled!"); + FileUtil fileUtil = new FileUtil(this); + YamlConfiguration config = fileUtil.getConfig(); + // Register commands getCommand("bvstart").setExecutor(new bvstart(this)); getCommand("bvsubmit").setExecutor(new bvsubmit(this)); diff --git a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java index 89ccd02..b791200 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -12,9 +13,11 @@ public class bvleaderboard implements CommandExecutor { private final Plugin plugin; + private final VaultUtil vaultUtil; public bvleaderboard(Plugin plugin) { this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin); } @Override @@ -24,10 +27,21 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + if (!vaultUtil.hasStarted()) { + sender.sendMessage("§cYou cannot check the leaderboard as the Vault has not been opened yet."); + return true; + } + // Load the player data file File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + // Check if 'vault_data' section exists + if (vaultData.getConfigurationSection("vault_data") == null) { + sender.sendMessage("§cNo player data found! Cannot produce leaderboard."); + return true; + } + // Get all player points data Set playerNames = vaultData.getConfigurationSection("vault_data").getKeys(false); Map playerPoints = new HashMap<>(); @@ -43,6 +57,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St // Display top 10 players sender.sendMessage("§aVault Leaderboard:"); + sender.sendMessage("§a================================="); int topPlayersCount = Math.min(10, sortedPlayers.size()); for (int i = 0; i < topPlayersCount; i++) { @@ -54,4 +69,4 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } -} \ No newline at end of file +} diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java index a8a053d..827e23d 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.FileUtil; import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -17,7 +18,7 @@ public class bvprogress implements CommandExecutor { public bvprogress(Plugin plugin) { this.plugin = plugin; - this.vaultUtil = new VaultUtil(plugin.getConfig()); + this.vaultUtil = new VaultUtil(plugin); } @Override diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java index 958fc7c..8a2aaab 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvstart.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.FileUtil; import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.ChatColor; import org.bukkit.command.Command; @@ -13,7 +14,7 @@ public class bvstart implements CommandExecutor { public bvstart(Plugin plugin) { this.plugin = plugin; - this.vaultUtil = new VaultUtil(plugin.getConfig()); + this.vaultUtil = new VaultUtil(plugin); } @Override diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java index a8b2de0..e581e86 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java @@ -1,5 +1,6 @@ package me.benrobson.blockvault.commands; +import me.benrobson.blockvault.util.FileUtil; import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.Material; import org.bukkit.command.Command; @@ -24,7 +25,7 @@ public class bvsubmit implements CommandExecutor { public bvsubmit(Plugin plugin) { this.plugin = plugin; - this.vaultUtil = new VaultUtil(plugin.getConfig()); + this.vaultUtil = new VaultUtil(plugin); } @Override diff --git a/src/main/java/me/benrobson/blockvault/util/FileUtil.java b/src/main/java/me/benrobson/blockvault/util/FileUtil.java new file mode 100644 index 0000000..410ef05 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/util/FileUtil.java @@ -0,0 +1,23 @@ +package me.benrobson.blockvault.util; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import java.io.File; + +public class FileUtil { + + private final Plugin plugin; // Make it an instance variable + + public FileUtil(Plugin plugin) { + this.plugin = plugin; + } + + /** + * Returns the plugin's configuration. + * + * @return The plugin's configuration. + */ + public YamlConfiguration getConfig() { + return YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "config.yml")); + } +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index 7cbbbd0..8211a87 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -8,28 +8,30 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class VaultUtil { + private final Plugin plugin; + private final FileUtil fileUtil; - private final FileConfiguration config; - - public VaultUtil(FileConfiguration config) { - this.config = config; + public VaultUtil(Plugin plugin) { + this.plugin = plugin; + this.fileUtil = new FileUtil(plugin); // Pass the plugin instance to FileUtil } // Method to check if the vault has started public boolean hasStarted() { - return config.getBoolean("vault.started", false); + return fileUtil.getConfig().getBoolean("vault.started", false); // Use fileUtil to access the config } // Method to log the start time of the vault challenge public void logVaultStart() { String currentDateTime = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - config.set("vault.start-time", currentDateTime); - config.set("vault.started", true); + fileUtil.getConfig().set("vault.start-time", currentDateTime); + fileUtil.getConfig().set("vault.started", true); } public static void generateVaultItems(Plugin plugin) { @@ -102,4 +104,68 @@ public static void updateRecentContributions(Player player, String itemName, Plu e.printStackTrace(); } } -} \ No newline at end of file + + public List getTopPlayers() { + // Load vault data to get the collected items count per player + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + Map playerProgress = new HashMap<>(); + + // Loop through each player and count their collected items + for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { + List collectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); + playerProgress.put(playerName, collectedItems.size()); + } + + // Sort players by their collected items (highest to lowest) + List> sortedPlayers = new ArrayList<>(playerProgress.entrySet()); + sortedPlayers.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())); // Sort in descending order + + // Get the top 5 players + List topPlayers = new ArrayList<>(); + for (int i = 0; i < Math.min(5, sortedPlayers.size()); i++) { + topPlayers.add(sortedPlayers.get(i).getKey()); + } + + return topPlayers; + } + + /** + * Gets the total number of items in the vault. + * + * @return The total number of items in the vault. + */ + private int getTotalVaultItems() { + File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); + YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); + return vaultItems.getConfigurationSection("items").getKeys(false).size(); + } + + /** + * Gets the number of collected items by a player. + * + * @return The total number of collected items. + */ + private int getCollectedItemsCount() { + // Load vault data and calculate total collected items + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + int collectedItems = 0; + for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { + collectedItems += vaultData.getStringList("vault_data." + playerName + ".collected_items").size(); + } + return collectedItems; + } + + /** + * Get the initial countdown time in seconds. + * + * @return The initial countdown time in seconds. + */ + private int getInitialCountdownTime() { + // Assuming that the duration is set somewhere in your config or hardcoded + return plugin.getConfig().getInt("challenge.duration_in_seconds", 600); // Default to 10 minutes + } +} From 32c99516bf58ee29ec63c6dd4411df7786593ea0 Mon Sep 17 00:00:00 2001 From: benrobson Date: Fri, 3 Jan 2025 19:27:17 +1100 Subject: [PATCH 08/28] Added dynamic update via item frame, added a scheduler for function and command. --- README.md | 6 +- dependency-reduced-pom.xml | 71 ++++++++++ .../me/benrobson/blockvault/BlockVault.java | 9 +- .../blockvault/commands/bvprogress.java | 1 - .../blockvault/commands/bvstart.java | 24 +++- .../blockvault/commands/bvsubmit.java | 2 +- .../blockvault/commands/bvupdatestate.java | 45 +++++++ .../blockvault/util/ScheduleUtil.java | 22 ++++ .../benrobson/blockvault/util/VaultUtil.java | 121 ++++++++++++++++-- src/main/resources/config.yml | 62 +++++++-- src/main/resources/plugin.yml | 4 +- 11 files changed, 337 insertions(+), 30 deletions(-) create mode 100644 dependency-reduced-pom.xml create mode 100644 src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java create mode 100644 src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java diff --git a/README.md b/README.md index 565ca23..01b1a7f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ BlockVault is an interactive challenge where players collaborate to collect ever ### Points System * Players earn points for each block they unlock and contribute to the museum. * The points awarded vary based on the rarity of the block/item: - * **Common Blocks:** 1 point per block (e.g., Cobblestone, Oak Logs). - * **Uncommon Blocks:** 5 points per block (e.g., Prismarine, Obsidian). - * **Rare Blocks/Items:** 10+ points per block (e.g., Dragon Egg, Netherite Block). + * **Common Blocks:** 1 point per block. + * **Uncommon Blocks:** 5 points per block. + * **Rare Blocks/Items:** 10+ points per block. * A running total of points is tracked for each player, contributing to their leaderboard position. ### Tracking Progress diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml new file mode 100644 index 0000000..e83815c --- /dev/null +++ b/dependency-reduced-pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + me.benrobson + blockvault + blockvault + 1.0.0 + + clean package + + + true + src/main/resources + + + + + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + maven-shade-plugin + 3.5.3 + + + package + + shade + + + + + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + + enginehub-maven + https://maven.enginehub.org/repo/ + + + + + io.papermc.paper + paper-api + 1.21.3-R0.1-SNAPSHOT + provided + + + com.sk89q.worldedit + worldedit-bukkit + 7.2.9 + provided + + + + 21 + UTF-8 + + diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 2453385..46068d7 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -2,10 +2,15 @@ import me.benrobson.blockvault.commands.*; import me.benrobson.blockvault.util.FileUtil; +import me.benrobson.blockvault.util.ScheduleUtil; import me.benrobson.blockvault.util.VaultUtil; +import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; +import static me.benrobson.blockvault.util.ScheduleUtil.scheduleVaultStateTask; import static org.bukkit.Bukkit.getConsoleSender; public final class BlockVault extends JavaPlugin { @@ -15,18 +20,20 @@ public void onEnable() { getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§aPlugin is now enabled!"); FileUtil fileUtil = new FileUtil(this); - YamlConfiguration config = fileUtil.getConfig(); + VaultUtil vaultUtil = new VaultUtil(this); // Register commands getCommand("bvstart").setExecutor(new bvstart(this)); getCommand("bvsubmit").setExecutor(new bvsubmit(this)); getCommand("bvprogress").setExecutor(new bvprogress(this)); getCommand("bvleaderboard").setExecutor(new bvleaderboard(this)); + getCommand("bvupdatestate").setExecutor(new bvupdatestate(this)); // Register events saveDefaultConfig(); VaultUtil.generateVaultItems(this); + scheduleVaultStateTask(this, vaultUtil); } @Override diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java index 827e23d..232a485 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -1,6 +1,5 @@ package me.benrobson.blockvault.commands; -import me.benrobson.blockvault.util.FileUtil; import me.benrobson.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java index 8a2aaab..58fd342 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvstart.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -11,29 +11,43 @@ public class bvstart implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; + private final FileUtil fileUtil; public bvstart(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); + this.fileUtil = new FileUtil(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { // Check for the necessary permission if (!sender.hasPermission("blockvault.start")) { - sender.sendMessage("§cYou don't have permission to use this command!"); + sender.sendMessage(ChatColor.RED + "You don't have permission to use this command!"); return true; } + // Check if the vault has already started if (vaultUtil.hasStarted()) { - sender.sendMessage("§cYou cannot submit items as the Vault has not been opened yet."); + sender.sendMessage(ChatColor.RED + "The Vault challenge has already started."); return true; } // Log the start date and time - vaultUtil.logVaultStart(); - plugin.saveConfig(); - sender.sendMessage(ChatColor.GREEN + "The vault challenge has started, the vault is open!."); + try { + String currentDateTime = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + + // Update the configuration + fileUtil.getConfig().set("vault.start-time", currentDateTime); + fileUtil.getConfig().set("vault.started", true); + + sender.sendMessage(ChatColor.GREEN + "The vault challenge has started, the vault is open!"); + } catch (Exception e) { + sender.sendMessage(ChatColor.RED + "An error occurred while starting the vault."); + e.printStackTrace(); + } + return true; } } \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java index e581e86..c0a304f 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java @@ -37,7 +37,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St Player player = (Player) sender; - if (vaultUtil.hasStarted()) { + if (!vaultUtil.hasStarted()) { sender.sendMessage("§cYou cannot submit items as the Vault has not been opened yet."); return true; } diff --git a/src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java b/src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java new file mode 100644 index 0000000..b8f445e --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java @@ -0,0 +1,45 @@ +package me.benrobson.blockvault.commands; + +import me.benrobson.blockvault.util.FileUtil; +import me.benrobson.blockvault.util.VaultUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +public class bvupdatestate implements CommandExecutor { + private final Plugin plugin; + private final VaultUtil vaultUtil; + private final FileUtil fileUtil; + + public bvupdatestate(Plugin plugin) { + this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin); + this.fileUtil = new FileUtil(plugin); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage("§cOnly players can use this command!"); + return true; + } + + Player player = (Player) sender; + + if (!player.hasPermission("blockvault.updatestate")) { + sender.sendMessage("§cYou don't have permission to use this command!"); + return true; + } + + vaultUtil.updateVaultState(player); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java b/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java new file mode 100644 index 0000000..a83a696 --- /dev/null +++ b/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java @@ -0,0 +1,22 @@ +package me.benrobson.blockvault.util; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +public class ScheduleUtil { + private final Plugin plugin; + private final VaultUtil vaultUtil; + private final FileUtil fileUtil; + + public ScheduleUtil(Plugin plugin) { + this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin); + this.fileUtil = new FileUtil(plugin); + } + + public static void scheduleVaultStateTask(Plugin plugin, VaultUtil vaultUtil) { + Bukkit.getScheduler().runTaskTimer(plugin, () -> { + vaultUtil.updateVaultState(null); + }, 0L, 20L * 60L * 5L); // Run every 5 minutes + } +} diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index 8211a87..b829406 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -1,8 +1,13 @@ package me.benrobson.blockvault.util; +import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.ItemFrame; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; @@ -19,7 +24,7 @@ public class VaultUtil { public VaultUtil(Plugin plugin) { this.plugin = plugin; - this.fileUtil = new FileUtil(plugin); // Pass the plugin instance to FileUtil + this.fileUtil = new FileUtil(plugin); } // Method to check if the vault has started @@ -27,13 +32,6 @@ public boolean hasStarted() { return fileUtil.getConfig().getBoolean("vault.started", false); // Use fileUtil to access the config } - // Method to log the start time of the vault challenge - public void logVaultStart() { - String currentDateTime = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - fileUtil.getConfig().set("vault.start-time", currentDateTime); - fileUtil.getConfig().set("vault.started", true); - } - public static void generateVaultItems(Plugin plugin) { File file = new File(plugin.getDataFolder(), "vault_items.yml"); @@ -168,4 +166,111 @@ private int getInitialCountdownTime() { // Assuming that the duration is set somewhere in your config or hardcoded return plugin.getConfig().getInt("challenge.duration_in_seconds", 600); // Default to 10 minutes } + + /** + * Checks if a specific item has been added to the vault by any player. + * + * @param itemName The name of the item to check. + * @return True if the item has been added, false otherwise. + */ + public boolean isItemAdded(String itemName) { + // Normalize item name + itemName = itemName.toLowerCase(); + + File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); + YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); + + if (!vaultData.contains("vault_data")) { + return false; + } + + for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { + List collectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); + + for (String collectedItem : collectedItems) { + if (collectedItem.toLowerCase().equals(itemName)) { + return true; + } + } + } + + return false; + } + + public void updateVaultState(Player player) { + try { + // Parse region coordinates from config + int upperX = fileUtil.getConfig().getInt("vault.region.upperregion.X"); + int upperY = fileUtil.getConfig().getInt("vault.region.upperregion.Y"); + int upperZ = fileUtil.getConfig().getInt("vault.region.upperregion.Z"); + + int lowerX = fileUtil.getConfig().getInt("vault.region.lowerregion.X"); + int lowerY = fileUtil.getConfig().getInt("vault.region.lowerregion.Y"); + int lowerZ = fileUtil.getConfig().getInt("vault.region.lowerregion.Z"); + + World world = Bukkit.getWorlds().get(0); + if (world == null) { + String errorMsg = "§cWorld not found! Stopping the update process."; + Bukkit.getLogger().warning(errorMsg); + if (player != null) player.sendMessage(errorMsg); + return; + } + + // Normalize coordinates + int minX = Math.min(upperX, lowerX); + int maxX = Math.max(upperX, lowerX); + int minY = Math.min(upperY, lowerY); + int maxY = Math.max(upperY, lowerY); + int minZ = Math.min(upperZ, lowerZ); + int maxZ = Math.max(upperZ, lowerZ); + + int foundCount = 0; + int updatedCount = 0; + + // Iterate over entities in the specified region + for (Entity entity : world.getEntities()) { + if (entity instanceof ItemFrame) { + Location loc = entity.getLocation(); + if (loc.getX() >= minX && loc.getX() <= maxX && + loc.getY() >= minY && loc.getY() <= maxY && + loc.getZ() >= minZ && loc.getZ() <= maxZ) { + + ItemFrame itemFrame = (ItemFrame) entity; + if (itemFrame.getItem() != null && itemFrame.getItem().getType() != Material.AIR) { + String itemName = itemFrame.getItem().getType().toString().toLowerCase(); + + boolean isInVault = isItemAdded(itemName); + String frameMessage = String.format("§aItemFrame at [%d, %d, %d] has item: %s (In Vault: %s).", + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), itemName, isInVault ? "Yes" : "No"); + Bukkit.getLogger().info(frameMessage); + if (player != null) player.sendMessage(frameMessage); + + // Get the block behind the item frame + Location blockBehindLoc = itemFrame.getLocation().getBlock().getRelative(itemFrame.getAttachedFace()).getLocation(); + Material newMaterial = isInVault ? Material.LIME_STAINED_GLASS : Material.RED_STAINED_GLASS; + blockBehindLoc.getBlock().setType(newMaterial); + + String updateMessage = String.format("§eBlock behind ItemFrame at [%d, %d, %d] replaced with %s.", + loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), newMaterial.name()); + Bukkit.getLogger().info(updateMessage); + if (player != null) player.sendMessage(updateMessage); + + foundCount++; + updatedCount++; + } + } + } + } + + String summaryMessage = String.format("§aFound %d item frames and updated %d blocks in the specified region.", + foundCount, updatedCount); + Bukkit.getLogger().info(summaryMessage); + if (player != null) player.sendMessage(summaryMessage); + } catch (Exception e) { + String errorMessage = "§cAn error occurred while processing the region."; + Bukkit.getLogger().severe(errorMessage); + e.printStackTrace(); + if (player != null) player.sendMessage(errorMessage); + } + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 360b42e..3dcb6e9 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,20 +1,62 @@ -# BlockVault Configuration -# Configures the BlockVault, collection goals, progress tracking, and rewards system. +# BlockVault # Vault settings vault: - start-time: "2025-01-01 14:30:00" + start-time: null started: false - location: - # Coordinates for the vault's center point - x: 100 - y: 64 - z: 100 - world: world + region: + upperregion: + X: -138 + Y: 97 + Z: 118 + lowerregion: + X: 133 + Y: 68 + Z: 96 + excludeditems: + - debug_stick + - structure_block + - barrier + - command_block + - chain_command_block + - repeating_command_block + - light + - jigsaw + - knowledge_book + - spawner + - end_portal_frame + - frosted_ice + - bedrock + - dragon_egg + - piglin_brute_spawn_egg + - parrot_spawn_egg + - turtle_spawn_egg + - zombie_spawn_egg + - mule_spawn_egg + - armadillo_spawn_egg + - tropical_fish_spawn_egg + - blaze_spawn_egg + - shulker_spawn_egg + - sniffer_spawn_egg + - polar_bear_spawn_egg + - phantom_spawn_egg + - guardian_spawn_egg + - ravager_spawn_egg + - wandering_trader_spawn_egg + - villager_spawn_egg + - wither_skeleton_spawn_egg + - warden_spawn_egg + - ender_dragon_spawn_egg + - bat_spawn_egg + - sniffer_egg + - camel_spawn_egg + - creeper_spawn_egg + - breeze_spawn_egg + - iron_golem_spawn_egg timer: enabled: false - timer-end: "2025-01-01 14:30:00" + timer-end: null # Points system settings points: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2630d5b..ff5af14 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,7 +2,6 @@ name: BlockVault version: '1.0.0' main: me.benrobson.blockvault.BlockVault api-version: '1.21' -depend: [WorldEdit] commands: bvsubmit: description: Command @@ -16,3 +15,6 @@ commands: bvstart: description: Command usage: /bvstart + bvupdatestate: + description: Command + usage: /bvupdatestate From 97eeee6a8ed4b7cda8030aba9e267ab47b475991 Mon Sep 17 00:00:00 2001 From: benrobson Date: Fri, 3 Jan 2025 19:47:05 +1100 Subject: [PATCH 09/28] Added excluded items (e.g. spawn eggs) and can be configured by config.yml --- .../me/benrobson/blockvault/BlockVault.java | 2 +- .../blockvault/util/ScheduleUtil.java | 11 +++- .../benrobson/blockvault/util/VaultUtil.java | 12 +++- src/main/resources/config.yml | 61 ++++++++++++++++++- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/me/benrobson/blockvault/BlockVault.java index 46068d7..e883b12 100644 --- a/src/main/java/me/benrobson/blockvault/BlockVault.java +++ b/src/main/java/me/benrobson/blockvault/BlockVault.java @@ -33,7 +33,7 @@ public void onEnable() { saveDefaultConfig(); VaultUtil.generateVaultItems(this); - scheduleVaultStateTask(this, vaultUtil); + scheduleVaultStateTask(this, vaultUtil, fileUtil); } @Override diff --git a/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java b/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java index a83a696..b1edffb 100644 --- a/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java @@ -14,9 +14,16 @@ public ScheduleUtil(Plugin plugin) { this.fileUtil = new FileUtil(plugin); } - public static void scheduleVaultStateTask(Plugin plugin, VaultUtil vaultUtil) { + public static void scheduleVaultStateTask(Plugin plugin, VaultUtil vaultUtil, FileUtil fileUtil) { + // Get the interval from the config file + int intervalMinutes = fileUtil.getConfig().getInt("vault.updateinterval", 15); // Default to 15 if not set + + // Convert the interval from minutes to ticks (20 ticks per second, 60 seconds per minute) + long intervalTicks = 20L * 60L * intervalMinutes; + + // Schedule the task to run every 'intervalTicks' ticks Bukkit.getScheduler().runTaskTimer(plugin, () -> { vaultUtil.updateVaultState(null); - }, 0L, 20L * 60L * 5L); // Run every 5 minutes + }, 0L, intervalTicks); // Runs based on the interval in the config } } diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index b829406..fba71ed 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -40,21 +40,27 @@ public static void generateVaultItems(Plugin plugin) { return; } + // Load excluded items from the config + List excludedItems = plugin.getConfig().getStringList("vault.excludeditems"); + + // Map to store valid items Map itemData = new HashMap<>(); for (Material material : Material.values()) { - // Add materials that are either blocks or items - if (material.isBlock() || material.isItem()) { + // Add materials that are either blocks or items and are not excluded + if ((material.isBlock() || material.isItem()) && + !excludedItems.contains(material.name().toLowerCase())) { itemData.put(material.name().toLowerCase(), "COMMON"); // Default category } } + // Create a YAML configuration and add the items YamlConfiguration yaml = new YamlConfiguration(); yaml.createSection("items", itemData); try { yaml.save(file); - plugin.getLogger().info("Generated vault_items.yml with all blocks and items set to default category COMMON."); + plugin.getLogger().info("Generated vault_items.yml with all valid blocks and items set to default category COMMON."); } catch (IOException e) { plugin.getLogger().severe("Failed to save vault_items.yml: " + e.getMessage()); e.printStackTrace(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3dcb6e9..758a7fd 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -2,8 +2,9 @@ # Vault settings vault: - start-time: null + starttime: null started: false + updateinterval: 15 region: upperregion: X: -138 @@ -53,6 +54,64 @@ vault: - creeper_spawn_egg - breeze_spawn_egg - iron_golem_spawn_egg + - hoglin_spawn_egg + - sheep_spawn_egg + - ocelot_spawn_egg + - witch_spawn_egg + - creaking_spawn_egg + - enderman_spawn_egg + - donkey_spawn_egg + - frog_spawn_egg + - llama_spawn_egg + - horse_spawn_egg + - glow_squid_spawn_egg + - bogged_spawn_egg + - chicken_spawn_egg + - piglin_spawn_egg + - wolf_spawn_egg + - cat_spawn_egg + - silverfish_spawn_egg + - rabbit_spawn_egg + - strider_spawn_egg + - slime_spawn_egg + - vindicator_spawn_egg + - wither_spawn_egg + - zombified_piglin_spawn_egg + - drowned_spawn_egg + - zombie_horse_spawn_egg + - husk_spawn_egg + - zombie_villager_spawn_egg + - trader_llama_spawn_egg + - mooshroom_spawn_egg + - stray_spawn_egg + - cow_spawn_egg + - vex_spawn_egg + - skeleton_horse_spawn_egg + - salmon_spawn_egg + - pig_spawn_egg + - cod_spawn_egg + - allay_spawn_egg + - dolphin_spawn_egg + - elder_guardian_spawn_egg + - zoglin_spawn_egg + - squid_spawn_egg + - cave_spider_spawn_egg + - ghast_spawn_egg + - axolotl_spawn_egg + - skeleton_spawn_egg + - pufferfish_spawn_egg + - goat_spawn_egg + - endermite_spawn_egg + - bee_spawn_egg + - snow_golem_spawn_egg + - evoker_spawn_egg + - panda_spawn_egg + - pillager_spawn_egg + - fox_spawn_egg + - spider_spawn_egg + - magma_cube_spawn_egg + - hoglin_spawn_egg + - sheep_spawn_egg timer: enabled: false From 4158e97773081f31d58a257a2fe1bb9549bc7e48 Mon Sep 17 00:00:00 2001 From: benrobson Date: Sat, 4 Jan 2025 00:22:34 +1100 Subject: [PATCH 10/28] Fixed vault start config not setting. --- .../blockvault/commands/bvstart.java | 11 ++--- .../benrobson/blockvault/util/FileUtil.java | 47 ++++++++++++++++++- .../benrobson/blockvault/util/VaultUtil.java | 15 ++---- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/me/benrobson/blockvault/commands/bvstart.java index 58fd342..049528b 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvstart.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvstart.java @@ -21,26 +21,23 @@ public bvstart(Plugin plugin) { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - // Check for the necessary permission if (!sender.hasPermission("blockvault.start")) { sender.sendMessage(ChatColor.RED + "You don't have permission to use this command!"); return true; } - // Check if the vault has already started if (vaultUtil.hasStarted()) { sender.sendMessage(ChatColor.RED + "The Vault challenge has already started."); return true; } - // Log the start date and time try { String currentDateTime = java.time.LocalDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - // Update the configuration - fileUtil.getConfig().set("vault.start-time", currentDateTime); - fileUtil.getConfig().set("vault.started", true); + // Update specific config values + fileUtil.updateConfigValue("vault.starttime", currentDateTime); + fileUtil.updateConfigValue("vault.started", true); sender.sendMessage(ChatColor.GREEN + "The vault challenge has started, the vault is open!"); } catch (Exception e) { @@ -50,4 +47,4 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } -} \ No newline at end of file +} diff --git a/src/main/java/me/benrobson/blockvault/util/FileUtil.java b/src/main/java/me/benrobson/blockvault/util/FileUtil.java index 410ef05..6210c9f 100644 --- a/src/main/java/me/benrobson/blockvault/util/FileUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/FileUtil.java @@ -2,14 +2,20 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; + import java.io.File; +import java.io.IOException; public class FileUtil { - private final Plugin plugin; // Make it an instance variable + private final Plugin plugin; + private final File configFile; + private YamlConfiguration config; public FileUtil(Plugin plugin) { this.plugin = plugin; + this.configFile = new File(plugin.getDataFolder(), "config.yml"); + reloadConfig(); // Load the configuration initially } /** @@ -18,6 +24,43 @@ public FileUtil(Plugin plugin) { * @return The plugin's configuration. */ public YamlConfiguration getConfig() { - return YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "config.yml")); + return config; + } + + /** + * Saves the current state of the configuration to the file. + */ + public void saveConfig() { + try { + config.save(configFile); + } catch (IOException e) { + plugin.getLogger().severe("Could not save config.yml: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Reloads the configuration from the file. + */ + public void reloadConfig() { + if (!configFile.exists()) { + plugin.saveResource("config.yml", false); // Ensure the default config exists + } + this.config = YamlConfiguration.loadConfiguration(configFile); + } + + /** + * Updates a specific configuration value without erasing others. + * + * @param path The configuration path. + * @param value The value to set. + */ + public void updateConfigValue(String path, Object value) { + // Set the new value + getConfig().set(path, value); + // Save the config to the file + saveConfig(); + // Reload the config to reflect changes immediately + plugin.reloadConfig(); } } \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index fba71ed..472c6bb 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -29,7 +29,10 @@ public VaultUtil(Plugin plugin) { // Method to check if the vault has started public boolean hasStarted() { - return fileUtil.getConfig().getBoolean("vault.started", false); // Use fileUtil to access the config + Boolean started = plugin.getConfig().getBoolean("vault.started"); + plugin.getLogger().info(String.valueOf(started)); + + return plugin.getConfig().getBoolean("vault.started", false); } public static void generateVaultItems(Plugin plugin) { @@ -163,16 +166,6 @@ private int getCollectedItemsCount() { return collectedItems; } - /** - * Get the initial countdown time in seconds. - * - * @return The initial countdown time in seconds. - */ - private int getInitialCountdownTime() { - // Assuming that the duration is set somewhere in your config or hardcoded - return plugin.getConfig().getInt("challenge.duration_in_seconds", 600); // Default to 10 minutes - } - /** * Checks if a specific item has been added to the vault by any player. * From fc5bca80be1a97177ba1fca99b36fe2b14d757a9 Mon Sep 17 00:00:00 2001 From: benrobson Date: Sat, 4 Jan 2025 16:59:13 +1100 Subject: [PATCH 11/28] Added progress bar and info for bvprogress --- .../blockvault/commands/bvleaderboard.java | 12 +- .../blockvault/commands/bvprogress.java | 39 +----- .../blockvault/commands/bvsubmit.java | 5 +- .../benrobson/blockvault/util/VaultUtil.java | 121 ++++++------------ 4 files changed, 52 insertions(+), 125 deletions(-) diff --git a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java index b791200..4cfa2e9 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java @@ -42,13 +42,15 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } - // Get all player points data + // Get all player points data, excluding "global_collected_items" Set playerNames = vaultData.getConfigurationSection("vault_data").getKeys(false); Map playerPoints = new HashMap<>(); for (String playerName : playerNames) { - int points = vaultData.getInt("vault_data." + playerName + ".points"); - playerPoints.put(playerName, points); + if (!playerName.equalsIgnoreCase("global_collected_items")) { // Exclude global_collected_items + int points = vaultData.getInt("vault_data." + playerName + ".points"); + playerPoints.put(playerName, points); + } } // Sort players by points in descending order @@ -59,7 +61,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sender.sendMessage("§aVault Leaderboard:"); sender.sendMessage("§a================================="); - int topPlayersCount = Math.min(10, sortedPlayers.size()); + int topPlayersCount = Math.min(5, sortedPlayers.size()); for (int i = 0; i < topPlayersCount; i++) { Map.Entry entry = sortedPlayers.get(i); String playerName = entry.getKey(); @@ -69,4 +71,4 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } -} +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java index 232a485..c99b751 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -44,41 +44,14 @@ public boolean onCommand(CommandSender sender, Command command, String label, St // Get the total number of items in the vault int totalItems = vaultItems.getConfigurationSection("items").getKeys(false).size(); - // Get the total number of collected items for this player - List playerCollectedItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); - int collectedItemCount = playerCollectedItems.size(); - - // Calculate the percentage of progress - int progressPercentage = (int) ((double) collectedItemCount / totalItems * 100); - - // Create a progress bar - StringBuilder progressBar = new StringBuilder("§a["); - int barLength = 20; - int progressLength = (int) ((progressPercentage / 100.0) * barLength); - for (int i = 0; i < barLength; i++) { - if (i < progressLength) { - progressBar.append("§a#"); - } else { - progressBar.append("§7#"); - } - } - progressBar.append("§a]"); + // Get the player's progress percentage and progress bar + String progress = vaultUtil.getProgress(vaultData, player, totalItems); // Display progress - player.sendMessage("§aVault Progress: " + progressPercentage + "% complete."); - player.sendMessage(progressBar.toString()); - - // Display the last 5 contributions - List recentContributions = vaultData.getStringList("vault_data." + player.getName() + ".recent_contributions"); - if (recentContributions.isEmpty()) { - player.sendMessage("§cNo recent contributions."); - } else { - player.sendMessage("§eLast 5 Contributions:"); - for (int i = Math.max(0, recentContributions.size() - 5); i < recentContributions.size(); i++) { - player.sendMessage("§7- " + recentContributions.get(i)); - } - } + player.sendMessage("§aVault Progress"); + player.sendMessage("=================================="); + player.sendMessage(progress); return true; } -} +} \ No newline at end of file diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java index c0a304f..755bfeb 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java @@ -16,8 +16,7 @@ import java.io.IOException; import java.util.List; -import static me.benrobson.blockvault.util.VaultUtil.formatMaterialName; -import static me.benrobson.blockvault.util.VaultUtil.updateRecentContributions; +import static me.benrobson.blockvault.util.VaultUtil.*; public class bvsubmit implements CommandExecutor { private final Plugin plugin; @@ -82,7 +81,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St int pointsPerCategory = config.getInt("points." + category, 1); // Default to 1 point playerItems.add(itemName); - updateRecentContributions(player, itemName, plugin); + vaultData.set("vault_data." + player.getName() + ".collected_items", playerItems); // Update player points diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index 472c6bb..97c5f35 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -29,9 +29,6 @@ public VaultUtil(Plugin plugin) { // Method to check if the vault has started public boolean hasStarted() { - Boolean started = plugin.getConfig().getBoolean("vault.started"); - plugin.getLogger().info(String.valueOf(started)); - return plugin.getConfig().getBoolean("vault.started", false); } @@ -85,87 +82,6 @@ public static String formatMaterialName(Material material) { return formattedName.toString().trim(); } - public static void updateRecentContributions(Player player, String itemName, Plugin plugin) { - // Load the vault data file - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - // Get the current list of recent contributions for the player - String playerPath = "vault_data." + player.getName() + ".recent_contributions"; - List recentContributions = vaultData.getStringList(playerPath); - - // Ensure the list has no more than 5 entries - if (recentContributions.size() >= 5) { - recentContributions.remove(0); // Remove the oldest contribution - } - - // Add the new contribution to the list - recentContributions.add(player.getName() + " collected " + itemName); - - // Save the updated list back into the YML file - vaultData.set(playerPath, recentContributions); - try { - vaultData.save(vaultDataFile); - } catch (IOException e) { - player.sendMessage("§cFailed to save recent contributions."); - e.printStackTrace(); - } - } - - public List getTopPlayers() { - // Load vault data to get the collected items count per player - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - Map playerProgress = new HashMap<>(); - - // Loop through each player and count their collected items - for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { - List collectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); - playerProgress.put(playerName, collectedItems.size()); - } - - // Sort players by their collected items (highest to lowest) - List> sortedPlayers = new ArrayList<>(playerProgress.entrySet()); - sortedPlayers.sort((entry1, entry2) -> entry2.getValue().compareTo(entry1.getValue())); // Sort in descending order - - // Get the top 5 players - List topPlayers = new ArrayList<>(); - for (int i = 0; i < Math.min(5, sortedPlayers.size()); i++) { - topPlayers.add(sortedPlayers.get(i).getKey()); - } - - return topPlayers; - } - - /** - * Gets the total number of items in the vault. - * - * @return The total number of items in the vault. - */ - private int getTotalVaultItems() { - File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); - YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); - return vaultItems.getConfigurationSection("items").getKeys(false).size(); - } - - /** - * Gets the number of collected items by a player. - * - * @return The total number of collected items. - */ - private int getCollectedItemsCount() { - // Load vault data and calculate total collected items - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - int collectedItems = 0; - for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { - collectedItems += vaultData.getStringList("vault_data." + playerName + ".collected_items").size(); - } - return collectedItems; - } - /** * Checks if a specific item has been added to the vault by any player. * @@ -272,4 +188,41 @@ public void updateVaultState(Player player) { if (player != null) player.sendMessage(errorMessage); } } + + /** + * Calculates the player's progress and generates a progress bar. + * + * @param vaultData The YamlConfiguration object for vault data. + * @param player The player whose progress is being calculated. + * @param totalItems The total number of items required for the vault. + * @return A formatted progress string showing percentage and progress bar. + */ + public String getProgress(YamlConfiguration vaultData, Player player, int totalItems) { + // Get the total number of collected items for this player + List playerCollectedItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); + int collectedItemCount = playerCollectedItems.size(); + + // Calculate the percentage of progress + int progressPercentage = (int) ((double) collectedItemCount / totalItems * 100); + + // Create a progress bar + StringBuilder progressBar = new StringBuilder("§a["); + int barLength = 50; // Total bar length + int progressLength = (int) ((progressPercentage / 100.0) * barLength); + + for (int i = 0; i < barLength; i++) { + if (i < progressLength) { + progressBar.append("§a|"); + } else { + progressBar.append("§7|"); + } + } + progressBar.append("§a]"); + + // Create the progress string with collected and total items + String progressInfo = String.format("§e%d/%d collected", collectedItemCount, totalItems); + + // Return the full progress string + return progressInfo + " §f(" + progressPercentage + "% complete)\n" + progressBar; + } } From b944f79b664f27d635db8b7debb6d0a28af9f4f2 Mon Sep 17 00:00:00 2001 From: benrobson Date: Sat, 4 Jan 2025 18:26:20 +1100 Subject: [PATCH 12/28] Added progress bar and info for bvprogress --- .../blockvault/commands/bvprogress.java | 2 +- .../benrobson/blockvault/util/VaultUtil.java | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java index c99b751..2800020 100644 --- a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java +++ b/src/main/java/me/benrobson/blockvault/commands/bvprogress.java @@ -45,7 +45,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St int totalItems = vaultItems.getConfigurationSection("items").getKeys(false).size(); // Get the player's progress percentage and progress bar - String progress = vaultUtil.getProgress(vaultData, player, totalItems); + String progress = vaultUtil.getProgress(vaultData, totalItems); // Display progress player.sendMessage("§aVault Progress"); diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java index 97c5f35..ee84218 100644 --- a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java +++ b/src/main/java/me/benrobson/blockvault/util/VaultUtil.java @@ -190,20 +190,23 @@ public void updateVaultState(Player player) { } /** - * Calculates the player's progress and generates a progress bar. + * Calculates the collective progress of all players in the vault and generates a progress bar. * * @param vaultData The YamlConfiguration object for vault data. - * @param player The player whose progress is being calculated. * @param totalItems The total number of items required for the vault. - * @return A formatted progress string showing percentage and progress bar. + * @return A formatted progress string showing the collective percentage and progress bar. */ - public String getProgress(YamlConfiguration vaultData, Player player, int totalItems) { - // Get the total number of collected items for this player - List playerCollectedItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); - int collectedItemCount = playerCollectedItems.size(); + public String getProgress(YamlConfiguration vaultData, int totalItems) { + int totalCollectedItems = 0; - // Calculate the percentage of progress - int progressPercentage = (int) ((double) collectedItemCount / totalItems * 100); + // Iterate through all player entries in the vault data to sum up collected items + for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { + List playerCollectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); + totalCollectedItems += playerCollectedItems.size(); + } + + // Calculate the percentage of progress for the collective effort + int progressPercentage = (int) ((double) totalCollectedItems / totalItems * 100); // Create a progress bar StringBuilder progressBar = new StringBuilder("§a["); @@ -219,8 +222,8 @@ public String getProgress(YamlConfiguration vaultData, Player player, int totalI } progressBar.append("§a]"); - // Create the progress string with collected and total items - String progressInfo = String.format("§e%d/%d collected", collectedItemCount, totalItems); + // Create the progress string with total collected and total required items + String progressInfo = String.format("§e%d/%d collected", totalCollectedItems, totalItems); // Return the full progress string return progressInfo + " §f(" + progressPercentage + "% complete)\n" + progressBar; From 8293cba9e784118ac6d947e47c843685ac12817f Mon Sep 17 00:00:00 2001 From: benrobson Date: Sat, 4 Jan 2025 22:53:19 +1100 Subject: [PATCH 13/28] Slightly better docs. --- README.md | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 01b1a7f..44c0540 100644 --- a/README.md +++ b/README.md @@ -15,31 +15,18 @@ BlockVault is an interactive challenge where players collaborate to collect ever * **Common Blocks:** 1 point per block. * **Uncommon Blocks:** 5 points per block. * **Rare Blocks/Items:** 10+ points per block. -* A running total of points is tracked for each player, contributing to their leaderboard position. +* A running total of points is tracked for each player which can be found using `/bvleaderboard`, contributing to their leaderboard position. ### Tracking Progress -* A real-time leaderboard displays the top contributors and their collected blocks/items. -* A progress bar or percentage indicator shows how close the server is to completing the collection. +* Leaderboard displays the top contributors and their points when using `/bvleaderboard`. +* A progress bar or percentage indicator shows how close the server is to completing the collection using `/bvprogress`. -### Progress Leaderboard & GUI Integration +### Progress Leaderboard * Progress can be viewed in real-time using a hologram display, showcasing the top contributors and collection milestones. -* Use the `/vault leaderboard` command to see a detailed leaderboard of top contributors, including points and blocks contributed. -* GUI integration allows players to easily browse and view all collected blocks in an organized interface. - -### Broadcast Announcements -* **Rare Blocks/Items Announcements:** Announce server-wide when a rare block or item is added to the vault, celebrating player contributions and encouraging participation. -* **Milestone Achievements:** Broadcast messages when major milestones are reached, such as completing 50% of the vault or finishing specific block categories. - -### Timer and Boss Integration -* A countdown timer can be enabled to add urgency, ticking down the time remaining for the challenge. -* A boss integration shows live updates on: - * The time elapsed. - * How many blocks have been collected. - * How much time is left (if the timer is enabled). +* Use the `/bvleaderboard` command to see a detailed leaderboard of top contributors, including points and blocks contributed. ## Commands -* `/vault start` – Starts the vault challenge (requires `blockvault.start`). -* `/vault generate` – Generates the vault based on the schematic (requires `blockvault.generate`). -* `/vault add ` – Manually adds a block/item to the collection. -* `/vault progress` – Displays the overall progress of the collection and the leaderboard. -* `/vault leaderboard` – Shows the top contributors and their points in a detailed list. \ No newline at end of file +* `/bvstart` – Starts the vault challenge (requires `blockvault.start`). +* `/bvsubmit` – Submit the block/item that the player is holding to the vault. +* `/bvprogress` – Displays the overall progress of the vault. +* `/bvleaderboard` – Shows the top contributors and their points. \ No newline at end of file From 62da210fadd1c9364d87413299ef4053b5a9e7c6 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 11:14:12 +1000 Subject: [PATCH 14/28] Implement core functionality for BlockVault plugin with command handling and file utilities --- blockvault.iml | 14 ---- dependency-reduced-pom.xml | 71 ------------------- .../anchorlight}/blockvault/BlockVault.java | 0 .../commands/LeaderboardCommand.java} | 0 .../blockvault/commands/ProgressCommand.java} | 0 .../blockvault/commands/StartCommand.java} | 0 .../blockvault/commands/SubmitCommand.java} | 0 .../commands/UpdateStateCommand.java} | 0 .../blockvault/util/FileUtil.java | 0 .../blockvault/util/ScheduleUtil.java | 0 .../blockvault/util/VaultUtil.java | 0 11 files changed, 85 deletions(-) delete mode 100644 blockvault.iml delete mode 100644 dependency-reduced-pom.xml rename src/main/java/{me/benrobson => dev/anchorlight}/blockvault/BlockVault.java (100%) rename src/main/java/{me/benrobson/blockvault/commands/bvleaderboard.java => dev/anchorlight/blockvault/commands/LeaderboardCommand.java} (100%) rename src/main/java/{me/benrobson/blockvault/commands/bvprogress.java => dev/anchorlight/blockvault/commands/ProgressCommand.java} (100%) rename src/main/java/{me/benrobson/blockvault/commands/bvstart.java => dev/anchorlight/blockvault/commands/StartCommand.java} (100%) rename src/main/java/{me/benrobson/blockvault/commands/bvsubmit.java => dev/anchorlight/blockvault/commands/SubmitCommand.java} (100%) rename src/main/java/{me/benrobson/blockvault/commands/bvupdatestate.java => dev/anchorlight/blockvault/commands/UpdateStateCommand.java} (100%) rename src/main/java/{me/benrobson => dev/anchorlight}/blockvault/util/FileUtil.java (100%) rename src/main/java/{me/benrobson => dev/anchorlight}/blockvault/util/ScheduleUtil.java (100%) rename src/main/java/{me/benrobson => dev/anchorlight}/blockvault/util/VaultUtil.java (100%) diff --git a/blockvault.iml b/blockvault.iml deleted file mode 100644 index bbeeb3e..0000000 --- a/blockvault.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - PAPER - ADVENTURE - - 1 - - - - \ No newline at end of file diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml deleted file mode 100644 index e83815c..0000000 --- a/dependency-reduced-pom.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - 4.0.0 - me.benrobson - blockvault - blockvault - 1.0.0 - - clean package - - - true - src/main/resources - - - - - maven-compiler-plugin - 3.13.0 - - ${java.version} - ${java.version} - - - - maven-shade-plugin - 3.5.3 - - - package - - shade - - - - - - - - - papermc-repo - https://repo.papermc.io/repository/maven-public/ - - - sonatype - https://oss.sonatype.org/content/groups/public/ - - - enginehub-maven - https://maven.enginehub.org/repo/ - - - - - io.papermc.paper - paper-api - 1.21.3-R0.1-SNAPSHOT - provided - - - com.sk89q.worldedit - worldedit-bukkit - 7.2.9 - provided - - - - 21 - UTF-8 - - diff --git a/src/main/java/me/benrobson/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/BlockVault.java rename to src/main/java/dev/anchorlight/blockvault/BlockVault.java diff --git a/src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/commands/bvleaderboard.java rename to src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java diff --git a/src/main/java/me/benrobson/blockvault/commands/bvprogress.java b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/commands/bvprogress.java rename to src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java diff --git a/src/main/java/me/benrobson/blockvault/commands/bvstart.java b/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/commands/bvstart.java rename to src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java diff --git a/src/main/java/me/benrobson/blockvault/commands/bvsubmit.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/commands/bvsubmit.java rename to src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java diff --git a/src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java b/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/commands/bvupdatestate.java rename to src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java diff --git a/src/main/java/me/benrobson/blockvault/util/FileUtil.java b/src/main/java/dev/anchorlight/blockvault/util/FileUtil.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/util/FileUtil.java rename to src/main/java/dev/anchorlight/blockvault/util/FileUtil.java diff --git a/src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java b/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/util/ScheduleUtil.java rename to src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java diff --git a/src/main/java/me/benrobson/blockvault/util/VaultUtil.java b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java similarity index 100% rename from src/main/java/me/benrobson/blockvault/util/VaultUtil.java rename to src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java From 3cc2755d234ba9e6c46dd6ce56c58123ea1f8ec6 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 11:14:20 +1000 Subject: [PATCH 15/28] Refactor package structure and update command classes for BlockVault plugin; enhance plugin.yml with descriptions and permissions. --- .gitignore | 7 ++- pom.xml | 42 +++------------ .../anchorlight/blockvault/BlockVault.java | 52 ++++++++++++------- .../commands/LeaderboardCommand.java | 8 +-- .../blockvault/commands/ProgressCommand.java | 8 +-- .../blockvault/commands/StartCommand.java | 10 ++-- .../blockvault/commands/SubmitCommand.java | 12 ++--- .../commands/UpdateStateCommand.java | 10 ++-- .../anchorlight/blockvault/util/FileUtil.java | 2 +- .../blockvault/util/ScheduleUtil.java | 2 +- .../blockvault/util/VaultUtil.java | 2 +- src/main/resources/plugin.yml | 32 +++++++++--- 12 files changed, 96 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index ec376bb..1188618 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea -target \ No newline at end of file +*.iml +target +dependency-reduced-pom.xml +# Live config holds MySQL credentials - never commit a populated one. +# The shipped template lives at src/main/resources/config.yml. +/config.yml diff --git a/pom.xml b/pom.xml index 791c697..72f866f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - me.benrobson + dev.anchorlight blockvault 1.0.0 jar @@ -12,7 +12,8 @@ blockvault - 21 + 25 + 25 UTF-8 @@ -23,23 +24,6 @@ org.apache.maven.plugins maven-compiler-plugin 3.13.0 - - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.3 - - - package - - shade - - - @@ -51,17 +35,9 @@ - - papermc-repo - https://repo.papermc.io/repository/maven-public/ - - - sonatype - https://oss.sonatype.org/content/groups/public/ - - enginehub-maven - https://maven.enginehub.org/repo/ + papermc-repo + https://repo.papermc.io/repository/maven-public/ @@ -69,13 +45,7 @@ io.papermc.paper paper-api - 1.21.3-R0.1-SNAPSHOT - provided - - - com.sk89q.worldedit - worldedit-bukkit - 7.2.9 + [26.2.build,) provided diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index e883b12..9038089 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -1,43 +1,55 @@ -package me.benrobson.blockvault; - -import me.benrobson.blockvault.commands.*; -import me.benrobson.blockvault.util.FileUtil; -import me.benrobson.blockvault.util.ScheduleUtil; -import me.benrobson.blockvault.util.VaultUtil; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; +package dev.anchorlight.blockvault; + +import dev.anchorlight.blockvault.commands.*; +import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.util.ScheduleUtil; +import dev.anchorlight.blockvault.util.VaultUtil; +import org.bukkit.command.PluginCommand; import org.bukkit.plugin.java.JavaPlugin; -import static me.benrobson.blockvault.util.ScheduleUtil.scheduleVaultStateTask; +import static dev.anchorlight.blockvault.util.ScheduleUtil.scheduleVaultStateTask; import static org.bukkit.Bukkit.getConsoleSender; public final class BlockVault extends JavaPlugin { @Override public void onEnable() { - getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§aPlugin is now enabled!"); + saveDefaultConfig(); + + getConsoleSender().sendMessage(translatePrefix() + "§aPlugin is now enabled!"); FileUtil fileUtil = new FileUtil(this); VaultUtil vaultUtil = new VaultUtil(this); // Register commands - getCommand("bvstart").setExecutor(new bvstart(this)); - getCommand("bvsubmit").setExecutor(new bvsubmit(this)); - getCommand("bvprogress").setExecutor(new bvprogress(this)); - getCommand("bvleaderboard").setExecutor(new bvleaderboard(this)); - getCommand("bvupdatestate").setExecutor(new bvupdatestate(this)); + register("bvstart", new StartCommand(this)); + register("bvsubmit", new SubmitCommand(this)); + register("bvprogress", new ProgressCommand(this)); + register("bvleaderboard", new LeaderboardCommand(this)); + register("bvupdatestate", new UpdateStateCommand(this)); // Register events - saveDefaultConfig(); VaultUtil.generateVaultItems(this); scheduleVaultStateTask(this, vaultUtil, fileUtil); } @Override public void onDisable() { - getConsoleSender().sendMessage(getConfig().get("lang.prefix") + "§cPlugin is now disabled."); + getConsoleSender().sendMessage(translatePrefix() + "§cPlugin is now disabled."); + } + + private void register(String name, org.bukkit.command.CommandExecutor executor) { + PluginCommand command = getCommand(name); + if (command == null) { + getLogger().severe("Command '" + name + "' is missing from plugin.yml; skipping registration."); + return; + } + command.setExecutor(executor); + } + + private String translatePrefix() { + return org.bukkit.ChatColor.translateAlternateColorCodes('&', + getConfig().getString("lang.prefix", "")); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java index 4cfa2e9..699d5ed 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java @@ -1,6 +1,6 @@ -package me.benrobson.blockvault.commands; +package dev.anchorlight.blockvault.commands; -import me.benrobson.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -11,11 +11,11 @@ import java.io.File; import java.util.*; -public class bvleaderboard implements CommandExecutor { +public class LeaderboardCommand implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; - public bvleaderboard(Plugin plugin) { + public LeaderboardCommand(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java index 2800020..e5d8f7a 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java @@ -1,6 +1,6 @@ -package me.benrobson.blockvault.commands; +package dev.anchorlight.blockvault.commands; -import me.benrobson.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -11,11 +11,11 @@ import java.io.File; import java.util.List; -public class bvprogress implements CommandExecutor { +public class ProgressCommand implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; - public bvprogress(Plugin plugin) { + public ProgressCommand(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java index 049528b..cf7956f 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java @@ -1,19 +1,19 @@ -package me.benrobson.blockvault.commands; +package dev.anchorlight.blockvault.commands; -import me.benrobson.blockvault.util.FileUtil; -import me.benrobson.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; -public class bvstart implements CommandExecutor { +public class StartCommand implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; private final FileUtil fileUtil; - public bvstart(Plugin plugin) { + public StartCommand(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); this.fileUtil = new FileUtil(plugin); diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index 755bfeb..829c783 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -1,7 +1,7 @@ -package me.benrobson.blockvault.commands; +package dev.anchorlight.blockvault.commands; -import me.benrobson.blockvault.util.FileUtil; -import me.benrobson.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -16,13 +16,13 @@ import java.io.IOException; import java.util.List; -import static me.benrobson.blockvault.util.VaultUtil.*; +import static dev.anchorlight.blockvault.util.VaultUtil.*; -public class bvsubmit implements CommandExecutor { +public class SubmitCommand implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; - public bvsubmit(Plugin plugin) { + public SubmitCommand(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java index b8f445e..a01eb6f 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java @@ -1,7 +1,7 @@ -package me.benrobson.blockvault.commands; +package dev.anchorlight.blockvault.commands; -import me.benrobson.blockvault.util.FileUtil; -import me.benrobson.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -14,12 +14,12 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; -public class bvupdatestate implements CommandExecutor { +public class UpdateStateCommand implements CommandExecutor { private final Plugin plugin; private final VaultUtil vaultUtil; private final FileUtil fileUtil; - public bvupdatestate(Plugin plugin) { + public UpdateStateCommand(Plugin plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); this.fileUtil = new FileUtil(plugin); diff --git a/src/main/java/dev/anchorlight/blockvault/util/FileUtil.java b/src/main/java/dev/anchorlight/blockvault/util/FileUtil.java index 6210c9f..35b6d5e 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/FileUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/FileUtil.java @@ -1,4 +1,4 @@ -package me.benrobson.blockvault.util; +package dev.anchorlight.blockvault.util; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; diff --git a/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java b/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java index b1edffb..339b754 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java @@ -1,4 +1,4 @@ -package me.benrobson.blockvault.util; +package dev.anchorlight.blockvault.util; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; diff --git a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java index ee84218..6828079 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java @@ -1,4 +1,4 @@ -package me.benrobson.blockvault.util; +package dev.anchorlight.blockvault.util; import org.bukkit.Bukkit; import org.bukkit.Location; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ff5af14..33e62c1 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,20 +1,38 @@ name: BlockVault version: '1.0.0' -main: me.benrobson.blockvault.BlockVault -api-version: '1.21' +main: dev.anchorlight.blockvault.BlockVault +api-version: '26.2' commands: bvsubmit: - description: Command + description: Submit the block you are holding to the vault. usage: /bvsubmit bvleaderboard: - description: Command + description: Show the top contributors. usage: /bvleaderboard bvprogress: - description: Command + description: Show current chapter and overall progress. usage: /bvprogress bvstart: - description: Command + description: Open the collection event. usage: /bvstart + permission: blockvault.start bvupdatestate: - description: Command + description: Reconcile the world display against the database. usage: /bvupdatestate + permission: blockvault.updatestate +permissions: + blockvault.submit: + description: Allows submitting blocks to the vault. + default: true + blockvault.progress: + description: Allows viewing vault progress. + default: true + blockvault.leaderboard: + description: Allows viewing the leaderboard. + default: true + blockvault.start: + description: Allows opening and closing the event. + default: op + blockvault.updatestate: + description: Allows reconciling the world display against the database. + default: op From cbe17fd6cdb83567c7060f71d228f34b6b166f4b Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 11:16:17 +1000 Subject: [PATCH 16/28] Add SQL schema and artifact generation script for BlockVault plugin --- src/main/resources/blockvault_schema.sql | 105 ++++++++++++++++++++ tools/gen_artifacts.py | 121 +++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/main/resources/blockvault_schema.sql create mode 100644 tools/gen_artifacts.py diff --git a/src/main/resources/blockvault_schema.sql b/src/main/resources/blockvault_schema.sql new file mode 100644 index 0000000..dcb56d7 --- /dev/null +++ b/src/main/resources/blockvault_schema.sql @@ -0,0 +1,105 @@ +-- BlockVault - MySQL 8.0+ / MariaDB 10.5+ +-- Column sizes checked against the real 26.2 target list: +-- longest block name = waxed_weathered_copper_golem_statue (35 chars) +-- Minecraft usernames are max 16 chars +-- InnoDB throughout: the plugin CONSUMES the submitted item, so every write +-- must be transactional. A lost write means a permanently lost block. + +SET NAMES utf8mb4; + +-- ---------------------------------------------------------------- people +CREATE TABLE IF NOT EXISTS bv_contributor ( + uuid BINARY(16) NOT NULL, + last_name VARCHAR(16) NOT NULL, + points INT UNSIGNED NOT NULL DEFAULT 0, + blocks_given SMALLINT UNSIGNED NOT NULL DEFAULT 0, + first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (uuid), + KEY idx_points (points DESC), + KEY idx_name (last_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------------------------------------------- the target list +-- One row per block the edition expects. Loaded from vault_items.yml + +-- vault_slots.json on first start, then treated as immutable for the season. +CREATE TABLE IF NOT EXISTS bv_target ( + material VARCHAR(64) NOT NULL, + edition VARCHAR(16) NOT NULL, + chapter TINYINT UNSIGNED NOT NULL, + rarity ENUM('common','uncommon','rare') NOT NULL, + section ENUM('building','coloured','functional','natural','redstone') NOT NULL, + sign_x INT NOT NULL, sign_y INT NOT NULL, sign_z INT NOT NULL, + frame_x INT NOT NULL, frame_y INT NOT NULL, frame_z INT NOT NULL, + head_x INT NOT NULL, head_y INT NOT NULL, head_z INT NOT NULL, + facing ENUM('north','south','east','west') NOT NULL, + PRIMARY KEY (material, edition), + KEY idx_chapter (edition, chapter) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------------------------------------------- submissions +-- material is the primary key: the database itself enforces one-of-each. +-- Insert and catch the duplicate-key error instead of read-then-write. +CREATE TABLE IF NOT EXISTS bv_submission ( + material VARCHAR(64) NOT NULL, + edition VARCHAR(16) NOT NULL, + uuid BINARY(16) NOT NULL, + points SMALLINT UNSIGNED NOT NULL, + profile JSON NULL COMMENT 'cached skin profile for the head', + submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (material, edition), + KEY idx_uuid (uuid), + KEY idx_time (submitted_at), + CONSTRAINT fk_sub_target FOREIGN KEY (material, edition) + REFERENCES bv_target (material, edition), + CONSTRAINT fk_sub_person FOREIGN KEY (uuid) + REFERENCES bv_contributor (uuid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------------------------------------------- chapters +CREATE TABLE IF NOT EXISTS bv_chapter ( + chapter TINYINT UNSIGNED NOT NULL, + edition VARCHAR(16) NOT NULL, + title VARCHAR(32) NOT NULL, + room VARCHAR(32) NOT NULL, + seal_x INT NULL, seal_y INT NULL, seal_z INT NULL, + opens_at DATETIME NULL, + opened_at DATETIME NULL, + completed_at DATETIME NULL, + completed_by BINARY(16) NULL, + PRIMARY KEY (chapter, edition) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------------------------------------------- audit +-- Every state change. This is the reconstruction log if anything is ever lost. +CREATE TABLE IF NOT EXISTS bv_audit ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + ts DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + actor BINARY(16) NULL, + action VARCHAR(32) NOT NULL, + material VARCHAR(64) NULL, + detail JSON NULL, + PRIMARY KEY (id), + KEY idx_ts (ts), + KEY idx_action (action) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ---------------------------------------------------------------- views +CREATE OR REPLACE VIEW bv_progress AS +SELECT t.edition, + t.chapter, + COUNT(*) AS total, + COUNT(s.material) AS collected, + ROUND(100 * COUNT(s.material) / COUNT(*), 1) AS pct +FROM bv_target t +LEFT JOIN bv_submission s + ON s.material = t.material AND s.edition = t.edition +GROUP BY t.edition, t.chapter; + +CREATE OR REPLACE VIEW bv_leaderboard AS +SELECT c.uuid, c.last_name, c.points, COUNT(s.material) AS blocks +FROM bv_contributor c +LEFT JOIN bv_submission s ON s.uuid = c.uuid +GROUP BY c.uuid, c.last_name, c.points +ORDER BY c.points DESC, blocks DESC; diff --git a/tools/gen_artifacts.py b/tools/gen_artifacts.py new file mode 100644 index 0000000..5e41517 --- /dev/null +++ b/tools/gen_artifacts.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Generate BlockVault data artefacts from the frozen target list (brief section 12). + +Input: tools/target_list.txt - the raw section-12 block, one entry per line: + block chapter rarity section x y z facing + (lines outside that shape - fences, prose - are ignored.) + +Outputs: + src/main/resources/vault_items.yml items: mapping of block -> rarity, sorted + src/main/resources/vault_slots.json full manifest (entries + leader + seals) + tools/spawn_frames.mcfunction one summon per entry, run from schematic origin + +frame = sign + (0,1,0) head = sign + (0,2,0) (invariant across all entries) +""" +from __future__ import annotations +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "src" / "main" / "resources" +LIST = ROOT / "tools" / "target_list.txt" + +VERSION = "26.2" +DATA_VERSION = 4903 +FACING_BYTE = {"north": 2, "south": 3, "west": 4, "east": 5} + +LEADER = {"head": [0, 11, 5], "name_sign": [-1, 11, 5], + "count_sign": [1, 11, 5], "title": [0, 12, 5]} +SEALS = {"2": [0, 8, 1], "3": [0, 18, 1], "4": [0, 28, 1], + "5": [0, 38, 1], "6": [0, 48, 1]} + +RARITIES = {"common", "uncommon", "rare"} +SECTIONS = {"building", "coloured", "functional", "natural", "redstone"} +LINE = re.compile( + r"^([a-z0-9_]+)\s+(\d+)\s+(common|uncommon|rare)\s+" + r"(building|coloured|functional|natural|redstone)\s+" + r"(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(north|south|east|west)\s*$" +) + + +def parse(path: Path): + entries = [] + seen = set() + for n, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + line = raw.strip() + if not line or line.startswith("```") or line.startswith("#"): + continue + m = LINE.match(line) + if not m: + # tolerate the header line "block chapter rarity section x y z facing" + if line.split()[:2] == ["block", "chapter"]: + continue + raise SystemExit(f"{path}:{n}: unparseable entry: {line!r}") + block, chap, rarity, section, x, y, z, facing = m.groups() + if block in seen: + raise SystemExit(f"{path}:{n}: duplicate block {block!r}") + seen.add(block) + sx, sy, sz = int(x), int(y), int(z) + entries.append({ + "block": block, "chapter": int(chap), "rarity": rarity, + "section": section, + "sign": [sx, sy, sz], + "frame": [sx, sy + 1, sz], + "head": [sx, sy + 2, sz], + "facing": facing, + }) + return entries + + +def main(): + if not LIST.exists(): + raise SystemExit(f"missing {LIST} - paste the brief section-12 block into it") + entries = parse(LIST) + + # vault_items.yml - sorted block -> rarity + items = "\n".join(f" {e['block']}: {e['rarity']}" + for e in sorted(entries, key=lambda e: e["block"])) + (SRC / "vault_items.yml").write_text( + "# Generated by tools/gen_artifacts.py - do not hand-edit.\n" + f"# edition {VERSION}, {len(entries)} blocks\n" + "items:\n" + items + "\n", encoding="utf-8") + + # vault_slots.json - manifest + manifest = { + "version": VERSION, "data_version": DATA_VERSION, + "entries": [dict(e, section="building" if False else e["section"]) for e in entries], + "leader": LEADER, "seals": SEALS, + } + (SRC / "vault_slots.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + # spawn_frames.mcfunction + lines = ["# Generated by tools/gen_artifacts.py - run standing on the schematic origin.", + f"# {len(entries)} item frames, edition {VERSION}"] + for e in entries: + fx, fy, fz = e["frame"] + b = FACING_BYTE[e["facing"]] + lines.append( + f"summon minecraft:item_frame ~{fx} ~{fy} ~{fz} " + f"{{Facing:{b}b,Fixed:1b,Invulnerable:1b,Silent:1b}}" + ) + (ROOT / "tools" / "spawn_frames.mcfunction").write_text( + "\n".join(lines) + "\n", encoding="utf-8") + + # summary + by_rarity = {r: 0 for r in RARITIES} + by_section = {s: 0 for s in SECTIONS} + for e in entries: + by_rarity[e["rarity"]] += 1 + by_section[e["section"]] += 1 + print(f"{len(entries)} entries") + print("rarity :", dict(sorted(by_rarity.items()))) + print("section:", dict(sorted(by_section.items()))) + pts = by_rarity["common"] * 1 + by_rarity["uncommon"] * 5 + by_rarity["rare"] * 10 + print("season points:", pts) + + +if __name__ == "__main__": + main() From 05930e60db2256ed8aed47d659a8a4a9227080cf Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 12:15:59 +1000 Subject: [PATCH 17/28] Remove unused vault_items.yml configuration file --- src/main/resources/vault_items.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/resources/vault_items.yml diff --git a/src/main/resources/vault_items.yml b/src/main/resources/vault_items.yml deleted file mode 100644 index e69de29..0000000 From a4cb4a66123ba5b4452e5b719bbd6a15e0ed0a97 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 12:16:06 +1000 Subject: [PATCH 18/28] Refactor UpdateStateCommand to allow console execution and simplify permission checks Enhance ScheduleUtil to improve task scheduling with configurable startup delay Revamp VaultUtil to streamline vault state updates and remove deprecated methods Update config.yml to reflect new database settings and remove obsolete vault settings Add Database class for managing MySQL connections and operations with HikariCP Implement Manifest and TargetEntry classes for structured target data management Introduce HeadUtil for handling player head placements and profile serialization --- pom.xml | 13 + .../anchorlight/blockvault/BlockVault.java | 88 ++++- .../commands/LeaderboardCommand.java | 82 ++--- .../blockvault/commands/ProgressCommand.java | 68 ++-- .../blockvault/commands/StartCommand.java | 40 +-- .../blockvault/commands/SubmitCommand.java | 184 ++++++---- .../commands/UpdateStateCommand.java | 32 +- .../anchorlight/blockvault/db/Database.java | 340 ++++++++++++++++++ .../blockvault/model/Manifest.java | 126 +++++++ .../blockvault/model/TargetEntry.java | 28 ++ .../anchorlight/blockvault/util/HeadUtil.java | 66 ++++ .../blockvault/util/ScheduleUtil.java | 33 +- .../blockvault/util/VaultUtil.java | 261 ++++---------- src/main/resources/config.yml | 156 ++------ src/main/resources/plugin.yml | 3 + 15 files changed, 983 insertions(+), 537 deletions(-) create mode 100644 src/main/java/dev/anchorlight/blockvault/db/Database.java create mode 100644 src/main/java/dev/anchorlight/blockvault/model/Manifest.java create mode 100644 src/main/java/dev/anchorlight/blockvault/model/TargetEntry.java create mode 100644 src/main/java/dev/anchorlight/blockvault/util/HeadUtil.java diff --git a/pom.xml b/pom.xml index 72f866f..6249f85 100644 --- a/pom.xml +++ b/pom.xml @@ -48,5 +48,18 @@ [26.2.build,) provided + + + com.zaxxer + HikariCP + 5.1.0 + provided + + + com.mysql + mysql-connector-j + 8.4.0 + provided + diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 9038089..5cf73ab 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -1,27 +1,66 @@ package dev.anchorlight.blockvault; import dev.anchorlight.blockvault.commands.*; +import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.model.Manifest; import dev.anchorlight.blockvault.util.FileUtil; import dev.anchorlight.blockvault.util.ScheduleUtil; import dev.anchorlight.blockvault.util.VaultUtil; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.CommandExecutor; import org.bukkit.command.PluginCommand; import org.bukkit.plugin.java.JavaPlugin; -import static dev.anchorlight.blockvault.util.ScheduleUtil.scheduleVaultStateTask; +import java.nio.file.Path; + import static org.bukkit.Bukkit.getConsoleSender; public final class BlockVault extends JavaPlugin { + private static BlockVault instance; + + private Database database; + private Manifest manifest; + + public static BlockVault get() { + return instance; + } + @Override public void onEnable() { + instance = this; saveDefaultConfig(); - getConsoleSender().sendMessage(translatePrefix() + "§aPlugin is now enabled!"); + // The target list is a required artefact - never regenerated (brief section 7). + try { + Path override = getDataFolder().toPath().resolve("vault_slots.json"); + this.manifest = Manifest.load(override, getClassLoader()); + } catch (Exception e) { + getLogger().severe("Could not load vault_slots.json: " + e.getMessage()); + getLogger().severe("Generate it with tools/gen_artifacts.py and restart. Disabling."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + // Bootstrap runs once at startup, before the server accepts players. All + // *runtime* database I/O goes through the async scheduler; this does not. + try { + this.database = new Database(this); + this.database.bootstrap(manifest); + } catch (Exception e) { + getLogger().severe("Database bootstrap failed: " + e.getMessage()); + getLogger().severe("Check config.yml credentials and that the schema is reachable. Disabling."); + getServer().getPluginManager().disablePlugin(this); + return; + } + + getConsoleSender().sendMessage(prefix() + "§aPlugin is now enabled!"); FileUtil fileUtil = new FileUtil(this); VaultUtil vaultUtil = new VaultUtil(this); - // Register commands register("bvstart", new StartCommand(this)); register("bvsubmit", new SubmitCommand(this)); register("bvprogress", new ProgressCommand(this)); @@ -30,16 +69,46 @@ public void onEnable() { // Register events - VaultUtil.generateVaultItems(this); - scheduleVaultStateTask(this, vaultUtil, fileUtil); + ScheduleUtil.scheduleVaultStateTask(this, vaultUtil, fileUtil); } @Override public void onDisable() { - getConsoleSender().sendMessage(translatePrefix() + "§cPlugin is now disabled."); + if (database != null) database.close(); + getConsoleSender().sendMessage(prefix() + "§cPlugin is now disabled."); + } + + public Database database() { + return database; } - private void register(String name, org.bukkit.command.CommandExecutor executor) { + public Manifest manifest() { + return manifest; + } + + /** The configured world the schematic origin sits in, or null if not loaded. */ + public World originWorld() { + return getServer().getWorld(getConfig().getString("origin.world", "world")); + } + + /** Resolve a manifest-relative coordinate to a live world location. */ + public Location resolve(int[] rel) { + return new Location(originWorld(), + getConfig().getInt("origin.x") + rel[0], + getConfig().getInt("origin.y") + rel[1], + getConfig().getInt("origin.z") + rel[2]); + } + + public String prefix() { + return ChatColor.translateAlternateColorCodes('&', getConfig().getString("lang.prefix", "")); + } + + /** Send a player-facing message through the configured prefix. */ + public void tell(org.bukkit.command.CommandSender to, String message) { + to.sendMessage(prefix() + message); + } + + private void register(String name, CommandExecutor executor) { PluginCommand command = getCommand(name); if (command == null) { getLogger().severe("Command '" + name + "' is missing from plugin.yml; skipping registration."); @@ -47,9 +116,4 @@ private void register(String name, org.bukkit.command.CommandExecutor executor) } command.setExecutor(executor); } - - private String translatePrefix() { - return org.bukkit.ChatColor.translateAlternateColorCodes('&', - getConfig().getString("lang.prefix", "")); - } } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java index 699d5ed..169fcbd 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java @@ -1,74 +1,60 @@ package dev.anchorlight.blockvault.commands; +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import java.io.File; -import java.util.*; +import java.util.List; +/** Top 10 contributors, plus the viewer's own rank if they are outside it. */ public class LeaderboardCommand implements CommandExecutor { - private final Plugin plugin; + + private final BlockVault plugin; private final VaultUtil vaultUtil; - public LeaderboardCommand(Plugin plugin) { + public LeaderboardCommand(BlockVault plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("§cOnly players can view the leaderboard!"); + if (!sender.hasPermission("blockvault.leaderboard")) { + plugin.tell(sender, "§cYou don't have permission to use this command!"); return true; } - if (!vaultUtil.hasStarted()) { - sender.sendMessage("§cYou cannot check the leaderboard as the Vault has not been opened yet."); + plugin.tell(sender, "§cThe vault has not been opened yet."); return true; } - // Load the player data file - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - // Check if 'vault_data' section exists - if (vaultData.getConfigurationSection("vault_data") == null) { - sender.sendMessage("§cNo player data found! Cannot produce leaderboard."); - return true; - } - - // Get all player points data, excluding "global_collected_items" - Set playerNames = vaultData.getConfigurationSection("vault_data").getKeys(false); - Map playerPoints = new HashMap<>(); - - for (String playerName : playerNames) { - if (!playerName.equalsIgnoreCase("global_collected_items")) { // Exclude global_collected_items - int points = vaultData.getInt("vault_data." + playerName + ".points"); - playerPoints.put(playerName, points); - } - } - - // Sort players by points in descending order - List> sortedPlayers = new ArrayList<>(playerPoints.entrySet()); - sortedPlayers.sort((entry1, entry2) -> Integer.compare(entry2.getValue(), entry1.getValue())); - - // Display top 10 players - sender.sendMessage("§aVault Leaderboard:"); - sender.sendMessage("§a================================="); - - int topPlayersCount = Math.min(5, sortedPlayers.size()); - for (int i = 0; i < topPlayersCount; i++) { - Map.Entry entry = sortedPlayers.get(i); - String playerName = entry.getKey(); - int points = entry.getValue(); - sender.sendMessage("§f" + (i + 1) + ". " + playerName + " - " + points + " points"); - } - + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + List top = plugin.database().topContributors(10); + int ownRank = (sender instanceof Player p) ? plugin.database().rankOf(p.getUniqueId()) : -1; + + plugin.getServer().getScheduler().runTask(plugin, () -> { + plugin.tell(sender, "§aTop contributors"); + if (top.isEmpty()) { + sender.sendMessage("§7 Nobody has donated a block yet."); + return; + } + int i = 1; + boolean sawViewer = false; + for (Database.LeaderRow r : top) { + if (sender instanceof Player p && p.getUniqueId().equals(r.uuid())) sawViewer = true; + sender.sendMessage(String.format("§e%2d. §f%-16s §7%d pts, %d blocks", + i++, r.name(), r.points(), r.blocks())); + } + if (!sawViewer && ownRank > 0) { + sender.sendMessage("§8 …"); + sender.sendMessage("§e" + ownRank + ". §f" + sender.getName() + " §7(you)"); + } + }); + }); return true; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java index e5d8f7a..1766f21 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java @@ -1,57 +1,63 @@ package dev.anchorlight.blockvault.commands; +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.model.TargetEntry; import dev.anchorlight.blockvault.util.VaultUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import java.io.File; -import java.util.List; +import java.util.Set; +/** Two bars: the current chapter, and the whole season. */ public class ProgressCommand implements CommandExecutor { - private final Plugin plugin; + + private final BlockVault plugin; private final VaultUtil vaultUtil; - public ProgressCommand(Plugin plugin) { + public ProgressCommand(BlockVault plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("§cOnly players can use this command!"); + if (!sender.hasPermission("blockvault.progress")) { + plugin.tell(sender, "§cYou don't have permission to use this command!"); return true; } - if (!vaultUtil.hasStarted()) { - sender.sendMessage("§cYou cannot check progress as the Vault has not been opened yet."); + plugin.tell(sender, "§cThe vault has not been opened yet."); return true; } - Player player = (Player) sender; - - // Load vault items and vault data - File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - - YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - // Get the total number of items in the vault - int totalItems = vaultItems.getConfigurationSection("items").getKeys(false).size(); - - // Get the player's progress percentage and progress bar - String progress = vaultUtil.getProgress(vaultData, totalItems); - - // Display progress - player.sendMessage("§aVault Progress"); - player.sendMessage("=================================="); - player.sendMessage(progress); + Set collected = plugin.database().collectedSnapshot(); + int chapter = plugin.getConfig().getInt("vault.current-chapter", 1); + + int chapTotal = 0, chapDone = 0, allTotal = 0, allDone = 0; + for (TargetEntry e : plugin.manifest().entries().values()) { + boolean done = collected.contains(e.material()); + allTotal++; + if (done) allDone++; + if (e.chapter() == chapter) { + chapTotal++; + if (done) chapDone++; + } + } + plugin.tell(sender, "§aVault progress"); + sender.sendMessage(bar("Chapter " + chapter, chapDone, chapTotal)); + sender.sendMessage(bar("Overall", allDone, allTotal)); return true; } -} \ No newline at end of file + + private static String bar(String labelText, int done, int total) { + int len = 30; + int filled = total == 0 ? 0 : (int) Math.round((double) done / total * len); + StringBuilder b = new StringBuilder("§f").append(labelText).append(" §8["); + for (int i = 0; i < len; i++) b.append(i < filled ? "§a|" : "§7|"); + int pct = total == 0 ? 0 : (int) Math.round((double) done / total * 100); + b.append("§8] §e").append(done).append('/').append(total).append(" §7(").append(pct).append("%)"); + return b.toString(); + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java index cf7956f..53ff777 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/StartCommand.java @@ -1,19 +1,20 @@ package dev.anchorlight.blockvault.commands; +import dev.anchorlight.blockvault.BlockVault; import dev.anchorlight.blockvault.util.FileUtil; import dev.anchorlight.blockvault.util.VaultUtil; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.plugin.Plugin; +/** {@code /bvstart} opens the event; {@code /bvstart stop} closes it again. */ public class StartCommand implements CommandExecutor { - private final Plugin plugin; + + private final BlockVault plugin; private final VaultUtil vaultUtil; private final FileUtil fileUtil; - public StartCommand(Plugin plugin) { + public StartCommand(BlockVault plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); this.fileUtil = new FileUtil(plugin); @@ -22,29 +23,28 @@ public StartCommand(Plugin plugin) { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("blockvault.start")) { - sender.sendMessage(ChatColor.RED + "You don't have permission to use this command!"); + plugin.tell(sender, "§cYou don't have permission to use this command!"); return true; } - if (vaultUtil.hasStarted()) { - sender.sendMessage(ChatColor.RED + "The Vault challenge has already started."); + boolean stopping = args.length > 0 && args[0].equalsIgnoreCase("stop"); + + if (stopping) { + if (!vaultUtil.hasStarted()) { + plugin.tell(sender, "§cThe vault is not open."); + return true; + } + fileUtil.updateConfigValue("vault.started", false); + plugin.tell(sender, "§eThe vault is now closed. Submissions are paused."); return true; } - try { - String currentDateTime = java.time.LocalDateTime.now() - .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - - // Update specific config values - fileUtil.updateConfigValue("vault.starttime", currentDateTime); - fileUtil.updateConfigValue("vault.started", true); - - sender.sendMessage(ChatColor.GREEN + "The vault challenge has started, the vault is open!"); - } catch (Exception e) { - sender.sendMessage(ChatColor.RED + "An error occurred while starting the vault."); - e.printStackTrace(); + if (vaultUtil.hasStarted()) { + plugin.tell(sender, "§cThe vault is already open."); + return true; } - + fileUtil.updateConfigValue("vault.started", true); + plugin.tell(sender, "§aThe vault is open! Players may now submit blocks."); return true; } } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index 829c783..18392a5 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -1,112 +1,156 @@ package dev.anchorlight.blockvault.commands; -import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.model.TargetEntry; +import dev.anchorlight.blockvault.util.HeadUtil; import dev.anchorlight.blockvault.util.VaultUtil; +import org.bukkit.GameMode; +import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Sound; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.plugin.Plugin; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -import static dev.anchorlight.blockvault.util.VaultUtil.*; +/** + * Submit the held block. Order is always: write -> confirm commit -> consume. + * If the database is unreachable the item stays in the player's hand. + */ public class SubmitCommand implements CommandExecutor { - private final Plugin plugin; + + private final BlockVault plugin; private final VaultUtil vaultUtil; - public SubmitCommand(Plugin plugin) { + public SubmitCommand(BlockVault plugin) { this.plugin = plugin; this.vaultUtil = new VaultUtil(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("§cOnly players can use this command!"); + if (!(sender instanceof Player player)) { + plugin.tell(sender, "§cOnly players can use this command!"); + return true; + } + if (!player.hasPermission("blockvault.submit")) { + plugin.tell(player, "§cYou don't have permission to submit blocks."); return true; } - - Player player = (Player) sender; - if (!vaultUtil.hasStarted()) { - sender.sendMessage("§cYou cannot submit items as the Vault has not been opened yet."); + plugin.tell(player, "§cThe vault has not been opened yet."); return true; } - - ItemStack itemInHand = player.getInventory().getItemInMainHand(); - - if (itemInHand == null || itemInHand.getType() == Material.AIR) { - player.sendMessage("§cYou are not holding any item or block!"); + if (player.getGameMode() == GameMode.CREATIVE) { + plugin.tell(player, "§cSubmissions are not allowed in creative mode."); return true; } - - // Get item display name or fallback to material name - ItemMeta meta = itemInHand.getItemMeta(); - String displayName = (meta != null && meta.hasDisplayName()) ? meta.getDisplayName() : formatMaterialName(itemInHand.getType()); - String itemName = itemInHand.getType().name().toLowerCase(); - - // Load configuration files - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - File vaultItemsFile = new File(plugin.getDataFolder(), "vault_items.yml"); - File configFile = new File(plugin.getDataFolder(), "config.yml"); - - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - YamlConfiguration vaultItems = YamlConfiguration.loadConfiguration(vaultItemsFile); - YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); - - // Check if item has already been collected globally - String collector = vaultData.getString("vault_data.global_collected_items." + itemName); - if (collector != null) { - player.sendMessage("§cItem " + displayName + " has already been collected by " + collector + "!"); + if (!inRegion(player.getLocation())) { + plugin.tell(player, "§cYou must be inside the vault to submit a block."); return true; } - // Add item to player's collected items - List playerItems = vaultData.getStringList("vault_data." + player.getName() + ".collected_items"); - if (playerItems.contains(itemName)) { - player.sendMessage("§cYou have already collected " + displayName + "!"); + ItemStack held = player.getInventory().getItemInMainHand(); + if (held.getType() == Material.AIR || held.getAmount() < 1) { + plugin.tell(player, "§cYou are not holding anything."); return true; } - // Determine the item's point category and corresponding points - String category = vaultItems.getString("items." + itemName, "COMMON"); // Default to COMMON - int pointsPerCategory = config.getInt("points." + category, 1); // Default to 1 point + Material type = held.getType(); + String material = type.getKey().getKey(); // lowercase registry id + String pretty = VaultUtil.formatMaterialName(type); - playerItems.add(itemName); + TargetEntry entry = plugin.manifest().entry(material); + if (entry == null) { + plugin.tell(player, "§c" + pretty + " is not part of this collection."); + return true; + } - vaultData.set("vault_data." + player.getName() + ".collected_items", playerItems); + Database db = plugin.database(); + if (db.isCollected(material)) { + plugin.tell(player, "§e" + pretty + " has already been donated."); + return true; + } - // Update player points - int currentPoints = vaultData.getInt("vault_data." + player.getName() + ".points", 0); - vaultData.set("vault_data." + player.getName() + ".points", currentPoints + pointsPerCategory); + int points = plugin.getConfig().getInt("points." + entry.rarity(), 1); + org.bukkit.profile.PlayerProfile profile = player.getPlayerProfile(); + String profileJson = HeadUtil.toJson(profile); + + plugin.tell(player, "§7Submitting " + pretty + "…"); + + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Database.SubmitOutcome outcome = + db.submit(material, player.getUniqueId(), player.getName(), points, profileJson); + + plugin.getServer().getScheduler().runTask(plugin, () -> { + switch (outcome) { + case ALREADY_TAKEN -> plugin.tell(player, + "§e" + pretty + " was donated by someone else first. Your block is untouched."); + case DB_ERROR -> plugin.tell(player, + "§cCould not record that right now - your block is still in your hand. Please retry."); + case OK -> { + // Write committed. Only now is it safe to consume the item. + consumeOne(player, type); + placeHead(entry, profile); + plugin.tell(player, "§aDonated " + pretty + "! §7(+" + points + + (points == 1 ? " point)" : " points)")); + celebrate(player, entry.rarity()); + } + } + }); + }); + return true; + } - // Update global collected items - vaultData.set("vault_data.global_collected_items." + itemName, player.getName()); + private void consumeOne(Player player, Material expected) { + ItemStack held = player.getInventory().getItemInMainHand(); + if (held.getType() != expected || held.getAmount() < 1) { + // Player swapped items between the command and the commit. The block + // is already recorded; not consuming it is a harmless edge case. + plugin.getLogger().warning(player.getName() + " moved " + expected + + " before it could be consumed; it was recorded but not removed."); + return; + } + if (held.getAmount() > 1) { + held.setAmount(held.getAmount() - 1); + } else { + player.getInventory().setItemInMainHand(null); + } + } - try { - vaultData.save(vaultDataFile); + private void placeHead(TargetEntry entry, org.bukkit.profile.PlayerProfile profile) { + // The head is only shown once the chapter floor is open. Chapter gating + // is applied in the reconciliation pass; place unconditionally for now. + Location loc = plugin.resolve(entry.head()); + if (loc.getWorld() == null) return; + HeadUtil.placeHead(loc, entry.face(), profile); + } - // Remove one item from the player's hand - if (itemInHand.getAmount() > 1) { - itemInHand.setAmount(itemInHand.getAmount() - 1); - } else { - player.getInventory().setItemInMainHand(null); - } + private void celebrate(Player player, String rarity) { + Sound sound = switch (rarity) { + case "rare" -> Sound.UI_TOAST_CHALLENGE_COMPLETE; + case "uncommon" -> Sound.ENTITY_PLAYER_LEVELUP; + default -> Sound.ENTITY_EXPERIENCE_ORB_PICKUP; + }; + player.playSound(player.getLocation(), sound, 1f, 1f); + } - player.sendMessage("§aSuccessfully added " + displayName + " to your vault! (§b+" + pointsPerCategory + " points§a)"); - } catch (IOException e) { - player.sendMessage("§cFailed to save collected item. Please try again."); - e.printStackTrace(); + private boolean inRegion(Location loc) { + if (loc.getWorld() == null || plugin.originWorld() == null + || !loc.getWorld().equals(plugin.originWorld())) { + return false; } - - return true; + int ox = plugin.getConfig().getInt("origin.x"); + int oy = plugin.getConfig().getInt("origin.y"); + int oz = plugin.getConfig().getInt("origin.z"); + int[] lo = plugin.manifest().min(); + int[] hi = plugin.manifest().max(); + int margin = 3; + int x = loc.getBlockX() - ox, y = loc.getBlockY() - oy, z = loc.getBlockZ() - oz; + return x >= lo[0] - margin && x <= hi[0] + margin + && y >= lo[1] - margin && y <= hi[1] + margin + && z >= lo[2] - margin && z <= hi[2] + margin; } } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java index a01eb6f..8ef27da 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/UpdateStateCommand.java @@ -1,45 +1,27 @@ package dev.anchorlight.blockvault.commands; -import dev.anchorlight.blockvault.util.FileUtil; +import dev.anchorlight.blockvault.BlockVault; import dev.anchorlight.blockvault.util.VaultUtil; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; -import org.bukkit.entity.ItemFrame; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; public class UpdateStateCommand implements CommandExecutor { - private final Plugin plugin; + private final VaultUtil vaultUtil; - private final FileUtil fileUtil; - public UpdateStateCommand(Plugin plugin) { - this.plugin = plugin; + public UpdateStateCommand(BlockVault plugin) { this.vaultUtil = new VaultUtil(plugin); - this.fileUtil = new FileUtil(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("§cOnly players can use this command!"); - return true; - } - - Player player = (Player) sender; - - if (!player.hasPermission("blockvault.updatestate")) { + if (!sender.hasPermission("blockvault.updatestate")) { sender.sendMessage("§cYou don't have permission to use this command!"); return true; } - - vaultUtil.updateVaultState(player); + // Console-runnable by design. + vaultUtil.updateVaultState(sender); return true; } -} \ No newline at end of file +} diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java new file mode 100644 index 0000000..eb7cec1 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -0,0 +1,340 @@ +package dev.anchorlight.blockvault.db; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.model.Manifest; +import dev.anchorlight.blockvault.model.TargetEntry; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.ByteBuffer; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Owns the connection pool and every SQL statement. All callers run the + * blocking methods here on the async scheduler - the main thread never enters. + * + *

The set of already-collected materials is held in memory as the read path + * and updated only after a write commits. + */ +public final class Database { + + /** MySQL error code for a duplicate primary/unique key. */ + private static final int ER_DUP_ENTRY = 1062; + + private final BlockVault plugin; + private final String edition; + private final HikariDataSource ds; + private final Set collected = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public enum SubmitOutcome { OK, ALREADY_TAKEN, DB_ERROR } + + public Database(BlockVault plugin) { + this.plugin = plugin; + this.edition = plugin.getConfig().getString("edition", "26.2"); + + var cfg = plugin.getConfig(); + String host = cfg.getString("database.host", "localhost"); + int port = cfg.getInt("database.port", 3306); + String name = cfg.getString("database.name", "blockvault"); + + HikariConfig hc = new HikariConfig(); + hc.setPoolName("BlockVault"); + hc.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + name + + "?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true"); + hc.setUsername(cfg.getString("database.user", "blockvault")); + hc.setPassword(cfg.getString("database.password", "")); + hc.setMaximumPoolSize(cfg.getInt("database.pool.maximum-pool-size", 6)); + hc.setMinimumIdle(cfg.getInt("database.pool.minimum-idle", 1)); + hc.setConnectionTimeout(cfg.getLong("database.pool.connection-timeout-ms", 8000)); + hc.setMaxLifetime(cfg.getLong("database.pool.max-lifetime-ms", 1_740_000)); + hc.setInitializationFailTimeout(-1); // don't crash enable; report on first use + + this.ds = new HikariDataSource(hc); + } + + public String edition() { + return edition; + } + + /** Apply the schema, seed the target list and chapters, warm the collected set. */ + public void bootstrap(Manifest manifest) throws SQLException { + try (Connection c = ds.getConnection()) { + runSchema(c); + seedTargets(c, manifest); + seedChapters(c, manifest); + warmCollected(c); + } + plugin.getLogger().info("Database ready: " + collected.size() + + " blocks already collected for edition " + edition + "."); + } + + private void runSchema(Connection c) throws SQLException { + StringBuilder sql = new StringBuilder(); + try (InputStream in = plugin.getResource("blockvault_schema.sql")) { + if (in == null) throw new SQLException("blockvault_schema.sql missing from jar"); + sql.append(new String(in.readAllBytes(), StandardCharsets.UTF_8)); + } catch (java.io.IOException e) { + throw new SQLException("cannot read blockvault_schema.sql", e); + } + try (Statement st = c.createStatement()) { + for (String raw : sql.toString().split(";\\s*\\r?\\n")) { + String stmt = stripComments(raw).trim(); + if (!stmt.isEmpty()) st.execute(stmt); + } + } + } + + private static String stripComments(String block) { + StringBuilder out = new StringBuilder(); + for (String line : block.split("\\r?\\n")) { + if (line.stripLeading().startsWith("--")) continue; + out.append(line).append('\n'); + } + return out.toString(); + } + + private void seedTargets(Connection c, Manifest manifest) throws SQLException { + if (rowCount(c, "bv_target") > 0) return; + String q = "INSERT INTO bv_target (material,edition,chapter,rarity,section," + + "sign_x,sign_y,sign_z,frame_x,frame_y,frame_z,head_x,head_y,head_z,facing) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + try (PreparedStatement ps = c.prepareStatement(q)) { + for (TargetEntry e : manifest.entries().values()) { + ps.setString(1, e.material()); + ps.setString(2, edition); + ps.setInt(3, e.chapter()); + ps.setString(4, e.rarity()); + ps.setString(5, e.section()); + ps.setInt(6, e.sign()[0]); ps.setInt(7, e.sign()[1]); ps.setInt(8, e.sign()[2]); + ps.setInt(9, e.frame()[0]); ps.setInt(10, e.frame()[1]); ps.setInt(11, e.frame()[2]); + ps.setInt(12, e.head()[0]); ps.setInt(13, e.head()[1]); ps.setInt(14, e.head()[2]); + ps.setString(15, e.facing()); + ps.addBatch(); + } + ps.executeBatch(); + } + plugin.getLogger().info("Seeded " + manifest.entries().size() + + " target rows for edition " + edition + "."); + } + + private void seedChapters(Connection c, Manifest manifest) throws SQLException { + if (rowCount(c, "bv_chapter") > 0) return; + String[][] meta = { + {"1", "Foundations", "The Undercroft"}, + {"2", "Green & Growing", "The Conservatory"}, + {"3", "Into the Deep", "The Deep"}, + {"4", "Every Colour", "The Gallery"}, + {"5", "The Nether", "The Forge"}, + {"6", "The End", "The Observatory"}, + }; + String q = "INSERT INTO bv_chapter (chapter,edition,title,room,seal_x,seal_y,seal_z) " + + "VALUES (?,?,?,?,?,?,?)"; + try (PreparedStatement ps = c.prepareStatement(q)) { + for (String[] m : meta) { + int ch = Integer.parseInt(m[0]); + int[] seal = manifest.seal(ch); // null for chapter 1 + ps.setInt(1, ch); + ps.setString(2, edition); + ps.setString(3, m[1]); + ps.setString(4, m[2]); + if (seal == null) { + ps.setNull(5, java.sql.Types.INTEGER); + ps.setNull(6, java.sql.Types.INTEGER); + ps.setNull(7, java.sql.Types.INTEGER); + } else { + ps.setInt(5, seal[0]); ps.setInt(6, seal[1]); ps.setInt(7, seal[2]); + } + ps.addBatch(); + } + ps.executeBatch(); + } + } + + private void warmCollected(Connection c) throws SQLException { + collected.clear(); + try (PreparedStatement ps = c.prepareStatement( + "SELECT material FROM bv_submission WHERE edition = ?")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) collected.add(rs.getString(1)); + } + } + } + + private int rowCount(Connection c, String table) throws SQLException { + try (Statement st = c.createStatement(); + ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + table)) { + return rs.next() ? rs.getInt(1) : 0; + } + } + + // ----------------------------------------------------------------- reads + + /** In-memory read path. Never hits the database. */ + public boolean isCollected(String material) { + return collected.contains(material); + } + + public Set collectedSnapshot() { + return new HashSet<>(collected); + } + + public int collectedCount() { + return collected.size(); + } + + /** material -> cached profile JSON (may be null) for this edition. Blocking. */ + public java.util.Map allProfiles() { + java.util.Map out = new java.util.HashMap<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT material, profile FROM bv_submission WHERE edition = ?")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) out.put(rs.getString(1), rs.getString(2)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Could not load cached profiles: " + e.getMessage()); + } + return out; + } + + // ----------------------------------------------------------------- writes + + /** + * Record a submission. Blocking - call on the async scheduler. + * The item must NOT be consumed until this returns {@link SubmitOutcome#OK}. + * + * @param profileJson cached skin profile, or null + */ + public SubmitOutcome submit(String material, UUID uuid, String name, + int points, String profileJson) { + byte[] id = toBytes(uuid); + try (Connection c = ds.getConnection()) { + c.setAutoCommit(false); + try { + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO bv_contributor (uuid,last_name) VALUES (?,?) " + + "ON DUPLICATE KEY UPDATE last_name=VALUES(last_name), " + + "last_seen=CURRENT_TIMESTAMP")) { + ps.setBytes(1, id); + ps.setString(2, name); + ps.executeUpdate(); + } + + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO bv_submission (material,edition,uuid,points,profile) " + + "VALUES (?,?,?,?,?)")) { + ps.setString(1, material); + ps.setString(2, edition); + ps.setBytes(3, id); + ps.setInt(4, points); + if (profileJson == null) ps.setNull(5, java.sql.Types.VARCHAR); + else ps.setString(5, profileJson); + ps.executeUpdate(); + } catch (SQLException dup) { + if (dup.getErrorCode() == ER_DUP_ENTRY) { + c.rollback(); + collected.add(material); // reconcile the memory cache + return SubmitOutcome.ALREADY_TAKEN; + } + throw dup; + } + + try (PreparedStatement ps = c.prepareStatement( + "UPDATE bv_contributor SET points = points + ?, " + + "blocks_given = blocks_given + 1 WHERE uuid = ?")) { + ps.setInt(1, points); + ps.setBytes(2, id); + ps.executeUpdate(); + } + + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO bv_audit (actor,action,material,detail) " + + "VALUES (?,?,?,JSON_OBJECT('points',?))")) { + ps.setBytes(1, id); + ps.setString(2, "submit"); + ps.setString(3, material); + ps.setInt(4, points); + ps.executeUpdate(); + } + + c.commit(); + collected.add(material); + return SubmitOutcome.OK; + } catch (SQLException e) { + c.rollback(); + throw e; + } + } catch (SQLException e) { + plugin.getLogger().severe("Submission write failed for " + material + + " by " + name + ": " + e.getMessage()); + return SubmitOutcome.DB_ERROR; + } + } + + public record LeaderRow(UUID uuid, String name, long points, long blocks) {} + + /** Top {@code limit} contributors, then the viewer's own row if outside it. Blocking. */ + public java.util.List topContributors(int limit) { + java.util.List rows = new java.util.ArrayList<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT uuid, last_name, points, blocks FROM bv_leaderboard LIMIT ?")) { + ps.setInt(1, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + rows.add(new LeaderRow(fromBytes(rs.getBytes(1)), rs.getString(2), + rs.getLong(3), rs.getLong(4))); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Leaderboard query failed: " + e.getMessage()); + } + return rows; + } + + /** 1-based rank of {@code uuid} on the leaderboard, or -1 if they have no row. Blocking. */ + public int rankOf(UUID uuid) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT COUNT(*) + 1 FROM bv_contributor a " + + "JOIN bv_contributor b ON b.uuid = ? " + + "WHERE a.points > b.points")) { + ps.setBytes(1, toBytes(uuid)); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : -1; + } + } catch (SQLException e) { + plugin.getLogger().severe("Rank query failed: " + e.getMessage()); + return -1; + } + } + + public void close() { + if (ds != null && !ds.isClosed()) ds.close(); + } + + static byte[] toBytes(UUID u) { + return ByteBuffer.allocate(16) + .putLong(u.getMostSignificantBits()) + .putLong(u.getLeastSignificantBits()) + .array(); + } + + static UUID fromBytes(byte[] b) { + ByteBuffer bb = ByteBuffer.wrap(b); + return new UUID(bb.getLong(), bb.getLong()); + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/model/Manifest.java b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java new file mode 100644 index 0000000..dc34400 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java @@ -0,0 +1,126 @@ +package dev.anchorlight.blockvault.model; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The parsed vault_slots.json manifest. Immutable for the season. + * + *

Loaded from the plugin data folder if present, otherwise the bundled + * resource. If neither exists the caller must fail loudly - the plugin never + * regenerates the target list (brief section 7). + */ +public final class Manifest { + + private final String version; + private final int dataVersion; + private final Map entries; + private final Map leader; + private final Map seals; + private final int[] min; + private final int[] max; + + private Manifest(String version, int dataVersion, Map entries, + Map leader, Map seals) { + this.version = version; + this.dataVersion = dataVersion; + this.entries = entries; + this.leader = leader; + this.seals = seals; + + int[] lo = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] hi = {Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE}; + for (TargetEntry e : entries.values()) { + for (int[] p : new int[][]{e.sign(), e.head()}) { + for (int i = 0; i < 3; i++) { + lo[i] = Math.min(lo[i], p[i]); + hi[i] = Math.max(hi[i], p[i]); + } + } + } + this.min = lo; + this.max = hi; + } + + /** Origin-relative bounding box of every shelf cell, inclusive. */ + public int[] min() { return min; } + public int[] max() { return max; } + + public String version() { return version; } + public int dataVersion() { return dataVersion; } + public Map entries() { return entries; } + public TargetEntry entry(String material) { return entries.get(material); } + public int[] leader(String key) { return leader.get(key); } + public int[] seal(int chapter) { return seals.get(chapter); } + + public static Manifest load(Path dataFolderFile, ClassLoader loader) throws IOException { + if (Files.isRegularFile(dataFolderFile)) { + try (Reader r = Files.newBufferedReader(dataFolderFile, StandardCharsets.UTF_8)) { + return parse(r); + } + } + try (InputStream in = loader.getResourceAsStream("vault_slots.json")) { + if (in == null) { + throw new IOException("vault_slots.json not found in " + dataFolderFile + + " or on the classpath - generate it with tools/gen_artifacts.py"); + } + try (Reader r = new InputStreamReader(in, StandardCharsets.UTF_8)) { + return parse(r); + } + } + } + + private static Manifest parse(Reader reader) { + Gson gson = new Gson(); + JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); + + String version = root.get("version").getAsString(); + int dataVersion = root.get("data_version").getAsInt(); + + Map entries = new LinkedHashMap<>(); + root.getAsJsonArray("entries").forEach(el -> { + JsonObject o = el.getAsJsonObject(); + String block = o.get("block").getAsString(); + entries.put(block, new TargetEntry( + block, + o.get("chapter").getAsInt(), + o.get("rarity").getAsString(), + o.get("section").getAsString(), + triple(o, "sign"), + triple(o, "frame"), + triple(o, "head"), + o.get("facing").getAsString() + )); + }); + + Map leader = new LinkedHashMap<>(); + JsonObject lo = root.getAsJsonObject("leader"); + for (String k : lo.keySet()) { + leader.put(k, gson.fromJson(lo.get(k), int[].class)); + } + + Map seals = new LinkedHashMap<>(); + JsonObject so = root.getAsJsonObject("seals"); + for (String k : so.keySet()) { + seals.put(Integer.parseInt(k), gson.fromJson(so.get(k), int[].class)); + } + + return new Manifest(version, dataVersion, entries, leader, seals); + } + + private static int[] triple(JsonObject o, String key) { + var arr = o.getAsJsonArray(key); + return new int[]{ arr.get(0).getAsInt(), arr.get(1).getAsInt(), arr.get(2).getAsInt() }; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/model/TargetEntry.java b/src/main/java/dev/anchorlight/blockvault/model/TargetEntry.java new file mode 100644 index 0000000..a4a5ac6 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/model/TargetEntry.java @@ -0,0 +1,28 @@ +package dev.anchorlight.blockvault.model; + +import org.bukkit.block.BlockFace; + +/** + * One row of the frozen target list. Coordinates are relative to the schematic + * origin; the runtime adds the configured world origin before touching blocks. + */ +public record TargetEntry( + String material, + int chapter, + String rarity, + String section, + int[] sign, + int[] frame, + int[] head, + String facing +) { + public BlockFace face() { + return switch (facing) { + case "north" -> BlockFace.NORTH; + case "south" -> BlockFace.SOUTH; + case "east" -> BlockFace.EAST; + case "west" -> BlockFace.WEST; + default -> throw new IllegalStateException("bad facing: " + facing); + }; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/util/HeadUtil.java b/src/main/java/dev/anchorlight/blockvault/util/HeadUtil.java new file mode 100644 index 0000000..becaa4b --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/util/HeadUtil.java @@ -0,0 +1,66 @@ +package dev.anchorlight.blockvault.util; + +import com.google.gson.JsonObject; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.Skull; +import org.bukkit.block.data.Directional; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; + +import java.net.URL; +import java.util.UUID; + +/** Places donor player heads and (de)serialises the cached skin profile JSON. */ +public final class HeadUtil { + + private HeadUtil() {} + + /** Place a wall-mounted player head at {@code loc} facing {@code aisle}, wearing {@code profile}. */ + public static void placeHead(Location loc, BlockFace aisle, PlayerProfile profile) { + Block block = loc.getBlock(); + block.setType(Material.PLAYER_WALL_HEAD, false); + if (block.getBlockData() instanceof Directional dir) { + dir.setFacing(aisle); + block.setBlockData(dir, false); + } + if (block.getState() instanceof Skull skull) { + skull.setOwnerProfile(profile); + skull.update(true, false); + } + } + + /** Minimal JSON snapshot of a profile, enough to rebuild the head years later. */ + public static String toJson(PlayerProfile profile) { + JsonObject o = new JsonObject(); + if (profile.getUniqueId() != null) o.addProperty("id", profile.getUniqueId().toString()); + if (profile.getName() != null) o.addProperty("name", profile.getName()); + URL skin = profile.getTextures().getSkin(); + if (skin != null) { + o.addProperty("skin", skin.toString()); + o.addProperty("model", profile.getTextures().getSkinModel().name()); + } + return o.toString(); + } + + /** Rebuild a profile from {@link #toJson}. Used by /bvrepair. */ + public static PlayerProfile fromJson(org.bukkit.Server server, JsonObject o) { + UUID id = o.has("id") ? UUID.fromString(o.get("id").getAsString()) : null; + String name = o.has("name") ? o.get("name").getAsString() : null; + PlayerProfile profile = server.createPlayerProfile(id, name); + if (o.has("skin")) { + try { + PlayerTextures tex = profile.getTextures(); + tex.setSkin(new URL(o.get("skin").getAsString()), + o.has("model") ? PlayerTextures.SkinModel.valueOf(o.get("model").getAsString()) + : PlayerTextures.SkinModel.CLASSIC); + profile.setTextures(tex); + } catch (Exception ignored) { + // fall back to name/id lookup + } + } + return profile; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java b/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java index 339b754..0756e71 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/ScheduleUtil.java @@ -3,27 +3,26 @@ import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; -public class ScheduleUtil { - private final Plugin plugin; - private final VaultUtil vaultUtil; - private final FileUtil fileUtil; +public final class ScheduleUtil { - public ScheduleUtil(Plugin plugin) { - this.plugin = plugin; - this.vaultUtil = new VaultUtil(plugin); - this.fileUtil = new FileUtil(plugin); - } + private ScheduleUtil() {} public static void scheduleVaultStateTask(Plugin plugin, VaultUtil vaultUtil, FileUtil fileUtil) { - // Get the interval from the config file - int intervalMinutes = fileUtil.getConfig().getInt("vault.updateinterval", 15); // Default to 15 if not set + int intervalMinutes = fileUtil.getConfig().getInt("update.interval-minutes", 15); + if (intervalMinutes < 1) { + plugin.getLogger().warning("update.interval-minutes was " + intervalMinutes + + "; clamping to 1. A value of 0 would fail to schedule."); + intervalMinutes = 1; + } + int startupDelaySeconds = fileUtil.getConfig().getInt("update.startup-delay-seconds", 30); + if (startupDelaySeconds < 0) startupDelaySeconds = 0; - // Convert the interval from minutes to ticks (20 ticks per second, 60 seconds per minute) - long intervalTicks = 20L * 60L * intervalMinutes; + long delayTicks = 20L * startupDelaySeconds; + long periodTicks = 20L * 60L * intervalMinutes; - // Schedule the task to run every 'intervalTicks' ticks - Bukkit.getScheduler().runTaskTimer(plugin, () -> { - vaultUtil.updateVaultState(null); - }, 0L, intervalTicks); // Runs based on the interval in the config + // Delay the first run so worlds are loaded before it touches blocks. + Bukkit.getScheduler().runTaskTimer(plugin, + () -> vaultUtil.updateVaultState(null), + delayTicks, periodTicks); } } diff --git a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java index 6828079..b3c4476 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java @@ -1,231 +1,102 @@ package dev.anchorlight.blockvault.util; -import org.bukkit.Bukkit; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.model.TargetEntry; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Entity; -import org.bukkit.entity.ItemFrame; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; +import org.bukkit.block.Block; +import org.bukkit.command.CommandSender; +import org.bukkit.profile.PlayerProfile; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; public class VaultUtil { - private final Plugin plugin; - private final FileUtil fileUtil; - public VaultUtil(Plugin plugin) { + private final BlockVault plugin; + + public VaultUtil(BlockVault plugin) { this.plugin = plugin; - this.fileUtil = new FileUtil(plugin); } - // Method to check if the vault has started public boolean hasStarted() { return plugin.getConfig().getBoolean("vault.started", false); } - public static void generateVaultItems(Plugin plugin) { - File file = new File(plugin.getDataFolder(), "vault_items.yml"); - - if (file.exists()) { - plugin.getLogger().info("vault_items.yml already exists. Skipping generation."); - return; - } - - // Load excluded items from the config - List excludedItems = plugin.getConfig().getStringList("vault.excludeditems"); - - // Map to store valid items - Map itemData = new HashMap<>(); - - for (Material material : Material.values()) { - // Add materials that are either blocks or items and are not excluded - if ((material.isBlock() || material.isItem()) && - !excludedItems.contains(material.name().toLowerCase())) { - itemData.put(material.name().toLowerCase(), "COMMON"); // Default category - } - } - - // Create a YAML configuration and add the items - YamlConfiguration yaml = new YamlConfiguration(); - yaml.createSection("items", itemData); - - try { - yaml.save(file); - plugin.getLogger().info("Generated vault_items.yml with all valid blocks and items set to default category COMMON."); - } catch (IOException e) { - plugin.getLogger().severe("Failed to save vault_items.yml: " + e.getMessage()); - e.printStackTrace(); - } - } - /** - * Formats a Material name to a user-friendly display format. - * - * @param material The Material to format. - * @return The formatted name. + * Turns a Material into a display name: {@code cut_sandstone} -> "Cut Sandstone". + * Guards against empty tokens from a malformed key. */ public static String formatMaterialName(Material material) { - String[] words = material.name().toLowerCase().split("_"); - StringBuilder formattedName = new StringBuilder(); - for (String word : words) { - formattedName.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)).append(" "); + StringBuilder out = new StringBuilder(); + for (String word : material.getKey().getKey().split("_")) { + if (word.isEmpty()) continue; + out.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)) + .append(' '); } - return formattedName.toString().trim(); + return out.toString().trim(); } /** - * Checks if a specific item has been added to the vault by any player. + * Reconcile the world display against the database. Non-destructive: only + * the manifest {@code head} cells are ever written, never structure. + * Emits exactly one summary line (brief section 7, acceptance criterion 1). * - * @param itemName The name of the item to check. - * @return True if the item has been added, false otherwise. + * @param sender optional command sender to echo the summary to */ - public boolean isItemAdded(String itemName) { - // Normalize item name - itemName = itemName.toLowerCase(); - - File vaultDataFile = new File(plugin.getDataFolder(), "vault_data.yml"); - YamlConfiguration vaultData = YamlConfiguration.loadConfiguration(vaultDataFile); - - if (!vaultData.contains("vault_data")) { - return false; + public void updateVaultState(CommandSender sender) { + World world = plugin.originWorld(); + if (world == null) { + String msg = "BlockVault: origin world '" + + plugin.getConfig().getString("origin.world") + "' is not loaded; skipping."; + plugin.getLogger().warning(msg); + if (sender != null) plugin.tell(sender, "§c" + msg); + return; } - for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { - List collectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); - - for (String collectedItem : collectedItems) { - if (collectedItem.toLowerCase().equals(itemName)) { - return true; + // Database read off the main thread; block edits applied back on it. + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Map profiles = plugin.database().allProfiles(); + + plugin.getServer().getScheduler().runTask(plugin, () -> { + int placed = 0, cleared = 0, ok = 0; + for (TargetEntry entry : plugin.manifest().entries().values()) { + Location loc = plugin.resolve(entry.head()); + Block block = loc.getBlock(); + boolean collected = plugin.database().isCollected(entry.material()); + boolean hasHead = block.getType() == Material.PLAYER_WALL_HEAD + || block.getType() == Material.PLAYER_HEAD; + + if (collected && !hasHead) { + HeadUtil.placeHead(loc, entry.face(), + parseProfile(world, profiles.get(entry.material()))); + placed++; + } else if (!collected && hasHead) { + block.setType(Material.AIR, false); + cleared++; + } else { + ok++; + } } - } - } - - return false; + String summary = String.format( + "BlockVault reconcile: %d in place, %d heads added, %d removed (%d targets).", + ok, placed, cleared, plugin.manifest().entries().size()); + plugin.getLogger().info(summary); + if (sender != null) plugin.tell(sender, "§a" + summary); + }); + }); } - public void updateVaultState(Player player) { + private static PlayerProfile parseProfile(World world, String json) { + if (json == null || json.isBlank()) return null; try { - // Parse region coordinates from config - int upperX = fileUtil.getConfig().getInt("vault.region.upperregion.X"); - int upperY = fileUtil.getConfig().getInt("vault.region.upperregion.Y"); - int upperZ = fileUtil.getConfig().getInt("vault.region.upperregion.Z"); - - int lowerX = fileUtil.getConfig().getInt("vault.region.lowerregion.X"); - int lowerY = fileUtil.getConfig().getInt("vault.region.lowerregion.Y"); - int lowerZ = fileUtil.getConfig().getInt("vault.region.lowerregion.Z"); - - World world = Bukkit.getWorlds().get(0); - if (world == null) { - String errorMsg = "§cWorld not found! Stopping the update process."; - Bukkit.getLogger().warning(errorMsg); - if (player != null) player.sendMessage(errorMsg); - return; - } - - // Normalize coordinates - int minX = Math.min(upperX, lowerX); - int maxX = Math.max(upperX, lowerX); - int minY = Math.min(upperY, lowerY); - int maxY = Math.max(upperY, lowerY); - int minZ = Math.min(upperZ, lowerZ); - int maxZ = Math.max(upperZ, lowerZ); - - int foundCount = 0; - int updatedCount = 0; - - // Iterate over entities in the specified region - for (Entity entity : world.getEntities()) { - if (entity instanceof ItemFrame) { - Location loc = entity.getLocation(); - if (loc.getX() >= minX && loc.getX() <= maxX && - loc.getY() >= minY && loc.getY() <= maxY && - loc.getZ() >= minZ && loc.getZ() <= maxZ) { - - ItemFrame itemFrame = (ItemFrame) entity; - if (itemFrame.getItem() != null && itemFrame.getItem().getType() != Material.AIR) { - String itemName = itemFrame.getItem().getType().toString().toLowerCase(); - - boolean isInVault = isItemAdded(itemName); - String frameMessage = String.format("§aItemFrame at [%d, %d, %d] has item: %s (In Vault: %s).", - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), itemName, isInVault ? "Yes" : "No"); - Bukkit.getLogger().info(frameMessage); - if (player != null) player.sendMessage(frameMessage); - - // Get the block behind the item frame - Location blockBehindLoc = itemFrame.getLocation().getBlock().getRelative(itemFrame.getAttachedFace()).getLocation(); - Material newMaterial = isInVault ? Material.LIME_STAINED_GLASS : Material.RED_STAINED_GLASS; - blockBehindLoc.getBlock().setType(newMaterial); - - String updateMessage = String.format("§eBlock behind ItemFrame at [%d, %d, %d] replaced with %s.", - loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), newMaterial.name()); - Bukkit.getLogger().info(updateMessage); - if (player != null) player.sendMessage(updateMessage); - - foundCount++; - updatedCount++; - } - } - } - } - - String summaryMessage = String.format("§aFound %d item frames and updated %d blocks in the specified region.", - foundCount, updatedCount); - Bukkit.getLogger().info(summaryMessage); - if (player != null) player.sendMessage(summaryMessage); + JsonObject o = JsonParser.parseString(json).getAsJsonObject(); + return HeadUtil.fromJson(org.bukkit.Bukkit.getServer(), o); } catch (Exception e) { - String errorMessage = "§cAn error occurred while processing the region."; - Bukkit.getLogger().severe(errorMessage); - e.printStackTrace(); - if (player != null) player.sendMessage(errorMessage); - } - } - - /** - * Calculates the collective progress of all players in the vault and generates a progress bar. - * - * @param vaultData The YamlConfiguration object for vault data. - * @param totalItems The total number of items required for the vault. - * @return A formatted progress string showing the collective percentage and progress bar. - */ - public String getProgress(YamlConfiguration vaultData, int totalItems) { - int totalCollectedItems = 0; - - // Iterate through all player entries in the vault data to sum up collected items - for (String playerName : vaultData.getConfigurationSection("vault_data").getKeys(false)) { - List playerCollectedItems = vaultData.getStringList("vault_data." + playerName + ".collected_items"); - totalCollectedItems += playerCollectedItems.size(); + return null; } - - // Calculate the percentage of progress for the collective effort - int progressPercentage = (int) ((double) totalCollectedItems / totalItems * 100); - - // Create a progress bar - StringBuilder progressBar = new StringBuilder("§a["); - int barLength = 50; // Total bar length - int progressLength = (int) ((progressPercentage / 100.0) * barLength); - - for (int i = 0; i < barLength; i++) { - if (i < progressLength) { - progressBar.append("§a|"); - } else { - progressBar.append("§7|"); - } - } - progressBar.append("§a]"); - - // Create the progress string with total collected and total required items - String progressInfo = String.format("§e%d/%d collected", totalCollectedItems, totalItems); - - // Return the full progress string - return progressInfo + " §f(" + progressPercentage + "% complete)\n" + progressBar; } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 758a7fd..d790bf9 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,128 +1,46 @@ -# BlockVault +# BlockVault configuration +# +# This file holds database credentials. It is git-ignored - never commit a +# populated copy. See src/main/resources/blockvault_schema.sql for the schema; +# apply it before first start. -# Vault settings -vault: - starttime: null - started: false - updateinterval: 15 - region: - upperregion: - X: -138 - Y: 97 - Z: 118 - lowerregion: - X: 133 - Y: 68 - Z: 96 - excludeditems: - - debug_stick - - structure_block - - barrier - - command_block - - chain_command_block - - repeating_command_block - - light - - jigsaw - - knowledge_book - - spawner - - end_portal_frame - - frosted_ice - - bedrock - - dragon_egg - - piglin_brute_spawn_egg - - parrot_spawn_egg - - turtle_spawn_egg - - zombie_spawn_egg - - mule_spawn_egg - - armadillo_spawn_egg - - tropical_fish_spawn_egg - - blaze_spawn_egg - - shulker_spawn_egg - - sniffer_spawn_egg - - polar_bear_spawn_egg - - phantom_spawn_egg - - guardian_spawn_egg - - ravager_spawn_egg - - wandering_trader_spawn_egg - - villager_spawn_egg - - wither_skeleton_spawn_egg - - warden_spawn_egg - - ender_dragon_spawn_egg - - bat_spawn_egg - - sniffer_egg - - camel_spawn_egg - - creeper_spawn_egg - - breeze_spawn_egg - - iron_golem_spawn_egg - - hoglin_spawn_egg - - sheep_spawn_egg - - ocelot_spawn_egg - - witch_spawn_egg - - creaking_spawn_egg - - enderman_spawn_egg - - donkey_spawn_egg - - frog_spawn_egg - - llama_spawn_egg - - horse_spawn_egg - - glow_squid_spawn_egg - - bogged_spawn_egg - - chicken_spawn_egg - - piglin_spawn_egg - - wolf_spawn_egg - - cat_spawn_egg - - silverfish_spawn_egg - - rabbit_spawn_egg - - strider_spawn_egg - - slime_spawn_egg - - vindicator_spawn_egg - - wither_spawn_egg - - zombified_piglin_spawn_egg - - drowned_spawn_egg - - zombie_horse_spawn_egg - - husk_spawn_egg - - zombie_villager_spawn_egg - - trader_llama_spawn_egg - - mooshroom_spawn_egg - - stray_spawn_egg - - cow_spawn_egg - - vex_spawn_egg - - skeleton_horse_spawn_egg - - salmon_spawn_egg - - pig_spawn_egg - - cod_spawn_egg - - allay_spawn_egg - - dolphin_spawn_egg - - elder_guardian_spawn_egg - - zoglin_spawn_egg - - squid_spawn_egg - - cave_spider_spawn_egg - - ghast_spawn_egg - - axolotl_spawn_egg - - skeleton_spawn_egg - - pufferfish_spawn_egg - - goat_spawn_egg - - endermite_spawn_egg - - bee_spawn_egg - - snow_golem_spawn_egg - - evoker_spawn_egg - - panda_spawn_egg - - pillager_spawn_egg - - fox_spawn_egg - - spider_spawn_egg - - magma_cube_spawn_egg - - hoglin_spawn_egg - - sheep_spawn_egg +# Current target-list version. Blocks are frozen per edition (see brief section 1.3). +edition: '26.2' -timer: - enabled: false - timer-end: null +database: + host: localhost + port: 3306 + name: blockvault + user: blockvault + password: changeme + # Extra HikariCP tuning. Pool is small - the plugin is not write-heavy. + pool: + maximum-pool-size: 6 + minimum-idle: 1 + connection-timeout-ms: 8000 + # Keep below the server's wait_timeout. + max-lifetime-ms: 1740000 -# Points system settings +# Where the schematic origin sits in the live world. Every manifest coordinate +# is relative to this point and is added to it at runtime. +origin: + world: world + x: 0 + y: 100 + z: 0 + +# State reconciliation (/bvupdatestate and the scheduled pass). +update: + # Minutes between scheduled reconciliation passes. Must be > 0. + interval-minutes: 15 + # Seconds to wait after enable before the first pass, so worlds are loaded. + startup-delay-seconds: 30 + +# Points awarded per rarity. Lowercase keys - Bukkit paths are case-sensitive. points: common: 1 uncommon: 5 rare: 10 -# Language settings lang: - prefix: "&8&l[&3BD&8&l]" \ No newline at end of file + prefix: "&8&l[&3BV&8&l] " diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 33e62c1..495ab9d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,6 +2,9 @@ name: BlockVault version: '1.0.0' main: dev.anchorlight.blockvault.BlockVault api-version: '26.2' +libraries: + - com.zaxxer:HikariCP:5.1.0 + - com.mysql:mysql-connector-j:8.4.0 commands: bvsubmit: description: Submit the block you are holding to the vault. From f0f4ba9d74c5c25cf2fd48a5112a0f9d5ffda54a Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 12:57:41 +1000 Subject: [PATCH 19/28] Implement chapter management and region protection features; add ChapterService and related listeners --- .../anchorlight/blockvault/BlockVault.java | 55 ++++++- .../blockvault/chapter/ChapterService.java | 151 ++++++++++++++++++ .../blockvault/commands/ProgressCommand.java | 3 +- .../blockvault/commands/SubmitCommand.java | 25 +-- .../anchorlight/blockvault/db/Database.java | 64 +++++++- .../listener/PlayerGuidanceListener.java | 56 +++++++ .../listener/RegionProtectionListener.java | 107 +++++++++++++ .../blockvault/model/Manifest.java | 1 - .../anchorlight/blockvault/model/Region.java | 43 +++++ .../blockvault/util/VaultUtil.java | 9 +- src/main/resources/plugin.yml | 3 + 11 files changed, 489 insertions(+), 28 deletions(-) create mode 100644 src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java create mode 100644 src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java create mode 100644 src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java create mode 100644 src/main/java/dev/anchorlight/blockvault/model/Region.java diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 5cf73ab..628ba07 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -1,8 +1,12 @@ package dev.anchorlight.blockvault; +import dev.anchorlight.blockvault.chapter.ChapterService; import dev.anchorlight.blockvault.commands.*; import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.listener.RegionProtectionListener; +import dev.anchorlight.blockvault.listener.PlayerGuidanceListener; import dev.anchorlight.blockvault.model.Manifest; +import dev.anchorlight.blockvault.model.Region; import dev.anchorlight.blockvault.util.FileUtil; import dev.anchorlight.blockvault.util.ScheduleUtil; import dev.anchorlight.blockvault.util.VaultUtil; @@ -23,6 +27,8 @@ public final class BlockVault extends JavaPlugin { private Database database; private Manifest manifest; + private ChapterService chapters; + private Region region; public static BlockVault get() { return instance; @@ -61,23 +67,70 @@ public void onEnable() { FileUtil fileUtil = new FileUtil(this); VaultUtil vaultUtil = new VaultUtil(this); + this.chapters = new ChapterService(this); + register("bvstart", new StartCommand(this)); register("bvsubmit", new SubmitCommand(this)); register("bvprogress", new ProgressCommand(this)); register("bvleaderboard", new LeaderboardCommand(this)); register("bvupdatestate", new UpdateStateCommand(this)); - // Register events + getServer().getPluginManager().registerEvents(new RegionProtectionListener(this), this); + PlayerGuidanceListener guidance = new PlayerGuidanceListener(this); + getServer().getPluginManager().registerEvents(guidance, this); + guidance.start(); + forceLoadChunks(true); + chapters.start(); ScheduleUtil.scheduleVaultStateTask(this, vaultUtil, fileUtil); } @Override public void onDisable() { + if (chapters != null) chapters.stop(); + forceLoadChunks(false); if (database != null) database.close(); getConsoleSender().sendMessage(prefix() + "§cPlugin is now disabled."); } + public ChapterService chapters() { + return chapters; + } + + /** + * The protected volume, built lazily once the origin world is loaded. + * Rebuilt by {@link #invalidateRegion()} after a config reload. + */ + public Region region() { + if (region == null) { + World world = originWorld(); + if (world == null) return null; + int[] origin = { + getConfig().getInt("origin.x"), + getConfig().getInt("origin.y"), + getConfig().getInt("origin.z") + }; + region = new Region(world, origin, manifest.min(), manifest.max(), 4); + } + return region; + } + + public void invalidateRegion() { + region = null; + } + + private void forceLoadChunks(boolean load) { + Region r = region(); + if (r == null || r.world() == null) return; + for (int cx = r.minChunkX(); cx <= r.maxChunkX(); cx++) { + for (int cz = r.minChunkZ(); cz <= r.maxChunkZ(); cz++) { + r.world().setChunkForceLoaded(cx, cz, load); + } + } + getLogger().info((load ? "Force-loaded " : "Released ") + + "vault chunks in " + r.world().getName() + "."); + } + public Database database() { return database; } diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java new file mode 100644 index 0000000..0a2fe90 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -0,0 +1,151 @@ +package dev.anchorlight.blockvault.chapter; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.model.TargetEntry; +import org.bukkit.Color; +import org.bukkit.FireworkEffect; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Firework; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.scheduler.BukkitTask; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Owns which chapters are open. A chapter opens on its scheduled date OR when + * the previous chapter reaches 90%, whichever comes first. Earlier chapters + * never close - only the spotlight (the "current" chapter) advances. + */ +public final class ChapterService { + + private static final double EARLY_OPEN_FRACTION = 0.90; + + private final BlockVault plugin; + private final Set open = Collections.synchronizedSet(new HashSet<>()); + private BukkitTask task; + + public ChapterService(BlockVault plugin) { + this.plugin = plugin; + } + + public void start() { + // Chapter 1 is always open. + open.add(1); + long everyFiveMinutes = 20L * 60L * 5L; + this.task = plugin.getServer().getScheduler().runTaskTimer( + plugin, this::check, 20L * 20L, everyFiveMinutes); + } + + public void stop() { + if (task != null) task.cancel(); + } + + public boolean isOpen(int chapter) { + return chapter <= 1 || open.contains(chapter); + } + + /** Highest currently-open chapter - the spotlight. */ + public int current() { + int c = 1; + synchronized (open) { + for (int ch : open) c = Math.max(c, ch); + } + return c; + } + + /** Recompute open state and run any pending unlock ceremonies. Safe to call often. */ + public void check() { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + List rows = plugin.database().chapters(); + Set collected = plugin.database().collectedSnapshot(); + + plugin.getServer().getScheduler().runTask(plugin, () -> { + long now = System.currentTimeMillis(); + for (Database.ChapterRow row : rows) { + if (row.openedAt() != null) { + open.add(row.chapter()); + continue; + } + if (isOpen(row.chapter())) continue; + + boolean dateReached = row.opensAt() != null + && now >= row.opensAt().getTime(); + boolean prevNearlyDone = fraction(row.chapter() - 1, collected) >= EARLY_OPEN_FRACTION; + + if (dateReached || prevNearlyDone) { + unlock(row, prevNearlyDone && !dateReached); + } + } + }); + }); + } + + private double fraction(int chapter, Set collected) { + if (chapter < 1) return 1.0; + int total = 0, done = 0; + for (TargetEntry e : plugin.manifest().entries().values()) { + if (e.chapter() != chapter) continue; + total++; + if (collected.contains(e.material())) done++; + } + return total == 0 ? 1.0 : (double) done / total; + } + + private void unlock(Database.ChapterRow row, boolean early) { + int chapter = row.chapter(); + if (!open.add(chapter)) return; // someone beat us to it this tick + + // Break the iron-bar seal: one setBlock to air. + int[] seal = plugin.manifest().seal(chapter); + if (seal == null && row.sealX() != null) { + seal = new int[]{row.sealX(), row.sealY(), row.sealZ()}; + } + if (seal != null) { + Location loc = plugin.resolve(seal); + if (loc.getWorld() != null) loc.getBlock().setType(Material.AIR, false); + } + + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, + () -> plugin.database().markChapterOpened(chapter)); + + ceremony(row, early); + } + + private void ceremony(Database.ChapterRow row, boolean early) { + String line = "§6§lChapter " + row.chapter() + " — " + row.title() + + " §r§7(" + row.room() + ")§r is now open!"; + plugin.getServer().broadcastMessage(plugin.prefix() + line); + if (early) { + plugin.getServer().broadcastMessage(plugin.prefix() + + "§7Unlocked early — the previous floor is nearly complete."); + } + + plugin.getServer().getOnlinePlayers().forEach(p -> { + p.sendTitle("§6Chapter " + row.chapter(), "§e" + row.title(), 10, 70, 20); + p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); + }); + + // Fireworks at the leader panel, if the world is up. + Location at = plugin.resolve(plugin.manifest().leader("head")); + if (at.getWorld() != null) { + for (int i = 0; i < 3; i++) { + Firework fw = at.getWorld().spawn(at.clone().add(0.5, 1, 0.5), Firework.class); + FireworkMeta meta = fw.getFireworkMeta(); + meta.addEffect(FireworkEffect.builder() + .withColor(Color.AQUA, Color.WHITE) + .with(FireworkEffect.Type.BALL_LARGE) + .withFlicker().withTrail().build()); + meta.setPower(1); + fw.setFireworkMeta(meta); + } + } + plugin.getLogger().info("Chapter " + row.chapter() + " opened" + + (early ? " (early, previous floor >= 90%)." : ".")); + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java index 1766f21..dae7611 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/ProgressCommand.java @@ -11,7 +11,6 @@ /** Two bars: the current chapter, and the whole season. */ public class ProgressCommand implements CommandExecutor { - private final BlockVault plugin; private final VaultUtil vaultUtil; @@ -32,7 +31,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } Set collected = plugin.database().collectedSnapshot(); - int chapter = plugin.getConfig().getInt("vault.current-chapter", 1); + int chapter = plugin.chapters().current(); int chapTotal = 0, chapDone = 0, allTotal = 0, allDone = 0; for (TargetEntry e : plugin.manifest().entries().values()) { diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index 18392a5..c726ea6 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -20,7 +20,6 @@ * If the database is unreachable the item stays in the player's hand. */ public class SubmitCommand implements CommandExecutor { - private final BlockVault plugin; private final VaultUtil vaultUtil; @@ -47,7 +46,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St plugin.tell(player, "§cSubmissions are not allowed in creative mode."); return true; } - if (!inRegion(player.getLocation())) { + var region = plugin.region(); + if (region == null || !region.contains(player.getLocation())) { plugin.tell(player, "§cYou must be inside the vault to submit a block."); return true; } @@ -121,8 +121,9 @@ private void consumeOne(Player player, Material expected) { } private void placeHead(TargetEntry entry, org.bukkit.profile.PlayerProfile profile) { - // The head is only shown once the chapter floor is open. Chapter gating - // is applied in the reconciliation pass; place unconditionally for now. + // Submissions for a not-yet-open chapter are credited, but the head + // stays hidden until the floor opens (the reconcile pass adds it then). + if (!plugin.chapters().isOpen(entry.chapter())) return; Location loc = plugin.resolve(entry.head()); if (loc.getWorld() == null) return; HeadUtil.placeHead(loc, entry.face(), profile); @@ -137,20 +138,4 @@ private void celebrate(Player player, String rarity) { player.playSound(player.getLocation(), sound, 1f, 1f); } - private boolean inRegion(Location loc) { - if (loc.getWorld() == null || plugin.originWorld() == null - || !loc.getWorld().equals(plugin.originWorld())) { - return false; - } - int ox = plugin.getConfig().getInt("origin.x"); - int oy = plugin.getConfig().getInt("origin.y"); - int oz = plugin.getConfig().getInt("origin.z"); - int[] lo = plugin.manifest().min(); - int[] hi = plugin.manifest().max(); - int margin = 3; - int x = loc.getBlockX() - ox, y = loc.getBlockY() - oy, z = loc.getBlockZ() - oz; - return x >= lo[0] - margin && x <= hi[0] + margin - && y >= lo[1] - margin && y <= hi[1] + margin - && z >= lo[2] - margin && z <= hi[2] + margin; - } } diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index eb7cec1..21e2b9e 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -28,7 +28,6 @@ * and updated only after a write commits. */ public final class Database { - /** MySQL error code for a duplicate primary/unique key. */ private static final int ER_DUP_ENTRY = 1062; @@ -284,6 +283,69 @@ public SubmitOutcome submit(String material, UUID uuid, String name, } } + // ----------------------------------------------------------------- chapters + + public record ChapterRow(int chapter, String title, String room, + Integer sealX, Integer sealY, Integer sealZ, + java.sql.Timestamp opensAt, java.sql.Timestamp openedAt) {} + + /** All chapter rows for this edition, ordered. Blocking. */ + public java.util.List chapters() { + java.util.List rows = new java.util.ArrayList<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT chapter,title,room,seal_x,seal_y,seal_z,opens_at,opened_at " + + "FROM bv_chapter WHERE edition = ? ORDER BY chapter")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + rows.add(new ChapterRow( + rs.getInt("chapter"), rs.getString("title"), rs.getString("room"), + (Integer) rs.getObject("seal_x"), + (Integer) rs.getObject("seal_y"), + (Integer) rs.getObject("seal_z"), + rs.getTimestamp("opens_at"), rs.getTimestamp("opened_at"))); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Chapter query failed: " + e.getMessage()); + } + return rows; + } + + /** Stamp a chapter open (only if not already). Blocking. Returns true if it changed. */ + public boolean markChapterOpened(int chapter) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "UPDATE bv_chapter SET opened_at = CURRENT_TIMESTAMP " + + "WHERE chapter = ? AND edition = ? AND opened_at IS NULL")) { + ps.setInt(1, chapter); + ps.setString(2, edition); + boolean changed = ps.executeUpdate() > 0; + if (changed) audit(null, "chapter_open", null, "{\"chapter\":" + chapter + "}"); + return changed; + } catch (SQLException e) { + plugin.getLogger().severe("Could not mark chapter " + chapter + " open: " + e.getMessage()); + return false; + } + } + + /** Append an audit row. Blocking; {@code detailJson} must be valid JSON or null. */ + public void audit(UUID actor, String action, String material, String detailJson) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "INSERT INTO bv_audit (actor,action,material,detail) VALUES (?,?,?,?)")) { + if (actor == null) ps.setNull(1, java.sql.Types.BINARY); + else ps.setBytes(1, toBytes(actor)); + ps.setString(2, action); + ps.setString(3, material); + ps.setString(4, detailJson); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Audit write failed (" + action + "): " + e.getMessage()); + } + } + public record LeaderRow(UUID uuid, String name, long points, long blocks) {} /** Top {@code limit} contributors, then the viewer's own row if outside it. Blocking. */ diff --git a/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java b/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java new file mode 100644 index 0000000..afb05a4 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java @@ -0,0 +1,56 @@ +package dev.anchorlight.blockvault.listener; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.util.VaultUtil; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.ItemStack; + +/** + * Nudges players toward blocks the vault still needs: an actionbar hint while + * holding one, and a progress line on join. Highest-value feature for actually + * finishing the collection. + */ +public final class PlayerGuidanceListener implements Listener { + private final BlockVault plugin; + private final VaultUtil vaultUtil; + + public PlayerGuidanceListener(BlockVault plugin) { + this.plugin = plugin; + this.vaultUtil = new VaultUtil(plugin); + } + + /** Called from onEnable: refresh the held-block actionbar hint on a short loop. */ + public void start() { + plugin.getServer().getScheduler().runTaskTimer(plugin, () -> { + if (!vaultUtil.hasStarted()) return; + for (Player p : plugin.getServer().getOnlinePlayers()) { + ItemStack held = p.getInventory().getItemInMainHand(); + Material type = held.getType(); + if (type == Material.AIR) continue; + String material = type.getKey().getKey(); + if (plugin.manifest().entry(material) == null) continue; + if (plugin.database().isCollected(material)) continue; + p.spigot().sendMessage(ChatMessageType.ACTION_BAR, + new TextComponent("§eThe vault still needs " + + VaultUtil.formatMaterialName(type) + " — §7/bvsubmit")); + } + }, 40L, 30L); + } + + @EventHandler + public void onJoin(PlayerJoinEvent e) { + if (!vaultUtil.hasStarted()) return; + int done = plugin.database().collectedCount(); + int total = plugin.manifest().entries().size(); + int pct = total == 0 ? 0 : (int) Math.round(100.0 * done / total); + plugin.tell(e.getPlayer(), "§7Vault progress: §e" + done + "§7/§e" + total + + " §7(" + pct + "%). Chapter " + plugin.chapters().current() + + " is the current floor. §f/bvprogress"); + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java b/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java new file mode 100644 index 0000000..5762166 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java @@ -0,0 +1,107 @@ +package dev.anchorlight.blockvault.listener; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.model.Region; +import org.bukkit.entity.EntityType; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.hanging.HangingBreakByEntityEvent; +import org.bukkit.event.hanging.HangingBreakEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; + +/** + * Everything that keeps the display layer and the seals intact. The display is + * entity-based (item frames + heads), so without this a single creeper erases + * a wall of donations. + */ +public final class RegionProtectionListener implements Listener { + private static final String BYPASS = "blockvault.build"; + + private final BlockVault plugin; + + public RegionProtectionListener(BlockVault plugin) { + this.plugin = plugin; + } + + private boolean inside(org.bukkit.Location loc) { + Region r = plugin.region(); + return r != null && r.contains(loc); + } + + // --- item frames ------------------------------------------------------- + + @EventHandler(ignoreCancelled = true) + public void onHangingBreak(HangingBreakEvent e) { + if (inside(e.getEntity().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onHangingBreakByEntity(HangingBreakByEntityEvent e) { + if (inside(e.getEntity().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onInteractEntity(PlayerInteractEntityEvent e) { + EntityType t = e.getRightClicked().getType(); + if ((t == EntityType.ITEM_FRAME || t == EntityType.GLOW_ITEM_FRAME) + && inside(e.getRightClicked().getLocation()) + && !e.getPlayer().hasPermission(BYPASS)) { + e.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onEntityDamageByEntity(EntityDamageByEntityEvent e) { + EntityType t = e.getEntity().getType(); + if ((t == EntityType.ITEM_FRAME || t == EntityType.GLOW_ITEM_FRAME) + && inside(e.getEntity().getLocation())) { + e.setCancelled(true); + } + } + + // --- blocks ---------------------------------------------------------- + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + public void onBreak(BlockBreakEvent e) { + if (inside(e.getBlock().getLocation()) && !e.getPlayer().hasPermission(BYPASS)) { + e.setCancelled(true); + plugin.tell(e.getPlayer(), "§cYou can't break blocks inside the vault."); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + public void onPlace(BlockPlaceEvent e) { + if (inside(e.getBlock().getLocation()) && !e.getPlayer().hasPermission(BYPASS)) { + e.setCancelled(true); + plugin.tell(e.getPlayer(), "§cYou can't place blocks inside the vault."); + } + } + + @EventHandler(ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent e) { + e.blockList().removeIf(b -> inside(b.getLocation())); + } + + @EventHandler(ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent e) { + e.blockList().removeIf(b -> inside(b.getLocation())); + } + + // --- mob spawning -------------------------------------------------- + + @EventHandler(ignoreCancelled = true) + public void onSpawn(CreatureSpawnEvent e) { + if (!inside(e.getLocation())) return; + switch (e.getSpawnReason()) { + case CUSTOM, COMMAND, SPAWNER_EGG, BREEDING, DISPENSE_EGG -> { /* allowed */ } + default -> e.setCancelled(true); + } + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/model/Manifest.java b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java index dc34400..e6ba2d3 100644 --- a/src/main/java/dev/anchorlight/blockvault/model/Manifest.java +++ b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java @@ -22,7 +22,6 @@ * regenerates the target list (brief section 7). */ public final class Manifest { - private final String version; private final int dataVersion; private final Map entries; diff --git a/src/main/java/dev/anchorlight/blockvault/model/Region.java b/src/main/java/dev/anchorlight/blockvault/model/Region.java new file mode 100644 index 0000000..5fe5af5 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/model/Region.java @@ -0,0 +1,43 @@ +package dev.anchorlight.blockvault.model; + +import org.bukkit.Location; +import org.bukkit.World; + +/** + * The protected volume around the vault, in absolute world coordinates. + * Derived from the manifest bounding box plus a margin, offset by the + * configured origin. Used for build/hanging/spawn protection and chunk + * force-loading. + */ +public final class Region { + + private final World world; + private final int minX, minY, minZ, maxX, maxY, maxZ; + + public Region(World world, int[] origin, int[] relMin, int[] relMax, int margin) { + this.world = world; + this.minX = origin[0] + relMin[0] - margin; + this.minY = origin[1] + relMin[1] - margin; + this.minZ = origin[2] + relMin[2] - margin; + this.maxX = origin[0] + relMax[0] + margin; + this.maxY = origin[1] + relMax[1] + margin; + this.maxZ = origin[2] + relMax[2] + margin; + } + + public World world() { + return world; + } + + public boolean contains(Location loc) { + if (loc.getWorld() == null || !loc.getWorld().equals(world)) return false; + int x = loc.getBlockX(), y = loc.getBlockY(), z = loc.getBlockZ(); + return x >= minX && x <= maxX + && y >= minY && y <= maxY + && z >= minZ && z <= maxZ; + } + + public int minChunkX() { return minX >> 4; } + public int maxChunkX() { return maxX >> 4; } + public int minChunkZ() { return minZ >> 4; } + public int maxChunkZ() { return maxZ >> 4; } +} diff --git a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java index b3c4476..ae31ca7 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java +++ b/src/main/java/dev/anchorlight/blockvault/util/VaultUtil.java @@ -66,15 +66,18 @@ public void updateVaultState(CommandSender sender) { for (TargetEntry entry : plugin.manifest().entries().values()) { Location loc = plugin.resolve(entry.head()); Block block = loc.getBlock(); - boolean collected = plugin.database().isCollected(entry.material()); + // A head is shown only when the block is collected AND its + // chapter floor is open. Anything else means: no head. + boolean shouldShow = plugin.database().isCollected(entry.material()) + && plugin.chapters().isOpen(entry.chapter()); boolean hasHead = block.getType() == Material.PLAYER_WALL_HEAD || block.getType() == Material.PLAYER_HEAD; - if (collected && !hasHead) { + if (shouldShow && !hasHead) { HeadUtil.placeHead(loc, entry.face(), parseProfile(world, profiles.get(entry.material()))); placed++; - } else if (!collected && hasHead) { + } else if (!shouldShow && hasHead) { block.setType(Material.AIR, false); cleared++; } else { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 495ab9d..4c4961a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -39,3 +39,6 @@ permissions: blockvault.updatestate: description: Allows reconciling the world display against the database. default: op + blockvault.build: + description: Bypass vault build/interaction protection. + default: op From d9f4ee9f87d33cb74dbad83dfa7b7a0f95fd8742 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:04:28 +1000 Subject: [PATCH 20/28] Add query and admin command functionalities; implement multiple commands and permissions in plugin.yml --- .../anchorlight/blockvault/BlockVault.java | 14 ++ .../blockvault/commands/AdminCommand.java | 158 +++++++++++++ .../blockvault/commands/QueryCommand.java | 222 ++++++++++++++++++ .../anchorlight/blockvault/db/Database.java | 98 ++++++++ src/main/resources/plugin.yml | 74 +++++- 5 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java create mode 100644 src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 628ba07..939558e 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -75,6 +75,20 @@ public void onEnable() { register("bvleaderboard", new LeaderboardCommand(this)); register("bvupdatestate", new UpdateStateCommand(this)); + QueryCommand query = new QueryCommand(this); + for (String c : new String[]{"bvfind", "bvinfo", "bvmissing", "bvcheck", + "bvme", "bvhistory", "bvedition", "bvreload"}) { + register(c, query); + PluginCommand pc = getCommand(c); + if (pc != null) pc.setTabCompleter(query); + } + AdminCommand admin = new AdminCommand(this); + for (String c : new String[]{"bvrevoke", "bvrepair", "bvbackup"}) { + register(c, admin); + PluginCommand pc = getCommand(c); + if (pc != null) pc.setTabCompleter(admin); + } + getServer().getPluginManager().registerEvents(new RegionProtectionListener(this), this); PlayerGuidanceListener guidance = new PlayerGuidanceListener(this); getServer().getPluginManager().registerEvents(guidance, this); diff --git a/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java new file mode 100644 index 0000000..75b9ce4 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java @@ -0,0 +1,158 @@ +package dev.anchorlight.blockvault.commands; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.model.TargetEntry; +import dev.anchorlight.blockvault.util.VaultUtil; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.ItemFrame; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** Admin dispatcher: {@code /bvrevoke /bvrepair /bvbackup}. */ +public final class AdminCommand implements CommandExecutor, TabCompleter { + private final BlockVault plugin; + + public AdminCommand(BlockVault plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + String name = command.getName().toLowerCase(); + if (!sender.hasPermission("blockvault." + name.substring(2))) { + plugin.tell(sender, "§cYou don't have permission to use this command."); + return true; + } + switch (name) { + case "bvrevoke" -> revoke(sender, args); + case "bvrepair" -> repair(sender); + case "bvbackup" -> backup(sender); + default -> plugin.tell(sender, "§cUnknown command."); + } + return true; + } + + private void revoke(CommandSender sender, String[] args) { + if (args.length == 0) { + plugin.tell(sender, "§cUsage: /bvrevoke "); + return; + } + String material = args[0].toLowerCase().replace("minecraft:", ""); + TargetEntry entry = plugin.manifest().entry(material); + if (entry == null) { + plugin.tell(sender, "§c'" + material + "' is not part of this collection."); + return; + } + java.util.UUID actor = (sender instanceof org.bukkit.entity.Player p) ? p.getUniqueId() : null; + + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Database.RevokeResult r = plugin.database().revoke(material, actor); + plugin.getServer().getScheduler().runTask(plugin, () -> { + if (!r.ok()) { + plugin.tell(sender, "§e" + material + " has no submission to revoke."); + return; + } + Location head = plugin.resolve(entry.head()); + if (head.getWorld() != null) head.getBlock().setType(Material.AIR, false); + String who = plugin.getServer().getOfflinePlayer(r.donor()).getName(); + plugin.tell(sender, "§aRevoked " + material + " from §f" + who + + "§a; refunded " + r.pointsRefunded() + " points."); + }); + }); + } + + private void repair(CommandSender sender) { + World world = plugin.originWorld(); + if (world == null) { + plugin.tell(sender, "§cOrigin world is not loaded."); + return; + } + int frames = 0, heads = 0; + for (TargetEntry e : plugin.manifest().entries().values()) { + Location frameLoc = plugin.resolve(e.frame()); + if (!hasItemFrame(world, frameLoc)) { + world.spawn(frameLoc, ItemFrame.class, f -> { + f.setFacingDirection(e.face(), true); + f.setFixed(true); + f.setInvulnerable(true); + f.setSilent(true); + }); + frames++; + } + } + // Heads are reconciled by the standard pass. + plugin.tell(sender, "§aRepair: respawned " + frames + " missing frames. " + + "Running head reconciliation…"); + new VaultUtil(plugin).updateVaultState(sender); + } + + private boolean hasItemFrame(World world, Location loc) { + for (var ent : world.getNearbyEntities(loc, 0.5, 0.5, 0.5)) { + if (ent instanceof ItemFrame) return true; + } + return false; + } + + private void backup(CommandSender sender) { + var cfg = plugin.getConfig(); + String stamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")); + File dir = new File(plugin.getDataFolder(), "backups"); + //noinspection ResultOfMethodCallIgnored + dir.mkdirs(); + File out = new File(dir, "blockvault-" + stamp + ".sql"); + + List cmd = new ArrayList<>(List.of( + "mysqldump", + "-h", cfg.getString("database.host", "localhost"), + "-P", String.valueOf(cfg.getInt("database.port", 3306)), + "-u", cfg.getString("database.user", "blockvault"), + "--databases", cfg.getString("database.name", "blockvault"))); + + plugin.tell(sender, "§7Starting backup to " + out.getName() + "…"); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + boolean ok = false; + String detail; + try { + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.environment().put("MYSQL_PWD", cfg.getString("database.password", "")); + pb.redirectErrorStream(false); + pb.redirectOutput(out); + Process p = pb.start(); + int code = p.waitFor(); + ok = code == 0 && out.length() > 0; + detail = ok ? (out.length() / 1024) + " KiB" : "mysqldump exit " + code; + } catch (Exception ex) { + detail = ex.getMessage(); + } + final boolean done = ok; + final String msg = detail; + plugin.getServer().getScheduler().runTask(plugin, () -> { + if (done) plugin.tell(sender, "§aBackup complete: " + out.getName() + " (" + msg + ")"); + else plugin.tell(sender, "§cBackup failed: " + msg + + " §7(is mysqldump on the server's PATH?)"); + }); + }); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (!command.getName().equalsIgnoreCase("bvrevoke") || args.length != 1) return List.of(); + String prefix = args[0].toLowerCase(); + List out = new ArrayList<>(); + for (String m : plugin.database().collectedSnapshot()) { + if (m.startsWith(prefix)) out.add(m); + } + return out; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java new file mode 100644 index 0000000..250d62c --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java @@ -0,0 +1,222 @@ +package dev.anchorlight.blockvault.commands; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.model.TargetEntry; +import dev.anchorlight.blockvault.util.VaultUtil; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Read-only player commands, one dispatcher for all of them: + * {@code /bvfind /bvinfo /bvmissing /bvcheck /bvme /bvhistory /bvedition /bvreload}. + */ +public final class QueryCommand implements CommandExecutor, TabCompleter { + + private static final int PAGE = 10; + private static final DateTimeFormatter DATE = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + private final BlockVault plugin; + + public QueryCommand(BlockVault plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + String name = command.getName().toLowerCase(); + if (!sender.hasPermission("blockvault." + name.substring(2))) { + plugin.tell(sender, "§cYou don't have permission to use this command."); + return true; + } + switch (name) { + case "bvfind" -> find(sender, args); + case "bvinfo" -> info(sender, args); + case "bvmissing" -> missing(sender, args); + case "bvcheck" -> check(sender); + case "bvme" -> me(sender); + case "bvhistory" -> history(sender, args); + case "bvedition" -> edition(sender); + case "bvreload" -> reload(sender); + default -> plugin.tell(sender, "§cUnknown command."); + } + return true; + } + + private TargetEntry require(CommandSender sender, String[] args) { + if (args.length == 0) { + plugin.tell(sender, "§cUsage: / "); + return null; + } + String key = args[0].toLowerCase().replace("minecraft:", ""); + TargetEntry e = plugin.manifest().entry(key); + if (e == null) plugin.tell(sender, "§c'" + key + "' is not part of this collection."); + return e; + } + + private String pretty(TargetEntry e) { + Material m = Material.matchMaterial(e.material()); + return m != null ? VaultUtil.formatMaterialName(m) : e.material(); + } + + private void find(CommandSender sender, String[] args) { + TargetEntry e = require(sender, args); + if (e == null) return; + int[] s = e.sign(); + org.bukkit.Location loc = plugin.resolve(e.sign()); + String room = roomFor(e.chapter()); + plugin.tell(sender, "§6" + pretty(e) + + " §7— Level " + e.chapter() + ", " + room); + plugin.tell(sender, "§7 shelf at §f" + loc.getBlockX() + " " + loc.getBlockY() + " " + + loc.getBlockZ() + " §7(relative " + s[0] + " " + s[1] + " " + s[2] + ", facing " + e.facing() + ")"); + } + + private void info(CommandSender sender, String[] args) { + TargetEntry e = require(sender, args); + if (e == null) return; + int pts = plugin.getConfig().getInt("points." + e.rarity(), 1); + boolean have = plugin.database().isCollected(e.material()); + plugin.tell(sender, "§6" + pretty(e)); + plugin.tell(sender, "§7 rarity §f" + e.rarity() + " §7· points §f" + pts + + " §7· chapter §f" + e.chapter() + " §7· section §f" + e.section()); + plugin.tell(sender, have ? "§a already in the vault" : "§e still needed"); + } + + private void missing(CommandSender sender, String[] args) { + int chapter = args.length > 0 ? parseInt(args[0], plugin.chapters().current()) + : plugin.chapters().current(); + int page = args.length > 1 ? Math.max(1, parseInt(args[1], 1)) : 1; + + List out = new ArrayList<>(); + for (TargetEntry e : plugin.manifest().entries().values()) { + if (e.chapter() == chapter && !plugin.database().isCollected(e.material())) { + out.add(e.material()); + } + } + if (out.isEmpty()) { + plugin.tell(sender, "§aChapter " + chapter + " is complete!"); + return; + } + int pages = (out.size() + PAGE - 1) / PAGE; + page = Math.min(page, pages); + plugin.tell(sender, "§6Chapter " + chapter + " — " + out.size() + + " outstanding §7(page " + page + "/" + pages + ")"); + for (int i = (page - 1) * PAGE; i < Math.min(out.size(), page * PAGE); i++) { + sender.sendMessage("§7 · §f" + out.get(i)); + } + if (page < pages) plugin.tell(sender, "§7/bvmissing " + chapter + " " + (page + 1) + " for more"); + } + + private void check(CommandSender sender) { + if (!(sender instanceof Player player)) { + plugin.tell(sender, "§cPlayers only."); + return; + } + List needed = new ArrayList<>(); + for (ItemStack it : player.getInventory().getContents()) { + if (it == null || it.getType() == Material.AIR) continue; + String key = it.getType().getKey().getKey(); + TargetEntry e = plugin.manifest().entry(key); + if (e != null && !plugin.database().isCollected(key) && !needed.contains(key)) { + needed.add(key); + } + } + if (needed.isEmpty()) { + plugin.tell(sender, "§7Nothing in your inventory is currently needed."); + return; + } + plugin.tell(sender, "§aThe vault needs " + needed.size() + " block(s) you're carrying:"); + needed.forEach(n -> sender.sendMessage("§7 · §f" + n + " §8— /bvsubmit")); + } + + private void me(CommandSender sender) { + if (!(sender instanceof Player player)) { + plugin.tell(sender, "§cPlayers only."); + return; + } + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Database.MyStats s = plugin.database().myStats(player.getUniqueId()); + plugin.getServer().getScheduler().runTask(plugin, () -> { + plugin.tell(player, "§6Your vault record"); + plugin.tell(player, "§7 blocks donated §f" + s.blocks() + + " §7· points §f" + s.points() + + " §7· rank §f" + (s.rank() > 0 ? "#" + s.rank() : "—")); + plugin.tell(player, "§7 every donation is a first — one of each block is allowed."); + }); + }); + } + + private void history(CommandSender sender, String[] args) { + TargetEntry e = require(sender, args); + if (e == null) return; + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Database.SubmissionRow row = plugin.database().submission(e.material()); + plugin.getServer().getScheduler().runTask(plugin, () -> { + if (row == null) { + plugin.tell(sender, "§e" + e.material() + " has not been donated yet."); + return; + } + String when = row.submittedAt().toLocalDateTime().format(DATE); + plugin.tell(sender, "§6" + e.material() + " §7was donated by §f" + row.name() + + " §7on §f" + when + " §7(+" + row.points() + " pts)"); + }); + }); + } + + private void edition(CommandSender sender) { + plugin.tell(sender, "§6Edition §f" + plugin.database().edition() + + " §7· manifest version §f" + plugin.manifest().version() + + " §7· data version §f" + plugin.manifest().dataVersion()); + plugin.tell(sender, "§7 " + plugin.manifest().entries().size() + + " blocks, frozen for the season. New blocks require a new edition."); + } + + private void reload(CommandSender sender) { + plugin.reloadConfig(); + plugin.invalidateRegion(); + plugin.tell(sender, "§aConfiguration reloaded."); + } + + // --- helpers ------------------------------------------------------- + + private String roomFor(int chapter) { + return switch (chapter) { + case 1 -> "The Undercroft"; + case 2 -> "The Conservatory"; + case 3 -> "The Deep"; + case 4 -> "The Gallery"; + case 5 -> "The Forge"; + case 6 -> "The Observatory"; + default -> "?"; + }; + } + + private static int parseInt(String s, int fallback) { + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return fallback; } + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + String name = command.getName().toLowerCase(); + boolean blockArg = (name.equals("bvfind") || name.equals("bvinfo") || name.equals("bvhistory")) + && args.length == 1; + if (!blockArg) return List.of(); + String prefix = args[0].toLowerCase(); + List out = new ArrayList<>(); + for (String m : plugin.manifest().entries().keySet()) { + if (m.startsWith(prefix)) out.add(m); + if (out.size() >= 50) break; + } + return out; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 21e2b9e..2efa187 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -346,6 +346,104 @@ public void audit(UUID actor, String action, String material, String detailJson) } } + // ----------------------------------------------------------------- lookups + + public record SubmissionRow(String material, UUID uuid, String name, + int points, java.sql.Timestamp submittedAt) {} + + /** Who donated {@code material} and when, or null if still outstanding. Blocking. */ + public SubmissionRow submission(String material) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT s.material, s.uuid, c.last_name, s.points, s.submitted_at " + + "FROM bv_submission s JOIN bv_contributor c ON c.uuid = s.uuid " + + "WHERE s.material = ? AND s.edition = ?")) { + ps.setString(1, material); + ps.setString(2, edition); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return new SubmissionRow(rs.getString(1), fromBytes(rs.getBytes(2)), + rs.getString(3), rs.getInt(4), rs.getTimestamp(5)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Submission lookup failed: " + e.getMessage()); + return null; + } + } + + public record MyStats(long points, long blocks, int rank) {} + + /** Personal totals for {@code uuid}. Blocking. */ + public MyStats myStats(UUID uuid) { + long points = 0, blocks = 0; + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT points, blocks_given FROM bv_contributor WHERE uuid = ?")) { + ps.setBytes(1, toBytes(uuid)); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { points = rs.getLong(1); blocks = rs.getLong(2); } + } + } catch (SQLException e) { + plugin.getLogger().severe("Stats query failed: " + e.getMessage()); + } + return new MyStats(points, blocks, rankOf(uuid)); + } + + // ----------------------------------------------------------------- admin + + public record RevokeResult(boolean ok, UUID donor, int pointsRefunded) {} + + /** Reverse a submission: delete it, refund the donor, audit. Blocking. */ + public RevokeResult revoke(String material, UUID actor) { + try (Connection c = ds.getConnection()) { + c.setAutoCommit(false); + try { + UUID donor; + int points; + try (PreparedStatement ps = c.prepareStatement( + "SELECT uuid, points FROM bv_submission WHERE material = ? AND edition = ?")) { + ps.setString(1, material); + ps.setString(2, edition); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { c.rollback(); return new RevokeResult(false, null, 0); } + donor = fromBytes(rs.getBytes(1)); + points = rs.getInt(2); + } + } + try (PreparedStatement ps = c.prepareStatement( + "DELETE FROM bv_submission WHERE material = ? AND edition = ?")) { + ps.setString(1, material); + ps.setString(2, edition); + ps.executeUpdate(); + } + try (PreparedStatement ps = c.prepareStatement( + "UPDATE bv_contributor SET points = GREATEST(0, points - ?), " + + "blocks_given = GREATEST(0, blocks_given - 1) WHERE uuid = ?")) { + ps.setInt(1, points); + ps.setBytes(2, toBytes(donor)); + ps.executeUpdate(); + } + try (PreparedStatement ps = c.prepareStatement( + "INSERT INTO bv_audit (actor,action,material,detail) VALUES (?,?,?,?)")) { + ps.setBytes(1, toBytes(actor)); + ps.setString(2, "revoke"); + ps.setString(3, material); + ps.setString(4, "{\"points\":" + points + "}"); + ps.executeUpdate(); + } + c.commit(); + collected.remove(material); + return new RevokeResult(true, donor, points); + } catch (SQLException e) { + c.rollback(); + throw e; + } + } catch (SQLException e) { + plugin.getLogger().severe("Revoke failed for " + material + ": " + e.getMessage()); + return new RevokeResult(false, null, 0); + } + } + public record LeaderRow(UUID uuid, String name, long points, long blocks) {} /** Top {@code limit} contributors, then the viewer's own row if outside it. Blocking. */ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4c4961a..270f890 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -16,13 +16,50 @@ commands: description: Show current chapter and overall progress. usage: /bvprogress bvstart: - description: Open the collection event. - usage: /bvstart + description: Open (or, with 'stop', close) the collection event. + usage: /bvstart [stop] permission: blockvault.start bvupdatestate: description: Reconcile the world display against the database. usage: /bvupdatestate permission: blockvault.updatestate + bvfind: + description: Where a block lives in the museum. + usage: /bvfind + bvinfo: + description: Rarity, points, chapter and status of a block. + usage: /bvinfo + bvmissing: + description: Outstanding blocks for a chapter. + usage: /bvmissing [chapter] [page] + bvcheck: + description: Scan your inventory for blocks the vault needs. + usage: /bvcheck + bvme: + description: Your donations, points and rank. + usage: /bvme + bvhistory: + description: Who donated a block and when. + usage: /bvhistory + bvedition: + description: Show the frozen target-list version. + usage: /bvedition + bvreload: + description: Reload config without a restart. + usage: /bvreload + permission: blockvault.reload + bvrevoke: + description: Reverse a submission and refund its points. + usage: /bvrevoke + permission: blockvault.revoke + bvrepair: + description: Rebuild missing frames and heads from the manifest. + usage: /bvrepair + permission: blockvault.repair + bvbackup: + description: Write a timestamped database dump. + usage: /bvbackup + permission: blockvault.backup permissions: blockvault.submit: description: Allows submitting blocks to the vault. @@ -33,12 +70,45 @@ permissions: blockvault.leaderboard: description: Allows viewing the leaderboard. default: true + blockvault.find: + description: Allows looking up block locations. + default: true + blockvault.info: + description: Allows looking up block details. + default: true + blockvault.missing: + description: Allows listing outstanding blocks. + default: true + blockvault.check: + description: Allows scanning your inventory against the target list. + default: true + blockvault.me: + description: Allows viewing your own vault record. + default: true + blockvault.history: + description: Allows viewing who donated a block. + default: true + blockvault.edition: + description: Allows viewing the edition version. + default: true blockvault.start: description: Allows opening and closing the event. default: op blockvault.updatestate: description: Allows reconciling the world display against the database. default: op + blockvault.reload: + description: Allows reloading the configuration. + default: op + blockvault.revoke: + description: Allows reversing a submission. + default: op + blockvault.repair: + description: Allows rebuilding frames and heads. + default: op + blockvault.backup: + description: Allows writing database dumps. + default: op blockvault.build: description: Bypass vault build/interaction protection. default: op From ca9304c3ba3beec5c316c2e8bd509b3c8815f3de Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:08:24 +1000 Subject: [PATCH 21/28] Add Discord webhook integration for rare submissions and chapter unlocks; implement StartupValidation utility --- .../anchorlight/blockvault/BlockVault.java | 10 ++ .../blockvault/chapter/ChapterService.java | 8 + .../blockvault/commands/AdminCommand.java | 3 +- .../blockvault/commands/SubmitCommand.java | 3 + .../anchorlight/blockvault/db/Database.java | 16 ++ .../blockvault/model/Manifest.java | 1 + .../blockvault/util/StartupValidation.java | 140 ++++++++++++++++++ .../anchorlight/blockvault/util/Webhook.java | 60 ++++++++ src/main/resources/config.yml | 5 + 9 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java create mode 100644 src/main/java/dev/anchorlight/blockvault/util/Webhook.java diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 939558e..f4a1a3c 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -9,7 +9,9 @@ import dev.anchorlight.blockvault.model.Region; import dev.anchorlight.blockvault.util.FileUtil; import dev.anchorlight.blockvault.util.ScheduleUtil; +import dev.anchorlight.blockvault.util.StartupValidation; import dev.anchorlight.blockvault.util.VaultUtil; +import dev.anchorlight.blockvault.util.Webhook; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.World; @@ -28,6 +30,7 @@ public final class BlockVault extends JavaPlugin { private Database database; private Manifest manifest; private ChapterService chapters; + private Webhook webhook; private Region region; public static BlockVault get() { @@ -68,6 +71,9 @@ public void onEnable() { VaultUtil vaultUtil = new VaultUtil(this); this.chapters = new ChapterService(this); + this.webhook = new Webhook(this); + + StartupValidation.run(this); register("bvstart", new StartCommand(this)); register("bvsubmit", new SubmitCommand(this)); @@ -111,6 +117,10 @@ public ChapterService chapters() { return chapters; } + public Webhook webhook() { + return webhook; + } + /** * The protected volume, built lazily once the origin world is loaded. * Rebuilt by {@link #invalidateRegion()} after a config reload. diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java index 0a2fe90..85ee329 100644 --- a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -125,12 +125,20 @@ private void ceremony(Database.ChapterRow row, boolean early) { plugin.getServer().broadcastMessage(plugin.prefix() + "§7Unlocked early — the previous floor is nearly complete."); } + plugin.webhook().chapterOpened(row.chapter(), row.title()); plugin.getServer().getOnlinePlayers().forEach(p -> { p.sendTitle("§6Chapter " + row.chapter(), "§e" + row.title(), 10, 70, 20); p.playSound(p.getLocation(), Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f); }); + org.bukkit.boss.BossBar bar = plugin.getServer().createBossBar( + "§6Chapter " + row.chapter() + " — " + row.title() + " is open", + org.bukkit.boss.BarColor.YELLOW, org.bukkit.boss.BarStyle.SOLID); + bar.setProgress(1.0); + plugin.getServer().getOnlinePlayers().forEach(bar::addPlayer); + plugin.getServer().getScheduler().runTaskLater(plugin, bar::removeAll, 200L); + // Fireworks at the leader panel, if the world is up. Location at = plugin.resolve(plugin.manifest().leader("head")); if (at.getWorld() != null) { diff --git a/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java index 75b9ce4..42338ad 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/AdminCommand.java @@ -78,7 +78,7 @@ private void repair(CommandSender sender) { plugin.tell(sender, "§cOrigin world is not loaded."); return; } - int frames = 0, heads = 0; + int frames = 0; for (TargetEntry e : plugin.manifest().entries().values()) { Location frameLoc = plugin.resolve(e.frame()); if (!hasItemFrame(world, frameLoc)) { @@ -87,6 +87,7 @@ private void repair(CommandSender sender) { f.setFixed(true); f.setInvulnerable(true); f.setSilent(true); + f.setGlowing("rare".equals(e.rarity())); // highlight rare-tier shelves }); frames++; } diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index c726ea6..039f54a 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -97,6 +97,9 @@ public boolean onCommand(CommandSender sender, Command command, String label, St plugin.tell(player, "§aDonated " + pretty + "! §7(+" + points + (points == 1 ? " point)" : " points)")); celebrate(player, entry.rarity()); + if ("rare".equals(entry.rarity())) { + plugin.webhook().rareSubmission(material, player.getName(), points); + } } } }); diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 2efa187..9a66b7e 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -193,6 +193,22 @@ public int collectedCount() { return collected.size(); } + /** Every material recorded in bv_target for this edition. Blocking. */ + public java.util.Set targetMaterials() { + java.util.Set out = new java.util.HashSet<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT material FROM bv_target WHERE edition = ?")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) out.add(rs.getString(1)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Target material query failed: " + e.getMessage()); + } + return out; + } + /** material -> cached profile JSON (may be null) for this edition. Blocking. */ public java.util.Map allProfiles() { java.util.Map out = new java.util.HashMap<>(); diff --git a/src/main/java/dev/anchorlight/blockvault/model/Manifest.java b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java index e6ba2d3..dc34400 100644 --- a/src/main/java/dev/anchorlight/blockvault/model/Manifest.java +++ b/src/main/java/dev/anchorlight/blockvault/model/Manifest.java @@ -22,6 +22,7 @@ * regenerates the target list (brief section 7). */ public final class Manifest { + private final String version; private final int dataVersion; private final Map entries; diff --git a/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java b/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java new file mode 100644 index 0000000..26902ba --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java @@ -0,0 +1,140 @@ +package dev.anchorlight.blockvault.util; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.model.TargetEntry; +import org.bukkit.Material; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.logging.Logger; + +/** + * Cross-checks the three sources of truth on startup: the manifest + * (vault_slots.json), the rarity list (vault_items.yml) and bv_target. + * Silent drift between them is how you discover in month eight that four + * blocks have no niche (brief section 8, Operations). + * + *

Logs findings; never aborts. Also flags blocks the running game has + * that the frozen edition does not cover. + */ +public final class StartupValidation { + /** Blocks that exist in the registry but cannot be obtained in survival (brief section 9). */ + private static final Set UNOBTAINABLE = Set.of( + "air", "barrier", "bedrock", "budding_amethyst", "chain_command_block", "chorus_plant", + "command_block", "dirt_path", "end_portal_frame", "farmland", "frogspawn", "jigsaw", "light", + "infested_chiseled_stone_bricks", "infested_cobblestone", "infested_cracked_stone_bricks", + "infested_deepslate", "infested_mossy_stone_bricks", "infested_stone", "infested_stone_bricks", + "reinforced_deepslate", "repeating_command_block", "spawner", "structure_block", + "structure_void", "suspicious_gravel", "suspicious_sand", "test_block", + "test_instance_block", "trial_spawner", "vault"); + + private StartupValidation() {} + + public static void run(BlockVault plugin) { + Logger log = plugin.getLogger(); + + Map manifest = new TreeMap<>(); + for (TargetEntry e : plugin.manifest().entries().values()) { + manifest.put(e.material(), e.rarity()); + } + + Map items = loadItems(plugin); + Set target = plugin.database().targetMaterials(); + + int problems = 0; + + // manifest <-> vault_items.yml + for (var entry : manifest.entrySet()) { + String r = items.get(entry.getKey()); + if (r == null) { + log.warning("[validation] " + entry.getKey() + " is in the manifest but missing from vault_items.yml"); + problems++; + } else if (!r.equalsIgnoreCase(entry.getValue())) { + log.warning("[validation] " + entry.getKey() + " rarity disagrees: manifest=" + + entry.getValue() + " vault_items.yml=" + r); + problems++; + } + } + for (String k : items.keySet()) { + if (!manifest.containsKey(k)) { + log.warning("[validation] " + k + " is in vault_items.yml but not the manifest"); + problems++; + } + } + + // manifest <-> bv_target + for (String k : manifest.keySet()) { + if (!target.contains(k)) { + log.warning("[validation] " + k + " is in the manifest but not bv_target (edition mismatch?)"); + problems++; + } + } + for (String k : target) { + if (!manifest.containsKey(k)) { + log.warning("[validation] bv_target has " + k + " which the manifest does not (stale edition row)"); + problems++; + } + } + + // manifest <-> live registry + Set newBlocks = new LinkedHashSet<>(); + for (Material m : Material.values()) { + if (m.isLegacy() || !m.isBlock() || !m.isItem()) continue; + String key = m.getKey().getKey(); + if (UNOBTAINABLE.contains(key)) continue; + if (!manifest.containsKey(key)) newBlocks.add(key); + } + for (String k : manifest.keySet()) { + Material m = Material.matchMaterial(k); + if (m == null) { + log.warning("[validation] manifest block " + k + " is not a Material on this server version"); + problems++; + } + } + if (!newBlocks.isEmpty()) { + log.warning("[validation] " + newBlocks.size() + " obtainable block(s) exist that edition " + + plugin.database().edition() + " does not cover - candidates for the next edition:"); + log.warning("[validation] " + String.join(", ", newBlocks)); + } + + if (problems == 0) { + log.info("[validation] " + manifest.size() + + " blocks matched across manifest, vault_items.yml and bv_target."); + } else { + log.warning("[validation] " + problems + " discrepancy/ies found - see above."); + } + } + + private static Map loadItems(BlockVault plugin) { + Map out = new TreeMap<>(); + java.io.File override = new java.io.File(plugin.getDataFolder(), "vault_items.yml"); + YamlConfiguration yaml; + if (override.isFile()) { + yaml = YamlConfiguration.loadConfiguration(override); + } else { + try (InputStream in = plugin.getResource("vault_items.yml")) { + if (in == null) { + plugin.getLogger().warning("[validation] vault_items.yml not found - skipping that check"); + return out; + } + yaml = YamlConfiguration.loadConfiguration( + new InputStreamReader(in, StandardCharsets.UTF_8)); + } catch (Exception e) { + plugin.getLogger().warning("[validation] could not read vault_items.yml: " + e.getMessage()); + return out; + } + } + var section = yaml.getConfigurationSection("items"); + if (section == null) return out; + for (String k : section.getKeys(false)) { + out.put(k.toLowerCase(), String.valueOf(section.get(k))); + } + return out; + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/util/Webhook.java b/src/main/java/dev/anchorlight/blockvault/util/Webhook.java new file mode 100644 index 0000000..dd42199 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/util/Webhook.java @@ -0,0 +1,60 @@ +package dev.anchorlight.blockvault.util; + +import dev.anchorlight.blockvault.BlockVault; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +/** + * Fire-and-forget Discord webhook for rare submissions and chapter unlocks. + * Doubles as an off-server recovery log. No-op when no URL is configured. + */ +public final class Webhook { + + private final BlockVault plugin; + private final HttpClient http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)).build(); + + public Webhook(BlockVault plugin) { + this.plugin = plugin; + } + + private String url() { + String u = plugin.getConfig().getString("discord.webhook-url", ""); + return (u == null || u.isBlank()) ? null : u; + } + + public void rareSubmission(String material, String player, int points) { + send("💎 **" + escape(player) + "** donated a rare block: `" + + escape(material) + "` (+" + points + " points)"); + } + + public void chapterOpened(int chapter, String title) { + send("🔓 **Chapter " + chapter + " — " + escape(title) + "** is now open!"); + } + + private void send(String content) { + String url = url(); + if (url == null) return; + String body = "{\"content\":\"" + content.replace("\"", "\\\"") + "\"," + + "\"allowed_mentions\":{\"parse\":[]}}"; + HttpRequest req = HttpRequest.newBuilder(URI.create(url)) + .timeout(Duration.ofSeconds(10)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + http.sendAsync(req, HttpResponse.BodyHandlers.discarding()) + .exceptionally(ex -> { + plugin.getLogger().warning("Discord webhook failed: " + ex.getMessage()); + return null; + }); + } + + /** Strip formatting/control characters so display names can't inject into the payload. */ + private static String escape(String s) { + return s.replaceAll("[\\p{Cntrl}`*_~|\\\\@]", ""); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d790bf9..51865e5 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -42,5 +42,10 @@ points: uncommon: 5 rare: 10 +# Optional Discord webhook for rare submissions and chapter unlocks. +# Doubles as an off-server recovery log. Leave blank to disable. +discord: + webhook-url: "" + lang: prefix: "&8&l[&3BV&8&l] " From 3979a7382a9b38ae7d0cbb1c882a72ed82741be9 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:12:28 +1000 Subject: [PATCH 22/28] Add DisplayService for leaderboard display; implement chapter completion tracking and monthly leaderboard functionality --- .../anchorlight/blockvault/BlockVault.java | 9 ++ .../blockvault/chapter/ChapterService.java | 38 ++++-- .../commands/LeaderboardCommand.java | 12 +- .../blockvault/commands/SubmitCommand.java | 2 + .../anchorlight/blockvault/db/Database.java | 43 ++++++ .../blockvault/display/DisplayService.java | 128 ++++++++++++++++++ src/main/resources/plugin.yml | 4 +- 7 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 src/main/java/dev/anchorlight/blockvault/display/DisplayService.java diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index f4a1a3c..8373ae0 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -3,6 +3,7 @@ import dev.anchorlight.blockvault.chapter.ChapterService; import dev.anchorlight.blockvault.commands.*; import dev.anchorlight.blockvault.db.Database; +import dev.anchorlight.blockvault.display.DisplayService; import dev.anchorlight.blockvault.listener.RegionProtectionListener; import dev.anchorlight.blockvault.listener.PlayerGuidanceListener; import dev.anchorlight.blockvault.model.Manifest; @@ -30,6 +31,7 @@ public final class BlockVault extends JavaPlugin { private Database database; private Manifest manifest; private ChapterService chapters; + private DisplayService displays; private Webhook webhook; private Region region; @@ -72,6 +74,7 @@ public void onEnable() { this.chapters = new ChapterService(this); this.webhook = new Webhook(this); + this.displays = new DisplayService(this); StartupValidation.run(this); @@ -102,11 +105,13 @@ public void onEnable() { forceLoadChunks(true); chapters.start(); + displays.start(); ScheduleUtil.scheduleVaultStateTask(this, vaultUtil, fileUtil); } @Override public void onDisable() { + if (displays != null) displays.stop(); if (chapters != null) chapters.stop(); forceLoadChunks(false); if (database != null) database.close(); @@ -121,6 +126,10 @@ public Webhook webhook() { return webhook; } + public DisplayService displays() { + return displays; + } + /** * The protected volume, built lazily once the origin world is loaded. * Rebuilt by {@link #invalidateRegion()} after a config reload. diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java index 85ee329..3e77642 100644 --- a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -28,6 +28,7 @@ public final class ChapterService { private final BlockVault plugin; private final Set open = Collections.synchronizedSet(new HashSet<>()); + private final Set completed = Collections.synchronizedSet(new HashSet<>()); private BukkitTask task; public ChapterService(BlockVault plugin) { @@ -68,24 +69,40 @@ public void check() { plugin.getServer().getScheduler().runTask(plugin, () -> { long now = System.currentTimeMillis(); for (Database.ChapterRow row : rows) { - if (row.openedAt() != null) { - open.add(row.chapter()); - continue; + int ch = row.chapter(); + if (row.openedAt() != null) open.add(ch); + + if (!isOpen(ch) && row.openedAt() == null) { + boolean dateReached = row.opensAt() != null + && now >= row.opensAt().getTime(); + boolean prevNearlyDone = fraction(ch - 1, collected) >= EARLY_OPEN_FRACTION; + if (dateReached || prevNearlyDone) { + unlock(row, prevNearlyDone && !dateReached); + } } - if (isOpen(row.chapter())) continue; - boolean dateReached = row.opensAt() != null - && now >= row.opensAt().getTime(); - boolean prevNearlyDone = fraction(row.chapter() - 1, collected) >= EARLY_OPEN_FRACTION; - - if (dateReached || prevNearlyDone) { - unlock(row, prevNearlyDone && !dateReached); + // Completion: earlier chapters never close, but 100% is worth marking. + if (fraction(ch, collected) >= 1.0 && completed.add(ch)) { + onChapterComplete(row); } } }); }); } + private void onChapterComplete(Database.ChapterRow row) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, + () -> plugin.database().markChapterComplete(row.chapter(), null)); + String line = "§a§lChapter " + row.chapter() + " — " + row.title() + + " §r§ais complete! Every block on that floor has been given."; + plugin.getServer().broadcastMessage(plugin.prefix() + line); + plugin.getServer().getOnlinePlayers().forEach(p -> + p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1.2f)); + plugin.webhook().chapterOpened(row.chapter(), row.title() + " (complete)"); + plugin.displays().refresh(); + plugin.getLogger().info("Chapter " + row.chapter() + " reached 100%."); + } + private double fraction(int chapter, Set collected) { if (chapter < 1) return 1.0; int total = 0, done = 0; @@ -115,6 +132,7 @@ private void unlock(Database.ChapterRow row, boolean early) { () -> plugin.database().markChapterOpened(chapter)); ceremony(row, early); + plugin.displays().refresh(); } private void ceremony(Database.ChapterRow row, boolean early) { diff --git a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java index 169fcbd..73f87a7 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/LeaderboardCommand.java @@ -32,12 +32,18 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + boolean monthly = args.length > 0 && args[0].equalsIgnoreCase("month"); + String ym = java.time.YearMonth.now().toString(); // YYYY-MM + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - List top = plugin.database().topContributors(10); - int ownRank = (sender instanceof Player p) ? plugin.database().rankOf(p.getUniqueId()) : -1; + List top = monthly + ? plugin.database().monthlyTop(ym, 10) + : plugin.database().topContributors(10); + int ownRank = (!monthly && sender instanceof Player p) + ? plugin.database().rankOf(p.getUniqueId()) : -1; plugin.getServer().getScheduler().runTask(plugin, () -> { - plugin.tell(sender, "§aTop contributors"); + plugin.tell(sender, monthly ? "§aTop contributors — " + ym : "§aTop contributors — all time"); if (top.isEmpty()) { sender.sendMessage("§7 Nobody has donated a block yet."); return; diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index 039f54a..92b05c9 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -100,6 +100,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St if ("rare".equals(entry.rarity())) { plugin.webhook().rareSubmission(material, player.getName(), points); } + plugin.displays().refresh(); + plugin.chapters().check(); } } }); diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 9a66b7e..941c39c 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -329,6 +329,49 @@ public java.util.List chapters() { return rows; } + /** Stamp a chapter complete (only if not already). Blocking. Returns true if it changed. */ + public boolean markChapterComplete(int chapter, UUID by) { + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "UPDATE bv_chapter SET completed_at = CURRENT_TIMESTAMP, completed_by = ? " + + "WHERE chapter = ? AND edition = ? AND completed_at IS NULL")) { + if (by == null) ps.setNull(1, java.sql.Types.BINARY); + else ps.setBytes(1, toBytes(by)); + ps.setInt(2, chapter); + ps.setString(3, edition); + boolean changed = ps.executeUpdate() > 0; + if (changed) audit(by, "chapter_complete", null, "{\"chapter\":" + chapter + "}"); + return changed; + } catch (SQLException e) { + plugin.getLogger().severe("Could not mark chapter " + chapter + " complete: " + e.getMessage()); + return false; + } + } + + /** Top contributors for a given calendar month (YYYY-MM). Blocking. */ + public java.util.List monthlyTop(String yearMonth, int limit) { + java.util.List rows = new java.util.ArrayList<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT c.uuid, c.last_name, SUM(s.points) AS pts, COUNT(*) AS blocks " + + "FROM bv_submission s JOIN bv_contributor c ON c.uuid = s.uuid " + + "WHERE s.edition = ? AND DATE_FORMAT(s.submitted_at, '%Y-%m') = ? " + + "GROUP BY c.uuid, c.last_name ORDER BY pts DESC, blocks DESC LIMIT ?")) { + ps.setString(1, edition); + ps.setString(2, yearMonth); + ps.setInt(3, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + rows.add(new LeaderRow(fromBytes(rs.getBytes(1)), rs.getString(2), + rs.getLong(3), rs.getLong(4))); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Monthly leaderboard query failed: " + e.getMessage()); + } + return rows; + } + /** Stamp a chapter open (only if not already). Blocking. Returns true if it changed. */ public boolean markChapterOpened(int chapter) { try (Connection c = ds.getConnection(); diff --git a/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java b/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java new file mode 100644 index 0000000..6d494d7 --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java @@ -0,0 +1,128 @@ +package dev.anchorlight.blockvault.display; + +import dev.anchorlight.blockvault.BlockVault; +import dev.anchorlight.blockvault.db.Database; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Sign; +import org.bukkit.block.Skull; +import org.bukkit.entity.Display; +import org.bukkit.entity.TextDisplay; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.scheduler.BukkitTask; + +import java.util.List; + +/** + * The vanilla display layer: the lobby leader panel (head + flanking signs) and + * a {@link TextDisplay} hologram with the standings. No third-party dependency - + * TextDisplay is vanilla since 1.19.4. + */ +public final class DisplayService { + + private final BlockVault plugin; + private final org.bukkit.NamespacedKey tagKey; + private BukkitTask task; + + public DisplayService(BlockVault plugin) { + this.plugin = plugin; + this.tagKey = new org.bukkit.NamespacedKey(plugin, "hologram"); + } + + public void start() { + this.task = plugin.getServer().getScheduler().runTaskTimer( + plugin, this::refresh, 20L * 15L, 20L * 60L); + } + + public void stop() { + if (task != null) task.cancel(); + } + + /** Rebuild the leader panel and hologram from the current standings. */ + public void refresh() { + if (plugin.originWorld() == null) return; + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + List top = plugin.database().topContributors(5); + int done = plugin.database().collectedCount(); + int total = plugin.manifest().entries().size(); + plugin.getServer().getScheduler().runTask(plugin, () -> { + updateLeaderPanel(top.isEmpty() ? null : top.get(0)); + updateHologram(top, done, total); + }); + }); + } + + private void updateLeaderPanel(Database.LeaderRow leader) { + setSign(plugin.manifest().leader("name_sign"), + "§lLeader", leader == null ? "—" : leader.name(), "", ""); + setSign(plugin.manifest().leader("count_sign"), + "§lContribution", leader == null ? "—" : leader.points() + " pts", + leader == null ? "" : leader.blocks() + " blocks", ""); + setSign(plugin.manifest().leader("title"), + "§6The Vault", "Hall of the", "First Givers", ""); + + int[] headRel = plugin.manifest().leader("head"); + if (headRel == null || leader == null) return; + Location loc = plugin.resolve(headRel); + if (loc.getWorld() == null) return; + if (loc.getBlock().getType() != Material.PLAYER_HEAD + && loc.getBlock().getType() != Material.PLAYER_WALL_HEAD) { + loc.getBlock().setType(Material.PLAYER_HEAD, false); + } + if (loc.getBlock().getState() instanceof Skull skull) { + PlayerProfile profile = Bukkit.createPlayerProfile(leader.uuid()); + skull.setOwnerProfile(profile); + skull.update(true, false); + } + } + + private void setSign(int[] rel, String... lines) { + if (rel == null) return; + Location loc = plugin.resolve(rel); + if (loc.getWorld() == null) return; + if (!(loc.getBlock().getState() instanceof Sign sign)) return; // leave real structure alone + for (int i = 0; i < 4 && i < lines.length; i++) { + sign.setLine(i, lines[i]); + } + sign.update(true, false); + } + + private void updateHologram(List top, int done, int total) { + int[] rel = plugin.manifest().leader("head"); + if (rel == null) return; + Location at = plugin.resolve(rel).add(0.5, 2.2, 0.5); + if (at.getWorld() == null) return; + + StringBuilder text = new StringBuilder("§6§lThe Vault\n§7") + .append(done).append(" / ").append(total) + .append(" §7(").append(total == 0 ? 0 : Math.round(100.0 * done / total)).append("%)\n"); + int rank = 1; + for (Database.LeaderRow r : top) { + text.append("\n§e").append(rank++).append(". §f").append(r.name()) + .append(" §7").append(r.points()).append(" pts"); + } + if (top.isEmpty()) text.append("\n§7No donations yet"); + + TextDisplay display = findHologram(at); + if (display == null) { + display = at.getWorld().spawn(at, TextDisplay.class, d -> { + d.getPersistentDataContainer().set(tagKey, PersistentDataType.BYTE, (byte) 1); + d.setBillboard(Display.Billboard.CENTER); + d.setDefaultBackground(false); + }); + } + display.setText(text.toString()); + } + + private TextDisplay findHologram(Location near) { + for (var e : near.getWorld().getNearbyEntities(near, 3, 3, 3)) { + if (e instanceof TextDisplay td + && td.getPersistentDataContainer().has(tagKey, PersistentDataType.BYTE)) { + return td; + } + } + return null; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 270f890..a37797b 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -10,8 +10,8 @@ commands: description: Submit the block you are holding to the vault. usage: /bvsubmit bvleaderboard: - description: Show the top contributors. - usage: /bvleaderboard + description: Show the top contributors (add 'month' for this month only). + usage: /bvleaderboard [month] bvprogress: description: Show current chapter and overall progress. usage: /bvprogress From a975319b519779eae71fc13025a162c3476250a2 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:15:08 +1000 Subject: [PATCH 23/28] Add chapter date synchronization and configuration updates; implement async reload for chapter dates --- README.md | 93 ++++++++++++++----- .../blockvault/commands/QueryCommand.java | 2 + .../anchorlight/blockvault/db/Database.java | 41 ++++++++ src/main/resources/config.yml | 12 +++ 4 files changed, 126 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 44c0540..ca636c1 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,80 @@ ![img](https://i.imgur.com/DPcMoaq.png) -BlockVault is an interactive challenge where players collaborate to collect every unique block and item in Minecraft. The centerpiece is a large, dynamic museum building where contributions are tracked, and progress is displayed, turning the collection into an engaging and rewarding experience. +BlockVault drives a purpose-built museum for a twelve-month, server-wide block +collection event. Players donate one of every obtainable block in Minecraft +Java **26.2**; each donation is consumed and recorded, a donor head appears on +the shelf, and six themed floors unlock over the season. -## Features -### Interactive Block Displays -* Sections dedicated to specific block categories (e.g., wood, ores, decorative blocks). -* Blocks and items can be placed on predefined slots with item frames or display stands, indicating where each item belongs and can be contributed to. +## Requirements -### Points System -* Players earn points for each block they unlock and contribute to the museum. -* The points awarded vary based on the rarity of the block/item: - * **Common Blocks:** 1 point per block. - * **Uncommon Blocks:** 5 points per block. - * **Rare Blocks/Items:** 10+ points per block. -* A running total of points is tracked for each player which can be found using `/bvleaderboard`, contributing to their leaderboard position. +| | | +|---|---| +| Minecraft | Java Edition **26.2** (`api-version '26.2'`) | +| Server | Paper | +| Build JDK | **25** (paper-api 26.2 ships Java 25 bytecode) | +| Database | MySQL 8.0+ / MariaDB 10.5+ | +| Package | `dev.anchorlight.blockvault` | -### Tracking Progress -* Leaderboard displays the top contributors and their points when using `/bvleaderboard`. -* A progress bar or percentage indicator shows how close the server is to completing the collection using `/bvprogress`. +HikariCP and the MySQL driver are pulled at runtime via `libraries:` in +`plugin.yml` — no shading. -### Progress Leaderboard -* Progress can be viewed in real-time using a hologram display, showcasing the top contributors and collection milestones. -* Use the `/bvleaderboard` command to see a detailed leaderboard of top contributors, including points and blocks contributed. +## Setup + +1. **Apply the schema.** Run `src/main/resources/blockvault_schema.sql` against + your database before first start. +2. **Generate the data artefacts** from section 12 of the implementation brief: + ``` + python tools/gen_artifacts.py # reads tools/target_list.txt + ``` + This produces `src/main/resources/vault_items.yml`, + `src/main/resources/vault_slots.json` and `tools/spawn_frames.mcfunction`. + The plugin **disables itself** if `vault_slots.json` is missing — it never + regenerates the target list. +3. **Paste the schematic** `sky_vault.schem` at the world origin, then run + `spawn_frames.mcfunction` standing on that origin to spawn the empty frames. +4. **Configure** `config.yml` (git-ignored — holds DB credentials): + database connection, the world `origin:` the schematic sits at, the chapter + `opens-at` dates, and optionally a `discord.webhook-url`. +5. Start the server. Bootstrap seeds `bv_target` / `bv_chapter` from the manifest + and runs a three-way validation pass (manifest ↔ `vault_items.yml` ↔ + `bv_target`) plus new-block detection. +6. `/bvstart` opens the event. + +## How it works + +- **Submissions are transactional.** Order is always write → confirm commit → + consume the item. If the database is unreachable the block stays in the + player's hand. `bv_submission.material` is the primary key, so one-of-each is + enforced by the database and simultaneous submits race safely. +- **The head is the state indicator.** No separate colour-glass state block. + `/bvupdatestate` reconciles the world against the database and only ever writes + the manifest `head` cells — never structure — in a single summary line. +- **Chapters** open on their scheduled date *or* at 90% of the previous chapter, + whichever comes first. Earlier chapters never close. Unlock breaks one + `iron_bars` seal and runs a broadcast/title/fireworks/BossBar ceremony. +- **The target list is frozen per `edition`.** New blocks go in as a new edition, + never the running one. ## Commands -* `/bvstart` – Starts the vault challenge (requires `blockvault.start`). -* `/bvsubmit` – Submit the block/item that the player is holding to the vault. -* `/bvprogress` – Displays the overall progress of the vault. -* `/bvleaderboard` – Shows the top contributors and their points. \ No newline at end of file + +| Command | Permission | Purpose | +|---|---|---| +| `/bvsubmit` | `blockvault.submit` | Donate the held block (in-region, no creative) | +| `/bvprogress` | `blockvault.progress` | Current-chapter bar + overall bar | +| `/bvleaderboard [month]` | `blockvault.leaderboard` | Top 10 all-time, or this month | +| `/bvfind ` | `blockvault.find` | Level, room, shelf coordinates | +| `/bvinfo ` | `blockvault.info` | Rarity, points, chapter, status | +| `/bvmissing [chapter] [page]` | `blockvault.missing` | Outstanding list, paginated | +| `/bvcheck` | `blockvault.check` | What in your inventory the vault needs | +| `/bvme` | `blockvault.me` | Your blocks, points, rank | +| `/bvhistory ` | `blockvault.history` | Who donated it and when | +| `/bvedition` | `blockvault.edition` | Frozen target-list version | +| `/bvstart [stop]` | `blockvault.start` | Open / close the event | +| `/bvupdatestate` | `blockvault.updatestate` | Reconcile world ↔ database (console-safe) | +| `/bvreload` | `blockvault.reload` | Reload config without restart | +| `/bvrevoke ` | `blockvault.revoke` | Reverse a submission, refund points, remove head | +| `/bvrepair` | `blockvault.repair` | Rebuild missing frames and heads from the manifest | +| `/bvbackup` | `blockvault.backup` | Timestamped `mysqldump` | + +`blockvault.build` bypasses in-region build/interaction protection. diff --git a/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java index 250d62c..f4d9b81 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/QueryCommand.java @@ -184,6 +184,8 @@ private void edition(CommandSender sender) { private void reload(CommandSender sender) { plugin.reloadConfig(); plugin.invalidateRegion(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, + () -> plugin.database().resyncChapterDates()); plugin.tell(sender, "§aConfiguration reloaded."); } diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 941c39c..0b466ef 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -72,6 +72,7 @@ public void bootstrap(Manifest manifest) throws SQLException { runSchema(c); seedTargets(c, manifest); seedChapters(c, manifest); + syncChapterDates(c); warmCollected(c); } plugin.getLogger().info("Database ready: " + collected.size() @@ -160,6 +161,46 @@ private void seedChapters(Connection c, Manifest manifest) throws SQLException { } } + /** + * Push the configured open dates onto any chapter that has not opened yet. + * Runs every start so an admin can move a date mid-season by editing config. + */ + /** Public re-sync for /bvreload. Blocking - call async. */ + public void resyncChapterDates() { + try (Connection c = ds.getConnection()) { + syncChapterDates(c); + } catch (SQLException e) { + plugin.getLogger().severe("Chapter date re-sync failed: " + e.getMessage()); + } + } + + private void syncChapterDates(Connection c) throws SQLException { + var section = plugin.getConfig().getConfigurationSection("chapters"); + if (section == null) return; + try (PreparedStatement ps = c.prepareStatement( + "UPDATE bv_chapter SET opens_at = ? " + + "WHERE chapter = ? AND edition = ? AND opened_at IS NULL")) { + for (String key : section.getKeys(false)) { + String raw = section.getString(key + ".opens-at"); + if (raw == null || raw.isBlank()) continue; + java.sql.Timestamp ts; + try { + ts = java.sql.Timestamp.valueOf( + java.time.LocalDateTime.parse(raw)); + } catch (Exception e) { + plugin.getLogger().warning("chapters." + key + + ".opens-at is not a valid ISO date-time: " + raw); + continue; + } + ps.setTimestamp(1, ts); + ps.setInt(2, Integer.parseInt(key)); + ps.setString(3, edition); + ps.addBatch(); + } + ps.executeBatch(); + } + } + private void warmCollected(Connection c) throws SQLException { collected.clear(); try (PreparedStatement ps = c.prepareStatement( diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 51865e5..a53d469 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -42,6 +42,18 @@ points: uncommon: 5 rare: 10 +# Scheduled open dates per chapter (ISO local date-time). A chapter opens on +# this date OR when the previous chapter reaches 90%, whichever comes first. +# Re-read every start and applied to any chapter not yet open, so you can adjust +# a date mid-season. Chapter 1 opens with /bvstart regardless. +chapters: + '1': { opens-at: '2026-01-01T00:00:00' } + '2': { opens-at: '2026-02-15T00:00:00' } + '3': { opens-at: '2026-05-15T00:00:00' } + '4': { opens-at: '2026-08-01T00:00:00' } + '5': { opens-at: '2026-09-15T00:00:00' } + '6': { opens-at: '2026-12-01T00:00:00' } + # Optional Discord webhook for rare submissions and chapter unlocks. # Doubles as an off-server recovery log. Leave blank to disable. discord: From b7a32106f53b79a8b1b1a69e46b524ae428571e8 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:26:21 +1000 Subject: [PATCH 24/28] Add chapter advancement system; implement AdvancementService and related datapack files --- README.md | 3 + pom.xml | 10 ++ .../anchorlight/blockvault/BlockVault.java | 14 +++ .../advancement/AdvancementService.java | 116 ++++++++++++++++++ .../blockvault/chapter/ChapterService.java | 1 + .../anchorlight/blockvault/db/Database.java | 16 +++ .../listener/PlayerGuidanceListener.java | 1 + .../blockvault/advancement/chapter_1.json | 14 +++ .../blockvault/advancement/chapter_2.json | 14 +++ .../blockvault/advancement/chapter_3.json | 14 +++ .../blockvault/advancement/chapter_4.json | 14 +++ .../blockvault/advancement/chapter_5.json | 14 +++ .../blockvault/advancement/chapter_6.json | 14 +++ src/main/resources/datapack/pack.mcmeta | 7 ++ 14 files changed, 252 insertions(+) create mode 100644 src/main/java/dev/anchorlight/blockvault/advancement/AdvancementService.java create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_1.json create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_2.json create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_3.json create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_4.json create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_5.json create mode 100644 src/main/resources/datapack/data/blockvault/advancement/chapter_6.json create mode 100644 src/main/resources/datapack/pack.mcmeta diff --git a/README.md b/README.md index ca636c1..6e129dd 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ HikariCP and the MySQL driver are pulled at runtime via `libraries:` in `iron_bars` seal and runs a broadcast/title/fireworks/BossBar ceremony. - **The target list is frozen per `edition`.** New blocks go in as a new edition, never the running one. +- **Chapter advancements** ship as a bundled datapack, written into + `/datapacks/blockvault/` on load and granted by the plugin when a + chapter reaches 100%. A world reload may be needed the first time. ## Commands diff --git a/pom.xml b/pom.xml index 6249f85..de78a7e 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,16 @@ src/main/resources true + + datapack/** + + + + src/main/resources + false + + datapack/** + diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 8373ae0..2e0477c 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -1,5 +1,6 @@ package dev.anchorlight.blockvault; +import dev.anchorlight.blockvault.advancement.AdvancementService; import dev.anchorlight.blockvault.chapter.ChapterService; import dev.anchorlight.blockvault.commands.*; import dev.anchorlight.blockvault.db.Database; @@ -32,6 +33,7 @@ public final class BlockVault extends JavaPlugin { private Manifest manifest; private ChapterService chapters; private DisplayService displays; + private AdvancementService advancements; private Webhook webhook; private Region region; @@ -39,6 +41,14 @@ public static BlockVault get() { return instance; } + @Override + public void onLoad() { + instance = this; + // Datapack must land before worlds load so the game picks it up. + this.advancements = new AdvancementService(this); + this.advancements.installDatapack(); + } + @Override public void onEnable() { instance = this; @@ -130,6 +140,10 @@ public DisplayService displays() { return displays; } + public AdvancementService advancements() { + return advancements; + } + /** * The protected volume, built lazily once the origin world is loaded. * Rebuilt by {@link #invalidateRegion()} after a config reload. diff --git a/src/main/java/dev/anchorlight/blockvault/advancement/AdvancementService.java b/src/main/java/dev/anchorlight/blockvault/advancement/AdvancementService.java new file mode 100644 index 0000000..21b5a0f --- /dev/null +++ b/src/main/java/dev/anchorlight/blockvault/advancement/AdvancementService.java @@ -0,0 +1,116 @@ +package dev.anchorlight.blockvault.advancement; + +import dev.anchorlight.blockvault.BlockVault; +import org.bukkit.NamespacedKey; +import org.bukkit.advancement.Advancement; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +/** + * Ships a bundled datapack ({@code src/main/resources/datapack}) with one + * advancement per chapter, granted by the plugin on completion. Paper has no + * runtime advancement-registration API, so the datapack is written into the + * world folder during {@link #installDatapack()} (called from onLoad, before + * worlds load) and picked up automatically. + */ +public final class AdvancementService { + + private static final int CHAPTERS = 6; + private static final String[] FILES = { + "pack.mcmeta", + "data/blockvault/advancement/chapter_1.json", + "data/blockvault/advancement/chapter_2.json", + "data/blockvault/advancement/chapter_3.json", + "data/blockvault/advancement/chapter_4.json", + "data/blockvault/advancement/chapter_5.json", + "data/blockvault/advancement/chapter_6.json", + }; + + private final BlockVault plugin; + + public AdvancementService(BlockVault plugin) { + this.plugin = plugin; + } + + /** Write the datapack into <worldContainer>/<level-name>/datapacks/blockvault. */ + public void installDatapack() { + try { + String level = levelName(); + Path root = plugin.getServer().getWorldContainer().toPath() + .resolve(level).resolve("datapacks").resolve("blockvault"); + String stamp = plugin.getPluginMeta().getVersion(); + Path marker = root.resolve(".plugin-version"); + + if (Files.isRegularFile(marker) && stamp.equals(Files.readString(marker).trim())) { + return; // already current + } + for (String rel : FILES) { + Path target = root.resolve(rel); + Files.createDirectories(target.getParent()); + try (InputStream in = plugin.getResource("datapack/" + rel)) { + if (in == null) { + plugin.getLogger().warning("Datapack resource missing: " + rel); + continue; + } + Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + Files.writeString(marker, stamp); + plugin.getLogger().info("Installed chapter-advancement datapack for level '" + level + + "'. A world reload may be needed on first install."); + } catch (Exception e) { + plugin.getLogger().warning("Could not install advancement datapack: " + e.getMessage()); + } + } + + private String levelName() { + File props = new File("server.properties"); + if (props.isFile()) { + try { + var p = new java.util.Properties(); + try (var r = Files.newBufferedReader(props.toPath())) { p.load(r); } + String name = p.getProperty("level-name"); + if (name != null && !name.isBlank()) return name.trim(); + } catch (Exception ignored) { + // fall through + } + } + return "world"; + } + + /** Award the chapter advancement to everyone online. Main thread. */ + public void grantAll(int chapter) { + Advancement adv = lookup(chapter); + if (adv == null) return; + for (Player p : plugin.getServer().getOnlinePlayers()) award(p, adv); + } + + /** On join, reconcile a player against the chapters already completed. */ + public void syncPlayer(Player player) { + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { + Set done = plugin.database().completedChapters(); + plugin.getServer().getScheduler().runTask(plugin, () -> { + for (int ch : done) { + Advancement adv = lookup(ch); + if (adv != null) award(player, adv); + } + }); + }); + } + + private Advancement lookup(int chapter) { + if (chapter < 1 || chapter > CHAPTERS) return null; + return plugin.getServer().getAdvancement( + new NamespacedKey("blockvault", "chapter_" + chapter)); + } + + private void award(Player player, Advancement adv) { + var progress = player.getAdvancementProgress(adv); + if (!progress.isDone()) progress.awardCriteria("granted"); + } +} diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java index 3e77642..5ddf3ba 100644 --- a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -100,6 +100,7 @@ private void onChapterComplete(Database.ChapterRow row) { p.playSound(p.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1f, 1.2f)); plugin.webhook().chapterOpened(row.chapter(), row.title() + " (complete)"); plugin.displays().refresh(); + plugin.advancements().grantAll(row.chapter()); plugin.getLogger().info("Chapter " + row.chapter() + " reached 100%."); } diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 0b466ef..8c2ef9e 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -389,6 +389,22 @@ public boolean markChapterComplete(int chapter, UUID by) { } } + /** Chapters with a completed_at stamp for this edition. Blocking. */ + public java.util.Set completedChapters() { + java.util.Set out = new java.util.HashSet<>(); + try (Connection c = ds.getConnection(); + PreparedStatement ps = c.prepareStatement( + "SELECT chapter FROM bv_chapter WHERE edition = ? AND completed_at IS NOT NULL")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) out.add(rs.getInt(1)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Completed-chapter query failed: " + e.getMessage()); + } + return out; + } + /** Top contributors for a given calendar month (YYYY-MM). Blocking. */ public java.util.List monthlyTop(String yearMonth, int limit) { java.util.List rows = new java.util.ArrayList<>(); diff --git a/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java b/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java index afb05a4..89adb82 100644 --- a/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java +++ b/src/main/java/dev/anchorlight/blockvault/listener/PlayerGuidanceListener.java @@ -45,6 +45,7 @@ public void start() { @EventHandler public void onJoin(PlayerJoinEvent e) { + plugin.advancements().syncPlayer(e.getPlayer()); if (!vaultUtil.hasStarted()) return; int done = plugin.database().collectedCount(); int total = plugin.manifest().entries().size(); diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_1.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_1.json new file mode 100644 index 0000000..9908c18 --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_1.json @@ -0,0 +1,14 @@ +{ + "display": { + "icon": { "id": "minecraft:chiseled_stone_bricks" }, + "title": { "text": "Foundations" }, + "description": { "text": "Complete Chapter 1 of the Vault — The Undercroft" }, + "background": "minecraft:textures/gui/advancements/backgrounds/stone.png", + "frame": "goal", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_2.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_2.json new file mode 100644 index 0000000..1638766 --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_2.json @@ -0,0 +1,14 @@ +{ + "parent": "blockvault:chapter_1", + "display": { + "icon": { "id": "minecraft:oak_sapling" }, + "title": { "text": "Green & Growing" }, + "description": { "text": "Complete Chapter 2 of the Vault — The Conservatory" }, + "frame": "goal", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_3.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_3.json new file mode 100644 index 0000000..83da52b --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_3.json @@ -0,0 +1,14 @@ +{ + "parent": "blockvault:chapter_2", + "display": { + "icon": { "id": "minecraft:deepslate_bricks" }, + "title": { "text": "Into the Deep" }, + "description": { "text": "Complete Chapter 3 of the Vault — The Deep" }, + "frame": "goal", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_4.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_4.json new file mode 100644 index 0000000..2dccab8 --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_4.json @@ -0,0 +1,14 @@ +{ + "parent": "blockvault:chapter_3", + "display": { + "icon": { "id": "minecraft:pink_wool" }, + "title": { "text": "Every Colour" }, + "description": { "text": "Complete Chapter 4 of the Vault — The Gallery" }, + "frame": "goal", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_5.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_5.json new file mode 100644 index 0000000..238667a --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_5.json @@ -0,0 +1,14 @@ +{ + "parent": "blockvault:chapter_4", + "display": { + "icon": { "id": "minecraft:nether_bricks" }, + "title": { "text": "The Nether" }, + "description": { "text": "Complete Chapter 5 of the Vault — The Forge" }, + "frame": "goal", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/data/blockvault/advancement/chapter_6.json b/src/main/resources/datapack/data/blockvault/advancement/chapter_6.json new file mode 100644 index 0000000..57e12a9 --- /dev/null +++ b/src/main/resources/datapack/data/blockvault/advancement/chapter_6.json @@ -0,0 +1,14 @@ +{ + "parent": "blockvault:chapter_5", + "display": { + "icon": { "id": "minecraft:purpur_block" }, + "title": { "text": "The End" }, + "description": { "text": "Complete Chapter 6 of the Vault — The Observatory" }, + "frame": "challenge", + "show_toast": true, + "announce_to_chat": true, + "hidden": false + }, + "criteria": { "granted": { "trigger": "minecraft:impossible" } }, + "requirements": [["granted"]] +} diff --git a/src/main/resources/datapack/pack.mcmeta b/src/main/resources/datapack/pack.mcmeta new file mode 100644 index 0000000..70db763 --- /dev/null +++ b/src/main/resources/datapack/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "BlockVault chapter advancements", + "pack_format": 88, + "supported_formats": { "min_inclusive": 57, "max_inclusive": 9999 } + } +} From 7c84a639cc59a49d4d31f87ef7c04501d2ab5395 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Sun, 6 Sep 2026 13:31:59 +1000 Subject: [PATCH 25/28] Refactor manifest entry creation in gen_artifacts.py; remove unnecessary section assignment --- tools/__pycache__/gen_artifacts.cpython-313.pyc | Bin 0 -> 6709 bytes tools/gen_artifacts.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tools/__pycache__/gen_artifacts.cpython-313.pyc diff --git a/tools/__pycache__/gen_artifacts.cpython-313.pyc b/tools/__pycache__/gen_artifacts.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eeaae649cb55adab7302ab00fbe43de8db2bcba GIT binary patch literal 6709 zcmbU_TW}jkaipb_!1xD@*$a!_z*==59(o45=By$s0b@Cw!u4i2`ni{-~!%V zNF1N ziGG#3NNco6+RUqr#+Ws$v1ZL_9MtTqwu^SnE~06B2b$)GU_B{5PqE$!he{n^?M`g5 z{XCrJoOViFuM;83W{-z3Q5r1l2facTvCPPmMW#HK31#hM^g1w3Sf;1 zJtnNZN=bDvS`GMm6VqVAMicHaVH3=0?&GwY0VK8Dq%;rItx(rav%UjA8e&*BWKq@(WmXOux;U%F=0)SWLK;a$6%ARNQw%X0(G9VCMvEx3qOOFDNK6$6 z4tx6qVM2{3jA2nUVzH>+Uz{E!)BB7CLlk>q1gL59ZEIccz+rKQ?B65CR7F%&Lt7N% ziUxbA%CI2rDCRUVbX|@cFs{j3#8|Y*hzsJPm=uBQh&oqZ)lG=#Vk}|kk+1?=%Z8}K zdIcDnRn(B8_lR*Vrjx#`DPm+!jcH1_&npO55(b<^FLI!3q5gR}qV{Ww9!qGjuKyMh zS1@8I^LpRnd=%I;+r#3#9FGGJVr;f}F0uDaiPs)ckAY;vCVMX=sYhdm-giR>k&3g4 zXcP#lky(WZoCqfZ=Q$`w6*&wtI0(m=qq^6mSQNP)mv5`VSxuf-^uGDfY(gzwPf3nM z#(H9Y9;g!`o8s;fwS-!{YCUvanU{@7NQ`NbxrizVCMEH-sDo@^zwSdl13iblK9{ zB5Ld?%Nld$(UO(B)G5gxKH7IU@)G>>Pe7f~6NUhm0FG?JVh*kosB&;=P_ef`ikMYo z-S9G+xdMJ{__N7m=E?s+(ZE8L5yYnKL==~h*~KL+W{D86Lj_?6iGZ*WiX~K|*yfCg z5ml_6;73Fnu}&7EW;dc`H)8*8#Gx{R!_;9+4PxpDriL(e^b9#VX5pVuuq_lL_ltSB zdb4$gNCVqd*+^(|6!SBQNHh#8iuq713hJzcF%LjDKsm5rX^b3Ae)Qw+fZUrr)O#ZM zql4aQ{b2WWxL6knFIhV*k&Vp>7VRaA^wKuHr4k2A)@J64-j^++&uf_%xMben61lh~ za)+?)Kv$?_^5JX}IMjdvqNENT3RpY=J4_?^&NrE-1_rkX|WLCDYfi017o5^EWBYi_vy zg>$*xb7kP;@~a31h%C(FtF&6%wZFVhwfSx3efrIPi1P0$)(IqVhAfio65mgoj)m;@ zF6>rzxPIhDlEcpc6#U#N+KmRwdnuIP=BwL98oCzs1P?M?h<_OW$M`p|X(wSs250?T z+D^ay2efHl!w+fu?UsZ@6xd^p`<)hc2hQ~zT!n>GtJNyC(ogxCEgIXk-@b!NISBDP z{DQyIU*)gvXGkqEy|AR*vz2>p$<hdI+PVtdIkVrn>9H!LR9xU7M9n2DC0>adus85YBdcr+3s zPQU0)y}V{xl>tJ@R`jXZUh?UfHH{i)`T&jGAGQ@etk(^uS~0LgQ4{lK5JBv<91a@F zf`J7cHZe@!3$_=0Rvb`Zybb+!gxKLr6O*Hu13wcYwwcVug0!fEFCATgwrx&}CE_|} z5fw1tQx!Grt-)-ZxCwj7odAQ1qGC$L41^SzC0k-@-aHa!i9obL;95IESf>^+ zwTP*tc8v6BeF45j_>wv?#U=l^qrxwr8=m&-il$GWRn+P8u@D?pHT3DRh%ug+nLb;N znx>1ccTQ1*aL9-mI`zdDF=t+Z<_Gd{u}Gf+laxXQcU5X^!|A>=lsdmr)0{c~my`D= zAC6@vb1kRVTgLJ&V{0uFzZqDsxx8XqrV5occNbR{@BCz${if2BHvasl@BK8JSa0af zH}pQN{rk4RZTppHePA>{F#4sdcEeS5_u|UMw6W%DS*E_Jta@DOxtm-`-dW0=&0Nh@ zwq?gwU(a{m729*HQ58wd^Spw@HlaY- z{`--{HGy?KL3_%4lqpc#)?6~5Ak3==QN(b`O;g@1AWg!LP9Ww95Fw1CN6PQCUAL)so9fz|F>|RC{2rwIPhukrDE#yr1#4TO-@G zRPX0NIt_lC3GDzI{XD=ugAPl2BDxGlgIMN3hqY2_G9QD@FOtFY@7`b5ooCM;=(6Z~ z(3C@`4*jsOV^%>r|s&&>VhGjm<4)07gNT4i5*Ahi-X zoTg9Hq?Y#fR)9@nbowRez4p&i+lxmpca~1?HMa%uf_oAN>1~+>%Cn{R05O1O*`fJ@ zkEr9WV#Nd&opKEk2Ptp|+jKjgOV{=Pqw5!>&XQ!Hf?k)|+t&+~>9YlCzlnLt!l)HH zcggG1bb?UsA@i!iaH*680fa;mRzZzvXfwt3AbShl<3O3gl@n!} zV~;Vjl$T4=qG&GwC*bTh5mkSY!foT3_Du%uY0eB}{_y^ArYAeITD^MwljhZmFY1rN z@b@~HY4mP$EA`AV$<}Zr>M1cgkLNQ+;Xg#p6M&#w?u>BT<8!#2SNFpX! z>S<#zifzS5KSWIA&7Z6)vbdctFrz^@fGwhiAx=qN{=c$JUjGoDsU*IHB#fzzyAuI@ zNxD-^)|*KVNEMLmLQnZz+EH=@5>_Q!OUZ?Kh{QsgJZlV-V;1uf;@hBTZzmn%PcE2g zn&ITWnVt)g1tmN@Fw--k-bzGCLJA^Q(w8DpI0dx+NF&#jYBl*r@;Wi|slwt&IpBnW^6IUiN1Bbw_^CQhlX>{cL zs1I|}=*T4rD8Y`H3n%6wa%MXtIYuor!h8T>e zh^n|qrAGgQ<8#iIJlC?$cIVmdO`3AlZPc}-Co;YVOm^&JSLSW7Fnyj)hIV)!*EMZ% zw4?cPYdfhs9`_7x+7aj7LJT+bog39Q|IMX>r)i_6@xjoRovpH^#=jGg&5?7q=DF5& z)|+R&8xX(>e6=o_|+S3t@X*efsu$Z>$K*bei69HDstv_kCyH)dfR*#a-8mtKdGk zdUo~5s`jwsVeLcTBl@8MM>*A4Xxx+5b4{-ha%ZXkWEh9AWk>EhIDk!99((7J><%Fipxi47p`)@s{+WS4O_Uhz6{`DT$_%QqHe%JWREarnj zV)uhVFRfi5no8V@w+Y*W!Eh`T3}PV|oJ~MNObG@xh#Qe+0Khcl&lr7C$9D40j7Db6 zb%fe1m?KzbCt1npEy%LL+m5_KZH*fDvu?5g0|2xt2RNmH{xtX_l zQ%nIC%)Rq=uF;#fdsFOVp(RXT6^X^kA=WlCTK0S#WJ3f75i=l)Q zDcf(|4Ike4G>P4fA0`3iIGEzTwISDm6px+bDZXHL{Au#th;=O|*O)zDvizT10L^8a%ya)Qz=u7| literal 0 HcmV?d00001 diff --git a/tools/gen_artifacts.py b/tools/gen_artifacts.py index 5e41517..d07a964 100644 --- a/tools/gen_artifacts.py +++ b/tools/gen_artifacts.py @@ -85,7 +85,7 @@ def main(): # vault_slots.json - manifest manifest = { "version": VERSION, "data_version": DATA_VERSION, - "entries": [dict(e, section="building" if False else e["section"]) for e in entries], + "entries": entries, "leader": LEADER, "seals": SEALS, } (SRC / "vault_slots.json").write_text( From e4de2cacee266951e9a1a8a2015df630952317d8 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Mon, 7 Sep 2026 20:39:53 +1000 Subject: [PATCH 26/28] Add validation and enhancements for chapter completion and inventory consumption; improve error handling in database queries and display service --- .../anchorlight/blockvault/BlockVault.java | 2 + .../blockvault/chapter/ChapterService.java | 21 +++++++--- .../blockvault/commands/SubmitCommand.java | 19 ++++----- .../anchorlight/blockvault/db/Database.java | 41 ++++++++++++------- .../blockvault/display/DisplayService.java | 2 +- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/main/java/dev/anchorlight/blockvault/BlockVault.java b/src/main/java/dev/anchorlight/blockvault/BlockVault.java index 2e0477c..40c7562 100644 --- a/src/main/java/dev/anchorlight/blockvault/BlockVault.java +++ b/src/main/java/dev/anchorlight/blockvault/BlockVault.java @@ -193,6 +193,8 @@ public World originWorld() { /** Resolve a manifest-relative coordinate to a live world location. */ public Location resolve(int[] rel) { + if (rel == null) throw new IllegalArgumentException( + "manifest coordinate missing - check vault_slots.json (leader/seals block)"); return new Location(originWorld(), getConfig().getInt("origin.x") + rel[0], getConfig().getInt("origin.y") + rel[1], diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java index 5ddf3ba..c86b8eb 100644 --- a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -82,7 +82,9 @@ public void check() { } // Completion: earlier chapters never close, but 100% is worth marking. - if (fraction(ch, collected) >= 1.0 && completed.add(ch)) { + // Requires the chapter to actually have targets - guards against a + // partial manifest reporting every empty chapter as "complete". + if (chapterSize(ch) > 0 && fraction(ch, collected) >= 1.0 && completed.add(ch)) { onChapterComplete(row); } } @@ -105,7 +107,7 @@ private void onChapterComplete(Database.ChapterRow row) { } private double fraction(int chapter, Set collected) { - if (chapter < 1) return 1.0; + if (chapter < 1) return 1.0; // "previous of chapter 1" is trivially done int total = 0, done = 0; for (TargetEntry e : plugin.manifest().entries().values()) { if (e.chapter() != chapter) continue; @@ -115,6 +117,14 @@ private double fraction(int chapter, Set collected) { return total == 0 ? 1.0 : (double) done / total; } + private int chapterSize(int chapter) { + int n = 0; + for (TargetEntry e : plugin.manifest().entries().values()) { + if (e.chapter() == chapter) n++; + } + return n; + } + private void unlock(Database.ChapterRow row, boolean early) { int chapter = row.chapter(); if (!open.add(chapter)) return; // someone beat us to it this tick @@ -158,9 +168,10 @@ private void ceremony(Database.ChapterRow row, boolean early) { plugin.getServer().getOnlinePlayers().forEach(bar::addPlayer); plugin.getServer().getScheduler().runTaskLater(plugin, bar::removeAll, 200L); - // Fireworks at the leader panel, if the world is up. - Location at = plugin.resolve(plugin.manifest().leader("head")); - if (at.getWorld() != null) { + // Fireworks at the leader panel, if the manifest defines it and the world is up. + int[] leaderHead = plugin.manifest().leader("head"); + Location at = leaderHead == null ? null : plugin.resolve(leaderHead); + if (at != null && at.getWorld() != null) { for (int i = 0; i < 3; i++) { Firework fw = at.getWorld().spawn(at.clone().add(0.5, 1, 0.5), Firework.class); FireworkMeta meta = fw.getFireworkMeta(); diff --git a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java index 92b05c9..c39bf6a 100644 --- a/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java +++ b/src/main/java/dev/anchorlight/blockvault/commands/SubmitCommand.java @@ -110,18 +110,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } private void consumeOne(Player player, Material expected) { - ItemStack held = player.getInventory().getItemInMainHand(); - if (held.getType() != expected || held.getAmount() < 1) { - // Player swapped items between the command and the commit. The block - // is already recorded; not consuming it is a harmless edge case. - plugin.getLogger().warning(player.getName() + " moved " + expected - + " before it could be consumed; it was recorded but not removed."); - return; - } - if (held.getAmount() > 1) { - held.setAmount(held.getAmount() - 1); - } else { - player.getInventory().setItemInMainHand(null); + // Remove one from anywhere in the inventory, not just the main hand: + // the write has committed and the block must be consumed (brief 1.1), + // even if the player shuffled it between the command and this tick. + var leftover = player.getInventory().removeItem(new ItemStack(expected, 1)); + if (!leftover.isEmpty()) { + plugin.getLogger().warning(player.getName() + " no longer had " + expected + + " when it was consumed; recorded but not removed from inventory."); } } diff --git a/src/main/java/dev/anchorlight/blockvault/db/Database.java b/src/main/java/dev/anchorlight/blockvault/db/Database.java index 8c2ef9e..dc511fc 100644 --- a/src/main/java/dev/anchorlight/blockvault/db/Database.java +++ b/src/main/java/dev/anchorlight/blockvault/db/Database.java @@ -28,6 +28,7 @@ * and updated only after a write commits. */ public final class Database { + /** MySQL error code for a duplicate primary/unique key. */ private static final int ER_DUP_ENTRY = 1062; @@ -105,7 +106,7 @@ private static String stripComments(String block) { } private void seedTargets(Connection c, Manifest manifest) throws SQLException { - if (rowCount(c, "bv_target") > 0) return; + if (editionRowCount(c, "bv_target") > 0) return; String q = "INSERT INTO bv_target (material,edition,chapter,rarity,section," + "sign_x,sign_y,sign_z,frame_x,frame_y,frame_z,head_x,head_y,head_z,facing) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; @@ -129,7 +130,7 @@ private void seedTargets(Connection c, Manifest manifest) throws SQLException { } private void seedChapters(Connection c, Manifest manifest) throws SQLException { - if (rowCount(c, "bv_chapter") > 0) return; + if (editionRowCount(c, "bv_chapter") > 0) return; String[][] meta = { {"1", "Foundations", "The Undercroft"}, {"2", "Green & Growing", "The Conservatory"}, @@ -212,10 +213,14 @@ private void warmCollected(Connection c) throws SQLException { } } - private int rowCount(Connection c, String table) throws SQLException { - try (Statement st = c.createStatement(); - ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + table)) { - return rs.next() ? rs.getInt(1) : 0; + /** Row count for the current edition. Table must have an {@code edition} column. */ + private int editionRowCount(Connection c, String table) throws SQLException { + try (PreparedStatement ps = c.prepareStatement( + "SELECT COUNT(*) FROM " + table + " WHERE edition = ?")) { + ps.setString(1, edition); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } } } @@ -583,14 +588,22 @@ public java.util.List topContributors(int limit) { /** 1-based rank of {@code uuid} on the leaderboard, or -1 if they have no row. Blocking. */ public int rankOf(UUID uuid) { - try (Connection c = ds.getConnection(); - PreparedStatement ps = c.prepareStatement( - "SELECT COUNT(*) + 1 FROM bv_contributor a " - + "JOIN bv_contributor b ON b.uuid = ? " - + "WHERE a.points > b.points")) { - ps.setBytes(1, toBytes(uuid)); - try (ResultSet rs = ps.executeQuery()) { - return rs.next() ? rs.getInt(1) : -1; + byte[] id = toBytes(uuid); + try (Connection c = ds.getConnection()) { + try (PreparedStatement ps = c.prepareStatement( + "SELECT points FROM bv_contributor WHERE uuid = ?")) { + ps.setBytes(1, id); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return -1; // never contributed + long myPoints = rs.getLong(1); + try (PreparedStatement r = c.prepareStatement( + "SELECT COUNT(*) + 1 FROM bv_contributor WHERE points > ?")) { + r.setLong(1, myPoints); + try (ResultSet rr = r.executeQuery()) { + return rr.next() ? rr.getInt(1) : -1; + } + } + } } } catch (SQLException e) { plugin.getLogger().severe("Rank query failed: " + e.getMessage()); diff --git a/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java b/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java index 6d494d7..955fe27 100644 --- a/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java +++ b/src/main/java/dev/anchorlight/blockvault/display/DisplayService.java @@ -93,7 +93,7 @@ private void updateHologram(List top, int done, int total) { int[] rel = plugin.manifest().leader("head"); if (rel == null) return; Location at = plugin.resolve(rel).add(0.5, 2.2, 0.5); - if (at.getWorld() == null) return; + if (at.getWorld() == null || !at.isChunkLoaded()) return; // don't spawn into an unloaded chunk StringBuilder text = new StringBuilder("§6§lThe Vault\n§7") .append(done).append(" / ").append(total) From 54fad5c3d7d9559c6593788624d31a648b8066ef Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Tue, 8 Sep 2026 06:49:26 +1000 Subject: [PATCH 27/28] Remove compiled Python bytecode file for gen_artifacts.py --- tools/__pycache__/gen_artifacts.cpython-313.pyc | Bin 6709 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tools/__pycache__/gen_artifacts.cpython-313.pyc diff --git a/tools/__pycache__/gen_artifacts.cpython-313.pyc b/tools/__pycache__/gen_artifacts.cpython-313.pyc deleted file mode 100644 index 6eeaae649cb55adab7302ab00fbe43de8db2bcba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6709 zcmbU_TW}jkaipb_!1xD@*$a!_z*==59(o45=By$s0b@Cw!u4i2`ni{-~!%V zNF1N ziGG#3NNco6+RUqr#+Ws$v1ZL_9MtTqwu^SnE~06B2b$)GU_B{5PqE$!he{n^?M`g5 z{XCrJoOViFuM;83W{-z3Q5r1l2facTvCPPmMW#HK31#hM^g1w3Sf;1 zJtnNZN=bDvS`GMm6VqVAMicHaVH3=0?&GwY0VK8Dq%;rItx(rav%UjA8e&*BWKq@(WmXOux;U%F=0)SWLK;a$6%ARNQw%X0(G9VCMvEx3qOOFDNK6$6 z4tx6qVM2{3jA2nUVzH>+Uz{E!)BB7CLlk>q1gL59ZEIccz+rKQ?B65CR7F%&Lt7N% ziUxbA%CI2rDCRUVbX|@cFs{j3#8|Y*hzsJPm=uBQh&oqZ)lG=#Vk}|kk+1?=%Z8}K zdIcDnRn(B8_lR*Vrjx#`DPm+!jcH1_&npO55(b<^FLI!3q5gR}qV{Ww9!qGjuKyMh zS1@8I^LpRnd=%I;+r#3#9FGGJVr;f}F0uDaiPs)ckAY;vCVMX=sYhdm-giR>k&3g4 zXcP#lky(WZoCqfZ=Q$`w6*&wtI0(m=qq^6mSQNP)mv5`VSxuf-^uGDfY(gzwPf3nM z#(H9Y9;g!`o8s;fwS-!{YCUvanU{@7NQ`NbxrizVCMEH-sDo@^zwSdl13iblK9{ zB5Ld?%Nld$(UO(B)G5gxKH7IU@)G>>Pe7f~6NUhm0FG?JVh*kosB&;=P_ef`ikMYo z-S9G+xdMJ{__N7m=E?s+(ZE8L5yYnKL==~h*~KL+W{D86Lj_?6iGZ*WiX~K|*yfCg z5ml_6;73Fnu}&7EW;dc`H)8*8#Gx{R!_;9+4PxpDriL(e^b9#VX5pVuuq_lL_ltSB zdb4$gNCVqd*+^(|6!SBQNHh#8iuq713hJzcF%LjDKsm5rX^b3Ae)Qw+fZUrr)O#ZM zql4aQ{b2WWxL6knFIhV*k&Vp>7VRaA^wKuHr4k2A)@J64-j^++&uf_%xMben61lh~ za)+?)Kv$?_^5JX}IMjdvqNENT3RpY=J4_?^&NrE-1_rkX|WLCDYfi017o5^EWBYi_vy zg>$*xb7kP;@~a31h%C(FtF&6%wZFVhwfSx3efrIPi1P0$)(IqVhAfio65mgoj)m;@ zF6>rzxPIhDlEcpc6#U#N+KmRwdnuIP=BwL98oCzs1P?M?h<_OW$M`p|X(wSs250?T z+D^ay2efHl!w+fu?UsZ@6xd^p`<)hc2hQ~zT!n>GtJNyC(ogxCEgIXk-@b!NISBDP z{DQyIU*)gvXGkqEy|AR*vz2>p$<hdI+PVtdIkVrn>9H!LR9xU7M9n2DC0>adus85YBdcr+3s zPQU0)y}V{xl>tJ@R`jXZUh?UfHH{i)`T&jGAGQ@etk(^uS~0LgQ4{lK5JBv<91a@F zf`J7cHZe@!3$_=0Rvb`Zybb+!gxKLr6O*Hu13wcYwwcVug0!fEFCATgwrx&}CE_|} z5fw1tQx!Grt-)-ZxCwj7odAQ1qGC$L41^SzC0k-@-aHa!i9obL;95IESf>^+ zwTP*tc8v6BeF45j_>wv?#U=l^qrxwr8=m&-il$GWRn+P8u@D?pHT3DRh%ug+nLb;N znx>1ccTQ1*aL9-mI`zdDF=t+Z<_Gd{u}Gf+laxXQcU5X^!|A>=lsdmr)0{c~my`D= zAC6@vb1kRVTgLJ&V{0uFzZqDsxx8XqrV5occNbR{@BCz${if2BHvasl@BK8JSa0af zH}pQN{rk4RZTppHePA>{F#4sdcEeS5_u|UMw6W%DS*E_Jta@DOxtm-`-dW0=&0Nh@ zwq?gwU(a{m729*HQ58wd^Spw@HlaY- z{`--{HGy?KL3_%4lqpc#)?6~5Ak3==QN(b`O;g@1AWg!LP9Ww95Fw1CN6PQCUAL)so9fz|F>|RC{2rwIPhukrDE#yr1#4TO-@G zRPX0NIt_lC3GDzI{XD=ugAPl2BDxGlgIMN3hqY2_G9QD@FOtFY@7`b5ooCM;=(6Z~ z(3C@`4*jsOV^%>r|s&&>VhGjm<4)07gNT4i5*Ahi-X zoTg9Hq?Y#fR)9@nbowRez4p&i+lxmpca~1?HMa%uf_oAN>1~+>%Cn{R05O1O*`fJ@ zkEr9WV#Nd&opKEk2Ptp|+jKjgOV{=Pqw5!>&XQ!Hf?k)|+t&+~>9YlCzlnLt!l)HH zcggG1bb?UsA@i!iaH*680fa;mRzZzvXfwt3AbShl<3O3gl@n!} zV~;Vjl$T4=qG&GwC*bTh5mkSY!foT3_Du%uY0eB}{_y^ArYAeITD^MwljhZmFY1rN z@b@~HY4mP$EA`AV$<}Zr>M1cgkLNQ+;Xg#p6M&#w?u>BT<8!#2SNFpX! z>S<#zifzS5KSWIA&7Z6)vbdctFrz^@fGwhiAx=qN{=c$JUjGoDsU*IHB#fzzyAuI@ zNxD-^)|*KVNEMLmLQnZz+EH=@5>_Q!OUZ?Kh{QsgJZlV-V;1uf;@hBTZzmn%PcE2g zn&ITWnVt)g1tmN@Fw--k-bzGCLJA^Q(w8DpI0dx+NF&#jYBl*r@;Wi|slwt&IpBnW^6IUiN1Bbw_^CQhlX>{cL zs1I|}=*T4rD8Y`H3n%6wa%MXtIYuor!h8T>e zh^n|qrAGgQ<8#iIJlC?$cIVmdO`3AlZPc}-Co;YVOm^&JSLSW7Fnyj)hIV)!*EMZ% zw4?cPYdfhs9`_7x+7aj7LJT+bog39Q|IMX>r)i_6@xjoRovpH^#=jGg&5?7q=DF5& z)|+R&8xX(>e6=o_|+S3t@X*efsu$Z>$K*bei69HDstv_kCyH)dfR*#a-8mtKdGk zdUo~5s`jwsVeLcTBl@8MM>*A4Xxx+5b4{-ha%ZXkWEh9AWk>EhIDk!99((7J><%Fipxi47p`)@s{+WS4O_Uhz6{`DT$_%QqHe%JWREarnj zV)uhVFRfi5no8V@w+Y*W!Eh`T3}PV|oJ~MNObG@xh#Qe+0Khcl&lr7C$9D40j7Db6 zb%fe1m?KzbCt1npEy%LL+m5_KZH*fDvu?5g0|2xt2RNmH{xtX_l zQ%nIC%)Rq=uF;#fdsFOVp(RXT6^X^kA=WlCTK0S#WJ3f75i=l)Q zDcf(|4Ike4G>P4fA0`3iIGEzTwISDm6px+bDZXHL{Au#th;=O|*O)zDvizT10L^8a%ya)Qz=u7| From f5a3ad05eb37aed29135e4d35711bd43e12bea68 Mon Sep 17 00:00:00 2001 From: Ben Robson Date: Tue, 8 Sep 2026 06:49:33 +1000 Subject: [PATCH 28/28] Enhance region protection and validation; add event handlers for environmental griefing and improve shelf cell validation in startup checks --- .gitignore | 4 ++ .../blockvault/chapter/ChapterService.java | 24 +++++++- .../listener/RegionProtectionListener.java | 55 +++++++++++++++++++ .../blockvault/util/StartupValidation.java | 13 +++++ tools/gen_artifacts.py | 7 +++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1188618..21aeec0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ dependency-reduced-pom.xml # Live config holds MySQL credentials - never commit a populated one. # The shipped template lives at src/main/resources/config.yml. /config.yml + +# Python +__pycache__/ +*.pyc diff --git a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java index c86b8eb..44fddcd 100644 --- a/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java +++ b/src/main/java/dev/anchorlight/blockvault/chapter/ChapterService.java @@ -29,6 +29,8 @@ public final class ChapterService { private final BlockVault plugin; private final Set open = Collections.synchronizedSet(new HashSet<>()); private final Set completed = Collections.synchronizedSet(new HashSet<>()); + private final java.util.concurrent.atomic.AtomicBoolean checking = + new java.util.concurrent.atomic.AtomicBoolean(false); private BukkitTask task; public ChapterService(BlockVault plugin) { @@ -62,11 +64,29 @@ public int current() { /** Recompute open state and run any pending unlock ceremonies. Safe to call often. */ public void check() { + // Collapse bursts (e.g. many submits in one tick) into a single pass. + if (!checking.compareAndSet(false, true)) return; plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - List rows = plugin.database().chapters(); - Set collected = plugin.database().collectedSnapshot(); + final List rows; + final Set collected; + final Set doneInDb; + try { + rows = plugin.database().chapters(); + collected = plugin.database().collectedSnapshot(); + doneInDb = plugin.database().completedChapters(); + } finally { + checking.set(false); // DB round-trips done; allow the next pass + } plugin.getServer().getScheduler().runTask(plugin, () -> { + // Re-attempt advancement grants for chapters already complete in the + // database. award() is idempotent, and this covers the case where the + // bundled datapack was not yet loaded when the chapter first completed. + for (int ch : doneInDb) { + completed.add(ch); + plugin.advancements().grantAll(ch); + } + long now = System.currentTimeMillis(); for (Database.ChapterRow row : rows) { int ch = row.chapter(); diff --git a/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java b/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java index 5762166..7150c76 100644 --- a/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java +++ b/src/main/java/dev/anchorlight/blockvault/listener/RegionProtectionListener.java @@ -7,9 +7,16 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockBurnEvent; import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockFromToEvent; +import org.bukkit.event.block.BlockIgniteEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.block.BlockSpreadEvent; import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.hanging.HangingBreakByEntityEvent; @@ -22,6 +29,7 @@ * a wall of donations. */ public final class RegionProtectionListener implements Listener { + private static final String BYPASS = "blockvault.build"; private final BlockVault plugin; @@ -94,6 +102,53 @@ public void onBlockExplode(BlockExplodeEvent e) { e.blockList().removeIf(b -> inside(b.getLocation())); } + // --- environmental griefing ------------------------------------------- + + @EventHandler(ignoreCancelled = true) + public void onEntityChangeBlock(EntityChangeBlockEvent e) { + // Endermen picking up blocks, silverfish infesting, sheep eating grass, + // falling blocks landing, etc. + if (inside(e.getBlock().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onPistonExtend(BlockPistonExtendEvent e) { + if (inside(e.getBlock().getLocation()) + || e.getBlocks().stream().anyMatch(b -> inside(b.getLocation()))) { + e.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onPistonRetract(BlockPistonRetractEvent e) { + if (inside(e.getBlock().getLocation()) + || e.getBlocks().stream().anyMatch(b -> inside(b.getLocation()))) { + e.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onBurn(BlockBurnEvent e) { + if (inside(e.getBlock().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onIgnite(BlockIgniteEvent e) { + if (inside(e.getBlock().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onSpread(BlockSpreadEvent e) { + // Fire spreading, mushrooms/vines growing across the build. + if (inside(e.getBlock().getLocation())) e.setCancelled(true); + } + + @EventHandler(ignoreCancelled = true) + public void onFlow(BlockFromToEvent e) { + // Water/lava flowing into the region. + if (inside(e.getToBlock().getLocation())) e.setCancelled(true); + } + // --- mob spawning -------------------------------------------------- @EventHandler(ignoreCancelled = true) diff --git a/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java b/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java index 26902ba..963ace5 100644 --- a/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java +++ b/src/main/java/dev/anchorlight/blockvault/util/StartupValidation.java @@ -24,6 +24,7 @@ * that the frozen edition does not cover. */ public final class StartupValidation { + /** Blocks that exist in the registry but cannot be obtained in survival (brief section 9). */ private static final Set UNOBTAINABLE = Set.of( "air", "barrier", "bedrock", "budding_amethyst", "chain_command_block", "chorus_plant", @@ -49,6 +50,18 @@ public static void run(BlockVault plugin) { int problems = 0; + // Two blocks must never share a shelf cell - the second head would hide the first. + Map byCell = new java.util.HashMap<>(); + for (TargetEntry e : plugin.manifest().entries().values()) { + String cell = e.sign()[0] + "," + e.sign()[1] + "," + e.sign()[2]; + String prev = byCell.putIfAbsent(cell, e.material()); + if (prev != null) { + log.warning("[validation] shelf " + cell + " is used by both " + + prev + " and " + e.material() + " - one will be unreachable"); + problems++; + } + } + // manifest <-> vault_items.yml for (var entry : manifest.entrySet()) { String r = items.get(entry.getKey()); diff --git a/tools/gen_artifacts.py b/tools/gen_artifacts.py index d07a964..3d6068b 100644 --- a/tools/gen_artifacts.py +++ b/tools/gen_artifacts.py @@ -43,6 +43,7 @@ def parse(path: Path): entries = [] seen = set() + coords = {} for n, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): line = raw.strip() if not line or line.startswith("```") or line.startswith("#"): @@ -58,6 +59,12 @@ def parse(path: Path): raise SystemExit(f"{path}:{n}: duplicate block {block!r}") seen.add(block) sx, sy, sz = int(x), int(y), int(z) + key = (sx, sy, sz) + if key in coords: + raise SystemExit( + f"{path}:{n}: shelf {sx} {sy} {sz} already used by " + f"{coords[key]!r} - two blocks cannot share a slot") + coords[key] = block entries.append({ "block": block, "chapter": int(chap), "rarity": rarity, "section": section,