diff --git a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java index 2c50b3e..08487b6 100644 --- a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java @@ -7,17 +7,28 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; import java.util.BitSet; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; import org.junit.jupiter.api.Test; +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.data.MCDataOutputWrapper; import codechicken.microblock.FaceMicroClass$; import codechicken.microblock.HollowMicroClass$; import codechicken.microblock.Microblock; import codechicken.microblock.MicroblockGenerator; +import codechicken.multipart.MultiPartRegistry; import codechicken.multipart.TFacePart; +import codechicken.multipart.TMultiPart; import codechicken.multipart.TileMultipart; import codechicken.multipart.asm.MultipartMixinFactory; import codechicken.multipart.scalatraits.TSlottedTile; @@ -65,6 +76,63 @@ class ForgeEnvironmentSmokeTest { assertFalse(((TFacePart) hollow).solid(0)); } + /** + * Part registration needs FML's active mod container, so none of this is reachable from a plain JVM test. Startup + * exercises both overloads downstream depends on: Content registers through the deprecated IPartFactory array form, + * and each microblock class registers through the IPartFactory2 varargs form. + */ + @Test + void registryResolvesPartsRegisteredThroughBothFactoryOverloads() throws Exception { + assertTrue(MultiPartRegistry.loaded(), "Registration must be closed once the server has started"); + + // mc_torch comes from the deprecated IPartFactory array overload. + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + assertNotNull(torch); + assertEquals("mc_torch", torch.getType()); + + // mcr_face comes from the IPartFactory2 varargs overload. Its factory reads the tag, so it needs a real one; + // an absent material name resolves to the missing-material placeholder rather than failing. + TMultiPart face = MultiPartRegistry.loadPart("mcr_face", new NBTTagCompound()); + assertNotNull(face); + assertEquals("mcr_face", face.getType()); + + assertNotNull(MultiPartRegistry.getModContainer("mc_torch")); + assertNotNull(MultiPartRegistry.getModContainer("mcr_face")); + } + + @Test + void partIdsRoundTripThroughTheRegistryCarrier() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes)); + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + MultiPartRegistry.writePartID(output, torch); + + TMultiPart read = MultiPartRegistry.readPart(input(bytes.toByteArray())); + + assertNotNull(read); + assertEquals("mc_torch", read.getType()); + } + + /** CodeChickenLib has no stream-backed MCDataInput, so the carrier reads are proxied. */ + private static MCDataInput input(byte[] bytes) { + DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes)); + return (MCDataInput) Proxy.newProxyInstance( + MCDataInput.class.getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, arguments) -> { + switch (method.getName()) { + case "readUByte": + return (short) data.readUnsignedByte(); + case "readUShort": + return data.readUnsignedShort(); + case "readInt": + return data.readInt(); + default: + throw new AssertionError("Unexpected read method: " + method.getName()); + } + }); + } + @Test void generatesAndCachesSlottedTileClass() { int traitId = MultipartMixinFactory.getId(TSlottedTile.class.getName().replace('.', '/')); diff --git a/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java new file mode 100644 index 0000000..89cea3b --- /dev/null +++ b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java @@ -0,0 +1,113 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.MultiPartRegistry.IPartConverter; + +/** + * Part registration needs FML's active mod container, so it cannot run headless; that half of the registry is + * characterized in the Forge server suite instead. Converter registration and dispatch touch neither FML nor the + * logger, so they are covered here. + */ +class MultiPartRegistryCharacterizationTest { + + @Test + void noPartsAreLoadedBeforeRegistrationRuns() { + assertFalse(MultiPartRegistry.loaded()); + } + + @Test + void unknownModContainerThrowsRatherThanReturningNull() { + assertThrows(NoSuchElementException.class, () -> MultiPartRegistry.getModContainer("test:nosuchpart")); + } + + @Test + void convertersAreDispatchedPerBlockAndFirstNonNullWins() { + Block block = new Block(Material.rock) {}; + Block other = new Block(Material.rock) {}; + + RecordingConverter declines = new RecordingConverter(block, null); + RecordingConverter accepts = new RecordingConverter(block, new TestPart()); + RecordingConverter unrelated = new RecordingConverter(other, new TestPart()); + + MultiPartRegistry.registerConverter(declines); + MultiPartRegistry.registerConverter(accepts); + MultiPartRegistry.registerConverter(unrelated); + + assertSame(accepts.result, MultiPartRegistry.convertBlock(null, null, block)); + assertEquals(1, declines.calls, "A converter returning null must not stop the scan"); + assertEquals(1, accepts.calls); + assertEquals(0, unrelated.calls, "Converters registered for another block must not be consulted"); + } + + @Test + void blockWithNoConverterYieldsNull() { + assertNull(MultiPartRegistry.convertBlock(null, null, new Block(Material.rock) {})); + } + + @Test + void aConverterIsRegisteredForEveryBlockItClaims() { + Block first = new Block(Material.rock) {}; + Block second = new Block(Material.rock) {}; + TestPart part = new TestPart(); + + RecordingConverter converter = new RecordingConverter(Arrays.asList(first, second), part); + MultiPartRegistry.registerConverter(converter); + + assertSame(part, MultiPartRegistry.convertBlock(null, null, first)); + assertSame(part, MultiPartRegistry.convertBlock(null, null, second)); + assertEquals(2, converter.calls); + } + + private static final class RecordingConverter implements IPartConverter { + + private final List blocks = new ArrayList<>(); + private final TMultiPart result; + private int calls; + + private RecordingConverter(Block block, TMultiPart result) { + this.blocks.add(block); + this.result = result; + } + + private RecordingConverter(List blocks, TMultiPart result) { + this.blocks.addAll(blocks); + this.result = result; + } + + @Override + public Iterable blockTypes() { + return blocks; + } + + @Override + public TMultiPart convert(World world, BlockCoord pos) { + calls++; + return result; + } + } + + private static final class TestPart extends TMultiPart { + + @Override + public String getType() { + return "test:converted"; + } + } +}