From b5503545b3efdb7f7cea9af9ef309bbce128dde3 Mon Sep 17 00:00:00 2001 From: MomokoKoigakubo Date: Wed, 22 Apr 2026 06:15:47 -0400 Subject: [PATCH] add box UV bbmodel support, enable unit tests, hide Share button outside library tab --- build.gradle | 5 ++ .../com/razz/dfashion/bbmodel/BbCube.java | 9 +++- .../razz/dfashion/bbmodel/BbModelParser.java | 47 +++++++++++++++++-- .../com/razz/dfashion/bbmodel/Bbmodel.java | 1 + .../dfashion/client/screen/ClosetScreen.java | 8 +++- .../dfashion/cosmetic/share/BbmodelCodec.java | 5 +- .../security/BbmodelCodecSecurityTest.java | 24 ++++++---- 7 files changed, 81 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 7d8f64a..f7fe52f 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,11 @@ neoForge { sourceSet(sourceSets.main) } } + + unitTest { + enable() + testedMod = mods."${mod_id}" + } } // Sets up a dependency configuration called 'localRuntime'. diff --git a/src/main/java/com/razz/dfashion/bbmodel/BbCube.java b/src/main/java/com/razz/dfashion/bbmodel/BbCube.java index b449e5b..59a747a 100644 --- a/src/main/java/com/razz/dfashion/bbmodel/BbCube.java +++ b/src/main/java/com/razz/dfashion/bbmodel/BbCube.java @@ -1,5 +1,6 @@ package com.razz.dfashion.bbmodel; +import org.jetbrains.annotations.Nullable; import org.joml.Vector3f; import java.util.Map; @@ -12,4 +13,10 @@ public record BbCube ( float inflate, Vector3f origin, Vector3f rotation, - Map faces){} + Map faces, + @Nullable + int[] uvOffset, + boolean mirrorUv, + @Nullable + Boolean boxUv +){} diff --git a/src/main/java/com/razz/dfashion/bbmodel/BbModelParser.java b/src/main/java/com/razz/dfashion/bbmodel/BbModelParser.java index 2d1bcd2..48db268 100644 --- a/src/main/java/com/razz/dfashion/bbmodel/BbModelParser.java +++ b/src/main/java/com/razz/dfashion/bbmodel/BbModelParser.java @@ -64,7 +64,7 @@ public class BbModelParser { JsonObject res = root.has("resolution") ? root.getAsJsonObject("resolution") : null; int resW = res != null && res.has("width") ? res.get("width").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 cubes = new ArrayList<>(); List locators = new ArrayList<>(); @@ -73,7 +73,10 @@ public class BbModelParser { JsonObject obj = e.getAsJsonObject(); String type = obj.has("type") ? obj.get("type").getAsString() : "cube"; 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)); // 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(); List outliner = GSON.fromJson(outlinerArr, new TypeToken>(){}.getType()); - Bbmodel model = new Bbmodel(resW, resH, cubes, locators, groups, outliner); + Bbmodel model = new Bbmodel(resW, resH, metaBoxUv, cubes, locators, groups, outliner); validate(model); return model; } @@ -188,6 +191,44 @@ public class BbModelParser { 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 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 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 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 { @Override diff --git a/src/main/java/com/razz/dfashion/bbmodel/Bbmodel.java b/src/main/java/com/razz/dfashion/bbmodel/Bbmodel.java index 20b74c5..edb0dc2 100644 --- a/src/main/java/com/razz/dfashion/bbmodel/Bbmodel.java +++ b/src/main/java/com/razz/dfashion/bbmodel/Bbmodel.java @@ -5,6 +5,7 @@ import java.util.List; public record Bbmodel( int resolutionWidth, int resolutionHeight, + boolean metaBoxUV, List elements, List locators, List groups, diff --git a/src/main/java/com/razz/dfashion/client/screen/ClosetScreen.java b/src/main/java/com/razz/dfashion/client/screen/ClosetScreen.java index 9d23f9e..6063051 100644 --- a/src/main/java/com/razz/dfashion/client/screen/ClosetScreen.java +++ b/src/main/java/com/razz/dfashion/client/screen/ClosetScreen.java @@ -120,6 +120,7 @@ public class ClosetScreen extends Screen { private final @Nullable ClosetBlockEntity closet; private CameraType previousCamera; private @Nullable Button leftArrow, rightArrow; + private @Nullable Button shareButton; // 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. 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 // uploader's pipeline (SafePngReader + BbModelParser + BbmodelCodec); this button // just swaps the closet UI into the form's widgets. - addRenderableWidget(Button.builder( + shareButton = Button.builder( Component.literal("Share"), 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) { selectedCategory = cat; cosmeticScroll = 0; lastKnownSkinCount = -1; // force the next skin-tab tick to re-evaluate the grid + if (shareButton != null) shareButton.visible = SHARED_TAB.equals(cat); rebuildCosmeticRow(); } diff --git a/src/main/java/com/razz/dfashion/cosmetic/share/BbmodelCodec.java b/src/main/java/com/razz/dfashion/cosmetic/share/BbmodelCodec.java index 5fe5a65..f475e7a 100644 --- a/src/main/java/com/razz/dfashion/cosmetic/share/BbmodelCodec.java +++ b/src/main/java/com/razz/dfashion/cosmetic/share/BbmodelCodec.java @@ -74,11 +74,12 @@ public final class BbmodelCodec { private static Bbmodel readBbmodel(ByteBuf buf) { int w = VarInt.read(buf); int h = VarInt.read(buf); + boolean metaBoxUv = false; List elements = readList(buf, BbmodelCodec::readCube); List locators = readList(buf, BbmodelCodec::readLocator); List groups = readList(buf, BbmodelCodec::readGroup); List 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 ---- @@ -123,7 +124,7 @@ public final class BbmodelCodec { String key = readString(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 ---- diff --git a/src/test/java/com/razz/dfashion/security/BbmodelCodecSecurityTest.java b/src/test/java/com/razz/dfashion/security/BbmodelCodecSecurityTest.java index b6c23dd..8e635e1 100644 --- a/src/test/java/com/razz/dfashion/security/BbmodelCodecSecurityTest.java +++ b/src/test/java/com/razz/dfashion/security/BbmodelCodecSecurityTest.java @@ -33,7 +33,7 @@ class BbmodelCodecSecurityTest { @Test 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); assertEquals(64, decoded.resolutionWidth()); assertEquals(64, decoded.resolutionHeight()); @@ -47,9 +47,10 @@ class BbmodelCodecSecurityTest { new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), 0f, 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(new BbOutlinerNode.ElementRef("uuid-1"))); Bbmodel decoded = roundTrip(original); @@ -63,12 +64,13 @@ class BbmodelCodecSecurityTest { new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), 0f, new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), - Map.of()); + Map.of(), + null, false, null); BbLocator loc = new BbLocator("l", "loc", new Vector3f(1, 2, 3), new Vector3f(0, 0, 0)); BbGroup grp = new BbGroup("g", "group", 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(new BbOutlinerNode.GroupRef("g", List.of( new BbOutlinerNode.ElementRef("c"), @@ -121,7 +123,7 @@ class BbmodelCodecSecurityTest { // A valid decode is followed by extra bytes — callers MUST verify // readableBytes() == 0 after decode (we do so in SharedCosmeticCache/uploader). // 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(); try { BbmodelCodec.CODEC.encode(buf, original); @@ -149,14 +151,16 @@ class BbmodelCodecSecurityTest { BbCube cubeA = new BbCube("u", "n", new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), 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", new Vector3f(0, 0, 0), new Vector3f(1, 1, 1), 0f, - new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), b); - Bbmodel modelA = new Bbmodel(64, 64, List.of(cubeA), List.of(), List.of(), + new Vector3f(0, 0, 0), new Vector3f(0, 0, 0), b, + null, false, null); + Bbmodel modelA = new Bbmodel(64, 64, false, List.of(cubeA), List.of(), List.of(), 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"))); assertArrayEquals(encode(modelA), encode(modelB),