# 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 ``` 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-.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//` 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 `/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--.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.