132 lines
6.7 KiB
Markdown
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.
|