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:
@@ -129,6 +129,31 @@ matching this phase's Enigmatica-2-focused scope. See MCMapper-Backend's README
|
||||
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`.
|
||||
|
||||
@@ -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 -> api {"type":"block_registry","entries":[{"id":4000,"name":"botania:manapool"}]}
|
||||
* mod -> api {"type":"block_textures","textures":[{"name":"botania:manapool","dataBase64":"..."}]}
|
||||
* mod -> 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;
|
||||
}
|
||||
}
|
||||
+125
-4
@@ -1,16 +1,25 @@
|
||||
package com.octoturge.mcmapper.forge1122;
|
||||
|
||||
import com.octoturge.mcmapper.common.protocol.BlockModelFile;
|
||||
import com.octoturge.mcmapper.common.protocol.BlockRegistryEntry;
|
||||
import com.octoturge.mcmapper.common.protocol.BlockTexture;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraftforge.fml.common.Loader;
|
||||
import net.minecraftforge.fml.common.ModContainer;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* Phase 11: best-effort classloader extraction of block textures + the numeric-id -> registry-
|
||||
@@ -23,10 +32,10 @@ import java.util.List;
|
||||
* {@code assets/<modid>/textures/blocks/<path>.png} directly with no jar-file bookkeeping.
|
||||
*
|
||||
* This is a *convention* match (the registry name's path segment as the texture filename), not a
|
||||
* real blockstate/model JSON resolution — that's Phase 12's job (see the plan's phase list). A
|
||||
* block whose model uses a differently-named/multi-texture layout (most non-cube blocks) simply
|
||||
* isn't found here and is skipped — no worse than pre-Phase-11 behavior for that block, just not
|
||||
* improved yet.
|
||||
* real blockstate/model JSON resolution — {@link #extractModels} (Phase 13) is what feeds that,
|
||||
* on the backend side (see MCMapper-Backend's {@code worker/src/models.rs}). A block whose texture
|
||||
* doesn't match this convention (most non-cube blocks) simply isn't found by {@link
|
||||
* #extractTextures} and is skipped there — no worse than pre-Phase-11 behavior for that block.
|
||||
*/
|
||||
final class BlockAssetExtractor {
|
||||
private BlockAssetExtractor() {
|
||||
@@ -85,4 +94,116 @@ final class BlockAssetExtractor {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 13: ships every {@code assets/<modid>/blockstates/*.json} and {@code
|
||||
* assets/<modid>/models/block/**\/*.json} file for every currently-active mod, keyed the same
|
||||
* way {@code worker/src/models.rs}'s {@code ModelRegistry::build} expects — see {@link
|
||||
* BlockModelFile}'s javadoc for why this ships every model file rather than trying to resolve
|
||||
* which ones a blockstate actually needs. Walks each mod's source (a jar in production, a raw
|
||||
* exploded directory when running from an IDE/dev environment — {@code ModContainer#getSource}
|
||||
* covers both) directly rather than the classloader, since listing a directory's contents
|
||||
* (unlike reading one known file by path, as {@link #extractTextures} does) isn't something
|
||||
* {@code ClassLoader#getResourceAsStream} can do.
|
||||
*/
|
||||
static List<BlockModelFile> extractModels(Logger logger) {
|
||||
List<BlockModelFile> files = new ArrayList<>();
|
||||
for (ModContainer mod : Loader.instance().getActiveModList()) {
|
||||
File source = mod.getSource();
|
||||
if (source == null) continue;
|
||||
if (source.isFile()) {
|
||||
extractModelsFromJar(source, mod.getModId(), files);
|
||||
} else if (source.isDirectory()) {
|
||||
extractModelsFromDirectory(source, mod.getModId(), files);
|
||||
}
|
||||
}
|
||||
logger.info("MCMapper (1.12.2 leaf) extracted " + files.size()
|
||||
+ " blockstate/model JSON files for Phase 13 non-cube model rendering");
|
||||
return files;
|
||||
}
|
||||
|
||||
private static void extractModelsFromJar(File jarFile, String modid, List<BlockModelFile> out) {
|
||||
try (ZipFile zip = new ZipFile(jarFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (entry.isDirectory()) continue;
|
||||
ModelPath parsed = classify(modid, entry.getName());
|
||||
if (parsed == null) continue;
|
||||
byte[] json = readZipEntry(zip, entry);
|
||||
if (json != null) out.add(new BlockModelFile(parsed.kind, parsed.name, new String(json, StandardCharsets.UTF_8)));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Best-effort, matching readTexture's convention — skip this mod's models entirely.
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readZipEntry(ZipFile zip, ZipEntry entry) {
|
||||
try (InputStream in = zip.getInputStream(entry)) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
|
||||
return out.toByteArray();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractModelsFromDirectory(File modRoot, String modid, List<BlockModelFile> out) {
|
||||
File assetsDir = new File(modRoot, "assets/" + modid);
|
||||
if (assetsDir.isDirectory()) walkDirectory(assetsDir, "assets/" + modid + "/", modid, out);
|
||||
}
|
||||
|
||||
private static void walkDirectory(File dir, String relPrefix, String modid, List<BlockModelFile> out) {
|
||||
File[] children = dir.listFiles();
|
||||
if (children == null) return;
|
||||
for (File child : children) {
|
||||
String rel = relPrefix + child.getName();
|
||||
if (child.isDirectory()) {
|
||||
walkDirectory(child, rel + "/", modid, out);
|
||||
continue;
|
||||
}
|
||||
ModelPath parsed = classify(modid, rel);
|
||||
if (parsed == null) continue;
|
||||
try {
|
||||
byte[] json = Files.readAllBytes(child.toPath());
|
||||
out.add(new BlockModelFile(parsed.kind, parsed.name, new String(json, StandardCharsets.UTF_8)));
|
||||
} catch (IOException e) {
|
||||
// Best-effort — skip this one file.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies one archive/directory entry path (e.g. {@code "assets/botania/blockstates/
|
||||
* manapool.json"}) into a wire-ready {@code (kind, name)} pair, or {@code null} if it's not a
|
||||
* blockstate/model JSON file for this mod. Blockstates are matched flat (no subfolders, matching
|
||||
* vanilla's own layout); models are matched recursively under {@code models/block/} since mods
|
||||
* are free to nest them (e.g. a shared {@code models/block/base/} folder).
|
||||
*/
|
||||
private static ModelPath classify(String modid, String entryPath) {
|
||||
String prefix = "assets/" + modid + "/";
|
||||
if (!entryPath.startsWith(prefix) || !entryPath.endsWith(".json")) return null;
|
||||
String rest = entryPath.substring(prefix.length());
|
||||
if (rest.startsWith("blockstates/") && rest.indexOf('/', "blockstates/".length()) < 0) {
|
||||
String path = rest.substring("blockstates/".length(), rest.length() - ".json".length());
|
||||
return new ModelPath("blockstate", modid + ":" + path);
|
||||
}
|
||||
if (rest.startsWith("models/block/")) {
|
||||
String path = rest.substring("models/block/".length(), rest.length() - ".json".length());
|
||||
return new ModelPath("model", modid + ":block/" + path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class ModelPath {
|
||||
final String kind;
|
||||
final String name;
|
||||
|
||||
ModelPath(String kind, String name) {
|
||||
this.kind = kind;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.octoturge.mcmapper.common.ChunkAdapter;
|
||||
import com.octoturge.mcmapper.common.DefaultBackendConnection;
|
||||
import com.octoturge.mcmapper.common.ReconciliationScheduler;
|
||||
import com.octoturge.mcmapper.common.config.MapperConfig;
|
||||
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;
|
||||
@@ -114,9 +115,11 @@ public class MCMapperMod {
|
||||
// silently no-op).
|
||||
List<BlockRegistryEntry> blockRegistry = BlockAssetExtractor.extractRegistry();
|
||||
List<BlockTexture> blockTextures = BlockAssetExtractor.extractTextures(LOGGER);
|
||||
List<BlockModelFile> blockModels = BlockAssetExtractor.extractModels(LOGGER);
|
||||
connection.setReadyListener(() -> {
|
||||
connection.sendBlockRegistry(blockRegistry);
|
||||
connection.sendBlockTextures(blockTextures);
|
||||
connection.sendBlockModels(blockModels);
|
||||
});
|
||||
|
||||
connection.connect(config.backendUrl, config.serverToken);
|
||||
|
||||
Reference in New Issue
Block a user