From 6e807cce346cd1e845f7d69561d70ed81aba12c9 Mon Sep 17 00:00:00 2001 From: Algent Date: Thu, 3 Sep 2026 23:25:29 +0200 Subject: [PATCH] test: characterize hollow microblock trait --- .../HollowMicroblockTraitsFunctionalTest.java | 248 +++++++++++++++++ src/test/fixtures/README.md | 16 ++ .../ReferenceScalaHollowMicroblock.scala | 56 ++++ ...wMicroblockTraitsCharacterizationTest.java | 262 ++++++++++++++++++ .../ReferenceHollowMicroblockBase.class.b64 | 1 + .../ReferenceScalaHollowMicroblock.class.b64 | 1 + 6 files changed, 584 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/HollowMicroblockTraitsFunctionalTest.java create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaHollowMicroblock.scala create mode 100644 src/test/java/codechicken/microblock/HollowMicroblockTraitsCharacterizationTest.java create mode 100644 src/test/resources/compat/ReferenceHollowMicroblockBase.class.b64 create mode 100644 src/test/resources/compat/ReferenceScalaHollowMicroblock.class.b64 diff --git a/src/functionalTest/java/codechicken/multipart/test/HollowMicroblockTraitsFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/HollowMicroblockTraitsFunctionalTest.java new file mode 100644 index 0000000..d38b9fe --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/HollowMicroblockTraitsFunctionalTest.java @@ -0,0 +1,248 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import net.minecraft.nbt.NBTTagCompound; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutputWrapper; +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; +import codechicken.microblock.FaceMicroClass; +import codechicken.microblock.HollowMicroClass$; +import codechicken.microblock.HollowMicroblock; +import codechicken.microblock.ISidedHollowConnect; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import scala.MatchError; +import scala.collection.JavaConversions; +import scala.collection.Seq; + +class HollowMicroblockTraitsFunctionalTest { + + @Test + void generatedHollowsKeepAllThicknessesOrientationsAndLiveConnectorOpenings() { + Microblock part = create(); + HollowMicroblock hollow = (HollowMicroblock) part; + Connector connector = new Connector(); + part.bind(new TileMultipart() { + + @Override + public TMultiPart partMap(int slot) { + assertEquals(6, slot); + return connector; + } + }); + assertSame(HollowMicroClass$.MODULE$, part.microClass()); + assertEquals("mcr_hllw", part.getType()); + for (int thickness = 1; thickness <= 7; thickness++) { + for (int side = 0; side < 6; side++) { + part.setShape(thickness, side); + for (int opening : new int[] { 0, 1, 8, 11 }) { + connector.size = opening; + assertEquals(opening, hollow.getHollowSize()); + assertEquals(side, connector.lastSide); + double t = thickness / 8D; + double gap = opening / 16D; + assertEquals(t * (1 - gap * gap), volume(hollow.getCollisionBoxes()), 1e-12); + assertEquals(t * (0.75 * 0.75 - gap * gap), volume(hollow.getOcclusionBoxes()), 1e-12); + assertEquals(t * (1 - 0.75 * 0.75), volume(hollow.getPartialOcclusionBoxes()), 1e-12); + int axis = new int[] { 1, 2, 0 }[side / 2]; + List collision = hollow.getCollisionBoxes(); + List subparts = hollow.getSubParts(); + assertEquals(4, collision.size()); + assertEquals(4, subparts.size()); + for (int i = 0; i < 4; i++) { + Cuboid6 box = collision.get(i); + double[] min = { box.min.x, box.min.y, box.min.z }; + double[] max = { box.max.x, box.max.y, box.max.z }; + assertEquals((side & 1) == 0 ? 0 : 1 - t, min[axis], 1e-12); + assertEquals((side & 1) == 0 ? t : 1, max[axis], 1e-12); + assertBox(box, subparts.get(i)); + assertEquals(0, subparts.get(i).data); + if (opening > 0) { + boolean missesCenter = false; + for (int j = 0; j < 3; j++) if (j != axis) missesCenter |= max[j] < 0.5 || min[j] > 0.5; + assertTrue(missesCenter, "central connector must fit through the hollow cover"); + } + } + assertSame(FaceMicroClass.aBounds()[thickness * 16 + side], part.getBounds()); + } + } + } + } + + @Test + @SuppressWarnings("unchecked") + void partialBoxesRemainLiveJavaViewsAndBoundsKeepSignedIndexFailures() { + Microblock part = create(); + HollowMicroblock hollow = (HollowMicroblock) part; + HollowMicroClass$ factory = HollowMicroClass$.MODULE$; + Cuboid6[] bounds = FaceMicroClass.aBounds(); + Seq[] partial = factory.pBoxes(); + Cuboid6 sentinel = new Cuboid6(0.1, 0.2, 0.3, 0.4, 0.5, 0.6); + part.setShape(3, 0); + try { + Cuboid6[] replacementBounds = new Cuboid6[256]; + replacementBounds[48] = sentinel; + FaceMicroClass.aBounds_$eq(replacementBounds); + assertSame(sentinel, part.getBounds()); + Seq[] replacement = (Seq[]) new Seq[256]; + List backing = new ArrayList<>(Arrays.asList(sentinel)); + replacement[48] = JavaConversions.asScalaBuffer(backing); + factory.pBoxes_$eq(replacement); + List view = hollow.getPartialOcclusionBoxes(); + assertSame(sentinel, view.get(0)); + backing.add(Cuboid6.full); + assertEquals(2, view.size()); + view.remove(0); + assertSame(Cuboid6.full, backing.get(0)); + replacement[48] = null; + List nullView = hollow.getPartialOcclusionBoxes(); + assertNotNull(nullView); + assertThrows(NullPointerException.class, nullView::size); + part.shape_$eq((byte) 0); + assertNull(part.getBounds()); + for (int shape = -128; shape < 0; shape++) { + part.shape_$eq((byte) shape); + assertThrows(ArrayIndexOutOfBoundsException.class, part::getBounds); + assertThrows(ArrayIndexOutOfBoundsException.class, hollow::getPartialOcclusionBoxes); + } + part.setShape(3, 0); + factory.pBoxes_$eq((Seq[]) new Seq[1]); + assertThrows(ArrayIndexOutOfBoundsException.class, hollow::getPartialOcclusionBoxes); + factory.pBoxes_$eq(null); + FaceMicroClass.aBounds_$eq(null); + assertThrows(NullPointerException.class, hollow::getPartialOcclusionBoxes); + assertThrows(NullPointerException.class, part::getBounds); + } finally { + factory.pBoxes_$eq(partial); + FaceMicroClass.aBounds_$eq(bounds); + } + } + + @Test + void normalBoxesUseLiveBoundsAndKeepNullIndexAndInvalidSlotFailureOrder() { + Microblock part = create(); + HollowMicroblock hollow = (HollowMicroblock) part; + HollowMicroClass$ factory = HollowMicroClass$.MODULE$; + Cuboid6[] original = factory.occBounds(); + Cuboid6[] replacement = new Cuboid6[256]; + replacement[48] = new Cuboid6(0.1, 0.2, 0.3, 0.9, 0.8, 0.7); + try { + factory.occBounds_$eq(replacement); + part.setShape(3, 0); + List boxes = (List) hollow.getOcclusionBoxes(); + assertEquals(4, boxes.size()); + assertBox(new Cuboid6(0.75, 0.2, 0.25, 0.9, 0.8, 0.75), boxes.get(0)); + assertThrows(UnsupportedOperationException.class, () -> boxes.set(0, Cuboid6.full)); + replacement[48].max.x = 0.8; + assertEquals(0.8, hollow.getOcclusionBoxes().iterator().next().max.x); + assertEquals(0.9, boxes.get(0).max.x); + for (int slot = 6; slot < 16; slot++) { + part.setShape(3, slot); + assertThrows(NullPointerException.class, hollow::getOcclusionBoxes); + replacement[48 + slot] = Cuboid6.full; + assertEquals( + Integer.toString(slot) + " (of class java.lang.Integer)", + assertThrows(MatchError.class, hollow::getOcclusionBoxes).getMessage()); + } + part.shape_$eq((byte) -1); + assertThrows(ArrayIndexOutOfBoundsException.class, hollow::getOcclusionBoxes); + factory.occBounds_$eq(null); + assertThrows(NullPointerException.class, hollow::getOcclusionBoxes); + } finally { + factory.occBounds_$eq(original); + } + } + + @Test + void allFortyTwoShapesKeepMaterialAndPackedNbtAndDescriptionBytes() { + Microblock part = create(); + Microblock loaded = create(); + ByteArrayOutputStream prefixBytes = new ByteArrayOutputStream(); + MicroMaterialRegistry.writeMaterialID(new MCDataOutputWrapper(new DataOutputStream(prefixBytes)), 0); + byte[] prefix = prefixBytes.toByteArray(); + for (int thickness = 1; thickness <= 7; thickness++) { + for (int side = 0; side < 6; side++) { + byte shape = (byte) (thickness * 16 + side); + part.setShape(thickness, side); + NBTTagCompound tag = new NBTTagCompound(); + part.save(tag); + assertEquals(shape, tag.getByte("shape")); + assertEquals(MicroMaterialRegistry.materialName(0), tag.getString("material")); + loaded.load(tag); + assertEquals(shape, loaded.shape()); + assertEquals(0, loaded.material()); + assertSame(part.getBounds(), loaded.getBounds()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + part.writeDesc(new MCDataOutputWrapper(new DataOutputStream(bytes))); + byte[] expected = Arrays.copyOf(prefix, prefix.length + 1); + expected[prefix.length] = shape; + assertArrayEquals(expected, bytes.toByteArray()); + AtomicInteger reads = new AtomicInteger(); + MCDataInput input = (MCDataInput) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, args) -> { + assertEquals("readByte", method.getName()); + reads.incrementAndGet(); + return shape; + }); + loaded.setShape(1, 0); + loaded.readDesc(input); + assertEquals(1, reads.get()); + assertEquals(shape, loaded.shape()); + assertSame(part.getBounds(), loaded.getBounds()); + } + } + } + + private static Microblock create() { + return MicroblockGenerator.create(HollowMicroClass$.MODULE$, 0, false); + } + + private static double volume(Iterable boxes) { + double result = 0; + for (Cuboid6 box : boxes) result += (box.max.x - box.min.x) * (box.max.y - box.min.y) * (box.max.z - box.min.z); + return result; + } + + private static void assertBox(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 Connector extends TMultiPart implements ISidedHollowConnect { + + int size; + int lastSide; + + @Override + public String getType() { + return "test:hollow_connector"; + } + + @Override + public int getHollowSize(int side) { + lastSide = side; + return size; + } + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 61c60f5..1b71894 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -249,3 +249,19 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouche | --- | --- | | `ReferencePostClientBase` | `88a996255cae7f981f4386eba5bc1d4b5308c2eef900d120b105ec5727536342` | | `ReferenceScalaPostMicroblockClient` | `71afd739893a0cff7022c758cccff8a653a219c7937310df7b893b31f0ef82a4` | + +### `ReferenceScalaHollowMicroblock` + +`scala/codechicken/multipart/compat/ReferenceScalaHollowMicroblock.scala` freezes hollow-cover forwarders with a +real Scala trait super chain. Tests pin repeated tile/raw-shape reads, connector slot dispatch, collision geometry, +list mutability and copying, normal/partial occlusion short-circuiting and superclass failures. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouched dev jar at `556fd51` +(SHA-256 `8bffb00300087a3f202354a836c9f3f2cf22c4cb3b145e03c8c2ac893028c311`). Source SHA-256 is +`277b708d11e37a66f311759af50b1b1e98d84238550add0e98b43793d1f55ad5`. Frozen class files under +`src/test/resources/compat/` have these SHA-256s; do not regenerate against the port: + +| Class | SHA-256 | +| --- | --- | +| `ReferenceHollowMicroblockBase` | `a329f86030d4706ba07a478d36a2fe9fd6b4e2bcaac76bd92e128769cd91110e` | +| `ReferenceScalaHollowMicroblock` | `3df6e92864c984ad973a115c56495deee8fb8a8c4014dbd34786a2caf754c7f6` | diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaHollowMicroblock.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaHollowMicroblock.scala new file mode 100644 index 0000000..941fd3f --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaHollowMicroblock.scala @@ -0,0 +1,56 @@ +package codechicken.multipart.compat + +import codechicken.lib.vec.Cuboid6 +import codechicken.microblock.{HollowMicroblock, Microblock} +import codechicken.multipart.{TMultiPart, TileMultipart} + +abstract class ReferenceHollowMicroblockBase extends Microblock(0) { + var events: java.util.List[String] = new java.util.ArrayList[String]() + var superResult = true + var superFailure = false + override def occlusionTest(part: TMultiPart): Boolean = { + events.add("super") + if (superFailure) throw new IllegalStateException("super failure") + superResult + } +} + +/** Frozen forwarders retain the real Scala trait super chain and observable virtual getters. */ +class ReferenceScalaHollowMicroblock extends ReferenceHollowMicroblockBase with HollowMicroblock { + var firstTile: TileMultipart = null + var secondTile: TileMultipart = null + var tileReads = 0 + var firstShape: Byte = 48 + var secondShape: Byte = 48 + var shapeReads = 0 + var slot = 0 + var overrideSize = false + var hollowSize = 8 + var overrideCollision = false + var collision: java.util.List[Cuboid6] = null + var normal: java.lang.Iterable[Cuboid6] = java.util.Collections.singletonList(Cuboid6.full) + + override def tile = { + events.add("tile") + tileReads += 1 + if (tileReads == 1) firstTile else secondTile + } + override def shape = { + events.add("shape") + shapeReads += 1 + if (shapeReads == 1) firstShape else secondShape + } + override def getSlot = { events.add("slot"); slot } + override def getHollowSize = { + events.add("size") + if (overrideSize) hollowSize else super.getHollowSize + } + override def getCollisionBoxes: java.util.List[Cuboid6] = { + events.add("collision") + if (overrideCollision) collision else super.getCollisionBoxes + } + override def getOcclusionBoxes: java.lang.Iterable[Cuboid6] = { + events.add("normal:self") + normal + } +} diff --git a/src/test/java/codechicken/microblock/HollowMicroblockTraitsCharacterizationTest.java b/src/test/java/codechicken/microblock/HollowMicroblockTraitsCharacterizationTest.java new file mode 100644 index 0000000..2b8c600 --- /dev/null +++ b/src/test/java/codechicken/microblock/HollowMicroblockTraitsCharacterizationTest.java @@ -0,0 +1,262 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +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.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; +import codechicken.multipart.JNormalOcclusion; +import codechicken.multipart.JPartialOcclusion; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; + +class HollowMicroblockTraitsCharacterizationTest { + + @Test + void centerLookupRepeatsTheVirtualTileGetterAndUsesTheVirtualSlot() throws Exception { + Fixture f = new Fixture(); + assertEquals(8, f.hollow.getHollowSize()); + assertEquals(Arrays.asList("size", "tile"), f.events); + ProbeTile unused = new ProbeTile(f.events); + ProbeTile selected = new ProbeTile(f.events); + f.set("firstTile", TileMultipart.class, unused); + f.set("secondTile", TileMultipart.class, selected); + f.set("slot", int.class, 37); + for (TMultiPart occupant : new TMultiPart[] { null, new Candidate(f.events) }) { + selected.center = occupant; + f.reset(); + assertEquals(8, f.hollow.getHollowSize()); + assertEquals(Arrays.asList("size", "tile", "tile", "map:6"), f.events); + } + Connector connector = new Connector(f.events); + selected.center = connector; + for (int size : new int[] { Integer.MIN_VALUE, -1, 0, 8, 11, 32, Integer.MAX_VALUE }) { + connector.size = size; + f.reset(); + assertEquals(size, f.hollow.getHollowSize()); + assertEquals(Arrays.asList("size", "tile", "tile", "map:6", "slot", "connector:37"), f.events); + } + f.set("secondTile", TileMultipart.class, null); + f.reset(); + assertThrows(NullPointerException.class, f.hollow::getHollowSize); + assertEquals(Arrays.asList("size", "tile", "tile"), f.events); + } + + @Test + void collisionsUseSeparateRawShapeReadsAndProduceFreshReadOnlyBoxes() throws Exception { + Fixture f = new Fixture(); + f.set("overrideSize", boolean.class, true); + f.set("firstShape", byte.class, (byte) 32); + f.set("secondShape", byte.class, (byte) 113); + List boxes = f.hollow.getCollisionBoxes(); + assertEquals(Arrays.asList("collision", "size", "shape", "shape"), f.events); + assertEquals(4, boxes.size()); + for (Cuboid6 box : boxes) { + assertEquals(0.75, box.min.y, 1e-12); + assertEquals(1, box.max.y, 1e-12); + } + assertEquals(0.1875, volume(boxes), 1e-12); + assertThrows(UnsupportedOperationException.class, () -> boxes.add(Cuboid6.full)); + assertThrows(UnsupportedOperationException.class, () -> boxes.set(0, Cuboid6.full)); + assertThrows(UnsupportedOperationException.class, () -> boxes.remove(0)); + f.reset(); + List again = f.hollow.getCollisionBoxes(); + assertNotSame(boxes, again); + for (int i = 0; i < 4; i++) assertNotSame(boxes.get(i), again.get(i)); + boxes.get(0).min.x = 123; + assertNotEquals(123, again.get(0).min.x); + } + + @Test + void collisionGeometryDoesNotClampOpeningOrSignedThickness() throws Exception { + Fixture f = new Fixture(); + f.set("overrideSize", boolean.class, true); + f.set("firstShape", byte.class, (byte) 0x80); + f.set("secondShape", byte.class, (byte) 0); + for (int size : new int[] { -8, 0, 8, 24 }) { + f.set("hollowSize", int.class, size); + f.reset(); + List boxes = f.hollow.getCollisionBoxes(); + double end = 0.5 - size / 32D; + assertBox(new Cuboid6(0, -1, Math.min(0, end), 1, 0, Math.max(0, end)), boxes.get(0)); + } + for (int side = 6; side < 16; side++) { + f.set("secondShape", byte.class, (byte) side); + f.reset(); + assertThrows(ArrayIndexOutOfBoundsException.class, f.hollow::getCollisionBoxes); + assertEquals(Arrays.asList("collision", "size", "shape", "shape"), f.events); + } + } + + @Test + void subpartsDispatchToCollisionAndCopyOrderedBoundsIntoAMutableList() throws Exception { + Fixture f = new Fixture(); + List input = new ArrayList<>(Arrays.asList(new Cuboid6(0, 1, 2, 3, 4, 5), Cuboid6.full.copy())); + f.set("overrideCollision", boolean.class, true); + f.set("collision", List.class, input); + List result = f.hollow.getSubParts(); + assertEquals(Collections.singletonList("collision"), f.events); + assertEquals(2, result.size()); + for (int i = 0; i < 2; i++) { + assertEquals(0, result.get(i).data); + assertBox(input.get(i), result.get(i)); + assertNotSame(input.get(i), result.get(i)); + assertNotSame(input.get(i).min, result.get(i).min); + } + input.get(0).min.x = 99; + input.clear(); + assertEquals(0, result.get(0).min.x); + result.add(new IndexedCuboid6(1, Cuboid6.full)); + result.set(0, result.get(1)); + result.remove(0); + assertEquals(2, result.size()); + f.set("collision", List.class, Collections.singletonList(null)); + assertThrows(NullPointerException.class, f.hollow::getSubParts); + f.set("collision", List.class, null); + assertThrows(NullPointerException.class, f.hollow::getSubParts); + } + + @Test + void normalAndPartialOverlapShortCircuitTheRealScalaSuperChain() throws Exception { + Fixture f = new Fixture(); + Candidate candidate = new Candidate(f.events); + Cuboid6 separate = new Cuboid6(2, 2, 2, 3, 3, 3); + for (boolean partial : new boolean[] { false, true }) { + candidate.normal = Collections.singletonList(partial ? separate : Cuboid6.full); + candidate.partial = Collections.singletonList(partial ? Cuboid6.full : separate); + f.reset(); + assertFalse(f.part.occlusionTest(candidate)); + assertEquals(Arrays.asList("normal:other", "partial:other", "normal:self"), f.events); + } + candidate.normal = Collections.singletonList(separate); + candidate.partial = Collections.emptyList(); + for (boolean result : new boolean[] { false, true }) { + f.set("superResult", boolean.class, result); + f.reset(); + assertEquals(result, f.part.occlusionTest(candidate)); + assertEquals(Arrays.asList("normal:other", "partial:other", "normal:self", "super"), f.events); + } + f.set("superFailure", boolean.class, true); + assertEquals( + "super failure", + assertThrows(IllegalStateException.class, () -> f.part.occlusionTest(candidate)).getMessage()); + } + + private static double volume(List boxes) { + double volume = 0; + for (Cuboid6 c : boxes) volume += (c.max.x - c.min.x) * (c.max.y - c.min.y) * (c.max.z - c.min.z); + return volume; + } + + private static void assertBox(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 ProbeTile extends TileMultipart { + + final List events; + TMultiPart center; + + ProbeTile(List events) { + this.events = events; + } + + @Override + public TMultiPart partMap(int slot) { + events.add("map:" + slot); + return center; + } + } + + private static class Candidate extends TMultiPart implements JNormalOcclusion, JPartialOcclusion { + + final List events; + Iterable normal = Collections.emptyList(); + Iterable partial = Collections.emptyList(); + + Candidate(List events) { + this.events = events; + } + + @Override + public String getType() { + throw new AssertionError("unexpected type lookup"); + } + + @Override + public Iterable getOcclusionBoxes() { + events.add("normal:other"); + return normal; + } + + @Override + public Iterable getPartialOcclusionBoxes() { + events.add("partial:other"); + return partial; + } + } + + private static final class Connector extends Candidate implements ISidedHollowConnect { + + int size; + + Connector(List events) { + super(events); + } + + @Override + public int getHollowSize(int side) { + events.add("connector:" + side); + return size; + } + } + + private static final class Fixture extends ClassLoader { + + final List events = new ArrayList<>(); + final Microblock part; + final HollowMicroblock hollow; + + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + for (String name : new String[] { "ReferenceHollowMicroblockBase", "ReferenceScalaHollowMicroblock" }) { + 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 = (Microblock) loadClass("codechicken.multipart.compat.ReferenceScalaHollowMicroblock") + .getConstructor().newInstance(); + hollow = (HollowMicroblock) part; + set("events", List.class, events); + } + + void set(String name, Class type, Object value) throws Exception { + part.getClass().getMethod(name + "_$eq", type).invoke(part, value); + } + + void reset() throws Exception { + events.clear(); + set("tileReads", int.class, 0); + set("shapeReads", int.class, 0); + } + } +} diff --git a/src/test/resources/compat/ReferenceHollowMicroblockBase.class.b64 b/src/test/resources/compat/ReferenceHollowMicroblockBase.class.b64 new file mode 100644 index 0000000..9e42e02 --- /dev/null +++ b/src/test/resources/compat/ReferenceHollowMicroblockBase.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQASwEAOmNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlSG9sbG93TWljcm9ibG9ja0Jhc2UHAAEBACFjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2sHAAMBACRSZWZlcmVuY2VTY2FsYUhvbGxvd01pY3JvYmxvY2suc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNpZ25hdHVyZTsBAAVieXRlcwECmAYBRTNRIQECAgIlEVFEVTNnS0osZ25ZM0lfMmR3bl4nakdKfCcNXDhkVwoLNy8aBgMHEQlhYVk4bmEGJChCQQMHAyVpVw8cO2phBhRIT0MBCAMtGXcOWjNkUSYcNy4aOAQBTREBQQMJAxc5aREBBAYDGxkJISJcNWRlPhRHblwybBMJeUFCAQZOUw4UeE4ZN3BHLkRRIQUBBQJJCWEBUDVvU1J0RCNBChEFUQFRIgECCQ9ZAQEZIUMBLwUxUU1eM29pTiwSAQcJBDN5AVMiAQ4LBW1hEgFCO3VTMlQRIUgBBVUGNBgtAwIgNQkhQSpbOnUhCQlzRQQCI0s1CTFFQwElAxUZOC0ZN2ITCTEzJQEEUWUWJFdNWgUDUSUSYWEVO3NTOjwnQgEUJBEdWQMBMUEFAjEKISIaPGZdUhx4DEozcikJaQMHBQIjXSURcWYJAgUrOkxHD0MEMlUFBQkZAQ0CB2EkEwcDBDQBAQZLAUcBCEtaLGcOXjohER0pBAExQQUCWQoxYl07cUtKFFZtXTttaVYJcQcFAiNxJRERaAkCCAU+fEcuWjFvER1ZBAExQQUCcQpxYl07cUtKFFZtXTttaX4jUy0dCwNbdUJxIQ0eAgIDB3EHAwRAAQEGS2FOAQ1nVgRYTR0qZmdWZEcPCQUIAwIBDREiATcDMRlYD10zcw0GTEcuHjpmER0ZBQExQQUCEQsBY107cUtKNBUtGzd2ZRZ8RiVaORUFNSoFYkIZQwMDBQ1hDgUHDwIBCxVCHAIbTSwILxo6R0MmZFdPXTMhERVJBQEiEUsDNXk3bRk3dmcmfGcOVjN0aVIRcWcTBQYZIgMNIVQBBWEGFEgPBQJPHzYJQSEDAlEJCVFBK1Q7bWkmBBZNHTsBAAZldmVudHMBABBMamF2YS91dGlsL0xpc3Q7AQAkTGphdmEvdXRpbC9MaXN0PExqYXZhL2xhbmcvU3RyaW5nOz47AQALc3VwZXJSZXN1bHQBAAFaAQAMc3VwZXJGYWlsdXJlAQASKClMamF2YS91dGlsL0xpc3Q7DAAJAAoJAAIAEAEABHRoaXMBADxMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L2NvbXBhdC9SZWZlcmVuY2VIb2xsb3dNaWNyb2Jsb2NrQmFzZTsBAApldmVudHNfJGVxAQATKExqYXZhL3V0aWwvTGlzdDspVgEAA3gkMQEAAygpWgwADAANCQACABgBAA9zdXBlclJlc3VsdF8kZXEBAAQoWilWDAAOAA0JAAIAHAEAEHN1cGVyRmFpbHVyZV8kZXEBAA1vY2NsdXNpb25UZXN0AQAlKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgwACQAPCgACACEBAAVzdXBlcggAIwEADmphdmEvdXRpbC9MaXN0BwAlAQADYWRkAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaDAAnACgLACYAKQwADgAXCgACACsBAB9qYXZhL2xhbmcvSWxsZWdhbFN0YXRlRXhjZXB0aW9uBwAtAQANc3VwZXIgZmFpbHVyZQgALwEABjxpbml0PgEAFShMamF2YS9sYW5nL1N0cmluZzspVgwAMQAyCgAuADMMAAwAFwoAAgA1AQAEcGFydAEAIkxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDsBAAMoKVYBAAQoSSlWDAAxADoKAAQAOwEAE2phdmEvdXRpbC9BcnJheUxpc3QHAD0MADEAOQoAPgA/AQAJU2lnbmF0dXJlAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEAJigpTGphdmEvdXRpbC9MaXN0PExqYXZhL2xhbmcvU3RyaW5nOz47AQAnKExqYXZhL3V0aWwvTGlzdDxMamF2YS9sYW5nL1N0cmluZzs+OylWAQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBABlSdW50aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcEIQACAAQAAAADAAIACQAKAAEAQQAAAAIACwACAAwADQAAAAIADgANAAAACAABAAkADwACAEIAAAAvAAEAAQAAAAUqtAARsAAAAAIAQwAAAAwAAQAAAAUAEgATAAAARAAAAAYAAQAAAAgAQQAAAAIARQABABQAFQACAEIAAAA6AAIAAgAAAAYqK7UAEbEAAAACAEMAAAAWAAIAAAAGABIAEwAAAAAABgAWAAoAAQBEAAAABgABAAAACABBAAAAAgBGAAEADAAXAAEAQgAAAC8AAQABAAAABSq0ABmsAAAAAgBDAAAADAABAAAABQASABMAAABEAAAABgABAAAACQABABoAGwABAEIAAAA6AAIAAgAAAAYqG7UAGbEAAAACAEMAAAAWAAIAAAAGABIAEwAAAAAABgAWAA0AAQBEAAAABgABAAAACQABAA4AFwABAEIAAAAvAAEAAQAAAAUqtAAdrAAAAAIAQwAAAAwAAQAAAAUAEgATAAAARAAAAAYAAQAAAAoAAQAeABsAAQBCAAAAOgACAAIAAAAGKhu1AB2xAAAAAgBDAAAAFgACAAAABgASABMAAAAAAAYAFgANAAEARAAAAAYAAQAAAAoAAQAfACAAAQBCAAAAZwADAAIAAAAiKrYAIhIkuQAqAgBXKrYALJkADbsALlkSMLcANL8qtgA2rAAAAAMAQwAAABYAAgAAACIAEgATAAAAAAAiADcAOAABAEQAAAAOAAMAAAAMAAwADQAdAA4ARwAAAAMAAR0AAQAxADkAAQBCAAAAUQADAAEAAAAbKgO3ADwquwA+WbcAQLUAESoEtQAZKgO1AB2xAAAAAgBDAAAADAABAAAAGwASABMAAABEAAAAEgAEAAAABwAFAAgAEAAJABUACgADAEgAAAACAAUASQAAAAsAAQAGAAEAB3MACABKAAAAAwUAAA== diff --git a/src/test/resources/compat/ReferenceScalaHollowMicroblock.class.b64 b/src/test/resources/compat/ReferenceScalaHollowMicroblock.class.b64 new file mode 100644 index 0000000..41fd5e4 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaHollowMicroblock.class.b64 @@ -0,0 +1 @@ +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5JGEEAgkxDGduWgUFAwcKaUQBBUppFhQYTRk3ZhElCTkFAWEBCgMJSSUBBm9fSmwXDVwwJUtGJDJBSUEmESVJE1FJQQECBAlJBAMFAlABAQsVQkEdAx1xd05dN2JZAkJxIWEVAQkDCikmAQN1UzIsRyNBDgkPBWUDASIRAlwFKTEPWzFxS1IJMQkDBAJgASEJRU4BCE8WJDgLXDh1ERkJGQcBQyFtBWlxLRo7SV8yZHduXipqdRZEcSFhGgEJAwpJJ0EJaEtSHHUOHDdqZyZ8Z05ROHlLTiQiIWECCQ8FNQQBIhECcAUJci0aO1BHDmRXb101cF0KewcwWjoVBQVlAgEACWZpcnN0VGlsZQEAJUxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVGlsZU11bHRpcGFydDsBAApzZWNvbmRUaWxlAQAJdGlsZVJlYWRzAQABSQEACmZpcnN0U2hhcGUBAAFCAQALc2Vjb25kU2hhcGUBAApzaGFwZVJlYWRzAQAEc2xvdAEADG92ZXJyaWRlU2l6ZQEAAVoBAApob2xsb3dTaXplAQARb3ZlcnJpZGVDb2xsaXNpb24BAAljb2xsaXNpb24BABBMamF2YS91dGlsL0xpc3Q7AQAvTGphdmEvdXRpbC9MaXN0PExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7PjsBAAZub3JtYWwBABRMamF2YS9sYW5nL0l0ZXJhYmxlOwEAM0xqYXZhL2xhbmcvSXRlcmFibGU8TGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjs+OwEAPGNvZGVjaGlja2VuJG1pY3JvYmxvY2skSG9sbG93TWljcm9ibG9jayQkc3VwZXIkb2NjbHVzaW9uVGVzdAEAJShMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVoBACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbiRjbGFzcwcAIQEADW9jY2x1c2lvblRlc3QBAE0oTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgwAIwAkCgAiACUBAAR0aGlzAQA9TGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFIb2xsb3dNaWNyb2Jsb2NrOwEABW5wYXJ0AQAiTGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OwEACm1pY3JvQ2xhc3MBACwoKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0hvbGxvd01pY3JvQ2xhc3MkOwEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svSG9sbG93TWljcm9ibG9jayRjbGFzcwcALQEAVShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Ib2xsb3dNaWNyb2Jsb2NrOylMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Ib2xsb3dNaWNyb0NsYXNzJDsMACsALwoALgAwAQAJZ2V0Qm91bmRzAQAfKClMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwEASChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Ib2xsb3dNaWNyb2Jsb2NrOylMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAMgA0CgAuADUBAE4oTGNvZGVjaGlja2VuL21pY3JvYmxvY2svSG9sbG93TWljcm9ibG9jaztMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVoMACMANwoALgA4AQAYZ2V0UGFydGlhbE9jY2x1c2lvbkJveGVzAQASKClMamF2YS91dGlsL0xpc3Q7AQA7KExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0hvbGxvd01pY3JvYmxvY2s7KUxqYXZhL3V0aWwvTGlzdDsMADoAPAoALgA9AQALZ2V0U3ViUGFydHMMAD8APAoALgBAAQAWYWxsb3dDb21wbGV0ZU9jY2x1c2lvbgEAAygpWgEALChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Ib2xsb3dNaWNyb2Jsb2NrOylaDABCAEQKAC4ARQEABXNvbGlkAQAEKEkpWgEALShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Ib2xsb3dNaWNyb2Jsb2NrO0kpWgwARwBJCgAuAEoBAARzaWRlAQAVcmVkc3RvbmVDb25kdWN0aW9uTWFwAQADKClJAQAsKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0hvbGxvd01pY3JvYmxvY2s7KUkMAE0ATwoALgBQAQALZ2V0U2xvdE1hc2sBAC1jb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2skY2xhc3MHAFMBACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspSQwAUgBVCgBUAFYBAAtpdGVtQ2xhc3NJRAwAWABVCgBUAFkBADtjb2RlY2hpY2tlbiRtaWNyb2Jsb2NrJFRNaWNyb09jY2x1c2lvbiQkc3VwZXIkb2NjbHVzaW9uVGVzdAwAIwAgCgAEAFwBABdlZGdlQ29ybmVyT2NjbHVzaW9uVGVzdAEAUyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uOylaAQB7KExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uOylaDABeAGAKACIAYQEABGVkZ2UBAChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247AQAGY29ybmVyAQAnKClMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RpbGVNdWx0aXBhcnQ7DAALAAwJAAIAZwEADWZpcnN0VGlsZV8kZXEBACgoTGNvZGVjaGlja2VuL211bHRpcGFydC9UaWxlTXVsdGlwYXJ0OylWAQADeCQxDAANAAwJAAIAbAEADnNlY29uZFRpbGVfJGVxDAAOAA8JAAIAbwEADXRpbGVSZWFkc18kZXEBAAQoSSlWAQADKClCDAAQABEJAAIAdAEADmZpcnN0U2hhcGVfJGVxAQAEKEIpVgwAEgARCQACAHgBAA9zZWNvbmRTaGFwZV8kZXEMABMADwkAAgB7AQAOc2hhcGVSZWFkc18kZXEMABQADwkAAgB+AQAIc2xvdF8kZXEMABUAFgkAAgCBAQAQb3ZlcnJpZGVTaXplXyRlcQEABChaKVYMABcADwkAAgCFAQAOaG9sbG93U2l6ZV8kZXEMABgAFgkAAgCIAQAVb3ZlcnJpZGVDb2xsaXNpb25fJGVxDAAZABoJAAIAiwEADWNvbGxpc2lvbl8kZXEBABMoTGphdmEvdXRpbC9MaXN0OylWAQAWKClMamF2YS9sYW5nL0l0ZXJhYmxlOwwAHAAdCQACAJABAApub3JtYWxfJGVxAQAXKExqYXZhL2xhbmcvSXRlcmFibGU7KVYBAAR0aWxlAQAGZXZlbnRzDACVADsKAAIAlggAlAEADmphdmEvdXRpbC9MaXN0BwCZAQADYWRkAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaDACbAJwLAJoAnQwADgBOCgACAJ8MAHEAcgoAAgChDAALAGYKAAIAowwADQBmCgACAKUBACNjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVGlsZU11bHRpcGFydAcApwEABXNoYXBlCACpDAATAE4KAAIAqwwAfQByCgACAK0MABAAcwoAAgCvDAASAHMKAAIAsQEAB2dldFNsb3QIABQMABQATgoAAgC1AQANZ2V0SG9sbG93U2l6ZQEABHNpemUIALgMABUAQwoAAgC6DAAXAE4KAAIAvAwAtwBPCgAuAL4BABFnZXRDb2xsaXNpb25Cb3hlcwgAGQwAGABDCgACAMIMABkAOwoAAgDEDADAADwKAC4AxgEAEWdldE9jY2x1c2lvbkJveGVzAQALbm9ybWFsOnNlbGYIAMkMABwAjwoAAgDLDAA/ADsKAAIAzQwAOgA7CgACAM8BACooKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGFzczsMACsALAoAAgDSAQArKClMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb0NsYXNzOwwAwAA7CgACANUBAAY8aW5pdD4BAAMoKVYMANcA2AoABADZAQAGJGluaXQkAQArKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspVgwA2wDcCgAiAN0BACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspVgwA2wDfCgBUAOABACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svSG9sbG93TWljcm9ibG9jazspVgwA2wDiCgAuAOMBABtjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDYHAOUBAARmdWxsAQAdTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjsMAOcA6AkA5gDpAQAVamF2YS91dGlsL0NvbGxlY3Rpb25zBwDrAQANc2luZ2xldG9uTGlzdAEAJChMamF2YS9sYW5nL09iamVjdDspTGphdmEvdXRpbC9MaXN0OwwA7QDuCgDsAO8BAAlTaWduYXR1cmUBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAPTGluZU51bWJlclRhYmxlAQAxKClMamF2YS91dGlsL0xpc3Q8TGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjs+OwEAPigpTGphdmEvdXRpbC9MaXN0PExjb2RlY2hpY2tlbi9saWIvcmF5dHJhY2VyL0luZGV4ZWRDdWJvaWQ2Oz47AQAyKExqYXZhL3V0aWwvTGlzdDxMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2Oz47KVYBADUoKUxqYXZhL2xhbmcvSXRlcmFibGU8TGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjs+OwEANihMamF2YS9sYW5nL0l0ZXJhYmxlPExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7PjspVgEADVN0YWNrTWFwVGFibGUBAApTb3VyY2VGaWxlAQAZUnVudGltZVZpc2libGVBbm5vdGF0aW9ucwEACFNjYWxhU2lnACEAAgAEAAEABgAMAAIACwAMAAAAAgANAAwAAAACAA4ADwAAAAIAEAARAAAAAgASABEAAAACABMADwAAAAIAFAAPAAAAAgAVABYAAAACABcADwAAAAIAGAAWAAAAAgAZABoAAQDxAAAAAgAbAAIAHAAdAAEA8QAAAAIAHgAxEAEAHwAgAAEA8gAAADoAAgACAAAABioruAAmrAAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGACkAKgABAPQAAAAGAAEAAAATAAEAKwAsAAEA8gAAAC8AAQABAAAABSq4ADGwAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAEwABADIAMwABAPIAAAAvAAEAAQAAAAUquAA2sAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABMAAQAjACAAAQDyAAAAOgACAAIAAAAGKiu4ADmsAAAAAgDzAAAAFgACAAAABgAnACgAAAAAAAYAKQAqAAEA9AAAAAYAAQAAABMAAQA6ADsAAgDyAAAALwABAAEAAAAFKrgAPrAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATAPEAAAACAPUAAQA/ADsAAgDyAAAALwABAAEAAAAFKrgAQbAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATAPEAAAACAPYAAQBCAEMAAQDyAAAALwABAAEAAAAFKrgARqwAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATAAEARwBIAAEA8gAAADoAAgACAAAABiobuABLrAAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAEwADwABAPQAAAAGAAEAAAATAAEATQBOAAEA8gAAAC8AAQABAAAABSq4AFGsAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAEwABAFIATgABAPIAAAAvAAEAAQAAAAUquABXrAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABMAAQBYAE4AAQDyAAAALwABAAEAAAAFKrgAWqwAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATEAEAWwAgAAEA8gAAADoAAgACAAAABiortwBdrAAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGACkAKgABAPQAAAAGAAEAAAATAAEAXgBfAAEA8gAAAEUAAwADAAAAByorLLgAYqwAAAACAPMAAAAgAAMAAAAHACcAKAAAAAAABwBjAGQAAQAAAAcAZQBkAAIA9AAAAAYAAQAAABMAAQALAGYAAQDyAAAALwABAAEAAAAFKrQAaLAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAAUAAEAaQBqAAEA8gAAADoAAgACAAAABiortQBosQAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAGsADAABAPQAAAAGAAEAAAAUAAEADQBmAAEA8gAAAC8AAQABAAAABSq0AG2wAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAFQABAG4AagABAPIAAAA6AAIAAgAAAAYqK7UAbbEAAAACAPMAAAAWAAIAAAAGACcAKAAAAAAABgBrAAwAAQD0AAAABgABAAAAFQABAA4ATgABAPIAAAAvAAEAAQAAAAUqtABwrAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABYAAQBxAHIAAQDyAAAAOgACAAIAAAAGKhu1AHCxAAAAAgDzAAAAFgACAAAABgAnACgAAAAAAAYAawAPAAEA9AAAAAYAAQAAABYAAQAQAHMAAQDyAAAALwABAAEAAAAFKrQAdawAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAAXAAEAdgB3AAEA8gAAADoAAgACAAAABiobtQB1sQAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAGsAEQABAPQAAAAGAAEAAAAXAAEAEgBzAAEA8gAAAC8AAQABAAAABSq0AHmsAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAGAABAHoAdwABAPIAAAA6AAIAAgAAAAYqG7UAebEAAAACAPMAAAAWAAIAAAAGACcAKAAAAAAABgBrABEAAQD0AAAABgABAAAAGAABABMATgABAPIAAAAvAAEAAQAAAAUqtAB8rAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABkAAQB9AHIAAQDyAAAAOgACAAIAAAAGKhu1AHyxAAAAAgDzAAAAFgACAAAABgAnACgAAAAAAAYAawAPAAEA9AAAAAYAAQAAABkAAQAUAE4AAQDyAAAALwABAAEAAAAFKrQAf6wAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAAaAAEAgAByAAEA8gAAADoAAgACAAAABiobtQB/sQAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAGsADwABAPQAAAAGAAEAAAAaAAEAFQBDAAEA8gAAAC8AAQABAAAABSq0AIKsAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAGwABAIMAhAABAPIAAAA6AAIAAgAAAAYqG7UAgrEAAAACAPMAAAAWAAIAAAAGACcAKAAAAAAABgBrABYAAQD0AAAABgABAAAAGwABABcATgABAPIAAAAvAAEAAQAAAAUqtACGrAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABwAAQCHAHIAAQDyAAAAOgACAAIAAAAGKhu1AIaxAAAAAgDzAAAAFgACAAAABgAnACgAAAAAAAYAawAPAAEA9AAAAAYAAQAAABwAAQAYAEMAAQDyAAAALwABAAEAAAAFKrQAiawAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAAdAAEAigCEAAEA8gAAADoAAgACAAAABiobtQCJsQAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAGsAFgABAPQAAAAGAAEAAAAdAAEAGQA7AAIA8gAAAC8AAQABAAAABSq0AIywAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAHgDxAAAAAgD1AAEAjQCOAAIA8gAAADoAAgACAAAABiortQCMsQAAAAIA8wAAABYAAgAAAAYAJwAoAAAAAAAGAGsAGgABAPQAAAAGAAEAAAAeAPEAAAACAPcAAQAcAI8AAgDyAAAALwABAAEAAAAFKrQAkbAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAAfAPEAAAACAPgAAQCSAJMAAgDyAAAAOgACAAIAAAAGKiu1AJGxAAAAAgDzAAAAFgACAAAABgAnACgAAAAAAAYAawAdAAEA9AAAAAYAAQAAAB8A8QAAAAIA+QABAJQAZgABAPIAAABtAAMAAQAAACoqtgCXEpi5AJ4CAFcqKrYAoARgtgCiKrYAoASgAAoqtgCkpwAHKrYAprAAAAADAPMAAAAMAAEAAAAqACcAKAAAAPQAAAASAAQAAAAiAAwAIwAWACQAKQAhAPoAAAAHAAIlQwcAqAABAKkAcwABAPIAAABrAAMAAQAAACoqtgCXEqq5AJ4CAFcqKrYArARgtgCuKrYArASgAAoqtgCwpwAHKrYAsqwAAAADAPMAAAAMAAEAAAAqACcAKAAAAPQAAAASAAQAAAAnAAwAKAAWACkAKQAmAPoAAAAFAAIlQwEAAQCzAE4AAQDyAAAAOwACAAEAAAARKrYAlxK0uQCeAgBXKrYAtqwAAAACAPMAAAAMAAEAAAARACcAKAAAAPQAAAAGAAEAAAArAAEAtwBOAAEA8gAAAFwAAgABAAAAHyq2AJcSubkAngIAVyq2ALuZAAoqtgC9pwAHKrgAv6wAAAADAPMAAAAMAAEAAAAfACcAKAAAAPQAAAAOAAMAAAAtAAwALgAeACwA+gAAAAUAAhpDAQABAMAAOwACAPIAAABeAAIAAQAAAB8qtgCXEsG5AJ4CAFcqtgDDmQAKKrYAxacAByq4AMewAAAAAwDzAAAADAABAAAAHwAnACgAAAD0AAAADgADAAAAMQAMADIAHgAwAPoAAAAHAAIaQwcAmgDxAAAAAgD1AAEAyACPAAIA8gAAAD8AAgABAAAAESq2AJcSyrkAngIAVyq2AMywAAAAAgDzAAAADAABAAAAEQAnACgAAAD0AAAACgACAAAANQAMADYA8QAAAAIA+BBBAD8AjwABAPIAAAAvAAEAAQAAAAUqtgDOsAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABMQQQA6AI8AAQDyAAAALwABAAEAAAAFKrYA0LAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATEEEAKwDRAAEA8gAAAC8AAQABAAAABSq2ANOwAAAAAgDzAAAADAABAAAABQAnACgAAAD0AAAABgABAAAAExBBACsA1AABAPIAAAAvAAEAAQAAAAUqtgDTsAAAAAIA8wAAAAwAAQAAAAUAJwAoAAAA9AAAAAYAAQAAABMQQQDAAI8AAQDyAAAALwABAAEAAAAFKrYA1rAAAAACAPMAAAAMAAEAAAAFACcAKAAAAPQAAAAGAAEAAAATAAEA1wDYAAEA8gAAAK8AAgABAAAAVSq3ANoquADeKrgA4Sq4AOQqAbUAaCoBtQBtKgO1AHAqEDC1AHUqEDC1AHkqA7UAfCoDtQB/KgO1AIIqEAi1AIYqA7UAiSoBtQCMKrIA6rgA8LUAkbEAAAACAPMAAAAMAAEAAABVACcAKAAAAPQAAAA2AA0AAAATABAAFAAVABUAGgAWAB8AFwAlABgAKwAZADAAGgA1ABsAOgAcAEAAHQBFAB4ASgAfAAMA+wAAAAIABwD8AAAACwABAAgAAQAJcwAKAP0AAAADBQAA