Reliably deliver events to server components

This commit is contained in:
Fingercomp 2023-07-14 02:17:12 +07:00
parent 7873d9957e
commit 2d791b0ab8
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
8 changed files with 96 additions and 101 deletions

View File

@ -6,12 +6,11 @@ import ocelot.desktop.inventory.SyncedInventory.SlotStatus.SlotStatus
import ocelot.desktop.inventory.SyncedInventory.SyncDirection.SyncDirection
import ocelot.desktop.inventory.SyncedInventory._
import ocelot.desktop.inventory.traits.ComponentItem
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.widget.EventHandlers
import ocelot.desktop.ui.event.{BrainEvent, Event, EventAware}
import ocelot.desktop.util.Logging
import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor
import totoro.ocelot.brain.entity.traits.{Entity, Environment, Inventory => BrainInventory}
import totoro.ocelot.brain.event.{InventoryEntityAddedEvent, InventoryEntityRemovedEvent}
import totoro.ocelot.brain.event.{InventoryEntityAddedEvent, InventoryEntityRemovedEvent, NodeEvent}
import totoro.ocelot.brain.nbt.NBTTagCompound
import scala.annotation.tailrec
@ -28,7 +27,7 @@ import scala.annotation.tailrec
* While synchronizing, relies on the convergence of changes, but guards against stack overflows
* by limiting the recursion depth.
*/
trait SyncedInventory extends PersistedInventory with Logging {
trait SyncedInventory extends EventAware with PersistedInventory with Logging {
override type I <: Item with ComponentItem
// to avoid synchronization while we're loading stuff
@ -38,8 +37,6 @@ trait SyncedInventory extends PersistedInventory with Logging {
private var syncFuel: Int = _
refuel()
protected def eventHandlers: EventHandlers
/**
* The backing [[BrainInventory brain Inventory]].
*/
@ -78,7 +75,7 @@ trait SyncedInventory extends PersistedInventory with Logging {
case None =>
throw ItemLoadException(
s"an item class ${itemClass.getName} cannot be instantiated " +
s"with $entity (class ${entity.getClass.getName})",
s"with $entity (class ${entity.getClass.getName})"
)
}
}
@ -91,6 +88,10 @@ trait SyncedInventory extends PersistedInventory with Logging {
slotNbt.setString(SlotEntityAddressTag, item.component.node.address)
}
override def shouldReceiveEventsFor(address: String): Boolean =
super.shouldReceiveEventsFor(address) ||
inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address))
eventHandlers += {
case BrainEvent(InventoryEntityAddedEvent(slot, _)) if slot.inventory.owner eq brainInventory =>
sync(slot.index, SyncDirection.BrainToDesktop)
@ -99,6 +100,17 @@ trait SyncedInventory extends PersistedInventory with Logging {
sync(slot.index, SyncDirection.BrainToDesktop)
}
override def handleEvent(event: Event): Unit = {
super.handleEvent(event)
for (slot <- inventoryIterator; item <- slot.get) {
event match {
case n: NodeEvent if !item.shouldReceiveEventsFor(n.address) => // ignore
case _ => item.handleEvent(event)
}
}
}
override def onItemAdded(slot: Slot): Unit = {
if (!isLoading) {
sync(slot.index, SyncDirection.DesktopToBrain)
@ -112,7 +124,9 @@ trait SyncedInventory extends PersistedInventory with Logging {
}
def syncSlots(direction: SyncDirection = SyncDirection.Reconcile): Unit = {
val occupiedSlots = inventoryIterator.map(_.index).toSet
val occupiedSlots = inventoryIterator
.map(_.index)
.toSet
.union(brainInventory.inventory.iterator.map(_.index).toSet)
for (slotIndex <- occupiedSlots)
@ -124,8 +138,7 @@ trait SyncedInventory extends PersistedInventory with Logging {
try {
doSync(slotIndex, direction)
}
finally {
} finally {
if (initialSync)
refuel()
}
@ -141,29 +154,28 @@ trait SyncedInventory extends PersistedInventory with Logging {
if (syncFuel < 0) {
// ignore: the limit has already been reached
}
else if (syncFuel == 0) {
} else if (syncFuel == 0) {
logger.error(
s"Got trapped in an infinite loop while trying to synchronize the slot $slotIndex " +
s"in $this (class ${this.getClass.getName})!",
s"in $this (class ${this.getClass.getName})!"
)
logger.error(
"The item in the slot: " +
Slot(slotIndex).get.map(item => s"$item (class ${item.getClass.getName})").getOrElse("<empty>"),
Slot(slotIndex).get.map(item => s"$item (class ${item.getClass.getName})").getOrElse("<empty>")
)
logger.error(
"The entity if the slot: " +
brainInventory.inventory(slotIndex)
brainInventory
.inventory(slotIndex)
.get
.map(entity => s"$entity (class ${entity.getClass.getName})")
.getOrElse("<empty>"),
.getOrElse("<empty>")
)
logger.error("Breaking the loop forcefully by removing the items.")
Slot(slotIndex).remove()
brainInventory.inventory(slotIndex).remove()
}
else {
} else {
direction match {
case _ if checkSlotStatus(slotIndex) == SlotStatus.Synchronized =>
@ -175,57 +187,59 @@ trait SyncedInventory extends PersistedInventory with Logging {
case SyncDirection.BrainToDesktop =>
val item = brainInventory.inventory(slotIndex).get match {
case Some(entity) => Items.recover(entity) match {
case Some(item) => Some(item.asInstanceOf[I])
case Some(entity) =>
Items.recover(entity) match {
case Some(item) => Some(item.asInstanceOf[I])
case None =>
logger.error(
s"An entity ($entity class ${entity.getClass.getName}) was inserted into a slot " +
s"(index: $slotIndex) of a brain inventory $brainInventory, " +
s"but we were unable to recover an Item from it.",
)
logger.error(
s"A Desktop inventory $this (class ${getClass.getName}) could not recover the item. Removing.",
)
logEntityLoss(slotIndex, entity)
case None =>
logger.error(
s"An entity ($entity class ${entity.getClass.getName}) was inserted into a slot " +
s"(index: $slotIndex) of a brain inventory $brainInventory, " +
s"but we were unable to recover an Item from it."
)
logger.error(
s"A Desktop inventory $this (class ${getClass.getName}) could not recover the item. Removing."
)
logEntityLoss(slotIndex, entity)
None
}
None
}
case None => None
}
Slot(slotIndex).set(item)
case SyncDirection.Reconcile => checkSlotStatus(slotIndex) match {
case SlotStatus.Synchronized => // no-op
case SyncDirection.Reconcile =>
checkSlotStatus(slotIndex) match {
case SlotStatus.Synchronized => // no-op
// let's just grab whatever we have
case SlotStatus.DesktopNonEmpty => doSync(slotIndex, SyncDirection.DesktopToBrain)
case SlotStatus.BrainNonEmpty => doSync(slotIndex, SyncDirection.BrainToDesktop)
// let's just grab whatever we have
case SlotStatus.DesktopNonEmpty => doSync(slotIndex, SyncDirection.DesktopToBrain)
case SlotStatus.BrainNonEmpty => doSync(slotIndex, SyncDirection.BrainToDesktop)
case SlotStatus.Conflict =>
// so, the brain inventory and the Desktop inventory have conflicting views on what the slot contains
// we'll let Desktop win because it has more info
case SlotStatus.Conflict =>
// so, the brain inventory and the Desktop inventory have conflicting views on what the slot contains
// we'll let Desktop win because it has more info
(brainInventory.inventory(slotIndex).get, Slot(slotIndex).get) match {
case (Some(entity), Some(item)) =>
logger.error(
s"Encountered an inventory conflict for slot $slotIndex! " +
s"The Desktop inventory believes the slot contains $item (class ${item.getClass.getName}), " +
s"but the brain inventory believes the slot contains $entity (class ${entity.getClass.getName}).",
)
logger.error("Resolving the conflict in favor of Ocelot Desktop.")
(brainInventory.inventory(slotIndex).get, Slot(slotIndex).get) match {
case (Some(entity), Some(item)) =>
logger.error(
s"Encountered an inventory conflict for slot $slotIndex! " +
s"The Desktop inventory believes the slot contains $item (class ${item.getClass.getName}), " +
s"but the brain inventory believes the slot contains $entity (class ${entity.getClass.getName})."
)
logger.error("Resolving the conflict in favor of Ocelot Desktop.")
case _ =>
// this really should not happen... but alright, let's throw an exception at least
throw new IllegalStateException(
"an inventory conflict was detected even though one of the slots is empty",
)
}
case _ =>
// this really should not happen... but alright, let's throw an exception at least
throw new IllegalStateException(
"an inventory conflict was detected even though one of the slots is empty"
)
}
doSync(slotIndex, SyncDirection.DesktopToBrain)
}
doSync(slotIndex, SyncDirection.DesktopToBrain)
}
}
}
}
@ -234,7 +248,7 @@ trait SyncedInventory extends PersistedInventory with Logging {
logger.error(
s"Encountered a data loss! " +
s"In the brain inventory $brainInventory (class ${brainInventory.getClass.getName}), " +
s"the entity $entity (class ${entity.getClass.getName}) is deleted from the slot $slotIndex.",
s"the entity $entity (class ${entity.getClass.getName}) is deleted from the slot $slotIndex."
)
}

View File

@ -52,6 +52,9 @@ trait ComponentItem extends Item with PersistableItem {
setWorkspace()
}
override def shouldReceiveEventsFor(address: String): Boolean =
super.shouldReceiveEventsFor(address) || Option(component.node).exists(_.address == address)
override def fillTooltip(tooltip: ItemTooltip): Unit = {
super.fillTooltip(tooltip)

View File

@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop
import ocelot.desktop.audio._
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.node.ComputerAwareNode.{ErrorMessageMoveSpeed, MaxErrorMessageDistance}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent
@ -17,10 +18,11 @@ import totoro.ocelot.brain.event._
import scala.collection.mutable
abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware)
extends SyncedInventoryEntityNode(entity)
with DiskActivityHandler
with ShiftClickNode
{
extends EntityNode(entity)
with SyncedInventory
with DiskActivityHandler
with ShiftClickNode {
private val messages = mutable.ArrayBuffer[(Float, ComputerErrorMessageLabel)]()
protected def addErrorMessage(message: ComputerErrorMessageLabel): Unit = synchronized {

View File

@ -49,7 +49,7 @@ abstract class EntityNode(val entity: Entity with Environment) extends Node {
override def getNodeByPort(port: NodePort): network.Node = entity.node
override def shouldReceiveEventsFor(address: String): Boolean =
entity.node != null && address == entity.node.address
super.shouldReceiveEventsFor(address) || entity.node != null && address == entity.node.address
override def dispose(): Unit = {
super.dispose()

View File

@ -124,8 +124,6 @@ abstract class Node extends Widget with DragHandler with ClickHandler with Hover
def getNodeByPort(port: NodePort): network.Node = throw new IllegalArgumentException("this node has no ports")
def shouldReceiveEventsFor(address: String): Boolean = false
def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator
def connect(portA: NodePort, node: Node, portB: NodePort): Unit = {

View File

@ -1,26 +0,0 @@
package ocelot.desktop.node
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.inventory.traits.ComponentItem
import ocelot.desktop.ui.event.Event
import totoro.ocelot.brain.entity.traits.{Entity, Environment}
abstract class SyncedInventoryEntityNode(entity: Entity with Environment)
extends EntityNode(entity)
with SyncedInventory {
override def shouldReceiveEventsFor(address: String): Boolean =
super.shouldReceiveEventsFor(address) ||
inventoryIterator
.flatMap(_.get)
.collect { case item: ComponentItem => item }
.exists(_.component.node.address == address)
override def handleEvent(event: Event): Unit = {
super.handleEvent(event)
for (slot <- inventoryIterator; item <- slot.get) {
item.handleEvent(event)
}
}
}

View File

@ -2,8 +2,9 @@ package ocelot.desktop.node.nodes
import ocelot.desktop.geometry.Rect2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize}
import ocelot.desktop.node.{LabeledEntityNode, ShiftClickNode, SyncedInventoryEntityNode, WindowedNode}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, ShiftClickNode, WindowedNode}
import ocelot.desktop.ui.event.ClickEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.contextmenu.ContextMenu
@ -12,13 +13,14 @@ import ocelot.desktop.windows.DiskDriveWindow
import totoro.ocelot.brain.entity.FloppyDiskDrive
class DiskDriveNode(entity: FloppyDiskDrive)
extends SyncedInventoryEntityNode(entity)
with LabeledEntityNode
with DiskDriveAware
with DiskActivityHandler
with ShiftClickNode
with WindowedNode[DiskDriveWindow]
{
extends EntityNode(entity)
with SyncedInventory
with LabeledEntityNode
with DiskDriveAware
with DiskActivityHandler
with ShiftClickNode
with WindowedNode[DiskDriveWindow] {
override def icon: String = "nodes/disk-drive/Default"
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
@ -40,9 +42,9 @@ class DiskDriveNode(entity: FloppyDiskDrive)
position.x + HighlightThickness,
position.y + HighlightThickness,
NoHighlightSize,
NoHighlightSize
NoHighlightSize,
),
"nodes/disk-drive/"
"nodes/disk-drive/",
)
}
@ -54,7 +56,7 @@ class DiskDriveNode(entity: FloppyDiskDrive)
override protected def onShiftClick(event: ClickEvent): Unit = {
if (isFloppyItemPresent)
eject()
eject()
}
override protected def hoveredShiftStatusBarText: String = "Eject floppy"

View File

@ -5,6 +5,8 @@ import ocelot.desktop.ui.widget.EventHandlers
trait EventAware {
protected val eventHandlers = new EventHandlers
def shouldReceiveEventsFor(address: String): Boolean = false
def handleEvent(event: Event): Unit = {
eventHandlers(event)
}