diff --git a/src/functionalTest/java/codechicken/multipart/test/MicroblockTraitsFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MicroblockTraitsFunctionalTest.java new file mode 100644 index 0000000..a05d9d1 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MicroblockTraitsFunctionalTest.java @@ -0,0 +1,87 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import codechicken.microblock.CommonMicroblock; +import codechicken.microblock.FaceMicroClass$; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockClient; +import codechicken.microblock.MicroblockClient$class; +import codechicken.microblock.MicroblockGenerator; + +class MicroblockTraitsFunctionalTest { + + @Test + void generatedCommonTraitUsesMaterialShapeBoundsAndClass() { + int material = MicroMaterialRegistry.materialID("minecraft:stone"); + Microblock part = MicroblockGenerator.create(FaceMicroClass$.MODULE$, material, false); + CommonMicroblock common = (CommonMicroblock) part; + assertFalse(part instanceof MicroblockClient); + assertEquals(material, part.material()); + assertEquals(0, part.shape()); + for (int side = 0; side < 6; side++) { + part.setShape(3, side); + assertEquals(side, common.getSlot()); + assertEquals(1 << side, common.getSlotMask()); + assertSame(FaceMicroClass$.MODULE$, common.microClass()); + assertEquals(FaceMicroClass$.MODULE$.getClassId(), part.itemClassID()); + assertEquals(1, common.getPartialOcclusionBoxes().size()); + assertSame(part.getBounds(), common.getPartialOcclusionBoxes().get(0)); + } + } + + @Test + void absentMaterialReachesTheClientOnlyStoneLookupOnTheServer() { + // getBrokenIcon only needs the Microblock base cast; a proxy would not satisfy that contract. + MissingMaterialClient part = new MissingMaterialClient(); + for (int side : new int[] { -1, 0, 5, 12 }) { + // Forge strips Block.getIcon on a dedicated server; reaching it proves the fallback was selected. + NoSuchMethodError error = assertThrows( + NoSuchMethodError.class, + () -> MicroblockClient$class.getBrokenIcon(part, side)); + assertTrue(error.getMessage().contains("net.minecraft.block.Block.getIcon(II)")); + } + assertEquals(4, part.materialReads); + } + + private static final class MissingMaterialClient extends Microblock implements MicroblockClient { + + int materialReads; + + MissingMaterialClient() { + super(-1); + } + + @Override + public codechicken.microblock.MicroMaterialRegistry.IMicroMaterial getIMaterial() { + materialReads++; + return null; + } + + @Override + public codechicken.microblock.MicroblockClass microClass() { + return FaceMicroClass$.MODULE$; + } + + @Override + public int itemClassID() { + return 0; + } + + @Override + public codechicken.lib.vec.Cuboid6 getBounds() { + return codechicken.lib.vec.Cuboid6.full; + } + + @Override + public net.minecraft.util.IIcon getBrokenIcon(int side) { + return MicroblockClient$class.getBrokenIcon(this, side); + } + + @Override + public void render(codechicken.lib.vec.Vector3 pos, int pass) {} + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 9a9e1c2..718f590 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -168,3 +168,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouche (reference dev-jar SHA-256 `69c0723afd70ab6208b97e5fab12062bfb28a941dfae12575c5d61274102ef6f`). Class-file SHA-256 is `b0f540b063bd6fffcc2a1462d72de1f31b4c8a68caa92ac9298dfdf18a9eb24f`, stored as `src/functionalTest/resources/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64`. + +### `ReferenceScalaMicroblockTraits` + +`scala/codechicken/multipart/compat/ReferenceScalaMicroblockTraits.scala` is a concrete `Microblock` with +`CommonMicroblockClient`. Its frozen Scala forwarders exercise all three `MicroblockTraits.scala` helper classes, +initialization, virtual slot/material/class dispatch, particle callbacks and both common rendering branches. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchanged dev jar at `f5d449d` +(SHA-256 `8061ae3ebb260b619a283519b1752a32e38126ccff537e0fbcdb33c1a736fde3`). Source SHA-256 is +`8a8754632121c8bac8c0c61bf2facbd317462c1c4e43b82127511908ffbcbd98`; class-file SHA-256 is +`dccc96505689e4c6084e11133812b56b39ae90b274762327815a22ef9a1ce75c`, stored as +`src/test/resources/compat/ReferenceScalaMicroblockTraits.class.b64`. Do not regenerate it against the Java helper. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroblockTraits.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroblockTraits.scala new file mode 100644 index 0000000..a98202c --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMicroblockTraits.scala @@ -0,0 +1,37 @@ +package codechicken.multipart.compat + +import codechicken.lib.vec.{Cuboid6, Vector3} +import codechicken.microblock._ +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial + +/** Frozen concrete consumer of the three traits and their generated helper bridges. */ +class ReferenceScalaMicroblockTraits extends Microblock(23) with CommonMicroblockClient { + val events = new java.util.ArrayList[String]() + var selectedMaterial: IMicroMaterial = _ + var selectedClass: CommonMicroClass = _ + var bounds = new Cuboid6(0, 0, 0, 1, 0.5, 1) + var overrideSlot = false + var selectedSlot = 0 + var commonRender = false + var renderedPosition: Vector3 = _ + + override def getIMaterial = { + events.add("material") + selectedMaterial + } + + def microClass = selectedClass + + def getBounds = { + events.add("bounds") + bounds + } + + override def getSlot = if (overrideSlot) selectedSlot else super.getSlot + + override def render(pos: Vector3, pass: Int) { + events.add("render:" + pass) + renderedPosition = pos + if (commonRender) super.render(pos, pass) + } +} diff --git a/src/test/java/codechicken/microblock/MicroblockTraitsCharacterizationTest.java b/src/test/java/codechicken/microblock/MicroblockTraitsCharacterizationTest.java new file mode 100644 index 0000000..4c0adc6 --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroblockTraitsCharacterizationTest.java @@ -0,0 +1,287 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.lang.reflect.Field; +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.List; +import java.util.Objects; +import java.util.Scanner; + +import net.minecraft.util.IIcon; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; +import codechicken.multipart.TileMultipart; + +/** Exercises frozen Scala forwarders compiled before the implementation extraction. */ +class MicroblockTraitsCharacterizationTest { + + @Test + void frozenConstructorAndSlotDispatchKeepStateAndJvmShiftSemantics() throws Exception { + Fixture fixture = new Fixture(); + assertEquals(23, fixture.part.material()); + assertEquals(0, fixture.part.shape()); + assertNull(fixture.client.renderBounds()); + assertEquals(0, fixture.client.renderMask()); + for (int shape = -128; shape < 128; shape++) { + fixture.part.shape_$eq((byte) shape); + assertEquals(shape & 15, fixture.client.getSlot()); + assertEquals(1 << (shape & 15), fixture.client.getSlotMask()); + } + fixture.set("overrideSlot", boolean.class, true); + for (int slot : new int[] { -1, 0, 31, 32, 35 }) { + fixture.set("selectedSlot", int.class, slot); + assertEquals(1 << slot, fixture.client.getSlotMask()); + } + } + + @Test + void partialBoxesAreFreshImmutableListsContainingTheOriginalBounds() throws Exception { + Fixture fixture = new Fixture(); + Cuboid6 bounds = fixture.bounds(); + List first = fixture.client.getPartialOcclusionBoxes(); + List second = fixture.client.getPartialOcclusionBoxes(); + assertNotSame(first, second); + assertEquals(1, first.size()); + assertSame(bounds, first.get(0)); + assertSame(bounds, fixture.client.getRenderBounds()); + bounds.min.x = 0.125; + assertEquals(0.125, first.get(0).min.x); + assertThrows(UnsupportedOperationException.class, () -> first.set(0, Cuboid6.full)); + assertThrows(UnsupportedOperationException.class, () -> first.add(Cuboid6.full)); + assertThrows(UnsupportedOperationException.class, () -> first.remove(0)); + fixture.set("bounds", Cuboid6.class, null); + assertNull(fixture.client.getPartialOcclusionBoxes().get(0)); + assertNull(fixture.client.getRenderBounds()); + assertSame(bounds, first.get(0)); + } + + @Test + void itemClassIdUsesTheCurrentVirtualClass() throws Exception { + Fixture fixture = new Fixture(); + assertThrows(NullPointerException.class, fixture.part::itemClassID); + fixture.set("selectedClass", CommonMicroClass.class, TestClass.create(19)); + assertEquals(19, fixture.part.itemClassID()); + fixture.set("selectedClass", CommonMicroClass.class, TestClass.create(-3)); + assertEquals(-3, fixture.part.itemClassID()); + } + + @Test + void brokenIconsReadTheMaterialOnceAndForwardTheRequestedSide() throws Exception { + Fixture fixture = new Fixture(); + IIcon icon = (IIcon) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] { IIcon.class }, + (proxy, method, args) -> null); + fixture.material((method, args) -> { + assertEquals("getBreakingIcon", method); + fixture.events.add("icon:" + args[0]); + return icon; + }); + for (int side : new int[] { -1, 0, 5, 9 }) { + fixture.events.clear(); + assertSame(icon, fixture.client.getBrokenIcon(side)); + assertEquals(Arrays.asList("material", "icon:" + side), fixture.events); + } + } + + @Test + void staticRenderingChecksThePassBeforeVirtualRender() throws Exception { + Fixture fixture = new Fixture(); + fixture.material((method, args) -> { + assertEquals("canRenderInPass", method); + fixture.events.add("pass:" + args[0]); + return ((Integer) args[0]) == 2; + }); + Vector3 position = new Vector3(4, 5, 6); + assertTrue(fixture.part.renderStatic(position, 2)); + assertSame(position, fixture.get("renderedPosition")); + assertEquals(Arrays.asList("material", "pass:2", "render:2"), fixture.events); + fixture.events.clear(); + assertFalse(fixture.part.renderStatic(position, 3)); + assertEquals(Arrays.asList("material", "pass:3"), fixture.events); + fixture.set("selectedMaterial", IMicroMaterial.class, null); + fixture.events.clear(); + assertThrows(NullPointerException.class, () -> fixture.part.renderStatic(position, 2)); + assertEquals(Arrays.asList("material"), fixture.events); + } + + @Test + void commonRenderingSelectsPhysicalOrShrunkBoundsAndFaceMask() throws Exception { + Fixture fixture = new Fixture(); + fixture.set("commonRender", boolean.class, true); + List faces = new ArrayList<>(); + fixture.material((method, args) -> { + assertEquals("renderMicroFace", method); + faces.add(args.clone()); + return null; + }); + Vector3 position = new Vector3(1, 2, 3); + Cuboid6 shrunk = new Cuboid6(0.1, 0.2, 0.3, 0.7, 0.8, 0.9); + fixture.client.renderBounds_$eq(shrunk); + fixture.client.renderMask_$eq(0b101010); + fixture.client.render(position, -4); + assertFaces(faces, 6, position, -4, fixture.bounds()); + assertEquals(Arrays.asList("render:-4", "material", "bounds"), fixture.events); + faces.clear(); + fixture.events.clear(); + fixture.client.render(position, 0); + assertFaces(faces, 3, position, 0, shrunk); + assertEquals(Arrays.asList("render:0", "material"), fixture.events); + faces.clear(); + fixture.client.renderMask_$eq(63); + fixture.client.render(position, 1); + assertTrue(faces.isEmpty()); + } + + @Test + void particleForwardersReachIconEffectsAndPreserveEvaluationOrder() throws Exception { + Fixture fixture = new Fixture(); + fixture.material((method, args) -> { + assertEquals("getBreakingIcon", method); + fixture.events.add("icon:" + args[0]); + return null; + }); + assertThrows(NullPointerException.class, () -> fixture.part.addDestroyEffects(null)); + assertEquals( + Arrays.asList( + "material", + "icon:0", + "material", + "icon:1", + "material", + "icon:2", + "material", + "icon:3", + "material", + "icon:4", + "material", + "icon:5", + "bounds"), + fixture.events); + fixture.part.bind(new TileMultipart()); + fixture.events.clear(); + MovingObjectPosition hit = new MovingObjectPosition(0, 0, 0, 3, Vec3.createVectorHelper(0, 0, 0)); + assertThrows(NullPointerException.class, () -> fixture.part.addHitEffects(hit, null)); + assertEquals(Arrays.asList("bounds", "material", "icon:3"), fixture.events); + } + + private static void assertFaces(List faces, int count, Vector3 position, int pass, Cuboid6 bounds) { + assertEquals(count, faces.size()); + for (Object[] face : faces) { + assertSame(position, face[0]); + assertEquals(pass, face[1]); + assertSame(bounds, face[2]); + } + } + + private interface MaterialCall { + + Object apply(String method, Object[] arguments); + } + + private static final class Fixture extends ClassLoader { + + final Microblock part; + final CommonMicroblockClient client; + final List events; + + @SuppressWarnings("unchecked") + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + InputStream input = Objects + .requireNonNull(getClass().getResourceAsStream("/compat/ReferenceScalaMicroblockTraits.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(); + client = (CommonMicroblockClient) part; + events = (List) get("events"); + } + + Object get(String name) throws Exception { + return part.getClass().getMethod(name).invoke(part); + } + + Cuboid6 bounds() throws Exception { + return (Cuboid6) get("bounds"); + } + + void set(String name, Class type, Object value) throws Exception { + part.getClass().getMethod(name + "_$eq", type).invoke(part, value); + } + + void material(MaterialCall call) throws Exception { + set( + "selectedMaterial", + IMicroMaterial.class, + Proxy.newProxyInstance( + getParent(), + new Class[] { IMicroMaterial.class }, + (proxy, method, args) -> call.apply(method.getName(), args))); + } + } + + private static final class TestClass extends CommonMicroClass { + + private int id; + + static TestClass create(int id) throws Exception { + // MicroblockClass's constructor registers a trait and requires Forge initialization. + Class unsafe = Class.forName("sun.misc.Unsafe"); + Field field = unsafe.getDeclaredField("theUnsafe"); + field.setAccessible(true); + TestClass value = (TestClass) unsafe.getMethod("allocateInstance", Class.class) + .invoke(field.get(null), TestClass.class); + value.id = id; + return value; + } + + @Override + public int getClassId() { + return id; + } + + @Override + public int itemSlot() { + return 0; + } + + @Override + public PlacementProperties placementProperties() { + return null; + } + + @Override + public String getName() { + return "test:trait-class"; + } + + @Override + public Class baseTrait() { + return Microblock.class; + } + + @Override + public Class clientTrait() { + return MicroblockClient.class; + } + + @Override + public float getResistanceFactor() { + return 1; + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaMicroblockTraits.class.b64 b/src/test/resources/compat/ReferenceScalaMicroblockTraits.class.b64 new file mode 100644 index 0000000..f504014 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaMicroblockTraits.class.b64 @@ -0,0 +1 @@ +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heUxpc3Q7AQApTGphdmEvdXRpbC9BcnJheUxpc3Q8TGphdmEvbGFuZy9TdHJpbmc7PjsBABBzZWxlY3RlZE1hdGVyaWFsAQA9TGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElNaWNyb01hdGVyaWFsOwEADXNlbGVjdGVkQ2xhc3MBAClMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb0NsYXNzOwEABmJvdW5kcwEAHUxjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7AQAMb3ZlcnJpZGVTbG90AQABWgEADHNlbGVjdGVkU2xvdAEAAUkBAAxjb21tb25SZW5kZXIBABByZW5kZXJlZFBvc2l0aW9uAQAdTGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMzsBAAxyZW5kZXJCb3VuZHMBAApyZW5kZXJNYXNrAQAfKClMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAGwATCQACAB4BAAR0aGlzAQA9TGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNaWNyb2Jsb2NrVHJhaXRzOwEAEHJlbmRlckJvdW5kc18kZXEBACAoTGNvZGVjaGlja2VuL2xpYi92ZWMvQ3Vib2lkNjspVgEAA3gkMQEAAygpSQwAHAAXCQACACYBAA5yZW5kZXJNYXNrXyRlcQEABChJKVYBAEFjb2RlY2hpY2tlbiRtaWNyb2Jsb2NrJFRNaWNyb09jY2x1c2lvbkNsaWVudCQkc3VwZXIkb25QYXJ0Q2hhbmdlZAEAJShMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVYBACBjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydAcALAEADW9uUGFydENoYW5nZWQMAC4AKwoALQAvAQAEcGFydAEAIkxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDsBADtjb2RlY2hpY2tlbiRtaWNyb2Jsb2NrJFRNaWNyb09jY2x1c2lvbkNsaWVudCQkc3VwZXIkb25BZGRlZAEAAygpVgEAB29uQWRkZWQMADUANAoALQA2AQA4Y29kZWNoaWNrZW4kbWljcm9ibG9jayRUTWljcm9PY2NsdXNpb25DbGllbnQkJHN1cGVyJHJlYWQBACUoTGNvZGVjaGlja2VuL2xpYi9kYXRhL01DRGF0YUlucHV0OylWAQAEcmVhZAwAOgA5CgAEADsBAAZwYWNrZXQBACJMY29kZWNoaWNrZW4vbGliL2RhdGEvTUNEYXRhSW5wdXQ7AQAyY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb25DbGllbnQkY2xhc3MHAD8BAFMoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uQ2xpZW50O0xjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspVgwALgBBCgBAAEIBADEoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uQ2xpZW50OylWDAA1AEQKAEAARQEAUyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb25DbGllbnQ7TGNvZGVjaGlja2VuL2xpYi9kYXRhL01DRGF0YUlucHV0OylWDAA6AEcKAEAASAEADHJlY2FsY0JvdW5kcwwASgBECgBAAEsBABBnZXRQcmlvcml0eUNsYXNzAQAxKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbkNsaWVudDspSQwATQBOCgBAAE8BAA1nZXRCcm9rZW5JY29uAQAdKEkpTG5ldC9taW5lY3JhZnQvdXRpbC9JSWNvbjsBAC1jb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQkY2xhc3MHAFMBAEYoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDtJKUxuZXQvbWluZWNyYWZ0L3V0aWwvSUljb247DABRAFUKAFQAVgEABHNpZGUBAA1hZGRIaXRFZmZlY3RzAQBaKExuZXQvbWluZWNyYWZ0L3V0aWwvTW92aW5nT2JqZWN0UG9zaXRpb247TG5ldC9taW5lY3JhZnQvY2xpZW50L3BhcnRpY2xlL0VmZmVjdFJlbmRlcmVyOylWAQCDKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQ7TG5ldC9taW5lY3JhZnQvdXRpbC9Nb3ZpbmdPYmplY3RQb3NpdGlvbjtMbmV0L21pbmVjcmFmdC9jbGllbnQvcGFydGljbGUvRWZmZWN0UmVuZGVyZXI7KVYMAFkAWwoAVABcAQADaGl0AQApTG5ldC9taW5lY3JhZnQvdXRpbC9Nb3ZpbmdPYmplY3RQb3NpdGlvbjsBAA5lZmZlY3RSZW5kZXJlcgEALkxuZXQvbWluZWNyYWZ0L2NsaWVudC9wYXJ0aWNsZS9FZmZlY3RSZW5kZXJlcjsBABFhZGREZXN0cm95RWZmZWN0cwEAMShMbmV0L21pbmVjcmFmdC9jbGllbnQvcGFydGljbGUvRWZmZWN0UmVuZGVyZXI7KVYBAFooTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDtMbmV0L21pbmVjcmFmdC9jbGllbnQvcGFydGljbGUvRWZmZWN0UmVuZGVyZXI7KVYMAGIAZAoAVABlAQAMcmVuZGVyU3RhdGljAQAhKExjb2RlY2hpY2tlbi9saWIvdmVjL1ZlY3RvcjM7SSlaAQBKKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQ7TGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMztJKVoMAGcAaQoAVABqAQADcG9zAQAEcGFzcwEAD2dldFJlbmRlckJvdW5kcwEASChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50OylMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAbgBvCgBUAHABAAtnZXRTbG90TWFzawEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jayRjbGFzcwcAcwEALChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrOylJDAByAHUKAHQAdgEAGGdldFBhcnRpYWxPY2NsdXNpb25Cb3hlcwEAEigpTGphdmEvdXRpbC9MaXN0OwEAOyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrOylMamF2YS91dGlsL0xpc3Q7DAB4AHoKAHQAewEAC2l0ZW1DbGFzc0lEDAB9AHUKAHQAfgEAO2NvZGVjaGlja2VuJG1pY3JvYmxvY2skVE1pY3JvT2NjbHVzaW9uJCRzdXBlciRvY2NsdXNpb25UZXN0AQAlKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgEADW9jY2x1c2lvblRlc3QMAIIAgQoALQCDAQAFbnBhcnQBACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbiRjbGFzcwcAhgEATShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OylaDACCAIgKAIcAiQEAF2VkZ2VDb3JuZXJPY2NsdXNpb25UZXN0AQBTKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247KVoBAHsoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247KVoMAIsAjQoAhwCOAQAEZWRnZQEAKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjsBAAZjb3JuZXIBABcoKUxqYXZhL3V0aWwvQXJyYXlMaXN0OwwACwAMCQACAJQBAD8oKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRlcmlhbDsMAA4ADwkAAgCXAQAUc2VsZWN0ZWRNYXRlcmlhbF8kZXEBAEAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElNaWNyb01hdGVyaWFsOylWAQArKClMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb0NsYXNzOwwAEAARCQACAJwBABFzZWxlY3RlZENsYXNzXyRlcQEALChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb0NsYXNzOylWDAASABMJAAIAoAEACmJvdW5kc18kZXEBAAMoKVoMABQAFQkAAgCkAQAQb3ZlcnJpZGVTbG90XyRlcQEABChaKVYMABYAFwkAAgCoAQAQc2VsZWN0ZWRTbG90XyRlcQwAGAAVCQACAKsBABBjb21tb25SZW5kZXJfJGVxAQAfKClMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzOwwAGQAaCQACAK8BABRyZW5kZXJlZFBvc2l0aW9uXyRlcQEAIChMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzOylWAQAMZ2V0SU1hdGVyaWFsDAALAJMKAAIAtAEACG1hdGVyaWFsCAC2AQATamF2YS91dGlsL0FycmF5TGlzdAcAuAEAA2FkZAEAFShMamF2YS9sYW5nL09iamVjdDspWgwAugC7CgC5ALwMAA4AlgoAAgC+AQAKbWljcm9DbGFzcwwAEACbCgACAMEBAAlnZXRCb3VuZHMIABIMABIAHQoAAgDFAQAHZ2V0U2xvdAwAFACjCgACAMgMABYAJQoAAgDKDADHAHUKAHQAzAEABnJlbmRlcgEAIShMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpVgEAJnNjYWxhL2NvbGxlY3Rpb24vbXV0YWJsZS9TdHJpbmdCdWlsZGVyBwDQAQAGPGluaXQ+DADSADQKANEA0wEAB3JlbmRlcjoIANUBAAZhcHBlbmQBADwoTGphdmEvbGFuZy9PYmplY3Q7KUxzY2FsYS9jb2xsZWN0aW9uL211dGFibGUvU3RyaW5nQnVpbGRlcjsMANcA2AoA0QDZAQAac2NhbGEvcnVudGltZS9Cb3hlc1J1blRpbWUHANsBAAxib3hUb0ludGVnZXIBABYoSSlMamF2YS9sYW5nL0ludGVnZXI7DADdAN4KANwA3wEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsMAOEA4goA0QDjDACxALIKAAIA5QwAGACjCgACAOcBADNjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2tDbGllbnQkY2xhc3MHAOkBAFAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9ja0NsaWVudDtMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpVgwAzgDrCgDqAOwBABYoKUxqYXZhL2xhbmcvSXRlcmFibGU7DAB4AHkKAAIA7wEAKigpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsYXNzOwwAwACbCgACAPIMANIAKQoABAD0AQAGJGluaXQkAQArKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspVgwA9gD3CgCHAPgBACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspVgwA9gD6CgB0APsBACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDspVgwA9gD9CgBUAP4MAPYARAoAQAEAAQAyKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2tDbGllbnQ7KVYMAPYBAgoA6gEDCgC5ANMBABtjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDYHAQYGP+AAAAAAAAABAAkoREREREREKVYMANIBCgoBBwELAQA7Y29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb01hdGVyaWFsUmVnaXN0cnkkSU1pY3JvTWF0ZXJpYWwHAQ0BACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeQcBDwEADklNaWNyb01hdGVyaWFsAQAJU2lnbmF0dXJlAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEAMSgpTGphdmEvdXRpbC9MaXN0PExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7PjsBACsoKUxqYXZhL3V0aWwvQXJyYXlMaXN0PExqYXZhL2xhbmcvU3RyaW5nOz47AQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAAxJbm5lckNsYXNzZXMBABlSdW50aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAQAGAAoAEgALAAwAAQESAAAAAgANAAIADgAPAAAAAgAQABEAAAACABIAEwAAAAIAFAAVAAAAAgAWABcAAAACABgAFQAAAAIAGQAaAAAAAgAbABMAAAACABwAFwAAAC4AAQAbAB0AAQETAAAALwABAAEAAAAFKrQAH7AAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAIAAEAIgAjAAEBEwAAADoAAgACAAAABiortQAfsQAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGACQAEwABARUAAAAGAAEAAAAIAAEAHAAlAAEBEwAAAC8AAQABAAAABSq0ACesAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAACAABACgAKQABARMAAAA6AAIAAgAAAAYqG7UAJ7EAAAACARQAAAAWAAIAAAAGACAAIQAAAAAABgAkABcAAQEVAAAABgABAAAACBABACoAKwABARMAAAA6AAIAAgAAAAYqK7cAMLEAAAACARQAAAAWAAIAAAAGACAAIQAAAAAABgAxADIAAQEVAAAABgABAAAACBABADMANAABARMAAAAvAAEAAQAAAAUqtwA3sQAAAAIBFAAAAAwAAQAAAAUAIAAhAAABFQAAAAYAAQAAAAgQAQA4ADkAAQETAAAAOgACAAIAAAAGKiu3ADyxAAAAAgEUAAAAFgACAAAABgAgACEAAAAAAAYAPQA+AAEBFQAAAAYAAQAAAAgAAQAuACsAAQETAAAAOgACAAIAAAAGKiu4AEOxAAAAAgEUAAAAFgACAAAABgAgACEAAAAAAAYAMQAyAAEBFQAAAAYAAQAAAAgAAQA1ADQAAQETAAAALwABAAEAAAAFKrgARrEAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAIAAEAOgA5AAEBEwAAADoAAgACAAAABioruABJsQAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGAD0APgABARUAAAAGAAEAAAAIAAEASgA0AAEBEwAAAC8AAQABAAAABSq4AEyxAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAACAABAE0AJQABARMAAAAvAAEAAQAAAAUquABQrAAAAAIBFAAAAAwAAQAAAAUAIAAhAAABFQAAAAYAAQAAAAgAAQBRAFIAAQETAAAAOgACAAIAAAAGKhu4AFewAAAAAgEUAAAAFgACAAAABgAgACEAAAAAAAYAWAAXAAEBFQAAAAYAAQAAAAgAAQBZAFoAAQETAAAARQADAAMAAAAHKissuABdsQAAAAIBFAAAACAAAwAAAAcAIAAhAAAAAAAHAF4AXwABAAAABwBgAGEAAgEVAAAABgABAAAACAABAGIAYwABARMAAAA6AAIAAgAAAAYqK7gAZrEAAAACARQAAAAWAAIAAAAGACAAIQAAAAAABgBgAGEAAQEVAAAABgABAAAACAABAGcAaAABARMAAABFAAMAAwAAAAcqKxy4AGusAAAAAgEUAAAAIAADAAAABwAgACEAAAAAAAcAbAAaAAEAAAAHAG0AFwACARUAAAAGAAEAAAAIAAEAbgAdAAEBEwAAAC8AAQABAAAABSq4AHGwAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAACAABAHIAJQABARMAAAAvAAEAAQAAAAUquAB3rAAAAAIBFAAAAAwAAQAAAAUAIAAhAAABFQAAAAYAAQAAAAgAAQB4AHkAAgETAAAALwABAAEAAAAFKrgAfLAAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAIARIAAAACARYAAQB9ACUAAQETAAAALwABAAEAAAAFKrgAf6wAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAIEAEAgACBAAEBEwAAADoAAgACAAAABiortwCErAAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGAIUAMgABARUAAAAGAAEAAAAIAAEAggCBAAEBEwAAADoAAgACAAAABioruACKrAAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGAIUAMgABARUAAAAGAAEAAAAIAAEAiwCMAAEBEwAAAEUAAwADAAAAByorLLgAj6wAAAACARQAAAAgAAMAAAAHACAAIQAAAAAABwCQAJEAAQAAAAcAkgCRAAIBFQAAAAYAAQAAAAgAAQALAJMAAgETAAAALwABAAEAAAAFKrQAlbAAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAJARIAAAACARcAAQAOAJYAAQETAAAALwABAAEAAAAFKrQAmLAAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAKAAEAmQCaAAEBEwAAADoAAgACAAAABiortQCYsQAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGACQADwABARUAAAAGAAEAAAAKAAEAEACbAAEBEwAAAC8AAQABAAAABSq0AJ2wAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAACwABAJ4AnwABARMAAAA6AAIAAgAAAAYqK7UAnbEAAAACARQAAAAWAAIAAAAGACAAIQAAAAAABgAkABEAAQEVAAAABgABAAAACwABABIAHQABARMAAAAvAAEAAQAAAAUqtAChsAAAAAIBFAAAAAwAAQAAAAUAIAAhAAABFQAAAAYAAQAAAAwAAQCiACMAAQETAAAAOgACAAIAAAAGKiu1AKGxAAAAAgEUAAAAFgACAAAABgAgACEAAAAAAAYAJAATAAEBFQAAAAYAAQAAAAwAAQAUAKMAAQETAAAALwABAAEAAAAFKrQApawAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAANAAEApgCnAAEBEwAAADoAAgACAAAABiobtQClsQAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGACQAFQABARUAAAAGAAEAAAANAAEAFgAlAAEBEwAAAC8AAQABAAAABSq0AKmsAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAADgABAKoAKQABARMAAAA6AAIAAgAAAAYqG7UAqbEAAAACARQAAAAWAAIAAAAGACAAIQAAAAAABgAkABcAAQEVAAAABgABAAAADgABABgAowABARMAAAAvAAEAAQAAAAUqtACsrAAAAAIBFAAAAAwAAQAAAAUAIAAhAAABFQAAAAYAAQAAAA8AAQCtAKcAAQETAAAAOgACAAIAAAAGKhu1AKyxAAAAAgEUAAAAFgACAAAABgAgACEAAAAAAAYAJAAVAAEBFQAAAAYAAQAAAA8AAQAZAK4AAQETAAAALwABAAEAAAAFKrQAsLAAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAQAAEAsQCyAAEBEwAAADoAAgACAAAABiortQCwsQAAAAIBFAAAABYAAgAAAAYAIAAhAAAAAAAGACQAGgABARUAAAAGAAEAAAAQAAEAswCWAAEBEwAAAD0AAgABAAAADyq2ALUSt7YAvVcqtgC/sAAAAAIBFAAAAAwAAQAAAA8AIAAhAAABFQAAAAoAAgAAABMACgAUAAEAwACbAAEBEwAAAC8AAQABAAAABSq2AMKwAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAAFwABAMMAHQABARMAAAA9AAIAAQAAAA8qtgC1EsS2AL1XKrYAxrAAAAACARQAAAAMAAEAAAAPACAAIQAAARUAAAAKAAIAAAAaAAoAGwABAMcAJQABARMAAABIAAEAAQAAABMqtgDJmQAKKrYAy6cAByq4AM2sAAAAAwEUAAAADAABAAAAEwAgACEAAAEVAAAABgABAAAAHgEYAAAABQACDkMBAAEAzgDPAAEBEwAAAIQAAwADAAAAMSq2ALW7ANFZtwDUEta2ANocuADgtgDatgDktgC9VyortgDmKrYA6JkACSorHLgA7bEAAAADARQAAAAgAAMAAAAxACAAIQAAAAAAMQBsABoAAQAAADEAbQAXAAIBFQAAABIABAAAACEAHgAiACMAIwAwACABGAAAAAMAATAQQQB4AO4AAQETAAAALwABAAEAAAAFKrYA8LAAAAACARQAAAAMAAEAAAAFACAAIQAAARUAAAAGAAEAAAAIEEEAwADxAAEBEwAAAC8AAQABAAAABSq2APOwAAAAAgEUAAAADAABAAAABQAgACEAAAEVAAAABgABAAAACAABANIANAABARMAAACGAA8AAQAAAEgqEBe3APUquAD5KrgA/Cq4AP8quAEBKrgBBCq7ALlZtwEFtQCVKrsBB1kODg4PFAEID7cBDLUAoSoDtQClKgO1AKkqA7UArLEAAAACARQAAAAMAAEAAABIACAAIQAAARUAAAAaAAYAAAAIABoACQAlAAwAOAANAD0ADgBCAA8ABAEZAAAAAgAHARoAAAAKAAEBDgEQAREGCQEbAAAACwABAAgAAQAJcwAKARwAAAADBQAA