From 47885d15a8088c27c44eea97cc65fe410d2e4eac Mon Sep 17 00:00:00 2001 From: Algent Date: Thu, 3 Sep 2026 22:50:05 +0200 Subject: [PATCH] test: characterize post microblock client --- .../PostMicroblockClientFunctionalTest.java | 212 ++++++++++ src/test/fixtures/README.md | 16 + .../ReferenceScalaPostMicroblockClient.scala | 67 +++ ...tMicroblockClientCharacterizationTest.java | 389 ++++++++++++++++++ .../compat/ReferencePostClientBase.class.b64 | 1 + ...ferenceScalaPostMicroblockClient.class.b64 | 1 + 6 files changed, 686 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/PostMicroblockClientFunctionalTest.java create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaPostMicroblockClient.scala create mode 100644 src/test/java/codechicken/microblock/PostMicroblockClientCharacterizationTest.java create mode 100644 src/test/resources/compat/ReferencePostClientBase.class.b64 create mode 100644 src/test/resources/compat/ReferenceScalaPostMicroblockClient.class.b64 diff --git a/src/functionalTest/java/codechicken/multipart/test/PostMicroblockClientFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/PostMicroblockClientFunctionalTest.java new file mode 100644 index 0000000..9a058e8 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/PostMicroblockClientFunctionalTest.java @@ -0,0 +1,212 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import net.minecraft.util.IIcon; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.FaceMicroClass$; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; +import codechicken.microblock.PostMicroClass$; +import codechicken.microblock.PostMicroblock; +import codechicken.microblock.PostMicroblock$class; +import codechicken.microblock.PostMicroblockClient; +import codechicken.microblock.PostMicroblockClient$class; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import scala.collection.mutable.ArrayBuffer; + +class PostMicroblockClientFunctionalTest { + + @Test + void realCoverAndPostBoundsSplitAndResetAcrossAllAxes() { + int material = MicroMaterialRegistry.materialID("minecraft:stone"); + int[] longAxes = { 1, 2, 0 }; + for (int axis = 0; axis < 3; axis++) { + ClientProbe part = new ClientProbe(material); + assertNull(part.renderBounds1()); + assertNull(part.renderBounds2()); + part.setShape(2, axis); + Microblock crossing = MicroblockGenerator.create(PostMicroClass$.MODULE$, material, false); + crossing.setShape(4, (axis + 1) % 3); + Microblock low = MicroblockGenerator.create(FaceMicroClass$.MODULE$, material, false); + Microblock high = MicroblockGenerator.create(FaceMicroClass$.MODULE$, material, false); + low.setShape(1, axis * 2); + high.setShape(1, axis * 2 + 1); + SlotTile tile = new SlotTile(); + tile.slots[axis * 2] = low; + tile.slots[axis * 2 + 1] = high; + ArrayBuffer parts = new ArrayBuffer<>(); + parts.$plus$eq(part); + parts.$plus$eq(low); + parts.$plus$eq(high); + parts.$plus$eq(crossing); + tile.partList_$eq(parts); + part.bind(tile); + Cuboid6 physical = part.getBounds().copy(); + part.onAdded(); + assertEquals(0.125, coordinate(part.renderBounds1().min, longAxes[axis]), 1e-12); + assertEquals(0.25, coordinate(part.renderBounds1().max, longAxes[axis]), 1e-12); + assertEquals(0.75, coordinate(part.renderBounds2().min, longAxes[axis]), 1e-12); + // The second segment is copied from physical bounds, after face clipping of only the first segment. + assertEquals(1, coordinate(part.renderBounds2().max, longAxes[axis]), 1e-12); + assertBounds(physical, part.getBounds()); + parts.remove(3); + part.onPartChanged(crossing); + assertNull(part.renderBounds2()); + assertEquals(0.125, coordinate(part.renderBounds1().min, longAxes[axis]), 1e-12); + assertEquals(0.875, coordinate(part.renderBounds1().max, longAxes[axis]), 1e-12); + tile.slots[axis * 2] = null; + tile.slots[axis * 2 + 1] = null; + part.onPartChanged(low); + assertBounds(physical, part.renderBounds1()); + assertNotSame(part.getBounds(), part.renderBounds1()); + } + } + + private static void assertBounds(Cuboid6 expected, Cuboid6 actual) { + assertArrayEquals( + new double[] { expected.min.x, expected.min.y, expected.min.z, expected.max.x, expected.max.y, + expected.max.z }, + new double[] { actual.min.x, actual.min.y, actual.min.z, actual.max.x, actual.max.y, actual.max.z }, + 1e-12); + } + + private static double coordinate(Vector3 vector, int axis) { + return axis == 0 ? vector.x : axis == 1 ? vector.y : vector.z; + } + + private static final class SlotTile extends TileMultipart { + + final TMultiPart[] slots = new TMultiPart[6]; + + @Override + public TMultiPart partMap(int slot) { + return slots[slot]; + } + } + + /** + * Executes client geometry with real Forge factories without requesting the stripped client factory entry point. + */ + private static final class ClientProbe extends Microblock implements PostMicroblockClient { + + private Cuboid6 first; + private Cuboid6 second; + + ClientProbe(int material) { + super(material); + PostMicroblockClient$class.$init$(this); + } + + @Override + public Cuboid6 renderBounds1() { + return first; + } + + @Override + public void renderBounds1_$eq(Cuboid6 value) { + first = value; + } + + @Override + public Cuboid6 renderBounds2() { + return second; + } + + @Override + public void renderBounds2_$eq(Cuboid6 value) { + second = value; + } + + @Override + public PostMicroClass$ microClass() { + return PostMicroClass$.MODULE$; + } + + @Override + public Cuboid6 getBounds() { + return PostMicroblock$class.getBounds(this); + } + + @Override + public List getOcclusionBoxes() { + return PostMicroblock$class.getOcclusionBoxes(this); + } + + @Override + public List getPartialOcclusionBoxes() { + return getOcclusionBoxes(); + } + + @Override + public int itemClassID() { + return PostMicroblock$class.itemClassID(this); + } + + @Override + public float getResistanceFactor() { + return PostMicroblock$class.getResistanceFactor(this); + } + + @Override + public IIcon getBrokenIcon(int side) { + throw new AssertionError("GPU icon lookup"); + } + + @Override + public void render(Vector3 pos, int pass) { + PostMicroblockClient$class.render(this, pos, pass); + } + + @Override + public void recalcBounds() { + PostMicroblockClient$class.recalcBounds(this); + } + + @Override + public void shrinkFace(int side) { + PostMicroblockClient$class.shrinkFace(this, side); + } + + @Override + public void shrinkPost(PostMicroblock post) { + PostMicroblockClient$class.shrinkPost(this, post); + } + + @Override + public boolean thisShrinks(PostMicroblock post) { + return PostMicroblockClient$class.thisShrinks(this, post); + } + + @Override + public void onAdded() { + PostMicroblockClient$class.onAdded(this); + } + + @Override + public void onPartChanged(TMultiPart part) { + PostMicroblockClient$class.onPartChanged(this, part); + } + + public void codechicken$microblock$PostMicroblockClient$$super$onAdded() { + super.onAdded(); + } + + public void codechicken$microblock$PostMicroblockClient$$super$read(MCDataInput packet) { + super.read(packet); + } + + public boolean codechicken$microblock$PostMicroblock$$super$occlusionTest(TMultiPart part) { + return super.occlusionTest(part); + } + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 43a8f33..61c60f5 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -233,3 +233,19 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchange | `ReferencePostMicroblockBase` | `a6bed1c01fd74be061edfa287b724679c4cdbcb9c550cc3a3304ebda6973c7ac` | | `ReferenceScalaPostMicroblock` | `ceafadad14da1f749ec60af6609d1b2eb7afa0666425fff25271dee1e46a7bc2` | | `ReferencePostFace` | `066037dc6000fbcf2404583ff4ba8a9048b140beb0e1c27700564284bc4d8c06` | + +### `ReferenceScalaPostMicroblockClient` + +`scala/codechicken/multipart/compat/ReferenceScalaPostMicroblockClient.scala` freezes the client forwarders and +lifecycle superclass predecessors. Tests observe render dispatch, material reuse, repeated virtual getters, +equality receiver/order, collection `foreach`, split/reset behavior and super-call failure ordering. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouched dev jar at `1c7dd14` +(SHA-256 `6c80983b6ba49bf7bc6c30248b9af4bdb0954bb1091ebd0e2ab4d823b994697b`). Source SHA-256 is +`6a04c03977858ce01ebd4951d105dba8be926977d0c5892aeec469c17f89ac39`. Frozen class files under +`src/test/resources/compat/` have these SHA-256s; do not regenerate against the port: + +| Class | SHA-256 | +| --- | --- | +| `ReferencePostClientBase` | `88a996255cae7f981f4386eba5bc1d4b5308c2eef900d120b105ec5727536342` | +| `ReferenceScalaPostMicroblockClient` | `71afd739893a0cff7022c758cccff8a653a219c7937310df7b893b31f0ef82a4` | diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaPostMicroblockClient.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaPostMicroblockClient.scala new file mode 100644 index 0000000..6b652f6 --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaPostMicroblockClient.scala @@ -0,0 +1,67 @@ +package codechicken.multipart.compat + +import codechicken.lib.data.MCDataInput +import codechicken.lib.vec.Cuboid6 +import codechicken.microblock.{Microblock, PostMicroblock, PostMicroblockClient} +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial +import codechicken.multipart.TMultiPart + +abstract class ReferencePostClientBase extends Microblock(41) { + var events: java.util.List[String] = new java.util.ArrayList[String]() + var failSuper = false + var seenPacket: MCDataInput = null + override def onAdded() { events.add("superAdded"); if (failSuper) throw new IllegalStateException("super") } + override def read(packet: MCDataInput) { + events.add("superRead") + if (failSuper) throw new IllegalStateException("super") + seenPacket = packet + } + override def onPartChanged(part: TMultiPart) { events.add("superChanged") } +} + +/** Frozen client forwarders with observable virtual dispatch, equality and lifecycle predecessors. */ +class ReferenceScalaPostMicroblockClient extends ReferencePostClientBase with PostMicroblockClient { + var label = "self" + var selectedMaterial: IMicroMaterial = null + var bounds = new Cuboid6(0.375, 0, 0.375, 0.625, 1, 0.625) + var selectedSize = 2 + var sizeAdvance = 0 + var selectedShape = 0 + var shapeAdvance = 0 + var transparent = false + var flipTransparency = false + var runRecalc = true + var runShrinks = true + var forceEquality = false + var equalityResult = false + + override def getIMaterial = { events.add("material"); selectedMaterial } + override def getBounds = { events.add("bounds:" + label); bounds } + override def getSize = { + events.add("size:" + label) + val result = selectedSize + selectedSize += sizeAdvance + result + } + override def getShape = { + events.add("shape:" + label) + val result = selectedShape + selectedShape += shapeAdvance + result + } + override def isTransparent = { + events.add("transparent:" + label) + val result = transparent + if (flipTransparency) transparent = !transparent + result + } + override def tile = { events.add("tile:" + label); super.tile } + override def recalcBounds() { events.add("recalc"); if (runRecalc) super.recalcBounds() } + override def shrinkFace(side: Int) { events.add("face:" + side); if (runShrinks) super.shrinkFace(side) } + override def shrinkPost(post: PostMicroblock) { events.add("post"); if (runShrinks) super.shrinkPost(post) } + override def equals(other: Any): Boolean = { + events.add("equals:" + label) + if (forceEquality) equalityResult else super.equals(other) + } + override def hashCode = System.identityHashCode(this) +} diff --git a/src/test/java/codechicken/microblock/PostMicroblockClientCharacterizationTest.java b/src/test/java/codechicken/microblock/PostMicroblockClientCharacterizationTest.java new file mode 100644 index 0000000..2116e08 --- /dev/null +++ b/src/test/java/codechicken/microblock/PostMicroblockClientCharacterizationTest.java @@ -0,0 +1,389 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import scala.collection.Seq; +import scala.collection.mutable.ArrayBuffer; + +class PostMicroblockClientCharacterizationTest { + + @Test + void onlyMinusOneRendersPhysicalBoundsAndAllOtherPassesUseBothSegments() throws Exception { + Fixture f = new Fixture(); + assertEquals(41, f.part.material()); + assertNull(f.client.renderBounds1()); + assertNull(f.client.renderBounds2()); + Cuboid6 first = new Cuboid6(0.1, 0.2, 0.3, 0.4, 0.5, 0.6); + Cuboid6 second = new Cuboid6(0.4, 0.5, 0.6, 0.7, 0.8, 0.9); + f.client.renderBounds1_$eq(first); + f.client.renderBounds2_$eq(second); + for (int pass : new int[] { -1, -2, 0, 1 }) { + f.clear(); + f.client.render(f.position, pass); + if (pass == -1) { + f.assertDraw(0, f.bounds, pass); + assertEquals(6, f.faces.size()); + assertEquals(Arrays.asList("material", "bounds:self"), f.events); + } else { + f.assertDraw(0, first, pass); + f.assertDraw(6, second, pass); + assertEquals(12, f.faces.size()); + assertEquals(Collections.singletonList("material"), f.events); + } + } + f.client.renderBounds2_$eq(null); + f.clear(); + f.client.render(f.position, 0); + assertEquals(6, f.faces.size()); + } + + @Test + void renderCachesMaterialButReadsTheSecondSegmentAfterTheFirstDraw() throws Exception { + Fixture f = new Fixture(); + Cuboid6 second = new Cuboid6(0.2, 0.6, 0.2, 0.8, 1, 0.8); + f.client.renderBounds1_$eq(f.bounds); + f.afterFace = () -> { + f.setUnchecked(f.part, "selectedMaterial", IMicroMaterial.class, null); + f.client.renderBounds2_$eq(second); + }; + f.client.render(f.position, 0); + f.assertDraw(0, f.bounds, 0); + f.assertDraw(6, second, 0); + assertEquals(12, f.faces.size()); + assertEquals(Collections.singletonList("material"), f.events); + f.afterFace = null; + f.clear(); + assertThrows(NullPointerException.class, () -> f.client.render(f.position, -1)); + assertEquals(Arrays.asList("material", "bounds:self"), f.events); + } + + @Test + void shrinkPriorityUsesSizeThenTransparencyThenAxisWithRepeatedVirtualReads() throws Exception { + Fixture f = new Fixture(); + Microblock other = f.newPart("other"); + for (int ownSize : new int[] { -1, 2, 4, 6 }) { + for (int otherSize : new int[] { -1, 2, 4, 6 }) { + for (boolean ownTransparent : new boolean[] { false, true }) { + for (boolean otherTransparent : new boolean[] { false, true }) { + for (int axis = 0; axis < 3; axis++) { + f.set(f.part, "selectedSize", int.class, ownSize); + f.set(other, "selectedSize", int.class, otherSize); + f.set(f.part, "transparent", boolean.class, ownTransparent); + f.set(other, "transparent", boolean.class, otherTransparent); + f.set(f.part, "selectedShape", int.class, axis); + f.set(other, "selectedShape", int.class, 1); + f.clear(); + boolean expected = ownSize != otherSize ? ownSize < otherSize + : ownTransparent != otherTransparent ? ownTransparent : axis > 1; + assertEquals(expected, f.client.thisShrinks((PostMicroblock) other)); + List calls = new ArrayList<>(Arrays.asList("size:self", "size:other")); + if (ownSize != otherSize) calls.addAll(Arrays.asList("size:self", "size:other")); + else { + calls.addAll(Arrays.asList("transparent:self", "transparent:other")); + calls.addAll( + ownTransparent != otherTransparent + ? Collections.singletonList("transparent:self") + : Arrays.asList("shape:self", "shape:other")); + } + assertEquals(calls, f.events); + } + } + } + } + } + f.set(f.part, "selectedSize", int.class, 2); + f.set(f.part, "sizeAdvance", int.class, 5); + f.set(other, "selectedSize", int.class, 3); + assertFalse(f.client.thisShrinks((PostMicroblock) other)); + f.set(f.part, "sizeAdvance", int.class, 0); + f.set(f.part, "selectedSize", int.class, 3); + f.set(f.part, "transparent", boolean.class, false); + f.set(f.part, "flipTransparency", boolean.class, true); + f.set(other, "transparent", boolean.class, true); + assertTrue(f.client.thisShrinks((PostMicroblock) other)); + } + + @Test + void lifecycleKeepsSuperOrderingPacketIdentityAndShortCircuitOnFailure() throws Exception { + Fixture f = new Fixture(); + f.set(f.part, "runRecalc", boolean.class, false); + f.part.onAdded(); + assertEquals(Arrays.asList("superAdded", "recalc"), f.events); + f.clear(); + MCDataInput packet = (MCDataInput) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, args) -> { throw new AssertionError(method.getName()); }); + f.part.read(packet); + assertSame(packet, f.part.getClass().getMethod("seenPacket").invoke(f.part)); + assertEquals(Arrays.asList("superRead", "recalc"), f.events); + f.clear(); + f.part.onPartChanged(null); + assertEquals(Collections.singletonList("recalc"), f.events); + f.set(f.part, "failSuper", boolean.class, true); + f.clear(); + assertThrows(IllegalStateException.class, f.part::onAdded); + assertEquals(Collections.singletonList("superAdded"), f.events); + f.clear(); + assertThrows(IllegalStateException.class, () -> f.part.read(packet)); + assertEquals(Collections.singletonList("superRead"), f.events); + } + + @Test + void equalityUsesTheOtherReceiverEvenForTheSameObjectAndCanSuppressShrinking() throws Exception { + Fixture f = new Fixture(); + Microblock other = f.newPart("other"); + f.set(other, "forceEquality", boolean.class, true); + f.set(other, "equalityResult", boolean.class, true); + f.client.shrinkPost((PostMicroblock) other); + assertEquals(Arrays.asList("post", "equals:other"), f.events); + f.clear(); + f.client.shrinkPost(f.client); + assertEquals(Arrays.asList("post", "equals:self"), f.events); + f.set(f.part, "forceEquality", boolean.class, true); + f.set(f.part, "equalityResult", boolean.class, false); + f.clear(); + f.client.shrinkPost(f.client); + assertEquals( + Arrays.asList( + "post", + "equals:self", + "size:self", + "size:self", + "transparent:self", + "transparent:self", + "shape:self", + "shape:self"), + f.events); + } + + @Test + void splitBoundsCopyOnceAndReadOtherBoundsAndOwnAxisTwice() throws Exception { + Fixture f = new Fixture(); + Microblock other = f.newPart("other"); + Cuboid6 crossing = new Cuboid6(0, 0.25, 0, 1, 0.75, 1); + f.set(other, "bounds", Cuboid6.class, crossing); + f.set(other, "selectedSize", int.class, 4); + f.client.renderBounds1_$eq(f.bounds.copy()); + f.client.shrinkPost((PostMicroblock) other); + assertEquals(0.25, f.client.renderBounds1().max.y); + assertEquals(0.75, f.client.renderBounds2().min.y); + assertNotSame(f.bounds, f.client.renderBounds2()); + assertEquals(0, f.bounds.min.y); + assertEquals(1, f.bounds.max.y); + assertEquals(2, Collections.frequency(f.events, "bounds:other")); + assertEquals(2, Collections.frequency(f.events, "shape:self")); + Cuboid6 second = f.client.renderBounds2(); + f.clear(); + f.client.shrinkPost((PostMicroblock) other); + assertSame(second, f.client.renderBounds2()); + assertFalse(f.events.contains("bounds:self")); + f.client.renderBounds1_$eq(Cuboid6.full.copy()); + f.client.renderBounds2_$eq(Cuboid6.full.copy()); + f.set(f.part, "shapeAdvance", int.class, 1); + f.client.shrinkPost((PostMicroblock) other); + assertEquals(0.25, f.client.renderBounds1().max.y); + assertEquals(1, f.client.renderBounds2().min.z); + assertEquals(0, f.client.renderBounds2().min.y); + } + + @Test + void recalculationResetsThenDispatchesBothFacesAndTheCollectionsForeachInOrder() throws Exception { + Fixture f = new Fixture(); + Microblock equal = f.newPart("equal"); + Microblock other = f.newPart("other"); + f.set(equal, "forceEquality", boolean.class, true); + f.set(equal, "equalityResult", boolean.class, true); + f.set(f.part, "runShrinks", boolean.class, false); + ArrayBuffer parts = new ArrayBuffer<>(); + for (TMultiPart p : Arrays.asList(null, new BarePart("bare"), f.part, equal, other)) parts.$plus$eq(p); + @SuppressWarnings("unchecked") + Seq sequence = (Seq) Proxy + .newProxyInstance(getClass().getClassLoader(), new Class[] { Seq.class }, (proxy, method, args) -> { + f.events.add(method.getName()); + return method.invoke(parts, args); + }); + TileMultipart tile = new TileMultipart(); + tile.partList_$eq(sequence); + f.part.bind(tile); + f.client.renderBounds2_$eq(Cuboid6.full); + f.clear(); + f.client.recalcBounds(); + assertEquals( + Arrays.asList( + "recalc", + "bounds:self", + "shape:self", + "face:0", + "shape:self", + "face:1", + "tile:self", + "foreach", + "equals:self", + "equals:equal", + "equals:other", + "post"), + f.events); + assertNotSame(f.bounds, f.client.renderBounds1()); + assertBounds(f.bounds, f.client.renderBounds1()); + assertNull(f.client.renderBounds2()); + } + + @Test + void faceFiltersAndFailuresPreserveStateAndCallOrdering() throws Exception { + Fixture f = new Fixture(); + SlotTile tile = new SlotTile(); + f.part.bind(tile); + Cuboid6 render = f.bounds.copy(); + f.client.renderBounds1_$eq(render); + f.client.shrinkFace(0); + tile.part = new BarePart("other"); + f.client.shrinkFace(0); + assertSame(render, f.client.renderBounds1()); + assertBounds(f.bounds, render); + tile.part = new BarePart(null); + assertThrows(NullPointerException.class, () -> f.client.shrinkFace(0)); + tile.part = new BarePart("mcr_face"); + assertThrows(ClassCastException.class, () -> f.client.shrinkFace(0)); + f.client.renderBounds2_$eq(render); + f.set(f.part, "bounds", Cuboid6.class, null); + assertThrows(NullPointerException.class, f.client::recalcBounds); + assertSame(render, f.client.renderBounds2()); + } + + private static void assertBounds(Cuboid6 expected, Cuboid6 actual) { + assertArrayEquals( + new double[] { expected.min.x, expected.min.y, expected.min.z, expected.max.x, expected.max.y, + expected.max.z }, + new double[] { actual.min.x, actual.min.y, actual.min.z, actual.max.x, actual.max.y, actual.max.z }, + 1e-12); + } + + private static final class BarePart extends TMultiPart { + + final String type; + + BarePart(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } + } + + private static final class SlotTile extends TileMultipart { + + TMultiPart part; + + @Override + public TMultiPart partMap(int slot) { + return part; + } + } + + private static final class Face { + + final Cuboid6 bounds; + final Vector3 position; + final int pass; + final int side; + + Face(Object[] args) { + position = (Vector3) args[0]; + pass = (Integer) args[1]; + bounds = (Cuboid6) args[2]; + side = MicroblockRender.face().get().side; + } + } + + private static final class Fixture extends ClassLoader { + + final List events = new ArrayList<>(); + final List faces = new ArrayList<>(); + final Microblock part; + final PostMicroblockClient client; + final Cuboid6 bounds; + final Vector3 position = new Vector3(2, 3, 4); + Runnable afterFace; + + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + for (String name : new String[] { "ReferencePostClientBase", "ReferenceScalaPostMicroblockClient" }) { + InputStream input = Objects + .requireNonNull(getClass().getResourceAsStream("/compat/" + name + ".class.b64")); + byte[] bytes; + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + bytes = Base64.getMimeDecoder().decode(scanner.next()); + } + defineClass(null, bytes, 0, bytes.length); + } + part = newPart("self"); + client = (PostMicroblockClient) part; + bounds = (Cuboid6) part.getClass().getMethod("bounds").invoke(part); + IMicroMaterial material = (IMicroMaterial) Proxy + .newProxyInstance(getParent(), new Class[] { IMicroMaterial.class }, (proxy, method, args) -> { + assertEquals("renderMicroFace", method.getName()); + faces.add(new Face(args)); + if (afterFace != null) afterFace.run(); + return null; + }); + set(part, "selectedMaterial", IMicroMaterial.class, material); + } + + Microblock newPart(String label) throws Exception { + Microblock result = (Microblock) loadClass( + "codechicken.multipart.compat.ReferenceScalaPostMicroblockClient").getConstructor().newInstance(); + set(result, "events", List.class, events); + set(result, "label", String.class, label); + return result; + } + + void set(Object target, String name, Class type, Object value) throws Exception { + target.getClass().getMethod(name + "_$eq", type).invoke(target, value); + } + + void setUnchecked(Object target, String name, Class type, Object value) { + try { + set(target, name, type, value); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + void clear() { + events.clear(); + faces.clear(); + } + + void assertDraw(int offset, Cuboid6 bounds, int pass) { + for (int side = 0; side < 6; side++) { + Face face = faces.get(offset + side); + assertSame(bounds, face.bounds); + assertSame(position, face.position); + assertEquals(pass, face.pass); + assertEquals(side, face.side); + } + } + } +} diff --git a/src/test/resources/compat/ReferencePostClientBase.class.b64 b/src/test/resources/compat/ReferencePostClientBase.class.b64 new file mode 100644 index 0000000..8556ad9 --- /dev/null +++ b/src/test/resources/compat/ReferencePostClientBase.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQAVQEANGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlUG9zdENsaWVudEJhc2UHAAEBACFjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2sHAAMBAChSZWZlcmVuY2VTY2FsYVBvc3RNaWNyb2Jsb2NrQ2xpZW50LnNjYWxhAQAeTHNjYWxhL3JlZmxlY3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBAvMGAQk0USEBAgICJRFxQ1UzZ0tKLGduWTNRX04kOAlcNWZdUhQVbV0zCwUNIRFBQjJwW0IMR08DAgYNBUlRLh43dVNCDCcPHgYCDwVZMW0cM2ZHIkw3bVszbwcBGSIBAQYRBS1xUSIBBwsFNTERQUM3akdKfCcNXDhkVyYRcQIEAgsbJhwnbxwybV8OXAciQgkBCQMREkECH2pdJiRoCEYBFCEJIQIhRAEDER0xAgExQQUCXQlhIRo8Zl1SHFgjAQ0RB2VxAiVEARsVCVlCJAEDdmkmZCciQQ8CCSkMZy9ZBQM/aRFBAVQ1dGlCERFlCggDRRVqEWEJBgJJBSkxb1kxbUMmEWFlSQEHIUosRy0aNAoFIUojQUIqdWUmdHdNAwInRyE5MQYBYQEKA2ETQUMzd0s6JDhvGBNmY1IRUQYNCQNFOUohYUwSAwlVcxcOHgUIYykKCRExARkDDUFIJQ0FB2cBAQsVAg0CDxU0WE0cO3RBITlRBwFhAQoDMRQhAzRiUzIcVg9dM3MrBTkEQwESORMJSTRFQQRDXz5kVy0ZOAkPbQIBGSFDAXkFaWEtWTVtJ1YEWE1dMCVLRiQiIUwfCQ9FUhQRIWEBbyExcQgBUSEKXQohQloxallOKwgvGjohER0JBQExQQUCCQshYl0zZl1CCzdtWzN1KwUZBUMBI0obBSklQgEkSAMRIRcNXjELBSEzEWEBN2pFJhEhKhICDBsOIxUNXjFKXUIsSA9DBE0BAQdJEUEnAh1NLFdNHCliRy4sR28YE2ZjUhFRRhQFCGMtCwkRMQFEERkBBgEpUQUHBlkxL1ozbyEGHDcuGjshERURBgEiEVQDHXlnLhEzZUsSJBIhDAUGKwIhCUVWAQVlFgxHDQYCLi8iKQEMFmEBBwYxAS9ZMmxLUkRRQRcBBUJtC1FiHDhRQ0okOAlbMW9PFiRHQ0EXXREVaRYMMQFfAxEBGE0dOxEFfQNXIgEDCgUFJCFBAytOazIkGA5VMXNpAgEABmV2ZW50cwEAEExqYXZhL3V0aWwvTGlzdDsBACRMamF2YS91dGlsL0xpc3Q8TGphdmEvbGFuZy9TdHJpbmc7PjsBAAlmYWlsU3VwZXIBAAFaAQAKc2VlblBhY2tldAEAIkxjb2RlY2hpY2tlbi9saWIvZGF0YS9NQ0RhdGFJbnB1dDsBABIoKUxqYXZhL3V0aWwvTGlzdDsMAAkACgkAAgARAQAEdGhpcwEANkxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVBvc3RDbGllbnRCYXNlOwEACmV2ZW50c18kZXEBABMoTGphdmEvdXRpbC9MaXN0OylWAQADeCQxAQADKClaDAAMAA0JAAIAGQEADWZhaWxTdXBlcl8kZXEBAAQoWilWAQAkKClMY29kZWNoaWNrZW4vbGliL2RhdGEvTUNEYXRhSW5wdXQ7DAAOAA8JAAIAHgEADnNlZW5QYWNrZXRfJGVxAQAlKExjb2RlY2hpY2tlbi9saWIvZGF0YS9NQ0RhdGFJbnB1dDspVgEAB29uQWRkZWQBAAMoKVYMAAkAEAoAAgAkAQAKc3VwZXJBZGRlZAgAJgEADmphdmEvdXRpbC9MaXN0BwAoAQADYWRkAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaDAAqACsLACkALAwADAAYCgACAC4BAB9qYXZhL2xhbmcvSWxsZWdhbFN0YXRlRXhjZXB0aW9uBwAwAQAFc3VwZXIIADIBAAY8aW5pdD4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYMADQANQoAMQA2AQAEcmVhZAEACXN1cGVyUmVhZAgAOQwAIAAhCgACADsBAAZwYWNrZXQBAA1vblBhcnRDaGFuZ2VkAQAlKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspVgEADHN1cGVyQ2hhbmdlZAgAQAEABHBhcnQBACJMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7AQAEKEkpVgwANABECgAEAEUBABNqYXZhL3V0aWwvQXJyYXlMaXN0BwBHDAA0ACMKAEgASQEACVNpZ25hdHVyZQEABENvZGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAA9MaW5lTnVtYmVyVGFibGUBACYoKUxqYXZhL3V0aWwvTGlzdDxMamF2YS9sYW5nL1N0cmluZzs+OwEAJyhMamF2YS91dGlsL0xpc3Q8TGphdmEvbGFuZy9TdHJpbmc7PjspVgEADVN0YWNrTWFwVGFibGUBAApTb3VyY2VGaWxlAQAZUnVudGltZVZpc2libGVBbm5vdGF0aW9ucwEACFNjYWxhU2lnBCEAAgAEAAAAAwACAAkACgABAEsAAAACAAsAAgAMAA0AAAACAA4ADwAAAAoAAQAJABAAAgBMAAAALwABAAEAAAAFKrQAErAAAAACAE0AAAAMAAEAAAAFABMAFAAAAE4AAAAGAAEAAAAKAEsAAAACAE8AAQAVABYAAgBMAAAAOgACAAIAAAAGKiu1ABKxAAAAAgBNAAAAFgACAAAABgATABQAAAAAAAYAFwAKAAEATgAAAAYAAQAAAAoASwAAAAIAUAABAAwAGAABAEwAAAAvAAEAAQAAAAUqtAAarAAAAAIATQAAAAwAAQAAAAUAEwAUAAAATgAAAAYAAQAAAAsAAQAbABwAAQBMAAAAOgACAAIAAAAGKhu1ABqxAAAAAgBNAAAAFgACAAAABgATABQAAAAAAAYAFwANAAEATgAAAAYAAQAAAAsAAQAOAB0AAQBMAAAALwABAAEAAAAFKrQAH7AAAAACAE0AAAAMAAEAAAAFABMAFAAAAE4AAAAGAAEAAAAMAAEAIAAhAAEATAAAADoAAgACAAAABiortQAfsQAAAAIATQAAABYAAgAAAAYAEwAUAAAAAAAGABcADwABAE4AAAAGAAEAAAAMAAEAIgAjAAEATAAAAFEAAwABAAAAHiq2ACUSJ7kALQIAVyq2AC+ZAA27ADFZEjO3ADe/sQAAAAMATQAAAAwAAQAAAB4AEwAUAAAATgAAAAYAAQAAAA0AUQAAAAMAAR0AAQA4ACEAAQBMAAAAaAADAAIAAAAjKrYAJRI6uQAtAgBXKrYAL5kADbsAMVkSM7cAN78qK7YAPLEAAAADAE0AAAAWAAIAAAAjABMAFAAAAAAAIwA9AA8AAQBOAAAADgADAAAADwAMABAAHQARAFEAAAADAAEdAAEAPgA/AAEATAAAAEEAAgACAAAADSq2ACUSQbkALQIAV7EAAAACAE0AAAAWAAIAAAANABMAFAAAAAAADQBCAEMAAQBOAAAABgABAAAAEwABADQAIwABAEwAAABSAAMAAQAAABwqECm3AEYquwBIWbcASrUAEioDtQAaKgG1AB+xAAAAAgBNAAAADAABAAAAHAATABQAAABOAAAAEgAEAAAACQAGAAoAEQALABYADAADAFIAAAACAAUAUwAAAAsAAQAGAAEAB3MACABUAAAAAwUAAA== diff --git a/src/test/resources/compat/ReferenceScalaPostMicroblockClient.class.b64 b/src/test/resources/compat/ReferenceScalaPostMicroblockClient.class.b64 new file mode 100644 index 0000000..cef03ba --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaPostMicroblockClient.class.b64 @@ -0,0 +1 @@ +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oYXBlAQAMc2hhcGVBZHZhbmNlAQALdHJhbnNwYXJlbnQBAAFaAQAQZmxpcFRyYW5zcGFyZW5jeQEACXJ1blJlY2FsYwEACnJ1blNocmlua3MBAA1mb3JjZUVxdWFsaXR5AQAOZXF1YWxpdHlSZXN1bHQBAA1yZW5kZXJCb3VuZHMxAQANcmVuZGVyQm91bmRzMgEAHygpTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjsMAB0AEAkAAgAgAQAEdGhpcwEAQUxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhUG9zdE1pY3JvYmxvY2tDbGllbnQ7AQARcmVuZGVyQm91bmRzMV8kZXEBACAoTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjspVgEAA3gkMQwAHgAQCQACACcBABFyZW5kZXJCb3VuZHMyXyRlcQEAOmNvZGVjaGlja2VuJG1pY3JvYmxvY2skUG9zdE1pY3JvYmxvY2tDbGllbnQkJHN1cGVyJG9uQWRkZWQBAAMoKVYBAAdvbkFkZGVkDAAsACsKAAQALQEAN2NvZGVjaGlja2VuJG1pY3JvYmxvY2skUG9zdE1pY3JvYmxvY2tDbGllbnQkJHN1cGVyJHJlYWQBACUoTGNvZGVjaGlja2VuL2xpYi9kYXRhL01DRGF0YUlucHV0OylWAQAEcmVhZAwAMQAwCgAEADIBAAZwYWNrZXQBACJMY29kZWNoaWNrZW4vbGliL2RhdGEvTUNEYXRhSW5wdXQ7AQAGcmVuZGVyAQAhKExjb2RlY2hpY2tlbi9saWIvdmVjL1ZlY3RvcjM7SSlWAQAxY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9ja0NsaWVudCRjbGFzcwcAOAEATihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9ja0NsaWVudDtMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpVgwANgA6CgA5ADsBAANwb3MBAB1MY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzOwEABHBhc3MBAA1vblBhcnRDaGFuZ2VkAQAlKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspVgEAUihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9ja0NsaWVudDtMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVYMAEAAQgoAOQBDAQAEcGFydAEAIkxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDsBADAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svUG9zdE1pY3JvYmxvY2tDbGllbnQ7KVYMACwARwoAOQBIAQBSKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrQ2xpZW50O0xjb2RlY2hpY2tlbi9saWIvZGF0YS9NQ0RhdGFJbnB1dDspVgwAMQBKCgA5AEsBAAt0aGlzU2hyaW5rcwEAKihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9jazspWgEAVyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9ja0NsaWVudDtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9jazspWgwATQBPCgA5AFABAAVvdGhlcgEAJ0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrOwEADWdldEJyb2tlbkljb24BAB0oSSlMbmV0L21pbmVjcmFmdC91dGlsL0lJY29uOwEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudCRjbGFzcwcAVgEARihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50O0kpTG5ldC9taW5lY3JhZnQvdXRpbC9JSWNvbjsMAFQAWAoAVwBZAQAEc2lkZQEADWFkZEhpdEVmZmVjdHMBAFooTG5ldC9taW5lY3JhZnQvdXRpbC9Nb3ZpbmdPYmplY3RQb3NpdGlvbjtMbmV0L21pbmVjcmFmdC9jbGllbnQvcGFydGljbGUvRWZmZWN0UmVuZGVyZXI7KVYBAIMoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDtMbmV0L21pbmVjcmFmdC91dGlsL01vdmluZ09iamVjdFBvc2l0aW9uO0xuZXQvbWluZWNyYWZ0L2NsaWVudC9wYXJ0aWNsZS9FZmZlY3RSZW5kZXJlcjspVgwAXABeCgBXAF8BAANoaXQBAClMbmV0L21pbmVjcmFmdC91dGlsL01vdmluZ09iamVjdFBvc2l0aW9uOwEADmVmZmVjdFJlbmRlcmVyAQAuTG5ldC9taW5lY3JhZnQvY2xpZW50L3BhcnRpY2xlL0VmZmVjdFJlbmRlcmVyOwEAEWFkZERlc3Ryb3lFZmZlY3RzAQAxKExuZXQvbWluZWNyYWZ0L2NsaWVudC9wYXJ0aWNsZS9FZmZlY3RSZW5kZXJlcjspVgEAWihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50O0xuZXQvbWluZWNyYWZ0L2NsaWVudC9wYXJ0aWNsZS9FZmZlY3RSZW5kZXJlcjspVgwAZQBnCgBXAGgBAAxyZW5kZXJTdGF0aWMBACEoTGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMztJKVoBAEooTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDtMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpWgwAagBsCgBXAG0BAA9nZXRSZW5kZXJCb3VuZHMBAEgoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDspTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjsMAG8AcAoAVwBxAQA6Y29kZWNoaWNrZW4kbWljcm9ibG9jayRQb3N0TWljcm9ibG9jayQkc3VwZXIkb2NjbHVzaW9uVGVzdAEAJShMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVoBACBjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydAcAdQEADW9jY2x1c2lvblRlc3QMAHcAdAoAdgB4AQAFbnBhcnQBAAptaWNyb0NsYXNzAQAqKClMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9DbGFzcyQ7AQArY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9jayRjbGFzcwcAfQEAUShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9jazspTGNvZGVjaGlja2VuL21pY3JvYmxvY2svUG9zdE1pY3JvQ2xhc3MkOwwAewB/CgB+AIABABFnZXRPY2NsdXNpb25Cb3hlcwEAEigpTGphdmEvdXRpbC9MaXN0OwEAOShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Qb3N0TWljcm9ibG9jazspTGphdmEvdXRpbC9MaXN0OwwAggCECgB+AIUBABhnZXRQYXJ0aWFsT2NjbHVzaW9uQm94ZXMMAIcAhAoAfgCIAQALaXRlbUNsYXNzSUQBAAMoKUkBACooTGNvZGVjaGlja2VuL21pY3JvYmxvY2svUG9zdE1pY3JvYmxvY2s7KUkMAIoAjAoAfgCNAQBMKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrO0xjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgwAdwCPCgB+AJABABNnZXRSZXNpc3RhbmNlRmFjdG9yAQADKClGAQAqKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrOylGDACSAJQKAH4AlQEAEmNhblBsYWNlVG9yY2hPblRvcAEAAygpWgwAlwBOCgB+AJkBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwwACwAMCQACAJwBAAlsYWJlbF8kZXEBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAD8oKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRlcmlhbDsMAA0ADgkAAgChAQAUc2VsZWN0ZWRNYXRlcmlhbF8kZXEBAEAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElNaWNyb01hdGVyaWFsOylWDAAPABAJAAIApQEACmJvdW5kc18kZXEMABEAEgkAAgCoAQAQc2VsZWN0ZWRTaXplXyRlcQEABChJKVYMABMAEgkAAgCsAQAPc2l6ZUFkdmFuY2VfJGVxDAAUABIJAAIArwEAEXNlbGVjdGVkU2hhcGVfJGVxDAAVABIJAAIAsgEAEHNoYXBlQWR2YW5jZV8kZXEMABYAFwkAAgC1AQAPdHJhbnNwYXJlbnRfJGVxAQAEKFopVgwAGAAXCQACALkBABRmbGlwVHJhbnNwYXJlbmN5XyRlcQwAGQAXCQACALwBAA1ydW5SZWNhbGNfJGVxDAAaABcJAAIAvwEADnJ1blNocmlua3NfJGVxDAAbABcJAAIAwgEAEWZvcmNlRXF1YWxpdHlfJGVxDAAcABcJAAIAxQEAEmVxdWFsaXR5UmVzdWx0XyRlcQEADGdldElNYXRlcmlhbAEABmV2ZW50cwwAyQCDCgACAMoBAAhtYXRlcmlhbAgAzAEADmphdmEvdXRpbC9MaXN0BwDOAQADYWRkAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaDADQANELAM8A0gwADQCgCgACANQBAAlnZXRCb3VuZHMBACZzY2FsYS9jb2xsZWN0aW9uL211dGFibGUvU3RyaW5nQnVpbGRlcgcA1wEABjxpbml0PgwA2QArCgDYANoBAAdib3VuZHM6CADcAQAGYXBwZW5kAQA8KExqYXZhL2xhbmcvT2JqZWN0OylMc2NhbGEvY29sbGVjdGlvbi9tdXRhYmxlL1N0cmluZ0J1aWxkZXI7DADeAN8KANgA4AwACwCbCgACAOIBAAh0b1N0cmluZwwA5ACbCgDYAOUMAA8AHwoAAgDnAQAHZ2V0U2l6ZQEABXNpemU6CADqDAARAIsKAAIA7AwAEwCLCgACAO4MAKoAqwoAAgDwAQAGcmVzdWx0AQAIZ2V0U2hhcGUBAAZzaGFwZToIAPQMABQAiwoAAgD2DAAVAIsKAAIA+AwAsQCrCgACAPoBAA1pc1RyYW5zcGFyZW50AQAMdHJhbnNwYXJlbnQ6CAD9DAAWAJgKAAIA/wwAGACYCgACAQEMALcAuAoAAgEDAQAEdGlsZQEAJygpTGNvZGVjaGlja2VuL211bHRpcGFydC9UaWxlTXVsdGlwYXJ0OwEABXRpbGU6CAEHDAEFAQYKAHYBCQEADHJlY2FsY0JvdW5kcwEABnJlY2FsYwgBDAwAGQCYCgACAQ4MAQsARwoAOQEQAQAKc2hyaW5rRmFjZQEABWZhY2U6CAETAQAac2NhbGEvcnVudGltZS9Cb3hlc1J1blRpbWUHARUBAAxib3hUb0ludGVnZXIBABYoSSlMamF2YS9sYW5nL0ludGVnZXI7DAEXARgKARYBGQwAGgCYCgACARsBADEoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svUG9zdE1pY3JvYmxvY2tDbGllbnQ7SSlWDAESAR0KADkBHgEACnNocmlua1Bvc3QBACooTGNvZGVjaGlja2VuL21pY3JvYmxvY2svUG9zdE1pY3JvYmxvY2s7KVYBAARwb3N0CAEiAQBXKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrQ2xpZW50O0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1Bvc3RNaWNyb2Jsb2NrOylWDAEgASQKADkBJQEABmVxdWFscwEAB2VxdWFsczoIASgMABsAmAoAAgEqDAAcAJgKAAIBLAEAEGphdmEvbGFuZy9PYmplY3QHAS4MAScA0QoBLwEwAQASTGphdmEvbGFuZy9PYmplY3Q7AQAIaGFzaENvZGUBABBqYXZhL2xhbmcvU3lzdGVtBwE0AQAQaWRlbnRpdHlIYXNoQ29kZQEAFShMamF2YS9sYW5nL09iamVjdDspSQwBNgE3CgE1ATgBABYoKUxqYXZhL2xhbmcvSXRlcmFibGU7DACHAIMKAAIBOwwAggCDCgACAT0BACooKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGFzczsMAHsAfAoAAgFACgAEANoBAAYkaW5pdCQMAUMBIQoAfgFEAQAsKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQ7KVYMAUMBRgoAVwFHDAFDAEcKADkBSQEABHNlbGYIAUsBABtjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDYHAU0GP9gAAAAAAAAGP+QAAAAAAAABAAkoREREREREKVYMANkBUwoBTgFUAQA7Y29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb01hdGVyaWFsUmVnaXN0cnkkSU1pY3JvTWF0ZXJpYWwHAVYBACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeQcBWAEADklNaWNyb01hdGVyaWFsAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEACVNpZ25hdHVyZQEAMSgpTGphdmEvdXRpbC9MaXN0PExjb2RlY2hpY2tlbi9saWIvdmVjL0N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