Merge branch 'develop'

This commit is contained in:
Fingercomp 2023-06-14 21:34:54 +07:00
commit 966b526f1a
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
144 changed files with 4502 additions and 1866 deletions

View File

@ -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}\"}"

213
README.md
View File

@ -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 <kbd>Shift</kbd> 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 <kbd>Ctrl</kbd> 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/

BIN
assets/addon-showcase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
assets/gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
assets/perf-graphs.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

BIN
assets/sound-card.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
assets/tps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
assets/window-scale.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

View File

@ -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"

@ -1 +1 @@
Subproject commit 42dd5a966456654968af777d625c3e2f8b9265fa
Subproject commit fbea0b4295d3866f7bff71b442afce0a859e3712

View File

@ -1 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

View File

@ -1 +1,2 @@
sbt.version = 1.7.1
# suppress inspection "UnusedProperty" for whole file
sbt.version = 1.8.3

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

@ -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
SoundCardWire7 = #000000
ErrorMessage = #ff3366

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -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

View File

@ -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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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")
}
}
}
}

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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] =

View File

@ -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)

View File

@ -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

View File

@ -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
)

View File

@ -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))
}
}

View File

@ -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 = {

View File

@ -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)")
}
}

View File

@ -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]) = {

View File

@ -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)")
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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[_, _]]
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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("<empty>"),
)
logger.error(
"The entity if the slot: " +
brainInventory.inventory(slotIndex)
.get
.map(entity => s"$entity (class ${entity.getClass.getName})")
.getOrElse("<empty>"),
)
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
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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(_)))
}
}

View File

@ -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
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}

View File

@ -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(_)))
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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"
})
}
}

View File

@ -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 = {}
}

View File

@ -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())
},
)
}
}

View File

@ -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)
}

View File

@ -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)
}))
})
}

View File

@ -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)
}

View File

@ -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 = {

View File

@ -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)

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
})
}
}
})

View File

@ -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))
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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))

View File

@ -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 = {}
}

View File

@ -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}

View File

@ -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

View File

@ -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 = {}
}

View File

@ -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)

View File

@ -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 })
}
})
}

View File

@ -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)
}

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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])

View File

@ -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
)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
)

View File

@ -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
/**

Some files were not shown because too many files have changed in this diff Show More