From 6813144d8401f5fdfca1670c38970fd66fdd819b Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 14 Aug 2026 21:23:45 +0200 Subject: [PATCH] test: characterize TItemMultiPart Freezes the placement logic before the port. getHitDepth projects the hit vector onto the axis for that side and adds 1 on even sides, giving the six frozen values, and it copies the vector rather than mutating the caller's. onItemUse tries the clicked block first only when the depth is below 1, then always tries the neighbour on that side, reusing and mutating a single BlockCoord across both attempts. Placement short-circuits as soon as newPart returns null, before any world, player or stack access, so all of that is reachable headless with null arguments. Real placement needs a world and stays on the functional and manual layers. Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar at 7843f13, whose forwarders call all three statics. ProjRed is the only consumer of this bridge. 57 plain-JVM tests, all passing against the untouched Scala. --- src/test/fixtures/README.md | 14 ++ .../compat/ReferenceScalaItemMultiPart.scala | 29 ++++ .../ItemMultiPartCharacterizationTest.java | 140 ++++++++++++++++++ ...TItemMultiPartBinaryCompatibilityTest.java | 77 ++++++++++ .../ReferenceScalaItemMultiPart.class.b64 | 66 +++++++++ 5 files changed, 326 insertions(+) create mode 100644 src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaItemMultiPart.scala create mode 100644 src/test/java/codechicken/multipart/ItemMultiPartCharacterizationTest.java create mode 100644 src/test/java/codechicken/multipart/TItemMultiPartBinaryCompatibilityTest.java create mode 100644 src/test/resources/compat/ReferenceScalaItemMultiPart.class.b64 diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index 248f1da..5abba95 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -73,3 +73,17 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc (SHA-256 `97469dae5152f5040db6106022e8176a1b84814e700c7dfca89f1364e11cf6af`). Class-file SHA-256 is `80255098990ee61d2c96f70c7020b3bede4f2654786c0c8a3b7618d5060923c7`, stored as `src/test/resources/compat/ReferenceScalaIconHitEffects.class.b64`. + +### `ReferenceScalaItemMultiPart` + +`scala/codechicken/multipart/compat/ReferenceScalaItemMultiPart.scala` mixes in `TItemMultiPart` without overriding +anything it supplies, so its compiled forwarders call all three `TItemMultiPart$class` statics: `getHitDepth`, +`onItemUse` and `$init$`. ProjRed depends on them. + +Its `newPart` records each attempted position and returns null, which is the short-circuit that keeps placement from +touching the world, so the fixture can be driven with null stack, player and world. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `7843f13` +(SHA-256 `c003d02407d901271f42e182b5036ed3c295c2acbf416603513d2bec5b8210d0`). Class-file SHA-256 is +`0b9d9dba78a25b0c0867fe545bc6c6400be272c418d755ef72f46dde6294c75e`, stored as +`src/test/resources/compat/ReferenceScalaItemMultiPart.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaItemMultiPart.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaItemMultiPart.scala new file mode 100644 index 0000000..20376fc --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaItemMultiPart.scala @@ -0,0 +1,29 @@ +package codechicken.multipart.compat + +import codechicken.lib.vec.{BlockCoord, Vector3} +import codechicken.multipart.{TItemMultiPart, TMultiPart} +import net.minecraft.entity.player.EntityPlayer +import net.minecraft.item.{Item, ItemStack} +import net.minecraft.world.World + +/** Mixes in TItemMultiPart without overriding anything it supplies, so the compiled forwarders call + * TItemMultiPart$class.getHitDepth, TItemMultiPart$class.onItemUse and TItemMultiPart$class.$init$. + * + * newPart records each attempted position and returns null, which is the short-circuit that keeps placement from + * touching the world. + */ +class ReferenceScalaItemMultiPart extends Item with TItemMultiPart { + var attempts = "" + + def newPart( + item: ItemStack, + player: EntityPlayer, + world: World, + pos: BlockCoord, + side: Int, + vhit: Vector3 + ): TMultiPart = { + attempts += s"${pos.x},${pos.y},${pos.z};" + null + } +} diff --git a/src/test/java/codechicken/multipart/ItemMultiPartCharacterizationTest.java b/src/test/java/codechicken/multipart/ItemMultiPartCharacterizationTest.java new file mode 100644 index 0000000..7ad0adb --- /dev/null +++ b/src/test/java/codechicken/multipart/ItemMultiPartCharacterizationTest.java @@ -0,0 +1,140 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.BlockCoord; +import codechicken.lib.vec.Vector3; + +/** + * Placement short-circuits as soon as newPart returns null, before any world, player or stack access, so the branch and + * offset logic is reachable headless with null arguments. Actual placement needs a world and stays on the functional + * and manual layers. + */ +class ItemMultiPartCharacterizationTest { + + @Test + void hitDepthProjectsOntoTheSideAxisAndOffsetsOddSides() { + SpyItem item = new SpyItem(); + Vector3 vhit = new Vector3(0.25, 0.5, 0.75); + + assertEquals(0.5, item.getHitDepth(vhit, 0)); + assertEquals(0.5, item.getHitDepth(vhit, 1)); + assertEquals(0.25, item.getHitDepth(vhit, 2)); + assertEquals(0.75, item.getHitDepth(vhit, 3)); + assertEquals(0.75, item.getHitDepth(vhit, 4)); + assertEquals(0.25, item.getHitDepth(vhit, 5)); + } + + @Test + void hitDepthDoesNotMutateTheSuppliedVector() { + SpyItem item = new SpyItem(); + Vector3 vhit = new Vector3(0.25, 0.5, 0.75); + + item.getHitDepth(vhit, 3); + + assertEquals(0.25, vhit.x); + assertEquals(0.5, vhit.y); + assertEquals(0.75, vhit.z); + } + + @Test + void deprecatedBridgeMatchesTheHitDepth() { + SpyItem item = new SpyItem(); + Vector3 vhit = new Vector3(0.25, 0.5, 0.75); + + for (int side = 0; side < 6; side++) { + assertEquals(item.getHitDepth(vhit, side), TItemMultiPart$class.getHitDepth(item, vhit, side)); + } + } + + @Test + void shallowHitTriesTheClickedBlockThenTheNeighbour() { + SpyItem item = new SpyItem(); + + // side 1 projects onto +y with no offset, so hitY 0.3 gives a depth below 1. + assertFalse(item.onItemUse(null, null, null, 4, 5, 6, 1, 0f, 0.3f, 0f)); + + assertEquals(2, item.attempts.size()); + assertEquals("4,5,6", item.attempts.get(0)); + assertEquals("4,6,6", item.attempts.get(1)); + } + + @Test + void deepHitOnlyTriesTheNeighbour() { + SpyItem item = new SpyItem(); + + // hitY 1.0 gives a depth of exactly 1, which is not below the threshold. + assertFalse(item.onItemUse(null, null, null, 4, 5, 6, 1, 0f, 1f, 0f)); + + assertEquals(1, item.attempts.size()); + assertEquals("4,6,6", item.attempts.get(0)); + } + + @Test + void bothAttemptsShareOneMutatedBlockCoordInstance() { + SpyItem item = new SpyItem(); + + item.onItemUse(null, null, null, 4, 5, 6, 1, 0f, 0.3f, 0f); + + assertEquals(2, item.positions.size()); + assertSame(item.positions.get(0), item.positions.get(1)); + } + + @Test + void newPartReceivesTheHitVectorAndSide() { + SpyItem item = new SpyItem(); + + // Depth here is 0.3, so both attempts run and each must see the same side and hit vector. + item.onItemUse(null, null, null, 0, 0, 0, 3, 0.1f, 0.2f, 0.3f); + + assertEquals(Arrays.asList(3, 3), item.sides); + assertSame(item.hits.get(0), item.hits.get(1)); + Vector3 vhit = item.hits.get(0); + assertEquals(0.1, vhit.x, 1e-6); + assertEquals(0.2, vhit.y, 1e-6); + assertEquals(0.3, vhit.z, 1e-6); + } + + /** Mirrors the forwarders Scala emits, so this file compiles against the trait and against the Java interface. */ + private static final class SpyItem extends Item implements TItemMultiPart { + + private final List attempts = new ArrayList<>(); + private final List positions = new ArrayList<>(); + private final List hits = new ArrayList<>(); + private final List sides = new ArrayList<>(); + + @Override + public double getHitDepth(Vector3 vhit, int side) { + return TItemMultiPart$class.getHitDepth(this, vhit, side); + } + + @Override + public boolean onItemUse(ItemStack item, EntityPlayer player, World world, int x, int y, int z, int side, + float hitX, float hitY, float hitZ) { + return TItemMultiPart$class.onItemUse(this, item, player, world, x, y, z, side, hitX, hitY, hitZ); + } + + @Override + public TMultiPart newPart(ItemStack item, EntityPlayer player, World world, BlockCoord pos, int side, + Vector3 vhit) { + attempts.add(pos.x + "," + pos.y + "," + pos.z); + positions.add(pos); + hits.add(vhit); + sides.add(side); + return null; + } + } +} diff --git a/src/test/java/codechicken/multipart/TItemMultiPartBinaryCompatibilityTest.java b/src/test/java/codechicken/multipart/TItemMultiPartBinaryCompatibilityTest.java new file mode 100644 index 0000000..5c31674 --- /dev/null +++ b/src/test/java/codechicken/multipart/TItemMultiPartBinaryCompatibilityTest.java @@ -0,0 +1,77 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.Scanner; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.Vector3; + +/** + * Loads a Scala class compiled against the reference dev jar, whose forwarders call all three TItemMultiPart$class + * statics. Its newPart returns null, so placement short-circuits before touching the world. + */ +class TItemMultiPartBinaryCompatibilityTest { + + @Test + void referenceScalaImplementorStillLinksThroughTheBridge() throws Exception { + Class fixtureClass = new FixtureClassLoader(TItemMultiPart.class.getClassLoader()).define(loadFixture()); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + assertTrue(fixture instanceof TItemMultiPart); + assertTrue(fixture instanceof Item); + TItemMultiPart item = (TItemMultiPart) fixture; + + assertEquals(0.75, item.getHitDepth(new Vector3(0.25, 0.5, 0.75), 3)); + + Method onItemUse = fixtureClass.getMethod( + "onItemUse", + ItemStack.class, + EntityPlayer.class, + World.class, + int.class, + int.class, + int.class, + int.class, + float.class, + float.class, + float.class); + assertFalse((Boolean) onItemUse.invoke(fixture, null, null, null, 4, 5, 6, 1, 0f, 0.3f, 0f)); + + // A shallow hit tries the clicked block first, then its neighbour on that side. + assertEquals("4,5,6;4,6,6;", fixtureClass.getMethod("attempts").invoke(fixture)); + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + TItemMultiPartBinaryCompatibilityTest.class + .getResourceAsStream("/compat/ReferenceScalaItemMultiPart.class.b64")); + try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) { + return Base64.getMimeDecoder().decode(scanner.next()); + } + } + + private static final class FixtureClassLoader extends ClassLoader { + + private FixtureClassLoader(ClassLoader parent) { + super(parent); + } + + private Class define(byte[] bytecode) { + return defineClass(null, bytecode, 0, bytecode.length); + } + } +} diff --git a/src/test/resources/compat/ReferenceScalaItemMultiPart.class.b64 b/src/test/resources/compat/ReferenceScalaItemMultiPart.class.b64 new file mode 100644 index 0000000..7f67dd8 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaItemMultiPart.class.b64 @@ -0,0 +1,66 @@ +yv66vgAAADQAgwEAOGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFJ +dGVtTXVsdGlQYXJ0BwABAQAXbmV0L21pbmVjcmFmdC9pdGVtL0l0ZW0HAAMBACRjb2RlY2hpY2tl +bi9tdWx0aXBhcnQvVEl0ZW1NdWx0aVBhcnQHAAUBACFSZWZlcmVuY2VTY2FsYUl0ZW1NdWx0aVBh +cnQuc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNpZ25hdHVyZTsBAAVieXRlcwECuAYBDTRB +IQECARMJWSIrGjRmZRZ0Ny1aKmRDMgwXCl4zbhtWZEcvGyliZVJUIWEBAwINDXxXDl0xdRUJKWEh +QQVuazIkGA5dMXNpKglxIUEGZF8SLDcNWzVkVxZ0NwFBCgQBKSECQ0EGExsFYSFCQQcPAxFJRy9a +NwsFPQESIUM3al0WHCcvGTR1FQUJEmEBOGZpJhExAwQCBRNSLFcOBQIWLTUJQSEDAhgJCXFBKxM7 +Zls2K0gOXjVRQ0okCCJCDQEJA1ESQQIfal0mJGgIRgEcIQlhAiFEAQMRHXECATFBBQJ9CQEiGTt1 +SzYESG9dCwJBQRERRUoHAkUpETEFSgEFWQZ0d01DASYDEVEXTV4xCgUdEiNBQip1ZSZ0dw1DBCoB +AQdJEQEWAhkFJEgvWjdxaU58RiVaORUFLQoEQwEXMBsFaSMiARgCC00cFw1cMQoFQWojAUIrb1NS +RHFBDRUCAgMHASVBAnlJRUJhAQ4BIUITARMhQzF1aRZsBw9eOiERFTEEASIBOAMdcVdtHiliZVIk +cgFPHkARPktmDAUCFnMlESEIAgILKTYrSA5eNVFDSiQIIkIHNgEEYQRDQQY+EwlxREJBBUppFmw3 +C14xZFciKQEpDmEBAwYxAQ9cMXpLSgQiQREkDgMNUyEBESMLBRVzEUFCM29pJiQYMAMCSAcKYVFJ +HDtqaWYERi5ZPWZlIikRKg5hARUGKXFvHDptSUIRMSpUBwIZKhERSkQFAx0yE1FhVjhzWRJEUQFV +G0ECRQsxAV04dCEJEXYrRAFUFQkhVitBAndLDlQhQRYEAgcxTCctAwJZJwpRIQlcOGRXDnt3Th0z +CQtpKwQZQS4CCU1MRy0aCQNZcUshIVgXAwclc0cPQwNgawEHAS0BA3dRJiQIQwEqYhMJETdLQQRX +Sw4keE5dGgEACGF0dGVtcHRzAQASTGphdmEvbGFuZy9TdHJpbmc7AQALZ2V0SGl0RGVwdGgBACEo +TGNvZGVjaGlja2VuL2xpYi92ZWMvVmVjdG9yMztJKUQBACpjb2RlY2hpY2tlbi9tdWx0aXBhcnQv +VEl0ZW1NdWx0aVBhcnQkY2xhc3MHAA8BAEcoTGNvZGVjaGlja2VuL211bHRpcGFydC9USXRlbU11 +bHRpUGFydDtMY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzO0kpRAwADQARCgAQABIBAAR0aGlz +AQA6TGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFJdGVtTXVsdGlQ +YXJ0OwEABHZoaXQBAB1MY29kZWNoaWNrZW4vbGliL3ZlYy9WZWN0b3IzOwEABHNpZGUBAAFJAQAJ +b25JdGVtVXNlAQBtKExuZXQvbWluZWNyYWZ0L2l0ZW0vSXRlbVN0YWNrO0xuZXQvbWluZWNyYWZ0 +L2VudGl0eS9wbGF5ZXIvRW50aXR5UGxheWVyO0xuZXQvbWluZWNyYWZ0L3dvcmxkL1dvcmxkO0lJ +SUlGRkYpWgEAkyhMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RJdGVtTXVsdGlQYXJ0O0xuZXQvbWlu +ZWNyYWZ0L2l0ZW0vSXRlbVN0YWNrO0xuZXQvbWluZWNyYWZ0L2VudGl0eS9wbGF5ZXIvRW50aXR5 +UGxheWVyO0xuZXQvbWluZWNyYWZ0L3dvcmxkL1dvcmxkO0lJSUlGRkYpWgwAGgAcCgAQAB0BAARp +dGVtAQAeTG5ldC9taW5lY3JhZnQvaXRlbS9JdGVtU3RhY2s7AQAGcGxheWVyAQAqTG5ldC9taW5l +Y3JhZnQvZW50aXR5L3BsYXllci9FbnRpdHlQbGF5ZXI7AQAFd29ybGQBABtMbmV0L21pbmVjcmFm +dC93b3JsZC9Xb3JsZDsBAAF4AQABeQEAAXoBAARoaXRYAQABRgEABGhpdFkBAARoaXRaAQAUKClM +amF2YS9sYW5nL1N0cmluZzsMAAsADAkAAgAtAQAMYXR0ZW1wdHNfJGVxAQAVKExqYXZhL2xhbmcv +U3RyaW5nOylWAQADeCQxAQAHbmV3UGFydAEAxShMbmV0L21pbmVjcmFmdC9pdGVtL0l0ZW1TdGFj +aztMbmV0L21pbmVjcmFmdC9lbnRpdHkvcGxheWVyL0VudGl0eVBsYXllcjtMbmV0L21pbmVjcmFm +dC93b3JsZC9Xb3JsZDtMY29kZWNoaWNrZW4vbGliL3ZlYy9CbG9ja0Nvb3JkO0lMY29kZWNoaWNr +ZW4vbGliL3ZlYy9WZWN0b3IzOylMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RNdWx0aVBhcnQ7AQAm +c2NhbGEvY29sbGVjdGlvbi9tdXRhYmxlL1N0cmluZ0J1aWxkZXIHADQBAAY8aW5pdD4BAAMoKVYM +ADYANwoANQA4DAALACwKAAIAOgEABmFwcGVuZAEAPChMamF2YS9sYW5nL09iamVjdDspTHNjYWxh +L2NvbGxlY3Rpb24vbXV0YWJsZS9TdHJpbmdCdWlsZGVyOwwAPAA9CgA1AD4BABNzY2FsYS9TdHJp +bmdDb250ZXh0BwBAAQANc2NhbGEvUHJlZGVmJAcAQgEAB01PRFVMRSQBAA9Mc2NhbGEvUHJlZGVm +JDsMAEQARQkAQwBGAQAQamF2YS9sYW5nL1N0cmluZwcASAEAAAgASgEAASwIAEwBAAE7CABOAQAT +W0xqYXZhL2xhbmcvT2JqZWN0OwcAUAEADHdyYXBSZWZBcnJheQEAPChbTGphdmEvbGFuZy9PYmpl +Y3Q7KUxzY2FsYS9jb2xsZWN0aW9uL211dGFibGUvV3JhcHBlZEFycmF5OwwAUgBTCgBDAFQBABko +THNjYWxhL2NvbGxlY3Rpb24vU2VxOylWDAA2AFYKAEEAVwEAEGphdmEvbGFuZy9PYmplY3QHAFkB +AB5jb2RlY2hpY2tlbi9saWIvdmVjL0Jsb2NrQ29vcmQHAFsMACUAGQkAXABdAQAac2NhbGEvcnVu +dGltZS9Cb3hlc1J1blRpbWUHAF8BAAxib3hUb0ludGVnZXIBABYoSSlMamF2YS9sYW5nL0ludGVn +ZXI7DABhAGIKAGAAYwwAJgAZCQBcAGUMACcAGQkAXABnAQAQZ2VuZXJpY1dyYXBBcnJheQEAOyhM +amF2YS9sYW5nL09iamVjdDspTHNjYWxhL2NvbGxlY3Rpb24vbXV0YWJsZS9XcmFwcGVkQXJyYXk7 +DABpAGoKAEMAawEAAXMBACooTHNjYWxhL2NvbGxlY3Rpb24vU2VxOylMamF2YS9sYW5nL1N0cmlu +ZzsMAG0AbgoAQQBvAQAIdG9TdHJpbmcMAHEALAoANQByDAAvADAKAAIAdAEAA3BvcwEAIExjb2Rl +Y2hpY2tlbi9saWIvdmVjL0Jsb2NrQ29vcmQ7CgAEADgBAAYkaW5pdCQBACkoTGNvZGVjaGlja2Vu +L211bHRpcGFydC9USXRlbU11bHRpUGFydDspVgwAeQB6CgAQAHsBAARDb2RlAQASTG9jYWxWYXJp +YWJsZVRhYmxlAQAPTGluZU51bWJlclRhYmxlAQAKU291cmNlRmlsZQEAGVJ1bnRpbWVWaXNpYmxl +QW5ub3RhdGlvbnMBAAhTY2FsYVNpZwAhAAIABAABAAYAAQACAAsADAAAAAYAAQANAA4AAQB9AAAA +RQADAAMAAAAHKiscuAATrwAAAAIAfgAAACAAAwAAAAcAFAAVAAAAAAAHABYAFwABAAAABwAYABkA +AgB/AAAABgABAAAADwABABoAGwABAH0AAACkAAsACwAAABYqKywtFQQVBRUGFQcXCBcJFwq4AB6s +AAAAAgB+AAAAcAALAAAAFgAUABUAAAAAABYAHwAgAAEAAAAWACEAIgACAAAAFgAjACQAAwAAABYA +JQAZAAQAAAAWACYAGQAFAAAAFgAnABkABgAAABYAGAAZAAcAAAAWACgAKQAIAAAAFgAqACkACQAA +ABYAKwApAAoAfwAAAAYAAQAAAA8AAQALACwAAQB9AAAALwABAAEAAAAFKrQALrAAAAACAH4AAAAM +AAEAAAAFABQAFQAAAH8AAAAGAAEAAAAQAAEALwAwAAEAfQAAADoAAgACAAAABiortQAusQAAAAIA +fgAAABYAAgAAAAYAFAAVAAAAAAAGADEADAABAH8AAAAGAAEAAAAQAAEAMgAzAAEAfQAAANoACQAH +AAAAcCq7ADVZtwA5KrYAO7YAP7sAQVmyAEcHvQBJWQMSS1NZBBJNU1kFEk1TWQYST1PAAFG2AFW3 +AFiyAEcGvQBaWQMZBLQAXrgAZFNZBBkEtABmuABkU1kFGQS0AGi4AGRTtgBstgBwtgA/tgBztgB1 +AbAAAAACAH4AAABIAAcAAABwABQAFQAAAAAAcAAfACAAAQAAAHAAIQAiAAIAAABwACMAJAADAAAA +cAB2AHcABAAAAHAAGAAZAAUAAABwABYAFwAGAH8AAAAKAAIAAAAaAG4AGwABADYANwABAH0AAAA9 +AAIAAQAAAA8qtwB4KrgAfCoSS7UALrEAAAACAH4AAAAMAAEAAAAPABQAFQAAAH8AAAAKAAIAAAAP +AAgAEAADAIAAAAACAAcAgQAAAAsAAQAIAAEACXMACgCCAAAAAwUAAA==