diff --git a/src/functionalTest/java/codechicken/multipart/test/BlockMicroMaterialFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/BlockMicroMaterialFunctionalTest.java new file mode 100644 index 0000000..7aae10e --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/BlockMicroMaterialFunctionalTest.java @@ -0,0 +1,55 @@ +package codechicken.multipart.test; + +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 static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.init.Blocks; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.FakePlayerFactory; + +import org.junit.jupiter.api.Test; + +import codechicken.microblock.BlockMicroMaterial; +import codechicken.microblock.MicroMaterialRegistry; + +class BlockMicroMaterialFunctionalTest { + + @Test + void dedicatedServerKeepsCommonMaterialBehaviorAndStripsClientMembers() throws Exception { + BlockMicroMaterial material = (BlockMicroMaterial) MicroMaterialRegistry.getMaterial("minecraft:stone"); + assertSame(Blocks.stone, material.block()); + assertEquals(0, material.meta()); + assertEquals("minecraft:stone", material.blockKey()); + + ItemStack item = material.getItem(); + assertSame(Item.getItemFromBlock(Blocks.stone), item.getItem()); + assertEquals(1, item.stackSize); + assertEquals(0, item.getItemDamage()); + assertEquals(item.getDisplayName(), material.getLocalizedName()); + assertFalse(material.isTransparent()); + assertTrue(new BlockMicroMaterial(Blocks.glass, 0).isTransparent()); + assertEquals(Blocks.glowstone.getLightValue(), new BlockMicroMaterial(Blocks.glowstone, 0).getLightValue()); + assertEquals(Blocks.stone.getHarvestLevel(0), material.getCutterStrength()); + assertSame(Blocks.stone.stepSound, material.getSound()); + assertEquals(Blocks.stone.getExplosionResistance(null), material.explosionResistance(null)); + WorldServer world = MinecraftServer.getServer().worldServers[0]; + EntityPlayer player = FakePlayerFactory.getMinecraft(world); + float hardness = Blocks.stone.getBlockHardness(null, 0, 0, 0); + float expected = player.getBreakSpeed(Blocks.stone, false, 0, 0, -1, 0) / hardness; + assertEquals(expected, material.getStrength(player)); + + assertThrows(NoSuchFieldException.class, () -> BlockMicroMaterial.class.getDeclaredField("icont")); + assertThrows(NoSuchMethodException.class, () -> BlockMicroMaterial.class.getDeclaredMethod("loadIcons")); + assertThrows( + NoSuchMethodException.class, + () -> BlockMicroMaterial.class.getDeclaredMethod("getBreakingIcon", int.class)); + assertSame(Blocks.stone, BlockMicroMaterial.class.getDeclaredMethod("block").invoke(material)); + } +} diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index d4622e4..ddf7d18 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -145,3 +145,14 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc (SHA-256 `1f571035283b5ca9120bf817f00ce961ebcbae258d901fb8ee9147636f71a481`). Class-file SHA-256 is `27543f97e0973f042c3867c1aa0ae5598324c60c895c8d9a7ea5a6cea2f5ee70`, stored as `src/functionalTest/resources/compat/ReferenceScalaMultipartSaveLoadConsumer.class.b64`. + +### `ReferenceScalaBlockMicroMaterialConsumer` + +`scala/codechicken/multipart/compat/ReferenceScalaBlockMicroMaterialConsumer.scala` calls the +`BlockMicroMaterial$.MODULE$` key/default methods and the `MaterialRenderHelper$.MODULE$` pass accessors used by +downstream Scala code. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouched classes at `1939159` +(reference dev-jar SHA-256 `3dfb365ee96603dec74e22787ba17e324792410265b85afd103b07aa57ddf8f6`). Class-file +SHA-256 is `710f925a5b4bd44e337672b6c4a3c7a0dddf0c5e31287989c4774c34cf628fa8`, stored as +`src/test/resources/compat/ReferenceScalaBlockMicroMaterialConsumer.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaBlockMicroMaterialConsumer.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaBlockMicroMaterialConsumer.scala new file mode 100644 index 0000000..95c2e69 --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaBlockMicroMaterialConsumer.scala @@ -0,0 +1,20 @@ +package codechicken.multipart.compat + +import codechicken.microblock.BlockMicroMaterial +import codechicken.microblock.MaterialRenderHelper + +/** Compiled against the reference dev jar. Calls the BlockMicroMaterial and MaterialRenderHelper companion methods + * used by downstream Scala code. + */ +class ReferenceScalaBlockMicroMaterialConsumer { + def materialKey(name: String, meta: Int): String = BlockMicroMaterial.materialKey(name, meta) + + def constructorDefault: Int = BlockMicroMaterial.$lessinit$greater$default$2 + + def registrationDefault: Int = BlockMicroMaterial.createAndRegister$default$2 + + def helperPass(value: Int): Int = { + MaterialRenderHelper.pass = value + MaterialRenderHelper.pass + } +} diff --git a/src/test/java/codechicken/microblock/BlockMicroMaterialBinaryCompatibilityTest.java b/src/test/java/codechicken/microblock/BlockMicroMaterialBinaryCompatibilityTest.java new file mode 100644 index 0000000..4fdf637 --- /dev/null +++ b/src/test/java/codechicken/microblock/BlockMicroMaterialBinaryCompatibilityTest.java @@ -0,0 +1,48 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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; + +/** Loads a Scala consumer compiled against the reference companion-singleton ABI used by downstream mods. */ +class BlockMicroMaterialBinaryCompatibilityTest { + + @Test + void referenceScalaConsumerStillLinksAgainstBothCompanions() throws Exception { + Class fixtureClass = new FixtureClassLoader(BlockMicroMaterial.class.getClassLoader()).define(loadFixture()); + Object fixture = fixtureClass.getDeclaredConstructor().newInstance(); + + assertEquals( + "example_3", + fixtureClass.getMethod("materialKey", String.class, int.class).invoke(fixture, "example", 3)); + assertEquals(0, fixtureClass.getMethod("constructorDefault").invoke(fixture)); + assertEquals(0, fixtureClass.getMethod("registrationDefault").invoke(fixture)); + assertEquals(7, fixtureClass.getMethod("helperPass", int.class).invoke(fixture, 7)); + } + + private static byte[] loadFixture() { + InputStream input = Objects.requireNonNull( + BlockMicroMaterialBinaryCompatibilityTest.class + .getResourceAsStream("/compat/ReferenceScalaBlockMicroMaterialConsumer.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/java/codechicken/microblock/BlockMicroMaterialCharacterizationTest.java b/src/test/java/codechicken/microblock/BlockMicroMaterialCharacterizationTest.java new file mode 100644 index 0000000..f57871e --- /dev/null +++ b/src/test/java/codechicken/microblock/BlockMicroMaterialCharacterizationTest.java @@ -0,0 +1,267 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; + +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.init.Blocks; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; + +import codechicken.lib.render.BlockRenderer.BlockFace; +import codechicken.lib.render.CCRenderPipeline; +import codechicken.lib.render.CCRenderState; +import codechicken.lib.render.ColourMultiplier; +import codechicken.lib.render.uv.MultiIconTransformation; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import scala.collection.JavaConversions; + +class BlockMicroMaterialCharacterizationTest { + + private static final Set THREAD_STATE_METHODS = signatures( + "builder()Lcodechicken/lib/render/CCRenderPipeline$PipelineBuilder;", + "builder_$eq(Lcodechicken/lib/render/CCRenderPipeline$PipelineBuilder;)V", + "pass()I", + "pass_$eq(I)V"); + private static final Set RENDER_HELPER_METHODS = signatures( + "blockAndMeta(Lnet/minecraft/block/Block;I)Lcodechicken/microblock/MaterialRenderHelper$;", + "blockColour(I)Lcodechicken/microblock/MaterialRenderHelper$;", + "builder()Lcodechicken/lib/render/CCRenderPipeline$PipelineBuilder;", + "builder_$eq(Lcodechicken/lib/render/CCRenderPipeline$PipelineBuilder;)V", + "lighting()Lcodechicken/microblock/MaterialRenderHelper$;", + "pass()I", + "pass_$eq(I)V", + "render()V", + "start(Lcodechicken/lib/vec/Vector3;ILcodechicken/lib/render/uv/UVTransformation;)Lcodechicken/microblock/MaterialRenderHelper$;"); + private static final Set MATERIAL_COMPANION_METHODS = signatures( + "$lessinit$greater$default$2()I", + "createAndRegister(Lnet/minecraft/block/Block;I)V", + "createAndRegister(Lnet/minecraft/block/Block;ILjava/lang/String;)V", + "createAndRegister(Lnet/minecraft/block/Block;ILjava/lang/String;Ljava/lang/String;)V", + "createAndRegister(Lnet/minecraft/block/Block;Lscala/collection/Seq;)V", + "createAndRegister(Lnet/minecraft/block/Block;Lscala/collection/Seq;Ljava/lang/String;)V", + "createAndRegister(Lnet/minecraft/block/Block;Lscala/collection/Seq;Ljava/lang/String;Ljava/lang/String;)V", + "createAndRegister$default$2()I", + "materialKey(Lnet/minecraft/block/Block;)Ljava/lang/String;", + "materialKey(Lnet/minecraft/block/Block;I)Ljava/lang/String;", + "materialKey(Ljava/lang/String;I)Ljava/lang/String;", + "oldKey(Lnet/minecraft/block/Block;)Ljava/lang/String;"); + private static final Set MATERIAL_INSTANCE_METHODS = signatures( + "block()Lnet/minecraft/block/Block;", + "blockKey()Ljava/lang/String;", + "canRenderInPass(I)Z", + "codechicken$microblock$BlockMicroMaterial$$safeIcon$1(Lnet/minecraft/block/Block;I)Lnet/minecraft/util/IIcon;", + "explosionResistance(Lnet/minecraft/entity/Entity;)F", + "getBreakingIcon(I)Lnet/minecraft/util/IIcon;", + "getColour(I)I", + "getCutterStrength()I", + "getItem()Lnet/minecraft/item/ItemStack;", + "getLightValue()I", + "getLocalizedName()Ljava/lang/String;", + "getSound()Lnet/minecraft/block/Block$SoundType;", + "getStrength(Lnet/minecraft/entity/player/EntityPlayer;)F", + "icont()Lcodechicken/lib/render/uv/MultiIconTransformation;", + "icont_$eq(Lcodechicken/lib/render/uv/MultiIconTransformation;)V", + "isTransparent()Z", + "loadIcons()V", + "meta()I", + "renderMicroFace(Lcodechicken/lib/vec/Vector3;ILcodechicken/lib/vec/Cuboid6;)V", + "toolClasses()Lscala/collection/Seq;"); + + @Test + void keepsTheFiveRetainedClassSurfacesAndStateShapes() throws Exception { + assertClass(ThreadState.class, Object.class, false, THREAD_STATE_METHODS); + assertField(ThreadState.class, "pass", int.class, false, false); + assertField(ThreadState.class, "builder", CCRenderPipeline.PipelineBuilder.class, false, false); + + assertClass(MaterialRenderHelper.class, Object.class, true, RENDER_HELPER_METHODS); + assertClass(MaterialRenderHelper$.class, Object.class, true, RENDER_HELPER_METHODS); + assertEquals(0, MaterialRenderHelper.class.getDeclaredFields().length); + assertField(MaterialRenderHelper$.class, "threadState", ThreadLocal.class, false, true); + assertModule(MaterialRenderHelper$.class, MaterialRenderHelper$.MODULE$); + + assertTrue(Modifier.isPublic(BlockMicroMaterial.class.getModifiers())); + assertFalse(Modifier.isFinal(BlockMicroMaterial.class.getModifiers())); + assertSame(Object.class, BlockMicroMaterial.class.getSuperclass()); + assertEquals(Arrays.asList(IMicroMaterial.class), Arrays.asList(BlockMicroMaterial.class.getInterfaces())); + Set materialMethods = new TreeSet<>(MATERIAL_COMPANION_METHODS); + materialMethods.addAll(MATERIAL_INSTANCE_METHODS); + assertEquals(materialMethods, publicDeclaredMethods(BlockMicroMaterial.class)); + assertTrue(Modifier.isPublic(BlockMicroMaterial.class.getConstructor(Block.class, int.class).getModifiers())); + assertEquals(1, BlockMicroMaterial.class.getDeclaredConstructors().length); + assertField(BlockMicroMaterial.class, "block", Block.class, false, true); + assertField(BlockMicroMaterial.class, "meta", int.class, false, true); + assertField(BlockMicroMaterial.class, "blockKey", String.class, false, true); + Field icons = assertField(BlockMicroMaterial.class, "icont", MultiIconTransformation.class, false, false); + assertSideOnly(icons.getAnnotation(SideOnly.class)); + assertEquals(4, BlockMicroMaterial.class.getDeclaredFields().length); + + assertClass(BlockMicroMaterial$.class, Object.class, true, MATERIAL_COMPANION_METHODS); + assertModule(BlockMicroMaterial$.class, BlockMicroMaterial$.MODULE$); + } + + @Test + void keepsTheExactClientOnlyBoundary() throws Exception { + for (Method method : BlockMicroMaterial.class.getDeclaredMethods()) { + SideOnly annotation = method.getAnnotation(SideOnly.class); + if (method.getName().equals("loadIcons") || method.getName().equals("getBreakingIcon")) { + assertSideOnly(annotation); + } else { + assertNull(annotation, method.toString()); + } + } + Method safeIcon = BlockMicroMaterial.class + .getDeclaredMethod("codechicken$microblock$BlockMicroMaterial$$safeIcon$1", Block.class, int.class); + assertTrue(Modifier.isFinal(safeIcon.getModifiers())); + } + + @Test + void keepsMaterialKeysDefaultsAndCommonBlockBehavior() { + Block block = new Block(Material.rock) {}; + BlockMicroMaterial stone = new BlockMicroMaterial(block, 17); + assertSame(block, stone.block()); + assertEquals(17, stone.meta()); + assertNull(stone.blockKey()); + assertEquals(block.getUnlocalizedName(), BlockMicroMaterial.oldKey(block)); + assertNull(BlockMicroMaterial.materialKey(block)); + assertEquals("example", BlockMicroMaterial.materialKey("example", 0)); + assertEquals("example", BlockMicroMaterial.materialKey("example", -1)); + assertEquals("example_2", BlockMicroMaterial.materialKey("example", 2)); + assertEquals(0, BlockMicroMaterial.$lessinit$greater$default$2()); + assertEquals(0, BlockMicroMaterial.createAndRegister$default$2()); + assertEquals(0, BlockMicroMaterial$.MODULE$.$lessinit$greater$default$2()); + assertEquals(0, BlockMicroMaterial$.MODULE$.createAndRegister$default$2()); + + assertEquals(!block.isOpaqueCube(), stone.isTransparent()); + assertEquals(block.getLightValue(), stone.getLightValue()); + assertEquals(block.getHarvestLevel(1), stone.getCutterStrength()); + assertSame(block.stepSound, stone.getSound()); + assertEquals(block.getExplosionResistance(null), stone.explosionResistance(null)); + assertEquals(block.canRenderInPass(1), stone.canRenderInPass(1)); + assertEquals((block.getBlockColor() << 8) | 0xFF, stone.getColour(-1)); + assertEquals(Arrays.asList("axe", "pickaxe", "shovel"), JavaConversions.seqAsJavaList(stone.toolClasses())); + + assertNull(stone.icont()); + MultiIconTransformation icons = new MultiIconTransformation((net.minecraft.util.IIcon) null); + stone.icont_$eq(icons); + assertSame(icons, stone.icont()); + } + + @Test + void keepsRenderHelperStateThreadLocalAndFacadeBacked() throws Exception { + ThreadState state = new ThreadState(); + assertEquals(0, state.pass()); + assertNull(state.builder()); + state.pass_$eq(4); + assertEquals(4, state.pass()); + + MaterialRenderHelper.pass_$eq(9); + assertEquals(9, MaterialRenderHelper.pass()); + assertEquals(9, MaterialRenderHelper$.MODULE$.pass()); + AtomicInteger otherThreadPass = new AtomicInteger(-1); + Thread thread = new Thread(() -> otherThreadPass.set(MaterialRenderHelper.pass())); + thread.start(); + thread.join(); + assertEquals(0, otherThreadPass.get()); + + MultiIconTransformation icons = new MultiIconTransformation((net.minecraft.util.IIcon) null); + assertSame(MaterialRenderHelper$.MODULE$, MaterialRenderHelper.start(new Vector3(), -1, icons)); + assertNotNull(MaterialRenderHelper.builder()); + MaterialRenderHelper.pass_$eq(0); + MaterialRenderHelper.builder_$eq(null); + } + + @Test + void baseMaterialBuildsTheInventoryRenderPipeline() throws Exception { + BlockMicroMaterial material = new BlockMicroMaterial(Blocks.stone, 0) { + + @Override + public int getColour(int pass) { + return 0x89ABCDEF; + } + }; + MultiIconTransformation icons = new MultiIconTransformation((net.minecraft.util.IIcon) null); + material.icont_$eq(icons); + CCRenderState state = CCRenderState.instance(); + state.resetInstance(); + BlockFace face = new BlockFace(); + face.side = 1; + state.model = face; + state.firstVertexIndex = 0; + state.lastVertexIndex = 0; + + material.renderMicroFace(new Vector3(), -1, Cuboid6.full); + + Field operationsField = CCRenderPipeline.class.getDeclaredField("ops"); + operationsField.setAccessible(true); + @SuppressWarnings("unchecked") + List operations = new ArrayList<>( + (List) operationsField.get(state.pipeline)); + assertTrue(operations.contains(icons)); + assertTrue(operations.stream().anyMatch(ColourMultiplier.class::isInstance)); + } + + private static void assertClass(Class type, Class superclass, boolean isFinal, Set methods) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertEquals(isFinal, Modifier.isFinal(type.getModifiers())); + assertSame(superclass, type.getSuperclass()); + assertEquals(methods, publicDeclaredMethods(type)); + } + + private static Field assertField(Class owner, String name, Class type, boolean isStatic, boolean isFinal) + throws Exception { + Field field = owner.getDeclaredField(name); + assertSame(type, field.getType()); + assertTrue(Modifier.isPrivate(field.getModifiers())); + assertEquals(isStatic, Modifier.isStatic(field.getModifiers())); + assertEquals(isFinal, Modifier.isFinal(field.getModifiers())); + return field; + } + + private static void assertModule(Class type, Object expected) throws Exception { + Field module = type.getField("MODULE$"); + assertSame(type, module.getType()); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + assertSame(expected, module.get(null)); + } + + private static void assertSideOnly(SideOnly annotation) { + assertNotNull(annotation); + assertSame(Side.CLIENT, annotation.value()); + } + + private static Set signatures(String... signatures) { + return new TreeSet<>(Arrays.asList(signatures)); + } + + private static Set publicDeclaredMethods(Class type) { + Set signatures = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + signatures.add(method.getName() + Type.getMethodDescriptor(method)); + } + } + return signatures; + } +} diff --git a/src/test/resources/compat/ReferenceScalaBlockMicroMaterialConsumer.class.b64 b/src/test/resources/compat/ReferenceScalaBlockMicroMaterialConsumer.class.b64 new file mode 100644 index 0000000..5f3b9b2 --- /dev/null +++ b/src/test/resources/compat/ReferenceScalaBlockMicroMaterialConsumer.class.b64 @@ -0,0 +1,31 @@ +yv66vgAAADQAOwEARWNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFC +bG9ja01pY3JvTWF0ZXJpYWxDb25zdW1lcgcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAC5SZWZl +cmVuY2VTY2FsYUJsb2NrTWljcm9NYXRlcmlhbENvbnN1bWVyLnNjYWxhAQAeTHNjYWxhL3JlZmxl +Y3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBAY8GAUEyQSEBAgETCUEjKxo0ZmUWdDctWipkQzIM +JwlcOGRXNks3TV04TkNSLCcvWzFtBz50Ny9eN2ZlKhExAUIBB0c+bAcvGTsLBRUxESFDN3ZZUkwH +Lxk6dRUFORFhQzJwSRYcBy5bMmxLOhwBYQUCARVBETFCRAcCGSkJUSJBA3RHBmQXLQMCEBkJMRFJ +XD1TSxpEUSEFAQUCSQlhAVA1b1NSdEQjQQoRBVEBUSIBAgkLWQFBEUEMAhc1DEcvGjpqQzJcVS0f +CwQxfQkDQ0ENHR0JWSEkAwIcGQUxAUtdM2VLGkwhIQgQAw1NIygvGzhoFQlZQgJDAyErAQcBJAED +b0M2LAciAhIWAQQZEwFCN2ZpBgQiYQMTCgUVYiFhQSVvaSIpcQUBQwFRBREybRw4dGlKLDgNXjhz +CRY0Fy0eN3UrBRkDIgIWAQkDQRNhBTpmTyYcSE9dMXVTPnRHKRo0YmsyJAgiAhcBCQNpE0EDNWZZ +QiwnD1UxdGdSETFFDAUGXy0CDWFJAQZtBmRXLxoBAAttYXRlcmlhbEtleQEAJyhMamF2YS9sYW5n +L1N0cmluZztJKUxqYXZhL2xhbmcvU3RyaW5nOwEAKmNvZGVjaGlja2VuL21pY3JvYmxvY2svQmxv +Y2tNaWNyb01hdGVyaWFsJAcACwEAB01PRFVMRSQBACxMY29kZWNoaWNrZW4vbWljcm9ibG9jay9C +bG9ja01pY3JvTWF0ZXJpYWwkOwwADQAOCQAMAA8MAAkACgoADAARAQAEdGhpcwEAR0xjb2RlY2hp +Y2tlbi9tdWx0aXBhcnQvY29tcGF0L1JlZmVyZW5jZVNjYWxhQmxvY2tNaWNyb01hdGVyaWFsQ29u +c3VtZXI7AQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABG1ldGEBAAFJAQASY29uc3RydWN0 +b3JEZWZhdWx0AQADKClJAQAbJGxlc3Npbml0JGdyZWF0ZXIkZGVmYXVsdCQyDAAbABoKAAwAHAEA +E3JlZ2lzdHJhdGlvbkRlZmF1bHQBABtjcmVhdGVBbmRSZWdpc3RlciRkZWZhdWx0JDIMAB8AGgoA +DAAgAQAKaGVscGVyUGFzcwEABChJKUkBACxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01hdGVyaWFs +UmVuZGVySGVscGVyJAcAJAEALkxjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01hdGVyaWFsUmVuZGVy +SGVscGVyJDsMAA0AJgkAJQAnAQAIcGFzc18kZXEBAAQoSSlWDAApACoKACUAKwEABHBhc3MMAC0A +GgoAJQAuAQAFdmFsdWUBAAY8aW5pdD4BAAMoKVYMADEAMgoABAAzAQAEQ29kZQEAEkxvY2FsVmFy +aWFibGVUYWJsZQEAD0xpbmVOdW1iZXJUYWJsZQEAClNvdXJjZUZpbGUBABlSdW50aW1lVmlzaWJs +ZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAAAAAAUAAQAJAAoAAQA1AAAARwADAAMAAAAJ +sgAQKxy2ABKwAAAAAgA2AAAAIAADAAAACQATABQAAAAAAAkAFQAWAAEAAAAJABcAGAACADcAAAAG +AAEAAAAHAAEAGQAaAAEANQAAADEAAQABAAAAB7IAELYAHawAAAACADYAAAAMAAEAAAAHABMAFAAA +ADcAAAAGAAEAAAAJAAEAHgAaAAEANQAAADEAAQABAAAAB7IAELYAIawAAAACADYAAAAMAAEAAAAH +ABMAFAAAADcAAAAGAAEAAAALAAEAIgAjAAEANQAAAEYAAgACAAAADrIAKBu2ACyyACi2AC+sAAAA +AgA2AAAAFgACAAAADgATABQAAAAAAA4AMAAYAAEANwAAAAoAAgAAAA4ABwAPAAEAMQAyAAEANQAA +AC8AAQABAAAABSq3ADSxAAAAAgA2AAAADAABAAAABQATABQAAAA3AAAABgABAAAAEQADADgAAAAC +AAUAOQAAAAsAAQAGAAEAB3MACAA6AAAAAwUAAA==