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==