diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4e13718..7950ab7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,33 +1,54 @@
-image: scalableminds/sbt:sbt-13-17
+default:
+ image: sbtscala/scala-sbt:eclipse-temurin-jammy-8u352-b08_1.8.3_2.13.10
+
+ cache:
+ key: "$CI_COMMIT_REF_SLUG"
+ paths:
+ - "sbt-cache"
+ - "target/streams"
+ - "lib/ocelot-brain/target"
variables:
GIT_SUBMODULE_STRATEGY: normal
SBT_OPTS: "-Dsbt.global.base=sbt-cache/.sbtboot -Dsbt.boot.directory=sbt-cache/.boot -Dsbt.ivy.home=sbt-cache/.ivy"
SBT_CACHE_DIR: "sbt-cache/.ivy/cache"
+ PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/ocelot-desktop/${CI_COMMIT_TAG}"
+ PACKAGE_NAME: "ocelot-desktop-${CI_COMMIT_TAG}.jar"
-cache:
- key: "$CI_COMMIT_REF_SLUG"
- paths:
- - "sbt-cache"
- - "target/streams"
- - "lib/ocelot-brain/target"
-
-before_script:
- - sbt -v sbtVersion
+stages:
+ - build
+ - upload
+ - deploy
+ - release
build:
stage: build
+ before_script:
+ - sbt -v sbtVersion
+ tags:
+ - public
script:
- sbt assembly
artifacts:
paths:
- target/scala-2.13/ocelot-desktop.jar
+upload:
+ stage: upload
+ image: curlimages/curl:latest
+ rules:
+ - if: $CI_COMMIT_TAG
+ script:
+ - |
+ curl --fail-with-body --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file target/scala-2.13/ocelot-desktop.jar "${PACKAGE_REGISTRY_URL}/${PACKAGE_NAME}"
+
pages:
stage: deploy
before_script: []
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "develop"
+ tags:
+ - public
script:
- rm -rf public
- mkdir public
@@ -36,3 +57,17 @@ pages:
artifacts:
paths:
- public
+
+release:
+ stage: release
+ image: registry.gitlab.com/gitlab-org/release-cli:latest
+ rules:
+ - if: $CI_COMMIT_TAG
+ script:
+ - echo "Creating a new release for tag $CI_COMMIT_TAG."
+ - |
+ release-cli create \
+ --name "Release $CI_COMMIT_TAG" \
+ --tag-name "$CI_COMMIT_TAG" \
+ --description "$CI_COMMIT_TAG_MESSAGE" \
+ --assets-link "{\"name\":\"${PACKAGE_NAME}\",\"link_type\":\"package\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE_NAME}\"}"
\ No newline at end of file
diff --git a/README.md b/README.md
index ecac393..75c5d2e 100644
--- a/README.md
+++ b/README.md
@@ -3,131 +3,188 @@
A desktop version of the renowned OpenComputers emulator Ocelot.
-[Download the latest build][download] / [mirror][download-mirror]
+[**Download** the latest build][download] / [mirror][download-mirror]
## Why
-You might already be happy with your choice of an OC emulator; after all, there
-is already a plenty of them.
-So why would you want to reconsider your life choices now all of a sudden?
+You might already be happy with your choice of an OC emulator;
+there is a plenty of them, after all.
+Why would you want to reconsider your life choices now all of a sudden?
A fine question, indeed; perhaps, a list of features will persuade you.
### Powered by ocelot-brain
At the heart of this emulator is [ocelot-brain][ocelot-brain] (uh, don't ask me),
-which is essentially the source code of OpenComputers stripped of everything
-Minecraft-specific and packaged as a Scala library.
+which is essentially the source code of OpenComputers decoupled of everything
+Minecraft-specific and repurposed as a Scala library.
This makes Ocelot Desktop **the most accurate emulator** ever made.
-Your programs will run on the same version of Lua as used by the mod and have
+Your programs will run on the Lua implementation used by the mod and exhibit
similar timings.
The performance and memory constraints present in OC are also emulated.
### Customizable setups
-Computers can have the following components:
+Taylor your computer build to your needs!
+We provide a variety of components to choose from:
-- the 3 tiers of graphics cards
-- all kinds of network cards (wired, wireless)
-- a linked card
-- an internet card
-- a sound card (Computronics)
-- a redstone card in the both tiers
-- a data card (again, you can pick any of the three tiers)
+- graphics cards
+- network cards (wired, wireless)
+- linked cards
+- internet cards
+- redstone cards (including the second tier!)
+- data cards
- hard disks
-- a floppy disk (in T3+ computer cases only)
+- floppy disks (in T3+ computer cases only, just like in OpenComputers)
-The choice is restricted by the tier of a computer case, just like in
-OpenComputers, to avoid building impossible configurations.
-Oh, did I forget to mention that Ocelot Desktop has both the CPUs and the APUs?
-Memory can likewise be installed according to your needs.
+Feel limited by the vanilla cards?
+No problem — we've even integrated some components from popular addons!
-If one computer is not enough, you can add another one.
-Or two, or a thousand, as long as your host doesn't collapse under the load,
-of course.
-The network cards are there for a reason — these newly spawned machines can
-communicate with each other.
-And relays may help you manage the wired networks.
+![Addon showcase][addon-showcase]
-Or, instead of employing an army of computers, you might want to connect a dozen
-of screens to a single machine, like in the movies.
-No problem — we've got that covered, too.
+- **Computronics:**
+ - `computer.beep()` does not excite your music sense enough?
+ Check out the **sound card**'s synthesis engine,
+ or string notes together on a bunch of **note blocks**!
-### Pretty graphical interface
+ - You have privacy concerns?
+ An ultra-precision blast of the **self-destructing card** might save your day!
+
+ - If, on the contrary, you're the kind to violate the privacy of others,
+ we've got a **camera** that reads video data from a real webcam.
+
+ - Those who seem to find no color in their life may appreciate the
+ **colorful lamps**.
+ Their iridescent glow will provide comfort in the darkest hour.
+ And they won't desert you.
+
+- **OpenFM:** for those lo-fi beats to chill and relax to while writing code.
+
+We'll make sure your setups are grounded in reality
+by having the maximum card tier in a slot depend on your computer case.
+Oh, did I forget to mention Ocelot Desktop has APUs as well?
+
+Explore the wonders of distributed computing by adding a couple of other
+computers to your workspace.
+(Or a thousand — if you think your host can handle this.)
+Network cards allow these newly spawned machines to talk to each other.
+And relays may prove useful to manage the wired networks.
+
+Perhaps, instead of enlisting a computer army,
+you want to attach a dozen of screens to a single machine
+(as they do in the movies).
+Well, no problem — we've got that covered, too.
+
+### Gorgeous graphical interface
![GUI][gui]
-A slick interface allows you to customize the setup to your liking.
-Add more computers, organize the connections between components, build complex
-setups, and manage your screen real estate to avoid distractions.
+Manage your screen real estate to avoid distractions.
+All nodes are draggable, as are windows.
+And screen windows in particular are also resizeable —
+click and drag the bottom-right corner if they take up too much space.
+Or hold Shift and let the window consume it all.
-Many additional options are hidden in the context menu — try hitting the
-right mouse button on the various things.
-For example, components will let you copy their address to the clipboard, and
-right-clicking on the TPS counter on the bottom allows you to change the
-simulation speed.
+![Window scaling][window-scaling]
-The emulator uses hardware acceleration to offload the daunting task of
-rendering its interface to a specialized device, so make sure you have a OpenGL
-2.1-capable graphics card.
+Many additional options are hidden in the context menus —
+try hitting the right mouse button on various things.
+For example, components will let you copy their address to the clipboard,
+and right-clicking on the TPS counter (on the bottom right)
+allows you to change the simulation speed.
+
+![TPS rate menu][tps-menu]
+
+Ocelot Desktop uses hardware acceleration to offload the daunting task of
+rendering its interface to a special-purposed device (your GPU),
+so make sure you have an **OpenGL 3.0-capable** graphics card.
+(Though, honestly, it's harder to find one that isn't, really.)
### Persistable workspaces
-It would be sad if, after all the hard work you put into adjusting the
-workspace, you have to do that again.
+Imagine putting many hours into wiring things up only to have to do it all from
+scratch the next time you open the emulator.
+That... would be sad and disappointing.
I mean, OpenComputers can persist its machines just fine, right?
-By basing the emulator on its code, we've essentially inherited the ability
-to save workspaces on the disk and load them afterwards.
+Good news: by reusing its code, we've essentially inherited the ability
+to save workspaces on the disk and load them afterwards!
Just in case, Ocelot Desktop will warn you if you smash the quit button without
saving.
+We'd rather you didn't feel sad and disappointed.
### Cool features
-![Performance graphs][graphs]
+![Performance graphs][perf-graphs]
![Sound card GUI][sound-card]
A few smaller features are worth mentioning, too:
-- Screens are resizeable — drag the bottom-right corner.
-- Windows are labeled with the corresponding block's address; however, you can
- set a custom label — look for an option in the context menu.
-- The button in a computer case window shows the performance graphs:
- the used memory, processor time and call budget.
-- Hold the Ctrl key while dragging blocks to have them snap to the grid.
+- Windows show the corresponding block's address by default.
+ However, you can relabel them: look for the option in the context menu.
+- The button ![][drawer-button] at the bottom of a computer case window
+ shows performance graphs: the memory, processor time, and call budget.
+- Hold the Ctrl key while dragging blocks to snap them to the grid.
## Download
-Decided to give Ocelot Desktop a shot? [Download the latest build][download] / [mirror][download-mirror]
+Decided to give Ocelot Desktop a shot?
-## How to build it?
-Just import the project in your favorite IDE.
-Make sure to have Scala and SBT installed (manually or through IDE).
+[**Download** the latest build][download] / [mirror][download-mirror]
-Ocelot Brain library is added as a Git submodule, so do not forget to fetch it too.
-Something like `git submodule update --init --recursive` should do the trick.
+## Hacking
+Just import the project in your favorite IDE.
+Make sure to have Scala and SBT installed (your IDE may assist you with that).
-Use `sbt run` to start Ocelot Desktop. Use `sbt assembly` to generate JAR file.
-(It will appear at `target/scala-2.13/ocelot-desktop.jar` location.)
+We include [ocelot-brain][] as a Git submodule: don't forget to fetch it!
-If the compiler is complaining about missing BuildInfo class, or the version in
-the window title / logs looks outdated, use `sbt buildInfo` to generate fresh class.
+```sh
+$ git submodule update --init --recursive
+```
+
+To build from source and start Ocelot Desktop, type:
+
+```sh
+$ sbt run
+```
+
+If you want to get a JAR, use this:
+
+```sh
+$ sbt assembly
+```
+
+(You'll find it at `target/scala-2.13/ocelot-desktop.jar`.)
+
+In case you see the compiler complain about `BuildInfo` or just want to refresh
+the version displayed in the window title and the logs, run:
+
+```sh
+$ sbt buildInfo
+```
## Credits
-- **LeshaInc**, the author and maintainer of Ocelot Desktop.
-- **Totoro**, the creator of ocelot-brain and ocelot-online.
-- **bpm140**, who created marvelous Ocelot Desktop landing page.
-- **rason**, who stirred the development at the critical moment.
-- **NE0**, the bug extermination specialist.
-- **ECS**, who fearlessly jumped right into Scala jungle.
-- **fingercomp**, who wrote this README.
+- **LeshaInc:** the original author of Ocelot Desktop.
+- **Totoro:** the creator of [ocelot-brain][] and [ocelot.online][ocelot-online].
+- **bpm140:** produced the marvelous Ocelot Desktop landing page.
+- **rason:** stirred the development at the critical moment!
+- **NE0:** the bug extermination specialist.
+- **ECS:** leaped fearlessly into the Scala jungle.
+- **Smok1e:** added some light and color with new components from Computronics.
+- **Saphire:** scaled the UI for HiDPI screens.
+- **fingercomp:** wrote this README.
## See also
-- [Ocelot Desktop][ocelot-desktop] web page (link to the latest build, FAQ)
-- [ocelot.online][ocelot-online], a rudimentary web version of Ocelot
-- [ocelot-brain][ocelot-brain], the backend library of Ocelot Desktop
+- The [Ocelot Desktop][ocelot-desktop] web page
+ (links to the latest build, FAQ).
+- [ocelot.online][ocelot-online], a rudimentary web version of Ocelot.
+- [ocelot-brain][], the backend library of Ocelot Desktop.
- [#cc.ru on IRC][irc] if you have any questions
- (Russian, but we're fine with English too)
-- [Discord][discord] if you prefer so (we will not judge)
+ (Russian, but we're fine with English too).
+- Or [Discord][discord] if that's your fancy (we won't judge).
-[banner]: https://i.imgur.com/OzkpQZv.png
+[banner]: ./assets/banner.png "The Ocelot banner"
[download]: https://cc-ru.gitlab.io/ocelot/ocelot-desktop/ocelot.jar
[download-mirror]: https://ocelot.fomalhaut.me/ocelot.jar
-[gui]: https://i.imgur.com/O4bF7I8.png
-[graphs]: https://i.imgur.com/mG8UjhV.png
-[sound-card]: https://i.imgur.com/gnh3D6N.png
+[addon-showcase]: ./assets/addon-showcase.png "A workspace with colorful lamps and an OpenFM radio."
+[gui]: ./assets/gui.png "A screenshot of the GUI."
+[window-scaling]: ./assets/window-scale.gif "Demonstrates the screen scaling."
+[tps-menu]: ./assets/tps.png "The simulation speed menu (right-click on the TPS)."
+[perf-graphs]: ./assets/perf-graphs.gif "A demo of performance graphs."
+[sound-card]: ./assets/sound-card.gif "Shows the sound card UI while playing a melody."
+[drawer-button]: ./sprites/buttons/BottomDrawerOpen.png
[ocelot-brain]: https://gitlab.com/cc-ru/ocelot/ocelot-brain
[ocelot-online]: https://ocelot.fomalhaut.me/
[ocelot-desktop]: https://ocelot.fomalhaut.me/desktop/
diff --git a/assets/addon-showcase.png b/assets/addon-showcase.png
new file mode 100644
index 0000000..d050320
Binary files /dev/null and b/assets/addon-showcase.png differ
diff --git a/assets/banner.png b/assets/banner.png
new file mode 100644
index 0000000..96d65af
Binary files /dev/null and b/assets/banner.png differ
diff --git a/assets/gui.png b/assets/gui.png
new file mode 100644
index 0000000..2882bce
Binary files /dev/null and b/assets/gui.png differ
diff --git a/assets/perf-graphs.gif b/assets/perf-graphs.gif
new file mode 100644
index 0000000..9c92811
Binary files /dev/null and b/assets/perf-graphs.gif differ
diff --git a/assets/sound-card.gif b/assets/sound-card.gif
new file mode 100644
index 0000000..fd74ef4
Binary files /dev/null and b/assets/sound-card.gif differ
diff --git a/assets/tps.png b/assets/tps.png
new file mode 100644
index 0000000..ac7d7af
Binary files /dev/null and b/assets/tps.png differ
diff --git a/assets/window-scale.gif b/assets/window-scale.gif
new file mode 100644
index 0000000..6df6e1a
Binary files /dev/null and b/assets/window-scale.gif differ
diff --git a/build.sbt b/build.sbt
index 6ec6027..203675a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,6 @@
name := "ocelot-desktop"
-version := "1.5.0"
-scalaVersion := "2.13.8"
+version := "1.7.1"
+scalaVersion := "2.13.10"
lazy val root = project.in(file("."))
.dependsOn(brain % "compile->compile")
@@ -16,6 +16,8 @@ lazy val root = project.in(file("."))
lazy val brain = ProjectRef(file("lib/ocelot-brain"), "ocelot-brain")
+libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
+
libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.20.0"
libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0"
libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.20.0"
@@ -33,7 +35,6 @@ libraryDependencies += "com.github.wendykierp" % "JTransforms" % "3.1"
libraryDependencies += "com.github.sarxos" % "webcam-capture" % "0.3.12"
// For OpenFM
-libraryDependencies += "javazoom" % "jlayer" % "1.0.1"
libraryDependencies += "com.googlecode.soundlibs" % "mp3spi" % "1.9.5.4"
libraryDependencies += "com.googlecode.soundlibs" % "vorbisspi" % "1.0.3.3"
@@ -42,4 +43,4 @@ assembly / assemblyMergeStrategy := {
case _ => MergeStrategy.first
}
-assembly / assemblyJarName := s"ocelot-desktop.jar"
+assembly / assemblyJarName := "ocelot-desktop.jar"
diff --git a/lib/ocelot-brain b/lib/ocelot-brain
index 42dd5a9..fbea0b4 160000
--- a/lib/ocelot-brain
+++ b/lib/ocelot-brain
@@ -1 +1 @@
-Subproject commit 42dd5a966456654968af777d625c3e2f8b9265fa
+Subproject commit fbea0b4295d3866f7bff71b442afce0a859e3712
diff --git a/project/assembly.sbt b/project/assembly.sbt
index c46ce74..f55a62c 100644
--- a/project/assembly.sbt
+++ b/project/assembly.sbt
@@ -1 +1 @@
-addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
diff --git a/project/build.properties b/project/build.properties
index d738b85..d0ac2d5 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1,2 @@
-sbt.version = 1.7.1
+# suppress inspection "UnusedProperty" for whole file
+sbt.version = 1.8.3
diff --git a/sprites/items/SelfDestructingCard.png b/sprites/items/SelfDestructingCard.png
new file mode 100644
index 0000000..38aa027
Binary files /dev/null and b/sprites/items/SelfDestructingCard.png differ
diff --git a/src/main/resources/ocelot/desktop/colorscheme.txt b/src/main/resources/ocelot/desktop/colorscheme.txt
index aa6091d..be0868d 100644
--- a/src/main/resources/ocelot/desktop/colorscheme.txt
+++ b/src/main/resources/ocelot/desktop/colorscheme.txt
@@ -58,10 +58,13 @@ TextInputForeground = #333333
TextInputBorderError = #aa8888
TextInputBorderErrorFocused = #cc6666
-ButtonBackground = #aaaaaa
-ButtonBorder = #888888
-ButtonForeground = #333333
-ButtonConfirm = #336633
+ButtonBackground = #aaaaaa
+ButtonBorder = #888888
+ButtonForeground = #333333
+ButtonBackgroundDisabled = #333333
+ButtonBorderDisabled = #666666
+ButtonForegroundDisabled = #888888
+ButtonConfirm = #336633
BottomDrawerBorder = #888888
@@ -80,6 +83,7 @@ VerticalMenuBorder = #dfdfdf
SliderBackground = #aaaaaa
SliderBorder = #888888
+SliderTick = #989898
SliderHandler = #bbbbbb
SliderForeground = #333333
@@ -134,4 +138,6 @@ SoundCardWire3 = #69237f
SoundCardWire4 = #7f2331
SoundCardWire5 = #7f5123
SoundCardWire6 = #7f7723
-SoundCardWire7 = #000000
\ No newline at end of file
+SoundCardWire7 = #000000
+
+ErrorMessage = #ff3366
\ No newline at end of file
diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png
index 1ee8c92..5865407 100644
Binary files a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png and b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png differ
diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt
index 8c11ffd..3601d5e 100644
--- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt
+++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt
@@ -1,16 +1,16 @@
BackgroundPattern 0 0 304 304
-BarSegment 329 349 16 4
+BarSegment 357 349 16 4
ComputerMotherboard 305 129 79 70
Empty 506 126 1 1
-EmptySlot 318 330 18 18
+EmptySlot 335 330 18 18
Knob 385 129 50 50
KnobCenter 436 129 50 50
KnobLimits 305 200 50 50
ShadowBorder 505 15 1 24
ShadowCorner 301 305 24 24
TabArrow 502 0 8 14
-buttons/BottomDrawerClose 337 330 18 18
-buttons/BottomDrawerOpen 356 330 18 18
+buttons/BottomDrawerClose 354 330 18 18
+buttons/BottomDrawerOpen 373 330 18 18
buttons/OpenFMRadioCloseOff 242 402 7 8
buttons/OpenFMRadioCloseOn 250 402 7 8
buttons/OpenFMRadioRedstoneOff 502 87 8 8
@@ -19,20 +19,20 @@ buttons/OpenFMRadioStartOff 326 305 24 24
buttons/OpenFMRadioStartOn 351 305 24 24
buttons/OpenFMRadioStopOff 376 305 24 24
buttons/OpenFMRadioStopOn 401 305 24 24
-buttons/OpenFMRadioVolumeDownOff 482 330 10 10
-buttons/OpenFMRadioVolumeDownOn 493 330 10 10
-buttons/OpenFMRadioVolumeUpOff 501 363 10 10
-buttons/OpenFMRadioVolumeUpOn 318 349 10 10
-buttons/PowerOff 375 330 18 18
-buttons/PowerOn 394 330 18 18
+buttons/OpenFMRadioVolumeDownOff 499 330 10 10
+buttons/OpenFMRadioVolumeDownOn 501 363 10 10
+buttons/OpenFMRadioVolumeUpOff 335 349 10 10
+buttons/OpenFMRadioVolumeUpOn 346 349 10 10
+buttons/PowerOff 392 330 18 18
+buttons/PowerOn 411 330 18 18
icons/ButtonCheck 301 363 17 17
icons/ButtonClipboard 319 363 17 17
icons/ButtonRandomize 337 363 17 17
icons/CPU 301 381 16 16
icons/Card 318 381 16 16
icons/ComponentBus 335 381 16 16
-icons/DragLMB 413 330 21 14
-icons/DragRMB 435 330 21 14
+icons/DragLMB 430 330 21 14
+icons/DragRMB 452 330 21 14
icons/EEPROM 352 381 16 16
icons/Floppy 369 381 16 16
icons/HDD 386 381 16 16
@@ -54,7 +54,7 @@ icons/WaveNoise 401 363 24 10
icons/WaveSawtooth 426 363 24 10
icons/WaveSine 451 363 24 10
icons/WaveSquare 476 363 24 10
-icons/WaveTriangle 457 330 24 10
+icons/WaveTriangle 474 330 24 10
icons/WireArrowLeft 507 15 4 8
icons/WireArrowRight 507 24 4 8
items/APU0 233 305 16 96
@@ -108,6 +108,7 @@ items/Memory5 424 251 16 16
items/NetworkCard 441 251 16 16
items/RedstoneCard0 458 251 16 16
items/RedstoneCard1 475 251 16 16
+items/SelfDestructingCard 318 330 16 32
items/Server0 492 251 16 16
items/Server1 305 268 16 16
items/Server2 322 268 16 16
diff --git a/src/main/resources/ocelot/desktop/messages.txt b/src/main/resources/ocelot/desktop/messages.txt
new file mode 100644
index 0000000..e3dda46
--- /dev/null
+++ b/src/main/resources/ocelot/desktop/messages.txt
@@ -0,0 +1,5 @@
+Error.NoCPU = No CPU is installed in the computer.
+Error.ComponentOverflow = Too many components connected to the computer.
+Error.NoRAM = No RAM is installed in the computer.
+Error.OutOfMemory = Out of memory.
+Error.InternalError = Internal error, please see the log file. This is probably a bug.
\ No newline at end of file
diff --git a/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg b/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg
new file mode 100644
index 0000000..dc55636
Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg differ
diff --git a/src/main/scala/ocelot/desktop/OcelotDesktop.scala b/src/main/scala/ocelot/desktop/OcelotDesktop.scala
index 842bfa9..ae43109 100644
--- a/src/main/scala/ocelot/desktop/OcelotDesktop.scala
+++ b/src/main/scala/ocelot/desktop/OcelotDesktop.scala
@@ -2,6 +2,7 @@ package ocelot.desktop
import buildinfo.BuildInfo
import li.flor.nativejfilechooser.NativeJFileChooser
+import ocelot.desktop.inventory.Items
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.swing.SplashScreen
import ocelot.desktop.ui.widget._
@@ -27,7 +28,7 @@ import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration.Duration
import scala.io.Source
import scala.jdk.CollectionConverters._
-import scala.util.{Failure, Success, Try}
+import scala.util.{Failure, Success, Try, Using}
object OcelotDesktop extends Logging {
private val splashScreen = new SplashScreen()
@@ -46,7 +47,7 @@ object OcelotDesktop extends Logging {
private val tickLock: Lock = new ReentrantLock()
- def withTickLockAcquired(f: => Unit): Unit = withLockAcquired(tickLock, f)
+ def withTickLockAcquired(f: => Unit): Unit = withLockAcquired(tickLock)(f)
private def mainInner(args: mutable.HashMap[Argument, Option[String]]): Unit = {
logger.info("Starting up Ocelot Desktop")
@@ -61,19 +62,21 @@ object OcelotDesktop extends Logging {
else
Paths.get(customConfigPath.get)
Settings.load(settingsFile)
+ Messages.load(Source.fromURL(getClass.getResource("/ocelot/desktop/messages.txt")))
ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt")))
+ Items.init()
+
splashScreen.setStatus("Initializing GUI...", 0.30f)
createWorkspace()
val loadRecentWorkspace = Settings.get.recentWorkspace.isDefined && Settings.get.openLastWorkspace
root = new RootWidget(!loadRecentWorkspace)
+ UiHandler.loadLibraries()
UiHandler.init(root)
splashScreen.setStatus("Loading resources...", 0.60f)
- // loading resources _after_ the UiHandler was initialized, because the native libraries are not available before
- ResourceManager.initResources()
splashScreen.setStatus("Loading workspace...", 0.90f)
val cmdLineWorkspaceArgument = args.get(CommandLine.WorkspacePath).flatten
@@ -102,28 +105,39 @@ object OcelotDesktop extends Logging {
}
}
- new Thread(() => {
- while (true) {
- Profiler.startTimeMeasurement("tick")
+ val updateThread = new Thread(() => try {
+ val currentThread = Thread.currentThread()
- withTickLockAcquired {
- workspace.update()
- tpsCounter.tick()
+ while (!currentThread.isInterrupted) {
+ Profiler.measure("tick") {
+ withTickLockAcquired {
+ workspace.update()
+ tpsCounter.tick()
+ }
}
- Profiler.endTimeMeasurement("tick")
ticker.waitNext()
}
- }).start()
+ } catch {
+ case _: InterruptedException => // ignore
+ }, "update-thread")
+
+ updateThread.start()
splashScreen.dispose()
logger.info("Ocelot Desktop is up and ready!")
UiHandler.start()
logger.info("Cleaning up")
+ updateThread.interrupt()
+
+ try updateThread.join() catch {
+ case _: InterruptedException =>
+ }
+
WebcamCapture.cleanup()
Settings.save(settingsFile)
- ResourceManager.freeResources()
UiHandler.terminate()
+ ResourceManager.checkEmpty()
Ocelot.shutdown()
@@ -173,22 +187,27 @@ object OcelotDesktop extends Logging {
private var savePath: Option[Path] = None
private val tmpPath = Files.createTempDirectory("ocelot-save")
- def newWorkspace(): Unit = {
+ Runtime.getRuntime.addShutdownHook(new Thread(() => {
+ FileUtils.deleteDirectory(tmpPath.toFile)
+ }))
+
+ def newWorkspace(): Unit = showCloseConfirmationDialog("Save workspace before opening a new one?") {
root.workspaceView.newWorkspace()
+ savePath = None
Settings.get.recentWorkspace = None
}
- def save(): Unit = {
- if (savePath.isEmpty) {
- saveAs()
- return
- }
-
+ private def saveTo(outputPath: Path): Unit = {
val oldPath = workspace.path
- val newPath = savePath.get
- if (oldPath != newPath) {
- val oldFiles = Files.list(oldPath).iterator.asScala.toArray
- val newFiles = Files.list(newPath).iterator.asScala.toArray
+
+ if (oldPath != outputPath) {
+ val (oldFiles, newFiles) =
+ Using.resources(Files.list(oldPath), Files.list(outputPath)) { (oldDirStream, newDirStream) =>
+ val oldFiles = oldDirStream.iterator.asScala.toArray
+ val newFiles = newDirStream.iterator.asScala.toArray
+ (oldFiles, newFiles)
+ }
+
val toRemove = newFiles.intersect(oldFiles)
for (path <- toRemove) {
@@ -201,7 +220,7 @@ object OcelotDesktop extends Logging {
for (path <- oldFiles) {
val oldFile = oldPath.resolve(path.getFileName).toFile
- val newFile = newPath.resolve(path.getFileName).toFile
+ val newFile = outputPath.resolve(path.getFileName).toFile
if (Files.isDirectory(path)) {
FileUtils.copyDirectory(oldFile, newFile)
} else {
@@ -209,81 +228,80 @@ object OcelotDesktop extends Logging {
}
}
- workspace.path = newPath
+ workspace.path = outputPath
}
- val path = newPath + "/workspace.nbt"
- val writer = new DataOutputStream(new FileOutputStream(path))
- val nbt = new NBTTagCompound
- saveWorld(nbt)
- CompressedStreamTools.writeCompressed(nbt, writer)
- writer.flush()
- logger.info(s"Saved workspace to: $newPath")
+ val path = outputPath + "/workspace.nbt"
+
+ Using.resource(new DataOutputStream(new FileOutputStream(path))) { writer =>
+ val nbt = new NBTTagCompound
+ saveWorld(nbt)
+ CompressedStreamTools.writeCompressed(nbt, writer)
+ }
+
+ logger.info(s"Saved workspace to: $outputPath")
}
- def saveAs(): Unit = {
- showFileChooserDialog(
- dir => Try {
- if (dir.isEmpty)
- return
+ def save(continuation: => Unit): Unit = savePath match {
+ case Some(savePath) =>
+ saveTo(savePath)
+ continuation
- savePath = dir.map(_.toPath)
- Settings.get.recentWorkspace = dir.map(_.getCanonicalPath)
- save()
- },
- JFileChooser.SAVE_DIALOG,
- JFileChooser.DIRECTORIES_ONLY
- )
+ case None => showSaveDialog(continuation)
}
- def open(): Unit = {
- showFileChooserDialog({
+ def saveAs(): Unit = showSaveDialog()
+
+ def showOpenDialog(): Unit =
+ showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) {
case Some(dir) => load(dir)
case None => Success(())
- }, JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY)
- }
+ }
def load(dir: File): Try[Unit] = {
val path = Paths.get(dir.getCanonicalPath, "workspace.nbt")
if (Files.exists(path)) {
Try {
- val reader = new DataInputStream(Files.newInputStream(path))
- val nbt = CompressedStreamTools.readCompressed(reader)
- savePath = Some(dir.toPath)
- Settings.get.recentWorkspace = Some(dir.getCanonicalPath)
- workspace.path = dir.toPath
- loadWorld(nbt)
+ Using.resource(new DataInputStream(Files.newInputStream(path))) { reader =>
+ val nbt = CompressedStreamTools.readCompressed(reader)
+ savePath = Some(dir.toPath)
+ Settings.get.recentWorkspace = Some(dir.getCanonicalPath)
+ workspace.path = dir.toPath
+ loadWorld(nbt)
+ }
}
} else Failure(new FileNotFoundException("Specified directory does not contain 'workspace.nbt'"))
}
- def showFileChooserDialog(fun: Option[File] => Try[Unit], dialogType: Int, selectionMode: Int): Unit = {
+ def showFileChooserDialog(dialogType: Int, selectionMode: Int)(f: Option[File] => Try[Unit]): Unit = {
new Thread(() => {
val lastFile = savePath.map(_.toFile).orNull
- val chooser: JFileChooser = try {
- new NativeJFileChooser(lastFile)
- } catch {
- case _: Throwable => new JFileChooser(lastFile)
- }
+ val chooser: JFileChooser = Option.when(Settings.get.useNativeFileChooser) {
+ Try(new NativeJFileChooser(lastFile)).toOption
+ }.flatten.getOrElse(
+ new JFileChooser(lastFile)
+ )
chooser.setFileSelectionMode(selectionMode)
chooser.setDialogType(dialogType)
- val option = chooser.showDialog(null, null)
+ val selectedFile =
+ Option.when(chooser.showDialog(null, null) == JFileChooser.APPROVE_OPTION)(chooser.getSelectedFile)
+ val result = f(selectedFile)
- val result = fun(if (option == JFileChooser.APPROVE_OPTION) Some(chooser.getSelectedFile) else None)
result match {
case Failure(exception) =>
new NotificationDialog(s"Something went wrong!\n($exception)\nCheck the log file for a full stacktrace.",
NotificationType.Error)
.addCloseButton()
.show()
+
case Success(_) =>
}
}).start()
}
- def addPlayerDialog(): Unit = new InputDialog(
+ def showAddPlayerDialog(): Unit = new InputDialog(
"Add new player",
text => OcelotDesktop.selectPlayer(text)
).show()
@@ -307,54 +325,10 @@ object OcelotDesktop extends Logging {
}
}
- def settings(): Unit = {
- new SettingsDialog().show()
- }
+ def showSettings(): Unit = new SettingsDialog().show()
- private def cleanup(): Unit = {
- FileUtils.deleteDirectory(tmpPath.toFile)
- }
-
- def exit(): Unit = {
- if (UiHandler.root.modalDialogPool.children.exists(_.isInstanceOf[ExitConfirmationDialog]))
- return
-
- if (Settings.get.saveOnExit && savePath.isDefined) {
- save()
- UiHandler.exit()
- cleanup()
- return
- }
-
- new ExitConfirmationDialog {
- override def onSaveSelected(): Unit = {
- if (savePath.isDefined) {
- save()
- UiHandler.exit()
- cleanup()
- } else {
- showFileChooserDialog(
- dir => Try {
- if (dir.isEmpty)
- return
-
- savePath = dir.map(_.toPath)
- Settings.get.recentWorkspace = dir.map(_.getCanonicalPath)
- save()
- UiHandler.exit()
- cleanup()
- },
- JFileChooser.SAVE_DIALOG,
- JFileChooser.DIRECTORIES_ONLY
- )
- }
- }
-
- override def onExitSelected(): Unit = {
- UiHandler.exit()
- cleanup()
- }
- }.show()
+ def exit(): Unit = showCloseConfirmationDialog() {
+ UiHandler.exit()
}
var workspace: Workspace = _
@@ -362,4 +336,50 @@ object OcelotDesktop extends Logging {
private def createWorkspace(): Unit = {
workspace = new Workspace(tmpPath)
}
+
+ private def showSaveDialog(continuation: => Unit): Unit =
+ showFileChooserDialog(JFileChooser.SAVE_DIALOG, JFileChooser.DIRECTORIES_ONLY) { dir =>
+ Try {
+ if (dir.nonEmpty) {
+ savePath = dir.map(_.toPath)
+ Settings.get.recentWorkspace = dir.map(_.getCanonicalPath)
+ save(continuation)
+ }
+ }
+ }
+
+ private def showCloseConfirmationDialog(prompt: Option[String])(continuation: => Unit): Unit = {
+ if (UiHandler.root.modalDialogPool.children.exists(_.isInstanceOf[CloseConfirmationDialog])) {
+ return
+ }
+
+ for (savePath <- savePath if Settings.get.saveOnExit) {
+ saveTo(savePath)
+ continuation
+
+ return
+ }
+
+ val prompt_ = prompt
+
+ new CloseConfirmationDialog {
+ override def prompt: String = prompt_.getOrElse(super.prompt)
+
+ override def onSaveSelected(): Unit = save {
+ close()
+ continuation
+ }
+
+ override def onNoSaveSelected(): Unit = {
+ close()
+ continuation
+ }
+ }.show()
+ }
+
+ private def showCloseConfirmationDialog(prompt: String)(continuation: => Unit): Unit =
+ showCloseConfirmationDialog(Some(prompt))(continuation)
+
+ private def showCloseConfirmationDialog()(continuation: => Unit): Unit =
+ showCloseConfirmationDialog(None)(continuation)
}
diff --git a/src/main/scala/ocelot/desktop/Settings.scala b/src/main/scala/ocelot/desktop/Settings.scala
index 7c191aa..67033d5 100644
--- a/src/main/scala/ocelot/desktop/Settings.scala
+++ b/src/main/scala/ocelot/desktop/Settings.scala
@@ -12,15 +12,23 @@ import java.util
import scala.io.{Codec, Source}
class Settings(val config: Config) extends SettingsData {
+ // TODO: refactor this mess (having to declare every field 3 times is extremely error-prone)
+
volumeMaster = (config.getDouble("ocelot.sound.volumeMaster") max 0 min 1).toFloat
volumeBeep = (config.getDouble("ocelot.sound.volumeBeep") max 0 min 1).toFloat
volumeEnvironment = (config.getDouble("ocelot.sound.volumeEnvironment") max 0 min 1).toFloat
volumeInterface = (if (config.hasPath("ocelot.sound.volumeInterface")) config.getDouble("ocelot.sound.volumeInterface") max 0 min 1 else 0.5).toFloat
+ audioDisable = config.getBooleanOrElse("ocelot.sound.audioDisable", default = false)
+ logAudioErrorStacktrace = config.getBooleanOrElse("ocelot.sound.logAudioErrorStacktrace", default = false)
+ scaleFactor = (if (config.hasPath("ocelot.window.scaleFactor")) config.getDouble("ocelot.window.scaleFactor") max 1 min 3 else 1).toFloat
windowSize = config.getInt2D("ocelot.window.size")
windowPosition = config.getInt2D("ocelot.window.position")
windowValidatePosition = config.getBooleanOrElse("ocelot.window.validatePosition", default = true)
windowFullscreen = config.getBooleanOrElse("ocelot.window.fullscreen", default = false)
+ disableVsync = config.getBooleanOrElse("ocelot.window.disableVsync", default = false)
+ debugLwjgl = config.getBooleanOrElse("ocelot.window.debugLwjgl", default = false)
+ useNativeFileChooser = config.getBooleanOrElse("ocelot.window.useNativeFileChooser", default = true)
// Windows uses life-hack when it sets position to (-8,-8) for maximized windows to hide the frame.
// (https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543)
@@ -129,10 +137,16 @@ object Settings extends Logging {
.withValuePreserveOrigin("ocelot.sound.volumeBeep", settings.volumeBeep)
.withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
.withValuePreserveOrigin("ocelot.sound.volumeInterface", settings.volumeInterface)
+ .withValuePreserveOrigin("ocelot.sound.audioDisable", settings.audioDisable)
+ .withValuePreserveOrigin("ocelot.sound.logAudioErrorStacktrace", settings.logAudioErrorStacktrace)
+ .withValuePreserveOrigin("ocelot.window.scaleFactor", settings.scaleFactor.doubleValue)
.withValue("ocelot.window.position", settings.windowPosition)
.withValuePreserveOrigin("ocelot.window.validatePosition", settings.windowValidatePosition)
.withValue("ocelot.window.size", settings.windowSize)
.withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen)
+ .withValuePreserveOrigin("ocelot.window.disableVsync", settings.disableVsync)
+ .withValuePreserveOrigin("ocelot.window.debugLwjgl", settings.debugLwjgl)
+ .withValuePreserveOrigin("ocelot.window.useNativeFileChooser", settings.useNativeFileChooser)
.withValue("ocelot.workspace.recent", settings.recentWorkspace)
.withValuePreserveOrigin("ocelot.workspace.stickyWindows", settings.stickyWindows)
.withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit)
diff --git a/src/main/scala/ocelot/desktop/audio/AL10W.scala b/src/main/scala/ocelot/desktop/audio/AL10W.scala
index cbf6820..26c7d85 100644
--- a/src/main/scala/ocelot/desktop/audio/AL10W.scala
+++ b/src/main/scala/ocelot/desktop/audio/AL10W.scala
@@ -1,6 +1,7 @@
package ocelot.desktop.audio
-import ocelot.desktop.util.Logging
+import ocelot.desktop.Settings
+import ocelot.desktop.util.{Logging, OpenAlException}
import org.lwjgl.openal.AL10
import java.nio.{ByteBuffer, IntBuffer}
@@ -17,71 +18,97 @@ object AL10W extends Logging {
case _: Exception => false
}
}).map(_.getName).getOrElse(err.toHexString)
- logger.error(s"OpenAL error: ${func}: ${errName}")
+ val exc = OpenAlException(func, errName, err)
+
+ if (Settings.get.logAudioErrorStacktrace) {
+ logger.error(exc)
+ } else {
+ logger.error(exc.getMessage)
+ }
+
+ throw exc
}
+
res
}
- def alIsExtensionPresent(name: String): Boolean = run("alIsExtensionPresent") {
- AL10.alIsExtensionPresent(name)
+ def alIsExtensionPresent(name: String): Boolean = OpenAlException.defaulting(false) {
+ run("alIsExtensionPresent") {
+ AL10.alIsExtensionPresent(name)
+ }
}
+ @throws[OpenAlException]
def alGenBuffers(): Int = run("alGenBuffers") {
AL10.alGenBuffers()
}
+ @throws[OpenAlException]
def alBufferData(buffer: Int, format: Int, data: ByteBuffer, freq: Int): Unit = run("alBufferData") {
AL10.alBufferData(buffer, format, data, freq)
}
+ @throws[OpenAlException]
def alGetBufferi(buffer: Int, pname: Int): Int = run("alGetBufferi") {
AL10.alGetBufferi(buffer, pname)
}
+ @throws[OpenAlException]
def alDeleteBuffers(buffer: Int): Unit = run("alDeleteBuffers") {
AL10.alDeleteBuffers(buffer)
}
+ @throws[OpenAlException]
def alGenSources(): Int = run("alGenSources") {
AL10.alGenSources()
}
+ @throws[OpenAlException]
def alSourceQueueBuffers(source: Int, buffer: Int): Unit = run("alSourceQueueBuffers") {
AL10.alSourceQueueBuffers(source, buffer)
}
+ @throws[OpenAlException]
def alSourceUnqueueBuffers(source: Int, buffers: IntBuffer): Unit = run("alSourceUnqueueBuffers") {
AL10.alSourceUnqueueBuffers(source, buffers)
}
+ @throws[OpenAlException]
def alGetSourcei(source: Int, pname: Int): Int = run("alGetSourcei") {
AL10.alGetSourcei(source, pname)
}
+ @throws[OpenAlException]
def alSourcei(source: Int, pname: Int, value: Int): Unit = run("alSourcei") {
AL10.alSourcei(source, pname, value)
}
+ @throws[OpenAlException]
def alSourcef(source: Int, pname: Int, value: Float): Unit = run("alSourcef") {
AL10.alSourcef(source, pname, value)
}
+ @throws[OpenAlException]
def alSource3f(source: Int, pname: Int, v1: Float, v2: Float, v3: Float): Unit = run("alSource3f") {
AL10.alSource3f(source, pname, v1, v2, v3)
}
+ @throws[OpenAlException]
def alSourcePlay(source: Int): Unit = run("alSourcePlay") {
AL10.alSourcePlay(source)
}
+ @throws[OpenAlException]
def alSourcePause(source: Int): Unit = run("alSourcePause") {
AL10.alSourcePause(source)
}
+ @throws[OpenAlException]
def alSourceStop(source: Int): Unit = run("alSourceStop") {
AL10.alSourceStop(source)
}
+ @throws[OpenAlException]
def alDeleteSources(source: Int): Unit = run("alDeleteSources") {
AL10.alDeleteSources(source)
}
diff --git a/src/main/scala/ocelot/desktop/audio/Audio.scala b/src/main/scala/ocelot/desktop/audio/Audio.scala
index 63b2242..bb01d83 100644
--- a/src/main/scala/ocelot/desktop/audio/Audio.scala
+++ b/src/main/scala/ocelot/desktop/audio/Audio.scala
@@ -1,12 +1,13 @@
package ocelot.desktop.audio
import ocelot.desktop.Settings
-import ocelot.desktop.util.Logging
+import ocelot.desktop.util.{Logging, OpenAlException, Transaction}
import org.lwjgl.LWJGLException
import org.lwjgl.openal.{AL, AL10, ALC10}
import java.nio.{ByteBuffer, ByteOrder}
import scala.collection.mutable
+import scala.util.control.Exception.catching
object Audio extends Logging {
val sampleRate: Int = 44100
@@ -35,32 +36,52 @@ object Audio extends Logging {
sources.size
}
- def newStream(soundCategory: SoundCategory.Value, pitch: Float = 1f,
- volume: Float = 1f): (SoundStream, SoundSource) =
- {
+ def newStream(
+ soundCategory: SoundCategory.Value,
+ pitch: Float = 1f,
+ volume: Float = 1f
+ ): (SoundStream, SoundSource) = {
var source: SoundSource = null
val stream = new SoundStream {
override def enqueue(samples: SoundSamples): Unit = Audio.synchronized {
- val sourceId = if (sources.contains(source)) {
- sources(source)
- } else {
- val sourceId = AL10W.alGenSources()
+ OpenAlException.ignoring {
+ Transaction.runAbortable { tx =>
+ if (!Audio.isDisabled) {
+ val sourceId = if (sources.contains(source)) {
+ sources(source)
+ } else {
+ Transaction.run { tx =>
+ val sourceId = AL10W.alGenSources()
+ tx.onFailure {
+ AL10W.alDeleteSources(sourceId)
+ }
- AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
- AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
- AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster)
- sources.put(source, sourceId)
+ AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
+ AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
+ AL10W.alSourcef(
+ sourceId,
+ AL10.AL_GAIN,
+ source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
+ )
+ sources.put(source, sourceId)
- sourceId
+ sourceId
+ }
+ }
+
+ cleanupSourceBuffers(sourceId)
+
+ val bufferId = samples.genBuffer().getOrElse { tx.abort() }
+ tx.onFailure { AL10W.alDeleteBuffers(bufferId) }
+ AL10W.alSourceQueueBuffers(sourceId, bufferId)
+
+ if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
+ AL10W.alSourcePlay(sourceId)
+ }
+ }
+ }
}
-
- cleanupSourceBuffers(sourceId)
-
- val bufferId = samples.genBuffer()
- AL10W.alSourceQueueBuffers(sourceId, bufferId)
- if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING)
- AL10W.alSourcePlay(sourceId)
}
}
@@ -73,56 +94,96 @@ object Audio extends Logging {
return SoundSource.Status.Stopped
val sourceId = sources(source)
- AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
- case AL10.AL_PLAYING => SoundSource.Status.Playing
- case AL10.AL_PAUSED => SoundSource.Status.Paused
+
+ catching(classOf[OpenAlException]) opt AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
+ case Some(AL10.AL_PLAYING) => SoundSource.Status.Playing
+ case Some(AL10.AL_PAUSED) => SoundSource.Status.Paused
case _ => SoundSource.Status.Stopped
}
}
def playSource(source: SoundSource): Unit = synchronized {
- if (getSourceStatus(source) == SoundSource.Status.Playing)
- return
+ OpenAlException.ignoring {
+ if (Audio.isDisabled) {
+ return
+ }
- if (sources.contains(source)) {
- AL10W.alSourcePlay(sources(source))
- return
+ if (getSourceStatus(source) == SoundSource.Status.Playing) {
+ return
+ }
+
+ if (sources.contains(source)) {
+ AL10W.alSourcePlay(sources(source))
+ return
+ }
+
+ Transaction.runAbortable { tx =>
+ val sourceId = AL10W.alGenSources()
+ tx.onFailure { AL10W.alDeleteSources(sourceId) }
+
+ source.kind match {
+ case SoundSource.KindSoundBuffer(buffer) =>
+ buffer.bufferId match {
+ case Some(bufferId) =>
+ AL10W.alSourcei(sourceId, AL10.AL_BUFFER, bufferId)
+
+ case None =>
+ logger.error(s"Called play on a SoundBuffer $buffer with bufferId = None")
+ tx.abort()
+ }
+
+ case SoundSource.KindSoundSamples(samples) =>
+ Transaction.run { innerTx =>
+ val bufferId = samples.genBuffer().getOrElse { tx.abort() }
+ innerTx.onFailure { AL10W.alDeleteBuffers(bufferId) }
+ AL10W.alSourceQueueBuffers(sourceId, bufferId)
+ }
+
+ case SoundSource.KindStream(_) =>
+ }
+
+ AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
+ AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
+ AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE)
+ AL10W.alSourcef(
+ sourceId,
+ AL10.AL_GAIN,
+ source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
+ )
+ AL10W.alSourcePlay(sourceId)
+
+ sources.put(source, sourceId)
+ }
}
-
- val sourceId = AL10W.alGenSources()
- source.kind match {
- case SoundSource.KindSoundBuffer(buffer) =>
- AL10W.alSourcei(sourceId, AL10.AL_BUFFER, buffer.bufferId)
- case SoundSource.KindSoundSamples(samples) =>
- val bufferId = samples.genBuffer()
- if (bufferId != -1) AL10W.alSourceQueueBuffers(sourceId, bufferId)
- case SoundSource.KindStream(_) =>
- }
-
- AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
- AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
- AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE)
- AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster)
- AL10W.alSourcePlay(sourceId)
-
- sources.put(source, sourceId)
}
def pauseSource(source: SoundSource): Unit = synchronized {
- if (getSourceStatus(source) == SoundSource.Status.Paused)
- return
+ OpenAlException.ignoring {
+ if (Audio.isDisabled) {
+ return
+ }
- if (sources.contains(source)) {
- AL10W.alSourcePause(sources(source))
+ if (getSourceStatus(source) == SoundSource.Status.Paused)
+ return
+
+ if (sources.contains(source)) {
+ AL10W.alSourcePause(sources(source))
+ }
}
}
def stopSource(source: SoundSource): Unit = synchronized {
- if (getSourceStatus(source) == SoundSource.Status.Stopped)
- return
+ OpenAlException.ignoring {
+ if (Audio.isDisabled) {
+ return
+ }
- if (sources.contains(source)) {
- AL10W.alSourceStop(sources(source))
+ if (getSourceStatus(source) == SoundSource.Status.Stopped)
+ return
+
+ if (sources.contains(source)) {
+ AL10W.alSourceStop(sources(source))
+ }
}
}
@@ -130,14 +191,21 @@ object Audio extends Logging {
if (isDisabled) return
sources.filterInPlace { case (source, sourceId) =>
- cleanupSourceBuffers(sourceId)
+ OpenAlException.defaulting(false) {
+ cleanupSourceBuffers(sourceId)
- AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster)
- AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
- case AL10.AL_STOPPED =>
- deleteSource(sourceId)
- false
- case _ => true
+ AL10W.alSourcef(
+ sourceId,
+ AL10.AL_GAIN,
+ source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
+ )
+
+ AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
+ case AL10.AL_STOPPED =>
+ deleteSource(sourceId)
+ false
+ case _ => true
+ }
}
}
}
@@ -146,19 +214,24 @@ object Audio extends Logging {
if (isDisabled) return
for ((_, sourceId) <- sources) {
- deleteSource(sourceId)
+ OpenAlException.ignoring {
+ deleteSource(sourceId)
+ }
}
sources.clear()
+ AL.destroy()
_disabled = true
}
+ @throws[OpenAlException]
private def deleteSource(sourceId: Int): Unit = {
AL10W.alSourceStop(sourceId)
cleanupSourceBuffers(sourceId)
AL10W.alDeleteSources(sourceId)
}
+ @throws[OpenAlException]
private def cleanupSourceBuffers(sourceId: Int): Unit = {
val count = AL10W.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED)
if (count <= 0) return
diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala
index 926dcab..2622fc1 100644
--- a/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala
+++ b/src/main/scala/ocelot/desktop/audio/SoundBuffer.scala
@@ -1,27 +1,29 @@
package ocelot.desktop.audio
-import ocelot.desktop.util.{FileUtils, Logging, Resource, ResourceManager}
+import ocelot.desktop.util.{FileUtils, Logging, OpenAlException, Resource}
import org.lwjgl.openal.AL10
class SoundBuffer(val file: String) extends Resource with Logging {
- private var _bufferId: Int = -1
+ private var _bufferId: Option[Int] = None
- ResourceManager.registerResource(this)
+ Audio.synchronized {
+ OpenAlException.ignoring {
+ if (!Audio.isDisabled) {
+ logger.debug(s"Loading sound buffer from '$file'...")
+ _bufferId = Some(AL10W.alGenBuffers())
- override def initResource(): Unit = {
- if (Audio.isDisabled)
- return
-
- logger.debug(s"Loading sound buffer from '$file'...")
- _bufferId = AL10W.alGenBuffers()
-
- if (AL10W.alIsExtensionPresent("AL_EXT_vorbis")) {
- initWithExt()
- } else {
- initFallback()
+ if (AL10W.alIsExtensionPresent("AL_EXT_vorbis")) {
+ initWithExt()
+ } else {
+ initFallback()
+ }
+ } else {
+ logger.debug(s"Skipping loading sound buffer from '$file' because audio is not available")
+ }
}
}
+ @throws[OpenAlException]
private def initWithExt(): Unit = {
val fileBuffer = FileUtils.load(file)
if (fileBuffer == null) {
@@ -29,40 +31,49 @@ class SoundBuffer(val file: String) extends Resource with Logging {
return
}
- AL10W.alBufferData(bufferId, AL10.AL_FORMAT_VORBIS_EXT, fileBuffer, -1)
+ AL10W.alBufferData(bufferId.get, AL10.AL_FORMAT_VORBIS_EXT, fileBuffer, -1)
}
+ @throws[OpenAlException]
private def initFallback(): Unit = {
val ogg = OggDecoder.decode(getClass.getResourceAsStream(file))
_bufferId = ogg.genBuffer()
}
- def numSamples: Int = {
- if (bufferId == -1) {
- return 0
+ def numSamples: Int = bufferId match {
+ case Some(bufferId) =>
+ OpenAlException.defaulting(0) {
+ val sizeBytes = AL10W.alGetBufferi(bufferId, AL10.AL_SIZE)
+ val channels = AL10W.alGetBufferi(bufferId, AL10.AL_CHANNELS)
+ val bits = AL10W.alGetBufferi(bufferId, AL10.AL_BITS)
+
+ sizeBytes * 8 / channels / bits
}
- val sizeBytes = AL10W.alGetBufferi(bufferId, AL10.AL_SIZE)
- val channels = AL10W.alGetBufferi(bufferId, AL10.AL_CHANNELS)
- val bits = AL10W.alGetBufferi(bufferId, AL10.AL_BITS)
-
- sizeBytes * 8 / channels / bits
+ case None => 0
}
- val sampleRate: Int = {
- if (bufferId == -1) {
- 44100
- } else {
- AL10W.alGetBufferi(bufferId, AL10.AL_FREQUENCY)
- }
+ val sampleRate: Int = bufferId match {
+ case Some(bufferId) =>
+ OpenAlException.defaulting(44100) {
+ Audio.synchronized {
+ AL10W.alGetBufferi(bufferId, AL10.AL_FREQUENCY)
+ }
+ }
+
+ case None => 44100
}
- def bufferId: Int = _bufferId
+ def bufferId: Option[Int] = _bufferId
- def freeResource(): Unit = {
- if (bufferId != -1) {
- AL10W.alDeleteBuffers(bufferId)
- logger.debug(s"Destroyed sound buffer (ID: $bufferId) loaded from $file")
+ override def freeResource(): Unit = {
+ super.freeResource()
+
+ bufferId.foreach { bufferId =>
+ OpenAlException.ignoring {
+ AL10W.alDeleteBuffers(bufferId)
+ logger.debug(s"Destroyed sound buffer (ID: $bufferId) loaded from $file")
+ }
}
}
}
diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala
index 779f67f..7f202ac 100644
--- a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala
+++ b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala
@@ -1,33 +1,51 @@
package ocelot.desktop.audio
-object SoundBuffers {
- val MachineComputerRunning = new SoundBuffer("/ocelot/desktop/sounds/machine/computer_running.ogg")
- val MachineFloppyAccess: Array[SoundBuffer] = Array(
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access1.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access2.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access3.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access4.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access5.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_access6.ogg"),
- )
- val MachineFloppyEject = new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_eject.ogg")
- val MachineFloppyInsert = new SoundBuffer("/ocelot/desktop/sounds/machine/floppy_insert.ogg")
- val MachineHDDAccess: Array[SoundBuffer] = Array(
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access1.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access2.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access3.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access4.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access5.ogg"),
- new SoundBuffer("/ocelot/desktop/sounds/machine/hdd_access6.ogg"),
- )
- val InterfaceClick = new SoundBuffer("/ocelot/desktop/sounds/interface/click.ogg")
- val InterfaceTick = new SoundBuffer("/ocelot/desktop/sounds/interface/tick.ogg")
- val MinecraftClick = new SoundBuffer("/ocelot/desktop/sounds/minecraft/click.ogg")
+import ocelot.desktop.util.Resource
- val NoteBlock: Map[String, SoundBuffer] = List(
+import scala.collection.mutable.ArrayBuffer
+
+object SoundBuffers extends Resource {
+ lazy val MachineComputerRunning: SoundBuffer = load("/ocelot/desktop/sounds/machine/computer_running.ogg")
+ lazy val MachineFloppyAccess: Array[SoundBuffer] = Array(
+ load("/ocelot/desktop/sounds/machine/floppy_access1.ogg"),
+ load("/ocelot/desktop/sounds/machine/floppy_access2.ogg"),
+ load("/ocelot/desktop/sounds/machine/floppy_access3.ogg"),
+ load("/ocelot/desktop/sounds/machine/floppy_access4.ogg"),
+ load("/ocelot/desktop/sounds/machine/floppy_access5.ogg"),
+ load("/ocelot/desktop/sounds/machine/floppy_access6.ogg"),
+ )
+ lazy val MachineFloppyEject: SoundBuffer = load("/ocelot/desktop/sounds/machine/floppy_eject.ogg")
+ lazy val MachineFloppyInsert: SoundBuffer = load("/ocelot/desktop/sounds/machine/floppy_insert.ogg")
+ lazy val MachineHDDAccess: Array[SoundBuffer] = Array(
+ load("/ocelot/desktop/sounds/machine/hdd_access1.ogg"),
+ load("/ocelot/desktop/sounds/machine/hdd_access2.ogg"),
+ load("/ocelot/desktop/sounds/machine/hdd_access3.ogg"),
+ load("/ocelot/desktop/sounds/machine/hdd_access4.ogg"),
+ load("/ocelot/desktop/sounds/machine/hdd_access5.ogg"),
+ load("/ocelot/desktop/sounds/machine/hdd_access6.ogg"),
+ )
+ lazy val InterfaceClick: SoundBuffer = load("/ocelot/desktop/sounds/interface/click.ogg")
+ lazy val InterfaceTick: SoundBuffer = load("/ocelot/desktop/sounds/interface/tick.ogg")
+ lazy val MinecraftClick: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click.ogg")
+ lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg")
+
+ lazy val NoteBlock: Map[String, SoundBuffer] = List(
"banjo", "basedrum", "bass", "bell", "bit", "chime", "cow_bell", "didgeridoo", "flute", "guitar",
"harp", "hat", "iron_xylophone", "pling", "snare", "xylophone"
).map(name => {
- (name, new SoundBuffer(s"/ocelot/desktop/sounds/minecraft/note_block/$name.ogg"))
+ (name, load(s"/ocelot/desktop/sounds/minecraft/note_block/$name.ogg"))
}).toMap
+
+ private val loaded = new ArrayBuffer[SoundBuffer]()
+
+ private def load(file: String): SoundBuffer = {
+ val buffer = new SoundBuffer(file)
+ loaded.append(buffer)
+ buffer
+ }
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ loaded.foreach(_.freeResource())
+ }
}
diff --git a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala
index ccf172b..6fddac2 100644
--- a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala
+++ b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala
@@ -1,24 +1,28 @@
package ocelot.desktop.audio
-import ocelot.desktop.util.Logging
+import ocelot.desktop.util.{Logging, OpenAlException}
import org.lwjgl.openal.AL10
import java.nio.ByteBuffer
+import scala.util.control.Exception.catching
case class SoundSamples(data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging {
- def genBuffer(): Int = {
- val bufferId = AL10W.alGenBuffers()
- val formatId = format match {
- case SoundSamples.Format.Stereo16 => AL10.AL_FORMAT_STEREO16
- case SoundSamples.Format.Mono16 => AL10.AL_FORMAT_MONO16
- case SoundSamples.Format.Mono8 => AL10.AL_FORMAT_MONO8
+ def genBuffer(): Option[Int] = Audio.synchronized {
+ if (Audio.isDisabled) return None
+
+ catching(classOf[OpenAlException]) opt {
+ val bufferId = AL10W.alGenBuffers()
+ val formatId = format match {
+ case SoundSamples.Format.Stereo16 => AL10.AL_FORMAT_STEREO16
+ case SoundSamples.Format.Mono16 => AL10.AL_FORMAT_MONO16
+ case SoundSamples.Format.Mono8 => AL10.AL_FORMAT_MONO8
+ }
+
+ AL10W.alBufferData(bufferId, formatId, data, rate)
+
+ bufferId
}
-
- AL10W.alBufferData(bufferId, formatId, data, rate)
- bufferId
}
-
-
}
object SoundSamples {
diff --git a/src/main/scala/ocelot/desktop/audio/SoundSource.scala b/src/main/scala/ocelot/desktop/audio/SoundSource.scala
index 6784653..b0fd365 100644
--- a/src/main/scala/ocelot/desktop/audio/SoundSource.scala
+++ b/src/main/scala/ocelot/desktop/audio/SoundSource.scala
@@ -1,6 +1,7 @@
package ocelot.desktop.audio
-import java.time.Duration
+import java.util.concurrent.TimeUnit
+import scala.concurrent.duration.Duration
class SoundSource(val kind: SoundSource.Kind,
val soundCategory: SoundCategory.Value,
@@ -8,20 +9,21 @@ class SoundSource(val kind: SoundSource.Kind,
val pitch: Float,
var volume: Float) {
- def duration: Duration = {
- val seconds = kind match {
- case SoundSource.KindSoundBuffer(buffer) =>
- buffer.numSamples.toFloat / buffer.sampleRate
- case SoundSource.KindSoundSamples(SoundSamples(buffer, rate, format)) =>
- val bps = format match {
- case SoundSamples.Format.Stereo16 => 2
- case SoundSamples.Format.Mono8 => 1
- case SoundSamples.Format.Mono16 => 2
- }
- buffer.limit().toFloat / (rate * bps)
- }
+ def duration: Option[Duration] = kind match {
+ case SoundSource.KindSoundBuffer(buffer) =>
+ Some(Duration(buffer.numSamples.toFloat / buffer.sampleRate, TimeUnit.SECONDS))
- Duration.ofNanos((seconds * 1e9).toLong)
+ case SoundSource.KindSoundSamples(SoundSamples(buffer, rate, format)) =>
+ val bps = format match {
+ case SoundSamples.Format.Stereo16 => 2
+ case SoundSamples.Format.Mono8 => 1
+ case SoundSamples.Format.Mono16 => 2
+ }
+
+ Some(Duration(buffer.limit().toFloat / (rate * bps), TimeUnit.SECONDS))
+
+ case SoundSource.KindStream(_) =>
+ None
}
def status: SoundSource.Status.Value = {
@@ -73,8 +75,7 @@ object SoundSource {
}
def fromStream(stream: SoundStream, soundCategory: SoundCategory.Value,
- looping: Boolean = false, pitch: Float = 1f, volume: Float = 1f): SoundSource =
- {
+ looping: Boolean = false, pitch: Float = 1f, volume: Float = 1f): SoundSource = {
new SoundSource(SoundSource.KindStream(stream), soundCategory, looping, pitch, volume)
}
diff --git a/src/main/scala/ocelot/desktop/audio/SoundSources.scala b/src/main/scala/ocelot/desktop/audio/SoundSources.scala
index 3dfd972..b788cde 100644
--- a/src/main/scala/ocelot/desktop/audio/SoundSources.scala
+++ b/src/main/scala/ocelot/desktop/audio/SoundSources.scala
@@ -7,4 +7,5 @@ object SoundSources {
val InterfaceTick = SoundSource.fromBuffer(SoundBuffers.InterfaceTick, SoundCategory.Interface)
val MinecraftClick = SoundSource.fromBuffer(SoundBuffers.MinecraftClick, SoundCategory.Interface)
+ val MinecraftExplosion = SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment)
}
diff --git a/src/main/scala/ocelot/desktop/entity/Camera.scala b/src/main/scala/ocelot/desktop/entity/Camera.scala
index 4336d02..a1895ba 100644
--- a/src/main/scala/ocelot/desktop/entity/Camera.scala
+++ b/src/main/scala/ocelot/desktop/entity/Camera.scala
@@ -2,19 +2,15 @@ package ocelot.desktop.entity
import ocelot.desktop.util.WebcamCapture
import totoro.ocelot.brain.Constants
-import totoro.ocelot.brain.entity.machine.{Arguments, Callback, Context}
+import totoro.ocelot.brain.entity.machine.{Arguments, Context}
import totoro.ocelot.brain.entity.traits.DeviceInfo.{DeviceAttribute, DeviceClass}
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, GenericCamera}
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.ResultWrapper.result
import totoro.ocelot.brain.workspace.Workspace
-object Camera {
- private val DistanceCallCost = 20
-}
-
class Camera extends Entity with GenericCamera with DeviceInfo {
- var webcamCapture: WebcamCapture = WebcamCapture.getDefault
+ var webcamCapture: Option[WebcamCapture] = WebcamCapture.getDefault
var flipHorizontally: Boolean = false
var flipVertically: Boolean = false
var directCalls: Boolean = false
@@ -23,31 +19,38 @@ class Camera extends Entity with GenericCamera with DeviceInfo {
if (!directCalls)
context.consumeCallBudget(1.0 / Camera.DistanceCallCost)
- var x: Float = 0f
- var y: Float = 0f
- if (args.count() == 2) {
- // [-1; 1] => [0; 1]
- x = (args.checkDouble(0).toFloat + 1f) / 2f
- y = (args.checkDouble(1).toFloat + 1f) / 2f
- }
+ result(webcamCapture match {
+ case Some(webcamCapture) =>
+ var x: Float = 0f
+ var y: Float = 0f
- if (!flipHorizontally) x = 1f - x
- if (!flipVertically) y = 1f - y
- result(webcamCapture.ray(x, y))
+ if (args.count() == 2) {
+ // [-1; 1] => [0; 1]
+ x = (args.checkDouble(0).toFloat + 1f) / 2f
+ y = (args.checkDouble(1).toFloat + 1f) / 2f
+ }
+
+ if (!flipHorizontally) x = 1f - x
+ if (!flipVertically) y = 1f - y
+
+ webcamCapture.ray(x, y)
+
+ case None => 0f
+ })
}
override def getDeviceInfo: Map[String, String] = Map(
DeviceAttribute.Class -> DeviceClass.Multimedia,
DeviceAttribute.Description -> "Dungeon Scanner 2.5D",
DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor,
- DeviceAttribute.Product -> webcamCapture.name
+ DeviceAttribute.Product -> webcamCapture.map(_.name).getOrElse("Blind Pirate")
)
override def load(nbt: NBTTagCompound, workspace: Workspace): Unit = {
super.load(nbt, workspace)
if (nbt.hasKey("device"))
- webcamCapture = WebcamCapture.getInstance(nbt.getString("device"))
+ webcamCapture = WebcamCapture.getInstance(nbt.getString("device")).orElse(WebcamCapture.getDefault)
if (nbt.hasKey("flipHorizontally"))
flipHorizontally = nbt.getBoolean("flipHorizontally")
@@ -62,9 +65,13 @@ class Camera extends Entity with GenericCamera with DeviceInfo {
override def save(nbt: NBTTagCompound): Unit = {
super.save(nbt)
- nbt.setString("device", webcamCapture.name)
+ webcamCapture.foreach(capture => nbt.setString("device", capture.name))
nbt.setBoolean("flipHorizontally", flipHorizontally)
nbt.setBoolean("flipVertically", flipVertically)
nbt.setBoolean("directCalls", directCalls)
}
}
+
+object Camera {
+ private val DistanceCallCost = 20
+}
diff --git a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala
index b677782..f674091 100644
--- a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala
+++ b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala
@@ -5,7 +5,6 @@ import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource}
import ocelot.desktop.color.IntColor
import ocelot.desktop.util.Logging
import org.lwjgl.BufferUtils
-import totoro.ocelot.brain.Ocelot.logger
import totoro.ocelot.brain.entity.machine.{Arguments, Callback, Context}
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, Environment}
import totoro.ocelot.brain.nbt.NBTTagCompound
@@ -21,7 +20,8 @@ import javax.sound.sampled.{AudioFormat, AudioSystem}
class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
override val node: Component =
- Network.newNode(this, Visibility.Network)
+ Network
+ .newNode(this, Visibility.Network)
.withComponent("openfm_radio", Visibility.Network)
.create()
@@ -105,22 +105,13 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
bytesRead = inputStream.read(buffer, 0, buffer.length)
}
+
+ logger.info("OpenFM input audio stream has reached EOF, closing thread")
}
catch {
case _: InterruptedException =>
case e: Exception => logger.error("OpenFM playback exception", e)
}
-
- // Stopping playback and cleaning up resources after exit from read loop
- this.synchronized {
- if (playbackSoundSource.isDefined) {
- playbackSoundSource.get.stop()
- playbackSoundSource = None
- }
-
- if (playbackThread.isDefined)
- playbackThread = None
- }
}
def play(): Boolean = {
@@ -139,24 +130,35 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
true
}
- def stop(): Boolean = {
+ def stop(): Unit = {
this.synchronized {
- if (playbackThread.isDefined)
+ // Stopping reading data from url
+ if (playbackThread.isDefined) {
playbackThread.get.interrupt()
- }
+ playbackThread = None
+ }
- true
+ // Stopping playback
+ if (playbackSoundSource.isDefined) {
+ playbackSoundSource.get.stop()
+ playbackSoundSource = None
+ }
+ }
}
- def isPlaying: Boolean = playbackThread.isDefined
+ def isPlaying: Boolean =
+ playbackSoundSource.isDefined && playbackSoundSource.get.isPlaying || playbackThread.isDefined
@Callback()
def start(context: Context, args: Arguments): Array[AnyRef] =
result(play())
@Callback()
- def stop(context: Context, args: Arguments): Array[AnyRef] =
- result(stop())
+ def stop(context: Context, args: Arguments): Array[AnyRef] = {
+ stop()
+
+ result(true)
+ }
@Callback()
def isPlaying(context: Context, args: Arguments): Array[AnyRef] =
diff --git a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala
index 4cbbe9a..d0d652e 100644
--- a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala
+++ b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala
@@ -8,9 +8,9 @@ object Rect2D {
//noinspection ScalaWeakerAccess,ScalaUnusedSymbol
case class Rect2D(x: Float, y: Float, w: Float, h: Float) {
- def contains(p: Vector2D): Boolean = p.x >= x && p.y >= y && p.x <= x + w && p.y <= y + h
+ def contains(p: Vector2D): Boolean = p.x >= x && p.y >= y && p.x < x + w && p.y < y + h
- def contains(r: Rect2D): Boolean = r.x >= x && r.y >= y && r.x + r.w <= x + w && r.y + r.h <= y + h
+ def contains(r: Rect2D): Boolean = r.x >= x && r.y >= y && r.x + r.w < x + w && r.y + r.h < y + h
def origin: Vector2D = Vector2D(x, y)
diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala
index c655f70..396ba52 100644
--- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala
+++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala
@@ -13,7 +13,7 @@ import scala.collection.mutable
import scala.util.control.Breaks._
//noinspection ScalaWeakerAccess,ScalaUnusedSymbol
-class Graphics extends Logging with Resource {
+class Graphics(private var scalingFactor: Float) extends Logging with Resource {
private var time = 0f
private var projection = Transform2D.viewport(800, 600)
@@ -29,6 +29,7 @@ class Graphics extends Logging with Resource {
private var oldFont: Font = _font
private val stack = mutable.Stack[GraphicsState](GraphicsState())
+
private var spriteRect = Spritesheet.sprites("Empty")
private val emptySpriteTrans = Transform2D.translate(spriteRect.x, spriteRect.y) >> Transform2D.scale(spriteRect.w, spriteRect.h)
@@ -43,16 +44,43 @@ class Graphics extends Logging with Resource {
shaderProgram.set("uTexture", 0)
shaderProgram.set("uTextTexture", 1)
- def resize(width: Int, height: Int): Unit = {
- offscreenTexture.bind()
- GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height, 0, GL11.GL_RGB,
- GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer])
- GL11.glViewport(0, 0, width, height)
+ scale(scalingFactor)
+
+ def resize(width: Int, height: Int, scaling: Float): Boolean = {
+ var viewportChanged = false
+
+ if (scaling != scalingFactor) {
+ scalingFactor = scaling
+ stack.last.transform = Transform2D.scale(scalingFactor)
+ viewportChanged = true
+ }
+
+ if (this.width != width || this.height != height) {
+ this.width = width
+ this.height = height
+
+ offscreenTexture.bind()
+ GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height, 0, GL11.GL_RGB,
+ GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer])
+ GL11.glViewport(0, 0, width, height)
+
+ viewportChanged = true
+ }
+ viewportChanged
}
override def freeResource(): Unit = {
- logger.debug(s"Destroyed FBO (ID: $offscreenFramebuffer)")
+ super.freeResource()
+
GL30.glDeleteFramebuffers(offscreenFramebuffer)
+ logger.debug(s"Destroyed FBO (ID: $offscreenFramebuffer)")
+
+ offscreenTexture.freeResource()
+ Spritesheet.freeResource()
+ smallFont.freeResource()
+ normalFont.freeResource()
+ renderer.freeResource()
+ shaderProgram.freeResource()
}
def font: Font = _font
@@ -106,16 +134,13 @@ class Graphics extends Logging with Resource {
offscreenTexture.bind()
foreground = RGBAColorNorm(1, 1, 1, alpha)
spriteRect = Rect2D(0, 1f, 1f, -1f)
- _rect(0, 0, width, height, fixUV = false)
+
+ _rect(0, 0, width / scalingFactor, height / scalingFactor, fixUV = false)
renderer.flush()
}
- def setViewport(width: Int, height: Int): Unit = {
- projection = Transform2D.viewport(width, height)
- this.width = width
- this.height = height
-
- shaderProgram.set("uProj", projection)
+ def startViewport(): Unit = {
+ shaderProgram.set("uProj", Transform2D.viewport(width, height))
}
def clear(): Unit = {
@@ -124,16 +149,19 @@ class Graphics extends Logging with Resource {
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT)
}
- def setScissor(x: Int, y: Int, width: Int, height: Int): Unit = {
+ def setScissor(x: Float, y: Float, width: Float, height: Float): Unit = {
flush()
- stack.head.scissor = Some((x, y, width, height))
- GL11.glScissor(x, this.height - height - y, width, height)
+ stack.head.scissor = Some((x, y, width * scalingFactor, height * scalingFactor))
+ // TODO: make them aware of the transform stack
+ GL11.glScissor(
+ Math.round(x * scalingFactor),
+ Math.round(this.height - height * scalingFactor - y * scalingFactor),
+ Math.round(width * scalingFactor),
+ Math.round(height * scalingFactor)
+ )
GL11.glEnable(GL11.GL_SCISSOR_TEST)
}
- def setScissor(x: Float, y: Float, width: Float, height: Float): Unit = {
- setScissor(x.round, y.round, width.round, height.round)
- }
def setScissor(): Unit = {
flush()
@@ -207,7 +235,7 @@ class Graphics extends Logging with Resource {
}
}
- def char(_x: Float, _y: Float, c: Char, scaleX: Float = 1f, scaleY: Float = 1f): Unit = {
+ def char(_x: Float, _y: Float, c: Int, scaleX: Float = 1f, scaleY: Float = 1f): Unit = {
val fontSize = fontSizeMultiplier * _font.fontSize
val x = _x.round
val y = _y.round
diff --git a/src/main/scala/ocelot/desktop/graphics/GraphicsState.scala b/src/main/scala/ocelot/desktop/graphics/GraphicsState.scala
index 620a089..16235cd 100644
--- a/src/main/scala/ocelot/desktop/graphics/GraphicsState.scala
+++ b/src/main/scala/ocelot/desktop/graphics/GraphicsState.scala
@@ -9,6 +9,6 @@ case class GraphicsState(
var fontSizeMultiplier: Float = 1f,
var alphaMultiplier: Float = 1f,
var sprite: String = "Empty",
- var scissor: Option[(Int, Int, Int, Int)] = None,
+ var scissor: Option[(Float, Float, Float, Float)] = None,
var transform: Transform2D = Transform2D.identity
)
diff --git a/src/main/scala/ocelot/desktop/graphics/Icons.scala b/src/main/scala/ocelot/desktop/graphics/Icons.scala
new file mode 100644
index 0000000..5ab8510
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/graphics/Icons.scala
@@ -0,0 +1,96 @@
+package ocelot.desktop.graphics
+
+import ocelot.desktop.graphics.IconDef.Animation
+import ocelot.desktop.ui.widget.modal.notification.NotificationType.NotificationType
+import totoro.ocelot.brain.util.DyeColor
+import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier
+import totoro.ocelot.brain.util.Tier.Tier
+
+object Icons {
+ // Icons
+ val CardIcon: IconDef = IconDef("icons/Card")
+ val CpuIcon: IconDef = IconDef("icons/CPU")
+ val HddIcon: IconDef = IconDef("icons/HDD")
+ val EepromIcon: IconDef = IconDef("icons/EEPROM")
+ val FloppyIcon: IconDef = IconDef("icons/Floppy")
+ val MemoryIcon: IconDef = IconDef("icons/Memory")
+
+ val TierIcon: Tier => IconDef = { tier =>
+ IconDef(s"icons/Tier${tier.id}")
+ }
+
+ val NotificationIcon: NotificationType => IconDef = { notificationType =>
+ IconDef(s"icons/Notification$notificationType", 4)
+ }
+
+ val SettingsSound: IconDef = IconDef("icons/SettingsSound")
+ val SettingsUI: IconDef = IconDef("icons/SettingsUI")
+
+ // Items
+ val Cpu: Tier => IconDef = { tier =>
+ IconDef(s"items/CPU${tier.id}")
+ }
+
+ val Apu: Tier => IconDef = { tier =>
+ IconDef(s"items/APU${tier.id}", animation = Some(Animations.Apu))
+ }
+
+ val GraphicsCard: Tier => IconDef = { tier =>
+ IconDef(s"items/GraphicsCard${tier.id}")
+ }
+
+ val NetworkCard: IconDef = IconDef("items/NetworkCard")
+
+ val WirelessNetworkCard: Tier => IconDef = { tier =>
+ IconDef(s"items/WirelessNetworkCard${tier.id}")
+ }
+
+ val LinkedCard: IconDef = IconDef("items/LinkedCard", animation = Some(Animations.LinkedCard))
+
+ val InternetCard: IconDef = IconDef("items/InternetCard", animation = Some(Animations.InternetCard))
+
+ val RedstoneCard: Tier => IconDef = { tier =>
+ IconDef(s"items/RedstoneCard${tier.id}")
+ }
+
+ val DataCard: Tier => IconDef = { tier =>
+ IconDef(s"items/DataCard${tier.id}", animation = Some(Animations.DataCard))
+ }
+
+ val SoundCard: IconDef = IconDef("items/SoundCard", animation = Some(Animations.DataCard))
+
+ val SelfDestructingCard: IconDef = IconDef("items/SelfDestructingCard", animation = Some(Animations.SelfDestructingCard))
+
+ val HardDiskDrive: Tier => IconDef = { tier =>
+ IconDef(s"items/HardDiskDrive${tier.id}")
+ }
+
+ val Eeprom: IconDef = IconDef("items/EEPROM")
+
+ val FloppyDisk: DyeColor => IconDef = { color =>
+ IconDef(s"items/FloppyDisk_${color.name}")
+ }
+
+ val Memory: ExtendedTier => IconDef = { tier =>
+ IconDef(s"items/Memory${tier.id}")
+ }
+
+ //noinspection ScalaWeakerAccess
+ object Animations {
+ val Apu: Animation = Array(
+ (0, 3f), (1, 3f), (2, 3f), (3, 3f), (4, 3f), (5, 3f),
+ (4, 3f), (3, 3f), (2, 3f), (1, 3f), (0, 3f))
+
+ val LinkedCard: Animation =
+ Array((0, 3f), (1, 3f), (2, 3f), (3, 3f), (4, 3f), (5, 3f))
+
+ val InternetCard: Animation = Array(
+ (0, 2f), (1, 7f), (0, 5f), (1, 4f), (0, 7f), (1, 2f), (0, 8f),
+ (1, 9f), (0, 6f), (1, 4f))
+
+ val DataCard: Animation = Array(
+ (0, 4f), (1, 4f), (2, 4f), (3, 4f), (4, 4f), (5, 4f), (6, 4f), (7, 4f))
+
+ val SelfDestructingCard: Animation = Array((0, 4f), (1, 4f))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala b/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala
index 1b3f749..15650b9 100644
--- a/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala
+++ b/src/main/scala/ocelot/desktop/graphics/ShaderProgram.scala
@@ -1,7 +1,7 @@
package ocelot.desktop.graphics
import ocelot.desktop.geometry.Transform2D
-import ocelot.desktop.util.{Logging, Resource, ResourceManager}
+import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.BufferUtils
import org.lwjgl.opengl.GL20
@@ -34,13 +34,12 @@ class ShaderProgram(name: String) extends Logging with Resource {
}
}
- ResourceManager.registerResource(this)
-
- def freeResource(): Unit = {
- logger.debug(s"Destroyed shader program ($name)")
+ override def freeResource(): Unit = {
+ super.freeResource()
GL20.glDeleteProgram(shaderProgram)
GL20.glDeleteShader(vertexShader)
GL20.glDeleteShader(fragmentShader)
+ logger.debug(s"Destroyed shader program ($name)")
}
def bind(): Unit = {
diff --git a/src/main/scala/ocelot/desktop/graphics/Texture.scala b/src/main/scala/ocelot/desktop/graphics/Texture.scala
index 5a1ca6c..5a7254a 100644
--- a/src/main/scala/ocelot/desktop/graphics/Texture.scala
+++ b/src/main/scala/ocelot/desktop/graphics/Texture.scala
@@ -1,6 +1,6 @@
package ocelot.desktop.graphics
-import ocelot.desktop.util.{Logging, Resource, ResourceManager}
+import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.BufferUtils
import org.lwjgl.opengl._
@@ -10,13 +10,6 @@ import java.nio.ByteBuffer
class Texture extends Logging with Resource {
val texture: Int = GL11.glGenTextures()
- ResourceManager.registerResource(this)
-
- def freeResource(): Unit = {
- logger.debug(s"Destroyed texture (ID: $texture)")
- GL11.glDeleteTextures(texture)
- }
-
bind()
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST)
@@ -75,4 +68,10 @@ class Texture extends Logging with Resource {
GL13.glActiveTexture(GL13.GL_TEXTURE0 + unit)
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture)
}
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ GL11.glDeleteTextures(texture)
+ logger.debug(s"Destroyed texture (ID: $texture)")
+ }
}
diff --git a/src/main/scala/ocelot/desktop/graphics/buffer/Buffer.scala b/src/main/scala/ocelot/desktop/graphics/buffer/Buffer.scala
index 092dc7a..bf83198 100644
--- a/src/main/scala/ocelot/desktop/graphics/buffer/Buffer.scala
+++ b/src/main/scala/ocelot/desktop/graphics/buffer/Buffer.scala
@@ -12,9 +12,10 @@ class Buffer[T <: BufferPut] extends Logging with Resource {
var capacity: Int = _
var stride: Int = _
- def freeResource(): Unit = {
- logger.debug(s"Destroyed buffer (ID: $buffer) of ${capacity * stride} bytes")
+ override def freeResource(): Unit = {
+ super.freeResource()
GL15.glDeleteBuffers(buffer)
+ logger.debug(s"Destroyed buffer (ID: $buffer) of ${capacity * stride} bytes")
}
def this(elements: Seq[T]) = {
diff --git a/src/main/scala/ocelot/desktop/graphics/mesh/VertexArray.scala b/src/main/scala/ocelot/desktop/graphics/mesh/VertexArray.scala
index 8a28f14..9547454 100644
--- a/src/main/scala/ocelot/desktop/graphics/mesh/VertexArray.scala
+++ b/src/main/scala/ocelot/desktop/graphics/mesh/VertexArray.scala
@@ -2,7 +2,7 @@ package ocelot.desktop.graphics.mesh
import ocelot.desktop.graphics.ShaderProgram
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
-import ocelot.desktop.util.{Logging, Resource, ResourceManager}
+import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.opengl._
class VertexArray(shader: ShaderProgram) extends Logging with Resource {
@@ -13,16 +13,6 @@ class VertexArray(shader: ShaderProgram) extends Logging with Resource {
else
ARBVertexArrayObject.glGenVertexArrays()
- ResourceManager.registerResource(this)
-
- def freeResource(): Unit = {
- logger.debug(s"Destroyed VAO (ID: $array)")
- if (isMacOS)
- APPLEVertexArrayObject.glDeleteVertexArraysAPPLE(array)
- else
- ARBVertexArrayObject.glDeleteVertexArrays(array)
- }
-
def addVertexBuffer[V <: Vertex](buffer: VertexBuffer[V], instanced: Boolean = false): Unit = {
bind()
@@ -47,4 +37,13 @@ class VertexArray(shader: ShaderProgram) extends Logging with Resource {
APPLEVertexArrayObject.glBindVertexArrayAPPLE(array)
else
ARBVertexArrayObject.glBindVertexArray(array)
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ if (isMacOS)
+ APPLEVertexArrayObject.glDeleteVertexArraysAPPLE(array)
+ else
+ ARBVertexArrayObject.glDeleteVertexArrays(array)
+ logger.debug(s"Destroyed VAO (ID: $array)")
+ }
}
diff --git a/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala b/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala
index dc3389e..5deb931 100644
--- a/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala
+++ b/src/main/scala/ocelot/desktop/graphics/render/InstanceRenderer.scala
@@ -3,14 +3,14 @@ package ocelot.desktop.graphics.render
import ocelot.desktop.graphics.ShaderProgram
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
import ocelot.desktop.graphics.mesh.{Mesh, MeshInstance, MeshVertex, VertexArray}
-import ocelot.desktop.util.Logging
+import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.BufferUtils
import org.lwjgl.opengl._
import java.nio.ByteBuffer
import scala.collection.mutable.ArrayBuffer
-class InstanceRenderer(mesh: Mesh, shader: ShaderProgram) extends Logging {
+class InstanceRenderer(mesh: Mesh, shader: ShaderProgram) extends Resource with Logging {
private val InitialCapacity: Int = 4096
private val vertexBuffer = new VertexBuffer[MeshVertex](mesh.vertices)
@@ -81,4 +81,12 @@ class InstanceRenderer(mesh: Mesh, shader: ShaderProgram) extends Logging {
data.flip
data
}
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ vertexArray.freeResource()
+ instanceBuffer.freeResource()
+ indexBuffer.foreach(_.freeResource())
+ vertexBuffer.freeResource()
+ }
}
diff --git a/src/main/scala/ocelot/desktop/inventory/Inventory.scala b/src/main/scala/ocelot/desktop/inventory/Inventory.scala
new file mode 100644
index 0000000..48570f6
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/Inventory.scala
@@ -0,0 +1,232 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.inventory.Inventory.SlotObserver
+
+import scala.collection.mutable
+
+/**
+ * Provides an inventory — a collection of [[Item]]s indexed by slots.
+ */
+trait Inventory {
+ // parallels totoro.ocelot.brain.entity.traits.Inventory
+ // this is intentional
+
+ /**
+ * The type of items stored in this inventory.
+ */
+ type I <: Item
+
+ private type WeakHashSet[A] = mutable.WeakHashMap[A, Unit]
+
+ private val slotItems = mutable.HashMap.empty[Int, I]
+ private val itemSlots = mutable.HashMap.empty[I, Int]
+ private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]]
+
+ /**
+ * Called after a new item is added to the inventory.
+ *
+ * @param slot the slot the item was added to
+ */
+ def onItemAdded(slot: Slot): Unit
+
+ /**
+ * Called after an item is removed from the inventory.
+ *
+ * When the item is replaced by another one, the event are sequenced in the following order:
+ *
+ * 1. the old item is removed
+ * 1. [[onItemRemoved]] is called, with `replacedBy` containing the new item
+ * 1. the new item is added
+ * 1. [[onItemAdded]] is called
+ *
+ * @param slot the slot the item was removed from
+ * @param removedItem the previously present item
+ * @param replacedBy if known, the item it was replaced by ([[onItemAdded]] is still called)
+ */
+ def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit
+
+ /**
+ * An iterator over all slots occupied in this inventory.
+ */
+ def inventoryIterator: Iterator[Slot] = slotItems.keysIterator.map(Slot(_))
+
+ def clearInventory(): Unit = {
+ for (slot <- slotItems.keys.toArray) {
+ Slot(slot).remove()
+ }
+ }
+
+ private def slotObservers(slotIndex: Int): Iterator[SlotObserver] =
+ observers.get(slotIndex).iterator.flatMap(_.view.keys)
+
+ private def onItemAddedImpl(slot: Slot): Unit = {
+ onItemAdded(slot)
+ slotObservers(slot.index).foreach(_.onItemAdded())
+ }
+
+ private def onItemRemovedImpl(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit = {
+ onItemRemoved(slot, removedItem, replacedBy)
+ slotObservers(slot.index).foreach(_.onItemRemoved(removedItem, replacedBy))
+ }
+
+ /**
+ * A proxy to access a slot of the inventory.
+ */
+ final class Slot private[Inventory](val index: Int) {
+ require(index >= 0)
+
+ def isEmpty: Boolean = get.isEmpty
+
+ def nonEmpty: Boolean = !isEmpty
+
+ /**
+ * Inserts the `item` into this slot (replacing the previous item if any).
+ */
+ def put(item: inventory.I): Unit = {
+ setSlot(index, Some(item))
+ }
+
+ /**
+ * Allows inserting/removing the item in this slot.
+ */
+ def set(item: Option[inventory.I]): Unit = {
+ setSlot(index, item)
+ }
+
+ /**
+ * Removes the item contained in this slot if there is one.
+ */
+ def remove(): Unit = {
+ setSlot(index, None)
+ }
+
+ /**
+ * The [[Item]] contained in this slot.
+ */
+ def get: Option[inventory.I] = slotItems.get(index)
+
+ val inventory: Inventory.this.type = Inventory.this
+
+ /**
+ * Registers an observer to receive item added/removed events.
+ *
+ * @note The inventory keeps a '''weak''' reference to the `observer`.
+ */
+ def addObserver(observer: SlotObserver): Unit = {
+ observers.getOrElseUpdate(index, new WeakHashSet) += observer -> ()
+ }
+
+ def removeObserver(observer: SlotObserver): Unit = {
+ for (slotObservers <- observers.get(index)) {
+ slotObservers.remove(observer)
+
+ if (slotObservers.isEmpty) {
+ observers.remove(index)
+ }
+ }
+ }
+
+ private[inventory] def notifySlot(notification: Item.Notification): Unit = {
+ slotObservers(index).foreach(_.onItemNotification(notification))
+ }
+
+ override def equals(other: Any): Boolean = other match {
+ case that: Inventory#Slot =>
+ // in case you're wondering wtf this is:
+ // IntelliJ IDEA has a bug that wrongly rejects `inventory == that.inventory` due to a typing error
+ // (the code is accepted by the compiler though)
+ // stripping this.type seems to help
+ val thisInv = classOf[Inventory].cast(inventory)
+ val thatInv = classOf[Inventory].cast(that.inventory)
+ thisInv == thatInv && index == that.index
+
+ case _ => false
+ }
+
+ override def hashCode(): Int = {
+ val state = Seq(inventory, index)
+ state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
+ }
+ }
+
+ final object Slot {
+ /**
+ * Creates a proxy to an inventory slot.
+ */
+ def apply(index: Int) = new Slot(index)
+ }
+
+ private def setSlot(index: Int, item: Option[I]): Unit = {
+ val slot = Slot(index)
+
+ (slotItems.get(index), item) match {
+ case (Some(oldItem), Some(newItem)) if oldItem == newItem =>
+ // no-op
+
+ case (Some(oldItem), Some(newItem)) =>
+ // replace old with new
+ doRemove(index)
+ onItemRemovedImpl(slot, oldItem, Some(newItem))
+ doInsert(index, newItem)
+ onItemAddedImpl(slot)
+
+ case (Some(oldItem), None) =>
+ // remove old
+ doRemove(index)
+ onItemRemovedImpl(slot, oldItem, None)
+
+ case (None, Some(newItem)) =>
+ // add new
+ doInsert(index, newItem)
+ onItemAddedImpl(slot)
+
+ case (None, None) =>
+ // no-op
+ }
+ }
+
+ private def doRemove(index: Int): Unit = {
+ for (oldItem <- slotItems.remove(index)) {
+ assert(oldItem.slot.exists(_.index == index))
+ oldItem.slot = None
+
+ val oldIndex = itemSlots.remove(oldItem)
+ assert(oldIndex.contains(index))
+ }
+ }
+
+ private def doInsert(index: Int, item: I): Unit = {
+ assert(!itemSlots.contains(item))
+ assert(!slotItems.contains(index))
+ assert(item.slot.isEmpty)
+
+ slotItems(index) = item
+ itemSlots(item) = index
+ item.slot = Some(Slot(index))
+ }
+}
+
+object Inventory {
+ trait SlotObserver {
+ /**
+ * Called after an item was inserted into this slot.
+ *
+ * @note [[Inventory.onItemAdded]] is called before this method.
+ */
+ def onItemAdded(): Unit
+
+ /**
+ * Called after an item was removed from this slot.
+ *
+ * In particular, the slot no longer contains the removed item.
+ *
+ * @note [[Inventory.onItemRemoved]] is called before this method.
+ */
+ def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit
+
+ /**
+ * Called when an item contained in this slot sends a notification via [[Item.notifySlot]].
+ */
+ def onItemNotification(notification: Item.Notification): Unit
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/Item.scala b/src/main/scala/ocelot/desktop/inventory/Item.scala
new file mode 100644
index 0000000..3fe127b
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/Item.scala
@@ -0,0 +1,92 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.ColorScheme
+import ocelot.desktop.color.Color
+import ocelot.desktop.graphics.IconDef
+import ocelot.desktop.ui.widget.contextmenu.ContextMenu
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+/**
+ * Something that can be stored in an [[Inventory]].
+ */
+trait Item {
+ private var _slot: Option[Inventory#Slot] = None
+
+ /**
+ * The slot this item is stored in.
+ */
+ def slot: Option[Inventory#Slot] = _slot
+
+ private[inventory] def slot_=(slot: Option[Inventory#Slot]): Unit = {
+ _slot = slot
+ }
+
+ /**
+ * Sends a `notification` to observers of the `slot`.
+ *
+ * (Which means, if the item is not put into any slot, nobody will receive the message.)
+ */
+ final def notifySlot(notification: Item.Notification): Unit = {
+ slot.foreach(_.notifySlot(notification))
+ }
+
+ /**
+ * The name of the item (as shown in the tooltip).
+ */
+ def name: String = factory.name
+
+ /**
+ * The icon used to draw the item in a slot.
+ */
+ def icon: IconDef = factory.icon
+
+ /**
+ * The tier of the item (if it has one).
+ *
+ * This affects the color of the item name in the tooltip.
+ */
+ def tier: Option[Tier] = factory.tier
+
+ protected def tooltipNameColor: Color = ColorScheme("Tier" + tier.getOrElse(Tier.One).id)
+
+ /**
+ * Override this in subclasses to customize the contents of the tooltip.
+ *
+ * @example {{{
+ * override protected def fillTooltipBody(body: Widget): Unit = {
+ * super.fillTooltipBody(body)
+ * body.children :+= new Label {
+ * override def text: String = "Hello world"
+ * override def color: Color = Color.Grey
+ * }
+ * }
+ * }}}
+ */
+ def fillTooltip(tooltip: ItemTooltip): Unit = {
+ tooltip.addLine(name, tooltipNameColor)
+ }
+
+ /**
+ * Override this in subclasses to add new entries to the context menu.
+ *
+ * It usually makes sense to call `super.fillRmbMenu` ''after'' you've added your own entries.
+ */
+ def fillRmbMenu(menu: ContextMenu): Unit = {}
+
+ /**
+ * The factory that can be used to build an independent instance of this [[Item]]
+ * in a way equivalent to this one (e.g. the same tier, label, EEPROM code).
+ *
+ * Keep this rather cheap.
+ */
+ def factory: ItemFactory
+}
+
+object Item {
+ /**
+ * A notification that can be sent with [[Item.notifySlot]].
+ */
+ trait Notification
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/ItemFactory.scala b/src/main/scala/ocelot/desktop/inventory/ItemFactory.scala
new file mode 100644
index 0000000..a451cd1
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/ItemFactory.scala
@@ -0,0 +1,59 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.graphics.IconDef
+import totoro.ocelot.brain.util.Tier.Tier
+
+/**
+ * Provides information about a class of [[Item]]s and allows to build an item instance.
+ *
+ * Used by [[ocelot.desktop.ui.widget.slot.SlotWidget SlotWidgets]]
+ * (and [[ocelot.desktop.ui.widget.slot.ItemChooser]]) to tell if you can insert an item into the slot
+ * without having to actually construct one.
+ * In particular, make sure the [[tier]] and [[itemClass]] provided by the factory are accurate.
+ */
+trait ItemFactory {
+ /**
+ * The concrete type of the [[Item]] built by the factory.
+ */
+ type I <: Item
+
+ /**
+ * The runtime class of the [[Item]] built by the factory.
+ *
+ * @note It's expected that `build().getClass == itemClass`.
+ */
+ def itemClass: Class[I]
+
+ /**
+ * A name that represents what will be built by the factory.
+ *
+ * Usually [[name]] and [[Item.name]] are the same (in fact, the latter defaults to this unless overridden).
+ *
+ * Used by the [[ocelot.desktop.ui.widget.slot.ItemChooser ItemChooser]] in its entries.
+ */
+ def name: String
+
+ /**
+ * The tier of an item this factory will construct in its [[build]] method.
+ *
+ * @note It's expected that `build().tier == tier`.
+ */
+ def tier: Option[Tier]
+
+ /**
+ * The icon of an item this factory will construct in its [[build]] method.
+ *
+ * Used by the [[ocelot.desktop.ui.widget.slot.ItemChooser ItemChooser]] in its entries.
+ */
+ def icon: IconDef
+
+ /**
+ * Constructs a new item instance.
+ */
+ def build(): I
+
+ /**
+ * Returns a list of recoverers provided by the factory,
+ */
+ def recoverers: Iterable[ItemRecoverer[_, _]]
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/ItemRecoverer.scala b/src/main/scala/ocelot/desktop/inventory/ItemRecoverer.scala
new file mode 100644
index 0000000..7a41969
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/ItemRecoverer.scala
@@ -0,0 +1,18 @@
+package ocelot.desktop.inventory
+
+import scala.reflect.{ClassTag, classTag}
+
+/**
+ * Allows recovering an [[Item]] from [[S]] (typically an [[totoro.ocelot.brain.entity.traits.Entity Entity]]).
+ *
+ * This is used for migration from old saves before the inventory system was introduced to Ocelot Desktop.
+ */
+final class ItemRecoverer[S: ClassTag, I <: Item](f: S => I) {
+ val sourceClass: Class[_] = classTag[S].runtimeClass
+
+ def recover(source: S): I = f(source)
+}
+
+object ItemRecoverer {
+ def apply[S: ClassTag, I <: Item](f: S => I): ItemRecoverer[S, I] = new ItemRecoverer(f)
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/Items.scala b/src/main/scala/ocelot/desktop/inventory/Items.scala
new file mode 100644
index 0000000..73f8263
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/Items.scala
@@ -0,0 +1,142 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.graphics.IconDef
+import ocelot.desktop.inventory.item._
+import ocelot.desktop.util.Logging
+import ocelot.desktop.util.ReflectionUtils.linearizationOrder
+import totoro.ocelot.brain.loot.Loot
+import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier
+import totoro.ocelot.brain.util.Tier.Tier
+import totoro.ocelot.brain.util.{ExtendedTier, Tier}
+
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+
+object Items extends Logging {
+ private val _groups = ArrayBuffer.empty[ItemGroup]
+ private val _recoverers = mutable.Map.empty[Class[_], ItemRecoverer[_, _]]
+
+ // this is just to force load the class during initialization
+ def init(): Unit = {}
+
+ /**
+ * Registers a recoverer for [[ItemRecoverer.sourceClass]].
+ */
+ def registerRecoverer(recoverer: ItemRecoverer[_, _]): Unit = {
+ if (!_recoverers.contains(recoverer.sourceClass)) {
+ _recoverers(recoverer.sourceClass) = recoverer
+ logger.info(s"Registered a recoverer for ${recoverer.sourceClass.getName}")
+ }
+ }
+
+ private def registerItemFactoryRecoverers(factory: ItemFactory): Unit = {
+ for (recoverer <- factory.recoverers) {
+ registerRecoverer(recoverer)
+ }
+ }
+
+ def registerSingleton(factory: ItemFactory): Unit = {
+ _groups += SingletonItemGroup(factory.name, factory)
+ registerItemFactoryRecoverers(factory)
+ }
+
+ def registerTiered(name: String, tiers: IterableOnce[Tier])(factory: Tier => ItemFactory): Unit = {
+ val group = TieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq)
+ _groups += group
+
+ for ((_, factory) <- group.factories) {
+ registerItemFactoryRecoverers(factory)
+ }
+ }
+
+ def registerExtendedTiered(name: String, tiers: IterableOnce[ExtendedTier])(
+ factory: ExtendedTier => ItemFactory
+ ): Unit = {
+ val group = ExtendedTieredItemGroup(name, tiers.iterator.map(tier => (tier, factory(tier))).toSeq)
+ _groups += group
+
+ for ((_, factory) <- group.factories) {
+ registerItemFactoryRecoverers(factory)
+ }
+ }
+
+ def registerArbitrary(name: String, icon: IconDef, factories: IterableOnce[(String, ItemFactory)]): Unit = {
+ val group = ArbitraryItemGroup(name, icon, factories.iterator.toSeq)
+ _groups += group
+
+ for ((_, factory) <- group.factories) {
+ registerItemFactoryRecoverers(factory)
+ }
+ }
+
+ def groups: Iterable[ItemGroup] = _groups
+
+ /**
+ * Attempts to recover an [[Item]] from `source`.
+ *
+ * Checks superclasses and traits while looking for a recoverer.
+ */
+ def recover[A](source: A): Option[Item] = {
+ linearizationOrder(source.getClass.asInstanceOf[Class[_]])
+ .flatMap(_recoverers.get)
+ .map(_.asInstanceOf[ItemRecoverer[_ >: A, _ <: Item]].recover(source))
+ .nextOption()
+ }
+
+ sealed trait ItemGroup {
+ def name: String
+ }
+
+ case class SingletonItemGroup(name: String, factory: ItemFactory) extends ItemGroup
+
+ case class TieredItemGroup(name: String, factories: Seq[(Tier, ItemFactory)]) extends ItemGroup
+
+ case class ExtendedTieredItemGroup(name: String, factories: Seq[(ExtendedTier, ItemFactory)]) extends ItemGroup
+
+ case class ArbitraryItemGroup(name: String, icon: IconDef, factories: Seq[(String, ItemFactory)]) extends ItemGroup
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ registerTiered("CPU", Tier.One to Tier.Three)(new CpuItem.Factory(_))
+ registerTiered("APU", Tier.Two to Tier.Creative)(tier => new ApuItem.Factory(tier.saturatingSub(1)))
+
+ registerExtendedTiered("Memory", ExtendedTier.One to ExtendedTier.ThreeHalf)(new MemoryItem.Factory(_))
+
+ registerTiered("HDD", Tier.One to Tier.Three)(new HddItem.Factory(managed = true, _))
+
+ registerArbitrary(
+ "Floppy",
+ FloppyItem.Factory.Empty.icon,
+ Loot.Floppies.iterator
+ .map(new FloppyItem.Factory.Loot(_))
+ .map(factory => (factory.name, factory)) ++ Some(("Empty", FloppyItem.Factory.Empty))
+ )
+
+ registerArbitrary(
+ "EEPROM",
+ EepromItem.Factory.Empty.icon,
+ Loot.Eeproms.iterator
+ .map(new EepromItem.Factory.Loot(_))
+ .map(factory => (factory.name, factory)) ++ Some(("Empty", EepromItem.Factory.Empty))
+ )
+
+ registerTiered("Graphics Card", Tier.One to Tier.Three)(new GraphicsCardItem.Factory(_))
+ registerSingleton(NetworkCardItem.Factory)
+ registerTiered("Wireless Net. Card", Tier.One to Tier.Two) {
+ case Tier.One => WirelessNetworkCardItem.Tier1.Factory
+ case Tier.Two => WirelessNetworkCardItem.Tier2.Factory
+ }
+ registerSingleton(LinkedCardItem.Factory)
+ registerSingleton(InternetCardItem.Factory)
+ registerTiered("Redstone Card", Tier.One to Tier.Two) {
+ case Tier.One => RedstoneCardItem.Tier1.Factory
+ case Tier.Two => RedstoneCardItem.Tier2.Factory
+ }
+ registerTiered("Data Card", Tier.One to Tier.Three) {
+ case Tier.One => DataCardItem.Tier1.Factory
+ case Tier.Two => DataCardItem.Tier2.Factory
+ case Tier.Three => DataCardItem.Tier3.Factory
+ }
+ registerSingleton(SoundCardItem.Factory)
+ registerSingleton(SelfDestructingCardItem.Factory)
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala b/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala
new file mode 100644
index 0000000..22739ce
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala
@@ -0,0 +1,119 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.OcelotDesktop
+import ocelot.desktop.inventory.PersistedInventory._
+import ocelot.desktop.inventory.traits.{ComponentItem, PersistableItem}
+import ocelot.desktop.util.Logging
+import ocelot.desktop.util.ReflectionUtils.findUnaryConstructor
+import totoro.ocelot.brain.nbt.ExtendedNBT.{extendNBTTagCompound, extendNBTTagList}
+import totoro.ocelot.brain.nbt.persistence.NBTPersistence
+import totoro.ocelot.brain.nbt.{NBT, NBTTagCompound}
+
+import scala.collection.mutable
+
+/**
+ * Provides persistence for an [[Inventory]].
+ *
+ * [[ComponentItem]]s are treated specially: their [[ComponentItem.component component]] is persisted
+ * along with the item when saving. When loading the item, it first loads the component and uses it to instantiate
+ * the item (as required by [[ComponentItem]]).
+ */
+trait PersistedInventory extends Inventory with Logging {
+ override type I <: Item with PersistableItem
+
+ def load(nbt: NBTTagCompound): Unit = {
+ val slotsToRemove = inventoryIterator.map(_.index).to(mutable.Set)
+
+ for (slotNbt <- nbt.getTagList(InventoryTag, NBT.TAG_COMPOUND).iterator[NBTTagCompound]) {
+ val slotIndex = slotNbt.getInteger(SlotIndexTag)
+ slotsToRemove -= slotIndex
+
+ val itemNbt = slotNbt.getCompoundTag(SlotItemTag)
+ val itemClass = Class.forName(slotNbt.getString(SlotItemClassTag))
+
+ try {
+ val item = if (classOf[ComponentItem].isAssignableFrom(itemClass))
+ loadComponentItem(itemClass, slotNbt)
+ else
+ loadPlainItem(itemClass)
+
+ item.load(itemNbt)
+ Slot(slotIndex).put(item)
+ } catch {
+ case ItemLoadException(message) =>
+ logger.error(
+ s"Could not restore an item in the slot $slotIndex of " +
+ s"the inventory $this (class ${this.getClass.getName}): $message",
+ )
+ onSlotLoadFailed(slotIndex)
+ }
+ }
+
+ for (slotIndex <- slotsToRemove) {
+ Slot(slotIndex).remove()
+ }
+ }
+
+ def save(nbt: NBTTagCompound): Unit = {
+ nbt.setNewTagList(
+ InventoryTag, inventoryIterator.map { slot =>
+ val item = slot.get.get
+
+ val slotNbt = new NBTTagCompound
+ slotNbt.setInteger(SlotIndexTag, slot.index)
+
+ val itemNbt = new NBTTagCompound
+ item.save(itemNbt)
+ slotNbt.setTag(SlotItemTag, itemNbt)
+ slotNbt.setString(SlotItemClassTag, item.getClass.getName)
+
+ item match {
+ case item: ComponentItem => saveComponentItem(slotNbt, item)
+ case _ =>
+ }
+
+ slotNbt
+ }
+ )
+ }
+
+ @throws[ItemLoadException]("if the item could not be loaded")
+ protected def loadComponentItem(itemClass: Class[_], slotNbt: NBTTagCompound): I = {
+ val entityNbt = slotNbt.getCompoundTag(SlotEntityTag)
+ val entity = NBTPersistence.load(entityNbt, OcelotDesktop.workspace)
+
+ val constructor = findUnaryConstructor(itemClass, entity.getClass) match {
+ case Some(constructor) => constructor
+ case None =>
+ throw ItemLoadException(
+ s"an item class ${itemClass.getName} cannot be instantiated with $entity (class ${entity.getClass.getName})"
+ )
+ }
+
+ constructor.newInstance(entity).asInstanceOf[I]
+ }
+
+ //noinspection ScalaWeakerAccess
+ @throws[ItemLoadException]("if the item could not be loaded")
+ protected def loadPlainItem(itemClass: Class[_]): I = {
+ itemClass.getConstructor().newInstance().asInstanceOf[I]
+ }
+
+ protected def onSlotLoadFailed(slotIndex: Int): Unit = {
+ Slot(slotIndex).remove()
+ }
+
+ protected def saveComponentItem(slotNbt: NBTTagCompound, item: ComponentItem): Unit = {
+ slotNbt.setTag(SlotEntityTag, NBTPersistence.save(item.component))
+ }
+}
+
+object PersistedInventory {
+ private val InventoryTag = "inventory"
+ private val SlotIndexTag = "slotIndex"
+ private val SlotItemTag = "item"
+ private val SlotItemClassTag = "class"
+ private val SlotEntityTag = "entity"
+
+ case class ItemLoadException(message: String) extends Exception(message)
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala
new file mode 100644
index 0000000..51466dd
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala
@@ -0,0 +1,294 @@
+package ocelot.desktop.inventory
+
+import ocelot.desktop.OcelotDesktop
+import ocelot.desktop.inventory.PersistedInventory.ItemLoadException
+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.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.nbt.NBTTagCompound
+
+import scala.annotation.tailrec
+
+/**
+ * A [[PersistedInventory]] backed by a [[BrainInventory brain Inventory]].
+ *
+ * Synchronizes the contents of the two inventories, propagating changes from one to the other.
+ *
+ * When a new [[Item]] is added to the Desktop inventory, its [[ComponentItem.component]] is added to
+ * the [[brainInventory]]. When a new [[Entity]] is added to the [[brainInventory]], an [[Item]] is recovered from it
+ * by using an appropriate [[ItemRecoverer]] from [[Items the registry]].
+ *
+ * While synchronizing, relies on the convergence of changes, but guards against stack overflows
+ * by limiting the recursion depth.
+ */
+trait SyncedInventory extends PersistedInventory with Logging {
+ override type I <: Item with ComponentItem
+
+ // to avoid synchronization while we're loading stuff
+ private var isLoading = false
+
+ @volatile
+ private var syncFuel: Int = _
+ refuel()
+
+ protected def eventHandlers: EventHandlers
+
+ /**
+ * The backing [[BrainInventory brain Inventory]].
+ */
+ def brainInventory: BrainInventory
+
+ override def load(nbt: NBTTagCompound): Unit = {
+ isLoading = true
+
+ try {
+ super.load(nbt)
+ } finally {
+ isLoading = false
+ }
+
+ val occupiedSlots = inventoryIterator.map(_.index).toSet
+ .union(brainInventory.inventory.iterator.map(_.index).toSet)
+
+ for (slotIndex <- occupiedSlots) {
+ sync(slotIndex, SyncDirection.Reconcile)
+ }
+ }
+
+ @throws[ItemLoadException]("if the item could not be loaded")
+ override protected def loadComponentItem(itemClass: Class[_], slotNbt: NBTTagCompound): I = {
+ val entityAddress = slotNbt.getString(SlotEntityAddressTag)
+
+ // why not OcelotDesktop.workspace.entityByAddress?
+ // well, that one only looks for tile entities (computers, screens, etc.), and our items are none of that...
+ val matchingEntities = brainInventory.inventory.iterator
+ .flatMap(_.get)
+ .collect { case env: Environment if env.node.address == entityAddress => env }
+
+ val entity = matchingEntities.nextOption() match {
+ case Some(entity) => entity
+ case None => throw ItemLoadException(s"entity $entityAddress has disappeared")
+ }
+
+ findUnaryConstructor(itemClass, entity.getClass) match {
+ case Some(constructor) => constructor.newInstance(entity).asInstanceOf[I]
+
+ case None =>
+ throw ItemLoadException(
+ s"an item class ${itemClass.getName} cannot be instantiated " +
+ s"with $entity (class ${entity.getClass.getName})",
+ )
+ }
+ }
+
+ override protected def onSlotLoadFailed(slotIndex: Int): Unit = {
+ // we'll deal with it the during synchronization
+ }
+
+ override protected def saveComponentItem(slotNbt: NBTTagCompound, item: ComponentItem): Unit = {
+ slotNbt.setString(SlotEntityAddressTag, item.component.node.address)
+ }
+
+ eventHandlers += {
+ case BrainEvent(InventoryEntityAddedEvent(slot, _)) if slot.inventory.owner eq brainInventory =>
+ sync(slot.index, SyncDirection.BrainToDesktop)
+
+ case BrainEvent(InventoryEntityRemovedEvent(slot, _)) if slot.inventory.owner eq brainInventory =>
+ sync(slot.index, SyncDirection.BrainToDesktop)
+ }
+
+ override def onItemAdded(slot: Slot): Unit = {
+ if (!isLoading) {
+ sync(slot.index, SyncDirection.DesktopToBrain)
+ }
+ }
+
+ override def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit = {
+ if (!isLoading) {
+ sync(slot.index, SyncDirection.DesktopToBrain)
+ }
+ }
+
+ private def sync(slotIndex: Int, direction: SyncDirection): Unit = {
+ val initialSync = syncFuel == InitialSyncFuel
+
+ try {
+ doSync(slotIndex, direction)
+ } finally {
+ if (initialSync) {
+ refuel()
+ }
+ }
+ }
+
+ private def refuel(): Unit = {
+ syncFuel = InitialSyncFuel
+ }
+
+ @tailrec
+ private def doSync(slotIndex: Int, direction: SyncDirection): Unit = {
+ syncFuel -= 1
+
+ if (syncFuel < 0) {
+ // ignore: the limit has already been reached
+ } 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})!",
+ )
+ logger.error(
+ "The item in the slot: " +
+ Slot(slotIndex).get.map(item => s"$item (class ${item.getClass.getName})").getOrElse(""),
+ )
+ logger.error(
+ "The entity if the slot: " +
+ brainInventory.inventory(slotIndex)
+ .get
+ .map(entity => s"$entity (class ${entity.getClass.getName})")
+ .getOrElse(""),
+ )
+
+ logger.error("Breaking the loop forcefully by removing the items.")
+ Slot(slotIndex).remove()
+ brainInventory.inventory(slotIndex).remove()
+ } else {
+ direction match {
+ case _ if checkSlotStatus(slotIndex) == SlotStatus.Synchronized =>
+
+ case SyncDirection.DesktopToBrain =>
+ // the `asInstanceOf` is indeed entirely superfluous, but IntelliJ IDEA complains otherwise...
+ OcelotDesktop.withTickLockAcquired {
+ brainInventory.inventory(slotIndex).set(Slot(slotIndex).get.map(_.asInstanceOf[ComponentItem].component))
+ }
+
+ 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 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
+ }
+
+ case None => None
+ }
+
+ Slot(slotIndex).set(item)
+
+ 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)
+
+ 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.")
+
+ 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)
+ }
+ }
+ }
+ }
+
+ private def logEntityLoss(slotIndex: Int, entity: Entity): Unit = {
+ 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.",
+ )
+ }
+
+ private def checkSlotStatus(slotIndex: Int): SlotStatus =
+ (Slot(slotIndex).get, brainInventory.inventory(slotIndex).get) match {
+ case (Some(item), Some(entity)) if item.component eq entity => SlotStatus.Synchronized
+ case (Some(_), Some(_)) => SlotStatus.Conflict
+ case (Some(_), None) => SlotStatus.DesktopNonEmpty
+ case (None, Some(_)) => SlotStatus.BrainNonEmpty
+ case (None, None) => SlotStatus.Synchronized
+ }
+}
+
+object SyncedInventory {
+ private val SlotEntityAddressTag = "entity"
+
+ private val InitialSyncFuel = 5
+
+ object SyncDirection extends Enumeration {
+ type SyncDirection = Value
+
+ /**
+ * Apply a change from the [[SyncedInventory]] to [[SyncedInventory.brainInventory]].
+ */
+ val DesktopToBrain: SyncDirection = Value
+
+ /**
+ * Apply a change from the [[SyncedInventory.brainInventory]] to [[SyncedInventory]].
+ */
+ val BrainToDesktop: SyncDirection = Value
+
+ /**
+ * Try to reconcile conflicts between [[SyncedInventory]] and its [[SyncedInventory.brainInventory]].
+ */
+ val Reconcile: SyncDirection = Value
+ }
+
+ object SlotStatus extends Enumeration {
+ type SlotStatus = Value
+
+ /**
+ * The slots are in sync (both are empty or they contain the same entity).
+ */
+ val Synchronized: SlotStatus = Value
+
+ /**
+ * [[SyncedInventory]]'s slot is non-empty; [[SyncedInventory.brainInventory]]'s is empty.
+ */
+ val DesktopNonEmpty: SlotStatus = Value
+
+ /**
+ * [[SyncedInventory]]'s slot is empty; [[SyncedInventory.brainInventory]]'s isn't.
+ */
+ val BrainNonEmpty: SlotStatus = Value
+
+ /**
+ * The [[SyncedInventory]] and the [[SyncedInventory.brainInventory]] are both non-empty
+ * and contain different items.
+ */
+ val Conflict: SlotStatus = Value
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/ApuItem.scala b/src/main/scala/ocelot/desktop/inventory/item/ApuItem.scala
new file mode 100644
index 0000000..3a00fc8
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/ApuItem.scala
@@ -0,0 +1,35 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{ComponentItem, CpuLikeItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.APU
+import totoro.ocelot.brain.entity.traits.{Entity, GenericCPU}
+import totoro.ocelot.brain.util.Tier.Tier
+
+class ApuItem(val apu: APU) extends Item with ComponentItem with PersistableItem with CpuLikeItem {
+ override def component: Entity with GenericCPU = apu
+
+ override def factory: ApuItem.Factory = new ApuItem.Factory(apu.tier)
+}
+
+object ApuItem {
+ class Factory(_tier: Tier) extends ItemFactory {
+ override type I = ApuItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = s"APU (${tier.get.label})"
+
+ // there are T2, T3, and creative APUs
+ // however, the APU class starts counting from one (so it's actually T1, T2, and T3)
+ // we keep the latter tier internally and increment it when dealing with the rest of the world
+ override def tier: Option[Tier] = Some(_tier.saturatingAdd(1))
+
+ override def icon: IconDef = Icons.Apu(_tier)
+
+ override def build(): ApuItem = new ApuItem(new APU(_tier))
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new ApuItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/CpuItem.scala b/src/main/scala/ocelot/desktop/inventory/item/CpuItem.scala
new file mode 100644
index 0000000..90d8e1a
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/CpuItem.scala
@@ -0,0 +1,32 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{ComponentItem, CpuLikeItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.CPU
+import totoro.ocelot.brain.entity.traits.{Entity, GenericCPU}
+import totoro.ocelot.brain.util.Tier.Tier
+
+class CpuItem(val cpu: CPU) extends Item with ComponentItem with PersistableItem with CpuLikeItem {
+ override def component: Entity with GenericCPU = cpu
+
+ override def factory: CpuItem.Factory = new CpuItem.Factory(cpu.tier)
+}
+
+object CpuItem {
+ class Factory(_tier: Tier) extends ItemFactory {
+ override type I = CpuItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = s"CPU (${_tier.label})"
+
+ override def tier: Option[Tier] = Some(_tier)
+
+ override def icon: IconDef = Icons.Cpu(_tier)
+
+ override def build(): CpuItem = new CpuItem(new CPU(_tier))
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new CpuItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/DataCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/DataCardItem.scala
new file mode 100644
index 0000000..82b0cd5
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/DataCardItem.scala
@@ -0,0 +1,79 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.DataCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+abstract class DataCardItem extends Item with ComponentItem with PersistableItem with CardItem
+
+object DataCardItem {
+ abstract class Factory extends ItemFactory {
+ override def name: String = s"Data Card (${tier.get.label})"
+
+ override def icon: IconDef = Icons.DataCard(tier.get)
+ }
+
+ class Tier1(val dataCard: DataCard.Tier1) extends DataCardItem {
+ override def component: Entity with Environment = dataCard
+
+ override def factory: Factory = DataCardItem.Tier1.Factory
+ }
+
+ object Tier1 {
+ object Factory extends Factory {
+ override type I = DataCardItem.Tier1
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.One)
+
+ override def build(): DataCardItem.Tier1 = new DataCardItem.Tier1(new DataCard.Tier1)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new DataCardItem.Tier1(_)))
+ }
+ }
+
+ class Tier2(val dataCard: DataCard.Tier2) extends DataCardItem {
+ override def component: Entity with Environment = dataCard
+
+ override def factory: Factory = DataCardItem.Tier2.Factory
+ }
+
+ object Tier2 {
+ object Factory extends Factory {
+ override type I = DataCardItem.Tier2
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def build(): DataCardItem.Tier2 = new DataCardItem.Tier2(new DataCard.Tier2)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new DataCardItem.Tier2(_)))
+ }
+ }
+
+ class Tier3(val dataCard: DataCard.Tier3) extends DataCardItem {
+ override def component: Entity with Environment = dataCard
+
+ override def factory: Factory = DataCardItem.Tier3.Factory
+ }
+
+ object Tier3 {
+ object Factory extends Factory {
+ override type I = DataCardItem.Tier3
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.Three)
+
+ override def build(): DataCardItem.Tier3 = new DataCardItem.Tier3(new DataCard.Tier3)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new DataCardItem.Tier3(_)))
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala b/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala
new file mode 100644
index 0000000..5401261
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala
@@ -0,0 +1,124 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.OcelotDesktop
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.InputDialog
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.EEPROM
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.loot.Loot.{EEPROMFactory => LootEepromFactory}
+import totoro.ocelot.brain.util.Tier.Tier
+
+import java.net.{MalformedURLException, URL}
+import javax.swing.JFileChooser
+import scala.util.Try
+
+class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with PersistableItem {
+ override def component: Entity with Environment = eeprom
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ val source = eeprom.codePath
+ .map(path => s"Source path: $path")
+ .orElse(eeprom.codeURL.map(url => s"Source URL: $url"))
+
+ for (source <- source) {
+ tooltip.addLine(source)
+ }
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ menu.addEntry(
+ new ContextMenuSubmenu("External data source") {
+ addEntry(
+ ContextMenuEntry("Local file") {
+ OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY) { file =>
+ Try {
+ for (file <- file) {
+ eeprom.codePath = Some(file.toPath)
+ }
+ }
+ }
+ }
+ )
+
+ addEntry(
+ ContextMenuEntry("File via URL") {
+ new InputDialog(
+ title = "File via URL",
+ onConfirmed = { text =>
+ eeprom.codeURL = Some(new URL(text))
+ },
+ inputValidator = text =>
+ try {
+ new URL(text)
+ true
+ } catch {
+ case _: MalformedURLException => false
+ },
+ ).show()
+ }
+ )
+
+ if (eeprom.codePath.nonEmpty || eeprom.codeURL.nonEmpty) {
+ addEntry(
+ ContextMenuEntry("Detach") {
+ eeprom.codeBytes = Some(Array.empty)
+ }
+ )
+ }
+ }
+ )
+
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: EepromItem.Factory = new EepromItem.Factory.Code(
+ code = eeprom.getBytes,
+ name = eeprom.label,
+ readonly = eeprom.readonly,
+ )
+}
+
+object EepromItem {
+ abstract class Factory extends ItemFactory {
+ override type I = EepromItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = None
+
+ override def icon: IconDef = Icons.Eeprom
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new EepromItem(_)))
+ }
+
+ object Factory {
+ class Loot(factory: LootEepromFactory) extends Factory {
+ override def name: String = factory.label
+
+ override def build(): EepromItem = new EepromItem(factory.create())
+ }
+
+ class Code(code: Array[Byte], override val name: String, readonly: Boolean) extends Factory {
+ override def build(): EepromItem = {
+ val eeprom = new EEPROM
+ eeprom.codeBytes = Some(code)
+ eeprom.label = name
+ eeprom.readonly = readonly
+
+ new EepromItem(eeprom)
+ }
+ }
+
+ object Empty extends Factory {
+ override def name: String = "EEPROM"
+
+ override def build(): EepromItem = new EepromItem(new EEPROM)
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala b/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala
new file mode 100644
index 0000000..ba712dc
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/FloppyItem.scala
@@ -0,0 +1,94 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.contextmenu.ContextMenu
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.traits.{Entity, Environment, Floppy}
+import totoro.ocelot.brain.entity.{FloppyManaged, FloppyUnmanaged}
+import totoro.ocelot.brain.loot.Loot.{LootFloppy, FloppyFactory => LootFloppyFactory}
+import totoro.ocelot.brain.util.DyeColor
+import totoro.ocelot.brain.util.Tier.Tier
+
+class FloppyItem(val floppy: Floppy) extends Item with ComponentItem with PersistableItem with DiskItem {
+ def color: DyeColor = floppy.color
+
+ override def component: Entity with Environment = floppy
+
+ override def name: String = floppy.name.getOrElse(super.name)
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ for (label <- floppy.label.labelOption) {
+ if (label != name) {
+ // this is true for loot floppies
+ addDiskLabelTooltip(tooltip, label)
+ }
+ }
+
+ floppy match {
+ case floppy: FloppyManaged => addSourcePathTooltip(tooltip, floppy)
+ case _ => // unmanaged floppies don't need any special entries
+ }
+
+ addManagedTooltip(tooltip, floppy)
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ floppy match {
+ case floppy: FloppyManaged => addSetDirectoryEntry(menu, floppy)
+ case _ => // unmanaged floppies don't need any special entries
+ }
+
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: FloppyItem.Factory = floppy match {
+ case floppy: LootFloppy =>
+ new FloppyItem.Factory.Loot(new LootFloppyFactory(floppy.name.get, floppy.color, floppy.path))
+
+ case floppy: FloppyManaged =>
+ new FloppyItem.Factory.Managed(floppy.label.labelOption, floppy.color)
+
+ case floppy: FloppyUnmanaged =>
+ new FloppyItem.Factory.Unmanaged(floppy.label.labelOption, floppy.color)
+ }
+}
+
+object FloppyItem {
+ abstract class Factory(val label: Option[String], val color: DyeColor, managed: Boolean) extends ItemFactory {
+ override type I = FloppyItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = label.getOrElse("Floppy Disk")
+
+ override def tier: Option[Tier] = None
+
+ override def icon: IconDef = Icons.FloppyDisk(color)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new FloppyItem(_)))
+ }
+
+ object Factory {
+ class Loot(factory: LootFloppyFactory) extends Factory(Some(factory.name), factory.color, managed = true) {
+ override def name: String = factory.name
+
+ override def build(): FloppyItem = new FloppyItem(factory.create())
+ }
+
+ object Empty extends Factory(None, DyeColor.Gray, managed = true) {
+ override def build(): FloppyItem = new FloppyItem(new FloppyManaged)
+ }
+
+ class Managed(label: Option[String], color: DyeColor) extends Factory(label, color, managed = true) {
+ override def build(): FloppyItem = new FloppyItem(new FloppyManaged(label, color))
+ }
+
+ class Unmanaged(label: Option[String], color: DyeColor) extends Factory(label, color, managed = false) {
+ override def build(): FloppyItem = new FloppyItem(new FloppyUnmanaged(label, color))
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/GraphicsCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/GraphicsCardItem.scala
new file mode 100644
index 0000000..d9f4801
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/GraphicsCardItem.scala
@@ -0,0 +1,30 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.GraphicsCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier.Tier
+
+class GraphicsCardItem(val gpu: GraphicsCard) extends Item with ComponentItem with PersistableItem with CardItem {
+ override def component: Entity with Environment = gpu
+
+ override def factory: GraphicsCardItem.Factory = new GraphicsCardItem.Factory(gpu.tier)
+}
+
+object GraphicsCardItem {
+ class Factory(_tier: Tier) extends ItemFactory {
+ override type I = GraphicsCardItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = s"Graphics Card (${_tier.label})"
+ override def tier: Option[Tier] = Some(_tier)
+ override def icon: IconDef = Icons.GraphicsCard(_tier)
+
+ override def build(): GraphicsCardItem = new GraphicsCardItem(new GraphicsCard(_tier))
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new GraphicsCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala b/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala
new file mode 100644
index 0000000..00b7ac3
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/HddItem.scala
@@ -0,0 +1,91 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.item.HddItem.Hdd
+import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.contextmenu.ContextMenu
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.traits.{Disk, Entity, Environment}
+import totoro.ocelot.brain.entity.{HDDManaged, HDDUnmanaged}
+import totoro.ocelot.brain.util.Tier.Tier
+
+class HddItem(val hdd: Hdd) extends Item with ComponentItem with PersistableItem with DiskItem {
+ // constructors for deserialization as required by [[ComponentItem]]
+ def this(hdd: HDDManaged) = {
+ this(Hdd.Managed(hdd))
+ }
+
+ def this(hdd: HDDUnmanaged) = {
+ this(Hdd.Unmanaged(hdd))
+ }
+
+ override def component: Entity with Environment = hdd.hdd
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ val label = hdd match {
+ case Hdd.Managed(hdd) => hdd.fileSystem.label.labelOption
+ case Hdd.Unmanaged(hdd) => hdd.label.labelOption
+ }
+
+ label.foreach(addDiskLabelTooltip(tooltip, _))
+
+ hdd match {
+ case Hdd.Managed(hdd) => addSourcePathTooltip(tooltip, hdd)
+ case Hdd.Unmanaged(_) => // unmanaged HDDs don't need any special entries
+ }
+
+ addManagedTooltip(tooltip, hdd.hdd)
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ hdd match {
+ case Hdd.Managed(hdd) =>
+ addSetDirectoryEntry(menu, hdd)
+
+ case _ => // unmanaged HDDs don't need any special entries
+ }
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: HddItem.Factory = hdd match {
+ case Hdd.Managed(hdd) => new HddItem.Factory(true, hdd.tier)
+ case Hdd.Unmanaged(hdd) => new HddItem.Factory(false, hdd.tier)
+ }
+}
+
+object HddItem {
+ class Factory(managed: Boolean, _tier: Tier) extends ItemFactory {
+ override type I = HddItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = s"Hard Disk Drive (${_tier.label})"
+
+ override def tier: Option[Tier] = Some(_tier)
+
+ override def icon: IconDef = Icons.HardDiskDrive(_tier)
+
+ override def build(): HddItem = new HddItem(
+ if (managed) Hdd.Managed(new HDDManaged(_tier))
+ else Hdd.Unmanaged(new HDDUnmanaged(_tier))
+ )
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Seq(
+ ItemRecoverer((hdd: HDDManaged) => new HddItem(Hdd.Managed(hdd))),
+ ItemRecoverer((hdd: HDDUnmanaged) => new HddItem(Hdd.Unmanaged(hdd))),
+ )
+ }
+
+ sealed trait Hdd {
+ def hdd: Disk with Entity
+ }
+
+ object Hdd {
+ case class Managed(hdd: HDDManaged) extends Hdd
+
+ case class Unmanaged(hdd: HDDUnmanaged) extends Hdd
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/InternetCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/InternetCardItem.scala
new file mode 100644
index 0000000..c644b16
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/InternetCardItem.scala
@@ -0,0 +1,33 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.InternetCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class InternetCardItem(val card: InternetCard) extends Item with ComponentItem with PersistableItem with CardItem {
+ override def component: Entity with Environment = card
+
+ override def factory: ItemFactory = InternetCardItem.Factory
+}
+
+object InternetCardItem {
+ object Factory extends ItemFactory {
+ override type I = InternetCardItem
+
+ override def itemClass: Class[InternetCardItem] = classOf
+
+ override def name: String = "Internet Card"
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def icon: IconDef = Icons.InternetCard
+
+ override def build(): InternetCardItem = new InternetCardItem(new InternetCard)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new InternetCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala
new file mode 100644
index 0000000..6844df5
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/LinkedCardItem.scala
@@ -0,0 +1,54 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.TunnelDialog
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.LinkedCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class LinkedCardItem(val linkedCard: LinkedCard) extends Item with ComponentItem with PersistableItem with CardItem {
+ override def component: Entity with Environment = linkedCard
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ tooltip.addLine(s"Channel: ${linkedCard.tunnel}")
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ menu.addEntry(
+ ContextMenuEntry("Set channel") {
+ new TunnelDialog(
+ tunnel => linkedCard.tunnel = tunnel,
+ linkedCard.tunnel,
+ ).show()
+ }
+ )
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: ItemFactory = LinkedCardItem.Factory
+}
+
+object LinkedCardItem {
+ object Factory extends ItemFactory {
+ override type I = LinkedCardItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = "Linked Card"
+
+ override def tier: Option[Tier] = Some(Tier.Three)
+
+ override def icon: IconDef = Icons.LinkedCard
+
+ override def build(): LinkedCardItem = new LinkedCardItem(new LinkedCard)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new LinkedCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/MemoryItem.scala b/src/main/scala/ocelot/desktop/inventory/item/MemoryItem.scala
new file mode 100644
index 0000000..4d1e0f0
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/MemoryItem.scala
@@ -0,0 +1,33 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.Memory
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class MemoryItem(val memory: Memory) extends Item with ComponentItem with PersistableItem {
+ override def component: Entity with Environment = memory
+
+ override def factory: ItemFactory = new MemoryItem.Factory(memory.memoryTier)
+}
+
+object MemoryItem {
+ class Factory(memoryTier: ExtendedTier) extends ItemFactory {
+ override type I = MemoryItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = s"Memory (${memoryTier.label})"
+
+ override def tier: Option[Tier] = Some(memoryTier.toTier)
+
+ override def icon: IconDef = Icons.Memory(memoryTier)
+
+ override def build(): MemoryItem = new MemoryItem(new Memory(memoryTier))
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new MemoryItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/NetworkCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/NetworkCardItem.scala
new file mode 100644
index 0000000..a5f4839
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/NetworkCardItem.scala
@@ -0,0 +1,31 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.NetworkCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class NetworkCardItem(val networkCard: NetworkCard) extends Item with ComponentItem with PersistableItem with CardItem {
+ override def component: Entity with Environment = networkCard
+
+ override def factory: ItemFactory = NetworkCardItem.Factory
+}
+
+object NetworkCardItem {
+ object Factory extends ItemFactory {
+ override type I = NetworkCardItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = "Network Card"
+ override def tier: Option[Tier] = Some(Tier.One)
+ override def icon: IconDef = Icons.NetworkCard
+
+ override def build(): NetworkCardItem = new NetworkCardItem(new NetworkCard)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new NetworkCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala
new file mode 100644
index 0000000..37cce12
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/RedstoneCardItem.scala
@@ -0,0 +1,97 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem, WindowProvider}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.card.{Redstone1Window, Redstone2Window}
+import ocelot.desktop.ui.widget.contextmenu.ContextMenu
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import ocelot.desktop.ui.widget.window.Window
+import totoro.ocelot.brain.entity.Redstone
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+abstract class RedstoneCardItem extends Item with ComponentItem with PersistableItem with CardItem
+
+object RedstoneCardItem {
+ abstract class Factory extends ItemFactory {
+ override def name: String = s"Redstone Card (${tier.get.label})"
+
+ override def icon: IconDef = Icons.RedstoneCard(tier.get)
+ }
+
+ class Tier1(val redstoneCard: Redstone.Tier1) extends RedstoneCardItem {
+ private val redstoneIoWindow: WindowProvider = new WindowProvider {
+ override def windowEntryLabel: String = "Redstone I/O"
+
+ override def makeWindow(): Window = new Redstone1Window(redstoneCard)
+ }
+
+ override def component: Entity with Environment = redstoneCard
+
+ protected def addWindowEntries(menu: ContextMenu): Unit = {
+ redstoneIoWindow.fillRmbMenu(menu)
+ }
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ if (redstoneCard.wakeThreshold > 0) {
+ tooltip.addLine(s"Wake threshold: ${redstoneCard.wakeThreshold}")
+ }
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ addWindowEntries(menu)
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: Factory = RedstoneCardItem.Tier1.Factory
+ }
+
+ object Tier1 {
+ object Factory extends Factory {
+ override type I = RedstoneCardItem.Tier1
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.One)
+
+ override def build(): RedstoneCardItem.Tier1 = new RedstoneCardItem.Tier1(new Redstone.Tier1)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new RedstoneCardItem.Tier1(_)))
+ }
+ }
+
+ class Tier2(override val redstoneCard: Redstone.Tier2) extends RedstoneCardItem.Tier1(redstoneCard) {
+ private val bundledIoWindow: WindowProvider = new WindowProvider {
+ override def windowEntryLabel: String = "Bundled I/O"
+
+ override def makeWindow(): Window = new Redstone2Window(redstoneCard)
+ }
+
+ override def component: Entity with Environment = redstoneCard
+
+ override def addWindowEntries(menu: ContextMenu): Unit = {
+ super.addWindowEntries(menu)
+ bundledIoWindow.fillRmbMenu(menu)
+ }
+
+ override def factory: Factory = RedstoneCardItem.Tier2.Factory
+ }
+
+ object Tier2 {
+ object Factory extends Factory {
+ override type I = RedstoneCardItem.Tier2
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def build(): RedstoneCardItem.Tier2 = new RedstoneCardItem.Tier2(new Redstone.Tier2)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new RedstoneCardItem.Tier2(_)))
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala
new file mode 100644
index 0000000..dc10530
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/SelfDestructingCardItem.scala
@@ -0,0 +1,38 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.SelfDestructingCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class SelfDestructingCardItem(val card: SelfDestructingCard)
+ extends Item
+ with ComponentItem
+ with PersistableItem
+ with CardItem {
+
+ override def component: Entity with Environment = card
+
+ override def factory: ItemFactory = SelfDestructingCardItem.Factory
+}
+
+object SelfDestructingCardItem {
+ object Factory extends ItemFactory {
+ override type I = SelfDestructingCardItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = "Self-Destructing Card"
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def icon: IconDef = Icons.SelfDestructingCard
+
+ override def build(): SelfDestructingCardItem = new SelfDestructingCardItem(new SelfDestructingCard)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new SelfDestructingCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala
new file mode 100644
index 0000000..a858053
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/SoundCardItem.scala
@@ -0,0 +1,47 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem, WindowProvider}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import ocelot.desktop.ui.widget.card.SoundCardWindow
+import ocelot.desktop.ui.widget.contextmenu.ContextMenu
+import ocelot.desktop.ui.widget.window.Window
+import totoro.ocelot.brain.entity.sound_card.SoundCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class SoundCardItem(val soundCard: SoundCard) extends Item with ComponentItem with PersistableItem with CardItem {
+ private val window = new WindowProvider {
+ override def windowEntryLabel: String = "Open"
+
+ override def makeWindow(): Window = new SoundCardWindow(soundCard)
+ }
+
+ override def component: Entity with Environment = soundCard
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ window.fillRmbMenu(menu)
+ super.fillRmbMenu(menu)
+ }
+
+ override def factory: ItemFactory = SoundCardItem.Factory
+}
+
+object SoundCardItem {
+ object Factory extends ItemFactory {
+ override type I = SoundCardItem
+
+ override def itemClass: Class[I] = classOf
+
+ override def name: String = "Sound Card"
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def icon: IconDef = Icons.SoundCard
+
+ override def build(): SoundCardItem = new SoundCardItem(new SoundCard)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new SoundCardItem(_)))
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/item/WirelessNetworkCardItem.scala b/src/main/scala/ocelot/desktop/inventory/item/WirelessNetworkCardItem.scala
new file mode 100644
index 0000000..8ec811a
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/item/WirelessNetworkCardItem.scala
@@ -0,0 +1,64 @@
+package ocelot.desktop.inventory.item
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
+import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
+import totoro.ocelot.brain.entity.WirelessNetworkCard
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
+
+abstract class WirelessNetworkCardItem(val card: WirelessNetworkCard)
+ extends Item
+ with ComponentItem
+ with PersistableItem
+ with CardItem {
+
+ override def component: Entity with Environment = card
+}
+
+object WirelessNetworkCardItem {
+ abstract class Factory extends ItemFactory {
+ override def name: String = s"Wireless Net. Card (${tier.get.label})"
+
+ override def icon: IconDef = Icons.WirelessNetworkCard(tier.get)
+ }
+
+ class Tier1(override val card: WirelessNetworkCard.Tier1) extends WirelessNetworkCardItem(card) {
+ override def factory: Factory = WirelessNetworkCardItem.Tier1.Factory
+ }
+
+ object Tier1 {
+ object Factory extends Factory {
+ override type I = WirelessNetworkCardItem.Tier1
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.One)
+
+ override def build(): WirelessNetworkCardItem.Tier1 =
+ new WirelessNetworkCardItem.Tier1(new WirelessNetworkCard.Tier1)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new WirelessNetworkCardItem.Tier1(_)))
+ }
+ }
+
+ class Tier2(override val card: WirelessNetworkCard.Tier2) extends WirelessNetworkCardItem(card) {
+ override def factory: Factory = WirelessNetworkCardItem.Tier2.Factory
+ }
+
+ object Tier2 {
+ object Factory extends Factory {
+ override type I = WirelessNetworkCardItem.Tier2
+
+ override def itemClass: Class[I] = classOf
+
+ override def tier: Option[Tier] = Some(Tier.Two)
+
+ override def build(): WirelessNetworkCardItem.Tier2 =
+ new WirelessNetworkCardItem.Tier2(new WirelessNetworkCard.Tier2)
+
+ override def recoverers: Iterable[ItemRecoverer[_, _]] = Some(ItemRecoverer(new WirelessNetworkCardItem.Tier2(_)))
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/CardItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/CardItem.scala
new file mode 100644
index 0000000..d91bdb6
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/CardItem.scala
@@ -0,0 +1,8 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.inventory.Item
+
+/**
+ * A marker trait implemented by [[Item]]s that can be put into card slots.
+ */
+trait CardItem extends ComponentItem
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala
new file mode 100644
index 0000000..7ed8ecb
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/ComponentItem.scala
@@ -0,0 +1,30 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.inventory.Item
+import ocelot.desktop.ui.UiHandler
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.traits.{Entity, Environment}
+
+/**
+ * Implemented by [[Item]]s that wrap a component.
+ *
+ * @note Subclasses must provide a unary constructor that accepts the [[component]].
+ */
+trait ComponentItem extends Item with PersistableItem {
+ def component: Entity with Environment
+
+ override def fillTooltip(tooltip: ItemTooltip): Unit = {
+ super.fillTooltip(tooltip)
+
+ tooltip.addLine(s"Address: ${component.node.address}")
+ }
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ menu.addEntry(ContextMenuEntry("Copy address") {
+ UiHandler.clipboard = component.node.address
+ })
+
+ super.fillRmbMenu(menu)
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala
new file mode 100644
index 0000000..f13eb65
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/CpuLikeItem.scala
@@ -0,0 +1,36 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.inventory.Item
+import ocelot.desktop.inventory.traits.CpuLikeItem.CpuArchitectureChangedNotification
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
+import totoro.ocelot.brain.entity.machine.MachineAPI
+import totoro.ocelot.brain.entity.traits.{Entity, GenericCPU}
+
+/**
+ * An [[Item]] that acts as a CPU and can therefore be inserted into a CPU slot.
+ */
+trait CpuLikeItem extends ComponentItem {
+ override def component: Entity with GenericCPU
+
+ override def fillRmbMenu(menu: ContextMenu): Unit = {
+ menu.addEntry(new ContextMenuSubmenu("Set architecture") {
+ for (arch <- component.allArchitectures) {
+ val name = MachineAPI.getArchitectureName(arch) +
+ (if (arch == component.architecture) " (current)" else "")
+ val entry = ContextMenuEntry(name) {
+ component.setArchitecture(arch)
+ notifySlot(CpuArchitectureChangedNotification)
+ }
+
+ entry.setEnabled(arch != component.architecture)
+ addEntry(entry)
+ }
+ })
+
+ super.fillRmbMenu(menu)
+ }
+}
+
+object CpuLikeItem {
+ case object CpuArchitectureChangedNotification extends Item.Notification
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala
new file mode 100644
index 0000000..4c5581a
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/DiskItem.scala
@@ -0,0 +1,54 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.OcelotDesktop
+import ocelot.desktop.inventory.Item
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
+import ocelot.desktop.ui.widget.tooltip.ItemTooltip
+import totoro.ocelot.brain.entity.traits.{Disk, DiskManaged, DiskUnmanaged}
+
+import javax.swing.JFileChooser
+import scala.util.Try
+
+/**
+ * A utility mixin for HDDs and floppies.
+ */
+trait DiskItem {
+ this: Item =>
+
+ protected def addSetDirectoryEntry(menu: ContextMenu, disk: DiskManaged): Unit = {
+ menu.addEntry(
+ ContextMenuEntry("Set directory") {
+ OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) { dir =>
+ Try {
+ for (dir <- dir) {
+ // trigger component_removed / component_added signals
+ val slot = this.slot
+ slot.foreach(_.remove())
+ disk.customRealPath = Some(dir.toPath.toAbsolutePath)
+ slot.foreach(slot => slot.put(this.asInstanceOf[slot.inventory.I]))
+ }
+ }
+ }
+ }
+ )
+
+ // TODO: unset directory?????? anyone?
+ }
+
+ protected def addDiskLabelTooltip(tooltip: ItemTooltip, label: String): Unit = {
+ tooltip.addLine(s"Label: $label")
+ }
+
+ protected def addSourcePathTooltip(tooltip: ItemTooltip, disk: DiskManaged): Unit = {
+ for (customRealPath <- disk.customRealPath) {
+ tooltip.addLine(s"Source path: $customRealPath")
+ }
+ }
+
+ protected def addManagedTooltip(tooltip: ItemTooltip, disk: Disk): Unit = {
+ tooltip.addLine(disk match {
+ case _: DiskManaged => "Mode: managed"
+ case _: DiskUnmanaged => "Mode: unmanaged"
+ })
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/PersistableItem.scala b/src/main/scala/ocelot/desktop/inventory/traits/PersistableItem.scala
new file mode 100644
index 0000000..2c09150
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/PersistableItem.scala
@@ -0,0 +1,13 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.inventory.Item
+import totoro.ocelot.brain.nbt.NBTTagCompound
+
+/**
+ * Provides serialization and deserialization for an [[Item]].
+ */
+trait PersistableItem extends Item {
+ def load(nbt: NBTTagCompound): Unit = {}
+
+ def save(nbt: NBTTagCompound): Unit = {}
+}
diff --git a/src/main/scala/ocelot/desktop/inventory/traits/WindowProvider.scala b/src/main/scala/ocelot/desktop/inventory/traits/WindowProvider.scala
new file mode 100644
index 0000000..24a9239
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/inventory/traits/WindowProvider.scala
@@ -0,0 +1,26 @@
+package ocelot.desktop.inventory.traits
+
+import ocelot.desktop.ui.UiHandler
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
+import ocelot.desktop.ui.widget.window.Window
+
+/**
+ * Potentially manages the windows of an [[ocelot.desktop.inventory.Item Item]] that has them (e.g., the sound card).
+ * Though at the moment it just provides a convenient way to add an entry to the item's context menu
+ * for opening a window.
+ */
+trait WindowProvider {
+ def windowEntryLabel: String
+
+ def makeWindow(): Window
+
+ def fillRmbMenu(menu: ContextMenu): Unit = {
+ // TODO: disable the entry while the window is open
+ // TODO: serialize the window
+ menu.addEntry(
+ ContextMenuEntry(windowEntryLabel) {
+ UiHandler.root.workspaceView.windowPool.openWindow(makeWindow())
+ },
+ )
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/node/Node.scala b/src/main/scala/ocelot/desktop/node/Node.scala
index 85f818f..060277f 100644
--- a/src/main/scala/ocelot/desktop/node/Node.scala
+++ b/src/main/scala/ocelot/desktop/node/Node.scala
@@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop
import ocelot.desktop.color.{Color, RGBAColor}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
+import ocelot.desktop.node.Node.{HoveredHighlight, MovingHighlight, NoHighlight}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent}
@@ -20,17 +21,20 @@ import totoro.ocelot.brain.util.Direction
import scala.collection.mutable.ArrayBuffer
-abstract class Node(val entity: Entity with Environment) extends Widget with DragHandler with ClickHandler with HoverHandler {
+abstract class Node(val entity: Entity with Environment)
+ extends Widget
+ with DragHandler
+ with ClickHandler
+ with HoverHandler {
+
if (!OcelotDesktop.workspace.getEntitiesIter.contains(entity))
OcelotDesktop.workspace.add(entity)
var workspaceView: WorkspaceView = _
- protected val MovingHighlight: RGBAColor = RGBAColor(240, 250, 240)
- protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160)
- protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0)
protected val highlight = new ColorAnimation(RGBAColor(0, 0, 0, 0))
protected val canOpen = false
+
protected def exposeAddress = true
protected var isMoving = false
@@ -42,10 +46,12 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
def load(nbt: NBTTagCompound): Unit = {
position = new Vector2D(nbt.getCompoundTag("pos"))
- window.foreach(window => {
- val tag = nbt.getCompoundTag("window")
- window.load(tag)
- })
+ window.foreach(
+ window => {
+ val tag = nbt.getCompoundTag("window")
+ window.load(tag)
+ },
+ )
val lbl = nbt.getString("label")
label = if (lbl == "") None else Some(lbl)
@@ -65,11 +71,13 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
position.save(posTag)
nbt.setTag("pos", posTag)
- window.foreach(window => {
- val tag = new NBTTagCompound
- window.save(tag)
- nbt.setTag("window", tag)
- })
+ window.foreach(
+ window => {
+ val tag = new NBTTagCompound
+ window.save(tag)
+ nbt.setTag("window", tag)
+ },
+ )
nbt.setString("label", label.getOrElse(""))
}
@@ -110,37 +118,47 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
def setupContextMenu(menu: ContextMenu): Unit = {
if (exposeAddress) { // TODO: lift the restriction
- menu.addEntry(new ContextMenuEntry(
- "Set label",
- () => new InputDialog(
- "Set label",
- text => label = if (text.isEmpty) None else Some(text),
- label.getOrElse("")
- ).show()
- ))
+ menu.addEntry(
+ ContextMenuEntry("Set label") {
+ new InputDialog(
+ "Set label",
+ text => label = if (text.isEmpty) None else Some(text),
+ label.getOrElse(""),
+ ).show()
+ },
+ )
}
if (exposeAddress && entity.node != null && entity.node.address != null) {
- menu.addEntry(new ContextMenuEntry("Copy address", () => {
- UiHandler.clipboard = entity.node.address
- }))
+ menu.addEntry(
+ ContextMenuEntry("Copy address") {
+ UiHandler.clipboard = entity.node.address
+ }
+ )
}
- menu.addEntry(new ContextMenuEntry("Disconnect", () => {
- disconnectFromAll()
- }))
+ menu.addEntry(
+ ContextMenuEntry("Disconnect") {
+ disconnectFromAll()
+ },
+ )
- menu.addEntry(new ContextMenuEntry("Delete", () => {
- dispose()
- workspaceView.nodes = workspaceView.nodes.filter(_ != this)
- }))
+ menu.addEntry(
+ ContextMenuEntry("Delete") {
+ destroy()
+ },
+ )
}
override def update(): Unit = {
super.update()
if (isHovered || isMoving) {
- if (canOpen)
- root.get.statusBar.addMouseEntry("icons/LMB", "Open")
+ if (canOpen) {
+ if (isClosed)
+ root.get.statusBar.addMouseEntry("icons/LMB", "Open")
+ else
+ root.get.statusBar.addMouseEntry("icons/LMB", "Close")
+ }
root.get.statusBar.addMouseEntry("icons/RMB", "Menu")
root.get.statusBar.addMouseEntry("icons/DragLMB", "Move node")
root.get.statusBar.addMouseEntry("icons/DragRMB", "Connect/Disconnect")
@@ -173,6 +191,11 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
node.onConnectionRemoved(portB, this, portA)
}
+ def destroy(): Unit = {
+ dispose()
+ workspaceView.nodes = workspaceView.nodes.filter(_ != this)
+ }
+
def disconnectFromAll(): Unit = {
for ((a, node, b) <- connections.toArray) {
disconnect(a, node, b)
@@ -194,7 +217,10 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
def onClick(event: ClickEvent): Unit = {
event match {
case ClickEvent(MouseEvent.Button.Left, _) =>
- window.foreach(window => workspaceView.windowPool.openWindow(window))
+ if (isClosed)
+ window.foreach(workspaceView.windowPool.openWindow(_))
+ else
+ window.foreach(_.hide())
case ClickEvent(MouseEvent.Button.Right, _) =>
val menu = new ContextMenu
setupContextMenu(menu)
@@ -203,6 +229,8 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
}
}
+ private def isClosed: Boolean = window.exists(!_.isOpen)
+
def portsBounds: Iterator[(NodePort, Array[Rect2D])] = {
val length = -4
val thickness = 4
@@ -221,14 +249,16 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
val left = Rect2D(centers(3) + Vector2D(-length, -thickness / 2), hsize)
val centersBounds = Array[Rect2D](top, right, bottom, left)
- val portBounds = (0 until 4).map(side => {
- val offset = thickness - numPorts * stride / 2 + portIdx * stride
- val rect = centersBounds(side)
- side match {
- case 0 | 2 => rect.mapX(_ + offset)
- case 1 | 3 => rect.mapY(_ + offset)
- }
- })
+ val portBounds = (0 until 4).map(
+ side => {
+ val offset = thickness - numPorts * stride / 2 + portIdx * stride
+ val rect = centersBounds(side)
+ side match {
+ case 0 | 2 => rect.mapX(_ + offset)
+ case 1 | 3 => rect.mapY(_ + offset)
+ }
+ },
+ )
(port, portBounds.toArray)
}
@@ -251,7 +281,10 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
val oldPos = position
val desiredPos = if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL))
- (pos - workspaceView.cameraOffset).snap(68) + workspaceView.cameraOffset + Vector2D(34 - width / 2, 34 - height / 2)
+ (pos - workspaceView.cameraOffset).snap(68) + workspaceView.cameraOffset + Vector2D(
+ 34 - width / 2,
+ 34 - height / 2,
+ )
else
pos - grabPoint
@@ -312,3 +345,9 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
OcelotDesktop.workspace.remove(entity)
}
}
+
+object Node {
+ protected val MovingHighlight: RGBAColor = RGBAColor(240, 250, 240)
+ protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160)
+ protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0)
+}
diff --git a/src/main/scala/ocelot/desktop/node/NodeRegistry.scala b/src/main/scala/ocelot/desktop/node/NodeRegistry.scala
index 3245da3..b92f8c0 100644
--- a/src/main/scala/ocelot/desktop/node/NodeRegistry.scala
+++ b/src/main/scala/ocelot/desktop/node/NodeRegistry.scala
@@ -1,60 +1,60 @@
package ocelot.desktop.node
-import ocelot.desktop.node.nodes._
-import totoro.ocelot.brain.entity.traits.GenericCamera
-import totoro.ocelot.brain.entity.{Cable, Case, ColorfulLamp, FloppyDiskDrive, IronNoteBlock, NoteBlock, Relay, Screen}
import ocelot.desktop.entity.{Camera, OpenFMRadio}
+import ocelot.desktop.node.nodes._
+import totoro.ocelot.brain.entity.{Cable, Case, ColorfulLamp, FloppyDiskDrive, IronNoteBlock, NoteBlock, Relay, Screen}
+import totoro.ocelot.brain.util.Tier
import scala.collection.mutable
object NodeRegistry {
val types: mutable.ArrayBuffer[NodeType] = mutable.ArrayBuffer[NodeType]()
- def register(t: NodeType): Unit = {
+ private def register(t: NodeType): Unit = {
types += t
}
- for (tier <- 0 to 2) {
- register(NodeType("Screen" + tier, "nodes/Screen", tier, () => {
+ for (tier <- Tier.One to Tier.Three) {
+ register(NodeType(s"Screen (${tier.label})", "nodes/Screen", tier) {
new ScreenNode(new Screen(tier)).setup()
- }))
+ })
}
- register(NodeType("Disk Drive", "nodes/DiskDrive", -1, () => {
- new DiskDriveNode(new FloppyDiskDrive())
- }))
+ register(NodeType("Disk Drive", "nodes/DiskDrive", None) {
+ new DiskDriveNode(new FloppyDiskDrive(), initDisk = true)
+ })
- for (tier <- 0 to 3) {
- register(NodeType("Computer" + tier, "nodes/Computer", tier, () => {
+ for (tier <- Tier.One to Tier.Creative) {
+ register(NodeType(s"Computer Case (${tier.label})", "nodes/Computer", tier) {
new ComputerNode(new Case(tier)).setup()
- }))
+ })
}
- register(NodeType("Relay", "nodes/Relay", -1, () => {
+ register(NodeType("Relay", "nodes/Relay", None) {
new RelayNode(new Relay)
- }))
+ })
- register(NodeType("Cable", "nodes/Cable", -1, () => {
+ register(NodeType("Cable", "nodes/Cable", None) {
new CableNode(new Cable)
- }))
+ })
- register(NodeType("NoteBlock", "nodes/NoteBlock", -1, () => {
+ register(NodeType("Note Block", "nodes/NoteBlock", None) {
new NoteBlockNode(new NoteBlock)
- }))
+ })
- register(NodeType("IronNoteBlock", "nodes/IronNoteBlock", -1, () => {
+ register(NodeType("Iron Note Block", "nodes/IronNoteBlock", None) {
new IronNoteBlockNode(new IronNoteBlock)
- }))
+ })
- register(NodeType("Camera", "nodes/Camera", -1, () => {
+ register(NodeType("Camera", "nodes/Camera", None) {
new CameraNode(new Camera)
- }))
+ })
- register(NodeType("ColorfulLamp", "nodes/Lamp", -1, () => {
+ register(NodeType("Colorful Lamp", "nodes/Lamp", None) {
new ColorfulLampNode(new ColorfulLamp)
- }))
+ })
- register(NodeType("OpenFM radio", "nodes/OpenFMRadio", -1, () => {
+ register(NodeType("OpenFM radio", "nodes/OpenFMRadio", None) {
new OpenFMRadioNode(new OpenFMRadio)
- }))
+ })
}
diff --git a/src/main/scala/ocelot/desktop/node/NodeType.scala b/src/main/scala/ocelot/desktop/node/NodeType.scala
index 8a97419..2e0f22c 100644
--- a/src/main/scala/ocelot/desktop/node/NodeType.scala
+++ b/src/main/scala/ocelot/desktop/node/NodeType.scala
@@ -1,5 +1,17 @@
package ocelot.desktop.node
-case class NodeType(name: String, icon: String, tier: Int, factory: () => Node) extends Ordered[NodeType] {
+import totoro.ocelot.brain.util.Tier.Tier
+
+class NodeType(val name: String, val icon: String, val tier: Option[Tier], factory: => Node) extends Ordered[NodeType] {
+ def make(): Node = factory
+
override def compare(that: NodeType): Int = this.name.compare(that.name)
}
+
+object NodeType {
+ def apply(name: String, icon: String, tier: Tier)(factory: => Node): NodeType =
+ new NodeType(name, icon, Some(tier), factory)
+
+ def apply(name: String, icon: String, tier: Option[Tier])(factory: => Node): NodeType =
+ new NodeType(name, icon, tier, factory)
+}
diff --git a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala
index d7d8727..2e52530 100644
--- a/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala
+++ b/src/main/scala/ocelot/desktop/node/NodeTypeWidget.scala
@@ -1,10 +1,12 @@
package ocelot.desktop.node
+import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
-import ocelot.desktop.ui.event.{ClickEvent, MouseEvent}
+import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.Widget
+import ocelot.desktop.ui.widget.tooltip.LabelTooltip
import ocelot.desktop.util.{Spritesheet, TierColor}
class NodeTypeWidget(val nodeType: NodeType) extends Widget with ClickHandler with HoverHandler {
@@ -19,11 +21,29 @@ class NodeTypeWidget(val nodeType: NodeType) extends Widget with ClickHandler wi
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, _) => onClick()
+ case HoverEvent(state) =>
+ if (state == HoverEvent.State.Enter) onHoverEnter()
+ else onHoverLeave()
}
+ private val labelTooltip = new LabelTooltip(nodeType.name)
+
+ def onHoverEnter(): Unit =
+ root.get.tooltipPool.addTooltip(labelTooltip)
+
+ def onHoverLeave(): Unit =
+ root.get.tooltipPool.closeTooltip(labelTooltip)
+
override def draw(g: Graphics): Unit = {
val size = Spritesheet.spriteSize(nodeType.icon) * 4
- g.sprite(nodeType.icon, position.x + 34 - size.width / 2, position.y + 34 - size.height / 2, size.width, size.height, TierColor.get(nodeType.tier))
+ g.sprite(
+ nodeType.icon,
+ position.x + 34 - size.width / 2,
+ position.y + 34 - size.height / 2,
+ size.width,
+ size.height,
+ nodeType.tier.map(TierColor.get).getOrElse(Color.White)
+ )
}
override def update(): Unit = {
diff --git a/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala
index b9b227d..17cd16c 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/ColorfulLampNode.scala
@@ -9,7 +9,7 @@ import totoro.ocelot.brain.entity.ColorfulLamp
class ColorfulLampNode(val lamp: ColorfulLamp) extends Node(lamp) {
private var lastColor: RGBAColor = RGBAColor(0, 0, 0)
private var mouseHover: Boolean = false
- override def exposeAddress = mouseHover
+ override def exposeAddress: Boolean = mouseHover
override def draw(g: Graphics): Unit = {
super.draw(g)
diff --git a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala
index 290a1b9..b21fd16 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala
@@ -1,46 +1,115 @@
package ocelot.desktop.node.nodes
+import ocelot.desktop.ColorScheme
import ocelot.desktop.audio._
import ocelot.desktop.color.Color
+import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
+import ocelot.desktop.inventory.item._
+import ocelot.desktop.inventory.traits.ComponentItem
+import ocelot.desktop.inventory.{Item, SyncedInventory}
import ocelot.desktop.node.Node
+import ocelot.desktop.node.nodes.ComputerNode.{ErrorMessageMoveSpeed, MaxErrorMessageDistance}
+import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{BrainEvent, ClickEvent, MouseEvent}
+import ocelot.desktop.ui.widget.Label
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
import ocelot.desktop.ui.widget.slot._
-import ocelot.desktop.util.{Logging, TierColor}
+import ocelot.desktop.util.{Logging, Messages, TierColor}
import ocelot.desktop.windows.ComputerWindow
import org.lwjgl.input.Keyboard
import totoro.ocelot.brain.Settings
-import totoro.ocelot.brain.entity.traits.{Entity, Environment, Floppy, GenericCPU, Inventory}
-import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, HDDUnmanaged, Memory}
+import totoro.ocelot.brain.entity.Case
+import totoro.ocelot.brain.entity.traits.{Environment, Inventory}
import totoro.ocelot.brain.event.FileSystemActivityType.Floppy
-import totoro.ocelot.brain.event.{BeepEvent, BeepPatternEvent, FileSystemActivityEvent, MachineCrashEvent, SoundCardAudioEvent}
+import totoro.ocelot.brain.event._
import totoro.ocelot.brain.loot.Loot
+import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
-import java.nio.ByteBuffer
+import scala.collection.mutable
+import scala.math.Ordering.Implicits.infixOrderingOps
import scala.reflect.ClassTag
import scala.util.Random
-class ComputerNode(val computer: Case) extends Node(computer) with Logging {
- var eepromSlot: EEPROMSlot = _
- var cpuSlot: CPUSlot = _
- var memorySlots: Array[MemorySlot] = _
- var cardSlots: Array[CardSlot] = _
- var diskSlots: Array[DiskSlot] = _
- var floppySlot: Option[FloppySlot] = None
+class ComputerNode(val computer: Case) extends Node(computer) with Logging with SyncedInventory {
+ node =>
- private val soundComputerRunning = SoundSource.fromBuffer(SoundBuffers.MachineComputerRunning, SoundCategory.Environment, looping = true)
+ override type I = Item with ComponentItem
+
+ var eepromSlot: EepromSlotWidget = _
+ var cpuSlot: CpuSlotWidget = _
+ var memorySlots: Array[MemorySlotWidget] = Array.empty
+ var cardSlots: Array[CardSlotWidget] = Array.empty
+ var diskSlots: Array[HddSlotWidget] = Array.empty
+ var floppySlot: Option[FloppySlotWidget] = None
+
+ override def brainInventory: Inventory = computer.inventory.owner
+
+ private def slots: IterableOnce[SlotWidget[I]] = (
+ // slots may be null during initialization
+ Option(eepromSlot).iterator ++
+ Option(cpuSlot).iterator ++
+ memorySlots.iterator ++
+ cardSlots.iterator ++
+ diskSlots.iterator ++
+ floppySlot.iterator
+ ).map(_.asInstanceOf[SlotWidget[I]])
+
+ // NOTE: `soundComputerRunning` must be lazy so that it doesn't get loaded before the audio subsystem is initialized
+ private lazy val soundComputerRunning = SoundSource.fromBuffer(
+ SoundBuffers.MachineComputerRunning,
+ SoundCategory.Environment,
+ looping = true,
+ )
+ // TODO: Scala has lazy vals. Use them.
private var soundCardStream: SoundStream = _
private var soundCardSource: SoundSource = _
setupSlots()
- refitSlots()
+
+ override def load(nbt: NBTTagCompound): Unit = {
+ super[Node].load(nbt)
+ super[SyncedInventory].load(nbt)
+ }
+
+ override def save(nbt: NBTTagCompound): Unit = {
+ super[Node].save(nbt)
+ super[SyncedInventory].save(nbt)
+ }
+
+ private class ErrorMessageLabel(override val text: String) extends Label {
+ override def isSmall: Boolean = true
+
+ var alpha: Float = 1f
+
+ override def color: Color = ColorScheme("ErrorMessage").toRGBANorm.mapA(_ * alpha)
+
+ val initialPosition: Vector2D = {
+ val position = node.position
+ val size = node.size
+
+ position + Vector2D(size.width / 2 - minimumSize.width / 2, -minimumSize.height)
+ }
+
+ position = initialPosition
+ }
eventHandlers += {
case BrainEvent(event: MachineCrashEvent) =>
- logger.info(s"[EVENT] Machine crash! (address = ${event.address}, ${event.message})")
+ val message = Messages.lift(event.message) match {
+ case Some(message) =>
+ logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message")
+ message
+
+ case None =>
+ logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}")
+ event.message
+ }
+
+ addErrorMessage(new ErrorMessageLabel(message))
case BrainEvent(event: BeepEvent) if !Audio.isDisabled =>
BeepGenerator.newBeep(".", event.frequency, event.duration).play()
@@ -49,8 +118,10 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
BeepGenerator.newBeep(event.pattern, 1000, 200).play()
case BrainEvent(event: FileSystemActivityEvent) if !Audio.isDisabled =>
- val soundFloppyAccess = SoundBuffers.MachineFloppyAccess.map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
- val soundHDDAccess = SoundBuffers.MachineHDDAccess.map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
+ val soundFloppyAccess = SoundBuffers.MachineFloppyAccess
+ .map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
+ val soundHDDAccess = SoundBuffers.MachineHDDAccess
+ .map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
val sound = if (event.activityType == Floppy) soundFloppyAccess else soundHDDAccess
sound(Random.between(0, sound.length)).play()
@@ -63,19 +134,31 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
}
soundCardStream.enqueue(samples)
soundCardSource.volume = event.volume
+
+ case BrainEvent(_: SelfDestructingCardBoomEvent) =>
+ computer.workspace.runLater(
+ () => {
+ SoundSources.MinecraftExplosion.play()
+ destroy()
+ },
+ )
}
override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) ||
computer.inventory.entities.exists { case env: Environment => env.node.address == address }
def setup(): ComputerNode = {
- cpuSlot.owner.put(new CPU(computer.tier.min(2)))
- memorySlots(0).owner.put(new Memory(computer.tier.min(2) * 2 + 1))
- memorySlots(1).owner.put(new Memory(computer.tier.min(2) * 2 + 1))
- cardSlots(0).owner.put(new GraphicsCard(computer.tier.min(1)))
- floppySlot.map(_.owner).foreach(_.put(Loot.OpenOsFloppy.create()))
- eepromSlot.owner.put(Loot.LuaBiosEEPROM.create())
- refitSlots()
+ cpuSlot.item = new CpuItem.Factory(computer.tier min Tier.Three).build()
+ memorySlots(0).item = new MemoryItem.Factory((computer.tier min Tier.Three).toExtended(true)).build()
+ memorySlots(1).item = new MemoryItem.Factory((computer.tier min Tier.Three).toExtended(true)).build()
+ cardSlots(0).item = new GraphicsCardItem.Factory(computer.tier min Tier.Two).build()
+
+ for (floppySlot <- floppySlot) {
+ floppySlot.item = new FloppyItem.Factory.Loot(Loot.OpenOsFloppy).build()
+ }
+
+ eepromSlot.item = new EepromItem.Factory.Loot(Loot.LuaBiosEEPROM).build()
+
this
}
@@ -99,20 +182,36 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
override def setupContextMenu(menu: ContextMenu): Unit = {
if (isRunning) {
- menu.addEntry(new ContextMenuEntry("Turn off", () => turnOff()))
- menu.addEntry(new ContextMenuEntry("Reboot", () => {
- computer.turnOff()
- computer.turnOn()
- }))
- } else
- menu.addEntry(new ContextMenuEntry("Turn on", () => turnOn()))
+ menu.addEntry(
+ ContextMenuEntry("Turn off") {
+ turnOff()
+ },
+ )
+ menu.addEntry(
+ ContextMenuEntry("Reboot") {
+ computer.turnOff()
+ computer.turnOn()
+ },
+ )
+ } else {
+ menu.addEntry(
+ ContextMenuEntry("Turn on") {
+ turnOn()
+ },
+ )
+ }
- menu.addEntry(new ContextMenuSubmenu("Set tier") {
- addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One)))
- addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two)))
- addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three)))
- addEntry(new ContextMenuEntry("Creative", () => changeTier(Tier.Four)))
- })
+ menu.addEntry(
+ new ContextMenuSubmenu("Set tier") {
+ for (tier <- Tier.One to Tier.Creative) {
+ addEntry(
+ ContextMenuEntry(tier.label) {
+ changeTier(tier)
+ },
+ )
+ }
+ },
+ )
menu.addSeparator()
super.setupContextMenu(menu)
@@ -121,107 +220,63 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
override def onClick(event: ClickEvent): Unit = {
event match {
case ClickEvent(MouseEvent.Button.Left, _) =>
- if (KeyEvents.isDown(Keyboard.KEY_LSHIFT))
- if (isRunning)
+ if (KeyEvents.isDown(Keyboard.KEY_LSHIFT)) {
+ if (isRunning) {
turnOff()
- else
+ } else {
turnOn()
- else
+ }
+ } else {
super.onClick(event)
+ }
+
case event => super.onClick(event)
}
}
- private def changeTier(n: Int): Unit = {
- computer.tier = n
+ private def changeTier(tier: Tier): Unit = {
+ computer.tier = tier
+
+ val items = slots.iterator.flatMap(_.item).toArray
+ clearInventory()
setupSlots()
- refitSlots()
- if (currentWindow != null) currentWindow.updateSlots()
- }
+ insertItems(items)
- private def slotAccepts(slot: Inventory#Slot, entity: Entity): Boolean = entity match {
- case cpu: GenericCPU => cpuSlot.owner.index == slot.index && cpuSlot.tier >= cpu.cpuTier
- case mem: Memory => memorySlots
- .exists(memSlot => memSlot.owner.index == slot.index && memSlot.tier >= (mem.tier + 1) / 2 - 1)
- case hdd: HDDManaged => diskSlots
- .exists(diskSlot => diskSlot.owner.index == slot.index && diskSlot.tier >= hdd.tier)
- case hdd: HDDUnmanaged => diskSlots
- .exists(diskSlot => diskSlot.owner.index == slot.index && diskSlot.tier >= hdd.tier)
- case _: EEPROM => eepromSlot.owner.index == slot.index
- case _: Floppy => floppySlot.exists(_.owner.index == slot.index)
- case card: Entity => cardSlots
- .exists(cardSlot => cardSlot.owner.index == slot.index && cardSlot.tier >= CardRegistry.getTier(card))
- }
-
- private def isSlotValid(slot: Inventory#Slot): Boolean = slot.get.exists(slotAccepts(slot, _))
-
- private def reloadSlots(): Unit = {
- cpuSlot.reloadItem()
- memorySlots.foreach(_.reloadItem())
- cardSlots.foreach(_.reloadItem())
- diskSlots.foreach(_.reloadItem())
- eepromSlot.reloadItem()
- floppySlot.foreach(_.reloadItem())
- }
-
- private def refitSlots(): Unit = {
- if (computer.inventory.forall(isSlotValid)) {
- reloadSlots()
-
- return
+ if (currentWindow != null) {
+ currentWindow.reloadWindow()
}
+ }
- val entities = computer.inventory.entities.toArray
- computer.inventory.clear()
-
- cpuSlot._item = None
- for (slot <- memorySlots) slot._item = None
- for (slot <- cardSlots) slot._item = None
- for (slot <- diskSlots) slot._item = None
- eepromSlot._item = None
- floppySlot.foreach(_._item = None)
-
- def findBestSlot[T <: InventorySlot[_]](entity: Entity, candidates: Array[T], tierProvider: T => Option[Int]): Option[T] = {
+ private def insertItems(items: IterableOnce[I]): Unit = {
+ def findBestSlot[A <: I](item: A, candidates: IterableOnce[SlotWidget[A]]): Option[SlotWidget[A]] = {
candidates.iterator
- .filter(_.owner.isEmpty)
- .filter(slot => slotAccepts(slot.owner, entity))
- .minByOption(tierProvider(_).getOrElse(Int.MinValue))
+ .filter(_.item.isEmpty)
+ .filter(_.isItemAccepted(item.factory))
+ .minByOption(_.slotTier)
}
- for (entity <- entities) {
- val newSlot = entity match {
- case _: GenericCPU => findBestSlot[CPUSlot](entity, Array(cpuSlot), slot => Some(slot.tier))
- case _: Memory => findBestSlot[MemorySlot](entity, memorySlots, slot => Some(slot.tier))
- case _: HDDManaged => findBestSlot[DiskSlot](entity, diskSlots, slot => Some(slot.tier))
- case _: HDDUnmanaged => findBestSlot[DiskSlot](entity, diskSlots, slot => Some(slot.tier))
- case _: EEPROM => findBestSlot[EEPROMSlot](entity, Array(eepromSlot), _ => None)
- case _: Floppy => findBestSlot[FloppySlot](entity, floppySlot.toArray, _ => None)
- case _: Entity => findBestSlot[CardSlot](entity, cardSlots, slot => Some(slot.tier))
- }
-
- newSlot.foreach(_.owner.put(entity))
+ for (item <- items; newSlot <- findBestSlot(item, slots)) {
+ newSlot.item = item
}
-
- reloadSlots()
}
private def setupSlots(): Unit = {
var slotIndex = 0
- def nextSlot(): computer.Slot = {
- val result = computer.inventory(slotIndex)
+ def nextSlot(): Slot = {
+ val result = Slot(slotIndex)
slotIndex += 1
result
}
- def addSlot[T <: InventorySlot[_]](factory: computer.Slot => T): T = {
+ def addSlot[T <: SlotWidget[_]](factory: Slot => T): T = {
val slot = nextSlot()
val widget = factory(slot)
widget
}
- def addSlots[T <: InventorySlot[_] : ClassTag](factories: (computer.Slot => T)*): Array[T] = {
+ def addSlots[T <: SlotWidget[_] : ClassTag](factories: (Slot => T)*): Array[T] = {
val array = Array.newBuilder[T]
for (factory <- factories) {
@@ -231,43 +286,51 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
array.result()
}
+ for (slot <- slots) {
+ slot.dispose()
+ }
+
computer.tier match {
case Tier.One =>
- cardSlots = addSlots(new CardSlot(_, Tier.One), new CardSlot(_, Tier.One))
- memorySlots = addSlots(new MemorySlot(_, Tier.One))
- diskSlots = addSlots(new DiskSlot(_, Tier.One))
+ cardSlots = addSlots(new CardSlotWidget(_, Tier.One), new CardSlotWidget(_, Tier.One))
+ memorySlots = addSlots(new MemorySlotWidget(_, Tier.One))
+ diskSlots = addSlots(new HddSlotWidget(_, Tier.One))
floppySlot = None
- cpuSlot = addSlot(new CPUSlot(_, this, Tier.One))
+ cpuSlot = addSlot(new CpuSlotWidget(_, this, Tier.One))
// no idea why on earth the memory slots are split in two here
- memorySlots :+= addSlot(new MemorySlot(_, Tier.One))
- eepromSlot = addSlot(new EEPROMSlot(_))
+ memorySlots :+= addSlot(new MemorySlotWidget(_, Tier.One))
+ eepromSlot = addSlot(new EepromSlotWidget(_))
case Tier.Two =>
- cardSlots = addSlots(new CardSlot(_, Tier.Two), new CardSlot(_, Tier.One))
- memorySlots = addSlots(new MemorySlot(_, Tier.Two), new MemorySlot(_, Tier.Two))
- diskSlots = addSlots(new DiskSlot(_, Tier.Two), new DiskSlot(_, Tier.One))
+ cardSlots = addSlots(new CardSlotWidget(_, Tier.Two), new CardSlotWidget(_, Tier.One))
+ memorySlots = addSlots(new MemorySlotWidget(_, Tier.Two), new MemorySlotWidget(_, Tier.Two))
+ diskSlots = addSlots(new HddSlotWidget(_, Tier.Two), new HddSlotWidget(_, Tier.One))
floppySlot = None
- cpuSlot = addSlot(new CPUSlot(_, this, Tier.Two))
- eepromSlot = addSlot(new EEPROMSlot(_))
+ cpuSlot = addSlot(new CpuSlotWidget(_, this, Tier.Two))
+ eepromSlot = addSlot(new EepromSlotWidget(_))
case _ =>
cardSlots = if (computer.tier == Tier.Three) {
- addSlots(new CardSlot(_, Tier.Three), new CardSlot(_, Tier.Two), new CardSlot(_, Tier.Two))
+ addSlots(new CardSlotWidget(_, Tier.Three), new CardSlotWidget(_, Tier.Two), new CardSlotWidget(_, Tier.Two))
} else {
- addSlots(new CardSlot(_, Tier.Three), new CardSlot(_, Tier.Three), new CardSlot(_, Tier.Three))
+ addSlots(
+ new CardSlotWidget(_, Tier.Three),
+ new CardSlotWidget(_, Tier.Three),
+ new CardSlotWidget(_, Tier.Three),
+ )
}
- memorySlots = addSlots(new MemorySlot(_, Tier.Three), new MemorySlot(_, Tier.Three))
+ memorySlots = addSlots(new MemorySlotWidget(_, Tier.Three), new MemorySlotWidget(_, Tier.Three))
diskSlots = if (computer.tier == Tier.Three) {
- addSlots(new DiskSlot(_, Tier.Three), new DiskSlot(_, Tier.Two))
+ addSlots(new HddSlotWidget(_, Tier.Three), new HddSlotWidget(_, Tier.Two))
} else {
- addSlots(new DiskSlot(_, Tier.Three), new DiskSlot(_, Tier.Three))
+ addSlots(new HddSlotWidget(_, Tier.Three), new HddSlotWidget(_, Tier.Three))
}
- floppySlot = Some(addSlot(new FloppySlot(_)))
- cpuSlot = addSlot(new CPUSlot(_, this, Tier.Three))
- eepromSlot = addSlot(new EEPROMSlot(_))
+ floppySlot = Some(addSlot(new FloppySlotWidget(_)))
+ cpuSlot = addSlot(new CpuSlotWidget(_, this, Tier.Three))
+ eepromSlot = addSlot(new EepromSlotWidget(_))
}
}
@@ -288,10 +351,21 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
override def update(): Unit = {
super.update()
- if (!isRunning && soundComputerRunning.isPlaying)
+
+ if (!isRunning && soundComputerRunning.isPlaying) {
soundComputerRunning.stop()
- if (isHovered || isMoving)
+ } else if (isRunning && !soundComputerRunning.isPlaying && !Audio.isDisabled) {
+ soundComputerRunning.play()
+ }
+
+ if (isHovered || isMoving) {
root.get.statusBar.addKeyMouseEntry("icons/LMB", "SHIFT", if (isRunning) "Turn Off" else "Turn On")
+ }
+ }
+
+ override def dispose(): Unit = {
+ super.dispose()
+ soundComputerRunning.stop()
}
private var currentWindow: ComputerWindow = _
@@ -303,4 +377,25 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
} else Some(currentWindow)
}
+ private val messages = mutable.ArrayBuffer[(Float, ErrorMessageLabel)]()
+
+ private def addErrorMessage(message: ErrorMessageLabel): Unit = synchronized {
+ messages += ((0f, message))
+ }
+
+ override def drawParticles(g: Graphics): Unit = synchronized {
+ for ((time, message) <- messages.reverseIterator) {
+ message.position = message.initialPosition + Vector2D(0, -MaxErrorMessageDistance * time)
+ message.alpha = 1 - time
+ message.draw(g)
+ }
+
+ messages.mapInPlace { case (t, message) => (t + ErrorMessageMoveSpeed * UiHandler.dt, message) }
+ messages.filterInPlace(_._1 <= 1f)
+ }
+}
+
+object ComputerNode {
+ private val MaxErrorMessageDistance: Float = 50
+ private val ErrorMessageMoveSpeed: Float = 0.5f
}
diff --git a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala
index fe702df..67b38ed 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala
@@ -2,57 +2,83 @@ package ocelot.desktop.node.nodes
import ocelot.desktop.color.IntColor
import ocelot.desktop.graphics.Graphics
+import ocelot.desktop.inventory.SyncedInventory
+import ocelot.desktop.inventory.item.FloppyItem
import ocelot.desktop.node.Node
-import ocelot.desktop.ui.widget.slot.FloppySlot
+import ocelot.desktop.ui.widget.slot.FloppySlotWidget
import ocelot.desktop.windows.DiskDriveWindow
import totoro.ocelot.brain.entity.FloppyDiskDrive
-import totoro.ocelot.brain.entity.traits.Floppy
+import totoro.ocelot.brain.entity.traits.Inventory
import totoro.ocelot.brain.loot.Loot
+import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.DyeColor
-class DiskDriveNode(val diskDrive: FloppyDiskDrive) extends Node(diskDrive) {
- val slot: FloppySlot = new FloppySlot(diskDrive.inventory(0))
- slot.item = floppy.getOrElse(Loot.OpenOsFloppy.create())
+class DiskDriveNode(val diskDrive: FloppyDiskDrive, initDisk: Boolean) extends Node(diskDrive) with SyncedInventory {
+ def this(diskDrive: FloppyDiskDrive) = {
+ this(diskDrive, false)
+ }
+
+ override type I = FloppyItem
+
+ override def brainInventory: Inventory = diskDrive.inventory.owner
+
+ val slot: FloppySlotWidget = new FloppySlotWidget(Slot(0))
+
+ if (initDisk) {
+ slot.item = new FloppyItem.Factory.Loot(Loot.OpenOsFloppy).build()
+ }
+
+ override def load(nbt: NBTTagCompound): Unit = {
+ super[Node].load(nbt)
+ super[SyncedInventory].load(nbt)
+ }
+
+ override def save(nbt: NBTTagCompound): Unit = {
+ super[Node].save(nbt)
+ super[SyncedInventory].save(nbt)
+ }
override def icon: String = "nodes/DiskDrive"
override protected val canOpen = true
private val colorMap: Map[DyeColor, Int] = Map(
- DyeColor.BLACK -> 0x444444, // 0x1E1B1B
- DyeColor.RED -> 0xB3312C,
- DyeColor.GREEN -> 0x339911, // 0x3B511A
- DyeColor.BROWN -> 0x51301A,
- DyeColor.BLUE -> 0x6666FF, // 0x253192
- DyeColor.PURPLE -> 0x7B2FBE,
- DyeColor.CYAN -> 0x66FFFF, // 0x287697
- DyeColor.SILVER -> 0xABABAB,
- DyeColor.GRAY -> 0x666666, // 0x434343
- DyeColor.PINK -> 0xD88198,
- DyeColor.LIME -> 0x66FF66, // 0x41CD34
- DyeColor.YELLOW -> 0xFFFF66, // 0xDECF2A
- DyeColor.LIGHT_BLUE -> 0xAAAAFF, // 0x6689D3
- DyeColor.MAGENTA -> 0xC354CD,
- DyeColor.ORANGE -> 0xEB8844,
- DyeColor.WHITE -> 0xF0F0F0
+ DyeColor.Black -> 0x444444, // 0x1E1B1B
+ DyeColor.Red -> 0xB3312C,
+ DyeColor.Green -> 0x339911, // 0x3B511A
+ DyeColor.Brown -> 0x51301A,
+ DyeColor.Blue -> 0x6666FF, // 0x253192
+ DyeColor.Purple -> 0x7B2FBE,
+ DyeColor.Cyan -> 0x66FFFF, // 0x287697
+ DyeColor.Silver -> 0xABABAB,
+ DyeColor.Gray -> 0x666666, // 0x434343
+ DyeColor.Pink -> 0xD88198,
+ DyeColor.Lime -> 0x66FF66, // 0x41CD34
+ DyeColor.Yellow -> 0xFFFF66, // 0xDECF2A
+ DyeColor.LightBlue -> 0xAAAAFF, // 0x6689D3
+ DyeColor.Magenta -> 0xC354CD,
+ DyeColor.Orange -> 0xEB8844,
+ DyeColor.White -> 0xF0F0F0
)
override def draw(g: Graphics): Unit = {
super.draw(g)
- if (System.currentTimeMillis() - diskDrive.lastDiskAccess < 400 && Math.random() > 0.1)
+ if (System.currentTimeMillis() - diskDrive.lastDiskAccess < 400 && Math.random() > 0.1) {
g.sprite("nodes/DiskDriveActivity", position.x + 2, position.y + 2, size.width - 4, size.height - 4)
+ }
- if (slot.item.isDefined)
- g.sprite("nodes/DiskDriveFloppy", position.x + 2, position.y + 2, size.width - 4, size.height - 4, IntColor(colorMap(slot.item.get.color)))
+ for (item <- slot.item) {
+ g.sprite(
+ "nodes/DiskDriveFloppy",
+ position.x + 2,
+ position.y + 2,
+ size.width - 4,
+ size.height - 4,
+ IntColor(colorMap(item.color)),
+ )
+ }
}
override val window: Option[DiskDriveWindow] = Some(new DiskDriveWindow(this))
-
- private def floppy: Option[Floppy] = {
- diskDrive.inventory(0).get match {
- case Some(floppy: Floppy) => Some(floppy)
- case _ => None
- }
- }
}
diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala
index 1290050..59794e6 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala
@@ -12,7 +12,9 @@ class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBloc
val maxLen = NoteBlockNode.Instruments.map(_._2.length).max
for ((instrument, name) <- NoteBlockNode.Instruments) {
val dot = if (noteBlock.instrument == instrument) '•' else ' '
- addEntry(new ContextMenuEntry(name.padTo(maxLen, ' ') + dot, () => noteBlock.instrument = instrument))
+ addEntry(ContextMenuEntry(name.padTo(maxLen, ' ') + dot) {
+ noteBlock.instrument = instrument
+ })
}
}
})
diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala
index 5edfb77..b430277 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala
@@ -22,7 +22,7 @@ abstract class NoteBlockNodeBase(entity: Entity with Environment) extends Node(e
private val particles = mutable.ArrayBuffer[(Float, Int)]()
- def addParticle(pitch: Int): Unit = {
+ private def addParticle(pitch: Int): Unit = {
synchronized {
particles += ((0f, pitch))
}
diff --git a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala
index a9332c6..8ef42a8 100644
--- a/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala
+++ b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala
@@ -50,9 +50,9 @@ class ScreenNode(val screen: Screen) extends Node(screen) {
override def setupContextMenu(menu: ContextMenu): Unit = {
if (screen.getPowerState)
- menu.addEntry(new ContextMenuEntry("Turn off", () => screen.setPowerState(false)))
+ menu.addEntry(ContextMenuEntry("Turn off") { screen.setPowerState(false) })
else
- menu.addEntry(new ContextMenuEntry("Turn on", () => screen.setPowerState(true)))
+ menu.addEntry(ContextMenuEntry("Turn on") { screen.setPowerState(true) })
menu.addSeparator()
super.setupContextMenu(menu)
diff --git a/src/main/scala/ocelot/desktop/ui/UiHandler.scala b/src/main/scala/ocelot/desktop/ui/UiHandler.scala
index 619654b..b0f0abd 100644
--- a/src/main/scala/ocelot/desktop/ui/UiHandler.scala
+++ b/src/main/scala/ocelot/desktop/ui/UiHandler.scala
@@ -1,8 +1,8 @@
package ocelot.desktop.ui
import buildinfo.BuildInfo
-import ocelot.desktop.audio.Audio
-import ocelot.desktop.geometry.{Size2D, Vector2D}
+import ocelot.desktop.audio.{Audio, SoundBuffers}
+import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.MouseEvent
import ocelot.desktop.ui.event.handlers.HoverHandler
@@ -34,8 +34,9 @@ object UiHandler extends Logging {
private var shouldUpdateHierarchy = true
private val fpsCalculator = new FPSCalculator
private val ticker = new Ticker
+ var scalingFactor = 1.0f
- ticker.tickInterval = 1.second / 60
+ ticker.tickInterval = 1.second / 144
def getHierarchy: Array[Widget] = hierarchy.toArray
@@ -62,7 +63,7 @@ object UiHandler extends Logging {
def dt: Float = fpsCalculator.dt
def mousePosition: Vector2D = {
- Vector2D(Mouse.getX, Display.getHeight - Mouse.getY)
+ Vector2D(Mouse.getX, Display.getHeight - Mouse.getY) / scalingFactor
}
private val _clipboard = Toolkit.getDefaultToolkit.getSystemClipboard
@@ -80,19 +81,25 @@ object UiHandler extends Logging {
_clipboard.setContents(data, data)
}
- def isFullScreen: Boolean = Display.isFullscreen
+ def fullScreen: Boolean = Display.isFullscreen
- def isFullScreen_=(value: Boolean): Any = {
+ def fullScreen_=(value: Boolean): Unit = {
if (value) {
Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode)
}
else {
// Updating size from settings
val settingsSize = Settings.get.windowSize
- root.size = if (settingsSize.isSet) Size2D(settingsSize.x, settingsSize.y) else Size2D(800, 600)
+
+ val unscaledSize = sanitizeWindowSize(
+ if (settingsSize.isSet) Size2D(settingsSize.x, settingsSize.y)
+ else Size2D(800, 600)
+ )
+
+ root.size = unscaledSize / scalingFactor
// Setting normal display mode (non-fullscreen by default)
- Display.setDisplayMode(new DisplayMode(root.size.width.toInt, root.size.height.toInt))
+ Display.setDisplayMode(new DisplayMode(unscaledSize.width.toInt, unscaledSize.height.toInt))
// Updating position from settings
if (Settings.get.windowPosition.isSet) {
@@ -131,39 +138,100 @@ object UiHandler extends Logging {
Settings.get.windowFullscreen = value
}
+ private def formatRect(rect: Rect2D): String = {
+ val Rect2D(x, y, w, h) = rect
+
+ f"$w%.0f×$h%.0f$x%+.0f$y%+.0f"
+ }
+
+ private def windowGeometry: Rect2D = new Rect2D(Display.getX, Display.getY, Display.getWidth, Display.getHeight)
+
+ private def sanitizeWindowSize(size: Size2D): Size2D =
+ Size2D(
+ if (size.width < 10) 800 else size.width,
+ if (size.height < 10) 600 else size.height,
+ )
+
+ private def sanitizeWindowGeometry(currentGeometry: Rect2D): Rect2D = {
+ val Rect2D(x, y, w, h) = currentGeometry
+ val newSize = sanitizeWindowSize(Size2D(w, h))
+
+ Rect2D(
+ x max 0,
+ y max 0,
+ newSize.width,
+ newSize.height
+ )
+ }
+
+ private def fixInsaneInitialWindowGeometry(): Unit = {
+ // what I mean by insane: ocelot.desktop.ui.UiHandler$ - Created window: 0×0 (at 960, -569)
+
+ val currentGeometry = windowGeometry
+ val geometry = sanitizeWindowGeometry(currentGeometry)
+
+ if (geometry != currentGeometry) {
+ logger.warn(s"Window geometry sanity check failed: ${formatRect(currentGeometry)} is officially insane")
+ logger.warn(s"Resetting to ${formatRect(geometry)}")
+ Display.setDisplayMode(new DisplayMode(geometry.w.toInt, geometry.h.toInt))
+ Display.setLocation(geometry.x.toInt, geometry.y.toInt)
+ }
+ }
+
def init(root: RootWidget): Unit = {
this.root = root
root.relayout()
- loadLibraries()
-
- isFullScreen = Settings.get.windowFullscreen
+ scalingFactor = Settings.get.scaleFactor
+ fullScreen = Settings.get.windowFullscreen
windowTitle = "Ocelot Desktop v" + BuildInfo.version
loadIcons()
- Display.setVSyncEnabled(true)
+
+ if (!Settings.get.disableVsync) {
+ logger.info("VSync enabled")
+ Display.setVSyncEnabled(true)
+ } else {
+ logger.info("VSync disabled (via config)")
+ }
+
Display.create()
+
+ if (Settings.get.windowValidatePosition) {
+ fixInsaneInitialWindowGeometry()
+ }
+
KeyEvents.init()
MouseEvents.init()
- logger.info(s"Created window with ${root.size}")
+ logger.info(s"Created window: ${formatRect(windowGeometry)}")
logger.info(s"OpenGL vendor: ${GL11.glGetString(GL11.GL_VENDOR)}")
logger.info(s"OpenGL renderer: ${GL11.glGetString(GL11.GL_RENDERER)}")
+ logger.info(s"OpenGL version: ${GL11.glGetString(GL11.GL_VERSION)}")
Spritesheet.load()
- graphics = new Graphics
+ graphics = new Graphics(scalingFactor)
- Audio.init()
+ if (Settings.get.audioDisable) {
+ logger.warn("Sound disabled (via config)")
+ } else {
+ Audio.init()
+ }
}
private var nativeLibrariesDir: String = _
- private def loadLibraries(): Unit = {
+ def loadLibraries(): Unit = {
// we cannot remove DLL files on Windows after they were loaded by Ocelot
// therefore we will create them in local directory and keep for future
- nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS)
- Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve( "natives")).toString
- else Files.createTempDirectory("ocelot-desktop").toString
+ nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS) {
+ Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve("natives")).toString
+ } else {
+ val directory = Files.createTempDirectory("ocelot-desktop")
+ Runtime.getRuntime.addShutdownHook(new Thread(() => FileUtils.deleteDirectory(new File(nativeLibrariesDir))))
+
+ directory.toString
+ }
val arch = System.getProperty("os.arch")
val is64bit = arch.startsWith("amd64")
@@ -202,6 +270,11 @@ object UiHandler extends Logging {
}
System.setProperty("org.lwjgl.librarypath", nativeLibrariesDir)
+
+ if (Settings.get.debugLwjgl) {
+ logger.info("Enabling LWJGL debug mode")
+ System.setProperty("org.lwjgl.util.Debug", true.toString)
+ }
}
private def loadIcons(): Unit = {
@@ -257,20 +330,21 @@ object UiHandler extends Logging {
KeyEvents.update()
MouseEvents.update()
- Profiler.startTimeMeasurement("000_update")
- update()
- Profiler.endTimeMeasurement("000_update")
+ Profiler.measure("000_update") {
+ update()
+ }
- Profiler.startTimeMeasurement("001_draw")
- draw()
- Profiler.endTimeMeasurement("001_draw")
+ Profiler.measure("001_draw") {
+ draw()
+ }
+ }
+
+ Profiler.measure("002_sleep") {
+ ticker.waitNext()
}
- Profiler.startTimeMeasurement("002_sleep")
- ticker.waitNext()
Display.update()
fpsCalculator.tick()
- Profiler.endTimeMeasurement("002_sleep")
}
}
@@ -278,11 +352,10 @@ object UiHandler extends Logging {
root.workspaceView.dispose()
KeyEvents.destroy()
MouseEvents.destroy()
+ graphics.freeResource()
+ SoundBuffers.freeResource()
Display.destroy()
Audio.destroy()
-
- if (!SystemUtils.IS_OS_WINDOWS)
- FileUtils.deleteDirectory(new File(nativeLibrariesDir))
}
private def update(): Unit = {
@@ -332,11 +405,15 @@ object UiHandler extends Logging {
val width = Display.getWidth
val height = Display.getHeight
- graphics.resize(width, height)
- root.size = Size2D(width, height)
+ if (graphics.resize(width, height, scalingFactor)) {
+ // Checking for window position changes here seems to fail (on linux at least)
+ // It instead happens on the previous frame
+
+ root.size = Size2D(width, height) / scalingFactor
+ }
// Settings fields should be updated only in non-fullscreen mode
- if (isFullScreen)
+ if (fullScreen)
return
Settings.get.windowSize.set(width, height)
@@ -344,7 +421,7 @@ object UiHandler extends Logging {
}
private def draw(): Unit = {
- graphics.setViewport(root.width.asInstanceOf[Int], root.height.asInstanceOf[Int])
+ graphics.startViewport()
graphics.clear()
root.draw(graphics)
diff --git a/src/main/scala/ocelot/desktop/ui/widget/Button.scala b/src/main/scala/ocelot/desktop/ui/widget/Button.scala
index 350ad7e..ee5dd38 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/Button.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/Button.scala
@@ -11,26 +11,40 @@ import ocelot.desktop.util.DrawUtils
class Button extends Widget with ClickHandler with ClickSoundSource {
def text: String = ""
+
def onClick(): Unit = {}
+ def enabled: Boolean = true
+
override def receiveMouseEvents: Boolean = true
eventHandlers += {
- case ClickEvent(MouseEvent.Button.Left, _) =>
+ case ClickEvent(MouseEvent.Button.Left, _) if enabled =>
onClick()
clickSoundSource.play()
}
override def minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
+
override def maximumSize: Size2D = minimumSize
override def draw(g: Graphics): Unit = {
- g.rect(bounds, ColorScheme("ButtonBackground"))
- DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("ButtonBorder"))
+ val (background, border, foreground) = if (enabled) (
+ ColorScheme("ButtonBackground"),
+ ColorScheme("ButtonBorder"),
+ ColorScheme("ButtonForeground")
+ ) else (
+ ColorScheme("ButtonBackgroundDisabled"),
+ ColorScheme("ButtonBorderDisabled"),
+ ColorScheme("ButtonForegroundDisabled")
+ )
+
+ g.rect(bounds, background)
+ DrawUtils.ring(g, position.x, position.y, width, height, 2, border)
g.background = Color.Transparent
- g.foreground = ColorScheme("ButtonForeground")
+ g.foreground = foreground
val textWidth = text.iterator.map(g.font.charWidth(_)).sum
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, text)
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala
index dbbe63a..d81f630 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/ChangeSimulationSpeedDialog.scala
@@ -91,10 +91,10 @@ class ChangeSimulationSpeedDialog() extends ModalDialog {
override def onClick(): Unit = close()
}, Padding2D(right = 8))
- // TODO: disable the button if tickInterval.isEmpty
children :+= new Button {
override def text: String = "Apply"
override def onClick(): Unit = confirm()
+ override def enabled: Boolean = tickInterval.nonEmpty
}
}
}, Padding2D.equal(16))
diff --git a/src/main/scala/ocelot/desktop/ui/widget/ExitConfirmationDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/CloseConfirmationDialog.scala
similarity index 82%
rename from src/main/scala/ocelot/desktop/ui/widget/ExitConfirmationDialog.scala
rename to src/main/scala/ocelot/desktop/ui/widget/CloseConfirmationDialog.scala
index c53e336..f90952b 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/ExitConfirmationDialog.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/CloseConfirmationDialog.scala
@@ -6,12 +6,14 @@ import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.modal.ModalDialog
import ocelot.desktop.util.Orientation
-class ExitConfirmationDialog extends ModalDialog {
+class CloseConfirmationDialog extends ModalDialog {
+ protected def prompt: String = "Save workspace before exiting?"
+
children :+= new PaddingBox(new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
children :+= new PaddingBox(new Label {
- override def text = "Save workspace before exiting?"
+ override def text: String = prompt
}, Padding2D(bottom = 16))
children :+= new Widget {
@@ -25,7 +27,7 @@ class ExitConfirmationDialog extends ModalDialog {
children :+= new PaddingBox(new Button {
override def text: String = "No"
- override def onClick(): Unit = onExitSelected()
+ override def onClick(): Unit = onNoSaveSelected()
}, Padding2D(right = 8))
children :+= new PaddingBox(new Button {
@@ -37,5 +39,5 @@ class ExitConfirmationDialog extends ModalDialog {
def onSaveSelected(): Unit = {}
- def onExitSelected(): Unit = {}
+ def onNoSaveSelected(): Unit = {}
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala
index 9ed531f..4122ef2 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/IconButton.scala
@@ -7,6 +7,7 @@ import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
+import ocelot.desktop.ui.widget.tooltip.LabelTooltip
import ocelot.desktop.util.animation.{ColorAnimation, ValueAnimation}
import ocelot.desktop.util.{DrawUtils, Spritesheet}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala
index 58bbed5..1dbefc0 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/Knob.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/Knob.scala
@@ -7,7 +7,7 @@ import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.MouseEvent
import totoro.ocelot.brain.util.DyeColor
-abstract class Knob(dyeColor: DyeColor = DyeColor.RED) extends Widget {
+abstract class Knob(dyeColor: DyeColor = DyeColor.Red) extends Widget {
def input: Int
def input_=(v: Int): Unit
diff --git a/src/main/scala/ocelot/desktop/ui/widget/LabelTooltip.scala b/src/main/scala/ocelot/desktop/ui/widget/LabelTooltip.scala
deleted file mode 100644
index c77b193..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/LabelTooltip.scala
+++ /dev/null
@@ -1,28 +0,0 @@
-package ocelot.desktop.ui.widget
-
-import ocelot.desktop.color.Color
-import ocelot.desktop.geometry.Padding2D
-import ocelot.desktop.ui.layout.LinearLayout
-import ocelot.desktop.ui.widget.tooltip.Tooltip
-import ocelot.desktop.util.Orientation
-
-class LabelTooltip(text: String) extends Tooltip {
- override val DelayTime: Float = 1.0f
-
- private val inner: Widget = new Widget {
- override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
- }
-
- for (line <- text.split("\n")) {
- inner.children :+= new Label {
- override def text: String = line
- override def color: Color = Color.White
- }
- }
-
- children :+= new PaddingBox(inner, Padding2D.equal(4))
-
- def onSaveSelected(): Unit = {}
-
- def onExitSelected(): Unit = {}
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala b/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala
index 6ce0774..02f965b 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/MenuBar.scala
@@ -3,8 +3,10 @@ package ocelot.desktop.ui.widget
import ocelot.desktop.audio.SoundSources
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.graphics.Graphics
+import ocelot.desktop.ui.event.KeyEvent
import ocelot.desktop.ui.widget.contextmenu.{ContextMenuEntry, ContextMenuSubmenu}
import ocelot.desktop.{ColorScheme, OcelotDesktop}
+import org.lwjgl.input.Keyboard
class MenuBar extends Widget {
override def receiveMouseEvents: Boolean = true
@@ -16,37 +18,40 @@ class MenuBar extends Widget {
private def addEntry(w: Widget): Unit = entries.children :+= w
addEntry(new MenuBarSubmenu("File", menu => {
- menu.addEntry(new ContextMenuEntry("New", () => OcelotDesktop.newWorkspace()))
- menu.addEntry(new ContextMenuEntry("Open", () => OcelotDesktop.open()))
- menu.addEntry(new ContextMenuEntry("Save", () => OcelotDesktop.save()))
- menu.addEntry(new ContextMenuEntry("Save as…", () => OcelotDesktop.saveAs()))
+ menu.addEntry(ContextMenuEntry("New") { OcelotDesktop.newWorkspace() })
+ menu.addEntry(ContextMenuEntry("Open") { OcelotDesktop.showOpenDialog() })
+ menu.addEntry(ContextMenuEntry("Save") { OcelotDesktop.save() })
+ menu.addEntry(ContextMenuEntry("Save as…") { OcelotDesktop.saveAs() })
menu.addSeparator()
- menu.addEntry(new ContextMenuEntry("Exit", () => OcelotDesktop.exit()))
+ menu.addEntry(ContextMenuEntry("Exit") { OcelotDesktop.exit() })
}))
addEntry(new MenuBarSubmenu("Player", menu => {
- menu.addEntry(new ContextMenuEntry("Add...", () => OcelotDesktop.addPlayerDialog()))
+ menu.addEntry(ContextMenuEntry("Add...") { OcelotDesktop.showAddPlayerDialog() })
menu.addSeparator()
OcelotDesktop.players.foreach(player => {
menu.addEntry(new ContextMenuSubmenu(
(if (player == OcelotDesktop.players.head) "● " else " ") + player.nickname,
() => OcelotDesktop.selectPlayer(player.nickname)
) {
- addEntry(new ContextMenuEntry(
- "Remove",
- () => OcelotDesktop.removePlayer(player.nickname),
- sound = SoundSources.InterfaceClickLow
- ))
+ addEntry(ContextMenuEntry("Remove", sound = SoundSources.InterfaceClickLow) {
+ OcelotDesktop.removePlayer(player.nickname)
+ })
})
})
}))
- addEntry(new MenuBarButton("Settings", () => OcelotDesktop.settings()))
+ addEntry(new MenuBarButton("Settings", () => OcelotDesktop.showSettings()))
addEntry(new Widget {
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
}) // fill remaining space
+ eventHandlers += {
+ case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F5, _) => OcelotDesktop.save()
+ case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_F9, _) => OcelotDesktop.showOpenDialog()
+ }
+
override def draw(g: Graphics): Unit = {
g.rect(bounds, ColorScheme("TitleBarBackground"))
drawChildren(g)
diff --git a/src/main/scala/ocelot/desktop/ui/widget/Oscilloscope.scala b/src/main/scala/ocelot/desktop/ui/widget/Oscilloscope.scala
index 92b8781..49b219c 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/Oscilloscope.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/Oscilloscope.scala
@@ -7,6 +7,7 @@ import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, Cont
import org.jtransforms.fft.FloatFFT_1D
import totoro.ocelot.brain.Settings
+import scala.collection.immutable.ArraySeq
import scala.collection.mutable
class Oscilloscope(isTiny: Boolean = false) extends Widget with ClickHandler {
@@ -101,7 +102,7 @@ class Oscilloscope(isTiny: Boolean = false) extends Widget with ClickHandler {
private def updateMode(f: Int => Int): Unit = {
modeIdx = f(modeIdx)
- children = Array(modes(modeIdx)())
+ children = ArraySeq(modes(modeIdx)())
}
override def minimumSize: Size2D = if (isTiny) Size2D(100, 68) else Size2D(500, 120)
@@ -110,7 +111,7 @@ class Oscilloscope(isTiny: Boolean = false) extends Widget with ClickHandler {
menu.addEntry(new ContextMenuSubmenu("FFT Size") {
for (v <- List(512, 1024, 2048, 4096, 8192, 16384, 32768)) {
val dot = if (v == fftSize) '•' else ' '
- addEntry(new ContextMenuEntry(s"$v".padTo(14, ' ') + dot, () => setFFTSize(v)))
+ addEntry(ContextMenuEntry(s"$v".padTo(14, ' ') + dot) { setFFTSize(v) })
}
})
}
@@ -120,7 +121,7 @@ class Oscilloscope(isTiny: Boolean = false) extends Widget with ClickHandler {
for (v <- List(None, Some(0.1f), Some(0.3f), Some(0.5f), Some(0.7f), Some(0.9f))) {
val dot = if (v == smoothing) '•' else ' '
val msg = if (v.isEmpty) "None" else s"${v.get}"
- addEntry(new ContextMenuEntry(msg.padTo(14, ' ') + dot, () => smoothing = v))
+ addEntry(ContextMenuEntry(msg.padTo(14, ' ') + dot) { smoothing = v })
}
})
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/PaddingBox.scala b/src/main/scala/ocelot/desktop/ui/widget/PaddingBox.scala
index 4367a5e..ec8f633 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/PaddingBox.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/PaddingBox.scala
@@ -3,8 +3,10 @@ package ocelot.desktop.ui.widget
import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.ui.layout.PaddingLayout
+import scala.collection.immutable.ArraySeq
+
class PaddingBox(inner: Widget, padding: Padding2D) extends Widget {
override protected val layout = new PaddingLayout(this, padding)
- children = Array(inner)
+ children = ArraySeq(inner)
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala
index f02bb07..1d6501c 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/RootWidget.scala
@@ -60,7 +60,7 @@ class RootWidget(setupDefaultWorkspace: Boolean = true) extends Widget {
}
case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F11, _) =>
- UiHandler.isFullScreen = !UiHandler.isFullScreen
+ UiHandler.fullScreen = !UiHandler.fullScreen
}
override def draw(g: Graphics): Unit = {
diff --git a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala
index 035fd57..a0da399 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/Slider.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/Slider.scala
@@ -10,8 +10,9 @@ import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent}
import ocelot.desktop.util.DrawUtils
import ocelot.desktop.util.MathUtils.ExtendedFloat
-class Slider(var value: Float, text: String) extends Widget with ClickHandler with DragHandler with ClickSoundSource {
+class Slider(var value: Float, val text: String, val snapPoints: Int = 0) extends Widget with ClickHandler with DragHandler with ClickSoundSource {
def onValueChanged(value: Float): Unit = {}
+ def onValueFinal(value: Float): Unit = {}
override def receiveMouseEvents: Boolean = true
@@ -24,10 +25,26 @@ class Slider(var value: Float, text: String) extends Widget with ClickHandler wi
onValueChanged(value)
}
+
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, pos) =>
calculateValue(pos.x)
clickSoundSource.play()
+ if (snapPoints > 1) {
+ value = (value * (snapPoints - 1)).round / (snapPoints - 1).toFloat
+ }
+ onValueFinal(value)
+
+ case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, pos) =>
+ calculateValue(pos.x)
+ if (lastSoundX == 0 || (pos.x - lastSoundX).abs > soundInterval) {
+ lastSoundX = pos.x
+ clickSoundSource.play()
+ }
+ if (snapPoints > 1) {
+ value = (value * (snapPoints - 1)).round / (snapPoints - 1).toFloat
+ }
+ onValueFinal(value)
case DragEvent(_, MouseEvent.Button.Left, pos) =>
calculateValue(pos.x)
@@ -40,18 +57,23 @@ class Slider(var value: Float, text: String) extends Widget with ClickHandler wi
override def minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)
+ def formatText: String = f"$text: ${value * 100}%.0f%%"
+
override def draw(g: Graphics): Unit = {
g.rect(bounds, ColorScheme("SliderBackground"))
DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("SliderBorder"))
+ for (i <- 1 until snapPoints - 1) {
+ g.rect(position.x + (i / (snapPoints.toFloat - 1)) * (bounds.w - handleWidth / 2), position.y + 6, handleWidth / 2, height - 12, ColorScheme("SliderTick"))
+ }
+
g.rect(position.x + value * (bounds.w - handleWidth), position.y, handleWidth, height, ColorScheme("SliderHandler"))
DrawUtils.ring(g, position.x + value * (bounds.w - handleWidth), position.y, handleWidth, height, 2, ColorScheme("SliderBorder"))
g.background = Color.Transparent
g.foreground = ColorScheme("SliderForeground")
- val fullText = f"$text: ${value * 100}%.0f%%"
- val textWidth = fullText.iterator.map(g.font.charWidth(_)).sum
- g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, fullText)
+ val textWidth = formatText.iterator.map(g.font.charWidth(_)).sum
+ g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, formatText)
}
override protected def clickSoundSource: SoundSource = SoundSources.InterfaceTick
diff --git a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala
index b714afb..2907e88 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/Widget.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/Widget.scala
@@ -6,10 +6,12 @@ import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.Event
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
+import scala.collection.immutable.ArraySeq
+
class Widget {
protected val layout: Layout = new LinearLayout(this)
- protected var _children: Array[Widget] = Array[Widget]()
+ protected var _children: ArraySeq[Widget] = ArraySeq.empty
protected var _parent: Option[Widget] = None
protected var _root: Option[RootWidget] = None
@@ -33,10 +35,10 @@ class Widget {
UiHandler.updateHierarchy()
}
- final def children: Array[Widget] = _children
+ final def children: ArraySeq[Widget] = _children
- final def children_=(value: Array[Widget]): Unit = {
- if (_children sameElements value) return
+ final def children_=(value: ArraySeq[Widget]): Unit = {
+ if (_children == value) return
_children = value
for (child <- _children) {
@@ -114,7 +116,7 @@ class Widget {
Rect2D(position, size).intersect(parentBounds)
}
- def hierarchy: Array[Widget] = children
+ def hierarchy: ArraySeq[Widget] = children
def shouldClip: Boolean = false
diff --git a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala
index 4798f33..8f07609 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala
@@ -15,11 +15,12 @@ import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings}
import org.lwjgl.input.Keyboard
import totoro.ocelot.brain.entity.traits.{Entity, Environment, SidedEnvironment}
import totoro.ocelot.brain.entity.{Case, Screen}
-import totoro.ocelot.brain.event.{EventBus, NodeEvent}
+import totoro.ocelot.brain.event.{EventBus, InventoryEntityAddedEvent, InventoryEvent, NodeEvent}
import totoro.ocelot.brain.nbt.ExtendedNBT._
import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound}
import totoro.ocelot.brain.util.{Direction, Tier}
+import scala.collection.immutable.ArraySeq
import scala.collection.{immutable, mutable}
import scala.jdk.CollectionConverters._
@@ -40,13 +41,17 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
override protected val layout: Layout = new CopyLayout(this)
children +:= windowPool
children +:= new NoLayoutBox {
- override def hierarchy: Array[Widget] = nodes.toArray
+ override def hierarchy: ArraySeq[Widget] = ArraySeq.from(nodes)
}
- private val eventSubscription = EventBus.subscribe { case event: NodeEvent =>
- nodes
- .filter(_.shouldReceiveEventsFor(event.address))
- .foreach(_.handleEvent(BrainEvent(event)))
+ private val eventSubscription = EventBus.subscribe {
+ case event: NodeEvent =>
+ nodes
+ .filter(_.shouldReceiveEventsFor(event.address))
+ .foreach(_.handleEvent(BrainEvent(event)))
+
+ case event: InventoryEvent =>
+ nodes.foreach(_.handleEvent(BrainEvent(event)))
}
def reset(): Unit = {
@@ -73,7 +78,9 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
val address = nbt.getString("address")
var entity = OcelotDesktop.workspace.entityByAddress(address).orNull
+
if (entity == null) {
+ // FIXME: 1. won't work for many components 2. should not be done this way at all 3. needs logging
val entityClass = Class.forName(nbt.getString("entityClass"))
entity = entityClass.getConstructor().newInstance().asInstanceOf[Entity]
}
@@ -177,7 +184,7 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
def createDefaultWorkspace(): Unit = {
addNode(new ComputerNode(new Case(Tier.Three)).setup(), Vector2D(68, 68))
addNode(new ScreenNode(new Screen(Tier.Two)).setup(), Vector2D(204, 136))
- nodes(0).connect(NodePort(), nodes(1), NodePort())
+ nodes.head.connect(NodePort(), nodes(1), NodePort())
}
override def receiveMouseEvents = true
@@ -449,11 +456,11 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
val numRepeatsX = math.ceil((size.width + backgroundOffsetX) / 304f).asInstanceOf[Int]
val numRepeatsY = math.ceil((size.height + backgroundOffsetY) / 304f).asInstanceOf[Int]
- g.translate(-backgroundOffsetX, -backgroundOffsetY)
+ g.translate(-backgroundOffsetX.toFloat, -backgroundOffsetY.toFloat)
for (x <- 0 to numRepeatsX) {
for (y <- 0 to numRepeatsY) {
- g.sprite("BackgroundPattern", x * 304, y * 304, 304, 304)
+ g.sprite("BackgroundPattern", x.toFloat * 304, y.toFloat * 304, 304, 304)
}
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/card/Redstone1Window.scala b/src/main/scala/ocelot/desktop/ui/widget/card/Redstone1Window.scala
index c1c7fd2..c426730 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/card/Redstone1Window.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/card/Redstone1Window.scala
@@ -7,22 +7,22 @@ import ocelot.desktop.ui.widget.window.BasicWindow
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
import ocelot.desktop.util.{DrawUtils, Orientation}
import totoro.ocelot.brain.entity.Redstone
-import totoro.ocelot.brain.util.DyeColor
+import totoro.ocelot.brain.util.{Direction, DyeColor}
class Redstone1Window(card: Redstone.Tier1) extends BasicWindow {
- private def redstoneKnob(side: Int) = new Knob(DyeColor.RED) {
+ private def redstoneKnob(side: Direction.Value) = new Knob(DyeColor.Red) {
override def input: Int = {
- card.redstoneInput(side) min 15 max 0
+ card.redstoneInput(side.id) min 15 max 0
}
override def input_=(v: Int): Unit = {
- card.redstoneInput(side) = v
+ card.setRedstoneInput(side, v)
}
- override def output: Int = card.redstoneOutput(side) min 15 max 0
+ override def output: Int = card.redstoneOutput(side.id) min 15 max 0
}
- private def redstoneBlock(side: Int, name: String) = new PaddingBox(new Widget {
+ private def redstoneBlock(side: Direction.Value, name: String) = new PaddingBox(new Widget {
override protected val layout: Layout = new LinearLayout(this, Orientation.Horizontal, contentAlignment = Alignment.Center)
children :+= new PaddingBox(new Label {
@@ -44,18 +44,18 @@ class Redstone1Window(card: Redstone.Tier1) extends BasicWindow {
children :+= new Widget {
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= redstoneBlock(0, "Bottom")
- children :+= redstoneBlock(1, " Top")
+ children :+= redstoneBlock(Direction.Down, "Bottom")
+ children :+= redstoneBlock(Direction.Up, " Top")
}
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= redstoneBlock(2, " Back")
- children :+= redstoneBlock(3, "Front")
+ children :+= redstoneBlock(Direction.Back, " Back")
+ children :+= redstoneBlock(Direction.Front, "Front")
}
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= redstoneBlock(4, "Right")
- children :+= redstoneBlock(5, " Left")
+ children :+= redstoneBlock(Direction.Right, "Right")
+ children :+= redstoneBlock(Direction.Left, " Left")
}
}
}, Padding2D.equal(8))
diff --git a/src/main/scala/ocelot/desktop/ui/widget/card/Redstone2Window.scala b/src/main/scala/ocelot/desktop/ui/widget/card/Redstone2Window.scala
index 694d714..6fd7bab 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/card/Redstone2Window.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/card/Redstone2Window.scala
@@ -7,22 +7,22 @@ import ocelot.desktop.ui.widget.window.BasicWindow
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
import ocelot.desktop.util.{DrawUtils, Orientation}
import totoro.ocelot.brain.entity.Redstone
-import totoro.ocelot.brain.util.DyeColor
+import totoro.ocelot.brain.util.{Direction, DyeColor}
class Redstone2Window(card: Redstone.Tier2) extends BasicWindow {
- private def bundledKnob(side: Int, col: Int) = new Knob(DyeColor.byCode(col)) {
+ private def bundledKnob(side: Direction.Value, col: Int) = new Knob(DyeColor.byCode(col)) {
override def input: Int = {
- card.bundledRedstoneInput(side)(col) min 15 max 0
+ card.bundledRedstoneInput(side.id)(col) min 15 max 0
}
override def input_=(v: Int): Unit = {
- card.bundledRedstoneInput(side)(col) = v
+ card.setBundledInput(side, col, v)
}
- override def output: Int = card.bundledRedstoneOutput(side)(col) min 15 max 0
+ override def output: Int = card.bundledRedstoneOutput(side.id)(col) min 15 max 0
}
- private def bundledBlock(side: Int, name: String) = new PaddingBox(new Widget {
+ private def bundledBlock(side: Direction.Value, name: String) = new PaddingBox(new Widget {
override protected val layout: Layout = new LinearLayout(this) {
orientation = Orientation.Vertical
contentAlignment = Alignment.Center
@@ -69,18 +69,18 @@ class Redstone2Window(card: Redstone.Tier2) extends BasicWindow {
children :+= new Widget {
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= bundledBlock(0, "Bottom")
- children :+= bundledBlock(1, "Top")
+ children :+= bundledBlock(Direction.Down, "Bottom")
+ children :+= bundledBlock(Direction.Up, "Top")
}
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= bundledBlock(2, "Back")
- children :+= bundledBlock(3, "Front")
+ children :+= bundledBlock(Direction.Back, "Back")
+ children :+= bundledBlock(Direction.Front, "Front")
}
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- children :+= bundledBlock(4, "Right")
- children :+= bundledBlock(5, "Left")
+ children :+= bundledBlock(Direction.Right, "Right")
+ children :+= bundledBlock(Direction.Left, "Left")
}
}
}, Padding2D.equal(8))
diff --git a/src/main/scala/ocelot/desktop/ui/widget/component/ComponentSelectors.scala b/src/main/scala/ocelot/desktop/ui/widget/component/ComponentSelectors.scala
index cf2b6ca..189030a 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/component/ComponentSelectors.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/component/ComponentSelectors.scala
@@ -7,10 +7,12 @@ import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.Widget
import org.lwjgl.input.Keyboard
+import scala.collection.immutable.ArraySeq
+
class ComponentSelectors extends Widget {
override protected val layout: Layout = new Layout(this)
- private def selectors: Array[ComponentSelector] = children.map(_.asInstanceOf[ComponentSelector])
+ private def selectors: ArraySeq[ComponentSelector] = children.map(_.asInstanceOf[ComponentSelector])
override def receiveAllMouseEvents: Boolean = true
diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala
index 5876f8f..3dd2efb 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenu.scala
@@ -9,6 +9,8 @@ import ocelot.desktop.util.animation.ValueAnimation
import ocelot.desktop.util.animation.easing.EaseInOutQuad
import ocelot.desktop.util.{DrawUtils, Orientation}
+import scala.collection.immutable.ArraySeq
+
class ContextMenu extends Widget {
private[contextmenu] var isClosing = false
private[contextmenu] var isOpening = false
@@ -38,7 +40,7 @@ class ContextMenu extends Widget {
inner.children :+= new Separator
}
- private[contextmenu] def entries: Array[ContextMenuEntry] = inner.children
+ private[contextmenu] def entries: ArraySeq[ContextMenuEntry] = inner.children
.filter(_.isInstanceOf[ContextMenuEntry])
.map(_.asInstanceOf[ContextMenuEntry])
diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala
index af59adf..8a5c51a 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenuEntry.scala
@@ -12,13 +12,17 @@ import ocelot.desktop.ui.widget._
import ocelot.desktop.util.animation.ValueAnimation
import ocelot.desktop.util.animation.easing.{EaseInQuad, EaseOutQuad}
-class ContextMenuEntry(label: String,
- onClick: () => Unit = () => {},
- icon: Option[IconDef] = None,
- sound: SoundSource = SoundSources.InterfaceClick,
- soundDisabled: SoundSource = SoundSources.InterfaceClickLow)
- extends Widget with ClickHandler with HoverHandler with ClickSoundSource
-{
+class ContextMenuEntry(
+ label: String,
+ onClick: () => Unit = () => {},
+ icon: Option[IconDef] = None,
+ sound: SoundSource = SoundSources.InterfaceClick,
+ soundDisabled: SoundSource = SoundSources.InterfaceClickLow
+) extends Widget
+ with ClickHandler
+ with HoverHandler
+ with ClickSoundSource {
+
private[contextmenu] val alpha = new ValueAnimation(0f, 10f)
private[contextmenu] val textAlpha = new ValueAnimation(0f, 5f)
private[contextmenu] val trans = new ValueAnimation(0f, 20f)
@@ -32,22 +36,27 @@ class ContextMenuEntry(label: String,
case _ => 12f
}
- children :+= new PaddingBox(new Widget {
- override val layout: Layout = new LinearLayout(this) {
- contentAlignment = Alignment.Center
- }
+ children :+= new PaddingBox(
+ new Widget {
+ override val layout: Layout = new LinearLayout(this) {
+ contentAlignment = Alignment.Center
+ }
- icon match {
- case Some(icon) =>
- children :+= new PaddingBox(new Icon(icon), Padding2D(left = 8f, right = 6f))
- case _ =>
- }
+ icon match {
+ case Some(icon) =>
+ children :+= new PaddingBox(new Icon(icon), Padding2D(left = 8f, right = 6f))
+ case _ =>
+ }
- children :+= new PaddingBox(new Label {
- override def text: String = label
- override def color: Color = ColorScheme("ContextMenuText")
- }, Padding2D(top = 3f, bottom = 3f))
- }, Padding2D(left = padLeft, right = 16f, top = 2f, bottom = 2f))
+ children :+= new PaddingBox(
+ new Label {
+ override def text: String = label
+
+ override def color: Color = ColorScheme("ContextMenuText")
+ }, Padding2D(top = 3f, bottom = 3f)
+ )
+ }, Padding2D(left = padLeft, right = 16f, top = 2f, bottom = 2f)
+ )
override def receiveMouseEvents: Boolean = !isGhost
@@ -113,3 +122,19 @@ class ContextMenuEntry(label: String,
override protected def clickSoundSource: SoundSource =
if (isEnabled) sound else soundDisabled
}
+
+object ContextMenuEntry {
+ def apply(
+ label: String,
+ icon: Option[IconDef] = None,
+ sound: SoundSource = SoundSources.InterfaceClick,
+ soundDisabled: SoundSource = SoundSources.InterfaceClickLow
+ )(onClick: => Unit): ContextMenuEntry =
+ new ContextMenuEntry(
+ label,
+ onClick = onClick _,
+ icon = icon,
+ sound = sound,
+ soundDisabled = soundDisabled
+ )
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala
index 3c345ae..214723d 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/contextmenu/ContextMenus.scala
@@ -8,12 +8,14 @@ import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.Widget
import org.lwjgl.input.Keyboard
+import scala.collection.immutable.ArraySeq
+
class ContextMenus extends Widget {
override protected val layout: Layout = new Layout(this)
private var ghost: Option[ContextMenuEntry] = None
- private def menus: Array[ContextMenu] = children.map(_.asInstanceOf[ContextMenu])
+ private def menus: ArraySeq[ContextMenu] = children.map(_.asInstanceOf[ContextMenu])
private[contextmenu] def setGhost(g: ContextMenuEntry): Unit = {
ghost = Some(g)
diff --git a/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala b/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala
index 562c46b..6dde9c4 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/modal/ModalDialogPool.scala
@@ -9,6 +9,8 @@ import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.Widget
import ocelot.desktop.util.animation.UnitAnimation
+import scala.collection.immutable.ArraySeq
+
class ModalDialogPool extends Widget with ClickHandler {
override protected val layout: Layout = new Layout(this) {
override def relayout(): Unit = {
@@ -22,7 +24,7 @@ class ModalDialogPool extends Widget with ClickHandler {
private val curtainsAlphaAnimation = UnitAnimation.easeInOutQuad(0.13f)
- private def dialogs: Array[ModalDialog] = children.map(_.asInstanceOf[ModalDialog])
+ private def dialogs: ArraySeq[ModalDialog] = children.map(_.asInstanceOf[ModalDialog])
def isVisible: Boolean = dialogs.nonEmpty
@@ -60,7 +62,7 @@ class ModalDialogPool extends Widget with ClickHandler {
else
curtainsAlphaAnimation.goUp()
if (dialogs.head.isClosed)
- children = Array()
+ children = ArraySeq.empty
} else if (dialogs.head.isClosed) {
children = children.take(children.length - 1)
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala b/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala
index 6f7db03..cb79f19 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/modal/notification/NotificationDialog.scala
@@ -3,7 +3,7 @@ package ocelot.desktop.ui.widget.modal.notification
import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Padding2D
-import ocelot.desktop.graphics.{Graphics, IconDef}
+import ocelot.desktop.graphics.{Graphics, IconDef, Icons}
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget._
import ocelot.desktop.ui.widget.modal.ModalDialog
@@ -32,7 +32,7 @@ class NotificationDialog(message: String, notificationType: NotificationType = N
// Icon
children :+= new PaddingBox(
- new Icon(new IconDef(s"icons/Notification${notificationType.toString}", 4)),
+ new Icon(Icons.NotificationIcon(notificationType)),
Padding2D.equal(10)
)
diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsTab.scala
index 8c36f98..a077ba7 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsTab.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SettingsTab.scala
@@ -1,5 +1,6 @@
package ocelot.desktop.ui.widget.settings
+import ocelot.desktop.graphics.IconDef
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.Widget
import ocelot.desktop.util.Orientation
@@ -7,7 +8,7 @@ import ocelot.desktop.util.Orientation
trait SettingsTab extends Widget {
override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical)
- val icon: String
+ val icon: IconDef
val label: String
/**
diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala
index f932a09..203c35b 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala
@@ -2,10 +2,11 @@ package ocelot.desktop.ui.widget.settings
import ocelot.desktop.Settings
import ocelot.desktop.geometry.{Padding2D, Size2D}
+import ocelot.desktop.graphics.{IconDef, Icons}
import ocelot.desktop.ui.widget.{PaddingBox, Slider}
class SoundSettingsTab extends SettingsTab {
- override val icon: String = "icons/SettingsSound"
+ override val icon: IconDef = Icons.SettingsSound
override val label: String = "Sound"
children :+= new PaddingBox(new Slider(Settings.get.volumeMaster, "Master Volume") {
diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala
index ce84078..43727ec 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/settings/UISettingsTab.scala
@@ -2,16 +2,22 @@ package ocelot.desktop.ui.widget.settings
import ocelot.desktop.Settings
import ocelot.desktop.geometry.{Padding2D, Size2D}
+import ocelot.desktop.graphics.{IconDef, Icons}
import ocelot.desktop.ui.UiHandler
-import ocelot.desktop.ui.widget.{Checkbox, PaddingBox}
+import ocelot.desktop.ui.widget.{Checkbox, PaddingBox, Slider}
class UISettingsTab extends SettingsTab {
- override val icon: String = "icons/SettingsUI"
+ override val icon: IconDef = Icons.SettingsUI
override val label: String = "UI"
override def applySettings(): Unit = {
- if (UiHandler.isFullScreen != Settings.get.windowFullscreen)
- UiHandler.isFullScreen = Settings.get.windowFullscreen
+ if (UiHandler.fullScreen != Settings.get.windowFullscreen) {
+ UiHandler.fullScreen = Settings.get.windowFullscreen
+ }
+
+ if (UiHandler.scalingFactor != Settings.get.scaleFactor) {
+ UiHandler.scalingFactor = Settings.get.scaleFactor
+ }
}
children :+= new PaddingBox(new Checkbox("Move workspace windows together with workspace",
@@ -50,4 +56,18 @@ class UISettingsTab extends SettingsTab {
Settings.get.openLastWorkspace = value
}
}, Padding2D(bottom = 8))
+
+ children :+= new PaddingBox(new Slider((Settings.get.scaleFactor - 1) / 2, "Interface scale", 5) {
+ override def minimumSize: Size2D = Size2D(512, 24)
+
+ // Interpolates [0; 1] as [1; 3] rounded to nearest 0.5
+ private def convertToScale(slider: Float): Float = (slider * 4 + 2).round.max(2).min(6) / 2.0f
+
+ override def formatText: String = f"$text: ${convertToScale(value)}%.1fx"
+
+ override def onValueFinal(value: Float): Unit = {
+ Settings.get.scaleFactor = convertToScale(value)
+ applySettings()
+ }
+ }, Padding2D(bottom = 8))
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CPUSlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CPUSlot.scala
deleted file mode 100644
index e7bb27c..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/CPUSlot.scala
+++ /dev/null
@@ -1,88 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.node.nodes.ComputerNode
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
-import totoro.ocelot.brain.entity.machine.MachineAPI
-import totoro.ocelot.brain.entity.traits.{Entity, GenericCPU, Inventory}
-import totoro.ocelot.brain.entity.{APU, CPU}
-import totoro.ocelot.brain.util.Tier
-
-class CPUSlot(owner: Inventory#Slot, node: ComputerNode, val tier: Int) extends InventorySlot[GenericCPU with Entity](owner) {
- private val APUAnimation = Some(Array(
- (0, 3f), (1, 3f), (2, 3f), (3, 3f), (4, 3f), (5, 3f),
- (4, 3f), (3, 3f), (2, 3f), (1, 3f), (0, 3f)))
-
- override def itemIcon: Option[IconDef] = item match {
- case Some(cpu: CPU) => Some(new IconDef("items/CPU" + cpu.tier))
- case Some(apu: APU) => Some(new IconDef("items/APU" + apu.tier, animation = APUAnimation))
- case _ => None
- }
-
- override def icon: IconDef = new IconDef("icons/CPU")
- override def tierIcon: Option[IconDef] = Some(new IconDef("icons/Tier" + tier))
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- menu.addEntry(new ContextMenuEntry("CPU (Tier 1)",
- () => item = new CPU(Tier.One),
- icon = Some(new IconDef("items/CPU0"))))
-
- if (tier >= Tier.Two) {
- menu.addEntry(new ContextMenuEntry("CPU (Tier 2)",
- () => item = new CPU(Tier.Two),
- icon = Some(new IconDef("items/CPU1"))))
- }
-
- if (tier >= Tier.Three) {
- menu.addEntry(new ContextMenuEntry("CPU (Tier 3)",
- () => item = new CPU(Tier.Three),
- icon = Some(new IconDef("items/CPU2"))))
- }
-
- if (tier < Tier.Two) return
-
- menu.addEntry(new ContextMenuSubmenu("APU (CPU + GPU)", icon = Some(new IconDef("items/GraphicsCard1"))) {
- addEntry(new ContextMenuEntry("Tier 2",
- () => item = new APU(Tier.One),
- icon = Some(new IconDef("items/APU0", animation = APUAnimation))))
-
- if (tier >= Tier.Three) {
- addEntry(new ContextMenuEntry("Tier 3",
- () => item = new APU(Tier.Two),
- icon = Some(new IconDef("items/APU1", animation = APUAnimation))))
-
- addEntry(new ContextMenuEntry("Creative",
- () => item = new APU(Tier.Three),
- icon = Some(new IconDef("items/APU2", animation = APUAnimation))))
- }
- })
- }
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- if (item.isEmpty) return
-
- val cpu = item.get
-
- menu.addEntry(new ContextMenuSubmenu("Set architecture") {
- for (arch <- cpu.allArchitectures) {
- val name = MachineAPI.getArchitectureName(arch) +
- (if (arch == cpu.architecture) " (current)" else "")
-
- addEntry(new ContextMenuEntry(name, () => {
- val machine = node.computer.machine
- if (machine.isRunning) {
- machine.stop()
- cpu.setArchitecture(arch)
- machine.start()
- } else {
- cpu.setArchitecture(arch)
- }
- }))
- }
- })
-
- super.fillRmbMenu(menu)
- }
-
- override def lmbMenuEnabled: Boolean = true
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala
deleted file mode 100644
index 544b3d7..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/CardRegistry.scala
+++ /dev/null
@@ -1,81 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.graphics.IconDef
-import totoro.ocelot.brain.entity.sound_card.SoundCard
-import totoro.ocelot.brain.entity.traits.{Entity, Tiered}
-import totoro.ocelot.brain.entity.{DataCard, GraphicsCard, InternetCard, LinkedCard, NetworkCard, Redstone, WirelessNetworkCard}
-
-import scala.collection.mutable
-import scala.reflect.{ClassTag, classTag}
-
-object CardRegistry {
- case class Entry(name: String, tier: Int, icon: IconDef, factory: () => Entity)
-
- val iconByClass: mutable.HashMap[String, IconDef] = mutable.HashMap()
- val tierByClass: mutable.HashMap[String, Int] = mutable.HashMap()
- val iconByClassAndTier: mutable.HashMap[(String, Int), IconDef] = mutable.HashMap()
- val entries: mutable.ArrayBuffer[Entry] = mutable.ArrayBuffer()
-
- def addEntry[T <: Entity : ClassTag](name: String, tier: Int, icon: IconDef, factory: () => T): Unit = {
- val entry = Entry(name, tier, icon, factory)
- entries += entry
- val clazz = classTag[T].runtimeClass.getName
- iconByClass.addOne((clazz, icon))
- tierByClass.addOne((clazz, tier))
- iconByClassAndTier.addOne(((clazz, entry.tier), icon))
- }
-
- def getIcon(entity: Entity): Option[IconDef] = {
- val clazz = entity.getClass.getName
- entity match {
- case t: Tiered => iconByClassAndTier.get((clazz, t.tier))
- case _ => iconByClass.get(clazz)
- }
- }
-
- def getTier(entity: Entity): Int = {
- val clazz = entity.getClass.getName
- entity match {
- case t: Tiered => t.tier
- case _ => tierByClass.getOrElse(clazz, 0)
- }
- }
-
- addEntry("Graphics Card", 0, new IconDef("items/GraphicsCard0"), () => new GraphicsCard(0))
- addEntry("Graphics Card", 1, new IconDef("items/GraphicsCard1"), () => new GraphicsCard(1))
- addEntry("Graphics Card", 2, new IconDef("items/GraphicsCard2"), () => new GraphicsCard(2))
-
- addEntry("Network Card", 0, new IconDef("items/NetworkCard"),
- () => new NetworkCard)
-
- addEntry("Wireless Net. Card", 0, new IconDef("items/WirelessNetworkCard0"),
- () => new WirelessNetworkCard.Tier1)
- addEntry("Wireless Net. Card", 1, new IconDef("items/WirelessNetworkCard1"),
- () => new WirelessNetworkCard.Tier2)
-
- private val LinkedCardAnimation = Some(Array((0, 3f), (1, 3f), (2, 3f), (3, 3f), (4, 3f), (5, 3f)))
-
- addEntry("Linked Card", 2, new IconDef("items/LinkedCard", animation = LinkedCardAnimation),
- () => new LinkedCard)
-
- private val InternetCardAnimation = Some(Array((0, 2f), (1, 7f), (0, 5f), (1, 4f), (0, 7f), (1, 2f), (0, 8f), (1, 9f), (0, 6f), (1, 4f)))
-
- addEntry("Internet Card", 1, new IconDef("items/InternetCard", animation = InternetCardAnimation),
- () => new InternetCard)
-
- addEntry("Redstone Card", 0, new IconDef("items/RedstoneCard0"), () => new Redstone.Tier1)
- addEntry("Redstone Card", 1, new IconDef("items/RedstoneCard1"), () => new Redstone.Tier2)
-
- private val DataCardAnimation = Some(Array((0, 4f), (1, 4f), (2, 4f), (3, 4f), (4, 4f), (5, 4f), (6, 4f), (7, 4f)))
-
- addEntry("Data Card", 0, new IconDef("items/DataCard0", animation = DataCardAnimation),
- () => new DataCard.Tier1)
- addEntry("Data Card", 1, new IconDef("items/DataCard1", animation = DataCardAnimation),
- () => new DataCard.Tier2)
- addEntry("Data Card", 2, new IconDef("items/DataCard2", animation = DataCardAnimation),
- () => new DataCard.Tier3)
-
- addEntry("Sound Card", 1, new IconDef("items/SoundCard", animation = DataCardAnimation),
- () => new SoundCard)
-}
-
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlot.scala
deleted file mode 100644
index aa5520f..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlot.scala
+++ /dev/null
@@ -1,85 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.color.Color
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.ui.UiHandler
-import ocelot.desktop.ui.widget.{Label, TunnelDialog, Widget}
-import ocelot.desktop.ui.widget.card.{Redstone1Window, Redstone2Window, SoundCardWindow}
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
-import totoro.ocelot.brain.entity.sound_card.SoundCard
-import totoro.ocelot.brain.entity.{LinkedCard, Redstone}
-import totoro.ocelot.brain.entity.traits.{Entity, Inventory}
-import totoro.ocelot.brain.util.Tier
-
-class CardSlot(owner: Inventory#Slot, val tier: Int) extends InventorySlot[Entity](owner) {
- override def itemIcon: Option[IconDef] = item.flatMap(it => CardRegistry.getIcon(it))
-
- override def icon: IconDef = new IconDef("icons/Card")
- override def tierIcon: Option[IconDef] = Some(new IconDef("icons/Tier" + tier))
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- val pool = UiHandler.root.workspaceView.windowPool
- item match {
- case Some(card: Redstone.Tier2) =>
- menu.addEntry(new ContextMenuEntry("Redstone I/O", () => pool.openWindow(new Redstone1Window(card))))
- menu.addEntry(new ContextMenuEntry("Bundled I/O", () => pool.openWindow(new Redstone2Window(card))))
- case Some(card: Redstone.Tier1) =>
- menu.addEntry(new ContextMenuEntry("Redstone I/O", () => pool.openWindow(new Redstone1Window(card))))
- case Some(card: LinkedCard) =>
- menu.addEntry(new ContextMenuEntry("Set channel", () => new TunnelDialog(
- tunnel => card.tunnel = tunnel,
- card.tunnel
- ).show()))
- case Some(card: SoundCard) =>
- menu.addEntry(new ContextMenuEntry("Open", () => pool.openWindow(new SoundCardWindow(card))))
- case _ =>
- }
- super.fillRmbMenu(menu)
- }
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- val entries = CardRegistry.entries
-
- var i = 0
- while (i < entries.length) {
- val entry = entries(i)
- val nextEntry = entries.lift(i + 1)
-
- if (nextEntry.isDefined && nextEntry.get.name == entry.name) {
- val groupName = entry.name
- if (tier >= entry.tier) {
- menu.addEntry(new ContextMenuSubmenu(groupName, icon = Some(nextEntry.get.icon)) {
- entries.view.slice(i, entries.length).takeWhile(_.name == groupName).foreach(entry => {
- val name = if (entry.tier == Tier.Four) "Creative" else "Tier " + (entry.tier + 1)
- if (tier >= entry.tier) {
- addEntry(new ContextMenuEntry(name, () => item = entry.factory(), Some(entry.icon)))
- }
- i += 1
- })
- })
- }
- } else {
- if (tier >= entry.tier) {
- menu.addEntry(new ContextMenuEntry(entry.name, () => item = entry.factory(), Some(entry.icon)))
- }
-
- i += 1
- }
- }
- }
-
- override def lmbMenuEnabled: Boolean = true
-
- override def tooltipChildrenAdder(inner: Widget): Unit = {
- super.tooltipChildrenAdder(inner)
-
- item match {
- case Some(card: LinkedCard) =>
- inner.children :+= new Label {
- override def text: String = s"Channel: ${card.tunnel}"
- override def color: Color = Color.Grey
- }
- case _ =>
- }
- }
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala
new file mode 100644
index 0000000..a91fa31
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/CardSlotWidget.scala
@@ -0,0 +1,11 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.Inventory
+import ocelot.desktop.inventory.traits.CardItem
+import totoro.ocelot.brain.util.Tier.Tier
+
+class CardSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[CardItem](slot) {
+ override def ghostIcon: Option[IconDef] = Some(Icons.CardIcon)
+ override def slotTier: Option[Tier] = Some(_tier)
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala
new file mode 100644
index 0000000..7603630
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/CpuSlotWidget.scala
@@ -0,0 +1,25 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.{Inventory, Item}
+import ocelot.desktop.inventory.traits.CpuLikeItem
+import ocelot.desktop.node.nodes.ComputerNode
+import totoro.ocelot.brain.util.Tier.Tier
+
+class CpuSlotWidget(slot: Inventory#Slot, node: ComputerNode, _tier: Tier) extends SlotWidget[CpuLikeItem](slot) {
+ override def ghostIcon: Option[IconDef] = Some(Icons.CpuIcon)
+ override def slotTier: Option[Tier] = Some(_tier)
+
+ protected override def onItemNotification(notification: Item.Notification): Unit = {
+ super.onItemNotification(notification)
+
+ notification match {
+ case CpuLikeItem.CpuArchitectureChangedNotification =>
+ val machine = node.computer.machine
+ machine.stop()
+ machine.start()
+
+ case _ =>
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/DiskOrFloppySlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/DiskOrFloppySlot.scala
deleted file mode 100644
index f785546..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/DiskOrFloppySlot.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.OcelotDesktop
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
-import totoro.ocelot.brain.entity.traits.{DiskManaged, Entity}
-
-import javax.swing.JFileChooser
-import scala.util.Try
-
-trait DiskOrFloppySlot {
- def addSetDirectoryEntryToRmbMenu(menu: ContextMenu, slot: InventorySlot[DiskManaged with Entity]): Unit = {
- menu.addEntry(new ContextMenuEntry(
- "Set directory",
- () => OcelotDesktop.showFileChooserDialog(
- dir => Try {
- if (dir.isDefined && slot.item.isDefined) {
- val item = slot.item
- // first - unload the item with old filesystem (triggers 'component_removed' signal)
- slot.item = None
- // then - trigger filesystem rebuild with a new path
- item.get.customRealPath = Some(dir.get.toPath.toAbsolutePath)
- // finally - add the item back (triggers 'component_added' signal)
- slot.item = item
- }
- },
- JFileChooser.OPEN_DIALOG,
- JFileChooser.DIRECTORIES_ONLY
- )
- ))
- }
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/DiskSlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/DiskSlot.scala
deleted file mode 100644
index 6a3d1a2..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/DiskSlot.scala
+++ /dev/null
@@ -1,43 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
-import totoro.ocelot.brain.entity.HDDManaged
-import totoro.ocelot.brain.entity.traits.{DiskManaged, Entity, Inventory}
-import totoro.ocelot.brain.util.Tier
-
-class DiskSlot(owner: Inventory#Slot, val tier: Int) extends InventorySlot[HDDManaged](owner) with DiskOrFloppySlot {
- override def itemIcon: Option[IconDef] = item.map(disk => new IconDef("items/HardDiskDrive" + disk.tier))
-
- override def icon: IconDef = new IconDef("icons/HDD")
- override def tierIcon: Option[IconDef] = Some(new IconDef("icons/Tier" + tier))
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- menu.addEntry(new ContextMenuEntry("HDD (Tier 1)",
- () => item = new HDDManaged(Tier.One),
- icon = Some(new IconDef("items/HardDiskDrive0"))))
-
- if (tier >= Tier.Two) {
- menu.addEntry(new ContextMenuEntry("HDD (Tier 2)",
- () => item = new HDDManaged(Tier.Two),
- icon = Some(new IconDef("items/HardDiskDrive1"))))
- }
-
- if (tier >= Tier.Three) {
- menu.addEntry(new ContextMenuEntry("HDD (Tier 3)",
- () => item = new HDDManaged(Tier.Three),
- icon = Some(new IconDef("items/HardDiskDrive2"))))
- }
- }
-
- override def lmbMenuEnabled: Boolean = true
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- addSetDirectoryEntryToRmbMenu(menu, this.asInstanceOf[InventorySlot[DiskManaged with Entity]])
-
- super.fillRmbMenu(menu)
- }
-
- override def tooltipLine1Text: String = getTooltipDeviceInfoText(item.get.fileSystem, item.get.tier)
-
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/EEPROMSlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/EEPROMSlot.scala
deleted file mode 100644
index 5a749d5..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/EEPROMSlot.scala
+++ /dev/null
@@ -1,96 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.OcelotDesktop.showFileChooserDialog
-import ocelot.desktop.color.Color
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
-import ocelot.desktop.ui.widget.{InputDialog, Label, Widget}
-import totoro.ocelot.brain.entity.EEPROM
-import totoro.ocelot.brain.entity.traits.Inventory
-import totoro.ocelot.brain.loot.Loot
-
-import java.net.{MalformedURLException, URL}
-import javax.swing.JFileChooser
-import scala.util.Try
-
-class EEPROMSlot(owner: Inventory#Slot) extends InventorySlot[EEPROM](owner) {
- override def itemIcon: Option[IconDef] = item.map(_ => new IconDef("items/EEPROM"))
- override def icon: IconDef = new IconDef("icons/EEPROM")
-
- override def lmbMenuEnabled: Boolean = true
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- menu.addEntry(new ContextMenuEntry("Lua BIOS",
- () => item = Loot.LuaBiosEEPROM.create(),
- icon = Some(new IconDef("items/EEPROM"))
- ))
-
- menu.addEntry(new ContextMenuEntry("AdvLoader",
- () => item = Loot.AdvLoaderEEPROM.create(),
- icon = Some(new IconDef("items/EEPROM"))
- ))
-
- menu.addEntry(new ContextMenuEntry("Empty",
- () => item = new EEPROM,
- icon = Some(new IconDef("items/EEPROM"))
- ))
- }
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- val dataSourceMenu = new ContextMenuSubmenu("External data source")
-
- dataSourceMenu.addEntry(new ContextMenuEntry("Local file", () => showFileChooserDialog(
- file => Try {
- if (file.isDefined)
- item.get.codePath = Some(file.get.toPath)
- },
- JFileChooser.OPEN_DIALOG,
- JFileChooser.FILES_ONLY
- )))
-
- dataSourceMenu.addEntry(new ContextMenuEntry("File via URL", () =>
- new InputDialog(
- title = "File via URL",
- text => item.get.codeURL = Some(new URL(text)),
- "",
- text => {
- try {
- new URL(text)
- true
- }
- catch {
- case _: MalformedURLException => false
- }
- }
- ).show()
- ))
-
- if (item.get.codePath.isDefined || item.get.codeURL.isDefined)
- dataSourceMenu.addEntry(new ContextMenuEntry("Detach", () => item.get.codeBytes = Some(Array.empty[Byte])))
-
- menu.addEntry(dataSourceMenu)
-
- super.fillRmbMenu(menu)
- }
-
- override def tooltipLine1Text: String = getTooltipSuffixedText(getTooltipDeviceInfoText(item.get), item.get.label)
-
- override def tooltipChildrenAdder(inner: Widget): Unit = {
- super.tooltipChildrenAdder(inner)
-
- if (item.get.codePath.isEmpty && item.get.codeURL.isEmpty)
- return
-
- inner.children :+= new Label {
- override def text: String = {
- if (item.isDefined) {
- if (item.get.codePath.isDefined) s"Source path: ${item.get.codePath.get.toString}"
- else if (item.get.codeURL.isDefined) s"Source URL: ${item.get.codeURL.get.toString}"
- else ""
- } else ""
- }
-
- override def color: Color = Color.Grey
- }
- }
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala
new file mode 100644
index 0000000..1395088
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/EepromSlotWidget.scala
@@ -0,0 +1,9 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.Inventory
+import ocelot.desktop.inventory.item.EepromItem
+
+class EepromSlotWidget(slot: Inventory#Slot) extends SlotWidget[EepromItem](slot) {
+ override def ghostIcon: Option[IconDef] = Some(Icons.EepromIcon)
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlot.scala
deleted file mode 100644
index 1c3e5eb..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlot.scala
+++ /dev/null
@@ -1,60 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource}
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
-import totoro.ocelot.brain.entity.FloppyManaged
-import totoro.ocelot.brain.entity.traits.{DiskManaged, Entity, Floppy, Inventory}
-import totoro.ocelot.brain.loot.Loot
-import totoro.ocelot.brain.util.DyeColor
-
-class FloppySlot(owner: Inventory#Slot) extends InventorySlot[Floppy](owner) with DiskOrFloppySlot {
- private val soundFloppyInsert = SoundSource.fromBuffer(SoundBuffers.MachineFloppyInsert, SoundCategory.Environment)
- private val soundFloppyEject = SoundSource.fromBuffer(SoundBuffers.MachineFloppyEject, SoundCategory.Environment)
-
- override def itemIcon: Option[IconDef] = item.map(fl => new IconDef("items/FloppyDisk_" + fl.color.name))
-
- override def icon: IconDef = new IconDef("icons/Floppy")
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- for (f <- FloppySlot.FloppyFactories) {
- val floppy = f.create()
- menu.addEntry(new ContextMenuEntry(floppy.label.getLabel,
- () => item = floppy,
- icon = Some(new IconDef("items/FloppyDisk_" + floppy.color.name))))
- }
-
- menu.addEntry(new ContextMenuEntry("Empty",
- () => item = new FloppyManaged("Floppy Disk", DyeColor.GRAY),
- icon = Some(new IconDef("items/FloppyDisk_dyeGray"))))
- }
-
- override def lmbMenuEnabled: Boolean = true
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- item match {
- case Some(_: FloppyManaged) =>
- addSetDirectoryEntryToRmbMenu(menu, this.asInstanceOf[InventorySlot[DiskManaged with Entity]])
- case _ =>
- }
-
- super.fillRmbMenu(menu)
- }
-
- override def onAdded(item: Floppy): Unit = {
- super.onAdded(item)
- soundFloppyInsert.play()
- }
-
- override def onRemoved(item: Floppy): Unit = {
- super.onRemoved(item)
- soundFloppyEject.play()
- }
-
- override def tooltipLine1Text: String = item.get.label.getLabel
-}
-
-object FloppySlot {
- private val FloppyFactories = Array(Loot.OpenOsFloppy, Loot.Plan9kFloppy, Loot.OPPMFloppy,
- Loot.OpenLoaderFloppy, Loot.NetworkFloppy, Loot.IrcFloppy, Loot.DataFloppy)
-}
\ No newline at end of file
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala
new file mode 100644
index 0000000..076fcdc
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/FloppySlotWidget.scala
@@ -0,0 +1,34 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.audio.{Audio, SoundBuffers, SoundCategory, SoundSource}
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.Inventory
+import ocelot.desktop.inventory.item.FloppyItem
+
+class FloppySlotWidget(slot: Inventory#Slot) extends SlotWidget[FloppyItem](slot) {
+ private lazy val soundFloppyInsert =
+ SoundSource.fromBuffer(SoundBuffers.MachineFloppyInsert, SoundCategory.Environment)
+ private lazy val soundFloppyEject = SoundSource.fromBuffer(SoundBuffers.MachineFloppyEject, SoundCategory.Environment)
+
+ override def ghostIcon: Option[IconDef] = Some(Icons.FloppyIcon)
+
+ override def onItemAdded(): Unit = {
+ super.onItemAdded()
+ // TODO: don't play the sound when loading a workspace
+ if (!Audio.isDisabled) {
+ soundFloppyInsert.play()
+ }
+ }
+
+ override def onItemRemoved(
+ removedItem: FloppyItem,
+ replacedBy: Option[FloppyItem],
+ ): Unit = {
+ super.onItemRemoved(removedItem, replacedBy)
+
+ // TODO: don't play the sound when loading a workspace
+ if (!Audio.isDisabled) {
+ soundFloppyEject.play()
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala
new file mode 100644
index 0000000..cbd9252
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/HddSlotWidget.scala
@@ -0,0 +1,11 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.Inventory
+import ocelot.desktop.inventory.item.HddItem
+import totoro.ocelot.brain.util.Tier.Tier
+
+class HddSlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[HddItem](slot) {
+ override def ghostIcon: Option[IconDef] = Some(Icons.HddIcon)
+ override def slotTier: Option[Tier] = Some(_tier)
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/InventorySlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/InventorySlot.scala
deleted file mode 100644
index 9d003d8..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/InventorySlot.scala
+++ /dev/null
@@ -1,122 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.color.Color
-import ocelot.desktop.geometry.Padding2D
-import ocelot.desktop.ui.UiHandler
-import ocelot.desktop.ui.layout.LinearLayout
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
-import ocelot.desktop.ui.widget.tooltip.Tooltip
-import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget}
-import ocelot.desktop.util.Orientation
-import totoro.ocelot.brain.entity.HDDManaged
-import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, Environment, Inventory, Tiered}
-
-abstract class InventorySlot[T <: Entity](val owner: Inventory#Slot) extends SlotWidget[T] {
- reloadItem()
-
- override def onAdded(item: T): Unit = {
- super.onAdded(item)
- owner.put(item)
- }
-
- override def onRemoved(item: T): Unit = {
- super.onRemoved(item)
- owner.remove()
- }
-
- override def fillRmbMenu(menu: ContextMenu): Unit = {
- item match {
- case Some(env: Environment) =>
- menu.addEntry(new ContextMenuEntry("Copy address", () => {
- UiHandler.clipboard = env.node.address
- }))
- case _ =>
- }
- super.fillRmbMenu(menu)
- }
-
- // --------------------------- Tooltip ---------------------------
-
- private var tooltip: Option[Tooltip] = None
-
- def tooltipLine1Color: Color = item match {
- case Some(tiered: Tiered) =>
- tiered.tier match {
- case 1 => Color.Yellow
- case 2 => Color.Cyan
- case _ => Color.White
- }
- case _ => Color.White
- }
-
- final protected def getTooltipSuffixedText(part1: String, part2: String): String = s"$part1 ($part2)"
- final protected def getTooltipTieredText(tier: String): String = s"Tier $tier"
- final protected def getTooltipTieredText(tier: Int): String = getTooltipTieredText((tier + 1).toString)
- final protected def getTooltipDeviceInfoText(deviceInfo: DeviceInfo): String =
- deviceInfo.getDeviceInfo(DeviceInfo.DeviceAttribute.Description)
- final protected def getTooltipDeviceInfoText(deviceInfo: DeviceInfo, part2: String): String =
- getTooltipSuffixedText(getTooltipDeviceInfoText(deviceInfo), part2)
- final protected def getTooltipDeviceInfoText(deviceInfo: DeviceInfo, tier: Int): String =
- getTooltipDeviceInfoText(deviceInfo, getTooltipTieredText(tier))
-
- def tooltipLine1Text: String = item.get match {
- case deviceInfoTiered: DeviceInfo with Tiered =>
- getTooltipDeviceInfoText(deviceInfoTiered, deviceInfoTiered.tier)
- case _ => ""
- }
-
- def tooltipChildrenAdder(inner: Widget): Unit = {
- inner.children :+= new Label {
- override def text: String = tooltipLine1Text
- override def color: Color = tooltipLine1Color
- }
-
- item match {
- case Some(environment: Environment) =>
- inner.children :+= new Label {
- override def text: String = s"Address: ${environment.node.address}"
- override def color: Color = Color.Grey
- }
- case _ =>
- }
-
- item match {
- case Some(hddManaged: HDDManaged) =>
- if (hddManaged.customRealPath.isDefined) {
- inner.children :+= new Label {
- override def text: String = s"Source path: ${hddManaged.customRealPath.get}"
- override def color: Color = Color.Grey
- }
- }
- case _ =>
- }
- }
-
- override def onHoverEnter(): Unit = {
- super.onHoverEnter()
-
- // Showing tooltip only if item is present in slot
- if (item.isEmpty)
- return
-
- tooltip = Some(new Tooltip())
-
- val inner: Widget = new Widget {
- override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
- }
-
- tooltipChildrenAdder(inner)
-
- tooltip.get.children :+= new PaddingBox(inner, Padding2D.equal(5))
-
- root.get.tooltipPool.addTooltip(tooltip.get)
- }
-
- override def onHoverLeave(): Unit = {
- super.onHoverLeave()
-
- tooltip.foreach(root.get.tooltipPool.closeTooltip(_))
- }
-
- def reloadItem(): Unit = _item = owner.get.map(_.asInstanceOf[T])
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/ItemChooser.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/ItemChooser.scala
new file mode 100644
index 0000000..81ca0bb
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/ItemChooser.scala
@@ -0,0 +1,63 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.IconDef
+import ocelot.desktop.inventory.Items.{ArbitraryItemGroup, ExtendedTieredItemGroup, SingletonItemGroup, TieredItemGroup}
+import ocelot.desktop.inventory._
+import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
+import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier
+import totoro.ocelot.brain.util.Tier.Tier
+
+class ItemChooser[I <: Item](slot: SlotWidget[I]) extends ContextMenu {
+ import ItemChooser._
+
+ private def makeMenuEntry(factory: ItemFactory, label: String): ContextMenuEntry = {
+ ContextMenuEntry(label, icon = Some(factory.icon)) {
+ slot.item = factory.build().asInstanceOf[I]
+ }
+ }
+
+ private def addSubmenu[T: HasLabel](
+ name: String,
+ factories: Seq[(T, ItemFactory)],
+ icon: Option[IconDef] = None
+ ): Unit = {
+ val tierLabel = implicitly[HasLabel[T]].label _
+
+ val acceptedFactories = factories.iterator.filter(p => slot.isItemAccepted(p._2)).toSeq
+
+ if (acceptedFactories.nonEmpty) {
+ addEntry(
+ new ContextMenuSubmenu(name, icon = Some(icon.getOrElse(acceptedFactories.last._2.icon))) {
+ for ((tier, factory) <- acceptedFactories) {
+ addEntry(makeMenuEntry(factory, tierLabel(tier)))
+ }
+ }
+ )
+ }
+ }
+
+ for (group <- Items.groups) {
+ group match {
+ case SingletonItemGroup(name, factory) =>
+ if (slot.isItemAccepted(factory)) {
+ addEntry(makeMenuEntry(factory, name))
+ }
+
+ case TieredItemGroup(name, factories) => addSubmenu(name, factories)
+
+ case ExtendedTieredItemGroup(name, factories) => addSubmenu(name, factories)
+
+ case ArbitraryItemGroup(name, icon, factories) => addSubmenu(name, factories, Some(icon))
+ }
+ }
+}
+
+object ItemChooser {
+ private trait HasLabel[A] {
+ def label(value: A): String
+ }
+
+ private implicit val TierHasLabel: HasLabel[Tier] = _.label
+ private implicit val ExtendedTierHasLabel: HasLabel[ExtendedTier] = _.label
+ private implicit val StringHasLabel: HasLabel[String] = identity
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlot.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlot.scala
deleted file mode 100644
index e081335..0000000
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlot.scala
+++ /dev/null
@@ -1,66 +0,0 @@
-package ocelot.desktop.ui.widget.slot
-
-import ocelot.desktop.color.Color
-import ocelot.desktop.graphics.IconDef
-import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
-import totoro.ocelot.brain.entity.Memory
-import totoro.ocelot.brain.entity.traits.{Inventory, Tiered}
-import totoro.ocelot.brain.util.Tier
-
-class MemorySlot(owner: Inventory#Slot, val tier: Int) extends InventorySlot[Memory](owner) {
- override def itemIcon: Option[IconDef] = item.map(mem => new IconDef("items/Memory" + mem.tier))
-
- override def icon: IconDef = new IconDef("icons/Memory")
- override def tierIcon: Option[IconDef] = Some(new IconDef("icons/Tier" + tier))
-
- override def fillLmbMenu(menu: ContextMenu): Unit = {
- menu.addEntry(new ContextMenuEntry("RAM (Tier 1)",
- () => item = new Memory(Tier.One),
- icon = Some(new IconDef("items/Memory0"))))
-
- menu.addEntry(new ContextMenuEntry("RAM (Tier 1.5)",
- () => item = new Memory(Tier.Two),
- icon = Some(new IconDef("items/Memory1"))))
-
- if (tier >= Tier.Two) {
- menu.addEntry(new ContextMenuEntry("RAM (Tier 2)",
- () => item = new Memory(Tier.Three),
- icon = Some(new IconDef("items/Memory2"))))
-
- menu.addEntry(new ContextMenuEntry("RAM (Tier 2.5)",
- () => item = new Memory(Tier.Four),
- icon = Some(new IconDef("items/Memory3"))))
- }
-
- if (tier >= Tier.Three) {
- menu.addEntry(new ContextMenuEntry("RAM (Tier 3)",
- () => item = new Memory(Tier.Five),
- icon = Some(new IconDef("items/Memory4"))))
-
- menu.addEntry(new ContextMenuEntry("RAM (Tier 3.5)",
- () => item = new Memory(Tier.Six),
- icon = Some(new IconDef("items/Memory5"))))
- }
- }
-
- override def lmbMenuEnabled: Boolean = true
-
- override def tooltipLine1Color: Color = item.get match {
- case tiered: Tiered => tiered.tier match {
- case 0 => Color.White
- case 1 => Color.White
- case 2 => Color.Yellow
- case 3 => Color.Yellow
- case 4 => Color.Cyan
- case _ => Color.Cyan
- }
- case _ => Color.White
- }
-
- override def tooltipLine1Text: String = {
- val tier = (item.get.tier + 2) / 2f
- val intTier = tier.toInt
-
- getTooltipDeviceInfoText(item.get, getTooltipTieredText(if (tier - intTier > 0) tier.toString else intTier.toString))
- }
-}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala
new file mode 100644
index 0000000..109ecbf
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/MemorySlotWidget.scala
@@ -0,0 +1,11 @@
+package ocelot.desktop.ui.widget.slot
+
+import ocelot.desktop.graphics.{IconDef, Icons}
+import ocelot.desktop.inventory.Inventory
+import ocelot.desktop.inventory.item.MemoryItem
+import totoro.ocelot.brain.util.Tier.Tier
+
+class MemorySlotWidget(slot: Inventory#Slot, _tier: Tier) extends SlotWidget[MemoryItem](slot) {
+ override def ghostIcon: Option[IconDef] = Some(Icons.MemoryIcon)
+ override def slotTier: Option[Tier] = Some(_tier)
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala b/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala
index f89ad9f..35afb61 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/slot/SlotWidget.scala
@@ -2,84 +2,153 @@ package ocelot.desktop.ui.widget.slot
import ocelot.desktop.audio.SoundSources
import ocelot.desktop.geometry.Size2D
-import ocelot.desktop.graphics.{Graphics, IconDef}
+import ocelot.desktop.graphics.{Graphics, IconDef, Icons}
+import ocelot.desktop.inventory.{Inventory, Item, ItemFactory}
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.Widget
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
+import ocelot.desktop.ui.widget.tooltip.{ItemTooltip, Tooltip}
+import totoro.ocelot.brain.util.Tier
+import totoro.ocelot.brain.util.Tier.Tier
-abstract class SlotWidget[T] extends Widget with ClickHandler with HoverHandler {
- def icon: IconDef
+import scala.math.Ordering.Implicits.infixOrderingOps
+import scala.reflect.ClassTag
- def tierIcon: Option[IconDef] = None
+class SlotWidget[I <: Item](private val slot: Inventory#Slot)(implicit slotItemTag: ClassTag[I])
+ extends Widget
+ with ClickHandler
+ with HoverHandler {
- def itemIcon: Option[IconDef] = None
+ slotWidget =>
- def onRemoved(item: T): Unit = {}
+ // NOTE: this must remain as a separate object
+ // (i. e. don't you dare inline it into the addObserver call below):
+ // this is the only strong reference to the observer
+ private val observer = new Inventory.SlotObserver {
+ override def onItemAdded(): Unit = {
+ slotWidget.onItemAdded()
+ }
- def onAdded(item: T): Unit = {}
+ override def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit = {
+ slotWidget.onItemRemoved(removedItem.asInstanceOf[I], replacedBy.map(_.asInstanceOf[I]))
+ }
- def fillLmbMenu(menu: ContextMenu): Unit = {}
-
- def fillRmbMenu(menu: ContextMenu): Unit = {
- menu.addEntry(new ContextMenuEntry("Remove", () => item = None, sound = SoundSources.InterfaceClickLow))
+ override def onItemNotification(notification: Item.Notification): Unit = {
+ slotWidget.onItemNotification(notification)
+ }
}
- def lmbMenuEnabled: Boolean = false
+ slot.addObserver(observer)
- def rmbMenuEnabled: Boolean = _item.isDefined
-
- final var _item: Option[T] = None
-
- final def item_=(v: Option[T]): Unit = {
- if (_item.isDefined)
- onRemoved(_item.get)
-
- _item = v
-
- if (v.isDefined)
- onAdded(v.get)
+ def dispose(): Unit = {
+ closeTooltip()
+ slot.removeObserver(observer)
}
- final def item_=(v: T): Unit = item = Some(v)
+ def ghostIcon: Option[IconDef] = None
- final def item: Option[T] = _item
+ def slotTier: Option[Tier] = None
+
+ def tierIcon: Option[IconDef] = slotTier.map(Icons.TierIcon)
+
+ def itemIcon: Option[IconDef] = item.map(_.icon)
+
+ def item: Option[I] = slot.get.map(_.asInstanceOf[I])
+
+ def item_=(item: Option[I]): Unit = {
+ slot.set(item.map(_.asInstanceOf[slot.inventory.I]))
+ }
+
+ def item_=(item: I): Unit = {
+ slot.put(item.asInstanceOf[slot.inventory.I])
+ }
+
+ def isItemAccepted(factory: ItemFactory): Boolean = {
+ // yes, you can compare Options. isn't it nice?
+ slotItemTag.runtimeClass.isAssignableFrom(factory.itemClass) && factory.tier.map(_ min Tier.Three) <= slotTier
+ }
+
+ protected def onItemAdded(): Unit = {}
+
+ protected def onItemRemoved(removedItem: I, replacedBy: Option[I]): Unit = {
+ closeTooltip()
+ }
+
+ protected def onItemNotification(notification: Item.Notification): Unit = {}
final override def minimumSize: Size2D = Size2D(36, 36)
final override def maximumSize: Size2D = minimumSize
-
final override def receiveMouseEvents: Boolean = true
+ protected def lmbMenuEnabled: Boolean = true
+
+ protected def rmbMenuEnabled: Boolean = item.nonEmpty
+
+ protected def fillRmbMenu(menu: ContextMenu): Unit = {
+ for (item <- item) {
+ item.fillRmbMenu(menu)
+ menu.addEntry(
+ ContextMenuEntry("Remove", sound = SoundSources.InterfaceClickLow) {
+ slot.remove()
+ }
+ )
+ }
+ }
+
+ private var _tooltip: Option[Tooltip] = None
+
+ def onHoverEnter(): Unit = {
+ for (item <- item) {
+ // just in case
+ closeTooltip()
+
+ val tooltip = new ItemTooltip
+ item.fillTooltip(tooltip)
+ _tooltip = Some(tooltip)
+ root.get.tooltipPool.addTooltip(tooltip)
+ }
+ }
+
+ def onHoverLeave(): Unit = {
+ closeTooltip()
+ }
+
+ private def closeTooltip(): Unit = {
+ for (tooltip <- _tooltip) {
+ root.get.tooltipPool.closeTooltip(tooltip)
+ }
+ }
+
eventHandlers += {
case ClickEvent(MouseEvent.Button.Left, _) if lmbMenuEnabled =>
- val menu = new ContextMenu
- fillLmbMenu(menu)
- root.get.contextMenus.open(menu)
+ root.get.contextMenus.open(new ItemChooser(this))
+
case ClickEvent(MouseEvent.Button.Right, _) if rmbMenuEnabled =>
val menu = new ContextMenu
fillRmbMenu(menu)
root.get.contextMenus.open(menu)
- case HoverEvent(state) =>
- if (state == HoverEvent.State.Enter)
- onHoverEnter()
- else
- onHoverLeave()
+
+ case HoverEvent(HoverEvent.State.Enter) => onHoverEnter()
+ case HoverEvent(HoverEvent.State.Leave) => onHoverLeave()
}
- def onHoverEnter(): Unit = {}
-
- def onHoverLeave(): Unit = {}
-
final override def draw(g: Graphics): Unit = {
g.sprite("EmptySlot", bounds)
val iconBounds = bounds.inflate(-2)
- if (itemIcon.isDefined) {
- g.sprite(itemIcon.get, iconBounds)
- } else {
- if (tierIcon.isDefined) g.sprite(tierIcon.get, iconBounds)
- g.sprite(icon, iconBounds)
+ itemIcon match {
+ case Some(itemIcon) => g.sprite(itemIcon, iconBounds)
+
+ case None =>
+ for (tierIcon <- tierIcon) {
+ g.sprite(tierIcon, iconBounds)
+ }
+
+ for (ghostIcon <- ghostIcon) {
+ g.sprite(ghostIcon, iconBounds)
+ }
}
}
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala
index 8f68bd2..794b3b4 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/statusbar/StatusBar.scala
@@ -12,6 +12,7 @@ import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import org.lwjgl.input.Keyboard
+import scala.collection.immutable.ArraySeq
import scala.concurrent.duration.DurationInt
class StatusBar extends Widget {
@@ -56,18 +57,18 @@ class StatusBar extends Widget {
eventHandlers += {
case ClickEvent(MouseEvent.Button.Right, pos) =>
val menu = new ContextMenu
- menu.addEntry(new ContextMenuEntry("Change simulation speed", () => {
+ menu.addEntry(ContextMenuEntry("Change simulation speed") {
new ChangeSimulationSpeedDialog().show()
- }))
- menu.addEntry(new ContextMenuEntry("Reset simulation speed", () => {
+ })
+ menu.addEntry(ContextMenuEntry("Reset simulation speed") {
OcelotDesktop.ticker.tickInterval = 50.millis
- }))
+ })
if (OcelotDesktop.tickerIntervalHistory.nonEmpty)
menu.addSeparator()
for (elem <- OcelotDesktop.tickerIntervalHistory.reverseIterator) {
- menu.addEntry(new ContextMenuEntry((1_000_000f / elem.toMicros).toString, () => {
+ menu.addEntry(ContextMenuEntry((1_000_000f / elem.toMicros).toString) {
OcelotDesktop.ticker.tickInterval = elem
- }))
+ })
}
root.get.contextMenus.open(menu, pos)
}
@@ -97,7 +98,7 @@ class StatusBar extends Widget {
g.rect(bounds, ColorScheme("StatusBar"))
g.line(position, position + Size2D(width, 0), 1, ColorScheme("StatusBarBorder"))
drawChildren(g)
- keyEntries.children = Array()
- keyMouseEntries.children = Array()
+ keyEntries.children = ArraySeq.empty
+ keyMouseEntries.children = ArraySeq.empty
}
}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/tooltip/ItemTooltip.scala b/src/main/scala/ocelot/desktop/ui/widget/tooltip/ItemTooltip.scala
new file mode 100644
index 0000000..0f8760a
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/tooltip/ItemTooltip.scala
@@ -0,0 +1,16 @@
+package ocelot.desktop.ui.widget.tooltip
+
+import ocelot.desktop.color.Color
+import ocelot.desktop.geometry.Padding2D
+import ocelot.desktop.ui.widget.Label
+
+class ItemTooltip extends Tooltip {
+ override def bodyPadding: Padding2D = Padding2D.equal(5)
+
+ def addLine(_text: String, _color: Color = Color.Grey): Unit = {
+ body.children :+= new Label {
+ override def text: String = _text
+ override def color: Color = _color
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/tooltip/LabelTooltip.scala b/src/main/scala/ocelot/desktop/ui/widget/tooltip/LabelTooltip.scala
new file mode 100644
index 0000000..0c69340
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/ui/widget/tooltip/LabelTooltip.scala
@@ -0,0 +1,22 @@
+package ocelot.desktop.ui.widget.tooltip
+
+import ocelot.desktop.color.Color
+import ocelot.desktop.geometry.Padding2D
+import ocelot.desktop.ui.widget.Label
+
+class LabelTooltip(text: String) extends Tooltip {
+ override val DelayTime: Float = 1.0f
+
+ for (line <- text.split("\n")) {
+ body.children :+= new Label {
+ override def text: String = line
+ override def color: Color = Color.White
+ }
+ }
+
+ override def bodyPadding: Padding2D = Padding2D.equal(4)
+
+ def onSaveSelected(): Unit = {}
+
+ def onExitSelected(): Unit = {}
+}
diff --git a/src/main/scala/ocelot/desktop/ui/widget/tooltip/Tooltip.scala b/src/main/scala/ocelot/desktop/ui/widget/tooltip/Tooltip.scala
index 26b73b2..0f456af 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/tooltip/Tooltip.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/tooltip/Tooltip.scala
@@ -1,15 +1,26 @@
package ocelot.desktop.ui.widget.tooltip
import ocelot.desktop.color.{Color, RGBAColor}
+import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.event.KeyEvent
-import ocelot.desktop.ui.widget.Widget
+import ocelot.desktop.ui.layout.LinearLayout
+import ocelot.desktop.ui.widget.{PaddingBox, Widget}
+import ocelot.desktop.util.Orientation
import ocelot.desktop.util.animation.UnitAnimation
-class Tooltip extends Widget {
+abstract class Tooltip extends Widget {
protected def tooltipPool: TooltipPool = parent.get.asInstanceOf[TooltipPool]
protected val openCloseAnimation: UnitAnimation = UnitAnimation.easeInOutQuad(0.3f)
+ val body: Widget = new Widget {
+ override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
+ }
+
+ def bodyPadding: Padding2D
+
+ children :+= new PaddingBox(body, bodyPadding)
+
/**
* Time before the tooltip will become visible (in seconds)
*/
diff --git a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala
index d6fbb93..1638473 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/verticalmenu/VerticalMenuButton.scala
@@ -12,7 +12,7 @@ import ocelot.desktop.ui.widget._
import ocelot.desktop.util.Orientation
import ocelot.desktop.util.animation.ColorAnimation
-class VerticalMenuButton(icon: String, label: String, handler: VerticalMenuButton => Unit = _ => {})
+class VerticalMenuButton(icon: IconDef, label: String, handler: VerticalMenuButton => Unit = _ => {})
extends Widget with ClickHandler with HoverHandler with ClickSoundSource {
val colorAnimation: ColorAnimation = new ColorAnimation(ColorScheme("VerticalMenuBackground"), 0.6f)
@@ -22,7 +22,7 @@ class VerticalMenuButton(icon: String, label: String, handler: VerticalMenuButto
override val layout = new LinearLayout(this, orientation = Orientation.Horizontal)
children :+= new PaddingBox(
- new Icon(IconDef(icon, 1f, color = ColorScheme("VerticalMenuEntryIcon"))),
+ new Icon(icon.copy(sizeMultiplier = 1f, color = ColorScheme("VerticalMenuEntryIcon"))),
Padding2D(right = 8)
)
diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/NodeSelector.scala b/src/main/scala/ocelot/desktop/ui/widget/window/NodeSelector.scala
index 359711e..c55fd36 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/window/NodeSelector.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/window/NodeSelector.scala
@@ -11,6 +11,8 @@ import ocelot.desktop.ui.widget.{PaddingBox, ScrollView, Widget, WorkspaceView}
import ocelot.desktop.util.animation.{ColorAnimation, UnitAnimation}
import ocelot.desktop.util.{DrawUtils, Logging, Orientation}
+import scala.collection.immutable.ArraySeq
+
class NodeSelector extends Window with Logging {
override protected val layout = new LinearLayout(this)
@@ -39,10 +41,10 @@ class NodeSelector extends Window with Logging {
private def workspaceView: WorkspaceView = windowPool.parent.get.asInstanceOf[WorkspaceView]
- private def rows: Array[Widget] = rowsWidget.children
+ private def rows: ArraySeq[Widget] = rowsWidget.children
// noinspection ScalaUnusedSymbol
- private def rows_=(rows: Array[Widget]): Unit = rowsWidget.children = rows
+ private def rows_=(rows: ArraySeq[Widget]): Unit = rowsWidget.children = rows
private def addNodeWidget(widget: Widget): Unit = {
val row = if (rows.lastOption.exists(row => row.children.length < 4)) {
@@ -60,7 +62,7 @@ class NodeSelector extends Window with Logging {
private def initNodeTypes(): Unit = {
NodeRegistry.types.foreach(t => addNodeWidget(new NodeTypeWidget(t) {
override def onClick(): Unit = {
- workspaceView.addNode(nodeType.factory())
+ workspaceView.addNode(nodeType.make())
hide()
}
}))
diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/ProfilerWindow.scala b/src/main/scala/ocelot/desktop/ui/widget/window/ProfilerWindow.scala
index cafce53..27bfa8b 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/window/ProfilerWindow.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/window/ProfilerWindow.scala
@@ -6,6 +6,8 @@ import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.{Label, PaddingBox, Widget}
import ocelot.desktop.util.{DrawUtils, Orientation, Profiler}
+import scala.collection.immutable.ArraySeq
+
class ProfilerWindow extends BasicWindow {
private val inner: Widget = new Widget {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
@@ -14,7 +16,7 @@ class ProfilerWindow extends BasicWindow {
children :+= new PaddingBox(inner, Padding2D.equal(8))
override def draw(g: Graphics): Unit = {
- inner.children = Array()
+ inner.children = ArraySeq.empty
for (line <- Profiler.report()) {
inner.children :+= new Label {
override def text: String = line
diff --git a/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala b/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala
index b9950ed..7ee8030 100644
--- a/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala
+++ b/src/main/scala/ocelot/desktop/ui/widget/window/WindowPool.scala
@@ -5,10 +5,12 @@ import ocelot.desktop.ui.event.handlers.DragHandler
import ocelot.desktop.ui.layout.Layout
import ocelot.desktop.ui.widget.Widget
+import scala.collection.immutable.ArraySeq
+
class WindowPool extends Widget with DragHandler {
override protected val layout: Layout = new Layout(this)
- private def windows: Array[Window] = children.map(_.asInstanceOf[Window])
+ private def windows: ArraySeq[Window] = children.map(_.asInstanceOf[Window])
def addWindow(window: Window): Unit = {
if (!children.contains(window)) {
diff --git a/src/main/scala/ocelot/desktop/util/Font.scala b/src/main/scala/ocelot/desktop/util/Font.scala
index 84cbc65..8cd465a 100644
--- a/src/main/scala/ocelot/desktop/util/Font.scala
+++ b/src/main/scala/ocelot/desktop/util/Font.scala
@@ -3,6 +3,7 @@ package ocelot.desktop.util
import ocelot.desktop.geometry.Rect2D
import ocelot.desktop.graphics.Texture
import org.lwjgl.opengl.GL11
+import totoro.ocelot.brain.util.FontUtils
import java.awt.image.{BufferedImage, DataBufferByte, IndexColorModel}
import java.io.InputStream
@@ -10,9 +11,7 @@ import java.nio.ByteBuffer
import scala.collection.mutable
import scala.io.{Codec, Source}
-class Font(val name: String, val fontSize: Int) extends Logging {
- val CodepointLimit: Int = 0x10000
-
+class Font(val name: String, val fontSize: Int) extends Resource with Logging {
val AtlasWidth = 4096
val AtlasHeight = 4096
var glyphCount = 0
@@ -24,12 +23,12 @@ class Font(val name: String, val fontSize: Int) extends Logging {
new BufferedImage(AtlasWidth, AtlasHeight, BufferedImage.TYPE_BYTE_BINARY, icm)
}
- val map = new mutable.HashMap[Char, Rect2D]
+ val map = new mutable.HashMap[Int, Rect2D]
var texture: Texture = _
init()
- def charWidth(char: Char): Int = (map.getOrElse(char, map('?')).w * AtlasWidth).toInt
+ def charWidth(codePoint: Int): Int = (map.getOrElse(codePoint, map('?')).w * AtlasWidth).toInt
private def init(): Unit = {
logger.info(f"Loading font $name")
@@ -43,47 +42,53 @@ class Font(val name: String, val fontSize: Int) extends Logging {
for (line <- source.getLines()) {
val colon = line.indexOf(':')
val charCode = Integer.parseInt(line.substring(0, colon), 16)
- if (charCode >= 0 && charCode < CodepointLimit) {
- val char = charCode.toChar
- val width = (line.length - colon - 1) * 4 / fontSize
- if (ox + width > AtlasWidth) {
- ox = 0
- oy += fontSize
- }
+ if (charCode >= 0 && charCode < FontUtils.codepoint_limit) {
+ val expectedWidth = FontUtils.wcwidth(charCode)
- map(char) = Rect2D(
- ox.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
- oy.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float],
- width.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
- fontSize.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float]
- )
+ // skip control characters
+ if (expectedWidth >= 1) {
+ val width = (line.length - colon - 1) * 4 / fontSize
- var x = 0
- var y = 0
+ if (ox + width > AtlasWidth) {
+ ox = 0
+ oy += fontSize
+ }
- for (char <- line.substring(colon + 1)) {
- for (i <- 3 to 0 by -1) {
- val bit = (Character.digit(char, 16) >> i) & 1
+ map(charCode) = Rect2D(
+ ox.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
+ oy.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float],
+ width.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
+ fontSize.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float]
+ )
- atlas.setRGB(ox + x, oy + y, bit match {
- case 0 => 0x00000000
- case 1 => 0xFFFFFFFF
- })
+ var x = 0
+ var y = 0
- x += 1
+ for (char <- line.substring(colon + 1)) {
+ for (i <- 3 to 0 by -1) {
+ val bit = (Character.digit(char, 16) >> i) & 1
- if (x == width) {
- x = 0
- y += 1
+ atlas.setRGB(ox + x, oy + y, bit match {
+ case 0 => 0x00000000
+ case 1 => 0xFFFFFFFF
+ })
+
+ x += 1
+
+ if (x == width) {
+ x = 0
+ y += 1
+ }
}
}
- }
- ox += width
- glyphCount += 1
+ ox += width
+ glyphCount += 1
+ }
} else {
outOfRangeGlyphCount += 1
+ logger.warn(String.format("Unicode font contained unexpected glyph: U+%04X, ignoring", charCode))
}
}
@@ -118,4 +123,9 @@ class Font(val name: String, val fontSize: Int) extends Logging {
tex
}
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ texture.freeResource()
+ }
}
\ No newline at end of file
diff --git a/src/main/scala/ocelot/desktop/util/Messages.scala b/src/main/scala/ocelot/desktop/util/Messages.scala
new file mode 100644
index 0000000..9075145
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/util/Messages.scala
@@ -0,0 +1,30 @@
+package ocelot.desktop.util
+
+import scala.collection.mutable
+import scala.io.Source
+
+object Messages extends Logging with PartialFunction[String, String] {
+ private val entries = new mutable.HashMap[String, String]()
+
+ override def apply(key: String): String = entries(key)
+
+ override def isDefinedAt(key: String): Boolean = entries.isDefinedAt(key)
+
+ def load(source: Source): Unit = {
+ logger.info(s"Loading messages")
+
+ val oldSize = entries.size
+
+ for (line_ <- source.getLines) {
+ val line = line_.trim
+ if (line.nonEmpty) {
+ val split = line.split("\\s*=\\s*")
+ val key = split(0)
+ val value = split(1)
+ entries(key) = value
+ }
+ }
+
+ logger.info(s"Loaded ${entries.size - oldSize} messages")
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/util/OpenAlException.scala b/src/main/scala/ocelot/desktop/util/OpenAlException.scala
new file mode 100644
index 0000000..5ff4a83
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/util/OpenAlException.scala
@@ -0,0 +1,14 @@
+package ocelot.desktop.util
+
+import scala.util.control
+import scala.util.control.Exception.Catch
+
+case class OpenAlException(func: String, errName: String, code: Int)
+ extends Exception(s"OpenAL error: $func: $errName")
+
+object OpenAlException {
+ def defaulting[T](default: => T): Catch[T] = control.Exception.failAsValue(classOf[OpenAlException])(default)
+
+ def ignoring: Catch[Unit] = defaulting(())
+}
+
diff --git a/src/main/scala/ocelot/desktop/util/Profiler.scala b/src/main/scala/ocelot/desktop/util/Profiler.scala
index 9c8ebca..52e719e 100644
--- a/src/main/scala/ocelot/desktop/util/Profiler.scala
+++ b/src/main/scala/ocelot/desktop/util/Profiler.scala
@@ -6,12 +6,22 @@ import scala.collection.mutable.ArrayBuffer
object Profiler {
private val timeEntries = mutable.HashMap[String, TimeEntry]()
- def startTimeMeasurement(name: String): Unit = {
+ def measure(name: String)(f: => Unit): Unit = {
+ startTimeMeasurement(name)
+
+ try {
+ f
+ } finally {
+ endTimeMeasurement(name)
+ }
+ }
+
+ private def startTimeMeasurement(name: String): Unit = {
val entry = timeEntries.getOrElseUpdate(name, new TimeEntry)
entry.startTime = System.nanoTime().toDouble / 1e9
}
- def endTimeMeasurement(name: String): Unit = {
+ private def endTimeMeasurement(name: String): Unit = {
val entry = timeEntries.getOrElseUpdate(name, new TimeEntry)
entry.addSample(System.nanoTime().toDouble / 1e9 - entry.startTime)
}
diff --git a/src/main/scala/ocelot/desktop/util/ReflectionUtils.scala b/src/main/scala/ocelot/desktop/util/ReflectionUtils.scala
new file mode 100644
index 0000000..bb89169
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/util/ReflectionUtils.scala
@@ -0,0 +1,53 @@
+package ocelot.desktop.util
+
+import java.lang.reflect.Constructor
+import scala.collection.mutable.ArrayBuffer
+
+object ReflectionUtils {
+ /**
+ * Returns the linearization order of a class.
+ *
+ * Roughly speaking, it's the order method calls are resolved in.
+ *
+ * It also acts as a list of superclasses (each occurring once).
+ */
+ def linearizationOrder(clazz: Class[_]): Iterator[Class[_]] = {
+ val result = ArrayBuffer[Class[_]](clazz)
+
+ for (interface <- clazz.getInterfaces.reverseIterator) {
+ result ++= linearizationOrder(interface)
+ }
+
+ for (superclass <- Option(clazz.getSuperclass)) {
+ result ++= linearizationOrder(superclass)
+ }
+
+ val merged = result.reverseIterator.distinct.toBuffer
+
+ merged.reverseIterator
+ }
+
+ /**
+ * Finds a (most specific) unary constructor of `constructedClass` that accepts `argumentClass` (or its subtype).
+ */
+ def findUnaryConstructor[A](constructedClass: Class[A], argumentClass: Class[_]): Option[Constructor[A]] = {
+ try {
+ // happy case: just grab the constructor directly
+ return Some(constructedClass.getConstructor(argumentClass))
+ } catch {
+ case _: NoSuchMethodException =>
+ // uh-oh, we'll have to try harder...
+ }
+
+ val constructors = constructedClass.getConstructors
+ .iterator
+ .filter(_.getParameterCount == 1)
+ .map(_.asInstanceOf[Constructor[A]])
+ .map(constructor => (constructor.getParameterTypes()(0), constructor))
+ .toMap
+
+ linearizationOrder(argumentClass)
+ .flatMap(constructors.get)
+ .nextOption()
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/util/Resource.scala b/src/main/scala/ocelot/desktop/util/Resource.scala
index 6c7e9e2..391e100 100644
--- a/src/main/scala/ocelot/desktop/util/Resource.scala
+++ b/src/main/scala/ocelot/desktop/util/Resource.scala
@@ -1,6 +1,9 @@
package ocelot.desktop.util
trait Resource {
- def initResource(): Unit = {}
- def freeResource(): Unit
+ ResourceManager.registerResource(this)
+
+ def freeResource(): Unit = {
+ ResourceManager.unregisterResource(this)
+ }
}
diff --git a/src/main/scala/ocelot/desktop/util/ResourceManager.scala b/src/main/scala/ocelot/desktop/util/ResourceManager.scala
index 1aedeb4..bf77359 100644
--- a/src/main/scala/ocelot/desktop/util/ResourceManager.scala
+++ b/src/main/scala/ocelot/desktop/util/ResourceManager.scala
@@ -2,37 +2,25 @@ package ocelot.desktop.util
import scala.collection.mutable.ArrayBuffer
-object ResourceManager {
- private var initialized = false
+object ResourceManager extends Logging {
private val resources = new ArrayBuffer[Resource]
def registerResource(resource: Resource): Unit = {
resources += resource
- if (initialized) resource.initResource()
}
def unregisterResource(resource: Resource): Unit = {
resources -= resource
}
- def initResources(): Unit = {
- if (!initialized) {
- forEach(_.initResource())
- initialized = true
+ def checkEmpty(): Unit = {
+ if (resources.isEmpty)
+ return
+
+ logger.error(s"${resources.length} resources have not been freed properly")
+
+ for ((groupClass, groupResources) <- resources.groupBy(_.getClass)) {
+ logger.error(s" - ${groupResources.length} resources of type ${groupClass.getName}")
}
}
-
- def forEach(predicate: Resource => Unit): Unit = {
- resources.foreach(predicate)
- }
-
- def freeResource(resource: Resource): Unit = {
- resource.freeResource()
- unregisterResource(resource)
- }
-
- def freeResources(): Unit = {
- forEach(_.freeResource())
- resources.clear()
- }
}
diff --git a/src/main/scala/ocelot/desktop/util/SettingsData.scala b/src/main/scala/ocelot/desktop/util/SettingsData.scala
index 021fc0d..53720fd 100644
--- a/src/main/scala/ocelot/desktop/util/SettingsData.scala
+++ b/src/main/scala/ocelot/desktop/util/SettingsData.scala
@@ -1,8 +1,15 @@
package ocelot.desktop.util
import ocelot.desktop.Settings.Int2D
+import ocelot.desktop.util.SettingsData.Fields
+
+import scala.reflect.runtime.universe
class SettingsData {
+ // implementation notes:
+ // this class relies on reflection to implement updateWith
+ // it's assumed all fields that should be copied are declared as **public var**
+
def this(data: SettingsData) {
this()
updateWith(data)
@@ -12,11 +19,17 @@ class SettingsData {
var volumeBeep: Float = 1f
var volumeEnvironment: Float = 1f
var volumeInterface: Float = 0.5f
+ var audioDisable: Boolean = false
+ var logAudioErrorStacktrace: Boolean = false
+ var scaleFactor: Float = 1f
var windowSize: Int2D = new Int2D()
var windowPosition: Int2D = new Int2D()
var windowValidatePosition: Boolean = true
var windowFullscreen: Boolean = false
+ var disableVsync: Boolean = false
+ var debugLwjgl: Boolean = false
+ var useNativeFileChooser: Boolean = true
var recentWorkspace: Option[String] = None
@@ -24,21 +37,24 @@ class SettingsData {
var saveOnExit: Boolean = true
var openLastWorkspace: Boolean = true
+ private val mirror = universe.runtimeMirror(getClass.getClassLoader).reflect(this)
+
def updateWith(data: SettingsData): Unit = {
- // TODO: maybe apply some automated mapping solution
- // TODO: please do that ☝
- this.volumeMaster = data.volumeMaster
- this.volumeBeep = data.volumeBeep
- this.volumeEnvironment = data.volumeEnvironment
- this.volumeInterface = data.volumeInterface
-
- this.windowPosition = data.windowPosition
- this.windowValidatePosition = data.windowValidatePosition
- this.windowSize = data.windowSize
- this.windowFullscreen = data.windowFullscreen
-
- this.stickyWindows = data.stickyWindows
- this.saveOnExit = data.saveOnExit
- this.openLastWorkspace = data.openLastWorkspace
+ for (fieldSym <- Fields) {
+ val value = data.mirror.reflectMethod(fieldSym.getter.asMethod)()
+ mirror.reflectMethod(fieldSym.setter.asMethod)(value)
+ }
}
}
+
+object SettingsData {
+ private val Fields =
+ universe.typeOf[SettingsData]
+ .decls
+ .filter(_.isTerm)
+ .map(_.asTerm)
+ .filter(_.isVar)
+ // the var itself is private: check setter and getter
+ .filter(v => v.setter.isPublic && v.getter.isPublic)
+ .toList
+}
diff --git a/src/main/scala/ocelot/desktop/util/Spritesheet.scala b/src/main/scala/ocelot/desktop/util/Spritesheet.scala
index f58fe60..042e529 100644
--- a/src/main/scala/ocelot/desktop/util/Spritesheet.scala
+++ b/src/main/scala/ocelot/desktop/util/Spritesheet.scala
@@ -7,7 +7,7 @@ import javax.imageio.ImageIO
import scala.collection.mutable
import scala.io.Source
-object Spritesheet extends Logging {
+object Spritesheet extends Resource with Logging {
val sprites = new mutable.HashMap[String, Rect2D]()
var texture: Texture = _
@@ -40,4 +40,9 @@ object Spritesheet extends Logging {
logger.info(s"Loaded ${sprites.size} sprites")
}
+
+ override def freeResource(): Unit = {
+ super.freeResource()
+ texture.freeResource()
+ }
}
diff --git a/src/main/scala/ocelot/desktop/util/TierColor.scala b/src/main/scala/ocelot/desktop/util/TierColor.scala
index f965856..7012711 100644
--- a/src/main/scala/ocelot/desktop/util/TierColor.scala
+++ b/src/main/scala/ocelot/desktop/util/TierColor.scala
@@ -1,7 +1,8 @@
package ocelot.desktop.util
import ocelot.desktop.ColorScheme
-import ocelot.desktop.color.{Color, RGBAColorNorm}
+import ocelot.desktop.color.RGBAColorNorm
+import totoro.ocelot.brain.util.Tier.Tier
object TierColor {
val Tier0: RGBAColorNorm = ColorScheme("Tier0")
@@ -11,5 +12,5 @@ object TierColor {
val Tiers: Array[RGBAColorNorm] = Array(Tier0, Tier1, Tier2, Tier3)
- def get(tier: Int): RGBAColorNorm = if (tier >= 0 && tier <= 3) Tiers(tier.min(3)) else Color.White
+ def get(tier: Tier): RGBAColorNorm = Tiers(tier.id)
}
diff --git a/src/main/scala/ocelot/desktop/util/Transaction.scala b/src/main/scala/ocelot/desktop/util/Transaction.scala
new file mode 100644
index 0000000..e2160ee
--- /dev/null
+++ b/src/main/scala/ocelot/desktop/util/Transaction.scala
@@ -0,0 +1,63 @@
+package ocelot.desktop.util
+
+import scala.collection.mutable.ArrayBuffer
+
+class Transaction protected() {
+ private val cleanupCallbacks = ArrayBuffer.empty[() => Unit]
+
+ def onFailure(f: => Unit): Unit = {
+ cleanupCallbacks += f _
+ }
+
+ protected def runCleanupCallbacks(exc: Exception): exc.type = {
+ for (callback <- cleanupCallbacks) {
+ try {
+ callback()
+ } catch {
+ case cleanupExc: Exception =>
+ exc.addSuppressed(cleanupExc)
+ }
+ }
+
+ exc
+ }
+}
+
+object Transaction {
+ private[Transaction] case class AbortedException(tx: AbortableTransaction) extends RuntimeException
+
+ class AbortableTransaction protected[Transaction]() extends Transaction {
+ def abort(): Nothing = {
+ throw Transaction.AbortedException(this)
+ }
+ }
+
+ def run[T](f: Transaction => T): T = {
+ val tx = new Transaction
+
+ try {
+ f(tx)
+ } catch {
+ case exc: Exception => throw tx.runCleanupCallbacks(exc)
+ }
+ }
+
+ def runAbortable[T](f: AbortableTransaction => T): Option[T] = {
+ val tx = new AbortableTransaction
+
+ try {
+ Some(f(tx))
+ } catch {
+ case exc @ AbortedException(`tx`) =>
+ val suppressed = tx.runCleanupCallbacks(exc).getSuppressed
+
+ if (suppressed.nonEmpty) {
+ throw exc
+ }
+
+ None
+ case exc: Exception =>
+ throw tx.runCleanupCallbacks(exc)
+ }
+ }
+}
diff --git a/src/main/scala/ocelot/desktop/util/WebcamCapture.scala b/src/main/scala/ocelot/desktop/util/WebcamCapture.scala
index b37b2aa..d5558ed 100644
--- a/src/main/scala/ocelot/desktop/util/WebcamCapture.scala
+++ b/src/main/scala/ocelot/desktop/util/WebcamCapture.scala
@@ -1,6 +1,6 @@
package ocelot.desktop.util
-import com.github.sarxos.webcam.{Webcam}
+import com.github.sarxos.webcam.Webcam
import java.awt.Color
import java.awt.image.BufferedImage
@@ -14,23 +14,24 @@ object WebcamCapture extends Logging {
private final val InvGamma: Float = 1f / 2.2f
private val instances = new mutable.HashMap[String, WebcamCapture]
- def getInstance(name: String): WebcamCapture = {
+ def getInstance(name: String): Option[WebcamCapture] = {
if (instances.contains(name))
- return instances(name)
+ return Some(instances(name))
- val webcam = Webcam.getWebcamByName(name)
- if (webcam == null) {
- logger.warn(s"No such webcam: $name")
- return getDefault
+ Option(Webcam.getWebcamByName(name)) match {
+ case Some(webcam) =>
+ val webcamCapture = new WebcamCapture(webcam)
+ instances(name) = webcamCapture
+ Some(webcamCapture)
+
+ case None =>
+ logger.warn(s"No such webcam: $name")
+ None
}
-
- val webcamCapture = new WebcamCapture(webcam)
- instances(name) = webcamCapture
- webcamCapture
}
- def getInstance(webcam: Webcam): WebcamCapture = getInstance(webcam.getName)
- def getDefault: WebcamCapture = getInstance(Webcam.getDefault)
+ def getInstance(webcam: Webcam): Option[WebcamCapture] = getInstance(webcam.getName)
+ def getDefault: Option[WebcamCapture] = Option(Webcam.getDefault).flatMap(getInstance)
def cleanup(): Unit = {
for (instance <- instances.values)
@@ -40,7 +41,7 @@ object WebcamCapture extends Logging {
instance.join()
}
- def toLinear(value: Int): Float = Math.pow(value / 255f, InvGamma).toFloat
+ private def toLinear(value: Int): Float = Math.pow(value / 255f, InvGamma).toFloat
}
class WebcamCapture(private val webcam: Webcam) extends Thread(s"WebcamCaptureThread-${webcam.getName}") with Logging {
diff --git a/src/main/scala/ocelot/desktop/util/package.scala b/src/main/scala/ocelot/desktop/util/package.scala
index c596766..5b9d54c 100644
--- a/src/main/scala/ocelot/desktop/util/package.scala
+++ b/src/main/scala/ocelot/desktop/util/package.scala
@@ -3,7 +3,7 @@ package ocelot.desktop
import java.util.concurrent.locks.Lock
package object util {
- final def withLockAcquired[T](lock: Lock, f: => T): T = {
+ final def withLockAcquired[T](lock: Lock)(f: => T): T = {
lock.lock()
try {
diff --git a/src/main/scala/ocelot/desktop/windows/CameraWindow.scala b/src/main/scala/ocelot/desktop/windows/CameraWindow.scala
index 00b875d..12404a7 100644
--- a/src/main/scala/ocelot/desktop/windows/CameraWindow.scala
+++ b/src/main/scala/ocelot/desktop/windows/CameraWindow.scala
@@ -19,7 +19,7 @@ class CameraWindow(cameraNode: CameraNode) extends BasicWindow {
children :+= new PaddingBox(new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- val label = new Label {
+ private val label = new Label {
override def text: String = s"Camera — ${cameraNode.labelOrAddress}"
override val isSmall: Boolean = true
}
@@ -29,12 +29,15 @@ class CameraWindow(cameraNode: CameraNode) extends BasicWindow {
override def minimumSize: Size2D = Size2D(256, 24)
override def maximumSize: Size2D = Size2D(512, 24)
override def size: Size2D = Size2D(label.size.width - 8, 24)
- override def text: String = camera.webcamCapture.name
+ override def text: String = camera.webcamCapture.map(_.name).getOrElse("")
override def onClick(): Unit = {
val menu = new ContextMenu
- for (webcam <- Webcam.getWebcams().asScala)
- menu.addEntry(new ContextMenuEntry(webcam.getName, () => cameraNode.camera.webcamCapture = WebcamCapture.getInstance(webcam)))
+ for (webcam <- Webcam.getWebcams().asScala) {
+ menu.addEntry(ContextMenuEntry(webcam.getName) {
+ cameraNode.camera.webcamCapture = WebcamCapture.getInstance(webcam)
+ })
+ }
root.get.contextMenus.open(menu, Vector2D(position.x, position.y + size.height))
}
diff --git a/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala b/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala
index 19f5c91..498afe0 100644
--- a/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala
+++ b/src/main/scala/ocelot/desktop/windows/ComputerWindow.scala
@@ -7,7 +7,7 @@ import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.nodes.ComputerNode
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
import ocelot.desktop.ui.widget._
-import ocelot.desktop.ui.widget.tooltip.Tooltip
+import ocelot.desktop.ui.widget.tooltip.{LabelTooltip, Tooltip}
import ocelot.desktop.ui.widget.window.BasicWindow
import ocelot.desktop.util.animation.UnitAnimation
import ocelot.desktop.util.{DrawUtils, Orientation}
@@ -15,101 +15,20 @@ import ocelot.desktop.{ColorScheme, OcelotDesktop}
import totoro.ocelot.brain.entity.Case
import totoro.ocelot.brain.nbt.NBTTagCompound
+import scala.collection.immutable.ArraySeq
+
class ComputerWindow(computerNode: ComputerNode) extends BasicWindow {
+ private var bottomDrawer: Widget = _
+
def computer: Case = computerNode.computer
- def updateSlots(): Unit = {
- eepromBox.children(0) = computerNode.eepromSlot
- inner.children(1).children(2) = slotsWidget
+ def reloadWindow(): Unit = {
+ children = makeChildren()
}
- private val eepromBox = new PaddingBox(computerNode.eepromSlot, Padding2D(right = 10))
private val bottomDrawerAnimation = UnitAnimation.easeInOutQuad(0.2f)
bottomDrawerAnimation.goDown()
- private val bottomDrawer = new Widget {
- override def shouldClip: Boolean = true
- override def minimumSize: Size2D = Size2D(layout.minimumSize.width, 0)
-
- children :+= new PaddingBox(new Widget {
- children :+= new PaddingBox(new Widget {
- override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
-
- class PerTickHistogram extends Histogram {
- private var lastTick = OcelotDesktop.ticker.tick
-
- def tickUpdate(): Unit = {}
-
- def reset(): Unit = {
- text = "N/A"
- history = Seq.fill(21)(0.0f).toArray
- }
-
- override def update(): Unit = {
- super.update()
- val curTick = OcelotDesktop.ticker.tick
- if (curTick > lastTick) {
- if (!computer.machine.isRunning) reset()
- lastTick = curTick
- tickUpdate()
- }
- }
- }
-
- children :+= new PerTickHistogram {
- override def tickUpdate(): Unit = {
- val (free, total) = computer.machine.latestMemoryUsage
- val used = total - free
-
- val ratio = if (total == 0) 0 else used.toFloat / total.toFloat
-
- text = f"${used.toFloat / 1024f / 1024f}%.1fM"
- history = history.slice(1, history.length) ++ Array(ratio)
- }
- override protected val tooltip: Option[Tooltip] = Some(
- new LabelTooltip("Memory usage (in Mb)")
- )
- }
-
- children :+= new PaddingBox(new PerTickHistogram {
- override def tickUpdate(): Unit = {
- val (start, _end) = computer.machine.latestExecutionInfo
- val end = if (start < _end) _end else System.nanoTime()
- val cpuUsage = if (start == 0) 0 else ((end - start).toFloat / 1000000000f * OcelotDesktop.tpsCounter.fps).min(1f)
- text = f"${cpuUsage * 100}%.0f%%"
- history = history.slice(1, history.length) ++ Array(cpuUsage)
- }
- override protected val tooltip: Option[Tooltip] = Some(
- new LabelTooltip("CPU usage (in %)")
- )
- }, Padding2D(top = 8))
-
- children :+= new PaddingBox(new PerTickHistogram {
- override def tickUpdate(): Unit = {
- val (budget, maxBudget) = computer.machine.latestCallBudget
- text = f"${maxBudget - budget}%.1f"
- history = history.slice(1, history.length) ++ Array((1.0 - budget / maxBudget).toFloat)
- }
- override protected val tooltip: Option[Tooltip] = Some(
- new LabelTooltip("CPU usage (call budget)")
- )
- }, Padding2D(top = 8))
- }, Padding2D.equal(7))
-
- override def draw(g: Graphics): Unit = {
- DrawUtils.panel(g, position.x, position.y, width, height)
- super.draw(g)
- }
- }, Padding2D(10, 12, 0, 12))
-
- override def draw(g: Graphics): Unit = {
- if (height < 1) return
-
- super.draw(g)
- g.rect(position.x + 6, position.y + 2, width - 12, 2, ColorScheme("BottomDrawerBorder"))
- }
- }
-
private val drawerButton = new IconButton(
"buttons/BottomDrawerOpen", "buttons/BottomDrawerClose",
isSwitch = true, tooltip = Some("Toggle computer usage histogram")
@@ -118,69 +37,169 @@ class ComputerWindow(computerNode: ComputerNode) extends BasicWindow {
override def onReleased(): Unit = bottomDrawerAnimation.goDown()
}
- private val inner = new Widget {
- override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
+ private def makeChildren(): ArraySeq[Widget] = {
+ var children = ArraySeq.empty[Widget]
- children :+= new PaddingBox(new Label {
- override def text: String = computerNode.labelOrAddress.take(36)
- override def isSmall: Boolean = true
- override def color: Color = ColorScheme("ComputerAddress")
- }, Padding2D(bottom = 8))
+ val slotsWidget = {
+ val rows = Array(
+ (19, computerNode.cardSlots),
+ (8, Array(computerNode.cpuSlot) ++ computerNode.memorySlots),
+ (8, computerNode.diskSlots ++ computerNode.floppySlot.toArray)
+ )
- children :+= new Widget {
- children :+= new PaddingBox(drawerButton, Padding2D(top = 120))
+ new Widget {
+ for ((padding, row) <- rows) {
+ children :+= new PaddingBox(new Widget {
+ override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
+ for (slot <- row) children :+= slot
+ }, Padding2D(left = padding.toFloat, top = 8))
+ }
+
+ override def minimumSize: Size2D = Size2D(158, 140)
+
+ override def draw(g: Graphics): Unit = {
+ g.sprite("ComputerMotherboard", bounds.mapX(_ - 4).mapW(_ - 4))
+ drawChildren(g)
+ }
+ }
+ }
+
+ val inner = new Widget {
+ override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
+
+ children :+= new PaddingBox(new Label {
+ override def text: String = computerNode.labelOrAddress.take(36)
+
+ override def isSmall: Boolean = true
+
+ override def color: Color = ColorScheme("ComputerAddress")
+ }, Padding2D(bottom = 8))
+
+ children :+= new Widget {
+ children :+= new PaddingBox(drawerButton, Padding2D(top = 120))
+
+ children :+= new PaddingBox(new Widget {
+ children :+= new PaddingBox(computerNode.eepromSlot, Padding2D(right = 10))
+
+ children :+= new IconButton(
+ "buttons/PowerOff",
+ "buttons/PowerOn",
+ isSwitch = true,
+ sizeMultiplier = 2
+ ) {
+ override def isOn: Boolean = computer.machine.isRunning
+
+ override def onPressed(): Unit = computerNode.turnOn()
+
+ override def onReleased(): Unit = computerNode.turnOff()
+
+ protected override def clickSoundSource: SoundSource = SoundSources.MinecraftClick
+ }
+ }, Padding2D(top = 44, left = 22))
+
+ children :+= slotsWidget
+ }
+ }
+
+ bottomDrawer = new Widget {
+ override def shouldClip: Boolean = true
+
+ override def minimumSize: Size2D = Size2D(layout.minimumSize.width, 0)
children :+= new PaddingBox(new Widget {
- children :+= new PaddingBox(computerNode.eepromSlot, Padding2D(right = 10))
-
- children :+= new IconButton(
- "buttons/PowerOff",
- "buttons/PowerOn",
- isSwitch = true,
- sizeMultiplier = 2
- ) {
- override def isOn: Boolean = computer.machine.isRunning
- override def onPressed(): Unit = computerNode.turnOn()
- override def onReleased(): Unit = computerNode.turnOff()
- protected override def clickSoundSource: SoundSource = SoundSources.MinecraftClick
- }
- }, Padding2D(top = 44, left = 22))
-
- children :+= slotsWidget
- }
- }
-
- children :+= new PaddingBox(new Widget {
- override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
-
- children :+= new PaddingBox(inner, Padding2D(10, 12, 0, 12))
- children :+= bottomDrawer
- }, Padding2D(bottom = 10))
-
- private def slotsWidget: Widget = {
- val rows = Array(
- (19, computerNode.cardSlots),
- (8, Array(computerNode.cpuSlot) ++ computerNode.memorySlots),
- (8, computerNode.diskSlots ++ computerNode.floppySlot.toArray)
- )
-
- new Widget {
- for ((padding, row) <- rows) {
children :+= new PaddingBox(new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
- for (slot <- row) children :+= slot
- }, Padding2D(left = padding, top = 8))
- }
- override def minimumSize: Size2D = Size2D(158, 140)
+ class PerTickHistogram extends Histogram {
+ private var lastTick = OcelotDesktop.ticker.tick
+
+ def tickUpdate(): Unit = {}
+
+ def reset(): Unit = {
+ text = "N/A"
+ history = Seq.fill(21)(0.0f).toArray
+ }
+
+ override def update(): Unit = {
+ super.update()
+ val curTick = OcelotDesktop.ticker.tick
+ if (curTick > lastTick) {
+ if (!computer.machine.isRunning) reset()
+ lastTick = curTick
+ tickUpdate()
+ }
+ }
+ }
+
+ children :+= new PerTickHistogram {
+ override def tickUpdate(): Unit = {
+ val (free, total) = computer.machine.latestMemoryUsage
+ val used = total - free
+
+ val ratio = if (total == 0) 0 else used.toFloat / total.toFloat
+
+ text = f"${used.toFloat / 1024f / 1024f}%.1fM"
+ history = history.slice(1, history.length) ++ Array(ratio)
+ }
+
+ override protected val tooltip: Option[Tooltip] = Some(
+ new LabelTooltip("Memory usage (in Mb)")
+ )
+ }
+
+ children :+= new PaddingBox(new PerTickHistogram {
+ override def tickUpdate(): Unit = {
+ val (start, _end) = computer.machine.latestExecutionInfo
+ val end = if (start < _end) _end else System.nanoTime()
+ val cpuUsage = if (start == 0) 0 else ((end - start).toFloat / 1000000000f * OcelotDesktop.tpsCounter.fps).min(1f)
+ text = f"${cpuUsage * 100}%.0f%%"
+ history = history.slice(1, history.length) ++ Array(cpuUsage)
+ }
+
+ override protected val tooltip: Option[Tooltip] = Some(
+ new LabelTooltip("CPU usage (in %)")
+ )
+ }, Padding2D(top = 8))
+
+ children :+= new PaddingBox(new PerTickHistogram {
+ override def tickUpdate(): Unit = {
+ val (budget, maxBudget) = computer.machine.latestCallBudget
+ text = f"${maxBudget - budget}%.1f"
+ history = history.slice(1, history.length) ++ Array((1.0 - budget / maxBudget).toFloat)
+ }
+
+ override protected val tooltip: Option[Tooltip] = Some(
+ new LabelTooltip("CPU usage (call budget)")
+ )
+ }, Padding2D(top = 8))
+ }, Padding2D.equal(7))
+
+ override def draw(g: Graphics): Unit = {
+ DrawUtils.panel(g, position.x, position.y, width, height)
+ super.draw(g)
+ }
+ }, Padding2D(10, 12, 0, 12))
override def draw(g: Graphics): Unit = {
- g.sprite("ComputerMotherboard", bounds.mapX(_ - 4).mapW(_ - 4))
- drawChildren(g)
+ if (height < 1) return
+
+ super.draw(g)
+ g.rect(position.x + 6, position.y + 2, width - 12, 2, ColorScheme("BottomDrawerBorder"))
}
}
+
+ children :+= new PaddingBox(new Widget {
+ override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
+
+ children :+= new PaddingBox(inner, Padding2D(10, 12, 0, 12))
+ children :+= bottomDrawer
+ }, Padding2D(bottom = 10))
+
+ children
}
+ children = makeChildren()
+
override def update(): Unit = {
super.update()
bottomDrawerAnimation.update()
diff --git a/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala b/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala
index 92f906d..ff82050 100644
--- a/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala
+++ b/src/main/scala/ocelot/desktop/windows/OpenFMRadioWindow.scala
@@ -54,7 +54,7 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow {
)
}
- addVolumeUpOrDownButton(false, 0)
+ addVolumeUpOrDownButton(isUp = false, 0)
// Volume label
children :+= new PaddingBox(
@@ -66,7 +66,7 @@ class OpenFMRadioWindow(radioNode: OpenFMRadioNode) extends BasicWindow {
Padding2D(2, 0, 0, 20)
)
- addVolumeUpOrDownButton(true, 8)
+ addVolumeUpOrDownButton(isUp = true, 8)
// Redstone button
children :+= new PaddingBox(
diff --git a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala
index 4fb89ef..b2b0448 100644
--- a/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala
+++ b/src/main/scala/ocelot/desktop/windows/ScreenWindow.scala
@@ -9,6 +9,7 @@ import ocelot.desktop.ui.event.sources.{KeyEvents, MouseEvents}
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent, ScrollEvent}
import ocelot.desktop.ui.widget.window.BasicWindow
import ocelot.desktop.util.{DrawUtils, Logging}
+import ocelot.desktop.windows.ScreenWindow.{BorderBottom, BorderHorizontal, BorderLeft, BorderRight, BorderTop, BorderVertical}
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import org.apache.commons.lang3.StringUtils
import org.lwjgl.input.Keyboard
@@ -44,7 +45,10 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
private def screenHeight: Int = screen.getHeight
- override def minimumSize: Size2D = Size2D(screenWidth * fontWidth * scaleX + 32, screenHeight * scaleY * fontHeight + 36)
+ override def minimumSize: Size2D = Size2D(
+ screenWidth * fontWidth * scaleX + BorderHorizontal,
+ screenHeight * scaleY * fontHeight + BorderVertical
+ )
override def receiveScrollEvents: Boolean = true
@@ -102,6 +106,11 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
val sx = point.x - mousePos.x
val sy = point.y - mousePos.y
+ val uiScale = UiHandler.scalingFactor
+ var newScale = scale
+
+ // TODO: refactor this mess, make it consider both sizes and not have two nearby slightly off "snap points"
+
if (sx.abs > sy.abs) {
val newWidth = startingWidth - sx
val maxWidth = screenWidth * fontWidth
@@ -110,15 +119,10 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
if (!KeyEvents.isDown(Keyboard.KEY_LSHIFT) && scale <= 1.001)
midScale = midScale.min(1f)
- var lowScale = (fontWidth * midScale).floor / fontWidth
- val highScale = (fontWidth * midScale).ceil / fontWidth
+ val lowScale = (fontWidth * midScale * uiScale).floor / fontWidth / uiScale
+ val highScale = (fontWidth * midScale * uiScale).ceil / fontWidth / uiScale
- // enforce minimal screen size
- if (lowScale < 0.3f) {
- lowScale = 0.25f
- }
-
- scale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
+ newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
}
else {
val newHeight = startingWidth * (screenHeight * fontHeight / screenWidth / fontWidth) - sy
@@ -128,9 +132,18 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
if (!KeyEvents.isDown(Keyboard.KEY_LSHIFT) && scale <= 1.001)
midScale = midScale.min(1f)
- val lowScale = (fontHeight * midScale).floor / fontHeight
- val highScale = (fontHeight * midScale).ceil / fontHeight
- scale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
+ val lowScale = (fontHeight * midScale * uiScale).floor / fontHeight / uiScale
+ val highScale = (fontHeight * midScale * uiScale).ceil / fontHeight / uiScale
+
+ newScale = if (midScale - lowScale > highScale - midScale) highScale else lowScale
+ }
+
+ if (newScale != scale)
+ scale = newScale
+
+ // enforce minimal screen size
+ if (scale <= 0.249f) {
+ scale = 0.25f
}
}
@@ -152,8 +165,8 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
override def show(): Unit = {
scale = math.min(
- ((UiHandler.root.width - 16) / (screenWidth * fontWidth + 32)).min(1f).max(0f),
- ((UiHandler.root.height - 32) / (screenHeight * fontHeight + 36)).min(1f).max(0f)
+ ((UiHandler.root.width - BorderLeft) / (screenWidth * fontWidth + BorderHorizontal)).min(1f).max(0f),
+ ((UiHandler.root.height - BorderTop) / (screenHeight * fontHeight + BorderVertical)).min(1f).max(0f)
)
super.show()
@@ -164,25 +177,32 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
private def convertMousePos(p: Vector2D): Vector2D = {
if (screenNode.screen.getPrecisionMode) {
Vector2D(
- (p.x - 16f - position.x) / fontWidth / scaleX,
- (p.y - 16f - position.y) / fontHeight / scaleY
+ (p.x - BorderLeft - position.x) / fontWidth / scaleX,
+ (p.y - BorderTop - position.y) / fontHeight / scaleY
)
} else {
Vector2D(
- math.floor((p.x - 16f - position.x) / fontWidth / scaleX),
- math.floor((p.y - 16f - position.y) / fontHeight / scaleY)
+ math.floor((p.x - BorderLeft - position.x) / fontWidth / scaleX),
+ math.floor((p.y - BorderTop - position.y) / fontHeight / scaleY)
)
}
}
override protected def dragRegions: Iterator[Rect2D] = Iterator(
- Rect2D(position.x, position.y, size.width, 20),
- Rect2D(position.x, position.y + size.height - 16, size.width - 16, 16),
- Rect2D(position.x, position.y, 16, size.height),
- Rect2D(position.x + size.width - 16, position.y, 16, size.height - 16),
+ Rect2D(position.x, position.y, size.width, BorderTop.toFloat),
+ Rect2D(position.x, position.y, BorderLeft.toFloat, size.height),
+
+ // these two must not include `scaleDragRegion`
+ Rect2D(position.x, position.y + size.height - BorderBottom, size.width - BorderRight, BorderBottom.toFloat),
+ Rect2D(position.x + size.width - BorderRight, position.y, BorderRight.toFloat, size.height - BorderBottom),
)
- private def scaleDragRegion: Rect2D = Rect2D(position.x + size.width - 16, position.y + size.height - 16, 16, 16)
+ private def scaleDragRegion: Rect2D = Rect2D(
+ position.x + size.width - BorderRight,
+ position.y + size.height - BorderBottom,
+ BorderRight.toFloat,
+ BorderBottom.toFloat,
+ )
override def update(): Unit = {
super.update()
@@ -210,8 +230,8 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
override def draw(g: Graphics): Unit = {
beginDraw(g)
- val startX = position.x + 16
- val startY = position.y + 20
+ val startX = position.x + BorderLeft
+ val startY = position.y + BorderTop
val windowWidth = screenWidth * fontWidth * scaleX
val windowHeight = screenHeight * fontHeight * scaleY
@@ -263,3 +283,13 @@ class ScreenWindow(screenNode: ScreenNode) extends BasicWindow with Logging {
endDraw(g)
}
}
+
+object ScreenWindow {
+ private val BorderTop = 20
+ private val BorderLeft = 16
+ private val BorderRight = 16
+ private val BorderBottom = 16
+
+ private val BorderVertical = BorderTop + BorderBottom
+ private val BorderHorizontal = BorderLeft + BorderRight
+}