diff --git a/src/functionalTest/java/codechicken/multipart/test/MultipartGeneratorFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MultipartGeneratorFunctionalTest.java new file mode 100644 index 0000000..4241c64 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MultipartGeneratorFunctionalTest.java @@ -0,0 +1,284 @@ +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.assertNotSame; +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 java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.BitSet; +import java.util.Collections; +import java.util.Objects; +import java.util.Scanner; + +import net.minecraft.init.Blocks; +import net.minecraft.server.MinecraftServer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.MultipartGenerator; +import codechicken.multipart.MultipartGenerator$; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TSlottedPart; +import codechicken.multipart.TileMultipart; +import codechicken.multipart.TileMultipartClient; +import codechicken.multipart.asm.MultipartMixinFactory; +import codechicken.multipart.handler.MultipartProxy; +import codechicken.multipart.handler.MultipartSaveLoad; +import codechicken.multipart.minecraft.TorchPart; +import codechicken.multipart.scalatraits.TSlottedTile; +import scala.collection.JavaConversions; +import scala.collection.Seq; +import scala.collection.mutable.Map; + +public class MultipartGeneratorFunctionalTest { + + @Test + void cachesCompleteHierarchySeparatelyPerSideAndKeepsFirstRegistration() { + MultipartGenerator$ generator = MultipartGenerator$.MODULE$; + String root = RootMarker.class.getName(); + String slotted = "codechicken.multipart.scalatraits.TSlottedTile"; + String redstone = "codechicken.multipart.scalatraits.TRedstoneTile"; + String tileChange = "codechicken.multipart.scalatraits.TTileChangeTile"; + MultipartGenerator.registerTrait(root, redstone, slotted); + HierarchyPart part = new HierarchyPart(); + BitSet server = generator.codechicken$multipart$MultipartGenerator$$traitsForPart(part, false); + BitSet client = generator.codechicken$multipart$MultipartGenerator$$traitsForPart(part, true); + assertEquals(bits(slotted), server); + assertEquals(bits(redstone), client); + assertNotSame(server, client); + assertSame( + server, + generator.codechicken$multipart$MultipartGenerator$$traitsForPart(new HierarchyPart(), false)); + + MultipartGenerator.registerTrait(root, slotted, redstone); + assertEquals(slotted.replace('.', '/'), map(false).apply(root.replace('.', '/'))); + assertEquals(redstone.replace('.', '/'), map(true).apply(root.replace('.', '/'))); + MultipartGenerator.registerTrait(LateMarker.class.getName(), tileChange, null); + assertSame(client, generator.codechicken$multipart$MultipartGenerator$$traitsForPart(part, true)); + assertEquals(bits(redstone), client, "Late registration does not invalidate an already-cached part class"); + BitSet fresh = bits(redstone, tileChange); + assertEquals( + fresh, + generator.codechicken$multipart$MultipartGenerator$$traitsForPart(new FreshHierarchyPart(), true)); + assertEquals( + bits(slotted), + generator.codechicken$multipart$MultipartGenerator$$traitsForPart(new FreshHierarchyPart(), false)); + + String missing = "codechicken.multipart.test.NonexistentGeneratorTrait"; + assertThrows( + ClassNotFoundException.class, + () -> MultipartGenerator.registerTrait(FailedMarker.class.getName(), null, missing)); + assertEquals( + missing.replace('.', '/'), + map(false).apply(FailedMarker.class.getName().replace('.', '/')), + "The reference records a side mapping before resolving its trait class"); + String passThrough = GeneratorPassThroughFixture.class.getName().replace('.', '/'); + assertTrue(map(false).contains(passThrough)); + assertFalse(map(true).contains(passThrough)); + } + + @Test + void reusesOnlyExactTraitSetsAndSnapshotsRegisteredClasses() { + MultipartGenerator$ generator = MultipartGenerator$.MODULE$; + BitSet registration = new BitSet(); + generator.registerTileClass(RegisteredTile.class, registration); + registration.set(4095); + BitSet scratch = MultipartGenerator.getBitSet(); + scratch.set(4095); + RegisteredTile registered = new RegisteredTile(); + assertSame(registered, generator.generateCompositeTile(registered, parts(), false)); + assertSame(scratch, MultipartGenerator.getBitSet()); + assertTrue(scratch.isEmpty(), "Every construction clears the reused scratch set"); + assertEquals("savedMultipart", MultipartSaveLoad.getClassToNameMap().get(RegisteredTile.class)); + + SlottedPart slotted = new SlottedPart(); + TileMultipart plain = generator.generateCompositeTile(null, parts(), false); + assertSame(plain, generator.generateCompositeTile(plain, parts(new PlainPart()), false)); + TileMultipart upgraded = generator.generateCompositeTile(plain, parts(slotted), false); + assertNotSame(plain, upgraded); + assertTrue(upgraded instanceof TSlottedTile); + assertSame( + upgraded, + generator.generateCompositeTile(upgraded, parts(slotted, new PlainPart(), slotted), false)); + assertSame( + upgraded.getClass(), + generator.generateCompositeTile(null, parts(new PlainPart(), slotted), false).getClass()); + TileMultipart client = generator.generateCompositeTile(upgraded, parts(slotted), true); + assertTrue(client instanceof TileMultipartClient); + assertNotSame(upgraded, client); + assertSame(client, generator.generateCompositeTile(client, parts(slotted), true)); + assertThrows( + java.util.NoSuchElementException.class, + () -> generator.generateCompositeTile(new UnregisteredTile(), parts(), false)); + } + + @Test + void upgradesAndDowngradesLiveTilesWithoutLosingParts() { + World world = MinecraftServer.getServer().worldServers[0]; + BlockCoord pos = new BlockCoord(48, 200, 48); + world.getChunkFromBlockCoords(pos.x, pos.z); + world.setBlockToAir(pos.x, pos.y, pos.z); + PlainPart first = new PlainPart(); + SlottedPart second = new SlottedPart(); + try { + TileMultipart plain = TileMultipart.addPart(world, pos, first); + TileMultipart upgraded = TileMultipart.addPart(world, pos, second); + assertNotSame(plain, upgraded); + assertTrue(plain.isInvalid()); + assertTrue(upgraded instanceof TSlottedTile); + assertSame(upgraded, world.getTileEntity(pos.x, pos.y, pos.z)); + assertEquals(Arrays.asList(first, second), upgraded.jPartList()); + assertSame(upgraded, first.tile()); + assertSame(second, upgraded.partMap(3)); + + TileMultipart downgraded = upgraded.remPart(second); + assertTrue(upgraded.isInvalid()); + assertNotSame(upgraded, downgraded); + assertSame(plain.getClass(), downgraded.getClass()); + assertSame(downgraded, world.getTileEntity(pos.x, pos.y, pos.z)); + assertSame(downgraded, first.tile()); + assertEquals(Collections.singletonList(first), downgraded.jPartList()); + } finally { + world.setBlockToAir(pos.x, pos.y, pos.z); + } + } + + @Test + void convertsVanillaBlockBeforeAppendingTheNewPart() { + World world = MinecraftServer.getServer().worldServers[0]; + BlockCoord pos = new BlockCoord(50, 200, 48); + world.getChunkFromBlockCoords(pos.x, pos.z); + world.setBlock(pos.x, pos.y - 1, pos.z, Blocks.stone, 0, 0); + world.setBlock(pos.x, pos.y, pos.z, Blocks.torch, 5, 0); + PlainPart added = new PlainPart(); + try { + TileMultipart tile = TileMultipart.addPart(world, pos, added); + assertSame(MultipartProxy.block(), world.getBlock(pos.x, pos.y, pos.z)); + assertSame(tile, world.getTileEntity(pos.x, pos.y, pos.z)); + assertEquals(2, tile.partList().size()); + assertTrue(tile.partList().apply(0) instanceof TorchPart); + assertSame(added, tile.partList().apply(1)); + assertSame(tile, tile.partList().apply(0).tile()); + } finally { + world.setBlockToAir(pos.x, pos.y, pos.z); + world.setBlockToAir(pos.x, pos.y - 1, pos.z); + } + } + + @Test + void frozenScalaConsumerLinksCompanionGenerationAndDefaultPassThroughRegistration() throws Exception { + byte[] bytes; + try (InputStream stream = Objects.requireNonNull( + getClass().getResourceAsStream("/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64")); + Scanner scanner = new Scanner(stream, StandardCharsets.UTF_8.name())) { + bytes = Base64.getMimeDecoder().decode(scanner.useDelimiter("\\A").next()); + } + Class type = new FixtureClassLoader(MultipartGenerator.class.getClassLoader()).define(bytes); + Object consumer = type.getDeclaredConstructor().newInstance(); + type.getMethod("register", String.class).invoke(consumer, FrozenPassThrough.class.getName()); + String key = FrozenPassThrough.class.getName().replace('.', '/'); + assertTrue(map(false).contains(key)); + assertEquals(map(false).apply(key), map(true).apply(key)); + Seq parts = parts(new PassThroughPart()); + TileMultipart tile = (TileMultipart) type + .getMethod("create", TileEntity.class, scala.collection.Iterable.class, boolean.class) + .invoke(consumer, null, parts, false); + tile.loadParts(parts); + assertEquals(37, ((FrozenPassThrough) tile).value()); + } + + private static Map map(boolean client) { + return MultipartGenerator$.MODULE$.codechicken$multipart$MultipartGenerator$$interfaceTraitMap(client); + } + + private static BitSet bits(String... names) { + BitSet bits = new BitSet(); + for (String name : names) bits.set(MultipartMixinFactory.getId(name.replace('.', '/'))); + return bits; + } + + private static Seq parts(TMultiPart... parts) { + return JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList(); + } + + public interface RootMarker { + } + + public interface ChildMarker extends RootMarker { + } + + public interface LateMarker { + } + + public interface FailedMarker { + } + + public interface FrozenPassThrough { + + int value(); + } + + public static class PlainPart extends TMultiPart { + + @Override + public String getType() { + return "mc_torch"; + } + + @Override + public boolean doesTick() { + return false; + } + } + + public static class ParentPart extends PlainPart implements ChildMarker { + } + + public static class HierarchyPart extends ParentPart implements LateMarker { + } + + public static class FreshHierarchyPart extends HierarchyPart { + } + + public static class SlottedPart extends PlainPart implements TSlottedPart { + + @Override + public int getSlotMask() { + return 1 << 3; + } + } + + public static class PassThroughPart extends PlainPart implements FrozenPassThrough { + + @Override + public int value() { + return 37; + } + } + + public static class RegisteredTile extends TileMultipart { + } + + public static class UnregisteredTile extends TileMultipart { + } + + private static final class FixtureClassLoader extends ClassLoader { + + private FixtureClassLoader(ClassLoader parent) { + super(parent); + } + + private Class define(byte[] bytes) { + return defineClass(null, bytes, 0, bytes.length); + } + } +} diff --git a/src/functionalTest/resources/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64 b/src/functionalTest/resources/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64 new file mode 100644 index 0000000..44825e1 --- /dev/null +++ b/src/functionalTest/resources/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64 @@ -0,0 +1,33 @@ +yv66vgAAADQALwEARWNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFN +dWx0aXBhcnRHZW5lcmF0b3JDb25zdW1lcgcAAQEAEGphdmEvbGFuZy9PYmplY3QHAAMBAC5SZWZl +cmVuY2VTY2FsYU11bHRpcGFydEdlbmVyYXRvckNvbnN1bWVyLnNjYWxhAQAeTHNjYWxhL3JlZmxl +Y3QvU2NhbGFTaWduYXR1cmU7AQAFYnl0ZXMBAi4GATUzQSEBAgETCUEjKxo0ZmUWdDctWipkQzIM +VyoeN3VTQgwnD14kZl0WFBgNXjhzBz50Ny9eN2ZlKhExAUIBB0c+bAcvGTsLBRUxESFDN3ZZUkwH +Lxk6dRUFORFhQzJwSRYcBy5bMmxLOhwBYQUCARVBETFCRAcCGSkJUSJBA3RHBmQXLQMCEBkJMRFJ +XD1TSxpEUSEFAQUCSQlhAVA1b1NSdEQjQQoRBVEBUSIBAgkLWQFBEUEMAg0NFFgtGTtmKRFBQgRL +HREFZVFSIgEDCgVtISEhBCtqWRZsVQ8cO2phBhRID0MDHisBB2EkAQN1UzIsB0NBECcbBQEjQkER +IwMpIRgOXDNmXVJMRy8fBgNHEQoRIlw1b0sOFBhNGjsLAxUKMUFcM3UTCTkDRQEGVVMyLFdJHDtq +aWZEUSFLC0ECKQpRAV0xc2lOBDJhSxo3HQlhE0cEAi5hNQlhRgMCMBEFMUFIXThwaXpKESFEBQNl +MQlxAV0xZFcGPFctAwI1awlBEQpeM3NDCmRXTQMCMxlBERFkTgUDcRERIQJWJ3ZZUkwHKxk6dREV +UVQDMQE8AxkZRy5bM29pQhExAlAFA3sxEXFBUThwWRYMZw5DA0ABEQUBKQEFc0seTDcPXjNzKQkJ +RQkFAgwFJhExCQQCBSs6TEcPQwNGfQEHYSkBA29DNiwHQ0EkSx0JWQEqAwJKGQUxAUtdM2VLGkwh +YRMnAw1NIygvGzhoFQlJRQIBAAZjcmVhdGUBAGgoTG5ldC9taW5lY3JhZnQvdGlsZWVudGl0eS9U +aWxlRW50aXR5O0xzY2FsYS9jb2xsZWN0aW9uL0l0ZXJhYmxlO1opTGNvZGVjaGlja2VuL211bHRp +cGFydC9UaWxlTXVsdGlwYXJ0OwEAKWNvZGVjaGlja2VuL211bHRpcGFydC9NdWx0aXBhcnRHZW5l +cmF0b3IkBwALAQAHTU9EVUxFJAEAK0xjb2RlY2hpY2tlbi9tdWx0aXBhcnQvTXVsdGlwYXJ0R2Vu +ZXJhdG9yJDsMAA0ADgkADAAPAQAVZ2VuZXJhdGVDb21wb3NpdGVUaWxlDAARAAoKAAwAEgEABHRo +aXMBAEdMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L2NvbXBhdC9SZWZlcmVuY2VTY2FsYU11bHRpcGFy +dEdlbmVyYXRvckNvbnN1bWVyOwEABHRpbGUBACVMbmV0L21pbmVjcmFmdC90aWxlZW50aXR5L1Rp +bGVFbnRpdHk7AQAFcGFydHMBABtMc2NhbGEvY29sbGVjdGlvbi9JdGVyYWJsZTsBAAZjbGllbnQB +AAFaAQAIcmVnaXN0ZXIBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABxyZWdpc3RlclBhc3NUaHJv +dWdoSW50ZXJmYWNlDAAeAB0KAAwAHwEABG5hbWUBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5p +dD4BAAMoKVYMACMAJAoABAAlAQAEQ29kZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAD0xpbmVOdW1i +ZXJUYWJsZQEACVNpZ25hdHVyZQEAjChMbmV0L21pbmVjcmFmdC90aWxlZW50aXR5L1RpbGVFbnRp +dHk7THNjYWxhL2NvbGxlY3Rpb24vSXRlcmFibGU8TGNvZGVjaGlja2VuL211bHRpcGFydC9UTXVs +dGlQYXJ0Oz47WilMY29kZWNoaWNrZW4vbXVsdGlwYXJ0L1RpbGVNdWx0aXBhcnQ7AQAKU291cmNl +RmlsZQEAGVJ1bnRpbWVWaXNpYmxlQW5ub3RhdGlvbnMBAAhTY2FsYVNpZwAhAAIABAAAAAAAAwAB +AAkACgACACcAAABSAAQABAAAAAqyABArLB22ABOwAAAAAgAoAAAAKgAEAAAACgAUABUAAAAAAAoA +FgAXAAEAAAAKABgAGQACAAAACgAaABsAAwApAAAABgABAAAACQAqAAAAAgArAAEAHAAdAAEAJwAA +ADwAAgACAAAACLIAECu2ACCxAAAAAgAoAAAAFgACAAAACAAUABUAAAAAAAgAIQAiAAEAKQAAAAYA +AQAAAAsAAQAjACQAAQAnAAAALwABAAEAAAAFKrcAJrEAAAACACgAAAAMAAEAAAAFABQAFQAAACkA +AAAGAAEAAAAMAAMALAAAAAIABQAtAAAACwABAAYAAQAHcwAIAC4AAAADBQAA diff --git a/src/test/fixtures/README.md b/src/test/fixtures/README.md index ddf7d18..9a9e1c2 100644 --- a/src/test/fixtures/README.md +++ b/src/test/fixtures/README.md @@ -156,3 +156,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouche (reference dev-jar SHA-256 `3dfb365ee96603dec74e22787ba17e324792410265b85afd103b07aa57ddf8f6`). Class-file SHA-256 is `710f925a5b4bd44e337672b6c4a3c7a0dddf0c5e31287989c4774c34cf628fa8`, stored as `src/test/resources/compat/ReferenceScalaBlockMicroMaterialConsumer.class.b64`. + +### `ReferenceScalaMultipartGeneratorConsumer` + +`scala/codechicken/multipart/compat/ReferenceScalaMultipartGeneratorConsumer.scala` calls +`MultipartGenerator$.MODULE$.generateCompositeTile(TileEntity, scala.collection.Iterable, boolean)` and the +one-argument `registerPassThroughInterface` companion method. GuideNH and Schematica depend on the former through +reflection; shipping Scala consumers link directly to the companion registration path. + +Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the untouched classes at `2f4972e` +(reference dev-jar SHA-256 `69c0723afd70ab6208b97e5fab12062bfb28a941dfae12575c5d61274102ef6f`). Class-file +SHA-256 is `b0f540b063bd6fffcc2a1462d72de1f31b4c8a68caa92ac9298dfdf18a9eb24f`, stored as +`src/functionalTest/resources/compat/ReferenceScalaMultipartGeneratorConsumer.class.b64`. diff --git a/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartGeneratorConsumer.scala b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartGeneratorConsumer.scala new file mode 100644 index 0000000..828aaf8 --- /dev/null +++ b/src/test/fixtures/scala/codechicken/multipart/compat/ReferenceScalaMultipartGeneratorConsumer.scala @@ -0,0 +1,12 @@ +package codechicken.multipart.compat + +import codechicken.multipart.{MultipartGenerator, TMultiPart, TileMultipart} +import net.minecraft.tileentity.TileEntity + +/** Frozen against Scala: companion-only generation and singleton pass-through registration. */ +class ReferenceScalaMultipartGeneratorConsumer { + def create(tile: TileEntity, parts: Iterable[TMultiPart], client: Boolean): TileMultipart = + MultipartGenerator.generateCompositeTile(tile, parts, client) + + def register(name: String): Unit = MultipartGenerator.registerPassThroughInterface(name) +} diff --git a/src/test/java/codechicken/multipart/MultipartGeneratorCharacterizationTest.java b/src/test/java/codechicken/multipart/MultipartGeneratorCharacterizationTest.java new file mode 100644 index 0000000..03634f8 --- /dev/null +++ b/src/test/java/codechicken/multipart/MultipartGeneratorCharacterizationTest.java @@ -0,0 +1,130 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import codechicken.multipart.asm.ScratchBitSet; + +class MultipartGeneratorCharacterizationTest { + + private static final Set FACADE = signatures( + "freshBitSet()Ljava/util/BitSet;", + "getBitSet()Ljava/util/BitSet;", + "registerPassThroughInterface(Ljava/lang/String;)V", + "registerPassThroughInterface(Ljava/lang/String;ZZ)V", + "registerTrait(Ljava/lang/String;Ljava/lang/String;)V", + "registerTrait(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + "silentAddTile(Lnet/minecraft/world/World;Lcodechicken/lib/vec/BlockCoord;Lnet/minecraft/tileentity/TileEntity;)V"); + + @Test + void keepsExactFacadeCompanionAndScalaMapSurfaceWithoutInitializingForge() throws Exception { + assertEquals(FACADE, publicMethods(MultipartGenerator.class)); + Set companion = new TreeSet<>(FACADE); + companion.addAll( + signatures( + "codechicken$multipart$asm$ScratchBitSet$$bitSets()Ljava/lang/ThreadLocal;", + "codechicken$multipart$asm$ScratchBitSet$_setter_$codechicken$multipart$asm$ScratchBitSet$$bitSets_$eq" + + "(Ljava/lang/ThreadLocal;)V", + "codechicken$multipart$MultipartGenerator$$interfaceTraitMap(Z)Lscala/collection/mutable/Map;", + "codechicken$multipart$MultipartGenerator$$traitsForPart(Lcodechicken/multipart/TMultiPart;Z)Ljava/util/BitSet;", + "addPart(Lnet/minecraft/world/World;Lcodechicken/lib/vec/BlockCoord;Lcodechicken/multipart/TMultiPart;)" + + "Lcodechicken/multipart/TileMultipart;", + "generateCompositeTile(Lnet/minecraft/tileentity/TileEntity;Lscala/collection/Iterable;Z)" + + "Lcodechicken/multipart/TileMultipart;", + "partRemoved(Lcodechicken/multipart/TileMultipart;)Lcodechicken/multipart/TileMultipart;", + "registerTileClass(Ljava/lang/Class;Ljava/util/BitSet;)V")); + assertEquals(companion, publicMethods(MultipartGenerator$.class)); + for (Class type : new Class[] { MultipartGenerator.class, MultipartGenerator$.class }) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertTrue(Modifier.isFinal(type.getModifiers())); + assertSame(Object.class, type.getSuperclass()); + } + assertArrayEquals(new Class[] { ScratchBitSet.class }, MultipartGenerator$.class.getInterfaces()); + assertEquals(0, MultipartGenerator.class.getDeclaredFields().length); + assertEquals(8, MultipartGenerator$.class.getDeclaredFields().length); + for (String name : new String[] { "tileTraitMap", "interfaceTraitMap_c", "interfaceTraitMap_s", + "partTraitMap_c", "partTraitMap_s" }) { + Field field = MultipartGenerator$.class.getDeclaredField(name); + assertSame(scala.collection.mutable.Map.class, field.getType()); + assertEquals(Modifier.PRIVATE | Modifier.FINAL, field.getModifiers()); + } + Field module = MultipartGenerator$.class.getDeclaredField("MODULE$"); + assertSame(MultipartGenerator$.class, module.getType()); + assertEquals(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL, module.getModifiers()); + assertTrue(Modifier.isPrivate(MultipartGenerator$.class.getDeclaredConstructor().getModifiers())); + } + + @Test + void keepsSideSafeProxyCallbackAndCompanionOnlyGenerationCall() throws Exception { + assertCall( + MultipartGenerator$.class, + "registerTileClass", + Opcodes.INVOKEVIRTUAL, + "codechicken/multipart/handler/MultipartProxy$", + "onTileClassBuilt", + "(Ljava/lang/Class;)V"); + assertCall( + codechicken.multipart.asm.MultipartMixinFactory$.class, + "onCompiled", + Opcodes.INVOKEVIRTUAL, + "codechicken/multipart/MultipartGenerator$", + "registerTileClass", + "(Ljava/lang/Class;Ljava/util/BitSet;)V"); + assertCall( + MultipartHelper.class, + "createTileFromParts", + Opcodes.INVOKEVIRTUAL, + "codechicken/multipart/MultipartGenerator$", + "generateCompositeTile", + "(Lnet/minecraft/tileentity/TileEntity;Lscala/collection/Iterable;Z)Lcodechicken/multipart/TileMultipart;"); + } + + private static Set publicMethods(Class type) { + Set result = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) + result.add(method.getName() + Type.getMethodDescriptor(method)); + } + return result; + } + + private static Set signatures(String... values) { + return new TreeSet<>(Arrays.asList(values)); + } + + private static void assertCall(Class type, String methodName, int opcode, String owner, String name, + String descriptor) throws Exception { + ClassNode node = new ClassNode(); + new ClassReader(type.getName()).accept(node, 0); + for (MethodNode method : node.methods) { + if (!method.name.equals(methodName)) continue; + for (AbstractInsnNode instruction : method.instructions.toArray()) { + if (!(instruction instanceof MethodInsnNode)) continue; + MethodInsnNode call = (MethodInsnNode) instruction; + if (call.getOpcode() == opcode && call.owner.equals(owner) + && call.name.equals(name) + && call.desc.equals(descriptor)) + return; + } + } + throw new AssertionError("Missing call " + owner + '.' + name + descriptor); + } +}