i suck at git. to whoever is reading this in the future, please excuse this absolutely terrible commit message and know that i'm doing my best.
cheers & love.

once dawd³ is in a better shape these commit messages will get better. for now they're terrible because the sheer amount of experimentation makes me forget that i have to `git commit` my work every so often.
This commit is contained in:
りき萌 2022-11-27 22:04:26 +01:00
parent 367f2fc80e
commit 337c9d08e1
129 changed files with 2072 additions and 244 deletions

View file

@ -55,6 +55,8 @@ loom {
runDir("build/datagen")
}
}
accessWidenerPath.set(file("src/main/resources/dawd3.accesswidener"))
}
sourceSets.named(mainSourceSet.name) {

BIN
proj/block/fader_bottom.ase Normal file

Binary file not shown.

BIN
proj/block/fader_handle.ase Normal file

Binary file not shown.

BIN
proj/block/fader_side.ase Normal file

Binary file not shown.

BIN
proj/block/fader_top.ase Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
proj/device/cable.ase Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +1,26 @@
// 1.19.2 2022-11-20T23:04:16.520652716 Models
e3c6aacd49a6395f37047d3df31f91a18a411267 assets/dawd3/models/item/speaker.json
// 1.19.2 2022-11-27T14:23:10.976127707 Models
0812a674d14cfc6fbb7c0e2ac1b473bf2afe1965 assets/dawd3/models/item/brown_patch_cable.json
bf0e322e33123cb6873c2da4e8c6ab85688deb4e assets/dawd3/models/item/gray_patch_cable.json
6a9fb209d82556f5941422a8d047a0ae2af1dc8f assets/dawd3/models/item/sine_oscillator.json
215b221d48639e96de11914625a770a389d65b81 assets/dawd3/models/item/green_patch_cable.json
e34001d3c974aecfa347d435beb6bc0b9d325897 assets/dawd3/models/item/cyan_patch_cable.json
8ba890b28c5ac57c59f19ccc8c72825caac10677 assets/dawd3/models/item/magenta_patch_cable.json
f7b47538f17992177e97e06842c0039ae5096b2b assets/dawd3/blockstates/speaker.json
bd0adfc8b3dc271042dd4b19a8cace4e0fffedfe assets/dawd3/models/block/speaker.json
9cf2cff42345ec60a944d7399b5047323aa8e88c assets/dawd3/models/item/red_patch_cable.json
8c6f0307320980a66c70622b0b7c72c8cfe78dc3 assets/dawd3/models/item/light_gray_patch_cable.json
5ed33e9ec3c70e8cc027eea6425951d51487437f assets/dawd3/models/item/blue_patch_cable.json
83866bdfd40b759257070558c9ccb942e082914a assets/dawd3/blockstates/fader.json
a4e8bc89d39021eb8d56ad7735216cb851d67287 assets/dawd3/models/item/light_blue_patch_cable.json
0023521ecb90e79515f0fa0088609c70aac9a605 assets/dawd3/models/block/speaker.json
8aa966337109315240614d5257eb72f959eba5d8 assets/dawd3/models/item/orange_patch_cable.json
12c4bfd825b2476955afcd3bb23c1f736ce68caa assets/dawd3/models/item/yellow_patch_cable.json
aab1bce7ec4e7c7dccd3d33d4242de12b63a981d assets/dawd3/models/item/white_patch_cable.json
6cdda36e539e23acf3db70d761338e88932a6ebb assets/dawd3/models/item/purple_patch_cable.json
e3c6aacd49a6395f37047d3df31f91a18a411267 assets/dawd3/models/item/speaker.json
32bb0e6e3bf75b9005602e8fb1042ad5d41286ad assets/dawd3/models/item/lime_patch_cable.json
956d8f117df95cf62c8cac375cff853df96840d6 assets/dawd3/models/item/pink_patch_cable.json
71fd99012b4338600f0a11dcadb9a97a6e0a15e5 assets/dawd3/models/block/fader.json
3b1811bab3ba394ba03b67ac2efc72cb35316dc8 assets/dawd3/blockstates/sine_oscillator.json
9c18a8292a0c9990cd23bebf5c6191c2114ccc6d assets/dawd3/models/item/black_patch_cable.json
d300f52d5aa2dcb2ec7d9a0cdb1138104e016d83 assets/dawd3/models/item/fader.json
0d759623578e206f437f2d9976443a773721a9f3 assets/dawd3/models/block/sine_oscillator.json

View file

@ -0,0 +1,19 @@
{
"variants": {
"facing=east": {
"model": "dawd3:block/fader",
"y": 90
},
"facing=north": {
"model": "dawd3:block/fader"
},
"facing=south": {
"model": "dawd3:block/fader",
"y": 180
},
"facing=west": {
"model": "dawd3:block/fader",
"y": 270
}
}
}

View file

@ -0,0 +1,19 @@
{
"variants": {
"facing=east": {
"model": "dawd3:block/sine_oscillator",
"y": 90
},
"facing=north": {
"model": "dawd3:block/sine_oscillator"
},
"facing=south": {
"model": "dawd3:block/sine_oscillator",
"y": 180
},
"facing=west": {
"model": "dawd3:block/sine_oscillator",
"y": 270
}
}
}

View file

@ -0,0 +1,77 @@
{
"parent": "block/block",
"elements": [
{
"faces": {
"down": {
"cullface": "down",
"texture": "#bottom"
},
"east": {
"cullface": "east",
"texture": "#right"
},
"north": {
"cullface": "north",
"texture": "#front"
},
"south": {
"cullface": "south",
"texture": "#back"
},
"up": {
"cullface": "up",
"texture": "#top"
},
"west": {
"cullface": "west",
"texture": "#left"
}
},
"from": [
0.0,
0.0,
0.0
],
"to": [
16.0,
8.0,
16.0
]
},
{
"faces": {
"north": {
"cullface": "north",
"texture": "#port",
"uv": [
4.0,
0.0,
8.0,
4.0
]
}
},
"from": [
6.0,
3.0,
-0.01
],
"to": [
10.0,
7.0,
0.01
]
}
],
"textures": {
"back": "dawd3:block/fader_side",
"bottom": "dawd3:block/fader_bottom",
"front": "dawd3:block/fader_side",
"left": "dawd3:block/fader_side",
"particle": "dawd3:block/fader_side",
"port": "dawd3:device/port",
"right": "dawd3:block/fader_side",
"top": "dawd3:block/fader_top"
}
}

View file

@ -0,0 +1,101 @@
{
"parent": "block/block",
"elements": [
{
"faces": {
"down": {
"cullface": "down",
"texture": "#bottom"
},
"east": {
"cullface": "east",
"texture": "#right"
},
"north": {
"cullface": "north",
"texture": "#front"
},
"south": {
"cullface": "south",
"texture": "#back"
},
"up": {
"cullface": "up",
"texture": "#top"
},
"west": {
"cullface": "west",
"texture": "#left"
}
},
"from": [
0.0,
0.0,
0.0
],
"to": [
16.0,
16.0,
16.0
]
},
{
"faces": {
"north": {
"cullface": "north",
"texture": "#port",
"uv": [
0.0,
0.0,
4.0,
4.0
]
}
},
"from": [
9.0,
6.0,
-0.01
],
"to": [
13.0,
10.0,
0.01
]
},
{
"faces": {
"north": {
"cullface": "north",
"texture": "#port",
"uv": [
4.0,
0.0,
8.0,
4.0
]
}
},
"from": [
3.0,
6.0,
-0.01
],
"to": [
7.0,
10.0,
0.01
]
}
],
"textures": {
"back": "dawd3:block/sine_oscillator_side",
"bottom": "dawd3:block/sine_oscillator_side",
"front": "dawd3:block/sine_oscillator_front",
"left": "dawd3:block/sine_oscillator_side",
"particle": "dawd3:block/sine_oscillator_side",
"port": "dawd3:device/port",
"right": "dawd3:block/sine_oscillator_side",
"top": "dawd3:block/sine_oscillator_side"
}
}

View file

@ -5,11 +5,11 @@
"faces": {
"down": {
"cullface": "down",
"texture": "#side"
"texture": "#bottom"
},
"east": {
"cullface": "east",
"texture": "#side"
"texture": "#right"
},
"north": {
"cullface": "north",
@ -17,15 +17,15 @@
},
"south": {
"cullface": "south",
"texture": "#side"
"texture": "#back"
},
"up": {
"cullface": "up",
"texture": "#side"
"texture": "#top"
},
"west": {
"cullface": "west",
"texture": "#side"
"texture": "#left"
}
},
"from": [
@ -53,21 +53,25 @@
}
},
"from": [
6.5,
6.5,
6.0,
3.0,
15.99
],
"to": [
9.5,
9.5,
10.0,
7.0,
16.01
]
}
],
"textures": {
"back": "dawd3:block/speaker_side",
"bottom": "dawd3:block/speaker_side",
"front": "dawd3:block/speaker_front",
"particle": "#side",
"left": "dawd3:block/speaker_side",
"particle": "dawd3:block/speaker_side",
"port": "dawd3:device/port",
"side": "dawd3:block/speaker_side"
"right": "dawd3:block/speaker_side",
"top": "dawd3:block/speaker_side"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/black_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/blue_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/brown_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/cyan_patch_cable"
}
}

View file

@ -0,0 +1,3 @@
{
"parent": "dawd3:block/fader"
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/gray_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/green_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/light_blue_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/light_gray_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/lime_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/magenta_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/orange_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/pink_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/purple_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/red_patch_cable"
}
}

View file

@ -0,0 +1,3 @@
{
"parent": "dawd3:block/sine_oscillator"
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/white_patch_cable"
}
}

View file

@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "dawd3:item/yellow_patch_cable"
}
}

View file

@ -6,8 +6,9 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.liquidev.d3r.D3r
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.block.Blocks
import net.liquidev.dawd3.block.entity.registerBlockEntityEvents
import net.liquidev.dawd3.block.entity.registerClientBlockEntityEvents
import net.liquidev.dawd3.item.Items
import net.liquidev.dawd3.net.Packets
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -34,7 +35,9 @@ object Mod : ModInitializer, ClientModInitializer {
D3r.unload()
}
registerBlockEntityEvents()
registerClientBlockEntityEvents()
Blocks.initializeClient()
Packets.registerClientReceivers()
}
private fun loggerName(name: String?): String =

View file

