Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project

TopBlock is a BentoBox addon that produces a Top Ten ranking for the AOneBlock game mode based on how many magic blocks each island has mined. It is **not** standalone — it depends on the BentoBox plugin and the AOneBlock addon being present at runtime, and refuses to enable otherwise.
TopBlock is a BentoBox addon that produces a Top Ten ranking for the AOneBlock and ChunkBlock game modes based on how many magic blocks each island has mined. It is **not** standalone — it depends on the BentoBox plugin plus at least one of the AOneBlock or ChunkBlock addons being present at runtime, and refuses to enable otherwise. Each hooked game mode gets its own independent top ten, command, and placeholders.

## Build & Test

Maven project, Java 21, Paper 1.21.11 API, BentoBox 3.14.0, AOneBlock 1.18.0.
Maven project, Java 21, Paper 1.21.11 API, BentoBox 3.14.0, AOneBlock 1.18.0, ChunkBlock 1.0.1 (both game modes `provided`; Level is a `test`-only dependency because mocking ChunkBlock requires its hard dependency on the classpath).

- Build (default goal is `clean package`): `mvn package` — produces a shaded jar in `target/` named `TopBlock-<revision><build.number>.jar`. The shade plugin bundles only `lv.id.bonne:panelutils`; everything else is `provided`.
- Run tests: `mvn test`
- Run a single test class: `mvn test -Dtest=TopBlockManagerTest`
- Run a single test method: `mvn test -Dtest=TopBlockManagerTest#testFormatLevelShorthandKilo`
- The Surefire config sets a long list of `--add-opens` JVM flags — required for Mockito + MockBukkit reflection on Java 21; do not remove them when tweaking the build.

