Merge branch 'develop'

This commit is contained in:
Fingercomp 2025-08-18 19:01:55 +03:00
commit b53311aa27
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
189 changed files with 3312 additions and 1809 deletions

View File

@ -16,8 +16,8 @@ variables:
PACKAGE_NAME: "ocelot-desktop-${CI_COMMIT_TAG}.jar" PACKAGE_NAME: "ocelot-desktop-${CI_COMMIT_TAG}.jar"
stages: stages:
- test
- build - build
- test
- upload - upload
- deploy - deploy
- release - release
@ -29,12 +29,14 @@ test:
script: script:
- sbt test - sbt test
scalafmt: # Rest in piece.
stage: test #scalafmt:
before_script: # stage: test
- sbt -v sbtVersion # allow_failure: true
script: # before_script:
- sbt scalafmtCheckAll # - sbt -v sbtVersion
# script:
# - sbt scalafmtCheckAll
build: build:
stage: build stage: build

View File

@ -1,24 +1,15 @@
version = 3.8.6 version = 3.9.9
runner.dialect = scala213 runner.dialect = scala213
preset = default preset = default
maxColumn = 120 maxColumn = 120
indent.defnSite = 2
align = { align = {
preset = none preset = none
openParenDefnSite = true openParenDefnSite = false
} }
newlines = { newlines = {
source = keep source = keep
topLevelStatementBlankLines = [
{ blanks { before = 1, after = 1, beforeAll = -1, afterAll = -1 } }
]
}
binPack = {
preset = Oneline
literalsExclude = []
} }
rewrite = { rewrite = {
@ -30,7 +21,15 @@ rewrite = {
} }
docstrings = { docstrings = {
oneline = fold style = SpaceAsterisk
blankFirstLine = unfold
oneline = unfold
wrap = keep wrap = keep
forceBlankLineBefore = false forceBlankLineBefore = false
} }
indent {
defnSite = 2
extendSite = 2
withSiteRelativeToExtends = 2
}

View File

@ -1,5 +1,5 @@
name := "ocelot-desktop" name := "ocelot-desktop"
version := "1.13.1" version := "1.14.0"
scalaVersion := "2.13.10" scalaVersion := "2.13.10"
lazy val root = project.in(file(".")) lazy val root = project.in(file("."))
@ -21,9 +21,9 @@ libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test" libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test"
libraryDependencies += "org.scalatest" %% "scalatest-funsuite" % "3.2.19" % "test" libraryDependencies += "org.scalatest" %% "scalatest-funsuite" % "3.2.19" % "test"
libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.20.0" libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.25.1"
libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0" libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.25.1"
libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.20.0" libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.25.1"
val lwjglVersion = "2.9.3" val lwjglVersion = "2.9.3"
@ -32,7 +32,9 @@ libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion class
libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-windows" libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-windows"
libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-osx" libraryDependencies += "org.lwjgl.lwjgl" % "lwjgl-platform" % lwjglVersion classifier "natives-osx"
libraryDependencies += "com.github.stephengold" % "j-ogg-all" % "1.0.3" Compile / unmanagedResourceDirectories += baseDirectory.value / "lib" / "native"
libraryDependencies += "com.github.stephengold" % "j-ogg-all" % "1.0.6"
libraryDependencies += "com.github.wendykierp" % "JTransforms" % "3.1" 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"

View File

@ -0,0 +1,25 @@
# How to compile LWJGL2 for Apple Silicon
LWJGL2 does not provide official native ARM support, therefore Ocelot uses a specially modified version by **shadowfacts**
Article: https://shadowfacts.net/2022/lwjgl-arm64/
Repository: https://github.com/shadowfacts/lwjgl2-arm64
This procedure is completely optional, as the precompiled library is already checked into the repository at `lib/native/liblwjgl-arm64.dylib`
## Compilation
This assumes that you are running macOS on **Apple Silicon** *(cross-compiling LWJGL seems to be impossible)*
1. Acquire a JDK8 built for ARM - for example, [Zulu 8 JDK](https://www.azul.com/downloads/?version=java-8-lts&os=macos&architecture=arm-64-bit&package=jdk#zulu), and add it to your `JAVA_HOME`
2. Get [`maven`](https://maven.apache.org/) and [`ant`](https://ant.apache.org/) - you may install them via `brew`, but be careful, as they will try to install a JDK as a dependency
3. Clone the LWJGL repository:
```bash
% git clone https://github.com/shadowfacts/lwjgl-arm64.git
```
4. Run the following commands in the repo:
```bash
% ant generate-all
% ant jars
% ant compile-native
```
5. Copy the compiled library from `libs/macosx/liblwjgl.dylib` into the `ocelot-desktop` project as `lib/native/liblwjgl-arm64.dylib`

Binary file not shown.

@ -1 +1 @@
Subproject commit 4b2d2fcec19f8e238dd1c4bdb09d42b11e9bb6e6 Subproject commit da5dd8877035f4994b77047658561d316319b54b

0
spritepack/spritepack.sh Normal file → Executable file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

BIN
sprites/icons/SideAny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

BIN
sprites/icons/SideDown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

BIN
sprites/icons/SideEast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

BIN
sprites/icons/SideNone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

BIN
sprites/icons/SideNorth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

BIN
sprites/icons/SideSouth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

BIN
sprites/icons/SideUp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

BIN
sprites/icons/SideWest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

BIN
sprites/particles/Smoke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

View File

@ -11,7 +11,7 @@ AboutButtonBackgroundActive = #888888
PortDown = #8382d8 PortDown = #8382d8
PortUp = #75bdc1 PortUp = #75bdc1
PortNorth = #c8ca5f PortNorth = #c8ca5f
PortSouth = #990da3 PortSouth = #ed8ef4
PortEast = #7ec95f PortEast = #7ec95f
PortWest = #db7d75 PortWest = #db7d75
PortAny = #9b9b9b PortAny = #9b9b9b
@ -23,6 +23,7 @@ Tier3 = #c354cd
Label = #333333 Label = #333333
LabelError = #aa0000 LabelError = #aa0000
LabelDisabled = #888888
Scrollbar = #e5e5e526 Scrollbar = #e5e5e526
ScrollbarThumb = #cc3f72 ScrollbarThumb = #cc3f72
@ -46,6 +47,8 @@ ComputerAddress = #333333
ScreenOff = #000000 ScreenOff = #000000
WindowBackground = #c6c6c6
StatusBar = #181818 StatusBar = #181818
StatusBarActive = #ffffff04 StatusBarActive = #ffffff04
StatusBarBorder = #222222 StatusBarBorder = #222222
@ -190,3 +193,6 @@ Flash = #ffffff
RelayTextLow = #009900 RelayTextLow = #009900
RelayTextMid = #999900 RelayTextMid = #999900
RelayTextHigh = #990000 RelayTextHigh = #990000
BoomCardGlowStart = #ff4010
BoomCardGlowEnd = #ff1010

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 149 KiB

View File

@ -1,22 +1,22 @@
BackgroundPattern 0 0 304 304 BackgroundPattern 0 0 304 304
BarSegment 385 434 16 4 BarSegment 385 434 16 4
Empty 134 618 16 16 Empty 134 632 16 16
EmptySlot 237 567 18 18 EmptySlot 246 567 18 18
Knob 203 434 50 50 Knob 203 434 50 50
KnobCenter 254 434 50 50 KnobCenter 254 434 50 50
KnobLimits 305 434 50 50 KnobLimits 305 434 50 50
Loading 0 305 48 448 Loading 0 305 48 448
Logo 305 0 168 200 Logo 305 0 168 200
ShadowBorder 201 540 1 24 ShadowBorder 279 305 1 24
ShadowCorner 233 674 24 24 ShadowCorner 233 674 24 24
TabArrow 225 600 8 14 TabArrow 569 567 8 14
blocks/Generic 151 618 16 16 blocks/Generic 151 632 16 16
blocks/HologramEffect 209 549 4 4 blocks/HologramEffect 291 305 4 4
blocks/HologramProjector1Top 168 618 16 16 blocks/HologramProjector1Top 168 632 16 16
blocks/HologramProjector2Top 185 618 16 16 blocks/HologramProjector2Top 185 632 16 16
blocks/HologramProjectorSide 202 618 16 16 blocks/HologramProjectorSide 202 632 16 16
buttons/BottomDrawerClose 256 567 18 18 buttons/BottomDrawerClose 265 567 18 18
buttons/BottomDrawerOpen 275 567 18 18 buttons/BottomDrawerOpen 284 567 18 18
buttons/OpenFMRadioCloseOff 404 445 7 8 buttons/OpenFMRadioCloseOff 404 445 7 8
buttons/OpenFMRadioCloseOn 412 445 7 8 buttons/OpenFMRadioCloseOn 412 445 7 8
buttons/OpenFMRadioRedstoneOff 359 445 8 8 buttons/OpenFMRadioRedstoneOff 359 445 8 8
@ -25,303 +25,319 @@ buttons/OpenFMRadioStartOff 258 674 24 24
buttons/OpenFMRadioStartOn 283 674 24 24 buttons/OpenFMRadioStartOn 283 674 24 24
buttons/OpenFMRadioStopOff 308 674 24 24 buttons/OpenFMRadioStopOff 308 674 24 24
buttons/OpenFMRadioStopOn 333 674 24 24 buttons/OpenFMRadioStopOn 333 674 24 24
buttons/OpenFMRadioVolumeDownOff 234 600 10 10 buttons/OpenFMRadioVolumeDownOff 578 567 10 10
buttons/OpenFMRadioVolumeDownOn 245 600 10 10 buttons/OpenFMRadioVolumeDownOn 589 567 10 10
buttons/OpenFMRadioVolumeUpOff 256 600 10 10 buttons/OpenFMRadioVolumeUpOff 600 567 10 10
buttons/OpenFMRadioVolumeUpOn 267 600 10 10 buttons/OpenFMRadioVolumeUpOn 611 567 10 10
buttons/PowerOff 294 567 18 18 buttons/PowerOff 303 567 18 18
buttons/PowerOn 313 567 18 18 buttons/PowerOn 322 567 18 18
buttons/RackRelayOff 134 655 65 18 buttons/RackRelayOff 134 655 65 18
buttons/RackRelayOn 200 655 65 18 buttons/RackRelayOn 200 655 65 18
icons/Antenna 219 618 16 16 icons/Antenna 219 632 16 16
icons/ArrowRight 236 618 16 16 icons/ArrowRight 236 632 16 16
icons/AspectRatio 253 618 16 16 icons/AspectRatio 253 632 16 16
icons/Book 270 618 16 16 icons/Book 270 632 16 16
icons/ButtonCheck 134 600 17 17 icons/ButtonCheck 143 600 17 17
icons/ButtonClipboard 152 600 17 17 icons/ButtonClipboard 161 600 17 17
icons/ButtonRandomize 170 600 17 17 icons/ButtonRandomize 179 600 17 17
icons/CPU 287 618 16 16 icons/CPU 287 632 16 16
icons/Card 304 618 16 16 icons/Card 304 632 16 16
icons/Close 209 600 15 14 icons/Close 553 567 15 14
icons/Code 321 618 16 16 icons/Code 321 632 16 16
icons/ComponentBus 338 618 16 16 icons/ComponentBus 338 632 16 16
icons/Copy 355 618 16 16 icons/Copy 355 632 16 16
icons/Cross 372 618 16 16 icons/Cross 372 632 16 16
icons/Delete 389 618 16 16 icons/Delete 389 632 16 16
icons/DragLMB 500 567 21 14 icons/DragLMB 509 567 21 14
icons/DragRMB 522 567 21 14 icons/DragRMB 531 567 21 14
icons/EEPROM 406 618 16 16 icons/EEPROM 406 632 16 16
icons/Edit 423 618 16 16 icons/Edit 423 632 16 16
icons/Eject 440 618 16 16 icons/Eject 440 632 16 16
icons/File 457 618 16 16 icons/File 457 632 16 16
icons/Floppy 474 618 16 16 icons/Floppy 474 632 16 16
icons/Folder 491 618 16 16 icons/Folder 491 632 16 16
icons/FolderSlash 508 618 16 16 icons/FolderSlash 508 632 16 16
icons/Grid 168 567 22 22 icons/Grid 177 567 22 22
icons/GridOff 191 567 22 22 icons/GridOff 200 567 22 22
icons/Guitar 525 618 16 16 icons/Guitar 525 632 16 16
icons/HDD 542 618 16 16 icons/HDD 542 632 16 16
icons/Help 559 618 16 16 icons/Help 559 632 16 16
icons/Home 214 567 22 22 icons/Home 223 567 22 22
icons/Keyboard 576 618 16 16 icons/Keyboard 576 632 16 16
icons/KeyboardOff 593 618 16 16 icons/KeyboardOff 593 632 16 16
icons/LMB 350 655 11 14 icons/LMB 298 540 11 14
icons/Label 610 618 16 16 icons/Label 610 632 16 16
icons/LinesHorizontal 627 618 16 16 icons/LinesHorizontal 627 632 16 16
icons/Link 644 618 16 16 icons/Link 644 632 16 16
icons/LinkSlash 661 618 16 16 icons/LinkSlash 661 632 16 16
icons/Memory 678 618 16 16 icons/Memory 678 632 16 16
icons/Microchip 695 618 16 16 icons/Microchip 695 632 16 16
icons/NA 712 618 16 16 icons/NA 712 632 16 16
icons/NotificationError 374 655 11 11 icons/NotificationError 322 540 11 11
icons/NotificationInfo 386 655 11 11 icons/NotificationInfo 334 540 11 11
icons/NotificationWarning 398 655 11 11 icons/NotificationWarning 346 540 11 11
icons/Ocelot 729 618 16 16 icons/Ocelot 729 632 16 16
icons/Pin 305 655 14 14 icons/Pin 253 540 14 14
icons/Plus 746 618 16 16 icons/Plus 746 632 16 16
icons/Power 763 618 16 16 icons/Power 763 632 16 16
icons/RMB 362 655 11 14 icons/RMB 310 540 11 14
icons/Restart 780 618 16 16 icons/Restart 780 632 16 16
icons/Save 797 618 16 16 icons/Save 797 632 16 16
icons/SaveAs 814 618 16 16 icons/SaveAs 814 632 16 16
icons/Server 831 618 16 16 icons/Server 831 632 16 16
icons/SettingsSound 266 655 12 17 icons/SettingsKeymap 201 540 12 17
icons/SettingsSystem 279 655 12 17 icons/SettingsSound 214 540 12 17
icons/SettingsUI 292 655 12 17 icons/SettingsSystem 227 540 12 17
icons/Tier0 848 618 16 16 icons/SettingsUI 240 540 12 17
icons/Tier1 865 618 16 16 icons/SideAny 358 540 11 11
icons/Tier2 882 618 16 16 icons/SideDown 370 540 11 11
icons/Tiers 899 618 16 16 icons/SideEast 382 540 11 11
icons/Unpin 320 655 14 14 icons/SideNone 394 540 11 11
icons/WaveLFSR 901 707 24 10 icons/SideNorth 406 540 11 11
icons/WaveNoise 926 707 24 10 icons/SideSouth 418 540 11 11
icons/WaveSawtooth 951 707 24 10 icons/SideUndefined 430 540 11 11
icons/WaveSine 976 707 24 10 icons/SideUp 442 540 11 11
icons/WaveSquare 134 724 24 10 icons/SideWest 454 540 11 11
icons/WaveTriangle 159 724 24 10 icons/Tier0 848 632 16 16
icons/Window 916 618 16 16 icons/Tier1 865 632 16 16
icons/WireArrowLeft 203 540 4 8 icons/Tier2 882 632 16 16
icons/WireArrowRight 208 540 4 8 icons/Tiers 899 632 16 16
icons/Unpin 268 540 14 14
icons/WaveLFSR 237 600 24 10
icons/WaveNoise 262 600 24 10
icons/WaveSawtooth 287 600 24 10
icons/WaveSine 312 600 24 10
icons/WaveSquare 337 600 24 10
icons/WaveTriangle 362 600 24 10
icons/Window 916 632 16 16
icons/WireArrowLeft 281 305 4 8
icons/WireArrowRight 286 305 4 8
items/APU0 49 655 16 96 items/APU0 49 655 16 96
items/APU1 66 655 16 96 items/APU1 66 655 16 96
items/APU2 83 655 16 96 items/APU2 83 655 16 96
items/CPU0 933 618 16 16 items/CPU0 933 632 16 16
items/CPU1 950 618 16 16 items/CPU1 950 632 16 16
items/CPU2 967 618 16 16 items/CPU2 967 632 16 16
items/CardBase 984 618 16 16 items/CardBase 984 632 16 16
items/CircuitBoard 1001 618 16 16 items/CircuitBoard 1001 632 16 16
items/ComponentBus0 134 635 16 16 items/ComponentBus0 358 674 16 16
items/ComponentBus1 151 635 16 16 items/ComponentBus1 375 674 16 16
items/ComponentBus2 168 635 16 16 items/ComponentBus2 392 674 16 16
items/ComponentBus3 185 635 16 16 items/ComponentBus3 409 674 16 16
items/DataCard0 49 526 16 128 items/DataCard0 49 526 16 128
items/DataCard1 66 526 16 128 items/DataCard1 66 526 16 128
items/DataCard2 83 526 16 128 items/DataCard2 83 526 16 128
items/DebugCard 202 635 16 16 items/DebugCard 426 674 16 16
items/DiskDriveMountable 219 635 16 16 items/DiskDriveMountable 443 674 16 16
items/EEPROM 236 635 16 16 items/EEPROM 460 674 16 16
items/FloppyDisk_dyeBlack 253 635 16 16 items/FloppyDisk_dyeBlack 477 674 16 16
items/FloppyDisk_dyeBlue 270 635 16 16 items/FloppyDisk_dyeBlue 494 674 16 16
items/FloppyDisk_dyeBrown 287 635 16 16 items/FloppyDisk_dyeBrown 511 674 16 16
items/FloppyDisk_dyeCyan 304 635 16 16 items/FloppyDisk_dyeCyan 528 674 16 16
items/FloppyDisk_dyeGray 321 635 16 16 items/FloppyDisk_dyeGray 545 674 16 16
items/FloppyDisk_dyeGreen 338 635 16 16 items/FloppyDisk_dyeGreen 562 674 16 16
items/FloppyDisk_dyeLightBlue 355 635 16 16 items/FloppyDisk_dyeLightBlue 579 674 16 16
items/FloppyDisk_dyeLightGray 372 635 16 16 items/FloppyDisk_dyeLightGray 596 674 16 16
items/FloppyDisk_dyeLime 389 635 16 16 items/FloppyDisk_dyeLime 613 674 16 16
items/FloppyDisk_dyeMagenta 406 635 16 16 items/FloppyDisk_dyeMagenta 630 674 16 16
items/FloppyDisk_dyeOrange 423 635 16 16 items/FloppyDisk_dyeOrange 647 674 16 16
items/FloppyDisk_dyePink 440 635 16 16 items/FloppyDisk_dyePink 664 674 16 16
items/FloppyDisk_dyePurple 457 635 16 16 items/FloppyDisk_dyePurple 681 674 16 16
items/FloppyDisk_dyeRed 474 635 16 16 items/FloppyDisk_dyeRed 698 674 16 16
items/FloppyDisk_dyeWhite 491 635 16 16 items/FloppyDisk_dyeWhite 715 674 16 16
items/FloppyDisk_dyeYellow 508 635 16 16 items/FloppyDisk_dyeYellow 732 674 16 16
items/GraphicsCard0 525 635 16 16 items/GraphicsCard0 749 674 16 16
items/GraphicsCard1 542 635 16 16 items/GraphicsCard1 766 674 16 16
items/GraphicsCard2 559 635 16 16 items/GraphicsCard2 783 674 16 16
items/HardDiskDrive0 576 635 16 16 items/HardDiskDrive0 800 674 16 16
items/HardDiskDrive1 593 635 16 16 items/HardDiskDrive1 817 674 16 16
items/HardDiskDrive2 610 635 16 16 items/HardDiskDrive2 834 674 16 16
items/InternetCard 134 567 16 32 items/InternetCard 143 567 16 32
items/LinkedCard 100 655 16 96 items/LinkedCard 100 655 16 96
items/Memory0 627 635 16 16 items/Memory0 851 674 16 16
items/Memory1 644 635 16 16 items/Memory1 868 674 16 16
items/Memory2 661 635 16 16 items/Memory2 885 674 16 16
items/Memory3 678 635 16 16 items/Memory3 902 674 16 16
items/Memory4 695 635 16 16 items/Memory4 919 674 16 16
items/Memory5 712 635 16 16 items/Memory5 936 674 16 16
items/Memory6 729 635 16 16 items/Memory6 953 674 16 16
items/NetworkCard 746 635 16 16 items/NetworkCard 970 674 16 16
items/OcelotCard 100 526 16 128 items/OcelotCard 100 526 16 128
items/RedstoneCard0 763 635 16 16 items/RedstoneCard0 987 674 16 16
items/RedstoneCard1 780 635 16 16 items/RedstoneCard1 1004 674 16 16
items/SelfDestructingCard 151 567 16 32 items/SelfDestructingCard 160 567 16 32
items/Server0 797 635 16 16 items/Server0 134 707 16 16
items/Server1 814 635 16 16 items/Server1 151 707 16 16
items/Server2 831 635 16 16 items/Server2 168 707 16 16
items/Server3 848 635 16 16 items/Server3 185 707 16 16
items/SoundCard 117 526 16 128 items/SoundCard 117 526 16 128
items/TapeCopper 865 635 16 16 items/TapeCopper 202 707 16 16
items/TapeDiamond 882 635 16 16 items/TapeDiamond 219 707 16 16
items/TapeGold 899 635 16 16 items/TapeGold 236 707 16 16
items/TapeGreg 916 635 16 16 items/TapeGreg 253 707 16 16
items/TapeIg 933 635 16 16 items/TapeIg 270 707 16 16
items/TapeIron 950 635 16 16 items/TapeIron 287 707 16 16
items/TapeNetherStar 967 635 16 16 items/TapeNetherStar 304 707 16 16
items/TapeSteel 984 635 16 16 items/TapeSteel 321 707 16 16
items/WirelessNetworkCard0 1001 635 16 16 items/WirelessNetworkCard0 338 707 16 16
items/WirelessNetworkCard1 358 674 16 16 items/WirelessNetworkCard1 355 707 16 16
light-panel/BookmarkLeft 882 707 18 14 light-panel/BookmarkLeft 218 600 18 14
light-panel/BookmarkRight 188 600 20 14 light-panel/BookmarkRight 197 600 20 14
light-panel/BorderB 214 549 4 4 light-panel/BorderB 296 305 4 4
light-panel/BorderL 304 549 4 2 light-panel/BorderL 284 314 4 2
light-panel/BorderR 219 549 4 4 light-panel/BorderR 301 305 4 4
light-panel/BorderT 224 549 4 4 light-panel/BorderT 306 305 4 4
light-panel/CornerBL 229 549 4 4 light-panel/CornerBL 311 305 4 4
light-panel/CornerBR 234 549 4 4 light-panel/CornerBR 316 305 4 4
light-panel/CornerTL 239 549 4 4 light-panel/CornerTL 321 305 4 4
light-panel/CornerTR 244 549 4 4 light-panel/CornerTR 326 305 4 4
light-panel/Fill 207 560 2 2 light-panel/Fill 410 305 2 2
light-panel/Vent 356 434 2 38 light-panel/Vent 356 434 2 38
nodes/Cable 377 445 8 8 nodes/Cable 377 445 8 8
nodes/Camera 375 674 16 16 nodes/Camera 372 707 16 16
nodes/Chest 335 655 14 14 nodes/Chest 283 540 14 14
nodes/HologramProjector0 392 674 16 16 nodes/HologramProjector0 389 707 16 16
nodes/HologramProjector1 409 674 16 16 nodes/HologramProjector1 406 707 16 16
nodes/IronNoteBlock 426 674 16 16 nodes/IronNoteBlock 423 707 16 16
nodes/Lamp 443 674 16 16 nodes/Lamp 440 707 16 16
nodes/LampFrame 460 674 16 16 nodes/LampFrame 457 707 16 16
nodes/LampGlow 49 305 128 128 nodes/LampGlow 49 305 128 128
nodes/NewNode 477 674 16 16 nodes/NewNode 474 707 16 16
nodes/NoteBlock 494 674 16 16 nodes/NoteBlock 491 707 16 16
nodes/OpenFMRadio 511 674 16 16 nodes/OpenFMRadio 508 707 16 16
nodes/Relay 528 674 16 16 nodes/Relay 525 707 16 16
nodes/TapeDrive 545 674 16 16 nodes/TapeDrive 542 707 16 16
nodes/computer/Default 562 674 16 16 nodes/computer/Default 559 707 16 16
nodes/computer/DiskActivity 579 674 16 16 nodes/computer/DiskActivity 576 707 16 16
nodes/computer/Error 596 674 16 16 nodes/computer/Error 593 707 16 16
nodes/computer/On 613 674 16 16 nodes/computer/On 610 707 16 16
nodes/disk-drive/Default 630 674 16 16 nodes/disk-drive/Default 627 707 16 16
nodes/disk-drive/DiskActivity 647 674 16 16 nodes/disk-drive/DiskActivity 644 707 16 16
nodes/disk-drive/Floppy 664 674 16 16 nodes/disk-drive/Floppy 661 707 16 16
nodes/holidays/Christmas 134 674 32 32 nodes/holidays/Christmas 134 674 32 32
nodes/holidays/Halloween 167 674 32 32 nodes/holidays/Halloween 167 674 32 32
nodes/holidays/Valentines 200 674 32 32 nodes/holidays/Valentines 200 674 32 32
nodes/microcontroller/Default 681 674 16 16 nodes/microcontroller/Default 678 707 16 16
nodes/microcontroller/Error 698 674 16 16 nodes/microcontroller/Error 695 707 16 16
nodes/microcontroller/On 715 674 16 16 nodes/microcontroller/On 712 707 16 16
nodes/ocelot-block/Default 117 655 16 80 nodes/ocelot-block/Default 117 655 16 80
nodes/ocelot-block/Rx 732 674 16 16 nodes/ocelot-block/Rx 729 707 16 16
nodes/ocelot-block/Tx 749 674 16 16 nodes/ocelot-block/Tx 746 707 16 16
nodes/rack/Default 766 674 16 16 nodes/rack/Default 763 707 16 16
nodes/rack/Empty 783 674 16 16 nodes/rack/Empty 780 707 16 16
nodes/rack/drive/0/Default 800 674 16 16 nodes/rack/drive/0/Default 797 707 16 16
nodes/rack/drive/0/DiskActivity 817 674 16 16 nodes/rack/drive/0/DiskActivity 814 707 16 16
nodes/rack/drive/0/Floppy 834 674 16 16 nodes/rack/drive/0/Floppy 831 707 16 16
nodes/rack/drive/1/Default 851 674 16 16 nodes/rack/drive/1/Default 848 707 16 16
nodes/rack/drive/1/DiskActivity 868 674 16 16 nodes/rack/drive/1/DiskActivity 865 707 16 16
nodes/rack/drive/1/Floppy 885 674 16 16 nodes/rack/drive/1/Floppy 882 707 16 16
nodes/rack/drive/2/Default 902 674 16 16 nodes/rack/drive/2/Default 899 707 16 16
nodes/rack/drive/2/DiskActivity 919 674 16 16 nodes/rack/drive/2/DiskActivity 916 707 16 16
nodes/rack/drive/2/Floppy 936 674 16 16 nodes/rack/drive/2/Floppy 933 707 16 16
nodes/rack/drive/3/Default 953 674 16 16 nodes/rack/drive/3/Default 950 707 16 16
nodes/rack/drive/3/DiskActivity 970 674 16 16 nodes/rack/drive/3/DiskActivity 967 707 16 16
nodes/rack/drive/3/Floppy 987 674 16 16 nodes/rack/drive/3/Floppy 984 707 16 16
nodes/rack/drive/Floppy 1004 674 16 16 nodes/rack/drive/Floppy 1001 707 16 16
nodes/rack/server/0/Default 134 707 16 16 nodes/rack/server/0/Default 266 655 16 16
nodes/rack/server/0/DiskActivity 151 707 16 16 nodes/rack/server/0/DiskActivity 283 655 16 16
nodes/rack/server/0/Error 168 707 16 16 nodes/rack/server/0/Error 300 655 16 16
nodes/rack/server/0/NetworkActivity 185 707 16 16 nodes/rack/server/0/NetworkActivity 317 655 16 16
nodes/rack/server/0/On 202 707 16 16 nodes/rack/server/0/On 334 655 16 16
nodes/rack/server/1/Default 219 707 16 16 nodes/rack/server/1/Default 351 655 16 16
nodes/rack/server/1/DiskActivity 236 707 16 16 nodes/rack/server/1/DiskActivity 368 655 16 16
nodes/rack/server/1/Error 253 707 16 16 nodes/rack/server/1/Error 385 655 16 16
nodes/rack/server/1/NetworkActivity 270 707 16 16 nodes/rack/server/1/NetworkActivity 402 655 16 16
nodes/rack/server/1/On 287 707 16 16 nodes/rack/server/1/On 419 655 16 16
nodes/rack/server/2/Default 304 707 16 16 nodes/rack/server/2/Default 436 655 16 16
nodes/rack/server/2/DiskActivity 321 707 16 16 nodes/rack/server/2/DiskActivity 453 655 16 16
nodes/rack/server/2/Error 338 707 16 16 nodes/rack/server/2/Error 470 655 16 16
nodes/rack/server/2/NetworkActivity 355 707 16 16 nodes/rack/server/2/NetworkActivity 487 655 16 16
nodes/rack/server/2/On 372 707 16 16 nodes/rack/server/2/On 504 655 16 16
nodes/rack/server/3/Default 389 707 16 16 nodes/rack/server/3/Default 521 655 16 16
nodes/rack/server/3/DiskActivity 406 707 16 16 nodes/rack/server/3/DiskActivity 538 655 16 16
nodes/rack/server/3/Error 423 707 16 16 nodes/rack/server/3/Error 555 655 16 16
nodes/rack/server/3/NetworkActivity 440 707 16 16 nodes/rack/server/3/NetworkActivity 572 655 16 16
nodes/rack/server/3/On 457 707 16 16 nodes/rack/server/3/On 589 655 16 16
nodes/raid/0/DiskActivity 474 707 16 16 nodes/raid/0/DiskActivity 606 655 16 16
nodes/raid/0/Error 491 707 16 16 nodes/raid/0/Error 623 655 16 16
nodes/raid/1/DiskActivity 508 707 16 16 nodes/raid/1/DiskActivity 640 655 16 16
nodes/raid/1/Error 525 707 16 16 nodes/raid/1/Error 657 655 16 16
nodes/raid/2/DiskActivity 542 707 16 16 nodes/raid/2/DiskActivity 674 655 16 16
nodes/raid/2/Error 559 707 16 16 nodes/raid/2/Error 691 655 16 16
nodes/raid/Default 576 707 16 16 nodes/raid/Default 708 655 16 16
nodes/screen/BottomLeft 593 707 16 16 nodes/screen/BottomLeft 725 655 16 16
nodes/screen/BottomMiddle 610 707 16 16 nodes/screen/BottomMiddle 742 655 16 16
nodes/screen/BottomRight 627 707 16 16 nodes/screen/BottomRight 759 655 16 16
nodes/screen/ColumnBottom 644 707 16 16 nodes/screen/ColumnBottom 776 655 16 16
nodes/screen/ColumnMiddle 661 707 16 16 nodes/screen/ColumnMiddle 793 655 16 16
nodes/screen/ColumnTop 678 707 16 16 nodes/screen/ColumnTop 810 655 16 16
nodes/screen/Middle 695 707 16 16 nodes/screen/Middle 827 655 16 16
nodes/screen/MiddleLeft 712 707 16 16 nodes/screen/MiddleLeft 844 655 16 16
nodes/screen/MiddleRight 729 707 16 16 nodes/screen/MiddleRight 861 655 16 16
nodes/screen/PowerOnOverlay 746 707 16 16 nodes/screen/PowerOnOverlay 878 655 16 16
nodes/screen/RowLeft 763 707 16 16 nodes/screen/RowLeft 895 655 16 16
nodes/screen/RowMiddle 780 707 16 16 nodes/screen/RowMiddle 912 655 16 16
nodes/screen/RowRight 797 707 16 16 nodes/screen/RowRight 929 655 16 16
nodes/screen/Standalone 814 707 16 16 nodes/screen/Standalone 946 655 16 16
nodes/screen/TopLeft 831 707 16 16 nodes/screen/TopLeft 963 655 16 16
nodes/screen/TopMiddle 848 707 16 16 nodes/screen/TopMiddle 980 655 16 16
nodes/screen/TopRight 865 707 16 16 nodes/screen/TopRight 997 655 16 16
panel/BorderB 249 549 4 4 panel/BorderB 331 305 4 4
panel/BorderL 309 549 4 2 panel/BorderL 289 314 4 2
panel/BorderR 254 549 4 4 panel/BorderR 336 305 4 4
panel/BorderT 259 549 4 4 panel/BorderT 341 305 4 4
panel/CornerBL 264 549 4 4 panel/CornerBL 346 305 4 4
panel/CornerBR 269 549 4 4 panel/CornerBR 351 305 4 4
panel/CornerTL 274 549 4 4 panel/CornerTL 356 305 4 4
panel/CornerTR 279 549 4 4 panel/CornerTR 361 305 4 4
panel/Fill 210 560 2 2 panel/Fill 413 305 2 2
particles/Note 377 434 7 10 particles/Note 377 434 7 10
screen/BorderB 206 549 2 8 particles/Smoke 134 567 8 64
screen/BorderT 203 549 2 10 screen/InnerBorderB 281 321 2 4
screen/CornerBL 386 445 8 8 screen/InnerBorderT 284 321 2 4
screen/CornerBR 395 445 8 8 screen/InnerCornerBL 366 305 4 4
screen/CornerTL 359 434 8 10 screen/InnerCornerBR 371 305 4 4
screen/CornerTR 368 434 8 10 screen/InnerCornerTL 376 305 4 4
window/BorderDark 203 560 1 4 screen/InnerCornerTR 381 305 4 4
window/BorderLight 205 560 1 4 screen/OuterBorderT 281 314 2 6
window/CornerBL 284 549 4 4 screen/OuterCornerBL 386 445 8 8
window/CornerBR 289 549 4 4 screen/OuterCornerBR 395 445 8 8
window/CornerTL 294 549 4 4 screen/OuterCornerTL 359 434 8 10
window/CornerTR 299 549 4 4 screen/OuterCornerTR 368 434 8 10
window/BorderDark 406 305 1 4
window/BorderLight 408 305 1 4
window/CornerBL 386 305 4 4
window/CornerBR 391 305 4 4
window/CornerTL 396 305 4 4
window/CornerTR 401 305 4 4
window/OpenFMRadio 474 0 232 105 window/OpenFMRadio 474 0 232 105
window/case/Motherboard 123 434 79 70 window/case/Motherboard 123 434 79 70
window/rack/Lines 49 434 73 91 window/rack/Lines 49 434 73 91
window/rack/Motherboard 178 305 100 78 window/rack/Motherboard 178 305 100 78
window/rack/NetworkBack 221 554 1 2 window/rack/NetworkBack 293 326 1 2
window/rack/NetworkBottom 223 554 1 2 window/rack/NetworkBottom 295 326 1 2
window/rack/NetworkConnector 225 554 1 2 window/rack/NetworkConnector 297 326 1 2
window/rack/NetworkLeft 227 554 1 2 window/rack/NetworkLeft 299 326 1 2
window/rack/NetworkRight 229 554 1 2 window/rack/NetworkRight 301 326 1 2
window/rack/NetworkTop 231 554 1 2 window/rack/NetworkTop 303 326 1 2
window/rack/NodeBack 314 549 5 1 window/rack/NodeBack 287 321 5 1
window/rack/NodeBottom 320 549 5 1 window/rack/NodeBottom 293 321 5 1
window/rack/NodeLeft 326 549 5 1 window/rack/NodeLeft 299 321 5 1
window/rack/NodeRight 332 549 5 1 window/rack/NodeRight 305 321 5 1
window/rack/NodeTop 338 549 5 1 window/rack/NodeTop 311 321 5 1
window/rack/SideBack 209 554 1 3 window/rack/SideBack 281 326 1 3
window/rack/SideBottom 211 554 1 3 window/rack/SideBottom 283 326 1 3
window/rack/SideConnector 213 554 1 3 window/rack/SideConnector 285 326 1 3
window/rack/SideLeft 215 554 1 3 window/rack/SideLeft 287 326 1 3
window/rack/SideRight 217 554 1 3 window/rack/SideRight 289 326 1 3
window/rack/SideTop 219 554 1 3 window/rack/SideTop 291 326 1 3
window/raid/Slots 134 540 66 26 window/raid/Slots 134 540 66 26
window/tape/Back 332 567 20 15 window/tape/Back 341 567 20 15
window/tape/BackPressed 353 567 20 15 window/tape/BackPressed 362 567 20 15
window/tape/Forward 374 567 20 15 window/tape/Forward 383 567 20 15
window/tape/ForwardPressed 395 567 20 15 window/tape/ForwardPressed 404 567 20 15
window/tape/Play 416 567 20 15 window/tape/Play 425 567 20 15
window/tape/PlayPressed 437 567 20 15 window/tape/PlayPressed 446 567 20 15
window/tape/Screen 134 526 146 13 window/tape/Screen 134 526 146 13
window/tape/Stop 458 567 20 15 window/tape/Stop 467 567 20 15
window/tape/StopPressed 479 567 20 15 window/tape/StopPressed 488 567 20 15

View File

@ -1,4 +1,4 @@
#version 140 #version 150
in vec3 inPos; in vec3 inPos;
in vec3 inNormal; in vec3 inNormal;

View File

@ -383,6 +383,7 @@ object OcelotDesktop extends LoggingConfiguration with Logging {
loadWorld(nbt) loadWorld(nbt)
resetAutosave() resetAutosave()
logger.info(s"Workspace successfully loaded from: $path")
} }
} }
} else Failure(new FileNotFoundException("Specified directory does not contain 'workspace.nbt'")) } else Failure(new FileNotFoundException("Specified directory does not contain 'workspace.nbt'"))