@ -2,12 +2,12 @@ package net.liquidev.dawd3.audio
import net.liquidev.d3r.D3r
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.generator.GeneratorWithProcessingState
import net.liquidev.dawd3.audio.generator.MixGenerator
import net.liquidev.dawd3.audio.unit.Decibels
/** Audio system and common settings. */
object Audio {
val logger = Mod.logger<Audio>()
private val logger = Mod.logger<Audio>()
const val sampleRate = 48000
const val sampleRateF = sampleRate.toFloat()
@ -18,13 +18,15 @@ object Audio {
private val outputStreamId: Int
val mixer = MixGenerator()
private val processingStateAdapter = GeneratorWithProcessingState(mixer)
val processingState get() = processingStateAdapter.processingState
init {
logger.info("initializing")
logger.info("${Decibels(-3.0f).toAmplitude()}")
D3r.openDefaultHost()
outputDeviceId = D3r.openDefaultOutputDevice()
outputStreamId = D3r.openOutputStream(outputDeviceId, sampleRate, 1, bufferSize, mixer)
outputStreamId =
D3r.openOutputStream(outputDeviceId, sampleRate, 1, bufferSize, processingStateAdapter)
D3r.startPlayback(outputStreamId)
}

View file

@ -4,6 +4,6 @@ package net.liquidev.dawd3.audio.device
interface Device {
fun process(sampleCount: Int, channels: Int)
fun visitInputPorts(visit: (PortName, InputPort) -> Unit) {}
fun visitOutputPorts(visit: (PortName, OutputPort) -> Unit) {}
fun visitInputPorts(visit: (InputPortName, InputPort) -> Unit) {}
fun visitOutputPorts(visit: (OutputPortName, OutputPort) -> Unit) {}
}

View file

@ -0,0 +1,7 @@
package net.liquidev.dawd3.audio.device
import net.minecraft.util.Identifier
interface DeviceDescriptor {
val id: Identifier
}

View file

@ -23,9 +23,16 @@ class DeviceInstance(val state: Device) {
}
}
fun process(sampleCount: Int, channels: Int) {
fun process(sampleCount: Int, channels: Int, processingState: ProcessingState) {
if (this !in processingState.processedDevices) {
println("processing $this")
processingState.processedDevices.add(this)
for ((_, port) in inputPortsByName) {
port.connectedOutput?.owningDevice?.process(sampleCount, channels, processingState)
}
state.process(sampleCount, channels)
}
}
override fun toString(): String {
return "DeviceInstance($state)"

View file

@ -1,12 +1,45 @@
package net.liquidev.dawd3.audio.device
import net.liquidev.dawd3.Mod
/** Device utility functions. */
object Devices {
private val logger = Mod.logger<Devices>()
data class InputAndOutputDevicePortPairs<T>(
val outputDevice: T,
val outputPort: OutputPortName,
val inputDevice: T,
val inputPort: InputPortName,
)
fun <T> sortPortsByInputAndOutput(
fromData: T,
fromPort: PortName,
toData: T,
toPort: PortName,
): InputAndOutputDevicePortPairs<T>? =
when {
fromPort is OutputPortName && toPort is InputPortName -> InputAndOutputDevicePortPairs(
outputDevice = fromData,
outputPort = fromPort,
inputDevice = toData,
inputPort = toPort,
)
fromPort is InputPortName && toPort is OutputPortName -> InputAndOutputDevicePortPairs(
outputDevice = toData,
outputPort = toPort,
inputDevice = fromData,
inputPort = fromPort,
)
else -> null
}
fun makeConnection(
from: DeviceInstance,
outputPortName: PortName,
to: DeviceInstance,
inputPortName: PortName
inputPortName: PortName,
) {
val outputPort = from.outputPortsByName[outputPortName]
val inputPort = to.inputPortsByName[inputPortName]
@ -20,31 +53,55 @@ object Devices {
inputPort.connectedOutput = outputPort
outputPort.connectedInputs.add(inputPort)
logger.debug("made connection between ports $outputPortName ($outputPort) and $inputPortName ($inputPort)")
}
/** Disconnects everything from the given port. Returns the number of ports disconnected. */
fun severAllConnections(device: DeviceInstance, portName: PortName): Int {
private fun severAllConnectionsInInputPort(inputPort: InputPort): Int {
var total = 0
val inputPort = device.inputPortsByName[portName]
if (inputPort != null) {
val connectedOutput = inputPort.connectedOutput
if (connectedOutput != null) {
connectedOutput.connectedInputs.remove(inputPort)
inputPort.connectedOutput = null
total += 1
}
return total
}
val outputPort = device.outputPortsByName[portName]
if (outputPort != null) {
private fun severAllConnectionsInOutputPort(outputPort: OutputPort): Int {
var total = 0
for (port in outputPort.connectedInputs) {
port.connectedOutput = null
}
total += outputPort.connectedInputs.size
outputPort.connectedInputs.clear()
return total
}
/** Disconnects everything from the given port. Returns the number of ports disconnected. */
fun severAllConnectionsInPort(device: DeviceInstance, portName: PortName): Int {
var total = 0
val inputPort = device.inputPortsByName[portName]
if (inputPort != null) {
total += severAllConnectionsInInputPort(inputPort)
}
val outputPort = device.outputPortsByName[portName]
if (outputPort != null) {
total += severAllConnectionsInOutputPort(outputPort)
}
return total
}
fun severAllConnectionsInDevice(device: DeviceInstance): Int {
var total = 0
for ((_, inputPort) in device.inputPortsByName) {
total += severAllConnectionsInInputPort(inputPort)
}
for ((_, outputPort) in device.outputPortsByName) {
total += severAllConnectionsInOutputPort(outputPort)
}
return total
}

View file

@ -0,0 +1,9 @@
package net.liquidev.dawd3.audio.device
class ProcessingState {
val processedDevices = mutableSetOf<DeviceInstance>()
fun reset() {
processedDevices.clear()
}
}

View file

@ -1,45 +0,0 @@
package net.liquidev.dawd3.audio.device
import net.liquidev.dawd3.Mod
/**
* Sim is the simulator for device graphs.
*/
class Sim {
private companion object {
val logger = Mod.logger<Sim>()
}
private val stack = arrayListOf<DeviceInstance>()
private val evalList = arrayListOf<DeviceInstance>()
private val visited = arrayListOf<DeviceInstance>()
/**
* Builds a simulation sequence: establishes the order in which devices in a graph have to be
* processed so that we arrive at the input signals fed into the terminal.
*/
fun rebuild(terminal: DeviceInstance) {
stack.add(terminal)
evalList.clear()
while (stack.isNotEmpty()) {
val device = stack.removeAt(stack.size - 1)
evalList.add(device)
for ((_, inputPort) in device.inputPortsByName) {
val connectedOutputPort = inputPort.connectedOutput
if (connectedOutputPort != null) {
stack.add(connectedOutputPort.owningDevice)
}
}
}
logger.info("rebuilt device graph; new evaluation order (reversed): $evalList")
}
/** Process all devices in the built sequence. */
fun simulate(sampleCount: Int, channels: Int) {
for (device in evalList.asReversed()) {
device.process(sampleCount, channels)
}
}
}

View file

@ -1,14 +1,18 @@
package net.liquidev.dawd3.audio.device
import net.liquidev.dawd3.audio.AudioBuffer
import net.minecraft.util.Identifier
class InputPort {
sealed class Port {
lateinit var owningDevice: DeviceInstance
}
class InputPort : Port() {
private companion object {
val emptyBuffer = AudioBuffer()
}
var connectedOutput: OutputPort? = null
lateinit var owningDevice: DeviceInstance
/**
* Convenience function that returns an empty buffer if there is no connected port, or the
@ -24,15 +28,76 @@ class InputPort {
}
}
class OutputPort(bufferCount: Int) {
class OutputPort(bufferCount: Int) : Port() {
init {
require(bufferCount >= 1) { "output port must have at least one buffer" }
}
val buffers = Array(bufferCount) { AudioBuffer() }
val connectedInputs = hashSetOf<InputPort>()
lateinit var owningDevice: DeviceInstance
}
/** Marker class that port name markers should inherit from. */
abstract class PortName
enum class PortDirection {
Input,
Output,
}
/** Marker interface that port name markers should inherit from. */
sealed interface PortName {
val id: Identifier
val direction: PortDirection
companion object {
private val registry = hashMapOf<Identifier, PortName>()
internal fun register(name: PortName) {
assert(name.id !in registry) { "there must not be two ports with the same name" }
registry[name.id] = name
}
fun fromString(name: String): PortName? {
return registry[Identifier(name)]
}
internal fun idInDevice(deviceId: Identifier, name: String): Identifier =
Identifier(deviceId.namespace, "${deviceId.path}/$name")
}
}
class InputPortName(override val id: Identifier) : PortName {
override val direction = PortDirection.Input
init {
PortName.register(this)
}
constructor(
parent: Identifier,
name: String,
) : this(PortName.idInDevice(parent, name))
override fun toString(): String = id.toString()
}
class OutputPortName private constructor(
override val id: Identifier,
private val instanceOf: OutputPortName?,
) : PortName {
override val direction = PortDirection.Output
init {
PortName.register(this)
}
constructor(
parent: Identifier,
name: String,
) : this(PortName.idInDevice(parent, name), instanceOf = null)
override fun toString(): String = id.toString()
fun makeInstanced(instanceName: String) =
OutputPortName(PortName.idInDevice(id, instanceName), instanceOf = this)
fun resolveInstance() = instanceOf ?: this
}

View file

@ -1,11 +1,17 @@
package net.liquidev.dawd3.audio.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.Device
import net.liquidev.dawd3.audio.device.DeviceDescriptor
import net.liquidev.dawd3.audio.device.OutputPort
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.audio.device.OutputPortName
import net.minecraft.util.Identifier
class ConstantDevice(var value: Float) : Device {
object Output : PortName()
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "constant")
val outputPort = OutputPortName(id, "output")
}
val output = OutputPort(bufferCount = 1)
@ -16,7 +22,7 @@ class ConstantDevice(var value: Float) : Device {
}
}
override fun visitOutputPorts(visit: (PortName, OutputPort) -> Unit) {
visit(Output, output)
override fun visitOutputPorts(visit: (OutputPortName, OutputPort) -> Unit) {
visit(outputPort, output)
}
}

View file

@ -1,20 +1,22 @@
package net.liquidev.dawd3.audio.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.audio.device.Device
import net.liquidev.dawd3.audio.device.InputPort
import net.liquidev.dawd3.audio.device.OutputPort
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.audio.device.*
import net.minecraft.util.Identifier
import kotlin.math.sin
private const val twoPi = 2.0f * kotlin.math.PI.toFloat()
class SineOscillatorDevice : Device {
object FrequencyCV : PortName()
object Output : PortName()
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "sine_oscillator")
val frequencyCVPort = InputPortName(id, "frequency_cv")
val outputPort = OutputPortName(id, "output")
}
val frequencyCV = InputPort()
val output = OutputPort(bufferCount = 1)
private val frequencyCV = InputPort()
private val output = OutputPort(bufferCount = 1)
private var phase = 0.0f
@ -29,11 +31,11 @@ class SineOscillatorDevice : Device {
}
}
override fun visitInputPorts(visit: (PortName, InputPort) -> Unit) {
visit(FrequencyCV, frequencyCV)
override fun visitInputPorts(visit: (InputPortName, InputPort) -> Unit) {
visit(frequencyCVPort, frequencyCV)
}
override fun visitOutputPorts(visit: (PortName, OutputPort) -> Unit) {
visit(Output, output)
override fun visitOutputPorts(visit: (OutputPortName, OutputPort) -> Unit) {
visit(outputPort, output)
}
}

View file

@ -1,11 +1,17 @@
package net.liquidev.dawd3.audio.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.Device
import net.liquidev.dawd3.audio.device.DeviceDescriptor
import net.liquidev.dawd3.audio.device.InputPort
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.audio.device.InputPortName
import net.minecraft.util.Identifier
class TerminalDevice : Device {
object Input : PortName()
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "terminal")
val inputPort = InputPortName(id, "input")
}
val input = InputPort()
@ -15,7 +21,7 @@ class TerminalDevice : Device {
// speakers.
}
override fun visitInputPorts(visit: (PortName, InputPort) -> Unit) {
visit(Input, input)
override fun visitInputPorts(visit: (InputPortName, InputPort) -> Unit) {
visit(inputPort, input)
}
}

View file

@ -1,25 +1,19 @@
package net.liquidev.dawd3.audio.generator
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.audio.device.Sim
import net.liquidev.dawd3.audio.devices.TerminalDevice
/** Audio generator that evaluates a device graph. */
class DeviceGraphGenerator : AudioGenerator() {
private val sim = Sim()
private val terminalDeviceState = TerminalDevice()
val terminalDevice = DeviceInstance(terminalDeviceState)
/**
* This should be called whenever the device graph changes to reestablish what order devices
* should be processed in.
*/
fun rebuildSim() {
sim.rebuild(terminalDevice)
}
override fun generate(output: FloatArray, sampleCount: Int, channelCount: Int) {
sim.simulate(sampleCount, channelCount)
// TODO: Maybe passing in the static processingState here is not the cleanest way to go
// about things, but I don't see how we could inject that context into this function
// without jumping through significant hoops.
terminalDevice.process(sampleCount, channelCount, Audio.processingState)
val buffer = terminalDeviceState.input.getConnectedOutputBuffer(0, sampleCount)
for (i in 0 until sampleCount) {
output[i] = buffer[i]

View file

@ -0,0 +1,16 @@
package net.liquidev.dawd3.audio.generator
import net.liquidev.dawd3.audio.device.ProcessingState
/**
* Adapter for adding device ProcessingState into an existing generator.
* The processing state is reset with every call to generate().
*/
class GeneratorWithProcessingState(val inner: AudioGenerator) : AudioGenerator() {
val processingState = ProcessingState()
override fun generate(output: FloatArray, sampleCount: Int, channelCount: Int) {
inner.generate(output, sampleCount, channelCount)
processingState.reset()
}
}

View file

@ -11,19 +11,19 @@ class MixGenerator : AudioGenerator() {
val logger = Mod.logger<MixGenerator>()
}
private val taskQueue = TaskQueue()
private val taskQueue = TaskQueue<Unit, Unit>()
private val channels = arrayListOf<WeakReference<Channel<AudioGenerator>>>()
fun <T : AudioGenerator> createChannel(generator: T): Channel<T> {
val channel = Channel(taskQueue, generator)
val weak = WeakReference(channel as Channel<AudioGenerator>)
taskQueue.execute { channels.add(weak) }
taskQueue.enqueue { channels.add(weak) }
return channel
}
override fun generate(output: FloatArray, sampleCount: Int, channelCount: Int) {
// Flush task queue as soon as possible to reduce latency.
taskQueue.flush()
taskQueue.flush(Unit)
reapStoppedChannels()
for (i in 0 until sampleCount) {
@ -61,7 +61,7 @@ class MixGenerator : AudioGenerator() {
}
class Channel<out T : AudioGenerator>(
private val taskQueue: TaskQueue,
private val taskQueue: TaskQueue<Unit, Unit>,
val generator: T,
) {
private companion object {
@ -75,7 +75,7 @@ class MixGenerator : AudioGenerator() {
/** Shuts down the channel on the next audio generation request. */
fun stop() {
taskQueue.execute { playing = false }
taskQueue.enqueue { playing = false }
}
fun finalize() {

View file

@ -1,10 +1,9 @@
package net.liquidev.dawd3.audio.generator
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.audio.unit.Amplitude
import kotlin.math.sin
class SineOscGenerator(frequency: Float, private val amplitude: Amplitude) : AudioGenerator() {
class SineOscGenerator(frequency: Float) : AudioGenerator() {
private val phaseStep = (1.0f / Audio.sampleRate.toFloat()) * frequency
private var phase = 0.0f

View file

@ -1,14 +1,21 @@
package net.liquidev.dawd3.block
import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry
import net.fabricmc.fabric.api.item.v1.FabricItemSettings
import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.fabricmc.fabric.api.`object`.builder.v1.block.entity.FabricBlockEntityTypeBuilder
import net.liquidev.dawd3.D3Registry
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.device.AnyDeviceBlockDescriptor
import net.liquidev.dawd3.block.device.DeviceBlock
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.block.device.DeviceBlockEntityRenderer
import net.liquidev.dawd3.block.devices.FaderBlockDescriptor
import net.liquidev.dawd3.block.devices.SineOscillatorBlockDescriptor
import net.liquidev.dawd3.block.devices.SpeakerBlockDescriptor
import net.liquidev.dawd3.item.Items
import net.minecraft.block.Block
import net.minecraft.block.Material
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.item.BlockItem
import net.minecraft.util.Identifier
@ -43,8 +50,27 @@ object Blocks {
return registeredDeviceBlock
}
// To not have to mess with rendering the vertices ourselves we just use a baked model that's
// represented as a block.
val patchCablePlug = Registry.register(
Registry.BLOCK,
Identifier(Mod.id, "patch_cable_plug"),
Block(FabricBlockSettings.of(Material.METAL))
)
// Device blocks
val speaker = registerDeviceBlock(SpeakerBlockDescriptor)
val sineOscillator = registerDeviceBlock(SineOscillatorBlockDescriptor)
val fader = registerDeviceBlock(FaderBlockDescriptor)
fun initialize() {}
fun initializeClient() {
for ((_, deviceBlock) in deviceBlocks) {
BlockEntityRendererRegistry.register(
deviceBlock.blockEntity,
::DeviceBlockEntityRenderer
)
}
}
}

View file

@ -1,27 +1,70 @@
package net.liquidev.dawd3.block.device
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.item.PatchCableItem
import net.minecraft.block.*
import net.minecraft.block.entity.BlockEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3f
import net.minecraft.util.shape.VoxelShape
import net.minecraft.util.shape.VoxelShapes
import net.minecraft.world.BlockView
import net.minecraft.world.World
class DeviceBlock(private val descriptor: AnyDeviceBlockDescriptor) :
BlockWithEntity(descriptor.blockSettings),
BlockEntityProvider {
val outlineCuboids = HorizontalDirection.values().map { direction ->
val cuboid = descriptor.cuboid
val centerOrigin = Vec3f(0.5f, 0.5f, 0.5f)
val from = cuboid.fromF - centerOrigin
val to = cuboid.toF - centerOrigin
Cuboids.newF(direction.rotateY(from) + centerOrigin, direction.rotateY(to) + centerOrigin)
}
override fun appendProperties(builder: StateManager.Builder<Block, BlockState>) {
builder.add(Properties.HORIZONTAL_FACING)
}
override fun getPlacementState(context: ItemPlacementContext): BlockState =
defaultState.with(Properties.HORIZONTAL_FACING, context.playerFacing.opposite)
defaultState.with(
Properties.HORIZONTAL_FACING,
context.playerFacing.opposite
)
@Deprecated("do not call directly")
@Deprecated("do not call this function directly")
override fun getRenderType(state: BlockState): BlockRenderType =
BlockRenderType.MODEL
override fun createBlockEntity(pos: BlockPos, state: BlockState): BlockEntity =
DeviceBlockEntity.factory(descriptor).create(pos, state)
@Deprecated("do not call this function directly")
override fun onStateReplaced(
state: BlockState,
world: World,
position: BlockPos,
newState: BlockState,
moved: Boolean,
) {
PatchCableItem.onBlockDestroyed(position)
@Suppress("DEPRECATION")
super.onStateReplaced(state, world, position, newState, moved)
}
@Deprecated("do not call this function directly")
override fun getOutlineShape(
state: BlockState,
world: BlockView,
pos: BlockPos,
context: ShapeContext,
): VoxelShape {
val direction = HorizontalDirection.fromDirection(state[Properties.HORIZONTAL_FACING])!!
return VoxelShapes.cuboid(outlineCuboids[direction.index])
}
}

View file

@ -1,16 +1,20 @@
package net.liquidev.dawd3.block.device
import FaceTextures
import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.common.Cuboids
import net.minecraft.block.AbstractBlock
import net.minecraft.block.Material
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Identifier
import net.minecraft.util.math.Box
typealias AnyDeviceBlockDescriptor = DeviceBlockDescriptor<DeviceBlockDescriptor.ClientState, Any>
interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out ServerState> {
val id: Identifier
val blockSettings: AbstractBlock.Settings
// The default block settings are used for metal-enclosed modules.
get() = FabricBlockSettings
@ -18,7 +22,12 @@ interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out
.hardness(5.0f)
.resistance(6.0f)
val portLayout: Array<PhysicalPort>
/** The cuboid that the main part of the block should fill. */
val cuboid: Box
get() = Cuboids.fullBlock
val portLayout: PhysicalPortLayout
val faceTextures: FaceTextures
get() = FaceTextures.withFrontAndSide { id }
fun onClientLoad(world: ClientWorld): CS
fun onClientUnload(state: @UnsafeVariance CS, world: ClientWorld) {}

View file

@ -1,11 +1,20 @@
package net.liquidev.dawd3.block.device
import net.fabricmc.fabric.api.`object`.builder.v1.block.entity.FabricBlockEntityTypeBuilder
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.Devices
import net.liquidev.dawd3.audio.device.InputPortName
import net.liquidev.dawd3.audio.device.OutputPortName
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.block.Blocks
import net.liquidev.dawd3.block.entity.D3BlockEntity
import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.client.world.ClientWorld
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtHelper
import net.minecraft.nbt.NbtList
import net.minecraft.util.math.BlockPos
private typealias DeviceBlockFactory = FabricBlockEntityTypeBuilder.Factory<DeviceBlockEntity>
@ -14,19 +23,64 @@ class DeviceBlockEntity(
type: BlockEntityType<DeviceBlockEntity>,
blockPos: BlockPos,
blockState: BlockState,
private val descriptor: AnyDeviceBlockDescriptor,
val descriptor: AnyDeviceBlockDescriptor,
) : D3BlockEntity(type, blockPos, blockState) {
companion object {
fun factory(descriptor: AnyDeviceBlockDescriptor): DeviceBlockFactory =
DeviceBlockFactory { blockPos, blockState ->
val type by lazy { Blocks.deviceBlocks[descriptor.id]!!.blockEntity }
DeviceBlockEntity(type, blockPos, blockState, descriptor)
}
}
private var clientState: DeviceBlockDescriptor.ClientState? = null
private var serverState: Any? = null
internal data class InputConnection(
val blockPosition: BlockPos,
val outputPortName: OutputPortName,
val color: Byte,
)
internal val inputConnections = hashMapOf<InputPortName, InputConnection>()
override fun readNbt(nbt: NbtCompound) {
super.readNbt(nbt)
val nbtConnections = nbt.getList("connections", NbtElement.COMPOUND_TYPE.toInt())
for (i in 0 until nbtConnections.size) {
val connectionNbt = nbtConnections.getCompound(i)
val outputString = connectionNbt.getString("output")
val output = PortName.fromString(outputString) ?: continue
if (output !is OutputPortName) {
logger.error("NBT declares 'output' field that refers to an input port")
continue
}
val inputString = connectionNbt.getString("input")
val input = PortName.fromString(inputString) ?: continue
if (input !is InputPortName) {
logger.error("NBT declares 'input' field that refers to an output port")
continue
}
val blockPosition = NbtHelper.toBlockPos(connectionNbt.getCompound("position"))
val color = connectionNbt.getByte("color")
inputConnections[input] = InputConnection(blockPosition, outputPortName = output, color)
}
}
override fun writeNbt(nbt: NbtCompound) {
super.writeNbt(nbt)
val connectionsNbt = NbtList()
for ((inputPortName, connection) in inputConnections) {
val connectionNbt = NbtCompound()
connectionNbt.putString("output", connection.outputPortName.toString())
connectionNbt.putString("input", inputPortName.toString())
connectionNbt.put("position", NbtHelper.fromBlockPos(connection.blockPosition))
connectionNbt.putByte("color", connection.color)
connectionsNbt.add(connectionNbt)
}
nbt.put("connections", connectionsNbt)
}
override fun onClientLoad(world: ClientWorld) {
clientState = descriptor.onClientLoad(world)
}
@ -35,6 +89,72 @@ class DeviceBlockEntity(
val clientState = clientState
if (clientState != null) {
descriptor.onClientUnload(clientState, world)
Devices.severAllConnectionsInDevice(clientState.logicalDevice)
}
}
internal fun reapInvalidConnections() {
inputConnections.keys.removeAll {
descriptor.portLayout[it] == null
}
val world = world
if (world != null) {
inputConnections.values.removeAll {
world.getBlockEntity(it.blockPosition) !is DeviceBlockEntity
}
}
}
companion object {
val logger = Mod.logger<DeviceBlockEntity>()
fun factory(descriptor: AnyDeviceBlockDescriptor): DeviceBlockFactory =
DeviceBlockFactory { blockPos, blockState ->
val type by lazy { Blocks.deviceBlocks[descriptor.id]!!.blockEntity }
DeviceBlockEntity(type, blockPos, blockState, descriptor)
}
fun connectLogicalDevices(
fromBlockEntity: DeviceBlockEntity,
fromPort: PortName,
toBlockEntity: DeviceBlockEntity,
toPort: PortName,
cableColor: Byte,
) {
val (outputBlockEntity, outputPort, inputBlockEntity, inputPort) = Devices.sortPortsByInputAndOutput(
fromBlockEntity,
fromPort,
toBlockEntity,
toPort
) ?: throw PortDirectionException("connected ports must be of opposing directions")
val outputDevice = outputBlockEntity.clientState?.logicalDevice ?: return
val inputDevice = inputBlockEntity.clientState?.logicalDevice ?: return
inputBlockEntity.inputConnections[inputPort] =
InputConnection(outputBlockEntity.pos, outputPort, cableColor)
Devices.makeConnection(
outputDevice,
outputPort.resolveInstance(),
inputDevice,
inputPort
)
}
fun connectPhysicalDevices(
fromBlockEntity: DeviceBlockEntity,
fromPort: PortName,
toBlockEntity: DeviceBlockEntity,
toPort: PortName,
cableColor: Byte,
) {
val (outputBlockEntity, outputPort, inputBlockEntity, inputPort) = Devices.sortPortsByInputAndOutput(
fromBlockEntity,
fromPort,
toBlockEntity,
toPort,
) ?: throw PortDirectionException("connected ports must be of opposing directions")
inputBlockEntity.inputConnections[inputPort] =
InputConnection(outputBlockEntity.pos, outputPort, cableColor)
outputBlockEntity.markDirty()
}
}
}

View file

@ -0,0 +1,401 @@
package net.liquidev.dawd3.block.device
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.Blocks
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.datagen.device.DeviceBlockModel
import net.liquidev.dawd3.item.PatchCableItem
import net.minecraft.block.BlockState
import net.minecraft.client.render.*
import net.minecraft.client.render.RenderLayer.MultiPhaseParameters
import net.minecraft.client.render.block.entity.BlockEntityRenderer
import net.minecraft.client.render.block.entity.BlockEntityRendererFactory
import net.minecraft.client.util.SpriteIdentifier
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.screen.PlayerScreenHandler
import net.minecraft.state.property.Properties
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Quaternion
import net.minecraft.util.math.Vec3d
import net.minecraft.util.math.Vec3f
import net.minecraft.world.World
import java.util.function.Function
import kotlin.math.abs
import kotlin.math.floor
class DeviceBlockEntityRenderer(context: BlockEntityRendererFactory.Context) : BlockEntityRenderer<DeviceBlockEntity> {
private companion object {
val logger = Mod.logger<DeviceBlockEntityRenderer>()
val renderLayer: RenderLayer = RenderLayer.of(
"solid",
VertexFormats.POSITION_COLOR_TEXTURE_LIGHT,
VertexFormat.DrawMode.TRIANGLE_STRIP,
2048,
MultiPhaseParameters.builder()
.lightmap(RenderPhase.ENABLE_LIGHTMAP)
.shader(RenderPhase.SOLID_SHADER)
.texture(RenderPhase.BLOCK_ATLAS_TEXTURE)
.cull(RenderPhase.DISABLE_CULLING)
.build(true)
)
val renderLayerFactory = Function<Identifier, RenderLayer> { renderLayer }
const val cableThickness = 0.03f
const val cableSegmentCount = 6
const val cableSag = 0.2f
val cableColorsSprite = SpriteIdentifier(
PlayerScreenHandler.BLOCK_ATLAS_TEXTURE,
Identifier(Mod.id, "device/cable")
)
}
private val blockRenderer = context.renderManager
private val patchCablePlug = Blocks.patchCablePlug.defaultState
override fun rendersOutsideBoundingBox(blockEntity: DeviceBlockEntity): Boolean {
// TODO: This needs to return true if the block entity has cables attached.
return true
}
override fun render(
blockEntity: DeviceBlockEntity,
tickDelta: Float,
matrixStack: MatrixStack,
vertexConsumers: VertexConsumerProvider,
light: Int,
overlay: Int,
) {
val world = blockEntity.world ?: return
val blockState = world.getBlockState(blockEntity.pos)
if (blockState.block !is DeviceBlock) {
// render() gets called for a frame while the block is still air after it's destroyed.
return
}
renderPlugsInsidePorts(
world,
blockState,
blockEntity,
matrixStack,
vertexConsumers,
overlay
)
}
private fun rotateToFaceFront(
blockState: BlockState,
matrices: MatrixStack,
) {
// Choose center as the transform origin. That way we can rotate by kπ/2 to get all the
// possible horizontal orientations of the block.
matrices.translate(0.5, 0.0, 0.5)
val facing = HorizontalDirection.fromDirection(blockState[Properties.HORIZONTAL_FACING])!!
matrices.multiply(Quaternion.fromEulerXyz(0f, -facing.angle, 0f))
}
private fun renderPlugsInsidePorts(
world: World,
blockState: BlockState,
blockEntity: DeviceBlockEntity,
matrixStack: MatrixStack,
vertexConsumers: VertexConsumerProvider,
overlay: Int,
) {
val facing = HorizontalDirection.fromDirection(blockState[Properties.HORIZONTAL_FACING])!!
matrixStack.push()
rotateToFaceFront(blockState, matrixStack)
for ((_, connection) in PatchCableItem.ongoingConnections) {
if (connection.blockPosition == blockEntity.pos) {
val port = blockEntity.descriptor.portLayout[connection.portName]
if (port != null) {
renderPlugInsidePort(
world, blockEntity, facing, port, matrixStack, vertexConsumers, overlay
)
} else {
logger.error("ongoing connection references port name '${connection.portName.id}' which does not exist on the block entity")
}
}
}
// TODO: There's probably a better place to call this.
// Maybe we should actually not call this at all here?
// I'm worried that for a frame a device could be something other than DeviceBlockEntity
// and we'll get plugs without cables rendering permanently. Which would be bad.
blockEntity.reapInvalidConnections()
for ((portName, _) in blockEntity.inputConnections) {
val inputPort = blockEntity.descriptor.portLayout[portName]!!
renderPlugInsidePort(
world, blockEntity, facing, inputPort, matrixStack, vertexConsumers, overlay
)
}
matrixStack.pop()
for ((_, connection) in blockEntity.inputConnections) {
val outputBlockEntity =
world.getBlockEntity(connection.blockPosition) as DeviceBlockEntity
val outputBlockState = world.getBlockState(connection.blockPosition)
val outputPort = outputBlockEntity.descriptor.portLayout[connection.outputPortName]
if (outputPort != null) {
val delta = (outputBlockEntity.pos - blockEntity.pos).toVec3d()
matrixStack.push()
matrixStack.translate(delta.x, delta.y, delta.z)
rotateToFaceFront(outputBlockState, matrixStack)
renderPlugInsidePort(
world,
outputBlockEntity,
facing,
outputPort,
matrixStack,
vertexConsumers,
overlay
)
matrixStack.pop()
}
}
for ((inputPortName, connection) in blockEntity.inputConnections) {
val startAbsolute = blockEntity.pos.toVec3d()
val endAbsolute = connection.blockPosition.toVec3d()
// TODO: Null checks here?
val inputPort = blockEntity.descriptor.portLayout[inputPortName]!!
val outputBlockState = world.getBlockState(connection.blockPosition)
// TODO: The line below will crash if the block is destroyed (becomes air)
// Though it never crashed for me during testing?
val outputBlockFacing =
HorizontalDirection.fromDirection(outputBlockState[Properties.HORIZONTAL_FACING])!!
val outputBlockEntity =
world.getBlockEntity(connection.blockPosition) as DeviceBlockEntity
val outputPort = outputBlockEntity.descriptor.portLayout[connection.outputPortName]!!
val protrusionAmount = 0.69f // nice
val start = inputPort.blockCablePosition(facing, protrusionAmount)
val end = outputPort.blockCablePosition(outputBlockFacing, protrusionAmount)
val delta = (endAbsolute - startAbsolute).toVec3f() + end
renderCable(
world,
matrixStack,
vertexConsumers,
worldFrom = blockEntity.pos.toVec3d(),
start,
delta,
connection.color.toFloat()
)
}
}
private fun getFacingInWorldSpace(
blockFacing: HorizontalDirection,
portFacing: HorizontalDirection,
) =
blockFacing + portFacing - HorizontalDirection.North
private fun renderPlugInsidePort(
world: World,
blockEntity: DeviceBlockEntity,
blockFacing: HorizontalDirection,
port: PhysicalPort,
matrices: MatrixStack,
vertexConsumers: VertexConsumerProvider,
overlay: Int,
) {
val positionOnSide =
(DeviceBlockModel.relativeToAbsolutePortPosition(port.position) + DeviceBlockModel.portSize / 2f) / 16f
val face = port.side.face
matrices.push()
matrices.multiply(
Quaternion.fromEulerXyz(0f, face.angle, 0f)
)
matrices.translate(1f - positionOnSide.x.toDouble(), 1f - positionOnSide.y.toDouble(), 0.0)
val facing = getFacingInWorldSpace(blockFacing, face)
val light =
WorldRenderer.getLightmapCoordinates(world, blockEntity.pos.add(facing.vector))
blockRenderer.renderBlockAsEntity(
patchCablePlug, matrices, vertexConsumers, light, overlay
)
matrices.pop()
}
private fun catenary(sag: Float, t: Float): Float {
// Not an actual catenary, but close enough.
val u = 2f * t - 1f
return sag * (u * u - 1f)
}
private fun renderCable(
world: World,
matrixStack: MatrixStack,
vertexConsumers: VertexConsumerProvider,
worldFrom: Vec3d,
from: Vec3f,
to: Vec3f,
colorIndex: Float,
) {
val forward = to - from
val right = forward.copy()
right.cross(Vec3f.POSITIVE_Y)
right.normalize()
right.multiplyComponentwise(cableThickness, cableThickness, cableThickness)
val up = right.copy()
up.cross(forward)
up.normalize()
up.multiplyComponentwise(cableThickness, cableThickness, cableThickness)
renderCableWithThicknessVector(
world,
matrixStack,
vertexConsumers,
worldFrom,
from,
to,
thicknessVector = up,
colorIndex
)
renderCableWithThicknessVector(
world,
matrixStack,
vertexConsumers,
worldFrom,
from,
to,
thicknessVector = right,
colorIndex
)
}
private fun renderCableWithThicknessVector(
world: World,
matrixStack: MatrixStack,
vertexConsumers: VertexConsumerProvider,
worldFrom: Vec3d,
from: Vec3f,
to: Vec3f,
thicknessVector: Vec3f,
colorIndex: Float,
) {
val vertexBuffer = cableColorsSprite.getVertexConsumer(
vertexConsumers,
renderLayerFactory
) as SpriteTexturedVertexConsumer
val matrices = matrixStack.peek()
// ↑ Cast needed due to different interface method resolution rules in Kotlin.
// See comment in addCableSegment.
val uLeft = colorIndex / 16f
val uRight = (colorIndex + 1) / 16f
addCableSegment(
world,
matrices,
vertexBuffer,
worldFrom.x,
worldFrom.y,
worldFrom.z,
from.x,
from.y,
from.z,
thicknessVector,
uLeft,
uRight,
v = 0f,
)
val sag = cableSag + abs(to.y - from.y) * 0.2f
for (i in 1 until cableSegmentCount) {
val t = i.toFloat() / cableSegmentCount.toFloat()
val sagOffset = catenary(sag, t)
val x = lerp(from.x, to.x, t)
val y = lerp(from.y, to.y, t) + sagOffset
val z = lerp(from.z, to.z, t)
addCableSegment(
world,
matrices,
vertexBuffer,
worldFrom.x,
worldFrom.y,
worldFrom.z,
x,
y,
z,
thicknessVector,
uLeft,
uRight,
v = t,
)
}
addCableSegment(
world,
matrices,
vertexBuffer,
worldFrom.x,
worldFrom.y,
worldFrom.z,
to.x,
to.y,
to.z,
thicknessVector,
uLeft,
uRight,
v = 1f,
)
}
private fun addCableSegment(
world: World,
matrices: MatrixStack.Entry,
vertexBuffer: SpriteTexturedVertexConsumer,
originX: Double,
originY: Double,
originZ: Double,
x: Float,
y: Float,
z: Float,
thickness: Vec3f,
uLeft: Float,
uRight: Float,
v: Float,
) {
val blockPosition = BlockPos(
floor(originX + x.toDouble()).toInt(),
floor(originY + y.toDouble()).toInt(),
floor(originZ + z.toDouble()).toInt(),
)
val light =
WorldRenderer.getLightmapCoordinates(world, blockPosition)
// NOTE: Unlike in Java, where we can chain these method calls, in Kotlin we have to
// specify the receiver explicitly for each one. This is because Kotlin resolves interface
// methods a little differently: instead of always resolving dynamically, it will instead
// always resolve default methods statically. Which is not what we want here, because that
// gives us bad UVs.
vertexBuffer.vertex(
matrices.positionMatrix,
x - thickness.x,
y - thickness.y,
z - thickness.z,
)
vertexBuffer.color(255, 255, 255, 255)
vertexBuffer.texture(uLeft, v)
vertexBuffer.light(light)
vertexBuffer.next()
vertexBuffer.vertex(
matrices.positionMatrix,
x + thickness.x,
y + thickness.y,
z + thickness.z,
)
vertexBuffer.color(255, 255, 255, 255)
vertexBuffer.texture(uRight, v)
vertexBuffer.light(light)
vertexBuffer.next()
}
}

View file

@ -1,31 +1,82 @@
package net.liquidev.dawd3.block.device
import net.liquidev.dawd3.audio.device.OutputPortName
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.common.Affine2x2
import net.minecraft.util.math.Direction
import net.liquidev.dawd3.common.Affine2x2f
import net.liquidev.dawd3.common.HorizontalDirection
import net.liquidev.dawd3.common.div
import net.liquidev.dawd3.common.plus
import net.liquidev.dawd3.datagen.device.DeviceBlockModel
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
/** The physical appearance of a port. */
data class PhysicalPort(
val port: PortName,
/** The coordinates are relative, in range [0.0, 1.0] where 0 is top-left and 1 is bottom-right. */
val position: Vec2f,
val side: Side,
val logicalPort: PortName,
) {
/** Where to place the port on the model. */
enum class Side(
/** The transform matrix to apply to ports' coordinates in the model. */
val transform: Affine2x2,
val modelTransform: Affine2x2f,
/** The model face on which the port is placed. */
val face: Direction,
val face: HorizontalDirection,
) {
Front(
transform = Affine2x2(1f, 0f, 0f, 1f),
face = Direction.NORTH,
modelTransform = Affine2x2f(1f, 0f, 0f, 1f),
face = HorizontalDirection.North,
),
Back(
transform = Affine2x2(-1f, 0f, 0f, -1f, translateX = 16f, translateY = 16f),
face = Direction.SOUTH,
modelTransform = Affine2x2f(-1f, 0f, 0f, -1f, translateX = 16f, translateY = 16f),
face = HorizontalDirection.South,
),
}
/** Where the port's patch cable cord is located relative to a block's center. */
fun blockCablePosition(
blockFacing: HorizontalDirection,
protrusionAmount: Float,
): Vec3f {
val facing = (blockFacing + side.face).clockwise()
val absolutePosition =
DeviceBlockModel.relativeToAbsolutePortPosition(position) + DeviceBlockModel.portSize / 2f
val invertedPosition = Vec2f(1f - absolutePosition.x / 16f, 1f - absolutePosition.y / 16f)
val forward = Vec3f(protrusionAmount, invertedPosition.y - 0.5f, invertedPosition.x - 0.5f)
return facing.rotateY(forward) + Vec3f(0.5f, 0.5f, 0.5f)
}
companion object {
class LayoutBuilder {
internal val layout = PhysicalPortLayout()
fun port(portName: PortName, physicalPort: PhysicalPort) {
layout[portName] = physicalPort
}
fun port(portName: PortName, position: Vec2f, side: Side) {
port(portName, PhysicalPort(position, side, portName))
}
/**
* Can be used to create physical ports whose name is different from the logical
* port name, to allow for aliasing multiple physical ports onto a single logical port.
*
* Note that this is only allowed for output ports, because input ports cannot have
* multiple signals going into them.
*/
fun port(portName: OutputPortName, instanceName: String, position: Vec2f, side: Side) {
port(portName.makeInstanced(instanceName), PhysicalPort(position, side, portName))
}
}
fun layout(build: LayoutBuilder.() -> Unit): PhysicalPortLayout {
val builder = LayoutBuilder()
build(builder)
return builder.layout
}
}
}
typealias PhysicalPortLayout = HashMap<PortName, PhysicalPort>

View file

@ -0,0 +1,40 @@
import net.minecraft.util.Identifier
interface FaceTextures {
val front: Identifier
val back: Identifier
val left: Identifier
val right: Identifier
val top: Identifier
val bottom: Identifier
val particle: Identifier
companion object {
fun withFrontAndSide(lazyId: () -> Identifier): FaceTextures = object : FaceTextures {
private val id get() = lazyId()
private val side = Identifier(id.namespace, "block/${id.path}_side")
override val front = Identifier(id.namespace, "block/${id.path}_front")
override val back = side
override val left = side
override val right = side
override val top = side
override val bottom = side
override val particle = side
}
fun withTopSideAndBottom(lazyId: () -> Identifier): FaceTextures = object : FaceTextures {
private val id get() = lazyId()
private val side = Identifier(id.namespace, "block/${id.path}_side")
override val front = side
override val back = side
override val left = side
override val right = side
override val top = Identifier(id.namespace, "block/${id.path}_top")
override val bottom = Identifier(id.namespace, "block/${id.path}_bottom")
override val particle = side
}
}
}

View file

@ -0,0 +1,3 @@
package net.liquidev.dawd3.block.device
class PortDirectionException(what: String) : Exception(what)

View file

@ -0,0 +1,33 @@
package net.liquidev.dawd3.block.devices
import FaceTextures
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.audio.devices.ConstantDevice
import net.liquidev.dawd3.block.device.DeviceBlockDescriptor
import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.common.Cuboids
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec2f
object FaderBlockDescriptor : DeviceBlockDescriptor<FaderBlockDescriptor.ClientState, Unit> {
override val id = Identifier(Mod.id, "fader")
override val cuboid = Cuboids.halfBlock
override val portLayout = PhysicalPort.layout {
port(
ConstantDevice.outputPort,
instanceName = "front",
position = Vec2f(0.5f, 0.75f),
side = PhysicalPort.Side.Front,
)
}
override val faceTextures = FaceTextures.withTopSideAndBottom { id }
class ClientState : DeviceBlockDescriptor.ClientState {
override val logicalDevice = DeviceInstance(ConstantDevice(value = 440.0f))
}
override fun onClientLoad(world: ClientWorld) = ClientState()
}

View file

@ -0,0 +1,33 @@
package net.liquidev.dawd3.block.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.audio.devices.SineOscillatorDevice
import net.liquidev.dawd3.block.device.DeviceBlockDescriptor
import net.liquidev.dawd3.block.device.PhysicalPort
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec2f
object SineOscillatorBlockDescriptor : DeviceBlockDescriptor<SineOscillatorBlockDescriptor.ClientState, Unit> {
override val id = Identifier(Mod.id, "sine_oscillator")
override val portLayout = PhysicalPort.layout {
port(
SineOscillatorDevice.frequencyCVPort,
position = Vec2f(0.25f, 0.5f),
side = PhysicalPort.Side.Front,
)
port(
SineOscillatorDevice.outputPort,
position = Vec2f(0.75f, 0.5f),
side = PhysicalPort.Side.Front
)
}
class ClientState : DeviceBlockDescriptor.ClientState {
override val logicalDevice = DeviceInstance(SineOscillatorDevice())
}
override fun onClientLoad(world: ClientWorld) = ClientState()
}

View file

@ -22,9 +22,9 @@ object SpeakerBlockDescriptor : DeviceBlockDescriptor<SpeakerBlockDescriptor.Cli
.hardness(2.0f).resistance(6.0f)
.sounds(BlockSoundGroup.WOOD)!!
override val portLayout = arrayOf(
PhysicalPort(TerminalDevice.Input, Vec2f(0.5f, 0.5f), PhysicalPort.Side.Back)
)
override val portLayout = PhysicalPort.layout {
port(TerminalDevice.inputPort, position = Vec2f(0.5f, 0.75f), side = PhysicalPort.Side.Back)
}
class ClientState : DeviceBlockDescriptor.ClientState {
internal val channel: MixGenerator.Channel<DeviceGraphGenerator>
@ -33,15 +33,12 @@ object SpeakerBlockDescriptor : DeviceBlockDescriptor<SpeakerBlockDescriptor.Cli
init {
val generator = DeviceGraphGenerator()
channel = Audio.mixer.createChannel(generator)
val terminal = generator.terminalDevice
logicalDevice = terminal
generator.rebuildSim()
}
}
override fun onClientLoad(world: ClientWorld): ClientState = ClientState()
override fun onClientLoad(world: ClientWorld) = ClientState()
override fun onClientUnload(state: ClientState, world: ClientWorld) {
state.channel.stop()

View file

@ -4,15 +4,26 @@ import net.minecraft.block.BlockState
import net.minecraft.block.entity.BlockEntity
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.client.world.ClientWorld
import net.minecraft.nbt.NbtCompound
import net.minecraft.network.Packet
import net.minecraft.network.listener.ClientPlayPacketListener
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket
import net.minecraft.util.math.BlockPos
abstract class D3BlockEntity(
type: BlockEntityType<out D3BlockEntity>,
pos: BlockPos,
state: BlockState,
) :
BlockEntity(type, pos, state) {
) : BlockEntity(type, pos, state) {
open fun onClientLoad(world: ClientWorld) {}
open fun onClientUnload(world: ClientWorld) {}
override fun toUpdatePacket(): Packet<ClientPlayPacketListener> {
return BlockEntityUpdateS2CPacket.create(this)
}
override fun toInitialChunkDataNbt(): NbtCompound {
return createNbt()
}
}

View file

@ -2,7 +2,7 @@ package net.liquidev.dawd3.block.entity
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientBlockEntityEvents
fun registerBlockEntityEvents() {
fun registerClientBlockEntityEvents() {
ClientBlockEntityEvents.BLOCK_ENTITY_LOAD.register { blockEntity, world ->
if (blockEntity is D3BlockEntity) {
blockEntity.onClientLoad(world)

View file

@ -4,7 +4,7 @@ import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
/** 2x2 matrix. */
data class Affine2x2(
data class Affine2x2f(
val xx: Float,
val xy: Float,
val yx: Float,

View file

@ -0,0 +1,20 @@
package net.liquidev.dawd3.common
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
/** Matrix with two rows and three columns. */
class Mat2x3f(
val xx: Float,
val xy: Float,
val xz: Float,
val yx: Float,
val yy: Float,
val yz: Float,
) {
operator fun times(vec: Vec3f) =
Vec2f(
vec.x * xx + vec.y * xy + vec.z * xz,
vec.x * yx + vec.y * yy + vec.z * yz
)
}

View file

@ -0,0 +1,21 @@
package net.liquidev.dawd3.common
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
/** Matrix with three columns and two rows. */
class Mat3x2f(
val xx: Float,
val xy: Float,
val yx: Float,
val yy: Float,
val zx: Float,
val zy: Float,
) {
operator fun times(vec: Vec2f): Vec3f =
Vec3f(
vec.x * xx + vec.y * xy,
vec.x * yx + vec.y * yy,
vec.x * zx + vec.y * zy,
)
}

View file

@ -1,17 +1,17 @@
package net.liquidev.dawd3.common
import java.util.concurrent.Executor
import java.util.function.Function
class TaskQueue : Executor {
private val tasks = arrayListOf<Runnable>()
class TaskQueue<T, R> {
private val tasks = arrayListOf<Function<T, R>>()
override fun execute(task: Runnable) {
fun enqueue(task: Function<T, R>) {
tasks.add(task)
}
fun flush() {
fun flush(argument: T) {
for (task in tasks) {
task.run()
task.apply(argument)
}
tasks.clear()
}

View file

@ -1,14 +1,147 @@
package net.liquidev.dawd3.common
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
import net.minecraft.util.math.*
import kotlin.math.PI
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
operator fun Vec2f.plus(other: Vec2f): Vec2f = Vec2f(x + other.x, y + other.y)
operator fun Vec2f.minus(other: Vec2f): Vec2f = Vec2f(x - other.x, y - other.y)
operator fun Vec2f.times(other: Vec2f): Vec2f = Vec2f(x * other.x, y * other.y)
operator fun Vec2f.div(other: Vec2f): Vec2f = Vec2f(x / other.x, y / other.y)
operator fun Vec2f.div(other: Float): Vec2f = Vec2f(x / other, y / other)
fun Vec2f.format() = "($x, $y)"
operator fun Vec3f.plus(other: Vec3f): Vec3f = Vec3f(x + other.x, y + other.y, z + other.z)
operator fun Vec3f.minus(other: Vec3f): Vec3f = Vec3f(x - other.x, y - other.y, z - other.z)
operator fun Vec3f.times(other: Vec3f): Vec3f = Vec3f(x * other.x, y * other.y, z * other.z)
operator fun Vec3f.times(other: Float): Vec3f = Vec3f(x * other, y * other, z * other)
operator fun Vec3f.div(other: Vec3f): Vec3f = Vec3f(x / other.x, y / other.y, z / other.z)
fun Vec3f.min(other: Vec3f): Vec3f = Vec3f(min(x, other.x), min(y, other.y), min(z, other.z))
fun Vec3f.max(other: Vec3f): Vec3f = Vec3f(max(x, other.x), max(y, other.y), max(z, other.z))
val Vec3f.lengthSquared get() = x * x + y * y + z * z
val Vec3f.length get() = sqrt(lengthSquared)
fun Vec3f.normalize() {
val length = length
if (length != 0f) {
set(x / length, y / length, z / length)
} else {
set(0f, 0f, 0f)
}
}
operator fun Vec3d.plus(other: Vec3d): Vec3d = Vec3d(x + other.x, y + other.y, z + other.z)
operator fun Vec3d.minus(other: Vec3d): Vec3d = Vec3d(x - other.x, y - other.y, z - other.z)
operator fun Vec3d.times(other: Vec3d): Vec3d = Vec3d(x * other.x, y * other.y, z * other.z)
operator fun Vec3d.div(other: Vec3d): Vec3d = Vec3d(x / other.x, y / other.y, z / other.z)
operator fun BlockPos.plus(other: BlockPos): BlockPos =
BlockPos(x + other.x, y + other.y, z + other.z)
operator fun BlockPos.minus(other: BlockPos): BlockPos =
BlockPos(x - other.x, y - other.y, z - other.z)
private object PlaneMatrices3Dto2D {
val xyPlane = Mat2x3f(1f, 0f, 0f, 0f, 1f, 0f)
val zyPlane = Mat2x3f(0f, 0f, 1f, 0f, 1f, 0f)
val xzPlane = Mat2x3f(1f, 0f, 0f, 0f, 0f, 1f)
}
val Direction.to2DPlane
get() =
when (this) {
Direction.DOWN -> PlaneMatrices3Dto2D.xzPlane
Direction.UP -> PlaneMatrices3Dto2D.xzPlane
Direction.NORTH -> PlaneMatrices3Dto2D.xyPlane
Direction.SOUTH -> PlaneMatrices3Dto2D.xyPlane
Direction.WEST -> PlaneMatrices3Dto2D.zyPlane
Direction.EAST -> PlaneMatrices3Dto2D.zyPlane
}
private object CorrectionMatrices {
val invertXandY = Affine2x2f(-1f, 0f, 0f, -1f, translateX = 1f, translateY = 1f)
val invertYOnly = Affine2x2f(1f, 0f, 0f, -1f, translateX = 0f, translateY = 1f)
}
enum class HorizontalDirection(
val index: Int,
val x: Int,
val z: Int,
val angle: Float,
val direction: Direction,
) {
East(0, x = 1, z = 0, angle = 0f, direction = Direction.EAST),
South(1, x = 0, z = 1, angle = 0.5f * PI.toFloat(), direction = Direction.SOUTH),
West(2, x = -1, z = 0, angle = PI.toFloat(), direction = Direction.WEST),
North(3, x = 0, z = -1, angle = 1.5f * PI.toFloat(), direction = Direction.NORTH);
operator fun plus(other: HorizontalDirection): HorizontalDirection =
fromIndex((index + other.index) % values.size)
operator fun minus(other: HorizontalDirection): HorizontalDirection =
fromIndex((index - other.index).mod(values.size))
fun clockwise() = plus(South)
fun counterClockwise() = minus(South)
fun rotateY(vec: Vec3f): Vec3f =
when (this) {
East -> vec
North -> Vec3f(vec.z, vec.y, -vec.x)
West -> Vec3f(-vec.x, vec.y, -vec.z)
South -> Vec3f(-vec.z, vec.y, vec.x)
}
val faceCorrection
get() =
when (this) {
East -> CorrectionMatrices.invertXandY
South -> CorrectionMatrices.invertYOnly
West -> CorrectionMatrices.invertYOnly
North -> CorrectionMatrices.invertXandY
}
val vector get() = Vec3i(x, 0, z)
companion object {
private val values = values()
fun fromIndex(i: Int) = values[i]
fun fromDirection(direction: Direction): HorizontalDirection? =
when (direction) {
Direction.EAST -> East
Direction.SOUTH -> South
Direction.WEST -> West
Direction.NORTH -> North
else -> null
}
}
}
fun Vec3d.toVec3f() = Vec3f(x.toFloat(), y.toFloat(), z.toFloat())
fun Vec3f.toVec3d() = Vec3d(x.toDouble(), y.toDouble(), z.toDouble())
fun Vec3i.toVec3f() = Vec3f(x.toFloat(), y.toFloat(), z.toFloat())
fun BlockPos.toVec3d() = Vec3d(x.toDouble(), y.toDouble(), z.toDouble())
fun pointInRectangle(point: Vec2f, topLeft: Vec2f, bottomRight: Vec2f) =
point.x >= topLeft.x && point.y >= topLeft.y &&
point.x <= bottomRight.x && point.y <= bottomRight.y
fun lerp(a: Float, b: Float, t: Float): Float =
a + t * (b - a)
object Cuboids {
val fullBlock = Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)
val halfBlock = Box(0.0, 0.0, 0.0, 1.0, 0.5, 1.0)
fun newF(from: Vec3f, to: Vec3f) = Box(from.toVec3d(), to.toVec3d())
}
val Box.fromF get() = Vec3f(minX.toFloat(), minY.toFloat(), minZ.toFloat())
val Box.toF get() = Vec3f(maxX.toFloat(), maxY.toFloat(), maxZ.toFloat())

View file

@ -0,0 +1,10 @@
package net.liquidev.dawd3.common
import net.minecraft.block.Block
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
fun World.syncBlockToClients(position: BlockPos) {
val state = getBlockState(position)
updateListeners(position, state, state, Block.NOTIFY_LISTENERS)
}

View file

@ -5,6 +5,8 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.Blocks
import net.liquidev.dawd3.datagen.device.DeviceBlockModel
import net.liquidev.dawd3.item.BasicItem
import net.liquidev.dawd3.item.Items
import net.minecraft.data.client.*
import net.minecraft.util.Identifier
import java.util.*
@ -19,7 +21,9 @@ class ModelDatagen(generator: FabricDataGenerator) : FabricModelProvider(generat
for ((id, deviceBlock) in Blocks.deviceBlocks) {
val modelId = Identifier(Mod.id, "block/${id.path}")
generator.modelCollector.accept(modelId) { DeviceBlockModel.generate(id, deviceBlock) }
generator.modelCollector.accept(modelId) {
DeviceBlockModel.generate(deviceBlock)
}
generator.blockStateCollector.accept(horizontallyRotatableBlockState(id, deviceBlock))
}
}
@ -36,14 +40,21 @@ class ModelDatagen(generator: FabricDataGenerator) : FabricModelProvider(generat
.coordinate(BlockStateModelGenerator.createNorthDefaultHorizontalRotationStates())
}
override fun generateItemModels(generator: ItemModelGenerator) {
logger.info("generating item models")
logger.info("generating block item models")
for ((id, deviceBlock) in Blocks.deviceBlocks) {
generator.register(
deviceBlock.item.item.item,
Model(Optional.of(Identifier(Mod.id, "block/${id.path}")), Optional.empty())
)
}
logger.info("generating other item models")
for (registered in Items.registry.registered) {
val item = registered.item.item
if (item is BasicItem) {
generator.register(registered.item.item, Models.GENERATED)
}
}
}
}

View file

@ -2,6 +2,8 @@ package net.liquidev.dawd3.datagen.device
import com.google.gson.JsonObject
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.PortDirection
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.block.Blocks
import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.common.*
@ -11,86 +13,87 @@ import net.liquidev.dawd3.datagen.json.faces
import net.liquidev.dawd3.datagen.json.uvRect
import net.liquidev.dawd3.datagen.jsonArray
import net.liquidev.dawd3.datagen.jsonObject
import net.minecraft.util.Identifier
import net.minecraft.util.math.Box
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3f
object DeviceBlockModel {
private val portSize = Vec2f(3f, 3f)
val portSize = Vec2f(4f, 4f)
private const val portTexture = "${Mod.id}:device/port"
private fun getSideTexture(id: Identifier) =
"${Mod.id}:block/${id.path}_side"
private fun getFrontTexture(id: Identifier) =
"${Mod.id}:block/${id.path}_front"
fun generate(
id: Identifier,
deviceBlock: Blocks.RegisteredDeviceBlock,
): JsonObject {
fun generate(deviceBlock: Blocks.RegisteredDeviceBlock): JsonObject {
val faceTextures = deviceBlock.descriptor.faceTextures
return jsonObject {
add("parent", "block/block")
add("textures", jsonObject {
add("side", getSideTexture(id))
add("front", getFrontTexture(id))
add("front", faceTextures.front.toString())
add("back", faceTextures.back.toString())
add("right", faceTextures.right.toString())
add("left", faceTextures.left.toString())
add("top", faceTextures.top.toString())
add("bottom", faceTextures.bottom.toString())
add("particle", faceTextures.particle.toString())
add("port", portTexture)
add("particle", "#side")
})
add("elements", jsonArray {
add(blockElement)
add(getBlockElement(deviceBlock.descriptor.cuboid))
val descriptor = deviceBlock.descriptor
for (port in descriptor.portLayout) {
add(getPortElement(port))
for ((portName, port) in descriptor.portLayout) {
add(getPortElement(portName, port))
}
})
}
}
private val blockElement = element(
from = Vec3f(0f, 0f, 0f),
to = Vec3f(16f, 16f, 16f),
private fun getBlockElement(cuboid: Box) = element(
from = Vec3f(cuboid.minX.toFloat(), cuboid.minY.toFloat(), cuboid.minZ.toFloat()) * 16f,
to = Vec3f(cuboid.maxX.toFloat(), cuboid.maxY.toFloat(), cuboid.maxZ.toFloat()) * 16f,
faces = faces(
north = face(texture = "#front", cullface = "north"),
east = face(texture = "#side", cullface = "east"),
south = face(texture = "#side", cullface = "south"),
west = face(texture = "#side", cullface = "west"),
up = face(texture = "#side", cullface = "up"),
down = face(texture = "#side", cullface = "down"),
east = face(texture = "#right", cullface = "east"),
south = face(texture = "#back", cullface = "south"),
west = face(texture = "#left", cullface = "west"),
up = face(texture = "#top", cullface = "up"),
down = face(texture = "#bottom", cullface = "down"),
)
)
private val portPlayArea = Vec2f(16f, 16f) - portSize
private fun relativeToAbsolutePortPosition(position: Vec2f): Vec2f {
val inv = Vec2f(1f - position.x, 1f - position.y)
return inv * portPlayArea
fun relativeToAbsolutePortPosition(position: Vec2f): Vec2f {
return position * portPlayArea
}
private fun getPortElement(port: PhysicalPort): JsonObject {
val bottomLeft2D = relativeToAbsolutePortPosition(port.position)
private fun getPortElement(portName: PortName, port: PhysicalPort): JsonObject {
val bottomLeft2D =
relativeToAbsolutePortPosition(Vec2f(1f - port.position.x, 1f - port.position.y))
val topRight2D = bottomLeft2D + portSize
val bottomLeft = Vec3f(bottomLeft2D.x, bottomLeft2D.y, -0.01f)
val topRight = Vec3f(topRight2D.x, topRight2D.y, 0.01f)
val rotatedBottomLeft = port.side.transform.timesXZ(bottomLeft)
val rotatedTopRight = port.side.transform.timesXZ(topRight)
val rotatedBottomLeft = port.side.modelTransform.timesXZ(bottomLeft)
val rotatedTopRight = port.side.modelTransform.timesXZ(topRight)
val min = rotatedBottomLeft.min(rotatedTopRight)
val max = rotatedBottomLeft.max(rotatedTopRight)
val uvX = when (portName.direction) {
PortDirection.Input -> 0f
PortDirection.Output -> 4f
}
return element(
from = min,
to = max,
faces = jsonObject {
val faceName = port.side.face.getName()
val faceName = port.side.face.direction.getName()
add(
faceName, face(
texture = "#port",
cullface = faceName,
uv = uvRect(0f, 0f, 4f, 4f)
uv = uvRect(uvX, 0f, uvX + 4f, 4f)
)
)
}

View file

@ -0,0 +1,6 @@
package net.liquidev.dawd3.item
import net.minecraft.item.Item
/** Parent class for telling apart block items from other items. */
abstract class BasicItem(settings: Settings) : Item(settings)

View file

@ -36,13 +36,32 @@ object Items {
)
// Tools
val patchCable =
addItem("patch_cable", PatchCable(FabricItemSettings().group(ItemGroup.REDSTONE)))
private fun coloredPatchCable(name: String, color: Byte) =
addItem(name, PatchCableItem(FabricItemSettings().group(ItemGroup.REDSTONE), color))
val patchCables = arrayOf(
coloredPatchCable("white_patch_cable", color = 0),
coloredPatchCable("orange_patch_cable", color = 1),
coloredPatchCable("magenta_patch_cable", color = 2),
coloredPatchCable("light_blue_patch_cable", color = 3),
coloredPatchCable("yellow_patch_cable", color = 4),
coloredPatchCable("lime_patch_cable", color = 5),
coloredPatchCable("pink_patch_cable", color = 6),
coloredPatchCable("gray_patch_cable", color = 7),
coloredPatchCable("light_gray_patch_cable", color = 8),
coloredPatchCable("cyan_patch_cable", color = 9),
coloredPatchCable("purple_patch_cable", color = 10),
coloredPatchCable("blue_patch_cable", color = 11),
coloredPatchCable("brown_patch_cable", color = 12),
coloredPatchCable("green_patch_cable", color = 13),
coloredPatchCable("red_patch_cable", color = 14),
coloredPatchCable("black_patch_cable", color = 15),
)
fun addItem(name: Identifier, item: Item): D3Registry.Registered<RegisteredItem> =
registry.add(name, RegisteredItem(item))
fun addItem(name: String, item: Item) =
private fun addItem(name: String, item: Item) =
addItem(Identifier(Mod.id, name), item)
data class RegisteredItem(

View file

@ -1,16 +0,0 @@
package net.liquidev.dawd3.item
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.sound.SoundEvents
import net.minecraft.util.Hand
import net.minecraft.util.TypedActionResult
import net.minecraft.world.World
class PatchCable(settings: Settings) : Item(settings) {
override fun use(world: World, user: PlayerEntity, hand: Hand): TypedActionResult<ItemStack> {
user.playSound(SoundEvents.BLOCK_METAL_PLACE, 1.0f, 1.0f)
return TypedActionResult.success(user.getStackInHand(hand))
}
}

View file

@ -0,0 +1,141 @@
package net.liquidev.dawd3.item
import net.fabricmc.fabric.api.networking.v1.PlayerLookup
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.block.device.PhysicalPort
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.datagen.device.DeviceBlockModel
import net.liquidev.dawd3.net.ConnectPorts
import net.liquidev.dawd3.net.StartConnectingPorts
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemUsageContext
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.server.world.ServerWorld
import net.minecraft.util.ActionResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3d
class PatchCableItem(settings: Settings, val color: Byte) : BasicItem(settings) {
override fun useOnBlock(context: ItemUsageContext): ActionResult {
val blockEntity = context.world.getBlockEntity(context.blockPos)
if (!context.world.isClient && blockEntity is DeviceBlockEntity) {
val side = HorizontalDirection.fromDirection(context.side)
if (side != null) {
val usePosition =
calculateUsePositionOnHorizontalFace(context.hitPos, context.blockPos, side)
for ((portName, port) in blockEntity.descriptor.portLayout) {
if (isUsedOnPort(port, usePosition)) {
useOnPort(context, portName)
break
}
}
}
}
return ActionResult.success(context.world.isClient)
}
private fun calculateUsePositionOnHorizontalFace(
hitPosition: Vec3d,
blockPosition: BlockPos,
side: HorizontalDirection,
): Vec2f {
val relativeHitPosition = (hitPosition - blockPosition.toVec3d()).toVec3f()
val faceCorrection = side.faceCorrection
return faceCorrection * (side.direction.to2DPlane * relativeHitPosition)
}
private fun isUsedOnPort(
port: PhysicalPort,
usePosition: Vec2f,
): Boolean {
val usePositionInPixels = usePosition * Vec2f(16f, 16f)
val portTopLeft = DeviceBlockModel.relativeToAbsolutePortPosition(port.position)
val portBottomRight = portTopLeft + DeviceBlockModel.portSize
return pointInRectangle(usePositionInPixels, portTopLeft, portBottomRight)
}
private fun useOnPort(context: ItemUsageContext, portName: PortName) {
val player = context.player
if (player == null || player !is ServerPlayerEntity) {
return
}
val ongoingConnection = ongoingConnections[player]
if (ongoingConnection == null) {
startConnecting(player, OngoingConnection(context.blockPos, portName, color))
ServerPlayNetworking.send(
player,
StartConnectingPorts.id,
StartConnectingPorts(context.blockPos, portName.toString(), color).serialize()
)
} else {
if (portName.direction != ongoingConnection.portName.direction) {
val world = context.world as ServerWorld
val fromPosition = ongoingConnection.blockPosition
val fromPort = ongoingConnection.portName
val toPosition = context.blockPos
val witnesses = PlayerLookup.tracking(world, fromPosition).toHashSet()
witnesses.addAll(PlayerLookup.tracking(world, toPosition))
for (witness in witnesses) {
ServerPlayNetworking.send(
witness,
ConnectPorts.id,
ConnectPorts(
fromPosition,
fromPort.id.toString(),
toPosition,
portName.id.toString(),
ongoingConnection.color,
).serialize()
)
}
val fromBlockEntity = world.getBlockEntity(fromPosition)
val toBlockEntity = world.getBlockEntity(toPosition)
if (fromBlockEntity !is DeviceBlockEntity || toBlockEntity !is DeviceBlockEntity) {
logger.error("from or to-block entity is no longer a device")
return
}
DeviceBlockEntity.connectPhysicalDevices(
fromBlockEntity,
fromPort,
toBlockEntity,
portName,
color,
)
clearOngoingConnection(player)
}
}
}
data class OngoingConnection(
val blockPosition: BlockPos,
val portName: PortName,
val color: Byte,
)
companion object {
private var logger = Mod.logger<PatchCableItem>()
internal val ongoingConnections = hashMapOf<PlayerEntity, OngoingConnection>()
private fun clearOngoingConnection(player: PlayerEntity) {
ongoingConnections.remove(player)
}
internal fun onBlockDestroyed(blockPosition: BlockPos) {
ongoingConnections.values.removeAll { it.blockPosition == blockPosition }
}
fun startConnecting(player: PlayerEntity, connection: OngoingConnection) {
ongoingConnections[player] = connection
}
}
}

View file

@ -0,0 +1,27 @@
package net.liquidev.dawd3.mixin
import net.liquidev.dawd3.render.Textures
import net.minecraft.client.render.TexturedRenderLayers
import net.minecraft.client.util.SpriteIdentifier
import org.spongepowered.asm.mixin.Mixin
import org.spongepowered.asm.mixin.injection.At
import org.spongepowered.asm.mixin.injection.Inject
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo
import java.util.function.Consumer
@Suppress("UNUSED")
@Mixin(TexturedRenderLayers::class)
abstract class NonModelTextures {
private companion object {
@Inject(
method = ["addDefaultTextures(Ljava/util/function/Consumer;)V"],
at = [At(value = "TAIL")]
)
@JvmStatic
private fun addDefaultTextures(adder: Consumer<SpriteIdentifier>, ci: CallbackInfo) {
for (texture in Textures.nonModel) {
adder.accept(texture)
}
}
}
}

View file

@ -0,0 +1,88 @@
package net.liquidev.dawd3.net
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
data class ConnectPorts(
val fromPosition: BlockPos,
val fromPort: String,
val toPosition: BlockPos,
val toPort: String,
val color: Byte,
) {
companion object {
private val logger = Mod.logger<ConnectPorts>()
val id = Identifier(Mod.id, "connect_ports")
fun registerClientReceiver() {
ClientPlayNetworking.registerGlobalReceiver(id) { client, _, buffer, _ ->
val packet = deserialize(buffer)
client.execute {
val world = client.world
if (world != null) {
packet.handleInWorld(world)
} else {
logger.warn("packet received without a client world")
}
}
}
}
private fun deserialize(buffer: PacketByteBuf): ConnectPorts {
return ConnectPorts(
fromPosition = buffer.readBlockPos(),
fromPort = buffer.readString(),
toPosition = buffer.readBlockPos(),
toPort = buffer.readString(),
color = buffer.readByte(),
)
}
}
fun serialize(): PacketByteBuf {
val buffer = PacketByteBufs.create()
buffer.writeBlockPos(fromPosition)
buffer.writeString(fromPort)
buffer.writeBlockPos(toPosition)
buffer.writeString(toPort)
buffer.writeByte(color.toInt()) // bruh
return buffer
}
private fun handleInWorld(world: World) {
val fromBlockEntity = world.getBlockEntity(fromPosition)
val toBlockEntity = world.getBlockEntity(toPosition)
if (fromBlockEntity !is DeviceBlockEntity || toBlockEntity !is DeviceBlockEntity) {
logger.warn("packet received with block entities not being device block entities")
return
}
val fromPortName = PortName.fromString(fromPort)
if (fromPortName == null) {
logger.warn("port $fromPort does not exist on the from-device")
return
}
val toPortName = PortName.fromString(toPort)
if (toPortName == null) {
logger.warn("port $toPort does not exist on the to-device")
return
}
logger.debug("connecting logical device ports ($fromBlockEntity):($fromPortName) -> ($toBlockEntity):($toPortName)")
DeviceBlockEntity.connectLogicalDevices(
fromBlockEntity,
fromPortName,
toBlockEntity,
toPortName,
color,
)
}
}

View file

@ -0,0 +1,8 @@
package net.liquidev.dawd3.net
object Packets {
fun registerClientReceivers() {
StartConnectingPorts.registerClientReceiver()
ConnectPorts.registerClientReceiver()
}
}

View file

@ -0,0 +1,62 @@
package net.liquidev.dawd3.net
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.item.PatchCableItem
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
class StartConnectingPorts(val blockPosition: BlockPos, val portName: String, val color: Byte) {
companion object {
private val logger = Mod.logger<ConnectPorts>()
val id = Identifier(Mod.id, "start_connecting_ports")
fun registerClientReceiver() {
ClientPlayNetworking.registerGlobalReceiver(id) { client, _, buffer, _ ->
val packet = deserialize(buffer)
client.execute {
val player = client.player
if (player != null) {
packet.handleForPlayer(player)
} else {
logger.warn("packet received without a client player")
}
}
}
}
private fun deserialize(buffer: PacketByteBuf): StartConnectingPorts {
return StartConnectingPorts(
blockPosition = buffer.readBlockPos(),
portName = buffer.readString(),
color = buffer.readByte(),
)
}
}
fun serialize(): PacketByteBuf {
val buffer = PacketByteBufs.create()
buffer.writeBlockPos(blockPosition)
buffer.writeString(portName)
buffer.writeByte(color.toInt())
return buffer
}
private fun handleForPlayer(player: PlayerEntity) {
val portName = PortName.fromString(portName)
if (portName == null) {
logger.warn("invalid port name ${this.portName}")
return
}
PatchCableItem.startConnecting(
player,
PatchCableItem.OngoingConnection(blockPosition, portName, color)
)
}
}

View file

@ -0,0 +1,19 @@
package net.liquidev.dawd3.render
import net.liquidev.dawd3.Mod
import net.minecraft.client.util.SpriteIdentifier
import net.minecraft.screen.PlayerScreenHandler
import net.minecraft.util.Identifier
object Textures {
/**
* The set of textures that are not referenced by models but need to be loaded into the
* block atlas.
*/
val nonModel = setOf(
SpriteIdentifier(
PlayerScreenHandler.BLOCK_ATLAS_TEXTURE,
Identifier(Mod.id, "device/cable")
)
)
}

View file

@ -0,0 +1,5 @@
{
"variants": {
"": { "model": "dawd3:device/patch_cable_plug" }
}
}

View file

@ -1,5 +1,20 @@
{
"itemGroup.dawd3.main": "dawd³",
"item.dawd3.patch_cable": "Patch Cable",
"item.dawd3.white_patch_cable": "White Patch Cable",
"item.dawd3.orange_patch_cable": "Orange Patch Cable",
"item.dawd3.magenta_patch_cable": "Magenta Patch Cable",
"item.dawd3.light_blue_patch_cable": "Light Blue Patch Cable",
"item.dawd3.yellow_patch_cable": "Yellow Patch Cable",
"item.dawd3.lime_patch_cable": "Lime Patch Cable",
"item.dawd3.pink_patch_cable": "Pink Patch Cable",
"item.dawd3.gray_patch_cable": "Gray Patch Cable",
"item.dawd3.light_gray_patch_cable": "Light Gray Patch Cable",
"item.dawd3.cyan_patch_cable": "Cyan Patch Cable",
"item.dawd3.purple_patch_cable": "Purple Patch Cable",
"item.dawd3.brown_patch_cable": "Brown Patch Cable",
"item.dawd3.blue_patch_cable": "Blue Patch Cable",
"item.dawd3.green_patch_cable": "Green Patch Cable",
"item.dawd3.red_patch_cable": "Red Patch Cable",
"item.dawd3.black_patch_cable": "Black Patch Cable",
"block.dawd3.speaker": "Speaker"
}

View file

@ -1,21 +0,0 @@
{
"parent": "block/block",
"textures": {
"side": "dawd3:block/module_side",
"particle": "#side"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 16, 16],
"faces": {
"down": { "texture": "#side", "cullface": "down" },
"up": { "texture": "#side", "cullface": "up" },
"north": { "texture": "#front", "cullface": "north" },
"east": { "texture": "#side", "cullface": "east" },
"south": { "texture": "#side", "cullface": "south" },
"west": { "texture": "#side", "cullface": "west" }
}
}
]
}

Some files were not shown because too many files have changed in this diff Show more