Files
chunkinspector/CLAUDE.md
T
2026-07-31 10:46:40 +00:00

132 lines
6.7 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Toolchain
Shell state does **not** persist between Bash calls. Source the toolchain in every command that runs
Gradle:
```fish
source /home/user/tools/env.sh && ./gradlew <task>
```
That sets `JAVA_HOME` (Temurin 21.0.12+8), puts Gradle 8.10.2 on `PATH`, and sets `GRADLE_USER_HOME`.
## Reading Minecraft's source
Decompiled, mojmapped sources are already extracted for grepping — **use these, not `javap`**:
- `/home/user/mc-sources/minecraft/` — 5364 Minecraft `.java` files
- `/home/user/mc-sources/neoforge/` — 953 NeoForge `.java` files
Verify vanilla behaviour there before asserting it in code or tests. Nearly every subtle bug in this
project came from guessing at a constant instead of grepping for it.
## Commands
```fish
./gradlew build # compile + unit tests + jar
./gradlew test # unit tests only (fast, no game runtime)
./gradlew test --tests 'dev.chunkinspector.analysis.LevelPropagationTest'
./gradlew test --tests '*.aChunkQueuedThenSupersededKeepsTheStrongerAnswer'
./gradlew jar # the mod jar, build/libs/chunkinspector-<version>.jar
./gradlew e2eTest # full end-to-end: installs and drives two real dedicated servers
./gradlew runServer # MDG dev server, for manual poking
```
`e2eTest` is deliberately excluded from `check` — it downloads NeoForge (~300 MB, cached in
`~/.cache/chunkinspector-e2e`) and boots two servers, ~90 s once the cache is warm. It `dependsOn jar`,
so it always tests the shipped artifact.
The server half can be run without Gradle: `./e2e/run-servers.sh` (honours `E2E_OUT`, `MOD_JAR`,
`NEO_VERSION`, `E2E_BOOT_TIMEOUT`). Servers are left in `build/e2e/<mode>/` afterwards — read
`build/e2e/light/console.log` and `build/e2e/*/world/chunk-summaries/latest.txt` when a test fails,
they are far more informative than the assertion message.
## Architecture
Minecraft 1.21.1, NeoForge 21.1.247, Mojang mappings, ModDevGradle. Java 21 + Lombok.
### The layering rule that everything else follows
`analysis/`, `report/` and `origin/` contain **no Minecraft types**. `game/` is the only package that
touches them, and its job is to reduce live game state to plain records (`TicketRecord`,
`ChunkRecord`) via `LevelCapture.of(ServerLevel)`.
This is not stylistic. It is what makes 62 unit tests runnable without a game runtime, and it is why
`build.gradle` adds `fastutil` and `gson` as *test* dependencies at the versions Minecraft itself
ships. Keep new logic on the Minecraft-free side of that line; if you need a game value, capture it
into a record rather than importing `net.minecraft` into `analysis/`.
### Two cost tiers, decided before Mixin application
The always-on tier injects **nothing**. `META-INF/accesstransformer.cfg` widens five
`DistanceManager`/`Ticket`/`ChunkMap` members for read-only access, and a snapshot walks them on
demand.
Deep mode (`-Dchunkinspector.deep=true`) adds a `StackWalker` capture at
`DistanceManager.addTicket(long, Ticket)` — the single choke point every ticket passes through.
`InspectorMixinPlugin.shouldApplyMixin` *declines to apply* `DistanceManagerMixin` and `TicketMixin`
when deep mode is off, so Minecraft's bytecode is untouched rather than carrying a disabled branch.
Because that decision happens during Mixin config load, all configuration is system properties
(`InspectorConfig`) — a config file would be read too late. Adding a new tuning knob means adding it
there, not to a NeoForge config spec.
### Snapshot lifecycle
`InspectorService` splits the work: `capture()` runs on the server thread and only copies two
collections; analysis, propagation and JSON serialisation happen on the single-threaded
`chunkinspector-report` executor against the immutable snapshot. That single thread is also what
makes `ReportWriter`'s check-then-create filename logic race-free.
Triggers are `periodic` (every `interval` ticks), `command`, and `shutdown` (always full analysis,
blocks up to 30 s). Output goes to `<levelDir>/chunk-summaries/`.
### The analysis itself
`LevelPropagation` replays vanilla's `ChunkTracker` rules — a ticket at chunk C level L gives every
chunk at Chebyshev distance d a level of L+d, lowest wins, stop above `ChunkLevel.MAX_LEVEL` — using
Dial's algorithm (bucket queue) so the pass is O(loaded chunks). Crucially it also records *which*
ticket won each chunk; the game throws that away, and it is the entire point of the mod.
`LevelAnalysis` turns that into totals, per-type breakdowns, suspects, hotspots and unattributed
chunks. "Influence" is a partition, not a sum: each loaded chunk is credited to exactly one winning
ticket, so per-ticket numbers add up rather than overlap.
Attribution is opt-in per snapshot (`attribution` flag / `snapshot full`) because it is the only
part that is more than O(tickets).
### Ticket identity without instrumentation
`TicketKeys.render` is what lets the free tier often be sufficient — a NeoForge forced-chunk key
already carries the requesting mod's id. Deep mode is only needed when the key is opaque.
## Gotchas
- `net.minecraft.server.level.Ticket` is `final`, so `ticket instanceof TicketOrigin` will not
compile even though `TicketMixin` makes it true at runtime. Cast through `Object`.
- `OriginRegistry` filters `dev.chunkinspector.*` frames out of every capture, so a test that calls
`record()` from within that package captures nothing. `src/test/java/example/mod/LeakyChunkLoader`
exists to be an outside caller.
- `/forceload add` takes **block** coordinates, not chunk coordinates. The e2e script force-loads
`1600 1600 1663 1663` to cover chunks (100,100)..(103,103).
- Snapshot filenames resolve to the second, so they carry the trigger (`summary-<stamp>-<trigger>.json`)
and uniquify on collision — a periodic and a command snapshot really do land in the same second.
- The mixin config's `defaultRequire: 1` means a silently non-applying injector fails the build at
load time. Check `console.log` for `Mixin apply failed` when a deep-mode e2e test goes quiet.
## Tests
`src/test/java/dev/chunkinspector/Fixtures.java` holds the shared scenario (a player ticket, a leaky
permanent ticket, a forceload, a portal ticket, and a chunk set) used across the analysis, report and
digest tests. Extend it rather than hand-rolling another fixture.
`src/e2e/` is a separate source set whose `compileClasspath` includes `main`'s output — the e2e tests
parse the servers' JSON back through the production `InspectionReport` records, which is deliberate:
it proves the file a server writes is a file this mod can read.
See `README.md` for the user-facing documentation: all nine system properties, the commands, and how
to read a report.