new rack UI

This commit is contained in:
りき萌 2023-05-07 12:48:15 +02:00
parent 766a9e10ee
commit 1a5a31aafe
16 changed files with 443 additions and 47 deletions

Binary file not shown.

View file

@ -51,6 +51,22 @@ class DeviceBlockEntity(
*/
var shelf: UUID? = null
/**
* Order of shelves when the UI is open.
*
* This is propagated and merged between devices upon the UI being opened.
*/
val shelfOrder = mutableListOf<UUID>()
/**
* At which position the device should be sorted in the rack. Lower priorities mean earlier
* positions.
*
* Devices start out with the maximum possible priority and no shelf, which means they'll be
* appended to the end of the sidebar in an undefined order.
*/
var sortPriority = Int.MAX_VALUE
/** NBT compound keys. */
private object Nbt {
const val controls = "controls"
@ -72,6 +88,8 @@ class DeviceBlockEntity(
}
const val shelf = "shelf"
const val shelfOrder = "shelfOrder"
const val sortPriority = "sortPriority"
}
override fun readNbt(nbt: NbtCompound) {
@ -131,9 +149,23 @@ class DeviceBlockEntity(
outputConnections[port] = blockPosition
}
if (nbt.containsUuid(Nbt.shelf)) {
shelf = nbt.getUuid(Nbt.shelf)
shelf = if (nbt.containsUuid(Nbt.shelf)) {
nbt.getUuid(Nbt.shelf)
} else {
null
}
val shelfOrderNbt = nbt.getList(Nbt.shelfOrder, NbtElement.INT_ARRAY_TYPE.toInt())
shelfOrder.clear()
for (i in 0 until shelfOrderNbt.size) {
try {
val uuid = NbtHelper.toUuid(shelfOrderNbt[i])
shelfOrder.add(uuid)
} catch (_: IllegalArgumentException) {
// toUuid may throw an IllegalArgumentException if the UUID doesn't follow the
// expected format. In that case we just ignore the error and move on.
}
}
sortPriority = nbt.getInt(Nbt.sortPriority)
}
override fun writeNbt(nbt: NbtCompound) {
@ -170,6 +202,18 @@ class DeviceBlockEntity(
outputConnectionsNbt.add(connectionNbt)
}
nbt.put(Nbt.outputConnections, outputConnectionsNbt)
if (shelf != null) {
nbt.putUuid(Nbt.shelf, shelf)
} else {
nbt.remove(Nbt.shelf)
}
val shelfOrderNbt = NbtList()
for (shelfUuid in shelfOrder) {
shelfOrderNbt.add(NbtHelper.fromUuid(shelfUuid))
}
nbt.put(Nbt.shelfOrder, shelfOrderNbt)
nbt.putInt(Nbt.sortPriority, sortPriority)
}
override fun onClientLoad(world: ClientWorld) {

View file

@ -0,0 +1,15 @@
package net.liquidev.dawd3.common
fun <T> moveElement(from: MutableList<T>, fromIndex: Int, to: MutableList<T>, toIndex: Int) {
if (from == to && fromIndex != toIndex) {
val element = from.removeAt(fromIndex)
to.add(
if (fromIndex < toIndex) toIndex - 1
else toIndex,
element,
)
} else {
val element = from.removeAt(fromIndex)
to.add(toIndex, element)
}
}

View file

@ -0,0 +1,104 @@
package net.liquidev.dawd3.net
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.block.device.DeviceBlockEntity
import net.liquidev.dawd3.net.serialization.readOptionalUuid
import net.liquidev.dawd3.net.serialization.writeOptionalUuid
import net.liquidev.dawd3.ui.widget.rack.shelves.ShelfEditing
import net.minecraft.block.Block
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import java.util.*
/**
* Assign a shelf and shelf order to a block at the given position.
*
* When sent C2S, the data of the device block entity at the given position is modified to write
* back its shelf UUID and shelf order. The server then propagates the shelf order to every block
* entity in the rack and sends this packet S2C to all witnesses of the propagation.
*/
data class EditRack(
val blockPosition: BlockPos,
val shelf: UUID?,
val shelfOrder: Array<UUID>,
) {
companion object {
val id = Identifier(Mod.id, "edit_rack")
fun registerServerReceiver() {
ServerPlayNetworking.registerGlobalReceiver(id) { server, player, _, buffer, _ ->
val packet = deserialize(buffer)
server.execute {
val blockState = player.world.getBlockState(packet.blockPosition)
val blockEntity =
player.world.getBlockEntity(packet.blockPosition) as? DeviceBlockEntity
?: return@execute
blockEntity.shelf = packet.shelf
ShelfEditing.propagateShelfOrderData(
player.world,
packet.blockPosition,
packet.shelfOrder,
)
blockEntity.markDirty()
player.world.updateListeners(
packet.blockPosition,
blockState,
blockState,
Block.NOTIFY_LISTENERS,
)
for (witness in PlayerLookup.tracking(blockEntity)) {
if (witness != player) {
ServerPlayNetworking.send(witness, id, buffer)
}
}
}
}
}
fun registerClientReceiver() {
}
private fun deserialize(buffer: PacketByteBuf) =
EditRack(
blockPosition = buffer.readBlockPos(),
shelf = buffer.readOptionalUuid(),
shelfOrder = Array(buffer.readVarInt()) { buffer.readUuid() }
)
}
fun serialize(): PacketByteBuf =
PacketByteBufs.create()
.writeBlockPos(blockPosition)
.writeOptionalUuid(shelf)
.writeVarInt(shelfOrder.size)
.apply {
for (shelfUuid in shelfOrder) {
writeUuid(shelfUuid)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EditRack
if (blockPosition != other.blockPosition) return false
if (shelf != other.shelf) return false
return shelfOrder.contentEquals(other.shelfOrder)
}
override fun hashCode(): Int {
var result = blockPosition.hashCode()
result = 31 * result + (shelf?.hashCode() ?: 0)
result = 31 * result + shelfOrder.contentHashCode()
return result
}
}

View file

@ -11,5 +11,7 @@ object Packets {
fun registerServerReceivers() {
TweakControl.registerServerReceiver()
ControlTweaked.registerServerReceiver()
EditRack.registerServerReceiver()
ReorderRack.registerServerReceiver()
}
}

View file

@ -0,0 +1,67 @@
package net.liquidev.dawd3.net
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.block.device.DeviceBlockEntity
import net.minecraft.block.Block
import net.minecraft.network.PacketByteBuf
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
data class ReorderRack(val entries: List<Entry>) {
data class Entry(val blockPosition: BlockPos, val sortPriority: Int)
companion object {
val id = Identifier(Mod.id, "reorder_rack")
fun registerServerReceiver() {
ServerPlayNetworking.registerGlobalReceiver(id) { server, player, _, buffer, _ ->
val packet = deserialize(buffer)
server.execute {
for (entry in packet.entries) {
val blockEntity =
player.world.getBlockEntity(entry.blockPosition) as? DeviceBlockEntity
?: continue
blockEntity.sortPriority = entry.sortPriority
blockEntity.markDirty()
val blockState = player.world.getBlockState(entry.blockPosition)
player.world.updateListeners(
entry.blockPosition,
blockState,
blockState,
Block.NOTIFY_LISTENERS
)
for (witness in PlayerLookup.tracking(blockEntity)) {
if (witness != player) {
ServerPlayNetworking.send(witness, id, buffer)
}
}
}
}
}
}
private fun deserialize(buffer: PacketByteBuf) =
ReorderRack(
entries = MutableList(buffer.readVarInt()) {
Entry(
blockPosition = buffer.readBlockPos(),
sortPriority = buffer.readVarInt(),
)
}
)
}
fun serialize(): PacketByteBuf {
val buffer = PacketByteBufs.create()
buffer.writeVarInt(entries.size)
for (entry in entries) {
buffer.writeBlockPos(entry.blockPosition)
buffer.writeVarInt(entry.sortPriority)
}
return buffer
}
}

View file

@ -0,0 +1,24 @@
package net.liquidev.dawd3.net.serialization
import net.minecraft.network.PacketByteBuf
import java.util.*
val zeroUuid = UUID(0L, 0L)
fun PacketByteBuf.readOptionalUuid(): UUID? {
val uuid = readUuid()
return if (uuid.leastSignificantBits == 0L && uuid.mostSignificantBits == 0L) {
null
} else {
uuid
}
}
fun PacketByteBuf.writeOptionalUuid(uuid: UUID?): PacketByteBuf {
if (uuid != null) {
writeUuid(uuid)
} else {
writeUuid(zeroUuid)
}
return this
}

View file

@ -27,6 +27,7 @@ object Render {
sprite: Sprite,
) {
RenderSystem.setShaderTexture(0, atlas.asset)
RenderSystem.enableBlend()
DrawableHelper.drawTexture(
matrices,
x.toInt(),
@ -166,6 +167,7 @@ object Render {
) {
// Temporarily defined to just defer to DrawableHelper, later on we should introduce support
// for rendering at floating-point coordinates.
RenderSystem.enableBlend()
DrawableHelper.drawTexture(
matrices,
x.toInt(),
@ -365,7 +367,7 @@ object Render {
tooltip(matrices, x, y, text, atlas, tooltip)
}
inline fun colorized(r: Float, g: Float, b: Float, a: Float, then: () -> Unit) {
inline fun colorized(r: Float, g: Float, b: Float, a: Float, crossinline then: () -> Unit) {
val (oldR, oldG, oldB, oldA) = RenderSystem.getShaderColor()
RenderSystem.setShaderColor(r, g, b, a)
RenderSystem.enableBlend() // sigh

View file

@ -21,7 +21,8 @@ class RackScreen(
shownDevices: Iterable<BlockPos>,
) : Screen(Text.translatable("screen.dawd3.rack.title")) {
private val rootWidget = Rack(width.toFloat(), height.toFloat(), world, shownDevices)
private val rootWidget =
Rack(width.toFloat(), height.toFloat(), world, shownDevices)
override fun init() {
super.init()

View file

@ -18,7 +18,7 @@ abstract class Widget<in C, out M : Message>(var x: Float, var y: Float) {
abstract fun event(context: C, event: Event): M
inline fun drawInside(matrices: MatrixStack, draw: () -> Unit) {
inline fun drawInside(matrices: MatrixStack, crossinline draw: () -> Unit) {
matrices.push()
matrices.translate(x.toDouble(), y.toDouble(), 0.0)
draw()

View file

@ -1,6 +1,10 @@
package net.liquidev.dawd3.ui.widget.rack
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.liquidev.dawd3.common.moveElement
import net.liquidev.dawd3.net.EditRack
import net.liquidev.dawd3.net.ReorderRack
import net.liquidev.dawd3.render.NinePatch
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.TextureStrip
@ -12,6 +16,7 @@ import net.minecraft.client.util.math.MatrixStack
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.math.BlockPos
import org.lwjgl.glfw.GLFW
import java.util.*
class Rack(
override var width: Float,
@ -20,10 +25,10 @@ class Rack(
shownDevices: Iterable<BlockPos>,
) : Widget<Unit, Message>(0f, 0f) {
private val sidebar = Sidebar(0f, 0f, 160f, 0f)
private val sidebar = Sidebar(0f, 0f, 0f, 0f)
private val blockPositionsByWidget = hashMapOf<DeviceWidget, BlockPos>()
private val shelves = MutableList(10) { Shelf(0f, 0f, width) }
private val shelves = mutableListOf<Shelf>()
enum class DragPlace {
Sidebar,
@ -33,6 +38,7 @@ class Rack(
data class DraggedWindow(
val window: Window,
val source: DragPlace,
val indexInSource: Int,
val byX: Float,
val byY: Float,
@ -42,14 +48,45 @@ class Rack(
private var draggedWindow: DraggedWindow? = null
init {
val shelvesByUuid = hashMapOf<UUID, Shelf>()
for (blockPosition in shownDevices) {
val blockEntity = world.getBlockEntity(blockPosition) as? DeviceBlockEntity ?: continue
val widget = blockEntity.descriptor.ui?.open(blockEntity.controls, x = 0f, y = 0f)
if (widget != null) {
sidebar.windows.add(widget)
blockPositionsByWidget[widget] = blockPosition
for (shelfUuid in blockEntity.shelfOrder) {
if (shelfUuid !in shelvesByUuid) {
val shelf = Shelf(0f, 0f, width, shelfUuid)
shelves.add(shelf)
shelvesByUuid[shelfUuid] = shelf
}
}
}
for (blockPosition in shownDevices) {
val blockEntity = world.getBlockEntity(blockPosition) as? DeviceBlockEntity ?: continue
val window = blockEntity.descriptor.ui?.open(blockEntity.controls, x = 0f, y = 0f)
if (window != null) {
val shelfUuid = blockEntity.shelf
if (shelfUuid != null) {
shelvesByUuid[shelfUuid]?.windows?.add(window)
} else {
sidebar.windows.add(window)
}
blockPositionsByWidget[window] = blockPosition
}
}
removeEmptyShelvesFromEnd()
addEmptyShelfIfNotPresent()
sortWindowsInContainer(sidebar.windows)
shelves.forEach { sortWindowsInContainer(it.windows) }
}
private fun <W : DeviceWidget> sortWindowsInContainer(container: MutableList<W>) {
container.sortBy { window ->
val blockPosition = blockPositionsByWidget[window]
val blockEntity = world.getBlockEntity(blockPosition) as? DeviceBlockEntity
blockEntity?.sortPriority ?: Int.MAX_VALUE
}
}
private fun <W : DeviceWidget> removeStaleWidgetsFromContainer(container: MutableList<W>) {
@ -67,6 +104,9 @@ class Rack(
for (shelf in shelves) {
shelf.draw(matrices, mouseX, mouseY, deltaTime)
}
if (shelves.size == 1 && shelves.last().windows.isEmpty()) {
shelves.last().drawUsageHint(matrices)
}
sidebar.draw(matrices, mouseX, mouseY, deltaTime)
@ -83,7 +123,6 @@ class Rack(
)
val destination = findDragDestination(mouseX, mouseY)
println(destination)
when (destination?.place) {
DragPlace.Sidebar -> {
sidebar.drawInside(matrices) {
@ -91,7 +130,7 @@ class Rack(
Sidebar.padding
} else if (destination.indexInPlace == sidebar.windows.size) {
val window = sidebar.windows.last()
window.y + window.height - Sidebar.spacingBetweenWindows / 2f
window.y + window.height + Sidebar.spacingBetweenWindows / 2f
} else {
val window = sidebar.windows[destination.indexInPlace]
window.y - Sidebar.spacingBetweenWindows / 2f
@ -143,7 +182,7 @@ class Rack(
source: DragPlace,
sourceShelfIndex: Int = 0,
): Message {
for (window in windows) {
windows.forEachIndexed { index, window ->
val blockPosition = blockPositionsByWidget[window]!!
val windowRelativeEvent = event.relativeTo(window.x, window.y)
val windowMessage =
@ -153,6 +192,7 @@ class Rack(
DraggedWindow(
window,
source,
index,
windowMessage.x,
windowMessage.y,
sourceShelfIndex
@ -192,21 +232,38 @@ class Rack(
if (draggedWindow != null) {
val destination = findDragDestination(event.mouseX, event.mouseY)
if (destination != null) {
removeWindowFromSource(
draggedWindow.source,
draggedWindow.sourceShelfIndex,
draggedWindow.window,
)
when (destination.place) {
DragPlace.Sidebar -> sidebar.windows.add(
destination.indexInPlace,
draggedWindow.window
)
DragPlace.Shelf -> shelves[destination.shelfIndex].windows.add(
destination.indexInPlace,
draggedWindow.window
)
val fromList: MutableList<Window> = when (draggedWindow.source) {
DragPlace.Sidebar -> sidebar.windows
DragPlace.Shelf -> shelves[draggedWindow.sourceShelfIndex].windows
}
val toList: MutableList<Window> = when (destination.place) {
DragPlace.Sidebar -> sidebar.windows
DragPlace.Shelf -> shelves[destination.shelfIndex].windows
}
moveElement(
fromList,
fromIndex = draggedWindow.indexInSource,
toList,
toIndex = destination.indexInPlace
)
val blockPosition = blockPositionsByWidget[draggedWindow.window]
if (blockPosition != null) {
when (destination.place) {
DragPlace.Shelf -> {
val shelf = shelves[destination.shelfIndex]
updateBlockData(blockPosition, shelf.uuid)
}
DragPlace.Sidebar -> {
updateBlockData(blockPosition, null)
}
}
}
removeEmptyShelvesFromEnd()
addEmptyShelfIfNotPresent()
updateBlockPriorities(fromList)
updateBlockPriorities(toList)
reflow()
}
}
@ -216,6 +273,27 @@ class Rack(
return Message.eventIgnored
}
private fun removeEmptyShelvesFromEnd() {
while (shelves.size > 1 && shelves.last().windows.isEmpty()) {
shelves.removeLast()
}
}
private fun addEmptyShelfIfNotPresent() {
if (shelves.size == 0 || shelves.last().windows.isNotEmpty()) {
shelves.add(Shelf(0f, 0f, width, UUID.randomUUID()))
}
}
private fun <W : DeviceWidget> updateBlockPriorities(windows: MutableList<W>) {
val packetEntries = mutableListOf<ReorderRack.Entry>()
windows.forEachIndexed { index, window ->
val blockPosition = blockPositionsByWidget[window] ?: return@forEachIndexed
packetEntries.add(ReorderRack.Entry(blockPosition, index))
}
ClientPlayNetworking.send(ReorderRack.id, ReorderRack(packetEntries).serialize())
}
internal data class DragDestination(
val place: DragPlace,
val indexInPlace: Int,
@ -238,22 +316,8 @@ class Rack(
return null
}
private fun removeWindowFromSource(
source: DragPlace,
indexOfSource: Int,
window: Window,
) {
when (source) {
DragPlace.Sidebar -> {
sidebar.windows.remove(window)
}
DragPlace.Shelf -> {
shelves[indexOfSource].windows.remove(window)
}
}
}
override fun reflow() {
sidebar.width = if (sidebar.windows.isNotEmpty()) 160f else 16f
sidebar.x = width - sidebar.width
sidebar.height = height
sidebar.reflow()
@ -262,11 +326,21 @@ class Rack(
for (shelf in shelves) {
shelf.reflow()
shelf.y = dy
shelf.width = width
shelf.width = width - sidebar.width
dy += shelf.height
}
}
private fun updateBlockData(editedDevicePosition: BlockPos, newShelfUuid: UUID?) {
ClientPlayNetworking.send(
EditRack.id, EditRack(
editedDevicePosition,
shelf = newShelfUuid,
shelfOrder = shelves.map { it.uuid }.toTypedArray()
).serialize()
)
}
companion object {
private val windowDraggingPreview =
NinePatch(u = 48f, v = 0f, width = 8f, height = 8f, border = 2f)

View file

@ -1,5 +1,6 @@
package net.liquidev.dawd3.ui.widget.rack
import net.liquidev.dawd3.common.rgba
import net.liquidev.dawd3.render.Render
import net.liquidev.dawd3.render.TextureStrip
import net.liquidev.dawd3.ui.Event
@ -8,9 +9,16 @@ import net.liquidev.dawd3.ui.RackScreen
import net.liquidev.dawd3.ui.widget.Widget
import net.liquidev.dawd3.ui.widget.Window
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import java.util.*
import kotlin.math.max
class Shelf(x: Float, y: Float, override var width: Float) : Widget<Nothing, Message>(x, y) {
class Shelf(
x: Float,
y: Float,
override var width: Float,
val uuid: UUID,
) : Widget<Nothing, Message>(x, y) {
override var height = 128f
private set
@ -39,6 +47,20 @@ class Shelf(x: Float, y: Float, override var width: Float) : Widget<Nothing, Mes
}
}
fun drawUsageHint(matrices: MatrixStack) {
drawInside(matrices) {
Render.colorized(1f, 1f, 1f, 0.6f) {
Render.textCentered(
matrices,
width / 2f,
height / 2f - 4,
Text.translatable("screen.dawd3.rack.shelf_hint"),
rgba(255, 255, 255, 127)
)
}
}
}
// NOTE: Events here are not handled directly by the Shelf, but rather by the parent rack
// screen which has extra context that needs to be passed down to windows. Hence this cannot
// be called (context is Nothing.)

View file

@ -12,7 +12,7 @@ import net.minecraft.client.util.math.MatrixStack
class Sidebar(
x: Float,
y: Float,
override val width: Float,
override var width: Float,
override var height: Float,
) : Widget<Nothing, Message>(x, y) {

View file

@ -0,0 +1,41 @@
package net.liquidev.dawd3.ui.widget.rack.shelves
import net.liquidev.dawd3.block.device.DeviceBlockEntity
import net.minecraft.block.Block
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import java.util.*
object ShelfEditing {
fun propagateShelfOrderData(world: World, from: BlockPos, shelfOrder: Array<UUID>) {
propagateShelfOrderDataRec(world, from, HashSet(), shelfOrder)
}
private fun propagateShelfOrderDataRec(
world: World,
position: BlockPos,
traversed: HashSet<BlockPos>,
sourceShelfOrder: Array<UUID>,
) {
val thisBlockEntity = world.getBlockEntity(position) as? DeviceBlockEntity ?: return
if (position !in traversed) {
traversed.add(position)
// Maybe not the fastest thing... but I don't want to share a single piece of mutable
// data between all the block entities, as that can cause weird bugs later down the line.
thisBlockEntity.shelfOrder.clear()
thisBlockEntity.shelfOrder.addAll(sourceShelfOrder)
thisBlockEntity.markDirty()
val blockState = world.getBlockState(position)
world.updateListeners(position, blockState, blockState, Block.NOTIFY_LISTENERS)
propagateShelfOrderDataRec(world, position.up(), traversed, sourceShelfOrder)
propagateShelfOrderDataRec(world, position.down(), traversed, sourceShelfOrder)
propagateShelfOrderDataRec(world, position.north(), traversed, sourceShelfOrder)
propagateShelfOrderDataRec(world, position.south(), traversed, sourceShelfOrder)
propagateShelfOrderDataRec(world, position.east(), traversed, sourceShelfOrder)
propagateShelfOrderDataRec(world, position.west(), traversed, sourceShelfOrder)
}
}
}

View file

@ -32,7 +32,7 @@
"block.dawd3.saw_oscillator": "Saw Oscillator",
"block.dawd3.sine_oscillator": "Sine Oscillator",
"block.dawd3.triangle_oscillator": "Triangle Oscillator",
"screen.dawd3.rack.title": "Rack",
"screen.dawd3.rack.shelf_hint": "Drag devices here to organize them",
"dawd3.control.dawd3.adsr.attack": "ATT",
"dawd3.control.dawd3.adsr.decay": "DEC",
"dawd3.control.dawd3.adsr.sustain": "SUS",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

After

Width:  |  Height:  |  Size: 548 B

Before After
Before After