From 47eb38341fc7e667cdc6d6fbcdbb8f9b131a9117 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 21 Apr 2026 18:05:29 +0200 Subject: [PATCH 1/8] - quick test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5efa817..8e5c5c0 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,4 @@ With this plugin you can plant your Christmas tree and upgrade it. Look for some * **LoneDev6** - *Optimization patches* - [LoneDev6](https://github.com/LoneDev6) * **montlikadani** - *Translation (hu)* - [montlikadani](https://github.com/montlikadani) -See also the list of [contributors](https://github.com/MelonCode/X-Mas/graphs/contributors) who participated in this project. +See also the list of [contributors](https://github.com/MelonCode/X-Mas/graphs/contributors) who participated in this project. From 559cc2e442205add8d013b99653f057da82299c8 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 21 Apr 2026 20:46:45 +0200 Subject: [PATCH 2/8] - Attempt to bump to PaperMC 26.1.2 with Java 25 - Fix potential dupe - Allows items to be returned at any stage of the tree - Gives only the items back that they invested - Move to MiniMessage - Move to 26.1.2 - Better use of materials names - Moved /xmas to legacy alias command - Default modern command: /xmastree - Changed prefix and a few other phrases - Bunch of other fixes --- .gitignore | 14 +- README.md | 258 +++++++++++- build.gradle | 69 ++++ pom.xml | 92 ----- settings.gradle | 15 + src/main/java/ru/meloncode/xmas/Effects.java | 12 +- src/main/java/ru/meloncode/xmas/Events.java | 66 +++- .../java/ru/meloncode/xmas/ItemMaker.java | 26 +- .../java/ru/meloncode/xmas/LocaleManager.java | 25 +- .../java/ru/meloncode/xmas/MagicTree.java | 221 ++++++++--- src/main/java/ru/meloncode/xmas/Main.java | 283 ++++++++++++-- .../ru/meloncode/xmas/ParticleContainer.java | 31 +- .../ru/meloncode/xmas/PlayParticlesTask.java | 2 +- .../ru/meloncode/xmas/TreeSerializer.java | 72 +++- src/main/java/ru/meloncode/xmas/XMas.java | 60 ++- .../java/ru/meloncode/xmas/XMasCommand.java | 366 ++++++++++++++++-- .../xmas/XMasPlaceholderExpansion.java | 44 +++ .../ru/meloncode/xmas/XMasPlaceholders.java | 127 ++++++ .../ru/meloncode/xmas/utils/TextUtils.java | 95 ++++- src/main/resources/config.yml | 172 ++++++-- src/main/resources/locales/default.yml | 37 +- src/main/resources/locales/en.yml | 45 ++- src/main/resources/locales/hu.yml | 21 +- src/main/resources/locales/ru.yml | 19 +- src/main/resources/locales/ru_santa.yml | 15 +- src/main/resources/plugin.yml | 17 +- 26 files changed, 1810 insertions(+), 394 deletions(-) create mode 100644 build.gradle delete mode 100644 pom.xml create mode 100644 settings.gradle create mode 100644 src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java create mode 100644 src/main/java/ru/meloncode/xmas/XMasPlaceholders.java diff --git a/.gitignore b/.gitignore index d1000bd..2b1070e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties +.gradle/ +build/ # IntelliJ project files .idea @@ -17,4 +19,14 @@ gen .project .classpath .settings -bin \ No newline at end of file +bin + +# Local editors and OS files +.DS_Store +Thumbs.db +.vscode/ +*.swp +*.swo + +# Local test servers and generated Paper data +servers/ diff --git a/README.md b/README.md index 8e5c5c0..f45e021 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,254 @@ -# X-Mas +# 1MB X-Mas Tree -![Image](http://puu.sh/dKlK1/85c3dad454.jpg) +Upgradeable Christmas trees for Paper servers. Players plant a magic spruce sapling, feed it resources, grow it through several tree levels, and collect presents that appear under the finished tree. -With this plugin you can plant your Christmas tree and upgrade it. Look for some gifts under it! +This fork keeps the old X-Mas event data usable for winter 2026 while moving the plugin forward for modern Paper and Java runtimes. -[SpigotMC plugin page](https://www.spigotmc.org/resources/x-mas-upgradeable-christmas-tree-event.2672/) +![X-Mas tree preview](http://puu.sh/dKlK1/85c3dad454.jpg) -## Authors +## Current targets -* **MelonCode** - *Original dev* - [MelonCode](https://github.com/MelonCode) -* **Ghost_chu** - *NMS fixes* - [Ghost_chu](https://github.com/Ghost-chu) -* **LoneDev6** - *Optimization patches* - [LoneDev6](https://github.com/LoneDev6) -* **montlikadani** - *Translation (hu)* - [montlikadani](https://github.com/montlikadani) +The Gradle build creates the legacy reference jar and the current Paper 26.1.2 target jar in `build/libs`: -See also the list of [contributors](https://github.com/MelonCode/X-Mas/graphs/contributors) who participated in this project. +| Jar | Purpose | +| --- | --- | +| `1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar` | Legacy reference jar copied from the deployed 2025 server jar. | +| `1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | + +The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the deployed working 2025 behavior can be compared or rolled back during testing. + +## Features + +- Magic Christmas Crystal item for planting event trees. +- Upgradeable spruce tree levels with configurable material requirements. +- Random present spawning under grown trees. +- Configurable present head skins using Mojang texture URLs. +- Configurable gift table using modern material names or saved exact items. +- MiniMessage support for locale strings, crystal display text, and plugin messages. +- Existing `plugins/X-Mas/trees.yml` data remains the event data source. +- Optional resource refunds when a tree is destroyed or cleaned up after the event. +- Configurable per-stage particles using Paper 26.1.2 particle names. +- `/xmastree debug` pages for status, commands, permissions, placeholders, and global boolean toggles. +- Primary `/xmastree` command with an optional legacy `/xmas` alias. +- Optional PlaceholderAPI placeholders for CMI holograms, ajLeaderboards, scoreboards, and menus. +- Legacy `trees.yml` world-name alias support for renamed destination worlds. + +## Installation + +1. Stop the Paper server. +2. Back up the existing `plugins/X-Mas` folder and the world folders. +3. Remove the old X-Mas jar from `plugins` so Paper does not load two copies of the same plugin. +4. Copy the jar for your server target into the server `plugins` folder. +5. Start the server with Java 25. +6. Check the console for XMas Tree startup messages, then run `/xmastree` in game or console. + +For the 2026 target, use the modern Paper 26.1.2 jar: + +- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar` + +## Building + +Requirements: + +- JDK 25 +- Gradle +- The local Paper server folder in `servers/Server-Two-Paper-26.1.2` +- The local PlaceholderAPI jar in `servers/Server-Two-Paper-26.1.2/plugins` +- The deployed legacy jar in `servers/Server-One-Paper-1.21.11/plugins` if you want `legacyJar` + +Build the current Paper 26.1.2 jar and the legacy reference jar: + +```bash +gradle clean buildAllJars +``` + +Build only the Paper 26.1.2 jar: + +```bash +gradle jar +``` + +The `paper2612Jar` task is kept as an alias: + +```bash +gradle paper2612Jar +``` + +Copy the deployed legacy jar into the requested legacy filename: + +```bash +gradle legacyJar +``` + +The build compiles against the Paper 26.1.2 API jars found in `servers/Server-Two-Paper-26.1.2`. If that folder is missing or has not been started far enough for Paper to download its libraries, Gradle will not have the Paper API classpath it needs. + +## Commands + +The primary command is `/xmastree`. + +If `core.commands.legacy-command-enabled` is `true`, the legacy `/xmas` alias is also registered. + +| Command | Description | +| --- | --- | +| `/xmastree` | Shows plugin version, event status, auto-end status, resource-back status, tree count, and owner count. | +| `/xmastree help` | Shows the command list. | +| `/xmastree give ` | Gives an online player a Christmas Crystal. | +| `/xmastree gifts` | Spawns a small batch of presents under every loaded Christmas tree. | +| `/xmastree addhand` | Adds the item in your main hand to the gift list and saves it to `config.yml`. | +| `/xmastree reload` | Reloads config, locale, present heads, gifts, luck settings, command alias settings, and tree level requirements. | +| `/xmastree debug [page]` | Shows paginated status, commands, permissions, placeholders, and toggleable global config keys. | +| `/xmastree debug toggle true\|false` | Toggles supported global boolean config keys and reloads the plugin config. | +| `/xmastree end` | Ends the event and sets `core.plugin-enabled` to `false`. | + +## Permissions + +| Permission | Default | Description | +| --- | --- | --- | +| `xmas.admin` | `op` | Allows use of the `/xmastree` command and all XMas Tree admin subcommands. | + +## Player flow + +Players can receive a Christmas Crystal from an admin, or craft one with diamonds around an emerald in a cross shape. Right-click a spruce sapling with the crystal to create a magic tree. + +After planting, players right-click the tree with the configured level-up materials. The requirement header is short in chat and includes a hover hint that explains the ingredients must be fed into the tree by right-clicking while holding them. When all requirements for a level are complete, right-click the tree again to grow it. Presents spawn around grown trees while the event is enabled. + +If `core.holiday-ends.resource-back` is enabled, confirmed tree destruction returns the upgrade materials that were actually spent on the tree. The plugin tries to place a chest first, then a barrel, then the player's inventory, and finally drops any overflow at the tree location. + +Ingredient accept sounds can be tuned live in `config.yml` under `core.sounds.grow`. Use `0.0` for silent, `0.1` for quiet, `0.5` for half volume, and `1.0` for full volume. `/xmastree reload` applies the new values without a server restart. + +## Configuration + +The plugin writes its files to `plugins/X-Mas`: + +- `config.yml` controls event timing, locale, tree limits, gift cooldowns, present skins, gift items, and level-up requirements. +- `trees.yml` stores placed tree data and should be kept when upgrading an existing event. +- `locales/*.yml` controls player-facing messages and crystal display text. + +Use modern Paper/Bukkit material names such as `GOLD_INGOT`, `SPRUCE_LOG`, and `PLAYER_HEAD`. Legacy numeric IDs and old material names are skipped to avoid modern Paper exceptions. + +Gift entries in `xmas.gifts` can be simple material names: + +```yaml +- DIAMOND +- EMERALD:3 +``` + +Admins can also hold an item and run `/xmastree addhand`. This saves the exact item as Base64 so custom names, lore, enchantments, and metadata can be used as gifts. + +Legacy world-name remapping lives under `migration.world-aliases`. This is useful when an old `trees.yml` was saved in worlds like `general`, `wild`, or `santa`, but the new Paper 26.1.2 server uses different world names: + +```yaml +migration: + world-aliases: + general: world + wild: world + santa: santa_event +``` + +The saved coordinates are preserved. If the destination world terrain or world border changed, some legacy tree locations may still need manual cleanup. + +Present head entries in `xmas.presents` should use `textures.minecraft.net` URLs. Old player-name entries are still accepted for compatibility. + +Per-stage particles live under `xmas.tree-lvl..particles`. Particle names should come from the Paper 26.1.2 `Particle` enum: + +[jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html](https://jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html) + +The config currently supports simple particles and `DUST`. + +## MiniMessage + +Locale messages, crystal names, crystal lore, and command messages support MiniMessage: + +```yaml +crystal: + name: Christmas Crystal + lore: + - Concentrated Christmas Spirit + - Use it on a spruce sapling to fill it with magic! +``` + +Legacy `&` color codes are still parsed for compatibility when a message does not contain MiniMessage tags. + +## Placeholders + +PlaceholderAPI is optional. If PlaceholderAPI is installed, X-Mas registers the `onembxmastree` expansion. + +PlaceholderAPI requires an underscore after the expansion identifier, so use: + +```text +%onembxmastree_event.active% +``` + +The dotted key after `onembxmastree_` is supported to keep the placeholders readable and namespaced. Underscore variants also work, for example `%onembxmastree_event_active%`. + +| Placeholder | Example output | Description | +| --- | --- | --- | +| `%onembxmastree_event.active%` | `true` | Whether the event is currently active. | +| `%onembxmastree_event.active_text%` | `Active` | Human-readable active/inactive state. | +| `%onembxmastree_event.status%` | `In Progress` | Current event status text. | +| `%onembxmastree_event.starts_at%` | `manual` | Start mode. The plugin currently starts from `core.plugin-enabled`, not a scheduled start date. | +| `%onembxmastree_event.ends_at%` | `10-01-2027 03-33-33` | Configured event end date. | +| `%onembxmastree_event.ends_in%` | `263d 7h` | Approximate time until the configured end date, or `disabled` when auto-end is off. | +| `%onembxmastree_event.ends_timestamp%` | `1799552013000` | Event end timestamp in milliseconds. | +| `%onembxmastree_event.auto_end%` | `true` | Whether automatic event ending is enabled. | +| `%onembxmastree_resource.back%` | `true` | Whether resource refunds are enabled. | +| `%onembxmastree_resource.back_text%` | `Yes` | Human-readable refund state. | +| `%onembxmastree_particles.enabled%` | `true` | Whether X-Mas particles are globally enabled. | +| `%onembxmastree_luck.enabled%` | `false` | Whether gift luck chance is enabled. | +| `%onembxmastree_luck.chance%` | `75` | Gift luck chance as a percent. | +| `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | +| `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | +| `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | +| `%onembxmastree_version%` | `2.0.1-010` | Loaded plugin version. | + +CMI hologram example: + +```text +&aX-Mas Event: &f%onembxmastree_event.active_text% +&aEnds in: &f%onembxmastree_event.ends_in% +&aResource back: &f%onembxmastree_resource.back_text% +&aTrees planted: &f%onembxmastree_trees.total% +``` + +ajLeaderboards placeholder examples: + +```text +%onembxmastree_trees.total% +%onembxmastree_trees.owners% +%onembxmastree_player.trees% +``` + +## Compatibility notes + +- Back up `plugins/X-Mas/trees.yml` before upgrading a live server. +- Existing tree records are loaded from the same `trees.yml` format. +- When saved world names no longer match the current server world names, `migration.world-aliases` can remap them without rewriting `trees.yml`. +- Existing present head player-name entries are still accepted, but new configs should prefer Mojang texture URLs. +- The modern jars are compiled with Java 25 bytecode and should be run on Java 25. +- The Paper 26.1.2 jar is the intended winter 2026 target. Paper 1.21.11 compatibility is no longer part of the active test path. + +## Security notes + +- Admin commands are gated by `xmas.admin`. +- Present texture URLs are restricted to `textures.minecraft.net`. +- Gift item Base64 entries are capped before deserialization. +- Config material names are resolved with modern `Material.matchMaterial` and invalid or legacy values are skipped. +- Treat `config.yml` and locale files as trusted admin-controlled files, especially when using MiniMessage click or hover tags. + +## Support + +Please report bugs, compatibility problems, and upgrade questions in the GitHub issues section: + +[github.com/mrfdev/XMasTree/issues](https://github.com/mrfdev/XMasTree/issues) + +## Credits + +- **MelonCode** - Original developer - [MelonCode](https://github.com/MelonCode) +- **Ghost_chu** - NMS fixes - [Ghost-chu](https://github.com/Ghost-chu) +- **LoneDev6** - Optimization patches - [LoneDev6](https://github.com/LoneDev6) +- **montlikadani** - Hungarian translation - [montlikadani](https://github.com/montlikadani) +- **1MB / mrfdev** - 2026 Paper modernization, Java 25 builds, and XMasTree maintenance + +Original SpigotMC listing: + +[spigotmc.org/resources/x-mas-upgradeable-christmas-tree-event.2672](https://www.spigotmc.org/resources/x-mas-upgradeable-christmas-tree-event.2672/) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..70258e1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'java' +} + +group = 'com.onemb.xmas' +version = '2.0.1-010' + +def legacyArchiveName = '1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar' +def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar' + +def serverOne = layout.projectDirectory.dir('servers/Server-One-Paper-1.21.11') +def serverTwo = layout.projectDirectory.dir('servers/Server-Two-Paper-26.1.2') +def legacyServerJar = serverOne.file('plugins/1MB-X-Mas_2025-1.21.8.jar') +def placeholderApiJar = serverTwo.file('plugins/PlaceholderAPI-2.12.3-DEV-265.jar') + +def paper2612Api = serverTwo.file('libraries/io/papermc/paper/paper-api/26.1.2.build.18-alpha/paper-api-26.1.2.build.18-alpha.jar') + +def paper2612Classpath = files(paper2612Api) + fileTree(dir: serverTwo.dir('libraries').asFile, include: '**/*.jar') + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +dependencies { + compileOnly paper2612Classpath + compileOnly files(placeholderApiJar) +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 25 +} + +tasks.named('processResources') { + filteringCharset = 'UTF-8' + filesMatching('plugin.yml') { + expand(version: project.version) + } +} + +tasks.named('jar') { + archiveFileName = paper2612ArchiveName +} + +tasks.register('paper2612Jar') { + description = 'Assembles the Paper 26.1.2 Java 25 plugin jar.' + group = 'build' + dependsOn tasks.named('jar') +} + +tasks.register('legacyJar', Copy) { + description = 'Copies the deployed legacy jar into the requested 2026 legacy filename.' + group = 'build' + from(legacyServerJar) + into(layout.buildDirectory.dir('libs')) + rename { legacyArchiveName } +} + +tasks.register('buildAllJars') { + description = 'Builds the legacy reference jar plus the modern Paper 26.1.2 target jar.' + group = 'build' + dependsOn tasks.named('legacyJar'), tasks.named('jar') +} + +tasks.named('assemble') { + dependsOn tasks.named('legacyJar'), tasks.named('paper2612Jar') +} diff --git a/pom.xml b/pom.xml deleted file mode 100644 index b894d55..0000000 --- a/pom.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - 4.0.0 - - me.meloncode - xmas - 2.5 - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - bungeecord-repo - https://oss.sonatype.org/content/repositories/snapshots - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.8 - 1.8 - UTF-8 - - - - maven-shade-plugin - - false - - - - package - - shade - - - - - - - - true - src/main/resources - - - - - - - - org.spigotmc - spigot-api - 1.15-R0.1-SNAPSHOT - provided - - - - org.bukkit - bukkit - 1.15-R0.1-SNAPSHOT - provided - - - - net.md-5 - bungeecord-api - 1.15-SNAPSHOT - - - - org.apache.commons - commons-lang3 - 3.0 - - - org.jetbrains - annotations - 17.0.0 - compile - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..192ee77 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + } +} + +rootProject.name = '1MB-XMas' diff --git a/src/main/java/ru/meloncode/xmas/Effects.java b/src/main/java/ru/meloncode/xmas/Effects.java index 0270af3..b7a7b31 100644 --- a/src/main/java/ru/meloncode/xmas/Effects.java +++ b/src/main/java/ru/meloncode/xmas/Effects.java @@ -6,15 +6,15 @@ class Effects { public static final ParticleContainer AMBIENT_SAPLING = new ParticleContainer(Particle.PORTAL, 0.2f, 0.25f, 0.2f, 0.1f, 16); public static final ParticleContainer AMBIENT_PORTAL = new ParticleContainer(Particle.PORTAL, 2f, 2f, 2f, 0.1f, 16); - public static final ParticleContainer TREE_SWAG = new ParticleContainer(Particle.REDSTONE, 0.25f, 0.25f, 0.25f, 10f, 16); + public static final ParticleContainer TREE_SWAG = new ParticleContainer(Particle.DUST, 0.25f, 0.25f, 0.25f, 10f, 16); public static final ParticleContainer TREE_HEARTS_AMBIENT = new ParticleContainer(Particle.HEART, 1.25f, 1.25f, 1.25f, 10f, 1); - public static final ParticleContainer TREE_RED_SWAG = new ParticleContainer(Particle.REDSTONE, 0.25f, 0.25f, 0.25f, 0f, 16); - public static final ParticleContainer TREE_WHITE_AMBIENT = new ParticleContainer(Particle.FIREWORKS_SPARK, 2.25f, 2.25f, 2.25f, 0f, 4); + public static final ParticleContainer TREE_RED_SWAG = new ParticleContainer(Particle.DUST, 0.25f, 0.25f, 0.25f, 0f, 16); + public static final ParticleContainer TREE_WHITE_AMBIENT = new ParticleContainer(Particle.FIREWORK, 2.25f, 2.25f, 2.25f, 0f, 4); public static final ParticleContainer TREE_CRIT_SWAG = new ParticleContainer(Particle.CRIT, 0.25f, 0.25f, 0.25f, 0f, 16); public static final ParticleContainer TREE_GOLD_SWAG = new ParticleContainer(Particle.FLAME, 0.25f, 0.25f, 0.25f, 0f, 16); - public static final ParticleContainer SMOKE = new ParticleContainer(Particle.SMOKE_LARGE, 0f, 0f, 0f, 0f, 16); + public static final ParticleContainer SMOKE = new ParticleContainer(Particle.LARGE_SMOKE, 0f, 0f, 0f, 0f, 16); - public static final ParticleContainer GROW = new ParticleContainer(Particle.VILLAGER_HAPPY, 0.25f, 0.25f, 0.25f, 1f, 16); - public static final ParticleContainer AMBIENT_SNOW = new ParticleContainer(Particle.SNOW_SHOVEL, 1.5f, 3f, 1.5f, 0, 16); + public static final ParticleContainer GROW = new ParticleContainer(Particle.HAPPY_VILLAGER, 0.25f, 0.25f, 0.25f, 1f, 16); + public static final ParticleContainer AMBIENT_SNOW = new ParticleContainer(Particle.ITEM_SNOWBALL, 1.5f, 3f, 1.5f, 0, 16); } diff --git a/src/main/java/ru/meloncode/xmas/Events.java b/src/main/java/ru/meloncode/xmas/Events.java index 5ce427c..e0d3b60 100644 --- a/src/main/java/ru/meloncode/xmas/Events.java +++ b/src/main/java/ru/meloncode/xmas/Events.java @@ -19,7 +19,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; -import org.jetbrains.annotations.Nullable; +import org.bukkit.persistence.PersistentDataType; import ru.meloncode.xmas.utils.TextUtils; import java.util.Collection; @@ -70,9 +70,15 @@ public void onPlayerClickBlock(PlayerInteractEvent event) { Player player = event.getPlayer(); if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { Block block = event.getClickedBlock(); + if (block == null) { + return; + } if (MagicTree.isBlockBelongs(block)) { event.setCancelled(true); MagicTree tree = MagicTree.getTreeByBlock(block); + if (tree == null) { + return; + } if (Main.inProgress) { if (tree.getLevel().hasNext()) { if (tree.canLevelUp()) { @@ -95,7 +101,7 @@ public void onPlayerClickBlock(PlayerInteractEvent event) { } if (tree.level.nextLevel != null) { TextUtils.sendMessage(player, LocaleManager.GROW_LVL_PROGRESS); - for (String line : TextUtils.generateChatReqList(tree)) { + for (var line : TextUtils.generateChatReqList(tree)) { TextUtils.sendMessage(player, line); } @@ -109,7 +115,7 @@ public void onPlayerClickBlock(PlayerInteractEvent event) { } } else { if (player.getUniqueId().equals(tree.getOwner())) { - tree.end(); + tree.end(player); TextUtils.sendMessage(player, LocaleManager.TIMEOUT); } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); @@ -123,7 +129,7 @@ public void onPlayerClickBlock(PlayerInteractEvent event) { if (XMas.getTreesPlayerOwn(player).size() < Main.MAX_TREE_COUNT) { if (is.getType() == XMas.XMAS_CRYSTAL.getType() && is.hasItemMeta() && is.getItemMeta().hasLore()) { ItemMeta im = is.getItemMeta(); - if (im.getLore().equals(XMas.XMAS_CRYSTAL.getItemMeta().getLore())) { + if (im != null && im.getPersistentDataContainer().has(Main.getCrystalKey(), PersistentDataType.BYTE)) { if (player.getGameMode() != GameMode.CREATIVE) { if (is.getAmount() > 1) { is.setAmount(is.getAmount() - 1); @@ -168,7 +174,8 @@ public void onItemSpawn(ItemSpawnEvent event) { ItemStack item = event.getEntity().getItemStack(); if (item.getType() == Material.PLAYER_HEAD) { SkullMeta meta = (SkullMeta) item.getItemMeta(); - if (meta.getOwner() != null && Main.getHeads().contains(meta.getOwner())) { + String headId = getHeadIdentifier(meta); + if (headId != null && Main.getHeads().contains(headId)) { event.setCancelled(true); } } @@ -213,17 +220,24 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { if (Main.inProgress) if (destroyers.containsKey(player.getUniqueId()) && System.currentTimeMillis() - destroyers.get(player.getUniqueId()) <= 10000) { - XMas.removeTree(tree); + if (Main.resourceBack) { + tree.end(player); + } else { + XMas.removeTree(tree); + } if (Main.inProgress) - TextUtils.sendMessage(player, ChatColor.DARK_RED + LocaleManager.MONSTER); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_COMPLETE); } else { destroyers.put(player.getUniqueId(), System.currentTimeMillis()); if (Main.inProgress) - TextUtils.sendMessage(player, ChatColor.RED + LocaleManager.DESTROY_WARNING); - TextUtils.sendMessage(player, ChatColor.DARK_RED + LocaleManager.DESTROY_TUT); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_WARNING); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_TUT); + if (Main.resourceBack) { + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_RESOURCE_BACK); + } } else { - tree.end(); + tree.end(player); } } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); @@ -232,9 +246,9 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { case SPRUCE_LEAVES: case GLOWSTONE: if (Main.inProgress) - TextUtils.sendMessage(player, ChatColor.DARK_GREEN + LocaleManager.DESTROY_LEAVES_SANTA); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_SANTA); if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { - TextUtils.sendMessage(player, ChatColor.RED + LocaleManager.DESTROY_LEAVES_TUT); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_TUT); } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); } @@ -243,16 +257,23 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { if (Main.inProgress) { if (destroyers.containsKey(player.getUniqueId()) && System.currentTimeMillis() - destroyers.get(player.getUniqueId()) <= 10000) { - XMas.removeTree(tree); - TextUtils.sendMessage(player, ChatColor.RED + LocaleManager.MONSTER); + if (Main.resourceBack) { + tree.end(player); + } else { + XMas.removeTree(tree); + } + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_COMPLETE); } else { destroyers.put(player.getUniqueId(), System.currentTimeMillis()); if (Main.inProgress) - TextUtils.sendMessage(player, ChatColor.RED + LocaleManager.DESTROY_SAPLING); - TextUtils.sendMessage(player, ChatColor.DARK_RED + LocaleManager.DESTROY_TUT); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_SAPLING); + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_TUT); + if (Main.resourceBack) { + TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_RESOURCE_BACK); + } } } else { - tree.end(); + tree.end(player); } } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); @@ -290,7 +311,7 @@ public void disableDecay(LeavesDecayEvent e) @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) private void disableFireworkDamage(EntityDamageByEntityEvent e) { - if (e.getDamager().getType() == EntityType.FIREWORK) { + if (e.getDamager().getType() == EntityType.FIREWORK_ROCKET) { if (e.getDamager().hasMetadata("nodamage")) { e.setCancelled(true); } @@ -309,4 +330,13 @@ private void chunkLoad(ChunkLoadEvent e) tree.spawnScheduledPresents(); } } + + private String getHeadIdentifier(SkullMeta meta) { + if (meta.getOwnerProfile() != null + && meta.getOwnerProfile().getTextures() != null + && meta.getOwnerProfile().getTextures().getSkin() != null) { + return meta.getOwnerProfile().getTextures().getSkin().toString(); + } + return meta.getOwner(); + } } diff --git a/src/main/java/ru/meloncode/xmas/ItemMaker.java b/src/main/java/ru/meloncode/xmas/ItemMaker.java index 444639f..eabe9c8 100644 --- a/src/main/java/ru/meloncode/xmas/ItemMaker.java +++ b/src/main/java/ru/meloncode/xmas/ItemMaker.java @@ -2,12 +2,13 @@ //I plan to make this plugin bigger. So... -import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; +import net.kyori.adventure.text.format.TextDecoration; +import ru.meloncode.xmas.utils.TextUtils; import java.util.ArrayList; import java.util.List; @@ -25,14 +26,14 @@ public ItemMaker(Material material) { public ItemMaker(Material material, String name) { is = new ItemStack(material); im = is.getItemMeta(); - im.setDisplayName(name); + im.displayName(TextUtils.parse(name).decoration(TextDecoration.ITALIC, false)); } public ItemMaker(Material material, String name, List lore) { is = new ItemStack(material); im = is.getItemMeta(); - im.setDisplayName(name); - im.setLore(lore); + im.displayName(TextUtils.parse(name).decoration(TextDecoration.ITALIC, false)); + im.lore(TextUtils.parseList(lore)); } public ItemMaker(Material material, int amount, short durability) { @@ -43,14 +44,14 @@ public ItemMaker(Material material, int amount, short durability) { public ItemMaker(Material material, int amount, short durability, String name) { is = new ItemStack(material, amount); im = is.getItemMeta(); - im.setDisplayName(name); + im.displayName(TextUtils.parse(name).decoration(TextDecoration.ITALIC, false)); } public ItemMaker(Material material, int amount, short durability, String name, List lore) { is = new ItemStack(material, amount); im = is.getItemMeta(); - im.setDisplayName(name); - im.setLore(lore); + im.displayName(TextUtils.parse(name).decoration(TextDecoration.ITALIC, false)); + im.lore(TextUtils.parseList(lore)); } public ItemMaker setAmount(int amount) { @@ -59,18 +60,19 @@ public ItemMaker setAmount(int amount) { } public ItemMaker setDurability(short data) { - //is.setDurability(data); - ((Damageable) is).setDamage(data); + if (im instanceof Damageable damageable) { + damageable.setDamage(data); + } return this; } public ItemMaker setName(String name) { - im.setDisplayName(ChatColor.RESET + name); + im.displayName(TextUtils.parse("" + name).decoration(TextDecoration.ITALIC, false)); return this; } public ItemMaker setLore(List lore) { - im.setLore(lore); + im.lore(TextUtils.parseList(lore)); return this; } @@ -82,7 +84,7 @@ public ItemMaker addLoreLine(String line) { lore = new ArrayList<>(); } lore.add(line); - im.setLore(lore); + im.lore(TextUtils.parseList(lore)); return this; } diff --git a/src/main/java/ru/meloncode/xmas/LocaleManager.java b/src/main/java/ru/meloncode/xmas/LocaleManager.java index 889d840..91cd990 100644 --- a/src/main/java/ru/meloncode/xmas/LocaleManager.java +++ b/src/main/java/ru/meloncode/xmas/LocaleManager.java @@ -1,6 +1,5 @@ package ru.meloncode.xmas; -import org.bukkit.ChatColor; import org.bukkit.configuration.file.FileConfiguration; import ru.meloncode.xmas.utils.ConfigUtils; import ru.meloncode.xmas.utils.TextUtils; @@ -18,19 +17,21 @@ public class LocaleManager { public static String GROW_LVL_READY; public static String GROW_LEVEL_MAX; public static String GROW_REQ_LIST_TITLE; + public static String GROW_REQ_LIST_HINT; public static String GROW_NOT_ENOUGH_PLACE; public static String TREE_LIMIT; public static String DESTROY_SAPLING; public static String DESTROY_LEAVES_SANTA; public static String DESTROY_LEAVES_TUT; public static String DESTROY_WARNING; + public static String DESTROY_RESOURCE_BACK; public static String DESTROY_FAIL_OWNER; public static String DESTROY_TUT; + public static String DESTROY_COMPLETE; public static String CRYSTAL_NAME; public static List CRYSTAL_LORE; public static String GIFT_LUCK; public static String GIFT_FAIL; - public static String MONSTER; public static String TIMEOUT; public static String HAPPY_NEW_YEAR; @@ -48,7 +49,7 @@ public static void loadLocale(String lang) { locale = def_locale; } else { locale = ConfigUtils.loadConfig(file); - TextUtils.sendConsoleMessage("Locale '" + lang + "' successfuly loaded"); + TextUtils.sendConsoleMessage("Locale '" + lang + "' successfully loaded"); } loadStrings(); } @@ -60,16 +61,18 @@ private static void loadStrings() { GROW_LVL_READY = getString("messages.tree.grow-lvl-ready"); GROW_LEVEL_MAX = getString("messages.tree.grow-lvl-max"); GROW_REQ_LIST_TITLE = getString("messages.tree.grow-req-list-title"); + GROW_REQ_LIST_HINT = getString("messages.tree.grow-req-list-hint"); GROW_NOT_ENOUGH_PLACE = getString("messages.tree.grow-not-enough-place"); TREE_LIMIT = getString("messages.tree.tree-limit"); DESTROY_SAPLING = getString("messages.tree.destroy-sapling"); DESTROY_LEAVES_SANTA = getString("messages.tree.destroy-leaves-santa"); DESTROY_LEAVES_TUT = getString("messages.tree.destroy-leaves-tut"); - MONSTER = getString("messages.tree.destroy-complete"); DESTROY_WARNING = getString("messages.tree.destroy-warning"); + DESTROY_RESOURCE_BACK = getString("messages.tree.destroy-resource-back"); DESTROY_TUT = getString("messages.tree.destroy-tut"); + DESTROY_COMPLETE = getString("messages.tree.destroy-complete"); DESTROY_FAIL_OWNER = getString("messages.tree.destroy-fail-owner"); - CRYSTAL_NAME = ChatColor.GREEN + getString("crystal.name"); + CRYSTAL_NAME = getString("crystal.name"); CRYSTAL_LORE = getStringList("crystal.lore"); GIFT_LUCK = getString("messages.gift.luck-message"); GIFT_FAIL = getString("messages.gift.unluck-message"); @@ -88,11 +91,11 @@ private static String getString(String path) { throw new NullPointerException("Locale not loaded"); try { - String message = ChatColor.translateAlternateColorCodes('&', locale.getString(path)); + String message = locale.getString(path); return message.contains("_UNUSED") ? null : message; } catch (NullPointerException e) { - TextUtils.sendConsoleMessage(ChatColor.DARK_RED + "Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); - TextUtils.sendConsoleMessage(ChatColor.DARK_RED + "Using default locale to get value"); + TextUtils.sendConsoleMessage("Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); + TextUtils.sendConsoleMessage("Using default locale to get value"); return def_locale.getString(path); } } @@ -105,13 +108,13 @@ private static List getStringList(String path) { List raw = locale.getStringList(path); List list = new ArrayList<>(); for (String s : raw) { - list.add(ChatColor.translateAlternateColorCodes('&', s)); + list.add(s); } return list; } catch (IllegalArgumentException e) { - TextUtils.sendConsoleMessage(ChatColor.DARK_RED + "Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); - TextUtils.sendConsoleMessage(ChatColor.DARK_RED + "Using default locale to get value"); + TextUtils.sendConsoleMessage("Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); + TextUtils.sendConsoleMessage("Using default locale to get value"); return def_locale.getStringList(path); } diff --git a/src/main/java/ru/meloncode/xmas/MagicTree.java b/src/main/java/ru/meloncode/xmas/MagicTree.java index da86ce7..4dc8f10 100644 --- a/src/main/java/ru/meloncode/xmas/MagicTree.java +++ b/src/main/java/ru/meloncode/xmas/MagicTree.java @@ -10,8 +10,13 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.FireworkMeta; import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.profile.PlayerProfile; +import org.bukkit.profile.PlayerTextures; +import ru.meloncode.xmas.utils.TextUtils; import org.bukkit.util.Vector; +import java.net.MalformedURLException; +import java.net.URL; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -54,10 +59,16 @@ public MagicTree(UUID owner, UUID uid, TreeLevel level, Location location, Map getLevelupRequirements() { public boolean grow(Material material) { if (levelupRequirements.containsKey(material)) { + int levelRequirement = level.getLevelupRequirements().getOrDefault(material, levelupRequirements.get(material)); + int remainingBefore = levelupRequirements.get(material); + float volume = levelRequirement == remainingBefore ? Main.growFirstSoundVolume : Main.growRepeatSoundVolume; if (levelupRequirements.get(material) <= 1) { levelupRequirements.remove(material); } else { @@ -94,10 +108,11 @@ public boolean grow(Material material) { for (Block block : blocks) { if (block.getType() == Material.SPRUCE_LEAVES || block.getType() == Material.SPRUCE_SAPLING) { Effects.GROW.playEffect(block.getLocation()); - for (int i = 0; i <= 3; i++) - location.getWorld().playSound(location, Sound.ENTITY_PLAYER_LEVELUP, 1, Main.RANDOM.nextFloat() + 0.2f); } } + if (volume > 0) { + location.getWorld().playSound(location, Sound.ENTITY_PLAYER_LEVELUP, volume, Main.RANDOM.nextFloat() + 0.2f); + } save(); return true; } @@ -120,7 +135,7 @@ public void playParticles() { if (blocks != null && blocks.size() > 0) { for (Block block : blocks) { - if(!block.getWorld().isChunkLoaded(block.getX() / 16, block.getZ() / 16)) + if (!block.getChunk().isLoaded()) continue; if (block.getType() == Material.SPRUCE_LEAVES) { if (level.getSwagEffect() != null) { @@ -179,9 +194,13 @@ public void unbuild() { } } location.clone().add(0, -1, 0).getBlock().setType(Material.GRASS_BLOCK); + blocks = null; } public void build() { + if (blocks != null && !blocks.isEmpty()) { + return; + } if (level.getStructureTemplate().canGrow(location)) { blocks = level.getStructureTemplate().build(location); for (Block block : blocks) { @@ -192,7 +211,7 @@ public void build() { @SuppressWarnings("deprecation") public void spawnPresent() { - if(!location.getWorld().isChunkLoaded((int)location.getX() / 16, (int)location.getZ() / 16)) + if (!location.getChunk().isLoaded()) { if(scheduledPresents + 1 <= 8) scheduledPresents++; @@ -216,15 +235,41 @@ public void spawnPresent() { //skull.setRotation(face); Rotatable skullRotatable = (Rotatable) skull.getBlockData(); skullRotatable.setRotation(face); + skull.setRotation(face); //skull.setSkullType(SkullType.PLAYER); skull.setType(Material.PLAYER_HEAD); //skull.setOwner(); - skull.setOwningPlayer(Bukkit.getOfflinePlayer(Main.getHeads().get(Main.RANDOM.nextInt(Main.getHeads().size())))); + applyConfiguredHead(skull, Main.getHeads().get(Main.RANDOM.nextInt(Main.getHeads().size()))); skull.update(true); } } } + private void applyConfiguredHead(Skull skull, String configuredHead) { + if (configuredHead == null || configuredHead.trim().isEmpty()) { + return; + } + String trimmedHead = configuredHead.trim(); + if (!trimmedHead.contains("://")) { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(trimmedHead)); + return; + } + try { + URL skinUrl = new URL(trimmedHead); + if (!"textures.minecraft.net".equalsIgnoreCase(skinUrl.getHost())) { + Bukkit.getLogger().warning("[X-Mas] Ignoring non-Mojang present skin URL: " + trimmedHead); + return; + } + PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID()); + PlayerTextures textures = profile.getTextures(); + textures.setSkin(skinUrl); + profile.setTextures(textures); + skull.setOwnerProfile(profile); + } catch (MalformedURLException e) { + Bukkit.getLogger().warning("[X-Mas] Invalid present skin URL: " + trimmedHead); + } + } + public boolean canLevelUp() { return getLevelupRequirements().size() == 0; } @@ -238,58 +283,138 @@ public void save() { } public void end() { + end(getPlayerOwner()); + } + + public void end(Player refundTarget) { unbuild(); - // Bad code. Need it fast. - Block bl; - if ((bl = location.clone().add(1, 0, 0).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - if ((bl = location.clone().add(-1, 0, 0).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - if ((bl = location.clone().add(0, 0, 1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - if ((bl = location.clone().add(0, 0, -1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - - if ((bl = location.clone().add(1, 0, 1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - if ((bl = location.clone().add(-1, 0, -1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - - if ((bl = location.clone().add(-1, 0, 1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); - if ((bl = location.clone().add(1, 0, -1).getBlock()).getType() == Material.PLAYER_HEAD) - bl.setType(Material.AIR); + clearNearbyPresents(); if (Main.resourceBack) { - bl = location.getBlock(); - bl.setType(Material.CHEST); - Chest chest = (Chest) bl.getState(); - Inventory inv = chest.getInventory(); - - inv.addItem(new ItemStack(Material.DIAMOND, 4)); - inv.addItem(new ItemStack(Material.EMERALD, 1)); - TreeLevel cLevel = TreeLevel.SAPLING; - while (cLevel != level) { - if (cLevel.getLevelupRequirements() != null && cLevel.getLevelupRequirements().size() > 0) { - for (Entry currItem : cLevel.getLevelupRequirements().entrySet()) { - inv.addItem(new ItemStack(currItem.getKey(), currItem.getValue())); - } + refundResources(refundTarget); + } + XMas.removeTree(this, false); + } + + private void clearNearbyPresents() { + Block bl; + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + if (x == 0 && z == 0) { + continue; + } + bl = location.clone().add(x, 0, z).getBlock(); + if (bl.getType() == Material.PLAYER_HEAD) { + bl.setType(Material.AIR); } + } + } + } + + private void refundResources(Player refundTarget) { + List refundItems = collectRefundItems(); + if (refundItems.isEmpty()) { + return; + } + + List leftovers = putRefundsInContainer(Material.CHEST, refundItems); + if (leftovers != null) { + dropRefunds(leftovers); + notifyRefund(refundTarget, "Your tree resources were returned in a chest."); + return; + } + + leftovers = putRefundsInContainer(Material.BARREL, refundItems); + if (leftovers != null) { + dropRefunds(leftovers); + notifyRefund(refundTarget, "Your tree resources were returned in a barrel."); + return; + } - if (cLevel.nextLevel == null) - break; - cLevel = cLevel.nextLevel; + leftovers = refundTarget != null ? addItems(refundTarget.getInventory(), refundItems) : refundItems; + dropRefunds(leftovers); + if (refundTarget != null) { + if (leftovers.isEmpty()) { + notifyRefund(refundTarget, "Your tree resources were returned to your inventory."); + } else { + notifyRefund(refundTarget, "Your tree resources were returned. Inventory overflow dropped at the tree."); } + } + } - int count = 0; + private List collectRefundItems() { + List refundItems = new ArrayList<>(); + + TreeLevel cLevel = TreeLevel.SAPLING; + while (cLevel != null && cLevel != level) { + addRequirements(refundItems, cLevel.getLevelupRequirements()); + cLevel = cLevel.nextLevel; + } + + if (level.getLevelupRequirements() != null) { for (Entry currItem : level.getLevelupRequirements().entrySet()) { - if (getLevelupRequirements().containsKey(currItem.getKey())) - count = getLevelupRequirements().get(currItem.getKey()); - if (currItem.getValue() - count > 0) - inv.addItem(new ItemStack(currItem.getKey(), currItem.getValue() - count)); - count = 0; + int remaining = getLevelupRequirements().getOrDefault(currItem.getKey(), 0); + int spent = Math.max(0, currItem.getValue() - remaining); + if (spent > 0) { + refundItems.add(new ItemStack(currItem.getKey(), spent)); + } } } - XMas.removeTree(this); + return refundItems; + } + + private void addRequirements(List refundItems, Map requirements) { + if (requirements == null || requirements.isEmpty()) { + return; + } + for (Entry currItem : requirements.entrySet()) { + if (currItem.getValue() > 0) { + refundItems.add(new ItemStack(currItem.getKey(), currItem.getValue())); + } + } + } + + private List putRefundsInContainer(Material containerMaterial, List refundItems) { + Block refundBlock = location.getBlock(); + try { + refundBlock.setType(containerMaterial, false); + BlockState state = refundBlock.getState(); + if (state instanceof Container container) { + return addItems(container.getInventory(), refundItems); + } + } catch (Exception e) { + Bukkit.getLogger().warning("[X-Mas] Failed to place refund " + containerMaterial + ": " + e.getMessage()); + } + refundBlock.setType(Material.AIR, false); + return null; + } + + private List addItems(Inventory inventory, List items) { + List clones = new ArrayList<>(); + for (ItemStack item : items) { + if (item != null && !item.getType().isAir()) { + clones.add(item.clone()); + } + } + Map leftovers = inventory.addItem(clones.toArray(new ItemStack[0])); + return new ArrayList<>(leftovers.values()); + } + + private void dropRefunds(List items) { + if (items == null || items.isEmpty() || location.getWorld() == null) { + return; + } + Location dropLocation = location.clone().add(0.5, 0.5, 0.5); + for (ItemStack item : items) { + if (item != null && !item.getType().isAir()) { + location.getWorld().dropItemNaturally(dropLocation, item.clone()); + } + } + } + + private void notifyRefund(Player refundTarget, String message) { + if (refundTarget != null && refundTarget.isOnline()) { + TextUtils.sendRawMessage(refundTarget, message); + } } public long getPresentCounter() { diff --git a/src/main/java/ru/meloncode/xmas/Main.java b/src/main/java/ru/meloncode/xmas/Main.java index bf3dc8f..ca3dab7 100644 --- a/src/main/java/ru/meloncode/xmas/Main.java +++ b/src/main/java/ru/meloncode/xmas/Main.java @@ -1,7 +1,7 @@ package ru.meloncode.xmas; -import com.google.common.collect.Lists; import org.bukkit.*; +import org.bukkit.command.PluginCommand; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.event.EventHandler; @@ -9,8 +9,10 @@ import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldUnloadEvent; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.Recipe; import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.Vector; @@ -19,6 +21,7 @@ import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Base64; import java.util.*; public class Main extends JavaPlugin implements Listener { @@ -31,14 +34,20 @@ public class Main extends JavaPlugin implements Listener { static boolean resourceBack; static int MAX_TREE_COUNT; static boolean autoEnd; + static boolean particlesEnabled; + static float growFirstSoundVolume; + static float growRepeatSoundVolume; static long endTime; static boolean inProgress; private static int UPDATE_SPEED; private static int PARTICLES_DELAY; + private static NamespacedKey crystalKey; private static List heads; private static Plugin plugin; + private static final int MAX_SERIALIZED_GIFT_LENGTH = 65536; private FileConfiguration config; private String locale; + private XMasPlaceholderExpansion placeholderExpansion; public static Plugin getInstance() { return plugin; @@ -48,6 +57,10 @@ public static List getHeads() { return heads; } + public static NamespacedKey getCrystalKey() { + return crystalKey; + } + @Override public void onLoad() { plugin = this; @@ -56,6 +69,7 @@ public void onLoad() { @Override public void onEnable() { this.saveDefaults(); + crystalKey = new NamespacedKey(this, "xmas_crystal"); config = getConfig(); locale = config.getString("core.locale"); @@ -69,8 +83,12 @@ public void onEnable() { UPDATE_SPEED = 7; } PARTICLES_DELAY = config.getInt("core.particles-delay"); - if (PARTICLES_DELAY <= 0) - config.set("particles-delay", 35); + if (PARTICLES_DELAY <= 0) { + config.set("core.particles-delay", 35); + PARTICLES_DELAY = 35; + } + particlesEnabled = config.getBoolean("core.particles-enabled", true); + loadSoundConfig(); autoEnd = config.getBoolean("core.holiday-ends.enabled"); resourceBack = config.getBoolean("core.holiday-ends.resource-back"); @@ -80,7 +98,7 @@ public void onEnable() { date = sdf.parse(config.getString("core.holiday-ends.date")); endTime = date.getTime(); } catch (ParseException e1) { - TextUtils.sendConsoleMessage("Unable to load date"); + TextUtils.sendConsoleMessage("Unable to load date"); } defineTreeLevels(); for (World world : getServer().getWorlds()) { @@ -90,33 +108,20 @@ public void onEnable() { LocaleManager.loadLocale(locale); heads = config.getStringList("xmas.presents"); if (heads.size() == 0) { - getLogger().warning(ChatColor.RED + "Warning! No heads loaded! Presents can't spawn without box!"); + getLogger().warning("[X-Mas] Warning! No heads loaded! Presents can't spawn without box!"); return; } gifts = new ArrayList<>(); - for (String cItem : config.getStringList("xmas.gifts")) { - try { - - if (cItem.contains(":")) { - String[] split = cItem.split(":"); - if (split.length == 0) throw new IllegalArgumentException(); - - Material material; - int amount = 1; - material = Material.valueOf(split[0]); - if (split.length > 1) amount = Integer.parseInt(split[1]); - gifts.add(new ItemStack(material, amount)); - } else { - gifts.add(new ItemStack(Material.valueOf(cItem))); - } - - } catch (IllegalArgumentException e) { - getLogger().severe(ChatColor.RED + "[X-Mas] Unable to get load gift from '" + cItem + "'"); - getLogger().warning(ChatColor.RED + "[X-Mas] For gifts - use format MATERIAL:AMOUNT:DATA. Amount and data are optional"); + for (String serializedItem : config.getStringList("xmas.gifts")) { + ItemStack item = deserializeItem(serializedItem); + if (item != null) { + gifts.add(item); + } else { + getLogger().warning("[X-Mas] Failed to load gift item: " + serializedItem); } } if (gifts.size() == 0) { - getLogger().warning(ChatColor.RED + "[X-Mas] Warning! No gifts loaded! No X-Mas without gifts!"); + getLogger().warning("[X-Mas] Warning! No gifts loaded! No X-Mas without gifts!"); return; } @@ -124,11 +129,16 @@ public void onEnable() { LUCK_CHANCE = (float) config.getInt("xmas.luck.chance") / 100; new Events().registerListener(); new MagicTask(this).runTaskTimer(this, 5, UPDATE_SPEED); - new PlayParticlesTask(this).runTaskTimerAsynchronously(this, 5, PARTICLES_DELAY); + new PlayParticlesTask(this).runTaskTimer(this, 5, PARTICLES_DELAY); XMas.XMAS_CRYSTAL = new ItemMaker(Material.EMERALD, LocaleManager.CRYSTAL_NAME, LocaleManager.CRYSTAL_LORE).make(); + ItemMeta crystalMeta = XMas.XMAS_CRYSTAL.getItemMeta(); + if (crystalMeta != null) { + crystalMeta.getPersistentDataContainer().set(crystalKey, PersistentDataType.BYTE, (byte) 1); + XMas.XMAS_CRYSTAL.setItemMeta(crystalMeta); + } ShapedRecipe grinderRecipe; - grinderRecipe = new ShapedRecipe(new NamespacedKey(this, "xmas"), XMas.XMAS_CRYSTAL).shape("#d#", "ded", "#d#").setIngredient('d', Material.DIAMOND).setIngredient('e', Material.EMERALD); + grinderRecipe = new ShapedRecipe(new NamespacedKey(this, "xmas"), XMas.XMAS_CRYSTAL).shape(" d ", "ded", " d ").setIngredient('d', Material.DIAMOND).setIngredient('e', Material.EMERALD); Iterator recipes = getServer().recipeIterator(); boolean registered = false; while (recipes.hasNext()) { @@ -145,6 +155,7 @@ public void onEnable() { } catch (Exception ignored) { } XMasCommand.register(this); + registerPlaceholderApi(); TextUtils.sendConsoleMessage(LocaleManager.PLUGIN_ENABLED); } @@ -160,17 +171,169 @@ public void onWorldUnload(WorldUnloadEvent event) { } } + public void reloadPluginConfig() { + reloadConfig(); + config = getConfig(); + locale = config.getString("core.locale"); + + inProgress = config.getBoolean("core.plugin-enabled", true); + UPDATE_SPEED = config.getInt("core.update-speed"); + if (UPDATE_SPEED <= 0) { + UPDATE_SPEED = 7; + } + PARTICLES_DELAY = config.getInt("core.particles-delay"); + if (PARTICLES_DELAY <= 0) { + PARTICLES_DELAY = 35; + } + particlesEnabled = config.getBoolean("core.particles-enabled", true); + loadSoundConfig(); + + autoEnd = config.getBoolean("core.holiday-ends.enabled"); + resourceBack = config.getBoolean("core.holiday-ends.resource-back"); + MAX_TREE_COUNT = config.getInt("core.tree-limit"); + try { + SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); + endTime = sdf.parse(config.getString("core.holiday-ends.date")).getTime(); + } catch (ParseException e) { + TextUtils.sendConsoleMessage("Invalid holiday end date in config.yml"); + } + + defineTreeLevels(); + LocaleManager.loadLocale(locale); + heads = config.getStringList("xmas.presents"); + + gifts = new ArrayList<>(); + for (String serializedItem : config.getStringList("xmas.gifts")) { + ItemStack item = deserializeItem(serializedItem); + if (item != null) { + gifts.add(item); + } else { + getLogger().warning("[X-Mas] Failed to deserialize gift item: " + serializedItem); + } + } + + LUCK_CHANCE_ENABLED = config.getBoolean("xmas.luck.enabled"); + LUCK_CHANCE = (float) config.getInt("xmas.luck.chance") / 100; + XMasCommand.refreshCommandConfiguration(this); + TextUtils.sendConsoleMessage("Configuration reloaded!"); + } + + private void loadSoundConfig() { + growFirstSoundVolume = clampVolume(config.getDouble("core.sounds.grow.first-volume", 0.5)); + growRepeatSoundVolume = clampVolume(config.getDouble("core.sounds.grow.repeat-volume", 0.2)); + } + + private float clampVolume(double volume) { + if (Double.isNaN(volume)) { + return 0; + } + return (float) Math.max(0, Math.min(1, volume)); + } + + public void addGiftItem(ItemStack item) { + ItemStack gift = item.clone(); + String serializedItem = serializeItem(gift); + if (serializedItem == null) { + getLogger().warning("Failed to serialize item for saving to the gift list. Item: " + gift); + return; + } + + List giftList = config.getStringList("xmas.gifts"); + giftList.add(serializedItem); + config.set("xmas.gifts", giftList); + saveConfig(); + gifts.add(gift); + } + + private String serializeItem(ItemStack item) { + try { + byte[] serializedBytes = item.serializeAsBytes(); + return Base64.getEncoder().encodeToString(serializedBytes); + } catch (Exception e) { + getLogger().severe("Failed to serialize item: " + e.getMessage()); + return null; + } + } + + public static ItemStack deserializeItem(String serializedItem) { + if (serializedItem == null) { + return null; + } + String trimmed = serializedItem.trim(); + if (trimmed.isEmpty()) { + return null; + } + + ItemStack materialItem = deserializeMaterialItem(trimmed); + if (materialItem != null) { + return materialItem; + } + + if (trimmed.length() > MAX_SERIALIZED_GIFT_LENGTH) { + Bukkit.getLogger().severe("[X-Mas] Gift item is too large to deserialize safely: " + trimmed.length() + " characters"); + return null; + } + + try { + byte[] serializedBytes = Base64.getDecoder().decode(trimmed); + return ItemStack.deserializeBytes(serializedBytes); + } catch (IllegalArgumentException e) { + Bukkit.getLogger().severe("[X-Mas] Invalid material name or Base64 gift item: " + trimmed); + return null; + } catch (Exception e) { + Bukkit.getLogger().severe("[X-Mas] Failed to deserialize gift item: " + e.getMessage()); + return null; + } + } + + private static ItemStack deserializeMaterialItem(String serializedItem) { + String materialName = serializedItem; + int amount = 1; + if (serializedItem.contains(":")) { + String[] split = serializedItem.split(":", 2); + materialName = split[0].trim(); + try { + amount = Integer.parseInt(split[1].trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + Material material = Material.matchMaterial(materialName); + if (material == null || material.isLegacy() || !material.isItem()) { + return null; + } + int maxStackSize = Math.max(1, material.getMaxStackSize()); + amount = Math.max(1, Math.min(amount, maxStackSize)); + return new ItemStack(material, amount); + } + @Override public void onDisable() { + if (placeholderExpansion != null && placeholderExpansion.isRegistered()) { + placeholderExpansion.unregister(); + } if (XMas.getAllTrees().size() > 0) for (MagicTree tree : XMas.getAllTrees()) { tree.unbuild(); } } + private void registerPlaceholderApi() { + if (!getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) { + return; + } + placeholderExpansion = new XMasPlaceholderExpansion(this); + if (placeholderExpansion.register()) { + getLogger().info("Registered PlaceholderAPI expansion: " + XMasPlaceholders.IDENTIFIER); + } else { + getLogger().warning("PlaceholderAPI is present, but placeholder registration failed."); + } + } + public void end() { - Bukkit.broadcastMessage(ChatColor.GREEN + LocaleManager.HAPPY_NEW_YEAR); + Bukkit.broadcast(TextUtils.parse("" + LocaleManager.HAPPY_NEW_YEAR)); inProgress = false; config.set("core.plugin-enabled", false); saveConfig(); @@ -179,7 +342,7 @@ public void end() { private void saveDefaults() { this.saveDefaultConfig(); plugin.saveResource("locales/default.yml", true); - ArrayList defaults = Lists.newArrayList("locales/en.yml", "locales/ru.yml", "locales/ru_santa.yml", "trees.yml"); + List defaults = Arrays.asList("locales/en.yml", "locales/ru.yml", "locales/ru_santa.yml", "trees.yml"); for (String path : defaults) if (!new File(getDataFolder(), '/' + path).exists()) plugin.saveResource(path, false); } @@ -196,7 +359,11 @@ private void defineTreeLevels() { Map smallLevelUp = TreeSerializer.convertRequirementsMap(lvlups.getConfigurationSection("small_tree.lvlup").getValues(false)); Map treeLevelUp = TreeSerializer.convertRequirementsMap(lvlups.getConfigurationSection("tree.lvlup").getValues(false)); - TreeLevel.MAGIC_TREE = new TreeLevel("magic_tree", Effects.TREE_WHITE_AMBIENT, Effects.TREE_SWAG, null, null, magic_delay, Collections.emptyMap(), new StructureTemplate(new HashMap() { + TreeLevel.MAGIC_TREE = new TreeLevel("magic_tree", + getParticleEffect("magic_tree", "ambient", Effects.TREE_WHITE_AMBIENT), + getParticleEffect("magic_tree", "swag", Effects.TREE_SWAG), + getParticleEffect("magic_tree", "body", null), + null, magic_delay, Collections.emptyMap(), new StructureTemplate(new HashMap() { private static final long serialVersionUID = 1L; { @@ -232,7 +399,11 @@ private void defineTreeLevels() { } })); - TreeLevel.TREE = new TreeLevel("tree", Effects.AMBIENT_SNOW, Effects.TREE_GOLD_SWAG, null, TreeLevel.MAGIC_TREE, tree_delay, treeLevelUp, new StructureTemplate(new HashMap() { + TreeLevel.TREE = new TreeLevel("tree", + getParticleEffect("tree", "ambient", Effects.AMBIENT_SNOW), + getParticleEffect("tree", "swag", Effects.TREE_GOLD_SWAG), + getParticleEffect("tree", "body", null), + TreeLevel.MAGIC_TREE, tree_delay, treeLevelUp, new StructureTemplate(new HashMap() { private static final long serialVersionUID = 1L; { @@ -265,7 +436,11 @@ private void defineTreeLevels() { } })); - TreeLevel.SMALL_TREE = new TreeLevel("small_tree", Effects.AMBIENT_PORTAL, Effects.TREE_RED_SWAG, null, TreeLevel.TREE, small_delay, smallLevelUp, new StructureTemplate(new HashMap() { + TreeLevel.SMALL_TREE = new TreeLevel("small_tree", + getParticleEffect("small_tree", "ambient", Effects.AMBIENT_PORTAL), + getParticleEffect("small_tree", "swag", Effects.TREE_RED_SWAG), + getParticleEffect("small_tree", "body", null), + TreeLevel.TREE, small_delay, smallLevelUp, new StructureTemplate(new HashMap() { private static final long serialVersionUID = 1L; { @@ -292,7 +467,11 @@ private void defineTreeLevels() { } })); - TreeLevel.SAPLING = new TreeLevel("sapling", Effects.AMBIENT_SAPLING, null, null, TreeLevel.SMALL_TREE, sapling_delay, saplingLevelUp, new StructureTemplate(new HashMap() { + TreeLevel.SAPLING = new TreeLevel("sapling", + getParticleEffect("sapling", "ambient", Effects.AMBIENT_SAPLING), + getParticleEffect("sapling", "swag", null), + getParticleEffect("sapling", "body", null), + TreeLevel.SMALL_TREE, sapling_delay, saplingLevelUp, new StructureTemplate(new HashMap() { private static final long serialVersionUID = 1L; { @@ -301,4 +480,42 @@ private void defineTreeLevels() { } })); } + + private ParticleContainer getParticleEffect(String level, String effect, ParticleContainer fallback) { + String path = "xmas.tree-lvl." + level + ".particles." + effect; + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + return fallback; + } + if (!section.getBoolean("enabled", fallback != null)) { + return null; + } + + String configuredParticle = section.getString("particle", fallback != null ? fallback.getType().name() : null); + if (configuredParticle == null || configuredParticle.trim().isEmpty()) { + return fallback; + } + + Particle particle; + try { + particle = Particle.valueOf(configuredParticle.trim().toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + getLogger().warning("[X-Mas] Unknown particle '" + configuredParticle + "' at " + path + ". Using fallback."); + return fallback; + } + + if (particle.getDataType() != Void.class && particle != Particle.DUST) { + getLogger().warning("[X-Mas] Particle '" + particle.name() + "' needs extra data and is not supported in config yet. Using fallback."); + return fallback; + } + + return new ParticleContainer( + particle, + (float) section.getDouble("offset-x", fallback != null ? fallback.getOffsetX() : 0), + (float) section.getDouble("offset-y", fallback != null ? fallback.getOffsetY() : 0), + (float) section.getDouble("offset-z", fallback != null ? fallback.getOffsetZ() : 0), + (float) section.getDouble("speed", fallback != null ? fallback.getSpeed() : 0), + Math.max(0, section.getInt("count", fallback != null ? fallback.getCount() : 0)) + ); + } } diff --git a/src/main/java/ru/meloncode/xmas/ParticleContainer.java b/src/main/java/ru/meloncode/xmas/ParticleContainer.java index fee0acb..6208ddf 100644 --- a/src/main/java/ru/meloncode/xmas/ParticleContainer.java +++ b/src/main/java/ru/meloncode/xmas/ParticleContainer.java @@ -1,6 +1,7 @@ package ru.meloncode.xmas; import org.bukkit.Color; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.Particle.DustOptions; @@ -36,19 +37,43 @@ public ParticleContainer(Particle type, float offsetX, float offsetY, float offs this.count = count; } + public Particle getType() { + return type; + } + + public float getOffsetX() { + return offsetX; + } + + public float getOffsetY() { + return offsetY; + } + + public float getOffsetZ() { + return offsetZ; + } + + public float getSpeed() { + return speed; + } + + public int getCount() { + return count; + } + public void playEffect(Location location) { - location = location.clone(); // prevent chaning pos of object + location = location.clone(); // prevent changing pos of object location.add(0.5, 0.5, 0.5); // A small fix for (Player player : location.getWorld().getPlayers()) if (player.getLocation().distance(location) < 16) { try { - if (type == Particle.REDSTONE) { + if (type == Particle.DUST) { player.spawnParticle(type, location, count, offsetX, offsetY, offsetZ, speed, COLORS[random.nextInt(6)]); } else { player.spawnParticle(type, location, count, offsetX, offsetY, offsetZ, speed); } } catch (Exception e) { - e.printStackTrace(); + Bukkit.getLogger().warning("[X-Mas] Failed to spawn particle " + type + ": " + e.getMessage()); } } } diff --git a/src/main/java/ru/meloncode/xmas/PlayParticlesTask.java b/src/main/java/ru/meloncode/xmas/PlayParticlesTask.java index 6950525..8092aea 100644 --- a/src/main/java/ru/meloncode/xmas/PlayParticlesTask.java +++ b/src/main/java/ru/meloncode/xmas/PlayParticlesTask.java @@ -12,7 +12,7 @@ class PlayParticlesTask extends BukkitRunnable { @Override public void run() { - if (Main.inProgress) + if (Main.inProgress && Main.particlesEnabled) for (MagicTree tree : XMas.getAllTrees()) { tree.playParticles(); } diff --git a/src/main/java/ru/meloncode/xmas/TreeSerializer.java b/src/main/java/ru/meloncode/xmas/TreeSerializer.java index b4108d9..32904e6 100644 --- a/src/main/java/ru/meloncode/xmas/TreeSerializer.java +++ b/src/main/java/ru/meloncode/xmas/TreeSerializer.java @@ -1,6 +1,5 @@ package ru.meloncode.xmas; -import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -13,11 +12,15 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; class TreeSerializer { private static final File treesFile = new File(Main.getInstance().getDataFolder() + "/trees.yml"); private static final FileConfiguration trees = ConfigUtils.loadConfig(treesFile); + private static final Set loggedWorldAliasMappings = ConcurrentHashMap.newKeySet(); public static void loadTrees(JavaPlugin plugin, World world) { try { @@ -31,7 +34,8 @@ public static void loadTrees(JavaPlugin plugin, World world) { if (trees.getConfigurationSection("trees") != null && trees.getConfigurationSection("trees").getKeys(false).size() > 0) { for (String cKey : trees.getConfigurationSection("trees").getKeys(false)) { - if (world.getName().equals(trees.getString("trees." + cKey + ".loc.world"))) { + String savedWorldName = trees.getString("trees." + cKey + ".loc.world"); + if (matchesSavedWorld(world, savedWorldName)) { try { treeUID = UUID.fromString(cKey); owner = UUID.fromString(trees.getString("trees." + cKey + ".owner")); @@ -51,16 +55,14 @@ public static void loadTrees(JavaPlugin plugin, World world) { XMas.addMagicTree(new MagicTree(owner, treeUID, level, loc, requirements, presentCounter, scheduledPresents)); } catch (Exception e) { - plugin.getLogger().severe(String.format("Error while loading tree `%s`", cKey)); - e.printStackTrace(); - System.out.println("================================================"); + plugin.getLogger().log(Level.SEVERE, String.format("Error while loading tree `%s`", cKey), e); } } } } } catch (Exception e) { - TextUtils.sendConsoleMessage(ChatColor.DARK_RED + "ERROR WHILE LOADING TREES"); - e.printStackTrace(); + TextUtils.sendConsoleMessage("ERROR WHILE LOADING TREES"); + plugin.getLogger().log(Level.SEVERE, "Unable to load X-Mas trees", e); } } @@ -76,13 +78,13 @@ public static void saveTree(MagicTree tree) { trees.set("trees." + cKey + ".loc.z", tree.getLocation().getZ()); if (tree.getLevelupRequirements() != null && tree.getLevelupRequirements().size() > 0) trees.createSection("trees." + cKey + ".levelup", tree.getLevelupRequirements()); + trees.set("trees." + cKey + ".present_counter", tree.getPresentCounter()); + trees.set("trees." + cKey + ".scheduled_presents", tree.getScheduledPresents()); try { trees.save(treesFile); } catch (IOException e) { - e.printStackTrace(); + Main.getInstance().getLogger().log(Level.SEVERE, "Unable to save X-Mas tree data", e); } - trees.set("trees." + cKey + ".present_counter", tree.getPresentCounter()); - trees.set("trees." + cKey + ".scheduled_presents", tree.getScheduledPresents()); } public static void removeTree(MagicTree tree) { @@ -90,7 +92,7 @@ public static void removeTree(MagicTree tree) { try { trees.save(treesFile); } catch (IOException e) { - e.printStackTrace(); + Main.getInstance().getLogger().log(Level.SEVERE, "Unable to remove X-Mas tree data", e); } } @@ -101,14 +103,52 @@ public static Map convertRequirementsMap(Map if (map != null) for (String sMaterial : map.keySet()) { try { - cMaterial = Material.valueOf(sMaterial); - value = (int) map.get(sMaterial); + cMaterial = Material.matchMaterial(sMaterial); + if (cMaterial == null || cMaterial.isLegacy()) { + TextUtils.sendConsoleMessage("Can't find modern material '" + sMaterial + "' for tree level."); + continue; + } + Object rawValue = map.get(sMaterial); + if (!(rawValue instanceof Number)) { + TextUtils.sendConsoleMessage("Tree level material '" + sMaterial + "' must use a numeric amount."); + continue; + } + value = ((Number) rawValue).intValue(); levelupRequirements.put(cMaterial, value); } catch (IllegalArgumentException e) { - TextUtils.sendConsoleMessage("Can't find material '" + sMaterial + "' for tree level."); - return null; + TextUtils.sendConsoleMessage("Can't load material '" + sMaterial + "' for tree level."); } } return levelupRequirements; } -} \ No newline at end of file + + private static boolean matchesSavedWorld(World world, String savedWorldName) { + if (world == null || savedWorldName == null || savedWorldName.isBlank()) { + return false; + } + if (world.getName().equalsIgnoreCase(savedWorldName)) { + return true; + } + + String configuredWorldName = getWorldAlias(savedWorldName); + if (configuredWorldName != null && world.getName().equalsIgnoreCase(configuredWorldName)) { + logWorldAliasMapping(savedWorldName, world.getName()); + return true; + } + return false; + } + + private static String getWorldAlias(String savedWorldName) { + if (!(Main.getInstance() instanceof Main plugin)) { + return null; + } + return plugin.getConfig().getString("migration.world-aliases." + savedWorldName); + } + + private static void logWorldAliasMapping(String savedWorldName, String worldName) { + String mapping = savedWorldName + "->" + worldName; + if (loggedWorldAliasMappings.add(mapping)) { + Main.getInstance().getLogger().info("Loading legacy X-Mas trees from saved world '" + savedWorldName + "' into '" + worldName + "' via migration.world-aliases."); + } + } +} diff --git a/src/main/java/ru/meloncode/xmas/XMas.java b/src/main/java/ru/meloncode/xmas/XMas.java index 7335851..507d79c 100644 --- a/src/main/java/ru/meloncode/xmas/XMas.java +++ b/src/main/java/ru/meloncode/xmas/XMas.java @@ -8,7 +8,6 @@ import org.bukkit.block.Skull; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.Nullable; import ru.meloncode.xmas.utils.LocationUtils; import ru.meloncode.xmas.utils.TextUtils; @@ -35,6 +34,10 @@ public static void createMagicTree(Player player, Location loc) { public static void addMagicTree(MagicTree tree) { trees.put(tree.getTreeUID(), tree); + trees_byChunk.computeIfAbsent(LocationUtils.getChunkKey(tree.getLocation()), aLong -> new ArrayList<>()); + List chunkTrees = trees_byChunk.get(LocationUtils.getChunkKey(tree.getLocation())); + chunkTrees.removeIf(existingTree -> existingTree.getTreeUID().equals(tree.getTreeUID())); + chunkTrees.add(tree); tree.build(); } @@ -42,39 +45,60 @@ public static Collection getAllTrees() { return trees.values(); } - @Nullable public static Collection getAllTreesInChunk(Chunk chunk) { return trees_byChunk.get(LocationUtils.getChunkKey(chunk)); } public static void removeTree(MagicTree tree) { - tree.unbuild(); + removeTree(tree, true); + } + + public static void removeTree(MagicTree tree, boolean unbuild) { + if (unbuild) { + tree.unbuild(); + } TreeSerializer.removeTree(tree); trees.remove(tree.getTreeUID()); - trees_byChunk.remove(LocationUtils.getChunkKey(tree.getLocation())); + List chunkTrees = trees_byChunk.get(LocationUtils.getChunkKey(tree.getLocation())); + if (chunkTrees != null) { + chunkTrees.remove(tree); + if (chunkTrees.isEmpty()) { + trees_byChunk.remove(LocationUtils.getChunkKey(tree.getLocation())); + } + } } public static void processPresent(Block block, Player player) { if (block.getType() == Material.PLAYER_HEAD) { Skull skull = (Skull) block.getState(); + String headId = getHeadIdentifier(skull); - if (Main.getHeads().contains(skull.getOwner())) { - Location loc = block.getLocation(); - World world = loc.getWorld(); - if (world != null) { - if (RANDOM.nextFloat() < Main.LUCK_CHANCE || !Main.LUCK_CHANCE_ENABLED) { - world.dropItemNaturally(loc, new ItemStack(Main.gifts.get(RANDOM.nextInt(Main.gifts.size())))); - Effects.TREE_SWAG.playEffect(loc); - TextUtils.sendMessage(player, LocaleManager.GIFT_LUCK); - } else { - Effects.SMOKE.playEffect(loc); - world.dropItemNaturally(loc, new ItemStack(Material.COAL)); - TextUtils.sendMessage(player, LocaleManager.GIFT_FAIL); - } + if (headId != null && Main.getHeads().contains(headId)) { + Location loc = block.getLocation(); + World world = loc.getWorld(); + if (world != null) { + if (RANDOM.nextFloat() < Main.LUCK_CHANCE || !Main.LUCK_CHANCE_ENABLED) { + world.dropItemNaturally(loc, new ItemStack(Main.gifts.get(RANDOM.nextInt(Main.gifts.size())))); + Effects.TREE_SWAG.playEffect(loc); + TextUtils.sendMessage(player, LocaleManager.GIFT_LUCK); + } else { + Effects.SMOKE.playEffect(loc); + world.dropItemNaturally(loc, new ItemStack(Material.COAL)); + TextUtils.sendMessage(player, LocaleManager.GIFT_FAIL); } - block.setType(Material.AIR); } + block.setType(Material.AIR); + } + } + } + + static String getHeadIdentifier(Skull skull) { + if (skull.getOwnerProfile() != null + && skull.getOwnerProfile().getTextures() != null + && skull.getOwnerProfile().getTextures().getSkin() != null) { + return skull.getOwnerProfile().getTextures().getSkin().toString(); } + return skull.getOwner(); } public static List getTreesPlayerOwn(Player player) { diff --git a/src/main/java/ru/meloncode/xmas/XMasCommand.java b/src/main/java/ru/meloncode/xmas/XMasCommand.java index 943ee1b..fd41d14 100644 --- a/src/main/java/ru/meloncode/xmas/XMasCommand.java +++ b/src/main/java/ru/meloncode/xmas/XMasCommand.java @@ -1,20 +1,37 @@ package ru.meloncode.xmas; import org.bukkit.Bukkit; +import org.bukkit.command.CommandMap; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import ru.meloncode.xmas.utils.TextUtils; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.text.SimpleDateFormat; -import java.util.HashSet; -import java.util.Random; -import java.util.Set; -import java.util.UUID; +import java.util.*; -import static org.bukkit.ChatColor.*; - -public class XMasCommand implements CommandExecutor { +public class XMasCommand implements CommandExecutor, TabCompleter { + public static final String PRIMARY_COMMAND = "xmastree"; + public static final String LEGACY_COMMAND = "xmas"; + private static final List COMMANDS = Arrays.asList("help", "give", "end", "gifts", "reload", "addhand", "debug"); + private static final Set DEBUG_TOGGLE_KEYS = new LinkedHashSet<>(Arrays.asList( + "core.commands.legacy-command-enabled", + "core.plugin-enabled", + "core.holiday-ends.enabled", + "core.holiday-ends.resource-back", + "core.particles-enabled", + "xmas.luck.enabled" + )); + private static final int DEBUG_PAGE_SIZE = 8; + private static XMasCommand registeredExecutor; + private static PluginCommand legacyAliasCommand; private final Main plugin; @@ -23,18 +40,33 @@ private XMasCommand(Main plugin) { } public static void register(Main plugin) { - plugin.getCommand("xmas").setExecutor(new XMasCommand(plugin)); + registeredExecutor = new XMasCommand(plugin); + PluginCommand primaryCommand = plugin.getCommand(PRIMARY_COMMAND); + if (primaryCommand == null) { + throw new IllegalStateException("Primary command '/" + PRIMARY_COMMAND + "' is not defined in plugin.yml"); + } + primaryCommand.setExecutor(registeredExecutor); + primaryCommand.setTabCompleter(registeredExecutor); + refreshCommandConfiguration(plugin); + } + + public static void refreshCommandConfiguration(Main plugin) { + if (registeredExecutor == null) { + register(plugin); + return; + } + syncLegacyAlias(plugin, registeredExecutor); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (args.length > 0) { - String action = args[0].toLowerCase(); + String action = args[0].toLowerCase(Locale.ENGLISH); switch (action) { case "help": { - for (String line : LocaleManager.COMMAND_HELP) { - sender.sendMessage(GREEN + line); + for (String line : getHelpLines()) { + TextUtils.sendRawMessage(sender, line); } break; } @@ -45,10 +77,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St if (player != null) { player.getInventory().addItem(XMas.XMAS_CRYSTAL); } else { - sender.sendMessage(LocaleManager.COMMAND_PLAYER_OFFLINE); + TextUtils.sendMessage(sender, LocaleManager.COMMAND_PLAYER_OFFLINE); } } else { - sender.sendMessage(LocaleManager.COMMAND_NO_PLAYER_NAME); + TextUtils.sendMessage(sender, LocaleManager.COMMAND_NO_PLAYER_NAME); } break; } @@ -63,7 +95,38 @@ public boolean onCommand(CommandSender sender, Command command, String label, St magicTree.spawnPresent(); } } - Bukkit.broadcastMessage(LocaleManager.COMMAND_GIVEAWAY); + Bukkit.broadcast(TextUtils.parse(LocaleManager.COMMAND_GIVEAWAY)); + break; + } + case "reload": { + if (!sender.hasPermission("xmas.admin")) { + TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + break; + } + plugin.reloadPluginConfig(); + TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " configuration reloaded."); + break; + } + case "addhand": { + if (!(sender instanceof Player player)) { + TextUtils.sendRawMessage(sender, "Only players can use this command."); + break; + } + if (!sender.hasPermission("xmas.admin")) { + TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + break; + } + ItemStack item = player.getInventory().getItemInMainHand(); + if (item.getType().isAir()) { + TextUtils.sendRawMessage(player, "Hold an item before running " + commandPath("addhand") + "."); + break; + } + plugin.addGiftItem(item.clone()); + TextUtils.sendRawMessage(player, "Added the held item to the gift list."); + break; + } + case "debug": { + handleDebug(sender, args); break; } @@ -76,8 +139,42 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List suggestions = new ArrayList<>(); + if (args.length == 1) { + String typed = args[0].toLowerCase(Locale.ENGLISH); + for (String subCommand : COMMANDS) { + if (subCommand.startsWith(typed)) { + suggestions.add(subCommand); + } + } + } else if (args.length == 2 && args[0].equalsIgnoreCase("give")) { + String typed = args[1].toLowerCase(Locale.ENGLISH); + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getName().toLowerCase(Locale.ENGLISH).startsWith(typed)) { + suggestions.add(player.getName()); + } + } + } else if (args[0].equalsIgnoreCase("debug")) { + if (args.length == 2) { + suggestions.addAll(filterStartingWith(Arrays.asList("1", "2", "3", "toggle"), args[1])); + } else if (args.length == 3 && args[1].equalsIgnoreCase("toggle")) { + suggestions.addAll(filterStartingWith(new ArrayList<>(DEBUG_TOGGLE_KEYS), args[2])); + } else if (args.length == 4 && args[1].equalsIgnoreCase("toggle")) { + suggestions.addAll(filterStartingWith(Arrays.asList("true", "false"), args[3])); + } + } + return suggestions; + } + private void sendStatus(CommandSender sender) { + for (String line : getStatusLines()) { + TextUtils.sendRawMessage(sender, line); + } + } + private List getStatusLines() { int treeCount = XMas.getAllTrees().size(); Set owners = new HashSet<>(); for (MagicTree magicTree : XMas.getAllTrees()) { @@ -85,19 +182,242 @@ private void sendStatus(CommandSender sender) { } SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); + List lines = new ArrayList<>(); - sender.sendMessage(DARK_GREEN + LocaleManager.PLUGIN_NAME + " " + plugin.getDescription().getVersion() + " Plugin Status"); - sender.sendMessage(""); - sender.sendMessage(GRAY + "Event Status: " + (Main.inProgress ? DARK_GREEN + "In Progress" : RED + "Holidays End")); + lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getDescription().getVersion() + " Plugin Status"); + lines.add(""); + lines.add("Event Status: " + (Main.inProgress ? "In Progress" : "Holidays End")); if (Main.inProgress) { - sender.sendMessage(DARK_GREEN + "Current Time: " + GREEN + sdf.format(System.currentTimeMillis())); - sender.sendMessage(DARK_GREEN + "Holidays end: " + RED + sdf.format(Main.endTime)); + lines.add("Current Time: " + sdf.format(System.currentTimeMillis())); + lines.add("Holidays end: " + sdf.format(Main.endTime)); + } + lines.add("Auto-End: " + (Main.autoEnd ? "Yes" : "No") + " | Resource Back: " + (Main.resourceBack ? "Yes" : "No") + " | Particles: " + (Main.particlesEnabled ? "Yes" : "No")); + lines.add(""); + lines.add("There are " + treeCount + " magic trees owned by " + owners.size() + " players"); + lines.add("Use " + commandPath("help") + " for command list"); + return lines; + } + + private void handleDebug(CommandSender sender, String[] args) { + if (!sender.hasPermission("xmas.admin")) { + TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + return; + } + + if (args.length >= 2 && args[1].equalsIgnoreCase("toggle")) { + handleDebugToggle(sender, args); + return; + } + + int page = 1; + if (args.length >= 2) { + try { + page = Integer.parseInt(args[1]); + } catch (NumberFormatException ignored) { + page = 1; + } + } + sendDebugPage(sender, page); + } + + private void handleDebugToggle(CommandSender sender, String[] args) { + if (args.length < 4) { + TextUtils.sendRawMessage(sender, "Usage: " + commandPath("debug toggle true|false")); + TextUtils.sendRawMessage(sender, "Keys: " + String.join(", ", DEBUG_TOGGLE_KEYS)); + return; + } + + String key = args[2].toLowerCase(Locale.ENGLISH); + if (!DEBUG_TOGGLE_KEYS.contains(key)) { + TextUtils.sendRawMessage(sender, "Unknown toggle key: " + args[2]); + TextUtils.sendRawMessage(sender, "Keys: " + String.join(", ", DEBUG_TOGGLE_KEYS)); + return; + } + if (!args[3].equalsIgnoreCase("true") && !args[3].equalsIgnoreCase("false")) { + TextUtils.sendRawMessage(sender, "Value must be true or false."); + return; } - sender.sendMessage(GREEN + "Auto-End: " + (Main.autoEnd ? DARK_GREEN + "Yes" : RED + "No") + GREEN + " | " + "Resource Back: " + (Main.resourceBack ? DARK_GREEN + "Yes" : "No")); - sender.sendMessage(""); - sender.sendMessage(DARK_GREEN + "There are " + GREEN + treeCount + DARK_GREEN + " magic trees owned by " + RED + owners.size() + DARK_GREEN + " players"); - sender.sendMessage(DARK_GREEN + "Use " + RED + "/xmas help" + DARK_GREEN + " for command list"); + boolean value = Boolean.parseBoolean(args[3]); + plugin.getConfig().set(key, value); + plugin.saveConfig(); + plugin.reloadPluginConfig(); + TextUtils.sendRawMessage(sender, "Set " + key + " to " + value + " and reloaded " + TextUtils.DISPLAY_NAME + "."); + } + + private void sendDebugPage(CommandSender sender, int requestedPage) { + List lines = new ArrayList<>(getStatusLines()); + lines.add(""); + lines.add("Commands"); + lines.add("" + commandPath("") + " - status"); + lines.add("" + commandPath("help") + " - command list"); + lines.add("" + commandPath("give ") + " - give a Christmas Crystal"); + lines.add("" + commandPath("gifts") + " - spawn presents under all trees"); + lines.add("" + commandPath("addhand") + " - add held item to gifts"); + lines.add("" + commandPath("reload") + " - reload config and locale"); + lines.add("" + commandPath("end") + " - end the event"); + lines.add("" + commandPath("debug [page]") + " - extended debug output"); + lines.add("" + commandPath("debug toggle true|false") + " - toggle global booleans"); + if (isLegacyAliasEnabled()) { + lines.add("Legacy alias enabled: /" + LEGACY_COMMAND); + } + lines.add(""); + lines.add("Permissions"); + lines.add("xmas.admin - allows all " + TextUtils.DISPLAY_NAME + " admin commands"); + lines.add(""); + lines.add("Placeholders"); + lines.add("Requires PlaceholderAPI. Use '_' after prefix, then dotted keys."); + for (String placeholder : XMasPlaceholders.EXAMPLES) { + lines.add("" + placeholder); + } + lines.add(""); + lines.add("Toggleable Config Keys"); + for (String key : DEBUG_TOGGLE_KEYS) { + lines.add("" + key + " = " + plugin.getConfig().getBoolean(key)); + } + + int pages = Math.max(1, (int) Math.ceil((double) lines.size() / DEBUG_PAGE_SIZE)); + int page = Math.max(1, Math.min(requestedPage, pages)); + int start = (page - 1) * DEBUG_PAGE_SIZE; + int end = Math.min(start + DEBUG_PAGE_SIZE, lines.size()); + + TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " Debug page " + page + "/" + pages); + for (int i = start; i < end; i++) { + TextUtils.sendRawMessage(sender, lines.get(i)); + } + if (page < pages) { + TextUtils.sendRawMessage(sender, "Next: " + commandPath("debug " + (page + 1))); + } + } + + private List getHelpLines() { + List lines = new ArrayList<>(); + for (String line : LocaleManager.COMMAND_HELP) { + lines.add(line.replace("/xmas", "/" + PRIMARY_COMMAND)); + } + if (isLegacyAliasEnabled()) { + lines.add("Legacy alias: /" + LEGACY_COMMAND + " still works."); + } + return lines; + } + + private String commandPath(String suffix) { + if (suffix == null || suffix.isBlank()) { + return "/" + PRIMARY_COMMAND; + } + return "/" + PRIMARY_COMMAND + " " + suffix; + } + + private boolean isLegacyAliasEnabled() { + return plugin.getConfig().getBoolean("core.commands.legacy-command-enabled", true); + } + + private static void syncLegacyAlias(Main plugin, XMasCommand executor) { + CommandMap commandMap = getCommandMap(); + if (commandMap == null) { + plugin.getLogger().warning("Unable to access the Bukkit command map. Skipping legacy /xmas alias registration."); + return; + } + if (!plugin.getConfig().getBoolean("core.commands.legacy-command-enabled", true)) { + unregisterLegacyAlias(commandMap); + return; + } + + Command existing = commandMap.getCommand(LEGACY_COMMAND); + if (existing instanceof PluginCommand existingPluginCommand) { + if (existingPluginCommand.getPlugin() == plugin) { + existingPluginCommand.setExecutor(executor); + existingPluginCommand.setTabCompleter(executor); + legacyAliasCommand = existingPluginCommand; + return; + } + plugin.getLogger().warning("Legacy alias '/" + LEGACY_COMMAND + "' is already owned by plugin '" + existingPluginCommand.getPlugin().getName() + "'. Skipping alias registration."); + return; + } + if (existing != null) { + plugin.getLogger().warning("Legacy alias '/" + LEGACY_COMMAND + "' is already registered by another command source. Skipping alias registration."); + return; + } + + PluginCommand aliasCommand = createPluginCommand(plugin, LEGACY_COMMAND); + if (aliasCommand == null) { + plugin.getLogger().warning("Unable to create the legacy /xmas alias command."); + return; + } + aliasCommand.setDescription("Legacy alias for /" + PRIMARY_COMMAND); + aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|debug|end]"); + aliasCommand.setPermission("xmas.admin"); + aliasCommand.setExecutor(executor); + aliasCommand.setTabCompleter(executor); + commandMap.register(plugin.getDescription().getName().toLowerCase(Locale.ENGLISH), aliasCommand); + legacyAliasCommand = aliasCommand; + plugin.getLogger().info("Registered legacy alias '/" + LEGACY_COMMAND + "' for '/" + PRIMARY_COMMAND + "'."); + } + + private static void unregisterLegacyAlias(CommandMap commandMap) { + if (legacyAliasCommand == null) { + Command existing = commandMap.getCommand(LEGACY_COMMAND); + if (existing instanceof PluginCommand existingPluginCommand && existingPluginCommand.getPlugin() == Main.getInstance()) { + legacyAliasCommand = existingPluginCommand; + } + } + if (legacyAliasCommand == null) { + return; + } + legacyAliasCommand.unregister(commandMap); + if (commandMap instanceof SimpleCommandMap simpleCommandMap) { + try { + Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands"); + knownCommandsField.setAccessible(true); + Object rawKnownCommands = knownCommandsField.get(simpleCommandMap); + if (rawKnownCommands instanceof Map rawMap) { + Iterator> iterator = rawMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getValue() == legacyAliasCommand) { + iterator.remove(); + } + } + } + } catch (ReflectiveOperationException ignored) { + } + } + legacyAliasCommand = null; + } + + private static CommandMap getCommandMap() { + try { + Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); + commandMapField.setAccessible(true); + Object rawCommandMap = commandMapField.get(Bukkit.getServer()); + if (rawCommandMap instanceof CommandMap commandMap) { + return commandMap; + } + } catch (ReflectiveOperationException ignored) { + } + return null; + } + + private static PluginCommand createPluginCommand(Main plugin, String name) { + try { + Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); + constructor.setAccessible(true); + return constructor.newInstance(name, plugin); + } catch (ReflectiveOperationException e) { + plugin.getLogger().warning("Unable to construct dynamic command '/" + name + "': " + e.getMessage()); + return null; + } + } + + private List filterStartingWith(List values, String typed) { + String lower = typed.toLowerCase(Locale.ENGLISH); + List matches = new ArrayList<>(); + for (String value : values) { + if (value.toLowerCase(Locale.ENGLISH).startsWith(lower)) { + matches.add(value); + } + } + return matches; } } diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java new file mode 100644 index 0000000..2567550 --- /dev/null +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java @@ -0,0 +1,44 @@ +package ru.meloncode.xmas; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import org.bukkit.OfflinePlayer; + +import java.util.List; + +public class XMasPlaceholderExpansion extends PlaceholderExpansion { + private final Main plugin; + + public XMasPlaceholderExpansion(Main plugin) { + this.plugin = plugin; + } + + @Override + public String getIdentifier() { + return XMasPlaceholders.IDENTIFIER; + } + + @Override + public String getAuthor() { + return "1MB / mrfdev"; + } + + @Override + public String getVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public boolean persist() { + return true; + } + + @Override + public List getPlaceholders() { + return XMasPlaceholders.EXAMPLES; + } + + @Override + public String onRequest(OfflinePlayer player, String params) { + return XMasPlaceholders.resolve(plugin, player, params); + } +} diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java new file mode 100644 index 0000000..e2cafbc --- /dev/null +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java @@ -0,0 +1,127 @@ +package ru.meloncode.xmas; + +import org.bukkit.OfflinePlayer; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +final class XMasPlaceholders { + public static final String IDENTIFIER = "onembxmastree"; + public static final List EXAMPLES = Arrays.asList( + "%onembxmastree_event.active%", + "%onembxmastree_event.active_text%", + "%onembxmastree_event.status%", + "%onembxmastree_event.starts_at%", + "%onembxmastree_event.ends_at%", + "%onembxmastree_event.ends_in%", + "%onembxmastree_event.ends_timestamp%", + "%onembxmastree_event.auto_end%", + "%onembxmastree_resource.back%", + "%onembxmastree_resource.back_text%", + "%onembxmastree_particles.enabled%", + "%onembxmastree_luck.enabled%", + "%onembxmastree_luck.chance%", + "%onembxmastree_trees.total%", + "%onembxmastree_trees.owners%", + "%onembxmastree_player.trees%", + "%onembxmastree_version%" + ); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH-mm-ss"); + + private XMasPlaceholders() { + } + + public static String resolve(Main plugin, OfflinePlayer player, String params) { + if (params == null || params.trim().isEmpty()) { + return null; + } + String key = normalize(params); + return switch (key) { + case "event_active" -> Boolean.toString(Main.inProgress); + case "event_active_text" -> Main.inProgress ? "Active" : "Inactive"; + case "event_status" -> Main.inProgress ? "In Progress" : "Holidays End"; + case "event_starts_at" -> "manual"; + case "event_ends_at" -> formatEndDate(); + case "event_ends_in" -> formatDurationUntilEnd(); + case "event_ends_timestamp" -> Long.toString(Main.endTime); + case "event_auto_end" -> Boolean.toString(Main.autoEnd); + case "resource_back" -> Boolean.toString(Main.resourceBack); + case "resource_back_text" -> Main.resourceBack ? "Yes" : "No"; + case "particles_enabled" -> Boolean.toString(Main.particlesEnabled); + case "luck_enabled" -> Boolean.toString(Main.LUCK_CHANCE_ENABLED); + case "luck_chance" -> Integer.toString(Math.round(Main.LUCK_CHANCE * 100)); + case "trees_total" -> Integer.toString(XMas.getAllTrees().size()); + case "trees_owners" -> Integer.toString(countOwners(XMas.getAllTrees())); + case "player_trees" -> Integer.toString(countPlayerTrees(player)); + case "version" -> plugin.getDescription().getVersion(); + default -> null; + }; + } + + private static String normalize(String params) { + return params.trim() + .toLowerCase(Locale.ENGLISH) + .replace('.', '_') + .replace('-', '_'); + } + + private static String formatEndDate() { + if (Main.endTime <= 0) { + return "unknown"; + } + return DATE_FORMAT.format(new Date(Main.endTime)); + } + + private static String formatDurationUntilEnd() { + if (!Main.autoEnd) { + return "disabled"; + } + if (Main.endTime <= 0) { + return "unknown"; + } + long remainingMillis = Main.endTime - System.currentTimeMillis(); + if (remainingMillis <= 0) { + return "ended"; + } + + long totalSeconds = remainingMillis / 1000; + long days = totalSeconds / 86400; + long hours = (totalSeconds % 86400) / 3600; + long minutes = (totalSeconds % 3600) / 60; + if (days > 0) { + return days + "d " + hours + "h"; + } + if (hours > 0) { + return hours + "h " + minutes + "m"; + } + return minutes + "m"; + } + + private static int countOwners(Collection trees) { + Set owners = new LinkedHashSet<>(); + for (MagicTree tree : trees) { + owners.add(tree.getOwner()); + } + return owners.size(); + } + + private static int countPlayerTrees(OfflinePlayer player) { + if (player == null || player.getUniqueId() == null) { + return 0; + } + int count = 0; + for (MagicTree tree : XMas.getAllTrees()) { + if (player.getUniqueId().equals(tree.getOwner())) { + count++; + } + } + return count; + } +} diff --git a/src/main/java/ru/meloncode/xmas/utils/TextUtils.java b/src/main/java/ru/meloncode/xmas/utils/TextUtils.java index 94eeddf..28925af 100644 --- a/src/main/java/ru/meloncode/xmas/utils/TextUtils.java +++ b/src/main/java/ru/meloncode/xmas/utils/TextUtils.java @@ -1,24 +1,40 @@ package ru.meloncode.xmas.utils; -import org.apache.commons.lang.NullArgumentException; -import org.apache.commons.lang.WordUtils; import org.bukkit.Bukkit; -import org.bukkit.ChatColor; import org.bukkit.Material; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import ru.meloncode.xmas.LocaleManager; import ru.meloncode.xmas.MagicTree; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class TextUtils { + public static final String DISPLAY_NAME = "XMas Tree"; + private static final String PREFIX = "[" + DISPLAY_NAME + "] "; + private static final String CONSOLE_PREFIX = "[" + DISPLAY_NAME + "] "; - public static List generateChatReqList(MagicTree tree) { - if (tree == null) - throw new NullArgumentException("tree"); - List list = new ArrayList<>(); - list.add(ChatColor.GOLD + LocaleManager.GROW_REQ_LIST_TITLE + ":"); + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); + private static final LegacyComponentSerializer LEGACY_SECTION = LegacyComponentSerializer.legacySection(); + + public static List generateChatReqList(MagicTree tree) { + Objects.requireNonNull(tree, "tree"); + List list = new ArrayList<>(); + Component title = Component.text(LocaleManager.GROW_REQ_LIST_TITLE, NamedTextColor.GOLD) + .decorate(TextDecoration.BOLD); + if (LocaleManager.GROW_REQ_LIST_HINT != null && !LocaleManager.GROW_REQ_LIST_HINT.isBlank()) { + title = title.hoverEvent(HoverEvent.showText(parse(LocaleManager.GROW_REQ_LIST_HINT))); + } + list.add(title); if (tree.getLevel().getLevelupRequirements() != null && tree.getLevel().getLevelupRequirements().size() > 0) for (Material cMaterial : tree.getLevel().getLevelupRequirements().keySet()) { int levelReq = tree.getLevel().getLevelupRequirements().get(cMaterial); @@ -26,18 +42,71 @@ public static List generateChatReqList(MagicTree tree) { if (tree.getLevelupRequirements().containsKey(cMaterial)) treeReq = tree.getLevelupRequirements().get(cMaterial); - list.add(ChatColor.BOLD + "" + (treeReq == 0 ? ChatColor.GREEN + "" + ChatColor.STRIKETHROUGH : ChatColor.RED) + WordUtils.capitalizeFully(String.valueOf(cMaterial).replace('_', ' ') + " : " + (levelReq - treeReq + " / " + levelReq))); + NamedTextColor color = treeReq == 0 ? NamedTextColor.GREEN : NamedTextColor.RED; + Component line = Component.translatable(cMaterial.getItemTranslationKey()) + .color(color) + .decorate(TextDecoration.BOLD) + .append(Component.text(" : " + (levelReq - treeReq) + " / " + levelReq, color).decorate(TextDecoration.BOLD)); + if (treeReq == 0) { + line = line.decorate(TextDecoration.STRIKETHROUGH); + } + list.add(line); } return list; } public static void sendMessage(Player player, String message) { - if (player != null && message != null) - player.sendMessage(ChatColor.DARK_RED + "[" + ChatColor.DARK_GREEN + LocaleManager.PLUGIN_NAME + ChatColor.DARK_RED + "] " + ChatColor.RESET + message); + sendMessage((CommandSender) player, message); + } + + public static void sendMessage(Player player, Component message) { + sendMessage((CommandSender) player, message); + } + + public static void sendMessage(CommandSender sender, String message) { + if (sender != null && message != null) { + sender.sendMessage(parse(PREFIX + message)); + } + } + + public static void sendMessage(CommandSender sender, Component message) { + if (sender != null && message != null) { + sender.sendMessage(parse(PREFIX).append(message)); + } + } + + public static void sendRawMessage(CommandSender sender, String message) { + if (sender != null && message != null) { + sender.sendMessage(parse(message)); + } } public static void sendConsoleMessage(String message) { - if (message != null) - Bukkit.getConsoleSender().sendMessage(ChatColor.DARK_RED + "[" + ChatColor.DARK_GREEN + "X" + ChatColor.DARK_RED + "-" + ChatColor.DARK_GREEN + "MAS" + ChatColor.DARK_RED + "] " + ChatColor.DARK_GREEN + message); + if (message != null) { + Bukkit.getConsoleSender().sendMessage(parse(CONSOLE_PREFIX + message)); + } + } + + public static Component parse(String message) { + if (message == null) { + return Component.empty(); + } + if (message.indexOf('§') >= 0) { + return LEGACY_SECTION.deserialize(message); + } + if (message.indexOf('&') >= 0 && message.indexOf('<') < 0) { + return LEGACY_AMPERSAND.deserialize(message); + } + return MINI_MESSAGE.deserialize(message); + } + + public static List parseList(List messages) { + List components = new ArrayList<>(); + if (messages != null) { + for (String message : messages) { + components.add(parse(message)); + } + } + return components; } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a7a74ac..31f61bb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,38 +1,67 @@ core: -#Plugin will automatically disabled after date in holiday-ends if it's enabled -#You can also disable plugin by using /xmas end -#In disabled mode players cannot create new trees, but can collect their trees and get resources back (configurable) + # Master switch for the event. When false, players can no longer create or grow trees. plugin-enabled: true + + # Locale file to load from plugins/X-Mas/locales/.yml. locale: en -#Max tree count per player + + # Maximum number of magic Christmas trees a single player can own. tree-limit: 3 -#On date all trees stop to spawn presents and particles. -#Players can unbuild it and get resources back (if enabled) + + commands: + # Primary admin command is /xmastree. + # When true, the legacy /xmas alias is also registered. + # /xmastree reload applies changes without a server restart. + legacy-command-enabled: true + + # Automatic end-of-event behavior. holiday-ends: + # When true, the plugin will switch plugin-enabled to false after the date below. enabled: true -#DD.MM.YYYY HH-MM-SS - date: 10-01-2021 03-33-33 -# if true - plugin will summon chest with spent resources under tree + + # Date/time when the event ends. Format: dd-MM-yyyy HH-mm-ss. + date: 10-01-2027 03-33-33 + + # When true, destroyed or ended trees return spent upgrade resources. + # The plugin tries a chest first, then a barrel, then the player's inventory, then floor drops. resource-back: true - - #Task timer speed. All time values are scaled, so it will affect only particle-spawning. - #I don't Recommend to touch it. - #20 = 1 update / sec - #Can't be 0 + + # Main tree update interval in ticks. 20 ticks is roughly 1 second. + # Gift cooldown values are scaled by this value. Must be greater than 0. update-speed: 7 + + # Particle update interval in ticks. Keep this higher if particles become noisy. particles-delay: 35 + + # Global particle switch. Can be toggled with /xmastree debug toggle core.particles-enabled true|false. + particles-enabled: true + + sounds: + grow: + # Volume for the first accepted item of each material requirement. + # Use 0.0 for silent, 0.1 for quiet, 0.5 for half volume, and 1.0 for full volume. + first-volume: 0.5 + + # Volume for repeated accepted items of the same material requirement. + # Example: first redstone dust uses first-volume, later redstone dust uses repeat-volume. + repeat-volume: 0.2 + xmas: luck: + # When true, gift openings can fail and drop the fallback item instead. enabled: false -# Value 1-100 + + # Success chance from 1-100 when luck is enabled. chance: 75 -#Add here all nicknames of players which head you can use as one of the gift skin + + # Present head skins. Prefer official MineSkin/Mojang texture URLs. + # For safety, texture URLs must use textures.minecraft.net. Old player-name entries still work. presents: - - CruXXx - - SeerPotion -# Alternative looking -# - MHF_Present1 -# - MHF_Present2 + - http://textures.minecraft.net/texture/21bc9d42b0041e8f95cb9b26628fdaf50cd0e36f7bb9d6b3a4d2af3949da97d6 + - http://textures.minecraft.net/texture/2b1ec7dc753061ca174424ea45cf9490b39cd5dcca477d138a603e6be755ec72 + + # Gift list for presents. Use modern Paper/Bukkit material names, optionally MATERIAL:AMOUNT. + # Admins can also run /xmastree addhand to save the exact held item as a Base64 entry. gifts: - DIAMOND - EMERALD @@ -50,29 +79,124 @@ xmas: - DIAMOND_HOE - NAME_TAG + # Tree levels. gift-cooldown is in seconds before scaling by core.update-speed. + # lvlup sections define the modern material names and counts needed for the next level. + # Particle enum reference for Paper 26.1.2: + # https://jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html + # Configured particles currently support simple particles and DUST. tree-lvl: sapling: - #-1 to disable present spawning + # -1 disables present spawning for this level. gift-cooldown: -1 - #Resources to up level tree to next. + # Visual effects for this stage. Set enabled: false to disable an effect slot. + particles: + ambient: + enabled: true + particle: SPORE_BLOSSOM_AIR + offset-x: 0.35 + offset-y: 0.45 + offset-z: 0.35 + speed: 0.01 + count: 2 + swag: + enabled: false + body: + enabled: false + # Materials required to grow from sapling to small_tree. lvlup: DIAMOND: 1 REDSTONE: 10 ENDER_PEARL: 1 small_tree: + # Seconds between present spawn attempts for small trees. gift-cooldown: 300 + # Small trees get a little more ambient motion and light ornament sparkle. + particles: + ambient: + enabled: true + particle: CHERRY_LEAVES + offset-x: 1.0 + offset-y: 1.5 + offset-z: 1.0 + speed: 0.01 + count: 3 + swag: + enabled: true + particle: DUST + offset-x: 0.25 + offset-y: 0.25 + offset-z: 0.25 + speed: 0.0 + count: 8 + body: + enabled: false + # Materials required to grow from small_tree to tree. lvlup: DIAMOND: 3 GOLD_INGOT: 5 BLAZE_POWDER: 10 SNOWBALL: 30 tree: + # Seconds between present spawn attempts for full trees. gift-cooldown: 180 + # Full trees are more visible, with snow and firefly sparkle. + particles: + ambient: + enabled: true + particle: SNOWFLAKE + offset-x: 1.5 + offset-y: 3.0 + offset-z: 1.5 + speed: 0.0 + count: 8 + swag: + enabled: true + particle: FIREFLY + offset-x: 0.35 + offset-y: 0.35 + offset-z: 0.35 + speed: 0.0 + count: 4 + body: + enabled: false + # Materials required to grow from tree to magic_tree. lvlup: DIAMOND: 5 EMERALD: 3 GOLD_NUGGET: 8 GLOWSTONE_DUST: 16 magic_tree: + # Seconds between present spawn attempts for max-level magic trees. gift-cooldown: 120 - lvlup: + # Magic trees use the strongest ambient set. + particles: + ambient: + enabled: true + particle: FIREFLY + offset-x: 2.25 + offset-y: 2.25 + offset-z: 2.25 + speed: 0.0 + count: 8 + swag: + enabled: true + particle: DUST + offset-x: 0.3 + offset-y: 0.3 + offset-z: 0.3 + speed: 10.0 + count: 16 + body: + enabled: false + # Max-level trees have no next-level requirements. + lvlup: + +migration: + # Map saved world names from legacy trees.yml data to the current server world names. + # This keeps old player trees loadable even when the destination server uses different world names. + # Example: + # world-aliases: + # general: world + # wild: world + # santa: santa_event + world-aliases: {} diff --git a/src/main/resources/locales/default.yml b/src/main/resources/locales/default.yml index 0e793bf..8112fbb 100644 --- a/src/main/resources/locales/default.yml +++ b/src/main/resources/locales/default.yml @@ -17,7 +17,7 @@ #_UNUSED is a keyword to disable message. #For chat -plugin-name: X-Mas +plugin-name: XMas Tree messages: plugin-enabled: Merry Christmas And Happy New Year! @@ -26,31 +26,36 @@ messages: tree: grow-lvl-ready: Woosh! Press Right Mouse Button to let tree grow! grow-lvl-progress: _UNUSED - grow-req-list-title: Required + grow-req-list-title: Still needed + grow-req-list-hint: Right-click the tree while holding the listed ingredient items to feed them into the tree. grow-not-enough-place: Not enough place to make it grow grow-lvl-max: This tree has reached it's maximum level! tree-limit: Hey, do not be greedy! - destroy-sapling: This miracle won't drop. Are you sure? + destroy-sapling: You can cut down this tree. destroy-leaves-santa: Santa Claus sees everything. destroy-leaves-tut: To destroy a tree - cut the log. - destroy-warning: Are you sure? Your progress will be lost and no resources back. - destroy-tut: To destroy your tree cut it again. + destroy-warning: You can cut down this tree. + destroy-resource-back: Your used upgrade items will be returned for collection. + destroy-tut: Hit it again to confirm. destroy-fail-owner: It's not your tree! - destroy-complete: You're MONSTER! + destroy-complete: Your tree has been packed away for the season. gift: luck-message: _UNUSED unluck-message: Probably Santa has put you in his blacklist. I hope you're lucky next time! crystal: - name: Christmas Crystal + name: Christmas Crystal lore: - - Concentrated Christmas Spirit - - Use it on Sapling to fill it with magic! + - Concentrated Christmas Spirit + - Use it on a spruce sapling to fill it with magic! command: help: - - '&2Use &c/xmas &2to show plugin version and status' - - '&2Use &c/xmas give &2 to give crystal somebody' - - '&2Use &c/xmas gifts &2to spawn some presents under trees!' - - '&2Use &c/xmas end &2to set plugin into ending mode' - player-offline: '&cPlayer not found' - no-player-name: '&6Missing player name' - giveaway: '&cHo! &2Ho! &cHo! &3Looks like it\s time to check presents!' \ No newline at end of file + - 'Use /xmastree to show plugin version and status' + - 'Use /xmastree give player to give a player a Christmas Crystal' + - 'Use /xmastree gifts to spawn presents under all trees' + - 'Use /xmastree addhand to add your held item as a gift' + - 'Use /xmastree reload to reload the plugin config' + - 'Use /xmastree debug for paginated status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree end to end the event' + player-offline: 'Player not found' + no-player-name: 'Missing player name' + giveaway: 'Ho! Ho! Ho! Looks like its time to check presents!' diff --git a/src/main/resources/locales/en.yml b/src/main/resources/locales/en.yml index 9abaef3..17b9664 100644 --- a/src/main/resources/locales/en.yml +++ b/src/main/resources/locales/en.yml @@ -1,7 +1,7 @@ #_UNUSED is a keyword to disable message. #For chat -plugin-name: X-Mas +plugin-name: XMas Tree messages: plugin-enabled: Merry Christmas And Happy New Year! @@ -11,37 +11,42 @@ messages: tree: grow-lvl-ready: Woosh! Press Right Mouse Button to let tree grow! grow-lvl-progress: _UNUSED - grow-req-list-title: Required + grow-req-list-title: Still needed + grow-req-list-hint: Right-click the tree while holding the listed ingredient items to feed them into the tree. grow-not-enough-place: Not enough place to make it grow grow-lvl-max: This tree has reached it's maximum level! tree-limit: Hey, do not be greedy! (Tree limit) - destroy-sapling: This miracle won't drop. Are you sure? + destroy-sapling: You can cut down this tree. destroy-leaves-santa: Santa Claus sees everything. destroy-leaves-tut: To destroy a tree - cut the log. - destroy-warning: Are you sure? Your progress will be lost and no resources back. - destroy-tut: To destroy your tree cut it again. + destroy-warning: You can cut down this tree. + destroy-resource-back: Your used upgrade items will be returned for collection. + destroy-tut: Hit it again to confirm. destroy-fail-owner: It's not your tree! - destroy-complete: You're MONSTER! + destroy-complete: Your tree has been packed away for the season. gift: luck-message: _UNUSED unluck-message: Probably Santa has put you in blacklist. I hope you're lucky next time! crystal: - name: Christmas Crystal + name: Christmas Crystal lore: - - Concentrated Christmas Spirit - - Use it on Sapling to fill it with magic! + - Concentrated Christmas Spirit + - Use it on a spruce sapling to fill it with magic! help: - - 'Use /xmas to show plugin version and status' - - 'Use /xmas give to give crystal somebody' - - 'Use /xmas gifts to spawn some presents under every Christmas Tree!' - - 'Use /xmas end to set plugin into ending mode' + - 'Use /xmastree to show plugin version and status' + - 'Use /xmastree give player to give crystal somebody' + - 'Use /xmastree gifts to spawn some presents under every Christmas Tree!' + - 'Use /xmastree end to set plugin into ending mode' command: help: - - '&2Use &c/xmas &2to show plugin version and status' - - '&2Use &c/xmas give &2 to give crystal somebody' - - '&2Use &c/xmas gifts &2to spawn some presents under trees!' - - '&2Use &c/xmas end &2to set plugin into ending mode' - player-offline: '&cPlayer not found' - no-player-name: '&6Missing player name' - giveaway: '&cHo! &2Ho! &cHo! &3Looks like its time to check presents!' \ No newline at end of file + - 'Use /xmastree to show plugin version and status' + - 'Use /xmastree give player to give a player a Christmas Crystal' + - 'Use /xmastree gifts to spawn presents under all trees' + - 'Use /xmastree addhand to add your held item as a gift' + - 'Use /xmastree reload to reload the plugin config' + - 'Use /xmastree debug for paginated status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree end to end the event' + player-offline: 'Player not found' + no-player-name: 'Missing player name' + giveaway: 'Ho! Ho! Ho! Looks like its time to check presents!' diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml index aefd1c2..876eec6 100644 --- a/src/main/resources/locales/hu.yml +++ b/src/main/resources/locales/hu.yml @@ -18,7 +18,7 @@ messages: destroy-sapling: Ez a csoda nem csökken. Biztos vagy ebben? destroy-leaves-santa: Mikulás lát mindent. destroy-leaves-tut: Egy fa megsemmisítéséhez - vágd ki a fát. - destroy-warning: Biztos vagy ebben? A fejlődésed elvész, és nincs erőforrás. + destroy-warning: Biztos vagy ebben? A fejlődésed elvész. destroy-tut: A fát megsemmisíteni, vágd ki újra. destroy-fail-owner: Ez nem a te fád! destroy-complete: SZÖRNY vagy! @@ -32,16 +32,19 @@ crystal: - Használd a fa fa-csemetékhez, hogy kitöltse mágiával! help: - 'Használd /xmas a plugin verziójának és állapotának megjelenítése' - - 'Használd /xmas give , hogy valakinek kristályt adj' + - 'Használd /xmas give név, hogy valakinek kristályt adj' - 'Használd /xmas ajándékok, hogy minden karácsonyi fa alá ajándékozzon néhány ajándékot!' - 'Használd /xmas véget a bővítmény beillesztésének módjába' command: help: - - '&2Használd &c/xmas &2a plugin verziójának és állapotának megjelenítése' - - '&2Használd &c/xmas give &2, hogy valakinek kristályt adj' - - '&2Használd &c/xmas gifts &2hogy fák alatt ajándékozzon néhány ajándékot!' - - '&2Használd &c/xmas end &2a bővítmény beillesztése véges módba' - player-offline: '&cA játékos nem található' - no-player-name: '&6Hiányzó játékos név' -giveaway: '&cHo! &2Ho! &cHo! &3Úgy néz ki, itt az ideje, hogy ellenőrizze ajándékokat!' + - 'Használd /xmas a plugin verziójának és állapotának megjelenítéséhez' + - 'Használd /xmas give név, hogy kristályt adj egy játékosnak' + - 'Használd /xmas gifts, hogy ajándékok jelenjenek meg a fák alatt' + - 'Használd /xmas addhand, hogy a kézben tartott tárgy ajándék legyen' + - 'Használd /xmas reload a konfiguráció újratöltéséhez' + - 'Használd /xmas debug a lapozható státusz és kapcsolók megjelenítéséhez' + - 'Használd /xmas end az esemény lezárásához' + player-offline: 'A játékos nem található' + no-player-name: 'Hiányzó játékos név' + giveaway: 'Ho! Ho! Ho! Ideje ellenőrizni az ajándékokat!' diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml index 46baeab..5b744f6 100644 --- a/src/main/resources/locales/ru.yml +++ b/src/main/resources/locales/ru.yml @@ -17,7 +17,7 @@ messages: destroy-sapling: Это чудо не выпадет. Вы уверены? :< destroy-leaves-santa: Дед Мороз все видит! destroy-leaves-tut: Для того чтобы срубить Ёлку - руби ствол! - destroy-warning: Вы уверены? Весь прогресс будет потерян. Ресурсы возвращены не будут. + destroy-warning: Вы уверены? Весь прогресс будет потерян. destroy-tut: Чтобы срубить Ёлку разрушьте блок еще раз. destroy-fail-owner: Это не ваше дерево. destroy-complete: И не жалко?! @@ -31,10 +31,13 @@ crystal: - Используйте его на саженце чтобы сделать его волшебным. command: help: - - '&2Команда &c/xmas &2- отобразить версию плагина и статус' - - '&2Команда &c/xmas give &2<Ник> - выдать игроку кристалл' - - '&2Команда &c/xmas gifts &2- под каждой елкой появится несколько подарков!' - - '&2Команда &c/xmas end &2Переводит плагин в режим завершения праздников' - player-offline: '&6Игрок не найден' - no-player-name: '&cВы не ввели имя игрока!' - giveaway: '&aКажется время проверять подарки!' \ No newline at end of file + - 'Команда /xmas - отобразить версию плагина и статус' + - 'Команда /xmas give Ник - выдать игроку кристалл' + - 'Команда /xmas gifts - создать подарки под елками' + - 'Команда /xmas addhand - добавить предмет в руке в подарки' + - 'Команда /xmas reload - перезагрузить конфиг' + - 'Команда /xmas debug - отладочный статус, команды и переключатели' + - 'Команда /xmas end - завершить праздник' + player-offline: 'Игрок не найден' + no-player-name: 'Вы не ввели имя игрока!' + giveaway: 'Кажется время проверять подарки!' diff --git a/src/main/resources/locales/ru_santa.yml b/src/main/resources/locales/ru_santa.yml index 239cd4c..732d369 100644 --- a/src/main/resources/locales/ru_santa.yml +++ b/src/main/resources/locales/ru_santa.yml @@ -17,7 +17,7 @@ messages: destroy-sapling: Это чудо не выпадет. Вы уверены? :< destroy-leaves-santa: Санта Клаус все видит! destroy-leaves-tut: Для того чтобы срубить Рождественское дерево - руби ствол! - destroy-warning: Вы уверены? Весь прогресс будет потерян. Ресурсы возвращены не будут. + destroy-warning: Вы уверены? Весь прогресс будет потерян. destroy-tut: Чтобы срубить Рождественское дерево разрушьте блок еще раз. destroy-fail-owner: Это не ваше дерево! destroy-complete: И не жалко?! @@ -31,10 +31,13 @@ crystal: - Используйте его на саженце чтобы сделать его волшебным. command: help: - - '&2Команда &c/xmas &2- отобразить версию плагина и статус' - - '&2Команда &c/xmas give &2<Ник> - выдать игроку кристалл' - - '&2Команда &c/xmas gifts &2- под каждой елкой появится несколько подарков!' - - '&2Команда &c/xmas end &2Переводит плагин в режим завершения праздников' + - 'Команда /xmas - отобразить версию плагина и статус' + - 'Команда /xmas give Ник - выдать игроку кристалл' + - 'Команда /xmas gifts - создать подарки под елками' + - 'Команда /xmas addhand - добавить предмет в руке в подарки' + - 'Команда /xmas reload - перезагрузить конфиг' + - 'Команда /xmas debug - отладочный статус, команды и переключатели' + - 'Команда /xmas end - завершить праздник' player-offline: 'Игрок не найден' no-player-name: 'Вы не ввели имя игрока!' - giveaway: '&cХоу! &2Хоу! &cХоу! &2Кажется время проверять подарки!' \ No newline at end of file + giveaway: 'Хоу! Хоу! Хоу! Кажется время проверять подарки!' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 8bcb742..92ca44d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,10 +1,15 @@ name: X-Mas -version: ${project.version} +version: ${version} main: ru.meloncode.xmas.Main load: POSTWORLD -api-version: '1.14' -softdepend: [Multiverse-Core, PlotMe, PlotSquared, Plotz] +api-version: '1.21' +softdepend: [Multiverse-Core, PlotMe, PlotSquared, Plotz, PlaceholderAPI] commands: - xmas: - usage: Use / help to see help - permission: xmas.admin \ No newline at end of file + xmastree: + description: Manage the 1MB XMas Tree event + usage: / [help|give|gifts|addhand|reload|debug|end] + permission: xmas.admin +permissions: + xmas.admin: + description: Allows all XMas Tree admin commands + default: op From 221e404be9f26f38a26f385015eff3c62c56ee97 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 21 Apr 2026 20:58:59 +0200 Subject: [PATCH 3/8] - more commands, permissions, - placeholders, updated readme, - further testing done. - pre-release beta. Dont use in live yet --- README.md | 32 ++++-- build.gradle | 4 +- src/main/java/ru/meloncode/xmas/Events.java | 6 +- .../java/ru/meloncode/xmas/XMasCommand.java | 103 ++++++++++++++++-- .../xmas/XMasPlaceholderExpansion.java | 2 +- src/main/resources/plugin.yml | 46 +++++++- 6 files changed, 162 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f45e021..c79ac56 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The Gradle build creates the legacy reference jar and the current Paper 26.1.2 t | Jar | Purpose | | --- | --- | | `1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar` | Legacy reference jar copied from the deployed 2025 server jar. | -| `1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | +| `1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the deployed working 2025 behavior can be compared or rolled back during testing. @@ -44,7 +44,7 @@ The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the d For the 2026 target, use the modern Paper 26.1.2 jar: -- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar` +- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar` ## Building @@ -52,9 +52,9 @@ Requirements: - JDK 25 - Gradle -- The local Paper server folder in `servers/Server-Two-Paper-26.1.2` -- The local PlaceholderAPI jar in `servers/Server-Two-Paper-26.1.2/plugins` -- The deployed legacy jar in `servers/Server-One-Paper-1.21.11/plugins` if you want `legacyJar` +- The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2` for Paper API jars and local smoke testing +- The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2/plugins/PlaceholderAPI-2.12.3-DEV-265.jar` for the optional PlaceholderAPI compile-time classpath +- The deployed legacy jar in `servers/Server-One-Paper-1.21.11/plugins` is only needed if you want the `legacyJar` copy task Build the current Paper 26.1.2 jar and the legacy reference jar: @@ -80,7 +80,9 @@ Copy the deployed legacy jar into the requested legacy filename: gradle legacyJar ``` -The build compiles against the Paper 26.1.2 API jars found in `servers/Server-Two-Paper-26.1.2`. If that folder is missing or has not been started far enough for Paper to download its libraries, Gradle will not have the Paper API classpath it needs. +End users do not need the `servers/` folder. The build output jars are written to `build/libs/`, and those are the files you install on a Paper server. + +In this workspace, the current Gradle setup compiles against the Paper 26.1.2 API jars found in `servers/Server-Two-Paper-26.1.2`. If that folder is missing or has not been started far enough for Paper to download its libraries, Gradle will not have the local Paper API classpath it currently expects. ## Commands @@ -104,7 +106,17 @@ If `core.commands.legacy-command-enabled` is `true`, the legacy `/xmas` alias is | Permission | Default | Description | | --- | --- | --- | -| `xmas.admin` | `op` | Allows use of the `/xmastree` command and all XMas Tree admin subcommands. | +| `onembxmastree.admin` | `op` | Umbrella permission for all XMas Tree commands and override actions. | +| `onembxmastree.command.status` | `true` | Allows viewing `/xmastree` status output. | +| `onembxmastree.command.help` | `true` | Allows viewing `/xmastree help`. | +| `onembxmastree.command.give` | `op` | Allows `/xmastree give `. | +| `onembxmastree.command.gifts` | `op` | Allows `/xmastree gifts`. | +| `onembxmastree.command.addhand` | `op` | Allows `/xmastree addhand`. | +| `onembxmastree.command.reload` | `op` | Allows `/xmastree reload`. | +| `onembxmastree.command.debug` | `op` | Allows `/xmastree debug [page]`. | +| `onembxmastree.command.debug.toggle` | `op` | Allows `/xmastree debug toggle true\|false`. | +| `onembxmastree.command.end` | `op` | Allows `/xmastree end`. | +| `onembxmastree.tree.override` | `op` | Allows managing other players' trees. | ## Player flow @@ -199,7 +211,7 @@ The dotted key after `onembxmastree_` is supported to keep the placeholders read | `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | | `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | | `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | -| `%onembxmastree_version%` | `2.0.1-010` | Loaded plugin version. | +| `%onembxmastree_version%` | `2.0.1-011` | Loaded plugin version. | CMI hologram example: @@ -229,7 +241,7 @@ ajLeaderboards placeholder examples: ## Security notes -- Admin commands are gated by `xmas.admin`. +- Admin and staff access is gated by `onembxmastree.*` permissions. - Present texture URLs are restricted to `textures.minecraft.net`. - Gift item Base64 entries are capped before deserialization. - Config material names are resolved with modern `Material.matchMaterial` and invalid or legacy values are skipped. @@ -247,7 +259,7 @@ Please report bugs, compatibility problems, and upgrade questions in the GitHub - **Ghost_chu** - NMS fixes - [Ghost-chu](https://github.com/Ghost-chu) - **LoneDev6** - Optimization patches - [LoneDev6](https://github.com/LoneDev6) - **montlikadani** - Hungarian translation - [montlikadani](https://github.com/montlikadani) -- **1MB / mrfdev** - 2026 Paper modernization, Java 25 builds, and XMasTree maintenance +- **mrfloris** - 2026 Paper modernization, Java 25 builds, and XMasTree maintenance - [mrfloris](https://github.com/mrfloris) Original SpigotMC listing: diff --git a/build.gradle b/build.gradle index 70258e1..cc34b26 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,10 @@ plugins { } group = 'com.onemb.xmas' -version = '2.0.1-010' +version = '2.0.1-011' def legacyArchiveName = '1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar' -def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-010-v25-26.1.2.jar' +def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar' def serverOne = layout.projectDirectory.dir('servers/Server-One-Paper-1.21.11') def serverTwo = layout.projectDirectory.dir('servers/Server-Two-Paper-26.1.2') diff --git a/src/main/java/ru/meloncode/xmas/Events.java b/src/main/java/ru/meloncode/xmas/Events.java index e0d3b60..604ca7d 100644 --- a/src/main/java/ru/meloncode/xmas/Events.java +++ b/src/main/java/ru/meloncode/xmas/Events.java @@ -217,7 +217,7 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { MagicTree tree = MagicTree.getTreeByBlock(block); switch (block.getType()) { case SPRUCE_LOG: - if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { + if (player.getUniqueId().equals(tree.getOwner()) || XMasCommand.canOverrideTree(player)) { if (Main.inProgress) if (destroyers.containsKey(player.getUniqueId()) && System.currentTimeMillis() - destroyers.get(player.getUniqueId()) <= 10000) { if (Main.resourceBack) { @@ -247,14 +247,14 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { case GLOWSTONE: if (Main.inProgress) TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_SANTA); - if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { + if (player.getUniqueId().equals(tree.getOwner()) || XMasCommand.canOverrideTree(player)) { TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_TUT); } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); } break; case SPRUCE_SAPLING: - if (player.getUniqueId().equals(tree.getOwner()) || player.hasPermission("xmas.admin")) { + if (player.getUniqueId().equals(tree.getOwner()) || XMasCommand.canOverrideTree(player)) { if (Main.inProgress) { if (destroyers.containsKey(player.getUniqueId()) && System.currentTimeMillis() - destroyers.get(player.getUniqueId()) <= 10000) { if (Main.resourceBack) { diff --git a/src/main/java/ru/meloncode/xmas/XMasCommand.java b/src/main/java/ru/meloncode/xmas/XMasCommand.java index fd41d14..05dd0cd 100644 --- a/src/main/java/ru/meloncode/xmas/XMasCommand.java +++ b/src/main/java/ru/meloncode/xmas/XMasCommand.java @@ -20,6 +20,17 @@ public class XMasCommand implements CommandExecutor, TabCompleter { public static final String PRIMARY_COMMAND = "xmastree"; public static final String LEGACY_COMMAND = "xmas"; + public static final String PERMISSION_ADMIN = "onembxmastree.admin"; + public static final String PERMISSION_STATUS = "onembxmastree.command.status"; + public static final String PERMISSION_HELP = "onembxmastree.command.help"; + public static final String PERMISSION_GIVE = "onembxmastree.command.give"; + public static final String PERMISSION_GIFTS = "onembxmastree.command.gifts"; + public static final String PERMISSION_ADDHAND = "onembxmastree.command.addhand"; + public static final String PERMISSION_RELOAD = "onembxmastree.command.reload"; + public static final String PERMISSION_DEBUG = "onembxmastree.command.debug"; + public static final String PERMISSION_DEBUG_TOGGLE = "onembxmastree.command.debug.toggle"; + public static final String PERMISSION_END = "onembxmastree.command.end"; + public static final String PERMISSION_TREE_OVERRIDE = "onembxmastree.tree.override"; private static final List COMMANDS = Arrays.asList("help", "give", "end", "gifts", "reload", "addhand", "debug"); private static final Set DEBUG_TOGGLE_KEYS = new LinkedHashSet<>(Arrays.asList( "core.commands.legacy-command-enabled", @@ -29,6 +40,7 @@ public class XMasCommand implements CommandExecutor, TabCompleter { "core.particles-enabled", "xmas.luck.enabled" )); + private static final Map PERMISSIONS = createPermissionDescriptions(); private static final int DEBUG_PAGE_SIZE = 8; private static XMasCommand registeredExecutor; private static PluginCommand legacyAliasCommand; @@ -65,12 +77,20 @@ public boolean onCommand(CommandSender sender, Command command, String label, St String action = args[0].toLowerCase(Locale.ENGLISH); switch (action) { case "help": { + if (!hasPermission(sender, PERMISSION_HELP)) { + sendNoPermission(sender); + break; + } for (String line : getHelpLines()) { TextUtils.sendRawMessage(sender, line); } break; } case "give": { + if (!hasPermission(sender, PERMISSION_GIVE)) { + sendNoPermission(sender); + break; + } if (args.length > 1) { String name = args[1]; Player player = Bukkit.getPlayer(name); @@ -85,10 +105,18 @@ public boolean onCommand(CommandSender sender, Command command, String label, St break; } case "end": { + if (!hasPermission(sender, PERMISSION_END)) { + sendNoPermission(sender); + break; + } plugin.end(); break; } case "gifts": { + if (!hasPermission(sender, PERMISSION_GIFTS)) { + sendNoPermission(sender); + break; + } Random random = new Random(); for (MagicTree magicTree : XMas.getAllTrees()) { for (int i = 0; i < 3 + random.nextInt(4); i++) { @@ -99,8 +127,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St break; } case "reload": { - if (!sender.hasPermission("xmas.admin")) { - TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + if (!hasPermission(sender, PERMISSION_RELOAD)) { + sendNoPermission(sender); break; } plugin.reloadPluginConfig(); @@ -112,8 +140,8 @@ public boolean onCommand(CommandSender sender, Command command, String label, St TextUtils.sendRawMessage(sender, "Only players can use this command."); break; } - if (!sender.hasPermission("xmas.admin")) { - TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + if (!hasPermission(sender, PERMISSION_ADDHAND)) { + sendNoPermission(sender); break; } ItemStack item = player.getInventory().getItemInMainHand(); @@ -134,7 +162,11 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return false; } } else { - sendStatus(sender); + if (!hasPermission(sender, PERMISSION_STATUS)) { + sendNoPermission(sender); + } else { + sendStatus(sender); + } } return true; } @@ -145,7 +177,7 @@ public List onTabComplete(CommandSender sender, Command command, String if (args.length == 1) { String typed = args[0].toLowerCase(Locale.ENGLISH); for (String subCommand : COMMANDS) { - if (subCommand.startsWith(typed)) { + if (subCommand.startsWith(typed) && canUseSubCommand(sender, subCommand)) { suggestions.add(subCommand); } } @@ -199,13 +231,17 @@ private List getStatusLines() { } private void handleDebug(CommandSender sender, String[] args) { - if (!sender.hasPermission("xmas.admin")) { - TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + if (args.length >= 2 && args[1].equalsIgnoreCase("toggle")) { + if (!hasPermission(sender, PERMISSION_DEBUG_TOGGLE)) { + sendNoPermission(sender); + return; + } + handleDebugToggle(sender, args); return; } - if (args.length >= 2 && args[1].equalsIgnoreCase("toggle")) { - handleDebugToggle(sender, args); + if (!hasPermission(sender, PERMISSION_DEBUG)) { + sendNoPermission(sender); return; } @@ -263,7 +299,9 @@ private void sendDebugPage(CommandSender sender, int requestedPage) { } lines.add(""); lines.add("Permissions"); - lines.add("xmas.admin - allows all " + TextUtils.DISPLAY_NAME + " admin commands"); + for (Map.Entry permission : PERMISSIONS.entrySet()) { + lines.add("" + permission.getKey() + " - " + permission.getValue()); + } lines.add(""); lines.add("Placeholders"); lines.add("Requires PlaceholderAPI. Use '_' after prefix, then dotted keys."); @@ -312,6 +350,10 @@ private boolean isLegacyAliasEnabled() { return plugin.getConfig().getBoolean("core.commands.legacy-command-enabled", true); } + public static boolean canOverrideTree(CommandSender sender) { + return hasPermission(sender, PERMISSION_TREE_OVERRIDE); + } + private static void syncLegacyAlias(Main plugin, XMasCommand executor) { CommandMap commandMap = getCommandMap(); if (commandMap == null) { @@ -346,7 +388,7 @@ private static void syncLegacyAlias(Main plugin, XMasCommand executor) { } aliasCommand.setDescription("Legacy alias for /" + PRIMARY_COMMAND); aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|debug|end]"); - aliasCommand.setPermission("xmas.admin"); + aliasCommand.setPermission(null); aliasCommand.setExecutor(executor); aliasCommand.setTabCompleter(executor); commandMap.register(plugin.getDescription().getName().toLowerCase(Locale.ENGLISH), aliasCommand); @@ -420,4 +462,41 @@ private List filterStartingWith(List values, String typed) { return matches; } + private boolean canUseSubCommand(CommandSender sender, String subCommand) { + return switch (subCommand.toLowerCase(Locale.ENGLISH)) { + case "help" -> hasPermission(sender, PERMISSION_HELP); + case "give" -> hasPermission(sender, PERMISSION_GIVE); + case "end" -> hasPermission(sender, PERMISSION_END); + case "gifts" -> hasPermission(sender, PERMISSION_GIFTS); + case "reload" -> hasPermission(sender, PERMISSION_RELOAD); + case "addhand" -> hasPermission(sender, PERMISSION_ADDHAND); + case "debug" -> hasPermission(sender, PERMISSION_DEBUG) || hasPermission(sender, PERMISSION_DEBUG_TOGGLE); + default -> false; + }; + } + + private static boolean hasPermission(CommandSender sender, String permission) { + return sender.hasPermission(PERMISSION_ADMIN) || sender.hasPermission(permission); + } + + private void sendNoPermission(CommandSender sender) { + TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + } + + private static Map createPermissionDescriptions() { + Map permissions = new LinkedHashMap<>(); + permissions.put(PERMISSION_ADMIN, "allows all " + TextUtils.DISPLAY_NAME + " commands and overrides"); + permissions.put(PERMISSION_STATUS, "shows /" + PRIMARY_COMMAND + " status output"); + permissions.put(PERMISSION_HELP, "shows /" + PRIMARY_COMMAND + " help output"); + permissions.put(PERMISSION_GIVE, "allows /" + PRIMARY_COMMAND + " give"); + permissions.put(PERMISSION_GIFTS, "allows /" + PRIMARY_COMMAND + " gifts"); + permissions.put(PERMISSION_ADDHAND, "allows /" + PRIMARY_COMMAND + " addhand"); + permissions.put(PERMISSION_RELOAD, "allows /" + PRIMARY_COMMAND + " reload"); + permissions.put(PERMISSION_DEBUG, "allows /" + PRIMARY_COMMAND + " debug"); + permissions.put(PERMISSION_DEBUG_TOGGLE, "allows /" + PRIMARY_COMMAND + " debug toggle"); + permissions.put(PERMISSION_END, "allows /" + PRIMARY_COMMAND + " end"); + permissions.put(PERMISSION_TREE_OVERRIDE, "allows managing other players' trees"); + return permissions; + } + } diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java index 2567550..f9f26f5 100644 --- a/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java @@ -19,7 +19,7 @@ public String getIdentifier() { @Override public String getAuthor() { - return "1MB / mrfdev"; + return "mrfloris"; } @Override diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 92ca44d..781a596 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -8,8 +8,48 @@ commands: xmastree: description: Manage the 1MB XMas Tree event usage: / [help|give|gifts|addhand|reload|debug|end] - permission: xmas.admin permissions: - xmas.admin: - description: Allows all XMas Tree admin commands + onembxmastree.admin: + description: Allows all XMas Tree commands and overrides + default: op + children: + onembxmastree.command.status: true + onembxmastree.command.help: true + onembxmastree.command.give: true + onembxmastree.command.gifts: true + onembxmastree.command.addhand: true + onembxmastree.command.reload: true + onembxmastree.command.debug: true + onembxmastree.command.debug.toggle: true + onembxmastree.command.end: true + onembxmastree.tree.override: true + onembxmastree.command.status: + description: Allows viewing /xmastree status + default: true + onembxmastree.command.help: + description: Allows viewing /xmastree help + default: true + onembxmastree.command.give: + description: Allows /xmastree give + default: op + onembxmastree.command.gifts: + description: Allows /xmastree gifts + default: op + onembxmastree.command.addhand: + description: Allows /xmastree addhand + default: op + onembxmastree.command.reload: + description: Allows /xmastree reload + default: op + onembxmastree.command.debug: + description: Allows /xmastree debug + default: op + onembxmastree.command.debug.toggle: + description: Allows /xmastree debug toggle + default: op + onembxmastree.command.end: + description: Allows /xmastree end + default: op + onembxmastree.tree.override: + description: Allows managing other players' trees default: op From 2d6a2ebc4c932a414026c0432acd20c0051a214a Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 21 Apr 2026 21:55:49 +0200 Subject: [PATCH 4/8] Modernize XMas Tree for Paper 26.1.2 / Java 25 - move the project from the legacy deployed build to an actively maintained Paper 26.1.2 / Java 25 Gradle build - simplify the build around the active Paper 26.1.2 target and remove the retired 1.21.11 local server dependency - keep build output clean and predictable in build/libs with the new 2026 versioned jar naming - keep legacy tree data compatible by continuing to read plugins/X-Mas/trees.yml - add world alias migration support so old saved trees can survive renamed worlds - preserve old event data while modernizing the runtime and admin tooling - make /xmastree the primary command - keep /xmas as an optional legacy alias controlled by config - fix legacy alias reload/unregister behavior so reload no longer crashes when alias settings change - improve /xmastree help output and keep it aligned with the actual command surface - add granular permissions under onembxmastree.* - replace the old xmas.admin permission with onembxmastree.admin - add separate permissions for status, help, give, gifts, addhand, reload, debug, debug.toggle, end, and tree.override - add a modern debug system with named categories: status, commands, permissions, placeholders, and config - keep numeric debug pages working as a legacy shortcut - improve debug output formatting with clearer key/value coloring - make invalid debug page/section requests return a helpful response instead of silently falling back - add /xmastree debug toggle true|false for live boolean config changes - keep tab completion focused on named debug categories instead of numeric page suggestions - add optional PlaceholderAPI support with the onembxmastree namespace - add placeholders for event state, end time, end countdown, auto-end, resource-back, particles, luck, tree totals, owner totals, player tree count, and plugin version - document placeholders in the README and show them in debug output - modernize message handling with MiniMessage support while keeping legacy color compatibility - improve player-facing text, prefixes, debug output, and help text - change the visible plugin/chat identity toward XMas Tree for clearer user-facing output - make the Christmas Crystal display name non-italic - fix resource-back so destroying a tree returns only the materials actually spent on that tree - fix the old refund dupe issue where the plugin could return more than the player had used - improve refund delivery with fallback order: chest -> barrel -> player inventory -> floor drops - reduce the loud grow/ingredient sound behavior - make first-hit and repeat-hit grow sound volumes configurable and reloadable - support silent/quiet/loud tuning through config without server restarts - modernize material and item handling for current Paper names - use safer material matching/validation to avoid legacy enum failures - improve displayed item names so materials such as Redstone Dust render properly in requirement output - add configurable per-stage particle effects using modern Paper particle names - harden config and item parsing: restrict present texture URLs to textures.minecraft.net cap Base64 gift payload handling skip invalid or legacy material names safely - update config comments and improve documentation for installation, building, commands, permissions, placeholders, support, and credits - point support to the GitHub issues page - refresh .gitignore for local dev/test folders and obvious OS/build junk i consider this a tested and worth public beta that can be used to figure out any bugs, report them on github as an issue. december is not around the corner, so we have a summer to find issues. Enjoy. --- README.md | 62 +++-- build.gradle | 22 +- .../java/ru/meloncode/xmas/XMasCommand.java | 233 +++++++++++++----- .../ru/meloncode/xmas/XMasPlaceholders.java | 25 ++ src/main/resources/locales/default.yml | 7 +- src/main/resources/locales/en.yml | 7 +- src/main/resources/plugin.yml | 2 +- 7 files changed, 256 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index c79ac56..60bf4b8 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,15 @@ This fork keeps the old X-Mas event data usable for winter 2026 while moving the ![X-Mas tree preview](http://puu.sh/dKlK1/85c3dad454.jpg) -## Current targets +## Current target -The Gradle build creates the legacy reference jar and the current Paper 26.1.2 target jar in `build/libs`: +The Gradle build creates the current Paper 26.1.2 target jar in `build/libs`: | Jar | Purpose | | --- | --- | -| `1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar` | Legacy reference jar copied from the deployed 2025 server jar. | -| `1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | +| `1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | -The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the deployed working 2025 behavior can be compared or rolled back during testing. +The checked-in source targets Paper 26.1.2 only. ## Features @@ -28,7 +27,7 @@ The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the d - Existing `plugins/X-Mas/trees.yml` data remains the event data source. - Optional resource refunds when a tree is destroyed or cleaned up after the event. - Configurable per-stage particles using Paper 26.1.2 particle names. -- `/xmastree debug` pages for status, commands, permissions, placeholders, and global boolean toggles. +- `/xmastree debug` sections for `status`, `commands`, `permissions`, `placeholders`, and `config`, plus live global boolean toggles. - Primary `/xmastree` command with an optional legacy `/xmas` alias. - Optional PlaceholderAPI placeholders for CMI holograms, ajLeaderboards, scoreboards, and menus. - Legacy `trees.yml` world-name alias support for renamed destination worlds. @@ -44,7 +43,7 @@ The checked-in source targets Paper 26.1.2. The legacy jar is preserved so the d For the 2026 target, use the modern Paper 26.1.2 jar: -- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar` +- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar` ## Building @@ -54,9 +53,8 @@ Requirements: - Gradle - The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2` for Paper API jars and local smoke testing - The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2/plugins/PlaceholderAPI-2.12.3-DEV-265.jar` for the optional PlaceholderAPI compile-time classpath -- The deployed legacy jar in `servers/Server-One-Paper-1.21.11/plugins` is only needed if you want the `legacyJar` copy task -Build the current Paper 26.1.2 jar and the legacy reference jar: +Build the current Paper 26.1.2 jar: ```bash gradle clean buildAllJars @@ -74,12 +72,6 @@ The `paper2612Jar` task is kept as an alias: gradle paper2612Jar ``` -Copy the deployed legacy jar into the requested legacy filename: - -```bash -gradle legacyJar -``` - End users do not need the `servers/` folder. The build output jars are written to `build/libs/`, and those are the files you install on a Paper server. In this workspace, the current Gradle setup compiles against the Paper 26.1.2 API jars found in `servers/Server-Two-Paper-26.1.2`. If that folder is missing or has not been started far enough for Paper to download its libraries, Gradle will not have the local Paper API classpath it currently expects. @@ -98,10 +90,44 @@ If `core.commands.legacy-command-enabled` is `true`, the legacy `/xmas` alias is | `/xmastree gifts` | Spawns a small batch of presents under every loaded Christmas tree. | | `/xmastree addhand` | Adds the item in your main hand to the gift list and saves it to `config.yml`. | | `/xmastree reload` | Reloads config, locale, present heads, gifts, luck settings, command alias settings, and tree level requirements. | -| `/xmastree debug [page]` | Shows paginated status, commands, permissions, placeholders, and toggleable global config keys. | +| `/xmastree debug` | Opens the `status` debug section by default. | +| `/xmastree debug [section\|page]` | Shows debug output for `status`, `commands`, `permissions`, `placeholders`, or `config`. Numeric pages `1-5` still work as a legacy shortcut. | | `/xmastree debug toggle true\|false` | Toggles supported global boolean config keys and reloads the plugin config. | | `/xmastree end` | Ends the event and sets `core.plugin-enabled` to `false`. | +### Debug sections + +The preferred debug syntax is category-based: + +| Section | Example | Purpose | +| --- | --- | --- | +| `status` | `/xmastree debug status` | Event state, end date, auto-end, refund state, particles, loaded tree count, and owner count. | +| `commands` | `/xmastree debug commands` | Command list, debug syntax, and legacy alias state. | +| `permissions` | `/xmastree debug permissions` | All registered `onembxmastree.*` permissions and what they allow. | +| `placeholders` | `/xmastree debug placeholders` | All built-in PlaceholderAPI placeholders plus their descriptions. | +| `config` | `/xmastree debug config` | The current values of the toggleable global config keys. | + +Numeric compatibility remains available for existing habits and old screenshots: + +| Page | Section | +| --- | --- | +| `1` | `status` | +| `2` | `commands` | +| `3` | `permissions` | +| `4` | `placeholders` | +| `5` | `config` | + +### Debug toggle keys + +`/xmastree debug toggle true|false` currently supports: + +- `core.commands.legacy-command-enabled` +- `core.plugin-enabled` +- `core.holiday-ends.enabled` +- `core.holiday-ends.resource-back` +- `core.particles-enabled` +- `xmas.luck.enabled` + ## Permissions | Permission | Default | Description | @@ -113,7 +139,7 @@ If `core.commands.legacy-command-enabled` is `true`, the legacy `/xmas` alias is | `onembxmastree.command.gifts` | `op` | Allows `/xmastree gifts`. | | `onembxmastree.command.addhand` | `op` | Allows `/xmastree addhand`. | | `onembxmastree.command.reload` | `op` | Allows `/xmastree reload`. | -| `onembxmastree.command.debug` | `op` | Allows `/xmastree debug [page]`. | +| `onembxmastree.command.debug` | `op` | Allows `/xmastree debug [section\|page]`. | | `onembxmastree.command.debug.toggle` | `op` | Allows `/xmastree debug toggle true\|false`. | | `onembxmastree.command.end` | `op` | Allows `/xmastree end`. | | `onembxmastree.tree.override` | `op` | Allows managing other players' trees. | @@ -211,7 +237,7 @@ The dotted key after `onembxmastree_` is supported to keep the placeholders read | `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | | `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | | `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | -| `%onembxmastree_version%` | `2.0.1-011` | Loaded plugin version. | +| `%onembxmastree_version%` | `2.0.1-021` | Loaded plugin version. | CMI hologram example: diff --git a/build.gradle b/build.gradle index cc34b26..e41b8e4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,10 @@ plugins { } group = 'com.onemb.xmas' -version = '2.0.1-011' +version = '2.0.1-021' +def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar' -def legacyArchiveName = '1MB-XMas-2026-v2.0.0-004-v21-1.21.8.jar' -def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-011-v25-26.1.2.jar' - -def serverOne = layout.projectDirectory.dir('servers/Server-One-Paper-1.21.11') def serverTwo = layout.projectDirectory.dir('servers/Server-Two-Paper-26.1.2') -def legacyServerJar = serverOne.file('plugins/1MB-X-Mas_2025-1.21.8.jar') def placeholderApiJar = serverTwo.file('plugins/PlaceholderAPI-2.12.3-DEV-265.jar') def paper2612Api = serverTwo.file('libraries/io/papermc/paper/paper-api/26.1.2.build.18-alpha/paper-api-26.1.2.build.18-alpha.jar') @@ -50,20 +46,12 @@ tasks.register('paper2612Jar') { dependsOn tasks.named('jar') } -tasks.register('legacyJar', Copy) { - description = 'Copies the deployed legacy jar into the requested 2026 legacy filename.' - group = 'build' - from(legacyServerJar) - into(layout.buildDirectory.dir('libs')) - rename { legacyArchiveName } -} - tasks.register('buildAllJars') { - description = 'Builds the legacy reference jar plus the modern Paper 26.1.2 target jar.' + description = 'Builds the current Paper 26.1.2 target jar.' group = 'build' - dependsOn tasks.named('legacyJar'), tasks.named('jar') + dependsOn tasks.named('jar') } tasks.named('assemble') { - dependsOn tasks.named('legacyJar'), tasks.named('paper2612Jar') + dependsOn tasks.named('paper2612Jar') } diff --git a/src/main/java/ru/meloncode/xmas/XMasCommand.java b/src/main/java/ru/meloncode/xmas/XMasCommand.java index 05dd0cd..1d6973a 100644 --- a/src/main/java/ru/meloncode/xmas/XMasCommand.java +++ b/src/main/java/ru/meloncode/xmas/XMasCommand.java @@ -40,8 +40,8 @@ public class XMasCommand implements CommandExecutor, TabCompleter { "core.particles-enabled", "xmas.luck.enabled" )); + private static final Map DEBUG_SECTIONS = createDebugSections(); private static final Map PERMISSIONS = createPermissionDescriptions(); - private static final int DEBUG_PAGE_SIZE = 8; private static XMasCommand registeredExecutor; private static PluginCommand legacyAliasCommand; @@ -190,7 +190,11 @@ public List onTabComplete(CommandSender sender, Command command, String } } else if (args[0].equalsIgnoreCase("debug")) { if (args.length == 2) { - suggestions.addAll(filterStartingWith(Arrays.asList("1", "2", "3", "toggle"), args[1])); + List debugSuggestions = new ArrayList<>(DEBUG_SECTIONS.keySet()); + if (hasPermission(sender, PERMISSION_DEBUG_TOGGLE)) { + debugSuggestions.add("toggle"); + } + suggestions.addAll(filterStartingWith(debugSuggestions, args[1])); } else if (args.length == 3 && args[1].equalsIgnoreCase("toggle")) { suggestions.addAll(filterStartingWith(new ArrayList<>(DEBUG_TOGGLE_KEYS), args[2])); } else if (args.length == 4 && args[1].equalsIgnoreCase("toggle")) { @@ -216,17 +220,20 @@ private List getStatusLines() { SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); List lines = new ArrayList<>(); - lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getDescription().getVersion() + " Plugin Status"); + lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getDescription().getVersion() + " Plugin Status"); lines.add(""); - lines.add("Event Status: " + (Main.inProgress ? "In Progress" : "Holidays End")); + lines.add(formatKeyValue("Event Status", Main.inProgress ? "In Progress" : "Holidays End")); if (Main.inProgress) { - lines.add("Current Time: " + sdf.format(System.currentTimeMillis())); - lines.add("Holidays end: " + sdf.format(Main.endTime)); + lines.add(formatKeyValue("Current Time", "" + sdf.format(System.currentTimeMillis()) + "")); + lines.add(formatKeyValue("Holidays End", "" + sdf.format(Main.endTime) + "")); } - lines.add("Auto-End: " + (Main.autoEnd ? "Yes" : "No") + " | Resource Back: " + (Main.resourceBack ? "Yes" : "No") + " | Particles: " + (Main.particlesEnabled ? "Yes" : "No")); + lines.add(formatKeyValue("Auto-End", booleanValue(Main.autoEnd))); + lines.add(formatKeyValue("Resource Back", booleanValue(Main.resourceBack))); + lines.add(formatKeyValue("Particles", booleanValue(Main.particlesEnabled))); lines.add(""); - lines.add("There are " + treeCount + " magic trees owned by " + owners.size() + " players"); - lines.add("Use " + commandPath("help") + " for command list"); + lines.add(formatKeyValue("Loaded Trees", "" + treeCount + "")); + lines.add(formatKeyValue("Tree Owners", "" + owners.size() + "")); + lines.add(formatKeyValue("Help", "" + commandPath("help") + "")); return lines; } @@ -245,32 +252,42 @@ private void handleDebug(CommandSender sender, String[] args) { return; } - int page = 1; - if (args.length >= 2) { - try { - page = Integer.parseInt(args[1]); - } catch (NumberFormatException ignored) { - page = 1; - } + if (args.length < 2) { + sendDebugSection(sender, "status"); + return; } - sendDebugPage(sender, page); + + String requested = args[1]; + try { + int page = Integer.parseInt(requested); + sendDebugPage(sender, page); + return; + } catch (NumberFormatException ignored) { + } + + String section = normalizeDebugSection(requested); + if (section == null) { + sendInvalidDebugSelection(sender, requested, DEBUG_SECTIONS.size()); + return; + } + sendDebugSection(sender, section); } private void handleDebugToggle(CommandSender sender, String[] args) { if (args.length < 4) { - TextUtils.sendRawMessage(sender, "Usage: " + commandPath("debug toggle true|false")); - TextUtils.sendRawMessage(sender, "Keys: " + String.join(", ", DEBUG_TOGGLE_KEYS)); + TextUtils.sendRawMessage(sender, formatKeyValue("Usage", "" + commandPath("debug toggle true|false") + "")); + TextUtils.sendRawMessage(sender, formatKeyValue("Keys", "" + String.join(", ", DEBUG_TOGGLE_KEYS) + "")); return; } String key = args[2].toLowerCase(Locale.ENGLISH); if (!DEBUG_TOGGLE_KEYS.contains(key)) { - TextUtils.sendRawMessage(sender, "Unknown toggle key: " + args[2]); - TextUtils.sendRawMessage(sender, "Keys: " + String.join(", ", DEBUG_TOGGLE_KEYS)); + TextUtils.sendRawMessage(sender, formatKeyValue("Unknown Toggle Key", "" + args[2] + "")); + TextUtils.sendRawMessage(sender, formatKeyValue("Keys", "" + String.join(", ", DEBUG_TOGGLE_KEYS) + "")); return; } if (!args[3].equalsIgnoreCase("true") && !args[3].equalsIgnoreCase("false")) { - TextUtils.sendRawMessage(sender, "Value must be true or false."); + TextUtils.sendRawMessage(sender, formatKeyValue("Value", "must be true or false")); return; } @@ -278,60 +295,119 @@ private void handleDebugToggle(CommandSender sender, String[] args) { plugin.getConfig().set(key, value); plugin.saveConfig(); plugin.reloadPluginConfig(); - TextUtils.sendRawMessage(sender, "Set " + key + " to " + value + " and reloaded " + TextUtils.DISPLAY_NAME + "."); + TextUtils.sendRawMessage(sender, formatKeyValue("Updated", "" + key + " -> " + booleanValue(value))); } - private void sendDebugPage(CommandSender sender, int requestedPage) { - List lines = new ArrayList<>(getStatusLines()); - lines.add(""); - lines.add("Commands"); - lines.add("" + commandPath("") + " - status"); - lines.add("" + commandPath("help") + " - command list"); - lines.add("" + commandPath("give ") + " - give a Christmas Crystal"); - lines.add("" + commandPath("gifts") + " - spawn presents under all trees"); - lines.add("" + commandPath("addhand") + " - add held item to gifts"); - lines.add("" + commandPath("reload") + " - reload config and locale"); - lines.add("" + commandPath("end") + " - end the event"); - lines.add("" + commandPath("debug [page]") + " - extended debug output"); - lines.add("" + commandPath("debug toggle true|false") + " - toggle global booleans"); + private LinkedHashMap> buildDebugSections() { + LinkedHashMap> sections = new LinkedHashMap<>(); + sections.put("status", getStatusLines()); + + List commandsPage = new ArrayList<>(); + commandsPage.add(""); + commandsPage.add(formatSectionTitle("Commands")); + commandsPage.add(formatListEntry(commandPath(""), "status")); + commandsPage.add(formatListEntry(commandPath("help"), "command list")); + commandsPage.add(formatListEntry(commandPath("give "), "give a Christmas Crystal")); + commandsPage.add(formatListEntry(commandPath("gifts"), "spawn presents under all trees")); + commandsPage.add(formatListEntry(commandPath("addhand"), "add held item to gifts")); + commandsPage.add(formatListEntry(commandPath("reload"), "reload config and locale")); + commandsPage.add(formatListEntry(commandPath("end"), "end the event")); + commandsPage.add(formatListEntry(commandPath("debug"), "open the status debug section")); + commandsPage.add(formatListEntry(commandPath("debug [section|page]"), "extended debug output by category")); + commandsPage.add(formatListEntry(commandPath("debug toggle true|false"), "toggle global booleans")); if (isLegacyAliasEnabled()) { - lines.add("Legacy alias enabled: /" + LEGACY_COMMAND); + commandsPage.add(formatKeyValue("Legacy Alias", "/" + LEGACY_COMMAND + "")); } - lines.add(""); - lines.add("Permissions"); + sections.put("commands", commandsPage); + + List permissionsPage = new ArrayList<>(); + permissionsPage.add(""); + permissionsPage.add(formatSectionTitle("Permissions")); for (Map.Entry permission : PERMISSIONS.entrySet()) { - lines.add("" + permission.getKey() + " - " + permission.getValue()); + permissionsPage.add(formatListEntry(permission.getKey(), permission.getValue())); } - lines.add(""); - lines.add("Placeholders"); - lines.add("Requires PlaceholderAPI. Use '_' after prefix, then dotted keys."); + sections.put("permissions", permissionsPage); + + List placeholdersPage = new ArrayList<>(); + placeholdersPage.add(""); + placeholdersPage.add(formatSectionTitle("Placeholders")); + placeholdersPage.add(formatKeyValue("Notes", "Requires PlaceholderAPI. Use '_' after prefix, then dotted keys.")); for (String placeholder : XMasPlaceholders.EXAMPLES) { - lines.add("" + placeholder); + placeholdersPage.add(formatListEntry(placeholder, XMasPlaceholders.DESCRIPTIONS.getOrDefault(placeholder, "registered placeholder"))); } - lines.add(""); - lines.add("Toggleable Config Keys"); + sections.put("placeholders", placeholdersPage); + + List togglesPage = new ArrayList<>(); + togglesPage.add(""); + togglesPage.add(formatSectionTitle("Toggleable Config Keys")); for (String key : DEBUG_TOGGLE_KEYS) { - lines.add("" + key + " = " + plugin.getConfig().getBoolean(key)); + togglesPage.add(formatKeyValue(key, booleanValue(plugin.getConfig().getBoolean(key)))); } + sections.put("config", togglesPage); - int pages = Math.max(1, (int) Math.ceil((double) lines.size() / DEBUG_PAGE_SIZE)); - int page = Math.max(1, Math.min(requestedPage, pages)); - int start = (page - 1) * DEBUG_PAGE_SIZE; - int end = Math.min(start + DEBUG_PAGE_SIZE, lines.size()); + return sections; + } + + private String normalizeDebugSection(String requested) { + if (requested == null || requested.isBlank()) { + return "status"; + } + return switch (requested.toLowerCase(Locale.ENGLISH)) { + case "status" -> "status"; + case "commands", "command" -> "commands"; + case "permissions", "permission", "perms", "perm" -> "permissions"; + case "placeholders", "placeholder", "papi" -> "placeholders"; + case "config", "configuration", "cfg", "toggles" -> "config"; + default -> null; + }; + } - TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " Debug page " + page + "/" + pages); - for (int i = start; i < end; i++) { - TextUtils.sendRawMessage(sender, lines.get(i)); + private void sendDebugSection(CommandSender sender, String sectionKey) { + LinkedHashMap> sections = buildDebugSections(); + if (!sections.containsKey(sectionKey)) { + sendInvalidDebugSelection(sender, sectionKey, sections.size()); + return; } - if (page < pages) { - TextUtils.sendRawMessage(sender, "Next: " + commandPath("debug " + (page + 1))); + renderDebugSection(sender, sectionKey, sections); + } + + private void sendDebugPage(CommandSender sender, int requestedPage) { + LinkedHashMap> sections = buildDebugSections(); + List sectionKeys = new ArrayList<>(sections.keySet()); + if (requestedPage < 1 || requestedPage > sectionKeys.size()) { + sendInvalidDebugSelection(sender, Integer.toString(requestedPage), sectionKeys.size()); + return; + } + renderDebugSection(sender, sectionKeys.get(requestedPage - 1), sections); + } + + private void renderDebugSection(CommandSender sender, String sectionKey, LinkedHashMap> sections) { + List sectionKeys = new ArrayList<>(sections.keySet()); + int sectionIndex = sectionKeys.indexOf(sectionKey); + int page = sectionIndex + 1; + int pageCount = sectionKeys.size(); + + TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " Debug " + DEBUG_SECTIONS.getOrDefault(sectionKey, sectionKey) + " (" + page + "/" + pageCount + ")"); + for (String line : sections.get(sectionKey)) { + TextUtils.sendRawMessage(sender, line); + } + if (page < pageCount) { + String nextSection = sectionKeys.get(sectionIndex + 1); + TextUtils.sendRawMessage(sender, formatKeyValue("Next", "" + commandPath("debug " + nextSection) + "")); } } + private void sendInvalidDebugSelection(CommandSender sender, String requested, int pageCount) { + TextUtils.sendRawMessage(sender, formatKeyValue("Debug Sections", "" + String.join(", ", DEBUG_SECTIONS.keySet()) + "")); + TextUtils.sendRawMessage(sender, formatKeyValue("Debug Pages", "1-" + pageCount + "")); + TextUtils.sendRawMessage(sender, formatKeyValue("Requested", "" + requested + "")); + TextUtils.sendRawMessage(sender, formatKeyValue("Try", "" + commandPath("debug status") + "")); + } + private List getHelpLines() { List lines = new ArrayList<>(); for (String line : LocaleManager.COMMAND_HELP) { - lines.add(line.replace("/xmas", "/" + PRIMARY_COMMAND)); + lines.add(line.replaceAll("/" + LEGACY_COMMAND + "(?![A-Za-z])", "/" + PRIMARY_COMMAND)); } if (isLegacyAliasEnabled()) { lines.add("Legacy alias: /" + LEGACY_COMMAND + " still works."); @@ -387,7 +463,7 @@ private static void syncLegacyAlias(Main plugin, XMasCommand executor) { return; } aliasCommand.setDescription("Legacy alias for /" + PRIMARY_COMMAND); - aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|debug|end]"); + aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|debug [section|page]|end]"); aliasCommand.setPermission(null); aliasCommand.setExecutor(executor); aliasCommand.setTabCompleter(executor); @@ -413,13 +489,15 @@ private static void unregisterLegacyAlias(CommandMap commandMap) { knownCommandsField.setAccessible(true); Object rawKnownCommands = knownCommandsField.get(simpleCommandMap); if (rawKnownCommands instanceof Map rawMap) { - Iterator> iterator = rawMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); + List keysToRemove = new ArrayList<>(); + for (Map.Entry entry : rawMap.entrySet()) { if (entry.getValue() == legacyAliasCommand) { - iterator.remove(); + keysToRemove.add(entry.getKey()); } } + for (Object key : keysToRemove) { + removeKnownCommand(rawMap, key); + } } } catch (ReflectiveOperationException ignored) { } @@ -427,6 +505,11 @@ private static void unregisterLegacyAlias(CommandMap commandMap) { legacyAliasCommand = null; } + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void removeKnownCommand(Map rawMap, Object key) { + ((Map) rawMap).remove(key); + } + private static CommandMap getCommandMap() { try { Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap"); @@ -483,6 +566,22 @@ private void sendNoPermission(CommandSender sender) { TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); } + private String formatSectionTitle(String title) { + return "" + title + ""; + } + + private String formatListEntry(String key, String value) { + return "" + key + " : " + value + ""; + } + + private String formatKeyValue(String key, String value) { + return "" + key + ": " + value; + } + + private String booleanValue(boolean value) { + return value ? "true" : "false"; + } + private static Map createPermissionDescriptions() { Map permissions = new LinkedHashMap<>(); permissions.put(PERMISSION_ADMIN, "allows all " + TextUtils.DISPLAY_NAME + " commands and overrides"); @@ -499,4 +598,14 @@ private static Map createPermissionDescriptions() { return permissions; } + private static Map createDebugSections() { + Map sections = new LinkedHashMap<>(); + sections.put("status", "Status"); + sections.put("commands", "Commands"); + sections.put("permissions", "Permissions"); + sections.put("placeholders", "Placeholders"); + sections.put("config", "Config"); + return sections; + } + } diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java index e2cafbc..c38a46c 100644 --- a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java @@ -6,9 +6,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -33,6 +35,7 @@ final class XMasPlaceholders { "%onembxmastree_player.trees%", "%onembxmastree_version%" ); + public static final Map DESCRIPTIONS = createDescriptions(); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH-mm-ss"); private XMasPlaceholders() { @@ -124,4 +127,26 @@ private static int countPlayerTrees(OfflinePlayer player) { } return count; } + + private static Map createDescriptions() { + Map descriptions = new LinkedHashMap<>(); + descriptions.put("%onembxmastree_event.active%", "whether the event is currently active"); + descriptions.put("%onembxmastree_event.active_text%", "human-readable active state"); + descriptions.put("%onembxmastree_event.status%", "current event status text"); + descriptions.put("%onembxmastree_event.starts_at%", "event start mode"); + descriptions.put("%onembxmastree_event.ends_at%", "configured event end date"); + descriptions.put("%onembxmastree_event.ends_in%", "time remaining until the event ends"); + descriptions.put("%onembxmastree_event.ends_timestamp%", "event end timestamp in milliseconds"); + descriptions.put("%onembxmastree_event.auto_end%", "whether automatic ending is enabled"); + descriptions.put("%onembxmastree_resource.back%", "whether resource refunds are enabled"); + descriptions.put("%onembxmastree_resource.back_text%", "human-readable refund state"); + descriptions.put("%onembxmastree_particles.enabled%", "whether XMas Tree particles are enabled"); + descriptions.put("%onembxmastree_luck.enabled%", "whether gift luck chance is enabled"); + descriptions.put("%onembxmastree_luck.chance%", "gift luck chance as a percent"); + descriptions.put("%onembxmastree_trees.total%", "total loaded tree count"); + descriptions.put("%onembxmastree_trees.owners%", "number of unique tree owners"); + descriptions.put("%onembxmastree_player.trees%", "loaded trees owned by the placeholder player"); + descriptions.put("%onembxmastree_version%", "loaded plugin version"); + return descriptions; + } } diff --git a/src/main/resources/locales/default.yml b/src/main/resources/locales/default.yml index 8112fbb..dcd933d 100644 --- a/src/main/resources/locales/default.yml +++ b/src/main/resources/locales/default.yml @@ -50,11 +50,14 @@ crystal: command: help: - 'Use /xmastree to show plugin version and status' - - 'Use /xmastree give player to give a player a Christmas Crystal' + - 'Use /xmastree help to show this command list' + - 'Use /xmastree give <player> to give a player a Christmas Crystal' - 'Use /xmastree gifts to spawn presents under all trees' - 'Use /xmastree addhand to add your held item as a gift' - 'Use /xmastree reload to reload the plugin config' - - 'Use /xmastree debug for paginated status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree debug to open the status debug section' + - 'Use /xmastree debug status and other categories for status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree debug toggle <key> true|false to change supported global toggles' - 'Use /xmastree end to end the event' player-offline: 'Player not found' no-player-name: 'Missing player name' diff --git a/src/main/resources/locales/en.yml b/src/main/resources/locales/en.yml index 17b9664..0368c92 100644 --- a/src/main/resources/locales/en.yml +++ b/src/main/resources/locales/en.yml @@ -41,11 +41,14 @@ help: command: help: - 'Use /xmastree to show plugin version and status' - - 'Use /xmastree give player to give a player a Christmas Crystal' + - 'Use /xmastree help to show this command list' + - 'Use /xmastree give <player> to give a player a Christmas Crystal' - 'Use /xmastree gifts to spawn presents under all trees' - 'Use /xmastree addhand to add your held item as a gift' - 'Use /xmastree reload to reload the plugin config' - - 'Use /xmastree debug for paginated status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree debug to open the status debug section' + - 'Use /xmastree debug status and other categories for status, commands, permissions, placeholders, and toggles' + - 'Use /xmastree debug toggle <key> true|false to change supported global toggles' - 'Use /xmastree end to end the event' player-offline: 'Player not found' no-player-name: 'Missing player name' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 781a596..f14bfd9 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,7 +7,7 @@ softdepend: [Multiverse-Core, PlotMe, PlotSquared, Plotz, PlaceholderAPI] commands: xmastree: description: Manage the 1MB XMas Tree event - usage: / [help|give|gifts|addhand|reload|debug|end] + usage: / [help|give|gifts|addhand|reload|debug [section|page]|end] permissions: onembxmastree.admin: description: Allows all XMas Tree commands and overrides From 7080616620c15ddc595b88183fcf1b77bb450348 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 21 Apr 2026 22:03:01 +0200 Subject: [PATCH 5/8] - v2 changelog --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/README.md b/README.md index 60bf4b8..5f1c8a2 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,62 @@ The checked-in source targets Paper 26.1.2 only. - Optional PlaceholderAPI placeholders for CMI holograms, ajLeaderboards, scoreboards, and menus. - Legacy `trees.yml` world-name alias support for renamed destination worlds. +## v2 changelog + +- modernize the plugin from the legacy deployed build to an actively maintained Paper 26.1.2 / Java 25 Gradle build +- simplify the build around the active Paper 26.1.2 target and remove the retired 1.21.11 local server dependency +- keep build output clean and predictable in `build/libs` with the 2026 versioned jar naming + +- keep legacy tree data compatible by continuing to read `plugins/X-Mas/trees.yml` +- add world alias migration support so old saved trees can survive renamed worlds +- preserve old event data while modernizing the runtime and admin tooling + +- make `/xmastree` the primary command +- keep `/xmas` as an optional legacy alias controlled by config +- fix legacy alias reload and unregister behavior so reload no longer crashes when alias settings change +- improve `/xmastree help` output and keep it aligned with the actual command surface + +- add granular permissions under `onembxmastree.*` +- replace the old `xmas.admin` permission with `onembxmastree.admin` +- add separate permissions for `status`, `help`, `give`, `gifts`, `addhand`, `reload`, `debug`, `debug.toggle`, `end`, and `tree.override` + +- add a modern debug system with named categories: `status`, `commands`, `permissions`, `placeholders`, and `config` +- keep numeric debug pages working as a legacy shortcut +- improve debug output formatting with clearer key/value coloring +- make invalid debug page or section requests return a helpful response instead of silently falling back +- add `/xmastree debug toggle true|false` for live boolean config changes +- keep tab completion focused on named debug categories instead of numeric page suggestions + +- add optional PlaceholderAPI support with the `onembxmastree` namespace +- add placeholders for event state, end time, end countdown, auto-end, resource-back, particles, luck, tree totals, owner totals, player tree count, and plugin version +- document placeholders in the README and show them in debug output + +- modernize message handling with MiniMessage support while keeping legacy color compatibility +- improve player-facing text, prefixes, debug output, and help text +- change the visible plugin and chat identity toward `XMas Tree` for clearer user-facing output +- make the Christmas Crystal display name non-italic + +- fix `resource-back` so destroying a tree returns only the materials actually spent on that tree +- fix the old refund dupe issue where the plugin could return more than the player had used +- improve refund delivery with fallback order: chest, barrel, player inventory, then floor drops + +- reduce the loud grow and ingredient sound behavior +- make first-hit and repeat-hit grow sound volumes configurable and reloadable +- support silent, quiet, and loud tuning through config without server restarts + +- modernize material and item handling for current Paper names +- use safer material matching and validation to avoid legacy enum failures +- improve displayed item names so materials such as Redstone Dust render properly in requirement output +- add configurable per-stage particle effects using modern Paper particle names + +- harden config and item parsing by restricting present texture URLs to `textures.minecraft.net` +- cap Base64 gift payload handling +- skip invalid or legacy material names safely + +- update config comments and improve documentation for installation, building, commands, permissions, placeholders, support, and credits +- point support to the GitHub issues page +- refresh `.gitignore` for local dev/test folders and obvious OS/build junk + ## Installation 1. Stop the Paper server. From 20618d0fe8adfeb95cf5db3c8a0e89730493a184 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Fri, 24 Apr 2026 19:13:19 +0200 Subject: [PATCH 6/8] - clean up deprecated warnings --- .gitignore | 3 + README.md | 32 +++++--- build.gradle | 74 +++++++++++++++---- src/main/java/ru/meloncode/xmas/Events.java | 19 ++--- .../java/ru/meloncode/xmas/ItemMaker.java | 11 +-- .../java/ru/meloncode/xmas/MagicTree.java | 23 +++--- src/main/java/ru/meloncode/xmas/Main.java | 6 ++ src/main/java/ru/meloncode/xmas/XMas.java | 10 +-- .../java/ru/meloncode/xmas/XMasCommand.java | 4 +- .../xmas/XMasPlaceholderExpansion.java | 2 +- .../ru/meloncode/xmas/XMasPlaceholders.java | 2 +- .../ru/meloncode/xmas/utils/ConfigUtils.java | 18 ++++- src/main/resources/plugin.yml | 2 +- todo.log | 8 ++ 14 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 todo.log diff --git a/.gitignore b/.gitignore index 2b1070e..38d9c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ Thumbs.db # Local test servers and generated Paper data servers/ + +# Local release jars for the centralized Paper runner +libs/ diff --git a/README.md b/README.md index 5f1c8a2..8509ec3 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ The Gradle build creates the current Paper 26.1.2 target jar in `build/libs`: | Jar | Purpose | | --- | --- | -| `1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | +| `1MB-XMas-2026-v2.0.1-023-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | -The checked-in source targets Paper 26.1.2 only. +The checked-in source compiles against the centralized Paper 26.1.2 cache and declares a plugin compatibility floor of `api-version: 1.21.11` so the same jar can be tested on Paper 1.21.11 and Paper 26.1.2. ## Features @@ -99,7 +99,7 @@ The checked-in source targets Paper 26.1.2 only. For the 2026 target, use the modern Paper 26.1.2 jar: -- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar` +- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-023-v25-26.1.2.jar` ## Building @@ -107,8 +107,10 @@ Requirements: - JDK 25 - Gradle -- The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2` for Paper API jars and local smoke testing -- The current local dev/test setup in this repo uses `servers/Server-Two-Paper-26.1.2/plugins/PlaceholderAPI-2.12.3-DEV-265.jar` for the optional PlaceholderAPI compile-time classpath +- Centralized Paper server cache at `/Users/floris/Projects/Codex/servers/cache/Paper-26.1.2` +- Centralized PlaceholderAPI jar at `/Users/floris/Projects/Codex/servers/cache/Paper-26.1.2/plugins/PlaceholderAPI-2.12.3-DEV-265.jar` + +This repo no longer uses a local `servers/` folder for compilation or testing. If a local `servers/` folder still exists here, it is ignored and treated as retired local data. Build the current Paper 26.1.2 jar: @@ -128,9 +130,21 @@ The `paper2612Jar` task is kept as an alias: gradle paper2612Jar ``` -End users do not need the `servers/` folder. The build output jars are written to `build/libs/`, and those are the files you install on a Paper server. +`buildAllJars` now: + +- compiles against centralized Paper API `26.1.2.build.20-alpha` +- keeps the Java release target at `25` +- writes the standard jar to `build/libs/` +- copies the same jar into `libs/` for the centralized test runner +- prints the active build config, compile target, and declared plugin API floor + +You can also inspect the build metadata directly with: + +```bash +gradle printBuildConfig +``` -In this workspace, the current Gradle setup compiles against the Paper 26.1.2 API jars found in `servers/Server-Two-Paper-26.1.2`. If that folder is missing or has not been started far enough for Paper to download its libraries, Gradle will not have the local Paper API classpath it currently expects. +End users do not need any `servers/` folder. The installable jars are written to `build/libs/`, and this project also keeps a local copy in `libs/` for shared test-runner use. ## Commands @@ -293,7 +307,7 @@ The dotted key after `onembxmastree_` is supported to keep the placeholders read | `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | | `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | | `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | -| `%onembxmastree_version%` | `2.0.1-021` | Loaded plugin version. | +| `%onembxmastree_version%` | `2.0.1-023` | Loaded plugin version. | CMI hologram example: @@ -319,7 +333,7 @@ ajLeaderboards placeholder examples: - When saved world names no longer match the current server world names, `migration.world-aliases` can remap them without rewriting `trees.yml`. - Existing present head player-name entries are still accepted, but new configs should prefer Mojang texture URLs. - The modern jars are compiled with Java 25 bytecode and should be run on Java 25. -- The Paper 26.1.2 jar is the intended winter 2026 target. Paper 1.21.11 compatibility is no longer part of the active test path. +- The Paper 26.1.2 jar is the intended winter 2026 target, and the same jar now declares `api-version: 1.21.11` so it can be smoke-tested on both Paper 1.21.11 and Paper 26.1.2. ## Security notes diff --git a/build.gradle b/build.gradle index e41b8e4..68714ea 100644 --- a/build.gradle +++ b/build.gradle @@ -3,36 +3,55 @@ plugins { } group = 'com.onemb.xmas' -version = '2.0.1-021' -def paper2612ArchiveName = '1MB-XMas-2026-v2.0.1-021-v25-26.1.2.jar' +version = '2.0.1-023' +def projectVersion = version.toString() -def serverTwo = layout.projectDirectory.dir('servers/Server-Two-Paper-26.1.2') -def placeholderApiJar = serverTwo.file('plugins/PlaceholderAPI-2.12.3-DEV-265.jar') +def javaRelease = 25 +def paperCompileVersion = '26.1.2.build.20-alpha' +def paperCompatibilityFloor = '1.21.11' +def sharedServersRoot = file(System.getenv('CODEX_SHARED_SERVERS_ROOT') ?: '/Users/floris/Projects/Codex/servers') +def sharedPaperCache = new File(sharedServersRoot, 'cache/Paper-26.1.2') +def paper2612LibrariesDir = new File(sharedPaperCache, 'libraries') +def paper2612Api = new File(sharedPaperCache, "libraries/io/papermc/paper/paper-api/${paperCompileVersion}/paper-api-${paperCompileVersion}.jar") +def placeholderApiJar = new File(sharedPaperCache, 'plugins/PlaceholderAPI-2.12.3-DEV-265.jar') +def paper2612ArchiveName = "1MB-XMas-2026-v${projectVersion}-v25-26.1.2.jar" -def paper2612Api = serverTwo.file('libraries/io/papermc/paper/paper-api/26.1.2.build.18-alpha/paper-api-26.1.2.build.18-alpha.jar') - -def paper2612Classpath = files(paper2612Api) + fileTree(dir: serverTwo.dir('libraries').asFile, include: '**/*.jar') +[ + [paper2612Api, "Paper API jar"], + [paper2612LibrariesDir, "Paper libraries directory"], + [placeholderApiJar, "PlaceholderAPI jar"], +].each { entry -> + File path = entry[0] as File + String label = entry[1] as String + if (!path.exists()) { + throw new GradleException("${label} not found at ${path}. This project now uses the centralized Paper cache under ${sharedServersRoot}.") + } +} java { toolchain { - languageVersion = JavaLanguageVersion.of(25) + languageVersion = JavaLanguageVersion.of(javaRelease) } } dependencies { - compileOnly paper2612Classpath + compileOnly files(paper2612Api) + compileOnly fileTree(dir: paper2612LibrariesDir, include: '**/*.jar') compileOnly files(placeholderApiJar) } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' - options.release = 25 + options.release = javaRelease } tasks.named('processResources') { filteringCharset = 'UTF-8' filesMatching('plugin.yml') { - expand(version: project.version) + expand( + version: projectVersion, + apiVersionFloor: paperCompatibilityFloor + ) } } @@ -40,16 +59,41 @@ tasks.named('jar') { archiveFileName = paper2612ArchiveName } -tasks.register('paper2612Jar') { - description = 'Assembles the Paper 26.1.2 Java 25 plugin jar.' +tasks.register('releaseJar', Copy) { + description = 'Copies the newest plugin jar into libs/ for the centralized Paper runner.' group = 'build' dependsOn tasks.named('jar') + from(tasks.named('jar')) + into(layout.projectDirectory.dir('libs')) +} + +tasks.register('paper2612Jar') { + description = 'Assembles the Paper 26.1.2 Java 25 plugin jar and copies it into libs/.' + group = 'build' + dependsOn tasks.named('releaseJar') +} + +tasks.register('printBuildConfig') { + description = 'Prints the active centralized build and compatibility configuration.' + group = 'help' + doLast { + println 'XMasTree build config' + println " version: ${projectVersion}" + println " compile target: Paper API ${paperCompileVersion}" + println " plugin api-version floor: ${paperCompatibilityFloor}" + println " java release target: ${javaRelease}" + println " centralized Paper cache: ${sharedPaperCache}" + println " PlaceholderAPI compile jar: ${placeholderApiJar}" + println " build/libs output: ${layout.buildDirectory.file("libs/${paper2612ArchiveName}").get().asFile}" + println " libs/ release copy: ${layout.projectDirectory.file("libs/${paper2612ArchiveName}").asFile}" + } } tasks.register('buildAllJars') { - description = 'Builds the current Paper 26.1.2 target jar.' + description = 'Builds the current Paper 26.1.2 target jar and copies it into libs/.' group = 'build' - dependsOn tasks.named('jar') + dependsOn tasks.named('releaseJar') + finalizedBy tasks.named('printBuildConfig') } tasks.named('assemble') { diff --git a/src/main/java/ru/meloncode/xmas/Events.java b/src/main/java/ru/meloncode/xmas/Events.java index 604ca7d..730b32d 100644 --- a/src/main/java/ru/meloncode/xmas/Events.java +++ b/src/main/java/ru/meloncode/xmas/Events.java @@ -4,6 +4,7 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Firework; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -311,10 +312,10 @@ public void disableDecay(LeavesDecayEvent e) @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) private void disableFireworkDamage(EntityDamageByEntityEvent e) { - if (e.getDamager().getType() == EntityType.FIREWORK_ROCKET) { - if (e.getDamager().hasMetadata("nodamage")) { - e.setCancelled(true); - } + if (e.getDamager().getType() == EntityType.FIREWORK_ROCKET + && e.getDamager() instanceof Firework firework + && firework.getPersistentDataContainer().has(Main.getNoDamageFireworkKey(), PersistentDataType.BYTE)) { + e.setCancelled(true); } } @@ -332,11 +333,11 @@ private void chunkLoad(ChunkLoadEvent e) } private String getHeadIdentifier(SkullMeta meta) { - if (meta.getOwnerProfile() != null - && meta.getOwnerProfile().getTextures() != null - && meta.getOwnerProfile().getTextures().getSkin() != null) { - return meta.getOwnerProfile().getTextures().getSkin().toString(); + if (meta.getPlayerProfile() != null + && meta.getPlayerProfile().getTextures() != null + && meta.getPlayerProfile().getTextures().getSkin() != null) { + return meta.getPlayerProfile().getTextures().getSkin().toString(); } - return meta.getOwner(); + return meta.getOwningPlayer() != null ? meta.getOwningPlayer().getName() : null; } } diff --git a/src/main/java/ru/meloncode/xmas/ItemMaker.java b/src/main/java/ru/meloncode/xmas/ItemMaker.java index eabe9c8..ea75c15 100644 --- a/src/main/java/ru/meloncode/xmas/ItemMaker.java +++ b/src/main/java/ru/meloncode/xmas/ItemMaker.java @@ -2,6 +2,7 @@ //I plan to make this plugin bigger. So... +import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; @@ -77,14 +78,14 @@ public ItemMaker setLore(List lore) { } public ItemMaker addLoreLine(String line) { - List lore; - if (im.getLore() != null) { - lore = im.getLore(); + List lore; + if (im.lore() != null) { + lore = new ArrayList<>(im.lore()); } else { lore = new ArrayList<>(); } - lore.add(line); - im.lore(TextUtils.parseList(lore)); + lore.add(TextUtils.parse(line)); + im.lore(lore); return this; } diff --git a/src/main/java/ru/meloncode/xmas/MagicTree.java b/src/main/java/ru/meloncode/xmas/MagicTree.java index 4dc8f10..4e07950 100644 --- a/src/main/java/ru/meloncode/xmas/MagicTree.java +++ b/src/main/java/ru/meloncode/xmas/MagicTree.java @@ -1,5 +1,6 @@ package ru.meloncode.xmas; +import com.destroystokyo.paper.profile.PlayerProfile; import org.bukkit.*; import org.bukkit.FireworkEffect.Type; import org.bukkit.block.*; @@ -9,13 +10,13 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.FireworkMeta; -import org.bukkit.metadata.FixedMetadataValue; -import org.bukkit.profile.PlayerProfile; import org.bukkit.profile.PlayerTextures; +import org.bukkit.persistence.PersistentDataType; import ru.meloncode.xmas.utils.TextUtils; import org.bukkit.util.Vector; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.util.*; import java.util.Map.Entry; @@ -175,7 +176,7 @@ private void levelUp() { FireworkMeta meta = fw.getFireworkMeta(); meta.addEffect(FireworkEffect.builder().trail(true).withColor(Color.RED).withFade(Color.LIME).withFlicker().with(Type.BURST).build()); fw.setFireworkMeta(meta); - fw.setMetadata("nodamage", new FixedMetadataValue(Main.getInstance(), true)); + fw.getPersistentDataContainer().set(Main.getNoDamageFireworkKey(), PersistentDataType.BYTE, (byte) 1); } build(); save(); @@ -209,7 +210,6 @@ public void build() { } } - @SuppressWarnings("deprecation") public void spawnPresent() { if (!location.getChunk().isLoaded()) { @@ -232,13 +232,10 @@ public void spawnPresent() { face = BlockFace.values()[Main.RANDOM.nextInt(BlockFace.values().length)]; } while (face == BlockFace.DOWN || face == BlockFace.UP || face == BlockFace.SELF); - //skull.setRotation(face); Rotatable skullRotatable = (Rotatable) skull.getBlockData(); skullRotatable.setRotation(face); - skull.setRotation(face); - //skull.setSkullType(SkullType.PLAYER); + skull.setBlockData(skullRotatable); skull.setType(Material.PLAYER_HEAD); - //skull.setOwner(); applyConfiguredHead(skull, Main.getHeads().get(Main.RANDOM.nextInt(Main.getHeads().size()))); skull.update(true); } @@ -251,21 +248,21 @@ private void applyConfiguredHead(Skull skull, String configuredHead) { } String trimmedHead = configuredHead.trim(); if (!trimmedHead.contains("://")) { - skull.setOwningPlayer(Bukkit.getOfflinePlayer(trimmedHead)); + skull.setPlayerProfile(Bukkit.createProfile(trimmedHead)); return; } try { - URL skinUrl = new URL(trimmedHead); + URL skinUrl = URI.create(trimmedHead).toURL(); if (!"textures.minecraft.net".equalsIgnoreCase(skinUrl.getHost())) { Bukkit.getLogger().warning("[X-Mas] Ignoring non-Mojang present skin URL: " + trimmedHead); return; } - PlayerProfile profile = Bukkit.createPlayerProfile(UUID.randomUUID()); + PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); PlayerTextures textures = profile.getTextures(); textures.setSkin(skinUrl); profile.setTextures(textures); - skull.setOwnerProfile(profile); - } catch (MalformedURLException e) { + skull.setPlayerProfile(profile); + } catch (IllegalArgumentException | MalformedURLException e) { Bukkit.getLogger().warning("[X-Mas] Invalid present skin URL: " + trimmedHead); } } diff --git a/src/main/java/ru/meloncode/xmas/Main.java b/src/main/java/ru/meloncode/xmas/Main.java index ca3dab7..a2f9c18 100644 --- a/src/main/java/ru/meloncode/xmas/Main.java +++ b/src/main/java/ru/meloncode/xmas/Main.java @@ -42,6 +42,7 @@ public class Main extends JavaPlugin implements Listener { private static int UPDATE_SPEED; private static int PARTICLES_DELAY; private static NamespacedKey crystalKey; + private static NamespacedKey noDamageFireworkKey; private static List heads; private static Plugin plugin; private static final int MAX_SERIALIZED_GIFT_LENGTH = 65536; @@ -61,6 +62,10 @@ public static NamespacedKey getCrystalKey() { return crystalKey; } + public static NamespacedKey getNoDamageFireworkKey() { + return noDamageFireworkKey; + } + @Override public void onLoad() { plugin = this; @@ -70,6 +75,7 @@ public void onLoad() { public void onEnable() { this.saveDefaults(); crystalKey = new NamespacedKey(this, "xmas_crystal"); + noDamageFireworkKey = new NamespacedKey(this, "no_damage_firework"); config = getConfig(); locale = config.getString("core.locale"); diff --git a/src/main/java/ru/meloncode/xmas/XMas.java b/src/main/java/ru/meloncode/xmas/XMas.java index 507d79c..3c4d7e7 100644 --- a/src/main/java/ru/meloncode/xmas/XMas.java +++ b/src/main/java/ru/meloncode/xmas/XMas.java @@ -93,12 +93,12 @@ public static void processPresent(Block block, Player player) { } static String getHeadIdentifier(Skull skull) { - if (skull.getOwnerProfile() != null - && skull.getOwnerProfile().getTextures() != null - && skull.getOwnerProfile().getTextures().getSkin() != null) { - return skull.getOwnerProfile().getTextures().getSkin().toString(); + if (skull.getPlayerProfile() != null + && skull.getPlayerProfile().getTextures() != null + && skull.getPlayerProfile().getTextures().getSkin() != null) { + return skull.getPlayerProfile().getTextures().getSkin().toString(); } - return skull.getOwner(); + return skull.getOwningPlayer() != null ? skull.getOwningPlayer().getName() : null; } public static List getTreesPlayerOwn(Player player) { diff --git a/src/main/java/ru/meloncode/xmas/XMasCommand.java b/src/main/java/ru/meloncode/xmas/XMasCommand.java index 1d6973a..821a0d1 100644 --- a/src/main/java/ru/meloncode/xmas/XMasCommand.java +++ b/src/main/java/ru/meloncode/xmas/XMasCommand.java @@ -220,7 +220,7 @@ private List getStatusLines() { SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); List lines = new ArrayList<>(); - lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getDescription().getVersion() + " Plugin Status"); + lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getPluginMeta().getVersion() + " Plugin Status"); lines.add(""); lines.add(formatKeyValue("Event Status", Main.inProgress ? "In Progress" : "Holidays End")); if (Main.inProgress) { @@ -467,7 +467,7 @@ private static void syncLegacyAlias(Main plugin, XMasCommand executor) { aliasCommand.setPermission(null); aliasCommand.setExecutor(executor); aliasCommand.setTabCompleter(executor); - commandMap.register(plugin.getDescription().getName().toLowerCase(Locale.ENGLISH), aliasCommand); + commandMap.register(plugin.getPluginMeta().getName().toLowerCase(Locale.ENGLISH), aliasCommand); legacyAliasCommand = aliasCommand; plugin.getLogger().info("Registered legacy alias '/" + LEGACY_COMMAND + "' for '/" + PRIMARY_COMMAND + "'."); } diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java index f9f26f5..c3d726f 100644 --- a/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholderExpansion.java @@ -24,7 +24,7 @@ public String getAuthor() { @Override public String getVersion() { - return plugin.getDescription().getVersion(); + return plugin.getPluginMeta().getVersion(); } @Override diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java index c38a46c..c46c4b8 100644 --- a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java @@ -63,7 +63,7 @@ public static String resolve(Main plugin, OfflinePlayer player, String params) { case "trees_total" -> Integer.toString(XMas.getAllTrees().size()); case "trees_owners" -> Integer.toString(countOwners(XMas.getAllTrees())); case "player_trees" -> Integer.toString(countPlayerTrees(player)); - case "version" -> plugin.getDescription().getVersion(); + case "version" -> plugin.getPluginMeta().getVersion(); default -> null; }; } diff --git a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java index 552986b..8358dcd 100644 --- a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java +++ b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java @@ -1,14 +1,30 @@ package ru.meloncode.xmas.utils; +import org.bukkit.Bukkit; +import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.logging.Level; public class ConfigUtils { public static FileConfiguration loadConfig(File file) { - return YamlConfiguration.loadConfiguration(file); + YamlConfiguration configuration = new YamlConfiguration(); + if (!file.exists()) { + return configuration; + } + + try { + configuration.loadFromString(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } catch (IOException | InvalidConfigurationException exception) { + Bukkit.getLogger().log(Level.WARNING, "Failed to load YAML configuration from " + file.getPath(), exception); + } + return configuration; } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f14bfd9..7149a5f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,7 +2,7 @@ name: X-Mas version: ${version} main: ru.meloncode.xmas.Main load: POSTWORLD -api-version: '1.21' +api-version: '${apiVersionFloor}' softdepend: [Multiverse-Core, PlotMe, PlotSquared, Plotz, PlaceholderAPI] commands: xmastree: diff --git a/todo.log b/todo.log new file mode 100644 index 0000000..96abf8d --- /dev/null +++ b/todo.log @@ -0,0 +1,8 @@ +Remaining warnings and deferred deprecation cleanup +================================================= + +- `compileJava` still prints the generic note that some source uses deprecated API. +- The remaining known spots are the block-skull profile accessors and setters in: + - `src/main/java/ru/meloncode/xmas/MagicTree.java` + - `src/main/java/ru/meloncode/xmas/XMas.java` +- These were left in place on purpose because the newer block-skull `ResolvableProfile` route is not a clearly low-risk drop-in if we want to preserve the current texture URL and player-name behavior on both Paper versions. From b7315a0818f41eff23916f1ba6d13da594170a74 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Tue, 28 Apr 2026 10:28:25 +0200 Subject: [PATCH 7/8] Improve config.yml handling and preserve comments on reload/save - add a shared managed config flow for config.yml using YamlConfiguration comment parsing - preserve admin values and existing comments while safely adding missing defaults and missing template comments - route startup, reload, and command-driven config saves through the same helper - expand config.yml comments so every setting documents defaults, valid values, format, and reload behavior - document config sync behavior in the README - bump build to 2.0.1-024 --- README.md | 7 +- build.gradle | 2 +- src/main/java/ru/meloncode/xmas/Main.java | 30 +- .../ru/meloncode/xmas/utils/ConfigUtils.java | 114 ++++- src/main/resources/config.yml | 475 ++++++++++++++++-- 5 files changed, 577 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 8509ec3..6b9d1f0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Gradle build creates the current Paper 26.1.2 target jar in `build/libs`: | Jar | Purpose | | --- | --- | -| `1MB-XMas-2026-v2.0.1-023-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | +| `1MB-XMas-2026-v2.0.1-024-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | The checked-in source compiles against the centralized Paper 26.1.2 cache and declares a plugin compatibility floor of `api-version: 1.21.11` so the same jar can be tested on Paper 1.21.11 and Paper 26.1.2. @@ -31,6 +31,7 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de - Primary `/xmastree` command with an optional legacy `/xmas` alias. - Optional PlaceholderAPI placeholders for CMI holograms, ajLeaderboards, scoreboards, and menus. - Legacy `trees.yml` world-name alias support for renamed destination worlds. +- Comment-preserving `config.yml` syncing that keeps admin values while safely adding missing defaults and missing template comments. ## v2 changelog @@ -99,7 +100,7 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de For the 2026 target, use the modern Paper 26.1.2 jar: -- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-023-v25-26.1.2.jar` +- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-024-v25-26.1.2.jar` ## Building @@ -307,7 +308,7 @@ The dotted key after `onembxmastree_` is supported to keep the placeholders read | `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | | `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | | `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | -| `%onembxmastree_version%` | `2.0.1-023` | Loaded plugin version. | +| `%onembxmastree_version%` | `2.0.1-024` | Loaded plugin version. | CMI hologram example: diff --git a/build.gradle b/build.gradle index 68714ea..8a24e5b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'com.onemb.xmas' -version = '2.0.1-023' +version = '2.0.1-024' def projectVersion = version.toString() def javaRelease = 25 diff --git a/src/main/java/ru/meloncode/xmas/Main.java b/src/main/java/ru/meloncode/xmas/Main.java index a2f9c18..8d94184 100644 --- a/src/main/java/ru/meloncode/xmas/Main.java +++ b/src/main/java/ru/meloncode/xmas/Main.java @@ -16,6 +16,7 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.Vector; +import ru.meloncode.xmas.utils.ConfigUtils; import ru.meloncode.xmas.utils.TextUtils; import java.io.File; @@ -25,6 +26,7 @@ import java.util.*; public class Main extends JavaPlugin implements Listener { + private static final String CONFIG_RESOURCE_PATH = "config.yml"; // Yeah. That's as it should be. static final Random RANDOM = new Random(Calendar.getInstance().get(Calendar.YEAR)); @@ -66,6 +68,32 @@ public static NamespacedKey getNoDamageFireworkKey() { return noDamageFireworkKey; } + private File getPluginConfigFile() { + return new File(getDataFolder(), CONFIG_RESOURCE_PATH); + } + + @Override + public FileConfiguration getConfig() { + if (config == null) { + config = ConfigUtils.loadManagedConfig(this, CONFIG_RESOURCE_PATH, getPluginConfigFile()); + } + return config; + } + + @Override + public void reloadConfig() { + config = ConfigUtils.loadManagedConfig(this, CONFIG_RESOURCE_PATH, getPluginConfigFile()); + } + + @Override + public void saveConfig() { + if (config == null) { + return; + } + ConfigUtils.synchronizeWithResource(this, CONFIG_RESOURCE_PATH, config); + ConfigUtils.saveConfig(getPluginConfigFile(), config); + } + @Override public void onLoad() { plugin = this; @@ -346,7 +374,7 @@ public void end() { } private void saveDefaults() { - this.saveDefaultConfig(); + reloadConfig(); plugin.saveResource("locales/default.yml", true); List defaults = Arrays.asList("locales/en.yml", "locales/ru.yml", "locales/ru_santa.yml", "trees.yml"); for (String path : defaults) diff --git a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java index 8358dcd..9e59551 100644 --- a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java +++ b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java @@ -4,17 +4,20 @@ import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.util.List; import java.util.logging.Level; public class ConfigUtils { - public static FileConfiguration loadConfig(File file) { - YamlConfiguration configuration = new YamlConfiguration(); + public static YamlConfiguration loadConfig(File file) { + YamlConfiguration configuration = newConfiguration(); if (!file.exists()) { return configuration; } @@ -27,4 +30,111 @@ public static FileConfiguration loadConfig(File file) { return configuration; } + public static YamlConfiguration loadManagedConfig(JavaPlugin plugin, String resourcePath, File file) { + YamlConfiguration configuration = loadConfig(file); + boolean changed = synchronizeWithResource(plugin, resourcePath, configuration); + if (!file.exists() || changed) { + saveConfig(file, configuration); + } + return configuration; + } + + public static boolean synchronizeWithResource(JavaPlugin plugin, String resourcePath, FileConfiguration configuration) { + if (!(configuration instanceof YamlConfiguration yamlConfiguration)) { + return false; + } + YamlConfiguration defaults = loadResourceConfig(plugin, resourcePath); + return mergeDefaultsAndComments(yamlConfiguration, defaults); + } + + public static void saveConfig(File file, FileConfiguration configuration) { + File parent = file.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + Bukkit.getLogger().warning("Unable to create configuration directory " + parent.getPath()); + return; + } + + try { + configuration.save(file); + } catch (IOException exception) { + Bukkit.getLogger().log(Level.WARNING, "Failed to save YAML configuration to " + file.getPath(), exception); + } + } + + private static YamlConfiguration loadResourceConfig(JavaPlugin plugin, String resourcePath) { + YamlConfiguration configuration = newConfiguration(); + try (InputStream inputStream = plugin.getResource(resourcePath)) { + if (inputStream == null) { + plugin.getLogger().warning("Missing bundled configuration resource: " + resourcePath); + return configuration; + } + configuration.loadFromString(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException | InvalidConfigurationException exception) { + plugin.getLogger().log(Level.WARNING, "Failed to load bundled configuration resource " + resourcePath, exception); + } + return configuration; + } + + private static YamlConfiguration newConfiguration() { + YamlConfiguration configuration = new YamlConfiguration(); + configuration.options().parseComments(true); + return configuration; + } + + private static boolean mergeDefaultsAndComments(YamlConfiguration target, YamlConfiguration defaults) { + boolean changed = false; + + if (isBlank(target.options().getHeader()) && !isBlank(defaults.options().getHeader())) { + target.options().setHeader(defaults.options().getHeader()); + changed = true; + } + if (isBlank(target.options().getFooter()) && !isBlank(defaults.options().getFooter())) { + target.options().setFooter(defaults.options().getFooter()); + changed = true; + } + + for (String path : defaults.getKeys(true)) { + if (defaults.isConfigurationSection(path)) { + if (!target.isConfigurationSection(path)) { + target.createSection(path); + changed = true; + } + } else if (!target.contains(path, true)) { + target.set(path, defaults.get(path)); + changed = true; + } + + if (copyMissingComments(target, path, defaults.getComments(path), defaults.getInlineComments(path))) { + changed = true; + } + } + + return changed; + } + + private static boolean copyMissingComments(YamlConfiguration target, String path, List blockComments, List inlineComments) { + boolean changed = false; + if (isBlank(target.getComments(path)) && !isBlank(blockComments)) { + target.setComments(path, blockComments); + changed = true; + } + if (isBlank(target.getInlineComments(path)) && !isBlank(inlineComments)) { + target.setInlineComments(path, inlineComments); + changed = true; + } + return changed; + } + + private static boolean isBlank(List comments) { + if (comments == null || comments.isEmpty()) { + return true; + } + for (String line : comments) { + if (line != null && !line.isBlank()) { + return false; + } + } + return true; + } + } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 31f61bb..1825a4e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,67 +1,163 @@ +# XMas Tree main configuration +# File: plugins/X-Mas/config.yml +# +# General config behavior: +# - Existing values are preserved when the plugin adds new defaults. +# - /xmastree reload re-reads this file without a full server restart unless noted otherwise below. +# - /xmastree debug toggle true|false can live-toggle a few global booleans and then reload the config. +# - Comments from this bundled template are merged into missing comment slots when the plugin loads the config. + core: - # Master switch for the event. When false, players can no longer create or grow trees. + # Master switch for the XMas Tree event. + # Default: true + # Safe values: true or false + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle core.plugin-enabled true|false applies immediately. + # - Turning the event off is immediate. + # - Turning it back on after the plugin has already ended the event is safest with a full server restart. plugin-enabled: true - # Locale file to load from plugins/X-Mas/locales/.yml. + # Locale file name loaded from plugins/X-Mas/locales/.yml. + # Default: en + # Safe values: a locale file name without .yml, for example en, ru, or ru_santa + # Reload behavior: /xmastree reload applies immediately. locale: en - # Maximum number of magic Christmas trees a single player can own. + # Maximum number of XMas trees one player may own at the same time. + # Default: 3 + # Safe values: 1 or higher + # Reload behavior: /xmastree reload applies immediately for future placement checks. tree-limit: 3 commands: - # Primary admin command is /xmastree. - # When true, the legacy /xmas alias is also registered. - # /xmastree reload applies changes without a server restart. + # Registers the legacy /xmas alias alongside the primary /xmastree command. + # Default: true + # Safe values: true or false + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle core.commands.legacy-command-enabled true|false applies immediately. legacy-command-enabled: true # Automatic end-of-event behavior. holiday-ends: - # When true, the plugin will switch plugin-enabled to false after the date below. + # Enables time-based event shutdown. + # Default: true + # Safe values: true or false + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle core.holiday-ends.enabled true|false applies immediately. enabled: true - # Date/time when the event ends. Format: dd-MM-yyyy HH-mm-ss. + # Date/time when the event should end automatically. + # Default: 10-01-2027 03-33-33 + # Required format: dd-MM-yyyy HH-mm-ss + # Example: 24-12-2026 18-00-00 + # Reload behavior: /xmastree reload applies immediately. date: 10-01-2027 03-33-33 - # When true, destroyed or ended trees return spent upgrade resources. - # The plugin tries a chest first, then a barrel, then the player's inventory, then floor drops. + # Returns spent tree-upgrade materials when a tree is destroyed or cleaned up after the event. + # Default: true + # Safe values: true or false + # Refund order: chest -> barrel -> player inventory -> floor drops + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle core.holiday-ends.resource-back true|false applies immediately. resource-back: true - # Main tree update interval in ticks. 20 ticks is roughly 1 second. - # Gift cooldown values are scaled by this value. Must be greater than 0. + # Interval for the main tree update task. + # Default: 7 + # Units: server ticks (20 ticks = about 1 second) + # Safe values: any whole number greater than 0 + # Notes: + # - This value is also used when scaling stage gift cooldowns into task ticks. + # - Smaller values make the plugin check trees more often. + # Reload behavior: + # - /xmastree reload re-reads the value. + # - A full server restart is recommended to recreate the scheduled task with the new cadence. update-speed: 7 - # Particle update interval in ticks. Keep this higher if particles become noisy. + # Interval for the particle task. + # Default: 35 + # Units: server ticks + # Safe values: any whole number greater than 0 + # Notes: + # - Smaller values make particle updates happen more often and can look noisier. + # Reload behavior: + # - /xmastree reload re-reads the value. + # - A full server restart is recommended to recreate the scheduled task with the new cadence. particles-delay: 35 - # Global particle switch. Can be toggled with /xmastree debug toggle core.particles-enabled true|false. + # Global particle master switch for tree visual effects. + # Default: true + # Safe values: true or false + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle core.particles-enabled true|false applies immediately. particles-enabled: true sounds: grow: - # Volume for the first accepted item of each material requirement. - # Use 0.0 for silent, 0.1 for quiet, 0.5 for half volume, and 1.0 for full volume. + # Volume for the first accepted item of a material requirement. + # Default: 0.5 + # Safe values: decimal numbers from 0.0 to 1.0 + # Examples: 0.0 = silent, 0.1 = quiet, 0.5 = half volume, 1.0 = full volume + # Reload behavior: /xmastree reload applies immediately. first-volume: 0.5 # Volume for repeated accepted items of the same material requirement. - # Example: first redstone dust uses first-volume, later redstone dust uses repeat-volume. + # Default: 0.2 + # Safe values: decimal numbers from 0.0 to 1.0 + # Example: the first redstone dust uses first-volume, later redstone dust uses repeat-volume + # Reload behavior: /xmastree reload applies immediately. repeat-volume: 0.2 xmas: luck: - # When true, gift openings can fail and drop the fallback item instead. + # Enables the optional luck check for presents. + # Default: false + # Safe values: true or false + # Notes: + # - When disabled, presents always use the main gift pool. + # - When enabled, the configured luck chance decides whether the normal gift is used. + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree debug toggle xmas.luck.enabled true|false applies immediately. enabled: false - # Success chance from 1-100 when luck is enabled. + # Success chance for the present luck system. + # Default: 75 + # Safe values: whole numbers from 1 to 100 + # Notes: + # - The plugin currently reads this as a percentage and divides by 100 internally. + # - Values outside the normal 1-100 range are not recommended. + # Reload behavior: /xmastree reload applies immediately. chance: 75 - # Present head skins. Prefer official MineSkin/Mojang texture URLs. - # For safety, texture URLs must use textures.minecraft.net. Old player-name entries still work. + # Present head skins used for spawned gifts under trees. + # Default: the two textures.minecraft.net URLs below + # Safe values: + # - preferred: textures.minecraft.net URLs + # - legacy compatibility: old player-name entries still load + # Security note: modern URL entries are restricted to textures.minecraft.net + # Reload behavior: /xmastree reload applies immediately for newly spawned presents. presents: - http://textures.minecraft.net/texture/21bc9d42b0041e8f95cb9b26628fdaf50cd0e36f7bb9d6b3a4d2af3949da97d6 - http://textures.minecraft.net/texture/2b1ec7dc753061ca174424ea45cf9490b39cd5dcca477d138a603e6be755ec72 - # Gift list for presents. Use modern Paper/Bukkit material names, optionally MATERIAL:AMOUNT. - # Admins can also run /xmastree addhand to save the exact held item as a Base64 entry. + # Gift pool for presents. + # Default: the material list below + # Safe values: + # - MATERIAL + # - MATERIAL:AMOUNT + # - Base64 item strings saved by /xmastree addhand + # Notes: + # - Use modern Paper/Bukkit material names only. + # - Invalid or legacy material names are skipped safely. + # - /xmastree addhand appends the exact held item and saves it immediately. + # Reload behavior: + # - File edits apply on /xmastree reload. + # - /xmastree addhand applies immediately and persists to disk. gifts: - DIAMOND - EMERALD @@ -79,124 +175,415 @@ xmas: - DIAMOND_HOE - NAME_TAG - # Tree levels. gift-cooldown is in seconds before scaling by core.update-speed. - # lvlup sections define the modern material names and counts needed for the next level. - # Particle enum reference for Paper 26.1.2: - # https://jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html - # Configured particles currently support simple particles and DUST. + # Tree stage settings. + # Notes: + # - gift-cooldown values are written in seconds and converted using core.update-speed. + # - lvlup sections use modern material names and item counts for the next stage. + # - Particle names should come from the Paper Particle enum: + # https://jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html + # - Existing loaded trees may keep their current stage objects until the next full restart. + # /xmastree reload is best for new trees and newly loaded trees, while a restart is recommended + # when you want every existing tree to use the refreshed stage settings. tree-lvl: sapling: - # -1 disables present spawning for this level. + # Delay before present spawns at the sapling stage. + # Default: -1 + # Units: seconds before scaling by core.update-speed + # Safe values: -1 to disable present spawning, or 0 and higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. gift-cooldown: -1 - # Visual effects for this stage. Set enabled: false to disable an effect slot. + particles: ambient: + # Enables the ambient particle effect for saplings. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the sapling ambient effect. + # Default: SPORE_BLOSSOM_AIR + # Safe values: a valid Particle enum name + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: SPORE_BLOSSOM_AIR + # Horizontal X offset for the sapling ambient particle spread. + # Default: 0.35 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 0.35 + # Vertical Y offset for the sapling ambient particle spread. + # Default: 0.45 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 0.45 + # Horizontal Z offset for the sapling ambient particle spread. + # Default: 0.35 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 0.35 + # Particle speed value passed to the Paper particle API. + # Default: 0.01 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.01 + # Number of ambient particles spawned per effect tick. + # Default: 2 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 2 swag: + # Enables the ornament-style particle effect for saplings. + # Default: false + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: false body: + # Enables the trunk/body particle effect for saplings. + # Default: false + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: false - # Materials required to grow from sapling to small_tree. + lvlup: + # Diamonds required to grow from sapling to small_tree. + # Default: 1 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. DIAMOND: 1 + # Redstone dust required to grow from sapling to small_tree. + # Default: 10 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. REDSTONE: 10 + # Ender pearls required to grow from sapling to small_tree. + # Default: 1 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. ENDER_PEARL: 1 + small_tree: - # Seconds between present spawn attempts for small trees. + # Delay before present spawns at the small_tree stage. + # Default: 300 + # Units: seconds before scaling by core.update-speed + # Safe values: 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. gift-cooldown: 300 - # Small trees get a little more ambient motion and light ornament sparkle. + particles: ambient: + # Enables the ambient particle effect for small trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the small tree ambient effect. + # Default: CHERRY_LEAVES + # Safe values: a valid Particle enum name + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: CHERRY_LEAVES + # Horizontal X offset for the small tree ambient particle spread. + # Default: 1.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 1.0 + # Vertical Y offset for the small tree ambient particle spread. + # Default: 1.5 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 1.5 + # Horizontal Z offset for the small tree ambient particle spread. + # Default: 1.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 1.0 + # Particle speed value passed to the Paper particle API. + # Default: 0.01 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.01 + # Number of ambient particles spawned per effect tick. + # Default: 3 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 3 swag: + # Enables the ornament-style particle effect for small trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the small tree ornament effect. + # Default: DUST + # Safe values: a valid Particle enum name supported by the plugin's simple particle loader + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: DUST + # Horizontal X offset for the small tree ornament particle spread. + # Default: 0.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 0.25 + # Vertical Y offset for the small tree ornament particle spread. + # Default: 0.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 0.25 + # Horizontal Z offset for the small tree ornament particle spread. + # Default: 0.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 0.25 + # Particle speed value passed to the Paper particle API. + # Default: 0.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.0 + # Number of ornament particles spawned per effect tick. + # Default: 8 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 8 body: + # Enables the trunk/body particle effect for small trees. + # Default: false + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: false - # Materials required to grow from small_tree to tree. + lvlup: + # Diamonds required to grow from small_tree to tree. + # Default: 3 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. DIAMOND: 3 + # Gold ingots required to grow from small_tree to tree. + # Default: 5 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. GOLD_INGOT: 5 + # Blaze powder required to grow from small_tree to tree. + # Default: 10 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. BLAZE_POWDER: 10 + # Snowballs required to grow from small_tree to tree. + # Default: 30 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. SNOWBALL: 30 + tree: - # Seconds between present spawn attempts for full trees. + # Delay before present spawns at the tree stage. + # Default: 180 + # Units: seconds before scaling by core.update-speed + # Safe values: 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. gift-cooldown: 180 - # Full trees are more visible, with snow and firefly sparkle. + particles: ambient: + # Enables the ambient particle effect for full trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the full tree ambient effect. + # Default: SNOWFLAKE + # Safe values: a valid Particle enum name + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: SNOWFLAKE + # Horizontal X offset for the full tree ambient particle spread. + # Default: 1.5 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 1.5 + # Vertical Y offset for the full tree ambient particle spread. + # Default: 3.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 3.0 + # Horizontal Z offset for the full tree ambient particle spread. + # Default: 1.5 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 1.5 + # Particle speed value passed to the Paper particle API. + # Default: 0.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.0 + # Number of ambient particles spawned per effect tick. + # Default: 8 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 8 swag: + # Enables the ornament-style particle effect for full trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the full tree ornament effect. + # Default: FIREFLY + # Safe values: a valid Particle enum name + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: FIREFLY + # Horizontal X offset for the full tree ornament particle spread. + # Default: 0.35 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 0.35 + # Vertical Y offset for the full tree ornament particle spread. + # Default: 0.35 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 0.35 + # Horizontal Z offset for the full tree ornament particle spread. + # Default: 0.35 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 0.35 + # Particle speed value passed to the Paper particle API. + # Default: 0.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.0 + # Number of ornament particles spawned per effect tick. + # Default: 4 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 4 body: + # Enables the trunk/body particle effect for full trees. + # Default: false + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: false - # Materials required to grow from tree to magic_tree. + lvlup: + # Diamonds required to grow from tree to magic_tree. + # Default: 5 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. DIAMOND: 5 + # Emeralds required to grow from tree to magic_tree. + # Default: 3 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. EMERALD: 3 + # Gold nuggets required to grow from tree to magic_tree. + # Default: 8 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. GOLD_NUGGET: 8 + # Glowstone dust required to grow from tree to magic_tree. + # Default: 16 + # Safe values: whole numbers 1 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. GLOWSTONE_DUST: 16 + magic_tree: - # Seconds between present spawn attempts for max-level magic trees. + # Delay before present spawns at the max-level magic_tree stage. + # Default: 120 + # Units: seconds before scaling by core.update-speed + # Safe values: 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. gift-cooldown: 120 - # Magic trees use the strongest ambient set. + particles: ambient: + # Enables the ambient particle effect for magic trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the magic tree ambient effect. + # Default: FIREFLY + # Safe values: a valid Particle enum name + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: FIREFLY + # Horizontal X offset for the magic tree ambient particle spread. + # Default: 2.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 2.25 + # Vertical Y offset for the magic tree ambient particle spread. + # Default: 2.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 2.25 + # Horizontal Z offset for the magic tree ambient particle spread. + # Default: 2.25 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 2.25 + # Particle speed value passed to the Paper particle API. + # Default: 0.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 0.0 + # Number of ambient particles spawned per effect tick. + # Default: 8 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 8 swag: + # Enables the ornament-style particle effect for magic trees. + # Default: true + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: true + # Particle type for the magic tree ornament effect. + # Default: DUST + # Safe values: a valid Particle enum name supported by the plugin's simple particle loader + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. particle: DUST + # Horizontal X offset for the magic tree ornament particle spread. + # Default: 0.3 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-x: 0.3 + # Vertical Y offset for the magic tree ornament particle spread. + # Default: 0.3 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-y: 0.3 + # Horizontal Z offset for the magic tree ornament particle spread. + # Default: 0.3 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. offset-z: 0.3 + # Particle speed value passed to the Paper particle API. + # Default: 10.0 + # Safe values: decimal numbers, usually 0.0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. speed: 10.0 + # Number of ornament particles spawned per effect tick. + # Default: 16 + # Safe values: whole numbers 0 or higher + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. count: 16 body: + # Enables the trunk/body particle effect for magic trees. + # Default: false + # Safe values: true or false + # Reload behavior: /xmastree reload for new trees; restart recommended for fully consistent existing-tree behavior. enabled: false - # Max-level trees have no next-level requirements. - lvlup: + + # Max-level trees have no next-stage ingredient requirements. + # Default: empty section + # Safe values: leave empty + # Reload behavior: informational only; no practical effect unless custom code starts reading it later. + lvlup: {} migration: - # Map saved world names from legacy trees.yml data to the current server world names. - # This keeps old player trees loadable even when the destination server uses different world names. + # Maps world names from legacy trees.yml data to current world names. + # Default: empty map + # Safe format: + # old_world_name: new_world_name # Example: - # world-aliases: # general: world # wild: world # santa: santa_event + # Reload behavior: + # - File edits are saved immediately when the file is written. + # - A full server restart or world reload is recommended before expecting already-saved trees to load using new aliases. world-aliases: {} From 4ce953c9f0b57f2378083f81586c4b3c3d203755 Mon Sep 17 00:00:00 2001 From: Floris Fiedeldij Dop Date: Sun, 21 Jun 2026 19:39:56 +0200 Subject: [PATCH 8/8] Modernize XMasTree for Paper 26.2 Target Paper API 26.2 build 29 on Java 25, consolidate translations, improve MiniMessage/theme handling, add admin debug/test/data tools, add gift pool management, preserve legacy tree data migration paths, and refresh README/todo documentation. --- .gitignore | 3 + README.md | 127 ++- build.gradle | 57 +- src/main/java/ru/meloncode/xmas/Events.java | 48 +- .../java/ru/meloncode/xmas/LocaleManager.java | 353 +++++-- .../java/ru/meloncode/xmas/MagicTree.java | 59 +- src/main/java/ru/meloncode/xmas/Main.java | 212 +++-- .../ru/meloncode/xmas/ParticleContainer.java | 4 +- .../ru/meloncode/xmas/TreeSerializer.java | 239 ++++- src/main/java/ru/meloncode/xmas/XMas.java | 49 +- .../java/ru/meloncode/xmas/XMasCommand.java | 873 ++++++++++++++++-- .../ru/meloncode/xmas/XMasPlaceholders.java | 70 +- .../ru/meloncode/xmas/utils/ConfigUtils.java | 27 +- .../xmas/utils/HeadProfileUtils.java | 98 ++ .../ru/meloncode/xmas/utils/TextUtils.java | 120 ++- src/main/resources/config.yml | 7 +- src/main/resources/locales/default.yml | 64 -- src/main/resources/locales/en.yml | 55 -- src/main/resources/locales/hu.yml | 50 - src/main/resources/locales/ru.yml | 43 - src/main/resources/locales/ru_santa.yml | 43 - src/main/resources/plugin.yml | 16 +- src/main/resources/translations/locale_en.yml | 371 ++++++++ todo.log | 97 +- 24 files changed, 2408 insertions(+), 677 deletions(-) create mode 100644 src/main/java/ru/meloncode/xmas/utils/HeadProfileUtils.java delete mode 100644 src/main/resources/locales/default.yml delete mode 100644 src/main/resources/locales/en.yml delete mode 100644 src/main/resources/locales/hu.yml delete mode 100644 src/main/resources/locales/ru.yml delete mode 100644 src/main/resources/locales/ru_santa.yml create mode 100644 src/main/resources/translations/locale_en.yml diff --git a/.gitignore b/.gitignore index 38d9c5f..3a246cc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ Thumbs.db # Local test servers and generated Paper data servers/ +cache/ +libraries/ +versions/ # Local release jars for the centralized Paper runner libs/ diff --git a/README.md b/README.md index 6b9d1f0..de7f2ee 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ This fork keeps the old X-Mas event data usable for winter 2026 while moving the ## Current target -The Gradle build creates the current Paper 26.1.2 target jar in `build/libs`: +The Gradle build creates the current Paper 26.2 target jar in `build/libs`: | Jar | Purpose | | --- | --- | -| `1MB-XMas-2026-v2.0.1-024-v25-26.1.2.jar` | Modern Paper 26.1.2 build, Java 25 bytecode. | +| `1MB-XMas-2026-v2.1.0-032-v25-26.2.jar` | Modern Paper 26.2 build, Java 25 bytecode. | -The checked-in source compiles against the centralized Paper 26.1.2 cache and declares a plugin compatibility floor of `api-version: 1.21.11` so the same jar can be tested on Paper 1.21.11 and Paper 26.1.2. +The checked-in source compiles against the Paper 26.2 API, targets Java 25, and declares `api-version: 26.2`. This fork now treats Paper 26.2 as the primary winter 2026 target, with future 26.x builds as the expected forward-compatibility path. ## Features @@ -26,17 +26,18 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de - MiniMessage support for locale strings, crystal display text, and plugin messages. - Existing `plugins/X-Mas/trees.yml` data remains the event data source. - Optional resource refunds when a tree is destroyed or cleaned up after the event. -- Configurable per-stage particles using Paper 26.1.2 particle names. +- Configurable per-stage particles using Paper 26.2 particle names. - `/xmastree debug` sections for `status`, `commands`, `permissions`, `placeholders`, and `config`, plus live global boolean toggles. - Primary `/xmastree` command with an optional legacy `/xmas` alias. - Optional PlaceholderAPI placeholders for CMI holograms, ajLeaderboards, scoreboards, and menus. - Legacy `trees.yml` world-name alias support for renamed destination worlds. - Comment-preserving `config.yml` syncing that keeps admin values while safely adding missing defaults and missing template comments. +- Present heads are tagged with plugin PDC data, so gift handling no longer depends on deprecated skull profile reads. ## v2 changelog -- modernize the plugin from the legacy deployed build to an actively maintained Paper 26.1.2 / Java 25 Gradle build -- simplify the build around the active Paper 26.1.2 target and remove the retired 1.21.11 local server dependency +- modernize the plugin from the legacy deployed build to an actively maintained Paper 26.2 / Java 25 Gradle build +- simplify the build around the active Paper 26.2 target and remove the retired 1.x local server dependency - keep build output clean and predictable in `build/libs` with the 2026 versioned jar naming - keep legacy tree data compatible by continuing to read `plugins/X-Mas/trees.yml` @@ -58,6 +59,12 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de - make invalid debug page or section requests return a helpful response instead of silently falling back - add `/xmastree debug toggle true|false` for live boolean config changes - keep tab completion focused on named debug categories instead of numeric page suggestions +- add `/xmastree inspect` for staff tree support checks, including owner, tree UUID, level, location, remaining requirements, refund preview, present timer, and scheduled presents +- make `/xmastree reload` print a short reload report with locale, gift/head/tree counts, key toggles, alias state, and sound volumes +- add `/xmastree test sound` and `/xmastree test particle` so admins can preview reloadable sound volume and particle settings without waiting for live tree interactions +- add `/xmastree data backup` and `/xmastree data validate` for safe `trees.yml` support checks before event work or migration testing +- add `/xmastree data migrate-world [dry-run|apply]` so saved tree world names can be reviewed and migrated safely with an automatic backup +- add `/xmastree gifts list`, `/xmastree gifts roll`, and `/xmastree gifts remove ` for command-line gift pool management before a GUI exists - add optional PlaceholderAPI support with the `onembxmastree` namespace - add placeholders for event state, end time, end countdown, auto-end, resource-back, particles, luck, tree totals, owner totals, player tree count, and plugin version @@ -88,6 +95,8 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de - update config comments and improve documentation for installation, building, commands, permissions, placeholders, support, and credits - point support to the GitHub issues page - refresh `.gitignore` for local dev/test folders and obvious OS/build junk +- remove deprecated skull-profile reads from gameplay logic and add an opt-in Gradle deprecation lint switch for future work +- promote the fork to a Paper 26.2-first custom 1MB target with a minor version bump ## Installation @@ -98,9 +107,10 @@ The checked-in source compiles against the centralized Paper 26.1.2 cache and de 5. Start the server with Java 25. 6. Check the console for XMas Tree startup messages, then run `/xmastree` in game or console. -For the 2026 target, use the modern Paper 26.1.2 jar: +For the 2026 target, use the modern Paper 26.2 jar: -- Paper 26.1.2: `1MB-XMas-2026-v2.0.1-024-v25-26.1.2.jar` +- Paper 26.2: `1MB-XMas-2026-v2.1.0-032-v25-26.2.jar` +- Future 26.x: use the same jar for forward-compatibility testing ## Building @@ -108,36 +118,37 @@ Requirements: - JDK 25 - Gradle -- Centralized Paper server cache at `/Users/floris/Projects/Codex/servers/cache/Paper-26.1.2` -- Centralized PlaceholderAPI jar at `/Users/floris/Projects/Codex/servers/cache/Paper-26.1.2/plugins/PlaceholderAPI-2.12.3-DEV-265.jar` +- Centralized Paper server cache at `/Users/floris/Projects/Codex/servers/cache/Paper-26.2` +- PlaceholderAPI build `266` in the centralized 26.2 cache, sourced from the matching 26.2 1MB CMI-API server setup -This repo no longer uses a local `servers/` folder for compilation or testing. If a local `servers/` folder still exists here, it is ignored and treated as retired local data. +This repo no longer requires a local `servers/` folder for compilation. If an ignored local `servers/` folder exists here for ad-hoc compatibility testing, treat it as optional local data rather than part of the project. -Build the current Paper 26.1.2 jar: +Build the current Paper 26.2 jar: ```bash gradle clean buildAllJars ``` -Build only the Paper 26.1.2 jar: +Build only the Paper 26.2 jar: ```bash gradle jar ``` -The `paper2612Jar` task is kept as an alias: +The `paper262Jar` task is kept as an alias: ```bash -gradle paper2612Jar +gradle paper262Jar ``` `buildAllJars` now: -- compiles against centralized Paper API `26.1.2.build.20-alpha` +- compiles against centralized Paper API `26.2.build.29-alpha` +- declares plugin `api-version: 26.2` - keeps the Java release target at `25` - writes the standard jar to `build/libs/` - copies the same jar into `libs/` for the centralized test runner -- prints the active build config, compile target, and declared plugin API floor +- prints the active build config, compile target, declared plugin API version, and forward-compatibility target You can also inspect the build metadata directly with: @@ -145,6 +156,12 @@ You can also inspect the build metadata directly with: gradle printBuildConfig ``` +Run an explicit deprecated-API lint pass when doing future refactors: + +```bash +gradle clean compileJava -PlintDeprecatedApi=true +``` + End users do not need any `servers/` folder. The installable jars are written to `build/libs/`, and this project also keeps a local copy in `libs/` for shared test-runner use. ## Commands @@ -159,8 +176,21 @@ If `core.commands.legacy-command-enabled` is `true`, the legacy `/xmas` alias is | `/xmastree help` | Shows the command list. | | `/xmastree give ` | Gives an online player a Christmas Crystal. | | `/xmastree gifts` | Spawns a small batch of presents under every loaded Christmas tree. | +| `/xmastree gifts spawn` | Explicit form of `/xmastree gifts`; spawns presents under every loaded Christmas tree. | +| `/xmastree gifts list [page]` | Lists configured gift rewards by index. | +| `/xmastree gifts roll` | Previews one random gift roll without giving it to a player. | +| `/xmastree gifts remove ` | Removes a configured gift reward by the index shown in `/xmastree gifts list`. | | `/xmastree addhand` | Adds the item in your main hand to the gift list and saves it to `config.yml`. | -| `/xmastree reload` | Reloads config, locale, present heads, gifts, luck settings, command alias settings, and tree level requirements. | +| `/xmastree reload` | Reloads config, translations, theme, prefix, crystal item text, present heads, gifts, luck settings, command alias settings, and tree level requirements, then prints a short reload report. | +| `/xmastree inspect` | Inspects the tree you are looking at, or the nearest tree if no tree block is targeted. | +| `/xmastree inspect nearest ` | Inspects the nearest loaded tree to an online player. | +| `/xmastree inspect ` | Inspects a specific loaded tree by UUID. | +| `/xmastree test sound first [player]` | Plays the configured first ingredient sound volume for yourself or an online player. | +| `/xmastree test sound repeat [player]` | Plays the configured repeat ingredient sound volume for yourself or an online player. | +| `/xmastree test particle [all\|ambient\|swag\|body] [player]` | Previews configured particle effects for `sapling`, `small_tree`, `tree`, or `magic_tree`. | +| `/xmastree data backup` | Creates a timestamped copy of `plugins/X-Mas/trees.yml` in `plugins/X-Mas/backups/`. | +| `/xmastree data validate` | Reads `trees.yml` and reports missing worlds, invalid IDs, owners, levels, coordinates, requirements, and duplicate locations. | +| `/xmastree data migrate-world [dry-run\|apply]` | Counts or rewrites saved tree world names in `trees.yml`. `dry-run` is the default. `apply` creates a backup first, writes the change, and should be followed by a server restart before testing migrated trees. | | `/xmastree debug` | Opens the `status` debug section by default. | | `/xmastree debug [section\|page]` | Shows debug output for `status`, `commands`, `permissions`, `placeholders`, or `config`. Numeric pages `1-5` still work as a legacy shortcut. | | `/xmastree debug toggle true\|false` | Toggles supported global boolean config keys and reloads the plugin config. | @@ -210,6 +240,9 @@ Numeric compatibility remains available for existing habits and old screenshots: | `onembxmastree.command.gifts` | `op` | Allows `/xmastree gifts`. | | `onembxmastree.command.addhand` | `op` | Allows `/xmastree addhand`. | | `onembxmastree.command.reload` | `op` | Allows `/xmastree reload`. | +| `onembxmastree.command.inspect` | `op` | Allows `/xmastree inspect`. | +| `onembxmastree.command.test` | `op` | Allows `/xmastree test sound` and `/xmastree test particle`. | +| `onembxmastree.command.data` | `op` | Allows `/xmastree data backup` and `/xmastree data validate`. | | `onembxmastree.command.debug` | `op` | Allows `/xmastree debug [section\|page]`. | | `onembxmastree.command.debug.toggle` | `op` | Allows `/xmastree debug toggle true\|false`. | | `onembxmastree.command.end` | `op` | Allows `/xmastree end`. | @@ -225,13 +258,16 @@ If `core.holiday-ends.resource-back` is enabled, confirmed tree destruction retu Ingredient accept sounds can be tuned live in `config.yml` under `core.sounds.grow`. Use `0.0` for silent, `0.1` for quiet, `0.5` for half volume, and `1.0` for full volume. `/xmastree reload` applies the new values without a server restart. +Admins can preview those sound values with `/xmastree test sound first` and `/xmastree test sound repeat`. Particle configuration can be previewed with `/xmastree test particle sapling`, `/xmastree test particle small_tree`, `/xmastree test particle tree`, or `/xmastree test particle magic_tree`. + ## Configuration The plugin writes its files to `plugins/X-Mas`: - `config.yml` controls event timing, locale, tree limits, gift cooldowns, present skins, gift items, and level-up requirements. - `trees.yml` stores placed tree data and should be kept when upgrading an existing event. -- `locales/*.yml` controls player-facing messages and crystal display text. +- `translations/locale_en.yml` is the bundled English source of truth for player/admin text, theme colors, and prefixes. +- optional custom translations can be added as `translations/locale_.yml`. Use modern Paper/Bukkit material names such as `GOLD_INGOT`, `SPRUCE_LOG`, and `PLAYER_HEAD`. Legacy numeric IDs and old material names are skipped to avoid modern Paper exceptions. @@ -244,7 +280,13 @@ Gift entries in `xmas.gifts` can be simple material names: Admins can also hold an item and run `/xmastree addhand`. This saves the exact item as Base64 so custom names, lore, enchantments, and metadata can be used as gifts. -Legacy world-name remapping lives under `migration.world-aliases`. This is useful when an old `trees.yml` was saved in worlds like `general`, `wild`, or `santa`, but the new Paper 26.1.2 server uses different world names: +Gift pool maintenance commands: + +- `/xmastree gifts list [page]` shows the currently loaded gift rewards. +- `/xmastree gifts roll` previews one random gift selection without giving it to anyone. +- `/xmastree gifts remove ` removes the indexed gift from `xmas.gifts`, saves `config.yml`, and reloads the gift pool. + +Legacy world-name remapping lives under `migration.world-aliases`. This is useful when an old `trees.yml` was saved in worlds like `general`, `wild`, or `santa`, but the new Paper 26.2 server uses different world names: ```yaml migration: @@ -256,28 +298,55 @@ migration: The saved coordinates are preserved. If the destination world terrain or world border changed, some legacy tree locations may still need manual cleanup. +Admins can preview or apply a world-name rewrite directly against `trees.yml`: + +```text +/xmastree data migrate-world general world +/xmastree data migrate-world general world apply +``` + +The first command is a dry run. The `apply` command creates a timestamped backup in `plugins/X-Mas/backups/`, rewrites matching saved world names, and should be followed by a server restart before testing migrated legacy trees. + Present head entries in `xmas.presents` should use `textures.minecraft.net` URLs. Old player-name entries are still accepted for compatibility. -Per-stage particles live under `xmas.tree-lvl..particles`. Particle names should come from the Paper 26.1.2 `Particle` enum: +Per-stage particles live under `xmas.tree-lvl..particles`. Particle names should come from the Paper 26.2 `Particle` enum: -[jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html](https://jd.papermc.io/paper/26.1.2/org/bukkit/Particle.html) +[jd.papermc.io/paper/26.2/org/bukkit/Particle.html](https://jd.papermc.io/paper/26.2/org/bukkit/Particle.html) The config currently supports simple particles and `DUST`. ## MiniMessage -Locale messages, crystal names, crystal lore, and command messages support MiniMessage: +Translation messages, crystal names, crystal lore, command/debug text, and prefixes support MiniMessage: ```yaml crystal: - name: Christmas Crystal + name: Christmas Crystal lore: - - Concentrated Christmas Spirit - - Use it on a spruce sapling to fill it with magic! + - Concentrated Christmas Spirit + - Use it on a spruce sapling to fill it with magic. ``` +The fork also ships a small semantic pastel tag set for locale files and command output: + +- `` main readable text +- `` softer secondary text +- `` mint accent text +- `` rose accent text +- `` warm label text +- `` command/path accent text +- ``, ``, ``, `` status tones + Legacy `&` color codes are still parsed for compatibility when a message does not contain MiniMessage tags. +The active translation path is: + +```text +plugins/X-Mas/translations/locale_en.yml +``` + +`core.locale: en` maps to `locale_en.yml`. A custom file such as `locale_fr.yml` can be loaded by setting `core.locale: fr`. + ## Placeholders PlaceholderAPI is optional. If PlaceholderAPI is installed, X-Mas registers the `onembxmastree` expansion. @@ -308,7 +377,7 @@ The dotted key after `onembxmastree_` is supported to keep the placeholders read | `%onembxmastree_trees.total%` | `14` | Total loaded X-Mas trees. | | `%onembxmastree_trees.owners%` | `6` | Number of unique loaded tree owners. | | `%onembxmastree_player.trees%` | `2` | Number of loaded trees owned by the placeholder player. | -| `%onembxmastree_version%` | `2.0.1-024` | Loaded plugin version. | +| `%onembxmastree_version%` | `2.1.0-032` | Loaded plugin version. | CMI hologram example: @@ -334,7 +403,7 @@ ajLeaderboards placeholder examples: - When saved world names no longer match the current server world names, `migration.world-aliases` can remap them without rewriting `trees.yml`. - Existing present head player-name entries are still accepted, but new configs should prefer Mojang texture URLs. - The modern jars are compiled with Java 25 bytecode and should be run on Java 25. -- The Paper 26.1.2 jar is the intended winter 2026 target, and the same jar now declares `api-version: 1.21.11` so it can be smoke-tested on both Paper 1.21.11 and Paper 26.1.2. +- The Paper 26.2 jar is the intended winter 2026 target, and the same jar should be used for forward-compatibility testing on newer 26.x builds. ## Security notes @@ -342,7 +411,7 @@ ajLeaderboards placeholder examples: - Present texture URLs are restricted to `textures.minecraft.net`. - Gift item Base64 entries are capped before deserialization. - Config material names are resolved with modern `Material.matchMaterial` and invalid or legacy values are skipped. -- Treat `config.yml` and locale files as trusted admin-controlled files, especially when using MiniMessage click or hover tags. +- Treat `config.yml` and translation files as trusted admin-controlled files, especially when using MiniMessage click or hover tags. ## Support diff --git a/build.gradle b/build.gradle index 8a24e5b..0ab0ad3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,22 +3,28 @@ plugins { } group = 'com.onemb.xmas' -version = '2.0.1-024' +version = '2.1.0-032' def projectVersion = version.toString() +def enableDeprecationLint = providers.gradleProperty('lintDeprecatedApi').map { it.toBoolean() }.getOrElse(false) def javaRelease = 25 -def paperCompileVersion = '26.1.2.build.20-alpha' -def paperCompatibilityFloor = '1.21.11' +def paperCompileVersion = '26.2.build.29-alpha' +def declaredPluginApiVersion = '26.2' +def experimentalCompatibilityTarget = 'future 26.x' +def resourceExpansionProperties = [ + version : projectVersion, + apiVersion: declaredPluginApiVersion +] def sharedServersRoot = file(System.getenv('CODEX_SHARED_SERVERS_ROOT') ?: '/Users/floris/Projects/Codex/servers') -def sharedPaperCache = new File(sharedServersRoot, 'cache/Paper-26.1.2') -def paper2612LibrariesDir = new File(sharedPaperCache, 'libraries') -def paper2612Api = new File(sharedPaperCache, "libraries/io/papermc/paper/paper-api/${paperCompileVersion}/paper-api-${paperCompileVersion}.jar") -def placeholderApiJar = new File(sharedPaperCache, 'plugins/PlaceholderAPI-2.12.3-DEV-265.jar') -def paper2612ArchiveName = "1MB-XMas-2026-v${projectVersion}-v25-26.1.2.jar" +def sharedPaperCache = new File(sharedServersRoot, 'cache/Paper-26.2') +def paper262LibrariesDir = new File(sharedPaperCache, 'libraries') +def paper262Api = new File(sharedPaperCache, "libraries/io/papermc/paper/paper-api/${paperCompileVersion}/paper-api-${paperCompileVersion}.jar") +def placeholderApiJar = new File(sharedPaperCache, 'plugins/PlaceholderAPI-2.12.3-DEV-266.jar') +def paper262ArchiveName = "1MB-XMas-2026-v${projectVersion}-v25-26.2.jar" [ - [paper2612Api, "Paper API jar"], - [paper2612LibrariesDir, "Paper libraries directory"], + [paper262Api, "Paper API jar"], + [paper262LibrariesDir, "Paper libraries directory"], [placeholderApiJar, "PlaceholderAPI jar"], ].each { entry -> File path = entry[0] as File @@ -35,28 +41,29 @@ java { } dependencies { - compileOnly files(paper2612Api) - compileOnly fileTree(dir: paper2612LibrariesDir, include: '**/*.jar') + compileOnly files(paper262Api) + compileOnly fileTree(dir: paper262LibrariesDir, include: '**/*.jar') compileOnly files(placeholderApiJar) } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' options.release = javaRelease + if (enableDeprecationLint) { + options.compilerArgs.add('-Xlint:deprecation') + } } tasks.named('processResources') { filteringCharset = 'UTF-8' + inputs.properties(resourceExpansionProperties) filesMatching('plugin.yml') { - expand( - version: projectVersion, - apiVersionFloor: paperCompatibilityFloor - ) + expand(resourceExpansionProperties) } } tasks.named('jar') { - archiveFileName = paper2612ArchiveName + archiveFileName = paper262ArchiveName } tasks.register('releaseJar', Copy) { @@ -67,8 +74,8 @@ tasks.register('releaseJar', Copy) { into(layout.projectDirectory.dir('libs')) } -tasks.register('paper2612Jar') { - description = 'Assembles the Paper 26.1.2 Java 25 plugin jar and copies it into libs/.' +tasks.register('paper262Jar') { + description = 'Assembles the Paper 26.2 Java 25 plugin jar and copies it into libs/.' group = 'build' dependsOn tasks.named('releaseJar') } @@ -80,22 +87,24 @@ tasks.register('printBuildConfig') { println 'XMasTree build config' println " version: ${projectVersion}" println " compile target: Paper API ${paperCompileVersion}" - println " plugin api-version floor: ${paperCompatibilityFloor}" + println " declared plugin api-version: ${declaredPluginApiVersion}" + println " experimental compatibility target: Paper ${experimentalCompatibilityTarget}" println " java release target: ${javaRelease}" println " centralized Paper cache: ${sharedPaperCache}" println " PlaceholderAPI compile jar: ${placeholderApiJar}" - println " build/libs output: ${layout.buildDirectory.file("libs/${paper2612ArchiveName}").get().asFile}" - println " libs/ release copy: ${layout.projectDirectory.file("libs/${paper2612ArchiveName}").asFile}" + println " deprecation lint enabled: ${enableDeprecationLint}" + println " build/libs output: ${layout.buildDirectory.file("libs/${paper262ArchiveName}").get().asFile}" + println " libs/ release copy: ${layout.projectDirectory.file("libs/${paper262ArchiveName}").asFile}" } } tasks.register('buildAllJars') { - description = 'Builds the current Paper 26.1.2 target jar and copies it into libs/.' + description = 'Builds the current Paper 26.2 target jar and copies it into libs/.' group = 'build' dependsOn tasks.named('releaseJar') finalizedBy tasks.named('printBuildConfig') } tasks.named('assemble') { - dependsOn tasks.named('paper2612Jar') + dependsOn tasks.named('paper262Jar') } diff --git a/src/main/java/ru/meloncode/xmas/Events.java b/src/main/java/ru/meloncode/xmas/Events.java index 730b32d..a44ee63 100644 --- a/src/main/java/ru/meloncode/xmas/Events.java +++ b/src/main/java/ru/meloncode/xmas/Events.java @@ -12,14 +12,12 @@ import org.bukkit.event.block.*; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.entity.ItemSpawnEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.StructureGrowEvent; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.persistence.PersistentDataType; import ru.meloncode.xmas.utils.TextUtils; @@ -41,7 +39,7 @@ public void onPlayerOpenPresent(PlayerInteractEvent event) { if (event.getHand() == EquipmentSlot.OFF_HAND) return; //Event firing for both hands if (event.getAction() == Action.RIGHT_CLICK_BLOCK) { Block block = event.getClickedBlock(); - if (block != null && block.getType() == Material.PLAYER_HEAD) { + if (XMas.isPresentHead(block)) { XMas.processPresent(block, event.getPlayer()); } } @@ -50,7 +48,8 @@ public void onPlayerOpenPresent(PlayerInteractEvent event) { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerOpenPresent(BlockBreakEvent event) { Block block = event.getBlock(); - if (block != null && block.getType() == Material.PLAYER_HEAD) { + if (XMas.isPresentHead(block)) { + event.setDropItems(false); XMas.processPresent(block, event.getPlayer()); } } @@ -170,18 +169,6 @@ public void onBlockBreakByExplosion(EntityExplodeEvent event) { } } - @EventHandler - public void onItemSpawn(ItemSpawnEvent event) { - ItemStack item = event.getEntity().getItemStack(); - if (item.getType() == Material.PLAYER_HEAD) { - SkullMeta meta = (SkullMeta) item.getItemMeta(); - String headId = getHeadIdentifier(meta); - if (headId != null && Main.getHeads().contains(headId)) { - event.setCancelled(true); - } - } - } - @EventHandler public void onPistonRetract(BlockPistonRetractEvent event) { if (MagicTree.isBlockBelongs(event.getBlock())) { @@ -227,14 +214,14 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { XMas.removeTree(tree); } if (Main.inProgress) - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_COMPLETE); + TextUtils.sendMessage(player, TextUtils.success(LocaleManager.DESTROY_COMPLETE)); } else { destroyers.put(player.getUniqueId(), System.currentTimeMillis()); if (Main.inProgress) - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_WARNING); - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_TUT); + TextUtils.sendMessage(player, TextUtils.warning(LocaleManager.DESTROY_WARNING)); + TextUtils.sendMessage(player, TextUtils.muted(LocaleManager.DESTROY_TUT)); if (Main.resourceBack) { - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_RESOURCE_BACK); + TextUtils.sendMessage(player, TextUtils.info(LocaleManager.DESTROY_RESOURCE_BACK)); } } else { @@ -247,9 +234,9 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { case SPRUCE_LEAVES: case GLOWSTONE: if (Main.inProgress) - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_SANTA); + TextUtils.sendMessage(player, TextUtils.accent(LocaleManager.DESTROY_LEAVES_SANTA)); if (player.getUniqueId().equals(tree.getOwner()) || XMasCommand.canOverrideTree(player)) { - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_LEAVES_TUT); + TextUtils.sendMessage(player, TextUtils.warning(LocaleManager.DESTROY_LEAVES_TUT)); } else { TextUtils.sendMessage(player, LocaleManager.DESTROY_FAIL_OWNER); } @@ -263,14 +250,14 @@ public void onPlayerBreakBlock(BlockBreakEvent event) { } else { XMas.removeTree(tree); } - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_COMPLETE); + TextUtils.sendMessage(player, TextUtils.success(LocaleManager.DESTROY_COMPLETE)); } else { destroyers.put(player.getUniqueId(), System.currentTimeMillis()); if (Main.inProgress) - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_SAPLING); - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_TUT); + TextUtils.sendMessage(player, TextUtils.warning(LocaleManager.DESTROY_SAPLING)); + TextUtils.sendMessage(player, TextUtils.muted(LocaleManager.DESTROY_TUT)); if (Main.resourceBack) { - TextUtils.sendMessage(player, "" + LocaleManager.DESTROY_RESOURCE_BACK); + TextUtils.sendMessage(player, TextUtils.info(LocaleManager.DESTROY_RESOURCE_BACK)); } } } else { @@ -331,13 +318,4 @@ private void chunkLoad(ChunkLoadEvent e) tree.spawnScheduledPresents(); } } - - private String getHeadIdentifier(SkullMeta meta) { - if (meta.getPlayerProfile() != null - && meta.getPlayerProfile().getTextures() != null - && meta.getPlayerProfile().getTextures().getSkin() != null) { - return meta.getPlayerProfile().getTextures().getSkin().toString(); - } - return meta.getOwningPlayer() != null ? meta.getOwningPlayer().getName() : null; - } } diff --git a/src/main/java/ru/meloncode/xmas/LocaleManager.java b/src/main/java/ru/meloncode/xmas/LocaleManager.java index 91cd990..88db720 100644 --- a/src/main/java/ru/meloncode/xmas/LocaleManager.java +++ b/src/main/java/ru/meloncode/xmas/LocaleManager.java @@ -1,17 +1,33 @@ package ru.meloncode.xmas; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; import ru.meloncode.xmas.utils.ConfigUtils; import ru.meloncode.xmas.utils.TextUtils; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; -public class LocaleManager { +public final class LocaleManager { - private static final FileConfiguration def_locale = ConfigUtils.loadConfig(new File(Main.getInstance().getDataFolder() + "/locales/default.yml")); - public static String PLUGIN_NAME; + public static final String DEFAULT_LOCALE_CODE = "en"; + private static final String DEFAULT_LOCALE_RESOURCE_PATH = "translations/locale_en.yml"; + private static final String TRANSLATIONS_DIRECTORY = "translations"; + private static final String LEGACY_LOCALES_DIRECTORY = "locales"; + + private static final String BOOTSTRAP_PLUGIN_NAME = "XMas Tree"; + private static final String BOOTSTRAP_PREFIX = "[{plugin_name}] "; + private static final String BOOTSTRAP_CONSOLE_PREFIX = "[{plugin_name}] "; + + private static final Map BOOTSTRAP_THEME = createBootstrapTheme(); + + public static String PLUGIN_NAME = BOOTSTRAP_PLUGIN_NAME; public static String PLUGIN_ENABLED; public static String GROW_LVL_PROGRESS; public static String GROW_LVL_READY; @@ -29,94 +45,297 @@ public class LocaleManager { public static String DESTROY_TUT; public static String DESTROY_COMPLETE; public static String CRYSTAL_NAME; - public static List CRYSTAL_LORE; + public static List CRYSTAL_LORE = new ArrayList<>(); public static String GIFT_LUCK; public static String GIFT_FAIL; public static String TIMEOUT; public static String HAPPY_NEW_YEAR; - public static List COMMAND_HELP; + public static List COMMAND_HELP = new ArrayList<>(); public static String COMMAND_PLAYER_OFFLINE; public static String COMMAND_NO_PLAYER_NAME; public static String COMMAND_GIVEAWAY; + + private static FileConfiguration defaultLocale; + private static FileConfiguration bundledDefaultLocale; private static FileConfiguration locale; + private static String activeLocaleCode = DEFAULT_LOCALE_CODE; + private static String chatPrefix = BOOTSTRAP_PREFIX; + private static String consolePrefix = BOOTSTRAP_CONSOLE_PREFIX; + private static Map themeAliases = buildThemeAliases(BOOTSTRAP_THEME); + + private LocaleManager() { + } - public static void loadLocale(String lang) { - File file = new File(Main.getInstance().getDataFolder() + "/locales/" + lang + ".yml"); - if (!file.exists()) { - TextUtils.sendConsoleMessage("Can't load locale '" + lang + "'"); - TextUtils.sendConsoleMessage("Switching to default locale 'en'"); - locale = def_locale; + public static void loadLocale(String localeCode) { + Main plugin = (Main) Main.getInstance(); + activeLocaleCode = normalizeLocaleCode(localeCode); + + File englishFile = getTranslationFile(plugin, DEFAULT_LOCALE_CODE); + migrateLegacyLocaleIfNeeded(plugin, DEFAULT_LOCALE_CODE, englishFile); + bundledDefaultLocale = ConfigUtils.loadResourceConfig(plugin, DEFAULT_LOCALE_RESOURCE_PATH); + defaultLocale = ConfigUtils.loadManagedConfig(plugin, DEFAULT_LOCALE_RESOURCE_PATH, englishFile); + + File requestedFile = getTranslationFile(plugin, activeLocaleCode); + if (!DEFAULT_LOCALE_CODE.equals(activeLocaleCode)) { + migrateLegacyLocaleIfNeeded(plugin, activeLocaleCode, requestedFile); + } + + if (DEFAULT_LOCALE_CODE.equals(activeLocaleCode)) { + locale = defaultLocale; + } else if (requestedFile.exists()) { + YamlConfiguration requestedLocale = ConfigUtils.loadConfig(requestedFile); + boolean changed = ConfigUtils.synchronizeWithDefaults(requestedLocale, defaultLocale); + locale = requestedLocale; + if (changed) { + ConfigUtils.saveConfig(requestedFile, requestedLocale); + } } else { - locale = ConfigUtils.loadConfig(file); - TextUtils.sendConsoleMessage("Locale '" + lang + "' successfully loaded"); + locale = defaultLocale; } + loadStrings(); + if (DEFAULT_LOCALE_CODE.equals(activeLocaleCode) || requestedFile.exists()) { + TextUtils.sendConsoleMessage(TextUtils.success(text("console.translation.loaded", "Loaded translation '{file}'.").replace("{file}", requestedFile.getName()))); + } else { + TextUtils.sendConsoleMessage(TextUtils.warning(text("console.translation.missing", "Translation '{file}' was not found.").replace("{file}", requestedFile.getName()))); + TextUtils.sendConsoleMessage(TextUtils.muted(text("console.translation.fallback", "Falling back to locale_en.yml."))); + } + } + + public static String text(String path) { + return text(path, null); + } + + public static String text(String path, String fallback) { + String value = resolveString(locale, path); + if (value != null) { + return replaceCommonTokens(value); + } + + value = resolveString(defaultLocale, path); + if (value != null) { + return replaceCommonTokens(value); + } + + return fallback == null ? null : replaceCommonTokens(fallback); + } + + public static String text(String path, String fallback, String... replacements) { + String value = text(path, fallback); + if (value == null || replacements == null) { + return value; + } + for (int i = 0; i + 1 < replacements.length; i += 2) { + value = value.replace(replacements[i], String.valueOf(replacements[i + 1])); + } + return value; + } + + public static List textList(String path) { + List values = resolveStringList(locale, path); + if (!values.isEmpty()) { + return replaceCommonTokens(values); + } + + values = resolveStringList(defaultLocale, path); + if (!values.isEmpty()) { + return replaceCommonTokens(values); + } + + return new ArrayList<>(); + } + + public static List bundledTextList(String path) { + List values = resolveStringList(bundledDefaultLocale, path); + return replaceCommonTokens(values); + } + + public static String replaceCommonTokens(String input) { + if (input == null) { + return null; + } + return input.replace("{plugin_name}", PLUGIN_NAME != null ? PLUGIN_NAME : BOOTSTRAP_PLUGIN_NAME); + } + + public static List replaceCommonTokens(List lines) { + List replaced = new ArrayList<>(); + for (String line : lines) { + replaced.add(replaceCommonTokens(line)); + } + return replaced; + } + + public static Map getThemeAliases() { + return themeAliases; + } + + public static String getChatPrefix() { + return replaceCommonTokens(chatPrefix); + } + + public static String getConsolePrefix() { + return replaceCommonTokens(consolePrefix); + } + + public static String getActiveLocaleCode() { + return activeLocaleCode; } private static void loadStrings() { - PLUGIN_NAME = getString("plugin-name"); - PLUGIN_ENABLED = getString("messages.plugin-enabled"); - GROW_LVL_PROGRESS = getString("messages.tree.grow-lvl-progress"); - GROW_LVL_READY = getString("messages.tree.grow-lvl-ready"); - GROW_LEVEL_MAX = getString("messages.tree.grow-lvl-max"); - GROW_REQ_LIST_TITLE = getString("messages.tree.grow-req-list-title"); - GROW_REQ_LIST_HINT = getString("messages.tree.grow-req-list-hint"); - GROW_NOT_ENOUGH_PLACE = getString("messages.tree.grow-not-enough-place"); - TREE_LIMIT = getString("messages.tree.tree-limit"); - DESTROY_SAPLING = getString("messages.tree.destroy-sapling"); - DESTROY_LEAVES_SANTA = getString("messages.tree.destroy-leaves-santa"); - DESTROY_LEAVES_TUT = getString("messages.tree.destroy-leaves-tut"); - DESTROY_WARNING = getString("messages.tree.destroy-warning"); - DESTROY_RESOURCE_BACK = getString("messages.tree.destroy-resource-back"); - DESTROY_TUT = getString("messages.tree.destroy-tut"); - DESTROY_COMPLETE = getString("messages.tree.destroy-complete"); - DESTROY_FAIL_OWNER = getString("messages.tree.destroy-fail-owner"); - CRYSTAL_NAME = getString("crystal.name"); - CRYSTAL_LORE = getStringList("crystal.lore"); - GIFT_LUCK = getString("messages.gift.luck-message"); - GIFT_FAIL = getString("messages.gift.unluck-message"); - TIMEOUT = getString("messages.timeout"); - HAPPY_NEW_YEAR = getString("messages.final-wish"); - - COMMAND_HELP = getStringList("command.help"); - COMMAND_PLAYER_OFFLINE = getString("command.player-offline"); - COMMAND_NO_PLAYER_NAME = getString("command.no-player-name"); - COMMAND_GIVEAWAY = getString("command.giveaway"); - - } - - private static String getString(String path) { - if (locale == null) - throw new NullPointerException("Locale not loaded"); + PLUGIN_NAME = text("plugin-name", BOOTSTRAP_PLUGIN_NAME); + chatPrefix = text("format.prefix", BOOTSTRAP_PREFIX); + consolePrefix = text("format.console-prefix", BOOTSTRAP_CONSOLE_PREFIX); + themeAliases = buildThemeAliases(readTheme()); - try { - String message = locale.getString(path); - return message.contains("_UNUSED") ? null : message; - } catch (NullPointerException e) { - TextUtils.sendConsoleMessage("Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); - TextUtils.sendConsoleMessage("Using default locale to get value"); - return def_locale.getString(path); + PLUGIN_ENABLED = text("messages.plugin-enabled"); + GROW_LVL_PROGRESS = text("messages.tree.grow-lvl-progress"); + GROW_LVL_READY = text("messages.tree.grow-lvl-ready"); + GROW_LEVEL_MAX = text("messages.tree.grow-lvl-max"); + GROW_REQ_LIST_TITLE = text("messages.tree.grow-req-list-title"); + GROW_REQ_LIST_HINT = text("messages.tree.grow-req-list-hint"); + GROW_NOT_ENOUGH_PLACE = text("messages.tree.grow-not-enough-place"); + TREE_LIMIT = text("messages.tree.tree-limit"); + DESTROY_SAPLING = text("messages.tree.destroy-sapling"); + DESTROY_LEAVES_SANTA = text("messages.tree.destroy-leaves-santa"); + DESTROY_LEAVES_TUT = text("messages.tree.destroy-leaves-tut"); + DESTROY_WARNING = text("messages.tree.destroy-warning"); + DESTROY_RESOURCE_BACK = text("messages.tree.destroy-resource-back"); + DESTROY_TUT = text("messages.tree.destroy-tut"); + DESTROY_COMPLETE = text("messages.tree.destroy-complete"); + DESTROY_FAIL_OWNER = text("messages.tree.destroy-fail-owner"); + CRYSTAL_NAME = text("crystal.name"); + CRYSTAL_LORE = textList("crystal.lore"); + GIFT_LUCK = text("messages.gift.luck-message"); + GIFT_FAIL = text("messages.gift.unluck-message"); + TIMEOUT = text("messages.timeout"); + HAPPY_NEW_YEAR = text("messages.final-wish"); + + COMMAND_HELP = textList("command.help"); + COMMAND_PLAYER_OFFLINE = text("command.player-offline"); + COMMAND_NO_PLAYER_NAME = text("command.no-player-name"); + COMMAND_GIVEAWAY = text("command.giveaway"); + } + + private static Map readTheme() { + Map theme = new LinkedHashMap<>(BOOTSTRAP_THEME); + theme.put("xm-text", text("theme.text", BOOTSTRAP_THEME.get("xm-text"))); + theme.put("xm-muted", text("theme.muted", BOOTSTRAP_THEME.get("xm-muted"))); + theme.put("xm-accent", text("theme.accent", BOOTSTRAP_THEME.get("xm-accent"))); + theme.put("xm-accent-2", text("theme.accent-secondary", BOOTSTRAP_THEME.get("xm-accent-2"))); + theme.put("xm-label", text("theme.label", BOOTSTRAP_THEME.get("xm-label"))); + theme.put("xm-success", text("theme.success", BOOTSTRAP_THEME.get("xm-success"))); + theme.put("xm-warning", text("theme.warning", BOOTSTRAP_THEME.get("xm-warning"))); + theme.put("xm-error", text("theme.error", BOOTSTRAP_THEME.get("xm-error"))); + theme.put("xm-info", text("theme.info", BOOTSTRAP_THEME.get("xm-info"))); + theme.put("xm-command", text("theme.command", text("theme.accent-secondary", BOOTSTRAP_THEME.get("xm-command")))); + return theme; + } + + private static Map buildThemeAliases(Map theme) { + Map aliases = new LinkedHashMap<>(); + for (Map.Entry entry : theme.entrySet()) { + aliases.put("<" + entry.getKey() + ">", "<" + entry.getValue() + ">"); + aliases.put("", ""); } + return aliases; } - private static List getStringList(String path) { - if (locale == null) - throw new NullPointerException("Locale not loaded"); + private static String resolveString(FileConfiguration configuration, String path) { + if (configuration == null || path == null) { + return null; + } - try { - List raw = locale.getStringList(path); - List list = new ArrayList<>(); - for (String s : raw) { - list.add(s); - } + String message = configuration.getString(path); + if (message == null || message.contains("_UNUSED")) { + return null; + } + return message; + } + + private static List resolveStringList(FileConfiguration configuration, String path) { + List list = new ArrayList<>(); + if (configuration == null || path == null) { + return list; + } + + List raw = configuration.getStringList(path); + if (raw == null) { return list; + } + for (String line : raw) { + list.add(line); + } + return list; + } - } catch (IllegalArgumentException e) { - TextUtils.sendConsoleMessage("Unable to find '" + path + "' in locale " + Main.getInstance().getConfig().getString("core.locale") + ". Bad File?"); - TextUtils.sendConsoleMessage("Using default locale to get value"); - return def_locale.getStringList(path); + private static File getTranslationFile(Main plugin, String localeCode) { + return new File(plugin.getDataFolder(), TRANSLATIONS_DIRECTORY + "/locale_" + localeCode + ".yml"); + } + + private static String normalizeLocaleCode(String localeCode) { + if (localeCode == null || localeCode.isBlank()) { + return DEFAULT_LOCALE_CODE; } + String normalized = localeCode.trim().toLowerCase(); + if (normalized.startsWith("locale_")) { + normalized = normalized.substring("locale_".length()); + } + if (normalized.endsWith(".yml")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + if (normalized.startsWith("translations/")) { + normalized = normalized.substring("translations/".length()); + } + return normalized.isBlank() ? DEFAULT_LOCALE_CODE : normalized; + } + + private static void migrateLegacyLocaleIfNeeded(Main plugin, String localeCode, File targetFile) { + if (targetFile.exists()) { + return; + } + + File legacyLocaleFile = new File(plugin.getDataFolder(), LEGACY_LOCALES_DIRECTORY + "/" + localeCode + ".yml"); + File legacyDefaultFile = new File(plugin.getDataFolder(), LEGACY_LOCALES_DIRECTORY + "/default.yml"); + File sourceFile = legacyLocaleFile.exists() ? legacyLocaleFile : (DEFAULT_LOCALE_CODE.equals(localeCode) && legacyDefaultFile.exists() ? legacyDefaultFile : null); + + if (sourceFile == null || !sourceFile.exists()) { + return; + } + + File parent = targetFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + plugin.getLogger().warning(text("console.translation.create-directory-failed", "Unable to create translations directory at {directory}", + "{directory}", parent.getPath())); + return; + } + + try { + Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info(text("console.translation.migrated-legacy", "Migrated legacy locale '{source}' to '{target}'.", + "{source}", sourceFile.getName(), + "{target}", targetFile.getPath())); + } catch (IOException exception) { + plugin.getLogger().warning(text("console.translation.migrate-legacy-failed", "Unable to migrate legacy locale '{source}' to '{target}': {error}", + "{source}", sourceFile.getPath(), + "{target}", targetFile.getPath(), + "{error}", exception.getMessage())); + } + } + + private static Map createBootstrapTheme() { + Map theme = new LinkedHashMap<>(); + theme.put("xm-text", "#f7f1e8"); + theme.put("xm-muted", "#c7c0bb"); + theme.put("xm-accent", "#9fe3d6"); + theme.put("xm-accent-2", "#f4c2d7"); + theme.put("xm-label", "#f3d38f"); + theme.put("xm-success", "#b9e8b5"); + theme.put("xm-warning", "#f6d58b"); + theme.put("xm-error", "#f3a7a7"); + theme.put("xm-info", "#a9d4ff"); + theme.put("xm-command", "#f4c2d7"); + return theme; } } diff --git a/src/main/java/ru/meloncode/xmas/MagicTree.java b/src/main/java/ru/meloncode/xmas/MagicTree.java index 4e07950..cb4dc44 100644 --- a/src/main/java/ru/meloncode/xmas/MagicTree.java +++ b/src/main/java/ru/meloncode/xmas/MagicTree.java @@ -1,6 +1,5 @@ package ru.meloncode.xmas; -import com.destroystokyo.paper.profile.PlayerProfile; import org.bukkit.*; import org.bukkit.FireworkEffect.Type; import org.bukkit.block.*; @@ -10,14 +9,11 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.FireworkMeta; -import org.bukkit.profile.PlayerTextures; import org.bukkit.persistence.PersistentDataType; +import ru.meloncode.xmas.utils.HeadProfileUtils; import ru.meloncode.xmas.utils.TextUtils; import org.bukkit.util.Vector; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -225,8 +221,7 @@ public void spawnPresent() { { pBlock.setType(Material.PLAYER_HEAD); BlockState state = pBlock.getState(); - if (state instanceof Skull) { - Skull skull = (Skull) state; + if (state instanceof Skull skull) { BlockFace face; do { face = BlockFace.values()[Main.RANDOM.nextInt(BlockFace.values().length)]; @@ -236,37 +231,17 @@ public void spawnPresent() { skullRotatable.setRotation(face); skull.setBlockData(skullRotatable); skull.setType(Material.PLAYER_HEAD); - applyConfiguredHead(skull, Main.getHeads().get(Main.RANDOM.nextInt(Main.getHeads().size()))); + HeadProfileUtils.applyConfiguredHead( + skull, + Main.getHeads().get(Main.RANDOM.nextInt(Main.getHeads().size())), + Main.getInstance().getLogger() + ); + skull.getPersistentDataContainer().set(Main.getPresentHeadKey(), PersistentDataType.BYTE, (byte) 1); skull.update(true); } } } - private void applyConfiguredHead(Skull skull, String configuredHead) { - if (configuredHead == null || configuredHead.trim().isEmpty()) { - return; - } - String trimmedHead = configuredHead.trim(); - if (!trimmedHead.contains("://")) { - skull.setPlayerProfile(Bukkit.createProfile(trimmedHead)); - return; - } - try { - URL skinUrl = URI.create(trimmedHead).toURL(); - if (!"textures.minecraft.net".equalsIgnoreCase(skinUrl.getHost())) { - Bukkit.getLogger().warning("[X-Mas] Ignoring non-Mojang present skin URL: " + trimmedHead); - return; - } - PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID()); - PlayerTextures textures = profile.getTextures(); - textures.setSkin(skinUrl); - profile.setTextures(textures); - skull.setPlayerProfile(profile); - } catch (IllegalArgumentException | MalformedURLException e) { - Bukkit.getLogger().warning("[X-Mas] Invalid present skin URL: " + trimmedHead); - } - } - public boolean canLevelUp() { return getLevelupRequirements().size() == 0; } @@ -300,7 +275,7 @@ private void clearNearbyPresents() { continue; } bl = location.clone().add(x, 0, z).getBlock(); - if (bl.getType() == Material.PLAYER_HEAD) { + if (XMas.isPresentHead(bl)) { bl.setType(Material.AIR); } } @@ -316,14 +291,14 @@ private void refundResources(Player refundTarget) { List leftovers = putRefundsInContainer(Material.CHEST, refundItems); if (leftovers != null) { dropRefunds(leftovers); - notifyRefund(refundTarget, "Your tree resources were returned in a chest."); + notifyRefund(refundTarget, TextUtils.success(LocaleManager.text("messages.tree.refund.chest", "Your tree resources were returned in a chest."))); return; } leftovers = putRefundsInContainer(Material.BARREL, refundItems); if (leftovers != null) { dropRefunds(leftovers); - notifyRefund(refundTarget, "Your tree resources were returned in a barrel."); + notifyRefund(refundTarget, TextUtils.success(LocaleManager.text("messages.tree.refund.barrel", "Your tree resources were returned in a barrel."))); return; } @@ -331,9 +306,9 @@ private void refundResources(Player refundTarget) { dropRefunds(leftovers); if (refundTarget != null) { if (leftovers.isEmpty()) { - notifyRefund(refundTarget, "Your tree resources were returned to your inventory."); + notifyRefund(refundTarget, TextUtils.success(LocaleManager.text("messages.tree.refund.inventory", "Your tree resources were returned to your inventory."))); } else { - notifyRefund(refundTarget, "Your tree resources were returned. Inventory overflow dropped at the tree."); + notifyRefund(refundTarget, TextUtils.info(LocaleManager.text("messages.tree.refund.inventory-overflow", "Your tree resources were returned. Inventory overflow dropped at the tree."))); } } } @@ -359,6 +334,10 @@ private List collectRefundItems() { return refundItems; } + public List getRefundPreviewItems() { + return collectRefundItems(); + } + private void addRequirements(List refundItems, Map requirements) { if (requirements == null || requirements.isEmpty()) { return; @@ -379,7 +358,9 @@ private List putRefundsInContainer(Material containerMaterial, List heads; private static Plugin plugin; private static final int MAX_SERIALIZED_GIFT_LENGTH = 65536; @@ -52,6 +55,20 @@ public class Main extends JavaPlugin implements Listener { private String locale; private XMasPlaceholderExpansion placeholderExpansion; + public record ReloadSummary( + String locale, + int giftCount, + int presentHeadCount, + int treeCount, + int treeOwnerCount, + boolean particlesEnabled, + boolean resourceBack, + boolean legacyAliasEnabled, + float growFirstSoundVolume, + float growRepeatSoundVolume + ) { + } + public static Plugin getInstance() { return plugin; } @@ -68,6 +85,10 @@ public static NamespacedKey getNoDamageFireworkKey() { return noDamageFireworkKey; } + public static NamespacedKey getPresentHeadKey() { + return presentHeadKey; + } + private File getPluginConfigFile() { return new File(getDataFolder(), CONFIG_RESOURCE_PATH); } @@ -104,15 +125,18 @@ public void onEnable() { this.saveDefaults(); crystalKey = new NamespacedKey(this, "xmas_crystal"); noDamageFireworkKey = new NamespacedKey(this, "no_damage_firework"); + presentHeadKey = new NamespacedKey(this, "present_head"); + crystalRecipeKey = new NamespacedKey(this, "xmas"); config = getConfig(); - locale = config.getString("core.locale"); + locale = config.getString("core.locale", LocaleManager.DEFAULT_LOCALE_CODE); + LocaleManager.loadLocale(locale); SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); inProgress = config.getBoolean("core.plugin-enabled", true); UPDATE_SPEED = config.getInt("core.update-speed"); if (UPDATE_SPEED <= 0) { - TextUtils.sendConsoleMessage("Update speed must be > 0"); - TextUtils.sendConsoleMessage("Setting value to default"); + TextUtils.sendConsoleMessage(TextUtils.warning(LocaleManager.text("console.config.update-speed-invalid", "Update speed must be > 0"))); + TextUtils.sendConsoleMessage(TextUtils.muted(LocaleManager.text("console.config.update-speed-reset", "Setting value to default"))); config.set("core.update-speed", 7); UPDATE_SPEED = 7; } @@ -132,17 +156,16 @@ public void onEnable() { date = sdf.parse(config.getString("core.holiday-ends.date")); endTime = date.getTime(); } catch (ParseException e1) { - TextUtils.sendConsoleMessage("Unable to load date"); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.config.unable-load-holiday-end-date", "Unable to load holiday end date"))); } defineTreeLevels(); for (World world : getServer().getWorlds()) { TreeSerializer.loadTrees(this, world); } - LocaleManager.loadLocale(locale); heads = config.getStringList("xmas.presents"); if (heads.size() == 0) { - getLogger().warning("[X-Mas] Warning! No heads loaded! Presents can't spawn without box!"); + getLogger().warning(LocaleManager.text("console.gifts.no-heads", "Warning! No heads loaded. Presents cannot spawn without a box head.")); return; } gifts = new ArrayList<>(); @@ -151,11 +174,12 @@ public void onEnable() { if (item != null) { gifts.add(item); } else { - getLogger().warning("[X-Mas] Failed to load gift item: " + serializedItem); + getLogger().warning(LocaleManager.text("console.gifts.load-item-failed", "Failed to load gift item: {item}", + "{item}", serializedItem)); } } if (gifts.size() == 0) { - getLogger().warning("[X-Mas] Warning! No gifts loaded! No X-Mas without gifts!"); + getLogger().warning(LocaleManager.text("console.gifts.no-gifts", "Warning! No gifts loaded. No X-Mas without gifts.")); return; } @@ -164,30 +188,8 @@ public void onEnable() { new Events().registerListener(); new MagicTask(this).runTaskTimer(this, 5, UPDATE_SPEED); new PlayParticlesTask(this).runTaskTimer(this, 5, PARTICLES_DELAY); - XMas.XMAS_CRYSTAL = new ItemMaker(Material.EMERALD, LocaleManager.CRYSTAL_NAME, LocaleManager.CRYSTAL_LORE).make(); - ItemMeta crystalMeta = XMas.XMAS_CRYSTAL.getItemMeta(); - if (crystalMeta != null) { - crystalMeta.getPersistentDataContainer().set(crystalKey, PersistentDataType.BYTE, (byte) 1); - XMas.XMAS_CRYSTAL.setItemMeta(crystalMeta); - } - - ShapedRecipe grinderRecipe; - grinderRecipe = new ShapedRecipe(new NamespacedKey(this, "xmas"), XMas.XMAS_CRYSTAL).shape(" d ", "ded", " d ").setIngredient('d', Material.DIAMOND).setIngredient('e', Material.EMERALD); - Iterator recipes = getServer().recipeIterator(); - boolean registered = false; - while (recipes.hasNext()) { - Recipe recipe = recipes.next(); - if (recipe.equals(grinderRecipe)) { - registered = true; - break; - } - - } - try { - if (!registered) - getServer().addRecipe(grinderRecipe); - } catch (Exception ignored) { - } + refreshCrystalItem(); + registerCrystalRecipe(); XMasCommand.register(this); registerPlaceholderApi(); TextUtils.sendConsoleMessage(LocaleManager.PLUGIN_ENABLED); @@ -205,10 +207,11 @@ public void onWorldUnload(WorldUnloadEvent event) { } } - public void reloadPluginConfig() { + public ReloadSummary reloadPluginConfig() { reloadConfig(); config = getConfig(); - locale = config.getString("core.locale"); + locale = config.getString("core.locale", LocaleManager.DEFAULT_LOCALE_CODE); + LocaleManager.loadLocale(locale); inProgress = config.getBoolean("core.plugin-enabled", true); UPDATE_SPEED = config.getInt("core.update-speed"); @@ -229,11 +232,10 @@ public void reloadPluginConfig() { SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); endTime = sdf.parse(config.getString("core.holiday-ends.date")).getTime(); } catch (ParseException e) { - TextUtils.sendConsoleMessage("Invalid holiday end date in config.yml"); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.config.invalid-holiday-end-date", "Invalid holiday end date in config.yml"))); } defineTreeLevels(); - LocaleManager.loadLocale(locale); heads = config.getStringList("xmas.presents"); gifts = new ArrayList<>(); @@ -242,14 +244,36 @@ public void reloadPluginConfig() { if (item != null) { gifts.add(item); } else { - getLogger().warning("[X-Mas] Failed to deserialize gift item: " + serializedItem); + getLogger().warning(LocaleManager.text("console.gifts.deserialize-item-failed", "Failed to deserialize gift item: {item}", + "{item}", serializedItem)); } } LUCK_CHANCE_ENABLED = config.getBoolean("xmas.luck.enabled"); LUCK_CHANCE = (float) config.getInt("xmas.luck.chance") / 100; + refreshCrystalItem(); + registerCrystalRecipe(); XMasCommand.refreshCommandConfiguration(this); - TextUtils.sendConsoleMessage("Configuration reloaded!"); + return createReloadSummary(); + } + + public ReloadSummary createReloadSummary() { + Set owners = new HashSet<>(); + for (MagicTree tree : XMas.getAllTrees()) { + owners.add(tree.getOwner()); + } + return new ReloadSummary( + LocaleManager.getActiveLocaleCode(), + gifts != null ? gifts.size() : 0, + heads != null ? heads.size() : 0, + XMas.getAllTrees().size(), + owners.size(), + particlesEnabled, + resourceBack, + getConfig().getBoolean("core.commands.legacy-command-enabled", true), + growFirstSoundVolume, + growRepeatSoundVolume + ); } private void loadSoundConfig() { @@ -268,7 +292,8 @@ public void addGiftItem(ItemStack item) { ItemStack gift = item.clone(); String serializedItem = serializeItem(gift); if (serializedItem == null) { - getLogger().warning("Failed to serialize item for saving to the gift list. Item: " + gift); + getLogger().warning(LocaleManager.text("console.gifts.serialize-list-item-failed", "Failed to serialize item for saving to the gift list. Item: {item}", + "{item}", gift.toString())); return; } @@ -279,12 +304,67 @@ public void addGiftItem(ItemStack item) { gifts.add(gift); } + public List getGiftItems() { + List snapshot = new ArrayList<>(); + if (gifts == null) { + return snapshot; + } + for (ItemStack gift : gifts) { + if (gift != null) { + snapshot.add(gift.clone()); + } + } + return snapshot; + } + + public ItemStack rollGiftItem() { + if (gifts == null || gifts.isEmpty()) { + return null; + } + return gifts.get(RANDOM.nextInt(gifts.size())).clone(); + } + + public ItemStack removeGiftItem(int displayIndex) { + if (displayIndex < 1) { + return null; + } + + List giftList = config.getStringList("xmas.gifts"); + int validIndex = 0; + for (int rawIndex = 0; rawIndex < giftList.size(); rawIndex++) { + ItemStack item = deserializeItem(giftList.get(rawIndex)); + if (item == null) { + continue; + } + validIndex++; + if (validIndex == displayIndex) { + giftList.remove(rawIndex); + config.set("xmas.gifts", giftList); + saveConfig(); + reloadGiftItemsFromConfig(); + return item; + } + } + return null; + } + + private void reloadGiftItemsFromConfig() { + gifts = new ArrayList<>(); + for (String serializedItem : config.getStringList("xmas.gifts")) { + ItemStack item = deserializeItem(serializedItem); + if (item != null) { + gifts.add(item); + } + } + } + private String serializeItem(ItemStack item) { try { byte[] serializedBytes = item.serializeAsBytes(); return Base64.getEncoder().encodeToString(serializedBytes); } catch (Exception e) { - getLogger().severe("Failed to serialize item: " + e.getMessage()); + getLogger().severe(LocaleManager.text("console.gifts.serialize-item-failed", "Failed to serialize item: {error}", + "{error}", e.getMessage())); return null; } } @@ -304,7 +384,8 @@ public static ItemStack deserializeItem(String serializedItem) { } if (trimmed.length() > MAX_SERIALIZED_GIFT_LENGTH) { - Bukkit.getLogger().severe("[X-Mas] Gift item is too large to deserialize safely: " + trimmed.length() + " characters"); + Bukkit.getLogger().severe(LocaleManager.text("console.gifts.too-large", "Gift item is too large to deserialize safely: {length} characters", + "{length}", Integer.toString(trimmed.length()))); return null; } @@ -312,10 +393,12 @@ public static ItemStack deserializeItem(String serializedItem) { byte[] serializedBytes = Base64.getDecoder().decode(trimmed); return ItemStack.deserializeBytes(serializedBytes); } catch (IllegalArgumentException e) { - Bukkit.getLogger().severe("[X-Mas] Invalid material name or Base64 gift item: " + trimmed); + Bukkit.getLogger().severe(LocaleManager.text("console.gifts.invalid-material-or-base64", "Invalid material name or Base64 gift item: {item}", + "{item}", trimmed)); return null; } catch (Exception e) { - Bukkit.getLogger().severe("[X-Mas] Failed to deserialize gift item: " + e.getMessage()); + Bukkit.getLogger().severe(LocaleManager.text("console.gifts.deserialize-error", "Failed to deserialize gift item: {error}", + "{error}", e.getMessage())); return null; } } @@ -360,14 +443,15 @@ private void registerPlaceholderApi() { } placeholderExpansion = new XMasPlaceholderExpansion(this); if (placeholderExpansion.register()) { - getLogger().info("Registered PlaceholderAPI expansion: " + XMasPlaceholders.IDENTIFIER); + getLogger().info(LocaleManager.text("console.placeholder.registered", "Registered PlaceholderAPI expansion: {identifier}", + "{identifier}", XMasPlaceholders.IDENTIFIER)); } else { - getLogger().warning("PlaceholderAPI is present, but placeholder registration failed."); + getLogger().warning(LocaleManager.text("console.placeholder.registration-failed", "PlaceholderAPI is present, but placeholder registration failed.")); } } public void end() { - Bukkit.broadcast(TextUtils.parse("" + LocaleManager.HAPPY_NEW_YEAR)); + Bukkit.broadcast(TextUtils.parse(TextUtils.accent(LocaleManager.HAPPY_NEW_YEAR))); inProgress = false; config.set("core.plugin-enabled", false); saveConfig(); @@ -375,10 +459,31 @@ public void end() { private void saveDefaults() { reloadConfig(); - plugin.saveResource("locales/default.yml", true); - List defaults = Arrays.asList("locales/en.yml", "locales/ru.yml", "locales/ru_santa.yml", "trees.yml"); - for (String path : defaults) - if (!new File(getDataFolder(), '/' + path).exists()) plugin.saveResource(path, false); + File treesFile = new File(getDataFolder(), TREES_RESOURCE_PATH); + if (!treesFile.exists()) { + plugin.saveResource(TREES_RESOURCE_PATH, false); + } + } + + private void refreshCrystalItem() { + XMas.XMAS_CRYSTAL = new ItemMaker(Material.EMERALD, LocaleManager.CRYSTAL_NAME, LocaleManager.CRYSTAL_LORE).make(); + ItemMeta crystalMeta = XMas.XMAS_CRYSTAL.getItemMeta(); + if (crystalMeta != null) { + crystalMeta.getPersistentDataContainer().set(crystalKey, PersistentDataType.BYTE, (byte) 1); + XMas.XMAS_CRYSTAL.setItemMeta(crystalMeta); + } + } + + private void registerCrystalRecipe() { + getServer().removeRecipe(crystalRecipeKey); + ShapedRecipe grinderRecipe = new ShapedRecipe(crystalRecipeKey, XMas.XMAS_CRYSTAL) + .shape(" d ", "ded", " d ") + .setIngredient('d', Material.DIAMOND) + .setIngredient('e', Material.EMERALD); + try { + getServer().addRecipe(grinderRecipe); + } catch (Exception ignored) { + } } private void defineTreeLevels() { @@ -534,12 +639,15 @@ private ParticleContainer getParticleEffect(String level, String effect, Particl try { particle = Particle.valueOf(configuredParticle.trim().toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException e) { - getLogger().warning("[X-Mas] Unknown particle '" + configuredParticle + "' at " + path + ". Using fallback."); + getLogger().warning(LocaleManager.text("console.particles.unknown", "Unknown particle '{particle}' at {path}. Using fallback.", + "{particle}", configuredParticle, + "{path}", path)); return fallback; } if (particle.getDataType() != Void.class && particle != Particle.DUST) { - getLogger().warning("[X-Mas] Particle '" + particle.name() + "' needs extra data and is not supported in config yet. Using fallback."); + getLogger().warning(LocaleManager.text("console.particles.extra-data-unsupported", "Particle '{particle}' needs extra data and is not supported in config yet. Using fallback.", + "{particle}", particle.name())); return fallback; } diff --git a/src/main/java/ru/meloncode/xmas/ParticleContainer.java b/src/main/java/ru/meloncode/xmas/ParticleContainer.java index 6208ddf..0b2db2b 100644 --- a/src/main/java/ru/meloncode/xmas/ParticleContainer.java +++ b/src/main/java/ru/meloncode/xmas/ParticleContainer.java @@ -73,7 +73,9 @@ public void playEffect(Location location) { player.spawnParticle(type, location, count, offsetX, offsetY, offsetZ, speed); } } catch (Exception e) { - Bukkit.getLogger().warning("[X-Mas] Failed to spawn particle " + type + ": " + e.getMessage()); + Bukkit.getLogger().warning(LocaleManager.text("console.particles.spawn-failed", "Failed to spawn particle {particle}: {error}", + "{particle}", type.name(), + "{error}", e.getMessage())); } } } diff --git a/src/main/java/ru/meloncode/xmas/TreeSerializer.java b/src/main/java/ru/meloncode/xmas/TreeSerializer.java index 32904e6..02eb3cd 100644 --- a/src/main/java/ru/meloncode/xmas/TreeSerializer.java +++ b/src/main/java/ru/meloncode/xmas/TreeSerializer.java @@ -2,7 +2,9 @@ import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Bukkit; import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.java.JavaPlugin; import ru.meloncode.xmas.utils.ConfigUtils; @@ -10,7 +12,13 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -21,6 +29,38 @@ class TreeSerializer { private static final File treesFile = new File(Main.getInstance().getDataFolder() + "/trees.yml"); private static final FileConfiguration trees = ConfigUtils.loadConfig(treesFile); private static final Set loggedWorldAliasMappings = ConcurrentHashMap.newKeySet(); + private static final DateTimeFormatter BACKUP_TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + public record TreeDataValidationReport( + int storedTreeCount, + int loadedTreeCount, + Set invalidTreeIds, + Set invalidOwners, + Set invalidLevels, + Set invalidLocations, + Set missingWorlds, + Set invalidRequirements, + Set duplicateLocations + ) { + public boolean hasWarnings() { + return !invalidTreeIds.isEmpty() + || !invalidOwners.isEmpty() + || !invalidLevels.isEmpty() + || !invalidLocations.isEmpty() + || !missingWorlds.isEmpty() + || !invalidRequirements.isEmpty() + || !duplicateLocations.isEmpty(); + } + } + + public record WorldMigrationReport( + String sourceWorld, + String targetWorld, + int matchedTrees, + boolean applied, + File backupFile + ) { + } public static void loadTrees(JavaPlugin plugin, World world) { try { @@ -55,14 +95,15 @@ public static void loadTrees(JavaPlugin plugin, World world) { XMas.addMagicTree(new MagicTree(owner, treeUID, level, loc, requirements, presentCounter, scheduledPresents)); } catch (Exception e) { - plugin.getLogger().log(Level.SEVERE, String.format("Error while loading tree `%s`", cKey), e); + plugin.getLogger().log(Level.SEVERE, LocaleManager.text("console.trees.load-tree-error", "Error while loading tree {tree}", + "{tree}", cKey), e); } } } } } catch (Exception e) { - TextUtils.sendConsoleMessage("ERROR WHILE LOADING TREES"); - plugin.getLogger().log(Level.SEVERE, "Unable to load X-Mas trees", e); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.trees.load-error", "Error while loading trees"))); + plugin.getLogger().log(Level.SEVERE, LocaleManager.text("console.trees.unable-load", "Unable to load X-Mas trees"), e); } } @@ -83,7 +124,7 @@ public static void saveTree(MagicTree tree) { try { trees.save(treesFile); } catch (IOException e) { - Main.getInstance().getLogger().log(Level.SEVERE, "Unable to save X-Mas tree data", e); + Main.getInstance().getLogger().log(Level.SEVERE, LocaleManager.text("console.trees.unable-save", "Unable to save X-Mas tree data"), e); } } @@ -92,8 +133,96 @@ public static void removeTree(MagicTree tree) { try { trees.save(treesFile); } catch (IOException e) { - Main.getInstance().getLogger().log(Level.SEVERE, "Unable to remove X-Mas tree data", e); + Main.getInstance().getLogger().log(Level.SEVERE, LocaleManager.text("console.trees.unable-remove", "Unable to remove X-Mas tree data"), e); + } + } + + public static File getTreesFile() { + return treesFile; + } + + public static File backupTreesFile() throws IOException { + File backupDirectory = new File(Main.getInstance().getDataFolder(), "backups"); + if (!backupDirectory.exists() && !backupDirectory.mkdirs()) { + throw new IOException("Unable to create backup directory " + backupDirectory.getPath()); + } + String timestamp = LocalDateTime.now().format(BACKUP_TIMESTAMP); + File backupFile = new File(backupDirectory, "trees-" + timestamp + ".yml"); + Files.copy(treesFile.toPath(), backupFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES); + return backupFile; + } + + public static TreeDataValidationReport validateTreesFile() { + FileConfiguration data = ConfigUtils.loadConfig(treesFile); + ConfigurationSection treeSection = data.getConfigurationSection("trees"); + Set invalidTreeIds = new LinkedHashSet<>(); + Set invalidOwners = new LinkedHashSet<>(); + Set invalidLevels = new LinkedHashSet<>(); + Set invalidLocations = new LinkedHashSet<>(); + Set missingWorlds = new LinkedHashSet<>(); + Set invalidRequirements = new LinkedHashSet<>(); + Set duplicateLocations = new LinkedHashSet<>(); + Set seenLocations = new HashSet<>(); + + if (treeSection == null) { + return new TreeDataValidationReport( + 0, + XMas.getAllTrees().size(), + invalidTreeIds, + invalidOwners, + invalidLevels, + invalidLocations, + missingWorlds, + invalidRequirements, + duplicateLocations + ); + } + + for (String treeKey : treeSection.getKeys(false)) { + validateTreeId(treeKey, invalidTreeIds); + validateUuid(data.getString("trees." + treeKey + ".owner"), treeKey, invalidOwners); + validateLevel(data.getString("trees." + treeKey + ".level"), treeKey, invalidLevels); + validateLocation(data, treeKey, missingWorlds, invalidLocations, duplicateLocations, seenLocations); + validateRequirements(data.getConfigurationSection("trees." + treeKey + ".levelup"), treeKey, invalidRequirements); } + + return new TreeDataValidationReport( + treeSection.getKeys(false).size(), + XMas.getAllTrees().size(), + invalidTreeIds, + invalidOwners, + invalidLevels, + invalidLocations, + missingWorlds, + invalidRequirements, + duplicateLocations + ); + } + + public static WorldMigrationReport migrateWorldName(String sourceWorld, String targetWorld, boolean apply) throws IOException { + FileConfiguration data = ConfigUtils.loadConfig(treesFile); + ConfigurationSection treeSection = data.getConfigurationSection("trees"); + int matchedTrees = 0; + + if (treeSection != null) { + for (String treeKey : treeSection.getKeys(false)) { + String path = "trees." + treeKey + ".loc.world"; + String savedWorldName = data.getString(path); + if (savedWorldName != null && savedWorldName.equalsIgnoreCase(sourceWorld)) { + matchedTrees++; + if (apply) { + data.set(path, targetWorld); + } + } + } + } + + File backupFile = null; + if (apply && matchedTrees > 0) { + backupFile = backupTreesFile(); + ConfigUtils.saveConfig(treesFile, data); + } + return new WorldMigrationReport(sourceWorld, targetWorld, matchedTrees, apply, backupFile); } public static Map convertRequirementsMap(Map map) { @@ -105,18 +234,21 @@ public static Map convertRequirementsMap(Map try { cMaterial = Material.matchMaterial(sMaterial); if (cMaterial == null || cMaterial.isLegacy()) { - TextUtils.sendConsoleMessage("Can't find modern material '" + sMaterial + "' for tree level."); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.trees.material-missing", "Cannot find modern material '{material}' for tree level.", + "{material}", sMaterial))); continue; } Object rawValue = map.get(sMaterial); if (!(rawValue instanceof Number)) { - TextUtils.sendConsoleMessage("Tree level material '" + sMaterial + "' must use a numeric amount."); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.trees.material-numeric-required", "Tree level material '{material}' must use a numeric amount.", + "{material}", sMaterial))); continue; } value = ((Number) rawValue).intValue(); levelupRequirements.put(cMaterial, value); } catch (IllegalArgumentException e) { - TextUtils.sendConsoleMessage("Can't load material '" + sMaterial + "' for tree level."); + TextUtils.sendConsoleMessage(TextUtils.error(LocaleManager.text("console.trees.material-load-failed", "Cannot load material '{material}' for tree level.", + "{material}", sMaterial))); } } return levelupRequirements; @@ -148,7 +280,96 @@ private static String getWorldAlias(String savedWorldName) { private static void logWorldAliasMapping(String savedWorldName, String worldName) { String mapping = savedWorldName + "->" + worldName; if (loggedWorldAliasMappings.add(mapping)) { - Main.getInstance().getLogger().info("Loading legacy X-Mas trees from saved world '" + savedWorldName + "' into '" + worldName + "' via migration.world-aliases."); + Main.getInstance().getLogger().info(LocaleManager.text("console.trees.world-alias", "Loading legacy X-Mas trees from saved world '{source}' into '{target}' via migration.world-aliases.", + "{source}", savedWorldName, + "{target}", worldName)); + } + } + + private static void validateTreeId(String treeKey, Set invalidTreeIds) { + try { + UUID.fromString(treeKey); + } catch (IllegalArgumentException exception) { + invalidTreeIds.add(treeKey); + } + } + + private static void validateUuid(String rawUuid, String treeKey, Set invalidOwners) { + try { + UUID.fromString(rawUuid); + } catch (IllegalArgumentException | NullPointerException exception) { + invalidOwners.add(treeKey); + } + } + + private static void validateLevel(String levelName, String treeKey, Set invalidLevels) { + try { + TreeLevel.fromString(levelName); + } catch (IllegalArgumentException exception) { + invalidLevels.add(treeKey + "=" + levelName); + } + } + + private static void validateLocation(FileConfiguration data, String treeKey, Set missingWorlds, Set invalidLocations, + Set duplicateLocations, Set seenLocations) { + String basePath = "trees." + treeKey + ".loc"; + String savedWorldName = data.getString(basePath + ".world"); + String resolvedWorldName = resolveWorldName(savedWorldName); + if (savedWorldName == null || savedWorldName.isBlank()) { + invalidLocations.add(treeKey + "=missing-world"); + return; + } + if (resolvedWorldName == null) { + missingWorlds.add(savedWorldName); + } + if (!data.isSet(basePath + ".x") || !data.isSet(basePath + ".y") || !data.isSet(basePath + ".z")) { + invalidLocations.add(treeKey + "=missing-coordinate"); + return; + } + + int x = data.getInt(basePath + ".x"); + int y = data.getInt(basePath + ".y"); + int z = data.getInt(basePath + ".z"); + if (y < -64 || y > 512) { + invalidLocations.add(treeKey + "=y:" + y); + } + + String locationKey = (resolvedWorldName != null ? resolvedWorldName : savedWorldName).toLowerCase() + ":" + x + ":" + y + ":" + z; + if (!seenLocations.add(locationKey)) { + duplicateLocations.add(locationKey); + } + } + + private static String resolveWorldName(String savedWorldName) { + if (savedWorldName == null || savedWorldName.isBlank()) { + return null; + } + World exactWorld = Bukkit.getWorld(savedWorldName); + if (exactWorld != null) { + return exactWorld.getName(); + } + String aliasWorldName = getWorldAlias(savedWorldName); + if (aliasWorldName == null || aliasWorldName.isBlank()) { + return null; + } + World aliasWorld = Bukkit.getWorld(aliasWorldName); + return aliasWorld != null ? aliasWorld.getName() : null; + } + + private static void validateRequirements(ConfigurationSection section, String treeKey, Set invalidRequirements) { + if (section == null) { + return; + } + for (String materialName : section.getKeys(false)) { + Material material = Material.matchMaterial(materialName); + if (material == null || material.isLegacy()) { + invalidRequirements.add(treeKey + "=" + materialName); + continue; + } + Object amount = section.get(materialName); + if (!(amount instanceof Number) || ((Number) amount).intValue() < 0) { + invalidRequirements.add(treeKey + "=" + materialName + ":" + amount); + } } } } diff --git a/src/main/java/ru/meloncode/xmas/XMas.java b/src/main/java/ru/meloncode/xmas/XMas.java index 3c4d7e7..20bafbb 100644 --- a/src/main/java/ru/meloncode/xmas/XMas.java +++ b/src/main/java/ru/meloncode/xmas/XMas.java @@ -8,6 +8,7 @@ import org.bukkit.block.Skull; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; import ru.meloncode.xmas.utils.LocationUtils; import ru.meloncode.xmas.utils.TextUtils; @@ -69,36 +70,34 @@ public static void removeTree(MagicTree tree, boolean unbuild) { } public static void processPresent(Block block, Player player) { - if (block.getType() == Material.PLAYER_HEAD) { - Skull skull = (Skull) block.getState(); - String headId = getHeadIdentifier(skull); - - if (headId != null && Main.getHeads().contains(headId)) { - Location loc = block.getLocation(); - World world = loc.getWorld(); - if (world != null) { - if (RANDOM.nextFloat() < Main.LUCK_CHANCE || !Main.LUCK_CHANCE_ENABLED) { - world.dropItemNaturally(loc, new ItemStack(Main.gifts.get(RANDOM.nextInt(Main.gifts.size())))); - Effects.TREE_SWAG.playEffect(loc); - TextUtils.sendMessage(player, LocaleManager.GIFT_LUCK); - } else { - Effects.SMOKE.playEffect(loc); - world.dropItemNaturally(loc, new ItemStack(Material.COAL)); - TextUtils.sendMessage(player, LocaleManager.GIFT_FAIL); - } - } - block.setType(Material.AIR); + if (!isPresentHead(block)) { + return; + } + + Location loc = block.getLocation(); + World world = loc.getWorld(); + if (world != null) { + if (RANDOM.nextFloat() < Main.LUCK_CHANCE || !Main.LUCK_CHANCE_ENABLED) { + world.dropItemNaturally(loc, new ItemStack(Main.gifts.get(RANDOM.nextInt(Main.gifts.size())))); + Effects.TREE_SWAG.playEffect(loc); + TextUtils.sendMessage(player, LocaleManager.GIFT_LUCK); + } else { + Effects.SMOKE.playEffect(loc); + world.dropItemNaturally(loc, new ItemStack(Material.COAL)); + TextUtils.sendMessage(player, LocaleManager.GIFT_FAIL); } + block.setType(Material.AIR); } } - static String getHeadIdentifier(Skull skull) { - if (skull.getPlayerProfile() != null - && skull.getPlayerProfile().getTextures() != null - && skull.getPlayerProfile().getTextures().getSkin() != null) { - return skull.getPlayerProfile().getTextures().getSkin().toString(); + public static boolean isPresentHead(Block block) { + if (block == null || block.getType() != Material.PLAYER_HEAD) { + return false; + } + if (!(block.getState() instanceof Skull skull)) { + return false; } - return skull.getOwningPlayer() != null ? skull.getOwningPlayer().getName() : null; + return skull.getPersistentDataContainer().has(Main.getPresentHeadKey(), PersistentDataType.BYTE); } public static List getTreesPlayerOwn(Player player) { diff --git a/src/main/java/ru/meloncode/xmas/XMasCommand.java b/src/main/java/ru/meloncode/xmas/XMasCommand.java index 821a0d1..d21333e 100644 --- a/src/main/java/ru/meloncode/xmas/XMasCommand.java +++ b/src/main/java/ru/meloncode/xmas/XMasCommand.java @@ -1,6 +1,12 @@ package ru.meloncode.xmas; import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; import org.bukkit.command.CommandMap; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -10,14 +16,19 @@ import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import ru.meloncode.xmas.utils.TextUtils; +import java.io.File; +import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.*; public class XMasCommand implements CommandExecutor, TabCompleter { + private static final PlainTextComponentSerializer PLAIN_TEXT = PlainTextComponentSerializer.plainText(); public static final String PRIMARY_COMMAND = "xmastree"; public static final String LEGACY_COMMAND = "xmas"; public static final String PERMISSION_ADMIN = "onembxmastree.admin"; @@ -29,9 +40,12 @@ public class XMasCommand implements CommandExecutor, TabCompleter { public static final String PERMISSION_RELOAD = "onembxmastree.command.reload"; public static final String PERMISSION_DEBUG = "onembxmastree.command.debug"; public static final String PERMISSION_DEBUG_TOGGLE = "onembxmastree.command.debug.toggle"; + public static final String PERMISSION_INSPECT = "onembxmastree.command.inspect"; + public static final String PERMISSION_TEST = "onembxmastree.command.test"; + public static final String PERMISSION_DATA = "onembxmastree.command.data"; public static final String PERMISSION_END = "onembxmastree.command.end"; public static final String PERMISSION_TREE_OVERRIDE = "onembxmastree.tree.override"; - private static final List COMMANDS = Arrays.asList("help", "give", "end", "gifts", "reload", "addhand", "debug"); + private static final List COMMANDS = Arrays.asList("help", "give", "end", "gifts", "reload", "addhand", "debug", "inspect", "test", "data"); private static final Set DEBUG_TOGGLE_KEYS = new LinkedHashSet<>(Arrays.asList( "core.commands.legacy-command-enabled", "core.plugin-enabled", @@ -41,7 +55,6 @@ public class XMasCommand implements CommandExecutor, TabCompleter { "xmas.luck.enabled" )); private static final Map DEBUG_SECTIONS = createDebugSections(); - private static final Map PERMISSIONS = createPermissionDescriptions(); private static XMasCommand registeredExecutor; private static PluginCommand legacyAliasCommand; @@ -117,13 +130,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sendNoPermission(sender); break; } - Random random = new Random(); - for (MagicTree magicTree : XMas.getAllTrees()) { - for (int i = 0; i < 3 + random.nextInt(4); i++) { - magicTree.spawnPresent(); - } - } - Bukkit.broadcast(TextUtils.parse(LocaleManager.COMMAND_GIVEAWAY)); + handleGifts(sender, args); break; } case "reload": { @@ -131,13 +138,40 @@ public boolean onCommand(CommandSender sender, Command command, String label, St sendNoPermission(sender); break; } - plugin.reloadPluginConfig(); - TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " configuration reloaded."); + Main.ReloadSummary summary = plugin.reloadPluginConfig(); + TextUtils.sendRawMessage(sender, LocaleManager.text("command.reload-success", TextUtils.success(TextUtils.displayName() + " configuration reloaded."))); + for (String line : getReloadSummaryLines(summary)) { + TextUtils.sendRawMessage(sender, line); + } + break; + } + case "inspect": { + if (!hasPermission(sender, PERMISSION_INSPECT)) { + sendNoPermission(sender); + break; + } + handleInspect(sender, args); + break; + } + case "test": { + if (!hasPermission(sender, PERMISSION_TEST)) { + sendNoPermission(sender); + break; + } + handleTest(sender, args); + break; + } + case "data": { + if (!hasPermission(sender, PERMISSION_DATA)) { + sendNoPermission(sender); + break; + } + handleData(sender, args); break; } case "addhand": { if (!(sender instanceof Player player)) { - TextUtils.sendRawMessage(sender, "Only players can use this command."); + TextUtils.sendRawMessage(sender, LocaleManager.text("command.player-only", TextUtils.error("Only players can use this command."))); break; } if (!hasPermission(sender, PERMISSION_ADDHAND)) { @@ -146,11 +180,15 @@ public boolean onCommand(CommandSender sender, Command command, String label, St } ItemStack item = player.getInventory().getItemInMainHand(); if (item.getType().isAir()) { - TextUtils.sendRawMessage(player, "Hold an item before running " + commandPath("addhand") + "."); + TextUtils.sendRawMessage(player, replaceToken( + LocaleManager.text("command.hold-item-first", TextUtils.error("Hold an item before running {command}.")), + "command", + commandPath("addhand") + )); break; } plugin.addGiftItem(item.clone()); - TextUtils.sendRawMessage(player, "Added the held item to the gift list."); + TextUtils.sendRawMessage(player, LocaleManager.text("command.gift-added", TextUtils.success("Added the held item to the gift list."))); break; } case "debug": { @@ -188,6 +226,53 @@ public List onTabComplete(CommandSender sender, Command command, String suggestions.add(player.getName()); } } + } else if (args[0].equalsIgnoreCase("inspect")) { + if (args.length == 2) { + List inspectSuggestions = new ArrayList<>(); + inspectSuggestions.add("nearest"); + for (MagicTree tree : XMas.getAllTrees()) { + inspectSuggestions.add(tree.getTreeUID().toString()); + } + suggestions.addAll(filterStartingWith(inspectSuggestions, args[1])); + } else if (args.length == 3 && args[1].equalsIgnoreCase("nearest")) { + String typed = args[2].toLowerCase(Locale.ENGLISH); + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getName().toLowerCase(Locale.ENGLISH).startsWith(typed)) { + suggestions.add(player.getName()); + } + } + } + } else if (args[0].equalsIgnoreCase("test")) { + if (args.length == 2) { + suggestions.addAll(filterStartingWith(Arrays.asList("sound", "particle"), args[1])); + } else if (args.length == 3 && args[1].equalsIgnoreCase("sound")) { + suggestions.addAll(filterStartingWith(Arrays.asList("first", "repeat"), args[2])); + } else if (args.length == 3 && args[1].equalsIgnoreCase("particle")) { + suggestions.addAll(filterStartingWith(treeLevelNames(), args[2])); + } else if (args.length == 4 && args[1].equalsIgnoreCase("particle")) { + suggestions.addAll(filterStartingWith(Arrays.asList("all", "ambient", "swag", "body"), args[3])); + } else if ((args.length == 4 && args[1].equalsIgnoreCase("sound")) || (args.length == 5 && args[1].equalsIgnoreCase("particle"))) { + String typed = args[args.length - 1].toLowerCase(Locale.ENGLISH); + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getName().toLowerCase(Locale.ENGLISH).startsWith(typed)) { + suggestions.add(player.getName()); + } + } + } + } else if (args[0].equalsIgnoreCase("data")) { + if (args.length == 2) { + suggestions.addAll(filterStartingWith(Arrays.asList("backup", "validate", "migrate-world"), args[1])); + } else if (args.length == 3 && args[1].equalsIgnoreCase("migrate-world")) { + suggestions.addAll(filterStartingWith(getWorldNames(), args[2])); + } else if (args.length == 4 && args[1].equalsIgnoreCase("migrate-world")) { + suggestions.addAll(filterStartingWith(getWorldNames(), args[3])); + } else if (args.length == 5 && args[1].equalsIgnoreCase("migrate-world")) { + suggestions.addAll(filterStartingWith(Arrays.asList("dry-run", "apply"), args[4])); + } + } else if (args[0].equalsIgnoreCase("gifts")) { + if (args.length == 2) { + suggestions.addAll(filterStartingWith(Arrays.asList("list", "roll", "remove", "spawn"), args[1])); + } } else if (args[0].equalsIgnoreCase("debug")) { if (args.length == 2) { List debugSuggestions = new ArrayList<>(DEBUG_SECTIONS.keySet()); @@ -204,6 +289,464 @@ public List onTabComplete(CommandSender sender, Command command, String return suggestions; } + private void handleGifts(CommandSender sender, String[] args) { + if (args.length < 2 || args[1].equalsIgnoreCase("spawn")) { + spawnPresentsUnderTrees(); + return; + } + if (args[1].equalsIgnoreCase("list")) { + int page = parsePositiveInt(args.length >= 3 ? args[2] : null, 1); + sendGiftList(sender, page); + return; + } + if (args[1].equalsIgnoreCase("roll")) { + sendGiftRoll(sender); + return; + } + if (args[1].equalsIgnoreCase("remove")) { + handleGiftRemove(sender, args); + return; + } + + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.unknown-action", "Unknown Gift Action"), TextUtils.error(args[1]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.usage", "Usage"), TextUtils.command(commandPath("gifts list")))); + } + + private void spawnPresentsUnderTrees() { + Random random = new Random(); + for (MagicTree magicTree : XMas.getAllTrees()) { + for (int i = 0; i < 3 + random.nextInt(4); i++) { + magicTree.spawnPresent(); + } + } + Bukkit.broadcast(TextUtils.parse(LocaleManager.COMMAND_GIVEAWAY)); + } + + private void sendGiftList(CommandSender sender, int page) { + List giftItems = plugin.getGiftItems(); + int pageSize = 8; + int pageCount = Math.max(1, (int) Math.ceil(giftItems.size() / (double) pageSize)); + page = Math.max(1, Math.min(page, pageCount)); + int start = (page - 1) * pageSize; + int end = Math.min(giftItems.size(), start + pageSize); + + TextUtils.sendRawMessage(sender, LocaleManager.text("ui.gifts.title", TextUtils.title("Gift Pool")) + " " + TextUtils.muted("(" + page + "/" + pageCount + ")")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.total", "Total"), TextUtils.text(Integer.toString(giftItems.size())))); + if (giftItems.isEmpty()) { + TextUtils.sendRawMessage(sender, LocaleManager.text("ui.gifts.values.empty", TextUtils.warning("No gifts are configured."))); + return; + } + for (int index = start; index < end; index++) { + TextUtils.sendRawMessage(sender, formatStyledListEntry(Integer.toString(index + 1), formatGiftItem(giftItems.get(index)))); + } + if (page < pageCount) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.next", "Next"), TextUtils.command(commandPath("gifts list " + (page + 1))))); + } + } + + private void sendGiftRoll(CommandSender sender) { + ItemStack gift = plugin.rollGiftItem(); + if (gift == null) { + TextUtils.sendRawMessage(sender, LocaleManager.text("ui.gifts.values.empty", TextUtils.warning("No gifts are configured."))); + return; + } + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.rolled", "Rolled"), formatGiftItem(gift))); + } + + private void handleGiftRemove(CommandSender sender, String[] args) { + if (args.length < 3) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.usage", "Usage"), TextUtils.command(commandPath("gifts remove ")))); + return; + } + int index = parsePositiveInt(args[2], -1); + if (index < 1) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.index", "Index"), TextUtils.error(args[2]))); + return; + } + ItemStack removed = plugin.removeGiftItem(index); + if (removed == null) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.index", "Index"), TextUtils.error(Integer.toString(index)))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.try", "Try"), TextUtils.command(commandPath("gifts list")))); + return; + } + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.gifts.labels.removed", "Removed"), formatGiftItem(removed))); + } + + private void handleTest(CommandSender sender, String[] args) { + if (args.length < 2) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.usage", "Usage"), TextUtils.command(commandPath("test sound first|repeat [player]")))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.usage", "Usage"), TextUtils.command(commandPath("test particle [all|ambient|swag|body] [player]")))); + return; + } + + if (args[1].equalsIgnoreCase("sound")) { + handleTestSound(sender, args); + return; + } + if (args[1].equalsIgnoreCase("particle")) { + handleTestParticle(sender, args); + return; + } + + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.unknown-test", "Unknown Test"), TextUtils.error(args[1]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.try", "Try"), TextUtils.command(commandPath("test sound first")))); + } + + private void handleTestSound(CommandSender sender, String[] args) { + if (args.length < 3) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.usage", "Usage"), TextUtils.command(commandPath("test sound first|repeat [player]")))); + return; + } + + boolean repeat; + if (args[2].equalsIgnoreCase("first")) { + repeat = false; + } else if (args[2].equalsIgnoreCase("repeat")) { + repeat = true; + } else { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.sound", "Sound"), TextUtils.error(args[2]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.try", "Try"), TextUtils.command(commandPath("test sound first")))); + return; + } + + Player target = resolveCommandTarget(sender, args, 3, commandPath("test sound first ")); + if (target == null) { + return; + } + + float volume = repeat ? Main.growRepeatSoundVolume : Main.growFirstSoundVolume; + if (volume > 0) { + target.playSound(target.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, volume, 0.8f); + } + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.sound", "Sound"), TextUtils.text(repeat ? "repeat" : "first"))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.target", "Target"), TextUtils.text(target.getName()))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.volume", "Volume"), TextUtils.text(Float.toString(volume)))); + } + + private void handleTestParticle(CommandSender sender, String[] args) { + if (args.length < 3) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.usage", "Usage"), TextUtils.command(commandPath("test particle [all|ambient|swag|body] [player]")))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.levels", "Levels"), TextUtils.text(String.join(", ", treeLevelNames())))); + return; + } + + TreeLevel level = treeLevelByName(args[2]); + if (level == null) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.level", "Level"), TextUtils.error(args[2]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.levels", "Levels"), TextUtils.text(String.join(", ", treeLevelNames())))); + return; + } + + String effectName = "all"; + int targetArgIndex = 3; + if (args.length >= 4 && isParticleEffectName(args[3])) { + effectName = args[3].toLowerCase(Locale.ENGLISH); + targetArgIndex = 4; + } + + Player target = resolveCommandTarget(sender, args, targetArgIndex, commandPath("test particle [all|ambient|swag|body] ")); + if (target == null) { + return; + } + + int played = playParticlePreview(level, effectName, target.getLocation()); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.particle", "Particle"), TextUtils.text(level.getLevelName() + " " + effectName))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.target", "Target"), TextUtils.text(target.getName()))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.effects-played", "Effects Played"), TextUtils.text(Integer.toString(played)))); + if (played == 0) { + TextUtils.sendRawMessage(sender, LocaleManager.text("command.test.no-particles", TextUtils.warning("No enabled particle effects matched that preview."))); + } + } + + private void handleData(CommandSender sender, String[] args) { + if (args.length < 2) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.usage", "Usage"), TextUtils.command(commandPath("data backup")))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.usage", "Usage"), TextUtils.command(commandPath("data validate")))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.usage", "Usage"), TextUtils.command(commandPath("data migrate-world [dry-run|apply]")))); + return; + } + + if (args[1].equalsIgnoreCase("backup")) { + handleDataBackup(sender); + return; + } + if (args[1].equalsIgnoreCase("validate")) { + handleDataValidate(sender); + return; + } + if (args[1].equalsIgnoreCase("migrate-world")) { + handleDataMigrateWorld(sender, args); + return; + } + + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.unknown-action", "Unknown Data Action"), TextUtils.error(args[1]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.try", "Try"), TextUtils.command(commandPath("data validate")))); + } + + private void handleDataBackup(CommandSender sender) { + try { + File backupFile = TreeSerializer.backupTreesFile(); + TextUtils.sendRawMessage(sender, LocaleManager.text("command.data.backup-created", TextUtils.success("Tree data backup created."))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.file", "File"), TextUtils.text(backupFile.getPath()))); + } catch (IOException exception) { + TextUtils.sendRawMessage(sender, LocaleManager.text("command.data.backup-failed", TextUtils.error("Unable to back up trees.yml."))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.error", "Error"), TextUtils.error(exception.getMessage()))); + } + } + + private void handleDataValidate(CommandSender sender) { + TreeSerializer.TreeDataValidationReport report = TreeSerializer.validateTreesFile(); + TextUtils.sendRawMessage(sender, formatSectionTitle(LocaleManager.text("ui.data.title", "Tree Data Validation"))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.file", "File"), TextUtils.text(TreeSerializer.getTreesFile().getPath()))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.stored-trees", "Stored Trees"), TextUtils.text(Integer.toString(report.storedTreeCount())))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.loaded-trees", "Loaded Trees"), TextUtils.text(Integer.toString(report.loadedTreeCount())))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.status", "Status"), report.hasWarnings() + ? LocaleManager.text("ui.data.values.warnings", TextUtils.warning("warnings")) + : LocaleManager.text("ui.data.values.clean", TextUtils.success("clean")))); + sendValidationSet(sender, "invalid-tree-ids", report.invalidTreeIds()); + sendValidationSet(sender, "invalid-owners", report.invalidOwners()); + sendValidationSet(sender, "invalid-levels", report.invalidLevels()); + sendValidationSet(sender, "invalid-locations", report.invalidLocations()); + sendValidationSet(sender, "missing-worlds", report.missingWorlds()); + sendValidationSet(sender, "invalid-requirements", report.invalidRequirements()); + sendValidationSet(sender, "duplicate-locations", report.duplicateLocations()); + } + + private void handleDataMigrateWorld(CommandSender sender, String[] args) { + if (args.length < 4) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.usage", "Usage"), TextUtils.command(commandPath("data migrate-world [dry-run|apply]")))); + return; + } + + String sourceWorld = args[2]; + String targetWorld = args[3]; + boolean apply = args.length >= 5 && args[4].equalsIgnoreCase("apply"); + if (args.length >= 5 && !args[4].equalsIgnoreCase("apply") && !args[4].equalsIgnoreCase("dry-run")) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.mode", "Mode"), TextUtils.error(args[4]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.usage", "Usage"), TextUtils.command(commandPath("data migrate-world [dry-run|apply]")))); + return; + } + + try { + TreeSerializer.WorldMigrationReport report = TreeSerializer.migrateWorldName(sourceWorld, targetWorld, apply); + TextUtils.sendRawMessage(sender, formatSectionTitle(LocaleManager.text("ui.data.world-migration-title", "Tree World Migration"))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.source-world", "Source World"), TextUtils.text(report.sourceWorld()))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.target-world", "Target World"), TextUtils.text(report.targetWorld()))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.matched-trees", "Matched Trees"), TextUtils.text(Integer.toString(report.matchedTrees())))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.mode", "Mode"), TextUtils.text(report.applied() ? "apply" : "dry-run"))); + if (report.backupFile() != null) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.backup", "Backup"), TextUtils.text(report.backupFile().getPath()))); + } + if (!report.applied()) { + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.apply-command", "Apply Command"), TextUtils.command(commandPath("data migrate-world " + sourceWorld + " " + targetWorld + " apply")))); + } else { + TextUtils.sendRawMessage(sender, LocaleManager.text("command.data.migration-applied", TextUtils.warning("World migration saved. Restart the server before testing migrated trees."))); + } + } catch (IOException exception) { + TextUtils.sendRawMessage(sender, LocaleManager.text("command.data.migration-failed", TextUtils.error("Unable to migrate tree world names."))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.data.labels.error", "Error"), TextUtils.error(exception.getMessage()))); + } + } + + private void sendValidationSet(CommandSender sender, String labelKey, Set values) { + if (values == null || values.isEmpty()) { + return; + } + String label = LocaleManager.text("ui.data.labels." + labelKey, labelKey); + TextUtils.sendRawMessage(sender, formatKeyValue(label, TextUtils.warning(joinLimited(values, 8)))); + } + + private String joinLimited(Set values, int limit) { + List limited = new ArrayList<>(); + int index = 0; + for (String value : values) { + if (index >= limit) { + break; + } + limited.add(value); + index++; + } + if (values.size() > limit) { + limited.add("+" + (values.size() - limit) + " more"); + } + return String.join(", ", limited); + } + + private int parsePositiveInt(String rawValue, int fallback) { + if (rawValue == null || rawValue.isBlank()) { + return fallback; + } + try { + return Integer.parseInt(rawValue); + } catch (NumberFormatException exception) { + return fallback; + } + } + + private Player resolveCommandTarget(CommandSender sender, String[] args, int targetArgIndex, String consoleUsage) { + if (args.length > targetArgIndex) { + Player target = Bukkit.getPlayer(args[targetArgIndex]); + if (target == null) { + TextUtils.sendMessage(sender, LocaleManager.COMMAND_PLAYER_OFFLINE); + } + return target; + } + if (sender instanceof Player player) { + return player; + } + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.test.labels.usage", "Usage"), TextUtils.command(consoleUsage))); + return null; + } + + private int playParticlePreview(TreeLevel level, String effectName, Location location) { + int played = 0; + if ((effectName.equals("all") || effectName.equals("ambient")) && level.getAmbientEffect() != null) { + level.getAmbientEffect().playEffect(location); + played++; + } + if ((effectName.equals("all") || effectName.equals("swag")) && level.getSwagEffect() != null) { + level.getSwagEffect().playEffect(location); + played++; + } + if ((effectName.equals("all") || effectName.equals("body")) && level.getBodyEffect() != null) { + level.getBodyEffect().playEffect(location); + played++; + } + return played; + } + + private boolean isParticleEffectName(String value) { + return value != null + && (value.equalsIgnoreCase("all") + || value.equalsIgnoreCase("ambient") + || value.equalsIgnoreCase("swag") + || value.equalsIgnoreCase("body")); + } + + private TreeLevel treeLevelByName(String levelName) { + try { + return TreeLevel.fromString(levelName); + } catch (IllegalArgumentException exception) { + return null; + } + } + + private List treeLevelNames() { + return Arrays.asList("sapling", "small_tree", "tree", "magic_tree"); + } + + private void handleInspect(CommandSender sender, String[] args) { + MagicTree tree = null; + if (args.length >= 2 && !args[1].equalsIgnoreCase("nearest")) { + tree = findTreeByUuid(args[1]); + if (tree == null) { + TextUtils.sendRawMessage(sender, formatKeyValue( + LocaleManager.text("ui.inspect.labels.requested", "Requested"), + TextUtils.error(args[1]) + )); + TextUtils.sendRawMessage(sender, formatKeyValue( + LocaleManager.text("ui.inspect.labels.try", "Try"), + TextUtils.command(commandPath("inspect nearest")) + )); + return; + } + } else if (args.length >= 3 && args[1].equalsIgnoreCase("nearest")) { + Player target = Bukkit.getPlayer(args[2]); + if (target == null) { + TextUtils.sendMessage(sender, LocaleManager.COMMAND_PLAYER_OFFLINE); + return; + } + tree = findNearestTree(target.getLocation(), 16); + } else if (sender instanceof Player player) { + tree = findTargetTree(player); + if (tree == null) { + tree = findNearestTree(player.getLocation(), 8); + } + } else { + TextUtils.sendRawMessage(sender, formatKeyValue( + LocaleManager.text("ui.inspect.labels.usage", "Usage"), + TextUtils.command(commandPath("inspect ")) + )); + return; + } + + if (tree == null) { + TextUtils.sendRawMessage(sender, LocaleManager.text("command.inspect.not-found", TextUtils.warning("No XMas Tree found nearby or in your line of sight."))); + return; + } + for (String line : getInspectLines(tree)) { + TextUtils.sendRawMessage(sender, line); + } + } + + private MagicTree findTreeByUuid(String rawUuid) { + try { + return XMas.getTree(UUID.fromString(rawUuid)); + } catch (IllegalArgumentException exception) { + return null; + } + } + + private MagicTree findTargetTree(Player player) { + Block targetBlock = player.getTargetBlockExact(8); + return MagicTree.getTreeByBlock(targetBlock); + } + + private MagicTree findNearestTree(Location location, double range) { + if (location == null || location.getWorld() == null) { + return null; + } + double maxDistanceSquared = range * range; + MagicTree nearest = null; + double nearestDistanceSquared = Double.MAX_VALUE; + for (MagicTree tree : XMas.getAllTrees()) { + Location treeLocation = tree.getLocation(); + if (treeLocation.getWorld() == null || !treeLocation.getWorld().equals(location.getWorld())) { + continue; + } + double distanceSquared = treeLocation.distanceSquared(location); + if (distanceSquared <= maxDistanceSquared && distanceSquared < nearestDistanceSquared) { + nearest = tree; + nearestDistanceSquared = distanceSquared; + } + } + return nearest; + } + + private List getInspectLines(MagicTree tree) { + List lines = new ArrayList<>(); + OfflinePlayer owner = Bukkit.getOfflinePlayer(tree.getOwner()); + Location location = tree.getLocation(); + String ownerName = owner.getName() != null ? owner.getName() : LocaleManager.text("ui.inspect.values.unknown-owner", "unknown"); + + lines.add(LocaleManager.text("ui.inspect.title", TextUtils.title(TextUtils.displayName() + " Tree Inspect"))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.owner", "Owner"), TextUtils.text(ownerName) + TextUtils.muted(" (" + tree.getOwner() + ")"))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.tree-id", "Tree ID"), TextUtils.text(tree.getTreeUID().toString()))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.level", "Level"), TextUtils.text(tree.getLevel().getLevelName()))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.location", "Location"), TextUtils.text(formatLocation(location)))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.can-level-up", "Can Level Up"), TextUtils.booleanValue(tree.canLevelUp()))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.present-timer", "Present Timer"), TextUtils.text(Long.toString(tree.getPresentCounter())))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.scheduled-presents", "Scheduled Presents"), TextUtils.text(Integer.toString(tree.getScheduledPresents())))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.remaining", "Remaining Requirements"), TextUtils.text(formatRequirements(tree.getLevelupRequirements())))); + lines.add(formatKeyValue(LocaleManager.text("ui.inspect.labels.refund-preview", "Refund Preview"), TextUtils.text(formatItems(tree.getRefundPreviewItems())))); + return lines; + } + + private List getReloadSummaryLines(Main.ReloadSummary summary) { + List lines = new ArrayList<>(); + lines.add(formatSectionTitle(LocaleManager.text("ui.reload.title", "Reload Summary"))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.locale", "Locale"), TextUtils.text(summary.locale()))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.gifts", "Gifts"), TextUtils.text(Integer.toString(summary.giftCount())))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.present-heads", "Present Heads"), TextUtils.text(Integer.toString(summary.presentHeadCount())))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.trees", "Trees"), TextUtils.text(Integer.toString(summary.treeCount())))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.owners", "Owners"), TextUtils.text(Integer.toString(summary.treeOwnerCount())))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.particles", "Particles"), TextUtils.booleanValue(summary.particlesEnabled()))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.resource-back", "Resource Back"), TextUtils.booleanValue(summary.resourceBack()))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.legacy-alias", "Legacy Alias"), TextUtils.booleanValue(summary.legacyAliasEnabled()))); + lines.add(formatKeyValue(LocaleManager.text("ui.reload.labels.sounds", "Sounds"), TextUtils.text("first=" + summary.growFirstSoundVolume() + ", repeat=" + summary.growRepeatSoundVolume()))); + return lines; + } + private void sendStatus(CommandSender sender) { for (String line : getStatusLines()) { TextUtils.sendRawMessage(sender, line); @@ -220,20 +763,31 @@ private List getStatusLines() { SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk-mm-ss"); List lines = new ArrayList<>(); - lines.add("" + TextUtils.DISPLAY_NAME + " " + plugin.getPluginMeta().getVersion() + " Plugin Status"); + lines.add( + replaceToken( + LocaleManager.text("ui.status.title", TextUtils.title(TextUtils.displayName()) + " " + TextUtils.text("{version}") + " " + TextUtils.title("Plugin Status")), + "version", + plugin.getPluginMeta().getVersion() + ) + ); lines.add(""); - lines.add(formatKeyValue("Event Status", Main.inProgress ? "In Progress" : "Holidays End")); + lines.add(formatKeyValue( + LocaleManager.text("ui.status.labels.event-status", "Event Status"), + Main.inProgress + ? LocaleManager.text("ui.status.values.in-progress", TextUtils.success("In Progress")) + : LocaleManager.text("ui.status.values.holidays-end", TextUtils.error("Holidays End")) + )); if (Main.inProgress) { - lines.add(formatKeyValue("Current Time", "" + sdf.format(System.currentTimeMillis()) + "")); - lines.add(formatKeyValue("Holidays End", "" + sdf.format(Main.endTime) + "")); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.current-time", "Current Time"), TextUtils.text(sdf.format(System.currentTimeMillis())))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.holidays-end", "Holidays End"), TextUtils.text(sdf.format(Main.endTime)))); } - lines.add(formatKeyValue("Auto-End", booleanValue(Main.autoEnd))); - lines.add(formatKeyValue("Resource Back", booleanValue(Main.resourceBack))); - lines.add(formatKeyValue("Particles", booleanValue(Main.particlesEnabled))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.auto-end", "Auto-End"), TextUtils.booleanValue(Main.autoEnd))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.resource-back", "Resource Back"), TextUtils.booleanValue(Main.resourceBack))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.particles", "Particles"), TextUtils.booleanValue(Main.particlesEnabled))); lines.add(""); - lines.add(formatKeyValue("Loaded Trees", "" + treeCount + "")); - lines.add(formatKeyValue("Tree Owners", "" + owners.size() + "")); - lines.add(formatKeyValue("Help", "" + commandPath("help") + "")); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.loaded-trees", "Loaded Trees"), TextUtils.text(Integer.toString(treeCount)))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.tree-owners", "Tree Owners"), TextUtils.text(Integer.toString(owners.size())))); + lines.add(formatKeyValue(LocaleManager.text("ui.status.labels.help", "Help"), TextUtils.command(commandPath("help")))); return lines; } @@ -275,19 +829,19 @@ private void handleDebug(CommandSender sender, String[] args) { private void handleDebugToggle(CommandSender sender, String[] args) { if (args.length < 4) { - TextUtils.sendRawMessage(sender, formatKeyValue("Usage", "" + commandPath("debug toggle true|false") + "")); - TextUtils.sendRawMessage(sender, formatKeyValue("Keys", "" + String.join(", ", DEBUG_TOGGLE_KEYS) + "")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.usage", "Usage"), TextUtils.command(commandPath("debug toggle true|false")))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.keys", "Keys"), TextUtils.text(String.join(", ", DEBUG_TOGGLE_KEYS)))); return; } String key = args[2].toLowerCase(Locale.ENGLISH); if (!DEBUG_TOGGLE_KEYS.contains(key)) { - TextUtils.sendRawMessage(sender, formatKeyValue("Unknown Toggle Key", "" + args[2] + "")); - TextUtils.sendRawMessage(sender, formatKeyValue("Keys", "" + String.join(", ", DEBUG_TOGGLE_KEYS) + "")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.unknown-toggle-key", "Unknown Toggle Key"), TextUtils.error(args[2]))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.keys", "Keys"), TextUtils.text(String.join(", ", DEBUG_TOGGLE_KEYS)))); return; } if (!args[3].equalsIgnoreCase("true") && !args[3].equalsIgnoreCase("false")) { - TextUtils.sendRawMessage(sender, formatKeyValue("Value", "must be true or false")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.value", "Value"), LocaleManager.text("ui.debug.invalid-boolean", TextUtils.error("must be true or false")))); return; } @@ -295,7 +849,7 @@ private void handleDebugToggle(CommandSender sender, String[] args) { plugin.getConfig().set(key, value); plugin.saveConfig(); plugin.reloadPluginConfig(); - TextUtils.sendRawMessage(sender, formatKeyValue("Updated", "" + key + " -> " + booleanValue(value))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.updated", "Updated"), TextUtils.command(key) + TextUtils.muted(" -> ") + TextUtils.booleanValue(value))); } private LinkedHashMap> buildDebugSections() { @@ -304,44 +858,57 @@ private LinkedHashMap> buildDebugSections() { List commandsPage = new ArrayList<>(); commandsPage.add(""); - commandsPage.add(formatSectionTitle("Commands")); - commandsPage.add(formatListEntry(commandPath(""), "status")); - commandsPage.add(formatListEntry(commandPath("help"), "command list")); - commandsPage.add(formatListEntry(commandPath("give "), "give a Christmas Crystal")); - commandsPage.add(formatListEntry(commandPath("gifts"), "spawn presents under all trees")); - commandsPage.add(formatListEntry(commandPath("addhand"), "add held item to gifts")); - commandsPage.add(formatListEntry(commandPath("reload"), "reload config and locale")); - commandsPage.add(formatListEntry(commandPath("end"), "end the event")); - commandsPage.add(formatListEntry(commandPath("debug"), "open the status debug section")); - commandsPage.add(formatListEntry(commandPath("debug [section|page]"), "extended debug output by category")); - commandsPage.add(formatListEntry(commandPath("debug toggle true|false"), "toggle global booleans")); + commandsPage.add(formatSectionTitle(debugSectionDisplayName("commands"))); + commandsPage.add(formatListEntry(commandPath(""), LocaleManager.text("ui.command-descriptions.status", "status"))); + commandsPage.add(formatListEntry(commandPath("help"), LocaleManager.text("ui.command-descriptions.help", "command list"))); + commandsPage.add(formatListEntry(commandPath("give "), LocaleManager.text("ui.command-descriptions.give", "give a Christmas Crystal"))); + commandsPage.add(formatListEntry(commandPath("gifts"), LocaleManager.text("ui.command-descriptions.gifts", "spawn presents under all trees"))); + commandsPage.add(formatListEntry(commandPath("gifts list [page]"), LocaleManager.text("ui.command-descriptions.gifts-list", "list configured gifts"))); + commandsPage.add(formatListEntry(commandPath("gifts roll"), LocaleManager.text("ui.command-descriptions.gifts-roll", "preview a random gift roll"))); + commandsPage.add(formatListEntry(commandPath("gifts remove "), LocaleManager.text("ui.command-descriptions.gifts-remove", "remove a configured gift"))); + commandsPage.add(formatListEntry(commandPath("addhand"), LocaleManager.text("ui.command-descriptions.addhand", "add held item to gifts"))); + commandsPage.add(formatListEntry(commandPath("reload"), LocaleManager.text("ui.command-descriptions.reload", "reload config and locale"))); + commandsPage.add(formatListEntry(commandPath("inspect"), LocaleManager.text("ui.command-descriptions.inspect", "inspect the tree you are looking at"))); + commandsPage.add(formatListEntry(commandPath("test sound first|repeat [player]"), LocaleManager.text("ui.command-descriptions.test-sound", "preview grow sounds"))); + commandsPage.add(formatListEntry(commandPath("test particle [effect] [player]"), LocaleManager.text("ui.command-descriptions.test-particle", "preview configured particles"))); + commandsPage.add(formatListEntry(commandPath("data backup"), LocaleManager.text("ui.command-descriptions.data-backup", "back up trees.yml"))); + commandsPage.add(formatListEntry(commandPath("data validate"), LocaleManager.text("ui.command-descriptions.data-validate", "validate trees.yml"))); + commandsPage.add(formatListEntry(commandPath("data migrate-world [dry-run|apply]"), LocaleManager.text("ui.command-descriptions.data-migrate-world", "migrate saved tree world names"))); + commandsPage.add(formatListEntry(commandPath("end"), LocaleManager.text("ui.command-descriptions.end", "end the event"))); + commandsPage.add(formatListEntry(commandPath("debug"), LocaleManager.text("ui.command-descriptions.debug", "open the status debug section"))); + commandsPage.add(formatListEntry(commandPath("debug [section|page]"), LocaleManager.text("ui.command-descriptions.debug-section", "extended debug output by category"))); + commandsPage.add(formatListEntry(commandPath("debug toggle true|false"), LocaleManager.text("ui.command-descriptions.debug-toggle", "toggle global booleans"))); if (isLegacyAliasEnabled()) { - commandsPage.add(formatKeyValue("Legacy Alias", "/" + LEGACY_COMMAND + "")); + commandsPage.add(formatKeyValue(LocaleManager.text("ui.status.labels.legacy-alias", "Legacy Alias"), TextUtils.command("/" + LEGACY_COMMAND))); } sections.put("commands", commandsPage); List permissionsPage = new ArrayList<>(); permissionsPage.add(""); - permissionsPage.add(formatSectionTitle("Permissions")); - for (Map.Entry permission : PERMISSIONS.entrySet()) { + permissionsPage.add(formatSectionTitle(debugSectionDisplayName("permissions"))); + for (Map.Entry permission : createPermissionDescriptions().entrySet()) { permissionsPage.add(formatListEntry(permission.getKey(), permission.getValue())); } sections.put("permissions", permissionsPage); List placeholdersPage = new ArrayList<>(); placeholdersPage.add(""); - placeholdersPage.add(formatSectionTitle("Placeholders")); - placeholdersPage.add(formatKeyValue("Notes", "Requires PlaceholderAPI. Use '_' after prefix, then dotted keys.")); + placeholdersPage.add(formatSectionTitle(debugSectionDisplayName("placeholders"))); + placeholdersPage.add(formatKeyValue( + LocaleManager.text("ui.debug.labels.notes", "Notes"), + LocaleManager.text("ui.debug.notes.placeholders", TextUtils.text("Requires PlaceholderAPI. Use '_' after prefix, then dotted keys.")) + )); + Map placeholderDescriptions = XMasPlaceholders.descriptions(); for (String placeholder : XMasPlaceholders.EXAMPLES) { - placeholdersPage.add(formatListEntry(placeholder, XMasPlaceholders.DESCRIPTIONS.getOrDefault(placeholder, "registered placeholder"))); + placeholdersPage.add(formatListEntry(placeholder, placeholderDescriptions.getOrDefault(placeholder, LocaleManager.text("ui.debug.placeholder-default-description", "registered placeholder")))); } sections.put("placeholders", placeholdersPage); List togglesPage = new ArrayList<>(); togglesPage.add(""); - togglesPage.add(formatSectionTitle("Toggleable Config Keys")); + togglesPage.add(formatSectionTitle(debugSectionDisplayName("config"))); for (String key : DEBUG_TOGGLE_KEYS) { - togglesPage.add(formatKeyValue(key, booleanValue(plugin.getConfig().getBoolean(key)))); + togglesPage.add(formatKeyValue(key, TextUtils.booleanValue(plugin.getConfig().getBoolean(key)))); } sections.put("config", togglesPage); @@ -387,21 +954,25 @@ private void renderDebugSection(CommandSender sender, String sectionKey, LinkedH int page = sectionIndex + 1; int pageCount = sectionKeys.size(); - TextUtils.sendRawMessage(sender, "" + TextUtils.DISPLAY_NAME + " Debug " + DEBUG_SECTIONS.getOrDefault(sectionKey, sectionKey) + " (" + page + "/" + pageCount + ")"); + TextUtils.sendRawMessage(sender, + LocaleManager.text("ui.debug.section-title", TextUtils.title(TextUtils.displayName() + " Debug")) + + " " + + TextUtils.text(debugSectionDisplayName(sectionKey)) + + TextUtils.muted(" (" + page + "/" + pageCount + ")")); for (String line : sections.get(sectionKey)) { TextUtils.sendRawMessage(sender, line); } if (page < pageCount) { String nextSection = sectionKeys.get(sectionIndex + 1); - TextUtils.sendRawMessage(sender, formatKeyValue("Next", "" + commandPath("debug " + nextSection) + "")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.next", "Next"), TextUtils.command(commandPath("debug " + nextSection)))); } } private void sendInvalidDebugSelection(CommandSender sender, String requested, int pageCount) { - TextUtils.sendRawMessage(sender, formatKeyValue("Debug Sections", "" + String.join(", ", DEBUG_SECTIONS.keySet()) + "")); - TextUtils.sendRawMessage(sender, formatKeyValue("Debug Pages", "1-" + pageCount + "")); - TextUtils.sendRawMessage(sender, formatKeyValue("Requested", "" + requested + "")); - TextUtils.sendRawMessage(sender, formatKeyValue("Try", "" + commandPath("debug status") + "")); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.sections", "Debug Sections"), TextUtils.text(String.join(", ", DEBUG_SECTIONS.keySet())))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.pages", "Debug Pages"), TextUtils.text("1-" + pageCount))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.requested", "Requested"), TextUtils.error(requested))); + TextUtils.sendRawMessage(sender, formatKeyValue(LocaleManager.text("ui.debug.labels.try", "Try"), TextUtils.command(commandPath("debug status")))); } private List getHelpLines() { @@ -409,8 +980,12 @@ private List getHelpLines() { for (String line : LocaleManager.COMMAND_HELP) { lines.add(line.replaceAll("/" + LEGACY_COMMAND + "(?![A-Za-z])", "/" + PRIMARY_COMMAND)); } + addMissingBundledHelpLines(lines); if (isLegacyAliasEnabled()) { - lines.add("Legacy alias: /" + LEGACY_COMMAND + " still works."); + lines.add(LocaleManager.text( + "command.legacy-alias-enabled", + TextUtils.muted("Legacy alias: ") + TextUtils.command("/" + LEGACY_COMMAND) + TextUtils.muted(" still works.") + )); } return lines; } @@ -422,6 +997,47 @@ private String commandPath(String suffix) { return "/" + PRIMARY_COMMAND + " " + suffix; } + private void addMissingBundledHelpLines(List lines) { + List bundledLines = LocaleManager.bundledTextList("command.help"); + for (String bundledLine : bundledLines) { + String normalizedLine = bundledLine.replaceAll("/" + LEGACY_COMMAND + "(?![A-Za-z])", "/" + PRIMARY_COMMAND); + String commandNeedle = findCommandNeedle(normalizedLine); + if (commandNeedle != null && lines.stream().noneMatch(line -> line.contains(commandNeedle))) { + lines.add(normalizedLine); + } + } + } + + private String findCommandNeedle(String line) { + List commandNeedles = List.of( + commandPath("debug toggle"), + commandPath("debug status"), + commandPath("debug"), + commandPath("test particle"), + commandPath("test sound"), + commandPath("data validate"), + commandPath("data migrate-world"), + commandPath("data backup"), + commandPath("gifts remove"), + commandPath("gifts roll"), + commandPath("gifts list"), + commandPath("inspect"), + commandPath("reload"), + commandPath("addhand"), + commandPath("gifts"), + commandPath("give"), + commandPath("help"), + commandPath("end"), + commandPath("") + ); + for (String commandNeedle : commandNeedles) { + if (line.contains(commandNeedle)) { + return commandNeedle; + } + } + return null; + } + private boolean isLegacyAliasEnabled() { return plugin.getConfig().getBoolean("core.commands.legacy-command-enabled", true); } @@ -433,7 +1049,7 @@ public static boolean canOverrideTree(CommandSender sender) { private static void syncLegacyAlias(Main plugin, XMasCommand executor) { CommandMap commandMap = getCommandMap(); if (commandMap == null) { - plugin.getLogger().warning("Unable to access the Bukkit command map. Skipping legacy /xmas alias registration."); + plugin.getLogger().warning(LocaleManager.text("console.alias.command-map-unavailable", "Unable to access the Bukkit command map. Skipping legacy /xmas alias registration.")); return; } if (!plugin.getConfig().getBoolean("core.commands.legacy-command-enabled", true)) { @@ -449,27 +1065,32 @@ private static void syncLegacyAlias(Main plugin, XMasCommand executor) { legacyAliasCommand = existingPluginCommand; return; } - plugin.getLogger().warning("Legacy alias '/" + LEGACY_COMMAND + "' is already owned by plugin '" + existingPluginCommand.getPlugin().getName() + "'. Skipping alias registration."); + plugin.getLogger().warning(LocaleManager.text("console.alias.owned-by-plugin", "Legacy alias '/{alias}' is already owned by plugin '{plugin}'. Skipping alias registration.", + "{alias}", LEGACY_COMMAND, + "{plugin}", existingPluginCommand.getPlugin().getName())); return; } if (existing != null) { - plugin.getLogger().warning("Legacy alias '/" + LEGACY_COMMAND + "' is already registered by another command source. Skipping alias registration."); + plugin.getLogger().warning(LocaleManager.text("console.alias.registered-by-other", "Legacy alias '/{alias}' is already registered by another command source. Skipping alias registration.", + "{alias}", LEGACY_COMMAND)); return; } PluginCommand aliasCommand = createPluginCommand(plugin, LEGACY_COMMAND); if (aliasCommand == null) { - plugin.getLogger().warning("Unable to create the legacy /xmas alias command."); + plugin.getLogger().warning(LocaleManager.text("console.alias.create-failed", "Unable to create the legacy /xmas alias command.")); return; } aliasCommand.setDescription("Legacy alias for /" + PRIMARY_COMMAND); - aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|debug [section|page]|end]"); + aliasCommand.setUsage("/" + LEGACY_COMMAND + " [help|give|gifts|addhand|reload|inspect|test|data|debug [section|page]|end]"); aliasCommand.setPermission(null); aliasCommand.setExecutor(executor); aliasCommand.setTabCompleter(executor); commandMap.register(plugin.getPluginMeta().getName().toLowerCase(Locale.ENGLISH), aliasCommand); legacyAliasCommand = aliasCommand; - plugin.getLogger().info("Registered legacy alias '/" + LEGACY_COMMAND + "' for '/" + PRIMARY_COMMAND + "'."); + plugin.getLogger().info(LocaleManager.text("console.alias.registered", "Registered legacy alias '/{alias}' for '/{primary}'.", + "{alias}", LEGACY_COMMAND, + "{primary}", PRIMARY_COMMAND)); } private static void unregisterLegacyAlias(CommandMap commandMap) { @@ -529,7 +1150,9 @@ private static PluginCommand createPluginCommand(Main plugin, String name) { constructor.setAccessible(true); return constructor.newInstance(name, plugin); } catch (ReflectiveOperationException e) { - plugin.getLogger().warning("Unable to construct dynamic command '/" + name + "': " + e.getMessage()); + plugin.getLogger().warning(LocaleManager.text("console.alias.construct-failed", "Unable to construct dynamic command '/{command}': {error}", + "{command}", name, + "{error}", e.getMessage())); return null; } } @@ -553,6 +1176,9 @@ private boolean canUseSubCommand(CommandSender sender, String subCommand) { case "gifts" -> hasPermission(sender, PERMISSION_GIFTS); case "reload" -> hasPermission(sender, PERMISSION_RELOAD); case "addhand" -> hasPermission(sender, PERMISSION_ADDHAND); + case "inspect" -> hasPermission(sender, PERMISSION_INSPECT); + case "test" -> hasPermission(sender, PERMISSION_TEST); + case "data" -> hasPermission(sender, PERMISSION_DATA); case "debug" -> hasPermission(sender, PERMISSION_DEBUG) || hasPermission(sender, PERMISSION_DEBUG_TOGGLE); default -> false; }; @@ -563,41 +1189,115 @@ private static boolean hasPermission(CommandSender sender, String permission) { } private void sendNoPermission(CommandSender sender) { - TextUtils.sendRawMessage(sender, "You do not have permission to use this command."); + TextUtils.sendRawMessage(sender, LocaleManager.text("command.no-permission", TextUtils.error("You do not have permission to use this command."))); } private String formatSectionTitle(String title) { - return "" + title + ""; + return TextUtils.title(title); } private String formatListEntry(String key, String value) { - return "" + key + " : " + value + ""; + return TextUtils.command(key) + TextUtils.muted(" : ") + TextUtils.text(value); } - private String formatKeyValue(String key, String value) { - return "" + key + ": " + value; + private String formatStyledListEntry(String key, String value) { + return TextUtils.command(key) + TextUtils.muted(" : ") + value; } - private String booleanValue(boolean value) { - return value ? "true" : "false"; + private String formatKeyValue(String key, String value) { + return TextUtils.label(key) + TextUtils.muted(": ") + value; } private static Map createPermissionDescriptions() { Map permissions = new LinkedHashMap<>(); - permissions.put(PERMISSION_ADMIN, "allows all " + TextUtils.DISPLAY_NAME + " commands and overrides"); - permissions.put(PERMISSION_STATUS, "shows /" + PRIMARY_COMMAND + " status output"); - permissions.put(PERMISSION_HELP, "shows /" + PRIMARY_COMMAND + " help output"); - permissions.put(PERMISSION_GIVE, "allows /" + PRIMARY_COMMAND + " give"); - permissions.put(PERMISSION_GIFTS, "allows /" + PRIMARY_COMMAND + " gifts"); - permissions.put(PERMISSION_ADDHAND, "allows /" + PRIMARY_COMMAND + " addhand"); - permissions.put(PERMISSION_RELOAD, "allows /" + PRIMARY_COMMAND + " reload"); - permissions.put(PERMISSION_DEBUG, "allows /" + PRIMARY_COMMAND + " debug"); - permissions.put(PERMISSION_DEBUG_TOGGLE, "allows /" + PRIMARY_COMMAND + " debug toggle"); - permissions.put(PERMISSION_END, "allows /" + PRIMARY_COMMAND + " end"); - permissions.put(PERMISSION_TREE_OVERRIDE, "allows managing other players' trees"); + permissions.put(PERMISSION_ADMIN, LocaleManager.text("ui.permission-descriptions.admin", "allows all {plugin_name} commands and overrides")); + permissions.put(PERMISSION_STATUS, LocaleManager.text("ui.permission-descriptions.status", "shows /xmastree status output")); + permissions.put(PERMISSION_HELP, LocaleManager.text("ui.permission-descriptions.help", "shows /xmastree help output")); + permissions.put(PERMISSION_GIVE, LocaleManager.text("ui.permission-descriptions.give", "allows /xmastree give")); + permissions.put(PERMISSION_GIFTS, LocaleManager.text("ui.permission-descriptions.gifts", "allows /xmastree gifts")); + permissions.put(PERMISSION_ADDHAND, LocaleManager.text("ui.permission-descriptions.addhand", "allows /xmastree addhand")); + permissions.put(PERMISSION_RELOAD, LocaleManager.text("ui.permission-descriptions.reload", "allows /xmastree reload")); + permissions.put(PERMISSION_INSPECT, LocaleManager.text("ui.permission-descriptions.inspect", "allows /xmastree inspect")); + permissions.put(PERMISSION_TEST, LocaleManager.text("ui.permission-descriptions.test", "allows /xmastree test")); + permissions.put(PERMISSION_DATA, LocaleManager.text("ui.permission-descriptions.data", "allows /xmastree data")); + permissions.put(PERMISSION_DEBUG, LocaleManager.text("ui.permission-descriptions.debug", "allows /xmastree debug")); + permissions.put(PERMISSION_DEBUG_TOGGLE, LocaleManager.text("ui.permission-descriptions.debug-toggle", "allows /xmastree debug toggle")); + permissions.put(PERMISSION_END, LocaleManager.text("ui.permission-descriptions.end", "allows /xmastree end")); + permissions.put(PERMISSION_TREE_OVERRIDE, LocaleManager.text("ui.permission-descriptions.tree-override", "allows managing other players' trees")); return permissions; } + private String formatLocation(Location location) { + if (location == null || location.getWorld() == null) { + return LocaleManager.text("ui.inspect.values.unknown-location", "unknown"); + } + return location.getWorld().getName() + + " " + + location.getBlockX() + + ", " + + location.getBlockY() + + ", " + + location.getBlockZ(); + } + + private String formatRequirements(Map requirements) { + if (requirements == null || requirements.isEmpty()) { + return LocaleManager.text("ui.inspect.values.none", "none"); + } + List parts = new ArrayList<>(); + for (Map.Entry entry : requirements.entrySet()) { + if (entry.getValue() > 0) { + parts.add(formatMaterial(entry.getKey()) + " x" + entry.getValue()); + } + } + return parts.isEmpty() ? LocaleManager.text("ui.inspect.values.none", "none") : String.join(", ", parts); + } + + private String formatItems(List items) { + if (items == null || items.isEmpty()) { + return LocaleManager.text("ui.inspect.values.none", "none"); + } + Map totals = new LinkedHashMap<>(); + for (ItemStack item : items) { + if (item != null && !item.getType().isAir()) { + totals.merge(item.getType(), item.getAmount(), Integer::sum); + } + } + return formatRequirements(totals); + } + + private String formatGiftItem(ItemStack item) { + if (item == null || item.getType().isAir()) { + return TextUtils.muted(LocaleManager.text("ui.inspect.values.none", "none")); + } + String name = formatMaterial(item.getType()); + ItemMeta meta = item.getItemMeta(); + if (meta != null && meta.hasDisplayName() && meta.displayName() != null) { + name = PLAIN_TEXT.serialize(meta.displayName()); + } + String amount = " x" + item.getAmount(); + if (name.equals(formatMaterial(item.getType()))) { + return TextUtils.text(name + amount); + } + return TextUtils.text(name + amount) + TextUtils.muted(" (" + item.getType().name() + ")"); + } + + private List getWorldNames() { + List worlds = new ArrayList<>(); + for (World world : Bukkit.getWorlds()) { + worlds.add(world.getName()); + } + return worlds; + } + + private String formatMaterial(Material material) { + return Arrays.stream(material.name().toLowerCase(Locale.ENGLISH).split("_")) + .filter(part -> !part.isBlank()) + .map(part -> Character.toUpperCase(part.charAt(0)) + part.substring(1)) + .reduce((left, right) -> left + " " + right) + .orElse(material.name()); + } + private static Map createDebugSections() { Map sections = new LinkedHashMap<>(); sections.put("status", "Status"); @@ -608,4 +1308,15 @@ private static Map createDebugSections() { return sections; } + private String debugSectionDisplayName(String sectionKey) { + return LocaleManager.text("ui.debug.sections." + sectionKey, DEBUG_SECTIONS.getOrDefault(sectionKey, sectionKey)); + } + + private String replaceToken(String template, String key, String value) { + if (template == null) { + return null; + } + return template.replace("{" + key + "}", TextUtils.escape(value)); + } + } diff --git a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java index c46c4b8..86feedf 100644 --- a/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java +++ b/src/main/java/ru/meloncode/xmas/XMasPlaceholders.java @@ -35,7 +35,6 @@ final class XMasPlaceholders { "%onembxmastree_player.trees%", "%onembxmastree_version%" ); - public static final Map DESCRIPTIONS = createDescriptions(); private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH-mm-ss"); private XMasPlaceholders() { @@ -48,15 +47,21 @@ public static String resolve(Main plugin, OfflinePlayer player, String params) { String key = normalize(params); return switch (key) { case "event_active" -> Boolean.toString(Main.inProgress); - case "event_active_text" -> Main.inProgress ? "Active" : "Inactive"; - case "event_status" -> Main.inProgress ? "In Progress" : "Holidays End"; - case "event_starts_at" -> "manual"; + case "event_active_text" -> Main.inProgress + ? LocaleManager.text("placeholders.values.active", "Active") + : LocaleManager.text("placeholders.values.inactive", "Inactive"); + case "event_status" -> Main.inProgress + ? LocaleManager.text("placeholders.values.in-progress", "In Progress") + : LocaleManager.text("placeholders.values.holidays-end", "Holidays End"); + case "event_starts_at" -> LocaleManager.text("placeholders.values.manual", "manual"); case "event_ends_at" -> formatEndDate(); case "event_ends_in" -> formatDurationUntilEnd(); case "event_ends_timestamp" -> Long.toString(Main.endTime); case "event_auto_end" -> Boolean.toString(Main.autoEnd); case "resource_back" -> Boolean.toString(Main.resourceBack); - case "resource_back_text" -> Main.resourceBack ? "Yes" : "No"; + case "resource_back_text" -> Main.resourceBack + ? LocaleManager.text("placeholders.values.yes", "Yes") + : LocaleManager.text("placeholders.values.no", "No"); case "particles_enabled" -> Boolean.toString(Main.particlesEnabled); case "luck_enabled" -> Boolean.toString(Main.LUCK_CHANCE_ENABLED); case "luck_chance" -> Integer.toString(Math.round(Main.LUCK_CHANCE * 100)); @@ -77,21 +82,21 @@ private static String normalize(String params) { private static String formatEndDate() { if (Main.endTime <= 0) { - return "unknown"; + return LocaleManager.text("placeholders.values.unknown", "unknown"); } return DATE_FORMAT.format(new Date(Main.endTime)); } private static String formatDurationUntilEnd() { if (!Main.autoEnd) { - return "disabled"; + return LocaleManager.text("placeholders.values.disabled", "disabled"); } if (Main.endTime <= 0) { - return "unknown"; + return LocaleManager.text("placeholders.values.unknown", "unknown"); } long remainingMillis = Main.endTime - System.currentTimeMillis(); if (remainingMillis <= 0) { - return "ended"; + return LocaleManager.text("placeholders.values.ended", "ended"); } long totalSeconds = remainingMillis / 1000; @@ -99,12 +104,17 @@ private static String formatDurationUntilEnd() { long hours = (totalSeconds % 86400) / 3600; long minutes = (totalSeconds % 3600) / 60; if (days > 0) { - return days + "d " + hours + "h"; + return LocaleManager.text("placeholders.values.duration-days-hours", "{days}d {hours}h", + "{days}", Long.toString(days), + "{hours}", Long.toString(hours)); } if (hours > 0) { - return hours + "h " + minutes + "m"; + return LocaleManager.text("placeholders.values.duration-hours-minutes", "{hours}h {minutes}m", + "{hours}", Long.toString(hours), + "{minutes}", Long.toString(minutes)); } - return minutes + "m"; + return LocaleManager.text("placeholders.values.duration-minutes", "{minutes}m", + "{minutes}", Long.toString(minutes)); } private static int countOwners(Collection trees) { @@ -128,25 +138,25 @@ private static int countPlayerTrees(OfflinePlayer player) { return count; } - private static Map createDescriptions() { + public static Map descriptions() { Map descriptions = new LinkedHashMap<>(); - descriptions.put("%onembxmastree_event.active%", "whether the event is currently active"); - descriptions.put("%onembxmastree_event.active_text%", "human-readable active state"); - descriptions.put("%onembxmastree_event.status%", "current event status text"); - descriptions.put("%onembxmastree_event.starts_at%", "event start mode"); - descriptions.put("%onembxmastree_event.ends_at%", "configured event end date"); - descriptions.put("%onembxmastree_event.ends_in%", "time remaining until the event ends"); - descriptions.put("%onembxmastree_event.ends_timestamp%", "event end timestamp in milliseconds"); - descriptions.put("%onembxmastree_event.auto_end%", "whether automatic ending is enabled"); - descriptions.put("%onembxmastree_resource.back%", "whether resource refunds are enabled"); - descriptions.put("%onembxmastree_resource.back_text%", "human-readable refund state"); - descriptions.put("%onembxmastree_particles.enabled%", "whether XMas Tree particles are enabled"); - descriptions.put("%onembxmastree_luck.enabled%", "whether gift luck chance is enabled"); - descriptions.put("%onembxmastree_luck.chance%", "gift luck chance as a percent"); - descriptions.put("%onembxmastree_trees.total%", "total loaded tree count"); - descriptions.put("%onembxmastree_trees.owners%", "number of unique tree owners"); - descriptions.put("%onembxmastree_player.trees%", "loaded trees owned by the placeholder player"); - descriptions.put("%onembxmastree_version%", "loaded plugin version"); + descriptions.put("%onembxmastree_event.active%", LocaleManager.text("placeholders.descriptions.event-active", "whether the event is currently active")); + descriptions.put("%onembxmastree_event.active_text%", LocaleManager.text("placeholders.descriptions.event-active-text", "human-readable active state")); + descriptions.put("%onembxmastree_event.status%", LocaleManager.text("placeholders.descriptions.event-status", "current event status text")); + descriptions.put("%onembxmastree_event.starts_at%", LocaleManager.text("placeholders.descriptions.event-starts-at", "event start mode")); + descriptions.put("%onembxmastree_event.ends_at%", LocaleManager.text("placeholders.descriptions.event-ends-at", "configured event end date")); + descriptions.put("%onembxmastree_event.ends_in%", LocaleManager.text("placeholders.descriptions.event-ends-in", "time remaining until the event ends")); + descriptions.put("%onembxmastree_event.ends_timestamp%", LocaleManager.text("placeholders.descriptions.event-ends-timestamp", "event end timestamp in milliseconds")); + descriptions.put("%onembxmastree_event.auto_end%", LocaleManager.text("placeholders.descriptions.event-auto-end", "whether automatic ending is enabled")); + descriptions.put("%onembxmastree_resource.back%", LocaleManager.text("placeholders.descriptions.resource-back", "whether resource refunds are enabled")); + descriptions.put("%onembxmastree_resource.back_text%", LocaleManager.text("placeholders.descriptions.resource-back-text", "human-readable refund state")); + descriptions.put("%onembxmastree_particles.enabled%", LocaleManager.text("placeholders.descriptions.particles-enabled", "whether XMas Tree particles are enabled")); + descriptions.put("%onembxmastree_luck.enabled%", LocaleManager.text("placeholders.descriptions.luck-enabled", "whether gift luck chance is enabled")); + descriptions.put("%onembxmastree_luck.chance%", LocaleManager.text("placeholders.descriptions.luck-chance", "gift luck chance as a percent")); + descriptions.put("%onembxmastree_trees.total%", LocaleManager.text("placeholders.descriptions.trees-total", "total loaded tree count")); + descriptions.put("%onembxmastree_trees.owners%", LocaleManager.text("placeholders.descriptions.trees-owners", "number of unique tree owners")); + descriptions.put("%onembxmastree_player.trees%", LocaleManager.text("placeholders.descriptions.player-trees", "loaded trees owned by the placeholder player")); + descriptions.put("%onembxmastree_version%", LocaleManager.text("placeholders.descriptions.version", "loaded plugin version")); return descriptions; } } diff --git a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java index 9e59551..fb04e03 100644 --- a/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java +++ b/src/main/java/ru/meloncode/xmas/utils/ConfigUtils.java @@ -5,6 +5,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; +import ru.meloncode.xmas.LocaleManager; import java.io.File; import java.io.IOException; @@ -25,7 +26,8 @@ public static YamlConfiguration loadConfig(File file) { try { configuration.loadFromString(Files.readString(file.toPath(), StandardCharsets.UTF_8)); } catch (IOException | InvalidConfigurationException exception) { - Bukkit.getLogger().log(Level.WARNING, "Failed to load YAML configuration from " + file.getPath(), exception); + Bukkit.getLogger().log(Level.WARNING, LocaleManager.text("console.config.load-yaml-failed", "Failed to load YAML configuration from {file}", + "{file}", file.getPath()), exception); } return configuration; } @@ -44,33 +46,44 @@ public static boolean synchronizeWithResource(JavaPlugin plugin, String resource return false; } YamlConfiguration defaults = loadResourceConfig(plugin, resourcePath); - return mergeDefaultsAndComments(yamlConfiguration, defaults); + return synchronizeWithDefaults(yamlConfiguration, defaults); + } + + public static boolean synchronizeWithDefaults(FileConfiguration configuration, FileConfiguration defaults) { + if (!(configuration instanceof YamlConfiguration yamlConfiguration) || !(defaults instanceof YamlConfiguration defaultYaml)) { + return false; + } + return mergeDefaultsAndComments(yamlConfiguration, defaultYaml); } public static void saveConfig(File file, FileConfiguration configuration) { File parent = file.getParentFile(); if (parent != null && !parent.exists() && !parent.mkdirs()) { - Bukkit.getLogger().warning("Unable to create configuration directory " + parent.getPath()); + Bukkit.getLogger().warning(LocaleManager.text("console.config.create-directory-failed", "Unable to create configuration directory {directory}", + "{directory}", parent.getPath())); return; } try { configuration.save(file); } catch (IOException exception) { - Bukkit.getLogger().log(Level.WARNING, "Failed to save YAML configuration to " + file.getPath(), exception); + Bukkit.getLogger().log(Level.WARNING, LocaleManager.text("console.config.save-yaml-failed", "Failed to save YAML configuration to {file}", + "{file}", file.getPath()), exception); } } - private static YamlConfiguration loadResourceConfig(JavaPlugin plugin, String resourcePath) { + public static YamlConfiguration loadResourceConfig(JavaPlugin plugin, String resourcePath) { YamlConfiguration configuration = newConfiguration(); try (InputStream inputStream = plugin.getResource(resourcePath)) { if (inputStream == null) { - plugin.getLogger().warning("Missing bundled configuration resource: " + resourcePath); + plugin.getLogger().warning(LocaleManager.text("console.config.missing-bundled-resource", "Missing bundled configuration resource: {resource}", + "{resource}", resourcePath)); return configuration; } configuration.loadFromString(new String(inputStream.readAllBytes(), StandardCharsets.UTF_8)); } catch (IOException | InvalidConfigurationException exception) { - plugin.getLogger().log(Level.WARNING, "Failed to load bundled configuration resource " + resourcePath, exception); + plugin.getLogger().log(Level.WARNING, LocaleManager.text("console.config.load-bundled-resource-failed", "Failed to load bundled configuration resource {resource}", + "{resource}", resourcePath), exception); } return configuration; } diff --git a/src/main/java/ru/meloncode/xmas/utils/HeadProfileUtils.java b/src/main/java/ru/meloncode/xmas/utils/HeadProfileUtils.java new file mode 100644 index 0000000..a3c0063 --- /dev/null +++ b/src/main/java/ru/meloncode/xmas/utils/HeadProfileUtils.java @@ -0,0 +1,98 @@ +package ru.meloncode.xmas.utils; + +import org.bukkit.Bukkit; +import org.bukkit.block.Skull; +import org.bukkit.profile.PlayerTextures; +import ru.meloncode.xmas.LocaleManager; + +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.UUID; +import java.util.logging.Logger; + +public final class HeadProfileUtils { + + private HeadProfileUtils() { + } + + public static void applyConfiguredHead(Skull skull, String configuredHead, Logger logger) { + Object legacyProfile = createLegacyProfile(configuredHead, logger); + if (legacyProfile == null) { + return; + } + + if (trySetResolvableProfile(skull, legacyProfile)) { + return; + } + if (trySetLegacyProfile(skull, legacyProfile)) { + return; + } + + logger.warning(LocaleManager.text("console.heads.apply-failed", "Unable to apply configured present head profile.")); + } + + private static Object createLegacyProfile(String configuredHead, Logger logger) { + if (configuredHead == null || configuredHead.trim().isEmpty()) { + return null; + } + + String trimmedHead = configuredHead.trim(); + try { + if (!trimmedHead.contains("://")) { + Method createProfile = Bukkit.class.getMethod("createProfile", String.class); + return createProfile.invoke(null, trimmedHead); + } + + URL skinUrl = URI.create(trimmedHead).toURL(); + if (!"textures.minecraft.net".equalsIgnoreCase(skinUrl.getHost())) { + logger.warning(LocaleManager.text("console.heads.non-mojang-url", "Ignoring non-Mojang present skin URL: {url}", + "{url}", trimmedHead)); + return null; + } + + Method createProfile = Bukkit.class.getMethod("createProfile", UUID.class); + Object legacyProfile = createProfile.invoke(null, UUID.randomUUID()); + Method getTextures = legacyProfile.getClass().getMethod("getTextures"); + PlayerTextures textures = (PlayerTextures) getTextures.invoke(legacyProfile); + textures.setSkin(skinUrl); + Method setTextures = legacyProfile.getClass().getMethod("setTextures", PlayerTextures.class); + setTextures.invoke(legacyProfile, textures); + return legacyProfile; + } catch (IllegalArgumentException | MalformedURLException exception) { + logger.warning(LocaleManager.text("console.heads.invalid-url", "Invalid present skin URL: {url}", + "{url}", trimmedHead)); + return null; + } catch (ReflectiveOperationException exception) { + logger.warning(LocaleManager.text("console.heads.build-failed", "Failed to build present head profile: {error}", + "{error}", exception.getMessage())); + return null; + } + } + + private static boolean trySetResolvableProfile(Skull skull, Object legacyProfile) { + try { + Class legacyProfileClass = Class.forName("com.destroystokyo.paper.profile.PlayerProfile"); + Class resolvableProfileClass = Class.forName("io.papermc.paper.datacomponent.item.ResolvableProfile"); + Method factoryMethod = resolvableProfileClass.getMethod("resolvableProfile", legacyProfileClass); + Object resolvableProfile = factoryMethod.invoke(null, legacyProfile); + Method setProfile = skull.getClass().getMethod("setProfile", resolvableProfileClass); + setProfile.invoke(skull, resolvableProfile); + return true; + } catch (ReflectiveOperationException exception) { + return false; + } + } + + private static boolean trySetLegacyProfile(Skull skull, Object legacyProfile) { + try { + Class legacyProfileClass = Class.forName("com.destroystokyo.paper.profile.PlayerProfile"); + Method setPlayerProfile = skull.getClass().getMethod("setPlayerProfile", legacyProfileClass); + setPlayerProfile.invoke(skull, legacyProfile); + return true; + } catch (ReflectiveOperationException exception) { + return false; + } + } +} diff --git a/src/main/java/ru/meloncode/xmas/utils/TextUtils.java b/src/main/java/ru/meloncode/xmas/utils/TextUtils.java index 28925af..7114ec2 100644 --- a/src/main/java/ru/meloncode/xmas/utils/TextUtils.java +++ b/src/main/java/ru/meloncode/xmas/utils/TextUtils.java @@ -6,7 +6,7 @@ import org.bukkit.entity.Player; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -15,12 +15,13 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; public class TextUtils { public static final String DISPLAY_NAME = "XMas Tree"; - private static final String PREFIX = "[" + DISPLAY_NAME + "] "; - private static final String CONSOLE_PREFIX = "[" + DISPLAY_NAME + "] "; + private static final String FALLBACK_SUCCESS_HEX = "#b9e8b5"; + private static final String FALLBACK_ERROR_HEX = "#f3a7a7"; private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); private static final LegacyComponentSerializer LEGACY_AMPERSAND = LegacyComponentSerializer.legacyAmpersand(); @@ -29,8 +30,7 @@ public class TextUtils { public static List generateChatReqList(MagicTree tree) { Objects.requireNonNull(tree, "tree"); List list = new ArrayList<>(); - Component title = Component.text(LocaleManager.GROW_REQ_LIST_TITLE, NamedTextColor.GOLD) - .decorate(TextDecoration.BOLD); + Component title = parse("" + escape(LocaleManager.GROW_REQ_LIST_TITLE) + ""); if (LocaleManager.GROW_REQ_LIST_HINT != null && !LocaleManager.GROW_REQ_LIST_HINT.isBlank()) { title = title.hoverEvent(HoverEvent.showText(parse(LocaleManager.GROW_REQ_LIST_HINT))); } @@ -42,7 +42,9 @@ public static List generateChatReqList(MagicTree tree) { if (tree.getLevelupRequirements().containsKey(cMaterial)) treeReq = tree.getLevelupRequirements().get(cMaterial); - NamedTextColor color = treeReq == 0 ? NamedTextColor.GREEN : NamedTextColor.RED; + TextColor color = treeReq == 0 + ? themeColor("xm-success", FALLBACK_SUCCESS_HEX) + : themeColor("xm-error", FALLBACK_ERROR_HEX); Component line = Component.translatable(cMaterial.getItemTranslationKey()) .color(color) .decorate(TextDecoration.BOLD) @@ -65,13 +67,13 @@ public static void sendMessage(Player player, Component message) { public static void sendMessage(CommandSender sender, String message) { if (sender != null && message != null) { - sender.sendMessage(parse(PREFIX + message)); + sender.sendMessage(parse(prefix() + message)); } } public static void sendMessage(CommandSender sender, Component message) { if (sender != null && message != null) { - sender.sendMessage(parse(PREFIX).append(message)); + sender.sendMessage(parse(prefix()).append(message)); } } @@ -83,7 +85,7 @@ public static void sendRawMessage(CommandSender sender, String message) { public static void sendConsoleMessage(String message) { if (message != null) { - Bukkit.getConsoleSender().sendMessage(parse(CONSOLE_PREFIX + message)); + Bukkit.getConsoleSender().sendMessage(parse(consolePrefix() + message)); } } @@ -91,13 +93,14 @@ public static Component parse(String message) { if (message == null) { return Component.empty(); } - if (message.indexOf('§') >= 0) { - return LEGACY_SECTION.deserialize(message); + String themed = applyThemeAliases(LocaleManager.replaceCommonTokens(message)); + if (themed.indexOf('§') >= 0) { + return LEGACY_SECTION.deserialize(themed); } - if (message.indexOf('&') >= 0 && message.indexOf('<') < 0) { - return LEGACY_AMPERSAND.deserialize(message); + if (themed.indexOf('&') >= 0 && themed.indexOf('<') < 0) { + return LEGACY_AMPERSAND.deserialize(themed); } - return MINI_MESSAGE.deserialize(message); + return MINI_MESSAGE.deserialize(themed); } public static List parseList(List messages) { @@ -109,4 +112,93 @@ public static List parseList(List messages) { } return components; } + + public static String displayName() { + if (LocaleManager.PLUGIN_NAME != null && !LocaleManager.PLUGIN_NAME.isBlank()) { + return LocaleManager.PLUGIN_NAME; + } + return DISPLAY_NAME; + } + + public static String escape(String text) { + if (text == null) { + return ""; + } + return MINI_MESSAGE.escapeTags(text); + } + + public static String title(String text) { + return "" + escape(text) + ""; + } + + public static String label(String text) { + return "" + escape(text) + ""; + } + + public static String text(String text) { + return "" + escape(text) + ""; + } + + public static String muted(String text) { + return "" + escape(text) + ""; + } + + public static String accent(String text) { + return "" + escape(text) + ""; + } + + public static String accentSecondary(String text) { + return "" + escape(text) + ""; + } + + public static String command(String text) { + return "" + escape(text) + ""; + } + + public static String success(String text) { + return "" + escape(text) + ""; + } + + public static String warning(String text) { + return "" + escape(text) + ""; + } + + public static String error(String text) { + return "" + escape(text) + ""; + } + + public static String info(String text) { + return "" + escape(text) + ""; + } + + public static String booleanValue(boolean value) { + return value ? success("true") : error("false"); + } + + private static String prefix() { + return LocaleManager.getChatPrefix(); + } + + private static String consolePrefix() { + return LocaleManager.getConsolePrefix(); + } + + private static String applyThemeAliases(String message) { + String themed = message; + for (Map.Entry entry : LocaleManager.getThemeAliases().entrySet()) { + themed = themed.replace(entry.getKey(), entry.getValue()); + } + return themed; + } + + private static TextColor themeColor(String themeAlias, String fallbackHex) { + String alias = LocaleManager.getThemeAliases().get("<" + themeAlias + ">"); + if (alias != null && alias.startsWith("<#") && alias.endsWith(">")) { + TextColor fromAlias = TextColor.fromHexString(alias.substring(1, alias.length() - 1)); + if (fromAlias != null) { + return fromAlias; + } + } + return TextColor.fromHexString(fallbackHex); + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 1825a4e..ae409db 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -18,9 +18,12 @@ core: # - Turning it back on after the plugin has already ended the event is safest with a full server restart. plugin-enabled: true - # Locale file name loaded from plugins/X-Mas/locales/.yml. + # Translation file loaded from plugins/X-Mas/translations/locale_.yml. # Default: en - # Safe values: a locale file name without .yml, for example en, ru, or ru_santa + # Safe values: + # - en -> plugins/X-Mas/translations/locale_en.yml + # - fr -> plugins/X-Mas/translations/locale_fr.yml + # - locale_en.yml also works, but short locale codes are preferred # Reload behavior: /xmastree reload applies immediately. locale: en diff --git a/src/main/resources/locales/default.yml b/src/main/resources/locales/default.yml deleted file mode 100644 index dcd933d..0000000 --- a/src/main/resources/locales/default.yml +++ /dev/null @@ -1,64 +0,0 @@ -## ## ### ######## ## ## #### ## ## ###### -## ## ## ## ## ## ## ### ## ## ### ## ## ## -## ## ## ## ## ## ## #### ## ## #### ## ## -## ## ## ## ## ######## ## ## ## ## ## ## ## ## #### -## ## ## ######### ## ## ## #### ## ## #### ## ## -## ## ## ## ## ## ## ## ### ## ## ### ## ## - ### ### ## ## ## ## ## ## #### ## ## ###### - -# EN: DO NOT TRY TO EDIT THIS FILE. IT WILL BE PURGED. -# EN: If you want to edit messages - use en.yml or define your one. -# EN: Don't forget to set your lang in config.yml - -# RU: Hе пытайтесь редактировать этот файл. Oн не будет сохранен!. -# RU: Eсли вы хотите отредактировать перевод - используйте ru.yml или создайте новый. -# RU: Hе забудьте указать ваш файл в config.yml - -#_UNUSED is a keyword to disable message. - -#For chat -plugin-name: XMas Tree - -messages: - plugin-enabled: Merry Christmas And Happy New Year! - timeout: The Holidays Are Over! See you next year! - final-wish: Happy New Year to all of you, my friends! - tree: - grow-lvl-ready: Woosh! Press Right Mouse Button to let tree grow! - grow-lvl-progress: _UNUSED - grow-req-list-title: Still needed - grow-req-list-hint: Right-click the tree while holding the listed ingredient items to feed them into the tree. - grow-not-enough-place: Not enough place to make it grow - grow-lvl-max: This tree has reached it's maximum level! - tree-limit: Hey, do not be greedy! - destroy-sapling: You can cut down this tree. - destroy-leaves-santa: Santa Claus sees everything. - destroy-leaves-tut: To destroy a tree - cut the log. - destroy-warning: You can cut down this tree. - destroy-resource-back: Your used upgrade items will be returned for collection. - destroy-tut: Hit it again to confirm. - destroy-fail-owner: It's not your tree! - destroy-complete: Your tree has been packed away for the season. - gift: - luck-message: _UNUSED - unluck-message: Probably Santa has put you in his blacklist. I hope you're lucky next time! -crystal: - name: Christmas Crystal - lore: - - Concentrated Christmas Spirit - - Use it on a spruce sapling to fill it with magic! -command: - help: - - 'Use /xmastree to show plugin version and status' - - 'Use /xmastree help to show this command list' - - 'Use /xmastree give <player> to give a player a Christmas Crystal' - - 'Use /xmastree gifts to spawn presents under all trees' - - 'Use /xmastree addhand to add your held item as a gift' - - 'Use /xmastree reload to reload the plugin config' - - 'Use /xmastree debug to open the status debug section' - - 'Use /xmastree debug status and other categories for status, commands, permissions, placeholders, and toggles' - - 'Use /xmastree debug toggle <key> true|false to change supported global toggles' - - 'Use /xmastree end to end the event' - player-offline: 'Player not found' - no-player-name: 'Missing player name' - giveaway: 'Ho! Ho! Ho! Looks like its time to check presents!' diff --git a/src/main/resources/locales/en.yml b/src/main/resources/locales/en.yml deleted file mode 100644 index 0368c92..0000000 --- a/src/main/resources/locales/en.yml +++ /dev/null @@ -1,55 +0,0 @@ -#_UNUSED is a keyword to disable message. - -#For chat -plugin-name: XMas Tree - -messages: - plugin-enabled: Merry Christmas And Happy New Year! - timeout: The Holidays Are Over! See you next year! - final-wish: Happy New Year to all of you, my friends! - - tree: - grow-lvl-ready: Woosh! Press Right Mouse Button to let tree grow! - grow-lvl-progress: _UNUSED - grow-req-list-title: Still needed - grow-req-list-hint: Right-click the tree while holding the listed ingredient items to feed them into the tree. - grow-not-enough-place: Not enough place to make it grow - grow-lvl-max: This tree has reached it's maximum level! - tree-limit: Hey, do not be greedy! (Tree limit) - destroy-sapling: You can cut down this tree. - destroy-leaves-santa: Santa Claus sees everything. - destroy-leaves-tut: To destroy a tree - cut the log. - destroy-warning: You can cut down this tree. - destroy-resource-back: Your used upgrade items will be returned for collection. - destroy-tut: Hit it again to confirm. - destroy-fail-owner: It's not your tree! - destroy-complete: Your tree has been packed away for the season. - gift: - luck-message: _UNUSED - unluck-message: Probably Santa has put you in blacklist. I hope you're lucky next time! -crystal: - name: Christmas Crystal - lore: - - Concentrated Christmas Spirit - - Use it on a spruce sapling to fill it with magic! -help: - - 'Use /xmastree to show plugin version and status' - - 'Use /xmastree give player to give crystal somebody' - - 'Use /xmastree gifts to spawn some presents under every Christmas Tree!' - - 'Use /xmastree end to set plugin into ending mode' - -command: - help: - - 'Use /xmastree to show plugin version and status' - - 'Use /xmastree help to show this command list' - - 'Use /xmastree give <player> to give a player a Christmas Crystal' - - 'Use /xmastree gifts to spawn presents under all trees' - - 'Use /xmastree addhand to add your held item as a gift' - - 'Use /xmastree reload to reload the plugin config' - - 'Use /xmastree debug to open the status debug section' - - 'Use /xmastree debug status and other categories for status, commands, permissions, placeholders, and toggles' - - 'Use /xmastree debug toggle <key> true|false to change supported global toggles' - - 'Use /xmastree end to end the event' - player-offline: 'Player not found' - no-player-name: 'Missing player name' - giveaway: 'Ho! Ho! Ho! Looks like its time to check presents!' diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml deleted file mode 100644 index 876eec6..0000000 --- a/src/main/resources/locales/hu.yml +++ /dev/null @@ -1,50 +0,0 @@ -#_UNUSED is a keyword to disable message. - -#For chat -plugin-name: X-Mas - -messages: - plugin-enabled: Boldog karácsonyt és boldog új évet! - timeout: Az ünnepek véget értek! Viszlát, jövőre! - final-wish: Boldog új évet mindenkinek, barátaim! - - tree: - grow-lvl-ready: Woosh! Nyomd meg a jobb egérgombot, hogy a fák növekedjenek! - grow-lvl-progress: _UNUSED - grow-req-list-title: Kötelező - grow-not-enough-place: Nincs elég hely ahhoz, hogy növekedjen - grow-lvl-max: Ez a fa elérte a maximális szintjét! - tree-limit: Hé, ne légy mohó! (Fa korlát) - destroy-sapling: Ez a csoda nem csökken. Biztos vagy ebben? - destroy-leaves-santa: Mikulás lát mindent. - destroy-leaves-tut: Egy fa megsemmisítéséhez - vágd ki a fát. - destroy-warning: Biztos vagy ebben? A fejlődésed elvész. - destroy-tut: A fát megsemmisíteni, vágd ki újra. - destroy-fail-owner: Ez nem a te fád! - destroy-complete: SZÖRNY vagy! - gift: - luck-message: _UNUSED - unluck-message: Valószínűleg a Mikulás a feketelistába tette. Remélem, szerencséje lesz legközelebb! -crystal: - name: Karácsonyi kristály - lore: - - Koncentrált karácsonyi szellem - - Használd a fa fa-csemetékhez, hogy kitöltse mágiával! -help: - - 'Használd /xmas a plugin verziójának és állapotának megjelenítése' - - 'Használd /xmas give név, hogy valakinek kristályt adj' - - 'Használd /xmas ajándékok, hogy minden karácsonyi fa alá ajándékozzon néhány ajándékot!' - - 'Használd /xmas véget a bővítmény beillesztésének módjába' - -command: - help: - - 'Használd /xmas a plugin verziójának és állapotának megjelenítéséhez' - - 'Használd /xmas give név, hogy kristályt adj egy játékosnak' - - 'Használd /xmas gifts, hogy ajándékok jelenjenek meg a fák alatt' - - 'Használd /xmas addhand, hogy a kézben tartott tárgy ajándék legyen' - - 'Használd /xmas reload a konfiguráció újratöltéséhez' - - 'Használd /xmas debug a lapozható státusz és kapcsolók megjelenítéséhez' - - 'Használd /xmas end az esemény lezárásához' - player-offline: 'A játékos nem található' - no-player-name: 'Hiányzó játékos név' - giveaway: 'Ho! Ho! Ho! Ideje ellenőrizni az ajándékokat!' diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml deleted file mode 100644 index 5b744f6..0000000 --- a/src/main/resources/locales/ru.yml +++ /dev/null @@ -1,43 +0,0 @@ -#_UNUSED is a keyword to disable message. - -#For chat -plugin-name: Новый Год! - -messages: - plugin-enabled: С Новым Годом! - timeout: Праздники подошли к концу. Увидимся в следующем году ;) - final-wish: Счастливого Нового года, друзья! - tree: - grow-lvl-ready: Отлично! Нажмите правой кнопкой чтобы поднять уровень дерева! - grow-lvl-progress: _UNUSED - grow-req-list-title: Требуется - grow-not-enough-place: Недостаточно места! - grow-lvl-max: Дерево уже имеет максимальный уровень - tree-limit: Не жадничайте! (Слишком много деревьев) - destroy-sapling: Это чудо не выпадет. Вы уверены? :< - destroy-leaves-santa: Дед Мороз все видит! - destroy-leaves-tut: Для того чтобы срубить Ёлку - руби ствол! - destroy-warning: Вы уверены? Весь прогресс будет потерян. - destroy-tut: Чтобы срубить Ёлку разрушьте блок еще раз. - destroy-fail-owner: Это не ваше дерево. - destroy-complete: И не жалко?! - gift: - luck-message: _UNUSED - unluck-message: Кажется кто-то плохо вел себя в этом году... Попробуй еще раз. -crystal: - name: Волшебный кристалл - lore: - - Содержит в себе концентрированный Новогодний дух! - - Используйте его на саженце чтобы сделать его волшебным. -command: - help: - - 'Команда /xmas - отобразить версию плагина и статус' - - 'Команда /xmas give Ник - выдать игроку кристалл' - - 'Команда /xmas gifts - создать подарки под елками' - - 'Команда /xmas addhand - добавить предмет в руке в подарки' - - 'Команда /xmas reload - перезагрузить конфиг' - - 'Команда /xmas debug - отладочный статус, команды и переключатели' - - 'Команда /xmas end - завершить праздник' - player-offline: 'Игрок не найден' - no-player-name: 'Вы не ввели имя игрока!' - giveaway: 'Кажется время проверять подарки!' diff --git a/src/main/resources/locales/ru_santa.yml b/src/main/resources/locales/ru_santa.yml deleted file mode 100644 index 732d369..0000000 --- a/src/main/resources/locales/ru_santa.yml +++ /dev/null @@ -1,43 +0,0 @@ -#_UNUSED is a keyword to disable message. - -#For chat -plugin-name: Новый Год! - -messages: - plugin-enabled: Счастливого Рождества! - timeout: Праздники подошли к концу. Увидимся в следующем году ;) - final-wish: Счастливого Нового года! - tree: - grow-lvl-ready: Нажмите правой кнопкой чтобы поднять уровень дерева! - grow-lvl-progress: _UNUSED - grow-req-list-title: Требуется - grow-not-enough-place: Недостаточно места! - grow-lvl-max: Дерево уже имеет максимальный уровень - tree-limit: Не жадничайте! (Слишком много деревьев) - destroy-sapling: Это чудо не выпадет. Вы уверены? :< - destroy-leaves-santa: Санта Клаус все видит! - destroy-leaves-tut: Для того чтобы срубить Рождественское дерево - руби ствол! - destroy-warning: Вы уверены? Весь прогресс будет потерян. - destroy-tut: Чтобы срубить Рождественское дерево разрушьте блок еще раз. - destroy-fail-owner: Это не ваше дерево! - destroy-complete: И не жалко?! - gift: - luck-message: _UNUSED - unluck-message: Кажется кто-то плохо вел себя в этом году... Попробуй еще раз. -crystal: - name: Волшебный кристалл - lore: - - Содержит в себе концентрированный Дух Рождества! - - Используйте его на саженце чтобы сделать его волшебным. -command: - help: - - 'Команда /xmas - отобразить версию плагина и статус' - - 'Команда /xmas give Ник - выдать игроку кристалл' - - 'Команда /xmas gifts - создать подарки под елками' - - 'Команда /xmas addhand - добавить предмет в руке в подарки' - - 'Команда /xmas reload - перезагрузить конфиг' - - 'Команда /xmas debug - отладочный статус, команды и переключатели' - - 'Команда /xmas end - завершить праздник' - player-offline: 'Игрок не найден' - no-player-name: 'Вы не ввели имя игрока!' - giveaway: 'Хоу! Хоу! Хоу! Кажется время проверять подарки!' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7149a5f..9c47602 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,12 +2,12 @@ name: X-Mas version: ${version} main: ru.meloncode.xmas.Main load: POSTWORLD -api-version: '${apiVersionFloor}' +api-version: '${apiVersion}' softdepend: [Multiverse-Core, PlotMe, PlotSquared, Plotz, PlaceholderAPI] commands: xmastree: description: Manage the 1MB XMas Tree event - usage: / [help|give|gifts|addhand|reload|debug [section|page]|end] + usage: / [help|give|gifts [list|roll|remove|spawn]|addhand|reload|inspect|test|data|debug [section|page]|end] permissions: onembxmastree.admin: description: Allows all XMas Tree commands and overrides @@ -19,6 +19,9 @@ permissions: onembxmastree.command.gifts: true onembxmastree.command.addhand: true onembxmastree.command.reload: true + onembxmastree.command.inspect: true + onembxmastree.command.test: true + onembxmastree.command.data: true onembxmastree.command.debug: true onembxmastree.command.debug.toggle: true onembxmastree.command.end: true @@ -41,6 +44,15 @@ permissions: onembxmastree.command.reload: description: Allows /xmastree reload default: op + onembxmastree.command.inspect: + description: Allows /xmastree inspect + default: op + onembxmastree.command.test: + description: Allows /xmastree test + default: op + onembxmastree.command.data: + description: Allows /xmastree data + default: op onembxmastree.command.debug: description: Allows /xmastree debug default: op diff --git a/src/main/resources/translations/locale_en.yml b/src/main/resources/translations/locale_en.yml new file mode 100644 index 0000000..97c6f54 --- /dev/null +++ b/src/main/resources/translations/locale_en.yml @@ -0,0 +1,371 @@ +# XMas Tree translations +# File: plugins/X-Mas/translations/locale_en.yml +# +# This file is the primary source of truth for: +# - player-facing text +# - admin command/debug wording +# - the chat/console prefix +# - the MiniMessage pastel theme aliases +# +# The plugin loads this file by default when core.locale is set to en. +# Custom locales can be added as: +# - plugins/X-Mas/translations/locale_fr.yml +# - plugins/X-Mas/translations/locale_de.yml +# and so on. + +plugin-name: XMas Tree + +theme: + text: '#f7f1e8' + muted: '#c7c0bb' + accent: '#9fe3d6' + accent-secondary: '#f4c2d7' + label: '#f3d38f' + success: '#b9e8b5' + warning: '#f6d58b' + error: '#f3a7a7' + info: '#a9d4ff' + command: '#f4c2d7' + +format: + prefix: '[{plugin_name}] ' + console-prefix: '[{plugin_name}] ' + +messages: + plugin-enabled: Merry Christmas and Happy New Year! + timeout: The holidays are over. See you next year! + final-wish: Happy New Year to all of you, my friends! + + tree: + grow-lvl-ready: Woosh! Right-click the tree to let it grow. + grow-lvl-progress: _UNUSED + grow-req-list-title: Still needed + grow-req-list-hint: Right-click the tree while holding the listed ingredient items to feed them into the tree. + grow-not-enough-place: There is not enough room for the tree to grow. + grow-lvl-max: This tree has reached its maximum level. + tree-limit: Hey, do not be greedy. You have reached your tree limit. + destroy-sapling: You can cut down this tree. + destroy-leaves-santa: Santa Claus sees everything. + destroy-leaves-tut: To destroy a tree, cut the log. + destroy-warning: You can cut down the tree. Hit it again to confirm. + destroy-resource-back: Your used upgrade items will be returned for collection. + destroy-tut: Hit it again to confirm. + destroy-fail-owner: It is not your tree. + destroy-complete: Your tree has been packed away for the season. + refund: + chest: Your tree resources were returned in a chest. + barrel: Your tree resources were returned in a barrel. + inventory: Your tree resources were returned to your inventory. + inventory-overflow: Your tree resources were returned. Inventory overflow dropped at the tree. + + gift: + luck-message: _UNUSED + unluck-message: Probably Santa has put you on the blacklist. Better luck next time. + +crystal: + name: Christmas Crystal + lore: + - Concentrated Christmas Spirit + - Use it on a spruce sapling to fill it with magic. + +command: + help: + - 'Use /xmastree to show plugin version and status.' + - 'Use /xmastree help to show this command list.' + - 'Use /xmastree give \ to give a player a Christmas Crystal.' + - 'Use /xmastree gifts to spawn presents under all trees.' + - 'Use /xmastree gifts list [page] to list configured gift rewards.' + - 'Use /xmastree gifts roll to preview one random gift roll.' + - 'Use /xmastree gifts remove \ to remove a configured gift reward.' + - 'Use /xmastree addhand to add your held item as a gift.' + - 'Use /xmastree reload to reload config, translations, theme, and crystal item text.' + - 'Use /xmastree inspect to inspect the tree you are looking at.' + - 'Use /xmastree test sound first|repeat [player] to preview grow sound volume.' + - 'Use /xmastree test particle \ [effect] [player] to preview configured particles.' + - 'Use /xmastree data backup to back up trees.yml.' + - 'Use /xmastree data validate to check saved tree data.' + - 'Use /xmastree data migrate-world \ \ [dry-run|apply] to migrate saved tree world names.' + - 'Use /xmastree debug to open the status debug section.' + - 'Use /xmastree debug status and other categories for status, commands, permissions, placeholders, and toggles.' + - 'Use /xmastree debug toggle \ true|false to change supported global toggles.' + - 'Use /xmastree end to end the event.' + player-offline: 'Player not found.' + no-player-name: 'Missing player name.' + giveaway: 'Ho! Ho! Ho! Looks like it is time to check presents.' + no-permission: 'You do not have permission to use this command.' + player-only: 'Only players can use this command.' + hold-item-first: 'Hold an item before running {command}.' + gift-added: 'Added the held item to the gift list.' + reload-success: '{plugin_name} configuration reloaded.' + inspect: + not-found: 'No XMas Tree found nearby or in your line of sight.' + test: + no-particles: 'No enabled particle effects matched that preview.' + data: + backup-created: 'Tree data backup created.' + backup-failed: 'Unable to back up trees.yml.' + migration-applied: 'World migration saved. Restart the server before testing migrated trees.' + migration-failed: 'Unable to migrate tree world names.' + legacy-alias-enabled: 'Legacy alias: /xmas still works.' + +ui: + status: + title: '{plugin_name} {version} Plugin Status' + labels: + event-status: Event Status + current-time: Current Time + holidays-end: Holidays End + auto-end: Auto-End + resource-back: Resource Back + particles: Particles + loaded-trees: Loaded Trees + tree-owners: Tree Owners + help: Help + legacy-alias: Legacy Alias + values: + in-progress: 'In Progress' + holidays-end: 'Holidays End' + + command-descriptions: + status: status + help: command list + give: give a Christmas Crystal + gifts: spawn presents under all trees + gifts-list: list configured gifts + gifts-roll: preview a random gift roll + gifts-remove: remove a configured gift + addhand: add held item to gifts + reload: reload config and translations + inspect: inspect the tree you are looking at + test-sound: preview grow sounds + test-particle: preview configured particles + data-backup: back up trees.yml + data-validate: validate trees.yml + data-migrate-world: migrate saved tree world names + end: end the event + debug: open the status debug section + debug-section: extended debug output by category + debug-toggle: toggle global booleans + + permission-descriptions: + admin: allows all {plugin_name} commands and overrides + status: shows /xmastree status output + help: shows /xmastree help output + give: allows /xmastree give + gifts: allows /xmastree gifts + addhand: allows /xmastree addhand + reload: allows /xmastree reload + inspect: allows /xmastree inspect + test: allows /xmastree test + data: allows /xmastree data + debug: allows /xmastree debug + debug-toggle: allows /xmastree debug toggle + end: allows /xmastree end + tree-override: allows managing other players' trees + + debug: + section-title: '{plugin_name} Debug' + sections: + status: Status + commands: Commands + permissions: Permissions + placeholders: Placeholders + config: Config + labels: + usage: Usage + keys: Keys + unknown-toggle-key: Unknown Toggle Key + value: Value + updated: Updated + next: Next + sections: Debug Sections + pages: Debug Pages + requested: Requested + try: Try + notes: Notes + invalid-boolean: 'must be true or false' + notes: + placeholders: 'Requires PlaceholderAPI. Use ''_'' after prefix, then dotted keys.' + placeholder-default-description: registered placeholder + + inspect: + title: '{plugin_name} Tree Inspect' + labels: + owner: Owner + tree-id: Tree ID + level: Level + location: Location + can-level-up: Can Level Up + present-timer: Present Timer + scheduled-presents: Scheduled Presents + remaining: Remaining Requirements + refund-preview: Refund Preview + requested: Requested + try: Try + usage: Usage + values: + none: none + unknown-owner: unknown + unknown-location: unknown + + reload: + title: Reload Summary + labels: + locale: Locale + gifts: Gifts + present-heads: Present Heads + trees: Trees + owners: Owners + particles: Particles + resource-back: Resource Back + legacy-alias: Legacy Alias + sounds: Sounds + + test: + labels: + usage: Usage + unknown-test: Unknown Test + try: Try + sound: Sound + particle: Particle + target: Target + volume: Volume + level: Level + levels: Levels + effects-played: Effects Played + + gifts: + title: Gift Pool + labels: + usage: Usage + unknown-action: Unknown Gift Action + total: Total + rolled: Rolled + removed: Removed + index: Index + try: Try + values: + empty: 'No gifts are configured.' + + data: + title: Tree Data Validation + world-migration-title: Tree World Migration + labels: + usage: Usage + unknown-action: Unknown Data Action + try: Try + file: File + error: Error + stored-trees: Stored Trees + loaded-trees: Loaded Trees + status: Status + invalid-tree-ids: Invalid Tree IDs + invalid-owners: Invalid Owners + invalid-levels: Invalid Levels + invalid-locations: Invalid Locations + missing-worlds: Missing Worlds + invalid-requirements: Invalid Requirements + duplicate-locations: Duplicate Locations + source-world: Source World + target-world: Target World + matched-trees: Matched Trees + mode: Mode + backup: Backup + apply-command: Apply Command + values: + clean: 'clean' + warnings: 'warnings' + +placeholders: + values: + active: Active + inactive: Inactive + in-progress: In Progress + holidays-end: Holidays End + manual: manual + yes: Yes + no: No + unknown: unknown + disabled: disabled + ended: ended + duration-days-hours: '{days}d {hours}h' + duration-hours-minutes: '{hours}h {minutes}m' + duration-minutes: '{minutes}m' + descriptions: + event-active: whether the event is currently active + event-active-text: human-readable active state + event-status: current event status text + event-starts-at: event start mode + event-ends-at: configured event end date + event-ends-in: time remaining until the event ends + event-ends-timestamp: event end timestamp in milliseconds + event-auto-end: whether automatic ending is enabled + resource-back: whether resource refunds are enabled + resource-back-text: human-readable refund state + particles-enabled: whether XMas Tree particles are enabled + luck-enabled: whether gift luck chance is enabled + luck-chance: gift luck chance as a percent + trees-total: total loaded tree count + trees-owners: number of unique tree owners + player-trees: loaded trees owned by the placeholder player + version: loaded plugin version + +console: + translation: + loaded: Loaded translation '{file}'. + missing: Translation '{file}' was not found. + fallback: Falling back to locale_en.yml. + create-directory-failed: Unable to create translations directory at {directory} + migrated-legacy: Migrated legacy locale '{source}' to '{target}'. + migrate-legacy-failed: 'Unable to migrate legacy locale ''{source}'' to ''{target}'': {error}' + config: + update-speed-invalid: Update speed must be > 0 + update-speed-reset: Setting value to default + unable-load-holiday-end-date: Unable to load holiday end date + invalid-holiday-end-date: Invalid holiday end date in config.yml + load-yaml-failed: Failed to load YAML configuration from {file} + create-directory-failed: Unable to create configuration directory {directory} + save-yaml-failed: Failed to save YAML configuration to {file} + missing-bundled-resource: Missing bundled configuration resource {resource} + load-bundled-resource-failed: Failed to load bundled configuration resource {resource} + gifts: + no-heads: Warning! No heads loaded. Presents cannot spawn without a box head. + load-item-failed: 'Failed to load gift item: {item}' + no-gifts: Warning! No gifts loaded. No X-Mas without gifts. + deserialize-item-failed: 'Failed to deserialize gift item: {item}' + serialize-list-item-failed: 'Failed to serialize item for saving to the gift list. Item: {item}' + serialize-item-failed: 'Failed to serialize item: {error}' + too-large: 'Gift item is too large to deserialize safely: {length} characters' + invalid-material-or-base64: 'Invalid material name or Base64 gift item: {item}' + deserialize-error: 'Failed to deserialize gift item: {error}' + placeholder: + registered: 'Registered PlaceholderAPI expansion: {identifier}' + registration-failed: PlaceholderAPI is present, but placeholder registration failed. + particles: + unknown: Unknown particle '{particle}' at {path}. Using fallback. + extra-data-unsupported: Particle '{particle}' needs extra data and is not supported in config yet. Using fallback. + spawn-failed: 'Failed to spawn particle {particle}: {error}' + trees: + load-tree-error: Error while loading tree {tree} + load-error: Error while loading trees + unable-load: Unable to load X-Mas trees + unable-save: Unable to save X-Mas tree data + unable-remove: Unable to remove X-Mas tree data + material-missing: Cannot find modern material '{material}' for tree level. + material-numeric-required: Tree level material '{material}' must use a numeric amount. + material-load-failed: Cannot load material '{material}' for tree level. + world-alias: Loading legacy X-Mas trees from saved world '{source}' into '{target}' via migration.world-aliases. + alias: + command-map-unavailable: Unable to access the Bukkit command map. Skipping legacy /xmas alias registration. + owned-by-plugin: Legacy alias '/{alias}' is already owned by plugin '{plugin}'. Skipping alias registration. + registered-by-other: Legacy alias '/{alias}' is already registered by another command source. Skipping alias registration. + create-failed: Unable to create the legacy /xmas alias command. + registered: Registered legacy alias '/{alias}' for '/{primary}'. + construct-failed: 'Unable to construct dynamic command ''/{command}'': {error}' + heads: + apply-failed: Unable to apply configured present head profile. + non-mojang-url: 'Ignoring non-Mojang present skin URL: {url}' + invalid-url: 'Invalid present skin URL: {url}' + build-failed: 'Failed to build present head profile: {error}' + refund: + place-container-failed: 'Failed to place refund {container}: {error}' diff --git a/todo.log b/todo.log index 96abf8d..7ebdcc5 100644 --- a/todo.log +++ b/todo.log @@ -1,8 +1,95 @@ Remaining warnings and deferred deprecation cleanup ================================================= -- `compileJava` still prints the generic note that some source uses deprecated API. -- The remaining known spots are the block-skull profile accessors and setters in: - - `src/main/java/ru/meloncode/xmas/MagicTree.java` - - `src/main/java/ru/meloncode/xmas/XMas.java` -- These were left in place on purpose because the newer block-skull `ResolvableProfile` route is not a clearly low-risk drop-in if we want to preserve the current texture URL and player-name behavior on both Paper versions. +- `compileJava` is clean after the Paper 26.x deprecation sweep. +- Present heads are now tagged with plugin PDC data, so gift detection no longer depends on deprecated skull-profile reads. +- The active custom-fork target is now Paper 26.2 with Java 25 bytecode. +- To run an explicit deprecation lint pass in future work, use: + - `gradle clean compileJava -PlintDeprecatedApi=true` +- Head texture application now uses a reflective bridge to Paper's current profile APIs so the plugin can preserve existing player-name and texture-URL configs without keeping deprecated profile methods in the compiled code. + + +Modernization and quality-of-life roadmap +========================================= + +Near-term implementation candidates +----------------------------------- + +- Finish MiniMessage theme polish: + - move any remaining visible colors, labels, and wording into `translations/locale_en.yml` + - tune the soft 1MB pastel theme for chat, debug pages, crystal text, requirement lists, and refund messages + +- Add admin preview and test commands: + - done: inspect tree state + - done: preview particles by tree stage with `/xmastree test particle` + - done: test first/repeat grow sounds with `/xmastree test sound` + - later: test refund delivery safely + +- Improve tree inspection: + - show owner, tree UUID, level, world/location, remaining requirements, spent/refundable resources, present timer, scheduled presents, and whether it can level up + - policy-must: existing trees from previous years must keep working; returning players waited for their legacy tree and should be able to keep it while receiving another crystal this year, letting them have two trees and earn more gifts + +- Add gift management UI: + - done: view configured gifts with `/xmastree gifts list` + - add held item + - done: remove gifts with `/xmastree gifts remove ` + - done: test a gift roll with `/xmastree gifts roll` + - eventually support weighted gift pools + +- Modernize PlaceholderAPI output: + - player tree count + - player highest tree level + - total active trees + - event status text + - future timer/status values useful for CMI holograms and ajLeaderboards + +- Add safer data tools: + - done: backup `trees.yml` with `/xmastree data backup` + - done: validate saved tree data with `/xmastree data validate` + - done: dry-run/apply saved world-name migration with `/xmastree data migrate-world [dry-run|apply]` + - report missing worlds, invalid owners, invalid levels, and duplicate/overlapping tree data + +- Continue present head and texture cleanup: + - verify Paper 26.2 profile behavior for player names and texture URLs + - improve diagnostics around invalid head entries + +- Add a reload report: + - show locale, gift count, present head count, tree count, tree owner count, particles setting, sound volumes, legacy alias state, and any warnings + +Future feature ideas to review +------------------------------ + +- Add event streaks: + - 6 gift claims in a day for a "hardcore elf" style daily milestone + - 7 days in a row + - every day of the event/month + - add a player command or GUI to view streak progress and claim milestone rewards + - possible milestone reward: trigger extra gifts for all online players + +- Add one extra progression level after the current max tree: + - "super tree" or similarly festive final stage + - modern Paper 26.2 particles/effects + - distinctive design for returning players who can upgrade their old max tree once more + +- Add seasonal admin presets: + - save/load named config presets for gift pools, particle styles, sound tuning, and event dates + +- Add player tree journal: + - command or GUI showing a player's tree history, current tree locations, old legacy tree, current-year tree, upgrade progress, and collected milestone rewards + +- Add community milestones: + - total server-wide gifts opened + - total trees grown + - unlock server-wide bonus gift waves when thresholds are reached + +- Add configurable tree protection helpers: + - staff-only teleport to a tree + - flag trees in unsafe/unloaded/missing worlds + - optional highlight particles around a selected tree for support checks + +- Add event wrap-up report: + - final tree count + - active owners + - gifts spawned/opened + - legacy trees preserved + - players eligible for returning-player rewards