first commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user