Merge branch 'develop'
@ -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:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: normal
|
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_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"
|
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:
|
stages:
|
||||||
key: "$CI_COMMIT_REF_SLUG"
|
- build
|
||||||
paths:
|
- upload
|
||||||
- "sbt-cache"
|
- deploy
|
||||||
- "target/streams"
|
- release
|
||||||
- "lib/ocelot-brain/target"
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- sbt -v sbtVersion
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
|
before_script:
|
||||||
|
- sbt -v sbtVersion
|
||||||
|
tags:
|
||||||
|
- public
|
||||||
script:
|
script:
|
||||||
- sbt assembly
|
- sbt assembly
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- target/scala-2.13/ocelot-desktop.jar
|
- 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:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
before_script: []
|
before_script: []
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "develop"
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_BRANCH == "develop"
|
||||||
|
tags:
|
||||||
|
- public
|
||||||
script:
|
script:
|
||||||
- rm -rf public
|
- rm -rf public
|
||||||
- mkdir public
|
- mkdir public
|
||||||
@ -36,3 +57,17 @@ pages:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- 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
@ -3,131 +3,188 @@
|
|||||||
|
|
||||||
A desktop version of the renowned OpenComputers emulator Ocelot.
|
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
|
## Why
|
||||||
You might already be happy with your choice of an OC emulator; after all, there
|
You might already be happy with your choice of an OC emulator;
|
||||||
is already a plenty of them.
|
there is a plenty of them, after all.
|
||||||
So why would you want to reconsider your life choices now all of a sudden?
|
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.
|
A fine question, indeed; perhaps, a list of features will persuade you.
|
||||||
|
|
||||||
### Powered by ocelot-brain
|
### Powered by ocelot-brain
|
||||||
At the heart of this emulator is [ocelot-brain][ocelot-brain] (uh, don't ask me),
|
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
|
which is essentially the source code of OpenComputers decoupled of everything
|
||||||
Minecraft-specific and packaged as a Scala library.
|
Minecraft-specific and repurposed as a Scala library.
|
||||||
This makes Ocelot Desktop **the most accurate emulator** ever made.
|
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.
|
similar timings.
|
||||||
The performance and memory constraints present in OC are also emulated.
|
The performance and memory constraints present in OC are also emulated.
|
||||||
|
|
||||||
### Customizable setups
|
### 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
|
- graphics cards
|
||||||
- all kinds of network cards (wired, wireless)
|
- network cards (wired, wireless)
|
||||||
- a linked card
|
- linked cards
|
||||||
- an internet card
|
- internet cards
|
||||||
- a sound card (Computronics)
|
- redstone cards (including the second tier!)
|
||||||
- a redstone card in the both tiers
|
- data cards
|
||||||
- a data card (again, you can pick any of the three tiers)
|
|
||||||
- hard disks
|
- 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
|
Feel limited by the vanilla cards?
|
||||||
OpenComputers, to avoid building impossible configurations.
|
No problem — we've even integrated some components from popular addons!
|
||||||
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.
|
|
||||||
|
|
||||||
If one computer is not enough, you can add another one.
|
![Addon showcase][addon-showcase]
|
||||||
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.
|
|
||||||
|
|
||||||
Or, instead of employing an army of computers, you might want to connect a dozen
|
- **Computronics:**
|
||||||
of screens to a single machine, like in the movies.
|
- `computer.beep()` does not excite your music sense enough?
|
||||||
No problem — we've got that covered, too.
|
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]
|
![GUI][gui]
|
||||||
|
|
||||||
A slick interface allows you to customize the setup to your liking.
|
Manage your screen real estate to avoid distractions.
|
||||||
Add more computers, organize the connections between components, build complex
|
All nodes are draggable, as are windows.
|
||||||
setups, and manage your screen real estate to avoid distractions.
|
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
|
![Window scaling][window-scaling]
|
||||||
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.
|
|
||||||
|
|
||||||
The emulator uses hardware acceleration to offload the daunting task of
|
Many additional options are hidden in the context menus —
|
||||||
rendering its interface to a specialized device, so make sure you have a OpenGL
|
try hitting the right mouse button on various things.
|
||||||
2.1-capable graphics card.
|
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
|
### Persistable workspaces
|
||||||
It would be sad if, after all the hard work you put into adjusting the
|
Imagine putting many hours into wiring things up only to have to do it all from
|
||||||
workspace, you have to do that again.
|
scratch the next time you open the emulator.
|
||||||
|
That... would be sad and disappointing.
|
||||||
I mean, OpenComputers can persist its machines just fine, right?
|
I mean, OpenComputers can persist its machines just fine, right?
|
||||||
By basing the emulator on its code, we've essentially inherited the ability
|
Good news: by reusing its code, we've essentially inherited the ability
|
||||||
to save workspaces on the disk and load them afterwards.
|
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
|
Just in case, Ocelot Desktop will warn you if you smash the quit button without
|
||||||
saving.
|
saving.
|
||||||
|
We'd rather you didn't feel sad and disappointed.
|
||||||
|
|
||||||
### Cool features
|
### Cool features
|
||||||
![Performance graphs][graphs]
|
![Performance graphs][perf-graphs]
|
||||||
![Sound card GUI][sound-card]
|
![Sound card GUI][sound-card]
|
||||||
|
|
||||||
A few smaller features are worth mentioning, too:
|
A few smaller features are worth mentioning, too:
|
||||||
|
|
||||||
- Screens are resizeable — drag the bottom-right corner.
|
- Windows show the corresponding block's address by default.
|
||||||
- Windows are labeled with the corresponding block's address; however, you can
|
However, you can relabel them: look for the option in the context menu.
|
||||||
set a custom label — look for an option in the context menu.
|
- The button ![][drawer-button] at the bottom of a computer case window
|
||||||
- The button in a computer case window shows the performance graphs:
|
shows performance graphs: the memory, processor time, and call budget.
|
||||||
the used memory, processor time and call budget.
|
- Hold the <kbd>Ctrl</kbd> key while dragging blocks to snap them to the grid.
|
||||||
- Hold the Ctrl key while dragging blocks to have them snap to the grid.
|
|
||||||
|
|
||||||
## Download
|
## 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?
|
[**Download** the latest build][download] / [mirror][download-mirror]
|
||||||
Just import the project in your favorite IDE.
|
|
||||||
Make sure to have Scala and SBT installed (manually or through IDE).
|
|
||||||
|
|
||||||
Ocelot Brain library is added as a Git submodule, so do not forget to fetch it too.
|
## Hacking
|
||||||
Something like `git submodule update --init --recursive` should do the trick.
|
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.
|
We include [ocelot-brain][] as a Git submodule: don't forget to fetch it!
|
||||||
(It will appear at `target/scala-2.13/ocelot-desktop.jar` location.)
|
|
||||||
|
|
||||||
If the compiler is complaining about missing BuildInfo class, or the version in
|
```sh
|
||||||
the window title / logs looks outdated, use `sbt buildInfo` to generate fresh class.
|
$ 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
|
## Credits
|
||||||
- **LeshaInc**, the author and maintainer of Ocelot Desktop.
|
- **LeshaInc:** the original author of Ocelot Desktop.
|
||||||
- **Totoro**, the creator of ocelot-brain and ocelot-online.
|
- **Totoro:** the creator of [ocelot-brain][] and [ocelot.online][ocelot-online].
|
||||||
- **bpm140**, who created marvelous Ocelot Desktop landing page.
|
- **bpm140:** produced the marvelous Ocelot Desktop landing page.
|
||||||
- **rason**, who stirred the development at the critical moment.
|
- **rason:** stirred the development at the critical moment!
|
||||||
- **NE0**, the bug extermination specialist.
|
- **NE0:** the bug extermination specialist.
|
||||||
- **ECS**, who fearlessly jumped right into Scala jungle.
|
- **ECS:** leaped fearlessly into the Scala jungle.
|
||||||
- **fingercomp**, who wrote this README.
|
- **Smok1e:** added some light and color with new components from Computronics.
|
||||||
|
- **Saphire:** scaled the UI for HiDPI screens.
|
||||||
|
- **fingercomp:** wrote this README.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
- [Ocelot Desktop][ocelot-desktop] web page (link to the latest build, FAQ)
|
- The [Ocelot Desktop][ocelot-desktop] web page
|
||||||
- [ocelot.online][ocelot-online], a rudimentary web version of Ocelot
|
(links to the latest build, FAQ).
|
||||||
- [ocelot-brain][ocelot-brain], the backend library of Ocelot Desktop
|
- [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
|
- [#cc.ru on IRC][irc] if you have any questions
|
||||||
(Russian, but we're fine with English too)
|
(Russian, but we're fine with English too).
|
||||||
- [Discord][discord] if you prefer so (we will not judge)
|
- 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]: https://cc-ru.gitlab.io/ocelot/ocelot-desktop/ocelot.jar
|
||||||
[download-mirror]: https://ocelot.fomalhaut.me/ocelot.jar
|
[download-mirror]: https://ocelot.fomalhaut.me/ocelot.jar
|
||||||
[gui]: https://i.imgur.com/O4bF7I8.png
|
[addon-showcase]: ./assets/addon-showcase.png "A workspace with colorful lamps and an OpenFM radio."
|
||||||
[graphs]: https://i.imgur.com/mG8UjhV.png
|
[gui]: ./assets/gui.png "A screenshot of the GUI."
|
||||||
[sound-card]: https://i.imgur.com/gnh3D6N.png
|
[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-brain]: https://gitlab.com/cc-ru/ocelot/ocelot-brain
|
||||||
[ocelot-online]: https://ocelot.fomalhaut.me/
|
[ocelot-online]: https://ocelot.fomalhaut.me/
|
||||||
[ocelot-desktop]: https://ocelot.fomalhaut.me/desktop/
|
[ocelot-desktop]: https://ocelot.fomalhaut.me/desktop/
|
||||||
|
|||||||
BIN
assets/addon-showcase.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/banner.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
assets/gui.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/perf-graphs.gif
Normal file
|
After Width: | Height: | Size: 648 KiB |
BIN
assets/sound-card.gif
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/tps.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/window-scale.gif
Normal file
|
After Width: | Height: | Size: 957 KiB |
@ -1,6 +1,6 @@
|
|||||||
name := "ocelot-desktop"
|
name := "ocelot-desktop"
|
||||||
version := "1.5.0"
|
version := "1.7.1"
|
||||||
scalaVersion := "2.13.8"
|
scalaVersion := "2.13.10"
|
||||||
|
|
||||||
lazy val root = project.in(file("."))
|
lazy val root = project.in(file("."))
|
||||||
.dependsOn(brain % "compile->compile")
|
.dependsOn(brain % "compile->compile")
|
||||||
@ -16,6 +16,8 @@ lazy val root = project.in(file("."))
|
|||||||
|
|
||||||
lazy val brain = ProjectRef(file("lib/ocelot-brain"), "ocelot-brain")
|
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-core" % "2.20.0"
|
||||||
libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0"
|
libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0"
|
||||||
libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "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"
|
libraryDependencies += "com.github.sarxos" % "webcam-capture" % "0.3.12"
|
||||||
|
|
||||||
// For OpenFM
|
// For OpenFM
|
||||||
libraryDependencies += "javazoom" % "jlayer" % "1.0.1"
|
|
||||||
libraryDependencies += "com.googlecode.soundlibs" % "mp3spi" % "1.9.5.4"
|
libraryDependencies += "com.googlecode.soundlibs" % "mp3spi" % "1.9.5.4"
|
||||||
libraryDependencies += "com.googlecode.soundlibs" % "vorbisspi" % "1.0.3.3"
|
libraryDependencies += "com.googlecode.soundlibs" % "vorbisspi" % "1.0.3.3"
|
||||||
|
|
||||||
@ -42,4 +43,4 @@ assembly / assemblyMergeStrategy := {
|
|||||||
case _ => MergeStrategy.first
|
case _ => MergeStrategy.first
|
||||||
}
|
}
|
||||||
|
|
||||||
assembly / assemblyJarName := s"ocelot-desktop.jar"
|
assembly / assemblyJarName := "ocelot-desktop.jar"
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 42dd5a966456654968af777d625c3e2f8b9265fa
|
Subproject commit fbea0b4295d3866f7bff71b442afce0a859e3712
|
||||||
@ -1 +1 @@
|
|||||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
|
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
sbt.version = 1.7.1
|
# suppress inspection "UnusedProperty" for whole file
|
||||||
|
sbt.version = 1.8.3
|
||||||
|
|||||||
BIN
sprites/items/SelfDestructingCard.png
Normal file
|
After Width: | Height: | Size: 391 B |
@ -58,10 +58,13 @@ TextInputForeground = #333333
|
|||||||
TextInputBorderError = #aa8888
|
TextInputBorderError = #aa8888
|
||||||
TextInputBorderErrorFocused = #cc6666
|
TextInputBorderErrorFocused = #cc6666
|
||||||
|
|
||||||
ButtonBackground = #aaaaaa
|
ButtonBackground = #aaaaaa
|
||||||
ButtonBorder = #888888
|
ButtonBorder = #888888
|
||||||
ButtonForeground = #333333
|
ButtonForeground = #333333
|
||||||
ButtonConfirm = #336633
|
ButtonBackgroundDisabled = #333333
|
||||||
|
ButtonBorderDisabled = #666666
|
||||||
|
ButtonForegroundDisabled = #888888
|
||||||
|
ButtonConfirm = #336633
|
||||||
|
|
||||||
BottomDrawerBorder = #888888
|
BottomDrawerBorder = #888888
|
||||||
|
|
||||||
@ -80,6 +83,7 @@ VerticalMenuBorder = #dfdfdf
|
|||||||
|
|
||||||
SliderBackground = #aaaaaa
|
SliderBackground = #aaaaaa
|
||||||
SliderBorder = #888888
|
SliderBorder = #888888
|
||||||
|
SliderTick = #989898
|
||||||
SliderHandler = #bbbbbb
|
SliderHandler = #bbbbbb
|
||||||
SliderForeground = #333333
|
SliderForeground = #333333
|
||||||
|
|
||||||
@ -134,4 +138,6 @@ SoundCardWire3 = #69237f
|
|||||||
SoundCardWire4 = #7f2331
|
SoundCardWire4 = #7f2331
|
||||||
SoundCardWire5 = #7f5123
|
SoundCardWire5 = #7f5123
|
||||||
SoundCardWire6 = #7f7723
|
SoundCardWire6 = #7f7723
|
||||||
SoundCardWire7 = #000000
|
SoundCardWire7 = #000000
|
||||||
|
|
||||||
|
ErrorMessage = #ff3366
|
||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 87 KiB |
@ -1,16 +1,16 @@
|
|||||||
BackgroundPattern 0 0 304 304
|
BackgroundPattern 0 0 304 304
|
||||||
BarSegment 329 349 16 4
|
BarSegment 357 349 16 4
|
||||||
ComputerMotherboard 305 129 79 70
|
ComputerMotherboard 305 129 79 70
|
||||||
Empty 506 126 1 1
|
Empty 506 126 1 1
|
||||||
EmptySlot 318 330 18 18
|
EmptySlot 335 330 18 18
|
||||||
Knob 385 129 50 50
|
Knob 385 129 50 50
|
||||||
KnobCenter 436 129 50 50
|
KnobCenter 436 129 50 50
|
||||||
KnobLimits 305 200 50 50
|
KnobLimits 305 200 50 50
|
||||||
ShadowBorder 505 15 1 24
|
ShadowBorder 505 15 1 24
|
||||||
ShadowCorner 301 305 24 24
|
ShadowCorner 301 305 24 24
|
||||||
TabArrow 502 0 8 14
|
TabArrow 502 0 8 14
|
||||||
buttons/BottomDrawerClose 337 330 18 18
|
buttons/BottomDrawerClose 354 330 18 18
|
||||||
buttons/BottomDrawerOpen 356 330 18 18
|
buttons/BottomDrawerOpen 373 330 18 18
|
||||||
buttons/OpenFMRadioCloseOff 242 402 7 8
|
buttons/OpenFMRadioCloseOff 242 402 7 8
|
||||||
buttons/OpenFMRadioCloseOn 250 402 7 8
|
buttons/OpenFMRadioCloseOn 250 402 7 8
|
||||||
buttons/OpenFMRadioRedstoneOff 502 87 8 8
|
buttons/OpenFMRadioRedstoneOff 502 87 8 8
|
||||||
@ -19,20 +19,20 @@ buttons/OpenFMRadioStartOff 326 305 24 24
|
|||||||
buttons/OpenFMRadioStartOn 351 305 24 24
|
buttons/OpenFMRadioStartOn 351 305 24 24
|
||||||
buttons/OpenFMRadioStopOff 376 305 24 24
|
buttons/OpenFMRadioStopOff 376 305 24 24
|
||||||
buttons/OpenFMRadioStopOn 401 305 24 24
|
buttons/OpenFMRadioStopOn 401 305 24 24
|
||||||
buttons/OpenFMRadioVolumeDownOff 482 330 10 10
|
buttons/OpenFMRadioVolumeDownOff 499 330 10 10
|
||||||
buttons/OpenFMRadioVolumeDownOn 493 330 10 10
|
buttons/OpenFMRadioVolumeDownOn 501 363 10 10
|
||||||
buttons/OpenFMRadioVolumeUpOff 501 363 10 10
|
buttons/OpenFMRadioVolumeUpOff 335 349 10 10
|
||||||
buttons/OpenFMRadioVolumeUpOn 318 349 10 10
|
buttons/OpenFMRadioVolumeUpOn 346 349 10 10
|
||||||
buttons/PowerOff 375 330 18 18
|
buttons/PowerOff 392 330 18 18
|
||||||
buttons/PowerOn 394 330 18 18
|
buttons/PowerOn 411 330 18 18
|
||||||
icons/ButtonCheck 301 363 17 17
|
icons/ButtonCheck 301 363 17 17
|
||||||
icons/ButtonClipboard 319 363 17 17
|
icons/ButtonClipboard 319 363 17 17
|
||||||
icons/ButtonRandomize 337 363 17 17
|
icons/ButtonRandomize 337 363 17 17
|
||||||
icons/CPU 301 381 16 16
|
icons/CPU 301 381 16 16
|
||||||
icons/Card 318 381 16 16
|
icons/Card 318 381 16 16
|
||||||
icons/ComponentBus 335 381 16 16
|
icons/ComponentBus 335 381 16 16
|
||||||
icons/DragLMB 413 330 21 14
|
icons/DragLMB 430 330 21 14
|
||||||
icons/DragRMB 435 330 21 14
|
icons/DragRMB 452 330 21 14
|
||||||
icons/EEPROM 352 381 16 16
|
icons/EEPROM 352 381 16 16
|
||||||
icons/Floppy 369 381 16 16
|
icons/Floppy 369 381 16 16
|
||||||
icons/HDD 386 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/WaveSawtooth 426 363 24 10
|
||||||
icons/WaveSine 451 363 24 10
|
icons/WaveSine 451 363 24 10
|
||||||
icons/WaveSquare 476 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/WireArrowLeft 507 15 4 8
|
||||||
icons/WireArrowRight 507 24 4 8
|
icons/WireArrowRight 507 24 4 8
|
||||||
items/APU0 233 305 16 96
|
items/APU0 233 305 16 96
|
||||||
@ -108,6 +108,7 @@ items/Memory5 424 251 16 16
|
|||||||
items/NetworkCard 441 251 16 16
|
items/NetworkCard 441 251 16 16
|
||||||
items/RedstoneCard0 458 251 16 16
|
items/RedstoneCard0 458 251 16 16
|
||||||
items/RedstoneCard1 475 251 16 16
|
items/RedstoneCard1 475 251 16 16
|
||||||
|
items/SelfDestructingCard 318 330 16 32
|
||||||
items/Server0 492 251 16 16
|
items/Server0 492 251 16 16
|
||||||
items/Server1 305 268 16 16
|
items/Server1 305 268 16 16
|
||||||
items/Server2 322 268 16 16
|
items/Server2 322 268 16 16
|
||||||
|
|||||||
5
src/main/resources/ocelot/desktop/messages.txt
Normal 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.
|
||||||
BIN
src/main/resources/ocelot/desktop/sounds/minecraft/explosion.ogg
Normal file
@ -2,6 +2,7 @@ package ocelot.desktop
|
|||||||
|
|
||||||
import buildinfo.BuildInfo
|
import buildinfo.BuildInfo
|
||||||
import li.flor.nativejfilechooser.NativeJFileChooser
|
import li.flor.nativejfilechooser.NativeJFileChooser
|
||||||
|
import ocelot.desktop.inventory.Items
|
||||||
import ocelot.desktop.ui.UiHandler
|
import ocelot.desktop.ui.UiHandler
|
||||||
import ocelot.desktop.ui.swing.SplashScreen
|
import ocelot.desktop.ui.swing.SplashScreen
|
||||||
import ocelot.desktop.ui.widget._
|
import ocelot.desktop.ui.widget._
|
||||||
@ -27,7 +28,7 @@ import scala.collection.mutable.ArrayBuffer
|
|||||||
import scala.concurrent.duration.Duration
|
import scala.concurrent.duration.Duration
|
||||||
import scala.io.Source
|
import scala.io.Source
|
||||||
import scala.jdk.CollectionConverters._
|
import scala.jdk.CollectionConverters._
|
||||||
import scala.util.{Failure, Success, Try}
|
import scala.util.{Failure, Success, Try, Using}
|
||||||
|
|
||||||
object OcelotDesktop extends Logging {
|
object OcelotDesktop extends Logging {
|
||||||
private val splashScreen = new SplashScreen()
|
private val splashScreen = new SplashScreen()
|
||||||
@ -46,7 +47,7 @@ object OcelotDesktop extends Logging {
|
|||||||
|
|
||||||
private val tickLock: Lock = new ReentrantLock()
|
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 = {
|
private def mainInner(args: mutable.HashMap[Argument, Option[String]]): Unit = {
|
||||||
logger.info("Starting up Ocelot Desktop")
|
logger.info("Starting up Ocelot Desktop")
|
||||||
@ -61,19 +62,21 @@ object OcelotDesktop extends Logging {
|
|||||||
else
|
else
|
||||||
Paths.get(customConfigPath.get)
|
Paths.get(customConfigPath.get)
|
||||||
Settings.load(settingsFile)
|
Settings.load(settingsFile)
|
||||||
|
Messages.load(Source.fromURL(getClass.getResource("/ocelot/desktop/messages.txt")))
|
||||||
ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt")))
|
ColorScheme.load(Source.fromURL(getClass.getResource("/ocelot/desktop/colorscheme.txt")))
|
||||||
|
|
||||||
|
Items.init()
|
||||||
|
|
||||||
splashScreen.setStatus("Initializing GUI...", 0.30f)
|
splashScreen.setStatus("Initializing GUI...", 0.30f)
|
||||||
createWorkspace()
|
createWorkspace()
|
||||||
|
|
||||||
val loadRecentWorkspace = Settings.get.recentWorkspace.isDefined && Settings.get.openLastWorkspace
|
val loadRecentWorkspace = Settings.get.recentWorkspace.isDefined && Settings.get.openLastWorkspace
|
||||||
root = new RootWidget(!loadRecentWorkspace)
|
root = new RootWidget(!loadRecentWorkspace)
|
||||||
|
|
||||||
|
UiHandler.loadLibraries()
|
||||||
UiHandler.init(root)
|
UiHandler.init(root)
|
||||||
|
|
||||||
splashScreen.setStatus("Loading resources...", 0.60f)
|
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)
|
splashScreen.setStatus("Loading workspace...", 0.90f)
|
||||||
val cmdLineWorkspaceArgument = args.get(CommandLine.WorkspacePath).flatten
|
val cmdLineWorkspaceArgument = args.get(CommandLine.WorkspacePath).flatten
|
||||||
@ -102,28 +105,39 @@ object OcelotDesktop extends Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new Thread(() => {
|
val updateThread = new Thread(() => try {
|
||||||
while (true) {
|
val currentThread = Thread.currentThread()
|
||||||
Profiler.startTimeMeasurement("tick")
|
|
||||||
|
|
||||||
withTickLockAcquired {
|
while (!currentThread.isInterrupted) {
|
||||||
workspace.update()
|
Profiler.measure("tick") {
|
||||||
tpsCounter.tick()
|
withTickLockAcquired {
|
||||||
|
workspace.update()
|
||||||
|
tpsCounter.tick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Profiler.endTimeMeasurement("tick")
|
|
||||||
ticker.waitNext()
|
ticker.waitNext()
|
||||||
}
|
}
|
||||||
}).start()
|
} catch {
|
||||||
|
case _: InterruptedException => // ignore
|
||||||
|
}, "update-thread")
|
||||||
|
|
||||||
|
updateThread.start()
|
||||||
splashScreen.dispose()
|
splashScreen.dispose()
|
||||||
logger.info("Ocelot Desktop is up and ready!")
|
logger.info("Ocelot Desktop is up and ready!")
|
||||||
UiHandler.start()
|
UiHandler.start()
|
||||||
|
|
||||||
logger.info("Cleaning up")
|
logger.info("Cleaning up")
|
||||||
|
updateThread.interrupt()
|
||||||
|
|
||||||
|
try updateThread.join() catch {
|
||||||
|
case _: InterruptedException =>
|
||||||
|
}
|
||||||
|
|
||||||
WebcamCapture.cleanup()
|
WebcamCapture.cleanup()
|
||||||
Settings.save(settingsFile)
|
Settings.save(settingsFile)
|
||||||
ResourceManager.freeResources()
|
|
||||||
UiHandler.terminate()
|
UiHandler.terminate()
|
||||||
|
ResourceManager.checkEmpty()
|
||||||
|
|
||||||
Ocelot.shutdown()
|
Ocelot.shutdown()
|
||||||
|
|
||||||
@ -173,22 +187,27 @@ object OcelotDesktop extends Logging {
|
|||||||
private var savePath: Option[Path] = None
|
private var savePath: Option[Path] = None
|
||||||
private val tmpPath = Files.createTempDirectory("ocelot-save")
|
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()
|
root.workspaceView.newWorkspace()
|
||||||
|
savePath = None
|
||||||
Settings.get.recentWorkspace = None
|
Settings.get.recentWorkspace = None
|
||||||
}
|
}
|
||||||
|
|
||||||
def save(): Unit = {
|
private def saveTo(outputPath: Path): Unit = {
|
||||||
if (savePath.isEmpty) {
|
|
||||||
saveAs()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldPath = workspace.path
|
val oldPath = workspace.path
|
||||||
val newPath = savePath.get
|
|
||||||
if (oldPath != newPath) {
|
if (oldPath != outputPath) {
|
||||||
val oldFiles = Files.list(oldPath).iterator.asScala.toArray
|
val (oldFiles, newFiles) =
|
||||||
val newFiles = Files.list(newPath).iterator.asScala.toArray
|
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)
|
val toRemove = newFiles.intersect(oldFiles)
|
||||||
|
|
||||||
for (path <- toRemove) {
|
for (path <- toRemove) {
|
||||||
@ -201,7 +220,7 @@ object OcelotDesktop extends Logging {
|
|||||||
|
|
||||||
for (path <- oldFiles) {
|
for (path <- oldFiles) {
|
||||||
val oldFile = oldPath.resolve(path.getFileName).toFile
|
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)) {
|
if (Files.isDirectory(path)) {
|
||||||
FileUtils.copyDirectory(oldFile, newFile)
|
FileUtils.copyDirectory(oldFile, newFile)
|
||||||
} else {
|
} else {
|
||||||
@ -209,81 +228,80 @@ object OcelotDesktop extends Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace.path = newPath
|
workspace.path = outputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
val path = newPath + "/workspace.nbt"
|
val path = outputPath + "/workspace.nbt"
|
||||||
val writer = new DataOutputStream(new FileOutputStream(path))
|
|
||||||
val nbt = new NBTTagCompound
|
Using.resource(new DataOutputStream(new FileOutputStream(path))) { writer =>
|
||||||
saveWorld(nbt)
|
val nbt = new NBTTagCompound
|
||||||
CompressedStreamTools.writeCompressed(nbt, writer)
|
saveWorld(nbt)
|
||||||
writer.flush()
|
CompressedStreamTools.writeCompressed(nbt, writer)
|
||||||
logger.info(s"Saved workspace to: $newPath")
|
}
|
||||||
|
|
||||||
|
logger.info(s"Saved workspace to: $outputPath")
|
||||||
}
|
}
|
||||||
|
|
||||||
def saveAs(): Unit = {
|
def save(continuation: => Unit): Unit = savePath match {
|
||||||
showFileChooserDialog(
|
case Some(savePath) =>
|
||||||
dir => Try {
|
saveTo(savePath)
|
||||||
if (dir.isEmpty)
|
continuation
|
||||||
return
|
|
||||||
|
|
||||||
savePath = dir.map(_.toPath)
|
case None => showSaveDialog(continuation)
|
||||||
Settings.get.recentWorkspace = dir.map(_.getCanonicalPath)
|
|
||||||
save()
|
|
||||||
},
|
|
||||||
JFileChooser.SAVE_DIALOG,
|
|
||||||
JFileChooser.DIRECTORIES_ONLY
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def open(): Unit = {
|
def saveAs(): Unit = showSaveDialog()
|
||||||
showFileChooserDialog({
|
|
||||||
|
def showOpenDialog(): Unit =
|
||||||
|
showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) {
|
||||||
case Some(dir) => load(dir)
|
case Some(dir) => load(dir)
|
||||||
case None => Success(())
|
case None => Success(())
|
||||||
}, JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
def load(dir: File): Try[Unit] = {
|
def load(dir: File): Try[Unit] = {
|
||||||
val path = Paths.get(dir.getCanonicalPath, "workspace.nbt")
|
val path = Paths.get(dir.getCanonicalPath, "workspace.nbt")
|
||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
Try {
|
Try {
|
||||||
val reader = new DataInputStream(Files.newInputStream(path))
|
Using.resource(new DataInputStream(Files.newInputStream(path))) { reader =>
|
||||||
val nbt = CompressedStreamTools.readCompressed(reader)
|
val nbt = CompressedStreamTools.readCompressed(reader)
|
||||||
savePath = Some(dir.toPath)
|
savePath = Some(dir.toPath)
|
||||||
Settings.get.recentWorkspace = Some(dir.getCanonicalPath)
|
Settings.get.recentWorkspace = Some(dir.getCanonicalPath)
|
||||||
workspace.path = dir.toPath
|
workspace.path = dir.toPath
|
||||||
loadWorld(nbt)
|
loadWorld(nbt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else Failure(new FileNotFoundException("Specified directory does not contain 'workspace.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(() => {
|
new Thread(() => {
|
||||||
val lastFile = savePath.map(_.toFile).orNull
|
val lastFile = savePath.map(_.toFile).orNull
|
||||||
val chooser: JFileChooser = try {
|
val chooser: JFileChooser = Option.when(Settings.get.useNativeFileChooser) {
|
||||||
new NativeJFileChooser(lastFile)
|
Try(new NativeJFileChooser(lastFile)).toOption
|
||||||
} catch {
|
}.flatten.getOrElse(
|
||||||
case _: Throwable => new JFileChooser(lastFile)
|
new JFileChooser(lastFile)
|
||||||
}
|
)
|
||||||
|
|
||||||
chooser.setFileSelectionMode(selectionMode)
|
chooser.setFileSelectionMode(selectionMode)
|
||||||
chooser.setDialogType(dialogType)
|
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 {
|
result match {
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
new NotificationDialog(s"Something went wrong!\n($exception)\nCheck the log file for a full stacktrace.",
|
new NotificationDialog(s"Something went wrong!\n($exception)\nCheck the log file for a full stacktrace.",
|
||||||
NotificationType.Error)
|
NotificationType.Error)
|
||||||
.addCloseButton()
|
.addCloseButton()
|
||||||
.show()
|
.show()
|
||||||
|
|
||||||
case Success(_) =>
|
case Success(_) =>
|
||||||
}
|
}
|
||||||
}).start()
|
}).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
def addPlayerDialog(): Unit = new InputDialog(
|
def showAddPlayerDialog(): Unit = new InputDialog(
|
||||||
"Add new player",
|
"Add new player",
|
||||||
text => OcelotDesktop.selectPlayer(text)
|
text => OcelotDesktop.selectPlayer(text)
|
||||||
).show()
|
).show()
|
||||||
@ -307,54 +325,10 @@ object OcelotDesktop extends Logging {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def settings(): Unit = {
|
def showSettings(): Unit = new SettingsDialog().show()
|
||||||
new SettingsDialog().show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private def cleanup(): Unit = {
|
def exit(): Unit = showCloseConfirmationDialog() {
|
||||||
FileUtils.deleteDirectory(tmpPath.toFile)
|
UiHandler.exit()
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var workspace: Workspace = _
|
var workspace: Workspace = _
|
||||||
@ -362,4 +336,50 @@ object OcelotDesktop extends Logging {
|
|||||||
private def createWorkspace(): Unit = {
|
private def createWorkspace(): Unit = {
|
||||||
workspace = new Workspace(tmpPath)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,15 +12,23 @@ import java.util
|
|||||||
import scala.io.{Codec, Source}
|
import scala.io.{Codec, Source}
|
||||||
|
|
||||||
class Settings(val config: Config) extends SettingsData {
|
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
|
volumeMaster = (config.getDouble("ocelot.sound.volumeMaster") max 0 min 1).toFloat
|
||||||
volumeBeep = (config.getDouble("ocelot.sound.volumeBeep") 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
|
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
|
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")
|
windowSize = config.getInt2D("ocelot.window.size")
|
||||||
windowPosition = config.getInt2D("ocelot.window.position")
|
windowPosition = config.getInt2D("ocelot.window.position")
|
||||||
windowValidatePosition = config.getBooleanOrElse("ocelot.window.validatePosition", default = true)
|
windowValidatePosition = config.getBooleanOrElse("ocelot.window.validatePosition", default = true)
|
||||||
windowFullscreen = config.getBooleanOrElse("ocelot.window.fullscreen", default = false)
|
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.
|
// 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)
|
// (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.volumeBeep", settings.volumeBeep)
|
||||||
.withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
|
.withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
|
||||||
.withValuePreserveOrigin("ocelot.sound.volumeInterface", settings.volumeInterface)
|
.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)
|
.withValue("ocelot.window.position", settings.windowPosition)
|
||||||
.withValuePreserveOrigin("ocelot.window.validatePosition", settings.windowValidatePosition)
|
.withValuePreserveOrigin("ocelot.window.validatePosition", settings.windowValidatePosition)
|
||||||
.withValue("ocelot.window.size", settings.windowSize)
|
.withValue("ocelot.window.size", settings.windowSize)
|
||||||
.withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen)
|
.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)
|
.withValue("ocelot.workspace.recent", settings.recentWorkspace)
|
||||||
.withValuePreserveOrigin("ocelot.workspace.stickyWindows", settings.stickyWindows)
|
.withValuePreserveOrigin("ocelot.workspace.stickyWindows", settings.stickyWindows)
|
||||||
.withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit)
|
.withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ocelot.desktop.audio
|
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 org.lwjgl.openal.AL10
|
||||||
|
|
||||||
import java.nio.{ByteBuffer, IntBuffer}
|
import java.nio.{ByteBuffer, IntBuffer}
|
||||||
@ -17,71 +18,97 @@ object AL10W extends Logging {
|
|||||||
case _: Exception => false
|
case _: Exception => false
|
||||||
}
|
}
|
||||||
}).map(_.getName).getOrElse(err.toHexString)
|
}).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
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
def alIsExtensionPresent(name: String): Boolean = run("alIsExtensionPresent") {
|
def alIsExtensionPresent(name: String): Boolean = OpenAlException.defaulting(false) {
|
||||||
AL10.alIsExtensionPresent(name)
|
run("alIsExtensionPresent") {
|
||||||
|
AL10.alIsExtensionPresent(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alGenBuffers(): Int = run("alGenBuffers") {
|
def alGenBuffers(): Int = run("alGenBuffers") {
|
||||||
AL10.alGenBuffers()
|
AL10.alGenBuffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alBufferData(buffer: Int, format: Int, data: ByteBuffer, freq: Int): Unit = run("alBufferData") {
|
def alBufferData(buffer: Int, format: Int, data: ByteBuffer, freq: Int): Unit = run("alBufferData") {
|
||||||
AL10.alBufferData(buffer, format, data, freq)
|
AL10.alBufferData(buffer, format, data, freq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alGetBufferi(buffer: Int, pname: Int): Int = run("alGetBufferi") {
|
def alGetBufferi(buffer: Int, pname: Int): Int = run("alGetBufferi") {
|
||||||
AL10.alGetBufferi(buffer, pname)
|
AL10.alGetBufferi(buffer, pname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alDeleteBuffers(buffer: Int): Unit = run("alDeleteBuffers") {
|
def alDeleteBuffers(buffer: Int): Unit = run("alDeleteBuffers") {
|
||||||
AL10.alDeleteBuffers(buffer)
|
AL10.alDeleteBuffers(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alGenSources(): Int = run("alGenSources") {
|
def alGenSources(): Int = run("alGenSources") {
|
||||||
AL10.alGenSources()
|
AL10.alGenSources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourceQueueBuffers(source: Int, buffer: Int): Unit = run("alSourceQueueBuffers") {
|
def alSourceQueueBuffers(source: Int, buffer: Int): Unit = run("alSourceQueueBuffers") {
|
||||||
AL10.alSourceQueueBuffers(source, buffer)
|
AL10.alSourceQueueBuffers(source, buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourceUnqueueBuffers(source: Int, buffers: IntBuffer): Unit = run("alSourceUnqueueBuffers") {
|
def alSourceUnqueueBuffers(source: Int, buffers: IntBuffer): Unit = run("alSourceUnqueueBuffers") {
|
||||||
AL10.alSourceUnqueueBuffers(source, buffers)
|
AL10.alSourceUnqueueBuffers(source, buffers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alGetSourcei(source: Int, pname: Int): Int = run("alGetSourcei") {
|
def alGetSourcei(source: Int, pname: Int): Int = run("alGetSourcei") {
|
||||||
AL10.alGetSourcei(source, pname)
|
AL10.alGetSourcei(source, pname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourcei(source: Int, pname: Int, value: Int): Unit = run("alSourcei") {
|
def alSourcei(source: Int, pname: Int, value: Int): Unit = run("alSourcei") {
|
||||||
AL10.alSourcei(source, pname, value)
|
AL10.alSourcei(source, pname, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourcef(source: Int, pname: Int, value: Float): Unit = run("alSourcef") {
|
def alSourcef(source: Int, pname: Int, value: Float): Unit = run("alSourcef") {
|
||||||
AL10.alSourcef(source, pname, value)
|
AL10.alSourcef(source, pname, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSource3f(source: Int, pname: Int, v1: Float, v2: Float, v3: Float): Unit = run("alSource3f") {
|
def alSource3f(source: Int, pname: Int, v1: Float, v2: Float, v3: Float): Unit = run("alSource3f") {
|
||||||
AL10.alSource3f(source, pname, v1, v2, v3)
|
AL10.alSource3f(source, pname, v1, v2, v3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourcePlay(source: Int): Unit = run("alSourcePlay") {
|
def alSourcePlay(source: Int): Unit = run("alSourcePlay") {
|
||||||
AL10.alSourcePlay(source)
|
AL10.alSourcePlay(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourcePause(source: Int): Unit = run("alSourcePause") {
|
def alSourcePause(source: Int): Unit = run("alSourcePause") {
|
||||||
AL10.alSourcePause(source)
|
AL10.alSourcePause(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alSourceStop(source: Int): Unit = run("alSourceStop") {
|
def alSourceStop(source: Int): Unit = run("alSourceStop") {
|
||||||
AL10.alSourceStop(source)
|
AL10.alSourceStop(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
def alDeleteSources(source: Int): Unit = run("alDeleteSources") {
|
def alDeleteSources(source: Int): Unit = run("alDeleteSources") {
|
||||||
AL10.alDeleteSources(source)
|
AL10.alDeleteSources(source)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
package ocelot.desktop.audio
|
package ocelot.desktop.audio
|
||||||
|
|
||||||
import ocelot.desktop.Settings
|
import ocelot.desktop.Settings
|
||||||
import ocelot.desktop.util.Logging
|
import ocelot.desktop.util.{Logging, OpenAlException, Transaction}
|
||||||
import org.lwjgl.LWJGLException
|
import org.lwjgl.LWJGLException
|
||||||
import org.lwjgl.openal.{AL, AL10, ALC10}
|
import org.lwjgl.openal.{AL, AL10, ALC10}
|
||||||
|
|
||||||
import java.nio.{ByteBuffer, ByteOrder}
|
import java.nio.{ByteBuffer, ByteOrder}
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
import scala.util.control.Exception.catching
|
||||||
|
|
||||||
object Audio extends Logging {
|
object Audio extends Logging {
|
||||||
val sampleRate: Int = 44100
|
val sampleRate: Int = 44100
|
||||||
@ -35,32 +36,52 @@ object Audio extends Logging {
|
|||||||
sources.size
|
sources.size
|
||||||
}
|
}
|
||||||
|
|
||||||
def newStream(soundCategory: SoundCategory.Value, pitch: Float = 1f,
|
def newStream(
|
||||||
volume: Float = 1f): (SoundStream, SoundSource) =
|
soundCategory: SoundCategory.Value,
|
||||||
{
|
pitch: Float = 1f,
|
||||||
|
volume: Float = 1f
|
||||||
|
): (SoundStream, SoundSource) = {
|
||||||
var source: SoundSource = null
|
var source: SoundSource = null
|
||||||
|
|
||||||
val stream = new SoundStream {
|
val stream = new SoundStream {
|
||||||
override def enqueue(samples: SoundSamples): Unit = Audio.synchronized {
|
override def enqueue(samples: SoundSamples): Unit = Audio.synchronized {
|
||||||
val sourceId = if (sources.contains(source)) {
|
OpenAlException.ignoring {
|
||||||
sources(source)
|
Transaction.runAbortable { tx =>
|
||||||
} else {
|
if (!Audio.isDisabled) {
|
||||||
val sourceId = AL10W.alGenSources()
|
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.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
|
||||||
AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
|
AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
|
||||||
AL10W.alSourcef(sourceId, AL10.AL_GAIN, source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster)
|
AL10W.alSourcef(
|
||||||
sources.put(source, sourceId)
|
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
|
return SoundSource.Status.Stopped
|
||||||
|
|
||||||
val sourceId = sources(source)
|
val sourceId = sources(source)
|
||||||
AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
|
|
||||||
case AL10.AL_PLAYING => SoundSource.Status.Playing
|
catching(classOf[OpenAlException]) opt AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
|
||||||
case AL10.AL_PAUSED => SoundSource.Status.Paused
|
case Some(AL10.AL_PLAYING) => SoundSource.Status.Playing
|
||||||
|
case Some(AL10.AL_PAUSED) => SoundSource.Status.Paused
|
||||||
case _ => SoundSource.Status.Stopped
|
case _ => SoundSource.Status.Stopped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def playSource(source: SoundSource): Unit = synchronized {
|
def playSource(source: SoundSource): Unit = synchronized {
|
||||||
if (getSourceStatus(source) == SoundSource.Status.Playing)
|
OpenAlException.ignoring {
|
||||||
return
|
if (Audio.isDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (sources.contains(source)) {
|
if (getSourceStatus(source) == SoundSource.Status.Playing) {
|
||||||
AL10W.alSourcePlay(sources(source))
|
return
|
||||||
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 {
|
def pauseSource(source: SoundSource): Unit = synchronized {
|
||||||
if (getSourceStatus(source) == SoundSource.Status.Paused)
|
OpenAlException.ignoring {
|
||||||
return
|
if (Audio.isDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (sources.contains(source)) {
|
if (getSourceStatus(source) == SoundSource.Status.Paused)
|
||||||
AL10W.alSourcePause(sources(source))
|
return
|
||||||
|
|
||||||
|
if (sources.contains(source)) {
|
||||||
|
AL10W.alSourcePause(sources(source))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def stopSource(source: SoundSource): Unit = synchronized {
|
def stopSource(source: SoundSource): Unit = synchronized {
|
||||||
if (getSourceStatus(source) == SoundSource.Status.Stopped)
|
OpenAlException.ignoring {
|
||||||
return
|
if (Audio.isDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (sources.contains(source)) {
|
if (getSourceStatus(source) == SoundSource.Status.Stopped)
|
||||||
AL10W.alSourceStop(sources(source))
|
return
|
||||||
|
|
||||||
|
if (sources.contains(source)) {
|
||||||
|
AL10W.alSourceStop(sources(source))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,14 +191,21 @@ object Audio extends Logging {
|
|||||||
if (isDisabled) return
|
if (isDisabled) return
|
||||||
|
|
||||||
sources.filterInPlace { case (source, sourceId) =>
|
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.alSourcef(
|
||||||
AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
|
sourceId,
|
||||||
case AL10.AL_STOPPED =>
|
AL10.AL_GAIN,
|
||||||
deleteSource(sourceId)
|
source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
|
||||||
false
|
)
|
||||||
case _ => true
|
|
||||||
|
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
|
if (isDisabled) return
|
||||||
|
|
||||||
for ((_, sourceId) <- sources) {
|
for ((_, sourceId) <- sources) {
|
||||||
deleteSource(sourceId)
|
OpenAlException.ignoring {
|
||||||
|
deleteSource(sourceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sources.clear()
|
sources.clear()
|
||||||
|
AL.destroy()
|
||||||
_disabled = true
|
_disabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
private def deleteSource(sourceId: Int): Unit = {
|
private def deleteSource(sourceId: Int): Unit = {
|
||||||
AL10W.alSourceStop(sourceId)
|
AL10W.alSourceStop(sourceId)
|
||||||
cleanupSourceBuffers(sourceId)
|
cleanupSourceBuffers(sourceId)
|
||||||
AL10W.alDeleteSources(sourceId)
|
AL10W.alDeleteSources(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
private def cleanupSourceBuffers(sourceId: Int): Unit = {
|
private def cleanupSourceBuffers(sourceId: Int): Unit = {
|
||||||
val count = AL10W.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED)
|
val count = AL10W.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED)
|
||||||
if (count <= 0) return
|
if (count <= 0) return
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
package ocelot.desktop.audio
|
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
|
import org.lwjgl.openal.AL10
|
||||||
|
|
||||||
class SoundBuffer(val file: String) extends Resource with Logging {
|
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 (AL10W.alIsExtensionPresent("AL_EXT_vorbis")) {
|
||||||
if (Audio.isDisabled)
|
initWithExt()
|
||||||
return
|
} else {
|
||||||
|
initFallback()
|
||||||
logger.debug(s"Loading sound buffer from '$file'...")
|
}
|
||||||
_bufferId = AL10W.alGenBuffers()
|
} else {
|
||||||
|
logger.debug(s"Skipping loading sound buffer from '$file' because audio is not available")
|
||||||
if (AL10W.alIsExtensionPresent("AL_EXT_vorbis")) {
|
}
|
||||||
initWithExt()
|
|
||||||
} else {
|
|
||||||
initFallback()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@throws[OpenAlException]
|
||||||
private def initWithExt(): Unit = {
|
private def initWithExt(): Unit = {
|
||||||
val fileBuffer = FileUtils.load(file)
|
val fileBuffer = FileUtils.load(file)
|
||||||
if (fileBuffer == null) {
|
if (fileBuffer == null) {
|
||||||
@ -29,40 +31,49 @@ class SoundBuffer(val file: String) extends Resource with Logging {
|
|||||||
return
|
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 = {
|
private def initFallback(): Unit = {
|
||||||
val ogg = OggDecoder.decode(getClass.getResourceAsStream(file))
|
val ogg = OggDecoder.decode(getClass.getResourceAsStream(file))
|
||||||
_bufferId = ogg.genBuffer()
|
_bufferId = ogg.genBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
def numSamples: Int = {
|
def numSamples: Int = bufferId match {
|
||||||
if (bufferId == -1) {
|
case Some(bufferId) =>
|
||||||
return 0
|
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)
|
case None => 0
|
||||||
val channels = AL10W.alGetBufferi(bufferId, AL10.AL_CHANNELS)
|
|
||||||
val bits = AL10W.alGetBufferi(bufferId, AL10.AL_BITS)
|
|
||||||
|
|
||||||
sizeBytes * 8 / channels / bits
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val sampleRate: Int = {
|
val sampleRate: Int = bufferId match {
|
||||||
if (bufferId == -1) {
|
case Some(bufferId) =>
|
||||||
44100
|
OpenAlException.defaulting(44100) {
|
||||||
} else {
|
Audio.synchronized {
|
||||||
AL10W.alGetBufferi(bufferId, AL10.AL_FREQUENCY)
|
AL10W.alGetBufferi(bufferId, AL10.AL_FREQUENCY)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case None => 44100
|
||||||
}
|
}
|
||||||
|
|
||||||
def bufferId: Int = _bufferId
|
def bufferId: Option[Int] = _bufferId
|
||||||
|
|
||||||
def freeResource(): Unit = {
|
override def freeResource(): Unit = {
|
||||||
if (bufferId != -1) {
|
super.freeResource()
|
||||||
AL10W.alDeleteBuffers(bufferId)
|
|
||||||
logger.debug(s"Destroyed sound buffer (ID: $bufferId) loaded from $file")
|
bufferId.foreach { bufferId =>
|
||||||
|
OpenAlException.ignoring {
|
||||||
|
AL10W.alDeleteBuffers(bufferId)
|
||||||
|
logger.debug(s"Destroyed sound buffer (ID: $bufferId) loaded from $file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,33 +1,51 @@
|
|||||||
package ocelot.desktop.audio
|
package ocelot.desktop.audio
|
||||||
|
|
||||||
object SoundBuffers {
|
import ocelot.desktop.util.Resource
|
||||||
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")
|
|
||||||
|
|
||||||
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",
|
"banjo", "basedrum", "bass", "bell", "bit", "chime", "cow_bell", "didgeridoo", "flute", "guitar",
|
||||||
"harp", "hat", "iron_xylophone", "pling", "snare", "xylophone"
|
"harp", "hat", "iron_xylophone", "pling", "snare", "xylophone"
|
||||||
).map(name => {
|
).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
|
}).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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
package ocelot.desktop.audio
|
package ocelot.desktop.audio
|
||||||
|
|
||||||
import ocelot.desktop.util.Logging
|
import ocelot.desktop.util.{Logging, OpenAlException}
|
||||||
import org.lwjgl.openal.AL10
|
import org.lwjgl.openal.AL10
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import scala.util.control.Exception.catching
|
||||||
|
|
||||||
case class SoundSamples(data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging {
|
case class SoundSamples(data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging {
|
||||||
def genBuffer(): Int = {
|
def genBuffer(): Option[Int] = Audio.synchronized {
|
||||||
val bufferId = AL10W.alGenBuffers()
|
if (Audio.isDisabled) return None
|
||||||
val formatId = format match {
|
|
||||||
case SoundSamples.Format.Stereo16 => AL10.AL_FORMAT_STEREO16
|
catching(classOf[OpenAlException]) opt {
|
||||||
case SoundSamples.Format.Mono16 => AL10.AL_FORMAT_MONO16
|
val bufferId = AL10W.alGenBuffers()
|
||||||
case SoundSamples.Format.Mono8 => AL10.AL_FORMAT_MONO8
|
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 {
|
object SoundSamples {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ocelot.desktop.audio
|
package ocelot.desktop.audio
|
||||||
|
|
||||||
import java.time.Duration
|
import java.util.concurrent.TimeUnit
|
||||||
|
import scala.concurrent.duration.Duration
|
||||||
|
|
||||||
class SoundSource(val kind: SoundSource.Kind,
|
class SoundSource(val kind: SoundSource.Kind,
|
||||||
val soundCategory: SoundCategory.Value,
|
val soundCategory: SoundCategory.Value,
|
||||||
@ -8,20 +9,21 @@ class SoundSource(val kind: SoundSource.Kind,
|
|||||||
val pitch: Float,
|
val pitch: Float,
|
||||||
var volume: Float) {
|
var volume: Float) {
|
||||||
|
|
||||||
def duration: Duration = {
|
def duration: Option[Duration] = kind match {
|
||||||
val seconds = kind match {
|
case SoundSource.KindSoundBuffer(buffer) =>
|
||||||
case SoundSource.KindSoundBuffer(buffer) =>
|
Some(Duration(buffer.numSamples.toFloat / buffer.sampleRate, TimeUnit.SECONDS))
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
def status: SoundSource.Status.Value = {
|
||||||
@ -73,8 +75,7 @@ object SoundSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def fromStream(stream: SoundStream, soundCategory: SoundCategory.Value,
|
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)
|
new SoundSource(SoundSource.KindStream(stream), soundCategory, looping, pitch, volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,4 +7,5 @@ object SoundSources {
|
|||||||
val InterfaceTick = SoundSource.fromBuffer(SoundBuffers.InterfaceTick, SoundCategory.Interface)
|
val InterfaceTick = SoundSource.fromBuffer(SoundBuffers.InterfaceTick, SoundCategory.Interface)
|
||||||
|
|
||||||
val MinecraftClick = SoundSource.fromBuffer(SoundBuffers.MinecraftClick, SoundCategory.Interface)
|
val MinecraftClick = SoundSource.fromBuffer(SoundBuffers.MinecraftClick, SoundCategory.Interface)
|
||||||
|
val MinecraftExplosion = SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,15 @@ package ocelot.desktop.entity
|
|||||||
|
|
||||||
import ocelot.desktop.util.WebcamCapture
|
import ocelot.desktop.util.WebcamCapture
|
||||||
import totoro.ocelot.brain.Constants
|
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.{DeviceAttribute, DeviceClass}
|
||||||
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, GenericCamera}
|
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, GenericCamera}
|
||||||
import totoro.ocelot.brain.nbt.NBTTagCompound
|
import totoro.ocelot.brain.nbt.NBTTagCompound
|
||||||
import totoro.ocelot.brain.util.ResultWrapper.result
|
import totoro.ocelot.brain.util.ResultWrapper.result
|
||||||
import totoro.ocelot.brain.workspace.Workspace
|
import totoro.ocelot.brain.workspace.Workspace
|
||||||
|
|
||||||
object Camera {
|
|
||||||
private val DistanceCallCost = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
class Camera extends Entity with GenericCamera with DeviceInfo {
|
class Camera extends Entity with GenericCamera with DeviceInfo {
|
||||||
var webcamCapture: WebcamCapture = WebcamCapture.getDefault
|
var webcamCapture: Option[WebcamCapture] = WebcamCapture.getDefault
|
||||||
var flipHorizontally: Boolean = false
|
var flipHorizontally: Boolean = false
|
||||||
var flipVertically: Boolean = false
|
var flipVertically: Boolean = false
|
||||||
var directCalls: Boolean = false
|
var directCalls: Boolean = false
|
||||||
@ -23,31 +19,38 @@ class Camera extends Entity with GenericCamera with DeviceInfo {
|
|||||||
if (!directCalls)
|
if (!directCalls)
|
||||||
context.consumeCallBudget(1.0 / Camera.DistanceCallCost)
|
context.consumeCallBudget(1.0 / Camera.DistanceCallCost)
|
||||||
|
|
||||||
var x: Float = 0f
|
result(webcamCapture match {
|
||||||
var y: Float = 0f
|
case Some(webcamCapture) =>
|
||||||
if (args.count() == 2) {
|
var x: Float = 0f
|
||||||
// [-1; 1] => [0; 1]
|
var y: Float = 0f
|
||||||
x = (args.checkDouble(0).toFloat + 1f) / 2f
|
|
||||||
y = (args.checkDouble(1).toFloat + 1f) / 2f
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flipHorizontally) x = 1f - x
|
if (args.count() == 2) {
|
||||||
if (!flipVertically) y = 1f - y
|
// [-1; 1] => [0; 1]
|
||||||
result(webcamCapture.ray(x, y))
|
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(
|
override def getDeviceInfo: Map[String, String] = Map(
|
||||||
DeviceAttribute.Class -> DeviceClass.Multimedia,
|
DeviceAttribute.Class -> DeviceClass.Multimedia,
|
||||||
DeviceAttribute.Description -> "Dungeon Scanner 2.5D",
|
DeviceAttribute.Description -> "Dungeon Scanner 2.5D",
|
||||||
DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor,
|
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 = {
|
override def load(nbt: NBTTagCompound, workspace: Workspace): Unit = {
|
||||||
super.load(nbt, workspace)
|
super.load(nbt, workspace)
|
||||||
|
|
||||||
if (nbt.hasKey("device"))
|
if (nbt.hasKey("device"))
|
||||||
webcamCapture = WebcamCapture.getInstance(nbt.getString("device"))
|
webcamCapture = WebcamCapture.getInstance(nbt.getString("device")).orElse(WebcamCapture.getDefault)
|
||||||
|
|
||||||
if (nbt.hasKey("flipHorizontally"))
|
if (nbt.hasKey("flipHorizontally"))
|
||||||
flipHorizontally = nbt.getBoolean("flipHorizontally")
|
flipHorizontally = nbt.getBoolean("flipHorizontally")
|
||||||
@ -62,9 +65,13 @@ class Camera extends Entity with GenericCamera with DeviceInfo {
|
|||||||
override def save(nbt: NBTTagCompound): Unit = {
|
override def save(nbt: NBTTagCompound): Unit = {
|
||||||
super.save(nbt)
|
super.save(nbt)
|
||||||
|
|
||||||
nbt.setString("device", webcamCapture.name)
|
webcamCapture.foreach(capture => nbt.setString("device", capture.name))
|
||||||
nbt.setBoolean("flipHorizontally", flipHorizontally)
|
nbt.setBoolean("flipHorizontally", flipHorizontally)
|
||||||
nbt.setBoolean("flipVertically", flipVertically)
|
nbt.setBoolean("flipVertically", flipVertically)
|
||||||
nbt.setBoolean("directCalls", directCalls)
|
nbt.setBoolean("directCalls", directCalls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Camera {
|
||||||
|
private val DistanceCallCost = 20
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource}
|
|||||||
import ocelot.desktop.color.IntColor
|
import ocelot.desktop.color.IntColor
|
||||||
import ocelot.desktop.util.Logging
|
import ocelot.desktop.util.Logging
|
||||||
import org.lwjgl.BufferUtils
|
import org.lwjgl.BufferUtils
|
||||||
import totoro.ocelot.brain.Ocelot.logger
|
|
||||||
import totoro.ocelot.brain.entity.machine.{Arguments, Callback, Context}
|
import totoro.ocelot.brain.entity.machine.{Arguments, Callback, Context}
|
||||||
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, Environment}
|
import totoro.ocelot.brain.entity.traits.{DeviceInfo, Entity, Environment}
|
||||||
import totoro.ocelot.brain.nbt.NBTTagCompound
|
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 {
|
class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
|
||||||
override val node: Component =
|
override val node: Component =
|
||||||
Network.newNode(this, Visibility.Network)
|
Network
|
||||||
|
.newNode(this, Visibility.Network)
|
||||||
.withComponent("openfm_radio", Visibility.Network)
|
.withComponent("openfm_radio", Visibility.Network)
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
@ -105,22 +105,13 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
|
|||||||
|
|
||||||
bytesRead = inputStream.read(buffer, 0, buffer.length)
|
bytesRead = inputStream.read(buffer, 0, buffer.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("OpenFM input audio stream has reached EOF, closing thread")
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
case _: InterruptedException =>
|
case _: InterruptedException =>
|
||||||
case e: Exception => logger.error("OpenFM playback exception", e)
|
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 = {
|
def play(): Boolean = {
|
||||||
@ -139,24 +130,35 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
def stop(): Boolean = {
|
def stop(): Unit = {
|
||||||
this.synchronized {
|
this.synchronized {
|
||||||
if (playbackThread.isDefined)
|
// Stopping reading data from url
|
||||||
|
if (playbackThread.isDefined) {
|
||||||
playbackThread.get.interrupt()
|
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()
|
@Callback()
|
||||||
def start(context: Context, args: Arguments): Array[AnyRef] =
|
def start(context: Context, args: Arguments): Array[AnyRef] =
|
||||||
result(play())
|
result(play())
|
||||||
|
|
||||||
@Callback()
|
@Callback()
|
||||||
def stop(context: Context, args: Arguments): Array[AnyRef] =
|
def stop(context: Context, args: Arguments): Array[AnyRef] = {
|
||||||
result(stop())
|
stop()
|
||||||
|
|
||||||
|
result(true)
|
||||||
|
}
|
||||||
|
|
||||||
@Callback()
|
@Callback()
|
||||||
def isPlaying(context: Context, args: Arguments): Array[AnyRef] =
|
def isPlaying(context: Context, args: Arguments): Array[AnyRef] =
|
||||||
|
|||||||
@ -8,9 +8,9 @@ object Rect2D {
|
|||||||
|
|
||||||
//noinspection ScalaWeakerAccess,ScalaUnusedSymbol
|
//noinspection ScalaWeakerAccess,ScalaUnusedSymbol
|
||||||
case class Rect2D(x: Float, y: Float, w: Float, h: Float) {
|
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)
|
def origin: Vector2D = Vector2D(x, y)
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import scala.collection.mutable
|
|||||||
import scala.util.control.Breaks._
|
import scala.util.control.Breaks._
|
||||||
|
|
||||||
//noinspection ScalaWeakerAccess,ScalaUnusedSymbol
|
//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 time = 0f
|
||||||
|
|
||||||
private var projection = Transform2D.viewport(800, 600)
|
private var projection = Transform2D.viewport(800, 600)
|
||||||
@ -29,6 +29,7 @@ class Graphics extends Logging with Resource {
|
|||||||
private var oldFont: Font = _font
|
private var oldFont: Font = _font
|
||||||
|
|
||||||
private val stack = mutable.Stack[GraphicsState](GraphicsState())
|
private val stack = mutable.Stack[GraphicsState](GraphicsState())
|
||||||
|
|
||||||
private var spriteRect = Spritesheet.sprites("Empty")
|
private var spriteRect = Spritesheet.sprites("Empty")
|
||||||
private val emptySpriteTrans = Transform2D.translate(spriteRect.x, spriteRect.y) >> Transform2D.scale(spriteRect.w, spriteRect.h)
|
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("uTexture", 0)
|
||||||
shaderProgram.set("uTextTexture", 1)
|
shaderProgram.set("uTextTexture", 1)
|
||||||
|
|
||||||
def resize(width: Int, height: Int): Unit = {
|
scale(scalingFactor)
|
||||||
offscreenTexture.bind()
|
|
||||||
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, width, height, 0, GL11.GL_RGB,
|
def resize(width: Int, height: Int, scaling: Float): Boolean = {
|
||||||
GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer])
|
var viewportChanged = false
|
||||||
GL11.glViewport(0, 0, width, height)
|
|
||||||
|
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 = {
|
override def freeResource(): Unit = {
|
||||||
logger.debug(s"Destroyed FBO (ID: $offscreenFramebuffer)")
|
super.freeResource()
|
||||||
|
|
||||||
GL30.glDeleteFramebuffers(offscreenFramebuffer)
|
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
|
def font: Font = _font
|
||||||
@ -106,16 +134,13 @@ class Graphics extends Logging with Resource {
|
|||||||
offscreenTexture.bind()
|
offscreenTexture.bind()
|
||||||
foreground = RGBAColorNorm(1, 1, 1, alpha)
|
foreground = RGBAColorNorm(1, 1, 1, alpha)
|
||||||
spriteRect = Rect2D(0, 1f, 1f, -1f)
|
spriteRect = Rect2D(0, 1f, 1f, -1f)
|
||||||
_rect(0, 0, width, height, fixUV = false)
|
|
||||||
|
_rect(0, 0, width / scalingFactor, height / scalingFactor, fixUV = false)
|
||||||
renderer.flush()
|
renderer.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
def setViewport(width: Int, height: Int): Unit = {
|
def startViewport(): Unit = {
|
||||||
projection = Transform2D.viewport(width, height)
|
shaderProgram.set("uProj", Transform2D.viewport(width, height))
|
||||||
this.width = width
|
|
||||||
this.height = height
|
|
||||||
|
|
||||||
shaderProgram.set("uProj", projection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def clear(): Unit = {
|
def clear(): Unit = {
|
||||||
@ -124,16 +149,19 @@ class Graphics extends Logging with Resource {
|
|||||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT)
|
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()
|
flush()
|
||||||
stack.head.scissor = Some((x, y, width, height))
|
stack.head.scissor = Some((x, y, width * scalingFactor, height * scalingFactor))
|
||||||
GL11.glScissor(x, this.height - height - y, width, height)
|
// 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)
|
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 = {
|
def setScissor(): Unit = {
|
||||||
flush()
|
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 fontSize = fontSizeMultiplier * _font.fontSize
|
||||||
val x = _x.round
|
val x = _x.round
|
||||||
val y = _y.round
|
val y = _y.round
|
||||||
|
|||||||
@ -9,6 +9,6 @@ case class GraphicsState(
|
|||||||
var fontSizeMultiplier: Float = 1f,
|
var fontSizeMultiplier: Float = 1f,
|
||||||
var alphaMultiplier: Float = 1f,
|
var alphaMultiplier: Float = 1f,
|
||||||
var sprite: String = "Empty",
|
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
|
var transform: Transform2D = Transform2D.identity
|
||||||
)
|
)
|
||||||
|
|||||||
96
src/main/scala/ocelot/desktop/graphics/Icons.scala
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package ocelot.desktop.graphics
|
package ocelot.desktop.graphics
|
||||||
|
|
||||||
import ocelot.desktop.geometry.Transform2D
|
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.BufferUtils
|
||||||
import org.lwjgl.opengl.GL20
|
import org.lwjgl.opengl.GL20
|
||||||
|
|
||||||
@ -34,13 +34,12 @@ class ShaderProgram(name: String) extends Logging with Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceManager.registerResource(this)
|
override def freeResource(): Unit = {
|
||||||
|
super.freeResource()
|
||||||
def freeResource(): Unit = {
|
|
||||||
logger.debug(s"Destroyed shader program ($name)")
|
|
||||||
GL20.glDeleteProgram(shaderProgram)
|
GL20.glDeleteProgram(shaderProgram)
|
||||||
GL20.glDeleteShader(vertexShader)
|
GL20.glDeleteShader(vertexShader)
|
||||||
GL20.glDeleteShader(fragmentShader)
|
GL20.glDeleteShader(fragmentShader)
|
||||||
|
logger.debug(s"Destroyed shader program ($name)")
|
||||||
}
|
}
|
||||||
|
|
||||||
def bind(): Unit = {
|
def bind(): Unit = {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package ocelot.desktop.graphics
|
package ocelot.desktop.graphics
|
||||||
|
|
||||||
import ocelot.desktop.util.{Logging, Resource, ResourceManager}
|
import ocelot.desktop.util.{Logging, Resource}
|
||||||
import org.lwjgl.BufferUtils
|
import org.lwjgl.BufferUtils
|
||||||
import org.lwjgl.opengl._
|
import org.lwjgl.opengl._
|
||||||
|
|
||||||
@ -10,13 +10,6 @@ import java.nio.ByteBuffer
|
|||||||
class Texture extends Logging with Resource {
|
class Texture extends Logging with Resource {
|
||||||
val texture: Int = GL11.glGenTextures()
|
val texture: Int = GL11.glGenTextures()
|
||||||
|
|
||||||
ResourceManager.registerResource(this)
|
|
||||||
|
|
||||||
def freeResource(): Unit = {
|
|
||||||
logger.debug(s"Destroyed texture (ID: $texture)")
|
|
||||||
GL11.glDeleteTextures(texture)
|
|
||||||
}
|
|
||||||
|
|
||||||
bind()
|
bind()
|
||||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)
|
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)
|
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)
|
GL13.glActiveTexture(GL13.GL_TEXTURE0 + unit)
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture)
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def freeResource(): Unit = {
|
||||||
|
super.freeResource()
|
||||||
|
GL11.glDeleteTextures(texture)
|
||||||
|
logger.debug(s"Destroyed texture (ID: $texture)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,10 @@ class Buffer[T <: BufferPut] extends Logging with Resource {
|
|||||||
var capacity: Int = _
|
var capacity: Int = _
|
||||||
var stride: Int = _
|
var stride: Int = _
|
||||||
|
|
||||||
def freeResource(): Unit = {
|
override def freeResource(): Unit = {
|
||||||
logger.debug(s"Destroyed buffer (ID: $buffer) of ${capacity * stride} bytes")
|
super.freeResource()
|
||||||
GL15.glDeleteBuffers(buffer)
|
GL15.glDeleteBuffers(buffer)
|
||||||
|
logger.debug(s"Destroyed buffer (ID: $buffer) of ${capacity * stride} bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
def this(elements: Seq[T]) = {
|
def this(elements: Seq[T]) = {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package ocelot.desktop.graphics.mesh
|
|||||||
|
|
||||||
import ocelot.desktop.graphics.ShaderProgram
|
import ocelot.desktop.graphics.ShaderProgram
|
||||||
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
|
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
|
||||||
import ocelot.desktop.util.{Logging, Resource, ResourceManager}
|
import ocelot.desktop.util.{Logging, Resource}
|
||||||
import org.lwjgl.opengl._
|
import org.lwjgl.opengl._
|
||||||
|
|
||||||
class VertexArray(shader: ShaderProgram) extends Logging with Resource {
|
class VertexArray(shader: ShaderProgram) extends Logging with Resource {
|
||||||
@ -13,16 +13,6 @@ class VertexArray(shader: ShaderProgram) extends Logging with Resource {
|
|||||||
else
|
else
|
||||||
ARBVertexArrayObject.glGenVertexArrays()
|
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 = {
|
def addVertexBuffer[V <: Vertex](buffer: VertexBuffer[V], instanced: Boolean = false): Unit = {
|
||||||
bind()
|
bind()
|
||||||
|
|
||||||
@ -47,4 +37,13 @@ class VertexArray(shader: ShaderProgram) extends Logging with Resource {
|
|||||||
APPLEVertexArrayObject.glBindVertexArrayAPPLE(array)
|
APPLEVertexArrayObject.glBindVertexArrayAPPLE(array)
|
||||||
else
|
else
|
||||||
ARBVertexArrayObject.glBindVertexArray(array)
|
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)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,14 @@ package ocelot.desktop.graphics.render
|
|||||||
import ocelot.desktop.graphics.ShaderProgram
|
import ocelot.desktop.graphics.ShaderProgram
|
||||||
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
|
import ocelot.desktop.graphics.buffer.{IndexBuffer, VertexBuffer}
|
||||||
import ocelot.desktop.graphics.mesh.{Mesh, MeshInstance, MeshVertex, VertexArray}
|
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.BufferUtils
|
||||||
import org.lwjgl.opengl._
|
import org.lwjgl.opengl._
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import scala.collection.mutable.ArrayBuffer
|
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 InitialCapacity: Int = 4096
|
||||||
|
|
||||||
private val vertexBuffer = new VertexBuffer[MeshVertex](mesh.vertices)
|
private val vertexBuffer = new VertexBuffer[MeshVertex](mesh.vertices)
|
||||||
@ -81,4 +81,12 @@ class InstanceRenderer(mesh: Mesh, shader: ShaderProgram) extends Logging {
|
|||||||
data.flip
|
data.flip
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def freeResource(): Unit = {
|
||||||
|
super.freeResource()
|
||||||
|
vertexArray.freeResource()
|
||||||
|
instanceBuffer.freeResource()
|
||||||
|
indexBuffer.foreach(_.freeResource())
|
||||||
|
vertexBuffer.freeResource()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
232
src/main/scala/ocelot/desktop/inventory/Inventory.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/scala/ocelot/desktop/inventory/Item.scala
Normal 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
|
||||||
|
}
|
||||||
59
src/main/scala/ocelot/desktop/inventory/ItemFactory.scala
Normal 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[_, _]]
|
||||||
|
}
|
||||||
18
src/main/scala/ocelot/desktop/inventory/ItemRecoverer.scala
Normal 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)
|
||||||
|
}
|
||||||
142
src/main/scala/ocelot/desktop/inventory/Items.scala
Normal 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)
|
||||||
|
}
|
||||||
119
src/main/scala/ocelot/desktop/inventory/PersistedInventory.scala
Normal 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)
|
||||||
|
}
|
||||||
294
src/main/scala/ocelot/desktop/inventory/SyncedInventory.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/scala/ocelot/desktop/inventory/item/ApuItem.scala
Normal 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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/main/scala/ocelot/desktop/inventory/item/CpuItem.scala
Normal 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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/main/scala/ocelot/desktop/inventory/item/EepromItem.scala
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/main/scala/ocelot/desktop/inventory/item/HddItem.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(_)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 = {}
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import ocelot.desktop.OcelotDesktop
|
|||||||
import ocelot.desktop.color.{Color, RGBAColor}
|
import ocelot.desktop.color.{Color, RGBAColor}
|
||||||
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
|
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
|
||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
|
import ocelot.desktop.node.Node.{HoveredHighlight, MovingHighlight, NoHighlight}
|
||||||
import ocelot.desktop.ui.UiHandler
|
import ocelot.desktop.ui.UiHandler
|
||||||
import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler}
|
import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler}
|
||||||
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent}
|
import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent}
|
||||||
@ -20,17 +21,20 @@ import totoro.ocelot.brain.util.Direction
|
|||||||
|
|
||||||
import scala.collection.mutable.ArrayBuffer
|
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))
|
if (!OcelotDesktop.workspace.getEntitiesIter.contains(entity))
|
||||||
OcelotDesktop.workspace.add(entity)
|
OcelotDesktop.workspace.add(entity)
|
||||||
|
|
||||||
var workspaceView: WorkspaceView = _
|
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 highlight = new ColorAnimation(RGBAColor(0, 0, 0, 0))
|
||||||
protected val canOpen = false
|
protected val canOpen = false
|
||||||
|
|
||||||
protected def exposeAddress = true
|
protected def exposeAddress = true
|
||||||
|
|
||||||
protected var isMoving = false
|
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 = {
|
def load(nbt: NBTTagCompound): Unit = {
|
||||||
position = new Vector2D(nbt.getCompoundTag("pos"))
|
position = new Vector2D(nbt.getCompoundTag("pos"))
|
||||||
window.foreach(window => {
|
window.foreach(
|
||||||
val tag = nbt.getCompoundTag("window")
|
window => {
|
||||||
window.load(tag)
|
val tag = nbt.getCompoundTag("window")
|
||||||
})
|
window.load(tag)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
val lbl = nbt.getString("label")
|
val lbl = nbt.getString("label")
|
||||||
label = if (lbl == "") None else Some(lbl)
|
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)
|
position.save(posTag)
|
||||||
nbt.setTag("pos", posTag)
|
nbt.setTag("pos", posTag)
|
||||||
|
|
||||||
window.foreach(window => {
|
window.foreach(
|
||||||
val tag = new NBTTagCompound
|
window => {
|
||||||
window.save(tag)
|
val tag = new NBTTagCompound
|
||||||
nbt.setTag("window", tag)
|
window.save(tag)
|
||||||
})
|
nbt.setTag("window", tag)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
nbt.setString("label", label.getOrElse(""))
|
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 = {
|
def setupContextMenu(menu: ContextMenu): Unit = {
|
||||||
if (exposeAddress) { // TODO: lift the restriction
|
if (exposeAddress) { // TODO: lift the restriction
|
||||||
menu.addEntry(new ContextMenuEntry(
|
menu.addEntry(
|
||||||
"Set label",
|
ContextMenuEntry("Set label") {
|
||||||
() => new InputDialog(
|
new InputDialog(
|
||||||
"Set label",
|
"Set label",
|
||||||
text => label = if (text.isEmpty) None else Some(text),
|
text => label = if (text.isEmpty) None else Some(text),
|
||||||
label.getOrElse("")
|
label.getOrElse(""),
|
||||||
).show()
|
).show()
|
||||||
))
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exposeAddress && entity.node != null && entity.node.address != null) {
|
if (exposeAddress && entity.node != null && entity.node.address != null) {
|
||||||
menu.addEntry(new ContextMenuEntry("Copy address", () => {
|
menu.addEntry(
|
||||||
UiHandler.clipboard = entity.node.address
|
ContextMenuEntry("Copy address") {
|
||||||
}))
|
UiHandler.clipboard = entity.node.address
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.addEntry(new ContextMenuEntry("Disconnect", () => {
|
menu.addEntry(
|
||||||
disconnectFromAll()
|
ContextMenuEntry("Disconnect") {
|
||||||
}))
|
disconnectFromAll()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
menu.addEntry(new ContextMenuEntry("Delete", () => {
|
menu.addEntry(
|
||||||
dispose()
|
ContextMenuEntry("Delete") {
|
||||||
workspaceView.nodes = workspaceView.nodes.filter(_ != this)
|
destroy()
|
||||||
}))
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def update(): Unit = {
|
override def update(): Unit = {
|
||||||
super.update()
|
super.update()
|
||||||
if (isHovered || isMoving) {
|
if (isHovered || isMoving) {
|
||||||
if (canOpen)
|
if (canOpen) {
|
||||||
root.get.statusBar.addMouseEntry("icons/LMB", "Open")
|
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/RMB", "Menu")
|
||||||
root.get.statusBar.addMouseEntry("icons/DragLMB", "Move node")
|
root.get.statusBar.addMouseEntry("icons/DragLMB", "Move node")
|
||||||
root.get.statusBar.addMouseEntry("icons/DragRMB", "Connect/Disconnect")
|
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)
|
node.onConnectionRemoved(portB, this, portA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def destroy(): Unit = {
|
||||||
|
dispose()
|
||||||
|
workspaceView.nodes = workspaceView.nodes.filter(_ != this)
|
||||||
|
}
|
||||||
|
|
||||||
def disconnectFromAll(): Unit = {
|
def disconnectFromAll(): Unit = {
|
||||||
for ((a, node, b) <- connections.toArray) {
|
for ((a, node, b) <- connections.toArray) {
|
||||||
disconnect(a, node, b)
|
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 = {
|
def onClick(event: ClickEvent): Unit = {
|
||||||
event match {
|
event match {
|
||||||
case ClickEvent(MouseEvent.Button.Left, _) =>
|
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, _) =>
|
case ClickEvent(MouseEvent.Button.Right, _) =>
|
||||||
val menu = new ContextMenu
|
val menu = new ContextMenu
|
||||||
setupContextMenu(menu)
|
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])] = {
|
def portsBounds: Iterator[(NodePort, Array[Rect2D])] = {
|
||||||
val length = -4
|
val length = -4
|
||||||
val thickness = 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 left = Rect2D(centers(3) + Vector2D(-length, -thickness / 2), hsize)
|
||||||
val centersBounds = Array[Rect2D](top, right, bottom, left)
|
val centersBounds = Array[Rect2D](top, right, bottom, left)
|
||||||
|
|
||||||
val portBounds = (0 until 4).map(side => {
|
val portBounds = (0 until 4).map(
|
||||||
val offset = thickness - numPorts * stride / 2 + portIdx * stride
|
side => {
|
||||||
val rect = centersBounds(side)
|
val offset = thickness - numPorts * stride / 2 + portIdx * stride
|
||||||
side match {
|
val rect = centersBounds(side)
|
||||||
case 0 | 2 => rect.mapX(_ + offset)
|
side match {
|
||||||
case 1 | 3 => rect.mapY(_ + offset)
|
case 0 | 2 => rect.mapX(_ + offset)
|
||||||
}
|
case 1 | 3 => rect.mapY(_ + offset)
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
(port, portBounds.toArray)
|
(port, portBounds.toArray)
|
||||||
}
|
}
|
||||||
@ -251,7 +281,10 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
|
|||||||
val oldPos = position
|
val oldPos = position
|
||||||
|
|
||||||
val desiredPos = if (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL))
|
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
|
else
|
||||||
pos - grabPoint
|
pos - grabPoint
|
||||||
|
|
||||||
@ -312,3 +345,9 @@ abstract class Node(val entity: Entity with Environment) extends Widget with Dra
|
|||||||
OcelotDesktop.workspace.remove(entity)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,60 +1,60 @@
|
|||||||
package ocelot.desktop.node
|
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.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
|
import scala.collection.mutable
|
||||||
|
|
||||||
object NodeRegistry {
|
object NodeRegistry {
|
||||||
val types: mutable.ArrayBuffer[NodeType] = mutable.ArrayBuffer[NodeType]()
|
val types: mutable.ArrayBuffer[NodeType] = mutable.ArrayBuffer[NodeType]()
|
||||||
|
|
||||||
def register(t: NodeType): Unit = {
|
private def register(t: NodeType): Unit = {
|
||||||
types += t
|
types += t
|
||||||
}
|
}
|
||||||
|
|
||||||
for (tier <- 0 to 2) {
|
for (tier <- Tier.One to Tier.Three) {
|
||||||
register(NodeType("Screen" + tier, "nodes/Screen", tier, () => {
|
register(NodeType(s"Screen (${tier.label})", "nodes/Screen", tier) {
|
||||||
new ScreenNode(new Screen(tier)).setup()
|
new ScreenNode(new Screen(tier)).setup()
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
register(NodeType("Disk Drive", "nodes/DiskDrive", -1, () => {
|
register(NodeType("Disk Drive", "nodes/DiskDrive", None) {
|
||||||
new DiskDriveNode(new FloppyDiskDrive())
|
new DiskDriveNode(new FloppyDiskDrive(), initDisk = true)
|
||||||
}))
|
})
|
||||||
|
|
||||||
for (tier <- 0 to 3) {
|
for (tier <- Tier.One to Tier.Creative) {
|
||||||
register(NodeType("Computer" + tier, "nodes/Computer", tier, () => {
|
register(NodeType(s"Computer Case (${tier.label})", "nodes/Computer", tier) {
|
||||||
new ComputerNode(new Case(tier)).setup()
|
new ComputerNode(new Case(tier)).setup()
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
register(NodeType("Relay", "nodes/Relay", -1, () => {
|
register(NodeType("Relay", "nodes/Relay", None) {
|
||||||
new RelayNode(new Relay)
|
new RelayNode(new Relay)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("Cable", "nodes/Cable", -1, () => {
|
register(NodeType("Cable", "nodes/Cable", None) {
|
||||||
new CableNode(new Cable)
|
new CableNode(new Cable)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("NoteBlock", "nodes/NoteBlock", -1, () => {
|
register(NodeType("Note Block", "nodes/NoteBlock", None) {
|
||||||
new NoteBlockNode(new NoteBlock)
|
new NoteBlockNode(new NoteBlock)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("IronNoteBlock", "nodes/IronNoteBlock", -1, () => {
|
register(NodeType("Iron Note Block", "nodes/IronNoteBlock", None) {
|
||||||
new IronNoteBlockNode(new IronNoteBlock)
|
new IronNoteBlockNode(new IronNoteBlock)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("Camera", "nodes/Camera", -1, () => {
|
register(NodeType("Camera", "nodes/Camera", None) {
|
||||||
new CameraNode(new Camera)
|
new CameraNode(new Camera)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("ColorfulLamp", "nodes/Lamp", -1, () => {
|
register(NodeType("Colorful Lamp", "nodes/Lamp", None) {
|
||||||
new ColorfulLampNode(new ColorfulLamp)
|
new ColorfulLampNode(new ColorfulLamp)
|
||||||
}))
|
})
|
||||||
|
|
||||||
register(NodeType("OpenFM radio", "nodes/OpenFMRadio", -1, () => {
|
register(NodeType("OpenFM radio", "nodes/OpenFMRadio", None) {
|
||||||
new OpenFMRadioNode(new OpenFMRadio)
|
new OpenFMRadioNode(new OpenFMRadio)
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
package ocelot.desktop.node
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package ocelot.desktop.node
|
package ocelot.desktop.node
|
||||||
|
|
||||||
|
import ocelot.desktop.color.Color
|
||||||
import ocelot.desktop.geometry.Size2D
|
import ocelot.desktop.geometry.Size2D
|
||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
|
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.Widget
|
||||||
|
import ocelot.desktop.ui.widget.tooltip.LabelTooltip
|
||||||
import ocelot.desktop.util.{Spritesheet, TierColor}
|
import ocelot.desktop.util.{Spritesheet, TierColor}
|
||||||
|
|
||||||
class NodeTypeWidget(val nodeType: NodeType) extends Widget with ClickHandler with HoverHandler {
|
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 += {
|
eventHandlers += {
|
||||||
case ClickEvent(MouseEvent.Button.Left, _) => onClick()
|
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 = {
|
override def draw(g: Graphics): Unit = {
|
||||||
val size = Spritesheet.spriteSize(nodeType.icon) * 4
|
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 = {
|
override def update(): Unit = {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import totoro.ocelot.brain.entity.ColorfulLamp
|
|||||||
class ColorfulLampNode(val lamp: ColorfulLamp) extends Node(lamp) {
|
class ColorfulLampNode(val lamp: ColorfulLamp) extends Node(lamp) {
|
||||||
private var lastColor: RGBAColor = RGBAColor(0, 0, 0)
|
private var lastColor: RGBAColor = RGBAColor(0, 0, 0)
|
||||||
private var mouseHover: Boolean = false
|
private var mouseHover: Boolean = false
|
||||||
override def exposeAddress = mouseHover
|
override def exposeAddress: Boolean = mouseHover
|
||||||
|
|
||||||
override def draw(g: Graphics): Unit = {
|
override def draw(g: Graphics): Unit = {
|
||||||
super.draw(g)
|
super.draw(g)
|
||||||
|
|||||||
@ -1,46 +1,115 @@
|
|||||||
package ocelot.desktop.node.nodes
|
package ocelot.desktop.node.nodes
|
||||||
|
|
||||||
|
import ocelot.desktop.ColorScheme
|
||||||
import ocelot.desktop.audio._
|
import ocelot.desktop.audio._
|
||||||
import ocelot.desktop.color.Color
|
import ocelot.desktop.color.Color
|
||||||
|
import ocelot.desktop.geometry.Vector2D
|
||||||
import ocelot.desktop.graphics.Graphics
|
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.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.sources.KeyEvents
|
||||||
import ocelot.desktop.ui.event.{BrainEvent, ClickEvent, MouseEvent}
|
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.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
|
||||||
import ocelot.desktop.ui.widget.slot._
|
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 ocelot.desktop.windows.ComputerWindow
|
||||||
import org.lwjgl.input.Keyboard
|
import org.lwjgl.input.Keyboard
|
||||||
import totoro.ocelot.brain.Settings
|
import totoro.ocelot.brain.Settings
|
||||||
import totoro.ocelot.brain.entity.traits.{Entity, Environment, Floppy, GenericCPU, Inventory}
|
import totoro.ocelot.brain.entity.Case
|
||||||
import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, HDDUnmanaged, Memory}
|
import totoro.ocelot.brain.entity.traits.{Environment, Inventory}
|
||||||
import totoro.ocelot.brain.event.FileSystemActivityType.Floppy
|
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.loot.Loot
|
||||||
|
import totoro.ocelot.brain.nbt.NBTTagCompound
|
||||||
import totoro.ocelot.brain.util.Tier
|
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.reflect.ClassTag
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
class ComputerNode(val computer: Case) extends Node(computer) with Logging with SyncedInventory {
|
||||||
var eepromSlot: EEPROMSlot = _
|
node =>
|
||||||
var cpuSlot: CPUSlot = _
|
|
||||||
var memorySlots: Array[MemorySlot] = _
|
|
||||||
var cardSlots: Array[CardSlot] = _
|
|
||||||
var diskSlots: Array[DiskSlot] = _
|
|
||||||
var floppySlot: Option[FloppySlot] = None
|
|
||||||
|
|
||||||
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 soundCardStream: SoundStream = _
|
||||||
private var soundCardSource: SoundSource = _
|
private var soundCardSource: SoundSource = _
|
||||||
|
|
||||||
setupSlots()
|
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 += {
|
eventHandlers += {
|
||||||
case BrainEvent(event: MachineCrashEvent) =>
|
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 =>
|
case BrainEvent(event: BeepEvent) if !Audio.isDisabled =>
|
||||||
BeepGenerator.newBeep(".", event.frequency, event.duration).play()
|
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()
|
BeepGenerator.newBeep(event.pattern, 1000, 200).play()
|
||||||
|
|
||||||
case BrainEvent(event: FileSystemActivityEvent) if !Audio.isDisabled =>
|
case BrainEvent(event: FileSystemActivityEvent) if !Audio.isDisabled =>
|
||||||
val soundFloppyAccess = SoundBuffers.MachineFloppyAccess.map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
|
val soundFloppyAccess = SoundBuffers.MachineFloppyAccess
|
||||||
val soundHDDAccess = SoundBuffers.MachineHDDAccess.map(buffer => SoundSource.fromBuffer(buffer, SoundCategory.Environment))
|
.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
|
val sound = if (event.activityType == Floppy) soundFloppyAccess else soundHDDAccess
|
||||||
sound(Random.between(0, sound.length)).play()
|
sound(Random.between(0, sound.length)).play()
|
||||||
|
|
||||||
@ -63,19 +134,31 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
}
|
}
|
||||||
soundCardStream.enqueue(samples)
|
soundCardStream.enqueue(samples)
|
||||||
soundCardSource.volume = event.volume
|
soundCardSource.volume = event.volume
|
||||||
|
|
||||||
|
case BrainEvent(_: SelfDestructingCardBoomEvent) =>
|
||||||
|
computer.workspace.runLater(
|
||||||
|
() => {
|
||||||
|
SoundSources.MinecraftExplosion.play()
|
||||||
|
destroy()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) ||
|
override def shouldReceiveEventsFor(address: String): Boolean = super.shouldReceiveEventsFor(address) ||
|
||||||
computer.inventory.entities.exists { case env: Environment => env.node.address == address }
|
computer.inventory.entities.exists { case env: Environment => env.node.address == address }
|
||||||
|
|
||||||
def setup(): ComputerNode = {
|
def setup(): ComputerNode = {
|
||||||
cpuSlot.owner.put(new CPU(computer.tier.min(2)))
|
cpuSlot.item = new CpuItem.Factory(computer.tier min Tier.Three).build()
|
||||||
memorySlots(0).owner.put(new Memory(computer.tier.min(2) * 2 + 1))
|
memorySlots(0).item = new MemoryItem.Factory((computer.tier min Tier.Three).toExtended(true)).build()
|
||||||
memorySlots(1).owner.put(new Memory(computer.tier.min(2) * 2 + 1))
|
memorySlots(1).item = new MemoryItem.Factory((computer.tier min Tier.Three).toExtended(true)).build()
|
||||||
cardSlots(0).owner.put(new GraphicsCard(computer.tier.min(1)))
|
cardSlots(0).item = new GraphicsCardItem.Factory(computer.tier min Tier.Two).build()
|
||||||
floppySlot.map(_.owner).foreach(_.put(Loot.OpenOsFloppy.create()))
|
|
||||||
eepromSlot.owner.put(Loot.LuaBiosEEPROM.create())
|
for (floppySlot <- floppySlot) {
|
||||||
refitSlots()
|
floppySlot.item = new FloppyItem.Factory.Loot(Loot.OpenOsFloppy).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
eepromSlot.item = new EepromItem.Factory.Loot(Loot.LuaBiosEEPROM).build()
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,20 +182,36 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
|
|
||||||
override def setupContextMenu(menu: ContextMenu): Unit = {
|
override def setupContextMenu(menu: ContextMenu): Unit = {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
menu.addEntry(new ContextMenuEntry("Turn off", () => turnOff()))
|
menu.addEntry(
|
||||||
menu.addEntry(new ContextMenuEntry("Reboot", () => {
|
ContextMenuEntry("Turn off") {
|
||||||
computer.turnOff()
|
turnOff()
|
||||||
computer.turnOn()
|
},
|
||||||
}))
|
)
|
||||||
} else
|
menu.addEntry(
|
||||||
menu.addEntry(new ContextMenuEntry("Turn on", () => turnOn()))
|
ContextMenuEntry("Reboot") {
|
||||||
|
computer.turnOff()
|
||||||
|
computer.turnOn()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
menu.addEntry(
|
||||||
|
ContextMenuEntry("Turn on") {
|
||||||
|
turnOn()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
menu.addEntry(new ContextMenuSubmenu("Set tier") {
|
menu.addEntry(
|
||||||
addEntry(new ContextMenuEntry("Tier 1", () => changeTier(Tier.One)))
|
new ContextMenuSubmenu("Set tier") {
|
||||||
addEntry(new ContextMenuEntry("Tier 2", () => changeTier(Tier.Two)))
|
for (tier <- Tier.One to Tier.Creative) {
|
||||||
addEntry(new ContextMenuEntry("Tier 3", () => changeTier(Tier.Three)))
|
addEntry(
|
||||||
addEntry(new ContextMenuEntry("Creative", () => changeTier(Tier.Four)))
|
ContextMenuEntry(tier.label) {
|
||||||
})
|
changeTier(tier)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
super.setupContextMenu(menu)
|
super.setupContextMenu(menu)
|
||||||
@ -121,107 +220,63 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
override def onClick(event: ClickEvent): Unit = {
|
override def onClick(event: ClickEvent): Unit = {
|
||||||
event match {
|
event match {
|
||||||
case ClickEvent(MouseEvent.Button.Left, _) =>
|
case ClickEvent(MouseEvent.Button.Left, _) =>
|
||||||
if (KeyEvents.isDown(Keyboard.KEY_LSHIFT))
|
if (KeyEvents.isDown(Keyboard.KEY_LSHIFT)) {
|
||||||
if (isRunning)
|
if (isRunning) {
|
||||||
turnOff()
|
turnOff()
|
||||||
else
|
} else {
|
||||||
turnOn()
|
turnOn()
|
||||||
else
|
}
|
||||||
|
} else {
|
||||||
super.onClick(event)
|
super.onClick(event)
|
||||||
|
}
|
||||||
|
|
||||||
case event => super.onClick(event)
|
case event => super.onClick(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def changeTier(n: Int): Unit = {
|
private def changeTier(tier: Tier): Unit = {
|
||||||
computer.tier = n
|
computer.tier = tier
|
||||||
|
|
||||||
|
val items = slots.iterator.flatMap(_.item).toArray
|
||||||
|
clearInventory()
|
||||||
setupSlots()
|
setupSlots()
|
||||||
refitSlots()
|
insertItems(items)
|
||||||
if (currentWindow != null) currentWindow.updateSlots()
|
|
||||||
}
|
|
||||||
|
|
||||||
private def slotAccepts(slot: Inventory#Slot, entity: Entity): Boolean = entity match {
|
if (currentWindow != null) {
|
||||||
case cpu: GenericCPU => cpuSlot.owner.index == slot.index && cpuSlot.tier >= cpu.cpuTier
|
currentWindow.reloadWindow()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val entities = computer.inventory.entities.toArray
|
private def insertItems(items: IterableOnce[I]): Unit = {
|
||||||
computer.inventory.clear()
|
def findBestSlot[A <: I](item: A, candidates: IterableOnce[SlotWidget[A]]): Option[SlotWidget[A]] = {
|
||||||
|
|
||||||
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] = {
|
|
||||||
candidates.iterator
|
candidates.iterator
|
||||||
.filter(_.owner.isEmpty)
|
.filter(_.item.isEmpty)
|
||||||
.filter(slot => slotAccepts(slot.owner, entity))
|
.filter(_.isItemAccepted(item.factory))
|
||||||
.minByOption(tierProvider(_).getOrElse(Int.MinValue))
|
.minByOption(_.slotTier)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (entity <- entities) {
|
for (item <- items; newSlot <- findBestSlot(item, slots)) {
|
||||||
val newSlot = entity match {
|
newSlot.item = item
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadSlots()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def setupSlots(): Unit = {
|
private def setupSlots(): Unit = {
|
||||||
var slotIndex = 0
|
var slotIndex = 0
|
||||||
|
|
||||||
def nextSlot(): computer.Slot = {
|
def nextSlot(): Slot = {
|
||||||
val result = computer.inventory(slotIndex)
|
val result = Slot(slotIndex)
|
||||||
slotIndex += 1
|
slotIndex += 1
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
def addSlot[T <: InventorySlot[_]](factory: computer.Slot => T): T = {
|
def addSlot[T <: SlotWidget[_]](factory: Slot => T): T = {
|
||||||
val slot = nextSlot()
|
val slot = nextSlot()
|
||||||
val widget = factory(slot)
|
val widget = factory(slot)
|
||||||
|
|
||||||
widget
|
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]
|
val array = Array.newBuilder[T]
|
||||||
|
|
||||||
for (factory <- factories) {
|
for (factory <- factories) {
|
||||||
@ -231,43 +286,51 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
array.result()
|
array.result()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (slot <- slots) {
|
||||||
|
slot.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
computer.tier match {
|
computer.tier match {
|
||||||
case Tier.One =>
|
case Tier.One =>
|
||||||
cardSlots = addSlots(new CardSlot(_, Tier.One), new CardSlot(_, Tier.One))
|
cardSlots = addSlots(new CardSlotWidget(_, Tier.One), new CardSlotWidget(_, Tier.One))
|
||||||
memorySlots = addSlots(new MemorySlot(_, Tier.One))
|
memorySlots = addSlots(new MemorySlotWidget(_, Tier.One))
|
||||||
diskSlots = addSlots(new DiskSlot(_, Tier.One))
|
diskSlots = addSlots(new HddSlotWidget(_, Tier.One))
|
||||||
floppySlot = None
|
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
|
// no idea why on earth the memory slots are split in two here
|
||||||
memorySlots :+= addSlot(new MemorySlot(_, Tier.One))
|
memorySlots :+= addSlot(new MemorySlotWidget(_, Tier.One))
|
||||||
eepromSlot = addSlot(new EEPROMSlot(_))
|
eepromSlot = addSlot(new EepromSlotWidget(_))
|
||||||
|
|
||||||
case Tier.Two =>
|
case Tier.Two =>
|
||||||
cardSlots = addSlots(new CardSlot(_, Tier.Two), new CardSlot(_, Tier.One))
|
cardSlots = addSlots(new CardSlotWidget(_, Tier.Two), new CardSlotWidget(_, Tier.One))
|
||||||
memorySlots = addSlots(new MemorySlot(_, Tier.Two), new MemorySlot(_, Tier.Two))
|
memorySlots = addSlots(new MemorySlotWidget(_, Tier.Two), new MemorySlotWidget(_, Tier.Two))
|
||||||
diskSlots = addSlots(new DiskSlot(_, Tier.Two), new DiskSlot(_, Tier.One))
|
diskSlots = addSlots(new HddSlotWidget(_, Tier.Two), new HddSlotWidget(_, Tier.One))
|
||||||
floppySlot = None
|
floppySlot = None
|
||||||
cpuSlot = addSlot(new CPUSlot(_, this, Tier.Two))
|
cpuSlot = addSlot(new CpuSlotWidget(_, this, Tier.Two))
|
||||||
eepromSlot = addSlot(new EEPROMSlot(_))
|
eepromSlot = addSlot(new EepromSlotWidget(_))
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
cardSlots = if (computer.tier == Tier.Three) {
|
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 {
|
} 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) {
|
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 {
|
} else {
|
||||||
addSlots(new DiskSlot(_, Tier.Three), new DiskSlot(_, Tier.Three))
|
addSlots(new HddSlotWidget(_, Tier.Three), new HddSlotWidget(_, Tier.Three))
|
||||||
}
|
}
|
||||||
|
|
||||||
floppySlot = Some(addSlot(new FloppySlot(_)))
|
floppySlot = Some(addSlot(new FloppySlotWidget(_)))
|
||||||
cpuSlot = addSlot(new CPUSlot(_, this, Tier.Three))
|
cpuSlot = addSlot(new CpuSlotWidget(_, this, Tier.Three))
|
||||||
eepromSlot = addSlot(new EEPROMSlot(_))
|
eepromSlot = addSlot(new EepromSlotWidget(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,10 +351,21 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
|
|
||||||
override def update(): Unit = {
|
override def update(): Unit = {
|
||||||
super.update()
|
super.update()
|
||||||
if (!isRunning && soundComputerRunning.isPlaying)
|
|
||||||
|
if (!isRunning && soundComputerRunning.isPlaying) {
|
||||||
soundComputerRunning.stop()
|
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")
|
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 = _
|
private var currentWindow: ComputerWindow = _
|
||||||
@ -303,4 +377,25 @@ class ComputerNode(val computer: Case) extends Node(computer) with Logging {
|
|||||||
} else Some(currentWindow)
|
} 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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,57 +2,83 @@ package ocelot.desktop.node.nodes
|
|||||||
|
|
||||||
import ocelot.desktop.color.IntColor
|
import ocelot.desktop.color.IntColor
|
||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
|
import ocelot.desktop.inventory.SyncedInventory
|
||||||
|
import ocelot.desktop.inventory.item.FloppyItem
|
||||||
import ocelot.desktop.node.Node
|
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 ocelot.desktop.windows.DiskDriveWindow
|
||||||
import totoro.ocelot.brain.entity.FloppyDiskDrive
|
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.loot.Loot
|
||||||
|
import totoro.ocelot.brain.nbt.NBTTagCompound
|
||||||
import totoro.ocelot.brain.util.DyeColor
|
import totoro.ocelot.brain.util.DyeColor
|
||||||
|
|
||||||
class DiskDriveNode(val diskDrive: FloppyDiskDrive) extends Node(diskDrive) {
|
class DiskDriveNode(val diskDrive: FloppyDiskDrive, initDisk: Boolean) extends Node(diskDrive) with SyncedInventory {
|
||||||
val slot: FloppySlot = new FloppySlot(diskDrive.inventory(0))
|
def this(diskDrive: FloppyDiskDrive) = {
|
||||||
slot.item = floppy.getOrElse(Loot.OpenOsFloppy.create())
|
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 def icon: String = "nodes/DiskDrive"
|
||||||
|
|
||||||
override protected val canOpen = true
|
override protected val canOpen = true
|
||||||
|
|
||||||
private val colorMap: Map[DyeColor, Int] = Map(
|
private val colorMap: Map[DyeColor, Int] = Map(
|
||||||
DyeColor.BLACK -> 0x444444, // 0x1E1B1B
|
DyeColor.Black -> 0x444444, // 0x1E1B1B
|
||||||
DyeColor.RED -> 0xB3312C,
|
DyeColor.Red -> 0xB3312C,
|
||||||
DyeColor.GREEN -> 0x339911, // 0x3B511A
|
DyeColor.Green -> 0x339911, // 0x3B511A
|
||||||
DyeColor.BROWN -> 0x51301A,
|
DyeColor.Brown -> 0x51301A,
|
||||||
DyeColor.BLUE -> 0x6666FF, // 0x253192
|
DyeColor.Blue -> 0x6666FF, // 0x253192
|
||||||
DyeColor.PURPLE -> 0x7B2FBE,
|
DyeColor.Purple -> 0x7B2FBE,
|
||||||
DyeColor.CYAN -> 0x66FFFF, // 0x287697
|
DyeColor.Cyan -> 0x66FFFF, // 0x287697
|
||||||
DyeColor.SILVER -> 0xABABAB,
|
DyeColor.Silver -> 0xABABAB,
|
||||||
DyeColor.GRAY -> 0x666666, // 0x434343
|
DyeColor.Gray -> 0x666666, // 0x434343
|
||||||
DyeColor.PINK -> 0xD88198,
|
DyeColor.Pink -> 0xD88198,
|
||||||
DyeColor.LIME -> 0x66FF66, // 0x41CD34
|
DyeColor.Lime -> 0x66FF66, // 0x41CD34
|
||||||
DyeColor.YELLOW -> 0xFFFF66, // 0xDECF2A
|
DyeColor.Yellow -> 0xFFFF66, // 0xDECF2A
|
||||||
DyeColor.LIGHT_BLUE -> 0xAAAAFF, // 0x6689D3
|
DyeColor.LightBlue -> 0xAAAAFF, // 0x6689D3
|
||||||
DyeColor.MAGENTA -> 0xC354CD,
|
DyeColor.Magenta -> 0xC354CD,
|
||||||
DyeColor.ORANGE -> 0xEB8844,
|
DyeColor.Orange -> 0xEB8844,
|
||||||
DyeColor.WHITE -> 0xF0F0F0
|
DyeColor.White -> 0xF0F0F0
|
||||||
)
|
)
|
||||||
|
|
||||||
override def draw(g: Graphics): Unit = {
|
override def draw(g: Graphics): Unit = {
|
||||||
super.draw(g)
|
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)
|
g.sprite("nodes/DiskDriveActivity", position.x + 2, position.y + 2, size.width - 4, size.height - 4)
|
||||||
|
}
|
||||||
|
|
||||||
if (slot.item.isDefined)
|
for (item <- slot.item) {
|
||||||
g.sprite("nodes/DiskDriveFloppy", position.x + 2, position.y + 2, size.width - 4, size.height - 4, IntColor(colorMap(slot.item.get.color)))
|
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))
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,9 @@ class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBloc
|
|||||||
val maxLen = NoteBlockNode.Instruments.map(_._2.length).max
|
val maxLen = NoteBlockNode.Instruments.map(_._2.length).max
|
||||||
for ((instrument, name) <- NoteBlockNode.Instruments) {
|
for ((instrument, name) <- NoteBlockNode.Instruments) {
|
||||||
val dot = if (noteBlock.instrument == instrument) '•' else ' '
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -22,7 +22,7 @@ abstract class NoteBlockNodeBase(entity: Entity with Environment) extends Node(e
|
|||||||
|
|
||||||
private val particles = mutable.ArrayBuffer[(Float, Int)]()
|
private val particles = mutable.ArrayBuffer[(Float, Int)]()
|
||||||
|
|
||||||
def addParticle(pitch: Int): Unit = {
|
private def addParticle(pitch: Int): Unit = {
|
||||||
synchronized {
|
synchronized {
|
||||||
particles += ((0f, pitch))
|
particles += ((0f, pitch))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,9 +50,9 @@ class ScreenNode(val screen: Screen) extends Node(screen) {
|
|||||||
|
|
||||||
override def setupContextMenu(menu: ContextMenu): Unit = {
|
override def setupContextMenu(menu: ContextMenu): Unit = {
|
||||||
if (screen.getPowerState)
|
if (screen.getPowerState)
|
||||||
menu.addEntry(new ContextMenuEntry("Turn off", () => screen.setPowerState(false)))
|
menu.addEntry(ContextMenuEntry("Turn off") { screen.setPowerState(false) })
|
||||||
else
|
else
|
||||||
menu.addEntry(new ContextMenuEntry("Turn on", () => screen.setPowerState(true)))
|
menu.addEntry(ContextMenuEntry("Turn on") { screen.setPowerState(true) })
|
||||||
|
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
super.setupContextMenu(menu)
|
super.setupContextMenu(menu)
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
package ocelot.desktop.ui
|
package ocelot.desktop.ui
|
||||||
|
|
||||||
import buildinfo.BuildInfo
|
import buildinfo.BuildInfo
|
||||||
import ocelot.desktop.audio.Audio
|
import ocelot.desktop.audio.{Audio, SoundBuffers}
|
||||||
import ocelot.desktop.geometry.{Size2D, Vector2D}
|
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
|
||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
import ocelot.desktop.ui.event.MouseEvent
|
import ocelot.desktop.ui.event.MouseEvent
|
||||||
import ocelot.desktop.ui.event.handlers.HoverHandler
|
import ocelot.desktop.ui.event.handlers.HoverHandler
|
||||||
@ -34,8 +34,9 @@ object UiHandler extends Logging {
|
|||||||
private var shouldUpdateHierarchy = true
|
private var shouldUpdateHierarchy = true
|
||||||
private val fpsCalculator = new FPSCalculator
|
private val fpsCalculator = new FPSCalculator
|
||||||
private val ticker = new Ticker
|
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
|
def getHierarchy: Array[Widget] = hierarchy.toArray
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ object UiHandler extends Logging {
|
|||||||
def dt: Float = fpsCalculator.dt
|
def dt: Float = fpsCalculator.dt
|
||||||
|
|
||||||
def mousePosition: Vector2D = {
|
def mousePosition: Vector2D = {
|
||||||
Vector2D(Mouse.getX, Display.getHeight - Mouse.getY)
|
Vector2D(Mouse.getX, Display.getHeight - Mouse.getY) / scalingFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _clipboard = Toolkit.getDefaultToolkit.getSystemClipboard
|
private val _clipboard = Toolkit.getDefaultToolkit.getSystemClipboard
|
||||||
@ -80,19 +81,25 @@ object UiHandler extends Logging {
|
|||||||
_clipboard.setContents(data, data)
|
_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) {
|
if (value) {
|
||||||
Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode)
|
Display.setDisplayModeAndFullscreen(Display.getDesktopDisplayMode)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Updating size from settings
|
// Updating size from settings
|
||||||
val settingsSize = Settings.get.windowSize
|
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)
|
// 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
|
// Updating position from settings
|
||||||
if (Settings.get.windowPosition.isSet) {
|
if (Settings.get.windowPosition.isSet) {
|
||||||
@ -131,39 +138,100 @@ object UiHandler extends Logging {
|
|||||||
Settings.get.windowFullscreen = value
|
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 = {
|
def init(root: RootWidget): Unit = {
|
||||||
this.root = root
|
this.root = root
|
||||||
root.relayout()
|
root.relayout()
|
||||||
|
|
||||||
loadLibraries()
|
scalingFactor = Settings.get.scaleFactor
|
||||||
|
fullScreen = Settings.get.windowFullscreen
|
||||||
isFullScreen = Settings.get.windowFullscreen
|
|
||||||
windowTitle = "Ocelot Desktop v" + BuildInfo.version
|
windowTitle = "Ocelot Desktop v" + BuildInfo.version
|
||||||
|
|
||||||
loadIcons()
|
loadIcons()
|
||||||
Display.setVSyncEnabled(true)
|
|
||||||
|
if (!Settings.get.disableVsync) {
|
||||||
|
logger.info("VSync enabled")
|
||||||
|
Display.setVSyncEnabled(true)
|
||||||
|
} else {
|
||||||
|
logger.info("VSync disabled (via config)")
|
||||||
|
}
|
||||||
|
|
||||||
Display.create()
|
Display.create()
|
||||||
|
|
||||||
|
if (Settings.get.windowValidatePosition) {
|
||||||
|
fixInsaneInitialWindowGeometry()
|
||||||
|
}
|
||||||
|
|
||||||
KeyEvents.init()
|
KeyEvents.init()
|
||||||
MouseEvents.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 vendor: ${GL11.glGetString(GL11.GL_VENDOR)}")
|
||||||
logger.info(s"OpenGL renderer: ${GL11.glGetString(GL11.GL_RENDERER)}")
|
logger.info(s"OpenGL renderer: ${GL11.glGetString(GL11.GL_RENDERER)}")
|
||||||
|
logger.info(s"OpenGL version: ${GL11.glGetString(GL11.GL_VERSION)}")
|
||||||
|
|
||||||
Spritesheet.load()
|
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 var nativeLibrariesDir: String = _
|
||||||
|
|
||||||
private def loadLibraries(): Unit = {
|
def loadLibraries(): Unit = {
|
||||||
// we cannot remove DLL files on Windows after they were loaded by Ocelot
|
// 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
|
// therefore we will create them in local directory and keep for future
|
||||||
nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS)
|
nativeLibrariesDir = if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve( "natives")).toString
|
Paths.get(this.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.resolve("natives")).toString
|
||||||
else Files.createTempDirectory("ocelot-desktop").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 arch = System.getProperty("os.arch")
|
||||||
val is64bit = arch.startsWith("amd64")
|
val is64bit = arch.startsWith("amd64")
|
||||||
@ -202,6 +270,11 @@ object UiHandler extends Logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
System.setProperty("org.lwjgl.librarypath", nativeLibrariesDir)
|
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 = {
|
private def loadIcons(): Unit = {
|
||||||
@ -257,20 +330,21 @@ object UiHandler extends Logging {
|
|||||||
KeyEvents.update()
|
KeyEvents.update()
|
||||||
MouseEvents.update()
|
MouseEvents.update()
|
||||||
|
|
||||||
Profiler.startTimeMeasurement("000_update")
|
Profiler.measure("000_update") {
|
||||||
update()
|
update()
|
||||||
Profiler.endTimeMeasurement("000_update")
|
}
|
||||||
|
|
||||||
Profiler.startTimeMeasurement("001_draw")
|
Profiler.measure("001_draw") {
|
||||||
draw()
|
draw()
|
||||||
Profiler.endTimeMeasurement("001_draw")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Profiler.measure("002_sleep") {
|
||||||
|
ticker.waitNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
Profiler.startTimeMeasurement("002_sleep")
|
|
||||||
ticker.waitNext()
|
|
||||||
Display.update()
|
Display.update()
|
||||||
fpsCalculator.tick()
|
fpsCalculator.tick()
|
||||||
Profiler.endTimeMeasurement("002_sleep")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,11 +352,10 @@ object UiHandler extends Logging {
|
|||||||
root.workspaceView.dispose()
|
root.workspaceView.dispose()
|
||||||
KeyEvents.destroy()
|
KeyEvents.destroy()
|
||||||
MouseEvents.destroy()
|
MouseEvents.destroy()
|
||||||
|
graphics.freeResource()
|
||||||
|
SoundBuffers.freeResource()
|
||||||
Display.destroy()
|
Display.destroy()
|
||||||
Audio.destroy()
|
Audio.destroy()
|
||||||
|
|
||||||
if (!SystemUtils.IS_OS_WINDOWS)
|
|
||||||
FileUtils.deleteDirectory(new File(nativeLibrariesDir))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def update(): Unit = {
|
private def update(): Unit = {
|
||||||
@ -332,11 +405,15 @@ object UiHandler extends Logging {
|
|||||||
val width = Display.getWidth
|
val width = Display.getWidth
|
||||||
val height = Display.getHeight
|
val height = Display.getHeight
|
||||||
|
|
||||||
graphics.resize(width, height)
|
if (graphics.resize(width, height, scalingFactor)) {
|
||||||
root.size = Size2D(width, height)
|
// 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
|
// Settings fields should be updated only in non-fullscreen mode
|
||||||
if (isFullScreen)
|
if (fullScreen)
|
||||||
return
|
return
|
||||||
|
|
||||||
Settings.get.windowSize.set(width, height)
|
Settings.get.windowSize.set(width, height)
|
||||||
@ -344,7 +421,7 @@ object UiHandler extends Logging {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def draw(): Unit = {
|
private def draw(): Unit = {
|
||||||
graphics.setViewport(root.width.asInstanceOf[Int], root.height.asInstanceOf[Int])
|
graphics.startViewport()
|
||||||
graphics.clear()
|
graphics.clear()
|
||||||
|
|
||||||
root.draw(graphics)
|
root.draw(graphics)
|
||||||
|
|||||||
@ -11,26 +11,40 @@ import ocelot.desktop.util.DrawUtils
|
|||||||
|
|
||||||
class Button extends Widget with ClickHandler with ClickSoundSource {
|
class Button extends Widget with ClickHandler with ClickSoundSource {
|
||||||
def text: String = ""
|
def text: String = ""
|
||||||
|
|
||||||
def onClick(): Unit = {}
|
def onClick(): Unit = {}
|
||||||
|
|
||||||
|
def enabled: Boolean = true
|
||||||
|
|
||||||
override def receiveMouseEvents: Boolean = true
|
override def receiveMouseEvents: Boolean = true
|
||||||
|
|
||||||
eventHandlers += {
|
eventHandlers += {
|
||||||
case ClickEvent(MouseEvent.Button.Left, _) =>
|
case ClickEvent(MouseEvent.Button.Left, _) if enabled =>
|
||||||
onClick()
|
onClick()
|
||||||
|
|
||||||
clickSoundSource.play()
|
clickSoundSource.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
|
override def minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
|
||||||
|
|
||||||
override def maximumSize: Size2D = minimumSize
|
override def maximumSize: Size2D = minimumSize
|
||||||
|
|
||||||
override def draw(g: Graphics): Unit = {
|
override def draw(g: Graphics): Unit = {
|
||||||
g.rect(bounds, ColorScheme("ButtonBackground"))
|
val (background, border, foreground) = if (enabled) (
|
||||||
DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("ButtonBorder"))
|
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.background = Color.Transparent
|
||||||
g.foreground = ColorScheme("ButtonForeground")
|
g.foreground = foreground
|
||||||
val textWidth = text.iterator.map(g.font.charWidth(_)).sum
|
val textWidth = text.iterator.map(g.font.charWidth(_)).sum
|
||||||
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, text)
|
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, text)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,10 +91,10 @@ class ChangeSimulationSpeedDialog() extends ModalDialog {
|
|||||||
override def onClick(): Unit = close()
|
override def onClick(): Unit = close()
|
||||||
}, Padding2D(right = 8))
|
}, Padding2D(right = 8))
|
||||||
|
|
||||||
// TODO: disable the button if tickInterval.isEmpty
|
|
||||||
children :+= new Button {
|
children :+= new Button {
|
||||||
override def text: String = "Apply"
|
override def text: String = "Apply"
|
||||||
override def onClick(): Unit = confirm()
|
override def onClick(): Unit = confirm()
|
||||||
|
override def enabled: Boolean = tickInterval.nonEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, Padding2D.equal(16))
|
}, Padding2D.equal(16))
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import ocelot.desktop.ui.layout.LinearLayout
|
|||||||
import ocelot.desktop.ui.widget.modal.ModalDialog
|
import ocelot.desktop.ui.widget.modal.ModalDialog
|
||||||
import ocelot.desktop.util.Orientation
|
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 {
|
children :+= new PaddingBox(new Widget {
|
||||||
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override val layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
|
|
||||||
children :+= new PaddingBox(new Label {
|
children :+= new PaddingBox(new Label {
|
||||||
override def text = "Save workspace before exiting?"
|
override def text: String = prompt
|
||||||
}, Padding2D(bottom = 16))
|
}, Padding2D(bottom = 16))
|
||||||
|
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
@ -25,7 +27,7 @@ class ExitConfirmationDialog extends ModalDialog {
|
|||||||
|
|
||||||
children :+= new PaddingBox(new Button {
|
children :+= new PaddingBox(new Button {
|
||||||
override def text: String = "No"
|
override def text: String = "No"
|
||||||
override def onClick(): Unit = onExitSelected()
|
override def onClick(): Unit = onNoSaveSelected()
|
||||||
}, Padding2D(right = 8))
|
}, Padding2D(right = 8))
|
||||||
|
|
||||||
children :+= new PaddingBox(new Button {
|
children :+= new PaddingBox(new Button {
|
||||||
@ -37,5 +39,5 @@ class ExitConfirmationDialog extends ModalDialog {
|
|||||||
|
|
||||||
def onSaveSelected(): Unit = {}
|
def onSaveSelected(): Unit = {}
|
||||||
|
|
||||||
def onExitSelected(): Unit = {}
|
def onNoSaveSelected(): Unit = {}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,7 @@ import ocelot.desktop.geometry.Size2D
|
|||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
|
import ocelot.desktop.ui.event.handlers.{ClickHandler, HoverHandler}
|
||||||
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
|
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.animation.{ColorAnimation, ValueAnimation}
|
||||||
import ocelot.desktop.util.{DrawUtils, Spritesheet}
|
import ocelot.desktop.util.{DrawUtils, Spritesheet}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import ocelot.desktop.ui.UiHandler
|
|||||||
import ocelot.desktop.ui.event.MouseEvent
|
import ocelot.desktop.ui.event.MouseEvent
|
||||||
import totoro.ocelot.brain.util.DyeColor
|
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: Int
|
||||||
def input_=(v: Int): Unit
|
def input_=(v: Int): Unit
|
||||||
|
|
||||||
|
|||||||
@ -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 = {}
|
|
||||||
}
|
|
||||||
@ -3,8 +3,10 @@ package ocelot.desktop.ui.widget
|
|||||||
import ocelot.desktop.audio.SoundSources
|
import ocelot.desktop.audio.SoundSources
|
||||||
import ocelot.desktop.geometry.{Padding2D, Size2D}
|
import ocelot.desktop.geometry.{Padding2D, Size2D}
|
||||||
import ocelot.desktop.graphics.Graphics
|
import ocelot.desktop.graphics.Graphics
|
||||||
|
import ocelot.desktop.ui.event.KeyEvent
|
||||||
import ocelot.desktop.ui.widget.contextmenu.{ContextMenuEntry, ContextMenuSubmenu}
|
import ocelot.desktop.ui.widget.contextmenu.{ContextMenuEntry, ContextMenuSubmenu}
|
||||||
import ocelot.desktop.{ColorScheme, OcelotDesktop}
|
import ocelot.desktop.{ColorScheme, OcelotDesktop}
|
||||||
|
import org.lwjgl.input.Keyboard
|
||||||
|
|
||||||
class MenuBar extends Widget {
|
class MenuBar extends Widget {
|
||||||
override def receiveMouseEvents: Boolean = true
|
override def receiveMouseEvents: Boolean = true
|
||||||
@ -16,37 +18,40 @@ class MenuBar extends Widget {
|
|||||||
private def addEntry(w: Widget): Unit = entries.children :+= w
|
private def addEntry(w: Widget): Unit = entries.children :+= w
|
||||||
|
|
||||||
addEntry(new MenuBarSubmenu("File", menu => {
|
addEntry(new MenuBarSubmenu("File", menu => {
|
||||||
menu.addEntry(new ContextMenuEntry("New", () => OcelotDesktop.newWorkspace()))
|
menu.addEntry(ContextMenuEntry("New") { OcelotDesktop.newWorkspace() })
|
||||||
menu.addEntry(new ContextMenuEntry("Open", () => OcelotDesktop.open()))
|
menu.addEntry(ContextMenuEntry("Open") { OcelotDesktop.showOpenDialog() })
|
||||||
menu.addEntry(new ContextMenuEntry("Save", () => OcelotDesktop.save()))
|
menu.addEntry(ContextMenuEntry("Save") { OcelotDesktop.save() })
|
||||||
menu.addEntry(new ContextMenuEntry("Save as…", () => OcelotDesktop.saveAs()))
|
menu.addEntry(ContextMenuEntry("Save as…") { OcelotDesktop.saveAs() })
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
menu.addEntry(new ContextMenuEntry("Exit", () => OcelotDesktop.exit()))
|
menu.addEntry(ContextMenuEntry("Exit") { OcelotDesktop.exit() })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
addEntry(new MenuBarSubmenu("Player", menu => {
|
addEntry(new MenuBarSubmenu("Player", menu => {
|
||||||
menu.addEntry(new ContextMenuEntry("Add...", () => OcelotDesktop.addPlayerDialog()))
|
menu.addEntry(ContextMenuEntry("Add...") { OcelotDesktop.showAddPlayerDialog() })
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
OcelotDesktop.players.foreach(player => {
|
OcelotDesktop.players.foreach(player => {
|
||||||
menu.addEntry(new ContextMenuSubmenu(
|
menu.addEntry(new ContextMenuSubmenu(
|
||||||
(if (player == OcelotDesktop.players.head) "● " else " ") + player.nickname,
|
(if (player == OcelotDesktop.players.head) "● " else " ") + player.nickname,
|
||||||
() => OcelotDesktop.selectPlayer(player.nickname)
|
() => OcelotDesktop.selectPlayer(player.nickname)
|
||||||
) {
|
) {
|
||||||
addEntry(new ContextMenuEntry(
|
addEntry(ContextMenuEntry("Remove", sound = SoundSources.InterfaceClickLow) {
|
||||||
"Remove",
|
OcelotDesktop.removePlayer(player.nickname)
|
||||||
() => OcelotDesktop.removePlayer(player.nickname),
|
})
|
||||||
sound = SoundSources.InterfaceClickLow
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
addEntry(new MenuBarButton("Settings", () => OcelotDesktop.settings()))
|
addEntry(new MenuBarButton("Settings", () => OcelotDesktop.showSettings()))
|
||||||
|
|
||||||
addEntry(new Widget {
|
addEntry(new Widget {
|
||||||
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
|
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 1)
|
||||||
}) // fill remaining space
|
}) // 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 = {
|
override def draw(g: Graphics): Unit = {
|
||||||
g.rect(bounds, ColorScheme("TitleBarBackground"))
|
g.rect(bounds, ColorScheme("TitleBarBackground"))
|
||||||
drawChildren(g)
|
drawChildren(g)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, Cont
|
|||||||
import org.jtransforms.fft.FloatFFT_1D
|
import org.jtransforms.fft.FloatFFT_1D
|
||||||
import totoro.ocelot.brain.Settings
|
import totoro.ocelot.brain.Settings
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
|
|
||||||
class Oscilloscope(isTiny: Boolean = false) extends Widget with ClickHandler {
|
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 = {
|
private def updateMode(f: Int => Int): Unit = {
|
||||||
modeIdx = f(modeIdx)
|
modeIdx = f(modeIdx)
|
||||||
children = Array(modes(modeIdx)())
|
children = ArraySeq(modes(modeIdx)())
|
||||||
}
|
}
|
||||||
|
|
||||||
override def minimumSize: Size2D = if (isTiny) Size2D(100, 68) else Size2D(500, 120)
|
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") {
|
menu.addEntry(new ContextMenuSubmenu("FFT Size") {
|
||||||
for (v <- List(512, 1024, 2048, 4096, 8192, 16384, 32768)) {
|
for (v <- List(512, 1024, 2048, 4096, 8192, 16384, 32768)) {
|
||||||
val dot = if (v == fftSize) '•' else ' '
|
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))) {
|
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 dot = if (v == smoothing) '•' else ' '
|
||||||
val msg = if (v.isEmpty) "None" else s"${v.get}"
|
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 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,10 @@ package ocelot.desktop.ui.widget
|
|||||||
import ocelot.desktop.geometry.Padding2D
|
import ocelot.desktop.geometry.Padding2D
|
||||||
import ocelot.desktop.ui.layout.PaddingLayout
|
import ocelot.desktop.ui.layout.PaddingLayout
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class PaddingBox(inner: Widget, padding: Padding2D) extends Widget {
|
class PaddingBox(inner: Widget, padding: Padding2D) extends Widget {
|
||||||
override protected val layout = new PaddingLayout(this, padding)
|
override protected val layout = new PaddingLayout(this, padding)
|
||||||
|
|
||||||
children = Array(inner)
|
children = ArraySeq(inner)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class RootWidget(setupDefaultWorkspace: Boolean = true) extends Widget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F11, _) =>
|
case KeyEvent(KeyEvent.State.Release, Keyboard.KEY_F11, _) =>
|
||||||
UiHandler.isFullScreen = !UiHandler.isFullScreen
|
UiHandler.fullScreen = !UiHandler.fullScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
override def draw(g: Graphics): Unit = {
|
override def draw(g: Graphics): Unit = {
|
||||||
|
|||||||
@ -10,8 +10,9 @@ import ocelot.desktop.ui.event.{ClickEvent, DragEvent, MouseEvent}
|
|||||||
import ocelot.desktop.util.DrawUtils
|
import ocelot.desktop.util.DrawUtils
|
||||||
import ocelot.desktop.util.MathUtils.ExtendedFloat
|
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 onValueChanged(value: Float): Unit = {}
|
||||||
|
def onValueFinal(value: Float): Unit = {}
|
||||||
|
|
||||||
override def receiveMouseEvents: Boolean = true
|
override def receiveMouseEvents: Boolean = true
|
||||||
|
|
||||||
@ -24,10 +25,26 @@ class Slider(var value: Float, text: String) extends Widget with ClickHandler wi
|
|||||||
onValueChanged(value)
|
onValueChanged(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
eventHandlers += {
|
eventHandlers += {
|
||||||
case ClickEvent(MouseEvent.Button.Left, pos) =>
|
case ClickEvent(MouseEvent.Button.Left, pos) =>
|
||||||
calculateValue(pos.x)
|
calculateValue(pos.x)
|
||||||
clickSoundSource.play()
|
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) =>
|
case DragEvent(_, MouseEvent.Button.Left, pos) =>
|
||||||
calculateValue(pos.x)
|
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 minimumSize: Size2D = Size2D(24 + text.length * 8, 24)
|
||||||
override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)
|
override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)
|
||||||
|
|
||||||
|
def formatText: String = f"$text: ${value * 100}%.0f%%"
|
||||||
|
|
||||||
override def draw(g: Graphics): Unit = {
|
override def draw(g: Graphics): Unit = {
|
||||||
g.rect(bounds, ColorScheme("SliderBackground"))
|
g.rect(bounds, ColorScheme("SliderBackground"))
|
||||||
DrawUtils.ring(g, position.x, position.y, width, height, 2, ColorScheme("SliderBorder"))
|
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"))
|
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"))
|
DrawUtils.ring(g, position.x + value * (bounds.w - handleWidth), position.y, handleWidth, height, 2, ColorScheme("SliderBorder"))
|
||||||
|
|
||||||
g.background = Color.Transparent
|
g.background = Color.Transparent
|
||||||
g.foreground = ColorScheme("SliderForeground")
|
g.foreground = ColorScheme("SliderForeground")
|
||||||
val fullText = f"$text: ${value * 100}%.0f%%"
|
val textWidth = formatText.iterator.map(g.font.charWidth(_)).sum
|
||||||
val textWidth = fullText.iterator.map(g.font.charWidth(_)).sum
|
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, formatText)
|
||||||
g.text(position.x + ((width - textWidth) / 2).round, position.y + 4, fullText)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override protected def clickSoundSource: SoundSource = SoundSources.InterfaceTick
|
override protected def clickSoundSource: SoundSource = SoundSources.InterfaceTick
|
||||||
|
|||||||
@ -6,10 +6,12 @@ import ocelot.desktop.ui.UiHandler
|
|||||||
import ocelot.desktop.ui.event.Event
|
import ocelot.desktop.ui.event.Event
|
||||||
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
|
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class Widget {
|
class Widget {
|
||||||
protected val layout: Layout = new LinearLayout(this)
|
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 _parent: Option[Widget] = None
|
||||||
protected var _root: Option[RootWidget] = None
|
protected var _root: Option[RootWidget] = None
|
||||||
|
|
||||||
@ -33,10 +35,10 @@ class Widget {
|
|||||||
UiHandler.updateHierarchy()
|
UiHandler.updateHierarchy()
|
||||||
}
|
}
|
||||||
|
|
||||||
final def children: Array[Widget] = _children
|
final def children: ArraySeq[Widget] = _children
|
||||||
|
|
||||||
final def children_=(value: Array[Widget]): Unit = {
|
final def children_=(value: ArraySeq[Widget]): Unit = {
|
||||||
if (_children sameElements value) return
|
if (_children == value) return
|
||||||
_children = value
|
_children = value
|
||||||
|
|
||||||
for (child <- _children) {
|
for (child <- _children) {
|
||||||
@ -114,7 +116,7 @@ class Widget {
|
|||||||
Rect2D(position, size).intersect(parentBounds)
|
Rect2D(position, size).intersect(parentBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
def hierarchy: Array[Widget] = children
|
def hierarchy: ArraySeq[Widget] = children
|
||||||
|
|
||||||
def shouldClip: Boolean = false
|
def shouldClip: Boolean = false
|
||||||
|
|
||||||
|
|||||||
@ -15,11 +15,12 @@ import ocelot.desktop.{ColorScheme, OcelotDesktop, Settings}
|
|||||||
import org.lwjgl.input.Keyboard
|
import org.lwjgl.input.Keyboard
|
||||||
import totoro.ocelot.brain.entity.traits.{Entity, Environment, SidedEnvironment}
|
import totoro.ocelot.brain.entity.traits.{Entity, Environment, SidedEnvironment}
|
||||||
import totoro.ocelot.brain.entity.{Case, Screen}
|
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.ExtendedNBT._
|
||||||
import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound}
|
import totoro.ocelot.brain.nbt.{NBT, NBTBase, NBTTagCompound}
|
||||||
import totoro.ocelot.brain.util.{Direction, Tier}
|
import totoro.ocelot.brain.util.{Direction, Tier}
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
import scala.collection.{immutable, mutable}
|
import scala.collection.{immutable, mutable}
|
||||||
import scala.jdk.CollectionConverters._
|
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)
|
override protected val layout: Layout = new CopyLayout(this)
|
||||||
children +:= windowPool
|
children +:= windowPool
|
||||||
children +:= new NoLayoutBox {
|
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 =>
|
private val eventSubscription = EventBus.subscribe {
|
||||||
nodes
|
case event: NodeEvent =>
|
||||||
.filter(_.shouldReceiveEventsFor(event.address))
|
nodes
|
||||||
.foreach(_.handleEvent(BrainEvent(event)))
|
.filter(_.shouldReceiveEventsFor(event.address))
|
||||||
|
.foreach(_.handleEvent(BrainEvent(event)))
|
||||||
|
|
||||||
|
case event: InventoryEvent =>
|
||||||
|
nodes.foreach(_.handleEvent(BrainEvent(event)))
|
||||||
}
|
}
|
||||||
|
|
||||||
def reset(): Unit = {
|
def reset(): Unit = {
|
||||||
@ -73,7 +78,9 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
|
|||||||
|
|
||||||
val address = nbt.getString("address")
|
val address = nbt.getString("address")
|
||||||
var entity = OcelotDesktop.workspace.entityByAddress(address).orNull
|
var entity = OcelotDesktop.workspace.entityByAddress(address).orNull
|
||||||
|
|
||||||
if (entity == null) {
|
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"))
|
val entityClass = Class.forName(nbt.getString("entityClass"))
|
||||||
entity = entityClass.getConstructor().newInstance().asInstanceOf[Entity]
|
entity = entityClass.getConstructor().newInstance().asInstanceOf[Entity]
|
||||||
}
|
}
|
||||||
@ -177,7 +184,7 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
|
|||||||
def createDefaultWorkspace(): Unit = {
|
def createDefaultWorkspace(): Unit = {
|
||||||
addNode(new ComputerNode(new Case(Tier.Three)).setup(), Vector2D(68, 68))
|
addNode(new ComputerNode(new Case(Tier.Three)).setup(), Vector2D(68, 68))
|
||||||
addNode(new ScreenNode(new Screen(Tier.Two)).setup(), Vector2D(204, 136))
|
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
|
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 numRepeatsX = math.ceil((size.width + backgroundOffsetX) / 304f).asInstanceOf[Int]
|
||||||
val numRepeatsY = math.ceil((size.height + backgroundOffsetY) / 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 (x <- 0 to numRepeatsX) {
|
||||||
for (y <- 0 to numRepeatsY) {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,22 +7,22 @@ import ocelot.desktop.ui.widget.window.BasicWindow
|
|||||||
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
|
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
|
||||||
import ocelot.desktop.util.{DrawUtils, Orientation}
|
import ocelot.desktop.util.{DrawUtils, Orientation}
|
||||||
import totoro.ocelot.brain.entity.Redstone
|
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 {
|
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 = {
|
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 = {
|
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)
|
override protected val layout: Layout = new LinearLayout(this, Orientation.Horizontal, contentAlignment = Alignment.Center)
|
||||||
|
|
||||||
children :+= new PaddingBox(new Label {
|
children :+= new PaddingBox(new Label {
|
||||||
@ -44,18 +44,18 @@ class Redstone1Window(card: Redstone.Tier1) extends BasicWindow {
|
|||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= redstoneBlock(0, "Bottom")
|
children :+= redstoneBlock(Direction.Down, "Bottom")
|
||||||
children :+= redstoneBlock(1, " Top")
|
children :+= redstoneBlock(Direction.Up, " Top")
|
||||||
}
|
}
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= redstoneBlock(2, " Back")
|
children :+= redstoneBlock(Direction.Back, " Back")
|
||||||
children :+= redstoneBlock(3, "Front")
|
children :+= redstoneBlock(Direction.Front, "Front")
|
||||||
}
|
}
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= redstoneBlock(4, "Right")
|
children :+= redstoneBlock(Direction.Right, "Right")
|
||||||
children :+= redstoneBlock(5, " Left")
|
children :+= redstoneBlock(Direction.Left, " Left")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, Padding2D.equal(8))
|
}, Padding2D.equal(8))
|
||||||
|
|||||||
@ -7,22 +7,22 @@ import ocelot.desktop.ui.widget.window.BasicWindow
|
|||||||
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
|
import ocelot.desktop.ui.widget.{Knob, Label, PaddingBox, Widget}
|
||||||
import ocelot.desktop.util.{DrawUtils, Orientation}
|
import ocelot.desktop.util.{DrawUtils, Orientation}
|
||||||
import totoro.ocelot.brain.entity.Redstone
|
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 {
|
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 = {
|
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 = {
|
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) {
|
override protected val layout: Layout = new LinearLayout(this) {
|
||||||
orientation = Orientation.Vertical
|
orientation = Orientation.Vertical
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@ -69,18 +69,18 @@ class Redstone2Window(card: Redstone.Tier2) extends BasicWindow {
|
|||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= bundledBlock(0, "Bottom")
|
children :+= bundledBlock(Direction.Down, "Bottom")
|
||||||
children :+= bundledBlock(1, "Top")
|
children :+= bundledBlock(Direction.Up, "Top")
|
||||||
}
|
}
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= bundledBlock(2, "Back")
|
children :+= bundledBlock(Direction.Back, "Back")
|
||||||
children :+= bundledBlock(3, "Front")
|
children :+= bundledBlock(Direction.Front, "Front")
|
||||||
}
|
}
|
||||||
children :+= new Widget {
|
children :+= new Widget {
|
||||||
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
children :+= bundledBlock(4, "Right")
|
children :+= bundledBlock(Direction.Right, "Right")
|
||||||
children :+= bundledBlock(5, "Left")
|
children :+= bundledBlock(Direction.Left, "Left")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, Padding2D.equal(8))
|
}, Padding2D.equal(8))
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import ocelot.desktop.ui.layout.Layout
|
|||||||
import ocelot.desktop.ui.widget.Widget
|
import ocelot.desktop.ui.widget.Widget
|
||||||
import org.lwjgl.input.Keyboard
|
import org.lwjgl.input.Keyboard
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class ComponentSelectors extends Widget {
|
class ComponentSelectors extends Widget {
|
||||||
override protected val layout: Layout = new Layout(this)
|
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
|
override def receiveAllMouseEvents: Boolean = true
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import ocelot.desktop.util.animation.ValueAnimation
|
|||||||
import ocelot.desktop.util.animation.easing.EaseInOutQuad
|
import ocelot.desktop.util.animation.easing.EaseInOutQuad
|
||||||
import ocelot.desktop.util.{DrawUtils, Orientation}
|
import ocelot.desktop.util.{DrawUtils, Orientation}
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class ContextMenu extends Widget {
|
class ContextMenu extends Widget {
|
||||||
private[contextmenu] var isClosing = false
|
private[contextmenu] var isClosing = false
|
||||||
private[contextmenu] var isOpening = false
|
private[contextmenu] var isOpening = false
|
||||||
@ -38,7 +40,7 @@ class ContextMenu extends Widget {
|
|||||||
inner.children :+= new Separator
|
inner.children :+= new Separator
|
||||||
}
|
}
|
||||||
|
|
||||||
private[contextmenu] def entries: Array[ContextMenuEntry] = inner.children
|
private[contextmenu] def entries: ArraySeq[ContextMenuEntry] = inner.children
|
||||||
.filter(_.isInstanceOf[ContextMenuEntry])
|
.filter(_.isInstanceOf[ContextMenuEntry])
|
||||||
.map(_.asInstanceOf[ContextMenuEntry])
|
.map(_.asInstanceOf[ContextMenuEntry])
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,17 @@ import ocelot.desktop.ui.widget._
|
|||||||
import ocelot.desktop.util.animation.ValueAnimation
|
import ocelot.desktop.util.animation.ValueAnimation
|
||||||
import ocelot.desktop.util.animation.easing.{EaseInQuad, EaseOutQuad}
|
import ocelot.desktop.util.animation.easing.{EaseInQuad, EaseOutQuad}
|
||||||
|
|
||||||
class ContextMenuEntry(label: String,
|
class ContextMenuEntry(
|
||||||
onClick: () => Unit = () => {},
|
label: String,
|
||||||
icon: Option[IconDef] = None,
|
onClick: () => Unit = () => {},
|
||||||
sound: SoundSource = SoundSources.InterfaceClick,
|
icon: Option[IconDef] = None,
|
||||||
soundDisabled: SoundSource = SoundSources.InterfaceClickLow)
|
sound: SoundSource = SoundSources.InterfaceClick,
|
||||||
extends Widget with ClickHandler with HoverHandler with ClickSoundSource
|
soundDisabled: SoundSource = SoundSources.InterfaceClickLow
|
||||||
{
|
) extends Widget
|
||||||
|
with ClickHandler
|
||||||
|
with HoverHandler
|
||||||
|
with ClickSoundSource {
|
||||||
|
|
||||||
private[contextmenu] val alpha = new ValueAnimation(0f, 10f)
|
private[contextmenu] val alpha = new ValueAnimation(0f, 10f)
|
||||||
private[contextmenu] val textAlpha = new ValueAnimation(0f, 5f)
|
private[contextmenu] val textAlpha = new ValueAnimation(0f, 5f)
|
||||||
private[contextmenu] val trans = new ValueAnimation(0f, 20f)
|
private[contextmenu] val trans = new ValueAnimation(0f, 20f)
|
||||||
@ -32,22 +36,27 @@ class ContextMenuEntry(label: String,
|
|||||||
case _ => 12f
|
case _ => 12f
|
||||||
}
|
}
|
||||||
|
|
||||||
children :+= new PaddingBox(new Widget {
|
children :+= new PaddingBox(
|
||||||
override val layout: Layout = new LinearLayout(this) {
|
new Widget {
|
||||||
contentAlignment = Alignment.Center
|
override val layout: Layout = new LinearLayout(this) {
|
||||||
}
|
contentAlignment = Alignment.Center
|
||||||
|
}
|
||||||
|
|
||||||
icon match {
|
icon match {
|
||||||
case Some(icon) =>
|
case Some(icon) =>
|
||||||
children :+= new PaddingBox(new Icon(icon), Padding2D(left = 8f, right = 6f))
|
children :+= new PaddingBox(new Icon(icon), Padding2D(left = 8f, right = 6f))
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
|
|
||||||
children :+= new PaddingBox(new Label {
|
children :+= new PaddingBox(
|
||||||
override def text: String = label
|
new Label {
|
||||||
override def color: Color = ColorScheme("ContextMenuText")
|
override def text: String = label
|
||||||
}, Padding2D(top = 3f, bottom = 3f))
|
|
||||||
}, Padding2D(left = padLeft, right = 16f, top = 2f, bottom = 2f))
|
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
|
override def receiveMouseEvents: Boolean = !isGhost
|
||||||
|
|
||||||
@ -113,3 +122,19 @@ class ContextMenuEntry(label: String,
|
|||||||
override protected def clickSoundSource: SoundSource =
|
override protected def clickSoundSource: SoundSource =
|
||||||
if (isEnabled) sound else soundDisabled
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -8,12 +8,14 @@ import ocelot.desktop.ui.layout.Layout
|
|||||||
import ocelot.desktop.ui.widget.Widget
|
import ocelot.desktop.ui.widget.Widget
|
||||||
import org.lwjgl.input.Keyboard
|
import org.lwjgl.input.Keyboard
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class ContextMenus extends Widget {
|
class ContextMenus extends Widget {
|
||||||
override protected val layout: Layout = new Layout(this)
|
override protected val layout: Layout = new Layout(this)
|
||||||
|
|
||||||
private var ghost: Option[ContextMenuEntry] = None
|
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 = {
|
private[contextmenu] def setGhost(g: ContextMenuEntry): Unit = {
|
||||||
ghost = Some(g)
|
ghost = Some(g)
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import ocelot.desktop.ui.layout.Layout
|
|||||||
import ocelot.desktop.ui.widget.Widget
|
import ocelot.desktop.ui.widget.Widget
|
||||||
import ocelot.desktop.util.animation.UnitAnimation
|
import ocelot.desktop.util.animation.UnitAnimation
|
||||||
|
|
||||||
|
import scala.collection.immutable.ArraySeq
|
||||||
|
|
||||||
class ModalDialogPool extends Widget with ClickHandler {
|
class ModalDialogPool extends Widget with ClickHandler {
|
||||||
override protected val layout: Layout = new Layout(this) {
|
override protected val layout: Layout = new Layout(this) {
|
||||||
override def relayout(): Unit = {
|
override def relayout(): Unit = {
|
||||||
@ -22,7 +24,7 @@ class ModalDialogPool extends Widget with ClickHandler {
|
|||||||
|
|
||||||
private val curtainsAlphaAnimation = UnitAnimation.easeInOutQuad(0.13f)
|
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
|
def isVisible: Boolean = dialogs.nonEmpty
|
||||||
|
|
||||||
@ -60,7 +62,7 @@ class ModalDialogPool extends Widget with ClickHandler {
|
|||||||
else
|
else
|
||||||
curtainsAlphaAnimation.goUp()
|
curtainsAlphaAnimation.goUp()
|
||||||
if (dialogs.head.isClosed)
|
if (dialogs.head.isClosed)
|
||||||
children = Array()
|
children = ArraySeq.empty
|
||||||
} else if (dialogs.head.isClosed) {
|
} else if (dialogs.head.isClosed) {
|
||||||
children = children.take(children.length - 1)
|
children = children.take(children.length - 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package ocelot.desktop.ui.widget.modal.notification
|
|||||||
import ocelot.desktop.ColorScheme
|
import ocelot.desktop.ColorScheme
|
||||||
import ocelot.desktop.color.Color
|
import ocelot.desktop.color.Color
|
||||||
import ocelot.desktop.geometry.Padding2D
|
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.layout.LinearLayout
|
||||||
import ocelot.desktop.ui.widget._
|
import ocelot.desktop.ui.widget._
|
||||||
import ocelot.desktop.ui.widget.modal.ModalDialog
|
import ocelot.desktop.ui.widget.modal.ModalDialog
|
||||||
@ -32,7 +32,7 @@ class NotificationDialog(message: String, notificationType: NotificationType = N
|
|||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
children :+= new PaddingBox(
|
children :+= new PaddingBox(
|
||||||
new Icon(new IconDef(s"icons/Notification${notificationType.toString}", 4)),
|
new Icon(Icons.NotificationIcon(notificationType)),
|
||||||
Padding2D.equal(10)
|
Padding2D.equal(10)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package ocelot.desktop.ui.widget.settings
|
package ocelot.desktop.ui.widget.settings
|
||||||
|
|
||||||
|
import ocelot.desktop.graphics.IconDef
|
||||||
import ocelot.desktop.ui.layout.LinearLayout
|
import ocelot.desktop.ui.layout.LinearLayout
|
||||||
import ocelot.desktop.ui.widget.Widget
|
import ocelot.desktop.ui.widget.Widget
|
||||||
import ocelot.desktop.util.Orientation
|
import ocelot.desktop.util.Orientation
|
||||||
@ -7,7 +8,7 @@ import ocelot.desktop.util.Orientation
|
|||||||
trait SettingsTab extends Widget {
|
trait SettingsTab extends Widget {
|
||||||
override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
override protected val layout = new LinearLayout(this, orientation = Orientation.Vertical)
|
||||||
|
|
||||||
val icon: String
|
val icon: IconDef
|
||||||
val label: String
|
val label: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||