From dc845bc91ed4f9f1d4590ff9dac784619c3e0bb5 Mon Sep 17 00:00:00 2001 From: Algent Date: Thu, 3 Sep 2026 21:37:51 +0200 Subject: [PATCH] test: characterize face microblock traits --- .../FaceMicroblockTraitsFunctionalTest.java | 105 ++++++++++ src/test/fixtures/README.md | 12 ++ .../ReferenceScalaFaceMicroblockClient.scala | 39 ++++ ...eMicroblockTraitsCharacterizationTest.java | 190 ++++++++++++++++++ ...ferenceScalaFaceMicroblockClient.class.b64 | 1 + 5 files changed, 347 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/FaceMicroblockTraitsFunctionalTest.java create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaFaceMicroblockClient.scala create mode 100644 src/test/java/codechicken/microblock/FaceMicroblockTraitsCharacterizationTest.java create mode 100644 src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64 diff --git a/src/functionalTest/java/codechicken/multipart/test/FaceMicroblockTraitsFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/FaceMicroblockTraitsFunctionalTest.java new file mode 100644 index 0000000..a23d020 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/FaceMicroblockTraitsFunctionalTest.java @@ -0,0 +1,105 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Cuboid6; +import codechicken.microblock.FaceMicroClass; +import codechicken.microblock.FaceMicroClass$; +import codechicken.microblock.FaceMicroblock; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockGenerator; +import scala.Tuple2; + +class FaceMicroblockTraitsFunctionalTest { + + @Test + void generatedFaceSelectsFactoryBoundsForEverySupportedShape() { + Microblock part = MicroblockGenerator.create(FaceMicroClass$.MODULE$, 0, false); + FaceMicroblock face = (FaceMicroblock) part; + assertSame(FaceMicroClass$.MODULE$, face.microClass()); + assertSame(FaceMicroClass$.MODULE$, part.microClass()); + for (int size = 1; size <= 7; size++) { + for (int side = 0; side < 6; side++) { + part.setShape(size, side); + Cuboid6 bounds = face.getBounds(); + assertSame(FaceMicroClass.aBounds()[size << 4 | side], bounds); + double thickness = size / 8D; + int axis = side / 2 == 0 ? 1 : side / 2 == 1 ? 2 : 0; + double[] min = { bounds.min.x, bounds.min.y, bounds.min.z }; + double[] max = { bounds.max.x, bounds.max.y, bounds.max.z }; + for (int coordinate = 0; coordinate < 3; coordinate++) { + assertEquals(coordinate == axis && (side & 1) != 0 ? 1 - thickness : 0, min[coordinate], 1e-12); + assertEquals(coordinate == axis && (side & 1) == 0 ? thickness : 1, max[coordinate], 1e-12); + } + } + } + } + + @Test + void boundsLookupKeepsSignedIndexingNullEntriesAndLiveArrayReplacement() { + Microblock part = MicroblockGenerator.create(FaceMicroClass$.MODULE$, 0, false); + FaceMicroblock face = (FaceMicroblock) part; + part.shape_$eq((byte) 0); + assertNull(face.getBounds()); + part.shape_$eq((byte) 127); + assertNull(face.getBounds()); + for (int shape = -128; shape < 0; shape++) { + part.shape_$eq((byte) shape); + assertThrows(ArrayIndexOutOfBoundsException.class, face::getBounds); + } + Cuboid6[] original = FaceMicroClass.aBounds(); + Cuboid6[] replacement = new Cuboid6[256]; + Cuboid6 sentinel = new Cuboid6(0.2, 0.3, 0.4, 0.5, 0.6, 0.7); + replacement[35] = sentinel; + part.shape_$eq((byte) 35); + try { + FaceMicroClass.aBounds_$eq(replacement); + assertSame(sentinel, face.getBounds()); + replacement[35] = null; + assertNull(face.getBounds()); + FaceMicroClass.aBounds_$eq(new Cuboid6[1]); + assertThrows(ArrayIndexOutOfBoundsException.class, face::getBounds); + } finally { + FaceMicroClass.aBounds_$eq(original); + } + assertSame(original[35], face.getBounds()); + } + + @Test + void solidityDelegatesToCurrentMaterialAndIgnoresTheSide() { + Microblock part = MicroblockGenerator.create(FaceMicroClass$.MODULE$, 0, false); + FaceMicroblock face = (FaceMicroblock) part; + Tuple2[] materials = MicroMaterialRegistry.getIdMap(); + Tuple2 original = materials[0]; + AtomicBoolean solid = new AtomicBoolean(); + AtomicInteger calls = new AtomicInteger(); + IMicroMaterial material = (IMicroMaterial) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] { IMicroMaterial.class }, + (proxy, method, args) -> { + assertEquals("isSolid", method.getName()); + calls.incrementAndGet(); + return solid.get(); + }); + try { + materials[0] = new Tuple2<>(original._1(), material); + for (boolean value : new boolean[] { false, true }) { + solid.set(value); + for (int side : new int[] { -1, 0, 1, 2, 3, 4, 5, 6 }) assertEquals(value, face.solid(side)); + } + assertEquals(16, calls.get()); + materials[0] = new Tuple2<>(original._1(), null); + assertThrows(NullPointerException.class, () -> face.solid(0)); + } finally { + materials[0] = original; + } + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 718f590..49de826 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -180,3 +180,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchange `8a8754632121c8bac8c0c61bf2facbd317462c1c4e43b82127511908ffbcbd98`; class-file SHA-256 is `dccc96505689e4c6084e11133812b56b39ae90b274762327815a22ef9a1ce75c`, stored as `src/test/resources/compat/ReferenceScalaMicroblockTraits.class.b64`. Do not regenerate it against the Java helper. + +### `ReferenceScalaFaceMicroblockClient` + +`scala/codechicken/multipart/compat/ReferenceScalaFaceMicroblockClient.scala` freezes a concrete face-client +implementor and its `FaceMicroblockClient$class.render` forwarder. Tests execute exact per-face rendering for all +six slots and 64 masks, negative passes, virtual transparency/slot dispatch and material caching across both draws. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the unchanged dev jar at `d89aaea` +(SHA-256 `c97d389524bab62fabe6584fcaace5b5fe2d59db3e575a60605fb083fc46856f`). Source SHA-256 is +`3848f0ba68344b103614e6aaa85dfe300a1b3c41b94cb171224b411a46ecdf3b`; class-file SHA-256 is +`96283797af5f81137e6fa354fbce5886e0de4f00655a6ab89a21de94bfadcbde`, stored as +`src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64`. Do not regenerate it against the port. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaFaceMicroblockClient.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaFaceMicroblockClient.scala new file mode 100644 index 0000000..21f41ec --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaFaceMicroblockClient.scala @@ -0,0 +1,39 @@ +package codechicken.multipart.compat + +import codechicken.lib.vec.Cuboid6 +import codechicken.microblock._ +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial + +/** Frozen face-client implementor; its forwarder invokes FaceMicroblockClient$class.render. */ +class ReferenceScalaFaceMicroblockClient extends Microblock(37) with FaceMicroblockClient { + val events = new java.util.ArrayList[String]() + var selectedMaterial: IMicroMaterial = _ + var bounds = new Cuboid6(0, 0, 0, 1, 0.25, 1) + var transparent = false + var selectedSlot = 0 + var slotAdvance = 0 + + def microClass: CommonMicroClass = null + + override def getIMaterial = { + events.add("material") + selectedMaterial + } + + override def isTransparent = { + events.add("transparent") + transparent + } + + def getBounds = { + events.add("bounds") + bounds + } + + override def getSlot = { + events.add("slot:" + selectedSlot) + val slot = selectedSlot + selectedSlot += slotAdvance + slot + } +} diff --git a/src/test/java/codechicken/microblock/FaceMicroblockTraitsCharacterizationTest.java b/src/test/java/codechicken/microblock/FaceMicroblockTraitsCharacterizationTest.java new file mode 100644 index 0000000..0c761a6 --- /dev/null +++ b/src/test/java/codechicken/microblock/FaceMicroblockTraitsCharacterizationTest.java @@ -0,0 +1,190 @@ +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.List; +import java.util.Objects; +import java.util.Scanner; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; + +/** Runs a frozen Scala face-client forwarder against the original or extracted helper. */ +class FaceMicroblockTraitsCharacterizationTest { + + @Test + void negativePassUsesPhysicalBoundsAndIgnoresTransparencyAndMasks() throws Exception { + Fixture f = new Fixture(); + assertEquals(37, f.part.material()); + assertNull(f.client.renderBounds()); + assertEquals(0, f.client.renderMask()); + f.client.renderMask_$eq(63); + for (int pass : new int[] { -1, -7 }) { + f.clear(); + f.client.render(f.position, pass); + assertDraw(f, 0, 6, f.bounds, pass, 0); + assertEquals(6, f.faces.size()); + assertEquals(Arrays.asList("material", "bounds"), f.events); + } + } + + @Test + void transparentPassRendersExactlyTheUnmaskedShrunkFaces() throws Exception { + Fixture f = new Fixture(); + f.set("transparent", boolean.class, true); + f.client.renderBounds_$eq(f.shrunk); + for (int mask = 0; mask < 64; mask++) { + f.clear(); + f.client.renderMask_$eq(mask); + f.client.render(f.position, 2); + int count = 6 - Integer.bitCount(mask); + assertDraw(f, 0, count, f.shrunk, 2, mask); + assertEquals(count, f.faces.size()); + assertEquals(Arrays.asList("transparent", "material"), f.events); + } + } + + @Test + void opaquePassRendersShrunkSidesThenTheOuterFaceForEveryMaskAndSlot() throws Exception { + Fixture f = new Fixture(); + f.client.renderBounds_$eq(f.shrunk); + for (int slot = 0; slot < 6; slot++) { + f.set("selectedSlot", int.class, slot); + for (int mask = 0; mask < 64; mask++) { + f.clear(); + f.client.renderMask_$eq(mask); + f.client.render(f.position, 0); + int innerMask = mask | 1 << slot; + int count = 6 - Integer.bitCount(innerMask); + assertDraw(f, 0, count, f.shrunk, 0, innerMask); + assertDraw(f, count, 1, Cuboid6.full, 0, ~(1 << slot)); + assertEquals(count + 1, f.faces.size()); + assertEquals(Arrays.asList("transparent", "material", "slot:" + slot, "slot:" + slot), f.events); + } + } + } + + @Test + void opaquePassCachesMaterialButEvaluatesTheSlotAgainAfterTheFirstDraw() throws Exception { + Fixture f = new Fixture(); + f.client.renderBounds_$eq(f.shrunk); + f.set("selectedSlot", int.class, 2); + f.set("slotAdvance", int.class, 1); + IMicroMaterial material = f.material; + f.afterFace = () -> { + try { + f.set("selectedMaterial", IMicroMaterial.class, null); + } catch (Exception e) { + throw new AssertionError(e); + } + }; + f.client.render(f.position, 1); + assertDraw(f, 0, 5, f.shrunk, 1, 1 << 2); + assertDraw(f, 5, 1, Cuboid6.full, 1, ~(1 << 3)); + assertEquals(6, f.faces.size()); + for (Face face : f.faces) assertSame(material, face.material); + assertEquals(Arrays.asList("transparent", "material", "slot:2", "slot:3"), f.events); + } + + @Test + void missingMaterialFailsOnlyWhenAnUnmaskedFaceIsEmitted() throws Exception { + Fixture f = new Fixture(); + f.set("selectedMaterial", IMicroMaterial.class, null); + f.set("transparent", boolean.class, true); + f.client.renderBounds_$eq(f.shrunk); + f.client.renderMask_$eq(63); + assertDoesNotThrow(() -> f.client.render(f.position, 0)); + assertTrue(f.faces.isEmpty()); + f.client.renderMask_$eq(0); + assertThrows(NullPointerException.class, () -> f.client.render(f.position, 0)); + f.set("transparent", boolean.class, false); + f.client.renderMask_$eq(63); + f.clear(); + assertThrows(NullPointerException.class, () -> f.client.render(f.position, 0)); + assertEquals(Arrays.asList("transparent", "material", "slot:0", "slot:0"), f.events); + } + + private static void assertDraw(Fixture f, int offset, int count, Cuboid6 bounds, int pass, int mask) { + int index = offset; + for (int side = 0; side < 6; side++) { + if ((mask & 1 << side) != 0) continue; + Face face = f.faces.get(index++); + assertEquals(side, face.side); + assertSame(bounds, face.bounds); + assertSame(f.position, face.position); + assertEquals(pass, face.pass); + } + assertEquals(offset + count, index); + } + + private static final class Face { + + final Object material; + final Vector3 position; + final Cuboid6 bounds; + final int pass; + final int side; + + Face(Object material, Object[] args) { + this.material = material; + 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 Microblock part; + final FaceMicroblockClient client; + final List events; + final List faces = new ArrayList<>(); + final Vector3 position = new Vector3(2, 3, 4); + final Cuboid6 shrunk = new Cuboid6(0.1, 0.05, 0.2, 0.8, 0.2, 0.9); + final Cuboid6 bounds; + final IMicroMaterial material; + Runnable afterFace; + + @SuppressWarnings("unchecked") + Fixture() throws Exception { + super(Microblock.class.getClassLoader()); + InputStream input = Objects.requireNonNull( + getClass().getResourceAsStream("/compat/ReferenceScalaFaceMicroblockClient.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 = (FaceMicroblockClient) part; + events = (List) part.getClass().getMethod("events").invoke(part); + bounds = (Cuboid6) part.getClass().getMethod("bounds").invoke(part); + material = (IMicroMaterial) Proxy + .newProxyInstance(getParent(), new Class[] { IMicroMaterial.class }, (proxy, method, args) -> { + assertEquals("renderMicroFace", method.getName()); + faces.add(new Face(proxy, args)); + if (afterFace != null) afterFace.run(); + return null; + }); + set("selectedMaterial", IMicroMaterial.class, material); + } + + void set(String name, Class type, Object value) throws Exception { + part.getClass().getMethod(name + "_$eq", type).invoke(part, value); + } + + void clear() { + events.clear(); + faces.clear(); + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64 b/src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64 new file mode 100644 index 0000000..7363d89 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaFaceMicroblockClient.class.b64 @@ -0,0 +1 @@ +yv66vgAAADQBFAEAP2NvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFGYWNlTWljcm9ibG9ja0NsaWVudAcAAQEAIWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9jawcAAwEAK2NvZGVjaGlja2VuL21pY3JvYmxvY2svRmFjZU1pY3JvYmxvY2tDbGllbnQHAAUBAChSZWZlcmVuY2VTY2FsYUZhY2VNaWNyb2Jsb2NrQ2xpZW50LnNjYWxhAQAeTHNjYWxhL3JlZmxlY3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBBMMGAQVdYQFCAQMBJREhRVUzZ0tKLGduWTNURwZkF01SMWRLNks3TV04Y1k+HDduETdqSzokKEJBAgUDGRl3Lhw5YmkqEVFBQgEKW1ZkRy8bOWJlUlQRYUIBDEc+JFdtGTVqRy4sZ24BARQHAVEBAwUCDB01CUFCAwIODQVRUS5bMnNfCmR3blk2CgU9YSFBQydqR0p8Jw1cOGRXQhExIkUFAyUxEUFDUjFkSzZLN01dOGNZPhw3bhE3aks6JAgiAgsBCQMpEkECH2pdJiRoCEYBFyEJOQIhRAEDER1JAkExQQUCaQlhIRo8Zl1SHFgjQQ4RB3EJMyVEAR4VCXFyJAEDdmkmZCciARECCSkMZy9ZBQNFdRERIhE6c0NmZBVuHTsRBRFSY0JBEykbBTEjIkEUAgtNHBcNXDEKBSUyE0ECKXNLEixnLQMCLFkJMTELHjpqXR5UISELFAkNOQIBFSEDHAMdKWcvGjh1ZwJCEQINAUECAwdJEUEZAiFNLEcuWjJ1SxJsFQ1eM3NTBmRXIwEaEQVNamRCARs8HQkpJEgEAjdzNQlxRwMCOREFMUFIXThwaXpKEWFCBQMbGUkhARAHAis1SzdNXThOQ1IsJy9bMW0lFjwXbh07c3MmEWFoEAIPEzZLN01dOE5DUiwnL1sxbRUJYUQCQwVCAQEHCRkhQwEFBiEyLxo3ZkdSLEctVDF1S0pMFw1cMCVLRiQiYREkEQUVIhVCQSMnBREpZi4bOwkPHQMVESFhAWUFGQEQShkJDSUDARUpAzMDRRlYDVwzZGkWJFcqGTtmZSYMRw4JBQgXAgENESIBTQMZEXcuHjhlZ1YJUQoFAk8nNglxSgMCUSMGGWEvWjILBUkzEWEBN2pFJhFBaxQCCAdWFHcuGzM3ER0xBgExQQUCXQshQlk4dl0SHHgMSjNyKQkZBQxDBEgrBgUJGUEnCQ1pAwEVKQNOAx0Rdy4eOGVnAkJxARgBQQITBVEsQQZ1ZQZ0Nw9dMXNLOiRYIwEwEQUVehZCATEnBR0RdW4cN2ZDOkRxQRkBQQITBTEtQQh1ZQZ0Nw9dMXNLOiR4DEozcikJGUUNQwRIQwYFCRkBMAkNGQQBFSkDXwMxISgvGTh0YQYUWE0cOyERHUEHATFBBQIlDEFiXTNtSw4kWA1aKm1fUiwSQRsJA0stTCEBHBQDByVzRw9DBG8BAQdJEUE4AiFNLEcuWjJ1SxIcRm4cO2BJFQxIQ0EicREdOVUuIUFBAilEYUEdASFCE1EXIUQ6ZlkWHEcvGjNUWT4kCAVDBHUBAQdJEUE1AhdNZHcOXiFlbQZ0Ny0aBQhtAgENESIBeAM9GUhuHDtCSVoMZ25ZM2BJFQxIQ0EieREdOVUvIUFBAilEYUEfASFCE1EXAUQ6bV9SDEVNXjFvRxYEAyICPwEJA2kYQUM3akdKfDcJXDF0Z1IJYRAFAgx/JhkREQEHAyENe1cuXDhvGyYcJ29cIm1DThwIYkJBAwERBRNxQQENTxYkGCpUMXVLSkwXDRwLAmUhORExAgEFQgU1ESFENXQpSgxnbh05YmUWdEcPRgFfERkJCQIBQwEZBklxLRo7Q19WdEdtHQUHAysBQRFJNQIPHSxHbxU3cGkCAQAGZXZlbnRzAQAVTGphdmEvdXRpbC9BcnJheUxpc3Q7AQApTGphdmEvdXRpbC9BcnJheUxpc3Q8TGphdmEvbGFuZy9TdHJpbmc7PjsBABBzZWxlY3RlZE1hdGVyaWFsAQA9TGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElNaWNyb01hdGVyaWFsOwEABmJvdW5kcwEAHUxjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7AQALdHJhbnNwYXJlbnQBAAFaAQAMc2VsZWN0ZWRTbG90AQABSQEAC3Nsb3RBZHZhbmNlAQAMcmVuZGVyQm91bmRzAQAKcmVuZGVyTWFzawEABnJlbmRlcgEAIShMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpVgEAMWNvZGVjaGlja2VuL21pY3JvYmxvY2svRmFjZU1pY3JvYmxvY2tDbGllbnQkY2xhc3MHABsBAE4oTGNvZGVjaGlja2VuL21pY3JvYmxvY2svRmFjZU1pY3JvYmxvY2tDbGllbnQ7TGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMztJKVYMABkAHQoAHAAeAQAEdGhpcwEAQUxjb2RlY2hpY2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhRmFjZU1pY3JvYmxvY2tDbGllbnQ7AQADcG9zAQAdTGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMzsBAARwYXNzAQAfKClMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAFwARCQACACYBABByZW5kZXJCb3VuZHNfJGVxAQAgKExjb2RlY2hpY2tlbi9saWIvdmVjL0N1Ym9pZDY7KVYBAAN4JDEBAAMoKUkMABgAFQkAAgAsAQAOcmVuZGVyTWFza18kZXEBAAQoSSlWAQBBY29kZWNoaWNrZW4kbWljcm9ibG9jayRUTWljcm9PY2NsdXNpb25DbGllbnQkJHN1cGVyJG9uUGFydENoYW5nZWQBACUoTGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OylWAQAgY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQHADIBAA1vblBhcnRDaGFuZ2VkDAA0ADEKADMANQEABHBhcnQBACJMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7AQA7Y29kZWNoaWNrZW4kbWljcm9ibG9jayRUTWljcm9PY2NsdXNpb25DbGllbnQkJHN1cGVyJG9uQWRkZWQBAAMoKVYBAAdvbkFkZGVkDAA7ADoKADMAPAEAOGNvZGVjaGlja2VuJG1pY3JvYmxvY2skVE1pY3JvT2NjbHVzaW9uQ2xpZW50JCRzdXBlciRyZWFkAQAlKExjb2RlY2hpY2tlbi9saWIvZGF0YS9NQ0RhdGFJbnB1dDspVgEABHJlYWQMAEAAPwoABABBAQAGcGFja2V0AQAiTGNvZGVjaGlja2VuL2xpYi9kYXRhL01DRGF0YUlucHV0OwEAMmNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uQ2xpZW50JGNsYXNzBwBFAQBTKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbkNsaWVudDtMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7KVYMADQARwoARgBIAQAxKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbkNsaWVudDspVgwAOwBKCgBGAEsBAFMoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uQ2xpZW50O0xjb2RlY2hpY2tlbi9saWIvZGF0YS9NQ0RhdGFJbnB1dDspVgwAQABNCgBGAE4BAAxyZWNhbGNCb3VuZHMMAFAASgoARgBRAQAQZ2V0UHJpb3JpdHlDbGFzcwEAMShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb25DbGllbnQ7KUkMAFMAVAoARgBVAQANZ2V0QnJva2VuSWNvbgEAHShJKUxuZXQvbWluZWNyYWZ0L3V0aWwvSUljb247AQAtY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50JGNsYXNzBwBZAQBGKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQ7SSlMbmV0L21pbmVjcmFmdC91dGlsL0lJY29uOwwAVwBbCgBaAFwBAARzaWRlAQANYWRkSGl0RWZmZWN0cwEAWihMbmV0L21pbmVjcmFmdC91dGlsL01vdmluZ09iamVjdFBvc2l0aW9uO0xuZXQvbWluZWNyYWZ0L2NsaWVudC9wYXJ0aWNsZS9FZmZlY3RSZW5kZXJlcjspVgEAgyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50O0xuZXQvbWluZWNyYWZ0L3V0aWwvTW92aW5nT2JqZWN0UG9zaXRpb247TG5ldC9taW5lY3JhZnQvY2xpZW50L3BhcnRpY2xlL0VmZmVjdFJlbmRlcmVyOylWDABfAGEKAFoAYgEAA2hpdAEAKUxuZXQvbWluZWNyYWZ0L3V0aWwvTW92aW5nT2JqZWN0UG9zaXRpb247AQAOZWZmZWN0UmVuZGVyZXIBAC5MbmV0L21pbmVjcmFmdC9jbGllbnQvcGFydGljbGUvRWZmZWN0UmVuZGVyZXI7AQARYWRkRGVzdHJveUVmZmVjdHMBADEoTG5ldC9taW5lY3JhZnQvY2xpZW50L3BhcnRpY2xlL0VmZmVjdFJlbmRlcmVyOylWAQBaKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvYmxvY2tDbGllbnQ7TG5ldC9taW5lY3JhZnQvY2xpZW50L3BhcnRpY2xlL0VmZmVjdFJlbmRlcmVyOylWDABoAGoKAFoAawEADHJlbmRlclN0YXRpYwEAIShMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpWgEASihMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50O0xjb2RlY2hpY2tlbi9saWIvdmVjL1ZlY3RvcjM7SSlaDABtAG8KAFoAcAEAD2dldFJlbmRlckJvdW5kcwEASChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb2Jsb2NrQ2xpZW50OylMY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2OwwAcgBzCgBaAHQBAAtnZXRTbG90TWFzawEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jayRjbGFzcwcAdwEALChMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrOylJDAB2AHkKAHgAegEAGGdldFBhcnRpYWxPY2NsdXNpb25Cb3hlcwEAEigpTGphdmEvdXRpbC9MaXN0OwEAOyhMY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrOylMamF2YS91dGlsL0xpc3Q7DAB8AH4KAHgAfwEAC2l0ZW1DbGFzc0lEDACBAHkKAHgAggEAO2NvZGVjaGlja2VuJG1pY3JvYmxvY2skVE1pY3JvT2NjbHVzaW9uJCRzdXBlciRvY2NsdXNpb25UZXN0AQAlKExjb2RlY2hpY2tlbi9tdWx0aXBhcnQvVE11bHRpUGFydDspWgEADW9jY2x1c2lvblRlc3QMAIYAhQoAMwCHAQAFbnBhcnQBACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbiRjbGFzcwcAigEATShMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247TGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVsdGlQYXJ0OylaDACGAIwKAIsAjQEAF2VkZ2VDb3JuZXJPY2NsdXNpb25UZXN0AQBTKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247KVoBAHsoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svVE1pY3JvT2NjbHVzaW9uO0xjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjtMY29kZWNoaWNrZW4vbWljcm9ibG9jay9UTWljcm9PY2NsdXNpb247KVoMAI8AkQoAiwCSAQAEZWRnZQEAKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjsBAAZjb3JuZXIBABcoKUxqYXZhL3V0aWwvQXJyYXlMaXN0OwwACwAMCQACAJgBAD8oKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRlcmlhbDsMAA4ADwkAAgCbAQAUc2VsZWN0ZWRNYXRlcmlhbF8kZXEBAEAoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElNaWNyb01hdGVyaWFsOylWDAAQABEJAAIAnwEACmJvdW5kc18kZXEBAAMoKVoMABIAEwkAAgCjAQAPdHJhbnNwYXJlbnRfJGVxAQAEKFopVgwAFAAVCQACAKcBABBzZWxlY3RlZFNsb3RfJGVxDAAWABUJAAIAqgEAD3Nsb3RBZHZhbmNlXyRlcQEACm1pY3JvQ2xhc3MBACsoKUxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvQ2xhc3M7AQAMZ2V0SU1hdGVyaWFsDAALAJcKAAIAsAEACG1hdGVyaWFsCACyAQATamF2YS91dGlsL0FycmF5TGlzdAcAtAEAA2FkZAEAFShMamF2YS9sYW5nL09iamVjdDspWgwAtgC3CgC1ALgMAA4AmgoAAgC6AQANaXNUcmFuc3BhcmVudAgAEgwAEgCiCgACAL4BAAlnZXRCb3VuZHMIABAMABAAJQoAAgDCAQAHZ2V0U2xvdAEAJnNjYWxhL2NvbGxlY3Rpb24vbXV0YWJsZS9TdHJpbmdCdWlsZGVyBwDFAQAGPGluaXQ+DADHADoKAMYAyAEABXNsb3Q6CADKAQAGYXBwZW5kAQA8KExqYXZhL2xhbmcvT2JqZWN0OylMc2NhbGEvY29sbGVjdGlvbi9tdXRhYmxlL1N0cmluZ0J1aWxkZXI7DADMAM0KAMYAzgwAFAArCgACANABABpzY2FsYS9ydW50aW1lL0JveGVzUnVuVGltZQcA0gEADGJveFRvSW50ZWdlcgEAFihJKUxqYXZhL2xhbmcvSW50ZWdlcjsMANQA1QoA0wDWAQAIdG9TdHJpbmcBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwwA2ADZCgDGANoMABYAKwoAAgDcDACpAC8KAAIA3gEABHNsb3QBABYoKUxqYXZhL2xhbmcvSXRlcmFibGU7DAB8AH0KAAIA4gEAKigpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsYXNzOwwArQCuCgACAOUMAMcALwoABADnAQAGJGluaXQkAQArKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL1RNaWNyb09jY2x1c2lvbjspVgwA6QDqCgCLAOsBACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svQ29tbW9uTWljcm9ibG9jazspVgwA6QDtCgB4AO4BACwoTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9ibG9ja0NsaWVudDspVgwA6QDwCgBaAPEMAOkASgoARgDzAQAzY29kZWNoaWNrZW4vbWljcm9ibG9jay9Db21tb25NaWNyb2Jsb2NrQ2xpZW50JGNsYXNzBwD1AQAyKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0NvbW1vbk1pY3JvYmxvY2tDbGllbnQ7KVYMAOkA9woA9gD4AQAwKExjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL0ZhY2VNaWNyb2Jsb2NrQ2xpZW50OylWDADpAPoKABwA+woAtQDIAQAbY29kZWNoaWNrZW4vbGliL3ZlYy9DdWJvaWQ2BwD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AAQABAAAABSq4AICwAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAACAEKAAAAAgEOAAEAgQArAAEBCwAAAC8AAQABAAAABSq4AIOsAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAACBABAIQAhQABAQsAAAA6AAIAAgAAAAYqK7cAiKwAAAACAQwAAAAWAAIAAAAGACAAIQAAAAAABgCJADgAAQENAAAABgABAAAACAABAIYAhQABAQsAAAA6AAIAAgAAAAYqK7gAjqwAAAACAQwAAAAWAAIAAAAGACAAIQAAAAAABgCJADgAAQENAAAABgABAAAACAABAI8AkAABAQsAAABFAAMAAwAAAAcqKyy4AJOsAAAAAgEMAAAAIAADAAAABwAgACEAAAAAAAcAlACVAAEAAAAHAJYAlQACAQ0AAAAGAAEAAAAIAAEACwCXAAIBCwAAAC8AAQABAAAABSq0AJmwAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAACQEKAAAAAgEPAAEADgCaAAEBCwAAAC8AAQABAAAABSq0AJywAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAACgABAJ0AngABAQsAAAA6AAIAAgAAAAYqK7UAnLEAAAACAQwAAAAWAAIAAAAGACAAIQAAAAAABgAqAA8AAQENAAAABgABAAAACgABABAAJQABAQsAAAAvAAEAAQAAAAUqtACgsAAAAAIBDAAAAAwAAQAAAAUAIAAhAAABDQAAAAYAAQAAAAsAAQChACkAAQELAAAAOgACAAIAAAAGKiu1AKCxAAAAAgEMAAAAFgACAAAABgAgACEAAAAAAAYAKgARAAEBDQAAAAYAAQAAAAsAAQASAKIAAQELAAAALwABAAEAAAAFKrQApKwAAAACAQwAAAAMAAEAAAAFACAAIQAAAQ0AAAAGAAEAAAAMAAEApQCmAAEBCwAAADoAAgACAAAABiobtQCksQAAAAIBDAAAABYAAgAAAAYAIAAhAAAAAAAGACoAEwABAQ0AAAAGAAEAAAAMAAEAFAArAAEBCwAAAC8AAQABAAAABSq0AKisAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAADQABAKkALwABAQsAAAA6AAIAAgAAAAYqG7UAqLEAAAACAQwAAAAWAAIAAAAGACAAIQAAAAAABgAqABUAAQENAAAABgABAAAADQABABYAKwABAQsAAAAvAAEAAQAAAAUqtACrrAAAAAIBDAAAAAwAAQAAAAUAIAAhAAABDQAAAAYAAQAAAA4AAQCsAC8AAQELAAAAOgACAAIAAAAGKhu1AKuxAAAAAgEMAAAAFgACAAAABgAgACEAAAAAAAYAKgAVAAEBDQAAAAYAAQAAAA4AAQCtAK4AAQELAAAALAABAAEAAAACAbAAAAACAQwAAAAMAAEAAAACACAAIQAAAQ0AAAAGAAEAAAAQAAEArwCaAAEBCwAAAD0AAgABAAAADyq2ALESs7YAuVcqtgC7sAAAAAIBDAAAAAwAAQAAAA8AIAAhAAABDQAAAAoAAgAAABMACgAUAAEAvACiAAEBCwAAAD0AAgABAAAADyq2ALESvbYAuVcqtgC/rAAAAAIBDAAAAAwAAQAAAA8AIAAhAAABDQAAAAoAAgAAABgACgAZAAEAwAAlAAEBCwAAAD0AAgABAAAADyq2ALESwbYAuVcqtgDDsAAAAAIBDAAAAAwAAQAAAA8AIAAhAAABDQAAAAoAAgAAAB0ACgAeAAEAxAArAAEBCwAAAHUAAwACAAAANSq2ALG7AMZZtwDJEsu2AM8qtgDRuADXtgDPtgDbtgC5Vyq2ANE8Kiq2ANEqtgDdYLYA3xusAAAAAgEMAAAAFgACAAAANQAgACEAAAAmAA4A4AAVAAEBDQAAABIABAAAACIAIQAjACYAJAAzACUQQQB8AOEAAQELAAAALwABAAEAAAAFKrYA47AAAAACAQwAAAAMAAEAAAAFACAAIQAAAQ0AAAAGAAEAAAAIEEEArQDkAAEBCwAAAC8AAQABAAAABSq2AOawAAAAAgEMAAAADAABAAAABQAgACEAAAENAAAABgABAAAACAABAMcAOgABAQsAAACKAA8AAQAAAEwqECW3AOgquADsKrgA7yq4APIquAD0KrgA+Sq4APwquwC1WbcA/bUAmSq7AP9ZDg4ODxQBAA+3AQS1AKAqA7UApCoDtQCoKgO1AKuxAAAAAgEMAAAADAABAAAATAAgACEAAAENAAAAGgAGAAAACAAeAAkAKQALADwADABBAA0ARgAOAAQBEAAAAAIABwERAAAACgABAQYBCAEJBgkBEgAAAAsAAQAIAAEACXMACgETAAAAAwUAAA==