Version handling is driven by Maven properties: `build.version` is the human version (currently 1.1.0), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b<num>` on CI, empty on master. Don't hand-edit `<version>` — bump `build.version`.
Version handling is driven by Maven properties: `build.version` is the human version (currently 2.0.0), `revision` resolves to `${build.version}-SNAPSHOT` locally and to `${build.version}` under the `master` profile (activated by `GIT_BRANCH=origin/master` on Jenkins). `build.number` is `-LOCAL` locally, `-b<num>` on CI, empty on master. Don't hand-edit `<version>` — bump `build.version`.

## Runtime entry points (Pladdon pattern)

Expand All @@ -25,21 +25,30 @@ There are **two** main classes and the distinction matters:
- `TopBlockPladdon` (referenced by `plugin.yml`) is the Bukkit-facing `Pladdon`. Spigot loads this; its only job is `getAddon() → new TopBlock()`.
- `TopBlock` (referenced by `addon.yml`) is the BentoBox `Addon`. All real lifecycle (`onLoad`, `onEnable`, `onDisable`) lives here.

`onEnable` looks up the AOneBlock addon via `getPlugin().getAddonsManager().getAddonByName("aoneblock")`; if missing or not a `GameModeAddon`, the addon disables itself. The `/<gamemode> topblock` command is registered against AOneBlock's player command, not as a top-level command.
`onEnable` looks up each supported game mode via `getPlugin().getAddonsManager().getAddonByName(...)` (`"aoneblock"`, `"chunkblock"` — lookup is case-insensitive); for each one present, enabled, and a `GameModeAddon`, it registers a `topblock` subcommand on that game mode's player command and adds a hook (see below). If no game mode hooks, the addon disables itself. `addon.yml` declares `softdepend: AOneBlock, ChunkBlock` (soft, because either alone is enough).

## Game mode hooks

AOneBlock and ChunkBlock have twin APIs (`getBlockListener().getAllIslands()`, `getOneBlockManager().getBlockProbs()`, `OneBlockIslands` data objects) but in unrelated packages, so `world.bentobox.topblock.hooks` abstracts them:

- `TopBlockHook` — interface: `getGameMode()`, `getAllIslandData()` (returns neutral `IslandBlockData` records), `getPhaseCount(blockNumber)`.
- `AOneBlockHook` / `ChunkBlockHook` — the **only** classes allowed to import their game mode's packages. Class loading is lazy, so a missing game mode is never class-loaded as long as its hook is only instantiated after the presence check in `onEnable`. Keep it that way: never import `world.bentobox.aoneblock.*` or `world.bentobox.chunkblock.*` anywhere else in main code.

`TopBlock.getHooks()` lists active hooks; `TopBlock.getHook(World)` resolves the hook owning a world via `GameModeAddon.inWorld`.

## Data flow

`TopBlockManager` is a `Listener` that reacts to `BentoBoxReadyEvent` (handler is `public void onBentoBoxReady` — Bukkit silently skips private @EventHandler methods, which is what broke the addon historically) to start a repeating Bukkit task. The task period is `settings.getRefreshTime() * 20L * 60` ticks (minutes → ticks). Each tick of the task:

1. Calls `AOneBlock.getBlockListener().getAllIslands()` — this reads every island, so the refresh interval is intentionally coarse (default 5 min, min 1 min).
2. Builds a fresh `List<TopTenData>` (record of island + blockNumber + lifetime + phaseName) — sorted at read time via `Comparator` on `lifetime` then `blockNumber`.
3. Updates `PlaceholderManager`'s cached snapshot.
1. Calls `refreshAll()` — for every hook, reads every island of that game mode via `hook.getAllIslandData()`, so the refresh interval is intentionally coarse (default 5 min, min 1 min).
2. Builds a fresh `List<TopTenData>` (record of island + blockNumber + lifetime + phaseName) per hook, kept in a `Map<TopBlockHook, List<TopTenData>>` — sorted at read time via `Comparator` on `lifetime` then `blockNumber`.
3. Updates `PlaceholderManager`'s cached per-hook snapshots.

Placeholders are registered once via a `runTaskLater` 10-tick delay after the first ready event (so PAPI / BentoBox's `PlaceholdersManager` is up). Names follow `island_<field>_top_<1..10>` and are scoped to the AOneBlock `GameModeAddon`. The `TopBlock.TEN` constant is the source of truth for the list size.
Placeholders are registered once per hook via a `runTaskLater` 10-tick delay after the first ready event (so PAPI / BentoBox's `PlaceholdersManager` is up). Names follow `island_<field>_top_<1..10>` and are scoped to each hook's `GameModeAddon`, so the PAPI prefix keeps game modes apart (`%aoneblock_...%` vs `%chunkblock_...%`). The `TopBlock.TEN` constant is the source of truth for the list size.

## Panel

`TopLevelPanel` uses BentoBox's `TemplatedPanelBuilder`. The template file is shipped in `src/main/resources/panels/top_panel.yml` and copied to the data folder on load via `saveResource("panels/top_panel.yml", false)` — players' edits to the on-disk file persist across restarts. Localization keys live under `topblock.gui.buttons.island.*` in `src/main/resources/locales/en-US.yml`. The icon material can be overridden per-player via the `<permissionPrefix>topblock.icon.<MATERIAL>` permission.
`TopLevelPanel` uses BentoBox's `TemplatedPanelBuilder`. The template file is shipped in `src/main/resources/panels/top_panel.yml` and copied to the data folder on load via `saveResource("panels/top_panel.yml", false)` — players' edits to the on-disk file persist across restarts. The panel shows the top ten of whichever game mode owns the command's world (`getTopTen(world, TEN)`). Localization keys live under `topblock.gui.buttons.island.*` in `src/main/resources/locales/en-US.yml`. The icon material can be overridden per-player via the `<permissionPrefix>topblock.icon.<MATERIAL>` permission.

The panel has no click actions (TopBlock doesn't bundle Warp/Visit hooks like Level does). The YAML still declares `warp`/`visit` actions with tooltips, but no click handler is registered — clicking does nothing.

Expand All @@ -57,4 +66,6 @@ JUnit 5 + Mockito + MockBukkit. Test classes extend `CommonTestSetup` which:

`TestWorldSettings` returns `"TopBlock"` for friendly name and `"topblock."` for permission prefix. The addon test (`TopBlockTest`) builds an in-memory `addon.jar` containing `config.yml` + `panels/top_panel.yml` because `Addon.saveResource` reads from a real JarFile.

Manager and placeholder tests mock `TopBlockHook` directly (no game mode classes needed); `AOneBlockHookTest` / `ChunkBlockHookTest` cover the real hook mapping. A freshly constructed `Addon` starts in `State.DISABLED` and only AddonsManager sets ENABLED — so enable-path tests call `setState(State.LOADED)` before `onEnable()` and treat DISABLED afterwards as "the addon disabled itself"; don't assert ENABLED after `onEnable()`.

JaCoCo excludes `**/*Names*` to avoid synthetic-field issues on JavaBeans — keep that exclusion if adding similar classes.
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@

## About

TopBlock is a [BentoBox](https://github.com/BentoBoxWorld/BentoBox) addon that produces a Top Ten ranking for the [AOneBlock](https://github.com/BentoBoxWorld/AOneBlock) game mode based on how many magic blocks each island has mined.
TopBlock is a [BentoBox](https://github.com/BentoBoxWorld/BentoBox) addon that produces a Top Ten ranking for the [AOneBlock](https://github.com/BentoBoxWorld/AOneBlock) and [ChunkBlock](https://github.com/BentoBoxWorld/ChunkBlock) game modes based on how many magic blocks each island has mined. Either game mode (or both) can be installed — each gets its own independent top ten, command, and placeholders.

## Requirements

- Paper 1.21.x (Spigot is no longer supported)
- Java 21
- BentoBox 3.14.0 or later
- AOneBlock 1.18.0 or later
- At least one of:
- AOneBlock 1.18.0 or later
- ChunkBlock 1.0.1 or later

## How to use

1. Drop the TopBlock jar into your server's `plugins/BentoBox/addons/` folder. AOneBlock must already be installed there too.
1. Drop the TopBlock jar into your server's `plugins/BentoBox/addons/` folder. AOneBlock and/or ChunkBlock must already be installed there too.
2. Restart the server. TopBlock will create `addons/TopBlock/config.yml` and `addons/TopBlock/panels/top_panel.yml`.
3. Edit `config.yml` if you want to tune anything (see below) and restart the server again to apply.

Expand All @@ -31,9 +33,11 @@ The panel layout lives in `addons/TopBlock/panels/top_panel.yml`. Edits are pres

## Commands

`/ob topblock` (alias: `/oneblock topblock`) — opens the Top Ten panel.
`/ob topblock` (alias: `/oneblock topblock`) — opens the AOneBlock Top Ten panel.

To get into the top ten, a player just needs to mine at least one magic block on their AOneBlock island. The list refreshes every `refresh-time` minutes; a player who just started mining may need to wait that long before appearing.
If ChunkBlock is installed, its player command gets a `topblock` subcommand too, which opens ChunkBlock's own Top Ten panel.

To get into the top ten, a player just needs to mine at least one magic block on their island. Each game mode keeps a separate ranking. The lists refresh every `refresh-time` minutes; a player who just started mining may need to wait that long before appearing.

## Permissions

Expand All @@ -47,11 +51,17 @@ permissions:
'aoneblock.intopten':
description: Player's island will be listed in the top ten. Remove from admins or testers to hide them.
default: true
'chunkblock.island.topblock':
description: Player can use the TopBlock command
default: true
'chunkblock.intopten':
description: Player's island will be listed in the top ten. Remove from admins or testers to hide them.
default: true
```

If an island owner is **online** and lacks `aoneblock.intopten`, their island is excluded from the top ten panel and from placeholders. **Offline** owners are always included — to hide an admin or tester, remove the perm from the player who can actually log in. Removing the perm from an entire group (e.g. ops) excludes everyone in that group while online.
If an island owner is **online** and lacks `<gamemode>.intopten`, their island is excluded from that game mode's top ten panel and placeholders. **Offline** owners are always included — to hide an admin or tester, remove the perm from the player who can actually log in. Removing the perm from an entire group (e.g. ops) excludes everyone in that group while online.

The icon shown for each rank can be overridden per player by granting `aoneblock.topblock.icon.<MATERIAL>` (for example `aoneblock.topblock.icon.diamond_block`). Without an override, the rank icon is the player's head.
The icon shown for each rank can be overridden per player by granting `<gamemode>.topblock.icon.<MATERIAL>` (for example `aoneblock.topblock.icon.diamond_block`). Without an override, the rank icon is the player's head.

## Placeholders

Expand All @@ -64,6 +74,8 @@ The icon shown for each rank can be overridden per player by granting `aoneblock
%aoneblock_island_lifetime_top_RANK% - Lifetime count of magic blocks mined
```

If ChunkBlock is installed, the same placeholders exist with the `chunkblock` prefix (e.g. `%chunkblock_island_count_top_1%`) and report ChunkBlock's own ranking.

`RANK` is `1` to `10`. If fewer than `RANK` islands qualify for the top ten, the placeholder returns an empty string.

## Building from source
Expand Down
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
<bentobox.version>3.14.0-SNAPSHOT</bentobox.version>
<!-- AOneBlock addon version -->
<aoneblock.version>1.18.0</aoneblock.version>
<!-- ChunkBlock addon version -->
<chunkblock.version>1.0.1</chunkblock.version>
<!-- Level addon version - needed only at test time to mock ChunkBlock, which hard-depends on it -->
<level.version>2.6.2</level.version>
<!-- Panel Utils version -->
<panelutils.version>1.1.0</panelutils.version>
<!-- Revision variable removes warning about dynamic version -->
Expand Down Expand Up @@ -165,6 +169,18 @@
<version>${aoneblock.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>world.bentobox</groupId>
<artifactId>chunkblock</artifactId>
<version>${chunkblock.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>world.bentobox</groupId>
<artifactId>level</artifactId>
<version>${level.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>lv.id.bonne</groupId>
<artifactId>panelutils</artifactId>
Expand Down
Loading
Loading