diff --git a/src/functionalTest/java/codechicken/multipart/test/EdgeMicroblockTraitsFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/EdgeMicroblockTraitsFunctionalTest.java new file mode 100644 index 0000000..57e9595 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/EdgeMicroblockTraitsFunctionalTest.java @@ -0,0 +1,132 @@ +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.Arrays; +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.vec.Cuboid6; +import codechicken.microblock.EdgeMicroClass; +import codechicken.microblock.EdgeMicroClass$; +import codechicken.microblock.EdgeMicroblock; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; + +class EdgeMicroblockTraitsFunctionalTest { + + @Test + void generatedEdgeKeepsClassIdentityAndAllEightyFourBounds() { + Microblock part = MicroblockGenerator.create(EdgeMicroClass$.MODULE$, 0, false); + EdgeMicroblock edgePart = (EdgeMicroblock) part; + assertSame(EdgeMicroClass$.MODULE$, edgePart.microClass()); + assertSame(EdgeMicroClass$.MODULE$, part.microClass()); + assertEquals("mcr_edge", part.getType()); + int[][] axisBits = { { 2, 0, 1 }, { 1, 2, 0 }, { 0, 1, 2 } }; + for (int size = 1; size <= 7; size++) { + for (int edge = 0; edge < 12; edge++) { + part.setShape(size, edge + 15); + assertEquals(size, part.getSize()); + assertEquals(edge + 15, edgePart.getSlot()); + assertEquals(1 << (edge + 15), edgePart.getSlotMask()); + assertFalse(edgePart.conductsRedstone()); + Cuboid6 bounds = edgePart.getBounds(); + assertSame(EdgeMicroClass.aBounds()[size * 16 + edge], bounds); + double distance = size / 8D; + double[] min = { bounds.min.x, bounds.min.y, bounds.min.z }; + double[] max = { bounds.max.x, bounds.max.y, bounds.max.z }; + for (int axis = 0; axis < 3; axis++) { + int bit = axisBits[edge / 4][axis]; + boolean positive = (edge & bit) != 0; + assertEquals(positive ? 1 - distance : 0, min[axis], 1e-12); + assertEquals(bit == 0 || positive ? 1 : distance, max[axis], 1e-12); + } + } + } + } + + @Test + void boundsUseSignedShapeIndexAndTheLiveReplaceableArray() { + Microblock part = MicroblockGenerator.create(EdgeMicroClass$.MODULE$, 0, false); + EdgeMicroblock edge = (EdgeMicroblock) part; + for (int shape : new int[] { 0, 12, 28, 127 }) { + part.shape_$eq((byte) shape); + assertNull(edge.getBounds()); + } + for (int shape = -128; shape < 0; shape++) { + part.shape_$eq((byte) shape); + assertThrows(ArrayIndexOutOfBoundsException.class, edge::getBounds); + } + Cuboid6[] original = EdgeMicroClass.aBounds(); + Cuboid6[] replacement = new Cuboid6[256]; + Cuboid6 sentinel = new Cuboid6(0.1, 0.2, 0.3, 0.4, 0.5, 0.6); + replacement[35] = sentinel; + part.setShape(2, 18); + try { + EdgeMicroClass.aBounds_$eq(replacement); + assertSame(sentinel, edge.getBounds()); + replacement[35] = null; + assertNull(edge.getBounds()); + EdgeMicroClass.aBounds_$eq(new Cuboid6[1]); + assertThrows(ArrayIndexOutOfBoundsException.class, edge::getBounds); + EdgeMicroClass.aBounds_$eq(null); + assertThrows(NullPointerException.class, edge::getBounds); + } finally { + EdgeMicroClass.aBounds_$eq(original); + } + assertSame(original[35], edge.getBounds()); + } + + @Test + void generatedEdgesKeepThePackedShapeInNbtAndDescriptions() { + Microblock part = MicroblockGenerator.create(EdgeMicroClass$.MODULE$, 0, false); + Microblock loaded = MicroblockGenerator.create(EdgeMicroClass$.MODULE$, 0, false); + ByteArrayOutputStream prefixBytes = new ByteArrayOutputStream(); + MicroMaterialRegistry.writeMaterialID(new MCDataOutputWrapper(new DataOutputStream(prefixBytes)), 0); + byte[] prefix = prefixBytes.toByteArray(); + for (int size = 1; size <= 7; size++) { + for (int edge = 0; edge < 12; edge++) { + byte shape = (byte) (size * 16 + edge); + part.setShape(size, edge + 15); + 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()); + assertEquals(edge + 15, ((EdgeMicroblock) loaded).getSlot()); + 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(2, 15); + loaded.readDesc(input); + assertEquals(1, reads.get()); + assertEquals(shape, loaded.shape()); + assertEquals(edge + 15, ((EdgeMicroblock) loaded).getSlot()); + } + } + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index ca8189c..e8300ad 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -204,3 +204,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchange `6c0ff48c4bc6ff5508a429d53f13b718062173cc4a117095c192aed9cf0a0311`; class-file SHA-256 is `5a6c3cf1ce593eb189af59f6db26a04d85902972073de83a77d8f0d920642cfa`, stored as `src/test/resources/compat/ReferenceScalaCornerMicroblock.class.b64`. Do not regenerate it against the port. + +### `ReferenceScalaEdgeMicroblock` + +`scala/codechicken/multipart/compat/ReferenceScalaEdgeMicroblock.scala` freezes a concrete edge implementor +whose forwarders call `EdgeMicroblock$class`. Observable shape accessors pin virtual dispatch, byte truncation, +out-of-range input behavior and the fifteen-slot offset; tests also exercise inherited `TEdgePart` conduction. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchanged dev jar at `33e0bc2` +(SHA-256 `764bff78657bc14ab918c267e836702982f7a72d64e998357709df13bce90058`). Source SHA-256 is +`6524a023b8015e6ecc75aaab9dafa09bb353e66ccdb72e7d76a3469288ba5952`; class-file SHA-256 is +`2b51d17fb89999fd0324e4ea53843596539c5885c2b2eb0a476926e18446b587`, stored as +`src/test/resources/compat/ReferenceScalaEdgeMicroblock.class.b64`. Do not regenerate it against the port. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaEdgeMicroblock.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaEdgeMicroblock.scala new file mode 100644 index 0000000..da8c464 --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaEdgeMicroblock.scala @@ -0,0 +1,23 @@ +package codechicken.multipart.compat + +import codechicken.microblock.{EdgeMicroblock, Microblock} + +/** Frozen edge-trait forwarders with observable virtual shape access. */ +class ReferenceScalaEdgeMicroblock extends Microblock(29) with EdgeMicroblock { + var writes = 0 + var writtenShape: Byte = 0 + var reads = 0 + var overrideShape = false + var selectedShape = 0 + + override def shape_$eq(value: Byte) { + writes += 1 + writtenShape = value + super.shape_$eq(value) + } + + override def getShape = { + reads += 1 + if (overrideShape) selectedShape else super.getShape + } +} diff --git a/src/test/java/codechicken/microblock/EdgeMicroblockTraitsCharacterizationTest.java b/src/test/java/codechicken/microblock/EdgeMicroblockTraitsCharacterizationTest.java new file mode 100644 index 0000000..dc92f16 --- /dev/null +++ b/src/test/java/codechicken/microblock/EdgeMicroblockTraitsCharacterizationTest.java @@ -0,0 +1,94 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.Test; + +/** Executes edge-trait forwarders compiled against the untouched Scala implementation. */ +class EdgeMicroblockTraitsCharacterizationTest { + + @Test + void packsSupportedEdgesThroughTheVirtualShapeSetterExactlyOnce() throws Exception { + Fixture f = new Fixture(); + assertEquals(29, f.part.material()); + assertEquals(0, f.part.shape()); + int writes = 0; + for (int size = 1; size <= 7; size++) { + for (int edge = 0; edge < 12; edge++) { + f.part.setShape(size, edge + 15); + byte expected = (byte) (size * 16 + edge); + assertEquals(expected, f.part.shape()); + assertEquals(expected, f.get("writtenShape")); + assertEquals(++writes, f.get("writes")); + assertEquals(size, f.part.getSize()); + assertEquals(edge + 15, f.edge.getSlot()); + assertEquals(1 << (edge + 15), f.edge.getSlotMask()); + assertFalse(f.edge.conductsRedstone()); + assertEquals(29, f.part.material()); + } + } + } + + @Test + void preservesTruncationAndOutOfRangeSlotBitsWithoutValidation() throws Exception { + Fixture f = new Fixture(); + int[][] cases = { { 1, 14, -1, -1, 30 }, { 8, 15, -128, -8, 15 }, { 16, 15, 0, 0, 15 }, { 0, 31, 16, 1, 15 }, + { -1, 26, -5, -1, 26 }, { Integer.MAX_VALUE, Integer.MIN_VALUE, -15, -1, 16 } }; + for (int[] value : cases) { + f.part.setShape(value[0], value[1]); + assertEquals((byte) value[2], f.part.shape()); + assertEquals(value[3], f.part.getSize()); + assertEquals(value[4], f.edge.getSlot()); + } + assertEquals(cases.length, f.get("writes")); + } + + @Test + void slotDecodingUsesTheVirtualGetterAndKeepsIntegerOverflow() throws Exception { + Fixture f = new Fixture(); + f.set("overrideShape", boolean.class, true); + int[][] cases = { { -16, -1 }, { 0, 15 }, { 17, 32 }, { Integer.MAX_VALUE, Integer.MIN_VALUE + 14 } }; + int reads = 0; + for (int[] value : cases) { + f.set("selectedShape", int.class, value[0]); + assertEquals(value[1], f.edge.getSlot()); + assertEquals(++reads, f.get("reads")); + assertEquals(1 << value[1], f.edge.getSlotMask()); + assertEquals(++reads, f.get("reads")); + } + assertEquals(0, f.get("writes")); + assertEquals(0, f.part.shape()); + } + + private static final class Fixture extends ClassLoader { + + final Microblock part; + final EdgeMicroblock edge; + + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + InputStream input = Objects + .requireNonNull(getClass().getResourceAsStream("/compat/ReferenceScalaEdgeMicroblock.class.b64")); + byte[] bytes; + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + bytes = Base64.getMimeDecoder().decode(scanner.next()); + } + part = (Microblock) defineClass(null, bytes, 0, bytes.length).getConstructor().newInstance(); + edge = (EdgeMicroblock) part; + } + + Object get(String name) throws Exception { + return part.getClass().getMethod(name).invoke(part); + } + + void set(String name, Class type, Object value) throws Exception { + part.getClass().getMethod(name + "_$eq", type).invoke(part, value); + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaEdgeMicroblock.class.b64 b/src/test/resources/compat/ReferenceScalaEdgeMicroblock.class.b64 new file mode 100644 index 0000000..9c4c1a9 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaEdgeMicroblock.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQAmQEAOWNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFFZGdlTWljcm9ibG9jawcAAQEAIWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9jawcAAwEAJWNvZGVjaGlja2VuL21pY3JvYmxvY2svRWRnZU1pY3JvYmxvY2sHAAUBACJSZWZlcmVuY2VTY2FsYUVkZ2VNaWNyb2Jsb2NrLnNjYWxhAQAeTHNjYWxhL3JlZmxlY3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBAzIGAW0zQSEBAgETCWEiKxo0ZmUWdDctWipkQzIMVwlaNGYbJhwnbxwybV8OXCdCQQIFAxkZdy4cOWJpKhFRQUIBCltWZEcvGzliZVJUEWFCAQxHPiRXbRk1akcuLGduAQEUBwFRAQMFAgwdNQlBQgMCDg0FUVEuWzJzXwpkd25ZNgoFPWEhQUMnakdKfCcNXDhkV0IRMSJFBQMlMRFhIhIzaEs2SzdNXThjWT4cNw5DAxUBEQVRIwEEPVM6TEdPEAsCLUERcQNBBwIFITkRBAFhAQoDURJBQjxzU1IsNy9GARwhCWFyJEQBHhUFcRIhQjpkQzIMF0IBER4FDUllDh4FCEUBAQ0RIgEkAyk5KC8bO2ZnfiNTLR0LA0kdAiIBSBMKBRlqIgFCK29TUkRxAUsRAgIDBzEkQQJ5SUVCYUELASFCE1kSYUI8c1NSLDcPCQUIWQEBDREiAS4DMTkoLxs7dUs6HAYuGTlmKwVxA0MBDzATCQFURAEDQ3NSLAdiAhoBAQQlCWFNARFvSkxHD14zbyciDAcvWjAlS0YkIgEKGwkPIQoUESFhAV0hMWEHAVEhCjkKUWIeOmppUixnbhU1YmEWBANiAh0BAQQlCUFHAQZlFgxHbR0FCHUBAQ0RIgE8AyURWC0ZM3Q/EioXDwYCJXkhOQEmT0EBAgRZAkICIAFBAyYxJAEEc0sGJDcPCQUIAQIBDREiAUIDNXlnLxo6c1MSLDcLWzFxS1YJIQkFAh0HJhFBKQgCCAU+fEcuWjFvER0xBQExQQUCHQsRYxw8ZmVKTEctWippQ0IsdwxKM3IpCSEDCkMEKQsGBQkZASIJDSkDARUpA0MDOXlnLxo6c1MSLDcLWzFxSwJCcQEUAUECEwUhJEEHdEsyLDcNXjNlJyIMBy8aBQgdAgENESIBUANFGVgNXDNkaRYkNwtbMXFLfiNTLR0LA0lBQ3EBSycCAgMHMQQDBFMBAQZLYUcBD2cWZFdtGTtmSU5DFw1dMyERFSEGASIRVgMlGQguGTlmPxIqFw8GAiUtIilxaxVhAV0FKWEvGTd2SyIpEQwBQyE1BkFxLRo7VFEGBFgNRgEcAQEABndyaXRlcwEAAUkBAAx3cml0dGVuU2hhcGUBAAFCAQAFcmVhZHMBAA1vdmVycmlkZVNoYXBlAQABWgEADXNlbGVjdGVkU2hhcGUBAAhzZXRTaGFwZQEABShJSSlWAQArY29kZWNoaWNrZW4vbWljcm9ibG9jay9FZGdlTWljcm9ibG9jayRjbGFzcwcAFQEALChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9FZGdlTWljcm9ibG9jaztJSSlWDAATABcKABYAGAEABHRoaXMBADtMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L2NvbXBhdC9SZWZlcmVuY2VTY2FsYUVkZ2VNaWNyb2Jsb2NrOwEABHNpemUBAARzbG90AQAKbWljcm9DbGFzcwEAKigpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svRWRnZU1pY3JvQ2xhc3MkOwEAUShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9FZGdlTWljcm9ibG9jazspTGNvZGVjaGlja2VuL21pY3JvYmxvY2svRWRnZU1pY3JvQ2xhc3MkOwwAHgAgCgAWACEBAAlnZXRCb3VuZHMBAB8oKUxjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7AQBGKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0VkZ2VNaWNyb2Jsb2NrOylMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAIwAlCgAWACYBAAdnZXRTbG90AQADKClJAQAqKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0VkZ2VNaWNyb2Jsb2NrOylJDAAoACoKABYAKwEAC2dldFNsb3RNYXNrAQAtY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrJGNsYXNzBwAuAQAsKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2s7KUkMAC0AMAoALwAxAQAYZ2V0UGFydGlhbE9jY2x1c2lvbkJveGVzAQASKClMamF2YS91dGlsL0xpc3Q7AQA7KExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2s7KUxqYXZhL3V0aWwvTGlzdDsMADMANQoALwA2AQALaXRlbUNsYXNzSUQMADgAMAoALwA5AQA7Y29kZWNoaWNrZW4kbWljcm9ibG9jayRUTWljcm9PY2NsdXNpb24kJHN1cGVyJG9jY2x1c2lvblRlc3QBACUoTGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OylaAQAgY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQHAD0BAA1vY2NsdXNpb25UZXN0DAA/ADwKAD4AQAEABW5wYXJ0AQAiTGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OwEALGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uJGNsYXNzBwBEAQBNKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVoMAD8ARgoARQBHAQAXZWRnZUNvcm5lck9jY2x1c2lvblRlc3QBAFMoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspWgEAeyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspWgwASQBLCgBFAEwBAARlZGdlAQAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uOwEABmNvcm5lcgwACwAMCQACAFEBAAp3cml0ZXNfJGVxAQAEKEkpVgEAA3gkMQEAAygpQgwADQAOCQACAFcBABB3cml0dGVuU2hhcGVfJGVxAQAEKEIpVgwADwAMCQACAFsBAAlyZWFkc18kZXEBAAMoKVoMABAAEQkAAgBfAQARb3ZlcnJpZGVTaGFwZV8kZXEBAAQoWilWDAASAAwJAAIAYwEAEXNlbGVjdGVkU2hhcGVfJGVxAQAJc2hhcGVfJGVxDAALACkKAAIAZwwAUwBUCgACAGkMAFkAWgoAAgBrDABmAFoKAAQAbQEABXZhbHVlAQAIZ2V0U2hhcGUMAA8AKQoAAgBxDABdAFQKAAIAcwwAEABeCgACAHUMABIAKQoAAgB3DABwACkKAAQAeQEAFigpTGphdmEvbGFuZy9JdGVyYWJsZTsMADMANAoAAgB8AQAqKClMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xhc3M7DAAeAB8KAAIAfwEAKygpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9DbGFzczsBAAY8aW5pdD4BAAMoKVYMAIIAVAoABACEAQAGJGluaXQkAQArKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspVgwAhgCHCgBFAIgBACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspVgwAhgCKCgAvAIsBACooTGNvZGVjaGlja2VuL21pY3JvYmxvY2svRWRnZU1pY3JvYmxvY2s7KVYMAIYAjQoAFgCOAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEACVNpZ25hdHVyZQEAMSgpTGphdmEvdXRpbC9MaXN0PExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7PjsBAA1TdGFja01hcFRhYmxlAQAKU291cmNlRmlsZQEAGVJ1bnRpbWVWaXNpYmxlQW5ub3RhdGlvbnMBAAhTY2FsYVNpZwAhAAIABAABAAYABQACAAsADAAAAAIADQAOAAAAAgAPAAwAAAACABAAEQAAAAIAEgAMAAAAGgABABMAFAABAJAAAABFAAMAAwAAAAcqGxy4ABmxAAAAAgCRAAAAIAADAAAABwAaABsAAAAAAAcAHAAMAAEAAAAHAB0ADAACAJIAAAAGAAEAAAAGAAEAHgAfAAEAkAAAAC8AAQABAAAABSq4ACKwAAAAAgCRAAAADAABAAAABQAaABsAAACSAAAABgABAAAABgABACMAJAABAJAAAAAvAAEAAQAAAAUquAAnsAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAYAAQAoACkAAQCQAAAALwABAAEAAAAFKrgALKwAAAACAJEAAAAMAAEAAAAFABoAGwAAAJIAAAAGAAEAAAAGAAEALQApAAEAkAAAAC8AAQABAAAABSq4ADKsAAAAAgCRAAAADAABAAAABQAaABsAAACSAAAABgABAAAABgABADMANAACAJAAAAAvAAEAAQAAAAUquAA3sAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAYAkwAAAAIAlAABADgAKQABAJAAAAAvAAEAAQAAAAUquAA6rAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAYQAQA7ADwAAQCQAAAAOgACAAIAAAAGKiu3AEGsAAAAAgCRAAAAFgACAAAABgAaABsAAAAAAAYAQgBDAAEAkgAAAAYAAQAAAAYAAQA/ADwAAQCQAAAAOgACAAIAAAAGKiu4AEisAAAAAgCRAAAAFgACAAAABgAaABsAAAAAAAYAQgBDAAEAkgAAAAYAAQAAAAYAAQBJAEoAAQCQAAAARQADAAMAAAAHKissuABNrAAAAAIAkQAAACAAAwAAAAcAGgAbAAAAAAAHAE4ATwABAAAABwBQAE8AAgCSAAAABgABAAAABgABAAsAKQABAJAAAAAvAAEAAQAAAAUqtABSrAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAcAAQBTAFQAAQCQAAAAOgACAAIAAAAGKhu1AFKxAAAAAgCRAAAAFgACAAAABgAaABsAAAAAAAYAVQAMAAEAkgAAAAYAAQAAAAcAAQANAFYAAQCQAAAALwABAAEAAAAFKrQAWKwAAAACAJEAAAAMAAEAAAAFABoAGwAAAJIAAAAGAAEAAAAIAAEAWQBaAAEAkAAAADoAAgACAAAABiobtQBYsQAAAAIAkQAAABYAAgAAAAYAGgAbAAAAAAAGAFUADgABAJIAAAAGAAEAAAAIAAEADwApAAEAkAAAAC8AAQABAAAABSq0AFysAAAAAgCRAAAADAABAAAABQAaABsAAACSAAAABgABAAAACQABAF0AVAABAJAAAAA6AAIAAgAAAAYqG7UAXLEAAAACAJEAAAAWAAIAAAAGABoAGwAAAAAABgBVAAwAAQCSAAAABgABAAAACQABABAAXgABAJAAAAAvAAEAAQAAAAUqtABgrAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAoAAQBhAGIAAQCQAAAAOgACAAIAAAAGKhu1AGCxAAAAAgCRAAAAFgACAAAABgAaABsAAAAAAAYAVQARAAEAkgAAAAYAAQAAAAoAAQASACkAAQCQAAAALwABAAEAAAAFKrQAZKwAAAACAJEAAAAMAAEAAAAFABoAGwAAAJIAAAAGAAEAAAALAAEAZQBUAAEAkAAAADoAAgACAAAABiobtQBksQAAAAIAkQAAABYAAgAAAAYAGgAbAAAAAAAGAFUADAABAJIAAAAGAAEAAAALAAEAZgBaAAEAkAAAAFEAAwACAAAAFSoqtgBoBGC2AGoqG7YAbCobtwBusQAAAAIAkQAAABYAAgAAABUAGgAbAAAAAAAVAG8ADgABAJIAAAAOAAMAAAAOAAoADwAPABAAAQBwACkAAQCQAAAAWgADAAEAAAAdKiq2AHIEYLYAdCq2AHaZAAoqtgB4pwAHKrcAeqwAAAADAJEAAAAMAAEAAAAdABoAGwAAAJIAAAAOAAMAAAAUAAoAFQAcABMAlQAAAAUAAhhDARBBADMAewABAJAAAAAvAAEAAQAAAAUqtgB9sAAAAAIAkQAAAAwAAQAAAAUAGgAbAAAAkgAAAAYAAQAAAAYQQQAeAH4AAQCQAAAALwABAAEAAAAFKrYAgLAAAAACAJEAAAAMAAEAAAAFABoAGwAAAJIAAAAGAAEAAAAGEEEAHgCBAAEAkAAAAC8AAQABAAAABSq2AICwAAAAAgCRAAAADAABAAAABQAaABsAAACSAAAABgABAAAABgABAIIAgwABAJAAAABqAAIAAQAAACwqEB23AIUquACJKrgAjCq4AI8qA7UAUioDtQBYKgO1AFwqA7UAYCoDtQBksQAAAAIAkQAAAAwAAQAAACwAGgAbAAAAkgAAABoABgAAAAYAEgAHABcACAAcAAkAIQAKACYACwADAJYAAAACAAcAlwAAAAsAAQAIAAEACXMACgCYAAAAAwUAAA==