View File

@ -5,11 +5,9 @@ import ocelot.desktop.Settings.ExtendedConfig
import ocelot.desktop.util.{Logging, SettingsData} import ocelot.desktop.util.{Logging, SettingsData}
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import java.io.InputStream
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import java.util import java.util
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) // TODO: refactor this mess (having to declare every field 3 times is extremely error-prone)
@ -44,9 +42,12 @@ class Settings(val config: Config) extends SettingsData {
windowSize.y -= 16 windowSize.y -= 16
} }
keymap.load(config.getConfig("ocelot.keymap"))
recentWorkspace = config.getOptionalString("ocelot.workspace.recent") recentWorkspace = config.getOptionalString("ocelot.workspace.recent")
pinNewWindows = config.getBooleanOrElse("ocelot.workspace.pinNewWindows", default = true) pinNewWindows = config.getBooleanOrElse("ocelot.workspace.pinNewWindows", default = true)
unfocusedWindowTransparency = config.getDoubleOrElse("ocelot.workspace.unfocusedWindowTransparency", 0.5) unfocusedWindowTransparency = config.getDoubleOrElse("ocelot.workspace.unfocusedWindowTransparency", 0.5).toFloat
unfocusedWindowHide = config.getBooleanOrElse("ocelot.workspace.unfocusedWindowHide", default = false)
saveOnExit = config.getBooleanOrElse("ocelot.workspace.saveOnExit", default = true) saveOnExit = config.getBooleanOrElse("ocelot.workspace.saveOnExit", default = true)
autosave = config.getBooleanOrElse("ocelot.workspace.autosave", default = true) autosave = config.getBooleanOrElse("ocelot.workspace.autosave", default = true)
autosavePeriod = config.getIntOrElse("ocelot.workspace.autosavePeriod", default = 300) autosavePeriod = config.getIntOrElse("ocelot.workspace.autosavePeriod", default = 300)
@ -105,6 +106,9 @@ object Settings extends Logging {
def withValue(path: String, value: Option[Any]): Config = def withValue(path: String, value: Option[Any]): Config =
config.withValue(path, ConfigValueFactory.fromAnyRef(value.orNull)) config.withValue(path, ConfigValueFactory.fromAnyRef(value.orNull))
def withValue(path: String, value: Int): Config =
config.withValue(path, ConfigValueFactory.fromAnyRef(value))
} }
class Int2D(var x: Int, var y: Int) { class Int2D(var x: Int, var y: Int) {
@ -132,39 +136,18 @@ object Settings extends Logging {
def get: Settings = settings def get: Settings = settings
def load(path: Path): Unit = { def load(path: Path): Unit = {
import java.lang.System.{lineSeparator => EOL}
if (Files.exists(path)) { if (Files.exists(path)) {
var stream: InputStream = null
try { try {
stream = Files.newInputStream(path) settings = new Settings(ConfigFactory.parseFile(path.toFile))
val source = Source.fromInputStream(stream)(Codec.UTF8)
val plain = source.getLines().mkString("", EOL, EOL)
val config = ConfigFactory.parseString(plain)
settings = new Settings(config)
source.close()
logger.info(s"Loaded Ocelot Desktop configuration from: $path") logger.info(s"Loaded Ocelot Desktop configuration from: $path")
return return
} catch { } catch {
case _: Throwable => case t: Throwable => logger.error(s"Failed to parse $path!", t)
logger.info(s"Failed to parse $path, using default Ocelot Desktop configuration.")
} finally {
if (stream != null)
stream.close()
} }
} }
val defaults = { logger.info(s"Using default Ocelot Desktop configuration...")
val in = getClass.getResourceAsStream("/ocelot/desktop/ocelot.conf") settings = new Settings(ConfigFactory.parseResources("/ocelot/desktop/ocelot.conf"))
val config = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL)
in.close()
ConfigFactory.parseString(config)
}
settings = new Settings(defaults)
} }
def save(path: Path): Unit = { def save(path: Path): Unit = {
@ -186,9 +169,11 @@ object Settings extends Logging {
.withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen) .withValuePreserveOrigin("ocelot.window.fullscreen", settings.windowFullscreen)
.withValuePreserveOrigin("ocelot.window.disableVsync", settings.disableVsync) .withValuePreserveOrigin("ocelot.window.disableVsync", settings.disableVsync)
.withValuePreserveOrigin("ocelot.window.debugLwjgl", settings.debugLwjgl) .withValuePreserveOrigin("ocelot.window.debugLwjgl", settings.debugLwjgl)
.withValue("ocelot.keymap", settings.keymap.save())
.withValue("ocelot.workspace.recent", settings.recentWorkspace) .withValue("ocelot.workspace.recent", settings.recentWorkspace)
.withValuePreserveOrigin("ocelot.workspace.pinNewWindows", settings.pinNewWindows) .withValuePreserveOrigin("ocelot.workspace.pinNewWindows", settings.pinNewWindows)
.withValuePreserveOrigin("ocelot.workspace.unfocusedWindowTransparency", settings.unfocusedWindowTransparency) .withValuePreserveOrigin("ocelot.workspace.unfocusedWindowTransparency", settings.unfocusedWindowTransparency)
.withValuePreserveOrigin("ocelot.workspace.unfocusedWindowHide", settings.unfocusedWindowHide)
.withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit) .withValuePreserveOrigin("ocelot.workspace.saveOnExit", settings.saveOnExit)
.withValuePreserveOrigin("ocelot.workspace.autosave", settings.autosave) .withValuePreserveOrigin("ocelot.workspace.autosave", settings.autosave)
.withValuePreserveOrigin("ocelot.workspace.autosavePeriod", settings.autosavePeriod) .withValuePreserveOrigin("ocelot.workspace.autosavePeriod", settings.autosavePeriod)

View File

@ -21,7 +21,7 @@ object AL10W extends Logging {
val exc = OpenAlException(func, errName, err) val exc = OpenAlException(func, errName, err)
if (Settings.get.logAudioErrorStacktrace) { if (Settings.get.logAudioErrorStacktrace) {
logger.error(exc) logger.error(exc.getMessage, exc)
} else { } else {
logger.error(exc.getMessage) logger.error(exc.getMessage)
} }

View File

@ -246,4 +246,12 @@ object Audio extends Logging {
AL10W.alDeleteBuffers(buf.get(i)) AL10W.alDeleteBuffers(buf.get(i))
} }
} }
def removeAllSources(): Unit = synchronized {
for (sourceId <- sources.values) {
deleteSource(sourceId)
}
sources.clear()
}
} }

