diff --git a/dependencies.gradle b/dependencies.gradle index 6b29b93ee..4e4ce68d6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,4 +11,6 @@ dependencies { compileOnly("curse.maven:cofh-core-69162:2388751") compileOnly("curse.maven:computercraft-67504:2269339") // https://www.curseforge.com/minecraft/mc-mods/computercraft/files/2269339 compileOnly files("dependencies/ColoredLightsCore-1.3.7.39.jar") + + testImplementation("junit:junit:4.13.2") } diff --git a/src/main/scala/mrtjp/projectred/integration/GateWireMasks.scala b/src/main/scala/mrtjp/projectred/integration/GateWireMasks.scala new file mode 100644 index 000000000..e4a8d8a11 --- /dev/null +++ b/src/main/scala/mrtjp/projectred/integration/GateWireMasks.scala @@ -0,0 +1,212 @@ +package mrtjp.projectred.integration + +import codechicken.lib.vec.Rectangle4i + +private[integration] object GateWireMasks { + private val names = Array[String]( + "OR-0", + "OR-1", + "OR-2", + "OR-3", + "NOR-0", + "NOR-1", + "NOR-2", + "NOR-3", + "NOT-0", + "NOT-1", + "NOT-2", + "NOT-3", + "AND-0", + "AND-1", + "AND-2", + "AND-3", + "NAND-0", + "NAND-1", + "NAND-2", + "NAND-3", + "XOR-0", + "XOR-1", + "XOR-2", + "XOR-3", + "XNOR-0", + "XNOR-1", + "XNOR-2", + "XNOR-3", + "XNOR-4", + "BUFFER-0", + "BUFFER-1", + "BUFFER-2", + "BUFFER-3", + "MULTIPLEXER-0", + "MULTIPLEXER-1", + "MULTIPLEXER-2", + "MULTIPLEXER-3", + "MULTIPLEXER-4", + "MULTIPLEXER-5", + "PULSE-0", + "PULSE-1", + "PULSE-2", + "REPEATER-0", + "REPEATER-1", + "RAND-0", + "RAND-1", + "RAND-2", + "RAND-3", + "RAND-4", + "RAND-5", + "RAND-6", + "RSLATCH-0", + "RSLATCH-1", + "RSLATCH2-0", + "RSLATCH2-1", + "RSLATCH2-2", + "RSLATCH2-3", + "TOGLATCH-0", + "TOGLATCH-1", + "TRANSLATCH-0", + "TRANSLATCH-1", + "TRANSLATCH-2", + "TRANSLATCH-3", + "TRANSLATCH-4", + "LIGHTSENSOR-0", + "RAINSENSOR-0", + "TIME-0", + "TIME-1", + "TIME-2", + "COUNT-0", + "COUNT-1", + "STATECELL-0", + "STATECELL-1", + "STATECELL-2", + "STATECELL-3", + "STATECELL-4", + "SYNC-0", + "SYNC-1", + "SYNC-2", + "SYNC-3", + "SYNC-4", + "SYNC-5", + "BUSXCVR-0", + "BUSXCVR-1", + "COMPARATOR-0", + "COMPARATOR-1", + "COMPARATOR-2", + "COMPARATOR-3", + "BUSRAND1-0", + "BUSRAND1-1", + "BUSRAND2-0", + "BUSRAND2-1", + "BUSCONV-0", + "BUSCONV-1", + "BUSCONV-2", + "BUSINPUT-0", + "INVCELL-0", + "BUFFCELL-0", + "BUFFCELL-1", + "ANDCELL-0", + "ANDCELL-1", + "STACKLATCH-0", + "STACKLATCH-1", + "STACKLATCH-2", + "STACKLATCH-3", + "STACKLATCH-4", + "DECRAND-0", + "DECRAND-1", + "DECRAND-2", + "DECRAND-3", + "DECRAND-4", + "DECRAND-5", + "IC1-0", + "IC1-1", + "IC1-2", + "IC1-3", + "IC2-0", + "IC2-1", + "IC2-2", + "IC2-3" + ) + private val offsets = Array[Int]( + 0, 3, 5, 6, 8, 11, 13, 14, 16, 19, 21, 22, 24, 26, 27, 28, 29, 32, 35, 36, + 37, 45, 50, 54, 58, 63, 68, 72, 76, 79, 82, 86, 87, 91, 96, 101, 104, 109, + 110, 111, 117, 120, 124, 127, 130, 131, 133, 134, 136, 137, 138, 139, 144, + 149, 154, 157, 162, 165, 168, 169, 174, 177, 180, 182, 183, 184, 185, 191, + 197, 198, 202, 206, 210, 212, 214, 217, 218, 221, 224, 228, 231, 233, 235, + 237, 238, 241, 242, 245, 250, 251, 252, 255, 258, 259, 260, 262, 263, 264, + 265, 269, 274, 275, 278, 282, 284, 286, 287, 292, 298, 300, 301, 303, 307, + 308, 309, 310, 311, 312, 313, 314, 315 + ) + private val packedRectangles = Array[Int]( + 0x0f04020a, 0x0d0c0202, 0x110c0202, 0x180f0602, 0x12110602, 0x0f12020c, + 0x020f0602, 0x08110802, 0x0f02020c, 0x0d0c0202, 0x110c0202, 0x180f0602, + 0x12110602, 0x0f12020c, 0x020f0602, 0x08110802, 0x0f02020a, 0x0d0a0202, + 0x110a0202, 0x140d0206, 0x160f0802, 0x0f10020e, 0x0a0d0206, 0x020f0802, + 0x0f020208, 0x060a1402, 0x0f10020e, 0x020f0602, 0x180f0602, 0x0f020206, + 0x11080202, 0x060a1402, 0x0f120204, 0x0d160204, 0x0f1a0204, 0x020f0802, + 0x160f0802, 0x0f020204, 0x0a060c02, 0x08080204, 0x16080204, 0x060a0202, + 0x0a0a0202, 0x140a0202, 0x180a0202, 0x090f0602, 0x110f0602, 0x0f110204, + 0x0d130202, 0x11130202, 0x020f0602, 0x04110204, 0x06150202, 0x08170802, + 0x180f0602, 0x1a110204, 0x18150202, 0x10170802, 0x0f020204, 0x0a060602, + 0x08080204, 0x060a0202, 0x0a0a0202, 0x0f020204, 0x10060602, 0x16080204, + 0x140a0202, 0x180a0202, 0x020f0602, 0x04110204, 0x06150202, 0x08170802, + 0x180f0602, 0x1a110204, 0x18150202, 0x10170802, 0x080f0602, 0x120f0602, + 0x0e110402, 0x0f060208, 0x0d0c0202, 0x110c0202, 0x14040206, 0x16060202, + 0x18080208, 0x1a100402, 0x0f12020c, 0x0a040206, 0x08060202, 0x06080208, + 0x02100402, 0x0f020204, 0x0a060602, 0x08080204, 0x060a0202, 0x0a0a0202, + 0x0f020204, 0x10060602, 0x16080204, 0x140a0202, 0x180a0202, 0x08100204, + 0x0c120206, 0x0a140202, 0x16120202, 0x10140602, 0x15160204, 0x111a0402, + 0x0f1c0202, 0x020f0802, 0x160f0802, 0x09060602, 0x07080208, 0x050e0202, + 0x090e0202, 0x0b100206, 0x0d120a02, 0x07120206, 0x09180602, 0x0f1a0204, + 0x0f060602, 0x15080208, 0x130e0202, 0x170e0202, 0x0f020206, 0x17060414, + 0x11080602, 0x130c020e, 0x11120202, 0x0f14020a, 0x0f020206, 0x180f0602, + 0x16110206, 0x0f180206, 0x020f0602, 0x08110206, 0x0f08020e, 0x11160602, + 0x09160602, 0x08070204, 0x060b020c, 0x020f0402, 0x08170202, 0x0a190802, + 0x0e050802, 0x16070202, 0x1809020c, 0x1a0f0402, 0x16150204, 0x12070206, + 0x140d0204, 0x16110208, 0x12170206, 0x14190202, 0x12050802, 0x1a070208, + 0x1c0f0202, 0x0c030206, 0x0a050202, 0x08070208, 0x0a0f0204, 0x0c130206, + 0x020f0202, 0x04110208, 0x06190802, 0x020f0802, 0x100f0602, 0x0a110602, + 0x160f0802, 0x0f02020a, 0x15080602, 0x070a0802, 0x110a0402, 0x1b0a0206, + 0x0f0e020a, 0x07180802, 0x0f1a0204, 0x070e0208, 0x05140202, 0x09140202, + 0x170d0206, 0x110f0602, 0x020f0602, 0x0f12020c, 0x0f0c0212, 0x0a030206, + 0x08050202, 0x06070208, 0x080d0202, 0x020f0402, 0x0a0f0402, 0x14030206, + 0x16050202, 0x18070208, 0x160d0202, 0x100f0602, 0x1a0f0402, 0x0f10020e, + 0x020f0802, 0x100f0602, 0x0a110602, 0x04190602, 0x18030206, 0x16050202, + 0x1a050202, 0x160f0802, 0x0e040206, 0x0c060202, 0x0a080204, 0x0c0c0206, + 0x0d160204, 0x0f1a0204, 0x14100402, 0x10120402, 0x020f0202, 0x04110202, + 0x06130402, 0x180f0602, 0x0c050402, 0x0a070202, 0x08090206, 0x10050402, + 0x14070202, 0x16090206, 0x08140204, 0x16140204, 0x0a180c02, 0x0f1a0204, + 0x0e0f0402, 0x0c110202, 0x12110202, 0x020f0202, 0x04110202, 0x1c0f0202, + 0x1a110202, 0x160e0804, 0x1e0e0204, 0x000e0a04, 0x0f020206, 0x11080402, + 0x150a0206, 0x160f0802, 0x15100208, 0x11180402, 0x0f1a0204, 0x020f0202, + 0x100f0602, 0x04110204, 0x0e110204, 0x06150802, 0x160f0802, 0x020f0802, + 0x160d0402, 0x1a0f0402, 0x16110402, 0x060d0402, 0x020f0402, 0x06110402, + 0x160f0802, 0x020f0802, 0x0e10040e, 0x0e1e0402, 0x0e000404, 0x0b0f0602, + 0x11190602, 0x0f0f0602, 0x15110206, 0x13150202, 0x17150202, 0x0f020214, + 0x0d0f0202, 0x110f0202, 0x0d140202, 0x11140202, 0x0f180206, 0x0b030602, + 0x09050206, 0x0b0b0210, 0x0e0f0802, 0x180f0202, 0x16110202, 0x18130206, + 0x14150206, 0x10170402, 0x0d070602, 0x0f090206, 0x0f180206, 0x09040802, + 0x07060202, 0x09080210, 0x03100208, 0x05180202, 0x0f050204, 0x11090204, + 0x0f0b0202, 0x130b0402, 0x170d0202, 0x190f0202, 0x111a0202, 0x0f1c0202, + 0x0d1a0202, 0x050f1002, 0x15110208, 0x13020206, 0x15040402, 0x19060206, + 0x1b0c0206, 0x0f020204, 0x1a0f0402, 0x0f1a0204, 0x020f0402, 0x0e020404, + 0x1a0e0404, 0x0e1a0404, 0x020e0404 + ) + + def rectangles(name: String): Seq[Rectangle4i] = { + val index = names.indexOf(name) + if (index < 0) return null + + val result = Seq.newBuilder[Rectangle4i] + var i = offsets(index) + while (i < offsets(index + 1)) { + val packed = packedRectangles(i) + result += new Rectangle4i( + packed >>> 24, + packed >>> 16 & 0xff, + packed >>> 8 & 0xff, + packed & 0xff + ) + i += 1 + } + result.result() + } +} diff --git a/src/main/scala/mrtjp/projectred/integration/components.scala b/src/main/scala/mrtjp/projectred/integration/components.scala index 87cc16096..eee3ce647 100644 --- a/src/main/scala/mrtjp/projectred/integration/components.scala +++ b/src/main/scala/mrtjp/projectred/integration/components.scala @@ -5,6 +5,9 @@ */ package mrtjp.projectred.integration +import java.util.{List => JList} +import javax.imageio.ImageIO + import codechicken.lib.colour.Colour import codechicken.lib.lighting.LightModel.Light import codechicken.lib.lighting.{LightModel, PlanarLightModel} @@ -17,58 +20,109 @@ import mrtjp.core.color.Colors import mrtjp.core.vec.{InvertX, VecLib} import mrtjp.projectred.core.{Configurator, RenderHalo} import mrtjp.projectred.transmission.{UVT, WireModelGen} +import net.minecraft.client.Minecraft +import net.minecraft.client.resources.{IResource, IResourceManager} import net.minecraft.client.renderer.texture.IIconRegister import net.minecraft.util.{IIcon, ResourceLocation} import scala.collection.JavaConversions._ -import scala.util.control.Breaks + +private[integration] object ComponentModelBakery { + val orientPrecomputed = (0 until 48).map(orientT).toArray + val bundledCablePrecomputed = (0 until 48) + .map((orient: Int) => { + val side = orient % 24 >> 2 + val r = orient & 3 + val reflect = orient >= 24 + val rotate = (r + WireModelGen.reorientSide(side)) % 4 >= 2 + + var t: Transformation = new RedundantTransformation + if (reflect) t = t.`with`(new Scale(-1, 0, 1)) + if (rotate) t = t.`with`(Rotation.quarterRotations(2)) + t + }) + .toArray + val redundantUVTransformation = new UVTransformationList() + + def orientT(orient: Int) = { + var t = Rotation.sideOrientation(orient % 24 >> 2, orient & 3) + if (orient >= 24) t = new Scale(-1, 1, 1).`with`(t) + t.at(Vector3.center) + } + + def dynamicT(orient: Int) = + if (orient == 0) new RedundantTransformation + else new Scale(-1, 1, 1).at(Vector3.center) + + def bakeCopy(base: CCModel, orient: Int) = { + val m = base.copy + if (orient >= 24) reverseFacing(m) + m.apply(orientT(orient)).computeLighting(LightModel.standardLightModel) + m + } + + def bakeDynamic(base: CCModel) = Array(base, reverseFacing(base.copy)) + + private def reverseFacing(m: CCModel) = { + for (i <- 0 until m.verts.length by 4) { + val vtmp = m.verts(i + 1) + val ntmp = m.normals()(i + 1) + m.verts(i + 1) = m.verts(i + 3) + m.normals()(i + 1) = m.normals()(i + 3) + m.verts(i + 3) = vtmp + m.normals()(i + 3) = ntmp + } + m + } +} object ComponentStore { - val base = loadBase("base") - val lightChip = loadModel("chip") - val leverOn = loadModel("leveron") - val leverOff = loadModel("leveroff") - val solarArray = loadModel("solar") - val rainSensor = loadModel("rainsensor") - val pointer = loadModel("pointer") - val busXcvr = loadModel("array/busxcvr") - val lightPanel1 = loadModel("array/lightpanel1") - val lightPanel2 = loadModel("array/lightpanel2") - val busRand = loadModel("array/busrand") - val busConv = loadModel("array/busconv") - val signalPanel = loadModel("array/signalpanel") - val busInput = loadModel("array/businput") - val icBundled = loadModel("array/icbundled") - - val nullCellWireBottom = + lazy val base = loadBase("base") + lazy val lightChip = loadModel("chip") + lazy val leverOn = loadModel("leveron") + lazy val leverOff = loadModel("leveroff") + lazy val solarArray = loadModel("solar") + lazy val rainSensor = loadModel("rainsensor") + lazy val pointer = loadModel("pointer") + lazy val busXcvr = loadModel("array/busxcvr") + lazy val lightPanel1 = loadModel("array/lightpanel1") + lazy val lightPanel2 = loadModel("array/lightpanel2") + lazy val busRand = loadModel("array/busrand") + lazy val busConv = loadModel("array/busconv") + lazy val signalPanel = loadModel("array/signalpanel") + lazy val busInput = loadModel("array/businput") + lazy val icBundled = loadModel("array/icbundled") + + lazy val nullCellWireBottom = loadModel("array/nullcellbottomwire").apply(new Translation(0.5, 0, 0.5)) - val nullCellWireTop = + lazy val nullCellWireTop = loadModel("array/nullcelltopwire").apply(new Translation(0.5, 0, 0.5)) - val nullCellBase = loadBase("array/nullcellbase") - val extendedCellWireBottom = loadModel("array/extendedcellbottomwire").apply( - new Translation(0.5, 0, 0.5) - ) - val extendedCellWireTop = + lazy val nullCellBase = loadBase("array/nullcellbase") + lazy val extendedCellWireBottom = + loadModel("array/extendedcellbottomwire").apply( + new Translation(0.5, 0, 0.5) + ) + lazy val extendedCellWireTop = loadModel("array/extendedcelltopwire").apply(new Translation(0.5, 0, 0.5)) - val extendedCellBase = loadBase("array/extendedcellbase") - val cellWireSide = + lazy val extendedCellBase = loadBase("array/extendedcellbase") + lazy val cellWireSide = loadModel("array/cellsidewire").apply(new Translation(0.5, 0, 0.5)) - val cellFrame = + lazy val cellFrame = loadModel("array/cellstand").apply(new Translation(0.5, 0, 0.5)) - val cellPlate = + lazy val cellPlate = loadModel("array/cellplate").apply(new Translation(0.5, 0, 0.5)) - val stackLatchWireBottom = + lazy val stackLatchWireBottom = loadModel("array/stacklatchwire").apply(new Translation(0.5, 0, 0.5)) - val stackStand = loadModel("array/latchstand") + lazy val stackStand = loadModel("array/latchstand") - val sevenSeg = loadCorrectedModels("array/7seg") - val sixteenSeg = loadCorrectedModels("array/16seg") - val segbus = loadModel("array/segbus") + lazy val sevenSeg = loadCorrectedModels("array/7seg") + lazy val sixteenSeg = loadCorrectedModels("array/16seg") + lazy val segbus = loadModel("array/segbus") - val icChip = loadCorrectedModel("icchip") - val icGlass = loadCorrectedModel("icglass") - val icHousing = loadCorrectedModel("ichousing") + lazy val icChip = loadCorrectedModel("icchip") + lazy val icGlass = loadCorrectedModel("icglass") + lazy val icHousing = loadCorrectedModel("ichousing") var baseIcon: IIcon = null var wireIcons: Array[IIcon] = new Array[IIcon](3) @@ -93,22 +147,6 @@ object ComponentStore { var icChipIconOff: IIcon = null var icHousingIcon: IIcon = null - val orientPrecomputed = (0 until 48).map(orientT).toArray - val bundledCablePrecomputed = (0 until 48) - .map((orient: Int) => { - val side = orient % 24 >> 2 - val r = orient & 3 - val reflect = orient >= 24 - val rotate = (r + WireModelGen.reorientSide(side)) % 4 >= 2 - - var t: Transformation = new RedundantTransformation - if (reflect) t = t.`with`(new Scale(-1, 0, 1)) - if (rotate) t = t.`with`(Rotation.quarterRotations(2)) - t - }) - .toArray - val redundantUVTransformation = new UVTransformationList() - def registerIcons(reg: IIconRegister) { val baseTex = "projectred:integration/" def register(path: String) = reg.registerIcon(baseTex + path) @@ -198,37 +236,6 @@ object ComponentStore { m } - def orientT(orient: Int) = { - var t = Rotation.sideOrientation(orient % 24 >> 2, orient & 3) - if (orient >= 24) t = new Scale(-1, 1, 1).`with`(t) - t.at(Vector3.center) - } - - def dynamicT(orient: Int) = - if (orient == 0) new RedundantTransformation - else new Scale(-1, 1, 1).at(Vector3.center) - - def bakeCopy(base: CCModel, orient: Int) = { - val m = base.copy - if (orient >= 24) reverseFacing(m) - m.apply(orientT(orient)).computeLighting(LightModel.standardLightModel) - m - } - - def bakeDynamic(base: CCModel) = Array(base.copy, reverseFacing(base.copy)) - - private def reverseFacing(m: CCModel) = { - for (i <- 0 until m.verts.length by 4) { - val vtmp = m.verts(i + 1) - val ntmp = m.normals()(i + 1) - m.verts(i + 1) = m.verts(i + 3) - m.normals()(i + 1) = m.normals()(i + 3) - m.verts(i + 3) = vtmp - m.normals()(i + 3) = ntmp - } - m - } - def generateWireModels(name: String, count: Int) = { val xs = Seq.newBuilder[TWireModel] for (i <- 0 until count) xs += generateWireModel(name + "-" + i) @@ -236,18 +243,46 @@ object ComponentStore { } def generateWireModel(name: String) = { - val data = TextureUtils.loadTextureColours( - new ResourceLocation( - "projectred:textures/blocks/integration/surface/" + name + ".png" - ) + val rectangles = loadWireRectangles( + name, + Minecraft.getMinecraft.getResourceManager ) - if (Configurator.logicwires3D) new WireModel3D(data) - else new WireModel2D(data) + if (Configurator.logicwires3D) new WireModel3D(rectangles) + else new WireModel2D(rectangles) + } + + private[integration] def loadWireRectangles( + name: String, + resourceManager: IResourceManager + ): Seq[Rectangle4i] = { + val location = new ResourceLocation( + "projectred:textures/blocks/integration/surface/" + name + ".png" + ) + val resources = resourceManager + .getAllResources(location) + .asInstanceOf[JList[IResource]] + .map(_.getInputStream) + try { + val builtIn = GateWireMasks.rectangles(name) + if (builtIn != null && resources.length == 1) return builtIn + if (resources.isEmpty) + throw new RuntimeException("Wire mask not found: " + location) + + val image = ImageIO.read(resources.last) + if (image == null) + throw new RuntimeException("Invalid wire mask: " + location) + if (image.getWidth != 32 || image.getHeight != 32) + throw new RuntimeException( + "Wire mask must be 32x32: " + location + " was " + image.getWidth + "x" + image.getHeight + ) + TWireModel.rectangulate(image.getRGB(0, 0, 32, 32, null, 0, 32)) + } finally resources.foreach(_.close()) } } import mrtjp.projectred.integration.ComponentStore._ +import mrtjp.projectred.integration.ComponentModelBakery._ abstract class ComponentModel { def renderModel(t: Transformation, orient: Int) @@ -255,13 +290,13 @@ abstract class ComponentModel { def registerIcons(reg: IIconRegister) {} } -abstract class SingleComponentModel(m: CCModel, pos: Vector3 = Vector3.zero) - extends ComponentModel { - // instead of creating 48 models, only make 2 (original + flipped orientation) - private val modelPair = { - val t = pos.copy.multiply(1 / 16d).translation - bakeDynamic(m.copy.apply(t)) - } +abstract class SingleComponentModel private[integration] ( + private[integration] val modelPair: Array[CCModel] +) extends ComponentModel { + def this(m: CCModel, pos: Vector3) = + this(SingleComponentModel.bakeModelPair(m, pos)) + + def this(m: CCModel) = this(m, Vector3.zero) def extraTransformModel(orient: Int): Transformation = orientPrecomputed( orient @@ -281,6 +316,13 @@ abstract class SingleComponentModel(m: CCModel, pos: Vector3 = Vector3.zero) } } +private[integration] object SingleComponentModel { + def bakeModelPair(m: CCModel, pos: Vector3) = { + val t = pos.copy.multiply(1 / 16d).translation + bakeDynamic(m.copy.apply(t)) + } +} + abstract class MultiComponentModel(m: Seq[CCModel], pos: Vector3 = Vector3.zero) extends ComponentModel { val models = { @@ -301,8 +343,13 @@ abstract class MultiComponentModel(m: Seq[CCModel], pos: Vector3 = Vector3.zero) } } -abstract class OnOffModel(m: CCModel, pos: Vector3 = Vector3.zero) - extends SingleComponentModel(m, pos) { +abstract class OnOffModel private[integration] (modelPair: Array[CCModel]) + extends SingleComponentModel(modelPair) { + def this(m: CCModel, pos: Vector3) = + this(SingleComponentModel.bakeModelPair(m, pos)) + + def this(m: CCModel) = this(m, Vector3.zero) + var on = false def getIcons: Array[IIcon] @@ -333,49 +380,61 @@ trait TWireModel extends ComponentModel { } object TWireModel { - def rectangulate(data: Array[Colour]) = { + def rectangulate(data: Array[Int]) = { val wireCorners = new Array[Boolean](1024) - for (y <- 0 to 30) for (x <- 0 to 30) Breaks.breakable { - if (data(y * 32 + x).rgba != -1) Breaks.break() - if (overlap(wireCorners, x, y)) Breaks.break() - if (!segment2x2(data, x, y)) - throw new RuntimeException( - "Wire segment not 2x2 at (" + x + ", " + y + ")" - ) - - wireCorners(y * 32 + x) = true + var y = 0 + while (y <= 30) { + var x = 0 + while (x <= 30) { + if (data(y * 32 + x) == -1 && !overlap(wireCorners, x, y)) { + if (!segment2x2(data, x, y)) + throw new RuntimeException( + "Wire segment not 2x2 at (" + x + ", " + y + ")" + ) + + wireCorners(y * 32 + x) = true + } + x += 1 + } + y += 1 } - var wireRectangles = Seq.newBuilder[Rectangle4i] - for (i <- 0 until 1024) if (wireCorners(i)) { - val rect = new Rectangle4i(i % 32, i / 32, 0, 0) - var x = rect.x + 2 - while (x < 30 && wireCorners(rect.y * 32 + x)) x += 2 - rect.w = x - rect.x - - var y = rect.y + 2 - Breaks.breakable { - while (y < 30) { - var advance = true + val wireRectangles = Seq.newBuilder[Rectangle4i] + var i = 0 + while (i < 1024) { + if (wireCorners(i)) { + val rect = new Rectangle4i(i % 32, i / 32, 0, 0) + var x = rect.x + 2 + while (x < 30 && wireCorners(rect.y * 32 + x)) x += 2 + rect.w = x - rect.x + + y = rect.y + 2 + var advance = true + while (y < 30 && advance) { var dx = rect.x while (dx < rect.x + rect.w && advance) { if (!wireCorners(y * 32 + dx)) advance = false dx += 2 } - if (!advance) Breaks.break() - - y += 2 + if (advance) y += 2 } - } - rect.h = y - rect.y + rect.h = y - rect.y - for (dy <- rect.y until rect.y + rect.h by 2) - for (dx <- rect.x until rect.x + rect.w by 2) - wireCorners(dy * 32 + dx) = false + var dy = rect.y + while (dy < rect.y + rect.h) { + var dx = rect.x + while (dx < rect.x + rect.w) { + wireCorners(dy * 32 + dx) = false + dx += 2 + } + dy += 2 + } - wireRectangles += rect + wireRectangles += rect + } + i += 1 } wireRectangles.result() @@ -386,10 +445,10 @@ object TWireModel { (y - 1) * 32 + x )) || (y > 0 && wireCorners((y - 1) * 32 + x - 1)) - def segment2x2(data: Array[Colour], x: Int, y: Int) = - data(y * 32 + x + 1).rgba == -1 && data( + def segment2x2(data: Array[Int], x: Int, y: Int) = + data(y * 32 + x + 1) == -1 && data( (y + 1) * 32 + x - ).rgba == -1 && data((y + 1) * 32 + x + 1).rgba == -1 + ) == -1 && data((y + 1) * 32 + x + 1) == -1 def border(wire: Rectangle4i) = { val border = new Rectangle4i(wire.x - 2, wire.y - 2, wire.w + 4, wire.h + 4) @@ -399,20 +458,76 @@ object TWireModel { if (border.y + border.h >= 32) border.h -= border.y + border.h - 32 border } + + private[integration] def buildTexMap(wireRectangles: Seq[Rectangle4i]) = { + val texMap = new Array[Int](1024) + for (rect <- wireRectangles) { + fillMask(texMap, rect, 2) + fillMask(texMap, border(rect), 1) + } + texMap + } + + private[integration] def buildTexture( + texMap: Array[Int], + wireData: Array[Array[Colour]], + tex: Int + ) = { + val pSize = Math.sqrt(wireData(0).length).asInstanceOf[Int] + val size = Math.max(32, pSize) + val relM = size / 32 + val relP = size / pSize + val imageData = new Array[Int](size * size) + + for (i <- 0 until imageData.length) { + val x = i % size + val y = i / size + val t = texMap(y / relM * 32 + x / relM) + if (t != 0) + imageData(i) = + wireData(if (t == 1) 0 else tex)(y / relP * pSize + x / relP).argb() + } + imageData + } + + private def fillMask(map: Array[Int], r: Rectangle4i, v: Int) { + for (i <- r.x until r.x + r.w) + for (j <- r.y until r.y + r.h) + if (map(j * 32 + i) < v) map(j * 32 + i) = v + } } -class WireModel3D(data: Array[Colour]) - extends SingleComponentModel(WireModel3D.generateModel(data)) +class WireModel3D private[integration] (wireRectangles: Seq[Rectangle4i]) + extends ComponentModel with TWireModel { - override def getUVT = + def this(data: Array[Int]) = this(TWireModel.rectangulate(data)) + + private[integration] val modelPair = WireModel3D.cachedModelPair( + wireRectangles + ) + + private def getUVT = if (disabled) new IconTransformation(wireIcons(0)) else if (on) new MultiIconTransformation(wireIcons(0), wireIcons(2)) else new MultiIconTransformation(wireIcons(0), wireIcons(1)) + + override def renderModel(t: Transformation, orient: Int) { + modelPair(if (orient < 24) 0 else 1).render( + new TransformationList(orientPrecomputed(orient), t), + new UVTransformationList(redundantUVTransformation, getUVT), + LightModel.standardLightModel + ) + } } object WireModel3D { - def generateModel(data: Array[Colour]) = { - val wireRectangles = TWireModel.rectangulate(data) + private val modelCache = scala.collection.mutable.HashMap + .empty[Vector[(Int, Int, Int, Int)], Array[CCModel]] + + def generateModel(data: Array[Int]): CCModel = + generateModel(TWireModel.rectangulate(data)) + + private[integration] def generateModel(wireRectangles: Seq[Rectangle4i]) = { val model = CCModel.quadModel(wireRectangles.length * 40) var i = 0 for (rect <- wireRectangles) { @@ -424,6 +539,41 @@ object WireModel3D { model } + private[integration] def generateModelPair( + wireRectangles: Seq[Rectangle4i] + ) = bakeModelPair(generateModel(wireRectangles)) + + private[integration] def cachedModelPair( + wireRectangles: Seq[Rectangle4i] + ) = { + val key = wireRectangles.map(r => (r.x, r.y, r.w, r.h)).toVector + modelCache.synchronized { + modelCache.getOrElseUpdate(key, generateModelPair(wireRectangles)) + } + } + + private[integration] def bakeModelPair(model: CCModel) = + Array(model, reflectedView(model)) + + private def reflectedView(model: CCModel) = { + val reflected = CCModel.quadModel(model.verts.length) + val normals = model.normals() + val reflectedNormals = reflected.getOrAllocate(CCRenderState.normalAttrib()) + var i = 0 + while (i < model.verts.length) { + reflected.verts(i) = model.verts(i) + reflected.verts(i + 1) = model.verts(i + 3) + reflected.verts(i + 2) = model.verts(i + 2) + reflected.verts(i + 3) = model.verts(i + 1) + reflectedNormals(i) = normals(i) + reflectedNormals(i + 1) = normals(i + 3) + reflectedNormals(i + 2) = normals(i + 2) + reflectedNormals(i + 3) = normals(i + 1) + i += 4 + } + reflected + } + def generateWireSegment(model: CCModel, i: Int, rect: Rectangle4i) { generateWireSegment(model, i, TWireModel.border(rect), 0.01, 0) generateWireSegment(model, i + 20, rect, 0.02, 1) @@ -446,7 +596,11 @@ object WireModel3D { } } -class WireModel2D(data: Array[Colour]) extends ComponentModel with TWireModel { +class WireModel2D private[integration] (wireRectangles: Seq[Rectangle4i]) + extends ComponentModel + with TWireModel { + def this(data: Array[Int]) = this(TWireModel.rectangulate(data)) + var icons: Array[TextureSpecial] = _ private val iconIndex = WireModel2D.claimIdx() @@ -460,44 +614,22 @@ class WireModel2D(data: Array[Colour]) extends ComponentModel with TWireModel { } override def registerIcons(reg: IIconRegister) { - val wireRectangles = TWireModel.rectangulate(data) + val texMap = TWireModel.buildTexMap(wireRectangles) icons = new Array[TextureSpecial](wireData.length) for (tex <- 0 until icons.length) { - val texMap = new Array[Int](1024) - for (rect <- wireRectangles) { - fillMask(texMap, rect, 2) - fillMask(texMap, TWireModel.border(rect), 1) - } - - val pSize = Math.sqrt(wireData(0).length).asInstanceOf[Int] - val size = Math.max(32, pSize) - val relM = size / 32 - val relP = size / pSize - - val imageData = new Array[Int](size * size) - for (i <- 0 until imageData.length) { - val x = i % size - val y = i / size - val t = texMap(y / relM * 32 + x / relM) - if (t != 0) - imageData(i) = - wireData(if (t == 1) 0 else tex)(y / relP * pSize + x / relP).argb() - } - icons(tex) = TextureUtils .getTextureSpecial( reg, "projectred:integration/wire2d_" + iconIndex + "_" + tex ) - .addTexture(new TextureDataHolder(imageData, size)) + .addTexture( + new TextureDataHolder( + TWireModel.buildTexture(texMap, wireData, tex), + Math.max(32, Math.sqrt(wireData(0).length).asInstanceOf[Int]) + ) + ) } } - - def fillMask(map: Array[Int], r: Rectangle4i, v: Int) { - for (i <- r.x until r.x + r.w) - for (j <- r.y until r.y + r.h) - if (map(j * 32 + i) < v) map(j * 32 + i) = v - } } object WireModel2D { @@ -521,7 +653,7 @@ trait TRedstoneTorchModel extends OnOffModel { } class RedstoneTorchModel(x: Double, z: Double, h: Int) - extends OnOffModel(RedstoneTorchModel.genModel(x, z, h)) + extends OnOffModel(RedstoneTorchModel.cachedModelPair(x, z, h)) with TRedstoneTorchModel { override val getLightPos = new Vector3(x, h - 1, z).multiply(1 / 16d) @@ -545,6 +677,14 @@ class FlippedRSTorchModel(x: Double, z: Double) } object RedstoneTorchModel { + private val modelCache = scala.collection.mutable.HashMap + .empty[(Double, Double, Int), Array[CCModel]] + + private[integration] def cachedModelPair(x: Double, z: Double, h: Int) = + modelCache.synchronized { + modelCache.getOrElseUpdate((x, z, h), bakeDynamic(genModel(x, z, h))) + } + def genModel(x: Double, z: Double, h: Int) = { val m = CCModel.quadModel(20) m.verts(0) = new Vertex5(7 / 16d, 10 / 16d, 9 / 16d, 7 / 16d, 8 / 16d) diff --git a/src/main/scala/mrtjp/projectred/integration/gaterenders.scala b/src/main/scala/mrtjp/projectred/integration/gaterenders.scala index 269c7dea2..2a76e3b29 100644 --- a/src/main/scala/mrtjp/projectred/integration/gaterenders.scala +++ b/src/main/scala/mrtjp/projectred/integration/gaterenders.scala @@ -6,59 +6,88 @@ package mrtjp.projectred.integration import java.util.Random +import java.util.concurrent.atomic.AtomicReferenceArray import codechicken.lib.math.MathHelper import codechicken.lib.render.{CCRenderState, TextureUtils} import codechicken.lib.vec.{RedundantTransformation, Transformation, Vector3} import mrtjp.core.color.Colors import mrtjp.projectred.core.TFaceOrient.flipMaskZ +import mrtjp.projectred.core.Configurator import mrtjp.projectred.integration.ComponentStore._ import net.minecraft.client.renderer.texture.IIconRegister import net.minecraft.item.ItemStack +private[integration] class LazyRendererSlots[A <: AnyRef]( + factories: Array[() => A] +) { + private val builders = factories.clone() + private val values = new AtomicReferenceArray[A](factories.length) + + def length = builders.length + + def apply(index: Int): A = { + var value = values.get(index) + if (value == null) synchronized { + value = values.get(index) + if (value == null) { + value = builders(index)() + values.set(index, value) + } + } + value + } + + def replace(index: Int, factory: () => A): Unit = synchronized { + builders(index) = factory + values.set(index, null.asInstanceOf[A]) + } +} + object RenderGate { - var renderers = buildRenders() - - def buildRenders() = Seq[GateRenderer[_]]( - new RenderOR, - new RenderNOR, - new RenderNOT, - new RenderAND, - new RenderNAND, - new RenderXOR, - new RenderXNOR, - new RenderBuffer, - new RenderMultiplexer, - new RenderPulse, - new RenderRepeater, - new RenderRandomizer, - new RenderSRLatch, - new RenderToggleLatch, - new RenderTransparentLatch, - new RenderLightSensor, - new RenderRainSensor, - new RenderTimer, - new RenderSequencer, - new RenderCounter, - new RenderStateCell, - new RenderSynchronizer, - new RenderBusXcvr, - new RenderNullCell, - new RenderInvertCell, - new RenderBufferCell, - new RenderComparator, - new RenderANDCell, - new RenderBusRandomizer, - new RenderBusConverter, - new RenderBusInputPanel, - new RenderStackingLatch, - new RenderSegmentDisplay, - new RenderDecodingRand, - GateRenderer.blank // circuit gate renderer will be injected. + private val renderers = new LazyRendererSlots[GateRenderer[_]]( + Array[() => GateRenderer[_]]( + () => new RenderOR, + () => new RenderNOR, + () => new RenderNOT, + () => new RenderAND, + () => new RenderNAND, + () => new RenderXOR, + () => new RenderXNOR, + () => new RenderBuffer, + () => new RenderMultiplexer, + () => new RenderPulse, + () => new RenderRepeater, + () => new RenderRandomizer, + () => new RenderSRLatch, + () => new RenderToggleLatch, + () => new RenderTransparentLatch, + () => new RenderLightSensor, + () => new RenderRainSensor, + () => new RenderTimer, + () => new RenderSequencer, + () => new RenderCounter, + () => new RenderStateCell, + () => new RenderSynchronizer, + () => new RenderBusXcvr, + () => new RenderNullCell, + () => new RenderInvertCell, + () => new RenderBufferCell, + () => new RenderComparator, + () => new RenderANDCell, + () => new RenderBusRandomizer, + () => new RenderBusConverter, + () => new RenderBusInputPanel, + () => new RenderStackingLatch, + () => new RenderSegmentDisplay, + () => new RenderDecodingRand, + () => GateRenderer.blank // circuit gate renderer will be injected. + ) ) def registerIcons(reg: IIconRegister) { - for (r <- renderers) r.registerIcons(reg) + if (!Configurator.logicwires3D) + for (i <- 0 until renderers.length) renderers(i).registerIcons(reg) } def renderStatic(gate: GatePart, pos: Vector3) { @@ -92,11 +121,8 @@ object RenderGate { .spawnParticles(gate, rand) } - def hotswap(r: GateRenderer[_], meta: Int) { - val ar = renderers.toArray - ar(meta) = r - renderers = ar.toSeq - } + def hotswap(r: => GateRenderer[_], meta: Int): Unit = + renderers.replace(meta, () => r) } abstract class GateRenderer[T <: GatePart] { diff --git a/src/test/resources/mrtjp/projectred/integration/gate-wire-2d-v1.txt b/src/test/resources/mrtjp/projectred/integration/gate-wire-2d-v1.txt new file mode 100644 index 000000000..ca656e879 --- /dev/null +++ b/src/test/resources/mrtjp/projectred/integration/gate-wire-2d-v1.txt @@ -0,0 +1 @@ +1fbc12977134f4c484cab8351a53f37a7ae58ec9cc7ab3c5dec603afef316477 diff --git a/src/test/resources/mrtjp/projectred/integration/gate-wire-baking-v1.txt b/src/test/resources/mrtjp/projectred/integration/gate-wire-baking-v1.txt new file mode 100644 index 000000000..80443351e --- /dev/null +++ b/src/test/resources/mrtjp/projectred/integration/gate-wire-baking-v1.txt @@ -0,0 +1 @@ +a9e81b7cda819306e288b45f42a1c0445f2cccebdfc7a37e981ddde80c8c9c29 diff --git a/src/test/resources/mrtjp/projectred/integration/gate-wire-geometry-v1.txt b/src/test/resources/mrtjp/projectred/integration/gate-wire-geometry-v1.txt new file mode 100644 index 000000000..707662118 --- /dev/null +++ b/src/test/resources/mrtjp/projectred/integration/gate-wire-geometry-v1.txt @@ -0,0 +1 @@ +7a39d31fb1365c57d285357a344e13c22598c477459d6f32270c63cb4d017b9c diff --git a/src/test/scala/mrtjp/projectred/integration/ComponentModelSharingTest.scala b/src/test/scala/mrtjp/projectred/integration/ComponentModelSharingTest.scala new file mode 100644 index 000000000..725f802bb --- /dev/null +++ b/src/test/scala/mrtjp/projectred/integration/ComponentModelSharingTest.scala @@ -0,0 +1,49 @@ +package mrtjp.projectred.integration + +import codechicken.lib.render.CCModel +import codechicken.lib.vec.Vector3 +import org.junit.Assert.{assertEquals, assertNotSame, assertSame} +import org.junit.Test + +class ComponentModelSharingTest { + + @Test + def equivalentTorchesShareOnlyGeometry(): Unit = { + val first = new RedstoneTorchModel(8, 8, 6) + val second = new RedstoneTorchModel(8, 8, 6) + val different = new RedstoneTorchModel(8, 8, 8) + val expected = SingleComponentModel.bakeModelPair( + RedstoneTorchModel.genModel(8, 8, 6), + Vector3.zero + ) + + assertSame(first.modelPair, second.modelPair) + assertNotSame(first.modelPair, different.modelPair) + assertModelsEqual(expected, first.modelPair) + first.on = true + assertEquals(false, second.on) + } + + private def assertModelsEqual( + expected: Array[CCModel], + actual: Array[CCModel] + ) = + for ( + pairIndex <- expected.indices; + vertexIndex <- expected(pairIndex).verts.indices + ) { + val expectedVertex = expected(pairIndex).verts(vertexIndex) + val actualVertex = actual(pairIndex).verts(vertexIndex) + val expectedNormal = expected(pairIndex).normals()(vertexIndex) + val actualNormal = actual(pairIndex).normals()(vertexIndex) + assertEquals(expectedVertex.vec.x, actualVertex.vec.x, 0) + assertEquals(expectedVertex.vec.y, actualVertex.vec.y, 0) + assertEquals(expectedVertex.vec.z, actualVertex.vec.z, 0) + assertEquals(expectedVertex.uv.u, actualVertex.uv.u, 0) + assertEquals(expectedVertex.uv.v, actualVertex.uv.v, 0) + assertEquals(expectedVertex.uv.tex, actualVertex.uv.tex) + assertEquals(expectedNormal.x, actualNormal.x, 0) + assertEquals(expectedNormal.y, actualNormal.y, 0) + assertEquals(expectedNormal.z, actualNormal.z, 0) + } +} diff --git a/src/test/scala/mrtjp/projectred/integration/GateWireGoldenTest.scala b/src/test/scala/mrtjp/projectred/integration/GateWireGoldenTest.scala new file mode 100644 index 000000000..10c5fc5ae --- /dev/null +++ b/src/test/scala/mrtjp/projectred/integration/GateWireGoldenTest.scala @@ -0,0 +1,311 @@ +package mrtjp.projectred.integration + +import java.io.{ByteArrayOutputStream, DataOutputStream, File} +import java.awt.image.BufferedImage +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.security.MessageDigest +import javax.imageio.ImageIO + +import codechicken.lib.colour.{Colour, ColourARGB} +import codechicken.lib.render.CCModel +import codechicken.lib.vec.Rectangle4i +import org.junit.Assert.{assertEquals, assertNotNull, assertNotSame, assertSame} +import org.junit.Test + +import scala.io.Source + +class GateWireGoldenTest { + + @Test + def wireGeometryMatchesGoldenOutput(): Unit = { + val actual = GateWireGoldenTest.maskNames.map(characterize) + assertGolden("geometry", GateWireGoldenTest.header, actual) + } + + @Test + def wireBakingMatchesGoldenOutput(): Unit = { + val actual = GateWireGoldenTest.maskNames.map(characterizeBaking) + assertGolden("baking", GateWireGoldenTest.bakingHeader, actual) + } + + @Test + def wire2DOutputMatchesGoldenOutput(): Unit = { + val wireData = GateWireGoldenTest.materialNames.map(loadImage).toArray + val planeDigest = digest { out => + WireModel2D.models.foreach(writeModel(out, _)) + } + val actual = ("plane " + planeDigest) +: GateWireGoldenTest.maskNames.map { + name => characterize2D(name, wireData) + } + assertGolden("2d", GateWireGoldenTest.twoDHeader, actual) + } + + @Test + def bakedModelsDoNotShareMutableGeometry(): Unit = { + val base = WireModel3D.generateModel(GateWireTestData.loadMask("OR-0")) + val pair = ComponentModelBakery.bakeDynamic(base) + + assertNotSame(pair(0), pair(1)) + assertNotSame(pair(0).verts(0), pair(1).verts(0)) + assertNotSame(pair(0).verts(0).vec, pair(1).verts(0).vec) + assertNotSame(pair(0).verts(0).uv, pair(1).verts(0).uv) + assertNotSame(pair(0).normals()(0), pair(1).normals()(0)) + + val originalX = pair(0).verts(0).vec.x + pair(1).verts(0).vec.x += 1 + assertEquals(originalX, pair(0).verts(0).vec.x, 0) + } + + @Test + def wireModelPairOwnsGeneratedBase(): Unit = { + val base = WireModel3D.generateModel(GateWireTestData.loadMask("OR-0")) + val pair = WireModel3D.bakeModelPair(base) + + assertSame(base, pair(0)) + assertNotSame(pair(0), pair(1)) + for (i <- pair(0).verts.indices by 4) { + assertSame(pair(0).verts(i), pair(1).verts(i)) + assertSame(pair(0).verts(i + 3), pair(1).verts(i + 1)) + assertSame(pair(0).verts(i + 2), pair(1).verts(i + 2)) + assertSame(pair(0).verts(i + 1), pair(1).verts(i + 3)) + assertSame(pair(0).normals()(i), pair(1).normals()(i)) + assertSame(pair(0).normals()(i + 3), pair(1).normals()(i + 1)) + assertSame(pair(0).normals()(i + 2), pair(1).normals()(i + 2)) + assertSame(pair(0).normals()(i + 1), pair(1).normals()(i + 3)) + } + } + + @Test + def equivalentWireLayoutsShareOnlyGeometry(): Unit = { + val rectangles = GateWireMasks.rectangles("OR-0") + val equivalent = rectangles.map(r => new Rectangle4i(r.x, r.y, r.w, r.h)) + val first = new WireModel3D(rectangles) + val second = new WireModel3D(equivalent) + + assertSame(first.modelPair, second.modelPair) + first.on = true + first.disabled = true + assertEquals(false, second.on) + assertEquals(false, second.disabled) + } + + private def characterize(name: String): String = { + val data = GateWireTestData.loadMask(name) + val rectangles = TWireModel.rectangulate(data) + val model = WireModel3D.generateModel(data) + + Seq( + name, + rectangles.length.toString, + model.verts.length.toString, + digestRectangles(rectangles), + digestModel(model) + ).mkString(" ") + } + + private def characterizeBaking(name: String): String = { + val modelPair = WireModel3D.generateModelPair( + TWireModel.rectangulate(GateWireTestData.loadMask(name)) + ) + val orientedDigest = digest { out => + for (orient <- 0 until 48) { + val model = modelPair(if (orient < 24) 0 else 1).copy + model.apply(ComponentModelBakery.orientPrecomputed(orient)) + writeModel(out, model) + } + } + name + " " + modelPair(0).verts.length + " " + orientedDigest + } + + private def characterize2D(name: String, wireData: Array[Array[Colour]]) = { + val texMap = TWireModel.buildTexMap( + TWireModel.rectangulate(GateWireTestData.loadMask(name)) + ) + val textures = wireData.indices.map { tex => + digestInts(TWireModel.buildTexture(texMap, wireData, tex)) + } + (name +: digestInts(texMap) +: textures).mkString(" ") + } + + private def loadImage(name: String): Array[Colour] = { + val image = loadImageData(name) + image + .getRGB(0, 0, image.getWidth, image.getHeight, null, 0, image.getWidth) + .map(new ColourARGB(_): Colour) + } + + private def loadImageData(name: String): BufferedImage = { + val path = GateWireGoldenTest.maskPath + name + ".png" + val stream = getClass.getResourceAsStream(path) + assertNotNull("Missing wire mask " + path, stream) + + val image = + try ImageIO.read(stream) + finally stream.close() + assertNotNull("Invalid wire mask " + path, image) + image + } + + private def digestRectangles(rectangles: Seq[Rectangle4i]): String = digest { + out => + out.writeInt(rectangles.length) + rectangles.foreach { rectangle => + out.writeInt(rectangle.x) + out.writeInt(rectangle.y) + out.writeInt(rectangle.w) + out.writeInt(rectangle.h) + } + } + + private def digestModel(model: CCModel): String = digest(writeModel(_, model)) + + private def digestInts(values: Array[Int]): String = digest { out => + out.writeInt(values.length) + values.foreach(out.writeInt) + } + + private def writeModel(out: DataOutputStream, model: CCModel): Unit = { + val normals = model.normals() + out.writeInt(model.vertexMode) + out.writeInt(model.vp) + out.writeInt(model.verts.length) + model.verts.indices.foreach { i => + val vertex = model.verts(i) + val normal = normals(i) + writeDouble(out, vertex.vec.x) + writeDouble(out, vertex.vec.y) + writeDouble(out, vertex.vec.z) + writeDouble(out, vertex.uv.u) + writeDouble(out, vertex.uv.v) + out.writeInt(vertex.uv.tex) + writeDouble(out, normal.x) + writeDouble(out, normal.y) + writeDouble(out, normal.z) + } + } + + private def digest(write: DataOutputStream => Unit): String = { + val bytes = new ByteArrayOutputStream + val out = new DataOutputStream(bytes) + write(out) + out.close() + sha256(bytes.toByteArray) + } + + private def sha256(bytes: Array[Byte]): String = + MessageDigest + .getInstance("SHA-256") + .digest(bytes) + .map("%02x".format(_)) + .mkString + + private def writeDouble(out: DataOutputStream, value: Double): Unit = { + require(!value.isNaN && !value.isInfinity, "Non-finite model value") + out.writeLong(Math.round((if (value == 0) 0 else value) * 1000000000L)) + } + + private def readResource(path: String): String = { + val stream = getClass.getResourceAsStream(path) + assertNotNull("Missing golden output " + path, stream) + val source = Source.fromInputStream(stream, "UTF-8") + try source.mkString + finally source.close() + } + + private def assertGolden( + kind: String, + header: String, + actual: Seq[String] + ): Unit = { + val actualText = (header +: actual).mkString("\n") + "\n" + val actualDigest = sha256(actualText.getBytes(StandardCharsets.UTF_8)) + val goldenPath = + "/mrtjp/projectred/integration/gate-wire-" + kind + "-v1.txt" + val expectedDigest = readResource(goldenPath).trim + + if (actualDigest != expectedDigest) { + val report = new File( + "build/reports/wire-golden/actual-" + kind + "-v1.txt" + ) + report.getParentFile.mkdirs() + Files.write(report.toPath, actualText.getBytes(StandardCharsets.UTF_8)) + } + + assertEquals( + "Wire " + kind + " changed; see build/reports/wire-golden/actual-" + kind + "-v1.txt", + expectedDigest, + actualDigest + ) + } +} + +object GateWireGoldenTest { + val maskPath = "/assets/projectred/textures/blocks/integration/surface/" + val header = "# gate-wire-geometry-v1 scale=1e-9 masks=120" + val bakingHeader = + "# gate-wire-baking-v1 scale=1e-9 masks=120 orientations=48" + val twoDHeader = + "# gate-wire-2d-v1 scale=1e-9 masks=120 textures=3 orientations=48" + val materialNames = Seq("bordermatte", "wirematte-OFF", "wirematte-ON") + + val maskNames = Seq( + "OR" -> 4, + "NOR" -> 4, + "NOT" -> 4, + "AND" -> 4, + "NAND" -> 4, + "XOR" -> 4, + "XNOR" -> 5, + "BUFFER" -> 4, + "MULTIPLEXER" -> 6, + "PULSE" -> 3, + "REPEATER" -> 2, + "RAND" -> 7, + "RSLATCH" -> 2, + "RSLATCH2" -> 4, + "TOGLATCH" -> 2, + "TRANSLATCH" -> 5, + "LIGHTSENSOR" -> 1, + "RAINSENSOR" -> 1, + "TIME" -> 3, + "COUNT" -> 2, + "STATECELL" -> 5, + "SYNC" -> 6, + "BUSXCVR" -> 2, + "COMPARATOR" -> 4, + "BUSRAND1" -> 2, + "BUSRAND2" -> 2, + "BUSCONV" -> 3, + "BUSINPUT" -> 1, + "INVCELL" -> 1, + "BUFFCELL" -> 2, + "ANDCELL" -> 2, + "STACKLATCH" -> 5, + "DECRAND" -> 6, + "IC1" -> 4, + "IC2" -> 4 + ).flatMap { case (name, count) => (0 until count).map(name + "-" + _) } + + require(maskNames.length == 120) +} + +private[integration] object GateWireTestData { + def loadMask(name: String): Array[Int] = { + val image = loadImageData(name) + assertEquals("Unexpected width for " + name, 32, image.getWidth) + assertEquals("Unexpected height for " + name, 32, image.getHeight) + image.getRGB(0, 0, image.getWidth, image.getHeight, null, 0, image.getWidth) + } + + private def loadImageData(name: String): BufferedImage = { + val path = GateWireGoldenTest.maskPath + name + ".png" + val stream = getClass.getResourceAsStream(path) + assertNotNull("Missing wire mask " + path, stream) + val image = + try ImageIO.read(stream) + finally stream.close() + assertNotNull("Invalid wire mask " + path, image) + image + } +} diff --git a/src/test/scala/mrtjp/projectred/integration/GateWireMasksTest.scala b/src/test/scala/mrtjp/projectred/integration/GateWireMasksTest.scala new file mode 100644 index 000000000..547a6e678 --- /dev/null +++ b/src/test/scala/mrtjp/projectred/integration/GateWireMasksTest.scala @@ -0,0 +1,121 @@ +package mrtjp.projectred.integration + +import java.io.{ByteArrayInputStream, InputStream} +import java.util.{ArrayList, Collections, List => JList, Set => JSet} + +import codechicken.lib.vec.Rectangle4i +import net.minecraft.client.resources.data.IMetadataSection +import net.minecraft.client.resources.{IResource, IResourceManager} +import net.minecraft.util.ResourceLocation +import org.apache.commons.io.IOUtils +import org.junit.Assert.{assertEquals, assertTrue} +import org.junit.Test + +class GateWireMasksTest { + + @Test + def precomputedLayoutsMatchSourceMasks(): Unit = { + GateWireGoldenTest.maskNames.foreach { name => + assertEquals( + name, + tuples(TWireModel.rectangulate(GateWireTestData.loadMask(name))), + tuples(GateWireMasks.rectangles(name)) + ) + } + } + + @Test + def builtInMasksHaveExpectedUniqueLayouts(): Unit = { + assertEquals( + 97, + GateWireGoldenTest.maskNames + .map(GateWireMasks.rectangles) + .map(tuples) + .distinct + .size + ) + } + + @Test + def builtInLayoutClosesButDoesNotReadSingleResource(): Unit = { + val stream = + new TrackingInputStream(Array.emptyByteArray, failOnRead = true) + val actual = ComponentStore.loadWireRectangles("OR-0", manager(stream)) + + assertEquals(tuples(GateWireMasks.rectangles("OR-0")), tuples(actual)) + assertTrue(stream.closed) + } + + @Test + def resourcePackOverrideUsesHighestPriorityMask(): Unit = { + val builtIn = + new TrackingInputStream(Array.emptyByteArray, failOnRead = true) + val overrideStream = new TrackingInputStream(resourceBytes("OR-1")) + val actual = ComponentStore.loadWireRectangles( + "OR-0", + manager(builtIn, overrideStream) + ) + + assertEquals(tuples(GateWireMasks.rectangles("OR-1")), tuples(actual)) + assertTrue(builtIn.closed) + assertTrue(overrideStream.closed) + } + + @Test + def unknownMaskFallsBackToPng(): Unit = { + val stream = new TrackingInputStream(resourceBytes("OR-1")) + val actual = ComponentStore.loadWireRectangles("ADDON", manager(stream)) + + assertEquals(tuples(GateWireMasks.rectangles("OR-1")), tuples(actual)) + assertTrue(stream.closed) + } + + private def tuples(rectangles: Seq[Rectangle4i]) = + rectangles.map(r => (r.x, r.y, r.w, r.h)) + + private def resourceBytes(name: String): Array[Byte] = { + val stream = + getClass.getResourceAsStream(GateWireGoldenTest.maskPath + name + ".png") + try IOUtils.toByteArray(stream) + finally stream.close() + } + + private def manager(streams: TrackingInputStream*) = new IResourceManager { + private val resources = new ArrayList[IResource]() + streams.foreach(stream => resources.add(new TestResource(stream))) + + override def getResourceDomains: JSet[_] = Collections.emptySet[AnyRef]() + override def getResource(location: ResourceLocation): IResource = + resources.get(resources.size() - 1) + override def getAllResources(location: ResourceLocation): JList[_] = + resources + } + + private class TestResource(stream: InputStream) extends IResource { + override def getInputStream = stream + override def hasMetadata = false + override def getMetadata(name: String): IMetadataSection = null + } + + private class TrackingInputStream( + bytes: Array[Byte], + failOnRead: Boolean = false + ) extends ByteArrayInputStream(bytes) { + var closed = false + + override def read(): Int = { + if (failOnRead) throw new AssertionError("Stream was read") + super.read() + } + + override def read(buffer: Array[Byte], offset: Int, length: Int): Int = { + if (failOnRead) throw new AssertionError("Stream was read") + super.read(buffer, offset, length) + } + + override def close(): Unit = { + closed = true + super.close() + } + } +} diff --git a/src/test/scala/mrtjp/projectred/integration/LazyRendererSlotsTest.scala b/src/test/scala/mrtjp/projectred/integration/LazyRendererSlotsTest.scala new file mode 100644 index 000000000..fccf9c28a --- /dev/null +++ b/src/test/scala/mrtjp/projectred/integration/LazyRendererSlotsTest.scala @@ -0,0 +1,73 @@ +package mrtjp.projectred.integration + +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.{Callable, CountDownLatch, Executors, TimeUnit} + +import org.junit.Assert.{assertEquals, assertSame} +import org.junit.Test + +class LazyRendererSlotsTest { + + @Test + def constructsSlotOnceOnConcurrentFirstAccess(): Unit = { + val constructions = new AtomicInteger + val slots = new LazyRendererSlots[String](Array(() => { + constructions.incrementAndGet() + new String("renderer") + })) + assertEquals(0, constructions.get()) + + val start = new CountDownLatch(1) + val executor = Executors.newFixedThreadPool(8) + try { + val futures = (0 until 8).map { _ => + executor.submit(new Callable[String] { + override def call(): String = { + start.await() + slots(0) + } + }) + } + start.countDown() + + val expected = futures.head.get(5, TimeUnit.SECONDS) + futures.tail.foreach(f => + assertSame(expected, f.get(5, TimeUnit.SECONDS)) + ) + assertEquals(1, constructions.get()) + } finally executor.shutdownNow() + } + + @Test + def replacementStaysLazyBeforeAndAfterAccess(): Unit = { + val originalConstructions = new AtomicInteger + val replacementConstructions = new AtomicInteger + val slots = new LazyRendererSlots[String](Array(() => { + originalConstructions.incrementAndGet() + "original" + })) + + slots.replace( + 0, + () => { + replacementConstructions.incrementAndGet() + "first replacement" + } + ) + assertEquals(0, originalConstructions.get()) + assertEquals(0, replacementConstructions.get()) + assertEquals("first replacement", slots(0)) + assertEquals(1, replacementConstructions.get()) + + slots.replace( + 0, + () => { + replacementConstructions.incrementAndGet() + "second replacement" + } + ) + assertEquals(1, replacementConstructions.get()) + assertEquals("second replacement", slots(0)) + assertEquals(2, replacementConstructions.get()) + } +}