that's a lotta changes

This commit is contained in:
りき萌 2023-05-01 12:17:34 +02:00
parent 97d2ea5cdf
commit 32f3c692b8
53 changed files with 1362 additions and 271 deletions

View file

@ -1,5 +1,6 @@
// 1.19.2 2022-11-27T14:23:10.976127707 Models
// 1.19.2 2023-04-29T18:02:19.599832005 Models
0812a674d14cfc6fbb7c0e2ac1b473bf2afe1965 assets/dawd3/models/item/brown_patch_cable.json
6e01a1aa07f3a36d7950a1b00d1bc6e9045b9995 assets/dawd3/blockstates/knob.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
@ -9,8 +10,8 @@ f7b47538f17992177e97e06842c0039ae5096b2b assets/dawd3/blockstates/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
aa1a1807d2c46f1f25e3d0c507952fefb5b3cd9f assets/dawd3/models/item/knob.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
@ -19,8 +20,7 @@ aab1bce7ec4e7c7dccd3d33d4242de12b63a981d assets/dawd3/models/item/white_patch_ca
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
ff4e86bc6228d4ced17fc8df632bd9d3812a51d0 assets/dawd3/models/block/knob.json
fb1d7e02f0cdd3807d828b0bec299c6533b9de57 assets/dawd3/models/block/sine_oscillator.json

View file