View File

@ -44,6 +44,8 @@ object SoundBuffers extends Resource {
lazy val MinecraftClickRelease: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click_release.ogg") lazy val MinecraftClickRelease: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click_release.ogg")
lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg") lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg")
lazy val SelfDestructingCardCountdownBeep: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/countdown_beep.ogg")
lazy val NoteBlock: Map[String, SoundBuffer] = List( 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",

View File

@ -34,15 +34,15 @@ class SoundSource(
Audio.getSourceStatus(this) Audio.getSourceStatus(this)
} }
def isPlaying: Boolean = { def playing: Boolean = {
status == SoundSource.Status.Playing status == SoundSource.Status.Playing
} }
def isPaused: Boolean = { def paused: Boolean = {
status == SoundSource.Status.Paused status == SoundSource.Status.Paused
} }
def isStopped: Boolean = { def stopped: Boolean = {
status == SoundSource.Status.Stopped status == SoundSource.Status.Stopped
} }
@ -124,9 +124,6 @@ object SoundSource {
SoundSource.fromBuffer(SoundBuffers.MinecraftClickRelease, SoundCategory.Interface) SoundSource.fromBuffer(SoundBuffers.MinecraftClickRelease, SoundCategory.Interface)
} }
lazy val MinecraftExplosion: SoundSource =
SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment)
lazy val MachineFloppyInsert: SoundSource = lazy val MachineFloppyInsert: SoundSource =
SoundSource.fromBuffer(SoundBuffers.MachineFloppyInsert, SoundCategory.Environment) SoundSource.fromBuffer(SoundBuffers.MachineFloppyInsert, SoundCategory.Environment)

View File

@ -1,5 +1,6 @@
package ocelot.desktop.color package ocelot.desktop.color
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
import ocelot.desktop.geometry.Vector3D import ocelot.desktop.geometry.Vector3D
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -60,6 +61,15 @@ case class RGBAColorNorm(r: Float, g: Float, b: Float, a: Float = 1f) extends Co
HSVAColor(hue, saturation, value, a) HSVAColor(hue, saturation, value, a)
} }
def lerp(dst: RGBAColorNorm, t: Float): RGBAColorNorm = {
RGBAColorNorm(
r.lerp(dst.r, t),
g.lerp(dst.g, t),
b.lerp(dst.b, t),
a.lerp(dst.a, t),
)
}
def withAlpha(alpha: Float): RGBAColorNorm = RGBAColorNorm(r, g, b, alpha) def withAlpha(alpha: Float): RGBAColorNorm = RGBAColorNorm(r, g, b, alpha)
// ʕʔ // ʕʔ

View File

@ -152,7 +152,7 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
} }
def isPlaying: Boolean = def isPlaying: Boolean =
playbackSoundSource.isDefined && playbackSoundSource.get.isPlaying || playbackThread.isDefined playbackSoundSource.isDefined && playbackSoundSource.get.playing || playbackThread.isDefined
@Callback() @Callback()
def start(context: Context, args: Arguments): Array[AnyRef] = def start(context: Context, args: Arguments): Array[AnyRef] =

View File

@ -4,6 +4,6 @@ object FloatUtils {
implicit class ExtendedFloat(val v: Float) extends AnyVal { implicit class ExtendedFloat(val v: Float) extends AnyVal {
def lerp(that: Float, alpha: Float): Float = v * (1 - alpha) + that * alpha def lerp(that: Float, alpha: Float): Float = v * (1 - alpha) + that * alpha
def clamp(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min) def clamped(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min)
} }
} }

View File

