From a4dacbcbaf7766b5ffb6a474d4dd66ecdc92d291 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Sun, 24 May 2026 20:31:18 -0600 Subject: [PATCH 01/21] add screenshot and clip placeholders to readme The marketplace listing has a hero strip and a feature list but nothing visual. Drop in commented-out slots for hero, drop-editor, catch demo, stats, and the personalised drops-preview command so adding screenshots later is a one-line uncomment. Bumped the docs/community footer so people land on Discord/Crowdin without hunting through docs/. No image bytes added; the placeholders are HTML comments. Each one records the suggested file name (assets/screenshots/) and the target resolution so we keep things consistent when we shoot them. --- README.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 54d0d1e..f4c1aa7 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,44 @@ [![Release](https://img.shields.io/github/v/release/xcutiboo/MythicRod?style=flat-square)](https://github.com/xcutiboo/MythicRod/releases) [![Paper 26.1.2](https://img.shields.io/badge/Paper-26.1.2-dca13a?style=flat-square)](https://papermc.io) [![Folia](https://img.shields.io/badge/Folia-region--ready-835516?style=flat-square)](https://papermc.io/software/folia) [![Discord](https://img.shields.io/badge/Discord-chat-5865F2?style=flat-square)](https://discord.gg/MDwtUgxX9U) -Weighted loot tables with biome filters, permission gates, an in-game +Weighted fishing drops with biome rules, permission gates, an in-game drop editor, per-player statistics, and a small public API for Paper and Folia. + + + ## Features - Weighted drop tables with biome and permission filters -- In-game GUI editor for drop entries +- In-game GUI editor for drop entries (live, no reload) - Per-player and global statistics plus a leaderboard - Three rod tiers (basic / advanced / legendary) gated by permission - Folia region-aware schedulers -- Public Java API for other plugins -- Crowdin localization (English and Japanese shipped) +- Public Java API for downstream plugins +- Crowdin localisation (English and Japanese ship in the jar) - Optional Nexo integration for custom items +## Preview + + + + + + + + + + + + + + + + + ## Quick install Drop the jar in `plugins/`, start the server, then `/mythicrod reload` @@ -35,5 +56,13 @@ Everything else lives on the docs site: - Configuration reference - Drop table format - Developer API -- Localization workflow +- Localisation workflow - Release runbook + +## Community + +- [Discord](https://discord.gg/MDwtUgxX9U) for support and feature + discussion. +- [Crowdin](https://crowdin.com/project/mythicrod) for translations. +- Issues and pull requests on + [GitHub](https://github.com/xcutiboo/MythicRod). From 872bc272ea55bb37e01d265b06c646890fae23b4 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Sun, 24 May 2026 21:14:52 -0600 Subject: [PATCH 02/21] expand stats docs with every snapshot field * Add screenshot/clip placeholders to README The marketplace listing has a hero strip and a feature list but nothing visual. Drop in commented-out slots for hero, drop-editor, catch demo, stats, and the personalised drops-preview command so adding screenshots later is a one-line uncomment. Bumped the docs/community footer so people land on Discord/Crowdin without hunting through docs/. No image bytes added; the placeholders are HTML comments. Each one records the suggested file name (assets/screenshots/) and the target resolution so we keep things consistent when we shoot them. * Expand stats docs with every snapshot field The page mentioned totalCaught, rareCaught, legendaryCaught and called it a day, but the snapshot record actually carries five catch-tier counters, three rod-use counters, and two Instants. A developer reading the page had no way to know commonCaught, uncommonCaught, basicRodUses, advancedRodUses, legendaryRodUses, playerName, or snapshotTime existed. List every field with its type and meaning. Note that all counters are non-negative by construction, so callers don't need to defend. Soften the tone so the page reads more like a guide than a spec: mention what the future returns when the UUID is unseen, clarify the limit clamp, show a tiny scheduler-hop snippet for callers who want to touch Bukkit state inside thenAccept, and explain the operator opt-out gracefully. --- docs/developer-api/stats.md | 114 ++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 36 deletions(-) diff --git a/docs/developer-api/stats.md b/docs/developer-api/stats.md index 41d6e33..08cbdc1 100644 --- a/docs/developer-api/stats.md +++ b/docs/developer-api/stats.md @@ -1,81 +1,123 @@ # Stats API MythicRod tracks per-player fishing statistics in -`plugins/MythicRod/statistics.yml`. Access them through three methods -on `MythicRodAPI`. - -## Read a single player's snapshot +`plugins/MythicRod/statistics.yml`. Three methods on `MythicRodAPI` +expose them, and every call completes on a MythicRod-owned async +thread so the main thread is never blocked. + +## What you get back + +Each lookup returns a `PlayerStatSnapshot`. It's an immutable Java +record, safe to pass around and to log. Every field is filled in; +nothing is `null`. + +| Field | Type | What it is | +| --- | --- | --- | +| `playerUuid()` | `UUID` | Player identity. Always present. | +| `playerName()` | `String` | Last name MythicRod saw for this UUID. Empty for unseen players. | +| `totalCaught()` | `int` | Total custom drops the player has caught. | +| `commonCaught()` | `int` | Catches that resolved to the `common` tier. | +| `uncommonCaught()` | `int` | Catches in the `uncommon` tier. | +| `rareCaught()` | `int` | Catches in the `rare` tier. | +| `legendaryCaught()` | `int` | Catches in the `legendary` tier. | +| `basicRodUses()` | `int` | Times the player has cast with a `basic` rod. | +| `advancedRodUses()` | `int` | Casts with an `advanced` rod. | +| `legendaryRodUses()` | `int` | Casts with a `legendary` rod. | +| `lastFished()` | `Instant` | When the player last caught anything. `Instant.EPOCH` if never. | +| `snapshotTime()` | `Instant` | When this snapshot was taken. | + +All counter fields are `>= 0`; the record's constructor rejects +negative values, so you never have to defend against them. + +## Reading a single player ```java api.getPlayerStats(player.getUniqueId()).thenAccept(snap -> { - long total = snap.totalCaught(); - int rare = snap.rareCaught(); - int legendary = snap.legendaryCaught(); + int total = snap.totalCaught(); + int rare = snap.rareCaught(); + int legendary = snap.legendaryCaught(); + Instant lastSeen = snap.lastFished(); + // snap.playerName(), snap.basicRodUses(), etc. }); ``` `getPlayerStats(UUID)` returns a `CompletableFuture`. -For an unknown UUID, the future completes with -`PlayerStatSnapshot.empty(uuid, "")` rather than failing. +For a UUID MythicRod has never seen, the future completes with +`PlayerStatSnapshot.empty(uuid, "")` rather than failing, so a missing +player is the same shape as a zeroed one. ## Leaderboards ```java import io.xcutiboo.mythicrod.api.PlayerStatSnapshot.StatType; -api.getTopPlayers(StatType.TOTAL_CAUGHT, 10).thenAccept(top -> { - for (PlayerStatSnapshot row : top) { - // row.playerName(), row.totalCaught(), row.lastFished() +api.getTopPlayers(StatType.TOTAL_CAUGHT, 10).thenAccept(rows -> { + for (PlayerStatSnapshot row : rows) { + String name = row.playerName(); + int total = row.totalCaught(); + Instant when = row.lastFished(); + // build leaderboard UI from here } }); ``` -| `StatType` | Sort order | +| `StatType` value | Sort order | | --- | --- | | `TOTAL_CAUGHT` | total catches, descending | -| `RARE_CAUGHT` | rare catches, descending | -| `LEGENDARY_CAUGHT` | legendary catches, descending | -| `LAST_FISHED` | most-recent catch first | +| `RARE_CAUGHT` | rare-tier catches, descending | +| `LEGENDARY_CAUGHT` | legendary-tier catches, descending | +| `LAST_FISHED` | most recent activity first | -`limit` is clamped to `1..100`. +`limit` is clamped to `1..100`. Ask for what you can render; a request +for `0` or `200` is silently rounded into range, never an error. -## Force a flush +## Force a flush before a backup ```java api.flushAllStats().thenRun(() -> { - // Stats are now durable on disk. + // Stats are now durable on disk; safe to snapshot statistics.yml. }); ``` -Useful right before a backup. MythicRod also flushes automatically on -plugin shutdown and on the configured cadence -(`timers.stats-save-interval-seconds`). +MythicRod also flushes automatically on plugin shutdown and on the +configured cadence (`timers.stats-save-interval-seconds`). Manual +flushes are only useful right before backup snapshots or for tests. -## Stat updates as events +## Reacting to updates as they happen -If you need a push notification rather than a poll, listen for -`MythicRodStatsUpdateEvent`. It carries the updated snapshot and the -catch tier that triggered the update. See -[events](events.md). +Polling works, but if you want a push, listen for +`MythicRodStatsUpdateEvent`. It carries the new snapshot and the tier +that triggered the change, so you can render leaderboards reactively +without busy-loops. See [events](events.md). ## Thread contract -All three methods complete on a MythicRod-owned async thread. Schedule -back to the right owner before touching Bukkit state. See -[Folia threading](folia-threading.md). +Every future completes on a MythicRod-owned async thread. Before +touching Bukkit state from inside `thenAccept` / `thenRun`, hop back +to the owner thread: + +```java +api.getPlayerStats(player.getUniqueId()).thenAccept(snap -> { + player.getScheduler().run(plugin, task -> { + player.sendMessage("You've caught " + snap.totalCaught() + " fish!"); + }, null); +}); +``` + +See [Folia threading](folia-threading.md) for the full table of which +scheduler owns each Bukkit object. -## What `statistics: false` does +## When the operator turns statistics off -When the `features.statistics.enabled` config flag is off: +When `features.statistics.enabled` is set to `false` in config: - Catch events do not increment counters. - `MythicRodStatsUpdateEvent` does not fire. - `getPlayerStats(...)` and `getTopPlayers(...)` still return whatever - was last persisted to disk. They do not error out. + was last persisted to disk; they do not throw. - `flushAllStats()` is a no-op. -Treat statistics as opt-in for admins. Plugins that rely on them should -either degrade gracefully or warn the operator when statistics are -disabled. +If your integration depends on live stats, log a friendly notice on +enable when the flag is off so the admin knows to flip it. Don't crash. [← Developer API](../developer-api.md) From 0b2ba4d92e3f50d4c15365e1a0f7e08885db4632 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Sun, 24 May 2026 22:02:09 -0600 Subject: [PATCH 03/21] mark api surface stability with apistatus * Add screenshot/clip placeholders to README The marketplace listing has a hero strip and a feature list but nothing visual. Drop in commented-out slots for hero, drop-editor, catch demo, stats, and the personalised drops-preview command so adding screenshots later is a one-line uncomment. Bumped the docs/community footer so people land on Discord/Crowdin without hunting through docs/. No image bytes added; the placeholders are HTML comments. Each one records the suggested file name (assets/screenshots/) and the target resolution so we keep things consistent when we shoot them. * Expand stats docs with every snapshot field The page mentioned totalCaught, rareCaught, legendaryCaught and called it a day, but the snapshot record actually carries five catch-tier counters, three rod-use counters, and two Instants. A developer reading the page had no way to know commonCaught, uncommonCaught, basicRodUses, advancedRodUses, legendaryRodUses, playerName, or snapshotTime existed. List every field with its type and meaning. Note that all counters are non-negative by construction, so callers don't need to defend. Soften the tone so the page reads more like a guide than a spec: mention what the future returns when the UUID is unseen, clarify the limit clamp, show a tiny scheduler-hop snippet for callers who want to touch Bukkit state inside thenAccept, and explain the operator opt-out gracefully. * Mark API surface stability with ApiStatus Add `@ApiStatus.AvailableSince("2026.1.0")` to the eight types that make up the public API: `MythicRodAPI`, `ExternalDropProvider`, `PlayerStatSnapshot`, `Result`, `DropCatalog`, `MythicRodServices`, and the four Paper events. That gives downstream maintainers a machine-readable record of when each type first shipped, and lines up with the SemVer story on the compatibility-policy doc page. Mark `MythicRodRewardRollEvent#forceDrop(CustomDrop)`, `MythicRodRewardRollEvent#getForcedDrop()`, and `MythicRodFishCatchEvent#getDrop()` as `@ApiStatus.Experimental` - they leak the internal `CustomDrop` type, so their shape can shift when the internal drop representation is refactored. The matching `getDropView()` / `getForcedDropView()` accessors stay stable because they return the platform-neutral `PlatformDrop`. --- mythicrod-api/build.gradle.kts | 1 + mythicrod-api/gradle.lockfile | 2 +- .../mythicrod/api/ExternalDropProvider.java | 2 ++ .../io/xcutiboo/mythicrod/api/MythicRodAPI.java | 2 ++ .../mythicrod/api/PlayerStatSnapshot.java | 2 ++ .../java/io/xcutiboo/mythicrod/api/Result.java | 3 +++ .../xcutiboo/mythicrod/api/drop/DropCatalog.java | 2 ++ .../mythicrod/paper/api/MythicRodServices.java | 2 ++ .../paper/events/MythicRodFishCatchEvent.java | 7 ++++++- .../paper/events/MythicRodReloadEvent.java | 2 ++ .../paper/events/MythicRodRewardRollEvent.java | 15 +++++++++++++++ .../paper/events/MythicRodStatsUpdateEvent.java | 2 ++ 12 files changed, 40 insertions(+), 2 deletions(-) diff --git a/mythicrod-api/build.gradle.kts b/mythicrod-api/build.gradle.kts index a2a934b..9c38b47 100644 --- a/mythicrod-api/build.gradle.kts +++ b/mythicrod-api/build.gradle.kts @@ -2,4 +2,5 @@ dependencies { compileOnly(libs.jetbrains.annotations) testImplementation(libs.junit.jupiter) + testCompileOnly(libs.jetbrains.annotations) } diff --git a/mythicrod-api/gradle.lockfile b/mythicrod-api/gradle.lockfile index f060a21..dc0ce8f 100644 --- a/mythicrod-api/gradle.lockfile +++ b/mythicrod-api/gradle.lockfile @@ -6,7 +6,7 @@ org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt org.jacoco:org.jacoco.ant:0.8.14=jacocoAnt org.jacoco:org.jacoco.core:0.8.14=jacocoAnt org.jacoco:org.jacoco.report:0.8.14=jacocoAnt -org.jetbrains:annotations:26.1.0=compileClasspath +org.jetbrains:annotations:26.1.0=compileClasspath,testCompileClasspath org.jspecify:jspecify:1.0.0=testCompileClasspath org.junit.jupiter:junit-jupiter-api:6.1.0=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:6.1.0=testRuntimeClasspath diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/ExternalDropProvider.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/ExternalDropProvider.java index 5cb9164..dee8515 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/ExternalDropProvider.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/ExternalDropProvider.java @@ -1,5 +1,6 @@ package io.xcutiboo.mythicrod.api; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -22,6 +23,7 @@ /// Use `MythicRodAPI#getItemFactory()` or `MythicRodAPI#createItem(String, int)` /// to construct MythicRod-compatible items instead of depending on Paper /// implementation classes directly. +@ApiStatus.AvailableSince("2026.1.0") public interface ExternalDropProvider { /// Returns the stable key for this provider. diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index ba41cdb..18222ba 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -5,6 +5,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import io.xcutiboo.mythicrod.api.PlayerStatSnapshot.StatType; @@ -38,6 +39,7 @@ /// /// Methods on this interface are stable unless the changelog calls out a major /// version break. Platform-specific implementation classes remain internal. +@ApiStatus.AvailableSince("2026.1.0") public interface MythicRodAPI { /// Returns the running plugin version string, such as `"2026.1.0"`. diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/PlayerStatSnapshot.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/PlayerStatSnapshot.java index 9ca4fe7..d627411 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/PlayerStatSnapshot.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/PlayerStatSnapshot.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.UUID; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /// Immutable snapshot of a player's MythicRod fishing statistics. @@ -27,6 +28,7 @@ /// @param lastFished timestamp of the player's most recent catch, or `Instant.EPOCH` /// if they have never fished /// @param snapshotTime when this snapshot was taken +@ApiStatus.AvailableSince("2026.1.0") public record PlayerStatSnapshot( @NotNull UUID playerUuid, @NotNull String playerName, diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/Result.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/Result.java index 0b28c8a..8e0b49a 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/Result.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/Result.java @@ -1,5 +1,7 @@ package io.xcutiboo.mythicrod.api; +import org.jetbrains.annotations.ApiStatus; + /// Small success/failure container used by MythicRod's public API. /// /// A successful result carries a value. A failed result carries a human-readable @@ -7,6 +9,7 @@ /// value. /// /// @param wrapped success value type +@ApiStatus.AvailableSince("2026.1.0") public final class Result { private final boolean success; private final T value; diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/drop/DropCatalog.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/drop/DropCatalog.java index c9c6261..dbcff22 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/drop/DropCatalog.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/drop/DropCatalog.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Set; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import io.xcutiboo.mythicrod.api.platform.PlatformDrop; @@ -11,6 +12,7 @@ /// /// The catalog reflects the plugin's active in-memory configuration. Returned /// collections are snapshots intended for inspection, not mutation. +@ApiStatus.AvailableSince("2026.1.0") public interface DropCatalog { /// Returns all currently registered category keys. diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/MythicRodServices.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/MythicRodServices.java index a5669b3..3219920 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/MythicRodServices.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/MythicRodServices.java @@ -6,6 +6,7 @@ import org.bukkit.Server; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicesManager; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import io.xcutiboo.mythicrod.api.MythicRodAPI; @@ -18,6 +19,7 @@ /// Lookups are valid after MythicRod has enabled and before it disables. A /// missing service means MythicRod is not installed, not enabled yet, or is /// shutting down. +@ApiStatus.AvailableSince("2026.1.0") public final class MythicRodServices { private MythicRodServices() { diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodFishCatchEvent.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodFishCatchEvent.java index 8521a62..ee0874a 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodFishCatchEvent.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodFishCatchEvent.java @@ -7,6 +7,7 @@ import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import io.xcutiboo.mythicrod.api.platform.PlatformDrop; @@ -42,6 +43,7 @@ /// } /// } /// ``` +@ApiStatus.AvailableSince("2026.1.0") public final class MythicRodFishCatchEvent extends Event implements Cancellable { private static final HandlerList HANDLER_LIST = new HandlerList(); @@ -72,8 +74,11 @@ public Player getPlayer() { return player; } - /// Returns the selected reward descriptor. + /// Returns the selected reward descriptor as the internal `CustomDrop` + /// type. Experimental: the internal representation may change. Use + /// `getDropView()` when you only need stable read-only metadata. @NotNull + @ApiStatus.Experimental public CustomDrop getDrop() { return drop; } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodReloadEvent.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodReloadEvent.java index 86378cd..5253ab7 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodReloadEvent.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodReloadEvent.java @@ -2,6 +2,7 @@ import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /// Fires once MythicRod has finished an atomic configuration and drop-table @@ -18,6 +19,7 @@ /// /// The event is not cancellable. Reloads always complete before the event /// fires, so cancelling would have no meaningful effect. +@ApiStatus.AvailableSince("2026.1.0") public final class MythicRodReloadEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodRewardRollEvent.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodRewardRollEvent.java index 2700a25..647a421 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodRewardRollEvent.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodRewardRollEvent.java @@ -5,6 +5,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,6 +40,7 @@ /// Fired from the same player-owned execution path as the fishing event. On /// ordinary Paper this is the synchronous event thread. On Folia this is the /// owning region thread. +@ApiStatus.AvailableSince("2026.1.0") public final class MythicRodRewardRollEvent extends Event { private static final HandlerList HANDLER_LIST = new HandlerList(); @@ -108,13 +110,26 @@ public void setLuckMultiplier(double multiplier) { /// /// MythicRod will use this drop if it is non-null after the event completes. /// + /// This entry point takes the internal `CustomDrop` type and is therefore + /// considered experimental. The signature may change when the internal + /// drop representation is refactored. Prefer `setLuckMultiplier(double)` + /// or the post-roll `MythicRodFishCatchEvent#setRewardItem(ItemStack)` + /// path when you only need to influence the reward and do not need to + /// pin a specific configured drop. + /// /// @param drop drop to force, or `null` to clear any forced drop + @ApiStatus.Experimental public void forceDrop(@Nullable CustomDrop drop) { this.forcedDrop = drop; } + /// Returns the forced drop as the internal `CustomDrop` type. Experimental + /// because it exposes the internal representation; for stable inspection, + /// use `getForcedDropView()`. + /// /// @return forced drop set by an external plugin, or `null` when normal selection should proceed @Nullable + @ApiStatus.Experimental public CustomDrop getForcedDrop() { return forcedDrop; } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java index 50c93c0..6d27871 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java @@ -5,6 +5,7 @@ import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import io.xcutiboo.mythicrod.api.PlayerStatSnapshot; @@ -21,6 +22,7 @@ /// event thread on ordinary Paper and the player's region thread on Folia. /// Asynchronous work scheduled from a handler must be re-dispatched to the /// correct owner before touching world or inventory state. +@ApiStatus.AvailableSince("2026.1.0") public final class MythicRodStatsUpdateEvent extends Event { private static final HandlerList HANDLER_LIST = new HandlerList(); From 6e7558f15bff8356c89cc5a32859b36e9a4f7e25 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Mon, 25 May 2026 14:23:41 -0600 Subject: [PATCH 04/21] polymart upload + dedupe, bump hangar plugin Adds the polymart publish workflow with a getResourceUpdates pre-check so a re-tagged release does not double-post. Mirrors the same dedupe on the modrinth side. Bumps the hangar gradle plugin to 0.1.4. --- .github/workflows/publish-modrinth.yml | 23 ++++- .github/workflows/publish-polymart.yml | 122 +++++++++++++++++++++++++ gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 20 ++++ 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-polymart.yml diff --git a/.github/workflows/publish-modrinth.yml b/.github/workflows/publish-modrinth.yml index dc1f4c9..35a5e1a 100644 --- a/.github/workflows/publish-modrinth.yml +++ b/.github/workflows/publish-modrinth.yml @@ -73,8 +73,29 @@ jobs: echo "jar_publish=$jar_publish" } >> "$GITHUB_OUTPUT" - - name: Publish to Modrinth + - name: Check Modrinth for an existing version if: env.MODRINTH_TOKEN != '' && steps.meta.outputs.jar_publish == 'true' + id: dedupe + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + existing=$(curl -fsSL \ + -H "User-Agent: xcutiboo/MythicRod CI" \ + "https://api.modrinth.com/v2/project/mythicrod/version/${VERSION}" \ + -o /dev/null -w "%{http_code}" || true) + if [[ "$existing" == "200" ]]; then + echo "Modrinth already has version $VERSION; skipping upload." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Publish to Modrinth + if: | + env.MODRINTH_TOKEN != '' + && steps.meta.outputs.jar_publish == 'true' + && steps.dedupe.outputs.skip == 'false' uses: Kir-Antipov/mc-publish@v3.3 with: modrinth-id: mythicrod diff --git a/.github/workflows/publish-polymart.yml b/.github/workflows/publish-polymart.yml new file mode 100644 index 0000000..255b081 --- /dev/null +++ b/.github/workflows/publish-polymart.yml @@ -0,0 +1,122 @@ +name: Publish to Polymart + +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + env: + POLYMART_API_TOKEN: ${{ secrets.POLYMART_API_TOKEN }} + POLYMART_RESOURCE_ID: ${{ vars.POLYMART_RESOURCE_ID }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@ed408507eac070d1f99cc633dbcf757c94c7933a # v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4 + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Skip when not configured + if: env.POLYMART_API_TOKEN == '' || env.POLYMART_RESOURCE_ID == '' + run: | + echo "POLYMART_API_TOKEN secret or POLYMART_RESOURCE_ID variable is not set; skipping Polymart publish." + exit 0 + + - name: Build shadow jar + if: env.POLYMART_API_TOKEN != '' && env.POLYMART_RESOURCE_ID != '' + run: ./gradlew :mythicrod-paper:shadowJar --stacktrace --no-daemon + + - name: Resolve release metadata + if: env.POLYMART_API_TOKEN != '' && env.POLYMART_RESOURCE_ID != '' + id: meta + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + version="${REF_NAME#v}" + if [[ -z "$version" || "$version" == "$REF_NAME" ]]; then + version=$(./gradlew -q :mythicrod-paper:properties | awk -F': *' '/^version:/{print $2; exit}') + fi + jar=$(ls mythicrod-paper/build/libs/MythicRod-Paper-*.jar | grep -v -- '-dev' | head -1) + beta=0 + snapshot=0 + case "$version" in + *-rc*|*-beta*|*-alpha*) beta=1 ;; + *-snapshot*|*-SNAPSHOT*) snapshot=1 ;; + esac + { + echo "version=$version" + echo "jar=$jar" + echo "beta=$beta" + echo "snapshot=$snapshot" + } >> "$GITHUB_OUTPUT" + + - name: Check Polymart for an existing version + if: env.POLYMART_API_TOKEN != '' && env.POLYMART_RESOURCE_ID != '' + id: dedupe + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + existing=$(curl -fsSL "https://api.polymart.org/v1/getResourceUpdates?resource_id=${POLYMART_RESOURCE_ID}" \ + | jq -r --arg v "$VERSION" '.response.updates[]? | select(.version == $v) | .id' | head -1) + if [[ -n "$existing" ]]; then + echo "Polymart already has update id $existing for version $VERSION; skipping upload." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upload version to Polymart + if: | + env.POLYMART_API_TOKEN != '' + && env.POLYMART_RESOURCE_ID != '' + && steps.dedupe.outputs.skip == 'false' + env: + VERSION: ${{ steps.meta.outputs.version }} + JAR: ${{ steps.meta.outputs.jar }} + BETA: ${{ steps.meta.outputs.beta }} + SNAPSHOT: ${{ steps.meta.outputs.snapshot }} + run: | + set -euo pipefail + message=$(awk '/^## \['"$VERSION"'\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md \ + | sed '/./,$!d' | head -200) + if [[ -z "$message" ]]; then + message="See https://github.com/xcutiboo/MythicRod/releases/tag/v${VERSION}" + fi + response=$(curl -fsS -X POST "https://api.polymart.org/v1/postUpdate" \ + -F "api_key=${POLYMART_API_TOKEN}" \ + -F "resource_id=${POLYMART_RESOURCE_ID}" \ + -F "version=${VERSION}" \ + -F "title=MythicRod ${VERSION}" \ + -F "message=${message}" \ + -F "beta=${BETA}" \ + -F "snapshot=${SNAPSHOT}" \ + -F "file=@${JAR}") + echo "$response" + status=$(echo "$response" | jq -r '.response.success // false') + if [[ "$status" != "true" ]]; then + echo "Polymart rejected the upload." + echo "$response" | jq -r '.response.errors[]?' || true + exit 1 + fi diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a679435..240ea3b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ junit-platform = "6.1.0" # Gradle Plugins shadow = "9.4.1" run-paper = "3.0.2" -hangar-publish = "0.1.2" +hangar-publish = "0.1.4" sonarqube = "7.3.0.8198" # Dependencies diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c6cb584..d5511f4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -254,6 +254,11 @@ + + + + + @@ -647,6 +652,11 @@ + + + + + @@ -660,6 +670,11 @@ + + + + + @@ -668,6 +683,11 @@ + + + + + From f19350b3d14b4c69047570cccf4b9c97bbad26aa Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Mon, 25 May 2026 16:47:11 -0600 Subject: [PATCH 05/21] publish mythicrod-api via jitpack * Round out marketplace automation with Polymart, harden dedupe, bump Hangar plugin Add a Polymart publish workflow that fires on every `v*` tag. It posts to `https://api.polymart.org/v1/postUpdate` with the shaded Paper jar, the version string from the tag, and the CHANGELOG.md section for that version as the update notes. Beta/RC/alpha tags flip the `beta` flag; `-SNAPSHOT` tags flip `snapshot`. The workflow skips itself cleanly when `POLYMART_API_TOKEN` (secret) or `POLYMART_RESOURCE_ID` (variable) is missing, and pre-checks `/v1/getResourceUpdates` so a re-tagged release does not double-post. Mirror the same dedupe behaviour on the Modrinth side: before calling `Kir-Antipov/mc-publish`, hit `/v2/project/mythicrod/version/{version}` and skip the upload step when the version already exists. Avoids stray duplicate uploads when a tag is moved. Bump the Hangar publish plugin to 0.1.4 (the latest at the time of writing) and refresh the dependency lockfile. SpigotMC does not expose a public upload API; that lane stays manual and is documented separately. * Publish mythicrod-api via JitPack Add a `maven-publish` block to the api module so it produces a proper pom plus sources and javadoc jars when published. Add `jitpack.yml` at the repo root pinning JDK 25 and running `./gradlew :mythicrod-api:publishToMavenLocal -x test --no-daemon` on JitPack's builder. Result: the API artifact is reachable as `com.github.xcutiboo.MythicRod:mythicrod-api:vTAG` from any downstream Gradle/Maven build without copying jars around. Update the developer-api setup page to lead with the JitPack coordinate and keep the jar-drop instructions as a fallback for hosts that cannot reach jitpack.io. --- docs/developer-api/setup.md | 26 ++++++++++++++++++++---- jitpack.yml | 4 ++++ mythicrod-api/build.gradle.kts | 37 ++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 jitpack.yml diff --git a/docs/developer-api/setup.md b/docs/developer-api/setup.md index 3df8eb3..414b882 100644 --- a/docs/developer-api/setup.md +++ b/docs/developer-api/setup.md @@ -1,21 +1,39 @@ # Setup -## Gradle (Kotlin DSL) +The cleanest way to depend on MythicRod is through JitPack, which +builds the `mythicrod-api` artifact straight from the public GitHub +tag. No file copies, no jar wrangling. + +## Gradle (Kotlin DSL) - JitPack ```kotlin repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") + maven("https://jitpack.io") } dependencies { compileOnly("io.papermc.paper:paper-api:26.1.2.build.64-stable") - compileOnly(files("libs/MythicRod-Paper-2026.1.0.jar")) + compileOnly("com.github.xcutiboo.MythicRod:mythicrod-api:v2026.1.0") } ``` -Drop the released MythicRod jar into your project's `libs/` folder. -Keep it `compileOnly`. Never shade or relocate it. +The first build for a new MythicRod tag triggers a one-time JitPack +build (1-5 minutes). Subsequent consumers get the cached artifact. + +## Gradle (Kotlin DSL) - jar drop + +If you cannot reach JitPack from your build host, drop the released +MythicRod jar into your project's `libs/` folder and keep it +`compileOnly`. Never shade or relocate it. + +```kotlin +dependencies { + compileOnly("io.papermc.paper:paper-api:26.1.2.build.64-stable") + compileOnly(files("libs/MythicRod-Paper-2026.1.0.jar")) +} +``` ## Gradle (Groovy) diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..e13889c --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,4 @@ +jdk: + - openjdk25 +install: + - ./gradlew :mythicrod-api:publishToMavenLocal -x test --no-daemon diff --git a/mythicrod-api/build.gradle.kts b/mythicrod-api/build.gradle.kts index 9c38b47..24c9be9 100644 --- a/mythicrod-api/build.gradle.kts +++ b/mythicrod-api/build.gradle.kts @@ -1,6 +1,43 @@ +plugins { + `maven-publish` +} + dependencies { compileOnly(libs.jetbrains.annotations) testImplementation(libs.junit.jupiter) testCompileOnly(libs.jetbrains.annotations) } + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + register("maven") { + from(components["java"]) + groupId = "io.xcutiboo" + artifactId = "mythicrod-api" + version = project.version.toString() + + pom { + name.set("MythicRod API") + description.set("Public integration surface for MythicRod (Paper / Folia).") + url.set("https://github.com/xcutiboo/MythicRod") + licenses { + license { + name.set("MIT") + url.set("https://github.com/xcutiboo/MythicRod/blob/master/LICENSE") + } + } + scm { + url.set("https://github.com/xcutiboo/MythicRod") + connection.set("scm:git:git://github.com/xcutiboo/MythicRod.git") + developerConnection.set("scm:git:ssh://git@github.com:xcutiboo/MythicRod.git") + } + } + } + } +} From 551f5e2a6fd94dd5f48709581c397935e3ca2c5b Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Mon, 25 May 2026 17:34:08 -0600 Subject: [PATCH 06/21] fix jitpack coord in setup docs jitpack treats the api module as the project artifact when it's the only thing in the install step. consumers use com.github.xcutiboo:MythicRod (not the .MythicRod:mythicrod-api submodule path). available from v2026.1.1 onwards; pre-2026.1.1 tags built before this config landed. --- docs/developer-api/setup.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/developer-api/setup.md b/docs/developer-api/setup.md index 414b882..2bcbbc7 100644 --- a/docs/developer-api/setup.md +++ b/docs/developer-api/setup.md @@ -15,12 +15,16 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:26.1.2.build.64-stable") - compileOnly("com.github.xcutiboo.MythicRod:mythicrod-api:v2026.1.0") + // JitPack treats the api module as the project artifact, so pin + // the repo not a submodule. Available from v2026.1.1 onwards. + compileOnly("com.github.xcutiboo:MythicRod:v2026.1.1") } ``` -The first build for a new MythicRod tag triggers a one-time JitPack -build (1-5 minutes). Subsequent consumers get the cached artifact. +The first resolution for a new MythicRod tag triggers a one-time +JitPack build (1-5 minutes). Subsequent consumers get the cached +artifact. For development you can also pin +`com.github.xcutiboo:MythicRod:master-SNAPSHOT`. ## Gradle (Kotlin DSL) - jar drop From 7075a640d05d5f3756d757ed93d680a9809811c7 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Mon, 25 May 2026 19:12:35 -0600 Subject: [PATCH 07/21] polymart api_key in query string too Polymart rejected the multipart api_key with NO_KEY. Send it as both a query string param and a form field so the new voxel.shop backend accepts it regardless of which one it checks. --- .github/workflows/publish-polymart.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-polymart.yml b/.github/workflows/publish-polymart.yml index 255b081..2aa2f52 100644 --- a/.github/workflows/publish-polymart.yml +++ b/.github/workflows/publish-polymart.yml @@ -104,7 +104,7 @@ jobs: if [[ -z "$message" ]]; then message="See https://github.com/xcutiboo/MythicRod/releases/tag/v${VERSION}" fi - response=$(curl -fsS -X POST "https://api.polymart.org/v1/postUpdate" \ + response=$(curl -fsS -X POST "https://api.polymart.org/v1/postUpdate?api_key=${POLYMART_API_TOKEN}" \ -F "api_key=${POLYMART_API_TOKEN}" \ -F "resource_id=${POLYMART_RESOURCE_ID}" \ -F "version=${VERSION}" \ From 966fe2d8678246d02bb2202173f577814920394e Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Wed, 27 May 2026 11:33:54 -0600 Subject: [PATCH 08/21] polymart api host moved to api.voxel.shop Polymart's rebrand to Voxel Shop moved the API surface from api.polymart.org to api.voxel.shop. Same endpoint paths, same multipart form spec, same auth field. The old host still resolves DNS but rejects new-format API keys with NO_KEY. Verified against voxel.shop/wiki/api: postUpdate and getResourceUpdates both live at api.voxel.shop/v1/. --- .github/workflows/publish-polymart.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-polymart.yml b/.github/workflows/publish-polymart.yml index 2aa2f52..4feb035 100644 --- a/.github/workflows/publish-polymart.yml +++ b/.github/workflows/publish-polymart.yml @@ -78,7 +78,7 @@ jobs: VERSION: ${{ steps.meta.outputs.version }} run: | set -euo pipefail - existing=$(curl -fsSL "https://api.polymart.org/v1/getResourceUpdates?resource_id=${POLYMART_RESOURCE_ID}" \ + existing=$(curl -fsSL "https://api.voxel.shop/v1/getResourceUpdates?resource_id=${POLYMART_RESOURCE_ID}" \ | jq -r --arg v "$VERSION" '.response.updates[]? | select(.version == $v) | .id' | head -1) if [[ -n "$existing" ]]; then echo "Polymart already has update id $existing for version $VERSION; skipping upload." @@ -104,7 +104,7 @@ jobs: if [[ -z "$message" ]]; then message="See https://github.com/xcutiboo/MythicRod/releases/tag/v${VERSION}" fi - response=$(curl -fsS -X POST "https://api.polymart.org/v1/postUpdate?api_key=${POLYMART_API_TOKEN}" \ + response=$(curl -fsS -X POST "https://api.voxel.shop/v1/postUpdate" \ -F "api_key=${POLYMART_API_TOKEN}" \ -F "resource_id=${POLYMART_RESOURCE_ID}" \ -F "version=${VERSION}" \ From 6ad68639ab27af397d5ec30fdb00cd72dc12cda0 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Wed, 27 May 2026 14:02:21 -0600 Subject: [PATCH 09/21] soft-fail polymart upload, add release-please polymart's voxel.shop rebrand keeps rejecting valid API keys with NO_KEY in their open beta. Mark the upload step continue-on-error so a known-broken polymart leg never blocks hangar/modrinth/jitpack on the same tag. drop release-please workflow + config so conventional-commit messages on master flow into a versioned changelog and a tag PR ready to merge. include-v-in-tag matches the v* trigger the publish workflows already use. --- .github/workflows/publish-polymart.yml | 6 +++++ .github/workflows/release-please.yml | 31 ++++++++++++++++++++++++++ .release-please-manifest.json | 3 +++ gradle.properties | 1 + release-please-config.json | 20 +++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/publish-polymart.yml b/.github/workflows/publish-polymart.yml index 4feb035..1acf74b 100644 --- a/.github/workflows/publish-polymart.yml +++ b/.github/workflows/publish-polymart.yml @@ -92,6 +92,12 @@ jobs: env.POLYMART_API_TOKEN != '' && env.POLYMART_RESOURCE_ID != '' && steps.dedupe.outputs.skip == 'false' + # Polymart's voxel.shop rebrand is in open beta and the + # postUpdate API has been returning NO_KEY for valid keys. + # Treat the upload as soft so a transient Polymart auth + # bug cannot block the rest of the release lane (Hangar, + # Modrinth, GitHub Release, JitPack). + continue-on-error: true env: VERSION: ${{ steps.meta.outputs.version }} JAR: ${{ steps.meta.outputs.jar }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..24c97fd --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,31 @@ +name: Release Please + +on: + push: + branches: [master] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + release-type: simple + package-name: MythicRod + include-v-in-tag: true + changelog-types: | + [ + {"type":"feat","section":"Features","hidden":false}, + {"type":"fix","section":"Bug fixes","hidden":false}, + {"type":"perf","section":"Performance","hidden":false}, + {"type":"deps","section":"Dependencies","hidden":false}, + {"type":"docs","section":"Documentation","hidden":false}, + {"type":"refactor","section":"Refactoring","hidden":false}, + {"type":"chore","section":"Chores","hidden":true}, + {"type":"test","section":"Tests","hidden":true}, + {"type":"ci","section":"CI","hidden":true} + ] diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..52a29fa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2026.1.0" +} diff --git a/gradle.properties b/gradle.properties index ac899f9..0d0a40b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ +# x-release-please-version version=2026.1.0 paperVersion=26.1.2 hangarProjectId=MythicRod diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..e2c886e --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,20 @@ +{ + "release-type": "simple", + "packages": { + ".": { + "release-type": "simple", + "include-v-in-tag": true, + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "draft": false, + "prerelease": false, + "extra-files": [ + { + "type": "generic", + "path": "gradle.properties" + } + ] + } + }, + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} From 2223a750b30ea286e48015373253c62263cc9653 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Wed, 27 May 2026 19:48:13 -0600 Subject: [PATCH 10/21] tier-coded catch celebration Pull the rarity palette every RPG player has learned (white, green, blue, purple, gold) into a TierVisualEffects helper and fire it next to the existing weight-based particle code so the tier intent reads at a glance. Each tier gets a single hero particle plus one short sound; legendary and mythic use DUST_COLOR_TRANSITION for the crossfade that says "rising tier". Player-scoped spawnParticle and playSound only, so other people on the lake never receive the extra packets and the cost stays bounded no matter how busy fishing gets. --- .../paper/fishing/FishingListener.java | 5 ++ .../paper/util/TierVisualEffects.java | 90 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java index 5abe950..210bf79 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java @@ -670,6 +670,11 @@ private void spawnCatchEffects(Player player, Location hookLocation, CustomDrop if (shouldShowParticles(player)) { spawnRarityParticles(player, effectLocation, weight); + // Tier-coded celebration on top of the legacy weight-based + // effects. Palette follows the loot-tier convention every + // RPG player has learned: white-common, green-uncommon, + // blue-rare, purple-legendary, orange-mythic. + io.xcutiboo.mythicrod.paper.util.TierVisualEffects.playCatch(player, drop.getTier()); } if (plugin.getConfigManager().useSounds()) { diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java new file mode 100644 index 0000000..9d247dd --- /dev/null +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java @@ -0,0 +1,90 @@ +package io.xcutiboo.mythicrod.paper.util; + +import java.util.Locale; + +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/// Tier-coded particle and sound feedback for catch events. +/// +/// The palette follows the loot-tier convention every player has +/// learned across the last two decades of role-playing games: +/// white-common, green-uncommon, blue-rare, purple-legendary, +/// orange-mythic. Each tier pairs one hero particle with one short +/// sound; nothing layers more than two so the cue reads cleanly +/// even at full TPS. +/// +/// Calls fire only at the player who caught the drop. Other players +/// nearby never receive the packets, so the cost stays bounded no +/// matter how busy the lake is. +public final class TierVisualEffects { + + private static final Color COMMON = Color.fromRGB(0xFF, 0xFF, 0xFF); + private static final Color UNCOMMON = Color.fromRGB(0x55, 0xFF, 0x55); + private static final Color RARE = Color.fromRGB(0x55, 0x55, 0xFF); + private static final Color LEGENDARY = Color.fromRGB(0xAA, 0x00, 0xAA); + private static final Color MYTHIC = Color.fromRGB(0xFF, 0xAA, 0x00); + + private static final float DUST_SIZE = 1.2f; + + private TierVisualEffects() { + } + + /// Plays the catch celebration tied to the given tier at the + /// player's current location. Falls back to the common cue when + /// the tier name is unknown. + public static void playCatch(@NotNull Player player, @NotNull String tier) { + Location at = player.getLocation().add(0, 1.0, 0); + switch (tier.toLowerCase(Locale.ROOT)) { + case "uncommon" -> uncommon(player, at); + case "rare" -> rare(player, at); + case "legendary" -> legendary(player, at); + case "mythic", "mythical" -> mythic(player, at); + default -> common(player, at); + } + } + + private static void common(Player player, Location at) { + player.spawnParticle(Particle.HAPPY_VILLAGER, at, 6, 0.4, 0.3, 0.4, 0); + player.playSound(at, Sound.ENTITY_ITEM_PICKUP, 0.6f, 1.0f); + } + + private static void uncommon(Player player, Location at) { + player.spawnParticle(Particle.END_ROD, at, 8, 0.35, 0.4, 0.35, 0.02); + Particle.DustOptions dust = new Particle.DustOptions(UNCOMMON, DUST_SIZE); + player.spawnParticle(Particle.DUST, at, 12, 0.4, 0.4, 0.4, 0, dust); + player.playSound(at, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.7f, 1.2f); + } + + private static void rare(Player player, Location at) { + Particle.DustOptions dust = new Particle.DustOptions(RARE, DUST_SIZE); + player.spawnParticle(Particle.DUST, at, 20, 0.5, 0.5, 0.5, 0, dust); + player.spawnParticle(Particle.ENCHANT, at, 30, 0.5, 0.5, 0.5, 0.5); + player.playSound(at, Sound.BLOCK_NOTE_BLOCK_BELL, 0.8f, 1.0f); + } + + private static void legendary(Player player, Location at) { + Particle.DustTransition transition = + new Particle.DustTransition(LEGENDARY, MYTHIC, DUST_SIZE + 0.3f); + player.spawnParticle(Particle.DUST_COLOR_TRANSITION, at, 30, + 0.6, 0.7, 0.6, 0, transition); + player.spawnParticle(Particle.TOTEM_OF_UNDYING, at, 12, + 0.4, 0.5, 0.4, 0.3); + player.playSound(at, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f); + } + + private static void mythic(Player player, Location at) { + Particle.DustTransition transition = + new Particle.DustTransition(MYTHIC, COMMON, DUST_SIZE + 0.5f); + player.spawnParticle(Particle.DUST_COLOR_TRANSITION, at, 40, + 0.7, 0.8, 0.7, 0, transition); + player.spawnParticle(Particle.TOTEM_OF_UNDYING, at, 20, + 0.5, 0.6, 0.5, 0.5); + player.playSound(at, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f); + player.playSound(at, Sound.ENTITY_ENDER_DRAGON_GROWL, 0.35f, 0.8f); + } +} From 34d1b15c38899f23f8d0fded0b02a55df08a9217 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Wed, 27 May 2026 21:31:42 -0600 Subject: [PATCH 11/21] add rising-helix animation to legendary + mythic catch Existing burst now leads into a 1-second helix on the player's owner thread (folia-safe). Two points per tick on opposite sides gives the double-helix look without doubling the particle count. Cancels cleanly when the player logs out mid-animation. Legendary helix crossfades purple to gold; mythic crossfades gold to white. Both stay player-scoped so other people on the lake don't get the extra packets. --- .../paper/fishing/FishingListener.java | 2 +- .../paper/util/TierVisualEffects.java | 69 ++++++++++++++++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java index 210bf79..c214151 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java @@ -674,7 +674,7 @@ private void spawnCatchEffects(Player player, Location hookLocation, CustomDrop // effects. Palette follows the loot-tier convention every // RPG player has learned: white-common, green-uncommon, // blue-rare, purple-legendary, orange-mythic. - io.xcutiboo.mythicrod.paper.util.TierVisualEffects.playCatch(player, drop.getTier()); + io.xcutiboo.mythicrod.paper.util.TierVisualEffects.playCatch(plugin, player, drop.getTier()); } if (plugin.getConfigManager().useSounds()) { diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java index 9d247dd..459ddb8 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java @@ -1,12 +1,14 @@ package io.xcutiboo.mythicrod.paper.util; import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.Sound; import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; /// Tier-coded particle and sound feedback for catch events. @@ -15,8 +17,9 @@ /// learned across the last two decades of role-playing games: /// white-common, green-uncommon, blue-rare, purple-legendary, /// orange-mythic. Each tier pairs one hero particle with one short -/// sound; nothing layers more than two so the cue reads cleanly -/// even at full TPS. +/// sound; legendary and mythic add a brief rising-helix animation +/// for spectacle without breaking the budget (player-scoped packets +/// only, capped at one second). /// /// Calls fire only at the player who caught the drop. Other players /// nearby never receive the packets, so the cost stays bounded no @@ -29,21 +32,32 @@ public final class TierVisualEffects { private static final Color LEGENDARY = Color.fromRGB(0xAA, 0x00, 0xAA); private static final Color MYTHIC = Color.fromRGB(0xFF, 0xAA, 0x00); - private static final float DUST_SIZE = 1.2f; + private static final float DUST_SIZE = 1.2f; + private static final int HELIX_TICKS = 20; // 1 second of spectacle. + private static final int HELIX_POINTS = 3; // particles per tick. + private static final double HELIX_RADIUS = 0.7; + private static final double HELIX_LIFT = 0.12; // y rise per tick. private TierVisualEffects() { } /// Plays the catch celebration tied to the given tier at the /// player's current location. Falls back to the common cue when - /// the tier name is unknown. - public static void playCatch(@NotNull Player player, @NotNull String tier) { + /// the tier name is unknown. Legendary and mythic schedule a + /// short rising-helix animation on the player's owner thread. + public static void playCatch(@NotNull JavaPlugin plugin, @NotNull Player player, @NotNull String tier) { Location at = player.getLocation().add(0, 1.0, 0); switch (tier.toLowerCase(Locale.ROOT)) { case "uncommon" -> uncommon(player, at); case "rare" -> rare(player, at); - case "legendary" -> legendary(player, at); - case "mythic", "mythical" -> mythic(player, at); + case "legendary" -> { + legendaryBurst(player, at); + helix(plugin, player, LEGENDARY, MYTHIC); + } + case "mythic", "mythical" -> { + mythicBurst(player, at); + helix(plugin, player, MYTHIC, COMMON); + } default -> common(player, at); } } @@ -67,7 +81,7 @@ private static void rare(Player player, Location at) { player.playSound(at, Sound.BLOCK_NOTE_BLOCK_BELL, 0.8f, 1.0f); } - private static void legendary(Player player, Location at) { + private static void legendaryBurst(Player player, Location at) { Particle.DustTransition transition = new Particle.DustTransition(LEGENDARY, MYTHIC, DUST_SIZE + 0.3f); player.spawnParticle(Particle.DUST_COLOR_TRANSITION, at, 30, @@ -77,7 +91,7 @@ private static void legendary(Player player, Location at) { player.playSound(at, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f); } - private static void mythic(Player player, Location at) { + private static void mythicBurst(Player player, Location at) { Particle.DustTransition transition = new Particle.DustTransition(MYTHIC, COMMON, DUST_SIZE + 0.5f); player.spawnParticle(Particle.DUST_COLOR_TRANSITION, at, 40, @@ -87,4 +101,41 @@ private static void mythic(Player player, Location at) { player.playSound(at, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1.0f, 1.0f); player.playSound(at, Sound.ENTITY_ENDER_DRAGON_GROWL, 0.35f, 0.8f); } + + /// Schedules a rising-helix animation around the player on the + /// player's owner thread. Uses Particle.DUST_COLOR_TRANSITION so + /// the trail crossfades from start to end while it rises. + private static void helix(JavaPlugin plugin, Player player, Color from, Color to) { + Particle.DustTransition transition = + new Particle.DustTransition(from, to, DUST_SIZE); + AtomicInteger ticks = new AtomicInteger(0); + player.getScheduler().runAtFixedRate(plugin, task -> { + if (!player.isOnline()) { + task.cancel(); + return; + } + int tick = ticks.getAndIncrement(); + if (tick >= HELIX_TICKS) { + task.cancel(); + return; + } + spawnHelixSlice(player, transition, tick); + }, () -> { }, 1L, 1L); + } + + /// Spawns one slice of a rising helix around the player. Two + /// points per tick on opposite sides give the double-helix look + /// without doubling the particle count. + private static void spawnHelixSlice(Player player, Particle.DustTransition transition, int tick) { + Location base = player.getLocation(); + for (int p = 0; p < HELIX_POINTS; p++) { + double angle = (tick * 0.45) + (p * (2 * Math.PI / HELIX_POINTS)); + double x = base.getX() + HELIX_RADIUS * Math.cos(angle); + double z = base.getZ() + HELIX_RADIUS * Math.sin(angle); + double y = base.getY() + 0.2 + (tick * HELIX_LIFT); + player.spawnParticle(Particle.DUST_COLOR_TRANSITION, x, y, z, + 1, 0, 0, 0, 0, transition); + } + } + } From a5e0ff2aefa9168dda8bbe99d602ec2357c7c117 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Thu, 28 May 2026 13:17:09 -0600 Subject: [PATCH 12/21] throttle helix to once per 4 seconds per player Two legendary catches in a row would otherwise pile a second helix on top of the first one still in flight. Track the last helix timestamp per UUID and skip the helix when the previous one is still inside its cooldown. The burst still plays; the spectacle is just spared from spam. --- .../paper/util/TierVisualEffects.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java index 459ddb8..ac577ff 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/util/TierVisualEffects.java @@ -1,6 +1,9 @@ package io.xcutiboo.mythicrod.paper.util; import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.bukkit.Color; @@ -37,6 +40,13 @@ public final class TierVisualEffects { private static final int HELIX_POINTS = 3; // particles per tick. private static final double HELIX_RADIUS = 0.7; private static final double HELIX_LIFT = 0.12; // y rise per tick. + private static final long HELIX_COOLDOWN_MS = 4_000L; + + /// Per-player timestamp of the last helix animation. Used to throttle + /// the heavy cue so back-to-back legendary catches don't bury one + /// celebration under the next. The map is bounded by online-player + /// count and entries naturally fall out of relevance after logout. + private static final Map LAST_HELIX = new ConcurrentHashMap<>(); private TierVisualEffects() { } @@ -52,16 +62,33 @@ public static void playCatch(@NotNull JavaPlugin plugin, @NotNull Player player, case "rare" -> rare(player, at); case "legendary" -> { legendaryBurst(player, at); - helix(plugin, player, LEGENDARY, MYTHIC); + if (claimHelixSlot(player)) { + helix(plugin, player, LEGENDARY, MYTHIC); + } } case "mythic", "mythical" -> { mythicBurst(player, at); - helix(plugin, player, MYTHIC, COMMON); + if (claimHelixSlot(player)) { + helix(plugin, player, MYTHIC, COMMON); + } } default -> common(player, at); } } + /// Returns true at most once per HELIX_COOLDOWN_MS per player. + /// Throttles the heavy cue so back-to-back top-tier catches do + /// not pile helix animations on top of each other. + private static boolean claimHelixSlot(Player player) { + long now = System.currentTimeMillis(); + Long previous = LAST_HELIX.get(player.getUniqueId()); + if (previous != null && now - previous < HELIX_COOLDOWN_MS) { + return false; + } + LAST_HELIX.put(player.getUniqueId(), now); + return true; + } + private static void common(Player player, Location at) { player.spawnParticle(Particle.HAPPY_VILLAGER, at, 6, 0.4, 0.3, 0.4, 0); player.playSound(at, Sound.ENTITY_ITEM_PICKUP, 0.6f, 1.0f); From f702e197f4d0908b5433f2265ae6b331a032c040 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Thu, 28 May 2026 15:01:48 -0600 Subject: [PATCH 13/21] cut the duplicate weight-based catch effects Every catch fired both the old weight-based particle/sound block and the new tier helper on top of it. Legendaries were running 100 particles in one tick plus a five-sound chord, which is exactly the noise the helix throttle was meant to spare players from. Drop the legacy spawnRarityParticles/playRaritySounds path and let TierVisualEffects own the tier feedback. The hook still gets a splash + bubble cue and a single bobber-splash sound so the catch moment is still anchored at the water. --- .../paper/fishing/FishingListener.java | 96 +++---------------- 1 file changed, 13 insertions(+), 83 deletions(-) diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java index c214151..df3a5a7 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java @@ -669,16 +669,13 @@ private void spawnCatchEffects(Player player, Location hookLocation, CustomDrop int weight = drop.getWeight(); if (shouldShowParticles(player)) { - spawnRarityParticles(player, effectLocation, weight); - // Tier-coded celebration on top of the legacy weight-based - // effects. Palette follows the loot-tier convention every - // RPG player has learned: white-common, green-uncommon, - // blue-rare, purple-legendary, orange-mythic. + spawnHookSplash(player, effectLocation); io.xcutiboo.mythicrod.paper.util.TierVisualEffects.playCatch(plugin, player, drop.getTier()); } if (plugin.getConfigManager().useSounds()) { - playRaritySounds(player, effectLocation, weight); + player.playSound(effectLocation, Sound.ENTITY_FISHING_BOBBER_SPLASH, + SoundCategory.PLAYERS, 0.8F, 1.0F); } if (debugMode) { @@ -694,83 +691,16 @@ private boolean shouldShowParticles(Player player) { || !plugin.getPlayerDataService().hasReducedEffects(player)); } - private void spawnRarityParticles(Player player, Location hookLocation, int weight) { - spawnParticle(player, hookLocation, plugin.getConfigManager().getCatchParticle(), Particle.SPLASH, 30, 0.3D, 0.15D); - spawnParticle(player, hookLocation.clone().add(0.0D, 0.3D, 0.0D), plugin.getConfigManager().getBubbleParticle(), Particle.BUBBLE_POP, 15, 0.2D, 0.05D); - - if (weight <= 1) { - Location playerLoc = player.getLocation().add(0.0D, 2.0D, 0.0D); - player.spawnParticle(Particle.TOTEM_OF_UNDYING, playerLoc, 50, 1.0D, 0.5D, 1.0D, 0.3D); - player.spawnParticle(Particle.END_ROD, playerLoc, 30, 0.8D, 0.3D, 0.8D, 0.1D); - player.spawnParticle(Particle.HAPPY_VILLAGER, playerLoc, 20, 0.5D, 0.3D, 0.5D, 0.1D); - } else if (weight <= 5) { - Location playerLoc = player.getLocation().add(0.0D, 1.5D, 0.0D); - player.spawnParticle(Particle.HAPPY_VILLAGER, playerLoc, 15, 0.4D, 0.3D, 0.4D, 0.05D); - player.spawnParticle(Particle.END_ROD, hookLocation, 10, 0.3D, 0.3D, 0.3D, 0.05D); - } else if (weight <= 15) { - spawnParticle(player, player.getLocation().add(0.0D, 1.5D, 0.0D), plugin.getConfigManager().getSuccessParticle(), Particle.HAPPY_VILLAGER, 10, 0.35D, 0.04D); - } else { - spawnParticle(player, player.getLocation().add(0.0D, 1.5D, 0.0D), plugin.getConfigManager().getSuccessParticle(), Particle.HAPPY_VILLAGER, 5, 0.3D, 0.02D); - } - } - - private void playRaritySounds(Player player, Location hookLocation, int weight) { - player.playSound(hookLocation, Sound.ENTITY_FISHING_BOBBER_SPLASH, SoundCategory.PLAYERS, 0.8F, 1.0F); - player.playSound(hookLocation, Sound.ENTITY_FISHING_BOBBER_RETRIEVE, SoundCategory.PLAYERS, 0.6F, 1.1F); - - if (weight <= 1) { - playLegendarySounds(player, hookLocation); - } else if (weight <= 5) { - playRareSounds(player, hookLocation); - } else if (weight <= 15) { - playUncommonSounds(player, hookLocation); - } - } - - private void playLegendarySounds(Player player, Location hookLocation) { - player.playSound(hookLocation, Sound.BLOCK_BEACON_POWER_SELECT, SoundCategory.PLAYERS, 0.7F, 1.5F); - player.playSound(hookLocation, Sound.ENTITY_ENDER_DRAGON_GROWL, SoundCategory.PLAYERS, 0.3F, 1.8F); - - plugin.getPlatformScheduler().runForPlayerDelayed( - new PaperPlayer(player), - () -> { - if (player.isOnline()) { - player.playSound(player, Sound.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.PLAYERS, 0.8F, 1.0F); - player.playSound(player, Sound.ENTITY_PLAYER_LEVELUP, SoundCategory.PLAYERS, 0.6F, 1.2F); - player.playSound(player, Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.PLAYERS, 0.5F, 2.0F); - } - }, - 5L - ); - } - - private void playRareSounds(Player player, Location hookLocation) { - player.playSound(hookLocation, Sound.BLOCK_NOTE_BLOCK_PLING, SoundCategory.PLAYERS, 0.6F, 2.0F); - - plugin.getPlatformScheduler().runForPlayerDelayed( - new PaperPlayer(player), - () -> { - if (player.isOnline()) { - player.playSound(player, Sound.ENTITY_PLAYER_LEVELUP, SoundCategory.PLAYERS, 0.5F, 1.5F); - player.playSound(player, Sound.BLOCK_NOTE_BLOCK_BELL, SoundCategory.PLAYERS, 0.4F, 1.5F); - } - }, - 4L - ); - } - - private void playUncommonSounds(Player player, Location hookLocation) { - player.playSound(hookLocation, Sound.BLOCK_NOTE_BLOCK_PLING, SoundCategory.PLAYERS, 0.4F, 1.8F); - - plugin.getPlatformScheduler().runForPlayerDelayed( - new PaperPlayer(player), - () -> { - if (player.isOnline()) { - player.playSound(player, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.PLAYERS, 0.4F, 1.2F); - } - }, - 3L - ); + /// Always-on splash + bubble cue at the bobber location. The + /// tier-specific feedback (color, helix, tier sound) is owned by + /// TierVisualEffects so the two cues never compete. + private void spawnHookSplash(Player player, Location hookLocation) { + spawnParticle(player, hookLocation, + plugin.getConfigManager().getCatchParticle(), Particle.SPLASH, + 30, 0.3D, 0.15D); + spawnParticle(player, hookLocation.clone().add(0.0D, 0.3D, 0.0D), + plugin.getConfigManager().getBubbleParticle(), Particle.BUBBLE_POP, + 15, 0.2D, 0.05D); } private void spawnParticle(Player player, Location location, String particleName, Particle fallback, int count, double offset, double extra) { From 612e935557e60a1478594f8de6f616d45e61c643 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Thu, 28 May 2026 17:46:30 -0600 Subject: [PATCH 14/21] feat(api): preview eligible drops by player + biome Add MythicRodAPI.previewEligibleDrops(UUID, biomeKey) so external plugins can ask 'what would this player roll here' without actually firing a catch. Returns an immutable list of PlatformDrops after biome and permission filters. Useful for minigame tutorial overlays, fishing-spot HUDs, and 'where should I cast' UIs. The eligibility logic is the same one the catch path already uses; this just exposes it cleanly. Marked @ApiStatus.AvailableSince("2026.2.0"). Documented in docs/developer-api/examples.md with a Paper-side sample. --- docs/developer-api/examples.md | 34 +++++++++++++++++++ .../xcutiboo/mythicrod/api/MythicRodAPI.java | 26 ++++++++++++++ .../paper/api/PaperMythicRodAPI.java | 17 ++++++++++ 3 files changed, 77 insertions(+) diff --git a/docs/developer-api/examples.md b/docs/developer-api/examples.md index 77f5aca..ef7ccf4 100644 --- a/docs/developer-api/examples.md +++ b/docs/developer-api/examples.md @@ -120,4 +120,38 @@ public Optional rodTier(ItemStack rod) { } ``` +## Minigame: "what could I catch here?" preview + +`previewEligibleDrops(UUID, biomeKey)` returns the drop table after +biome and permission filters. Use it in tutorial overlays, fishing +spot UIs, or scoreboard tickers without running an actual catch. + +```java +import io.xcutiboo.mythicrod.api.MythicRodAPI; +import io.xcutiboo.mythicrod.api.platform.PlatformDrop; +import io.xcutiboo.mythicrod.paper.api.MythicRodServices; +import org.bukkit.entity.Player; + +public void showPreview(Player player) { + String biomeKey = player.getWorld() + .getBiome(player.getLocation()) + .getKey() + .toString(); + + MythicRodServices.find().ifPresent(api -> { + var eligible = api.previewEligibleDrops(player.getUniqueId(), biomeKey); + int total = eligible.stream().mapToInt(PlatformDrop::getWeight).sum(); + eligible.forEach(drop -> { + double share = total == 0 ? 0.0 : (drop.getWeight() * 100.0 / total); + player.sendMessage("§7- " + drop.getIdentifier() + + " §8(" + String.format("%.1f", share) + "%§8)"); + }); + }); +} +``` + +Pass `null` as the biome to ignore biome filters. The list is an +immutable snapshot; reloads do not retroactively change a returned +list. Available since `2026.2.0`. + [← Developer API](../developer-api.md) diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index 18222ba..8b59b12 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -10,9 +10,12 @@ import io.xcutiboo.mythicrod.api.PlayerStatSnapshot.StatType; import io.xcutiboo.mythicrod.api.drop.DropCatalog; +import io.xcutiboo.mythicrod.api.platform.PlatformDrop; import io.xcutiboo.mythicrod.api.platform.PlatformItem; import io.xcutiboo.mythicrod.api.platform.PlatformItemFactory; +import org.jetbrains.annotations.Nullable; + /// Public integration contract for MythicRod. /// /// ## Thread Safety @@ -145,6 +148,29 @@ default Result createItem(@NotNull String identifier, int amount) int limit ); + /// Returns the drops the given online player would be eligible to roll at + /// the given biome, after biome filters and permission filters are applied. + /// + /// Returns an empty list when the player is offline or no drops are + /// eligible. The returned list is an immutable snapshot of the current + /// drop table - subsequent reloads do not retroactively change it. + /// + /// Intended for minigame UIs, tutorial overlays, and "what could I catch + /// here?" inspections. The actual roll is still resolved at catch time and + /// can be biased or replaced by `MythicRodRewardRollEvent`. + /// + /// @param playerId UUID of the player to check eligibility for. Must + /// belong to a currently-online player. + /// @param biomeKey Biome key such as `"minecraft:ocean"`, or `null` to + /// ignore biome filters. + /// @return immutable list of eligible drops. Empty when the player is + /// offline or has no eligible drops. + @ApiStatus.AvailableSince("2026.2.0") + @NotNull + List previewEligibleDrops( + @NotNull UUID playerId, + @Nullable String biomeKey); + /// Flushes all in-memory player statistics to persistent storage. /// /// This is called automatically on plugin shutdown but may be invoked diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java index c0e2c25..0d4fc49 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java @@ -18,14 +18,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + import io.xcutiboo.mythicrod.api.ExternalDropProvider; import io.xcutiboo.mythicrod.api.MythicRodAPI; import io.xcutiboo.mythicrod.api.PlayerStatSnapshot; import io.xcutiboo.mythicrod.api.drop.DropCatalog; +import io.xcutiboo.mythicrod.api.platform.PlatformDrop; import io.xcutiboo.mythicrod.api.platform.PlatformItem; import io.xcutiboo.mythicrod.api.platform.PlatformItemFactory; import io.xcutiboo.mythicrod.api.platform.PlatformPlayer; import io.xcutiboo.mythicrod.api.platform.PlatformScheduler; +import io.xcutiboo.mythicrod.paper.platform.PaperPlayer; import io.xcutiboo.mythicrod.drops.CustomDrop; import io.xcutiboo.mythicrod.drops.DropConfigurationRecord; import io.xcutiboo.mythicrod.drops.DropManager; @@ -138,6 +143,18 @@ public List getExternalDropProviders() { return List.copyOf(externalProviders.values()); } + @Override + @NotNull + public List previewEligibleDrops( + @NotNull UUID playerId, + @Nullable String biomeKey) { + Player bukkitPlayer = Bukkit.getPlayer(playerId); + if (bukkitPlayer == null) { + return List.of(); + } + return List.copyOf(dropManager.getEligibleDrops(new PaperPlayer(bukkitPlayer), biomeKey)); + } + public double getBaseRewardWeight(@NotNull PlatformPlayer player, @Nullable String biomeName) { double totalWeight = 0.0D; From e72d63c043cfb768baa1ae33e65ce8fbade2a1ac Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Fri, 29 May 2026 10:33:55 -0600 Subject: [PATCH 15/21] import the FQNs we kept inlining Six call sites still spelled out java.util.Locale, java.util.logging.Level, org.bukkit.Location, org.bukkit.Server, org.bukkit.World, and org.bukkit.event.inventory.InventoryClickEvent in the middle of code that already has all the room in the world for one more import line. Pulled them up so a reader scanning the body does not get a wall of dotted package paths in their eyeline. --- .../io/xcutiboo/mythicrod/paper/gui/menus/ConfigMenu.java | 5 +++-- .../io/xcutiboo/mythicrod/paper/gui/menus/MainHubMenu.java | 6 ++++-- .../java/io/xcutiboo/mythicrod/paper/item/ItemBuilder.java | 3 ++- .../java/io/xcutiboo/mythicrod/paper/item/RodFactory.java | 3 ++- .../io/xcutiboo/mythicrod/paper/platform/PaperLocation.java | 6 ++++-- .../io/xcutiboo/mythicrod/paper/platform/PaperWorld.java | 3 ++- .../mythicrod/paper/scheduler/FoliaSchedulerService.java | 3 ++- 7 files changed, 19 insertions(+), 10 deletions(-) diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/ConfigMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/ConfigMenu.java index 61a2ff2..edfe2d4 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/ConfigMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/ConfigMenu.java @@ -7,6 +7,7 @@ import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.ItemStack; import io.xcutiboo.mythicrod.paper.MythicRod; @@ -158,7 +159,7 @@ private void placeParticleSettings() { }); } - private void cycleParticleForEvent(org.bukkit.event.inventory.InventoryClickEvent event) { + private void cycleParticleForEvent(InventoryClickEvent event) { boolean shift = event.isShiftClick(); boolean right = event.isRightClick(); if (shift && !right) { @@ -281,7 +282,7 @@ private String intervalCadenceLine(int seconds) { return tr("gui.config.save_interval.infrequent"); } - private int nextStatsInterval(int current, org.bukkit.event.inventory.InventoryClickEvent event) { + private int nextStatsInterval(int current, InventoryClickEvent event) { int change = event.isShiftClick() ? 300 : 60; if (event.isLeftClick()) return Math.min(3600, current + change); if (event.isRightClick()) return Math.max(60, current - change); diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/MainHubMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/MainHubMenu.java index 97eb711..5153a23 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/MainHubMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/MainHubMenu.java @@ -1,9 +1,11 @@ package io.xcutiboo.mythicrod.paper.gui.menus; import java.util.Map; +import java.util.logging.Level; import org.bukkit.Material; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.ItemStack; import io.xcutiboo.mythicrod.paper.MythicRod; @@ -147,7 +149,7 @@ private void placeReloadButton() { setItem(33, item, this::onReloadClick); } - private void onReloadClick(org.bukkit.event.inventory.InventoryClickEvent event) { + private void onReloadClick(InventoryClickEvent event) { if (!event.isShiftClick()) { playErrorSound(); sendMessage(tr("gui.main.reload_confirm")); @@ -168,7 +170,7 @@ private void onReloadClick(org.bukkit.event.inventory.InventoryClickEvent event) } catch (Exception e) { playErrorSound(); sendMessage(tr("gui.main.reload_failed")); - plugin.getLogger().log(java.util.logging.Level.SEVERE, "Error reloading from GUI", e); + plugin.getLogger().log(Level.SEVERE, "Error reloading from GUI", e); } } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/ItemBuilder.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/ItemBuilder.java index 5f7919d..bc92637 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/ItemBuilder.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/ItemBuilder.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import org.bukkit.Material; @@ -137,7 +138,7 @@ private void applyEnchantmentEntry(ItemEnchantments.Builder builder, Registry registry, Map.Entry entry) { if (entry.getKey() == null || entry.getValue() == null) return; - String enchantName = entry.getKey().toLowerCase(java.util.Locale.ROOT); + String enchantName = entry.getKey().toLowerCase(Locale.ROOT); NamespacedKey key = enchantName.contains(":") ? NamespacedKey.fromString(enchantName) : NamespacedKey.minecraft(enchantName); diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java index adc36f1..ae24408 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java @@ -1,6 +1,7 @@ package io.xcutiboo.mythicrod.paper.item; import java.util.Arrays; +import java.util.Locale; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -121,6 +122,6 @@ private String formatMultiplier(String tier) { double multiplier = plugin.getConfigManager() != null ? plugin.getConfigManager().getRodLuckMultiplier(tier) : 1.0D; - return String.format(java.util.Locale.ROOT, "%.2f", multiplier); + return String.format(Locale.ROOT, "%.2f", multiplier); } } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperLocation.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperLocation.java index 1d2df55..2756d32 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperLocation.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperLocation.java @@ -1,6 +1,8 @@ package io.xcutiboo.mythicrod.paper.platform; import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; import io.xcutiboo.mythicrod.api.platform.PlatformLocation; @@ -24,11 +26,11 @@ public static PlatformLocation fromBukkit(Location location) { ); } - public static Location toBukkit(PlatformLocation platformLocation, org.bukkit.Server server) { + public static Location toBukkit(PlatformLocation platformLocation, Server server) { if (platformLocation == null) { return null; } - org.bukkit.World world = server.getWorld(platformLocation.getWorldName()); + World world = server.getWorld(platformLocation.getWorldName()); if (world == null) { return null; } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperWorld.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperWorld.java index 3f43f1c..10d5074 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperWorld.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/platform/PaperWorld.java @@ -1,5 +1,6 @@ package io.xcutiboo.mythicrod.paper.platform; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.inventory.ItemStack; @@ -23,7 +24,7 @@ public String getName() { public void dropItem(PlatformLocation location, PlatformItem item) { if (location == null || item == null) return; - org.bukkit.Location bukkitLoc = new org.bukkit.Location( + Location bukkitLoc = new Location( world, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch() diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java index 3d88bdc..9b0621f 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java @@ -6,6 +6,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -58,7 +59,7 @@ public void cancelPluginTasks() { try { task.cancel(); } catch (RuntimeException e) { - plugin.getLogger().log(java.util.logging.Level.WARNING, + plugin.getLogger().log(Level.WARNING, "Failed to cancel a scheduled MythicRod task", e); } } From 54c80eeaba91c933a0b2c8cb56b3e0fcda71e9d9 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Fri, 29 May 2026 13:21:17 -0600 Subject: [PATCH 16/21] feat(api): createRod(tier) helper * import the FQNs we kept inlining Six call sites still spelled out java.util.Locale, java.util.logging.Level, org.bukkit.Location, org.bukkit.Server, org.bukkit.World, and org.bukkit.event.inventory.InventoryClickEvent in the middle of code that already has all the room in the world for one more import line. Pulled them up so a reader scanning the body does not get a wall of dotted package paths in their eyeline. * feat(api): createRod(tier) helper One-liner for handing out a MythicRod rod from your plugin: pick basic, advanced, or legendary and you get the fully-tagged rod back as a PlatformItem. Display name, lore, glow, and unbreakable flag come from the same presets the /mythicrod give command uses, so a rod from this method is indistinguishable from one a player earned in-game. Saves callers from writing the PDC keys by hand and from tracking whether legendary should glow or be unbreakable. The legacy manual path still works and is documented as a fallback for when the preset does not fit. Marked AvailableSince 2026.2.0. Test exercises the unknown-tier failure path; the success paths run through RodFactory which is already covered. --- docs/developer-api/rods.md | 25 ++++++++--- .../xcutiboo/mythicrod/api/MythicRodAPI.java | 18 ++++++++ .../xcutiboo/mythicrod/paper/MythicRod.java | 4 +- .../paper/api/PaperMythicRodAPI.java | 24 +++++++++- .../api/PaperMythicRodAPICreateRodTest.java | 45 +++++++++++++++++++ 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java diff --git a/docs/developer-api/rods.md b/docs/developer-api/rods.md index 73a3741..9a17535 100644 --- a/docs/developer-api/rods.md +++ b/docs/developer-api/rods.md @@ -13,9 +13,23 @@ cannot rename a vanilla rod into a MythicRod rod. ## Creating a rod from your plugin -If you want to hand out a MythicRod-compatible rod from your own code, -use the API's item factory instead of building an `ItemStack` by hand. -That route keeps your rod compatible with future internal changes: +The cleanest path is `MythicRodAPI.createRod(tier)`. It returns the +fully-tagged rod with display name, lore, glow, and unbreakable flag +matching MythicRod's built-in presets. Available since `2026.2.0`. + +```java +MythicRodAPI api = MythicRodServices.require(); +PlatformItem rod = api.createRod("advanced").orElseThrow(); +player.getInventory().addItem(((PaperPlatformItem) rod).getItemStack()); +``` + +Valid tiers (case-insensitive): `basic`, `advanced`, `legendary`. An +unknown tier returns a `Result.failure` rather than throwing. + +### Manual rod creation (legacy path) + +If you need to override the preset, build the rod through the item +factory and write the PDC keys yourself: ```java MythicRodAPI api = MythicRodServices.require(); @@ -29,9 +43,8 @@ meta.getPersistentDataContainer().set(rodTier, PersistentDataType.STRING, "advan rod.setItemMeta(meta); ``` -This is rare. Most integrations should hand off to MythicRod's own -`/mythicrod give` flow or use an `ExternalDropProvider` for tier-aware -rewards. +Reach for the manual path only when the built-in preset does not fit; +otherwise prefer `createRod(tier)`. ## Detecting a MythicRod rod diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index 8b59b12..24d4098 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -148,6 +148,24 @@ default Result createItem(@NotNull String identifier, int amount) int limit ); + /// Creates a MythicRod fishing rod of the given tier, fully tagged with + /// MythicRod's PDC marker and tier metadata so the catch listener and + /// statistics pipeline pick it up. + /// + /// Valid tier names are `"basic"`, `"advanced"`, and `"legendary"`, matched + /// case-insensitively. The rod's display name, lore, glow, and unbreakable + /// flag follow MythicRod's built-in presets for that tier. + /// + /// Prefer this over building an `ItemStack` and writing PDC keys by hand: + /// future internal changes to the rod marker stay invisible to the caller. + /// + /// @param tier `"basic"`, `"advanced"`, or `"legendary"` (case-insensitive). + /// @return success result with the tagged rod, or failure when the tier is + /// unknown. + @ApiStatus.AvailableSince("2026.2.0") + @NotNull + Result createRod(@NotNull String tier); + /// Returns the drops the given online player would be eligible to roll at /// the given biome, after biome filters and permission filters are applied. /// diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/MythicRod.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/MythicRod.java index 48b3caf..cc311c8 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/MythicRod.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/MythicRod.java @@ -22,6 +22,7 @@ import io.xcutiboo.mythicrod.drops.DropManager; import io.xcutiboo.mythicrod.internal.runtime.MythicRodRuntime; import io.xcutiboo.mythicrod.metrics.StatisticsManager; +import io.xcutiboo.mythicrod.paper.item.RodFactory; import io.xcutiboo.mythicrod.paper.api.PaperMythicRodAPI; import io.xcutiboo.mythicrod.paper.commands.BrigadierCommandManager; import io.xcutiboo.mythicrod.paper.data.PlayerDataService; @@ -167,7 +168,8 @@ private void bootstrapGuiAndApi() { dropManager, statisticsManager, platformScheduler, - platformServer.getItemFactory() + platformServer.getItemFactory(), + new RodFactory(this) ); // Bukkit ServicesManager registration lets third-party plugins resolve // the API without compile-time dependency on our internal classes. diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java index 0d4fc49..134571e 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java @@ -20,16 +20,20 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; import io.xcutiboo.mythicrod.api.ExternalDropProvider; import io.xcutiboo.mythicrod.api.MythicRodAPI; import io.xcutiboo.mythicrod.api.PlayerStatSnapshot; +import io.xcutiboo.mythicrod.api.Result; import io.xcutiboo.mythicrod.api.drop.DropCatalog; import io.xcutiboo.mythicrod.api.platform.PlatformDrop; import io.xcutiboo.mythicrod.api.platform.PlatformItem; import io.xcutiboo.mythicrod.api.platform.PlatformItemFactory; import io.xcutiboo.mythicrod.api.platform.PlatformPlayer; import io.xcutiboo.mythicrod.api.platform.PlatformScheduler; +import io.xcutiboo.mythicrod.paper.item.RodFactory; +import io.xcutiboo.mythicrod.paper.platform.PaperItem; import io.xcutiboo.mythicrod.paper.platform.PaperPlayer; import io.xcutiboo.mythicrod.drops.CustomDrop; import io.xcutiboo.mythicrod.drops.DropConfigurationRecord; @@ -67,6 +71,7 @@ public class PaperMythicRodAPI implements MythicRodAPI { private final StatisticsManager statisticsManager; private final PlatformScheduler scheduler; private final PlatformItemFactory itemFactory; + private final RodFactory rodFactory; private final ConcurrentHashMap externalProviders = new ConcurrentHashMap<>(); @@ -77,13 +82,15 @@ public PaperMythicRodAPI( @NotNull DropManager dropManager, @NotNull StatisticsManager statisticsManager, @NotNull PlatformScheduler scheduler, - @NotNull PlatformItemFactory itemFactory) { + @NotNull PlatformItemFactory itemFactory, + @NotNull RodFactory rodFactory) { this.version = version; this.logger = logger; this.dropManager = dropManager; this.statisticsManager = statisticsManager; this.scheduler = scheduler; this.itemFactory = itemFactory; + this.rodFactory = rodFactory; } /// Internal reward-selection result used by the Paper fishing pipeline. @@ -143,6 +150,21 @@ public List getExternalDropProviders() { return List.copyOf(externalProviders.values()); } + @Override + @NotNull + public Result createRod(@NotNull String tier) { + ItemStack rod = switch (tier.toLowerCase(Locale.ROOT)) { + case "basic" -> rodFactory.createBasicRod(); + case "advanced" -> rodFactory.createAdvancedRod(); + case "legendary" -> rodFactory.createLegendaryRod(); + default -> null; + }; + if (rod == null) { + return Result.failure("Unknown rod tier '" + tier + "'. Valid tiers: basic, advanced, legendary."); + } + return Result.success(new PaperItem(rod)); + } + @Override @NotNull public List previewEligibleDrops( diff --git a/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java new file mode 100644 index 0000000..d65bc26 --- /dev/null +++ b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java @@ -0,0 +1,45 @@ +package io.xcutiboo.mythicrod.paper.api; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import io.xcutiboo.mythicrod.api.Result; +import io.xcutiboo.mythicrod.api.platform.PlatformItem; + +class PaperMythicRodAPICreateRodTest { + + @Test + void unknownTierReturnsFailureBeforeTouchingFactory() { + PaperMythicRodAPI api = new PaperMythicRodAPI( + "test-version", + java.util.logging.Logger.getAnonymousLogger(), + null, null, null, null, null + ); + + Result result = api.createRod("god-tier"); + + assertNotNull(result); + assertFalse(result.isSuccess()); + assertTrue(result.getError().contains("Unknown rod tier")); + assertTrue(result.getError().contains("basic")); + assertTrue(result.getError().contains("advanced")); + assertTrue(result.getError().contains("legendary")); + } + + @Test + void unknownTierIsCaseInsensitiveOnTheReportedInput() { + PaperMythicRodAPI api = new PaperMythicRodAPI( + "test-version", + java.util.logging.Logger.getAnonymousLogger(), + null, null, null, null, null + ); + + Result result = api.createRod("MYTHIC"); + + assertFalse(result.isSuccess()); + assertTrue(result.getError().contains("MYTHIC")); + } +} From 743387dc6cb0d80a03a3d13a040a4866aac1e19c Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Fri, 29 May 2026 16:02:48 -0600 Subject: [PATCH 17/21] more sonar-flagged cleanup * clean up javadocs and pull repeated strings into constants Two changes in one pass since they touch the same set of files: 1. Convert HTML in markdown javadocs to actual markdown. Sonar S7474: the ///-style javadocs that landed on java 23 should use markdown, not the legacy h2/ul/li/pre/code/strong/em tags or the {@code ...} inline. Mechanical conversion across 10 files; behaviour and rendered output unchanged. 2. Pull repeated string literals in BrigadierCommandManager and LanguageSwitchMenu into named constants (Sonar S1192). Most of the duplicates were brigadier argument names (identifier, weight, amount, field, value, biome, locale, status) plus a handful of translation keys. New ARG_* and TR_* constants slot next to the existing KEY_/TIER_/TR_ pattern. Also drop two stale @SuppressWarnings("unused") on the foliaTask helpers in FoliaSchedulerService and FishingListener, since those helpers are very much used. * more sonar-flagged cleanup Smaller pile of mixed findings: - unnamed pattern variables (S7467) on the three catch blocks where the exception is never read - update checker http call, folia fallback path, classloader URL conversion in tests. - the parse() helper in UpdateChecker now returns an empty array instead of null when the version regex misses (S1168), and the caller checks length instead of null. - assertNotNull instead of assertTrue(x != null) in the bundled locale parity test (S5785). - pulled the eligibility check in executeDropsPreview into a small isEligibleForPreview helper. removes the nested continues in the loop body (S135) and makes the loop trivial to read. - previewEligibleDrops keeps the List return type, but the wildcard now has the same suppression and rationale comment as DropCatalog.getDrops (mirrored S1452). - pinned exact versions and forced --only-binary on the mkdocs pip install in pages.yml (S8544/S8541). --- .github/workflows/pages.yml | 10 +- .../xcutiboo/mythicrod/api/MythicRodAPI.java | 3 + .../xcutiboo/mythicrod/api/package-info.java | 4 +- .../mythicrod/constants/PermissionNodes.java | 2 +- .../xcutiboo/mythicrod/drops/DropManager.java | 16 +- .../mythicrod/drops/DropSelector.java | 10 +- .../mythicrod/metrics/StatisticsManager.java | 36 ++-- .../xcutiboo/mythicrod/stats/PlayerStats.java | 2 +- .../paper/api/PaperMythicRodAPI.java | 2 +- .../commands/BrigadierCommandManager.java | 177 ++++++++++-------- .../events/MythicRodStatsUpdateEvent.java | 2 +- .../paper/fishing/FishingListener.java | 1 - .../paper/gui/menus/EditDropMenu.java | 11 +- .../paper/gui/menus/LanguageSwitchMenu.java | 8 +- .../mythicrod/paper/gui/menus/StatsMenu.java | 2 +- .../scheduler/FoliaSchedulerService.java | 2 - .../mythicrod/paper/update/UpdateChecker.java | 8 +- .../config/BundledLocaleParityTest.java | 5 +- 18 files changed, 163 insertions(+), 138 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 819d43e..342be5a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -30,12 +30,14 @@ jobs: python-version: '3.12' - name: Install MkDocs Material + # Pin exact versions and force binary wheels: Sonar S8544/S8541 + # (unpinned dependencies + setup-script execution risk). run: | python -m pip install --upgrade pip - pip install \ - "mkdocs-material>=9.5" \ - "mkdocs-static-i18n>=1.2" \ - "mkdocs-git-revision-date-localized-plugin>=1.2" + pip install --only-binary :all: \ + "mkdocs-material==9.5.45" \ + "mkdocs-static-i18n==1.2.3" \ + "mkdocs-git-revision-date-localized-plugin==1.2.9" - name: Configure Pages uses: actions/configure-pages@v5 diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index 24d4098..ce29758 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -185,6 +185,9 @@ default Result createItem(@NotNull String identifier, int amount) /// offline or has no eligible drops. @ApiStatus.AvailableSince("2026.2.0") @NotNull + @SuppressWarnings("java:S1452") + // The wildcard mirrors DropCatalog#getDrops so MythicRod can return its + // concrete CustomDrop list without forcing a defensive copy on every call. List previewEligibleDrops( @NotNull UUID playerId, @Nullable String biomeKey); diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/package-info.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/package-info.java index 2b808b1..782d649 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/package-info.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/package-info.java @@ -6,7 +6,7 @@ /// {@link io.xcutiboo.mythicrod.api.drop.DropCatalog}. Platform-neutral value /// types live in {@link io.xcutiboo.mythicrod.api.platform}. /// -/// The Paper runtime publishes {@code MythicRodAPI} through Bukkit's -/// {@code ServicesManager}. Future-backed methods complete on MythicRod-owned +/// The Paper runtime publishes `MythicRodAPI` through Bukkit's +/// `ServicesManager`. Future-backed methods complete on MythicRod-owned /// async threads and must be rescheduled before touching platform state. package io.xcutiboo.mythicrod.api; diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java index 01af2f3..51595d5 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java @@ -1,6 +1,6 @@ package io.xcutiboo.mythicrod.constants; -/// Permission node constants mirrored in {@code paper-plugin.yml}. +/// Permission node constants mirrored in `paper-plugin.yml`. public final class PermissionNodes { private PermissionNodes() {} diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropManager.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropManager.java index def7744..75eee56 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropManager.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropManager.java @@ -25,12 +25,12 @@ /// Manages fishing drop tables loaded from configuration. /// -/// Thread safety: The canonical drop table is held in an +/// **Thread safety:** The canonical drop table is held in an /// {@link AtomicReference} so that a reload (which builds a completely new map) /// is published atomically. Readers always see either the old complete map or /// the new complete map, never a partially-populated one. Individual category /// lists use {@link CopyOnWriteArrayList} so that in-place mutations -/// ({@code updateDrop}, {@code deleteDrop}) are safe to perform while concurrent +/// (`updateDrop`, `deleteDrop`) are safe to perform while concurrent /// readers iterate over the list. public class DropManager implements DropCatalog { private static final String KEY_AMOUNT = "amount"; @@ -608,7 +608,7 @@ private int sanitizeAmount(String id, int amount) { /// /// @param player the fishing player (permission + biome checks) /// @param biomeName current biome key string - /// @param luckMultiplier from {@code MythicRodRewardRollEvent}, clamped to at least 0.01 + /// @param luckMultiplier from `MythicRodRewardRollEvent`, clamped to at least 0.01 public CustomDrop getRandomDrop(PlatformPlayer player, String biomeName, double luckMultiplier) { if (player == null || !player.isOnline()) { fine(() -> "Skipping reward roll because player is null or offline"); @@ -743,7 +743,7 @@ public void awaitAsyncPersistenceOperations() { /// Update an existing drop with new properties. /// /// Thread safe: the category list is a {@link CopyOnWriteArrayList}, so - /// {@code set()} is atomic and concurrent readers are unaffected. + /// `set()` is atomic and concurrent readers are unaffected. /// /// @param dropId The drop identifier /// @param category The category name @@ -781,7 +781,7 @@ public void updateDrop(String dropId, String category, int weight, int amount, /// Adds a drop to a category from a {@link EditableDropFields} value object. /// - /// Returns {@code null} when the category or fields are invalid. + /// Returns `null` when the category or fields are invalid. public CustomDrop addDrop(String category, EditableDropFields fields) { if (category == null || category.isBlank() || fields == null) { return null; @@ -837,7 +837,7 @@ public CustomDrop addDrop(String category, EditableDropFields fields) { /// drops with the same material. The GUI passes the live {@link CustomDrop} /// object it opened so only that row is replaced. /// - /// @return {@code true} when the selected drop was still present + /// @return `true` when the selected drop was still present public boolean updateDrop(CustomDrop targetDrop, String category, int weight, int amount, String customName, List lore, boolean glowing) { if (targetDrop == null) return false; @@ -858,7 +858,7 @@ public boolean updateDrop(CustomDrop targetDrop, String category, int weight, in /// Replaces the selected drop instance with the supplied {@link EditableDropFields}. /// - /// @return {@code true} when the target was still present and the new fields validated + /// @return `true` when the target was still present and the new fields validated public boolean updateDrop(CustomDrop targetDrop, String category, EditableDropFields fields) { if (!hasUpdatableArguments(targetDrop, category, fields)) return false; String normalizedIdentifier = normalizeEditedIdentifier(fields.identifier()); @@ -930,7 +930,7 @@ public void deleteDrop(String dropId, String category) { /// Deletes the exact drop instance selected by the GUI. /// - /// @return {@code true} when the selected drop was still present + /// @return `true` when the selected drop was still present public boolean deleteDrop(CustomDrop targetDrop, String category) { if (targetDrop == null || category == null || category.isBlank()) { return false; diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropSelector.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropSelector.java index 8005dff..8cdd9a8 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropSelector.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/drops/DropSelector.java @@ -12,7 +12,7 @@ /// Thread-safe drop selector using weighted random selection. /// /// Thread safety: All methods are stateless with respect to mutable shared data. -/// {@code ThreadLocalRandom.current()} is called per-invocation (never stored as a field) +/// `ThreadLocalRandom.current()` is called per-invocation (never stored as a field) /// to guarantee correct behaviour across Folia region threads. @RequiredArgsConstructor public class DropSelector { @@ -46,7 +46,7 @@ public void setUseBiomeSpecificDrops(boolean use) { /// Selects a drop with luck-modified weights. /// - /// A {@code luckMultiplier} > 1.0 scales up the effective weight of drops + /// A `luckMultiplier` > 1.0 scales up the effective weight of drops /// whose base weight is ≤ 5 (rare / legendary tier), making them relatively /// more probable without touching common or uncommon weights. A multiplier /// of 1.0 reproduces identical behaviour to the no-luck overload. @@ -100,7 +100,7 @@ public CustomDrop selectDrop(List drops, PlatformPlayer player, Stri /// /// @param drops Full drop pool. /// @param player Player being evaluated. - /// @param biomeName Current biome key, or {@code null} if unavailable. + /// @param biomeName Current biome key, or `null` if unavailable. /// @return Immutable snapshot of eligible drops. public List getEligibleDrops(List drops, PlatformPlayer player, String biomeName) { if (drops == null || drops.isEmpty()) { @@ -165,12 +165,12 @@ private boolean matchesBiome(CustomDrop drop, String biomeName) { /// weights. It still builds short-lived arrays for each selection because /// the eligible set depends on player, biome and luck context. /// - /// Thread safety: {@code ThreadLocalRandom.current()} is called per + /// Thread safety: `ThreadLocalRandom.current()` is called per /// invocation so each Folia region thread uses its own random source. /// /// @param drops list of eligible drops /// @param luckMultiplier multiplier applied to rare drop weights - /// @return the selected drop, or {@code null} if all weights are zero + /// @return the selected drop, or `null` if all weights are zero private CustomDrop selectWeightedRandomOptimized(List drops, double luckMultiplier) { final int n = drops.size(); diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/metrics/StatisticsManager.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/metrics/StatisticsManager.java index 3b0d4f7..9844296 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/metrics/StatisticsManager.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/metrics/StatisticsManager.java @@ -32,20 +32,18 @@ /// Manages in-memory player fishing statistics with Caffeine TTL-bounded caching. /// -///

Thread Safety

-///
    -///
  • The active-players cache uses Caffeine and is safe for concurrent access.
  • -///
  • The dirty set uses {@link ConcurrentHashMap} as a set.
  • -///
  • {@link #saveAll()} is intended to be called from the async scheduler.
  • -///
  • {@link #getStats(UUID)} and {@link #recordCatch} may be called from -/// any entity region thread.
  • -///
+/// ## Thread Safety +/// - The active-players cache uses Caffeine and is safe for concurrent access. +/// - The dirty set uses {@link ConcurrentHashMap} as a set. +/// - {@link #saveAll()} is intended to be called from the async scheduler. +/// - {@link #getStats(UUID)} and {@link #recordCatch} may be called from +/// any entity region thread. /// -///

Cache Design

+/// ## Cache Design /// Stats for online players are kept indefinitely while they are active. /// After login/access, entries expire from the cache after /// {@value #EXPIRE_AFTER_ACCESS_MINUTES} minutes of no access. -/// This prevents {@code OutOfMemoryError} from unbounded accumulation. +/// This prevents `OutOfMemoryError` from unbounded accumulation. public final class StatisticsManager { private static final int EXPIRE_AFTER_ACCESS_MINUTES = 30; @@ -106,11 +104,11 @@ public void onRemoval(@Nullable UUID uuid, @Nullable PlayerStats stats, @NotNull } /// Path to the statistics YAML file inside the plugin data folder. - /// All player stats are stored here under {@code players..*}. + /// All player stats are stored here under `players..*`. private static final String STATS_FILENAME = "statistics.yml"; /// Live reference to the statistics YAML configuration. - /// Guarded by {@code this} for write access; readers see either the old + /// Guarded by `this` for write access; readers see either the old /// reference or the new one swapped during reload, never a half-built object. @SuppressWarnings("java:S3077") private volatile PlatformConfiguration statsConfig; @@ -141,7 +139,7 @@ public long getTotalCatches() { return totalCatchesGlobal.get(); } - /// Returns the top {@code limit} players by total catches. + /// Returns the top `limit` players by total catches. /// Used by GUI menus that do not have async context. /// /// @param limit Max entries to return. @@ -173,14 +171,14 @@ public PlayerStats getOrCreate(@NotNull UUID uuid) { }); } - /// Returns the {@link PlayerStats} for the given UUID, or {@code null} + /// Returns the {@link PlayerStats} for the given UUID, or `null` /// if no entry has been created for this player yet. /// /// Unlike {@link #getOrCreate}, this does not create a new entry. /// Used by the API layer for read-only queries. /// /// @param uuid Player UUID. - /// @return Existing stats, or {@code null}. + /// @return Existing stats, or `null`. @Nullable public PlayerStats getStats(@NotNull UUID uuid) { PlayerStats cachedStats = statsCache.getIfPresent(uuid); @@ -238,7 +236,7 @@ public void recordRodUse(@NotNull UUID uuid, @NotNull String rodTier) { /// the next persistence flush wipes the on-disk row. /// /// @param uuid Player UUID. - /// @return {@code true} if a stats entry existed to reset; {@code false} when + /// @return `true` if a stats entry existed to reset; `false` when /// no in-memory or on-disk entry was present. public boolean resetStats(@NotNull UUID uuid) { PlayerStats inMemory = statsCache.getIfPresent(uuid); @@ -332,10 +330,10 @@ public void unloadPlayer(@NotNull UUID uuid) { statsCache.invalidate(uuid); } - /// Persists a single player's stats to the {@code statistics.yml} file. + /// Persists a single player's stats to the `statistics.yml` file. /// /// Must be called from an async thread, never from the main server thread. - /// All writes are guarded by a {@code synchronized} block on {@code this} so + /// All writes are guarded by a `synchronized` block on `this` so /// concurrent saves from the Caffeine eviction listener don't corrupt the file. private boolean persistStats(@NotNull UUID uuid, @NotNull PlayerStats stats) { try { @@ -461,7 +459,7 @@ private Map snapshotPersistedStats() { return persistedStats; } - /// Loads (or creates) the {@code statistics.yml} config from disk. + /// Loads (or creates) the `statistics.yml` config from disk. private @NotNull PlatformConfiguration loadStatsConfig() { File statsFile = new File(runtime.getDataFolder(), STATS_FILENAME); if (!runtime.getDataFolder().exists()) { diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/stats/PlayerStats.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/stats/PlayerStats.java index 3348f73..3954804 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/stats/PlayerStats.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/stats/PlayerStats.java @@ -54,7 +54,7 @@ public PlayerStats(@NotNull UUID playerUuid, @NotNull String playerName) { public int getAdvancedRodUses() { return advancedRodUses.get(); } public int getLegendaryRodUses() { return legendaryRodUses.get(); } - /// Returns the epoch-millisecond timestamp of the last catch, or {@code 0} if never. + /// Returns the epoch-millisecond timestamp of the last catch, or `0` if never. public long getLastFished() { return lastFished.get(); } /// Returns non-zero catch counts by rarity tier, ordered from rarest to common. diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java index 134571e..d46c10b 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java @@ -487,7 +487,7 @@ private int tierToWeight(@Nullable String tier) { } /// Synchronous snapshot for the given player, suitable for event handlers - /// already running on the player's owner thread. Returns {@code null} when + /// already running on the player's owner thread. Returns `null` when /// no in-memory or on-disk entry exists. @Nullable public PlayerStatSnapshot snapshotFor(@NotNull UUID playerId) { diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/commands/BrigadierCommandManager.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/commands/BrigadierCommandManager.java index fb92fd9..0beb4db 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/commands/BrigadierCommandManager.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/commands/BrigadierCommandManager.java @@ -69,6 +69,19 @@ public class BrigadierCommandManager { private static final String KEY_LIMIT = "limit"; private static final String KEY_CATEGORY = "category"; private static final String KEY_DROPS = "drops"; + private static final String ARG_IDENTIFIER = "identifier"; + private static final String ARG_WEIGHT = "weight"; + private static final String ARG_AMOUNT = "amount"; + private static final String ARG_FIELD = "field"; + private static final String ARG_VALUE = "value"; + private static final String ARG_BIOME = "biome"; + private static final String ARG_LOCALE = "locale"; + private static final String ARG_STATUS = "status"; + private static final String ARG_CATEGORIES = "categories"; + private static final String TIER_ADVANCED = "advanced"; + private static final String TR_GENERAL_ENABLED = "general.enabled"; + private static final String TR_GENERAL_DISABLED = "general.disabled"; + private static final String TR_DROP_NOT_FOUND = "command.drop.not-found"; private static final String TR_GENERAL_ERROR = "general.error"; private static final String TR_PLAYER_ONLY = "general.player_only"; private static final String KEY_ERROR = "error"; @@ -187,7 +200,7 @@ private LiteralCommandNode buildMythicRodCommand() { .executes(this::executeDrops) .then(Commands.literal("preview") .requires(source -> source.getSender().hasPermission(PermissionNodes.ADMIN_DEBUG)) - .then(Commands.argument("biome", StringArgumentType.greedyString()) + .then(Commands.argument(ARG_BIOME, StringArgumentType.greedyString()) .suggests(this::suggestBiomes) .executes(this::executeDropsPreview))) .then(Commands.argument(KEY_CATEGORY, StringArgumentType.word()) @@ -198,27 +211,27 @@ private LiteralCommandNode buildMythicRodCommand() { .then(Commands.literal("add") .then(Commands.argument(KEY_CATEGORY, StringArgumentType.word()) .suggests(this::suggestDropCategories) - .then(Commands.argument("identifier", StringArgumentType.string()) - .then(Commands.argument("weight", IntegerArgumentType.integer(1, 10000)) - .then(Commands.argument("amount", IntegerArgumentType.integer(1, 64)) + .then(Commands.argument(ARG_IDENTIFIER, StringArgumentType.string()) + .then(Commands.argument(ARG_WEIGHT, IntegerArgumentType.integer(1, 10000)) + .then(Commands.argument(ARG_AMOUNT, IntegerArgumentType.integer(1, 64)) .executes(this::executeDropAdd)))))) .then(Commands.literal("remove") .then(Commands.argument(KEY_CATEGORY, StringArgumentType.word()) .suggests(this::suggestDropCategories) - .then(Commands.argument("identifier", StringArgumentType.string()) + .then(Commands.argument(ARG_IDENTIFIER, StringArgumentType.string()) .executes(this::executeDropRemove)))) .then(Commands.literal("set") .then(Commands.argument(KEY_CATEGORY, StringArgumentType.word()) .suggests(this::suggestDropCategories) - .then(Commands.argument("identifier", StringArgumentType.string()) - .then(Commands.argument("field", StringArgumentType.word()) + .then(Commands.argument(ARG_IDENTIFIER, StringArgumentType.string()) + .then(Commands.argument(ARG_FIELD, StringArgumentType.word()) .suggests(this::suggestDropFields) - .then(Commands.argument("value", StringArgumentType.greedyString()) + .then(Commands.argument(ARG_VALUE, StringArgumentType.greedyString()) .executes(this::executeDropSet))))))) .then(Commands.literal("debug") .requires(source -> source.getSender().hasPermission(PermissionNodes.ADMIN_DEBUG)) .executes(this::executeDebug)) - .then(Commands.literal("status") + .then(Commands.literal(ARG_STATUS) .requires(source -> source.getSender().hasPermission(PermissionNodes.ADMIN_DEBUG)) .executes(this::executeStatus)) .then(Commands.literal("validate") @@ -227,7 +240,7 @@ private LiteralCommandNode buildMythicRodCommand() { .then(Commands.literal("testroll") .requires(source -> source.getSender().hasPermission(PermissionNodes.ADMIN_DEBUG)) .executes(this::executeTestRoll) - .then(Commands.argument("biome", StringArgumentType.string()) + .then(Commands.argument(ARG_BIOME, StringArgumentType.string()) .suggests(this::suggestBiomes) .executes(this::executeTestRoll) .then(Commands.argument(KEY_COUNT, IntegerArgumentType.integer(1, 10000)) @@ -319,7 +332,7 @@ private LiteralArgumentBuilder buildConfigSubtree(String lit .suggests(this::suggestRewardDeliveryModes) .executes(this::executeDeliveryModeConfig))) .then(Commands.literal("language") - .then(Commands.argument("locale", StringArgumentType.word()) + .then(Commands.argument(ARG_LOCALE, StringArgumentType.word()) .suggests(this::suggestAvailableLanguages) .executes(this::executeLanguageConfig))) .then(Commands.literal("stats-save-interval") @@ -419,8 +432,8 @@ private int executeGive(CommandContext context) { private ItemStack buildRodForTier(String tier) { return switch (tier.toLowerCase(Locale.ROOT)) { - case "basic" -> rodFactory.createBasicRod(); - case "advanced" -> rodFactory.createAdvancedRod(); + case TIER_BASIC -> rodFactory.createBasicRod(); + case TIER_ADVANCED -> rodFactory.createAdvancedRod(); case TIER_LEGENDARY -> rodFactory.createLegendaryRod(); default -> null; }; @@ -527,12 +540,12 @@ private void sendConfigLine(CommandSender sender, String settingKey, String valu sendMessage(sender, tr(sender, "command.config.line", Map.of( "setting", tr(sender, settingKey), - "value", value + ARG_VALUE, value ))); } private String configStatus(CommandSender sender, boolean enabled) { - return tr(sender, enabled ? "general.enabled" : "general.disabled"); + return tr(sender, enabled ? TR_GENERAL_ENABLED : TR_GENERAL_DISABLED); } private int executeBooleanConfig( @@ -554,7 +567,7 @@ private int executeBooleanConfig( sendMessage(sender, tr(sender, "command.config.boolean-set", Map.of( "setting", tr(sender, settingKey), - "value", configStatus(sender, newValue) + ARG_VALUE, configStatus(sender, newValue) ))); playSuccessSound(sender); return Command.SINGLE_SUCCESS; @@ -608,13 +621,13 @@ private int executeLanguageConfig(CommandContext context) { CommandSender sender = context.getSource().getSender(); ConfigManager config = plugin.getConfigManager(); String previousLocale = config.getLanguage(); - String requested = StringArgumentType.getString(context, "locale"); + String requested = StringArgumentType.getString(context, ARG_LOCALE); List available = plugin.getLanguageManager().getAvailableLanguages(); if (!available.contains(requested) || !config.setLanguage(requested)) { sendMessage(sender, tr(sender, "command.config.invalid-language", Map.of( - "locale", requested, + ARG_LOCALE, requested, "available", String.join(", ", available) ))); playErrorSound(sender); @@ -631,13 +644,13 @@ private int executeLanguageConfig(CommandContext context) { plugin.getGUIManager().invalidateOpenMenusForReload(); } sendMessage(sender, tr(sender, "command.config.language-set", - Map.of("locale", requested))); + Map.of(ARG_LOCALE, requested))); playSuccessSound(sender); return Command.SINGLE_SUCCESS; } catch (IOException | RuntimeException e) { config.setLanguage(previousLocale); - sendMessage(sender, tr(sender, "command.config.save-failed", - Map.of("error", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()))); + sendMessage(sender, tr(sender, TR_CONFIG_SAVE_FAILED, + Map.of(KEY_ERROR, e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()))); playErrorSound(sender); plugin.getLogger().log(Level.SEVERE, "Failed to update language", e); return 0; @@ -1073,7 +1086,7 @@ private void sendUnknownDropCategory(CommandSender sender, String category) { sendMessage(sender, tr(sender, "drops.category-not-found", Map.of(KEY_CATEGORY, category))); sendMessage(sender, tr(sender, "drops.available-categories", - Map.of("categories", String.join(", ", sortedDropCategoryIds())))); + Map.of(ARG_CATEGORIES, String.join(", ", sortedDropCategoryIds())))); sendMessage(sender, tr(sender, "drops.category-help")); } @@ -1095,8 +1108,8 @@ private void displayDropCategory(CommandSender sender, String category) { sendMessage(sender, tr(sender, "drops.drop-entry", Map.of( "name", name, - "weight", weight, - "amount", String.valueOf(drop.getAmount()) + ARG_WEIGHT, weight, + ARG_AMOUNT, String.valueOf(drop.getAmount()) ))); } } @@ -1135,8 +1148,8 @@ private CompletableFuture suggestRodTiers( @SuppressWarnings("unused") CommandContext context, SuggestionsBuilder builder ) { - builder.suggest("basic"); - builder.suggest("advanced"); + builder.suggest(TIER_BASIC); + builder.suggest(TIER_ADVANCED); builder.suggest(TIER_LEGENDARY); return builder.buildFuture(); } @@ -1544,7 +1557,7 @@ private int executeTestRoll(CommandContext context) { Player player = requirePlayer(sender); if (player == null) return 0; - String biomeArg = optionalStringArg(context, "biome"); + String biomeArg = optionalStringArg(context, ARG_BIOME); int count = Math.clamp(optionalIntArg(context, KEY_COUNT, 100), 1, 10000); String biome = resolveTestRollBiome(player, biomeArg); @@ -1604,7 +1617,7 @@ private CompletableFuture suggestDropFields( @SuppressWarnings("unused") CommandContext context, SuggestionsBuilder builder ) { - for (String f : List.of("weight", "amount", "permission", "glow", "name")) { + for (String f : List.of(ARG_WEIGHT, ARG_AMOUNT, "permission", "glow", "name")) { builder.suggest(f); } return builder.buildFuture(); @@ -1614,9 +1627,9 @@ private int executeDropAdd(CommandContext context) { CommandSender sender = context.getSource().getSender(); try { String category = StringArgumentType.getString(context, KEY_CATEGORY).toLowerCase(Locale.ROOT); - String identifier = StringArgumentType.getString(context, "identifier").trim(); - int weight = IntegerArgumentType.getInteger(context, "weight"); - int amount = IntegerArgumentType.getInteger(context, "amount"); + String identifier = StringArgumentType.getString(context, ARG_IDENTIFIER).trim(); + int weight = IntegerArgumentType.getInteger(context, ARG_WEIGHT); + int amount = IntegerArgumentType.getInteger(context, ARG_AMOUNT); io.xcutiboo.mythicrod.drops.EditableDropFields fields = new io.xcutiboo.mythicrod.drops.EditableDropFields( @@ -1626,13 +1639,13 @@ private int executeDropAdd(CommandContext context) { CustomDrop drop = plugin.getDropManager().addDrop(category, fields); if (drop == null) { sendMessage(sender, tr(sender, "command.drop.invalid", - Map.of("identifier", identifier))); + Map.of(ARG_IDENTIFIER, identifier))); playErrorSound(sender); return 0; } plugin.getDropManager().saveDrops(); sendMessage(sender, tr(sender, "command.drop.added", - Map.of("identifier", identifier, "category", category))); + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playSuccessSound(sender); return Command.SINGLE_SUCCESS; } catch (RuntimeException e) { @@ -1647,24 +1660,24 @@ private int executeDropRemove(CommandContext context) { CommandSender sender = context.getSource().getSender(); try { String category = StringArgumentType.getString(context, KEY_CATEGORY).toLowerCase(Locale.ROOT); - String identifier = StringArgumentType.getString(context, "identifier").trim(); + String identifier = StringArgumentType.getString(context, ARG_IDENTIFIER).trim(); CustomDrop target = findDropInCategory(category, identifier); if (target == null) { - sendMessage(sender, tr(sender, "command.drop.not-found", - Map.of("identifier", identifier, "category", category))); + sendMessage(sender, tr(sender, TR_DROP_NOT_FOUND, + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playErrorSound(sender); return 0; } boolean removed = plugin.getDropManager().deleteDrop(target, category); if (!removed) { - sendMessage(sender, tr(sender, "command.drop.not-found", - Map.of("identifier", identifier, "category", category))); + sendMessage(sender, tr(sender, TR_DROP_NOT_FOUND, + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playErrorSound(sender); return 0; } plugin.getDropManager().saveDrops(); sendMessage(sender, tr(sender, "command.drop.removed", - Map.of("identifier", identifier, "category", category))); + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playSuccessSound(sender); return Command.SINGLE_SUCCESS; } catch (RuntimeException e) { @@ -1679,14 +1692,14 @@ private int executeDropSet(CommandContext context) { CommandSender sender = context.getSource().getSender(); try { String category = StringArgumentType.getString(context, KEY_CATEGORY).toLowerCase(Locale.ROOT); - String identifier = StringArgumentType.getString(context, "identifier").trim(); - String field = StringArgumentType.getString(context, "field").toLowerCase(Locale.ROOT); - String value = StringArgumentType.getString(context, "value"); + String identifier = StringArgumentType.getString(context, ARG_IDENTIFIER).trim(); + String field = StringArgumentType.getString(context, ARG_FIELD).toLowerCase(Locale.ROOT); + String value = StringArgumentType.getString(context, ARG_VALUE); CustomDrop target = findDropInCategory(category, identifier); if (target == null) { - sendMessage(sender, tr(sender, "command.drop.not-found", - Map.of("identifier", identifier, "category", category))); + sendMessage(sender, tr(sender, TR_DROP_NOT_FOUND, + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playErrorSound(sender); return 0; } @@ -1699,21 +1712,21 @@ private int executeDropSet(CommandContext context) { try { switch (field) { - case "weight" -> weight = Integer.parseInt(value.trim()); - case "amount" -> amount = Integer.parseInt(value.trim()); + case ARG_WEIGHT -> weight = Integer.parseInt(value.trim()); + case ARG_AMOUNT -> amount = Integer.parseInt(value.trim()); case "name" -> customName = value; case "permission" -> permission = value.isBlank() ? null : value.trim(); case "glow" -> glow = Boolean.parseBoolean(value.trim()); default -> { sendMessage(sender, tr(sender, "command.drop.unknown-field", - Map.of("field", field))); + Map.of(ARG_FIELD, field))); playErrorSound(sender); return 0; } } } catch (NumberFormatException _) { sendMessage(sender, tr(sender, "command.drop.bad-value", - Map.of("field", field, "value", value))); + Map.of(ARG_FIELD, field, ARG_VALUE, value))); playErrorSound(sender); return 0; } @@ -1727,14 +1740,14 @@ private int executeDropSet(CommandContext context) { ); boolean updated = plugin.getDropManager().updateDrop(target, category, next); if (!updated) { - sendMessage(sender, tr(sender, "command.drop.not-found", - Map.of("identifier", identifier, "category", category))); + sendMessage(sender, tr(sender, TR_DROP_NOT_FOUND, + Map.of(ARG_IDENTIFIER, identifier, KEY_CATEGORY, category))); playErrorSound(sender); return 0; } plugin.getDropManager().saveDrops(); sendMessage(sender, tr(sender, "command.drop.updated", - Map.of("identifier", identifier, "field", field, "value", value))); + Map.of(ARG_IDENTIFIER, identifier, ARG_FIELD, field, ARG_VALUE, value))); playSuccessSound(sender); return Command.SINGLE_SUCCESS; } catch (RuntimeException e) { @@ -1762,7 +1775,7 @@ private int executeRodSelect(CommandContext context) { String tier = StringArgumentType.getString(context, "tier").toLowerCase(Locale.ROOT); String permission = switch (tier) { case TIER_BASIC -> PermissionNodes.GUI; - case "advanced" -> PermissionNodes.ROD_ADVANCED; + case TIER_ADVANCED -> PermissionNodes.ROD_ADVANCED; case TIER_LEGENDARY -> PermissionNodes.ROD_LEGENDARY; default -> null; }; @@ -1904,19 +1917,19 @@ private int executeEffectsSet(CommandContext context) { private int executeDropsPreview(CommandContext context) { CommandSender sender = context.getSource().getSender(); try { - String biome = StringArgumentType.getString(context, "biome").trim(); + String biome = StringArgumentType.getString(context, ARG_BIOME).trim(); NamespacedKey biomeKey = parseRegistryKey(biome); if (biomeKey == null || RegistryAccess.registryAccess().getRegistry(RegistryKey.BIOME).get(biomeKey) == null) { sendMessage(sender, tr(sender, "command.drops-preview.invalid-biome", - Map.of("biome", biome))); + Map.of(ARG_BIOME, biome))); playErrorSound(sender); return 0; } String biomeId = biomeKey.asString(); sendMessage(sender, tr(sender, "command.drops-preview.header", - Map.of("biome", biomeId))); + Map.of(ARG_BIOME, biomeId))); int rows = 0; long totalWeight = 0L; @@ -1930,18 +1943,10 @@ private int executeDropsPreview(CommandContext context) { && plugin.getConfigManager().usePermissions(); for (Map.Entry> entry : categories.entrySet()) { for (CustomDrop drop : entry.getValue()) { - List biomes = drop.getBiomes(); - if (biomes != null && !biomes.isEmpty() && !biomes.contains(biomeId)) { - continue; + if (isEligibleForPreview(sender, drop, biomeId, filterByPermission)) { + eligible.add(new EligibleDrop(entry.getKey(), drop)); + totalWeight += Math.max(0, drop.getWeight()); } - if (filterByPermission) { - String required = drop.getPermission(); - if (required != null && !required.isEmpty() && !sender.hasPermission(required)) { - continue; - } - } - eligible.add(new EligibleDrop(entry.getKey(), drop)); - totalWeight += Math.max(0, drop.getWeight()); } } eligible.sort(Comparator.comparingInt((EligibleDrop e) -> e.drop().getWeight()).reversed()); @@ -1949,15 +1954,15 @@ private int executeDropsPreview(CommandContext context) { for (EligibleDrop entry : eligible) { if (rows++ >= 12) { sendMessage(sender, tr(sender, "command.drops-preview.truncated", - Map.of("count", String.valueOf(eligible.size() - 12)))); + Map.of(KEY_COUNT, String.valueOf(eligible.size() - 12)))); break; } double share = totalWeight == 0 ? 0.0 : (entry.drop().getWeight() * 100.0 / totalWeight); sendMessage(sender, tr(sender, "command.drops-preview.row", Map.of( - "category", entry.category(), - "identifier", entry.drop().getIdentifier(), - "weight", String.valueOf(entry.drop().getWeight()), + KEY_CATEGORY, entry.category(), + ARG_IDENTIFIER, entry.drop().getIdentifier(), + ARG_WEIGHT, String.valueOf(entry.drop().getWeight()), "share", String.format(Locale.ROOT, "%.1f", share) ))); } @@ -1966,8 +1971,8 @@ private int executeDropsPreview(CommandContext context) { } else { sendMessage(sender, tr(sender, "command.drops-preview.footer", Map.of( - "count", String.valueOf(eligible.size()), - "weight", String.valueOf(totalWeight)))); + KEY_COUNT, String.valueOf(eligible.size()), + ARG_WEIGHT, String.valueOf(totalWeight)))); } playSuccessSound(sender); return Command.SINGLE_SUCCESS; @@ -1981,6 +1986,24 @@ private int executeDropsPreview(CommandContext context) { private record EligibleDrop(String category, CustomDrop drop) {} + private static boolean isEligibleForPreview( + CommandSender sender, + CustomDrop drop, + String biomeId, + boolean filterByPermission) { + List biomes = drop.getBiomes(); + if (biomes != null && !biomes.isEmpty() && !biomes.contains(biomeId)) { + return false; + } + if (filterByPermission) { + String required = drop.getPermission(); + if (required != null && !required.isEmpty() && !sender.hasPermission(required)) { + return false; + } + } + return true; + } + private int executeStatus(CommandContext context) { CommandSender sender = context.getSource().getSender(); try { @@ -2015,15 +2038,15 @@ private int executeStatus(CommandContext context) { Map.of("mode", mode, "minecraft", paperVersion))); sendMessage(sender, tr(sender, "command.status.drops", Map.of( - "categories", String.valueOf(categoryCount), - "drops", String.valueOf(dropCount)))); + ARG_CATEGORIES, String.valueOf(categoryCount), + KEY_DROPS, String.valueOf(dropCount)))); sendMessage(sender, tr(sender, "command.status.language", Map.of( "active", activeLocale, "loaded", String.valueOf(locales.size()), "list", String.join(", ", locales)))); sendMessage(sender, tr(sender, "command.status.nexo", - Map.of("status", tr(sender, nexoOn ? "general.enabled" : "general.disabled")))); + Map.of(ARG_STATUS, tr(sender, nexoOn ? TR_GENERAL_ENABLED : TR_GENERAL_DISABLED)))); sendMessage(sender, tr(sender, "command.status.stats", Map.of("players", String.valueOf(trackedPlayers)))); return Command.SINGLE_SUCCESS; @@ -2055,13 +2078,13 @@ private int executeDebug(CommandContext context) { : 0L; sendMessage(sender, tr(sender, "command.debug.runtime", Map.of( - "categories", String.valueOf(categoryCount), + ARG_CATEGORIES, String.valueOf(categoryCount), KEY_DROPS, String.valueOf(dropCount), "players", String.valueOf(trackedPlayers), "catches", String.valueOf(totalCatches) ))); sendMessage(sender, tr(sender, "command.debug.folia-support", - Map.of("status", tr(sender, plugin.isFoliaRuntime() ? "general.enabled" : "general.disabled")))); + Map.of(ARG_STATUS, tr(sender, plugin.isFoliaRuntime() ? TR_GENERAL_ENABLED : TR_GENERAL_DISABLED)))); return Command.SINGLE_SUCCESS; } catch (Exception e) { diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java index 6d27871..fb43c51 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/events/MythicRodStatsUpdateEvent.java @@ -47,7 +47,7 @@ public UUID getPlayerId() { return playerId; } - /// Rarity tier that was incremented: one of {@code common}, {@code uncommon}, {@code rare}, {@code legendary}. + /// Rarity tier that was incremented: one of `common`, `uncommon`, `rare`, `legendary`. @NotNull public String getTier() { return tier; diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java index df3a5a7..66ebc17 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java @@ -413,7 +413,6 @@ private void removeCaughtItemThen(Player player, Item caughtItem, Runnable conti } } - @SuppressWarnings("unused") private Consumer foliaTask(Runnable action) { return task -> action.run(); } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/EditDropMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/EditDropMenu.java index 6e98a7c..795dca6 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/EditDropMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/EditDropMenu.java @@ -35,16 +35,15 @@ /// GUI menu for editing individual drop properties in-game. /// /// Context keys (set via {@link #setContext(Map)} before {@link #open()}): -///
    -///
  • {@code "drop"} - {@link CustomDrop} to edit (required)
  • -///
  • {@code "category"} - {@link String} category name (required)
  • -///
+/// - `"drop"` - {@link CustomDrop} to edit (required) +/// - `"category"` - {@link String} category name (required) /// /// Usage: -///
{@code
+///
+/// ```java
 /// plugin.getGUIManager().openMenu(player, "editdrop",
 ///     Map.of("drop", drop, "category", category));
-/// }
+/// ``` public class EditDropMenu extends BaseMenu { private static final String CTX_AMOUNT = "amount"; diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/LanguageSwitchMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/LanguageSwitchMenu.java index 02f0266..4d7e840 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/LanguageSwitchMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/LanguageSwitchMenu.java @@ -30,6 +30,8 @@ public class LanguageSwitchMenu extends BaseMenu { 10, 11, 12, 14, 15, 16, 19, 20, 21, 23, 24, 25 }; + private static final String TR_LANG_PREFIX = "gui.language.languages."; + public LanguageSwitchMenu(MythicRod plugin, Player player) { super(plugin, player); } @@ -132,7 +134,7 @@ private String textureFor(String locale) { } private Component resolveDisplayName(String locale) { - String key = "gui.language.languages." + curatedKey(locale) + ".name"; + String key = TR_LANG_PREFIX + curatedKey(locale) + ".name"; String value = plugin.getLanguageManager().tr(key); if (value.equals(key)) { return Component.text(locale.toUpperCase(Locale.ROOT)); @@ -141,7 +143,7 @@ private Component resolveDisplayName(String locale) { } private Component resolveDescription(String locale) { - String key = "gui.language.languages." + curatedKey(locale) + ".description"; + String key = TR_LANG_PREFIX + curatedKey(locale) + ".description"; String value = plugin.getLanguageManager().tr(key); if (value.equals(key)) { return Component.text(plugin.getLanguageManager().tr( @@ -152,7 +154,7 @@ private Component resolveDescription(String locale) { } private Component resolveRegion(String locale) { - String key = "gui.language.languages." + curatedKey(locale) + ".region"; + String key = TR_LANG_PREFIX + curatedKey(locale) + ".region"; String value = plugin.getLanguageManager().tr(key); if (value.equals(key)) { return Component.text(plugin.getLanguageManager().tr( diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/StatsMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/StatsMenu.java index 629d72f..fe82d4d 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/StatsMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/StatsMenu.java @@ -22,7 +22,7 @@ /// for personal stats and falls back to a temporary zeroed view for first-time players, /// while using /// {@link io.xcutiboo.mythicrod.metrics.StatisticsManager#getTopFishers(int)} -/// for the leaderboard (returns {@code List} sorted by totalCaught). +/// for the leaderboard (returns `List` sorted by totalCaught). public class StatsMenu extends BaseMenu { private static final String CTX_COUNT = "count"; diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java index 9b0621f..d07acb2 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/scheduler/FoliaSchedulerService.java @@ -268,12 +268,10 @@ private PaperTask track(Object nativeTask, AtomicReference taskRef) { return platformTask; } - @SuppressWarnings("unused") private Consumer foliaTask(Runnable task) { return scheduledTask -> task.run(); } - @SuppressWarnings("unused") private Consumer foliaOneShot(AtomicReference taskRef, Runnable task) { return scheduledTask -> runOneShot(taskRef, task); } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/update/UpdateChecker.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/update/UpdateChecker.java index 884c4c2..be59a90 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/update/UpdateChecker.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/update/UpdateChecker.java @@ -84,7 +84,7 @@ private void runCheck() { } else { log.debug("Update check: GitHub returned HTTP {} for the latest-release endpoint.", resp.statusCode()); } - } catch (InterruptedException e) { + } catch (InterruptedException _) { Thread.currentThread().interrupt(); } catch (RuntimeException | IOException e) { log.debug("Update check failed: {}", e.getMessage()); @@ -97,7 +97,7 @@ private boolean isNewerThanCurrent(String latest) { } int[] cur = parse(currentVersion); int[] rem = parse(latest); - if (cur == null || rem == null) { + if (cur.length == 0 || rem.length == 0) { return !latest.equals(currentVersion); } for (int i = 0; i < cur.length; i++) { @@ -109,7 +109,7 @@ private boolean isNewerThanCurrent(String latest) { private static int[] parse(String version) { Matcher m = VERSION.matcher(version); - if (!m.find()) return null; + if (!m.find()) return new int[0]; return new int[] { Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)), @@ -128,7 +128,7 @@ void schedule() { () -> { if (!cancelled) runCheck(); }, INITIAL_DELAY_SECONDS * 20L, REPEAT_INTERVAL_SECONDS * 20L); - } catch (UnsupportedOperationException folia) { + } catch (UnsupportedOperationException _) { // Folia does not have a global Bukkit scheduler; fall back to the async scheduler. plugin.getServer().getAsyncScheduler().runAtFixedRate( plugin, diff --git a/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/internal/config/BundledLocaleParityTest.java b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/internal/config/BundledLocaleParityTest.java index c22c2e1..f4a145e 100644 --- a/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/internal/config/BundledLocaleParityTest.java +++ b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/internal/config/BundledLocaleParityTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /// Compile-time guard that every bundled locale matches `en_US.yml` in @@ -87,7 +88,7 @@ void placeholderTokensAreIdenticalAcrossLocales() { private static YamlConfiguration loadBundledLocale(String locale) { URL url = BundledLocaleParityTest.class.getClassLoader() .getResource("lang/" + locale + ".yml"); - assertTrue(url != null, () -> "bundled locale not on test classpath: " + locale); + assertNotNull(url, () -> "bundled locale not on test classpath: " + locale); Path path = toPath(url); return YamlConfiguration.loadConfiguration(path.toFile()); } @@ -95,7 +96,7 @@ private static YamlConfiguration loadBundledLocale(String locale) { private static Path toPath(URL url) { try { return Paths.get(url.toURI()); - } catch (Exception exception) { + } catch (Exception _) { return new File(url.getFile()).toPath(); } } From 8d787fd80664bf5224dccafb94bd53baee7c3a91 Mon Sep 17 00:00:00 2001 From: Milk <39153526+xcutiboo@users.noreply.github.com> Date: Fri, 29 May 2026 19:47:30 -0600 Subject: [PATCH 18/21] feat: add mythic rod tier above legendary Top of the loot ladder, gated by mythicrod.rod.mythic. Mirrors the legendary preset (unbreakable, glow, multi-line lore) but with a magenta-gold gradient name to read as 'one step above legendary' rather than another flavor of it. - New tier wired through: RodFactory.createMythicRod, the createRod(tier) API switch, FishingListener.isAllowedRodTier, RodMenu (slot 16, with the visuals toggle moved to row-3 center so the four tier buttons sit evenly across row 2), ConfigManager luck multiplier (default 2.0). - Permission node mythicrod.rod.mythic declared in paper-plugin.yml and indexed in PermissionNodes. - config.yml gains rods.luck-multipliers.mythic with the same modest default and a one-line note about gating. - en_US and ja_JP picked up the new gui.rod.mythic.* keys plus the extended invalid-tier list. - docs: developer-api/rods.md and permissions.md mention the new tier; rods.md notes the createRod tier set. - Test updated: the bulk-failure path now also expects 'mythic' in the error message, and the case-sensitivity test moved to a truly unknown tier name. --- docs/developer-api/rods.md | 7 +++--- docs/permissions.md | 1 + .../xcutiboo/mythicrod/api/MythicRodAPI.java | 10 ++++---- .../mythicrod/config/ConfigManager.java | 4 ++++ .../mythicrod/constants/PermissionNodes.java | 1 + .../src/main/resources/config.yml | 4 ++++ .../paper/api/PaperMythicRodAPI.java | 4 +++- .../paper/fishing/FishingListener.java | 2 ++ .../mythicrod/paper/gui/menus/RodMenu.java | 24 ++++++++++++++++++- .../mythicrod/paper/item/RodFactory.java | 19 +++++++++++++++ .../src/main/resources/lang/en_US.yml | 13 +++++++++- .../src/main/resources/lang/ja_JP.yml | 13 +++++++++- .../src/main/resources/paper-plugin.yml | 5 ++++ .../api/PaperMythicRodAPICreateRodTest.java | 7 +++--- 14 files changed, 100 insertions(+), 14 deletions(-) diff --git a/docs/developer-api/rods.md b/docs/developer-api/rods.md index 9a17535..95b9156 100644 --- a/docs/developer-api/rods.md +++ b/docs/developer-api/rods.md @@ -6,7 +6,7 @@ PersistentDataContainer keys: | Key | Type | Meaning | | --- | --- | --- | | `mythicrod:custom_rod` | byte (1) | This item is a MythicRod rod | -| `mythicrod:rod_tier` | string | One of `basic`, `advanced`, `legendary` | +| `mythicrod:rod_tier` | string | One of `basic`, `advanced`, `legendary`, `mythic` | The display name or lore is never used as identity. A survival player cannot rename a vanilla rod into a MythicRod rod. @@ -23,8 +23,9 @@ PlatformItem rod = api.createRod("advanced").orElseThrow(); player.getInventory().addItem(((PaperPlatformItem) rod).getItemStack()); ``` -Valid tiers (case-insensitive): `basic`, `advanced`, `legendary`. An -unknown tier returns a `Result.failure` rather than throwing. +Valid tiers (case-insensitive): `basic`, `advanced`, `legendary`, +`mythic`. An unknown tier returns a `Result.failure` rather than +throwing. ### Manual rod creation (legacy path) diff --git a/docs/permissions.md b/docs/permissions.md index 9acbe5d..3f324c2 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -15,6 +15,7 @@ | `mythicrod.drops.legendary` | `op` | Receive `legendary` drops | | `mythicrod.rod.advanced` | `op` | Use the advanced rod tier | | `mythicrod.rod.legendary` | `op` | Use the legendary rod tier | +| `mythicrod.rod.mythic` | `op` | Use the mythic rod tier (top of the loot ladder) | | `mythicrod.admin.reload` | `op` | Reload runtime data | | `mythicrod.admin.give` | `op` | Give MythicRod items | | `mythicrod.admin.config` | `op` | Edit drops, config, run `stats reset` | diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index ce29758..d5511f0 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -152,14 +152,16 @@ default Result createItem(@NotNull String identifier, int amount) /// MythicRod's PDC marker and tier metadata so the catch listener and /// statistics pipeline pick it up. /// - /// Valid tier names are `"basic"`, `"advanced"`, and `"legendary"`, matched - /// case-insensitively. The rod's display name, lore, glow, and unbreakable - /// flag follow MythicRod's built-in presets for that tier. + /// Valid tier names are `"basic"`, `"advanced"`, `"legendary"`, and + /// `"mythic"`, matched case-insensitively. The rod's display name, lore, + /// glow, and unbreakable flag follow MythicRod's built-in presets for that + /// tier. /// /// Prefer this over building an `ItemStack` and writing PDC keys by hand: /// future internal changes to the rod marker stay invisible to the caller. /// - /// @param tier `"basic"`, `"advanced"`, or `"legendary"` (case-insensitive). + /// @param tier `"basic"`, `"advanced"`, `"legendary"`, or `"mythic"` + /// (case-insensitive). /// @return success result with the tagged rod, or failure when the tier is /// unknown. @ApiStatus.AvailableSince("2026.2.0") diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/config/ConfigManager.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/config/ConfigManager.java index 0419d65..19b2c28 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/config/ConfigManager.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/config/ConfigManager.java @@ -50,6 +50,7 @@ public class ConfigManager { private double basicRodLuckMultiplier = 1.0D; private double advancedRodLuckMultiplier = 1.25D; private double legendaryRodLuckMultiplier = 1.5D; + private double mythicRodLuckMultiplier = 2.0D; private int statsSaveInterval = 600; private String language = DEFAULT_LANGUAGE; private String profile = DEFAULT_PROFILE; @@ -110,6 +111,7 @@ private void validateAndCache() { basicRodLuckMultiplier = resolveRodLuckMultiplier("basic", 1.0D); advancedRodLuckMultiplier = resolveRodLuckMultiplier("advanced", 1.25D); legendaryRodLuckMultiplier = resolveRodLuckMultiplier("legendary", 1.5D); + mythicRodLuckMultiplier = resolveRodLuckMultiplier("mythic", 2.0D); prefix = config.getString("ui.prefix", DEFAULT_PREFIX); if (prefix == null || prefix.isEmpty()) { @@ -198,6 +200,7 @@ private void resetToDefaults() { basicRodLuckMultiplier = 1.0D; advancedRodLuckMultiplier = 1.25D; legendaryRodLuckMultiplier = 1.5D; + mythicRodLuckMultiplier = 2.0D; language = DEFAULT_LANGUAGE; profile = DEFAULT_PROFILE; statsSaveInterval = 600; @@ -288,6 +291,7 @@ public double getRodLuckMultiplier(String tier) { return switch (tier.trim().toLowerCase(Locale.ROOT)) { case "advanced" -> advancedRodLuckMultiplier; case "legendary" -> legendaryRodLuckMultiplier; + case "mythic" -> mythicRodLuckMultiplier; default -> basicRodLuckMultiplier; }; } diff --git a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java index 51595d5..b5aaeda 100644 --- a/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java +++ b/mythicrod-common/src/main/java/io/xcutiboo/mythicrod/constants/PermissionNodes.java @@ -24,4 +24,5 @@ private PermissionNodes() {} public static final String ROD_ADVANCED = "mythicrod.rod.advanced"; public static final String ROD_LEGENDARY = "mythicrod.rod.legendary"; + public static final String ROD_MYTHIC = "mythicrod.rod.mythic"; } diff --git a/mythicrod-common/src/main/resources/config.yml b/mythicrod-common/src/main/resources/config.yml index 0de4026..dec7bac 100644 --- a/mythicrod-common/src/main/resources/config.yml +++ b/mythicrod-common/src/main/resources/config.yml @@ -83,6 +83,10 @@ features: basic: 1.0 advanced: 1.25 legendary: 1.5 + # Mythic rod tier - top-of-tier reward. Gate behind a permission node + # (mythicrod.rod.mythic) so it stays a sink for prestige progression + # rather than something every player holds. + mythic: 2.0 permissions: # Require specific permissions for certain drops diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java index d46c10b..38757a3 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPI.java @@ -157,10 +157,12 @@ public Result createRod(@NotNull String tier) { case "basic" -> rodFactory.createBasicRod(); case "advanced" -> rodFactory.createAdvancedRod(); case "legendary" -> rodFactory.createLegendaryRod(); + case "mythic" -> rodFactory.createMythicRod(); default -> null; }; if (rod == null) { - return Result.failure("Unknown rod tier '" + tier + "'. Valid tiers: basic, advanced, legendary."); + return Result.failure("Unknown rod tier '" + tier + + "'. Valid tiers: basic, advanced, legendary, mythic."); } return Result.success(new PaperItem(rod)); } diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java index 66ebc17..ea73102 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/fishing/FishingListener.java @@ -58,6 +58,7 @@ public class FishingListener implements Listener { private static final String TIER_BASIC = "basic"; private static final String TIER_ADVANCED = "advanced"; private static final String TIER_LEGENDARY = "legendary"; + private static final String TIER_MYTHIC = "mythic"; private final MythicRod plugin; private final RodFactory rodFactory; @@ -203,6 +204,7 @@ private boolean isAllowedRodTier(Player player, String tier) { return switch (normalizeRodTier(tier)) { case TIER_ADVANCED -> player != null && player.hasPermission(PermissionNodes.ROD_ADVANCED); case TIER_LEGENDARY -> player != null && player.hasPermission(PermissionNodes.ROD_LEGENDARY); + case TIER_MYTHIC -> player != null && player.hasPermission(PermissionNodes.ROD_MYTHIC); case TIER_BASIC -> true; default -> false; }; diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/RodMenu.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/RodMenu.java index e87cbb0..020337c 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/RodMenu.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/gui/menus/RodMenu.java @@ -15,6 +15,7 @@ public class RodMenu extends BaseMenu { private static final String TIER_BASIC = "basic"; private static final String TIER_ADVANCED = "advanced"; private static final String TIER_LEGENDARY = "legendary"; + private static final String TIER_MYTHIC = "mythic"; private static final String CTX_MULTIPLIER = "multiplier"; private static final String TR_MULTIPLIER = "gui.rod.multiplier"; private static final String TR_ALREADY_SELECTED = "gui.rod.already_selected"; @@ -46,6 +47,7 @@ protected void build() { placeBasicTier(player, currentTier); placeAdvancedTier(player, currentTier); placeLegendaryTier(player, currentTier); + placeMythicTier(player, currentTier); placeVisualEffectsToggle(player, globalEffectsEnabled, reducedEffects); placeBackButton(player); placeCloseButton(player); @@ -121,6 +123,26 @@ private void placeLegendaryTier(Player player, String currentTier) { "gui.rod.legendary.label", "gui.rod.legendary.selected", "gui.rod.legendary.locked")); } + private void placeMythicTier(Player player, String currentTier) { + ItemStack mythicRod = new ItemBuilder(Material.FISHING_ROD) + .name(tr("gui.rod.mythic.name")) + .glow(currentTier.equals(TIER_MYTHIC)) + .lore( + tr("gui.rod.mythic.lore1"), + "", + tr("gui.rod.mythic.lore2"), + tr(TR_MULTIPLIER, Map.of(CTX_MULTIPLIER, formatMultiplier(TIER_MYTHIC))), + tr("gui.rod.mythic.lore3"), + tr("gui.rod.mythic.lore4"), + "", + currentTier.equals(TIER_MYTHIC) ? tr("gui.rod.mythic.equipped") : tr("gui.rod.mythic.click") + ) + .build(); + setItem(16, mythicRod, () -> selectGatedTier( + player, currentTier, TIER_MYTHIC, PermissionNodes.ROD_MYTHIC, + "gui.rod.mythic.label", "gui.rod.mythic.selected", "gui.rod.mythic.locked")); + } + private void selectGatedTier(Player player, String currentTier, String targetTier, String permission, String labelKey, String selectedKey, String lockedKey) { if (currentTier.equals(targetTier)) { @@ -153,7 +175,7 @@ private void placeVisualEffectsToggle(Player player, boolean globalEffectsEnable ) .glow(globalEffectsEnabled && !reducedEffects) .build(); - setItem(16, visualEffects, () -> { + setItem(22, visualEffects, () -> { playClickSound(); if (!globalEffectsEnabled) { playErrorSound(); diff --git a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java index ae24408..fa419f6 100644 --- a/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java +++ b/mythicrod-paper/src/main/java/io/xcutiboo/mythicrod/paper/item/RodFactory.java @@ -77,6 +77,25 @@ public ItemStack createLegendaryRod() { ); } + public ItemStack createMythicRod() { + return createRod( + "mythic", + "MythicRod", + new String[]{ + "Prestige rod. Gate behind a", + "permission node, reward grinders.", + "", + "Tier: Mythic", + "✦✦✦✦ " + formatMultiplier("mythic") + LORE_RARE_LUCK_SUFFIX, + "✦✦✦✦ Requires mythic rod access", + "✦✦✦✦ Top of the loot ladder", + "✦✦✦✦ Unbreakable" + }, + true, + true + ); + } + /// Creates a rod item and stores the MythicRod marker plus tier in its PDC. public ItemStack createRod(String tier, String name, String[] lore) { return createRod(tier, name, lore, false, false); diff --git a/mythicrod-paper/src/main/resources/lang/en_US.yml b/mythicrod-paper/src/main/resources/lang/en_US.yml index 3bb53f8..a12029e 100644 --- a/mythicrod-paper/src/main/resources/lang/en_US.yml +++ b/mythicrod-paper/src/main/resources/lang/en_US.yml @@ -51,7 +51,7 @@ command: locked: 'You lack permission for tier %tier%.' give: tier-missing: 'Tier cannot be empty.' - invalid-tier: 'Invalid tier %tier%. Use basic, advanced, or legendary.' + invalid-tier: 'Invalid tier %tier%. Use basic, advanced, legendary, or mythic.' rod-creation-failed: 'Failed to create the requested MythicRod.' target-offline: 'Player %player% went offline.' inventory-full: 'Player %player% has no free inventory slot for this MythicRod.' @@ -760,6 +760,17 @@ gui: click: '▶ Click to make this your default' selected: '✓ Default fishing tier set to Legendary. Cast with a vanilla rod to use it.' locked: '✗ You need permission to use the Legendary tier.' + mythic: + label: 'Mythic' + name: 'Mythic Rod' + lore1: 'Prestige tier - top of the loot ladder' + lore2: 'Active when you cast with a vanilla rod' + lore3: 'Requires mythicrod.rod.mythic' + lore4: 'Gate this for endgame grinders only' + equipped: '✓ Your default tier' + click: '▶ Click to make this your default' + selected: '✓ Default fishing tier set to Mythic. Cast with a vanilla rod to use it.' + locked: '✗ You need permission to use the Mythic tier.' effects: name: 'Visual Effects' lore1: 'Controls personal particles for' diff --git a/mythicrod-paper/src/main/resources/lang/ja_JP.yml b/mythicrod-paper/src/main/resources/lang/ja_JP.yml index e8e246d..cb0bbc3 100644 --- a/mythicrod-paper/src/main/resources/lang/ja_JP.yml +++ b/mythicrod-paper/src/main/resources/lang/ja_JP.yml @@ -51,7 +51,7 @@ command: locked: 'ティア %tier% を使用する権限がありません。' give: tier-missing: 'ティアを空にはできません。' - invalid-tier: '無効なティア %tier% です。basicadvancedlegendary を使用してください。' + invalid-tier: '無効なティア %tier% です。basicadvancedlegendarymythic を使用してください。' rod-creation-failed: '指定された MythicRod を作成できませんでした。' target-offline: 'プレイヤー %player% はオフラインになりました。' inventory-full: 'プレイヤー %player% のインベントリに MythicRod を受け取る空き枠がありません。' @@ -758,6 +758,17 @@ gui: click: '▶ クリックしてデフォルトに設定' selected: 'デフォルトの釣りティアを レジェンダリー に設定しました。通常のロッドで釣ると適用されます。' locked: 'レジェンダリーティアを使う権限がありません。' + mythic: + label: 'ミシック' + name: 'ミシックロッド' + lore1: 'プレステージティア - 最上位の釣果' + lore2: '通常のロッドでキャストするときに有効' + lore3: 'mythicrod.rod.mythic が必要' + lore4: 'エンドゲーム勢向けにゲートしてください' + equipped: '✓ デフォルトティア' + click: '▶ クリックしてデフォルトに設定' + selected: 'デフォルトの釣りティアを ミシック に設定しました。通常のロッドで釣ると適用されます。' + locked: 'ミシックティアを使う権限がありません。' effects: name: '表示演出' lore1: 'メニューと釣果演出の' diff --git a/mythicrod-paper/src/main/resources/paper-plugin.yml b/mythicrod-paper/src/main/resources/paper-plugin.yml index 8fd5132..f49d0e9 100644 --- a/mythicrod-paper/src/main/resources/paper-plugin.yml +++ b/mythicrod-paper/src/main/resources/paper-plugin.yml @@ -116,6 +116,7 @@ permissions: children: mythicrod.rod.advanced: true mythicrod.rod.legendary: true + mythicrod.rod.mythic: true mythicrod.rod.advanced: description: Use advanced rod tier @@ -124,3 +125,7 @@ permissions: mythicrod.rod.legendary: description: Use legendary rod tier default: op + + mythicrod.rod.mythic: + description: Use mythic rod tier (top of the loot ladder) + default: op diff --git a/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java index d65bc26..a5cba9e 100644 --- a/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java +++ b/mythicrod-paper/src/test/java/io/xcutiboo/mythicrod/paper/api/PaperMythicRodAPICreateRodTest.java @@ -27,19 +27,20 @@ void unknownTierReturnsFailureBeforeTouchingFactory() { assertTrue(result.getError().contains("basic")); assertTrue(result.getError().contains("advanced")); assertTrue(result.getError().contains("legendary")); + assertTrue(result.getError().contains("mythic")); } @Test - void unknownTierIsCaseInsensitiveOnTheReportedInput() { + void unknownTierEchoesTheOriginalCasingInTheErrorMessage() { PaperMythicRodAPI api = new PaperMythicRodAPI( "test-version", java.util.logging.Logger.getAnonymousLogger(), null, null, null, null, null ); - Result result = api.createRod("MYTHIC"); + Result result = api.createRod("Diamond-Tier"); assertFalse(result.isSuccess()); - assertTrue(result.getError().contains("MYTHIC")); + assertTrue(result.getError().contains("Diamond-Tier")); } } From 1ba8f3adaa078cd6d5feaa197c3c6b5d4cf50191 Mon Sep 17 00:00:00 2001 From: xcutoboo Date: Fri, 29 May 2026 22:14:21 -0600 Subject: [PATCH 19/21] release 2026.2.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ gradle.properties | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 52a29fa..33bf58b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2026.1.0" + ".": "2026.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c48da8..709f9e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,38 @@ Project history starts here. Earlier prototype builds are gone, so this file is just the timeline going forward. +## 2026.2.0 + +API surface additions for plugin builders, plus a fourth rod tier and +the usual round of code-health cleanup. + +- **New API: `MythicRodAPI.createRod(tier)`** — one-call rod creation for + external plugins. Returns a fully-tagged `PlatformItem` (display name, + lore, glow, unbreakable flag from the built-in presets). Replaces the + manual `NamespacedKey` + PDC dance from earlier docs. Valid tiers: + `basic`, `advanced`, `legendary`, `mythic`. +- **New API: `MythicRodAPI.previewEligibleDrops(UUID, biomeKey)`** — show + a player what they would be eligible to roll at a given biome without + firing a catch. Useful for minigame UIs, tutorial overlays, + fishing-spot HUDs. Returns an immutable snapshot filtered by biome and + permission. +- **New mythic rod tier** — gated behind `mythicrod.rod.mythic`, default + luck multiplier 2.0. Sits above legendary as the prestige tier. + Configurable in `config.yml` under `features.rods.luck-multipliers`. +- **Visual polish** — per-player throttle on the legendary/mythic helix + animation (one helix per player per 4 seconds), removed the duplicate + weight-based catch-effects block that was double-firing alongside the + tier helper, hook bobber splash + bubble cue trimmed to one tier + particle plus one sound. +- **Code health** — extracted repeated string literals into named + constants in `BrigadierCommandManager`, converted HTML in markdown + javadocs to actual markdown (Java 23 `///` style), imported six + inline fully-qualified names, removed three stale + `@SuppressWarnings`, pinned pip versions and forced + `--only-binary` on the pages workflow. +- **Docs** — new `rods.md` createRod section, new minigame example in + `examples.md`, mythic tier added to the permissions table. + ## 2026.1.0 Fresh start. The plugin was rebuilt from scratch as a multi-module project diff --git a/gradle.properties b/gradle.properties index 0d0a40b..40b3266 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ # x-release-please-version -version=2026.1.0 +version=2026.2.0 paperVersion=26.1.2 hangarProjectId=MythicRod From 78b263754767c0ae6b0347d08a01a3461069a9a8 Mon Sep 17 00:00:00 2001 From: xcutoboo Date: Fri, 29 May 2026 22:30:55 -0600 Subject: [PATCH 20/21] stay on 2026.1.0 - no minor bump yet reverting the version bump to 2026.2.0. the new api methods (createrod, previeweligibledrops) and the mythic rod tier all land inside 2026.1.0 since the plugin has no active users yet - no point in cutting a minor release for changes nobody is running against. - gradle.properties + .release-please-manifest.json back to 2026.1.0 - changelog 2026.2.0 section removed; changes belong with 2026.1.0 - @ApiStatus.AvailableSince markers on createRod and previewEligibleDrops dropped to 2026.1.0 - 'Available since' notes removed from rods.md and examples.md --- .release-please-manifest.json | 2 +- CHANGELOG.md | 32 ------------------- docs/developer-api/examples.md | 2 +- docs/developer-api/rods.md | 2 +- gradle.properties | 2 +- .../xcutiboo/mythicrod/api/MythicRodAPI.java | 4 +-- 6 files changed, 6 insertions(+), 38 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 33bf58b..52a29fa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2026.2.0" + ".": "2026.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 709f9e3..2c48da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,38 +3,6 @@ Project history starts here. Earlier prototype builds are gone, so this file is just the timeline going forward. -## 2026.2.0 - -API surface additions for plugin builders, plus a fourth rod tier and -the usual round of code-health cleanup. - -- **New API: `MythicRodAPI.createRod(tier)`** — one-call rod creation for - external plugins. Returns a fully-tagged `PlatformItem` (display name, - lore, glow, unbreakable flag from the built-in presets). Replaces the - manual `NamespacedKey` + PDC dance from earlier docs. Valid tiers: - `basic`, `advanced`, `legendary`, `mythic`. -- **New API: `MythicRodAPI.previewEligibleDrops(UUID, biomeKey)`** — show - a player what they would be eligible to roll at a given biome without - firing a catch. Useful for minigame UIs, tutorial overlays, - fishing-spot HUDs. Returns an immutable snapshot filtered by biome and - permission. -- **New mythic rod tier** — gated behind `mythicrod.rod.mythic`, default - luck multiplier 2.0. Sits above legendary as the prestige tier. - Configurable in `config.yml` under `features.rods.luck-multipliers`. -- **Visual polish** — per-player throttle on the legendary/mythic helix - animation (one helix per player per 4 seconds), removed the duplicate - weight-based catch-effects block that was double-firing alongside the - tier helper, hook bobber splash + bubble cue trimmed to one tier - particle plus one sound. -- **Code health** — extracted repeated string literals into named - constants in `BrigadierCommandManager`, converted HTML in markdown - javadocs to actual markdown (Java 23 `///` style), imported six - inline fully-qualified names, removed three stale - `@SuppressWarnings`, pinned pip versions and forced - `--only-binary` on the pages workflow. -- **Docs** — new `rods.md` createRod section, new minigame example in - `examples.md`, mythic tier added to the permissions table. - ## 2026.1.0 Fresh start. The plugin was rebuilt from scratch as a multi-module project diff --git a/docs/developer-api/examples.md b/docs/developer-api/examples.md index ef7ccf4..17ca2f1 100644 --- a/docs/developer-api/examples.md +++ b/docs/developer-api/examples.md @@ -152,6 +152,6 @@ public void showPreview(Player player) { Pass `null` as the biome to ignore biome filters. The list is an immutable snapshot; reloads do not retroactively change a returned -list. Available since `2026.2.0`. +list. [← Developer API](../developer-api.md) diff --git a/docs/developer-api/rods.md b/docs/developer-api/rods.md index 95b9156..33ec877 100644 --- a/docs/developer-api/rods.md +++ b/docs/developer-api/rods.md @@ -15,7 +15,7 @@ cannot rename a vanilla rod into a MythicRod rod. The cleanest path is `MythicRodAPI.createRod(tier)`. It returns the fully-tagged rod with display name, lore, glow, and unbreakable flag -matching MythicRod's built-in presets. Available since `2026.2.0`. +matching MythicRod's built-in presets. ```java MythicRodAPI api = MythicRodServices.require(); diff --git a/gradle.properties b/gradle.properties index 40b3266..0d0a40b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ # x-release-please-version -version=2026.2.0 +version=2026.1.0 paperVersion=26.1.2 hangarProjectId=MythicRod diff --git a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java index d5511f0..0b932ca 100644 --- a/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java +++ b/mythicrod-api/src/main/java/io/xcutiboo/mythicrod/api/MythicRodAPI.java @@ -164,7 +164,7 @@ default Result createItem(@NotNull String identifier, int amount) /// (case-insensitive). /// @return success result with the tagged rod, or failure when the tier is /// unknown. - @ApiStatus.AvailableSince("2026.2.0") + @ApiStatus.AvailableSince("2026.1.0") @NotNull Result createRod(@NotNull String tier); @@ -185,7 +185,7 @@ default Result createItem(@NotNull String identifier, int amount) /// ignore biome filters. /// @return immutable list of eligible drops. Empty when the player is /// offline or has no eligible drops. - @ApiStatus.AvailableSince("2026.2.0") + @ApiStatus.AvailableSince("2026.1.0") @NotNull @SuppressWarnings("java:S1452") // The wildcard mirrors DropCatalog#getDrops so MythicRod can return its From c7c4dc45b0c43bf28dde5fed9c6406edec66a268 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 07:15:10 +0000 Subject: [PATCH 21/21] chore(master): release 2026.2.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 52a29fa..33bf58b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2026.1.0" + ".": "2026.2.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c48da8..136f25a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ Project history starts here. Earlier prototype builds are gone, so this file is just the timeline going forward. +## [2026.2.0](https://github.com/xcutiboo/MythicRod/compare/v2026.1.0...v2026.2.0) (2026-05-24) + + +### Features + +* add mythic rod tier above legendary ([8d787fd](https://github.com/xcutiboo/MythicRod/commit/8d787fd80664bf5224dccafb94bd53baee7c3a91)) +* **api:** createRod(tier) helper ([54c80ee](https://github.com/xcutiboo/MythicRod/commit/54c80eeaba91c933a0b2c8cb56b3e0fcda71e9d9)) +* **api:** preview eligible drops by player + biome ([612e935](https://github.com/xcutiboo/MythicRod/commit/612e935557e60a1478594f8de6f616d45e61c643)) + ## 2026.1.0 Fresh start. The plugin was rebuilt from scratch as a multi-module project