@ -1,19 +0,0 @@
{
"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

@ -1,77 +0,0 @@
{
"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

@ -39,30 +39,6 @@
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": {
@ -86,6 +62,30 @@
10.0,
0.01
]
},
{
"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
]
}
],
"textures": {

View file

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

View file

@ -24,6 +24,7 @@ object Mod : ModInitializer, ClientModInitializer {
logger.info("hello, sound traveler! welcome to the dawd³ experience")
Blocks.initialize()
Items.registry.registerAll()
Packets.registerServerReceivers()
}
override fun onInitializeClient() {

View file

@ -4,6 +4,7 @@ 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.Frequency
/** Audio system and common settings. */
object Audio {
@ -14,6 +15,8 @@ object Audio {
const val sampleRateFInv = 1.0f / sampleRateF
private const val bufferSize = 256
val a4 = Frequency(440f)
private val outputDeviceId: Int
private val outputStreamId: Int

View file

@ -1,8 +1,8 @@
package net.liquidev.dawd3.audio.device
/** Device that can process audio. */
interface Device {
fun process(sampleCount: Int, channels: Int)
interface Device<C : ControlSet> {
fun process(sampleCount: Int, channels: Int, controls: C)
fun visitInputPorts(visit: (InputPortName, InputPort) -> Unit) {}
fun visitOutputPorts(visit: (OutputPortName, OutputPort) -> Unit) {}

View file

@ -1,6 +1,6 @@
package net.liquidev.dawd3.audio.device
class DeviceInstance(val state: Device) {
class DeviceInstance private constructor(val state: Device<ControlSet>, val controls: ControlSet) {
val inputPortsByName = hashMapOf<PortName, InputPort>()
val outputPortsByName = hashMapOf<PortName, OutputPort>()
@ -29,11 +29,18 @@ class DeviceInstance(val state: Device) {
for ((_, port) in inputPortsByName) {
port.connectedOutput?.owningDevice?.process(sampleCount, channels, processingState)
}
state.process(sampleCount, channels)
state.process(sampleCount, channels, controls)
}
}
override fun toString(): String {
return "DeviceInstance($state)"
}
companion object {
fun <T : ControlSet> create(state: Device<T>, controls: T) =
// NOTE: We're erasing the type from concrete T to Controls, the cast is fine.
@Suppress("UNCHECKED_CAST")
DeviceInstance(state as Device<ControlSet>, controls)
}
}

View file

@ -0,0 +1,57 @@
package net.liquidev.dawd3.audio.device
import net.minecraft.util.Identifier
import java.util.concurrent.atomic.AtomicInteger
class ControlName(parent: Identifier, name: String) {
val id = idInDevice(parent, name)
init {
registry[id] = this
}
override fun toString() = id.toString()
companion object {
private val registry = hashMapOf<Identifier, ControlName>()
fun fromString(name: String): ControlName? =
registry[Identifier(name)]
}
}
data class ControlDescriptor(
val name: ControlName,
val initialValue: Float,
) {
constructor(
parent: Identifier,
name: String,
initialValue: Float,
) : this(ControlName(parent, name), initialValue)
}
class Control(val descriptor: ControlDescriptor) {
private val internalValue = AtomicInteger(descriptor.initialValue.toBits())
var value: Float
get() = Float.fromBits(internalValue.get())
set(value) = internalValue.set(value.toBits())
}
interface ControlSet {
fun visitControls(visit: (ControlDescriptor, Control) -> Unit)
}
object NoControls : ControlSet {
override fun visitControls(visit: (ControlDescriptor, Control) -> Unit) {}
}
class ControlMap(set: ControlSet) {
private val map = hashMapOf<ControlName, Control>()
init {
set.visitControls { controlDescriptor, control -> map[controlDescriptor.name] = control }
}
operator fun get(name: ControlName): Control? = map[name]
}

View file

@ -1,5 +1,7 @@
package net.liquidev.dawd3.audio.device
class ControlAlreadyExistsException(what: String) : Exception(what)
class NoSuchPortException(what: String) : Exception(what)
class PortAlreadyExistsException(what: String) : Exception(what)

View file

@ -0,0 +1,6 @@
package net.liquidev.dawd3.audio.device
import net.minecraft.util.Identifier
internal fun idInDevice(deviceId: Identifier, name: String): Identifier =
Identifier(deviceId.namespace, "${deviceId.path}/$name")

View file

@ -55,12 +55,8 @@ sealed interface PortName {
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")
fun fromString(name: String): PortName? =
registry[Identifier(name)]
}
}
@ -74,7 +70,7 @@ class InputPortName(override val id: Identifier) : PortName {
constructor(
parent: Identifier,
name: String,
) : this(PortName.idInDevice(parent, name))
) : this(idInDevice(parent, name))
override fun toString(): String = id.toString()
}
@ -92,12 +88,12 @@ class OutputPortName private constructor(
constructor(
parent: Identifier,
name: String,
) : this(PortName.idInDevice(parent, name), instanceOf = null)
) : this(idInDevice(parent, name), instanceOf = null)
override fun toString(): String = id.toString()
fun makeInstanced(instanceName: String) =
OutputPortName(PortName.idInDevice(id, instanceName), instanceOf = this)
OutputPortName(idInDevice(id, instanceName), instanceOf = this)
fun resolveInstance() = instanceOf ?: this
}

View file

@ -1,24 +1,30 @@
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.OutputPortName
import net.liquidev.dawd3.audio.device.*
import net.minecraft.util.Identifier
class ConstantDevice(var value: Float) : Device {
class ConstantDevice : Device<ConstantDevice.Controls> {
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "constant")
val valueControl = ControlDescriptor(id, "value", 0f)
val outputPort = OutputPortName(id, "output")
}
class Controls : ControlSet {
val value = Control(valueControl)
override fun visitControls(visit: (ControlDescriptor, Control) -> Unit) {
visit(valueControl, value)
}
}
val output = OutputPort(bufferCount = 1)
override fun process(sampleCount: Int, channels: Int) {
override fun process(sampleCount: Int, channels: Int, controls: Controls) {
val outputBuffer = output.buffers[0].getOrReallocate(sampleCount)
for (i in 0 until sampleCount) {
outputBuffer[i] = value
outputBuffer[i] = controls.value.value
}
}

View file

@ -2,13 +2,15 @@ package net.liquidev.dawd3.audio.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.audio.AudioBuffer
import net.liquidev.dawd3.audio.device.*
import net.liquidev.dawd3.audio.unit.VOct
import net.minecraft.util.Identifier
import kotlin.math.sin
private const val twoPi = 2.0f * kotlin.math.PI.toFloat()
class SineOscillatorDevice : Device {
class SineOscillatorDevice : Device<NoControls> {
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "sine_oscillator")
val frequencyCVPort = InputPortName(id, "frequency_cv")
@ -18,10 +20,16 @@ class SineOscillatorDevice : Device {
private val frequencyCV = InputPort()
private val output = OutputPort(bufferCount = 1)
private val frequencyBuffer = AudioBuffer()
private var phase = 0.0f
override fun process(sampleCount: Int, channels: Int) {
val frequencyBuffer = frequencyCV.getConnectedOutputBuffer(0, sampleCount)
override fun process(sampleCount: Int, channels: Int, controls: NoControls) {
val voctBuffer = frequencyCV.getConnectedOutputBuffer(0, sampleCount)
val frequencyBuffer = frequencyBuffer.getOrReallocate(sampleCount)
for (i in 0 until sampleCount) {
frequencyBuffer[i] = VOct(voctBuffer[i]).toFrequency(Audio.a4).value
}
val outputBuffer = output.buffers[0].getOrReallocate(sampleCount)
for (i in 0 until sampleCount) {
val phaseStep = Audio.sampleRateFInv * frequencyBuffer[i]

View file

@ -1,13 +1,10 @@
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.InputPortName
import net.liquidev.dawd3.audio.device.*
import net.minecraft.util.Identifier
class TerminalDevice : Device {
class TerminalDevice : Device<NoControls> {
companion object : DeviceDescriptor {
override val id = Identifier(Mod.id, "terminal")
val inputPort = InputPortName(id, "input")
@ -15,7 +12,7 @@ class TerminalDevice : Device {
val input = InputPort()
override fun process(sampleCount: Int, channels: Int) {
override fun process(sampleCount: Int, channels: Int, controls: NoControls) {
// Terminals don't do any audio processing.
// The output port connected to `input` is instead used by terminal block entities like
// speakers.

View file

@ -2,12 +2,13 @@ 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.NoControls
import net.liquidev.dawd3.audio.devices.TerminalDevice
/** Audio generator that evaluates a device graph. */
class DeviceGraphGenerator : AudioGenerator() {
private val terminalDeviceState = TerminalDevice()
val terminalDevice = DeviceInstance(terminalDeviceState)
val terminalDevice = DeviceInstance.create(terminalDeviceState, NoControls)
override fun generate(output: FloatArray, sampleCount: Int, channelCount: Int) {
// TODO: Maybe passing in the static processingState here is not the cleanest way to go

View file

@ -0,0 +1,12 @@
package net.liquidev.dawd3.audio.unit
import kotlin.math.pow
@JvmInline
value class VOct(val value: Float) {
fun toFrequency(a4: Frequency): Frequency = Frequency(a4.value * 2f.pow(value / 12f))
}
@JvmInline
value class Frequency(val value: Float)

View file

@ -10,7 +10,7 @@ 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.KnobBlockDescriptor
import net.liquidev.dawd3.block.devices.SineOscillatorBlockDescriptor
import net.liquidev.dawd3.block.devices.SpeakerBlockDescriptor
import net.liquidev.dawd3.item.Items
@ -61,7 +61,7 @@ object Blocks {
// Device blocks
val speaker = registerDeviceBlock(SpeakerBlockDescriptor)
val sineOscillator = registerDeviceBlock(SineOscillatorBlockDescriptor)
val fader = registerDeviceBlock(FaderBlockDescriptor)
val knob = registerDeviceBlock(KnobBlockDescriptor)
fun initialize() {}

View file

@ -2,11 +2,18 @@ package net.liquidev.dawd3.block.device
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.item.PatchCableItem
import net.liquidev.dawd3.ui.Rack
import net.minecraft.block.*
import net.minecraft.block.entity.BlockEntity
import net.minecraft.client.MinecraftClient
import net.minecraft.client.world.ClientWorld
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemPlacementContext
import net.minecraft.state.StateManager
import net.minecraft.state.property.Properties
import net.minecraft.util.ActionResult
import net.minecraft.util.Hand
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3f
import net.minecraft.util.shape.VoxelShape
@ -67,4 +74,39 @@ class DeviceBlock(private val descriptor: AnyDeviceBlockDescriptor) :
val direction = HorizontalDirection.fromDirection(state[Properties.HORIZONTAL_FACING])!!
return VoxelShapes.cuboid(outlineCuboids[direction.index])
}
@Deprecated("do not call this function directly")
override fun onUse(
state: BlockState,
world: World,
pos: BlockPos,
player: PlayerEntity,
hand: Hand,
hit: BlockHitResult,
): ActionResult {
val blockEntity =
world.getBlockEntity(pos) as? DeviceBlockEntity ?: return ActionResult.PASS
// We have to check this event ahead of time (in addition to doing it in PatchCableItem)
// because block interactions take priority over item interactions.
val usedPortName = DeviceBlockInteractions.findUsedPort(hit, blockEntity.descriptor)
// TODO: Right-clicking the port should pop out its patch cable.
if (usedPortName == null && world is ClientWorld && !player.isSneaking) {
val rack = Rack(world, Rack.collectAdjacentDevices(world, pos))
if (rack.hasOpenWindows()) {
MinecraftClient.getInstance().setScreen(rack)
return ActionResult.SUCCESS
}
}
if (usedPortName != null && player.isSneaking) {
return if (blockEntity.severConnectionsInPort(usedPortName)) {
ActionResult.SUCCESS
} else {
ActionResult.PASS
}
}
return ActionResult.PASS
}
}

View file

@ -2,17 +2,19 @@ package net.liquidev.dawd3.block.device
import FaceTextures
import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.liquidev.dawd3.audio.device.ControlSet
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.common.Cuboids
import net.liquidev.dawd3.ui.widget.Widget
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>
typealias AnyDeviceBlockDescriptor = DeviceBlockDescriptor<DeviceBlockDescriptor.ClientState, ControlSet>
interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out ServerState> {
interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out Controls : ControlSet> {
val id: Identifier
val blockSettings: AbstractBlock.Settings
@ -29,11 +31,15 @@ interface DeviceBlockDescriptor<out CS : DeviceBlockDescriptor.ClientState, out
val faceTextures: FaceTextures
get() = FaceTextures.withFrontAndSide { id }
fun onClientLoad(world: ClientWorld): CS
fun initControls(): Controls
fun onClientLoad(controls: @UnsafeVariance Controls, world: ClientWorld): CS
fun onClientUnload(state: @UnsafeVariance CS, world: ClientWorld) {}
interface ClientState {
val logicalDevice: DeviceInstance
}
fun openUI(controls: @UnsafeVariance Controls, x: Int, y: Int): Widget? = null
}

View file

@ -2,19 +2,13 @@ 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.audio.device.*
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.nbt.*
import net.minecraft.util.math.BlockPos
private typealias DeviceBlockFactory = FabricBlockEntityTypeBuilder.Factory<DeviceBlockEntity>
@ -27,7 +21,8 @@ class DeviceBlockEntity(
) : D3BlockEntity(type, blockPos, blockState) {
private var clientState: DeviceBlockDescriptor.ClientState? = null
private var serverState: Any? = null
val controls = descriptor.initControls()
val controlMap = ControlMap(controls)
internal data class InputConnection(
val blockPosition: BlockPos,
@ -45,6 +40,8 @@ class DeviceBlockEntity(
/** NBT compound keys. */
private object Nbt {
const val controls = "controls"
const val inputConnections = "inputConnections"
object InputConnection {
@ -65,6 +62,15 @@ class DeviceBlockEntity(
override fun readNbt(nbt: NbtCompound) {
super.readNbt(nbt)
val controlsNbt = nbt.getCompound(Nbt.controls)
controls.visitControls { controlDescriptor, control ->
val controlName = controlDescriptor.name.toString()
if (controlName in controlsNbt) {
val value = controlsNbt.getFloat(controlName)
control.value = value
}
}
val inputConnectionsNbt =
nbt.getList(Nbt.inputConnections, NbtElement.COMPOUND_TYPE.toInt())
for (i in 0 until inputConnectionsNbt.size) {
@ -112,6 +118,12 @@ class DeviceBlockEntity(
override fun writeNbt(nbt: NbtCompound) {
super.writeNbt(nbt)
val controlsNbt = NbtCompound()
controls.visitControls { controlDescriptor, control ->
controlsNbt.put(controlDescriptor.name.toString(), NbtFloat.of(control.value))
}
nbt.put(Nbt.controls, controlsNbt)
val inputConnectionsNbt = NbtList()
for ((inputPortName, connection) in inputConnections) {
val connectionNbt = NbtCompound()
@ -140,7 +152,7 @@ class DeviceBlockEntity(
}
override fun onClientLoad(world: ClientWorld) {
clientState = descriptor.onClientLoad(world)
clientState = descriptor.onClientLoad(controls, world)
WorldDeviceLoading.enqueueDeviceForRebuild(this)
}
@ -164,9 +176,37 @@ class DeviceBlockEntity(
}
}
internal fun reapAllConnections() {
// TODO: This should be triggered whenever a device block's destroyed and reap all
// existing connections to the device.
fun severConnectionsInPort(portName: PortName): Boolean {
val world = world ?: return false
val clientState = clientState
val severedConnections = if (clientState != null) {
val resolvedPortName =
if (portName is OutputPortName) portName.resolveInstance() else portName
Devices.severAllConnectionsInPort(clientState.logicalDevice, resolvedPortName)
} else 0
when (portName) {
is InputPortName -> {
val inputConnection = inputConnections.remove(portName)
if (inputConnection != null) {
val blockEntity = world.getBlockEntity(inputConnection.blockPosition)
if (blockEntity is DeviceBlockEntity) {
blockEntity.outputConnections.remove(inputConnection.outputPortName)
}
}
}
is OutputPortName -> {
val blockPosition = outputConnections.remove(portName)
if (blockPosition != null) {
val blockEntity = world.getBlockEntity(blockPosition)
if (blockEntity is DeviceBlockEntity) {
blockEntity.inputConnections.values.removeAll { it.outputPortName == portName }
}
}
}
}
return severedConnections != 0
}
companion object {

View file

@ -42,7 +42,6 @@ class DeviceBlockEntityRenderer(context: BlockEntityRendererFactory.Context) : B
)
val renderLayerFactory = Function<Identifier, RenderLayer> { renderLayer }
const val cableProtrusionAmount = 0.69f // nice
const val cableThickness = 0.03f
const val cableSegmentCount = 6

View file

@ -0,0 +1,46 @@
package net.liquidev.dawd3.block.device
import net.liquidev.dawd3.audio.device.PortName
import net.liquidev.dawd3.common.*
import net.liquidev.dawd3.datagen.device.DeviceBlockModel
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec2f
import net.minecraft.util.math.Vec3d
object DeviceBlockInteractions {
fun findUsedPort(
hit: BlockHitResult,
descriptor: AnyDeviceBlockDescriptor,
): PortName? {
val horizontalSide = HorizontalDirection.fromDirection(hit.side) ?: return null
val usePosition =
calculateUsePositionOnHorizontalFace(hit.pos, hit.blockPos, horizontalSide)
for ((portName, port) in descriptor.portLayout) {
if (isUsedOnPort(port, usePosition)) {
return portName
}
}
return null
}
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)
}
}

View file

@ -1,33 +0,0 @@
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,55 @@
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.liquidev.dawd3.ui.widget.Knob
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.world.ClientWorld
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec2f
object KnobBlockDescriptor : DeviceBlockDescriptor<KnobBlockDescriptor.ClientState, ConstantDevice.Controls> {
override val id = Identifier(Mod.id, "knob")
override val cuboid = Cuboids.fullBlock
override val portLayout = PhysicalPort.layout {
port(
ConstantDevice.outputPort,
instanceName = "front",
position = Vec2f(0.5f, 0.8125f),
side = PhysicalPort.Side.Front,
)
}
override val faceTextures = FaceTextures.withFrontAndSide { id }
class ClientState(controls: ConstantDevice.Controls) : DeviceBlockDescriptor.ClientState {
override val logicalDevice =
DeviceInstance.create(ConstantDevice(), controls)
}
override fun initControls() = ConstantDevice.Controls()
override fun onClientLoad(controls: ConstantDevice.Controls, world: ClientWorld) =
ClientState(controls)
override fun openUI(controls: ConstantDevice.Controls, x: Int, y: Int): Widget =
Window(x, y, 48, 48, Text.translatable("block.dawd3.knob")).apply {
children.add(
Knob(
x = 14,
y = 18,
control = controls.value,
min = -48f,
max = 48f,
color = Knob.Color.Blue
)
)
}
}

View file

@ -2,6 +2,7 @@ package net.liquidev.dawd3.block.devices
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.audio.device.NoControls
import net.liquidev.dawd3.audio.devices.SineOscillatorDevice
import net.liquidev.dawd3.block.device.DeviceBlockDescriptor
import net.liquidev.dawd3.block.device.PhysicalPort
@ -9,7 +10,7 @@ import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec2f
object SineOscillatorBlockDescriptor : DeviceBlockDescriptor<SineOscillatorBlockDescriptor.ClientState, Unit> {
object SineOscillatorBlockDescriptor : DeviceBlockDescriptor<SineOscillatorBlockDescriptor.ClientState, NoControls> {
override val id = Identifier(Mod.id, "sine_oscillator")
override val portLayout = PhysicalPort.layout {
@ -25,9 +26,11 @@ object SineOscillatorBlockDescriptor : DeviceBlockDescriptor<SineOscillatorBlock
)
}
class ClientState : DeviceBlockDescriptor.ClientState {
override val logicalDevice = DeviceInstance(SineOscillatorDevice())
class ClientState(controls: NoControls) : DeviceBlockDescriptor.ClientState {
override val logicalDevice = DeviceInstance.create(SineOscillatorDevice(), controls)
}
override fun onClientLoad(world: ClientWorld) = ClientState()
override fun initControls() = NoControls
override fun onClientLoad(controls: NoControls, world: ClientWorld) = ClientState(controls)
}

View file

@ -4,6 +4,7 @@ import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.audio.Audio
import net.liquidev.dawd3.audio.device.DeviceInstance
import net.liquidev.dawd3.audio.device.NoControls
import net.liquidev.dawd3.audio.devices.TerminalDevice
import net.liquidev.dawd3.audio.generator.DeviceGraphGenerator
import net.liquidev.dawd3.audio.generator.MixGenerator
@ -15,7 +16,7 @@ import net.minecraft.sound.BlockSoundGroup
import net.minecraft.util.Identifier
import net.minecraft.util.math.Vec2f
object SpeakerBlockDescriptor : DeviceBlockDescriptor<SpeakerBlockDescriptor.ClientState, Unit> {
object SpeakerBlockDescriptor : DeviceBlockDescriptor<SpeakerBlockDescriptor.ClientState, NoControls> {
override val id = Identifier(Mod.id, "speaker")
override val blockSettings = FabricBlockSettings
.of(Material.WOOD)
@ -38,7 +39,9 @@ object SpeakerBlockDescriptor : DeviceBlockDescriptor<SpeakerBlockDescriptor.Cli
}
}
override fun onClientLoad(world: ClientWorld) = ClientState()
override fun initControls() = NoControls
override fun onClientLoad(controls: NoControls, world: ClientWorld) = ClientState()
override fun onClientUnload(state: ClientState, world: ClientWorld) {
state.channel.stop()

View file

@ -1,18 +1,23 @@
package net.liquidev.dawd3.common
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.function.Function
class TaskQueue<T, R> {
private val tasks = arrayListOf<Function<T, R>>()
private val tasks = ConcurrentLinkedQueue<Function<T, R>>()
fun enqueue(task: Function<T, R>) {
tasks.add(task)
}
fun flush(argument: T) {
for (task in tasks) {
task.apply(argument)
while (true) {
val task = tasks.poll()
if (task != null) {
task.apply(argument)
} else {
break
}
}
tasks.clear()
}
}

View file

@ -0,0 +1,6 @@
package net.liquidev.dawd3.common
fun rgba(r: Int, g: Int, b: Int, a: Int): Int =
(a shl 24) or (r shl 16) or (g shl 8) or b
fun rgb(r: Int, g: Int, b: Int): Int = rgba(r, g, b, 255)

View file

@ -26,15 +26,6 @@ fun Vec3f.max(other: Vec3f): Vec3f = Vec3f(max(x, other.x), max(y, other.y), max
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)
@ -136,6 +127,12 @@ fun pointInRectangle(point: Vec2f, topLeft: Vec2f, bottomRight: Vec2f) =
fun lerp(a: Float, b: Float, t: Float): Float =
a + t * (b - a)
fun mapRange(value: Float, fromMin: Float, fromMax: Float, toMin: Float, toMax: Float): Float =
toMin + (value - fromMin) / (fromMax - fromMin) * (toMax - toMin)
fun clamp(value: Float, min: Float, max: Float): Float =
max(min(value, max), min)
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)
@ -145,3 +142,15 @@ object Cuboids {
val Box.fromF get() = Vec3f(minX.toFloat(), minY.toFloat(), minZ.toFloat())
val Box.toF get() = Vec3f(maxX.toFloat(), maxY.toFloat(), maxZ.toFloat())
@JvmInline
value class Radians(val value: Float) {
fun toDegrees() =
Degrees(value / PI.toFloat() * 180f)
}
@JvmInline
value class Degrees(val value: Float) {
fun toRadians() =
Radians(value / 180f * PI.toFloat())
}

View file

@ -5,9 +5,7 @@ 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.block.device.DeviceBlockInteractions
import net.liquidev.dawd3.events.PlayerEvents
import net.liquidev.dawd3.net.ConnectPorts
import net.liquidev.dawd3.net.StartConnectingPorts
@ -18,46 +16,23 @@ 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
}
if (context.player?.isSneaking != true) {
val blockEntity = context.world.getBlockEntity(context.blockPos)
if (!context.world.isClient && blockEntity is DeviceBlockEntity) {
val portName =
DeviceBlockInteractions.findUsedPort(context.hitResult, blockEntity.descriptor)
if (portName != null) {
useOnPort(context, portName)
}
}
return ActionResult.success(context.world.isClient)
} else {
return context.world.getBlockState(context.blockPos)
.onUse(context.world, context.player, context.hand, context.hitResult)
}
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) {

View file

@ -0,0 +1,42 @@
package net.liquidev.dawd3.net
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
/** C2S message that a block's save data needs to be marked dirty as a result of control tweaks. */
data class ControlTweaked(val position: BlockPos) {
companion object {
val id = Identifier(Mod.id, "control_tweaked")
fun registerServerReceiver() {
ServerPlayNetworking.registerGlobalReceiver(id) { server, player, _, buffer, _ ->
val packet = deserialize(buffer)
server.execute {
val world = player.world
if (world != null) {
val blockEntity =
world.getBlockEntity(packet.position) as? DeviceBlockEntity
?: return@execute
blockEntity.markDirty()
}
}
}
}
private fun deserialize(buffer: PacketByteBuf) =
ControlTweaked(
position = buffer.readBlockPos(),
)
}
fun serialize(): PacketByteBuf {
val buffer = PacketByteBufs.create()
buffer.writeBlockPos(position)
return buffer
}
}

View file

@ -4,5 +4,11 @@ object Packets {
fun registerClientReceivers() {
StartConnectingPorts.registerClientReceiver()
ConnectPorts.registerClientReceiver()
TweakControl.registerClientReceiver()
}
fun registerServerReceivers() {
TweakControl.registerServerReceiver()
ControlTweaked.registerServerReceiver()
}
}

View file

@ -54,7 +54,6 @@ class StartConnectingPorts(val blockPosition: BlockPos, val portName: String, va
return
}
println("[PLAYER] Started connecting ports $player")
PatchCableItem.startConnecting(
player,
PatchCableItem.OngoingConnection(blockPosition, portName, color)

View file

@ -0,0 +1,87 @@
package net.liquidev.dawd3.net
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
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.ControlName
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
data class TweakControl(val position: BlockPos, val control: Identifier, val newValue: Float) {
companion object {
val id = Identifier(Mod.id, "tweak_control")
fun registerServerReceiver() {
ServerPlayNetworking.registerGlobalReceiver(id) { server, player, _, buffer, _ ->
// TODO: This should check for distance or something along those lines
// to prevent cheating. We aren't currently performing any such checks because
// it's kind of hard (as a rack may span tens of meters with many controls.)
val packet = deserialize(buffer)
server.execute {
val blockEntity =
player.world.getBlockEntity(packet.position) as? DeviceBlockEntity
?: return@execute
val controlName =
ControlName.fromString(packet.control.toString()) ?: return@execute
val control = blockEntity.controlMap[controlName]
control?.value = packet.newValue
val witnesses = PlayerLookup.tracking(blockEntity)
for (witness in witnesses) {
// The sending player is the person tweaking, we don't wanna overwrite their
// changes spuriously if their connection is laggy.
if (witness != player) {
ServerPlayNetworking.send(
witness,
id,
TweakControl(
packet.position,
packet.control,
packet.newValue,
).serialize()
)
}
}
}
}
}
fun registerClientReceiver() {
ClientPlayNetworking.registerGlobalReceiver(id) { client, _, buffer, _ ->
val packet = deserialize(buffer)
client.execute {
val world = client.world
if (world != null) {
val blockEntity =
world.getBlockEntity(packet.position) as? DeviceBlockEntity
?: return@execute
val controlName =
ControlName.fromString(packet.control.toString()) ?: return@execute
val control = blockEntity.controlMap[controlName]
control?.value = packet.newValue
}
}
}
}
private fun deserialize(buffer: PacketByteBuf) =
TweakControl(
position = buffer.readBlockPos(),
control = buffer.readIdentifier(),
newValue = buffer.readFloat(),
)
}
fun serialize(): PacketByteBuf {
val buffer = PacketByteBufs.create()
buffer.writeBlockPos(position)
buffer.writeIdentifier(control)
buffer.writeFloat(newValue)
return buffer
}
}

View file

@ -0,0 +1,8 @@
package net.liquidev.dawd3.render
import net.minecraft.util.Identifier
data class Atlas(
val asset: Identifier,
val size: Int,
)

View file

@ -0,0 +1,8 @@
package net.liquidev.dawd3.render
data class Icon(
val u: Int,
val v: Int,
val width: Int,
val height: Int,
)

View file

@ -0,0 +1,23 @@
package net.liquidev.dawd3.render
data class NinePatch(
val u: Int,
val v: Int,
val width: Int,
val height: Int,
val borderTop: Int,
val borderBottom: Int,
val borderLeft: Int,
val borderRight: Int,
) {
constructor(u: Int, v: Int, width: Int, height: Int, border: Int) : this(
u,
v,
width,
height,
border,
border,
border,
border,
)
}

View file

@ -0,0 +1,305 @@
package net.liquidev.dawd3.render
import com.mojang.blaze3d.systems.RenderSystem
import net.liquidev.dawd3.common.Radians
import net.liquidev.dawd3.common.lerp
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
object Render {
fun icon(
matrices: MatrixStack,
x: Int,
y: Int,
width: Int,
height: Int,
atlas: Atlas,
icon: Icon,
) {
RenderSystem.setShaderTexture(0, atlas.asset)
DrawableHelper.drawTexture(
matrices,
x,
y,
width,
height,
icon.u.toFloat(),
icon.v.toFloat(),
icon.width,
icon.height,
atlas.size,
atlas.size
)
}
fun icon(matrices: MatrixStack, x: Int, y: Int, atlas: Atlas, icon: Icon) {
icon(matrices, x, y, icon.width, icon.height, atlas, icon)
}
fun line(
matrices: MatrixStack,
x1: Float,
y1: Float,
x2: Float,
y2: Float,
thickness: Float,
atlas: Atlas,
textureStrip: TextureStrip,
) {
if (x1 == x2 && y1 == y2) return
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, atlas.asset)
val matrix = matrices.peek().positionMatrix
val buffer = Tessellator.getInstance().buffer
val dx = x2 - x1
val dy = y2 - y1
val length = sqrt(dx * dx + dy * dy)
val ndx = dx / length
val ndy = dy / length
buffer.begin(VertexFormat.DrawMode.TRIANGLE_STRIP, VertexFormats.POSITION_TEXTURE)
buffer.vertex(matrix, x2 - ndy * thickness, y2 + ndx * thickness, 0f)
buffer.texture(textureStrip.u1 / atlas.size, textureStrip.v2 / atlas.size)
buffer.next()
buffer.vertex(matrix, x2 + ndy * thickness, y2 - ndx * thickness, 0f)
buffer.texture(textureStrip.u2 / atlas.size, textureStrip.v2 / atlas.size)
buffer.next()
buffer.vertex(matrix, x1 - ndy * thickness, y1 + ndx * thickness, 0f)
buffer.texture(textureStrip.u1 / atlas.size, textureStrip.v1 / atlas.size)
buffer.next()
buffer.vertex(matrix, x1 + ndy * thickness, y1 - ndx * thickness, 0f)
buffer.texture(textureStrip.u2 / atlas.size, textureStrip.v1 / atlas.size)
buffer.next()
Tessellator.getInstance().draw()
}
fun arcOutline(
matrices: MatrixStack,
centerX: Float,
centerY: Float,
radius: Float,
thickness: Float,
startAngle: Radians,
endAngle: Radians,
vertexCount: Int,
atlas: Atlas,
textureStrip: TextureStrip,
) {
RenderSystem.setShader(GameRenderer::getPositionTexShader)
RenderSystem.setShaderTexture(0, atlas.asset)
val matrix = matrices.peek().positionMatrix
val buffer = Tessellator.getInstance().buffer
// delta is the angle between subdivisions of the circle.
val delta = (endAngle.value - startAngle.value) / vertexCount
// alpha is the isosceles trapezoids' acute angle, which we need to correct the thickness
// and create a proper mitre joint.
val alpha = (PI.toFloat() - delta) / 2
val offset = thickness / sin(alpha)
val innerRadius = radius - offset / 2
val cos = cos(delta)
val sin = sin(delta)
var angleX = cos(startAngle.value)
var angleY = sin(startAngle.value)
val u1 = textureStrip.u1 / atlas.size
val u2 = textureStrip.u2 / atlas.size
buffer.begin(VertexFormat.DrawMode.TRIANGLE_STRIP, VertexFormats.POSITION_TEXTURE)
for (i in 0..vertexCount) {
val innerX = centerX + angleX * innerRadius
val innerY = centerY + angleY * innerRadius
val outerX = centerX + angleX * (innerRadius + offset)
val outerY = centerY + angleY * (innerRadius + offset)
val t = i.toFloat() / vertexCount
val v = lerp(textureStrip.v1, textureStrip.v2, t) / atlas.size
buffer.vertex(matrix, outerX, outerY, 0f)
buffer.texture(u2, v)
buffer.next()
buffer.vertex(matrix, innerX, innerY, 0f)
buffer.texture(u1, v)
buffer.next()
val aX = angleX
val aY = angleY
angleX = aX * cos - aY * sin
angleY = aX * sin + aY * cos
}
Tessellator.getInstance().draw()
}
fun ninePatch(
matrices: MatrixStack,
x: Int,
y: Int,
width: Int,
height: Int,
atlas: Atlas,
ninePatch: NinePatch,
) {
RenderSystem.setShaderTexture(0, atlas.asset)
// Top left
DrawableHelper.drawTexture(
matrices,
x,
y,
ninePatch.borderLeft,
ninePatch.borderTop,
ninePatch.u.toFloat(),
ninePatch.v.toFloat(),
ninePatch.borderLeft,
ninePatch.borderTop,
atlas.size,
atlas.size
)
// Top center
DrawableHelper.drawTexture(
matrices,
x + ninePatch.borderLeft,
y,
width - ninePatch.borderLeft - ninePatch.borderRight,
ninePatch.borderTop,
(ninePatch.u + ninePatch.borderLeft).toFloat(),
ninePatch.v.toFloat(),
ninePatch.width - ninePatch.borderLeft - ninePatch.borderRight,
ninePatch.borderTop,
atlas.size,
atlas.size
)
// Top right
DrawableHelper.drawTexture(
matrices,
x + width - ninePatch.borderRight,
y,
ninePatch.borderRight,
ninePatch.borderTop,
(ninePatch.u + ninePatch.width - ninePatch.borderRight).toFloat(),
ninePatch.v.toFloat(),
ninePatch.borderRight,
ninePatch.borderTop,
atlas.size,
atlas.size
)
// Middle left
DrawableHelper.drawTexture(
matrices,
x,
y + ninePatch.borderTop,
ninePatch.borderLeft,
height - ninePatch.borderTop - ninePatch.borderBottom,
ninePatch.u.toFloat(),
(ninePatch.v + ninePatch.borderTop).toFloat(),
ninePatch.borderLeft,
ninePatch.height - ninePatch.borderTop - ninePatch.borderBottom,
atlas.size,
atlas.size
)
// Center
DrawableHelper.drawTexture(
matrices,
x + ninePatch.borderLeft,
y + ninePatch.borderTop,
width - ninePatch.borderLeft - ninePatch.borderRight,
height - ninePatch.borderTop - ninePatch.borderBottom,
(ninePatch.u + ninePatch.borderLeft).toFloat(),
(ninePatch.v + ninePatch.borderTop).toFloat(),
ninePatch.width - ninePatch.borderLeft - ninePatch.borderRight,
ninePatch.height - ninePatch.borderTop - ninePatch.borderBottom,
atlas.size,
atlas.size
)
// Middle right
DrawableHelper.drawTexture(
matrices,
x + width - ninePatch.borderRight,
y + ninePatch.borderTop,
ninePatch.borderLeft,
height - ninePatch.borderTop - ninePatch.borderBottom,
(ninePatch.u + ninePatch.width - ninePatch.borderRight).toFloat(),
(ninePatch.v + ninePatch.borderTop).toFloat(),
ninePatch.borderLeft,
ninePatch.height - ninePatch.borderTop - ninePatch.borderBottom,
atlas.size,
atlas.size
)
// Bottom left
DrawableHelper.drawTexture(
matrices,
x,
y + height - ninePatch.borderBottom,
ninePatch.borderLeft,
ninePatch.borderBottom,
ninePatch.u.toFloat(),
(ninePatch.v + ninePatch.height - ninePatch.borderBottom).toFloat(),
ninePatch.borderLeft,
ninePatch.borderBottom,
atlas.size,
atlas.size
)
// Bottom center
DrawableHelper.drawTexture(
matrices,
x + ninePatch.borderLeft,
y + height - ninePatch.borderBottom,
width - ninePatch.borderLeft - ninePatch.borderRight,
ninePatch.borderBottom,
(ninePatch.u + ninePatch.borderLeft).toFloat(),
(ninePatch.v + ninePatch.height - ninePatch.borderBottom).toFloat(),
ninePatch.width - ninePatch.borderLeft - ninePatch.borderRight,
ninePatch.borderBottom,
atlas.size,
atlas.size
)
// Bottom right
DrawableHelper.drawTexture(
matrices,
x + width - ninePatch.borderRight,
y + height - ninePatch.borderBottom,
ninePatch.borderRight,
ninePatch.borderBottom,
(ninePatch.u + ninePatch.width - ninePatch.borderRight).toFloat(),
(ninePatch.v + ninePatch.height - ninePatch.borderBottom).toFloat(),
ninePatch.borderRight,
ninePatch.borderBottom,
atlas.size,
atlas.size
)
}
fun textCentered(matrices: MatrixStack, centerX: Int, y: Int, text: Text, color: Int) {
val textRenderer = MinecraftClient.getInstance().textRenderer
val textWidth = textRenderer.getWidth(text)
val x = centerX - textWidth / 2
textRenderer.draw(
matrices,
text,
x.toFloat(),
y.toFloat(),
color
)
}
}

View file

@ -0,0 +1,4 @@
package net.liquidev.dawd3.render
/** Vertical texture strip. */
data class TextureStrip(val u1: Float, val v1: Float, val u2: Float, val v2: Float)

View file

@ -6,18 +6,19 @@ 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.
*/
private val nonModel = arrayOf(
private val nonModelBlockTextures = arrayOf(
Identifier(Mod.id, "device/cable"),
)
fun initializeClient() {
ClientSpriteRegistryCallback.event(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE)
.register { _, registry ->
for (id in nonModel) {
for (id in nonModelBlockTextures) {
registry.register(id)
}
}

View file

@ -0,0 +1,128 @@
package net.liquidev.dawd3.ui
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.liquidev.dawd3.Mod
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.render.Atlas
import net.liquidev.dawd3.render.Icon
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.ui.widget.Widget
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.client.world.ClientWorld
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
@Environment(EnvType.CLIENT)
class Rack(
val world: ClientWorld,
shownDevices: Iterable<BlockPos>,
) : Screen(Text.translatable("screen.dawd3.rack.title")) {
private data class OpenWidget(
val blockPosition: BlockPos,
val widget: Widget,
)
private var openWidgets = run {
var windowX = 32
ArrayList(shownDevices.mapNotNull { blockPosition ->
val blockEntity = world.getBlockEntity(blockPosition) as DeviceBlockEntity
blockEntity.descriptor.openUI(blockEntity.controls, windowX, 32)
?.let { widget ->
windowX += widget.width + 8
OpenWidget(blockPosition, widget)
}
})
}
fun hasOpenWindows(): Boolean = openWidgets.isNotEmpty()
override fun render(matrices: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrices)
// Handle blocks being destroyed behind our back.
openWidgets.removeIf { world.getBlockEntity(it.blockPosition) !is DeviceBlockEntity }
if (openWidgets.isEmpty()) {
close()
}
for (openWidget in openWidgets) {
openWidget.widget.draw(matrices, mouseX, mouseY, delta)
}
Render.icon(matrices, 8, 8, badge.width * 2, badge.height * 2, atlas, badge)
}
private fun propagateEvent(event: Event): Boolean {
for (openWidget in openWidgets) {
if (openWidget.widget.event(
EventContext(world, openWidget.blockPosition),
event.relativeTo(
openWidget.widget.x.toDouble(),
openWidget.widget.y.toDouble(),
)
) == null
) return true
}
return false
}
override fun mouseMoved(mouseX: Double, mouseY: Double) {
propagateEvent(MouseMove(mouseX, mouseY))
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean =
propagateEvent(MouseButton(Action.Down, mouseX, mouseY, button))
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean =
propagateEvent(MouseButton(Action.Up, mouseX, mouseY, button))
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
return if (super.keyPressed(keyCode, scanCode, modifiers)) {
true
} else if (client?.options?.inventoryKey?.matchesKey(keyCode, scanCode) == true) {
// Handle the player pressing 'E' to close the rack, since even though it is not an
// inventory, it's just more intuitive that way.
close()
true
} else {
false
}
}
override fun shouldPause(): Boolean = false
companion object {
val atlas = Atlas(asset = Identifier(Mod.id, "textures/ui/rack.png"), size = 64)
val badge = Icon(u = 0, v = 16, width = 6, height = 3)
// TODO: This "adjacent device" system is kind of janky but it allows for pretty nice
// organization of your devices into multiple racks of sorts. Maybe in the future we can
// think of collecting non-adjacent devices as well.
fun collectAdjacentDevices(world: World, position: BlockPos): HashSet<BlockPos> {
val result = hashSetOf<BlockPos>()
collectAdjacentDevicesRec(result, world, position)
return result
}
private fun collectAdjacentDevicesRec(
outBlockPositions: HashSet<BlockPos>,
world: World,
position: BlockPos,
) {
if (position !in outBlockPositions && world.getBlockEntity(position) is DeviceBlockEntity) {
outBlockPositions.add(position)
collectAdjacentDevicesRec(outBlockPositions, world, position.up())
collectAdjacentDevicesRec(outBlockPositions, world, position.down())
collectAdjacentDevicesRec(outBlockPositions, world, position.north())
collectAdjacentDevicesRec(outBlockPositions, world, position.south())
collectAdjacentDevicesRec(outBlockPositions, world, position.east())
collectAdjacentDevicesRec(outBlockPositions, world, position.west())
}
}
}
}

View file

@ -0,0 +1,51 @@
package net.liquidev.dawd3.ui
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.BlockPos
sealed interface Event {
fun relativeTo(x: Double, y: Double): Event
}
enum class Action {
Down,
Up,
}
data class MouseButton(
val action: Action,
val mouseX: Double,
val mouseY: Double,
val absoluteMouseX: Double,
val absoluteMouseY: Double,
val button: Int,
) : Event {
constructor(action: Action, mouseX: Double, mouseY: Double, button: Int) : this(
action,
mouseX,
mouseY,
mouseX,
mouseY,
button
)
override fun relativeTo(x: Double, y: Double) =
MouseButton(action, mouseX - x, mouseY - y, absoluteMouseX, absoluteMouseY, button)
}
data class MouseMove(
val mouseX: Double,
val mouseY: Double,
val absoluteMouseX: Double,
val absoluteMouseY: Double,
) : Event {
constructor(mouseX: Double, mouseY: Double) : this(mouseX, mouseY, mouseX, mouseY)
override fun relativeTo(x: Double, y: Double): Event =
MouseMove(mouseX - x, mouseY - y, absoluteMouseX, absoluteMouseY)
}
class EventContext(
val world: ClientWorld,
val blockPosition: BlockPos,
)

View file

@ -0,0 +1,174 @@
package net.liquidev.dawd3.ui.widget
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.liquidev.dawd3.audio.device.Control
import net.liquidev.dawd3.common.Degrees
import net.liquidev.dawd3.common.Radians
import net.liquidev.dawd3.common.clamp
import net.liquidev.dawd3.common.mapRange
import net.liquidev.dawd3.net.ControlTweaked
import net.liquidev.dawd3.net.TweakControl
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.TextureStrip
import net.liquidev.dawd3.ui.*
import net.minecraft.client.MinecraftClient
import net.minecraft.client.util.InputUtil
import net.minecraft.client.util.math.MatrixStack
import org.lwjgl.glfw.GLFW
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
class Knob(
x: Int,
y: Int,
val control: Control,
val min: Float,
val max: Float,
val color: Color,
val sensitivity: Float = 0.25f,
) : Widget(x, y) {
override val width = 20
override val height = 20
private data class DraggingInfo(var previousMouseY: Double)
private var draggingInfo: DraggingInfo? = null
override fun drawContent(matrices: MatrixStack, mouseX: Int, mouseY: Int, deltaTime: Float) {
val centerX = width.toFloat() / 2
val centerY = height.toFloat() / 2
val radius = width.toFloat() / 2
val valueAngle = mapRange(control.value, min, max, startAngle.value, endAngle.value)
val zeroAngle = clamp(
mapRange(0f, min, max, startAngle.value, endAngle.value),
startAngle.value,
endAngle.value
)
Render.arcOutline(
matrices,
centerX,
centerY,
radius - lineThickness * 0.75f,
lineThickness * 0.75f,
Radians(min(valueAngle, zeroAngle)),
Radians(max(valueAngle, zeroAngle)),
vertexCount = 16,
Rack.atlas,
coloredStrip(color)
)
Render.arcOutline(
matrices,
centerX,
centerY,
radius,
lineThickness,
startAngle,
endAngle,
vertexCount = 16,
Rack.atlas,
blackStrip
)
Render.line(
matrices,
centerX + cos(valueAngle) * (radius * 0.25f),
centerY + sin(valueAngle) * (radius * 0.25f),
centerX + cos(valueAngle) * radius,
centerY + sin(valueAngle) * radius,
lineThickness / 2,
Rack.atlas,
blackStrip
)
}
override fun event(context: EventContext, event: Event): Event? {
val client = MinecraftClient.getInstance()
when (event) {
is MouseButton -> if (event.button == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
when (event.action) {
Action.Down -> if (
containsRelativePoint(
event.mouseX.toInt(),
event.mouseY.toInt()
)
) {
draggingInfo = DraggingInfo(event.absoluteMouseY)
val guiScale =
client.options.guiScale.value.toDouble()
InputUtil.setCursorParameters(
client.window.handle,
GLFW.GLFW_CURSOR_DISABLED,
event.absoluteMouseX * guiScale,
event.absoluteMouseY * guiScale,
)
return null
}
Action.Up -> {
val draggingInfo = draggingInfo
if (draggingInfo != null) {
InputUtil.setCursorParameters(
client.window.handle,
GLFW.GLFW_CURSOR_NORMAL,
0.0,
0.0,
)
ClientPlayNetworking.send(
ControlTweaked.id,
ControlTweaked(context.blockPosition).serialize()
)
this.draggingInfo = null
}
}
}
}
is MouseMove -> {
val draggingInfo = draggingInfo
if (draggingInfo != null) {
val guiScale = client.options.guiScale.value.toFloat()
val deltaY = (draggingInfo.previousMouseY - event.absoluteMouseY) * guiScale
ClientPlayNetworking.send(
TweakControl.id,
TweakControl(
context.blockPosition,
control.descriptor.name.id,
newValue = alterValue(control.value, by = deltaY.toFloat()),
).serialize()
)
// Reflect the change locally immediately for lower latency.
control.value = alterValue(control.value, by = deltaY.toFloat())
draggingInfo.previousMouseY = event.absoluteMouseY
}
}
}
return event
}
private fun alterValue(value: Float, by: Float): Float =
max(min(value + by * sensitivity, max), min)
enum class Color(val index: Int) {
Red(0),
Orange(1),
Yellow(2),
Green(3),
Blue(4),
Purple(5),
}
companion object {
private val startAngle = Degrees(135f).toRadians()
private val endAngle = Degrees(405f).toRadians()
private const val lineThickness = 2f
private val blackStrip = TextureStrip(16f, 16f, 16f, 32f)
private fun coloredStrip(color: Color): TextureStrip {
val u = 18f + color.index.toFloat()
return TextureStrip(u, 16f, u, 32f)
}
}
}

View file

@ -0,0 +1,50 @@
package net.liquidev.dawd3.ui.widget
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.EventContext
import net.minecraft.client.util.math.MatrixStack
abstract class Widget(var x: Int, var y: Int) {
abstract val width: Int
abstract val height: Int
protected abstract fun drawContent(
matrices: MatrixStack,
mouseX: Int,
mouseY: Int,
deltaTime: Float,
)
/** Returns non-null to propagate the event, or null to consume it. */
abstract fun event(context: EventContext, event: Event): Event?
fun draw(matrices: MatrixStack, mouseX: Int, mouseY: Int, deltaTime: Float) {
matrices.push()
matrices.translate(x.toDouble(), y.toDouble(), 0.0)
drawContent(matrices, mouseX, mouseY, deltaTime)
matrices.pop()
}
fun containsRelativePoint(x: Int, y: Int) =
x >= 0 && y >= 0 && x <= width && y <= height
companion object {
/** Propagates the event through the given iterable, returns whether it was consumed in the end. */
fun propagateEvent(
context: EventContext,
event: Event,
through: Iterable<Widget>,
): Boolean {
for (widget in through) {
if (widget.event(
context,
event.relativeTo(widget.x.toDouble(), widget.y.toDouble())
) == null
) {
return true
}
}
return false
}
}
}

View file

@ -0,0 +1,49 @@
package net.liquidev.dawd3.ui.widget
import net.liquidev.dawd3.render.NinePatch
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.ui.Event
import net.liquidev.dawd3.ui.EventContext
import net.liquidev.dawd3.ui.Rack
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
class Window(x: Int, y: Int, override val width: Int, override val height: Int, val title: Text) :
Widget(x, y) {
val children = mutableListOf<Widget>()
override fun drawContent(matrices: MatrixStack, mouseX: Int, mouseY: Int, deltaTime: Float) {
Render.ninePatch(matrices, 2, 2, width, height, Rack.atlas, windowShadow)
Render.ninePatch(matrices, 0, 0, width, height, Rack.atlas, windowBackground)
Render.textCentered(
matrices,
width / 2,
4,
title,
0x111111
)
for (child in children) {
child.draw(matrices, mouseX, mouseY, deltaTime)
}
}
override fun event(context: EventContext, event: Event): Event? {
return if (propagateEvent(context, event, children)) null else event
}
companion object {
val windowBackground = NinePatch(
u = 0, v = 0,
width = 16, height = 16,
border = 3
)
val windowShadow = NinePatch(
u = 16, v = 0,
width = 16, height = 16,
border = 3
)
}
}

View file

@ -18,5 +18,6 @@
"item.dawd3.black_patch_cable": "Black Patch Cable",
"block.dawd3.speaker": "Speaker",
"block.dawd3.sine_oscillator": "Sine Oscillator",
"block.dawd3.fader": "Fader"
"block.dawd3.knob": "Knob",
"screen.dawd3.rack.title": "Rack"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

View file

@ -1,5 +1,7 @@
accessWidener v1 named
accessible method net/minecraft/item/ItemUsageContext getHitResult ()Lnet/minecraft/util/hit/BlockHitResult;
# The functionality for creating render layers is inaccessible by default for some reason and we need it to create a layer for rendering cables.
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/render/RenderLayer$MultiPhase
@ -11,4 +13,4 @@ accessible method net/minecraft/client/render/RenderLayer of (Ljava/lang/String;
accessible field net/minecraft/client/render/RenderPhase ENABLE_LIGHTMAP Lnet/minecraft/client/render/RenderPhase$Lightmap;
accessible field net/minecraft/client/render/RenderPhase SOLID_SHADER Lnet/minecraft/client/render/RenderPhase$Shader;
accessible field net/minecraft/client/render/RenderPhase BLOCK_ATLAS_TEXTURE Lnet/minecraft/client/render/RenderPhase$Texture;
accessible field net/minecraft/client/render/RenderPhase DISABLE_CULLING Lnet/minecraft/client/render/RenderPhase$Cull;
accessible field net/minecraft/client/render/RenderPhase DISABLE_CULLING Lnet/minecraft/client/render/RenderPhase$Cull;