@ -76,7 +76,9 @@ case class Rect2D(x: Float, y: Float, w: Float, h: Float) {
this this
} }
def inflate(addition: Float): Rect2D = Rect2D(x - addition, y - addition, w + addition * 2, h + addition * 2) def inflated(dx: Float, dy: Float): Rect2D = Rect2D(x - dx, y - dy, w + dx * 2, h + dy * 2)
def inflated(delta: Float): Rect2D = inflated(delta, delta)
def center: Vector2D = { def center: Vector2D = {
min + (size * 0.5f).toVector min + (size * 0.5f).toVector

View File

@ -24,7 +24,7 @@ object Transform2D {
Transform2D.translate(-1f, 1f) >> Transform2D.scale(2f / width, -2f / height) Transform2D.translate(-1f, 1f) >> Transform2D.scale(2f / width, -2f / height)
def rotate(angle: Float): Transform2D = { def rotate(angle: Float): Transform2D = {
val (s, c) = (math.sin(angle).asInstanceOf[Float], math.cos(angle).asInstanceOf[Float]) val (s, c) = (math.sin(angle).toFloat, math.cos(angle).toFloat)
// format: off // format: off
Transform2D( Transform2D(

View File

@ -36,9 +36,6 @@ case class Vector2D(x: Float, y: Float) extends Persistable {
def *(scalar: Float): Vector2D = Vector2D(x * scalar, y * scalar) def *(scalar: Float): Vector2D = Vector2D(x * scalar, y * scalar)
// TODO: remove
def *(scalar: Double): Vector2D = Vector2D(x * scalar, y * scalar)
def /(scalar: Float): Vector2D = Vector2D(x / scalar, y / scalar) def /(scalar: Float): Vector2D = Vector2D(x / scalar, y / scalar)
def snap(v: Float): Vector2D = Vector2D((x / v).floor * v, (y / v).floor * v) def snap(v: Float): Vector2D = Vector2D((x / v).floor * v, (y / v).floor * v)

View File

@ -45,7 +45,7 @@ case class Vector3D(x: Float, y: Float, z: Float) {
) )
def angle(that: Vector3D): Float = { def angle(that: Vector3D): Float = {
math.acos((dot(that) / length / that.length).clamp(-1, 1)).toFloat math.acos((dot(that) / length / that.length).clamped(-1, 1)).toFloat
} }
def lerp(that: Vector3D, alpha: Float): Vector3D = { def lerp(that: Vector3D, alpha: Float): Vector3D = {

View File

@ -2,7 +2,6 @@ package ocelot.desktop.graphics
import ocelot.desktop.color.{Color, RGBAColorNorm} import ocelot.desktop.color.{Color, RGBAColorNorm}
import ocelot.desktop.geometry.{Rect2D, Size2D, Transform2D, Vector2D} import ocelot.desktop.geometry.{Rect2D, Size2D, Transform2D, Vector2D}
import ocelot.desktop.graphics.IconSource.Animation
import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.Texture.MinFilteringMode
import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D} import ocelot.desktop.graphics.mesh.{Mesh2D, MeshInstance2D, MeshVertex2D}
import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.graphics.render.InstanceRenderer
@ -280,94 +279,50 @@ class Graphics(private var width: Int, private var height: Int, private var scal
// I hate scala. Overloaded methods with default arguments are not allowed // I hate scala. Overloaded methods with default arguments are not allowed
def sprite(icon: IconSource, bounds: Rect2D): Unit = { def sprite(icon: IconSource, bounds: Rect2D): Unit = {
sprite(icon.path, bounds.x, bounds.y, bounds.w, bounds.h, Color.White, icon.animation) sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, Color.White)
}
def sprite(icon: IconSource, bounds: Rect2D, color: Color): Unit = {
sprite(icon, bounds.x, bounds.y, bounds.w, bounds.h, color)
} }
def sprite(icon: IconSource, pos: Vector2D, size: Size2D): Unit = { def sprite(icon: IconSource, pos: Vector2D, size: Size2D): Unit = {
sprite(icon.path, pos.x, pos.y, size.width, size.height, Color.White, icon.animation) sprite(icon, pos.x, pos.y, size.width, size.height, Color.White)
}
def sprite(icon: IconSource, pos: Vector2D, color: Color): Unit = {
sprite(icon, pos.x, pos.y, color)
} }
def sprite(icon: IconSource, pos: Vector2D, size: Size2D, color: Color): Unit = { def sprite(icon: IconSource, pos: Vector2D, size: Size2D, color: Color): Unit = {
sprite(icon.path, pos.x, pos.y, size.width, size.height, color, icon.animation) sprite(icon, pos.x, pos.y, size.width, size.height, color)
} }
def sprite(icon: IconSource, x: Float, y: Float): Unit = { def sprite(icon: IconSource, x: Float, y: Float): Unit = {
sprite(icon.path, x, y, icon.animation) sprite(icon, x, y, Color.White)
} }
def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = { def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = {
sprite(icon.path, x, y, width, height, animation = icon.animation) sprite(icon, x, y, width, height, Color.White)
} }
def sprite(icon: IconSource, x: Float, y: Float, width: Float, height: Float, color: Color): Unit = { def sprite(icon: IconSource, x: Float, y: Float, color: Color): Unit = {
sprite(icon.path, x, y, width, height, color, icon.animation) val size = Spritesheet.spriteSize(icon.path)
sprite(icon, x, y, size.width, size.height, color)
} }
def sprite(name: String, bounds: Rect2D): Unit = { def sprite(
sprite(name, bounds.origin, bounds.size, Color.White) icon: IconSource,
} x: Float,
y: Float,
def sprite(name: String, x: Float, y: Float, color: Color): Unit = { width: Float,
sprite(name, Vector2D(x, y), Spritesheet.spriteSize(name), color) height: Float,
} color: Color,
): Unit = {
def sprite(name: String, pos: Vector2D, color: Color): Unit = { sprite = icon.path
sprite(name, pos, Spritesheet.spriteSize(name), color)
}
def sprite(name: String, pos: Vector2D, size: Size2D, color: Color): Unit = {
sprite(name, pos.x, pos.y, size.width, size.height, color)
}
def sprite(name: String, pos: Vector2D, size: Size2D): Unit = {
sprite(name, pos.x, pos.y, size.width, size.height)
}
def sprite(name: String, x: Float, y: Float): Unit = {
sprite(name, x, y, Color.White, None)
}
def sprite(name: String, x: Float, y: Float, animation: Option[Animation]): Unit = {
sprite(name, x, y, Color.White, animation)
}
def sprite(name: String, x: Float, y: Float, color: Color, animation: Option[Animation]): Unit = {
val size = Spritesheet.spriteSize(name)
sprite(name, x, y, size.width, size.height, color, animation)
}
def sprite(name: String, x: Float, y: Float, width: Float, height: Float,
color: Color = Color.White,
animation: Option[Animation] = None): Unit = {
sprite = name
foreground = color foreground = color
_rect(x, y, width, height, fixUV = true, animation)
}
def rect(r: Rect2D, color: Color): Unit = { val spriteRect = icon.animation.map { animation =>
rect(r.x, r.y, r.w, r.h, color)
}
def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = {
sprite("Empty", x, y, width, height, color)
}
private def checkFont(): Unit = {
if (_font != oldFont) {
val newFont = _font
_font = oldFont
flush()
_font = newFont
oldFont = _font
}
}
private def _rect(x: Float, y: Float, width: Float, height: Float,
fixUV: Boolean = true,
animation: Option[Animation] = None): Unit = {
val spriteRect = animation match {
case None => this.spriteRect
case Some(animation) =>
val duration = animation.frames.map(_._2).sum val duration = animation.frames.map(_._2).sum
var timeOffset = 0f var timeOffset = 0f
var curFrame = 0 var curFrame = 0
@ -384,9 +339,52 @@ class Graphics(private var width: Int, private var height: Int, private var scal
case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width) case Some(size) => Size2D(this.spriteRect.w, this.spriteRect.w * size.height / size.width)
case None => Size2D(this.spriteRect.w, this.spriteRect.w) case None => Size2D(this.spriteRect.w, this.spriteRect.w)
} }
this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height) this.spriteRect.copy(y = this.spriteRect.y + curFrame * size.height, h = size.height)
} }
_rect(x, y, width, height, fixUV = true, spriteRect)
}
def sprite(
name: String,
x: Float,
y: Float,
width: Float,
height: Float,
color: Color,
spriteRect: Option[Rect2D],
fixUV: Boolean = true,
): Unit = {
sprite = name
foreground = color
_rect(x, y, width, height, fixUV, spriteRect)
}
def rect(r: Rect2D, color: Color): Unit = {
rect(r.x, r.y, r.w, r.h, color)
}
def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = {
sprite(IconSource.Empty, x, y, width, height, color)
}
private def checkFont(): Unit = {
if (_font != oldFont) {
val newFont = _font
_font = oldFont
flush()
_font = newFont
oldFont = _font
}
}
private def _rect(x: Float, y: Float, width: Float, height: Float,
fixUV: Boolean = true,
spriteRectOptional: Option[Rect2D] = None): Unit = {
val spriteRect = spriteRectOptional.getOrElse(this.spriteRect)
val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >> val uvTransform = Transform2D.translate(spriteRect.x, spriteRect.y) >>
(if (fixUV) (if (fixUV)
Transform2D.scale(spriteRect.w - 0.25f / 1024, spriteRect.h - 0.25f / 1024) Transform2D.scale(spriteRect.w - 0.25f / 1024, spriteRect.h - 0.25f / 1024)

View File

@ -3,96 +3,148 @@ package ocelot.desktop.graphics
import ocelot.desktop.geometry.Size2D import ocelot.desktop.geometry.Size2D
import ocelot.desktop.ui.widget.modal.notification.NotificationType.NotificationType import ocelot.desktop.ui.widget.modal.notification.NotificationType.NotificationType
import totoro.ocelot.brain.entity.tape.Tape.{Kind => TapeKind} import totoro.ocelot.brain.entity.tape.Tape.{Kind => TapeKind}
import totoro.ocelot.brain.util.Direction.{Direction, Down, East, North, South, Up, West}
import totoro.ocelot.brain.util.DyeColor import totoro.ocelot.brain.util.DyeColor
import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier import totoro.ocelot.brain.util.ExtendedTier.ExtendedTier
import totoro.ocelot.brain.util.Tier.Tier import totoro.ocelot.brain.util.Tier.Tier
case class IconSource( case class IconSource(path: String, animation: Option[IconSource.Animation] = None)
path: String,
animation: Option[IconSource.Animation] = None,
)
object IconSource { object IconSource {
val CardIcon: IconSource = IconSource("icons/Card") case class Animation(frames: Array[(Int, Float)], frameSize: Option[Size2D])
val CpuIcon: IconSource = IconSource("icons/CPU")
val HddIcon: IconSource = IconSource("icons/HDD")
val EepromIcon: IconSource = IconSource("icons/EEPROM")
val FloppyIcon: IconSource = IconSource("icons/Floppy")
val MemoryIcon: IconSource = IconSource("icons/Memory")
val ServerIcon: IconSource = IconSource("icons/Server")
val ComponentBusIcon: IconSource = IconSource("icons/ComponentBus")
val TierIcon: Tier => IconSource = { tier => object Animation {
IconSource(s"icons/Tier${tier.id}") def apply(frames: (Int, Float)*) = new Animation(frames.toArray, None)
def apply(size: Size2D)(frames: (Int, Float)*) = new Animation(frames.toArray, Some(size))
def apply(size: Option[Size2D] = None)(frames: Array[(Int, Float)]) = new Animation(frames, size)
} }
object Items { class IconScope(directory: String)(implicit parent: Option[IconScope]) {
// this makes nested `IconScope` declarations use `this` as their parent scope.
implicit def scope: Option[IconScope] = Some(this)
private def prefix: String = parent match {
case Some(parent) => s"${parent.prefix}/$directory"
case None => directory
}
protected def get(name: String): IconSource = {
get(name, None)
}
protected def get(name: String, animation: Animation): IconSource = {
get(name, Some(animation))
}
protected def get(name: String, animation: Option[Animation]): IconSource = {
IconSource(s"$prefix/$name", animation)
}
}
private implicit val scope: Option[IconScope] = None
val Empty: IconSource = IconSource("Empty")
val EmptySlot: IconSource = IconSource("EmptySlot")
val ShadowCorner: IconSource = IconSource("ShadowCorner")
val ShadowBorder: IconSource = IconSource("ShadowBorder")
val TabArrow: IconSource = IconSource("TabArrow")
val BarSegment: IconSource = IconSource("BarSegment")
val BackgroundPattern: IconSource = IconSource("BackgroundPattern")
val Logo: IconSource = IconSource("Logo")
val Knob: IconSource = IconSource("Knob")
val KnobLimits: IconSource = IconSource("KnobLimits")
val KnobCenter: IconSource = IconSource("KnobCenter")
val Loading: IconSource = IconSource(
"Loading",
Some(
Animation(Size2D(48, 32))(
(0, 0.7f),
(1, 0.7f),
(2, 0.7f),
(3, 0.7f),
(4, 0.7f),
(5, 0.7f),
(6, 0.7f),
(7, 0.7f),
(8, 0.7f),
(9, 0.7f),
(10, 0.7f),
(11, 0.7f),
(12, 0.7f),
(13, 0.7f),
)
),
)
object Items extends IconScope("items") {
val Cpu: Tier => IconSource = { tier => val Cpu: Tier => IconSource = { tier =>
IconSource(s"items/CPU${tier.id}") get(s"CPU${tier.id}")
} }
val Apu: Tier => IconSource = { tier => val Apu: Tier => IconSource = { tier =>
IconSource(s"items/APU${tier.id}", animation = Some(Animations.Apu)) get(s"APU${tier.id}", Animations.Apu)
} }
val GraphicsCard: Tier => IconSource = { tier => val GraphicsCard: Tier => IconSource = { tier =>
IconSource(s"items/GraphicsCard${tier.id}") get(s"GraphicsCard${tier.id}")
} }
val NetworkCard: IconSource = IconSource("items/NetworkCard") val NetworkCard: IconSource = get("NetworkCard")
val WirelessNetworkCard: Tier => IconSource = { tier => val WirelessNetworkCard: Tier => IconSource = { tier =>
IconSource(s"items/WirelessNetworkCard${tier.id}") get(s"WirelessNetworkCard${tier.id}")
} }
val LinkedCard: IconSource = IconSource("items/LinkedCard", animation = Some(Animations.LinkedCard)) val LinkedCard: IconSource = get("LinkedCard", Animations.LinkedCard)
val InternetCard: IconSource = IconSource("items/InternetCard", animation = Some(Animations.InternetCard)) val InternetCard: IconSource = get("InternetCard", Animations.InternetCard)
val RedstoneCard: Tier => IconSource = { tier => val RedstoneCard: Tier => IconSource = { tier =>
IconSource(s"items/RedstoneCard${tier.id}") get(s"RedstoneCard${tier.id}")
} }
val DataCard: Tier => IconSource = { tier => val DataCard: Tier => IconSource = { tier =>
IconSource(s"items/DataCard${tier.id}", animation = Some(Animations.DataCard)) get(s"DataCard${tier.id}", Animations.DataCard)
} }
val SoundCard: IconSource = IconSource("items/SoundCard", animation = Some(Animations.DataCard)) val SoundCard: IconSource = get("SoundCard", Animations.DataCard)
val SelfDestructingCard: IconSource = val SelfDestructingCard: IconSource =
IconSource("items/SelfDestructingCard", animation = Some(Animations.SelfDestructingCard)) get("SelfDestructingCard", Animations.SelfDestructingCard)
val OcelotCard: IconSource = IconSource("items/OcelotCard", animation = Some(Animations.OcelotCard)) val OcelotCard: IconSource = get("OcelotCard", Animations.OcelotCard)
val HardDiskDrive: Tier => IconSource = { tier => val HardDiskDrive: Tier => IconSource = { tier =>
IconSource(s"items/HardDiskDrive${tier.id}") get(s"HardDiskDrive${tier.id}")
} }
val Eeprom: IconSource = IconSource("items/EEPROM") val Eeprom: IconSource = get("EEPROM")
val FloppyDisk: DyeColor => IconSource = { color => val FloppyDisk: DyeColor => IconSource = { color =>
IconSource(s"items/FloppyDisk_${color.name}") get(s"FloppyDisk_${color.name}")
} }
val Memory: ExtendedTier => IconSource = { tier => val Memory: ExtendedTier => IconSource = { tier =>
IconSource(s"items/Memory${tier.id}") get(s"Memory${tier.id}")
} }
val Server: Tier => IconSource = { tier => val Server: Tier => IconSource = { tier =>
IconSource(s"items/Server${tier.id}") get(s"Server${tier.id}")
} }
val ComponentBus: Tier => IconSource = { tier => val ComponentBus: Tier => IconSource = { tier =>
IconSource(s"items/ComponentBus${tier.id}") get(s"ComponentBus${tier.id}")
} }
val Tape: TapeKind => IconSource = { val Tape: TapeKind => IconSource = {
case TapeKind.Golder => Tape(TapeKind.Gold) case TapeKind.Golder => Tape(TapeKind.Gold)
case TapeKind.NetherStarrer => Tape(TapeKind.NetherStar) case TapeKind.NetherStarrer => Tape(TapeKind.NetherStar)
case kind => IconSource(s"items/Tape$kind") case kind => get(s"Tape$kind")
} }
val DiskDriveMountable: IconSource = IconSource("items/DiskDriveMountable") val DiskDriveMountable: IconSource = get("DiskDriveMountable")
// noinspection ScalaWeakerAccess // noinspection ScalaWeakerAccess
object Animations { object Animations {
@ -114,223 +166,368 @@ object IconSource {
} }
} }
case class Animation(frames: Array[(Int, Float)], frameSize: Option[Size2D]) object Icons extends IconScope("icons") {
val Card: IconSource = get("Card")
val Cpu: IconSource = get("CPU")
val Hdd: IconSource = get("HDD")
val Eeprom: IconSource = get("EEPROM")
val Floppy: IconSource = get("Floppy")
val Memory: IconSource = get("Memory")
val Server: IconSource = get("Server")
val ComponentBus: IconSource = get("ComponentBus")
object Animation { val Tier: Tier => IconSource = { tier =>
def apply(frames: (Int, Float)*) = new Animation(frames.toArray, None) get(s"Tier${tier.id}")
def apply(size: Size2D, frames: (Int, Float)*) = new Animation(frames.toArray, Some(size))
def apply(size: Option[Size2D] = None, frames: Array[(Int, Float)]) = new Animation(frames, size)
} }
// ----------------------- Ocelot interface icons ----------------------- val SideNone: IconSource = get("SideNone")
val SideAny: IconSource = get("SideAny")
val SideUndefined: IconSource = get("SideUndefined")
val Side: Direction => IconSource = {
case Down => get("SideDown")
case Up => get("SideUp")
case North => get("SideNorth")
case South => get("SideSouth")
case West => get("SideWest")
case East => get("SideEast")
case _ => SideUndefined
}
val Notification: NotificationType => IconSource = { notificationType => val Notification: NotificationType => IconSource = { notificationType =>
IconSource(s"icons/Notification$notificationType") get(s"Notification$notificationType")
} }
val Loading: IconSource = IconSource( val NA: IconSource = get("NA")
"Loading", val SettingsKeymap: IconSource = get("SettingsKeymap")
animation = Some(Animation( val SettingsSystem: IconSource = get("SettingsSystem")
Size2D(48, 32), val SettingsSound: IconSource = get("SettingsSound")
(0, 0.7f), val SettingsUI: IconSource = get("SettingsUI")
(1, 0.7f), val Delete: IconSource = get("Delete")
(2, 0.7f), val Label: IconSource = get("Label")
(3, 0.7f), val Copy: IconSource = get("Copy")
(4, 0.7f), val AspectRatio: IconSource = get("AspectRatio")
(5, 0.7f), val Eject: IconSource = get("Eject")
(6, 0.7f), val Restart: IconSource = get("Restart")
(7, 0.7f), val Edit: IconSource = get("Edit")
(8, 0.7f), val Folder: IconSource = get("Folder")
(9, 0.7f), val FolderSlash: IconSource = get("FolderSlash")
(10, 0.7f), val Code: IconSource = get("Code")
(11, 0.7f), val File: IconSource = get("File")
(12, 0.7f), val Link: IconSource = get("Link")
(13, 0.7f), val LinkSlash: IconSource = get("LinkSlash")
)), val Power: IconSource = get("Power")
) val Save: IconSource = get("Save")
val SaveAs: IconSource = get("SaveAs")
val Plus: IconSource = get("Plus")
val Cross: IconSource = get("Cross")
val Microchip: IconSource = get("Microchip")
val Antenna: IconSource = get("Antenna")
val Window: IconSource = get("Window")
val Tiers: IconSource = get("Tiers")
val LinesHorizontal: IconSource = get("LinesHorizontal")
val ArrowRight: IconSource = get("ArrowRight")
val Book: IconSource = get("Book")
val Help: IconSource = get("Help")
val Ocelot: IconSource = get("Ocelot")
val Guitar: IconSource = get("Guitar")
val Keyboard: IconSource = get("Keyboard")
val KeyboardOff: IconSource = get("KeyboardOff")
val ButtonRandomize: IconSource = get("ButtonRandomize")
val ButtonClipboard: IconSource = get("ButtonClipboard")
val ButtonCheck: IconSource = get("ButtonCheck")
val Home: IconSource = get("Home")
val Pin: IconSource = get("Pin")
val Unpin: IconSource = get("Unpin")
val Close: IconSource = get("Close")
val Grid: IconSource = get("Grid")
val GridOff: IconSource = get("GridOff")
val LMB: IconSource = get("LMB")
val RMB: IconSource = get("RMB")
val DragLMB: IconSource = get("DragLMB")
val DragRMB: IconSource = get("DragRMB")
val SettingsSystem: IconSource = IconSource("icons/SettingsSystem") val WireArrowLeft: IconSource = get("WireArrowLeft")
val SettingsSound: IconSource = IconSource("icons/SettingsSound") val WireArrowRight: IconSource = get("WireArrowRight")
val SettingsUI: IconSource = IconSource("icons/SettingsUI")
val Delete: IconSource = IconSource("icons/Delete")
val Label: IconSource = IconSource("icons/Label")
val Copy: IconSource = IconSource("icons/Copy")
val AspectRatio: IconSource = IconSource("icons/AspectRatio")
val Eject: IconSource = IconSource("icons/Eject")
val Restart: IconSource = IconSource("icons/Restart")
val Edit: IconSource = IconSource("icons/Edit")
val Folder: IconSource = IconSource("icons/Folder")
val FolderSlash: IconSource = IconSource("icons/FolderSlash")
val Code: IconSource = IconSource("icons/Code")
val File: IconSource = IconSource("icons/File")
val Link: IconSource = IconSource("icons/Link")
val LinkSlash: IconSource = IconSource("icons/LinkSlash")
val Power: IconSource = IconSource("icons/Power")
val Save: IconSource = IconSource("icons/Save")
val SaveAs: IconSource = IconSource("icons/SaveAs")
val Plus: IconSource = IconSource("icons/Plus")
val Cross: IconSource = IconSource("icons/Cross")
val Microchip: IconSource = IconSource("icons/Microchip")
val Antenna: IconSource = IconSource("icons/Antenna")
val Window: IconSource = IconSource("icons/Window")
val Tiers: IconSource = IconSource("icons/Tiers")
val LinesHorizontal: IconSource = IconSource("icons/LinesHorizontal")
val ArrowRight: IconSource = IconSource("icons/ArrowRight")
val Book: IconSource = IconSource("icons/Book")
val Help: IconSource = IconSource("icons/Help")
val Ocelot: IconSource = IconSource("icons/Ocelot")
val Guitar: IconSource = IconSource("icons/Guitar")
val Keyboard: IconSource = IconSource("icons/Keyboard")
val KeyboardOff: IconSource = IconSource("icons/KeyboardOff")
// ----------------------- Node icons ----------------------- val WaveSine: IconSource = get("WaveSine")
val WaveTriangle: IconSource = get("WaveTriangle")
val WaveSawtooth: IconSource = get("WaveSawtooth")
val WaveSquare: IconSource = get("WaveSquare")
val WaveNoise: IconSource = get("WaveNoise")
val WaveLFSR: IconSource = get("WaveLFSR")
}
val NA: IconSource = IconSource("icons/NA") object Nodes extends IconScope("nodes") {
val NewNode: IconSource = get("NewNode")
object Nodes { val Cable: IconSource = get("Cable")
val NewNode: IconSource = IconSource("nodes/NewNode") val Camera: IconSource = get("Camera")
val Chest: IconSource = get("Chest")
val Cable: IconSource = IconSource("nodes/Cable")
val Camera: IconSource = IconSource("nodes/Camera")
val Chest: IconSource = IconSource("nodes/Chest")
val HologramProjector: Tier => IconSource = { tier => val HologramProjector: Tier => IconSource = { tier =>
IconSource(s"nodes/HologramProjector${tier.id}") get(s"HologramProjector${tier.id}")
} }
val IronNoteBlock: IconSource = IconSource("nodes/IronNoteBlock") val IronNoteBlock: IconSource = get("IronNoteBlock")
val NoteBlock: IconSource = IconSource("nodes/NoteBlock") val NoteBlock: IconSource = get("NoteBlock")
val OpenFMRadio: IconSource = IconSource("nodes/OpenFMRadio") val OpenFMRadio: IconSource = get("OpenFMRadio")
val Relay: IconSource = IconSource("nodes/Relay") val Relay: IconSource = get("Relay")
val TapeDrive: IconSource = IconSource("nodes/TapeDrive") val TapeDrive: IconSource = get("TapeDrive")
object Computer extends PowerIconSource with DiskActivityIconSource { val Lamp: IconSource = get("Lamp")
override protected def prefix: String = "nodes/computer" val LampFrame: IconSource = get("LampFrame")
val LampGlow: IconSource = get("LampGlow")
val Default: IconSource = IconSource(s"$prefix/Default") object Computer extends IconScope("computer") with PowerIconSource with DiskActivityIconSource {
val Default: IconSource = get("Default")
} }
object DiskDrive extends DiskActivityIconSource with FloppyDriveIconSource { object DiskDrive extends IconScope("disk-drive") with DiskActivityIconSource with FloppyDriveIconSource {
override protected def prefix: String = "nodes/disk-drive" val Default: IconSource = get("Default")
val Default: IconSource = IconSource(s"$prefix/Default")
} }
object Lamp extends IconSource("nodes/Lamp") { object Microcontroller extends IconScope("microcontroller") with PowerIconSource {
val Frame: IconSource = IconSource("nodes/LampFrame") val Default: IconSource = get("Default")
val Glow: IconSource = IconSource("nodes/LampGlow")
} }
object Microcontroller extends PowerIconSource { object OcelotBlock extends IconScope("ocelot-block") {
override protected def prefix: String = "nodes/microcontroller" val Default: IconSource = get(
"Default",
val Default: IconSource = IconSource(s"$prefix/Default") Some(Animation(Size2D(16, 16))((0, 30f), (1, 5f), (2, 2f), (0, 20f), (3, 3f), (4, 2f))),
}
object OcelotBlock {
val Default: IconSource = IconSource(
"nodes/ocelot-block/Default",
animation = Some(Animation(Size2D(16, 16), (0, 30f), (1, 5f), (2, 2f), (0, 20f), (3, 3f), (4, 2f))),
) )
val Rx: IconSource = IconSource("nodes/ocelot-block/Rx") val Rx: IconSource = get("Rx")
val Tx: IconSource = IconSource("nodes/ocelot-block/Tx") val Tx: IconSource = get("Tx")
} }
object Rack { object Rack extends IconScope("rack") {
protected val prefix: String = "nodes/rack" val Empty: IconSource = get("Empty")
val Default: IconSource = get("Default")
val Empty: IconSource = IconSource(s"$prefix/Empty")
val Default: IconSource = IconSource(s"$prefix/Default")
val Server: Array[Server] = Array.tabulate(4)(new Server(_)) val Server: Array[Server] = Array.tabulate(4)(new Server(_))
val Drive: Array[Drive] = Array.tabulate(4)(new Drive(_)) val Drive: Array[Drive] = Array.tabulate(4)(new Drive(_))
class Server(val slot: Int) extends PowerIconSource with DiskActivityIconSource with NetworkActivityIconSource { class Server(val slot: Int)
override protected def prefix: String = s"${Rack.prefix}/server/$slot" extends IconScope(s"server/$slot")
with PowerIconSource
with DiskActivityIconSource
with NetworkActivityIconSource {
val Default: IconSource = IconSource(s"$prefix/Default") val Default: IconSource = get("Default")
} }
class Drive(val slot: Int) extends DiskActivityIconSource with FloppyDriveIconSource { class Drive(val slot: Int)
override protected def prefix: String = s"${Rack.prefix}/drive/$slot" extends IconScope(s"drive/$slot")
with DiskActivityIconSource
with FloppyDriveIconSource {
val Default: IconSource = IconSource(s"$prefix/Default") val Default: IconSource = get("Default")
} }
} }
object Raid { object Raid extends IconScope("raid") {
protected val prefix: String = "nodes/raid" val Default: IconSource = get("Default")
val Default: IconSource = IconSource(s"$prefix/Default")
val Drive: Array[Drive] = Array.tabulate(3)(new Drive(_)) val Drive: Array[Drive] = Array.tabulate(3)(new Drive(_))
class Drive(val slot: Int) extends DiskActivityIconSource { class Drive(val slot: Int) extends IconScope(slot.toString) with DiskActivityIconSource {
override protected def prefix: String = s"${Raid.prefix}/$slot" val Error: IconSource = get("Error")
val Error: IconSource = IconSource(s"$prefix/Error")
} }
} }
object Screen { object Screen extends IconScope("screen") {
protected val prefix: String = "nodes/screen" val Standalone: IconSource = get("Standalone")
val PowerOnOverlay: IconSource = get("PowerOnOverlay")
val Standalone: IconSource = IconSource(s"$prefix/Standalone") val ColumnTop: IconSource = get("ColumnTop")
val PowerOnOverlay: IconSource = IconSource(s"$prefix/PowerOnOverlay") val ColumnMiddle: IconSource = get("ColumnMiddle")
val ColumnBottom: IconSource = get("ColumnBottom")
val ColumnTop: IconSource = IconSource(s"$prefix/ColumnTop") val RowLeft: IconSource = get("RowLeft")
val ColumnMiddle: IconSource = IconSource(s"$prefix/ColumnMiddle") val RowMiddle: IconSource = get("RowMiddle")
val ColumnBottom: IconSource = IconSource(s"$prefix/ColumnBottom") val RowRight: IconSource = get("RowRight")
val RowLeft: IconSource = IconSource(s"$prefix/RowLeft") val TopLeft: IconSource = get("TopLeft")
val RowMiddle: IconSource = IconSource(s"$prefix/RowMiddle") val TopMiddle: IconSource = get("TopMiddle")
val RowRight: IconSource = IconSource(s"$prefix/RowRight") val TopRight: IconSource = get("TopRight")
val TopLeft: IconSource = IconSource(s"$prefix/TopLeft") val MiddleLeft: IconSource = get("MiddleLeft")
val TopMiddle: IconSource = IconSource(s"$prefix/TopMiddle") val Middle: IconSource = get("Middle")
val TopRight: IconSource = IconSource(s"$prefix/TopRight") val MiddleRight: IconSource = get("MiddleRight")
val MiddleLeft: IconSource = IconSource(s"$prefix/MiddleLeft") val BottomLeft: IconSource = get("BottomLeft")
val Middle: IconSource = IconSource(s"$prefix/Middle") val BottomMiddle: IconSource = get("BottomMiddle")
val MiddleRight: IconSource = IconSource(s"$prefix/MiddleRight") val BottomRight: IconSource = get("BottomRight")
val BottomLeft: IconSource = IconSource(s"$prefix/BottomLeft")
val BottomMiddle: IconSource = IconSource(s"$prefix/BottomMiddle")
val BottomRight: IconSource = IconSource(s"$prefix/BottomRight")
} }
object Holidays { object Holidays extends IconScope("holidays") {
protected val prefix: String = "nodes/holidays" val Christmas: IconSource = get("Christmas")
val Valentines: IconSource = get("Valentines")
val Christmas: IconSource = IconSource(s"$prefix/Christmas") val Halloween: IconSource = get("Halloween")
val Valentines: IconSource = IconSource(s"$prefix/Valentines")
val Halloween: IconSource = IconSource(s"$prefix/Halloween")
} }
} }
trait PowerIconSource { trait PowerIconSource extends IconScope {
protected def prefix: String val On: IconSource = get("On")
val Error: IconSource = get("Error")
val On: IconSource = IconSource(s"$prefix/On")
val Error: IconSource = IconSource(s"$prefix/Error")
} }
trait DiskActivityIconSource { trait DiskActivityIconSource extends IconScope {
protected def prefix: String val DiskActivity: IconSource = get("DiskActivity")
val DiskActivity: IconSource = IconSource(s"$prefix/DiskActivity")
} }
trait NetworkActivityIconSource { trait NetworkActivityIconSource extends IconScope {
protected def prefix: String val NetworkActivity: IconSource = get("NetworkActivity")
val NetworkActivity: IconSource = IconSource(s"$prefix/NetworkActivity")
} }
trait FloppyDriveIconSource { trait FloppyDriveIconSource extends IconScope {
protected def prefix: String val Floppy: IconSource = get("Floppy")
}
val Floppy: IconSource = IconSource(s"$prefix/Floppy") object Screen extends IconScope("screen") {
val InnerCornerTL: IconSource = get("InnerCornerTL")
val InnerCornerTR: IconSource = get("InnerCornerTR")
val InnerCornerBL: IconSource = get("InnerCornerBL")
val InnerCornerBR: IconSource = get("InnerCornerBR")
val OuterCornerTL: IconSource = get("OuterCornerTL")
val OuterCornerTR: IconSource = get("OuterCornerTR")
val OuterCornerBL: IconSource = get("OuterCornerBL")
val OuterCornerBR: IconSource = get("OuterCornerBR")
val InnerBorderT: IconSource = get("InnerBorderT")
val OuterBorderT: IconSource = get("OuterBorderT")
val InnerBorderB: IconSource = get("InnerBorderB")
}
object Particles extends IconScope("particles") {
val Note: IconSource = get("Note")
val Smoke: IconSource = get("Smoke")
}
object Buttons extends IconScope("buttons") {
val BottomDrawerOpen: IconSource = get("BottomDrawerOpen")
val BottomDrawerClose: IconSource = get("BottomDrawerClose")
val PowerOff: IconSource = get("PowerOff")
val PowerOn: IconSource = get("PowerOn")
val OpenFMRadioVolumeOff: Boolean => IconSource = { isUp =>
get(s"OpenFMRadioVolume${if (isUp) "Up" else "Down"}Off")
}
val OpenFMRadioVolumeOn: Boolean => IconSource = { isUp =>
get(s"OpenFMRadioVolume${if (isUp) "Up" else "Down"}On")
}
val OpenFMRadioRedstoneOff: IconSource = get("OpenFMRadioRedstoneOff")
val OpenFMRadioRedstoneOn: IconSource = get("OpenFMRadioRedstoneOn")
val OpenFMRadioCloseOff: IconSource = get("OpenFMRadioCloseOff")
val OpenFMRadioCloseOn: IconSource = get("OpenFMRadioCloseOn")
val OpenFMRadioStartOff: IconSource = get("OpenFMRadioStartOff")
val OpenFMRadioStopOn: IconSource = get("OpenFMRadioStopOn")
val RackRelayOff: IconSource = get("RackRelayOff")
val RackRelayOn: IconSource = get("RackRelayOn")
}
object Window extends IconScope("window") {
val CornerTL: IconSource = get("CornerTL")
val CornerTR: IconSource = get("CornerTR")
val CornerBL: IconSource = get("CornerBL")
val CornerBR: IconSource = get("CornerBR")
val BorderLight: IconSource = get("BorderLight")
val BorderDark: IconSource = get("BorderDark")
object Tape extends IconScope("tape") {
abstract class TapeButtonIconSource private[Tape] (sprite: String) {
val Released: IconSource = get(sprite)
val Pressed: IconSource = get(s"${sprite}Pressed")
}
object Back extends TapeButtonIconSource("Back")
object Play extends TapeButtonIconSource("Play")
object Stop extends TapeButtonIconSource("Stop")
object Forward extends TapeButtonIconSource("Forward")
val Screen: IconSource = get("Screen")
}
object Case extends IconScope("case") {
val Motherboard: IconSource = get("Motherboard")
}
object Rack extends IconScope("rack") {
val Motherboard: IconSource = get("Motherboard")
val Lines: IconSource = get("Lines")
trait DirectionIconSource {
protected def iconPrefix: String
val DirectionIcon: Direction => IconSource = { direction =>
get(s"$iconPrefix${direction.side.capitalize}")
}
}
trait ConnectorIconSource {
protected def iconPrefix: String
val Connector: IconSource = get(s"${iconPrefix}Connector")
}
object Side extends DirectionIconSource with ConnectorIconSource {
override protected def iconPrefix: String = "Side"
}
object Network extends DirectionIconSource with ConnectorIconSource {
override protected def iconPrefix: String = "Network"
}
object Node extends DirectionIconSource {
override protected def iconPrefix: String = "Node"
}
}
object Raid extends IconScope("raid") {
val Slots: IconSource = get("Slots")
}
val OpenFMRadio: IconSource = get("OpenFMRadio")
}
object Panel extends IconScope("panel") {
val CornerTL: IconSource = get("CornerTL")
val CornerTR: IconSource = get("CornerTR")
val CornerBL: IconSource = get("CornerBL")
val CornerBR: IconSource = get("CornerBR")
val BorderT: IconSource = get("BorderT")
val BorderB: IconSource = get("BorderB")
val BorderL: IconSource = get("BorderL")
val BorderR: IconSource = get("BorderR")
val Fill: IconSource = get("Fill")
}
object LightPanel extends IconScope("light-panel") {
val CornerTL: IconSource = get("CornerTL")
val CornerTR: IconSource = get("CornerTR")
val CornerBL: IconSource = get("CornerBL")
val CornerBR: IconSource = get("CornerBR")
val BorderT: IconSource = get("BorderT")
val BorderB: IconSource = get("BorderB")
val BorderL: IconSource = get("BorderL")
val BorderR: IconSource = get("BorderR")
val Fill: IconSource = get("Fill")
val Vent: IconSource = get("Vent")
val BookmarkLeft: IconSource = get("BookmarkLeft")
val BookmarkRight: IconSource = get("BookmarkRight")
} }
} }

View File

@ -1,17 +1,22 @@
package ocelot.desktop.inventory package ocelot.desktop.inventory
import ocelot.desktop.inventory.Inventory.SlotObserver import ocelot.desktop.inventory.Inventory.SlotObserver
import ocelot.desktop.ui.event.{Event, EventAware} import ocelot.desktop.ui.event.{BrainEvent, Dispatchable, EventAware}
import ocelot.desktop.util.Disposable
import totoro.ocelot.brain.event.NodeEvent import totoro.ocelot.brain.event.NodeEvent
import scala.collection.mutable import scala.collection.mutable
/** Provides an inventory — a collection of [[Item]]s indexed by slots. */ /**
trait Inventory extends EventAware { * Provides an inventory a collection of [[Item]]s indexed by slots.
*/
trait Inventory extends EventAware with Disposable {
// parallels totoro.ocelot.brain.entity.traits.Inventory // parallels totoro.ocelot.brain.entity.traits.Inventory
// this is intentional // this is intentional
/** The type of items stored in this inventory. */ /**
* The type of items stored in this inventory.
*/
type I <: Item type I <: Item
private type WeakHashSet[A] = mutable.WeakHashMap[A, Unit] private type WeakHashSet[A] = mutable.WeakHashMap[A, Unit]
@ -20,13 +25,20 @@ trait Inventory extends EventAware {
private val itemSlots = mutable.HashMap.empty[I, Int] private val itemSlots = mutable.HashMap.empty[I, Int]
private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]] private val observers = mutable.HashMap.empty[Int, WeakHashSet[SlotObserver]]
/** Called after a new item is added to the inventory. override def dispose(): Unit = {
super.dispose()
inventoryIterator.foreach(_.removeAndDispose())
}
/**
* Called after a new item is added to the inventory.
* *
* @param slot the slot the item was added to * @param slot the slot the item was added to
*/ */
def onItemAdded(slot: Slot): Unit def onItemAdded(slot: Slot): Unit
/** Called after an item is removed from the inventory. /**
* 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: * When the item is replaced by another one, the event are sequenced in the following order:
* *
@ -41,7 +53,9 @@ trait Inventory extends EventAware {
*/ */
def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit def onItemRemoved(slot: Slot, removedItem: I, replacedBy: Option[I]): Unit
/** An iterator over all slots occupied in this inventory. */ /**
* An iterator over all slots occupied in this inventory.
*/
def inventoryIterator: Iterator[Slot] = slotItems.keysIterator.map(Slot(_)) def inventoryIterator: Iterator[Slot] = slotItems.keysIterator.map(Slot(_))
def clearInventory(): Unit = { def clearInventory(): Unit = {
@ -67,18 +81,20 @@ trait Inventory extends EventAware {
super.shouldReceiveEventsFor(address) || super.shouldReceiveEventsFor(address) ||
inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address)) inventoryIterator.flatMap(_.get).exists(_.shouldReceiveEventsFor(address))
override def handleEvent(event: Event): Unit = { override def handleEvent(event: Dispatchable): Unit = {
super.handleEvent(event) super.handleEvent(event)
for (slot <- inventoryIterator; item <- slot.get) { for (slot <- inventoryIterator; item <- slot.get) {
event match { event match {
case n: NodeEvent if !item.shouldReceiveEventsFor(n.address) => // ignore case BrainEvent(e: NodeEvent) if !item.shouldReceiveEventsFor(e.address) => // ignore
case _ => item.handleEvent(event) case _ => item.handleEvent(event)
} }
} }
} }
/** A proxy to access a slot of the inventory. */ /**
* A proxy to access a slot of the inventory.
*/
final class Slot private[Inventory] (val index: Int) { final class Slot private[Inventory] (val index: Int) {
require(index >= 0) require(index >= 0)
@ -86,27 +102,46 @@ trait Inventory extends EventAware {
def nonEmpty: Boolean = !isEmpty def nonEmpty: Boolean = !isEmpty
/** Inserts the `item` into this slot (replacing the previous item if any). */ /**
* Inserts the `item` into this slot (replacing the previous item if any).
*/
def put(item: inventory.I): Unit = { def put(item: inventory.I): Unit = {
setSlot(index, Some(item)) setSlot(index, Some(item))
} }
/** Allows inserting/removing the item in this slot. */ /**
* Allows inserting/removing the item in this slot.
*/
def set(item: Option[inventory.I]): Unit = { def set(item: Option[inventory.I]): Unit = {
setSlot(index, item) setSlot(index, item)
} }
/** Removes the item contained in this slot if there is one. */ /**
* Removes the item contained in this slot if there is one.
*/
def remove(): Unit = { def remove(): Unit = {
setSlot(index, None) setSlot(index, None)
} }
/** The [[Item]] contained in this slot. */ /**
* Removes the item contained in this slot if there is one, calling the item's [[dispose]] method.
*/
def removeAndDispose(): Unit = {
for (item <- get) {
remove()
item.dispose()
}
}
/**
* The [[Item]] contained in this slot.
*/
def get: Option[inventory.I] = slotItems.get(index) def get: Option[inventory.I] = slotItems.get(index)
val inventory: Inventory.this.type = Inventory.this val inventory: Inventory.this.type = Inventory.this
/** Registers an observer to receive item added/removed events. /**
* Registers an observer to receive item added/removed events.
* *
* @note The inventory keeps a '''weak''' reference to the `observer`. * @note The inventory keeps a '''weak''' reference to the `observer`.
*/ */
@ -148,8 +183,9 @@ trait Inventory extends EventAware {
} }
final object Slot { final object Slot {
/**
/** Creates a proxy to an inventory slot. */ * Creates a proxy to an inventory slot.
*/
def apply(index: Int) = new Slot(index) def apply(index: Int) = new Slot(index)
} }
@ -206,13 +242,15 @@ trait Inventory extends EventAware {
object Inventory { object Inventory {
trait SlotObserver { trait SlotObserver {
/** Called after an item was inserted into this slot. /**
* Called after an item was inserted into this slot.
* *
* @note [[Inventory.onItemAdded]] is called before this method. * @note [[Inventory.onItemAdded]] is called before this method.
*/ */
def onItemAdded(): Unit def onItemAdded(): Unit
/** Called after an item was removed from this slot. /**
* Called after an item was removed from this slot.
* *
* In particular, the slot no longer contains the removed item. * In particular, the slot no longer contains the removed item.
* *
@ -220,7 +258,9 @@ object Inventory {
*/ */
def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit def onItemRemoved(removedItem: Item, replacedBy: Option[Item]): Unit
/** Called when an item contained in this slot sends a notification via [[Item.notifySlot]]. */ /**
* Called when an item contained in this slot sends a notification via [[Item.notifySlot]].
*/
def onItemNotification(notification: Item.Notification): Unit def onItemNotification(notification: Item.Notification): Unit
} }
} }

View File

@ -20,80 +20,7 @@ object Items extends Logging {
// this is just to force load the class during initialization // this is just to force load the class during initialization
def init(): Unit = {} def init(): Unit = {}
/** Registers a recoverer for [[ItemRecoverer.sourceClass]]. */ logger.debug("Initialize item serialization...")
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: IconSource, 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: IconSource, factories: Seq[(String, ItemFactory)]) extends ItemGroup
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
registerTiered("CPU", Tier.One to Tier.Three)(new CpuItem.Factory(_)) registerTiered("CPU", Tier.One to Tier.Three)(new CpuItem.Factory(_))
registerTiered("APU", Tier.Two to Tier.Creative)(tier => new ApuItem.Factory(tier.saturatingSub(1))) registerTiered("APU", Tier.Two to Tier.Creative)(tier => new ApuItem.Factory(tier.saturatingSub(1)))
@ -166,4 +93,81 @@ object Items extends Logging {
.map(new TapeItem.Factory(_)) .map(new TapeItem.Factory(_))
.map(factory => (f"${factory.name}%s (${Tape.lengthMinutes(factory.kind)}%.0f min)", factory)), .map(factory => (f"${factory.name}%s (${Tape.lengthMinutes(factory.kind)}%.0f min)", factory)),
) )
logger.debug("Item serialization initialization finished.")
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** Registers a recoverer for [[ItemRecoverer.sourceClass]]. */
private def registerRecoverer(recoverer: ItemRecoverer[_, _]): Unit = {
if (!_recoverers.contains(recoverer.sourceClass)) {
_recoverers(recoverer.sourceClass) = recoverer
logger.debug(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: IconSource, 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: IconSource, factories: Seq[(String, ItemFactory)]) extends ItemGroup
} }

View File

@ -104,7 +104,7 @@ trait PersistedInventory extends Inventory with Logging with Persistable {
} }
protected def onSlotLoadFailed(slotIndex: Int): Unit = { protected def onSlotLoadFailed(slotIndex: Int): Unit = {
Slot(slotIndex).remove() Slot(slotIndex).removeAndDispose()
} }
protected def saveEntityItem(slotNbt: NBTTagCompound, item: EntityItem): Unit = { protected def saveEntityItem(slotNbt: NBTTagCompound, item: EntityItem): Unit = {

View File

@ -178,7 +178,7 @@ trait SyncedInventory extends PersistedInventory with EventAware with Logging {
) )
logger.error("Breaking the loop forcefully by removing the items.") logger.error("Breaking the loop forcefully by removing the items.")
Slot(slotIndex).remove() Slot(slotIndex).removeAndDispose()
brainInventory.inventory(slotIndex).remove() brainInventory.inventory(slotIndex).remove()
} else { } else {
direction match { direction match {

View File

@ -45,8 +45,8 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis
} }
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(new ContextMenuSubmenu("External data source", Some(ContextMenuIcon(IconSource.Code))) { menu.addEntry(new ContextMenuSubmenu("External data source", Some(ContextMenuIcon(IconSource.Icons.Code))) {
addEntry(ContextMenuEntry("Local file", IconSource.File) { addEntry(ContextMenuEntry("Local file", IconSource.Icons.File) {
OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY) { file => OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.FILES_ONLY) { file =>
Try { Try {
for (file <- file) { for (file <- file) {
@ -56,7 +56,7 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis
} }
}) })
addEntry(ContextMenuEntry("File via URL", IconSource.Link) { addEntry(ContextMenuEntry("File via URL", IconSource.Icons.Link) {
new InputDialog( new InputDialog(
title = "File via URL", title = "File via URL",
onConfirmed = { text => onConfirmed = { text =>
@ -74,7 +74,7 @@ class EepromItem(val eeprom: EEPROM) extends Item with ComponentItem with Persis
}) })
if (eeprom.codePath.nonEmpty || eeprom.codeURL.nonEmpty) { if (eeprom.codePath.nonEmpty || eeprom.codeURL.nonEmpty) {
addEntry(ContextMenuEntry("Detach", IconSource.LinkSlash) { addEntry(ContextMenuEntry("Detach", IconSource.Icons.LinkSlash) {
eeprom.codeBytes = Some(Array.empty) eeprom.codeBytes = Some(Array.empty)
}) })
} }

View File

@ -3,37 +3,44 @@ package ocelot.desktop.inventory.item
import ocelot.desktop.graphics.IconSource import ocelot.desktop.graphics.IconSource
import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem} import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem}
import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
import ocelot.desktop.ui.widget.DiskEditWindow
import ocelot.desktop.ui.widget.tooltip.ItemTooltip import ocelot.desktop.ui.widget.tooltip.ItemTooltip
import totoro.ocelot.brain.entity.fs.ReadWriteLabel import totoro.ocelot.brain.entity.fs.ReadWriteLabel
import totoro.ocelot.brain.entity.traits.{Disk, Entity, Floppy} import totoro.ocelot.brain.entity.traits.{Disk, Entity, Floppy}
import totoro.ocelot.brain.entity.{FloppyManaged, FloppyUnmanaged} import totoro.ocelot.brain.entity.{FloppyManaged, FloppyUnmanaged}
import totoro.ocelot.brain.loot.Loot.{FloppyFactory => LootFloppyFactory, LootFloppy} import totoro.ocelot.brain.loot.Loot.{LootFloppy, FloppyFactory => LootFloppyFactory}
import totoro.ocelot.brain.util.DyeColor import totoro.ocelot.brain.util.DyeColor
import totoro.ocelot.brain.util.Tier.Tier import totoro.ocelot.brain.util.Tier.Tier
class FloppyItem(var floppy: Floppy) extends Item with ComponentItem with PersistableItem with DiskItem { class FloppyItem(var floppy: Floppy)
extends Item
with ComponentItem
with PersistableItem
with DiskItem
with DiskEditWindow.HasColor {
override def entity: Entity with Disk = floppy override def entity: Entity with Disk = floppy
override def diskKind: String = "Floppy" override def diskKind: String = "Floppy"
override def name: String = floppy.name.getOrElse(super.name) override def name: String = floppy.name.getOrElse(super.name)
override def label: Option[String] = floppy.label.labelOption override def diskLabel: Option[String] = floppy.label.labelOption
override def isLabelWriteable: Boolean = floppy.label.isInstanceOf[ReadWriteLabel] override def diskLabel_=(label: Option[String]): Unit = {
override def setLabel(label: Option[String]): Unit = {
floppy.label.setLabel(label.orNull) floppy.label.setLabel(label.orNull)
} }
override def color: Some[DyeColor] = Some(floppy.color) override def isLabelWriteable: Boolean = floppy.label.isInstanceOf[ReadWriteLabel]
override def setColor(color: DyeColor): Unit = { override def color: DyeColor = floppy.color
override def color_=(color: DyeColor): Unit = {
floppy.color = color floppy.color = color
} }
override def setManaged(managed: Boolean): Unit = { override def setManaged(managed: Boolean): Unit = {
val label = this.label val label = this.diskLabel
reinserting { reinserting {
floppy = floppy =
@ -41,13 +48,13 @@ class FloppyItem(var floppy: Floppy) extends Item with ComponentItem with Persis
else new FloppyUnmanaged(floppy.name, floppy.color) else new FloppyUnmanaged(floppy.name, floppy.color)
} }
setLabel(label) diskLabel = label
} }
override def fillTooltip(tooltip: ItemTooltip): Unit = { override def fillTooltip(tooltip: ItemTooltip): Unit = {
super.fillTooltip(tooltip) super.fillTooltip(tooltip)
for (label <- label) { for (label <- diskLabel) {
if (label != name) { if (label != name) {
// this is true for loot floppies // this is true for loot floppies
addDiskLabelTooltip(tooltip, label) addDiskLabelTooltip(tooltip, label)

View File

@ -6,7 +6,7 @@ import ocelot.desktop.inventory.traits.{ComponentItem, DiskItem, PersistableItem
import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
import ocelot.desktop.ui.widget.tooltip.ItemTooltip import ocelot.desktop.ui.widget.tooltip.ItemTooltip
import totoro.ocelot.brain.entity.fs.{Label, ReadWriteLabel} import totoro.ocelot.brain.entity.fs.{Label, ReadWriteLabel}
import totoro.ocelot.brain.entity.traits.{Disk, Entity} import totoro.ocelot.brain.entity.traits.{Disk, Entity, Environment}
import totoro.ocelot.brain.entity.{HDDManaged, HDDUnmanaged} import totoro.ocelot.brain.entity.{HDDManaged, HDDUnmanaged}
import totoro.ocelot.brain.util.Tier.Tier import totoro.ocelot.brain.util.Tier.Tier
@ -29,14 +29,14 @@ class HddItem(var hdd: Hdd) extends Item with ComponentItem with PersistableItem
case Hdd.Unmanaged(hdd) => hdd.label case Hdd.Unmanaged(hdd) => hdd.label
} }
def label: Option[String] = fsLabel.labelOption def diskLabel: Option[String] = fsLabel.labelOption
override def diskLabel_=(label: Option[String]): Unit = fsLabel.setLabel(label.orNull)
override def isLabelWriteable: Boolean = fsLabel.isInstanceOf[ReadWriteLabel] override def isLabelWriteable: Boolean = fsLabel.isInstanceOf[ReadWriteLabel]
override def setLabel(label: Option[String]): Unit = fsLabel.setLabel(label.orNull)
override def setManaged(managed: Boolean): Unit = { override def setManaged(managed: Boolean): Unit = {
val label = this.label val label = this.diskLabel
reinserting { reinserting {
hdd = hdd =
@ -44,13 +44,13 @@ class HddItem(var hdd: Hdd) extends Item with ComponentItem with PersistableItem
else Hdd(new HDDUnmanaged(tier.get)) else Hdd(new HDDUnmanaged(tier.get))
} }
setLabel(label) diskLabel = label
} }
override def fillTooltip(tooltip: ItemTooltip): Unit = { override def fillTooltip(tooltip: ItemTooltip): Unit = {
super.fillTooltip(tooltip) super.fillTooltip(tooltip)
label.foreach(addDiskLabelTooltip(tooltip, _)) diskLabel.foreach(addDiskLabelTooltip(tooltip, _))
hdd match { hdd match {
case Hdd.Managed(hdd) => addSourcePathTooltip(tooltip, hdd) case Hdd.Managed(hdd) => addSourcePathTooltip(tooltip, hdd)

View File

@ -22,7 +22,7 @@ class LinkedCardItem(val linkedCard: LinkedCard) extends Item with ComponentItem
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry( menu.addEntry(
ContextMenuEntry("Set channel", IconSource.Antenna) { ContextMenuEntry("Set channel", IconSource.Icons.Antenna) {
new TunnelDialog( new TunnelDialog(
tunnel => linkedCard.tunnel = tunnel, tunnel => linkedCard.tunnel = tunnel,
linkedCard.tunnel, linkedCard.tunnel,

View File

@ -23,7 +23,7 @@ class OcelotCardItem(val ocelotCard: OcelotCard)
override def tooltipNameColor: Color = ColorScheme("OcelotCardTooltip") override def tooltipNameColor: Color = ColorScheme("OcelotCardTooltip")
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(ContextMenuEntry("Open console", IconSource.Window) { menu.addEntry(ContextMenuEntry("Open console", IconSource.Icons.Window) {
window.open() window.open()
}) })

View File

@ -13,7 +13,7 @@ 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 totoro.ocelot.brain.util.Tier.Tier
abstract class RedstoneCardItem extends Item with ComponentItem with PersistableItem with CardItem {} abstract class RedstoneCardItem extends Item with ComponentItem with PersistableItem with CardItem
object RedstoneCardItem { object RedstoneCardItem {
abstract class Factory extends ItemFactory { abstract class Factory extends ItemFactory {
@ -43,7 +43,7 @@ object RedstoneCardItem {
} }
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(ContextMenuEntry("Redstone I/O", IconSource.ArrowRight) { menu.addEntry(ContextMenuEntry("Redstone I/O", IconSource.Icons.ArrowRight) {
windowed.window.open() windowed.window.open()
}) })
@ -63,6 +63,12 @@ object RedstoneCardItem {
windowed.save(nbt) windowed.save(nbt)
} }
override def dispose(): Unit = {
windowed.closeAndDisposeWindow()
super.dispose()
}
} }
object Tier1 { object Tier1 {
@ -93,7 +99,7 @@ object RedstoneCardItem {
} }
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(ContextMenuEntry("Bundled I/O", IconSource.LinesHorizontal) { menu.addEntry(ContextMenuEntry("Bundled I/O", IconSource.Icons.LinesHorizontal) {
windowed.window.open() windowed.window.open()
}) })
@ -111,6 +117,12 @@ object RedstoneCardItem {
windowed.save(nbt) windowed.save(nbt)
} }
override def dispose(): Unit = {
windowed.closeAndDisposeWindow()
super.dispose()
}
} }
object Tier2 { object Tier2 {

View File

@ -22,7 +22,7 @@ class SelfDestructingCardItem(val card: SelfDestructingCard)
tooltip.addLine( tooltip.addLine(
if (card.remainingTime < 0) "Fuse has not been set" if (card.remainingTime < 0) "Fuse has not been set"
else if (card.remainingTime == 0) "BOOM!" else if (card.remainingTime == 0) "BOOM!"
else card.remainingTime.toString else f"Time to explosion: ${card.remainingTime / 20f}%.2fs"
) )
} }
} }

View File

@ -1,25 +1,52 @@
package ocelot.desktop.inventory.item package ocelot.desktop.inventory.item
import ocelot.desktop.audio.{Audio, SoundCategory, SoundSamples, SoundSource, SoundStream}
import ocelot.desktop.graphics.IconSource import ocelot.desktop.graphics.IconSource
import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem} import ocelot.desktop.inventory.traits.{CardItem, ComponentItem, PersistableItem}
import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer} import ocelot.desktop.inventory.{Item, ItemFactory, ItemRecoverer}
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.widget.card.SoundCardWindow import ocelot.desktop.ui.widget.card.SoundCardWindow
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.window.Windowed import ocelot.desktop.ui.widget.window.Windowed
import ocelot.desktop.util.Lazy
import totoro.ocelot.brain.Settings
import totoro.ocelot.brain.entity.sound_card.SoundCard import totoro.ocelot.brain.entity.sound_card.SoundCard
import totoro.ocelot.brain.entity.traits.{Entity, Environment} import totoro.ocelot.brain.event.SoundCardAudioEvent
import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier
import totoro.ocelot.brain.util.Tier.Tier import totoro.ocelot.brain.util.Tier.Tier
class SoundCardItem(val soundCard: SoundCard) class SoundCardItem(val soundCard: SoundCard)
extends Item with ComponentItem with PersistableItem with CardItem with Windowed[SoundCardWindow] { extends Item
with ComponentItem
with PersistableItem
with CardItem
with Windowed[SoundCardWindow] {
override def createWindow(): SoundCardWindow = new SoundCardWindow(soundCard) override def createWindow(): SoundCardWindow = new SoundCardWindow(soundCard)
override def entity: Entity with Environment = soundCard override def entity: SoundCard = soundCard
private val streamPair = Lazy(Audio.newStream(SoundCategory.Records))
private def stream: SoundStream = streamPair.getSync._1
private def source: SoundSource = streamPair.getSync._2
eventHandlers += {
case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled =>
val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8)
stream.enqueue(samples)
source.volume = event.volume
}
override def dispose(): Unit = {
super.dispose()
for (stream <- streamPair.getOption) {
stream._2.stop()
}
}
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Window) { menu.addEntry(ContextMenuEntry("Open card interface", IconSource.Icons.Window) {
window.open() window.open()
}) })

View File

@ -22,7 +22,7 @@ trait ComponentItem extends EntityItem {
} }
} }
private val copyAddressEntry = ContextMenuEntry("Copy address", IconSource.Copy) { private val copyAddressEntry = ContextMenuEntry("Copy address", IconSource.Icons.Copy) {
UiHandler.clipboard = entity.node.address UiHandler.clipboard = entity.node.address
} }

View File

@ -13,7 +13,7 @@ trait CpuLikeItem extends ComponentItem {
override def entity: Entity with GenericCPU override def entity: Entity with GenericCPU
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
menu.addEntry(new ContextMenuSubmenu("Set architecture", Some(ContextMenuIcon(IconSource.Microchip))) { menu.addEntry(new ContextMenuSubmenu("Set architecture", Some(ContextMenuIcon(IconSource.Icons.Microchip))) {
for (arch <- entity.allArchitectures) { for (arch <- entity.allArchitectures) {
val name = MachineAPI.getArchitectureName(arch) + val name = MachineAPI.getArchitectureName(arch) +
(if (arch == entity.architecture) " (current)" else "") (if (arch == entity.architecture) " (current)" else "")

View File

@ -4,40 +4,38 @@ import ocelot.desktop.OcelotDesktop
import ocelot.desktop.graphics.IconSource import ocelot.desktop.graphics.IconSource
import ocelot.desktop.node.nodes.RaidNode import ocelot.desktop.node.nodes.RaidNode
import ocelot.desktop.ui.widget.DiskEditWindow import ocelot.desktop.ui.widget.DiskEditWindow
import ocelot.desktop.ui.widget.DiskEditWindow.{CanChangeManaged, EditableDisk, Lockable}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.tooltip.ItemTooltip import ocelot.desktop.ui.widget.tooltip.ItemTooltip
import ocelot.desktop.ui.widget.window.{Window, Windowed} import ocelot.desktop.ui.widget.window.{Window, Windowed}
import totoro.ocelot.brain.entity.traits.{Disk, DiskManaged, DiskRealPathAware, DiskUnmanaged, Entity} import totoro.ocelot.brain.entity.traits.{Disk, DiskManaged, DiskRealPathAware, DiskUnmanaged, Entity}
import totoro.ocelot.brain.util.DyeColor
import javax.swing.JFileChooser import javax.swing.JFileChooser
import scala.util.Try import scala.util.Try
/** A utility mixin for HDDs and floppies. */ /**
trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] { * A utility mixin for HDDs and floppies.
*/
trait DiskItem
extends ComponentItem
with EditableDisk
with Lockable
with CanChangeManaged
with Windowed[DiskEditWindow] {
override def entity: Entity with Disk override def entity: Entity with Disk
def diskKind: String override def disk: Option[Entity with Disk] = Option(entity)
def color: Option[DyeColor] = None override def capacity: Long = disk.fold(0L)(_.capacity)
def setColor(color: DyeColor): Unit = throw new UnsupportedOperationException() override def lock(): Unit = reinserting {
def label: Option[String]
def isLabelWriteable: Boolean
def setLabel(label: Option[String]): Unit
def setManaged(managed: Boolean): Unit
def lock(): Unit = reinserting {
entity.setLocked(OcelotDesktop.player.nickname) entity.setLocked(OcelotDesktop.player.nickname)
} }
override def createWindow(): DiskEditWindow = new DiskEditWindow(DiskItem.this) override def createWindow(): DiskEditWindow = new DiskEditWindow(this)
def isEditingAllowed: Boolean = { def editingAllowed: Boolean = {
slot.fold(false)(_.inventory match { slot.fold(false)(_.inventory match {
case _: RaidNode => false case _: RaidNode => false
case _ => true case _ => true
@ -45,16 +43,19 @@ trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] {
} }
override def fillRmbMenu(menu: ContextMenu): Unit = { override def fillRmbMenu(menu: ContextMenu): Unit = {
if (isEditingAllowed) { if (editingAllowed) {
// Real path // Real path
entity match { entity match {
case diskManaged: DiskManaged => case diskManaged: DiskManaged =>
DiskItem.addRealPathContextMenuEntries(menu, diskManaged, DiskItem.addRealPathContextMenuEntries(
menu,
diskManaged,
realPathSetter => { realPathSetter => {
reinserting { reinserting {
realPathSetter() realPathSetter()
} }
}) },
)
case _ => case _ =>
} }
@ -86,18 +87,22 @@ trait DiskItem extends ComponentItem with Windowed[DiskEditWindow] {
override def fillTooltip(tooltip: ItemTooltip): Unit = { override def fillTooltip(tooltip: ItemTooltip): Unit = {
super.fillTooltip(tooltip) super.fillTooltip(tooltip)
if (entity != null) { if (entity != null) {
tooltip.addLine(s"Capacity: ${entity.capacity / 1024} kB") tooltip.addLine(s"Capacity: ${capacity / 1024} kB")
} }
} }
} }
object DiskItem { object DiskItem {
def addRealPathContextMenuEntries(menu: ContextMenu, diskRealPathAware: DiskRealPathAware, def addRealPathContextMenuEntries(
realPathSetter: (() => Unit) => Unit): Unit = { menu: ContextMenu,
diskRealPathAware: DiskRealPathAware,
realPathSetter: (() => Unit) => Unit,
): Unit = {
menu.addEntry(ContextMenuEntry( menu.addEntry(ContextMenuEntry(
if (diskRealPathAware.customRealPath.isDefined) "Change directory" else "Set directory", if (diskRealPathAware.customRealPath.isDefined) "Change directory" else "Set directory",
IconSource.Folder, IconSource.Icons.Folder,
) { ) {
OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) { dir => OcelotDesktop.showFileChooserDialog(JFileChooser.OPEN_DIALOG, JFileChooser.DIRECTORIES_ONLY) { dir =>
Try { Try {
@ -112,7 +117,7 @@ object DiskItem {
}) })
if (diskRealPathAware.customRealPath.isDefined) { if (diskRealPathAware.customRealPath.isDefined) {
menu.addEntry(ContextMenuEntry("Reset directory", IconSource.FolderSlash) { menu.addEntry(ContextMenuEntry("Reset directory", IconSource.Icons.FolderSlash) {
realPathSetter(() => { realPathSetter(() => {
// trigger component_removed / component_added signals // trigger component_removed / component_added signals
diskRealPathAware.customRealPath = None diskRealPathAware.customRealPath = None
@ -122,7 +127,7 @@ object DiskItem {
} }
def addEditDiskContextMenuEntries[T <: Window](menu: ContextMenu, windowed: Windowed[T]): Unit = { def addEditDiskContextMenuEntries[T <: Window](menu: ContextMenu, windowed: Windowed[T]): Unit = {
menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Edit) { menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Icons.Edit) {
windowed.window.open() windowed.window.open()
}) })
} }

View File

@ -0,0 +1,102 @@
package ocelot.desktop.node
import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource}
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.inventory.item.SelfDestructingCardItem
import ocelot.desktop.node.BoomCardFxHandler.{ExpandIntensity, ExpandPeriod, FlickerAlpha, FlickerDuty, GlowAlpha, MaxSize, MinSize}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import totoro.ocelot.brain.event.SelfDestructingCardBoomEvent
trait BoomCardFxHandler extends Node with PositionalSoundSourcesNode with SmokeParticleNode {
private var boomPhase: Float = -1
private lazy val explosionSound = {
SoundSource.fromBuffer(SoundBuffers.MinecraftExplosion, SoundCategory.Environment)
}
private lazy val countdownBeepSound = {
SoundSource.fromBuffer(SoundBuffers.SelfDestructingCardCountdownBeep, SoundCategory.Environment)
}
override def soundSources: Seq[SoundSource] = super.soundSources ++ Seq(
explosionSound,
countdownBeepSound,
)
eventHandlers += {
case BrainEvent(_: SelfDestructingCardBoomEvent) =>
OcelotDesktop.updateThreadTasks.add(() => {
explosionSound.play()
emitSmoke()
destroy()
})
}
protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem]
private var phase = 0f
private var flickerPhase = 0f
private def updateBoomCardState(): Unit = {
phase = (phase + UiHandler.dt / ExpandPeriod) % 1f
flickerPhase = 0f.max(flickerPhase - UiHandler.dt)
boomPhase = -1
for (item <- selfDestructingCards) {
if (item.card.time > 0) {
// If multiple SDCs are ticking, let the most soon exploding one to define the glow
boomPhase = boomPhase.max(1 - item.card.time.toFloat / item.card.initialTime)
if (item.card.lastBeepTime < 0 || item.card.lastBeepTime - item.card.time >= 20) {
countdownBeepSound.play()
item.card.lastBeepTime = item.card.time
flickerPhase = FlickerDuty
}
}
}
}
override def update(): Unit = {
super.update()
updateBoomCardState()
}
private def expandFactor(phase: Float): Float = {
math.sin(2 * math.Pi * phase).toFloat
}
override def drawLight(g: Graphics): Unit = {
super.drawLight(g)
if (boomPhase > 0) {
val expand = expandFactor(phase)
val glowSize = MinSize.lerp(MaxSize, boomPhase + ExpandIntensity * expand)
val alpha = boomPhase * GlowAlpha
if (flickerPhase >= 0.01) {
g.rect(bounds, Color.White.withAlpha(FlickerAlpha * (1 - boomPhase)))
}
g.sprite(
IconSource.Nodes.LampGlow,
position - size * glowSize,
size * (1 + 2 * glowSize),
ColorScheme("BoomCardGlowStart").lerp(ColorScheme("BoomCardGlowEnd"), boomPhase).withAlpha(alpha),
)
}
}
}
object BoomCardFxHandler {
private val ExpandPeriod = 1f
private val ExpandIntensity = 0.05f
private val FlickerDuty = 0.33f
private val FlickerAlpha = 0.05f
private val MinSize = 0.1f
private val MaxSize = 0.5f
private val GlowAlpha = 0.4f
}

View File

@ -1,9 +1,6 @@
package ocelot.desktop.node package ocelot.desktop.node
import ocelot.desktop.OcelotDesktop
import ocelot.desktop.{Settings => DesktopSettings}
import ocelot.desktop.audio._ import ocelot.desktop.audio._
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.inventory.SyncedInventory import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.node.ComputerAwareNode._ import ocelot.desktop.node.ComputerAwareNode._
@ -11,85 +8,39 @@ import ocelot.desktop.node.Node.Size
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.ComputerErrorMessageLabel import ocelot.desktop.ui.particle.Particle
import ocelot.desktop.util.Messages import ocelot.desktop.util.Messages
import totoro.ocelot.brain.Settings import ocelot.desktop.{ColorScheme, Settings => DesktopSettings}
import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware} import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware}
import totoro.ocelot.brain.event._ import totoro.ocelot.brain.event._
import java.util.Calendar import java.util.Calendar
import scala.collection.mutable.ArrayBuffer
abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware) abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware)
extends EntityNode(entity) extends EntityNode(entity)
with SyncedInventory with SyncedInventory
with DiskActivityHandler with DiskActivityHandler
with OcelotLogParticleNode with OcelotLogParticleNode
with BoomCardFxHandler
with ShiftClickNode { with ShiftClickNode {
// access should be synchronized because messages are added in the update thread
private val messages = ArrayBuffer.empty[(Float, ComputerErrorMessageLabel)]
private def addErrorMessage(message: ComputerErrorMessageLabel): Unit = messages.synchronized {
messages += ((0f, message))
}
private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records)
private def soundCardStream: SoundStream = soundCardSounds._1
private def soundCardSource: SoundSource = soundCardSounds._2
eventHandlers += { eventHandlers += {
case BrainEvent(event: MachineCrashEvent) => case BrainEvent(event: MachineCrashEvent) =>
val message = Messages.lift(event.message) match { val message = Messages.lift(event.message) match {
case Some(message) => case Some(message) =>
logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message") logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message")
message message
case None => case None =>
logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}") logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}")
event.message event.message
} }
UiHandler.root.workspaceView.particleSystem.add(new ErrorMessageParticle(message))
addErrorMessage(new ComputerErrorMessageLabel(this, 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()
case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled => case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled =>
BeepGenerator.newBeep(event.pattern, 1000, 200).play() BeepGenerator.newBeep(event.pattern, 1000, 200).play()
case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled =>
val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8)
soundCardStream.enqueue(samples)
soundCardSource.volume = event.volume
case BrainEvent(_: SelfDestructingCardBoomEvent) =>
OcelotDesktop.updateThreadTasks.add(() => {
SoundSource.MinecraftExplosion.play()
destroy()
})
}
override def update(): Unit = {
messages.synchronized {
messages.mapInPlace { case (t, message) => (t + ErrorMessageMoveSpeed * UiHandler.dt, message) }
messages.filterInPlace(_._1 <= 1f)
}
super.update()
}
private def drawMessageParticles(g: Graphics): Unit = messages.synchronized {
for ((time, message) <- messages.reverseIterator) {
message.position = message.initialPosition + Vector2D(0, -MaxErrorMessageDistance * time)
message.alpha = 1 - time
message.draw(g)
}
}
override def drawParticles(g: Graphics): Unit = {
super.drawParticles(g)
drawMessageParticles(g)
} }
protected def drawOverlay(g: Graphics): Unit = HolidayIcon match { protected def drawOverlay(g: Graphics): Unit = HolidayIcon match {
@ -110,9 +61,19 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
super.draw(g) super.draw(g)
drawOverlay(g) drawOverlay(g)
} }
private class ErrorMessageParticle(message: String) extends Particle(speed = ErrorMessageMoveSpeed) {
private val offsetX = size.width / 2 - message.length * 4
private val offsetY = -8
override def draw(g: Graphics): Unit = {
g.setSmallFont()
g.foreground = ColorScheme("ErrorMessage").withAlpha(1 - (2 * time - 1).min(1).max(0))
g.text(position.x + offsetX, position.y + offsetY - MaxErrorMessageDistance * time, message)
g.setNormalFont()
}
}
} }
object ComputerAwareNode { object ComputerAwareNode {

View File

@ -35,7 +35,7 @@ abstract class EntityNode(val entity: Entity with Environment) extends Node {
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
if (exposeAddress && entity.node != null && entity.node.address != null) { if (exposeAddress && entity.node != null && entity.node.address != null) {
menu.addEntry( menu.addEntry(
ContextMenuEntry("Copy address", IconSource.Copy) { ContextMenuEntry("Copy address", IconSource.Icons.Copy) {
UiHandler.clipboard = entity.node.address UiHandler.clipboard = entity.node.address
} }
) )

View File

@ -26,7 +26,7 @@ trait LabeledNode extends Node {
} }
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
menu.addEntry(ContextMenuEntry("Set label", IconSource.Label) { menu.addEntry(ContextMenuEntry("Set label", IconSource.Icons.Label) {
new InputDialog( new InputDialog(
"Set label", "Set label",
text => { text => {
@ -40,6 +40,8 @@ trait LabeledNode extends Node {
} }
override def drawLabel(g: Graphics): Unit = { override def drawLabel(g: Graphics): Unit = {
super.drawLabel(g)
for (label <- label) { for (label <- label) {
g.setSmallFont() g.setSmallFont()
g.background = RGBAColor(0, 0, 0, 0) g.background = RGBAColor(0, 0, 0, 0)

View File

@ -14,6 +14,7 @@ import ocelot.desktop.util.Persistable
import ocelot.desktop.util.animation.ColorAnimation import ocelot.desktop.util.animation.ColorAnimation
import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.network import totoro.ocelot.brain.network
import totoro.ocelot.brain.util.Direction.Direction
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
@ -89,12 +90,12 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
if (ports.nonEmpty) { if (ports.nonEmpty) {
menu.addEntry(ContextMenuEntry("Disconnect", IconSource.LinkSlash, SoundSource.InterfaceClickLow) { menu.addEntry(ContextMenuEntry("Disconnect", IconSource.Icons.LinkSlash, SoundSource.InterfaceClickLow) {
disconnectFromAll() disconnectFromAll()
}) })
} }
menu.addEntry(ContextMenuEntry("Remove", IconSource.Delete, SoundSource.InterfaceClickLow) { menu.addEntry(ContextMenuEntry("Remove", IconSource.Icons.Delete, SoundSource.InterfaceClickLow) {
destroy() destroy()
}) })
} }
@ -103,11 +104,11 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
super.update() super.update()
if (isHovered || isMoving) { if (isHovered || isMoving) {
root.get.statusBar.addMouseEntry("icons/RMB", "Menu") root.get.statusBar.addMouseEntry(IconSource.Icons.RMB, "Menu")
root.get.statusBar.addMouseEntry("icons/DragLMB", "Move node") root.get.statusBar.addMouseEntry(IconSource.Icons.DragLMB, "Move node")
if (ports.nonEmpty) { if (ports.nonEmpty) {
root.get.statusBar.addMouseEntry("icons/DragRMB", "Connect/Disconnect") root.get.statusBar.addMouseEntry(IconSource.Icons.DragRMB, "Connect/Disconnect")
} }
} }
} }
@ -118,7 +119,7 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
super.dispose() super.dispose()
} }
def iconSource: IconSource = IconSource.NA def icon: IconSource = IconSource.Icons.NA
def iconColor: Color = RGBAColor(255, 255, 255) def iconColor: Color = RGBAColor(255, 255, 255)
@ -126,6 +127,8 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
def getNodeByPort(port: NodePort): network.Node = throw new IllegalArgumentException("this node has no ports") def getNodeByPort(port: NodePort): network.Node = throw new IllegalArgumentException("this node has no ports")
def rotatable: Boolean
def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator
def connect(portA: NodePort, node: Node, portB: NodePort): Unit = { def connect(portA: NodePort, node: Node, portB: NodePort): Unit = {
@ -260,39 +263,64 @@ abstract class Node extends Widget with MouseHandler with HoverHandler with Pers
drawHighlight(g) drawHighlight(g)
g.sprite( g.sprite(
iconSource.path, icon,
position.x + HighlightThickness, position.x + HighlightThickness,
position.y + HighlightThickness, position.y + HighlightThickness,
size.width - HighlightThickness * 2, size.width - HighlightThickness * 2,
size.height - HighlightThickness * 2, size.height - HighlightThickness * 2,
iconColor, iconColor,
iconSource.animation,
) )
} }
private def drawPortLegend(g: Graphics): Unit = {
if (highlight.color.a < 0.001) return
g.setSmallFont()
g.background = PortLegendBg.mapA(_ * highlight.color.a)
val sidedPorts = ports.iterator.filter(_.direction.isDefined).toSeq.sorted
val legendHeight = sidedPorts.size * 8
val startY = position.y + (height - legendHeight) / 2
for ((port, line) <- sidedPorts.iterator.zipWithIndex) {
g.foreground = port.getColor.toRGBANorm.mapA(_ * highlight.color.a)
g.text(bounds.max.x + HoverInfoMargin, startY + line * 8, directionLabel(port.direction.get), shrink = 1)
}
g.setNormalFont()
}
def drawLight(g: Graphics): Unit = {} def drawLight(g: Graphics): Unit = {}
def drawLabel(g: Graphics): Unit = {} def drawLabel(g: Graphics): Unit = {
drawPortLegend(g)
def drawParticles(g: Graphics): Unit = {} }
def drawPorts(g: Graphics): Unit = { def drawPorts(g: Graphics): Unit = {
for ((port, rects) <- portsBounds) { for ((port, rects) <- portsBounds) {
val color = port.getColor val color = port.getColor
for (rect <- rects) for (rect <- rects) {
g.rect(rect, color) g.rect(rect, color)
} }
} }
} }
def directionLabel(direction: Direction): String = {
(if (rotatable) direction.side else direction.cardinal).capitalize
}
}
object Node { object Node {
protected val MovingHighlight: RGBAColor = RGBAColor(240, 250, 240) protected val MovingHighlight: RGBAColor = RGBAColor(240, 250, 240)
protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160) protected val HoveredHighlight: RGBAColor = RGBAColor(160, 160, 160)
protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0) protected val NoHighlight: RGBAColor = RGBAColor(160, 160, 160, 0)
private val PortLegendBg = Color.Black.withAlpha(0.8f)
val TexelCount = 16f val TexelCount = 16f
val Scale = 4f val Scale = 4f
val HighlightThickness = 2f val HighlightThickness = 2f
private val HoverInfoMargin = 9f
val NoHighlightSize: Float = TexelCount * Scale val NoHighlightSize: Float = TexelCount * Scale
val Size: Float = NoHighlightSize + HighlightThickness * 2f val Size: Float = NoHighlightSize + HighlightThickness * 2f

View File

@ -2,6 +2,7 @@ package ocelot.desktop.node
import ocelot.desktop.ColorScheme import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color import ocelot.desktop.color.Color
import ocelot.desktop.graphics.IconSource
import totoro.ocelot.brain.util.Direction import totoro.ocelot.brain.util.Direction
case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[NodePort] { case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[NodePort] {
@ -15,6 +16,11 @@ case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[N
case _ => ColorScheme("PortAny") case _ => ColorScheme("PortAny")
} }
def getIcon: IconSource = direction match {
case Some(direction) => IconSource.Icons.Side(direction)
case None => IconSource.Icons.SideAny
}
override def compare(that: NodePort): Int = this.direction.compare(that.direction) override def compare(that: NodePort): Int = this.direction.compare(that.direction)
def toByte: Byte = direction match { def toByte: Byte = direction match {

View File

@ -2,7 +2,7 @@ package ocelot.desktop.node
import ocelot.desktop.color.Color 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, IconSource}
import ocelot.desktop.node.Node.Size import ocelot.desktop.node.Node.Size
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler} import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent} import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
@ -39,20 +39,19 @@ class NodeTypeWidget(val nodeType: NodeType) extends Widget with MouseHandler wi
val size = Spritesheet.spriteSize(nodeType.icon) * 4 val size = Spritesheet.spriteSize(nodeType.icon) * 4
g.sprite( g.sprite(
nodeType.icon.path, nodeType.icon,
position.x + Size / 2 - size.width / 2, position.x + Size / 2 - size.width / 2,
position.y + Size / 2 - size.height / 2, position.y + Size / 2 - size.height / 2,
size.width, size.width,
size.height, size.height,
nodeType.tier.map(TierColor.get).getOrElse(Color.White), nodeType.tier.map(TierColor.get).getOrElse(Color.White),
nodeType.icon.animation,
) )
} }
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
if (isHovered) { if (isHovered) {
root.get.statusBar.addMouseEntry("icons/LMB", "Add node") root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Add node")
} }
} }
} }

View File

@ -8,62 +8,53 @@ import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.OcelotLogParticleNode._ import ocelot.desktop.node.OcelotLogParticleNode._
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.particle.Particle
import scala.collection.mutable
import scala.util.Random import scala.util.Random
trait OcelotLogParticleNode extends Node { trait OcelotLogParticleNode extends Node {
private case class LogParticle( private var queuedParticles: Int = 0
var time: Float = -LogParticleGrow,
angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle),
)
// access should be synchronized because log particles are added in the update thread
private val logParticles = mutable.ArrayDeque.empty[LogParticle]
private def addLogParticle(): Unit = logParticles.synchronized {
if (logParticles.length < MaxLogParticles) {
logParticles += LogParticle()
}
}
eventHandlers += { eventHandlers += {
case BrainEvent(OcelotInterface.LogEvent.CardToUser(_, _)) => case BrainEvent(OcelotInterface.LogEvent.CardToUser(_, _)) =>
addLogParticle() queuedParticles += 1
} }
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
spawnParticles()
logParticles.synchronized {
logParticles.foreach(particle => particle.time += LogParticleMoveSpeed * UiHandler.dt)
logParticles.filterInPlace(_.time <= 1f)
}
} }
private def drawLogParticles(g: Graphics): Unit = logParticles.synchronized { private def spawnParticles(): Unit = {
for (particle <- logParticles) { val system = UiHandler.root.workspaceView.particleSystem
val size = (1 + particle.time / LogParticleGrow).clamp() * LogParticleSize val toSpawn = queuedParticles min (MaxLogParticles - system.count[LogParticle](Some(this))) max 0
val offset = particle.time.clamp() * LogParticleMoveDistance
val alpha = 1 - particle.time.clamp()
val r1 = (bounds.w max bounds.h) / math.sqrt(2) + offset + LogParticlePadding for (_ <- 0 until toSpawn) {
system.add(new LogParticle)
}
queuedParticles = 0
}
private class LogParticle extends Particle(time = -LogParticleGrow, speed = LogParticleMoveSpeed, origin = Some(this)) {
private val angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle)
override def draw(g: Graphics): Unit = {
val size = (1 + time / LogParticleGrow).clamped() * LogParticleSize
val offset = time.clamped() * LogParticleMoveDistance
val alpha = 1 - time.clamped()
val r1 = (bounds.w max bounds.h) / math.sqrt(2).toFloat + offset + LogParticlePadding
val r2 = r1 + size val r2 = r1 + size
for (i <- 0 until LogParticleCount) { for (i <- 0 until LogParticleCount) {
val angle = particle.angle + (2 * math.Pi).toFloat * i / LogParticleCount val a = angle + (2 * math.Pi).toFloat * i / LogParticleCount
val v = Vector2D.unit(angle) val v = Vector2D.unit(a)
val p1 = v * r1 + bounds.center val p1 = v * r1 + bounds.center
val p2 = v * r2 + bounds.center val p2 = v * r2 + bounds.center
g.line(p1, p2, 1f, ColorScheme("LogParticle").mapA(_ => alpha)) g.line(p1, p2, 1f, ColorScheme("LogParticle").mapA(_ => alpha))
} }
} }
} }
override def drawParticles(g: Graphics): Unit = {
super.drawParticles(g)
drawLogParticles(g)
}
} }
object OcelotLogParticleNode { object OcelotLogParticleNode {

View File

@ -4,18 +4,22 @@ import ocelot.desktop.audio.SoundSource
import ocelot.desktop.geometry.Vector3D import ocelot.desktop.geometry.Vector3D
import ocelot.desktop.{OcelotDesktop, Settings} import ocelot.desktop.{OcelotDesktop, Settings}
/**
* Updates sound sources' position depending on where the camera is.
*
* @note OpenAL only applies positioning to mono sources!
* If your source is stereo, this trait will have no audible effect.
*/
trait PositionalSoundSourcesNode extends Node { trait PositionalSoundSourcesNode extends Node {
// Every node can have multiple sound sources playing at the same time // Every node can have multiple sound sources playing at the same time
def soundSources: Seq[SoundSource] def soundSources: Seq[SoundSource] = Seq()
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
var soundPosition: Vector3D = null
// Calculating position of sound source relative to center of workspace // Calculating position of sound source relative to center of workspace
// but only if corresponding setting is enabled // but only if corresponding setting is enabled
if (Settings.get.soundPositional) { val soundPosition = if (Settings.get.soundPositional) {
val rootWidthHalf = OcelotDesktop.root.width / 2 val rootWidthHalf = OcelotDesktop.root.width / 2
val rootHeightHalf = OcelotDesktop.root.height / 2 val rootHeightHalf = OcelotDesktop.root.height / 2
@ -26,13 +30,13 @@ trait PositionalSoundSourcesNode extends Node {
// large monitors the sound may become too "non-audiophile" // large monitors the sound may become too "non-audiophile"
val limit = 0.05f val limit = 0.05f
soundPosition = Vector3D( Vector3D(
(nodeCenterX - rootWidthHalf) / rootWidthHalf * limit, (nodeCenterX - rootWidthHalf) / rootWidthHalf * limit,
(nodeCenterY - rootHeightHalf) / rootHeightHalf * limit, (nodeCenterY - rootHeightHalf) / rootHeightHalf * limit,
0, 0,
) )
} else { } else {
soundPosition = Vector3D.Zero Vector3D.Zero
} }
for (soundSource <- soundSources) for (soundSource <- soundSources)

View File

@ -1,5 +1,6 @@
package ocelot.desktop.node package ocelot.desktop.node
import ocelot.desktop.graphics.IconSource
import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.event.{ClickEvent, MouseEvent}
@ -22,6 +23,6 @@ trait ShiftClickNode extends Node {
super.update() super.update()
if (isHovered || isMoving) if (isHovered || isMoving)
root.get.statusBar.addKeyMouseEntry("icons/LMB", "SHIFT", hoveredShiftStatusBarText) root.get.statusBar.addKeyMouseEntry(IconSource.Icons.LMB, "SHIFT", hoveredShiftStatusBarText)
} }
} }

View File

@ -0,0 +1,79 @@
package ocelot.desktop.node
import ocelot.desktop.color.RGBAColorNorm
import ocelot.desktop.geometry.{Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.node.SmokeParticleNode._
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.particle.Particle
import ocelot.desktop.util.Spritesheet
import scala.util.Random
trait SmokeParticleNode extends Node {
protected def emitSmoke(): Unit = synchronized {
for (_ <- 1 to randomCount) {
UiHandler.root.workspaceView.particleSystem.add(new SmokeParticle)
}
}
private class SmokeParticle extends Particle(ttl = randomDuration) {
private val color: RGBAColorNorm = randomColor
private var velocity: Vector2D = Vector2D(randomVelocityComponent, randomVelocityComponent)
private var offset: Vector2D = Vector2D(0, 0)
override def update(dt: Float): Unit = {
time += dt
offset += (velocity + SmokeParticleVolatilizationSpeed) * dt * speed
velocity *= math.pow(SmokeParticleVelocityDamping, dt).toFloat
}
override def draw(g: Graphics): Unit = {
val spriteRect = Spritesheet.sprites(IconSource.Particles.Smoke.path)
val animationFrameCount = spriteRect.h / spriteRect.w
val animationFrame = (time / ttl * animationFrameCount).toInt
val particlePosition = bounds.center + offset - SmokeParticleSize.toVector * .5f
g.sprite(
IconSource.Particles.Smoke.path,
particlePosition.x,
particlePosition.y,
SmokeParticleSize.width,
SmokeParticleSize.height,
color,
Some(spriteRect.copy(
y = spriteRect.y + animationFrame * spriteRect.w,
h = spriteRect.w,
)),
)
}
}
}
private object SmokeParticleNode {
private final val SmokeParticleSize: Size2D = Size2D(32, 32)
private final val SmokeParticleVelocityRange: Float = 300
private final val SmokeParticleVolatilizationSpeed: Vector2D = Vector2D(0, -50)
private final val SmokeParticleVelocityDamping: Float = .1f
private final val SmokeParticleCount: (Int, Int) = (10, 20)
private final val SmokeParticleAnimationDuration: (Float, Float) = (1f, 4f)
private def randomVelocityComponent: Float = Random.between(
-SmokeParticleVelocityRange,
SmokeParticleVelocityRange,
)
private def randomColor: RGBAColorNorm = {
val channel = Random.between(.5f, .9f)
RGBAColorNorm(channel, channel, channel)
}
private def randomDuration: Float = Random.between(
SmokeParticleAnimationDuration._1,
SmokeParticleAnimationDuration._2,
)
private def randomCount: Int = Random.between(
SmokeParticleCount._1,
SmokeParticleCount._2,
)
}

View File

@ -1,9 +1,9 @@
package ocelot.desktop.node package ocelot.desktop.node
import ocelot.desktop.graphics.IconSource
import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.sources.KeyEvents
import ocelot.desktop.ui.event.{ClickEvent, MouseEvent} import ocelot.desktop.ui.event.{ClickEvent, MouseEvent}
import ocelot.desktop.ui.widget.window.{Window, Windowed} import ocelot.desktop.ui.widget.window.{Window, Windowed}
import org.lwjgl.input.Keyboard
trait WindowedNode[T <: Window] extends Node with Windowed[T] { trait WindowedNode[T <: Window] extends Node with Windowed[T] {
override def dispose(): Unit = { override def dispose(): Unit = {
@ -14,7 +14,7 @@ trait WindowedNode[T <: Window] extends Node with Windowed[T] {
override def update(): Unit = { override def update(): Unit = {
if (isHovered || isMoving) if (isHovered || isMoving)
root.get.statusBar.addMouseEntry("icons/LMB", if (windowCreated && window.isOpen) "Close" else "Open") root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, if (windowCreated && window.isOpen) "Close" else "Open")
super.update() super.update()
} }

View File

@ -6,7 +6,9 @@ import ocelot.desktop.node.EntityNode
import totoro.ocelot.brain.entity.Cable import totoro.ocelot.brain.entity.Cable
class CableNode(val cable: Cable) extends EntityNode(cable) { class CableNode(val cable: Cable) extends EntityNode(cable) {
override def iconSource: IconSource = IconSource.Nodes.Cable override def icon: IconSource = IconSource.Nodes.Cable
override def rotatable: Boolean = false
override def minimumSize: Size2D = Size2D(36, 36) override def minimumSize: Size2D = Size2D(36, 36)

View File

@ -6,7 +6,9 @@ import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode}
import ocelot.desktop.windows.CameraWindow import ocelot.desktop.windows.CameraWindow
class CameraNode(val camera: Camera) extends EntityNode(camera) with LabeledEntityNode with WindowedNode[CameraWindow] { class CameraNode(val camera: Camera) extends EntityNode(camera) with LabeledEntityNode with WindowedNode[CameraWindow] {
override def iconSource: IconSource = IconSource.Nodes.Camera override def icon: IconSource = IconSource.Nodes.Camera
override def rotatable: Boolean = true
override def createWindow(): CameraWindow = new CameraWindow(this) override def createWindow(): CameraWindow = new CameraWindow(this)
} }

View File

@ -10,7 +10,9 @@ import ocelot.desktop.windows.ChestWindow
class ChestNode extends LabeledNode with WindowedNode[ChestWindow] with PersistedInventory { class ChestNode extends LabeledNode with WindowedNode[ChestWindow] with PersistedInventory {
override type I = Item with PersistableItem override type I = Item with PersistableItem
override def iconSource: IconSource = IconSource.Nodes.Chest override def icon: IconSource = IconSource.Nodes.Chest
override def rotatable: Boolean = false
override def minimumSize: Size2D = Size2D(28, 30) * 2 + 4 override def minimumSize: Size2D = Size2D(28, 30) * 2 + 4

View File

@ -10,6 +10,8 @@ class ColorfulLampNode(val lamp: ColorfulLamp) extends EntityNode(lamp) with Lab
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 rotatable: Boolean = false
override def label: Option[String] = super.label.filter(_ => mouseHover) override def label: Option[String] = super.label.filter(_ => mouseHover)
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
@ -22,12 +24,12 @@ class ColorfulLampNode(val lamp: ColorfulLamp) extends EntityNode(lamp) with Lab
) )
g.rect(position.x + 2, position.y + 2, size.width - 4, size.height - 4, lastColor) g.rect(position.x + 2, position.y + 2, size.width - 4, size.height - 4, lastColor)
g.sprite(IconSource.Nodes.Lamp.Frame, position.x + 2, position.y + 2, size.width - 4, size.height - 4) g.sprite(IconSource.Nodes.LampFrame, position.x + 2, position.y + 2, size.width - 4, size.height - 4)
} }
override def drawLight(g: Graphics): Unit = { override def drawLight(g: Graphics): Unit = {
super.drawLight(g) super.drawLight(g)
g.sprite(IconSource.Nodes.Lamp.Glow, position - size / 2, size * 2, lastColor) g.sprite(IconSource.Nodes.LampGlow, position - size / 2, size * 2, lastColor)
} }
eventHandlers += { eventHandlers += {

View File

@ -2,6 +2,7 @@ package ocelot.desktop.node.nodes
import ocelot.desktop.color.Color import ocelot.desktop.color.Color
import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.inventory.item.SelfDestructingCardItem
import ocelot.desktop.node.Node.HighlightThickness import ocelot.desktop.node.Node.HighlightThickness
import ocelot.desktop.node.{ComputerAwareNode, WindowedNode} import ocelot.desktop.node.{ComputerAwareNode, WindowedNode}
import ocelot.desktop.ui.event.ClickEvent import ocelot.desktop.ui.event.ClickEvent
@ -15,10 +16,15 @@ import totoro.ocelot.brain.entity.traits.Inventory
import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier
class ComputerNode(val computerCase: Case) class ComputerNode(val computerCase: Case)
extends ComputerAwareNode(computerCase) with AudibleComputerAware with WindowedNode[ComputerWindow] { extends ComputerAwareNode(computerCase)
override val iconSource: IconSource = IconSource.Nodes.Computer.Default with AudibleComputerAware
with WindowedNode[ComputerWindow] {
override val icon: IconSource = IconSource.Nodes.Computer.Default
override def iconColor: Color = TierColor.get(computerCase.tier) override def iconColor: Color = TierColor.get(computerCase.tier)
override def rotatable: Boolean = true
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
addPowerContextMenuEntries(menu) addPowerContextMenuEntries(menu)
addTierContextMenuEntries(menu) addTierContextMenuEntries(menu)
@ -28,6 +34,12 @@ class ComputerNode(val computerCase: Case)
super.setupContextMenu(menu, event) super.setupContextMenu(menu, event)
} }
override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = {
inventoryIterator.flatMap(_.get).collect {
case item: SelfDestructingCardItem => item
}
}
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()

View File

@ -21,7 +21,9 @@ class DiskDriveNode(entity: FloppyDiskDrive)
with ShiftClickNode with ShiftClickNode
with WindowedNode[DiskDriveWindow] { with WindowedNode[DiskDriveWindow] {
override def iconSource: IconSource = IconSource.Nodes.DiskDrive.Default override def icon: IconSource = IconSource.Nodes.DiskDrive.Default
override def rotatable: Boolean = true
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
if (isFloppyItemPresent) { if (isFloppyItemPresent) {

View File

@ -8,7 +8,9 @@ import totoro.ocelot.brain.entity.HologramProjector
class HologramProjectorNode(val hologramProjector: HologramProjector) class HologramProjectorNode(val hologramProjector: HologramProjector)
extends EntityNode(hologramProjector) with LabeledEntityNode with WindowedNode[HologramProjectorWindow] { extends EntityNode(hologramProjector) with LabeledEntityNode with WindowedNode[HologramProjectorWindow] {
override def iconSource: IconSource = IconSource.Nodes.HologramProjector(hologramProjector.tier) override def icon: IconSource = IconSource.Nodes.HologramProjector(hologramProjector.tier)
override def rotatable: Boolean = false
override def createWindow(): HologramProjectorWindow = new HologramProjectorWindow(this) override def createWindow(): HologramProjectorWindow = new HologramProjectorWindow(this)
} }

View File

@ -9,7 +9,9 @@ import totoro.ocelot.brain.event.{EventBus, NoteBlockTriggerEvent}
import scala.util.Random import scala.util.Random
class IronNoteBlockNode(val ironNoteBlock: IronNoteBlock) extends NoteBlockNodeBase(ironNoteBlock) { class IronNoteBlockNode(val ironNoteBlock: IronNoteBlock) extends NoteBlockNodeBase(ironNoteBlock) {
override def iconSource: IconSource = IconSource.Nodes.IronNoteBlock override def icon: IconSource = IconSource.Nodes.IronNoteBlock
override def rotatable: Boolean = false
override def onClick(event: ClickEvent): Unit = { override def onClick(event: ClickEvent): Unit = {
event match { event match {

View File

@ -20,7 +20,8 @@ class MicrocontrollerNode(val microcontroller: Microcontroller)
with ComputerAware with ComputerAware
with DefaultSlotItemsFillable with DefaultSlotItemsFillable
with WindowedNode[ComputerWindow] { with WindowedNode[ComputerWindow] {
override val iconSource: IconSource = IconSource.Nodes.Microcontroller.Default
override val icon: IconSource = IconSource.Nodes.Microcontroller.Default
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
addPowerContextMenuEntries(menu) addPowerContextMenuEntries(menu)
@ -54,6 +55,8 @@ class MicrocontrollerNode(val microcontroller: Microcontroller)
override def getNodeByPort(port: NodePort): network.Node = override def getNodeByPort(port: NodePort): network.Node =
microcontroller.sidedNode(port.direction.get) microcontroller.sidedNode(port.direction.get)
override def rotatable: Boolean = true
override def shouldReceiveEventsFor(address: String): Boolean = { override def shouldReceiveEventsFor(address: String): Boolean = {
address == microcontroller.machine.node.address || address == microcontroller.machine.node.address ||
super.shouldReceiveEventsFor(address) super.shouldReceiveEventsFor(address)
@ -65,6 +68,12 @@ class MicrocontrollerNode(val microcontroller: Microcontroller)
override def computerType: ComputerType = ComputerType.Microcontroller override def computerType: ComputerType = ComputerType.Microcontroller
override def brainInventory: Inventory = microcontroller.inventory.owner override def brainInventory: Inventory = microcontroller.inventory.owner
override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = {
inventoryIterator.flatMap(_.get).collect {
case item: SelfDestructingCardItem => item
}
}
override def addSlotsBasedOnTier(): Unit = { override def addSlotsBasedOnTier(): Unit = {
microcontroller.tier match { microcontroller.tier match {
case Tier.One => case Tier.One =>
@ -120,23 +129,26 @@ class MicrocontrollerNode(val microcontroller: Microcontroller)
override def fillSlotsWithDefaultItems(): Unit = { override def fillSlotsWithDefaultItems(): Unit = {
cardSlots(0).item = { cardSlots(0).item = {
if (microcontroller.tier == Tier.Two) if (microcontroller.tier >= Tier.Two) {
WirelessNetworkCardItem.Tier2.Factory.build() WirelessNetworkCardItem.Tier2.Factory.build()
else } else {
WirelessNetworkCardItem.Tier1.Factory.build() WirelessNetworkCardItem.Tier1.Factory.build()
} }
}
cardSlots(1).item = { cardSlots(1).item = {
if (microcontroller.tier == Tier.Two) if (microcontroller.tier == Tier.Creative) {
RedstoneCardItem.Tier2.Factory.build() RedstoneCardItem.Tier2.Factory.build()
else } else {
RedstoneCardItem.Tier1.Factory.build() RedstoneCardItem.Tier1.Factory.build()
} }
}
cpuSlot.item = new CpuItem.Factory(Tier.One).build() cpuSlot.item = new CpuItem.Factory(Tier.One).build()
for (memorySlot <- memorySlots) for (memorySlot <- memorySlots) {
memorySlot.item = new MemoryItem.Factory(Tier.One.toExtended(false)).build() memorySlot.item = new MemoryItem.Factory(Tier.One.toExtended(false)).build()
}
eepromSlot.item = EepromItem.Factory.Empty.build() eepromSlot.item = EepromItem.Factory.Empty.build()
} }

View File

@ -7,10 +7,12 @@ import totoro.ocelot.brain.entity.NoteBlock
import totoro.ocelot.brain.event.{EventBus, NoteBlockTriggerEvent} import totoro.ocelot.brain.event.{EventBus, NoteBlockTriggerEvent}
class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBlock) { class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBlock) {
override def iconSource: IconSource = IconSource.Nodes.NoteBlock override def icon: IconSource = IconSource.Nodes.NoteBlock
override def rotatable: Boolean = false
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
menu.addEntry(new ContextMenuSubmenu("Instrument", Some(ContextMenuIcon(IconSource.Guitar))) { menu.addEntry(new ContextMenuSubmenu("Instrument", Some(ContextMenuIcon(IconSource.Icons.Guitar))) {
{ {
val maxLen = NoteBlockNode.Instruments.map(_._2.length).max val maxLen = NoteBlockNode.Instruments.map(_._2.length).max

View File

@ -3,15 +3,14 @@ package ocelot.desktop.node.nodes
import ocelot.desktop.ColorScheme import ocelot.desktop.ColorScheme
import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource} import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource}
import ocelot.desktop.geometry.{Size2D, Vector2D} import ocelot.desktop.geometry.{Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode} import ocelot.desktop.node.{EntityNode, LabeledEntityNode}
import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.particle.Particle
import totoro.ocelot.brain.entity.traits.{Entity, Environment} import totoro.ocelot.brain.entity.traits.{Entity, Environment}
import totoro.ocelot.brain.event.NoteBlockTriggerEvent import totoro.ocelot.brain.event.NoteBlockTriggerEvent
import scala.collection.mutable
abstract class NoteBlockNodeBase(entity: Entity with Environment) extends EntityNode(entity) with LabeledEntityNode { abstract class NoteBlockNodeBase(entity: Entity with Environment) extends EntityNode(entity) with LabeledEntityNode {
eventHandlers += { eventHandlers += {
case BrainEvent(event: NoteBlockTriggerEvent) => case BrainEvent(event: NoteBlockTriggerEvent) =>
@ -22,31 +21,25 @@ abstract class NoteBlockNodeBase(entity: Entity with Environment) extends Entity
volume = event.volume.toFloat.min(1f).max(0f), volume = event.volume.toFloat.min(1f).max(0f),
).play() ).play()
addParticle(event.pitch) UiHandler.root.workspaceView.particleSystem.add(new NoteParticle(event.pitch))
}
private val particles = mutable.ArrayBuffer[(Float, Int)]()
private def addParticle(pitch: Int): Unit = {
synchronized {
particles += ((0f, pitch))
}
}
override def drawParticles(g: Graphics): Unit = synchronized {
for ((time, pitch) <- particles.reverseIterator) {
val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0))
g.sprite("particles/Note", position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time),
Size2D(14, 20), col)
}
particles.mapInPlace { case (t, p) => (t + 1.2f * UiHandler.dt, p) }
particles.filterInPlace(_._1 <= 1f)
} }
override def update(): Unit = { override def update(): Unit = {
super.update() super.update()
if (isHovered || isMoving) {
root.get.statusBar.addMouseEntry(IconSource.Icons.LMB, "Play sample")
}
}
if (isHovered || isMoving) private class NoteParticle(pitch: Int) extends Particle(speed = 1.2f) {
root.get.statusBar.addMouseEntry("icons/LMB", "Play sample") override def draw(g: Graphics): Unit = {
val col = ColorScheme("Note" + pitch.min(24).max(0)).withAlpha(1 - (2 * time - 1).min(1).max(0))
g.sprite(
IconSource.Particles.Note,
position + Vector2D(pitch / 24f * 40f + 5, height / 2 - 10 - 100 * time),
Size2D(14, 20),
col,
)
}
} }
} }

View File

@ -22,7 +22,9 @@ class OcelotBlockNode(val ocelot: OcelotBlock)
override def name: String = "Ocelot Block" override def name: String = "Ocelot Block"
override def iconSource: IconSource = IconSource.Nodes.OcelotBlock.Default override def icon: IconSource = IconSource.Nodes.OcelotBlock.Default
override def rotatable: Boolean = false
override def ocelotInterface: OcelotInterface = ocelot override def ocelotInterface: OcelotInterface = ocelot
@ -62,17 +64,16 @@ class OcelotBlockNode(val ocelot: OcelotBlock)
} }
private def drawActivity(g: Graphics, icon: IconSource, lastActivity: Long, currentTime: Long): Unit = { private def drawActivity(g: Graphics, icon: IconSource, lastActivity: Long, currentTime: Long): Unit = {
val alpha = (1 - (currentTime - lastActivity) / ActivityFadeOutMs).clamp() val alpha = (1 - (currentTime - lastActivity) / ActivityFadeOutMs).clamped()
if (alpha > 0) { if (alpha > 0) {
g.sprite( g.sprite(
icon.path, icon,
position.x + HighlightThickness, position.x + HighlightThickness,
position.y + HighlightThickness, position.y + HighlightThickness,
size.width - HighlightThickness * 2, size.width - HighlightThickness * 2,
size.height - HighlightThickness * 2, size.height - HighlightThickness * 2,
RGBAColorNorm(1f, 1f, 1f, alpha), RGBAColorNorm(1f, 1f, 1f, alpha),
icon.animation,
) )
} }
} }

View File

@ -9,7 +9,9 @@ import ocelot.desktop.windows.OpenFMRadioWindow
class OpenFMRadioNode(val openFMRadio: OpenFMRadio) class OpenFMRadioNode(val openFMRadio: OpenFMRadio)
extends EntityNode(openFMRadio) with LabeledEntityNode with WindowedNode[OpenFMRadioWindow] { extends EntityNode(openFMRadio) with LabeledEntityNode with WindowedNode[OpenFMRadioWindow] {
override def iconSource: IconSource = IconSource.Nodes.OpenFMRadio override def icon: IconSource = IconSource.Nodes.OpenFMRadio
override def rotatable: Boolean = true
private var lastTick = OcelotDesktop.ticker.tick private var lastTick = OcelotDesktop.ticker.tick
private var animationIndex = 0 private var animationIndex = 0

View File

@ -3,7 +3,7 @@ package ocelot.desktop.node.nodes
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource} import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.inventory.Item import ocelot.desktop.inventory.Item
import ocelot.desktop.inventory.item.{DiskDriveMountableItem, ServerItem} import ocelot.desktop.inventory.item.{DiskDriveMountableItem, SelfDestructingCardItem, ServerItem}
import ocelot.desktop.inventory.traits.RackMountableItem import ocelot.desktop.inventory.traits.RackMountableItem
import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize, Size, TexelCount} import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize, Size, TexelCount}
import ocelot.desktop.node.{ComputerAwareNode, NodePort, WindowedNode} import ocelot.desktop.node.{ComputerAwareNode, NodePort, WindowedNode}
@ -18,7 +18,7 @@ import totoro.ocelot.brain.network
import totoro.ocelot.brain.util.Direction import totoro.ocelot.brain.util.Direction
class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode[RackWindow] { class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode[RackWindow] {
override val iconSource: IconSource = IconSource.Nodes.Rack.Empty override val icon: IconSource = IconSource.Nodes.Rack.Empty
override def exposeAddress = false override def exposeAddress = false
override def ports: Array[NodePort] = Array( override def ports: Array[NodePort] = Array(
@ -31,6 +31,8 @@ class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode
override def getNodeByPort(port: NodePort): network.Node = rack.sidedNode(port.direction.get) override def getNodeByPort(port: NodePort): network.Node = rack.sidedNode(port.direction.get)
override def rotatable: Boolean = true
override def shouldReceiveEventsFor(address: String): Boolean = { override def shouldReceiveEventsFor(address: String): Boolean = {
super.shouldReceiveEventsFor(address) || super.shouldReceiveEventsFor(address) ||
rack.inventory.entities.exists { rack.inventory.entities.exists {
@ -43,6 +45,15 @@ class RackNode(val rack: Rack) extends ComputerAwareNode(rack) with WindowedNode
} }
} }
override protected def selfDestructingCards: IterableOnce[SelfDestructingCardItem] = {
inventoryIterator.flatMap(_.get).collect({
case item: ServerItem =>
item.inventoryIterator.flatMap(_.get).collect {
case card: SelfDestructingCardItem => card
}
}).flatten
}
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
RackNode.addContextMenuEntriesOfMountable(menu, getMountableByClick(event)) RackNode.addContextMenuEntriesOfMountable(menu, getMountableByClick(event))
@ -183,7 +194,7 @@ object RackNode {
def addContextMenuEntriesOfMountable(menu: ContextMenu, item: Option[RackMountableItem]): Unit = { def addContextMenuEntriesOfMountable(menu: ContextMenu, item: Option[RackMountableItem]): Unit = {
item match { item match {
case Some(serverItem: ServerItem) => case Some(serverItem: ServerItem) =>
menu.addEntry(ContextMenuEntry("Set up", IconSource.Window) { menu.addEntry(ContextMenuEntry("Set up", IconSource.Icons.Window) {
serverItem.window.open() serverItem.window.open()
}) })
@ -206,7 +217,7 @@ object RackNode {
"Change floppy" "Change floppy"
else else
"Set floppy", "Set floppy",
IconSource.Save, IconSource.Icons.Save,
) { ) {
diskDriveMountableItem.window.open() diskDriveMountableItem.window.open()
}) })

View File

@ -8,12 +8,16 @@ import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode} import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode}
import ocelot.desktop.ui.event.ClickEvent import ocelot.desktop.ui.event.ClickEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.contextmenu.ContextMenu import ocelot.desktop.ui.widget.DiskEditWindow
import ocelot.desktop.ui.widget.DiskEditWindow.EditableDisk
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.ui.widget.slot.HddSlotWidget import ocelot.desktop.ui.widget.slot.HddSlotWidget
import ocelot.desktop.ui.widget.window.Windowed
import ocelot.desktop.util.{DefaultSlotItemsFillable, DrawUtils} import ocelot.desktop.util.{DefaultSlotItemsFillable, DrawUtils}
import ocelot.desktop.windows.RaidWindow import ocelot.desktop.windows.RaidWindow
import totoro.ocelot.brain.entity.Raid import totoro.ocelot.brain.entity.Raid
import totoro.ocelot.brain.entity.traits.Inventory import totoro.ocelot.brain.entity.traits.{Environment, Inventory}
import totoro.ocelot.brain.nbt.NBTTagCompound
import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier
import scala.util.Random import scala.util.Random
@ -24,10 +28,14 @@ class RaidNode(val raid: Raid)
with LabeledEntityNode with LabeledEntityNode
with DiskActivityHandler with DiskActivityHandler
with DefaultSlotItemsFillable with DefaultSlotItemsFillable
with WindowedNode[RaidWindow] { with WindowedNode[RaidWindow]
with EditableDisk {
var diskSlots: Array[HddSlotWidget] = Array.tabulate(3)(index => new HddSlotWidget(Slot(index), Tier.Three)) var diskSlots: Array[HddSlotWidget] = Array.tabulate(3)(index => new HddSlotWidget(Slot(index), Tier.Three))
override val iconSource: IconSource = IconSource.Nodes.Raid.Default override val icon: IconSource = IconSource.Nodes.Raid.Default
override def rotatable: Boolean = true
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
super.draw(g) super.draw(g)
@ -56,30 +64,51 @@ class RaidNode(val raid: Raid)
override def shouldReceiveEventsFor(address: String): Boolean = override def shouldReceiveEventsFor(address: String): Boolean =
super.shouldReceiveEventsFor(address) || raid.filesystem.exists(_.node.address == address) super.shouldReceiveEventsFor(address) || raid.filesystem.exists(_.node.address == address)
private val diskEditWindow = new Windowed[DiskEditWindow] {
override protected def createWindow(): DiskEditWindow = new DiskEditWindow(RaidNode.this)
override protected def windowNBTKey: String = "diskEditWindow"
}
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
DiskItem.addRealPathContextMenuEntries(menu, raid, DiskItem.addRealPathContextMenuEntries(
menu,
raid,
realPathSetter => { realPathSetter => {
realPathSetter() realPathSetter()
},
)
menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Icons.Edit) {
diskEditWindow.window.open()
}) })
// TODO: Implement DiskDriveWindow later, because at this moment every
// TODO: instance of 'Windowed' trait can have only 1 persistable window
// TODO: Perhaps we should rework this system with List[Window] and
// TODO: registerWindow(...) or something similar
// menu.addEntry(ContextMenuEntry("Edit disk", IconSource.Edit) {
// window.open()
// })
menu.addSeparator() menu.addSeparator()
super.setupContextMenu(menu, event) super.setupContextMenu(menu, event)
} }
override def load(nbt: NBTTagCompound): Unit = {
super.load(nbt)
diskEditWindow.load(nbt)
}
override def save(nbt: NBTTagCompound): Unit = {
super.save(nbt)
diskEditWindow.save(nbt)
}
override def dispose(): Unit = {
diskEditWindow.closeAndDisposeWindow()
super.dispose()
}
// -------------------------------- LabeledEntityNode -------------------------------- // -------------------------------- LabeledEntityNode --------------------------------
override def fallbackLabelAddress: Option[String] = raid.filesystem.map(_.node.address) override def fallbackLabelAddress: Option[String] = diskAddress
// -------------------------------- Inventory -------------------------------- // -------------------------------- Inventory --------------------------------
@ -97,4 +126,22 @@ class RaidNode(val raid: Raid)
// -------------------------------- Window -------------------------------- // -------------------------------- Window --------------------------------
override def createWindow(): RaidWindow = new RaidWindow(this) override def createWindow(): RaidWindow = new RaidWindow(this)
// -------------------------------- EditableDisk --------------------------------
override def disk: Option[Environment] = raid.filesystem
override def diskKind: String = "RAID"
override def diskAddress: Option[String] = raid.filesystem.map(_.node.address)
override def capacity: Long = raid.filesystem.fold(0L)(_.fileSystem.spaceTotal)
override def diskLabel: Option[String] = Option(raid.label.getLabel)
override def diskLabel_=(label: Option[String]): Unit = raid.label.setLabel(label.orNull)
override def isLabelWriteable: Boolean = true
override def editingAllowed: Boolean = raid.filesystem.isDefined
} }

View File

@ -11,7 +11,7 @@ import totoro.ocelot.brain.util.Direction
class RelayNode(val relay: Relay) class RelayNode(val relay: Relay)
extends EntityNode(relay) with SyncedInventory with LabeledNode with WindowedNode[RelayWindow] { extends EntityNode(relay) with SyncedInventory with LabeledNode with WindowedNode[RelayWindow] {
override val iconSource: IconSource = IconSource.Nodes.Relay override val icon: IconSource = IconSource.Nodes.Relay
override def ports: Array[NodePort] = Array( override def ports: Array[NodePort] = Array(
NodePort(Some(Direction.North)), NodePort(Some(Direction.North)),
@ -24,6 +24,8 @@ class RelayNode(val relay: Relay)
override def getNodeByPort(port: NodePort): network.Node = relay.sidedNode(port.direction.get) override def getNodeByPort(port: NodePort): network.Node = relay.sidedNode(port.direction.get)
override def rotatable: Boolean = false
override def exposeAddress = false override def exposeAddress = false
override def shouldReceiveEventsFor(address: String): Boolean = { override def shouldReceiveEventsFor(address: String): Boolean = {

View File

@ -91,32 +91,34 @@ class ScreenNode(val screen: Screen)
} }
} }
override def iconSource: IconSource = IconSource.Nodes.Screen.Standalone override def icon: IconSource = IconSource.Nodes.Screen.Standalone
override def iconColor: Color = TierColor.get(screen.tier) override def iconColor: Color = TierColor.get(screen.tier)
override def rotatable: Boolean = true
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
// no synchronization here: the methods to turn the screen on/off are indirect. // no synchronization here: the methods to turn the screen on/off are indirect.
if (screen.getPowerState) { if (screen.getPowerState) {
menu.addEntry(ContextMenuEntry("Turn off", IconSource.Power) { menu.addEntry(ContextMenuEntry("Turn off", IconSource.Icons.Power) {
screen.setPowerState(false) screen.setPowerState(false)
}) })
} else { } else {
menu.addEntry(ContextMenuEntry("Turn on", IconSource.Power) { menu.addEntry(ContextMenuEntry("Turn on", IconSource.Icons.Power) {
screen.setPowerState(true) screen.setPowerState(true)
}) })
} }
menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.AspectRatio) { menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.Icons.AspectRatio) {
new ScreenAspectRatioDialog(this).show() new ScreenAspectRatioDialog(this).show()
}) })
if (keyboard.isDefined) { if (keyboard.isDefined) {
menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.KeyboardOff) { menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.Icons.KeyboardOff) {
detachKeyboard() detachKeyboard()
}) })
} else { } else {
menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Keyboard) { menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Icons.Keyboard) {
attachKeyboard() attachKeyboard()
}) })
} }

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