Files
MCMapper-Mod/README.md
T
octoturge 52417a7b92 Phase 10: implement neoforge-26_1 leaf (WS, deltas, chat, link, reconciliation, player tracking)
Ports the full feature set proven by the two legacy leaves to modern, post-Flattening
NeoForge (loader assumption confirmed: NeoForge 26.1.2.94 is real and published).
Neoforge261ChunkAdapter reads BlockState via LevelChunk/LevelChunkSection instead of raw
id+meta, carrying the full packed state id in DeltaEvent#blockStateId (a lossless int,
unlike the pre-Flattening 16-bit encoding) while documenting a known truncation caveat for
SectionData's char[]-based 3D backfill on very large modded registries. MCMapperMod uses
constructor-injected event buses (IEventBus/ModContainer) and NeoForge.EVENT_BUS instead of
@Mod.EventHandler methods, ModConfigSpec instead of legacy Configuration, and LinkCommand is
a Brigadier registration (no CommandBase in this era) fired from RegisterCommandsEvent.
Tracks its own loaded-chunk set via ChunkEvent.Load/Unload rather than querying chunk
provider internals (no stable public API for that in modern MC).

Gets its own standalone Gradle wrapper + settings.gradle (Foojay toolchain resolver) since
ModDevGradle needs Gradle 8+ and a Java 25 toolchain (MC itself now requires Java 25),
incompatible with the legacy leaves' Gradle-7/JDK-8 pin in one invocation.

Verified against real NeoForge 26.1.2.94 + decompiled MC 26.1.2 source via
./gradlew build from inside neoforge-26_1/ (two real API mismatches caught and fixed by
the compiler: ChunkPos is now a record — x()/z() methods, not fields — and
ResourceLocation was renamed to Identifier, ResourceKey#identifier() not #location()).
2026-08-09 22:44:06 +02:00

6.4 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 (see common/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 (see THIRD_PARTY_NOTICES.md).
  • forge-1_7_10/ — MC 1.7.10 Forge (Phase 9). Second priority, fully wired against the same common interfaces as forge-1_12_2. Builds with legacy ForgeGradle 1.2, same fork family, one Forge-tooling generation further back — pre-block-state (raw Block + metadata int, no IBlockState) and pre-FML-repackage (cpw.mods.fml.*, not net.minecraftforge.fml.*).
  • neoforge-26_1/ — MC 26.1.2 (Phase 10; loader confirmed NeoForge). Lowest MVP priority, fully wired against the same common interfaces as the two legacy leaves. Post-Flattening, mixin-era API: block reads are BlockState, not raw id+meta; events/commands/config live under net.neoforged.* with constructor-injected event buses instead of @Mod.EventHandler methods and Brigadier commands instead of CommandBase. Builds standalone via ModDevGradle — see "Building" below, not part of the root multi-project build (see settings.gradle).

Version targets and priority

  1. MC 1.12.2 Forge — highest priority (Enigmatica 2)
  2. MC 1.7.10 Forge — also a priority
  3. 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.

Attribution

See THIRD_PARTY_NOTICES.md.