diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 58f0d0b..f80c4b7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -59,6 +59,8 @@ release: image: registry.gitlab.com/gitlab-org/release-cli:latest rules: - if: $CI_COMMIT_TAG + before_script: + - apk add git script: - echo "Creating a new release for tag $CI_COMMIT_TAG." - | diff --git a/build.sbt b/build.sbt index b79ac9a..a6bc077 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,5 @@ name := "ocelot-desktop" -version := "1.9.0" +version := "1.9.1" scalaVersion := "2.13.10" lazy val root = project.in(file(".")) diff --git a/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala index 824210e..c6f54f7 100644 --- a/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala +++ b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala @@ -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(""), + Slot(slotIndex).get.map(item => s"$item (class ${item.getClass.getName})").getOrElse("") ) logger.error( "The entity if the slot: " + - brainInventory.inventory(slotIndex) + brainInventory + .inventory(slotIndex) .get .map(entity => s"$entity (class ${entity.getClass.getName})") - .getOrElse(""), + .getOrElse("") ) 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." ) } diff --git a/src/main/scala/ocelot/desktop/inventory/item/DiskDriveMountableItem.scala b/src/main/scala/ocelot/desktop/inventory/item/DiskDriveMountableItem.scala index 8d41ba1..8ffabd0 100644 --- a/src/main/scala/ocelot/desktop/inventory/item/DiskDriveMountableItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/item/DiskDriveMountableItem.scala @@ -28,7 +28,12 @@ object DiskDriveMountableItem { override def icon: IconSource = IconSource.DiskDriveMountable - override def build(): DiskDriveMountableItem = new DiskDriveMountableItem(new DiskDriveMountable()) + override def build(): DiskDriveMountableItem = { + val item = new DiskDriveMountableItem(new DiskDriveMountable()) + item.fillSlotsWithDefaultItems() + + item + } override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new DiskDriveMountableItem(_))) } diff --git a/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala index aaab5ec..228ea49 100644 --- a/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala +++ b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala @@ -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) diff --git a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala index a8e6989..c70b3bc 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala +++ b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala @@ -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 { diff --git a/src/main/scala/ocelot/desktop/node/EntityNode.scala b/src/main/scala/ocelot/desktop/node/EntityNode.scala index 0bcd848..d7a7768 100644 --- a/src/main/scala/ocelot/desktop/node/EntityNode.scala +++ b/src/main/scala/ocelot/desktop/node/EntityNode.scala @@ -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() diff --git a/src/main/scala/ocelot/desktop/node/Node.scala b/src/main/scala/ocelot/desktop/node/Node.scala index 33f4cbb..f29c10a 100644 --- a/src/main/scala/ocelot/desktop/node/Node.scala +++ b/src/main/scala/ocelot/desktop/node/Node.scala @@ -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 = { @@ -222,9 +220,13 @@ abstract class Node extends Widget with DragHandler with ClickHandler with Hover val desiredPos = if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) - (pos - workspaceView.cameraOffset).snap(Size) + - workspaceView.cameraOffset - - grabPoint.snap(Size) + (pos - workspaceView.cameraOffset).snap(Size) + // snap the position to the grid relative to the origin + workspaceView.cameraOffset - // restore the camera offset + grabPoint.snap(Size) + // accounts for multi-block screens + Vector2D( + ((Size - width) % Size) / 2, + ((Size - height) % Size) / 2, + ) // if a node is not full-size, moves it to the center of the grid cell else pos - grabPoint diff --git a/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala b/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala deleted file mode 100644 index 488d485..0000000 --- a/src/main/scala/ocelot/desktop/node/SyncedInventoryEntityNode.scala +++ /dev/null @@ -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) - } - } -} diff --git a/src/main/scala/ocelot/desktop/node/WindowedNode.scala b/src/main/scala/ocelot/desktop/node/WindowedNode.scala index 8672035..d0155e2 100644 --- a/src/main/scala/ocelot/desktop/node/WindowedNode.scala +++ b/src/main/scala/ocelot/desktop/node/WindowedNode.scala @@ -1,9 +1,11 @@ package ocelot.desktop.node +import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.widget.window.{Window, Windowed} +import org.lwjgl.input.Keyboard -trait WindowedNode[T <: Window] extends Node with Windowed[T]{ +trait WindowedNode[T <: Window] extends Node with Windowed[T] { override def dispose(): Unit = { closeAndDisposeWindowIfCreated() @@ -21,7 +23,8 @@ trait WindowedNode[T <: Window] extends Node with Windowed[T]{ super.onClick(event) event match { - case ClickEvent(MouseEvent.Button.Left, _) => + // FIXME: this. is. ugly. do proper modifier tracking. + case ClickEvent(MouseEvent.Button.Left, _) if !KeyEvents.isDown(Keyboard.KEY_LSHIFT) => if (hasWindowCreated && window.isOpen) window.close() else diff --git a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala index e300906..25c94ab 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala @@ -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" diff --git a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala index 8ba889b..3459153 100644 --- a/src/main/scala/ocelot/desktop/ui/event/EventAware.scala +++ b/src/main/scala/ocelot/desktop/ui/event/EventAware.scala @@ -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) } diff --git a/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala b/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala index ae8f08a..39d81c4 100644 --- a/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala +++ b/src/main/scala/ocelot/desktop/util/DiskDriveAware.scala @@ -22,6 +22,7 @@ trait DiskDriveAware with Windowed[DiskDriveWindow] { override type I = FloppyItem + def floppyDiskDrive: FloppyDiskDrive override def brainInventory: Inventory = floppyDiskDrive.inventory.owner