Merge branch 'develop'

This commit is contained in:
Fingercomp 2023-10-18 23:52:00 +07:00
commit 8be35b72f0
No known key found for this signature in database
GPG Key ID: BBC71CEE45D86E37
41 changed files with 768 additions and 338 deletions

107
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,107 @@
# Contributing to Ocelot
The development of Ocelot is a volunteer effort — and that means we're always understaffed.
If you'd like to help us out, we'll appreciate it greatly!
And to help *you* help us, we've written a guide, which you are reading right now, describing how we do things around here.
## The Ocelot projects
In fact, Ocelot consists of multiple projects, of which Ocelot Desktop is but a part.
The most important component is [ocelot-brain].
It is a Scala library that does all the actual OC emulation.
In terms of responsibilities, it roughly corresponds to the server-side OpenComputers code except abstracted away from Minecraft specifics.
It's ocelot-brain that defines the behavior of all entities: blocks, cards, etc.; and it's also ocelot-brain that runs all the Lua code.
Ocelot Desktop builds on top of ocelot-brain and provides a graphical interface to the library.
It does rendering, audio, handles user interaction, and translates user input into calls to ocelot-brain.
This is also where all items are defined.
Such architectural separation of concerns admits alternative interfaces to ocelot-brain.
[Ocelot.online] is a demonstration of that — it uses the same ocelot-brain library for emulation but provides a web interface instead.
At the moment Ocelot.online is not particularly full of features, unfortunately, and we won't focus on it in this document.
## Issues
We use GitLab issues to track feature requests and bugs.
As touched on in the previous section, we have three repositories — and, therefore, three issue trackers.
Which one should you report your issue to?
- If you want to request support for a new OC component or report incorrect emulation of an existing component, consider posting an issue to the [ocelot-brain] tracker.
- If you'd like to tell us about UI glitches (or ask for some UI improvement), this is squarely in the Ocelot Desktop territory.
- Since [Ocelot.online] is not actively developed, it's unlikely that it would be a good choice for an issue — we'd prefer if you told us about issues directly via IRC, Discord, or email.
If you're not quite sure which one to pick anyway, Ocelot Desktop is a safe bet.
Don't worry if you make a mistake — we can always move the issue to the appropriate place.
### Feature requests
Ocelot Desktop wouldn't become so great if nobody was asking for features!
However, if you'd like to ask for one, keep in mind that we are a small group of developer volunteering our time and attention to this project.
Many issues stayed open for several years before somebody went ahead and finally realized the asked feature.
So we'll appreciate it if you tell us why you're requesting a feature and how you would use it if it were added.
Seeing a clear use-case helps us stay on track if we get to implementing it — and it's also a great source of motivation for developers since this shows how, concretely, their efforts can help you. :)
### Bugs
Bugs may be embarrassing — but this is no reason to hide them.
On the contrary, we'd love to hear about them.
If you find one, **please tell us** — or, better yet, **report it on the issue tracker**, which makes sure we don't accidentally forget about it and also helps other users who have the same problem!
Ocelot Desktop should never crash.
This is, of course, but a hopeless dream, and it does crash sometimes.
**Please do not ignore crashes** (unless you're sure Ocelot is not at fault): we realize this is the most disruptive kind of bug, and we'd like to fix it as soon as possible.
If you're reporting a bug, please **describe your actions** preceding it: was it pressing a button? loading a workspace? or something else?
Ideally (if you can reproduce the issue — that is, trigger it again), you should tell us step-by-step what you did that lead to the problem.
But we understand that it's sometimes just impossible — we'd rather have a report alerting us of the bug than be completely oblivious to it.
Please attach logs if you have them (`%APPDATA%/Ocelot/Logs/Desktop.log` on Windows, `~/.local/share/ocelot/desktop.log` on Linux) — and **especially** if it's a crash.
Logs don't always help us, but when they do, they help tremendously: a crash would have a stack trace pointing to the exact line of code that triggered it, for example.
If the bug only happens with a specific workspace, you could attach the save directory (as a zip archive) as well.
Finally, sometimes we'd like to ask for additional information after we get a bug report.
Though we don't expect an instant reply, it would be sad if you just ghosted us.
We don't have a stale issue policy, but we may close issues that were reported such a long time ago that they are likely no longer relevant.
## Code contribution
Admittedly, there's little documentation, and Scala, a nice language though it might be, is not among the most popular choices for software development.
Just to warn you of what kind of mess you're getting yourself into if you're going to contribute code. :)
This means you'll probably have to figure out how things work by reading the source code.
It would be a good idea to chat with us if you're not deterred because of the admonition above.
Especially if you're implementing a new feature.
Doing this can help you avoid some pitfalls and get a better idea of how things should work.
New code should be submitted as a merge request.
We'll review the changes, perhaps suggesting some things to improve.
Once we're happy with the code, we'll gladly accept the MR.
If your work requires changing ocelot-brain and Ocelot Desktop at the same time, you'll need to make two MRs — one for each repository.
Here are a few things we'd like you to do when writing code.
- Keep sensible commit names unless you're planning to squash them at the end.
- Do not commit code that doesn't compile.
It makes bisection harder.
- Try to adhere to the style of existing code.
It might be a bit haphazard currently, but please avoid introducing yet another coding convention.
- Speaking of conventions: we use Scala's naming scheme.
For instance, immutable constants are spelled `LikeThis`.
- Use Scala idioms.
The language features an extensive standard library that covers many aspects of programming.
Using `Option[T]` instead of `null`, modeling data as case classes extending a sealed trait, and using pattern matching — these things, though perhaps not as widely used in mainstream languages like C++ or Java, make code safer and more consistent.
## Communication
The primary means of communication is via [IRC] or [Discord] (thread "forums ‣ Ocelot Dev").
Either English or Russian is fine.
(Please give people some time to respond — they might even be asleep at the time you're posting a message.)
If you despise instant messaging, you can shoot us an email to no-reply at fomalhaut me.
(Don't mind the weird address.)
[ocelot-brain]: https://gitlab.com/cc-ru/ocelot/ocelot-brain "The ocelot-brain repository."
[Ocelot.online]: https://gitlab.com/cc-ru/ocelot/ocelot-online "The Ocelot.online repository."
[IRC]: ircs://irc.esper.net:6697/cc.ru "#cc.ru at irc.esper.net"
[Discord]: https://discord.com/invite/FM9qWGm

View File

@ -1,5 +1,5 @@
name := "ocelot-desktop"
version := "1.10.1"
version := "1.10.2"
scalaVersion := "2.13.10"
lazy val root = project.in(file("."))

@ -1 +1 @@
Subproject commit 2ef2cf3bda8f05688c9801d859c1ab16088bd4c4
Subproject commit 2a52322a780ac562316f449f8ce54227dd0586fd

BIN
sprites/icons/Guitar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -1,175 +1,176 @@
BackgroundPattern 0 0 304 304
BarSegment 385 434 16 4
Empty 327 572 16 16
EmptySlot 305 597 18 18
Empty 242 674 16 16
EmptySlot 220 699 18 18
Knob 203 434 50 50
KnobCenter 254 434 50 50
KnobLimits 305 434 50 50
Loading 0 305 48 448
Logo 305 0 168 200
ShadowBorder 279 305 1 24
ShadowCorner 202 572 24 24
TabArrow 49 716 8 14
blocks/Generic 344 572 16 16
ShadowCorner 117 674 24 24
TabArrow 134 628 8 14
blocks/Generic 259 674 16 16
blocks/HologramEffect 287 314 4 4
blocks/HologramProjector1Top 361 572 16 16
blocks/HologramProjector2Top 378 572 16 16
blocks/HologramProjectorSide 395 572 16 16
buttons/BottomDrawerClose 324 597 18 18
buttons/BottomDrawerOpen 343 597 18 18
blocks/HologramProjector1Top 276 674 16 16
blocks/HologramProjector2Top 293 674 16 16
blocks/HologramProjectorSide 310 674 16 16
buttons/BottomDrawerClose 239 699 18 18
buttons/BottomDrawerOpen 258 699 18 18
buttons/OpenFMRadioCloseOff 404 445 7 8
buttons/OpenFMRadioCloseOn 412 445 7 8
buttons/OpenFMRadioRedstoneOff 359 445 8 8
buttons/OpenFMRadioRedstoneOn 368 445 8 8
buttons/OpenFMRadioStartOff 227 572 24 24
buttons/OpenFMRadioStartOn 252 572 24 24
buttons/OpenFMRadioStopOff 277 572 24 24
buttons/OpenFMRadioStopOn 302 572 24 24
buttons/OpenFMRadioVolumeDownOff 58 716 10 10
buttons/OpenFMRadioVolumeDownOn 69 716 10 10
buttons/OpenFMRadioVolumeUpOff 80 716 10 10
buttons/OpenFMRadioVolumeUpOn 91 716 10 10
buttons/PowerOff 362 597 18 18
buttons/PowerOn 381 597 18 18
buttons/RackRelayOff 202 553 65 18
buttons/RackRelayOn 268 553 65 18
icons/Antenna 412 572 16 16
icons/ArrowRight 429 572 16 16
icons/AspectRatio 446 572 16 16
icons/Book 463 572 16 16
icons/ButtonCheck 202 630 17 17
icons/ButtonClipboard 220 630 17 17
icons/ButtonRandomize 238 630 17 17
icons/CPU 480 572 16 16
icons/Card 497 572 16 16
icons/Close 49 683 15 14
icons/Code 514 572 16 16
icons/ComponentBus 531 572 16 16
icons/Copy 548 572 16 16
icons/Cross 565 572 16 16
icons/Delete 582 572 16 16
icons/DragLMB 400 597 21 14
icons/DragRMB 422 597 21 14
icons/EEPROM 599 572 16 16
icons/Edit 616 572 16 16
icons/Eject 633 572 16 16
icons/File 650 572 16 16
icons/Floppy 667 572 16 16
icons/Folder 684 572 16 16
icons/FolderSlash 701 572 16 16
icons/Grid 236 597 22 22
icons/GridOff 259 597 22 22
icons/HDD 718 572 16 16
icons/Help 735 572 16 16
icons/Home 282 597 22 22
icons/LMB 133 698 11 14
icons/Label 752 572 16 16
icons/LinesHorizontal 769 572 16 16
icons/Link 786 572 16 16
icons/LinkSlash 803 572 16 16
icons/Memory 820 572 16 16
icons/Microchip 837 572 16 16
icons/NA 854 572 16 16
icons/NotificationError 157 698 11 11
icons/NotificationInfo 169 698 11 11
icons/NotificationWarning 181 698 11 11
icons/Ocelot 871 572 16 16
icons/Pin 88 698 14 14
icons/Plus 888 572 16 16
icons/Power 905 572 16 16
icons/RMB 145 698 11 14
icons/Restart 922 572 16 16
icons/Save 939 572 16 16
icons/SaveAs 956 572 16 16
icons/Server 973 572 16 16
icons/SettingsSound 49 698 12 17
icons/SettingsSystem 62 698 12 17
icons/SettingsUI 75 698 12 17
icons/Tier0 990 572 16 16
icons/Tier1 1007 572 16 16
icons/Tier2 334 553 16 16
icons/Tiers 351 553 16 16
icons/Unpin 103 698 14 14
icons/WaveLFSR 969 655 24 10
icons/WaveNoise 994 655 24 10
icons/WaveSawtooth 49 672 24 10
icons/WaveSine 74 672 24 10
icons/WaveSquare 99 672 24 10
icons/WaveTriangle 124 672 24 10
icons/Window 368 553 16 16
buttons/OpenFMRadioStartOff 142 674 24 24
buttons/OpenFMRadioStartOn 167 674 24 24
buttons/OpenFMRadioStopOff 192 674 24 24
buttons/OpenFMRadioStopOn 217 674 24 24
buttons/OpenFMRadioVolumeDownOff 143 628 10 10
buttons/OpenFMRadioVolumeDownOn 154 628 10 10
buttons/OpenFMRadioVolumeUpOff 165 628 10 10
buttons/OpenFMRadioVolumeUpOn 176 628 10 10
buttons/PowerOff 277 699 18 18
buttons/PowerOn 296 699 18 18
buttons/RackRelayOff 117 655 65 18
buttons/RackRelayOn 183 655 65 18
icons/Antenna 327 674 16 16
icons/ArrowRight 344 674 16 16
icons/AspectRatio 361 674 16 16
icons/Book 378 674 16 16
icons/ButtonCheck 117 732 17 17
icons/ButtonClipboard 135 732 17 17
icons/ButtonRandomize 153 732 17 17
icons/CPU 395 674 16 16
icons/Card 412 674 16 16
icons/Close 134 595 15 14
icons/Code 429 674 16 16
icons/ComponentBus 446 674 16 16
icons/Copy 463 674 16 16
icons/Cross 480 674 16 16
icons/Delete 497 674 16 16
icons/DragLMB 483 699 21 14
icons/DragRMB 505 699 21 14
icons/EEPROM 514 674 16 16
icons/Edit 531 674 16 16
icons/Eject 548 674 16 16
icons/File 565 674 16 16
icons/Floppy 582 674 16 16
icons/Folder 599 674 16 16
icons/FolderSlash 616 674 16 16
icons/Grid 151 699 22 22
icons/GridOff 174 699 22 22
icons/Guitar 633 674 16 16
icons/HDD 650 674 16 16
icons/Help 667 674 16 16
icons/Home 197 699 22 22
icons/LMB 218 610 11 14
icons/Label 684 674 16 16
icons/LinesHorizontal 701 674 16 16
icons/Link 718 674 16 16
icons/LinkSlash 735 674 16 16
icons/Memory 752 674 16 16
icons/Microchip 769 674 16 16
icons/NA 786 674 16 16
icons/NotificationError 242 610 11 11
icons/NotificationInfo 254 610 11 11
icons/NotificationWarning 266 610 11 11
icons/Ocelot 803 674 16 16
icons/Pin 173 610 14 14
icons/Plus 820 674 16 16
icons/Power 837 674 16 16
icons/RMB 230 610 11 14
icons/Restart 854 674 16 16
icons/Save 871 674 16 16
icons/SaveAs 888 674 16 16
icons/Server 905 674 16 16
icons/SettingsSound 134 610 12 17
icons/SettingsSystem 147 610 12 17
icons/SettingsUI 160 610 12 17
icons/Tier0 922 674 16 16
icons/Tier1 939 674 16 16
icons/Tier2 956 674 16 16
icons/Tiers 973 674 16 16
icons/Unpin 188 610 14 14
icons/WaveLFSR 901 567 24 10
icons/WaveNoise 926 567 24 10
icons/WaveSawtooth 951 567 24 10
icons/WaveSine 976 567 24 10
icons/WaveSquare 134 584 24 10
icons/WaveTriangle 159 584 24 10
icons/Window 990 674 16 16
icons/WireArrowLeft 281 305 4 8
icons/WireArrowRight 286 305 4 8
items/APU0 134 553 16 96
items/APU1 151 553 16 96
items/APU2 168 553 16 96
items/CPU0 385 553 16 16
items/CPU1 402 553 16 16
items/CPU2 419 553 16 16
items/CardBase 436 553 16 16
items/CircuitBoard 453 553 16 16
items/ComponentBus0 470 553 16 16
items/ComponentBus1 487 553 16 16
items/ComponentBus2 504 553 16 16
items/ComponentBus3 521 553 16 16
items/APU0 49 655 16 96
items/APU1 66 655 16 96
items/APU2 83 655 16 96
items/CPU0 1007 674 16 16
items/CPU1 249 655 16 16
items/CPU2 266 655 16 16
items/CardBase 283 655 16 16
items/CircuitBoard 300 655 16 16
items/ComponentBus0 317 655 16 16
items/ComponentBus1 334 655 16 16
items/ComponentBus2 351 655 16 16
items/ComponentBus3 368 655 16 16
items/DataCard0 49 526 16 128
items/DataCard1 66 526 16 128
items/DataCard2 83 526 16 128
items/DebugCard 538 553 16 16
items/DiskDriveMountable 555 553 16 16
items/EEPROM 572 553 16 16
items/FloppyDisk_dyeBlack 589 553 16 16
items/FloppyDisk_dyeBlue 606 553 16 16
items/FloppyDisk_dyeBrown 623 553 16 16
items/FloppyDisk_dyeCyan 640 553 16 16
items/FloppyDisk_dyeGray 657 553 16 16
items/FloppyDisk_dyeGreen 674 553 16 16
items/FloppyDisk_dyeLightBlue 691 553 16 16
items/FloppyDisk_dyeLightGray 708 553 16 16
items/FloppyDisk_dyeLime 725 553 16 16
items/FloppyDisk_dyeMagenta 742 553 16 16
items/FloppyDisk_dyeOrange 759 553 16 16
items/FloppyDisk_dyePink 776 553 16 16
items/FloppyDisk_dyePurple 793 553 16 16
items/FloppyDisk_dyeRed 810 553 16 16
items/FloppyDisk_dyeWhite 827 553 16 16
items/FloppyDisk_dyeYellow 844 553 16 16
items/GraphicsCard0 861 553 16 16
items/GraphicsCard1 878 553 16 16
items/GraphicsCard2 895 553 16 16
items/HardDiskDrive0 912 553 16 16
items/HardDiskDrive1 929 553 16 16
items/HardDiskDrive2 946 553 16 16
items/InternetCard 202 597 16 32
items/LinkedCard 185 553 16 96
items/Memory0 963 553 16 16
items/Memory1 980 553 16 16
items/Memory2 997 553 16 16
items/Memory3 201 526 16 16
items/Memory4 218 526 16 16
items/Memory5 235 526 16 16
items/Memory6 252 526 16 16
items/NetworkCard 269 526 16 16
items/DebugCard 385 655 16 16
items/DiskDriveMountable 402 655 16 16
items/EEPROM 419 655 16 16
items/FloppyDisk_dyeBlack 436 655 16 16
items/FloppyDisk_dyeBlue 453 655 16 16
items/FloppyDisk_dyeBrown 470 655 16 16
items/FloppyDisk_dyeCyan 487 655 16 16
items/FloppyDisk_dyeGray 504 655 16 16
items/FloppyDisk_dyeGreen 521 655 16 16
items/FloppyDisk_dyeLightBlue 538 655 16 16
items/FloppyDisk_dyeLightGray 555 655 16 16
items/FloppyDisk_dyeLime 572 655 16 16
items/FloppyDisk_dyeMagenta 589 655 16 16
items/FloppyDisk_dyeOrange 606 655 16 16
items/FloppyDisk_dyePink 623 655 16 16
items/FloppyDisk_dyePurple 640 655 16 16
items/FloppyDisk_dyeRed 657 655 16 16
items/FloppyDisk_dyeWhite 674 655 16 16
items/FloppyDisk_dyeYellow 691 655 16 16
items/GraphicsCard0 708 655 16 16
items/GraphicsCard1 725 655 16 16
items/GraphicsCard2 742 655 16 16
items/HardDiskDrive0 759 655 16 16
items/HardDiskDrive1 776 655 16 16
items/HardDiskDrive2 793 655 16 16
items/InternetCard 117 699 16 32
items/LinkedCard 100 655 16 96
items/Memory0 810 655 16 16
items/Memory1 827 655 16 16
items/Memory2 844 655 16 16
items/Memory3 861 655 16 16
items/Memory4 878 655 16 16
items/Memory5 895 655 16 16
items/Memory6 912 655 16 16
items/NetworkCard 929 655 16 16
items/OcelotCard 100 526 16 128
items/RedstoneCard0 286 526 16 16
items/RedstoneCard1 303 526 16 16
items/SelfDestructingCard 219 597 16 32
items/Server0 320 526 16 16
items/Server1 337 526 16 16
items/Server2 354 526 16 16
items/Server3 371 526 16 16
items/RedstoneCard0 946 655 16 16
items/RedstoneCard1 963 655 16 16
items/SelfDestructingCard 134 699 16 32
items/Server0 980 655 16 16
items/Server1 997 655 16 16
items/Server2 201 540 16 16
items/Server3 218 540 16 16
items/SoundCard 117 526 16 128
items/TapeCopper 388 526 16 16
items/TapeDiamond 405 526 16 16
items/TapeGold 422 526 16 16
items/TapeGreg 439 526 16 16
items/TapeIg 456 526 16 16
items/TapeIron 473 526 16 16
items/TapeNetherStar 490 526 16 16
items/TapeSteel 507 526 16 16
items/WirelessNetworkCard0 524 526 16 16
items/WirelessNetworkCard1 541 526 16 16
light-panel/BookmarkLeft 950 655 18 14
light-panel/BookmarkRight 256 630 20 14
items/TapeCopper 235 540 16 16
items/TapeDiamond 252 540 16 16
items/TapeGold 269 540 16 16
items/TapeGreg 286 540 16 16
items/TapeIg 303 540 16 16
items/TapeIron 320 540 16 16
items/TapeNetherStar 337 540 16 16
items/TapeSteel 354 540 16 16
items/WirelessNetworkCard0 371 540 16 16
items/WirelessNetworkCard1 388 540 16 16
light-panel/BookmarkLeft 882 567 18 14
light-panel/BookmarkRight 171 732 20 14
light-panel/BorderB 292 314 4 4
light-panel/BorderL 382 314 4 2
light-panel/BorderR 297 314 4 4
@ -181,88 +182,88 @@ light-panel/CornerTR 322 314 4 4
light-panel/Fill 285 325 2 2
light-panel/Vent 356 434 2 38
nodes/Cable 377 445 8 8
nodes/Camera 558 526 16 16
nodes/Chest 118 698 14 14
nodes/HologramProjector0 575 526 16 16
nodes/HologramProjector1 592 526 16 16
nodes/IronNoteBlock 609 526 16 16
nodes/Lamp 626 526 16 16
nodes/LampFrame 643 526 16 16
nodes/Camera 405 540 16 16
nodes/Chest 203 610 14 14
nodes/HologramProjector0 422 540 16 16
nodes/HologramProjector1 439 540 16 16
nodes/IronNoteBlock 456 540 16 16
nodes/Lamp 473 540 16 16
nodes/LampFrame 490 540 16 16
nodes/LampGlow 49 305 128 128
nodes/NewNode 660 526 16 16
nodes/NoteBlock 677 526 16 16
nodes/OpenFMRadio 694 526 16 16
nodes/Relay 711 526 16 16
nodes/TapeDrive 728 526 16 16
nodes/computer/Default 745 526 16 16
nodes/computer/DiskActivity 762 526 16 16
nodes/computer/Error 779 526 16 16
nodes/computer/On 796 526 16 16
nodes/disk-drive/Default 813 526 16 16
nodes/disk-drive/DiskActivity 830 526 16 16
nodes/disk-drive/Floppy 847 526 16 16
nodes/microcontroller/Default 864 526 16 16
nodes/microcontroller/Error 881 526 16 16
nodes/microcontroller/On 898 526 16 16
nodes/rack/Default 915 526 16 16
nodes/rack/Empty 932 526 16 16
nodes/rack/drive/0/Default 949 526 16 16
nodes/rack/drive/0/DiskActivity 966 526 16 16
nodes/rack/drive/0/Floppy 983 526 16 16
nodes/rack/drive/1/Default 1000 526 16 16
nodes/rack/drive/1/DiskActivity 49 655 16 16
nodes/rack/drive/1/Floppy 66 655 16 16
nodes/rack/drive/2/Default 83 655 16 16
nodes/rack/drive/2/DiskActivity 100 655 16 16
nodes/rack/drive/2/Floppy 117 655 16 16
nodes/rack/drive/3/Default 134 655 16 16
nodes/rack/drive/3/DiskActivity 151 655 16 16
nodes/rack/drive/3/Floppy 168 655 16 16
nodes/rack/drive/Floppy 185 655 16 16
nodes/rack/server/0/Default 202 655 16 16
nodes/rack/server/0/DiskActivity 219 655 16 16
nodes/rack/server/0/Error 236 655 16 16
nodes/rack/server/0/NetworkActivity 253 655 16 16
nodes/rack/server/0/On 270 655 16 16
nodes/rack/server/1/Default 287 655 16 16
nodes/rack/server/1/DiskActivity 304 655 16 16
nodes/rack/server/1/Error 321 655 16 16
nodes/rack/server/1/NetworkActivity 338 655 16 16
nodes/rack/server/1/On 355 655 16 16
nodes/rack/server/2/Default 372 655 16 16
nodes/rack/server/2/DiskActivity 389 655 16 16
nodes/rack/server/2/Error 406 655 16 16
nodes/rack/server/2/NetworkActivity 423 655 16 16
nodes/rack/server/2/On 440 655 16 16
nodes/rack/server/3/Default 457 655 16 16
nodes/rack/server/3/DiskActivity 474 655 16 16
nodes/rack/server/3/Error 491 655 16 16
nodes/rack/server/3/NetworkActivity 508 655 16 16
nodes/rack/server/3/On 525 655 16 16
nodes/raid/0/DiskActivity 542 655 16 16
nodes/raid/0/Error 559 655 16 16
nodes/raid/1/DiskActivity 576 655 16 16
nodes/raid/1/Error 593 655 16 16
nodes/raid/2/DiskActivity 610 655 16 16
nodes/raid/2/Error 627 655 16 16
nodes/raid/Default 644 655 16 16
nodes/screen/BottomLeft 661 655 16 16
nodes/screen/BottomMiddle 678 655 16 16
nodes/screen/BottomRight 695 655 16 16
nodes/screen/ColumnBottom 712 655 16 16
nodes/screen/ColumnMiddle 729 655 16 16
nodes/screen/ColumnTop 746 655 16 16
nodes/screen/Middle 763 655 16 16
nodes/screen/MiddleLeft 780 655 16 16
nodes/screen/MiddleRight 797 655 16 16
nodes/screen/PowerOnOverlay 814 655 16 16
nodes/screen/RowLeft 831 655 16 16
nodes/screen/RowMiddle 848 655 16 16
nodes/screen/RowRight 865 655 16 16
nodes/screen/Standalone 882 655 16 16
nodes/screen/TopLeft 899 655 16 16
nodes/screen/TopMiddle 916 655 16 16
nodes/screen/TopRight 933 655 16 16
nodes/NewNode 507 540 16 16
nodes/NoteBlock 524 540 16 16
nodes/OpenFMRadio 541 540 16 16
nodes/Relay 558 540 16 16
nodes/TapeDrive 575 540 16 16
nodes/computer/Default 592 540 16 16
nodes/computer/DiskActivity 609 540 16 16
nodes/computer/Error 626 540 16 16
nodes/computer/On 643 540 16 16
nodes/disk-drive/Default 660 540 16 16
nodes/disk-drive/DiskActivity 677 540 16 16
nodes/disk-drive/Floppy 694 540 16 16
nodes/microcontroller/Default 711 540 16 16
nodes/microcontroller/Error 728 540 16 16
nodes/microcontroller/On 745 540 16 16
nodes/rack/Default 762 540 16 16
nodes/rack/Empty 779 540 16 16
nodes/rack/drive/0/Default 796 540 16 16
nodes/rack/drive/0/DiskActivity 813 540 16 16
nodes/rack/drive/0/Floppy 830 540 16 16
nodes/rack/drive/1/Default 847 540 16 16
nodes/rack/drive/1/DiskActivity 864 540 16 16
nodes/rack/drive/1/Floppy 881 540 16 16
nodes/rack/drive/2/Default 898 540 16 16
nodes/rack/drive/2/DiskActivity 915 540 16 16
nodes/rack/drive/2/Floppy 932 540 16 16
nodes/rack/drive/3/Default 949 540 16 16
nodes/rack/drive/3/DiskActivity 966 540 16 16
nodes/rack/drive/3/Floppy 983 540 16 16
nodes/rack/drive/Floppy 1000 540 16 16
nodes/rack/server/0/Default 134 567 16 16
nodes/rack/server/0/DiskActivity 151 567 16 16
nodes/rack/server/0/Error 168 567 16 16
nodes/rack/server/0/NetworkActivity 185 567 16 16
nodes/rack/server/0/On 202 567 16 16
nodes/rack/server/1/Default 219 567 16 16
nodes/rack/server/1/DiskActivity 236 567 16 16
nodes/rack/server/1/Error 253 567 16 16
nodes/rack/server/1/NetworkActivity 270 567 16 16
nodes/rack/server/1/On 287 567 16 16
nodes/rack/server/2/Default 304 567 16 16
nodes/rack/server/2/DiskActivity 321 567 16 16
nodes/rack/server/2/Error 338 567 16 16
nodes/rack/server/2/NetworkActivity 355 567 16 16
nodes/rack/server/2/On 372 567 16 16
nodes/rack/server/3/Default 389 567 16 16
nodes/rack/server/3/DiskActivity 406 567 16 16
nodes/rack/server/3/Error 423 567 16 16
nodes/rack/server/3/NetworkActivity 440 567 16 16
nodes/rack/server/3/On 457 567 16 16
nodes/raid/0/DiskActivity 474 567 16 16
nodes/raid/0/Error 491 567 16 16
nodes/raid/1/DiskActivity 508 567 16 16
nodes/raid/1/Error 525 567 16 16
nodes/raid/2/DiskActivity 542 567 16 16
nodes/raid/2/Error 559 567 16 16
nodes/raid/Default 576 567 16 16
nodes/screen/BottomLeft 593 567 16 16
nodes/screen/BottomMiddle 610 567 16 16
nodes/screen/BottomRight 627 567 16 16
nodes/screen/ColumnBottom 644 567 16 16
nodes/screen/ColumnMiddle 661 567 16 16
nodes/screen/ColumnTop 678 567 16 16
nodes/screen/Middle 695 567 16 16
nodes/screen/MiddleLeft 712 567 16 16
nodes/screen/MiddleRight 729 567 16 16
nodes/screen/PowerOnOverlay 746 567 16 16
nodes/screen/RowLeft 763 567 16 16
nodes/screen/RowMiddle 780 567 16 16
nodes/screen/RowRight 797 567 16 16
nodes/screen/Standalone 814 567 16 16
nodes/screen/TopLeft 831 567 16 16
nodes/screen/TopMiddle 848 567 16 16
nodes/screen/TopRight 865 567 16 16
panel/BorderB 327 314 4 4
panel/BorderL 387 314 4 2
panel/BorderR 332 314 4 4
@ -306,4 +307,13 @@ window/rack/SideConnector 291 319 1 3
window/rack/SideLeft 293 319 1 3
window/rack/SideRight 295 319 1 3
window/rack/SideTop 297 319 1 3
window/raid/Slots 134 526 66 26
window/raid/Slots 134 540 66 26
window/tape/Back 315 699 20 15
window/tape/BackPressed 336 699 20 15
window/tape/Forward 357 699 20 15
window/tape/ForwardPressed 378 699 20 15
window/tape/Play 399 699 20 15
window/tape/PlayPressed 420 699 20 15
window/tape/Screen 134 526 146 13
window/tape/Stop 441 699 20 15
window/tape/StopPressed 462 699 20 15

View File

@ -14,6 +14,10 @@ ocelot {
# Set to 0.0 to disable sound completely.
volumeMaster: 1.0
# Volume level for note block/sound card/tape drive sounds. Ranges from 0.0 to 1.0
# Set to 0.0 to disable sound completely.
volumeRecords: 1.0
# Volume level for computer case beeps. Ranges from 0.0 to 1.0
# Set to 0.0 to disable sound completely.
volumeBeep: 0.4
@ -21,6 +25,9 @@ ocelot {
# Volume level for environmental sounds (like computer fans or HDD activity). Ranges from 0.0 to 1.0
# Set to 0.0 to disable sound completely.
volumeEnvironment: 1.0
# Is it necessary to simulate the position of the sound relative to the workspace nodes or not
positional: true
}
window {

View File

@ -16,11 +16,14 @@ class Settings(val config: Config) extends SettingsData {
brainCustomConfigPath = config.getOptionalString("ocelot.brain.customConfigPath")
volumeMaster = (config.getDouble("ocelot.sound.volumeMaster") max 0 min 1).toFloat
volumeBeep = (config.getDouble("ocelot.sound.volumeBeep") max 0 min 1).toFloat
volumeEnvironment = (config.getDouble("ocelot.sound.volumeEnvironment") max 0 min 1).toFloat
volumeInterface = (config.getDoubleOrElse("ocelot.sound.volumeInterface", default = 0.5) max 0 min 1).toFloat
volumeMaster = (config.getDoubleOrElse("ocelot.sound.volumeMaster", 1) max 0 min 1).toFloat
volumeRecords = (config.getDoubleOrElse("ocelot.sound.volumeRecords", 1) max 0 min 1).toFloat
volumeBeep = (config.getDoubleOrElse("ocelot.sound.volumeBeep", 1) max 0 min 1).toFloat
volumeEnvironment = (config.getDoubleOrElse("ocelot.sound.volumeEnvironment", 1) max 0 min 1).toFloat
volumeInterface = (config.getDoubleOrElse("ocelot.sound.volumeInterface", 0.5) max 0 min 1).toFloat
audioDisable = config.getBooleanOrElse("ocelot.sound.audioDisable", default = false)
soundPositional = config.getBooleanOrElse("ocelot.sound.positional", default = true)
logAudioErrorStacktrace = config.getBooleanOrElse("ocelot.sound.logAudioErrorStacktrace", default = false)
scaleFactor = (config.getDoubleOrElse("ocelot.window.scaleFactor", default = 1) max 1 min 3).toFloat
@ -155,10 +158,12 @@ object Settings extends Logging {
val updatedConfig = settings.config
.withValuePreserveOrigin("ocelot.brain.customConfigPath", settings.brainCustomConfigPath)
.withValuePreserveOrigin("ocelot.sound.volumeMaster", settings.volumeMaster)
.withValuePreserveOrigin("ocelot.sound.volumeRecords", settings.volumeRecords)
.withValuePreserveOrigin("ocelot.sound.volumeBeep", settings.volumeBeep)
.withValuePreserveOrigin("ocelot.sound.volumeEnvironment", settings.volumeEnvironment)
.withValuePreserveOrigin("ocelot.sound.volumeInterface", settings.volumeInterface)
.withValuePreserveOrigin("ocelot.sound.audioDisable", settings.audioDisable)
.withValuePreserveOrigin("ocelot.sound.positional", settings.soundPositional)
.withValuePreserveOrigin("ocelot.sound.logAudioErrorStacktrace", settings.logAudioErrorStacktrace)
.withValuePreserveOrigin("ocelot.window.scaleFactor", settings.scaleFactor.doubleValue)
.withValue("ocelot.window.position", settings.windowPosition)

View File

@ -58,12 +58,10 @@ object Audio extends Logging {
}
AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
AL10W.alSourcef(
sourceId,
AL10.AL_GAIN,
source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
)
setPosition(sourceId, source)
setGain(sourceId, source)
sources.put(source, sourceId)
sourceId
@ -143,13 +141,9 @@ object Audio extends Logging {
}
AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
AL10W.alSource3f(sourceId, AL10.AL_POSITION, 0f, 0f, 0f)
setPosition(sourceId, source)
AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE)
AL10W.alSourcef(
sourceId,
AL10.AL_GAIN,
source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
)
setGain(sourceId, source)
AL10W.alSourcePlay(sourceId)
sources.put(source, sourceId)
@ -159,9 +153,8 @@ object Audio extends Logging {
def pauseSource(source: SoundSource): Unit = synchronized {
OpenAlException.ignoring {
if (Audio.isDisabled) {
if (Audio.isDisabled)
return
}
if (getSourceStatus(source) == SoundSource.Status.Paused)
return
@ -187,6 +180,18 @@ object Audio extends Logging {
}
}
private def setPosition(sourceId: Int, source: SoundSource): Unit = {
AL10W.alSource3f(sourceId, AL10.AL_POSITION, source.position.x, source.position.y, source.position.z)
}
private def setGain(sourceId: Int, source: SoundSource): Unit = {
AL10W.alSourcef(
sourceId,
AL10.AL_GAIN,
source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
)
}
def update(): Unit = synchronized {
if (isDisabled) return
@ -194,11 +199,8 @@ object Audio extends Logging {
OpenAlException.defaulting(false) {
cleanupSourceBuffers(sourceId)
AL10W.alSourcef(
sourceId,
AL10.AL_GAIN,
source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster
)
setPosition(sourceId, source)
setGain(sourceId, source)
AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
case AL10.AL_STOPPED =>

View File

@ -24,8 +24,15 @@ object SoundBuffers extends Resource {
load("/ocelot/desktop/sounds/machine/hdd_access5.ogg"),
load("/ocelot/desktop/sounds/machine/hdd_access6.ogg"),
)
lazy val MachineTapeButton: SoundBuffer = load("/ocelot/desktop/sounds/machine/tape_button.ogg")
lazy val MachineTapeEject: SoundBuffer = load("/ocelot/desktop/sounds/machine/tape_eject.ogg")
lazy val MachineTapeInsert: SoundBuffer = load("/ocelot/desktop/sounds/machine/tape_insert.ogg")
lazy val MachineTapeRewind: SoundBuffer = load("/ocelot/desktop/sounds/machine/tape_rewind.ogg")
lazy val InterfaceClick: SoundBuffer = load("/ocelot/desktop/sounds/interface/click.ogg")
lazy val InterfaceTick: SoundBuffer = load("/ocelot/desktop/sounds/interface/tick.ogg")
lazy val MinecraftClick: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/click.ogg")
lazy val MinecraftExplosion: SoundBuffer = load("/ocelot/desktop/sounds/minecraft/explosion.ogg")

View File

@ -4,10 +4,11 @@ import ocelot.desktop.Settings
//noinspection ScalaWeakerAccess
object SoundCategory extends Enumeration {
val Environment, Beep, Interface = Value
val Environment, Beep, Interface, Records = Value
def getSettingsValue(soundCategory: SoundCategory.Value): Float = soundCategory match {
case Environment => Settings.get.volumeEnvironment
case Records => Settings.get.volumeRecords
case Beep => Settings.get.volumeBeep
case Interface => Settings.get.volumeInterface
}

View File

@ -6,7 +6,12 @@ import org.lwjgl.openal.AL10
import java.nio.ByteBuffer
import scala.util.control.Exception.catching
case class SoundSamples(data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging {
case class SoundSamples(private var data: ByteBuffer, rate: Int, format: SoundSamples.Format.Value) extends Logging {
if (!data.isDirect) {
data = ByteBuffer.allocateDirect(data.remaining()).put(data)
data.flip()
}
def genBuffer(): Option[Int] = Audio.synchronized {
if (Audio.isDisabled) return None
@ -29,4 +34,4 @@ object SoundSamples {
object Format extends Enumeration {
val Stereo16, Mono8, Mono16 = Value
}
}
}

View File

@ -1,14 +1,18 @@
package ocelot.desktop.audio
import ocelot.desktop.geometry.Vector3D
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration
class SoundSource(val kind: SoundSource.Kind,
val soundCategory: SoundCategory.Value,
val looping: Boolean,
val pitch: Float,
var volume: Float) {
class SoundSource(
val kind: SoundSource.Kind,
val soundCategory: SoundCategory.Value,
val looping: Boolean,
val pitch: Float,
var volume: Float,
var position: Vector3D = Vector3D(0, 0, 0)
) {
def duration: Option[Duration] = kind match {
case SoundSource.KindSoundBuffer(buffer) =>
Some(Duration(buffer.numSamples.toFloat / buffer.sampleRate, TimeUnit.SECONDS))

View File

@ -82,7 +82,7 @@ class OpenFMRadio extends Entity with Environment with DeviceInfo with Logging {
Format.Mono16
// Creating Ocelot output sound stream
val (outputStream, outputSource) = Audio.newStream(SoundCategory.Environment, volume = volume)
val (outputStream, outputSource) = Audio.newStream(SoundCategory.Records, volume = volume)
playbackSoundSource = Option(outputSource)
// Reading chunks from input stream and passing them to output

View File

@ -165,4 +165,5 @@ object IconSource {
val Book: IconSource = IconSource("icons/Book")
val Help: IconSource = IconSource("icons/Help")
val Ocelot: IconSource = IconSource("icons/Ocelot")
val Guitar: IconSource = IconSource("icons/Guitar")
}

View File

@ -1,18 +1,18 @@
package ocelot.desktop.node
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import ocelot.desktop.audio._
import ocelot.desktop.entity.OcelotCard
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.node.ComputerAwareNode.{ErrorMessageMoveSpeed, LogParticleCount, LogParticleGrow, LogParticleMaxAngle, LogParticleMoveDistance, LogParticleMoveSpeed, LogParticlePadding, LogParticleSize, MaxErrorMessageDistance, MaxLogParticles}
import ocelot.desktop.node.ComputerAwareNode._
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.ComputerErrorMessageLabel
import ocelot.desktop.util.Messages
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import totoro.ocelot.brain.Settings
import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware}
import totoro.ocelot.brain.event._
@ -48,9 +48,9 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
}
}
// TODO: Scala has lazy vals. Use them.
private var soundCardStream: SoundStream = _
private var soundCardSource: SoundSource = _
private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records)
private def soundCardStream: SoundStream = soundCardSounds._1
private def soundCardSource: SoundSource = soundCardSounds._2
eventHandlers += {
case BrainEvent(event: MachineCrashEvent) =>
@ -74,11 +74,6 @@ abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceA
case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled =>
val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8)
if (soundCardStream == null) {
val (stream, source) = Audio.newStream(SoundCategory.Beep)
soundCardStream = stream
soundCardSource = source
}
soundCardStream.enqueue(samples)
soundCardSource.volume = event.volume

View File

@ -1,12 +1,8 @@
package ocelot.desktop.node
trait LabeledEntityNode extends EntityNode with LabeledNode {
protected def fallbackLabelAddress: Option[String] = Some(entity.node.address)
override def label: Option[String] =
super.label
.orElse(
Option(entity.node)
.flatMap(node => Option(node.address))
.orElse(Some("unknown"))
.filter(_ => exposeAddress)
)
super.label.orElse(if (exposeAddress) fallbackLabelAddress else None)
}

View File

@ -0,0 +1,42 @@
package ocelot.desktop.node
import ocelot.desktop.audio.SoundSource
import ocelot.desktop.geometry.Vector3D
import ocelot.desktop.{OcelotDesktop, Settings}
trait PositionalSoundSourcesNode extends Node {
// Every node can have multiple sound sources playing at the same time
def soundSources: Seq[SoundSource]
override def update(): Unit = {
super.update()
var soundPosition: Vector3D = null
// Calculating position of sound source relative to center of workspace
// but only if corresponding setting is enabled
if (Settings.get.soundPositional) {
val rootWidthHalf = OcelotDesktop.root.width / 2
val rootHeightHalf = OcelotDesktop.root.height / 2
val nodeCenterX = position.x + width / 2
val nodeCenterY = position.y + height / 2
// Limiting "significance" of the position as a percentage value because on
// large monitors the sound may become too "non-audiophile"
val limit = 0.05f
soundPosition = Vector3D(
(nodeCenterX - rootWidthHalf) / rootWidthHalf * limit,
(nodeCenterY - rootHeightHalf) / rootHeightHalf * limit,
0
)
}
else {
soundPosition = Vector3D.Zero
}
for (soundSource <- soundSources)
soundSource.position = soundPosition
}
}

View File

@ -48,6 +48,10 @@ class DiskDriveNode(entity: FloppyDiskDrive)
)
}
// -------------------------------- LabeledEntityNode --------------------------------
override def fallbackLabelAddress: Option[String] = entity.filesystemNode.map(_.address)
// ---------------------------- DiskDriveAware ----------------------------
override def floppyDiskDrive: FloppyDiskDrive = entity

View File

@ -1,18 +1,21 @@
package ocelot.desktop.node.nodes
import ocelot.desktop.graphics.IconSource
import ocelot.desktop.ui.event.ClickEvent
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuSubmenu}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry, ContextMenuIcon, ContextMenuSubmenu}
import totoro.ocelot.brain.entity.NoteBlock
class NoteBlockNode(val noteBlock: NoteBlock) extends NoteBlockNodeBase(noteBlock) {
override def icon: String = "nodes/NoteBlock"
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
menu.addEntry(new ContextMenuSubmenu("Instrument") {
menu.addEntry(new ContextMenuSubmenu("Instrument", Some(ContextMenuIcon(IconSource.Guitar))) {
{
val maxLen = NoteBlockNode.Instruments.map(_._2.length).max
for ((instrument, name) <- NoteBlockNode.Instruments) {
val dot = if (noteBlock.instrument == instrument) '•' else ' '
addEntry(ContextMenuEntry(name.padTo(maxLen, ' ') + dot) {
noteBlock.instrument = instrument
})

View File

@ -15,7 +15,7 @@ import scala.collection.mutable
abstract class NoteBlockNodeBase(entity: Entity with Environment) extends EntityNode(entity) with LabeledEntityNode {
eventHandlers += {
case BrainEvent(event: NoteBlockTriggerEvent) =>
SoundSource.fromBuffer(SoundBuffers.NoteBlock(event.instrument), SoundCategory.Beep,
SoundSource.fromBuffer(SoundBuffers.NoteBlock(event.instrument), SoundCategory.Records,
pitch = NoteBlockNode.Pitches(event.pitch), volume = event.volume.toFloat.min(1f).max(0f)).play()
addParticle(event.pitch)
}

View File

@ -5,7 +5,7 @@ import ocelot.desktop.inventory.item.HddItem
import ocelot.desktop.inventory.traits.DiskItem
import ocelot.desktop.inventory.{Item, SyncedInventory}
import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize}
import ocelot.desktop.node.{EntityNode, WindowedNode}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode}
import ocelot.desktop.ui.event.ClickEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.contextmenu.ContextMenu
@ -21,6 +21,7 @@ import scala.util.Random
class RaidNode(val raid: Raid) extends
EntityNode(raid)
with SyncedInventory
with LabeledEntityNode
with DiskActivityHandler
with DefaultSlotItemsFillable
with WindowedNode[RaidWindow]
@ -34,7 +35,6 @@ class RaidNode(val raid: Raid) extends
val x = position.x + HighlightThickness
val y = position.y + HighlightThickness
val activityIndex = Random.between(0, raid.getSizeInventory)
var prefix: String = null
for (i <- 0 until raid.getSizeInventory) {
@ -56,6 +56,10 @@ class RaidNode(val raid: Raid) extends
}
}
// Required for disk activity events processing
override def shouldReceiveEventsFor(address: String): Boolean =
super.shouldReceiveEventsFor(address) || raid.filesystem.exists(_.node.address == address)
override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = {
DiskItem.addRealPathContextMenuEntries(menu, raid, realPathSetter => {
realPathSetter()
@ -76,6 +80,10 @@ class RaidNode(val raid: Raid) extends
super.setupContextMenu(menu, event)
}
// -------------------------------- LabeledEntityNode --------------------------------
override def fallbackLabelAddress: Option[String] = raid.filesystem.map(_.node.address)
// -------------------------------- Inventory --------------------------------
override type I = Item with HddItem

View File

@ -1,22 +1,84 @@
package ocelot.desktop.node.nodes
import ocelot.desktop.audio._
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.inventory.item.TapeItem
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, WindowedNode}
import ocelot.desktop.node.{EntityNode, LabeledEntityNode, PositionalSoundSourcesNode, WindowedNode}
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.windows.TapeDriveWindow
import totoro.ocelot.brain.entity.TapeDrive
import totoro.ocelot.brain.entity.tape.{AudioPacketDfpwm, Dfpwm}
import totoro.ocelot.brain.entity.{TapeDrive, TapeDriveState}
import totoro.ocelot.brain.event.{TapeDriveAudioEvent, TapeDriveStopEvent}
class TapeDriveNode(entity: TapeDrive)
extends EntityNode(entity)
class TapeDriveNode(val tapeDrive: TapeDrive)
extends EntityNode(tapeDrive)
with SyncedInventory
with LabeledEntityNode
with WindowedNode[TapeDriveWindow] {
with PositionalSoundSourcesNode
with WindowedNode[TapeDriveWindow]
{
override def icon: String = "nodes/TapeDrive"
private lazy val soundTapeRewind: SoundSource = SoundSource.fromBuffer(
SoundBuffers.MachineTapeRewind,
SoundCategory.Records,
looping = true
)
private lazy val streams: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records)
private def stream: SoundStream = streams._1
private def source: SoundSource = streams._2
private val codec = new Dfpwm
eventHandlers += {
// FIXME: if the tape drive rapidly switches from Playing to another state and back to Playing,
// we may get TapeDriveStopEvent and TapeDriveAudioEvent during the same brain update cycle.
// however, since `source.stop()` defers removing the source until the next GUI update,
// it can potentially remove the newly queued buffers too.
case BrainEvent(_: TapeDriveStopEvent) => source.stop()
case BrainEvent(TapeDriveAudioEvent(_, pkt @ AudioPacketDfpwm(volume, frequency, _))) if !Audio.isDisabled =>
// FIXME: OpenAL does not enjoy mixing buffers with different sample rates.
// yet if the playback speed changes, it will affect the frequency.
// thus we'll be enqueueing a buffer with a different sample rate than the previous buffers.
// we'll get errors.
val samples = SoundSamples(pkt.decode(codec), frequency, SoundSamples.Format.Mono8)
stream.enqueue(samples)
source.volume = volume / 127f
}
override def update(): Unit = {
super.update()
val isRewinding = tapeDrive.state.state == TapeDriveState.State.Rewinding || tapeDrive.state.state == TapeDriveState.State.Forwarding
if (!isRewinding && soundTapeRewind.isPlaying) {
soundTapeRewind.stop()
} else if (isRewinding && !soundTapeRewind.isPlaying && !Audio.isDisabled) {
soundTapeRewind.play()
}
}
override def dispose(): Unit = {
super.dispose()
soundTapeRewind.stop()
}
// -------------------------------- PositionalSoundSourcesNode --------------------------------
override def soundSources: Seq[SoundSource] = Seq(source)
// -------------------------------- Inventory --------------------------------
override type I = TapeItem
override def brainInventory: TapeDrive = entity
override def brainInventory: TapeDrive = tapeDrive
// -------------------------------- Windowed --------------------------------
override protected def createWindow(): TapeDriveWindow = new TapeDriveWindow(this)
}

View File

@ -1,46 +1,65 @@
package ocelot.desktop.ui.widget.settings
import ocelot.desktop.Settings
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.IconSource
import ocelot.desktop.ui.widget.{PaddingBox, Slider}
import ocelot.desktop.ui.layout.LinearLayout
import ocelot.desktop.ui.widget.{Checkbox, Slider}
import ocelot.desktop.util.Orientation
class SoundSettingsTab extends SettingsTab {
override val layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 8)
override val icon: IconSource = IconSource.SettingsSound
override val label: String = "Sound"
children :+= new PaddingBox(new Slider(Settings.get.volumeMaster, "Master Volume") {
private def addSlider(name: String, value: Float, valueSetter: Float => Unit): Unit = {
children :+= new Slider(value, name) {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = {
valueSetter(value)
applySettings()
}
}
}
addSlider(
"Master volume",
Settings.get.volumeMaster,
Settings.get.volumeMaster = _
)
addSlider(
"Music blocks volume",
Settings.get.volumeRecords,
Settings.get.volumeRecords = _
)
addSlider(
"Environment volume",
Settings.get.volumeEnvironment,
Settings.get.volumeEnvironment = _
)
addSlider(
"Beep volume",
Settings.get.volumeBeep,
Settings.get.volumeBeep = _
)
addSlider(
"Interface volume",
Settings.get.volumeInterface,
Settings.get.volumeInterface = _
)
children :+= new Checkbox("Use positional sound", Settings.get.soundPositional) {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = {
Settings.get.volumeMaster = value
override def onValueChanged(value: Boolean): Unit = {
Settings.get.soundPositional = value
applySettings()
}
}, Padding2D(bottom = 8))
children :+= new PaddingBox(new Slider(Settings.get.volumeBeep, "Beep Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = {
Settings.get.volumeBeep = value
}
}, Padding2D(bottom = 8))
children :+= new PaddingBox(new Slider(Settings.get.volumeEnvironment, "Environment Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = {
Settings.get.volumeEnvironment = value
applySettings()
}
}, Padding2D(bottom = 8))
children :+= new PaddingBox(new Slider(Settings.get.volumeInterface, "Interface Volume") {
override def minimumSize: Size2D = Size2D(512, 24)
override def onValueChanged(value: Float): Unit = {
Settings.get.volumeInterface = value
applySettings()
}
}, Padding2D(bottom = 8))
}
}

View File

@ -4,6 +4,8 @@ import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.config.Configurator
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory
import java.nio.file.Files
trait LoggingConfiguration {
{
val builder = ConfigurationBuilderFactory.newConfigurationBuilder()
@ -17,6 +19,8 @@ trait LoggingConfiguration {
builder.add(consoleAppender)
Files.createDirectories(OcelotPaths.desktopLog.getParent)
// File
val fileAppender = builder.newAppender("file", "File")
fileAppender.addAttribute("fileName", OcelotPaths.desktopLog)

View File

@ -3,10 +3,11 @@ package ocelot.desktop.util
import org.apache.commons.lang3.SystemUtils
import java.nio.file.{Path, Paths}
import java.util.Objects
object OcelotPaths {
def windowsAppDataDirectoryName: String = System.getenv("APPDATA")
def linuxHomeDirectoryName: String = System.getProperty(SystemUtils.USER_HOME)
def windowsAppDataDirectoryName: String = Objects.requireNonNull(System.getenv("APPDATA"), "%APPDATA% is null")
def linuxHomeDirectoryName: String = Objects.requireNonNull(SystemUtils.USER_HOME, "USER_HOME is null")
def openComputersConfigName: String = {
if (SystemUtils.IS_OS_WINDOWS)

View File

@ -19,9 +19,11 @@ class SettingsData {
var volumeMaster: Float = 1f
var volumeBeep: Float = 1f
var volumeRecords: Float = 1f
var volumeEnvironment: Float = 1f
var volumeInterface: Float = 0.5f
var audioDisable: Boolean = false
var soundPositional: Boolean = false
var logAudioErrorStacktrace: Boolean = false
var scaleFactor: Float = 1f

View File

@ -1,19 +1,164 @@
package ocelot.desktop.windows
import ocelot.desktop.geometry.Padding2D
import ocelot.desktop.audio.{Audio, SoundBuffers, SoundCategory, SoundSource}
import ocelot.desktop.color.{Color, RGBAColorNorm}
import ocelot.desktop.geometry.{Padding2D, Size2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.inventory.item.TapeItem
import ocelot.desktop.node.nodes.TapeDriveNode
import ocelot.desktop.ui.widget.PaddingBox
import ocelot.desktop.ui.layout.{AlignItems, Layout, LinearLayout}
import ocelot.desktop.ui.widget.IconButton.{DefaultModel, ReadOnlyModel}
import ocelot.desktop.ui.widget.slot.SlotWidget
import ocelot.desktop.ui.widget.window.PanelWindow
import ocelot.desktop.ui.widget.{IconButton, PaddingBox, Widget}
import ocelot.desktop.util.Orientation
import totoro.ocelot.brain.entity.TapeDriveState
class TapeDriveWindow(host: TapeDriveNode) extends PanelWindow {
override protected def title: String = s"Tape Drive ${host.entity.node.address}"
class TapeDriveWindow(val tapeDriveNode: TapeDriveNode) extends PanelWindow {
private val playbackOverlayColor: RGBAColorNorm = RGBAColorNorm(1, 1, 1, 0.01f)
override def titleMaxLength: Int = 16
private lazy val tapeButtonSound: SoundSource = SoundSource.fromBuffer(SoundBuffers.MachineTapeButton, SoundCategory.Environment)
private lazy val tapeInsert: SoundSource = SoundSource.fromBuffer(SoundBuffers.MachineTapeInsert, SoundCategory.Environment)
private lazy val tapeEject: SoundSource = SoundSource.fromBuffer(SoundBuffers.MachineTapeEject, SoundCategory.Environment)
override protected def title: String = s"Tape Drive ${tapeDriveNode.tapeDrive.node.address}"
override def titleMaxLength: Int = 35
override def minimumSize: Size2D = Size2D(352, 182)
override def maximumSize: Size2D = minimumSize
setInner(new PaddingBox(
new SlotWidget[TapeItem](host.Slot(0)),
Padding2D(8, 64, 8, 64)
new Widget {
override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical, gap = 16, alignItems = AlignItems.Center)
// Screen
children :+= new Widget {
override def minimumSize: Size2D = Size2D(292, 26)
override def maximumSize: Size2D = minimumSize
override def draw(g: Graphics): Unit = {
// Screen background
g.sprite(
"window/tape/Screen",
bounds
)
// A barely noticeable overlay showing the playback progress
// Btw Computronix doesn't have this feature, so I won't ruin the canon and make it too annoying
val playedPart = tapeDriveNode.tapeDrive.position.toFloat / tapeDriveNode.tapeDrive.size.toFloat
if (playedPart > 0) {
val offset = 2
g.rect(
position.x + offset,
position.y + offset,
(width - offset * 2) * playedPart,
height - offset * 2,
playbackOverlayColor
)
}
// Screen text
g.background = Color.Transparent
var text: String = null
if (tapeDriveNode.tapeDrive.tape.isDefined) {
g.foreground = Color.White
text = tapeDriveNode.tapeDrive.storageName
if (text.isEmpty)
text = "Unnamed tape"
}
else {
g.foreground = Color.Red
text = "No tape"
}
// Clipping text if it's too long
val textMaxLength = 32
if (text.length > textMaxLength)
text = text.take(textMaxLength - 1) + "…"
// Finally drawing it
val textWidth = text.map(g.font.charWidth(_)).sum
val textHeight = 16
g.text(position.x + width / 2 - textWidth / 2, position.y + height / 2 - textHeight / 2, text)
}
}
// Slot
children :+= new SlotWidget[TapeItem](tapeDriveNode.Slot(0)) {
override def onItemAdded(): Unit = {
super.onItemAdded()
if (!Audio.isDisabled)
tapeInsert.play()
}
override def onItemRemoved(removedItem: TapeItem, replacedBy: Option[TapeItem]): Unit = {
super.onItemRemoved(removedItem, replacedBy)
if (!Audio.isDisabled)
tapeEject.play()
}
}
// Buttons
children :+= new Widget {
override protected val layout: Layout = new LinearLayout(this, gap = 0)
def addButton(
sprite: String,
pressedState: TapeDriveState.State,
isToggle: Boolean = true
): Unit = {
children :+= new IconButton(
s"window/tape/$sprite",
s"window/tape/${sprite}Pressed",
sizeMultiplier = 2,
mode =
if (isToggle)
IconButton.Mode.Switch
else
IconButton.Mode.Regular,
model =
if (isToggle)
ReadOnlyModel(tapeDriveNode.tapeDrive.state.state == pressedState)
else
DefaultModel(false),
) {
override def onPressed(): Unit = tapeDriveNode.tapeDrive.state.switchState(pressedState)
protected override def clickSoundSource: SoundSource = tapeButtonSound
}
}
addButton(
"Back",
TapeDriveState.State.Rewinding
)
addButton(
"Play",
TapeDriveState.State.Playing
)
addButton(
"Stop",
TapeDriveState.State.Stopped,
false
)
addButton(
"Forward",
TapeDriveState.State.Forwarding
)
}
},
Padding2D(top = 10, left = 18)
))
}