add box UV bbmodel support, enable unit tests, hide Share button outside library tab

main
MomokoKoigakubo 4 weeks ago
parent 1d92791f74
commit b5503545b3

@ -103,6 +103,11 @@ neoForge {
sourceSet(sourceSets.main) sourceSet(sourceSets.main)
} }
} }
unitTest {
enable()
testedMod = mods."${mod_id}"
}
} }
// Sets up a dependency configuration called 'localRuntime'. // Sets up a dependency configuration called 'localRuntime'.

@ -1,5 +1,6 @@
package com.razz.dfashion.bbmodel; package com.razz.dfashion.bbmodel;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector3f; import org.joml.Vector3f;
import java.util.Map; import java.util.Map;
@ -12,4 +13,10 @@ public record BbCube (
float inflate, float inflate,
Vector3f origin, Vector3f origin,
Vector3f rotation, Vector3f rotation,
Map<String, BbFace> faces){} Map<String, BbFace> faces,
@Nullable
int[] uvOffset,
boolean mirrorUv,
@Nullable
Boolean boxUv
){}

@ -64,7 +64,7 @@ public class BbModelParser {
JsonObject res = root.has("resolution") ? root.getAsJsonObject("resolution") : null; JsonObject res = root.has("resolution") ? root.getAsJsonObject("resolution") : null;
int resW = res != null && res.has("width") ? res.get("width").getAsInt() : 64; int resW = res != null && res.has("width") ? res.get("width").getAsInt() : 64;
int resH = res != null && res.has("height") ? res.get("height").getAsInt() : 64; int resH = res != null && res.has("height") ? res.get("height").getAsInt() : 64;
boolean metaBoxUv = meta != null && meta.has("box_uv") && meta.get("box_uv").getAsBoolean();
List<BbCube> cubes = new ArrayList<>(); List<BbCube> cubes = new ArrayList<>();
List<BbLocator> locators = new ArrayList<>(); List<BbLocator> locators = new ArrayList<>();
@ -73,7 +73,10 @@ public class BbModelParser {
JsonObject obj = e.getAsJsonObject(); JsonObject obj = e.getAsJsonObject();
String type = obj.has("type") ? obj.get("type").getAsString() : "cube"; String type = obj.has("type") ? obj.get("type").getAsString() : "cube";
switch (type) { switch (type) {
case "cube" -> cubes.add(GSON.fromJson(obj, BbCube.class)); case "cube" -> {
BbCube raw = GSON.fromJson(obj, BbCube.class);
cubes.add(expandBoxUvIfNeeded(raw, metaBoxUv));
}
case "locator" -> locators.add(GSON.fromJson(obj, BbLocator.class)); case "locator" -> locators.add(GSON.fromJson(obj, BbLocator.class));
// meshes, texture_meshes, null_objects, etc. silently dropped — renderer ignores them // meshes, texture_meshes, null_objects, etc. silently dropped — renderer ignores them
} }
@ -85,7 +88,7 @@ public class BbModelParser {
JsonArray outlinerArr = root.has("outliner") ? root.getAsJsonArray("outliner") : new JsonArray(); JsonArray outlinerArr = root.has("outliner") ? root.getAsJsonArray("outliner") : new JsonArray();
List<BbOutlinerNode> outliner = GSON.fromJson(outlinerArr, new TypeToken<List<BbOutlinerNode>>(){}.getType()); List<BbOutlinerNode> outliner = GSON.fromJson(outlinerArr, new TypeToken<List<BbOutlinerNode>>(){}.getType());
Bbmodel model = new Bbmodel(resW, resH, cubes, locators, groups, outliner); Bbmodel model = new Bbmodel(resW, resH, metaBoxUv, cubes, locators, groups, outliner);
validate(model); validate(model);
return model; return model;
} }
@ -188,6 +191,44 @@ public class BbModelParser {
if (!Float.isFinite(v)) throw new BadBbmodelException(label + " is not finite: " + v); if (!Float.isFinite(v)) throw new BadBbmodelException(label + " is not finite: " + v);
} }
private static BbCube expandBoxUvIfNeeded(BbCube c, boolean metaBoxUv) {
boolean effectiveBoxUv = c.boxUv() != null ? c.boxUv() : metaBoxUv;
if (!effectiveBoxUv) return c;
if (c.uvOffset() == null) return c;
if (c.faces() != null && !c.faces().isEmpty()) return c;
Map<String, BbFace> synth = buildBoxUvFaces(c);
return new BbCube(
c.uuid(), c.name(), c.from(), c.to(), c.inflate(),
c.origin(), c.rotation(), synth,
c.uvOffset(), c.mirrorUv(), c.boxUv()
);
}
private static Map<String, BbFace> buildBoxUvFaces(BbCube c) {
float u = c.uvOffset()[0];
float v = c.uvOffset()[1];
float w = c.to().x - c.from().x;
float h = c.to().y - c.from().y;
float d = c.to().z - c.from().z;
Map<String, BbFace> faces = new java.util.LinkedHashMap<>();
faces.put("up", new BbFace(new float[]{u + d, v, u + d + w, v + d}, 0, 0));
faces.put("down", new BbFace(new float[]{u + d + w, v, u + d + 2*w, v + d}, 0, 0));
faces.put("east", new BbFace(new float[]{u, v + d, u + d, v + d + h}, 0, 0));
faces.put("north", new BbFace(new float[]{u + d, v + d, u + d + w, v + d + h}, 0, 0));
faces.put("west", new BbFace(new float[]{u + d + w, v + d, u + d + w + d, v + d + h}, 0, 0));
faces.put("south", new BbFace(new float[]{u + d + w + d, v + d, u + d + 2*w + d, v + d + h}, 0, 0));
if (Boolean.TRUE.equals(c.mirrorUv())) { // c.mirrorUv() is primitive boolean, just use it:
BbFace e = faces.get("east");
faces.put("east", faces.get("west"));
faces.put("west", e);
}
return faces;
}
public static class Vector3fDeserializer implements JsonDeserializer<Vector3f> { public static class Vector3fDeserializer implements JsonDeserializer<Vector3f> {
@Override @Override

@ -5,6 +5,7 @@ import java.util.List;
public record Bbmodel( public record Bbmodel(
int resolutionWidth, int resolutionWidth,
int resolutionHeight, int resolutionHeight,
boolean metaBoxUV,
List<BbCube> elements, List<BbCube> elements,
List<BbLocator> locators, List<BbLocator> locators,
List<BbGroup> groups, List<BbGroup> groups,

@ -120,6 +120,7 @@ public class ClosetScreen extends Screen {
private final @Nullable ClosetBlockEntity closet; private final @Nullable ClosetBlockEntity closet;
private CameraType previousCamera; private CameraType previousCamera;
private @Nullable Button leftArrow, rightArrow; private @Nullable Button leftArrow, rightArrow;
private @Nullable Button shareButton;
// Body/head yaw applied every tick. yRot (camera-facing yaw) is left untouched so // Body/head yaw applied every tick. yRot (camera-facing yaw) is left untouched so
// rotating doesn't pan the camera — only the player's visible body/head turn. // rotating doesn't pan the camera — only the player's visible body/head turn.
private float displayYaw = 0f; private float displayYaw = 0f;
@ -286,16 +287,19 @@ public class ClosetScreen extends Screen {
// Open the shared-cosmetic upload form. All validation + parsing happens in the // Open the shared-cosmetic upload form. All validation + parsing happens in the
// uploader's pipeline (SafePngReader + BbModelParser + BbmodelCodec); this button // uploader's pipeline (SafePngReader + BbModelParser + BbmodelCodec); this button
// just swaps the closet UI into the form's widgets. // just swaps the closet UI into the form's widgets.
addRenderableWidget(Button.builder( shareButton = Button.builder(
Component.literal("Share"), Component.literal("Share"),
b -> openShareForm() b -> openShareForm()
).bounds(this.width - 140, y, 60, 20).build()); ).bounds(this.width - 140, y, 60, 20).build();
shareButton.visible = SHARED_TAB.equals(selectedCategory);
addRenderableWidget(shareButton);
} }
private void selectCategory(String cat) { private void selectCategory(String cat) {
selectedCategory = cat; selectedCategory = cat;
cosmeticScroll = 0; cosmeticScroll = 0;
lastKnownSkinCount = -1; // force the next skin-tab tick to re-evaluate the grid lastKnownSkinCount = -1; // force the next skin-tab tick to re-evaluate the grid
if (shareButton != null) shareButton.visible = SHARED_TAB.equals(cat);
rebuildCosmeticRow(); rebuildCosmeticRow();
} }

@ -74,11 +74,12 @@ public final class BbmodelCodec {
private static Bbmodel readBbmodel(ByteBuf buf) { private static Bbmodel readBbmodel(ByteBuf buf) {
int w = VarInt.read(buf); int w = VarInt.read(buf);
int h = VarInt.read(buf); int h = VarInt.read(buf);
boolean metaBoxUv = false;
List<BbCube> elements = readList(buf, BbmodelCodec::readCube); List<BbCube> elements = readList(buf, BbmodelCodec::readCube);
List<BbLocator> locators = readList(buf, BbmodelCodec::readLocator); List<BbLocator> locators = readList(buf, BbmodelCodec::readLocator);
List<BbGroup> groups = readList(buf, BbmodelCodec::readGroup); List<BbGroup> groups = readList(buf, BbmodelCodec::readGroup);
List<BbOutlinerNode> outliner = readList(buf, BbmodelCodec::readOutlinerNode); List<BbOutlinerNode> outliner = readList(buf, BbmodelCodec::readOutlinerNode);
return new Bbmodel(w, h, elements, locators, groups, outliner); return new Bbmodel(w, h, metaBoxUv, elements, locators, groups, outliner);
} }
// ---- BbCube ---- // ---- BbCube ----
@ -123,7 +124,7 @@ public final class BbmodelCodec {
String key = readString(buf); String key = readString(buf);
faces.put(key, readFace(buf)); faces.put(key, readFace(buf));
} }
return new BbCube(uuid, name, from, to, inflate, origin, rotation, faces); return new BbCube(uuid, name, from, to, inflate, origin, rotation, faces, null, false, null);
} }
// ---- BbFace ---- // ---- BbFace ----

@ -33,7 +33,7 @@ class BbmodelCodecSecurityTest {
@Test @Test
void roundTripEmptyModel() { void roundTripEmptyModel() {
Bbmodel original = new Bbmodel(64, 64, List.of(), List.of(), List.of(), List.of()); Bbmodel original = new Bbmodel(64, 64, false, List.of(), List.of(), List.of(), List.of());
Bbmodel decoded = roundTrip(original); Bbmodel decoded = roundTrip(original);
assertEquals(64, decoded.resolutionWidth()); assertEquals(64, decoded.resolutionWidth());
assertEquals(64, decoded.resolutionHeight()); assertEquals(64, decoded.resolutionHeight());
@ -47,9 +47,10 @@ class BbmodelCodecSecurityTest {
new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), new Vector3f(0, 0, 0), new Vector3f(1, 1, 1),
0f, 0f,
new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), new Vector3f(0, 0, 0),
Map.of() Map.of(),
null, false, null
); );
Bbmodel original = new Bbmodel(64, 64, Bbmodel original = new Bbmodel(64, 64, false,
List.of(cube), List.of(), List.of(), List.of(cube), List.of(), List.of(),
List.of(new BbOutlinerNode.ElementRef("uuid-1"))); List.of(new BbOutlinerNode.ElementRef("uuid-1")));
Bbmodel decoded = roundTrip(original); Bbmodel decoded = roundTrip(original);
@ -63,12 +64,13 @@ class BbmodelCodecSecurityTest {
new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), new Vector3f(0, 0, 0), new Vector3f(1, 1, 1),
0f, 0f,
new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), new Vector3f(0, 0, 0),
Map.of()); Map.of(),
null, false, null);
BbLocator loc = new BbLocator("l", "loc", BbLocator loc = new BbLocator("l", "loc",
new Vector3f(1, 2, 3), new Vector3f(0, 0, 0)); new Vector3f(1, 2, 3), new Vector3f(0, 0, 0));
BbGroup grp = new BbGroup("g", "group", BbGroup grp = new BbGroup("g", "group",
new Vector3f(0, 0, 0), new Vector3f(0, 0, 0)); new Vector3f(0, 0, 0), new Vector3f(0, 0, 0));
Bbmodel original = new Bbmodel(64, 64, Bbmodel original = new Bbmodel(64, 64, false,
List.of(cube), List.of(loc), List.of(grp), List.of(cube), List.of(loc), List.of(grp),
List.of(new BbOutlinerNode.GroupRef("g", List.of( List.of(new BbOutlinerNode.GroupRef("g", List.of(
new BbOutlinerNode.ElementRef("c"), new BbOutlinerNode.ElementRef("c"),
@ -121,7 +123,7 @@ class BbmodelCodecSecurityTest {
// A valid decode is followed by extra bytes — callers MUST verify // A valid decode is followed by extra bytes — callers MUST verify
// readableBytes() == 0 after decode (we do so in SharedCosmeticCache/uploader). // readableBytes() == 0 after decode (we do so in SharedCosmeticCache/uploader).
// Codec itself doesn't enforce that; this test documents the contract. // Codec itself doesn't enforce that; this test documents the contract.
Bbmodel original = new Bbmodel(64, 64, List.of(), List.of(), List.of(), List.of()); Bbmodel original = new Bbmodel(64, 64, false, List.of(), List.of(), List.of(), List.of());
ByteBuf buf = Unpooled.buffer(); ByteBuf buf = Unpooled.buffer();
try { try {
BbmodelCodec.CODEC.encode(buf, original); BbmodelCodec.CODEC.encode(buf, original);
@ -149,14 +151,16 @@ class BbmodelCodecSecurityTest {
BbCube cubeA = new BbCube("u", "n", BbCube cubeA = new BbCube("u", "n",
new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), new Vector3f(0, 0, 0), new Vector3f(1, 1, 1),
0f, 0f,
new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), a); new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), a,
null, false, null);
BbCube cubeB = new BbCube("u", "n", BbCube cubeB = new BbCube("u", "n",
new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), new Vector3f(0, 0, 0), new Vector3f(1, 1, 1),
0f, 0f,
new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), b); new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), b,
Bbmodel modelA = new Bbmodel(64, 64, List.of(cubeA), List.of(), List.of(), null, false, null);
Bbmodel modelA = new Bbmodel(64, 64, false, List.of(cubeA), List.of(), List.of(),
List.of(new BbOutlinerNode.ElementRef("u"))); List.of(new BbOutlinerNode.ElementRef("u")));
Bbmodel modelB = new Bbmodel(64, 64, List.of(cubeB), List.of(), List.of(), Bbmodel modelB = new Bbmodel(64, 64, false, List.of(cubeB), List.of(), List.of(),
List.of(new BbOutlinerNode.ElementRef("u"))); List.of(new BbOutlinerNode.ElementRef("u")));
assertArrayEquals(encode(modelA), encode(modelB), assertArrayEquals(encode(modelA), encode(modelB),

Loading…
Cancel
Save