Phase 13: forge-1_12_2 ships blockstate/model JSON for non-cube models

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
This commit is contained in:
2026-08-10 01:19:36 +02:00
parent 47490309fe
commit 376d2d0507
6 changed files with 222 additions and 4 deletions
@@ -1,5 +1,6 @@
package com.octoturge.mcmapper.common;
import com.octoturge.mcmapper.common.protocol.BlockModelFile;
import com.octoturge.mcmapper.common.protocol.BlockRegistryEntry;
import com.octoturge.mcmapper.common.protocol.BlockTexture;
import com.octoturge.mcmapper.common.protocol.DeltaEvent;
@@ -54,6 +55,13 @@ public interface BackendConnection {
*/
void sendBlockTextures(List<BlockTexture> textures);
/**
* Sends this leaf's raw blockstate/model JSON dump (Phase 13) — see {@link
* BlockModelFile}'s javadoc. Implementations should batch calls the same way {@link
* #sendBlockTextures} does, for the same heavily-modded-pack reason.
*/
void sendBlockModels(List<BlockModelFile> files);
/**
* Registers a callback fired every time the connection successfully authenticates (including
* after an automatic reconnect) — {@code connect()} itself is async (the real handshake
@@ -112,6 +120,10 @@ public interface BackendConnection {
public void sendBlockTextures(List<BlockTexture> textures) {
}
@Override
public void sendBlockModels(List<BlockModelFile> files) {
}
@Override
public void setReadyListener(Runnable onReady) {
}
@@ -1,6 +1,7 @@
package com.octoturge.mcmapper.common;
import com.octoturge.mcmapper.common.json.MiniJson;
import com.octoturge.mcmapper.common.protocol.BlockModelFile;
import com.octoturge.mcmapper.common.protocol.BlockRegistryEntry;
import com.octoturge.mcmapper.common.protocol.BlockTexture;
import com.octoturge.mcmapper.common.protocol.DeltaEvent;
@@ -40,6 +41,7 @@ import java.util.function.Consumer;
*
* mod -&gt; api {"type":"block_registry","entries":[{"id":4000,"name":"botania:manapool"}]}
* mod -&gt; api {"type":"block_textures","textures":[{"name":"botania:manapool","dataBase64":"..."}]}
* mod -&gt; api {"type":"block_models","entries":[{"kind":"blockstate","name":"botania:manapool","json":"..."},{"kind":"model","name":"botania:block/manapool","json":"..."}]}
* </pre>
*
* Phase 3: {@code link_request} is sent by {@code /mcmapper link}; {@code chat} both directions
@@ -83,6 +85,13 @@ import java.util.function.Consumer;
* {@link #sendBlockTextures} batches into multiple messages (see {@code BLOCK_TEXTURE_BATCH_SIZE})
* rather than one huge frame, since a heavily-modded server can have thousands of block textures.
*
* Phase 13: {@code block_models} is sent the same way, once, alongside {@code block_registry}/
* {@code block_textures} — see {@link BlockModelFile}'s javadoc for why the mod ships every model
* file it finds rather than trying to resolve blockstate-to-model references itself.
* {@link #sendBlockModels} batches the same way {@link #sendBlockTextures} does, for the same
* reason (blockstate/model JSON files, while individually small, can number in the thousands on
* a heavily-modded pack).
*
* This class avoids a third-party JSON/WS library entirely (see {@link SimpleWebSocketClient}
* and {@link MiniJson}'s javadoc) to keep the mod's classpath free of anything that would need
* shading through legacy ForgeGradle.
@@ -352,6 +361,28 @@ public class DefaultBackendConnection implements BackendConnection {
}
}
private static final int BLOCK_MODEL_BATCH_SIZE = 50;
@Override
public void sendBlockModels(List<BlockModelFile> files) {
if (files.isEmpty() || !serverReady) return;
for (int start = 0; start < files.size(); start += BLOCK_MODEL_BATCH_SIZE) {
int end = Math.min(start + BLOCK_MODEL_BATCH_SIZE, files.size());
List<Object> entryList = new ArrayList<>();
for (BlockModelFile f : files.subList(start, end)) {
Map<String, Object> obj = new LinkedHashMap<>();
obj.put("kind", f.kind);
obj.put("name", f.name);
obj.put("json", f.json);
entryList.add(obj);
}
Map<String, Object> msg = new LinkedHashMap<>();
msg.put("type", "block_models");
msg.put("entries", entryList);
sendRaw("block_models", msg);
}
}
@Override
public void setReadyListener(Runnable onReady) {
this.readyListener = onReady;
@@ -0,0 +1,26 @@
package com.octoturge.mcmapper.common.protocol;
/**
* One raw blockstate or model JSON file, extracted by a leaf off its own classloader (Phase 13) —
* same classloader-is-already-there rationale as {@link BlockTexture}. {@code kind} is either
* {@code "blockstate"} (a {@code assets/<modid>/blockstates/<path>.json} file, keyed by the
* block's own registry name) or {@code "model"} (a {@code assets/<modid>/models/block/<path>.json}
* file, keyed by {@code "<modid>:block/<path>"} — the same reference shape a blockstate's
* {@code "model"} field or another model's {@code "parent"} field uses to point at it). The mod
* ships every model file it finds under a mod's {@code models/block/} tree rather than trying to
* pick out just the ones a given blockstate needs — a blockstate can reference a model at a path
* that doesn't match the block's own registry path 1:1 (e.g. a shared base model reused by several
* blocks), and the mod has no JSON parser to work that out itself; that resolution is the
* backend's job (see MCMapper-Backend's {@code worker/src/models.rs}).
*/
public final class BlockModelFile {
public final String kind;
public final String name;
public final String json;
public BlockModelFile(String kind, String name, String json) {
this.kind = kind;
this.name = name;
this.json = json;
}
}