diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ee0f0f6 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 diff --git a/build.sbt b/build.sbt index 404a6d9..43a1cea 100644 --- a/build.sbt +++ b/build.sbt @@ -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(".")) diff --git a/lib/ocelot-brain b/lib/ocelot-brain index 2ef2cf3..2a52322 160000 --- a/lib/ocelot-brain +++ b/lib/ocelot-brain @@ -1 +1 @@ -Subproject commit 2ef2cf3bda8f05688c9801d859c1ab16088bd4c4 +Subproject commit 2a52322a780ac562316f449f8ce54227dd0586fd diff --git a/sprites/icons/Guitar.png b/sprites/icons/Guitar.png new file mode 100644 index 0000000..72cf10c Binary files /dev/null and b/sprites/icons/Guitar.png differ diff --git a/sprites/window/tape/Back.png b/sprites/window/tape/Back.png new file mode 100644 index 0000000..c62c1ed Binary files /dev/null and b/sprites/window/tape/Back.png differ diff --git a/sprites/window/tape/BackPressed.png b/sprites/window/tape/BackPressed.png new file mode 100644 index 0000000..d1e66cc Binary files /dev/null and b/sprites/window/tape/BackPressed.png differ diff --git a/sprites/window/tape/Forward.png b/sprites/window/tape/Forward.png new file mode 100644 index 0000000..5a9aaf7 Binary files /dev/null and b/sprites/window/tape/Forward.png differ diff --git a/sprites/window/tape/ForwardPressed.png b/sprites/window/tape/ForwardPressed.png new file mode 100644 index 0000000..ed0d9e0 Binary files /dev/null and b/sprites/window/tape/ForwardPressed.png differ diff --git a/sprites/window/tape/Play.png b/sprites/window/tape/Play.png new file mode 100644 index 0000000..0ac140d Binary files /dev/null and b/sprites/window/tape/Play.png differ diff --git a/sprites/window/tape/PlayPressed.png b/sprites/window/tape/PlayPressed.png new file mode 100644 index 0000000..8393833 Binary files /dev/null and b/sprites/window/tape/PlayPressed.png differ diff --git a/sprites/window/tape/Screen.png b/sprites/window/tape/Screen.png new file mode 100644 index 0000000..b053a56 Binary files /dev/null and b/sprites/window/tape/Screen.png differ diff --git a/sprites/window/tape/Stop.png b/sprites/window/tape/Stop.png new file mode 100644 index 0000000..878f216 Binary files /dev/null and b/sprites/window/tape/Stop.png differ diff --git a/sprites/window/tape/StopPressed.png b/sprites/window/tape/StopPressed.png new file mode 100644 index 0000000..d557e87 Binary files /dev/null and b/sprites/window/tape/StopPressed.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png index 234a185..4e21706 100644 Binary files a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png and b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.png differ diff --git a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt index 2c94834..bae38c2 100644 --- a/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/images/spritesheet/spritesheet.txt @@ -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 diff --git a/src/main/resources/ocelot/desktop/ocelot.conf b/src/main/resources/ocelot/desktop/ocelot.conf index ec44680..01c5cf0 100644 --- a/src/main/resources/ocelot/desktop/ocelot.conf +++ b/src/main/resources/ocelot/desktop/ocelot.conf @@ -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 { diff --git a/src/main/resources/ocelot/desktop/sounds/machine/tape_button.ogg b/src/main/resources/ocelot/desktop/sounds/machine/tape_button.ogg new file mode 100644 index 0000000..56ef81c Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/machine/tape_button.ogg differ diff --git a/src/main/resources/ocelot/desktop/sounds/machine/tape_eject.ogg b/src/main/resources/ocelot/desktop/sounds/machine/tape_eject.ogg new file mode 100644 index 0000000..475ab98 Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/machine/tape_eject.ogg differ diff --git a/src/main/resources/ocelot/desktop/sounds/machine/tape_insert.ogg b/src/main/resources/ocelot/desktop/sounds/machine/tape_insert.ogg new file mode 100644 index 0000000..c3d76a9 Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/machine/tape_insert.ogg differ diff --git a/src/main/resources/ocelot/desktop/sounds/machine/tape_rewind.ogg b/src/main/resources/ocelot/desktop/sounds/machine/tape_rewind.ogg new file mode 100644 index 0000000..aab934e Binary files /dev/null and b/src/main/resources/ocelot/desktop/sounds/machine/tape_rewind.ogg differ diff --git a/src/main/scala/ocelot/desktop/Settings.scala b/src/main/scala/ocelot/desktop/Settings.scala index 53391fb..4ee6466 100644 --- a/src/main/scala/ocelot/desktop/Settings.scala +++ b/src/main/scala/ocelot/desktop/Settings.scala @@ -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) diff --git a/src/main/scala/ocelot/desktop/audio/Audio.scala b/src/main/scala/ocelot/desktop/audio/Audio.scala index 1002152..434b2e3 100644 --- a/src/main/scala/ocelot/desktop/audio/Audio.scala +++ b/src/main/scala/ocelot/desktop/audio/Audio.scala @@ -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 => diff --git a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala index 7f202ac..1ac05a3 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundBuffers.scala @@ -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") diff --git a/src/main/scala/ocelot/desktop/audio/SoundCategory.scala b/src/main/scala/ocelot/desktop/audio/SoundCategory.scala index ae16313..0644f91 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundCategory.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundCategory.scala @@ -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 } diff --git a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala index db8d215..a174564 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSamples.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSamples.scala @@ -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 } -} \ No newline at end of file +} diff --git a/src/main/scala/ocelot/desktop/audio/SoundSource.scala b/src/main/scala/ocelot/desktop/audio/SoundSource.scala index 464eaa8..2ad92f2 100644 --- a/src/main/scala/ocelot/desktop/audio/SoundSource.scala +++ b/src/main/scala/ocelot/desktop/audio/SoundSource.scala @@ -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)) diff --git a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala index f674091..e924882 100644 --- a/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala +++ b/src/main/scala/ocelot/desktop/entity/OpenFMRadio.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/graphics/IconSource.scala b/src/main/scala/ocelot/desktop/graphics/IconSource.scala index aa0c837..dff6d29 100644 --- a/src/main/scala/ocelot/desktop/graphics/IconSource.scala +++ b/src/main/scala/ocelot/desktop/graphics/IconSource.scala @@ -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") } diff --git a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala index 1aaebdd..000ac12 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala +++ b/src/main/scala/ocelot/desktop/node/ComputerAwareNode.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/node/LabeledEntityNode.scala b/src/main/scala/ocelot/desktop/node/LabeledEntityNode.scala index 614ab4a..82ff6ee 100644 --- a/src/main/scala/ocelot/desktop/node/LabeledEntityNode.scala +++ b/src/main/scala/ocelot/desktop/node/LabeledEntityNode.scala @@ -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) } diff --git a/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala b/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala new file mode 100644 index 0000000..5314f8b --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/PositionalSoundSourcesNode.scala @@ -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 + } +} diff --git a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala index 25c94ab..fd118f1 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/DiskDriveNode.scala @@ -48,6 +48,10 @@ class DiskDriveNode(entity: FloppyDiskDrive) ) } + // -------------------------------- LabeledEntityNode -------------------------------- + + override def fallbackLabelAddress: Option[String] = entity.filesystemNode.map(_.address) + // ---------------------------- DiskDriveAware ---------------------------- override def floppyDiskDrive: FloppyDiskDrive = entity diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala index 9e0d713..ad1e6f1 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNode.scala @@ -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 }) diff --git a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala index 3b86dff..d330338 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/NoteBlockNodeBase.scala @@ -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) } diff --git a/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala b/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala index 3be6d17..e5c6c4e 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/RaidNode.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala b/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala index 8db0c4d..38cc6ea 100644 --- a/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/TapeDriveNode.scala @@ -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) } diff --git a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala index bc715f0..3422b26 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/settings/SoundSettingsTab.scala @@ -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)) + } } diff --git a/src/main/scala/ocelot/desktop/util/LoggingConfiguration.scala b/src/main/scala/ocelot/desktop/util/LoggingConfiguration.scala index c33237a..33bdb8c 100644 --- a/src/main/scala/ocelot/desktop/util/LoggingConfiguration.scala +++ b/src/main/scala/ocelot/desktop/util/LoggingConfiguration.scala @@ -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) diff --git a/src/main/scala/ocelot/desktop/util/OcelotPaths.scala b/src/main/scala/ocelot/desktop/util/OcelotPaths.scala index c6fea81..70ec6ee 100644 --- a/src/main/scala/ocelot/desktop/util/OcelotPaths.scala +++ b/src/main/scala/ocelot/desktop/util/OcelotPaths.scala @@ -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) diff --git a/src/main/scala/ocelot/desktop/util/SettingsData.scala b/src/main/scala/ocelot/desktop/util/SettingsData.scala index 5ff4ecf..153d463 100644 --- a/src/main/scala/ocelot/desktop/util/SettingsData.scala +++ b/src/main/scala/ocelot/desktop/util/SettingsData.scala @@ -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 diff --git a/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala b/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala index 0ab5483..d82bd0f 100644 --- a/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala +++ b/src/main/scala/ocelot/desktop/windows/TapeDriveWindow.scala @@ -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) )) }