47490309fe
Forge doesn't split client/server jars, so a modded server's own classpath already has every loaded mod's block textures, unused server-side. BlockAssetExtractor reads them via the classloader (best-effort convention match on registry name -> texture filename) plus the numeric-id -> registry- name mapping, and ships both to the backend once per connection via two new wire messages (block_registry, block_textures), fired through a new BackendConnection#setReadyListener callback so the one-time send can't race the async WS handshake. Only forge-1_12_2 wires it up so far; the other two leaves can adopt it later with no protocol change.
135 lines
8.0 KiB
Markdown
135 lines
8.0 KiB
Markdown
# MCMapper-Mod
|
|
|
|
Thin, version-independent Forge/NeoForge client for [MCMapper-Backend](https://git.octoturge.com/octoturge/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.
|
|
|
|
### 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).
|
|
|
|
## Attribution
|
|
|
|
See `THIRD_PARTY_NOTICES.md`.
|