BlockAssetExtractor#extractModels walks every active mod's own jar/directory (Loader#getActiveModList's ModContainer#getSource, not the classloader, since directory listing isn't a classloader operation) for every blockstates/*.json and models/block/**/*.json file, shipping all of them over a new batched block_models WS message alongside the existing Phase 11 block_registry/block_textures dump. Resolution of this data into real non-cube geometry is entirely backend-side (see MCMapper-Backend's worker/src/models.rs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
9.8 KiB
MCMapper-Mod
Thin, version-independent Forge/NeoForge client for MCMapper-Backend. Streams delta block updates out to the backend instead of rendering the map on the MC server itself (the Bluemap/Dynmap resource problem this project exists to avoid). Full architecture and phased delivery plan lives in the backend repo's planning docs / was tracked during design in Claude Code's plan mode.
Repo layout
common/— loader-agnostic Java: protocol types, config model, and the interfaces (ChunkAdapter,ChatBridge,BackendConnection) each leaf implements. Not a compiled dependency — pulled in as source per leaf (seecommon/README.md).forge-1_12_2/— primary leaf, MC 1.12.2 Forge. First implemented; this is the actual driving use case (an Enigmatica 2 modpack server). Builds with legacy ForgeGradle 2.3 via anatawa12's Gradle-7-compatible fork (seeTHIRD_PARTY_NOTICES.md).forge-1_7_10/— MC 1.7.10 Forge (Phase 9). Second priority, fully wired against the samecommoninterfaces asforge-1_12_2. Builds with legacy ForgeGradle 1.2, same fork family, one Forge-tooling generation further back — pre-block-state (rawBlock+ metadata int, noIBlockState) and pre-FML-repackage (cpw.mods.fml.*, notnet.minecraftforge.fml.*).neoforge-26_1/— MC 26.1.2 (Phase 10; loader confirmed NeoForge). Lowest MVP priority, fully wired against the samecommoninterfaces as the two legacy leaves. Post-Flattening, mixin-era API: block reads areBlockState, not raw id+meta; events/commands/config live undernet.neoforged.*with constructor-injected event buses instead of@Mod.EventHandlermethods and Brigadier commands instead ofCommandBase. Builds standalone via ModDevGradle — see "Building" below, not part of the root multi-project build (seesettings.gradle).
Version targets and priority
- MC 1.12.2 Forge — highest priority (Enigmatica 2)
- MC 1.7.10 Forge — also a priority
- MC 26.1.2 — lowest of the three MVP targets
Fabric support is an explicit future phase, not part of the MVP.
Building
Both legacy leaves share one root build, on Gradle 7.6 / JDK 8 (pinned via
org.gradle.java.home in gradle.properties — see settings.gradle for why):
./gradlew :forge-1_12_2:build
./gradlew :forge-1_7_10:build
The first build downloads Minecraft/Forge artifacts and MCP mappings from Forge's Maven and can take a while / needs network access.
neoforge-26_1 is not part of the root build — it needs Gradle 8+ and a Java 25 toolchain
(Minecraft itself now requires Java 25 as of the 26.x cycle), incompatible with the legacy
leaves' Gradle-7/JDK-8 pin within one Gradle invocation. It has its own wrapper; build it from
inside its own directory:
cd neoforge-26_1
./gradlew build
The Gradle daemon can run on any modern JDK on PATH/JAVA_HOME (or pinned via
org.gradle.java.home in a $GRADLE_USER_HOME/gradle.properties, same mechanism as the legacy
leaves' JDK 8 pin — see settings.gradle's comment) — it does not need to already be JDK 25
itself. settings.gradle applies the Foojay toolchain resolver so Gradle auto-provisions an
actual JDK 25 (into its own GRADLE_USER_HOME cache, not a system-wide install) for the
compile/run tasks. The first build also downloads and decompiles/patches Minecraft itself via
NeoForge's NeoForm pipeline (the modern equivalent of the legacy leaves' MCP step) — expect it to
take several minutes and a real chunk of disk/network the first time; subsequent builds are fast.
Configuration (all three leaves)
On first server start the leaf writes config/mcmapper.cfg with defaults. Set:
backendUrl=ws://<backend-host>:3000/ws
serverToken=<token from MCMapper-Backend's `bun run seed`>
then restart. With those set, the mod connects to the backend, backfills already-loaded
overworld chunks, and streams event-driven column deltas (block place/break) in batches every
deltaFlushIntervalTicks (default 20 = 1s). A periodic reconciliation sweep (Phase 7) also
walks a bounded number of currently-loaded chunks per tick (reconciliationChunksPerSweep,
default 4) and re-sends any that drifted from what the backend last acknowledged, to catch
mutations event hooks miss (world-gen, other mods, /fill). Only the overworld is tracked. The
WS client and JSON encoding are hand-rolled (no third-party dependency) — see
common/src/main/java/.../ws/SimpleWebSocketClient.java and .../json/MiniJson.java for why.
forge-1_7_10 and neoforge-26_1 implement the identical config/flush/reconciliation/
player-tracking behavior against their own API generation (see Forge1710ChunkAdapter's and
Neoforge261ChunkAdapter's javadocs for what differs). Neither has a MultiPlaceEvent-equivalent
hook wired (1.7.10: that class doesn't reliably exist at this Forge version; 26.1.2: skipped for
symmetry, no strong need identified), so multi-block placements like doors/beds are only caught
by the reconciliation sweep rather than immediately, unlike forge-1_12_2. neoforge-26_1 writes
its config to config/mcmapper-server.toml (NeoForge's ModConfigSpec/TOML format, not the
legacy leaves' .cfg), and — being post-Flattening — carries the full registry-wide packed
BlockState id as DeltaEvent#blockStateId (an int, so this is lossless for the 2D column
pipeline); 3D section backfill (SectionData#blocks, a char[] for wire-size reasons inherited
from the pre-Flattening leaves) truncates to the low 16 bits, a known, documented collision risk
for very large modded registries — see Neoforge261ChunkAdapter's javadoc.
Player position tracking (Phase 7b)
When playerTrackingEnabled (default true), the leaf sends a throttled roster of online
overworld players ({"type":"player_positions",...}, see DefaultBackendConnection's
class-level wire-protocol doc comment) every playerPositionIntervalTicks (default 40 = 2s) —
always the full current roster, not a diff, so a logged-out player simply stops appearing in
the next send. This is a mod-local toggle only: whether the backend actually relays positions on
to web viewers is a separate, independent per-server admin setting on the backend side (see
MCMapper-Backend's README) — a server operator can track positions server-side without exposing
them publicly, or vice versa.
Real block textures (Phase 11)
Forge doesn't split client/server jars, so a dedicated server's own classpath already has every
loaded mod's assets/<modid>/textures/blocks/*.png (or textures/block/ on newer conventions),
sitting there unused server-side. forge-1_12_2 (this project's primary, most-modded target —
Enigmatica 2) reads them once at server-starting time via BlockAssetExtractor: Block.REGISTRY
gives every block's numeric id + registry name (same source as the existing column/section wire
encoding), and each block's texture is guessed by a best-effort convention match — the registry
name's path segment tried as a texture filename — not a real blockstate/model JSON resolution
(that's Phase 12's job; a block whose model doesn't follow the convention is just skipped, no
worse than before this phase). The registry mapping (block_registry) and extracted PNGs
(block_textures, batched 50/message) are sent to the backend once, right after the WS handshake
completes — via BackendConnection#setReadyListener, since connect() is async and a naive send
right after calling it would silently no-op before the handshake lands.
forge-1_7_10 and neoforge-26_1 don't implement extraction yet — sendBlockRegistry/
sendBlockTextures/setReadyListener live on the shared BackendConnection interface (so either
leaf can adopt them later with no protocol change) but only forge-1_12_2 calls them so far,
matching this phase's Enigmatica-2-focused scope. See MCMapper-Backend's README for what the
backend currently does with this data (short version: stores it; per-server render-time
resolution is explicitly deferred, documented there).
Real (non-cube) block models (Phase 13)
BlockAssetExtractor#extractModels ships every assets/<modid>/blockstates/*.json (flat, matching
vanilla's own layout — no subfolders expected) and assets/<modid>/models/block/**/*.json
(recursive — mods are free to nest these, e.g. a shared models/block/base/ folder) file for every
currently-active mod, keyed the way MCMapper-Backend's worker/src/models.rs expects. Unlike
extractTextures, this can't use the classloader (there's no ClassLoader API to list a
directory's contents, only to read one known file by path) — it walks each mod's own source
instead, via net.minecraftforge.fml.common.Loader#getActiveModList's ModContainer#getSource,
handling both shapes that can show up there: a packed jar (opened as a ZipFile) in production, or
a raw exploded directory when running from an IDE/dev environment.
The mod deliberately ships every model file it finds under a mod's models/block/ tree rather
than trying to work out which ones a given blockstate actually references — a blockstate's
"model" field (or another model's "parent") can point at a path that doesn't match the
referencing block's own registry path 1:1 (e.g. several blocks sharing one base model), and the
mod has no JSON parser to resolve that itself. That resolution — parent-chain walking, texture-
variable substitution, picking a representative variant — is entirely backend-side (see
MCMapper-Backend's README's own Phase 13 section for what it does and doesn't handle, e.g. no
multipart blockstate support, no property-based variant selection).
Sent as a new block_models WS message, batched 50/message like block_textures, from the same
ready-listener callback as the Phase 11 registry/texture dump. Verified via a real
./gradlew :forge-1_12_2:compileJava against the pinned legacy ForgeGradle toolchain.
Attribution
See THIRD_PARTY_NOTICES.md.