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:
+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