From 63410017431d4163a1733403c596befcfd30ab88 Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Sun, 7 Sep 2014 16:10:04 -0500 Subject: [PATCH] Initial version: Based on last source available in SourceForge, converted to a Maven project format --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 2 + manifest.mf | 3 + nbactions.xml | 48 + pom.xml | 108 ++ src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/main/java/.DS_Store | Bin 0 -> 6148 bytes src/main/java/jace/.DS_Store | Bin 0 -> 6148 bytes src/main/java/jace/ConvertDiskImage.java | 128 ++ src/main/java/jace/Emulator.java | 180 +++ src/main/java/jace/EmulatorUILogic.java | 372 +++++ src/main/java/jace/JaceApplication.java | 38 + src/main/java/jace/JaceUIController.java | 38 + src/main/java/jace/apple2e/Apple2e.java | 433 ++++++ src/main/java/jace/apple2e/MOS65C02.java | 1313 +++++++++++++++++ src/main/java/jace/apple2e/RAM128k.java | 286 ++++ src/main/java/jace/apple2e/SoftSwitches.java | 180 +++ src/main/java/jace/apple2e/Speaker.java | 369 +++++ src/main/java/jace/apple2e/VideoDHGR.java | 747 ++++++++++ src/main/java/jace/apple2e/VideoNTSC.java | 433 ++++++ .../apple2e/softswitch/IntC8SoftSwitch.java | 83 ++ .../softswitch/KeyboardSoftSwitch.java | 50 + .../apple2e/softswitch/Memory2SoftSwitch.java | 56 + .../apple2e/softswitch/MemorySoftSwitch.java | 52 + .../apple2e/softswitch/VideoSoftSwitch.java | 52 + src/main/java/jace/applesoft/Command.java | 199 +++ src/main/java/jace/applesoft/Line.java | 141 ++ src/main/java/jace/applesoft/Program.java | 88 ++ src/main/java/jace/cheat/Cheats.java | 57 + src/main/java/jace/cheat/MemorySpy.form | 295 ++++ src/main/java/jace/cheat/MemorySpy.java | 395 +++++ src/main/java/jace/cheat/MemorySpyGrid.form | 42 + src/main/java/jace/cheat/MemorySpyGrid.java | 489 ++++++ src/main/java/jace/cheat/MetaCheatForm.form | 516 +++++++ src/main/java/jace/cheat/MetaCheatForm.java | 623 ++++++++ src/main/java/jace/cheat/MetaCheats.java | 320 ++++ .../java/jace/cheat/PrinceOfPersiaCheats.java | 455 ++++++ .../java/jace/config/BooleanComponent.java | 61 + src/main/java/jace/config/ClassSelection.java | 137 ++ .../java/jace/config/ConfigurableField.java | 47 + src/main/java/jace/config/Configuration.java | 512 +++++++ .../java/jace/config/ConfigurationPanel.form | 113 ++ .../java/jace/config/ConfigurationPanel.java | 280 ++++ .../jace/config/DynamicSelectComponent.java | 151 ++ .../java/jace/config/DynamicSelection.java | 62 + src/main/java/jace/config/FileComponent.java | 287 ++++ src/main/java/jace/config/ISelection.java | 37 + .../java/jace/config/IntegerComponent.java | 65 + .../java/jace/config/InvokableAction.java | 72 + src/main/java/jace/config/Name.java | 35 + src/main/java/jace/config/Reconfigurable.java | 29 + .../java/jace/config/StringComponent.java | 65 + src/main/java/jace/core/CPU.java | 167 +++ src/main/java/jace/core/Card.java | 171 +++ src/main/java/jace/core/Computer.java | 159 ++ src/main/java/jace/core/Debugger.java | 73 + src/main/java/jace/core/Device.java | 114 ++ src/main/java/jace/core/Font.java | 64 + src/main/java/jace/core/KeyHandler.java | 45 + src/main/java/jace/core/Keyboard.java | 346 +++++ src/main/java/jace/core/Motherboard.java | 217 +++ src/main/java/jace/core/PagedMemory.java | 146 ++ src/main/java/jace/core/Palette.java | 57 + src/main/java/jace/core/RAM.java | 295 ++++ src/main/java/jace/core/RAMEvent.java | 132 ++ src/main/java/jace/core/RAMListener.java | 166 +++ src/main/java/jace/core/SoftSwitch.java | 285 ++++ src/main/java/jace/core/SoundMixer.java | 264 ++++ src/main/java/jace/core/TimedDevice.java | 178 +++ src/main/java/jace/core/Utility.java | 552 +++++++ src/main/java/jace/core/Video.java | 299 ++++ src/main/java/jace/core/VideoWriter.java | 62 + .../java/jace/hardware/CardAppleMouse.java | 637 ++++++++ src/main/java/jace/hardware/CardDiskII.java | 220 +++ src/main/java/jace/hardware/CardExt80Col.java | 100 ++ .../jace/hardware/CardHayesMicromodem.java | 145 ++ .../java/jace/hardware/CardMockingboard.java | 407 +++++ .../java/jace/hardware/CardRamFactor.java | 234 +++ src/main/java/jace/hardware/CardRamworks.java | 153 ++ src/main/java/jace/hardware/CardSSC.java | 490 ++++++ .../java/jace/hardware/CardThunderclock.java | 329 +++++ src/main/java/jace/hardware/ConsoleProbe.java | 166 +++ .../jace/hardware/ConsoleProbeSimple.java | 99 ++ src/main/java/jace/hardware/DiskIIDrive.java | 279 ++++ src/main/java/jace/hardware/FloppyDisk.java | 420 ++++++ src/main/java/jace/hardware/Joystick.java | 279 ++++ .../jace/hardware/PassportMidiInterface.java | 551 +++++++ src/main/java/jace/hardware/ProdosDriver.java | 126 ++ .../java/jace/hardware/SmartportDriver.java | 124 ++ .../hardware/massStorage/CardMassStorage.java | 260 ++++ .../hardware/massStorage/DirectoryNode.java | 275 ++++ .../jace/hardware/massStorage/DiskNode.java | 259 ++++ .../jace/hardware/massStorage/FileNode.java | 199 +++ .../hardware/massStorage/FreespaceBitmap.java | 73 + .../java/jace/hardware/massStorage/IDisk.java | 42 + .../jace/hardware/massStorage/LargeDisk.java | 172 +++ .../massStorage/MassStorageDrive.java | 92 ++ .../massStorage/ProdosVirtualDisk.java | 217 +++ .../jace/hardware/massStorage/SubNode.java | 68 + .../hardware/mockingboard/AY8910_old.java | 638 ++++++++ .../mockingboard/EnvelopeGenerator.java | 111 ++ .../hardware/mockingboard/NoiseGenerator.java | 47 + .../java/jace/hardware/mockingboard/PSG.java | 276 ++++ .../jace/hardware/mockingboard/R6522.java | 313 ++++ .../hardware/mockingboard/SoundGenerator.java | 81 + .../hardware/mockingboard/TimedGenerator.java | 81 + .../jace/library/DiskTransferHandler.java | 59 + src/main/java/jace/library/DiskType.java | 80 + src/main/java/jace/library/DriveIcon.java | 61 + src/main/java/jace/library/MediaCache.java | 461 ++++++ src/main/java/jace/library/MediaConsumer.java | 37 + .../jace/library/MediaConsumerParent.java | 27 + src/main/java/jace/library/MediaEditUI.form | 384 +++++ src/main/java/jace/library/MediaEditUI.java | 442 ++++++ src/main/java/jace/library/MediaEntry.java | 64 + src/main/java/jace/library/MediaLibrary.java | 122 ++ .../java/jace/library/MediaLibraryUI.form | 198 +++ .../java/jace/library/MediaLibraryUI.java | 471 ++++++ .../java/jace/library/MediaManagerUI.form | 202 +++ .../java/jace/library/MediaManagerUI.java | 148 ++ src/main/java/jace/library/TocTreeModel.java | 144 ++ .../jace/library/TransferableMediaEntry.java | 48 + src/main/java/jace/state/ObjectGraphNode.java | 186 +++ src/main/java/jace/state/State.java | 102 ++ src/main/java/jace/state/StateManager.java | 409 +++++ src/main/java/jace/state/StateValue.java | 95 ++ src/main/java/jace/state/Stateful.java | 40 + src/main/java/jace/tracker/Command.java | 58 + src/main/java/jace/tracker/EditableLabel.java | 288 ++++ src/main/java/jace/tracker/Pattern.java | 30 + .../java/jace/tracker/PlaybackEngine.java | 90 ++ src/main/java/jace/tracker/Row.java | 219 +++ src/main/java/jace/tracker/Song.java | 44 + src/main/java/jace/tracker/SongPersistor.java | 40 + src/main/java/jace/tracker/UserInterface.java | 257 ++++ .../java/jace/ui/AbstractEmulatorFrame.form | 35 + .../java/jace/ui/AbstractEmulatorFrame.java | 496 +++++++ src/main/java/jace/ui/DebuggerPanel.form | 598 ++++++++ src/main/java/jace/ui/DebuggerPanel.java | 454 ++++++ src/main/java/jace/ui/EmulatorFrame.form | 82 + src/main/java/jace/ui/EmulatorFrame.java | 288 ++++ src/main/java/jace/ui/Library.java | 50 + src/main/java/jace/ui/MainFrame.form | 60 + src/main/java/jace/ui/MainFrame.java | 138 ++ src/main/java/jace/ui/OutlinedLabel.java | 81 + src/main/java/jace/ui/ScreenCanvas.java | 45 + src/main/java/jace/ui/ScreenPanel.java | 45 + src/main/resources/.DS_Store | Bin 0 -> 6148 bytes src/main/resources/fxml/JaceUI.fxml | 15 + src/main/resources/jace/data/35_floppy.png | Bin 0 -> 1132 bytes src/main/resources/jace/data/525_floppy.png | Bin 0 -> 881 bytes .../jace/data/6502_functional_test.bin | Bin 0 -> 65536 bytes src/main/resources/jace/data/DiskII.rom | Bin 0 -> 256 bytes src/main/resources/jace/data/RAMFactor14.rom | Bin 0 -> 8192 bytes src/main/resources/jace/data/SSC.rom | Bin 0 -> 2048 bytes src/main/resources/jace/data/apple2e.rom | Bin 0 -> 20480 bytes .../resources/jace/data/apple2e_debug.rom | Bin 0 -> 20480 bytes src/main/resources/jace/data/apple2plus.rom | Bin 0 -> 20480 bytes src/main/resources/jace/data/ayenvelope0.png | Bin 0 -> 747 bytes src/main/resources/jace/data/ayenvelope10.png | Bin 0 -> 755 bytes src/main/resources/jace/data/ayenvelope11.png | Bin 0 -> 897 bytes src/main/resources/jace/data/ayenvelope13.png | Bin 0 -> 508 bytes src/main/resources/jace/data/ayenvelope14.png | Bin 0 -> 787 bytes src/main/resources/jace/data/ayenvelope4.png | Bin 0 -> 810 bytes src/main/resources/jace/data/ayenvelope8.png | Bin 0 -> 966 bytes src/main/resources/jace/data/clock.png | Bin 0 -> 4657 bytes src/main/resources/jace/data/clock_fix.png | Bin 0 -> 5595 bytes src/main/resources/jace/data/disk_ii.png | Bin 0 -> 13149 bytes .../resources/jace/data/drive-harddisk.png | Bin 0 -> 3317 bytes src/main/resources/jace/data/envelopes.xcf | Bin 0 -> 13334 bytes src/main/resources/jace/data/font.gif | Bin 0 -> 4527 bytes src/main/resources/jace/data/harddrive.png | Bin 0 -> 2183 bytes src/main/resources/jace/data/input-mouse.png | Bin 0 -> 3497 bytes .../resources/jace/data/network-wired.png | Bin 0 -> 2661 bytes src/main/resources/jace/data/ram.png | Bin 0 -> 5520 bytes src/main/resources/jace/data/rom_image.png | Bin 0 -> 954 bytes .../resources/jace/data/thunderclock_plus.rom | Bin 0 -> 2048 bytes src/main/resources/jace/data/woz_figure.gif | Bin 0 -> 3252 bytes src/main/resources/styles/Styles.css | 3 + target/classes/.netbeans_automatic_build | 0 target/classes/fxml/JaceUI.fxml | 15 + target/classes/jace/data/35_floppy.png | Bin 0 -> 1132 bytes target/classes/jace/data/525_floppy.png | Bin 0 -> 881 bytes .../jace/data/6502_functional_test.bin | Bin 0 -> 65536 bytes target/classes/jace/data/DiskII.rom | Bin 0 -> 256 bytes target/classes/jace/data/RAMFactor14.rom | Bin 0 -> 8192 bytes target/classes/jace/data/SSC.rom | Bin 0 -> 2048 bytes target/classes/jace/data/apple2e.rom | Bin 0 -> 20480 bytes target/classes/jace/data/apple2e_debug.rom | Bin 0 -> 20480 bytes target/classes/jace/data/apple2plus.rom | Bin 0 -> 20480 bytes target/classes/jace/data/ayenvelope0.png | Bin 0 -> 747 bytes target/classes/jace/data/ayenvelope10.png | Bin 0 -> 755 bytes target/classes/jace/data/ayenvelope11.png | Bin 0 -> 897 bytes target/classes/jace/data/ayenvelope13.png | Bin 0 -> 508 bytes target/classes/jace/data/ayenvelope14.png | Bin 0 -> 787 bytes target/classes/jace/data/ayenvelope4.png | Bin 0 -> 810 bytes target/classes/jace/data/ayenvelope8.png | Bin 0 -> 966 bytes target/classes/jace/data/clock.png | Bin 0 -> 4657 bytes target/classes/jace/data/clock_fix.png | Bin 0 -> 5595 bytes target/classes/jace/data/disk_ii.png | Bin 0 -> 13149 bytes target/classes/jace/data/drive-harddisk.png | Bin 0 -> 3317 bytes target/classes/jace/data/envelopes.xcf | Bin 0 -> 13334 bytes target/classes/jace/data/font.gif | Bin 0 -> 4527 bytes target/classes/jace/data/harddrive.png | Bin 0 -> 2183 bytes target/classes/jace/data/input-mouse.png | Bin 0 -> 3497 bytes target/classes/jace/data/network-wired.png | Bin 0 -> 2661 bytes target/classes/jace/data/ram.png | Bin 0 -> 5520 bytes target/classes/jace/data/rom_image.png | Bin 0 -> 954 bytes .../classes/jace/data/thunderclock_plus.rom | Bin 0 -> 2048 bytes target/classes/jace/data/woz_figure.gif | Bin 0 -> 3252 bytes target/classes/styles/Styles.css | 3 + target/maven-archiver/pom.properties | 5 + .../compile/default-compile/createdFiles.lst | 348 +++++ .../compile/default-compile/inputFiles.lst | 128 ++ .../default-testCompile/inputFiles.lst | 0 target/test-classes/.netbeans_automatic_build | 0 217 files changed, 29994 insertions(+) create mode 100644 .DS_Store create mode 100644 manifest.mf create mode 100644 nbactions.xml create mode 100644 pom.xml create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store create mode 100644 src/main/java/.DS_Store create mode 100644 src/main/java/jace/.DS_Store create mode 100644 src/main/java/jace/ConvertDiskImage.java create mode 100644 src/main/java/jace/Emulator.java create mode 100644 src/main/java/jace/EmulatorUILogic.java create mode 100644 src/main/java/jace/JaceApplication.java create mode 100644 src/main/java/jace/JaceUIController.java create mode 100644 src/main/java/jace/apple2e/Apple2e.java create mode 100644 src/main/java/jace/apple2e/MOS65C02.java create mode 100644 src/main/java/jace/apple2e/RAM128k.java create mode 100644 src/main/java/jace/apple2e/SoftSwitches.java create mode 100644 src/main/java/jace/apple2e/Speaker.java create mode 100644 src/main/java/jace/apple2e/VideoDHGR.java create mode 100644 src/main/java/jace/apple2e/VideoNTSC.java create mode 100644 src/main/java/jace/apple2e/softswitch/IntC8SoftSwitch.java create mode 100644 src/main/java/jace/apple2e/softswitch/KeyboardSoftSwitch.java create mode 100644 src/main/java/jace/apple2e/softswitch/Memory2SoftSwitch.java create mode 100644 src/main/java/jace/apple2e/softswitch/MemorySoftSwitch.java create mode 100644 src/main/java/jace/apple2e/softswitch/VideoSoftSwitch.java create mode 100755 src/main/java/jace/applesoft/Command.java create mode 100755 src/main/java/jace/applesoft/Line.java create mode 100755 src/main/java/jace/applesoft/Program.java create mode 100644 src/main/java/jace/cheat/Cheats.java create mode 100644 src/main/java/jace/cheat/MemorySpy.form create mode 100644 src/main/java/jace/cheat/MemorySpy.java create mode 100644 src/main/java/jace/cheat/MemorySpyGrid.form create mode 100644 src/main/java/jace/cheat/MemorySpyGrid.java create mode 100644 src/main/java/jace/cheat/MetaCheatForm.form create mode 100644 src/main/java/jace/cheat/MetaCheatForm.java create mode 100644 src/main/java/jace/cheat/MetaCheats.java create mode 100644 src/main/java/jace/cheat/PrinceOfPersiaCheats.java create mode 100644 src/main/java/jace/config/BooleanComponent.java create mode 100644 src/main/java/jace/config/ClassSelection.java create mode 100644 src/main/java/jace/config/ConfigurableField.java create mode 100644 src/main/java/jace/config/Configuration.java create mode 100644 src/main/java/jace/config/ConfigurationPanel.form create mode 100644 src/main/java/jace/config/ConfigurationPanel.java create mode 100644 src/main/java/jace/config/DynamicSelectComponent.java create mode 100644 src/main/java/jace/config/DynamicSelection.java create mode 100644 src/main/java/jace/config/FileComponent.java create mode 100644 src/main/java/jace/config/ISelection.java create mode 100644 src/main/java/jace/config/IntegerComponent.java create mode 100644 src/main/java/jace/config/InvokableAction.java create mode 100644 src/main/java/jace/config/Name.java create mode 100644 src/main/java/jace/config/Reconfigurable.java create mode 100644 src/main/java/jace/config/StringComponent.java create mode 100644 src/main/java/jace/core/CPU.java create mode 100644 src/main/java/jace/core/Card.java create mode 100644 src/main/java/jace/core/Computer.java create mode 100644 src/main/java/jace/core/Debugger.java create mode 100644 src/main/java/jace/core/Device.java create mode 100644 src/main/java/jace/core/Font.java create mode 100644 src/main/java/jace/core/KeyHandler.java create mode 100644 src/main/java/jace/core/Keyboard.java create mode 100644 src/main/java/jace/core/Motherboard.java create mode 100644 src/main/java/jace/core/PagedMemory.java create mode 100644 src/main/java/jace/core/Palette.java create mode 100644 src/main/java/jace/core/RAM.java create mode 100644 src/main/java/jace/core/RAMEvent.java create mode 100644 src/main/java/jace/core/RAMListener.java create mode 100644 src/main/java/jace/core/SoftSwitch.java create mode 100644 src/main/java/jace/core/SoundMixer.java create mode 100644 src/main/java/jace/core/TimedDevice.java create mode 100644 src/main/java/jace/core/Utility.java create mode 100644 src/main/java/jace/core/Video.java create mode 100644 src/main/java/jace/core/VideoWriter.java create mode 100644 src/main/java/jace/hardware/CardAppleMouse.java create mode 100644 src/main/java/jace/hardware/CardDiskII.java create mode 100644 src/main/java/jace/hardware/CardExt80Col.java create mode 100644 src/main/java/jace/hardware/CardHayesMicromodem.java create mode 100644 src/main/java/jace/hardware/CardMockingboard.java create mode 100644 src/main/java/jace/hardware/CardRamFactor.java create mode 100644 src/main/java/jace/hardware/CardRamworks.java create mode 100644 src/main/java/jace/hardware/CardSSC.java create mode 100644 src/main/java/jace/hardware/CardThunderclock.java create mode 100644 src/main/java/jace/hardware/ConsoleProbe.java create mode 100644 src/main/java/jace/hardware/ConsoleProbeSimple.java create mode 100644 src/main/java/jace/hardware/DiskIIDrive.java create mode 100644 src/main/java/jace/hardware/FloppyDisk.java create mode 100644 src/main/java/jace/hardware/Joystick.java create mode 100644 src/main/java/jace/hardware/PassportMidiInterface.java create mode 100644 src/main/java/jace/hardware/ProdosDriver.java create mode 100644 src/main/java/jace/hardware/SmartportDriver.java create mode 100644 src/main/java/jace/hardware/massStorage/CardMassStorage.java create mode 100644 src/main/java/jace/hardware/massStorage/DirectoryNode.java create mode 100644 src/main/java/jace/hardware/massStorage/DiskNode.java create mode 100644 src/main/java/jace/hardware/massStorage/FileNode.java create mode 100644 src/main/java/jace/hardware/massStorage/FreespaceBitmap.java create mode 100644 src/main/java/jace/hardware/massStorage/IDisk.java create mode 100644 src/main/java/jace/hardware/massStorage/LargeDisk.java create mode 100644 src/main/java/jace/hardware/massStorage/MassStorageDrive.java create mode 100644 src/main/java/jace/hardware/massStorage/ProdosVirtualDisk.java create mode 100644 src/main/java/jace/hardware/massStorage/SubNode.java create mode 100644 src/main/java/jace/hardware/mockingboard/AY8910_old.java create mode 100644 src/main/java/jace/hardware/mockingboard/EnvelopeGenerator.java create mode 100644 src/main/java/jace/hardware/mockingboard/NoiseGenerator.java create mode 100644 src/main/java/jace/hardware/mockingboard/PSG.java create mode 100644 src/main/java/jace/hardware/mockingboard/R6522.java create mode 100644 src/main/java/jace/hardware/mockingboard/SoundGenerator.java create mode 100644 src/main/java/jace/hardware/mockingboard/TimedGenerator.java create mode 100644 src/main/java/jace/library/DiskTransferHandler.java create mode 100644 src/main/java/jace/library/DiskType.java create mode 100644 src/main/java/jace/library/DriveIcon.java create mode 100644 src/main/java/jace/library/MediaCache.java create mode 100644 src/main/java/jace/library/MediaConsumer.java create mode 100644 src/main/java/jace/library/MediaConsumerParent.java create mode 100644 src/main/java/jace/library/MediaEditUI.form create mode 100644 src/main/java/jace/library/MediaEditUI.java create mode 100644 src/main/java/jace/library/MediaEntry.java create mode 100644 src/main/java/jace/library/MediaLibrary.java create mode 100644 src/main/java/jace/library/MediaLibraryUI.form create mode 100644 src/main/java/jace/library/MediaLibraryUI.java create mode 100644 src/main/java/jace/library/MediaManagerUI.form create mode 100644 src/main/java/jace/library/MediaManagerUI.java create mode 100644 src/main/java/jace/library/TocTreeModel.java create mode 100644 src/main/java/jace/library/TransferableMediaEntry.java create mode 100644 src/main/java/jace/state/ObjectGraphNode.java create mode 100644 src/main/java/jace/state/State.java create mode 100644 src/main/java/jace/state/StateManager.java create mode 100644 src/main/java/jace/state/StateValue.java create mode 100644 src/main/java/jace/state/Stateful.java create mode 100644 src/main/java/jace/tracker/Command.java create mode 100644 src/main/java/jace/tracker/EditableLabel.java create mode 100644 src/main/java/jace/tracker/Pattern.java create mode 100644 src/main/java/jace/tracker/PlaybackEngine.java create mode 100644 src/main/java/jace/tracker/Row.java create mode 100644 src/main/java/jace/tracker/Song.java create mode 100644 src/main/java/jace/tracker/SongPersistor.java create mode 100644 src/main/java/jace/tracker/UserInterface.java create mode 100644 src/main/java/jace/ui/AbstractEmulatorFrame.form create mode 100644 src/main/java/jace/ui/AbstractEmulatorFrame.java create mode 100644 src/main/java/jace/ui/DebuggerPanel.form create mode 100644 src/main/java/jace/ui/DebuggerPanel.java create mode 100644 src/main/java/jace/ui/EmulatorFrame.form create mode 100644 src/main/java/jace/ui/EmulatorFrame.java create mode 100644 src/main/java/jace/ui/Library.java create mode 100644 src/main/java/jace/ui/MainFrame.form create mode 100644 src/main/java/jace/ui/MainFrame.java create mode 100644 src/main/java/jace/ui/OutlinedLabel.java create mode 100644 src/main/java/jace/ui/ScreenCanvas.java create mode 100644 src/main/java/jace/ui/ScreenPanel.java create mode 100644 src/main/resources/.DS_Store create mode 100644 src/main/resources/fxml/JaceUI.fxml create mode 100644 src/main/resources/jace/data/35_floppy.png create mode 100644 src/main/resources/jace/data/525_floppy.png create mode 100644 src/main/resources/jace/data/6502_functional_test.bin create mode 100644 src/main/resources/jace/data/DiskII.rom create mode 100644 src/main/resources/jace/data/RAMFactor14.rom create mode 100644 src/main/resources/jace/data/SSC.rom create mode 100644 src/main/resources/jace/data/apple2e.rom create mode 100644 src/main/resources/jace/data/apple2e_debug.rom create mode 100644 src/main/resources/jace/data/apple2plus.rom create mode 100644 src/main/resources/jace/data/ayenvelope0.png create mode 100644 src/main/resources/jace/data/ayenvelope10.png create mode 100644 src/main/resources/jace/data/ayenvelope11.png create mode 100644 src/main/resources/jace/data/ayenvelope13.png create mode 100644 src/main/resources/jace/data/ayenvelope14.png create mode 100644 src/main/resources/jace/data/ayenvelope4.png create mode 100644 src/main/resources/jace/data/ayenvelope8.png create mode 100644 src/main/resources/jace/data/clock.png create mode 100644 src/main/resources/jace/data/clock_fix.png create mode 100644 src/main/resources/jace/data/disk_ii.png create mode 100644 src/main/resources/jace/data/drive-harddisk.png create mode 100644 src/main/resources/jace/data/envelopes.xcf create mode 100644 src/main/resources/jace/data/font.gif create mode 100644 src/main/resources/jace/data/harddrive.png create mode 100644 src/main/resources/jace/data/input-mouse.png create mode 100644 src/main/resources/jace/data/network-wired.png create mode 100644 src/main/resources/jace/data/ram.png create mode 100644 src/main/resources/jace/data/rom_image.png create mode 100644 src/main/resources/jace/data/thunderclock_plus.rom create mode 100644 src/main/resources/jace/data/woz_figure.gif create mode 100644 src/main/resources/styles/Styles.css create mode 100644 target/classes/.netbeans_automatic_build create mode 100644 target/classes/fxml/JaceUI.fxml create mode 100644 target/classes/jace/data/35_floppy.png create mode 100644 target/classes/jace/data/525_floppy.png create mode 100644 target/classes/jace/data/6502_functional_test.bin create mode 100644 target/classes/jace/data/DiskII.rom create mode 100644 target/classes/jace/data/RAMFactor14.rom create mode 100644 target/classes/jace/data/SSC.rom create mode 100644 target/classes/jace/data/apple2e.rom create mode 100644 target/classes/jace/data/apple2e_debug.rom create mode 100644 target/classes/jace/data/apple2plus.rom create mode 100644 target/classes/jace/data/ayenvelope0.png create mode 100644 target/classes/jace/data/ayenvelope10.png create mode 100644 target/classes/jace/data/ayenvelope11.png create mode 100644 target/classes/jace/data/ayenvelope13.png create mode 100644 target/classes/jace/data/ayenvelope14.png create mode 100644 target/classes/jace/data/ayenvelope4.png create mode 100644 target/classes/jace/data/ayenvelope8.png create mode 100644 target/classes/jace/data/clock.png create mode 100644 target/classes/jace/data/clock_fix.png create mode 100644 target/classes/jace/data/disk_ii.png create mode 100644 target/classes/jace/data/drive-harddisk.png create mode 100644 target/classes/jace/data/envelopes.xcf create mode 100644 target/classes/jace/data/font.gif create mode 100644 target/classes/jace/data/harddrive.png create mode 100644 target/classes/jace/data/input-mouse.png create mode 100644 target/classes/jace/data/network-wired.png create mode 100644 target/classes/jace/data/ram.png create mode 100644 target/classes/jace/data/rom_image.png create mode 100644 target/classes/jace/data/thunderclock_plus.rom create mode 100644 target/classes/jace/data/woz_figure.gif create mode 100644 target/classes/styles/Styles.css create mode 100644 target/maven-archiver/pom.properties create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst create mode 100644 target/test-classes/.netbeans_automatic_build diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bc23809d4031d2e94918bf79fa9411d2744531b3 GIT binary patch literal 6148 zcmeHK!D`z;5Z$%iR>F{~&_jCLTP{A>*sYHhYI^D=ppqPt;7FE<1wv~qxjwiC9owYM zca-FZddSCQW_KJCoZLbPW(H>7Xf(6(K8YQV5JCpCq$7kUgm6F+Yc7}v1n;A+NsS_P zK#6MjZl||wsYSovZmHg<_HyZ6xXy>2?&oi@{`u?o z+4-Np|1K@f623?y?+90L1rF^vDTY}LzZg>~^dp?+na)okvRYtO$jA&Z1I)n2FhB>K z)7+RH@kW>dX5gt9!1F + + + run + + jar + + + -X + -e + process-classes + org.codehaus.mojo:exec-maven-plugin:1.2.1:exec + + + -classpath target/jace-2.0-SNAPSHOT.jar jace.Emulator + java + + + + debug + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:1.2.1:exec + + + -Xdebug -Xrunjdwp:transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath jace.Emulator + java + true + + + + profile + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:1.2.1:exec + + + -classpath %classpath jace.Emulator + java + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6c8e776 --- /dev/null +++ b/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + org.badvision + jace + 2.0-SNAPSHOT + jar + + jace + + + UTF-8 + jace.MainApp + + + + + Your Organisation + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + unpack-dependencies + package + + unpack-dependencies + + + system + junit,org.mockito,org.hamcrest + ${project.build.directory}/classes + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + unpack-dependencies + + package + + exec + + + ${java.home}/../bin/javafxpackager + + -createjar + -nocss2bin + -appclass + ${mainClass} + -srcdir + ${project.build.directory}/classes + -outdir + ${project.build.directory} + -outfile + ${project.build.finalName}.jar + + + + + default-cli + + exec + + + ${java.home}/bin/java + ${runfx.args} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + ${sun.boot.class.path}${path.separator}${java.home}/lib/jfxrt.jar + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + + ${java.home}/lib/jfxrt.jar + + + + + + + diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7b0d36729e2ee777a660f9e8c6709dd97bc2fb68 GIT binary patch literal 6148 zcmeH~F>V4u3`M`g7D#EfOgRk)$PGpaPQV2S&>%r5BKkQx-ySzvsH0W%E!l7ES!;KH zv9ktX>-*^w7y&HlPOLmk%$N_j;tOYdpMH*)!|itQBJHgMp3+B5_H$d10#ZNMM4T9irw zDe%t}uwk>?toc%Twm!X{*Y{cVb)%DUIm54?049DEf6&9YUwlEZ41Ihw+$ZCa@h6X)Lo@aaP z8SN|9V*s`}A0B}EG1^C)retP&UmZ5UN|LYIV^7GId!wugko_!<1Nx*JyEL| z5Cd}tZqr?R|9_(XGXKwu@UIxKVf(z@@RO>yES5Z5h*h;^Gzl*o9>se(_xJ9MlY-}<}k)AP{e`_<{QELs1s5!kr_aaYXtrD zGKggW*Y6YA6!?z};JYib3+A&HYkz)!!DSq!S+)Ac3p2&pxp~XBOLl2tv2@~fWZ%pD ztd}qUF;0Rv98SgnvTX6ut8I#AsE3ccxGC7CXXpvDdAu&J<5Cc=dfH~T%(iH54 zwm=LJ1Ha1vo(}>P(KVQBR7VFGH39$@A*=*^oF%YE7<3Kh8sQEI*QJ2El$#NQ>vD(- zljj=DHR^K4&G5m^lew8txOzIQFH}0?u12!N05R~D0i69n9PIx)KhOUvi3~A73`{2j zyx8=b4cMFMtrNS%UMoP)Kv6I+*ElW#N3~+e#a3JfRRVE=4xno=*9Z|1`XiudAVUoN GDFYwh)m;hz literal 0 HcmV?d00001 diff --git a/src/main/java/jace/.DS_Store b/src/main/java/jace/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 settings = new HashMap<>(); + if (args != null) { + for (int i = 0; i < args.length; i++) { + if (args[i].startsWith("-")) { + String key = args[i].substring(1); + if ((i + 1) < args.length) { + String val = args[i + 1]; + if (!val.startsWith("-")) { + settings.put(key, val); + i++; + } else { + settings.put(key, "true"); + } + } else { + settings.put(key, "true"); + } + } else { + System.err.println("Did not understand parameter " + args[i] + ", skipping."); + } + } + } + Configuration.applySettings(settings); + +// theApp = new MainFrame(); + theApp = new EmulatorFrame(); + try { + theApp.setIconImage(ImageIO.read(Emulator.class.getClassLoader().getResourceAsStream("jace/data/woz_figure.gif"))); + } catch (IOException ex) { + Logger.getLogger(Emulator.class.getName()).log(Level.SEVERE, null, ex); + } + //theApp.setBounds(new Rectangle((140*6),(192*3))); + theApp.setVisible(true); + theApp.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + theApp.setFocusTraversalKeysEnabled(false); + theApp.setTitle("Java Apple Computer Emulator"); + theApp.addKeyListener(computer.getKeyboard().getListener()); + theApp.addComponentListener(new ComponentListener() { + // theApp.screen.addComponentListener(new ComponentListener() { + @Override + public void componentResized(ComponentEvent e) { +// System.out.println("Screen resized"); + resizeVideo(); + } + + @Override + public void componentMoved(ComponentEvent e) { + resizeVideo(); + } + + @Override + public void componentShown(ComponentEvent e) { + } + + @Override + public void componentHidden(ComponentEvent e) { + } + }); + theApp.addWindowListener(new WindowListener() { + @Override + public void windowOpened(WindowEvent e) { + } + + @Override + public void windowClosing(WindowEvent e) { + } + + @Override + public void windowClosed(WindowEvent e) { + } + + @Override + public void windowIconified(WindowEvent e) { + Computer.getComputer().getVideo().suspend(); + } + + @Override + public void windowDeiconified(WindowEvent e) { + Computer.getComputer().getVideo().resume(); + resizeVideo(); + } + + @Override + public void windowActivated(WindowEvent e) { + resizeVideo(); + } + + @Override + public void windowDeactivated(WindowEvent e) { + resizeVideo(); + } + }); + EmulatorUILogic.registerDebugger(); + computer.getVideo().setScreen(theApp.getScreenGraphics()); + computer.coldStart(); + } + + public static void resizeVideo() { + AbstractEmulatorFrame window = getFrame(); + if (window != null) { + window.resizeVideo(); + } + } + + public static Component getScreen() { + AbstractEmulatorFrame window = getFrame(); + if (window != null) { + return window.getScreen(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/jace/EmulatorUILogic.java b/src/main/java/jace/EmulatorUILogic.java new file mode 100644 index 0000000..3f9de1f --- /dev/null +++ b/src/main/java/jace/EmulatorUILogic.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace; + +import jace.apple2e.MOS65C02; +import jace.apple2e.RAM128k; +import jace.apple2e.SoftSwitches; +import jace.config.ConfigurationPanel; +import jace.config.InvokableAction; +import jace.core.CPU; +import jace.core.Computer; +import jace.core.Debugger; +import jace.core.RAM; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import static jace.core.Utility.*; +import jace.library.MediaLibrary; +import jace.ui.AbstractEmulatorFrame; +import jace.ui.DebuggerPanel; +import java.awt.Color; +import java.awt.HeadlessException; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import javax.imageio.ImageIO; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * This class contains miscellaneous user-invoked actions such as debugger + * operations and running arbitrary files in the emulator. It is possible for + * these methods to be later refactored into more sensible locations. Created on + * April 16, 2007, 10:30 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class EmulatorUILogic { + + static Debugger debugger; + + static { + debugger = new Debugger() { + @Override + public void updateStatus() { + enableDebug(true); + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); + updateCPURegisters(cpu); + } + }; + } + + public static void updateCPURegisters(MOS65C02 cpu) { + DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel(); + debuggerPanel.valueA.setText(Integer.toHexString(cpu.A)); + debuggerPanel.valueX.setText(Integer.toHexString(cpu.X)); + debuggerPanel.valueY.setText(Integer.toHexString(cpu.Y)); + debuggerPanel.valuePC.setText(Integer.toHexString(cpu.getProgramCounter())); + debuggerPanel.valueSP.setText(Integer.toHexString(cpu.getSTACK())); + debuggerPanel.valuePC2.setText(cpu.getFlags()); + debuggerPanel.valueINST.setText(cpu.disassemble()); + } + + public static void enableDebug(boolean b) { + DebuggerPanel debuggerPanel = Emulator.getFrame().getDebuggerPanel(); + debugger.setActive(b); + debuggerPanel.enableDebug.setSelected(b); + debuggerPanel.setBackground( + b ? Color.RED : new Color(0, 0, 0x040)); + } + + public static void enableTrace(boolean b) { + Computer.getComputer().getCpu().setTraceEnabled(b); + } + + public static void stepForward() { + debugger.step = true; + } + + static void registerDebugger() { + Computer.getComputer().getCpu().setDebug(debugger); + } + + public static Integer getValidAddress(String s) { + try { + int addr = Integer.parseInt(s.toUpperCase(), 16); + if (addr >= 0 && addr < 0x10000) { + return addr; + } + return null; + } catch (NumberFormatException ex) { + return null; + } + } + public static List watches = new ArrayList<>(); + + public static void updateWatchList(final DebuggerPanel panel) { + java.awt.EventQueue.invokeLater(() -> { + watches.stream().forEach((oldWatch) -> { + Computer.getComputer().getMemory().removeListener(oldWatch); + }); + if (panel == null) { + return; + } + addWatch(panel.textW1, panel.valueW1); + addWatch(panel.textW2, panel.valueW2); + addWatch(panel.textW3, panel.valueW3); + addWatch(panel.textW4, panel.valueW4); + }); + } + + private static void addWatch(JTextField watch, final JLabel watchValue) { + final Integer address = getValidAddress(watch.getText()); + if (address != null) { + //System.out.println("Adding watch for "+Integer.toString(address, 16)); + RAMListener newListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(address); + } + + @Override + protected void doEvent(RAMEvent e) { + watchValue.setText(Integer.toHexString(e.getNewValue() & 0x0FF)); + } + }; + Computer.getComputer().getMemory().addListener(newListener); + watches.add(newListener); + // Print out the current value right away + byte b = Computer.getComputer().getMemory().readRaw(address); + watchValue.setText(Integer.toString(b & 0x0ff, 16)); + } else { + watchValue.setText("00"); + } + } + + public static void updateBreakpointList(final DebuggerPanel panel) { + java.awt.EventQueue.invokeLater(() -> { + Integer address; + debugger.getBreakpoints().clear(); + if (panel == null) { + return; + } + address = getValidAddress(panel.textBP1.getText()); + if (address != null) { + debugger.getBreakpoints().add(address); + } + address = getValidAddress(panel.textBP2.getText()); + if (address != null) { + debugger.getBreakpoints().add(address); + } + address = getValidAddress(panel.textBP3.getText()); + if (address != null) { + debugger.getBreakpoints().add(address); + } + address = getValidAddress(panel.textBP4.getText()); + if (address != null) { + debugger.getBreakpoints().add(address); + } + debugger.updateBreakpoints(); + }); + } + + @InvokableAction( + name = "BRUN file", + category = "file", + description = "Loads a binary file in memory and executes it. File should end with #06xxxx, where xxxx is the start address in hex", + alternatives = "Execute program;Load binary;Load program;Load rom;Play single-load game") + public static void runFile() { + Computer.pause(); + JFileChooser select = new JFileChooser(); + select.showDialog(Emulator.getFrame(), "Execute binary file"); + File binary = select.getSelectedFile(); + if (binary == null) { + Computer.resume(); + return; + } + runFile(binary); + } + + public static void runFile(File binary) { + String fileName = binary.getName().toLowerCase(); + try { + if (fileName.contains("#06")) { + String addressStr = fileName.substring(fileName.length() - 4); + int address = Integer.parseInt(addressStr, 16); + brun(binary, address); + } else if (fileName.contains("#fc")) { + gripe("BASIC not supported yet"); + } + } catch (NumberFormatException | IOException ex) { + } + Computer.getComputer().getCpu().resume(); + } + + public static void brun(File binary, int address) throws FileNotFoundException, IOException { + // If it was halted already, then it was initiated outside of an opcode execution + // If it was not yet halted, then it is the case that the CPU is processing another opcode + // So if that is the case, the program counter will need to be decremented here to compensate + // TODO: Find a better mousetrap for this one -- it's an ugly hack + Computer.pause(); + FileInputStream in = new FileInputStream(binary); + byte[] data = new byte[in.available()]; + in.read(data); + RAM ram = Computer.getComputer().getMemory(); + for (int i = 0; i < data.length; i++) { + ram.write(address + i, data[i], false, true); + } + CPU cpu = Computer.getComputer().getCpu(); + Computer.getComputer().getCpu().setProgramCounter(address); + Computer.resume(); + } + + @InvokableAction( + name = "Adjust display", + category = "display", + description = "Adjusts window size to 1:1 aspect ratio for optimal viewing.", + alternatives = "Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;") + static public void scaleIntegerRatio() { + AbstractEmulatorFrame frame = Emulator.getFrame(); + if (frame == null) { + return; + } + Computer.pause(); + frame.enforceIntegerRatio(); + Computer.resume(); + } + + @InvokableAction( + name = "Toggle Debug", + category = "debug", + description = "Show/hide the debug panel", + alternatives = "Show Debug;Hide Debug") + public static void toggleDebugPanel() { + AbstractEmulatorFrame frame = Emulator.getFrame(); + if (frame == null) { + return; + } + frame.setShowDebug(!frame.isShowDebug()); + frame.reconfigure(); + Emulator.resizeVideo(); + } + + public static void toggleFullscreen() { + AbstractEmulatorFrame frame = Emulator.getFrame(); + if (frame == null) { + return; + } + Computer.pause(); + frame.toggleFullscreen(); + Computer.resume(); + } + + @InvokableAction( + name = "Save Raw Screenshot", + category = "general", + description = "Save raw (RAM) format of visible screen", + alternatives = "screendump, raw screenshot") + public static void saveScreenshotRaw() throws FileNotFoundException, IOException { + SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss"); + String timestamp = df.format(new Date()); + String type; + int start = Computer.getComputer().getVideo().getCurrentWriter().actualWriter().getYOffset(0); + int len = 0; + if (start < 0x02000) { + // Lo-res or double-lores + len = 0x0400; + type = "gr"; + } else { + // Hi-res or double-hires + len = 0x02000; + type = "hgr"; + } + boolean dres = SoftSwitches._80COL.getState() && (SoftSwitches.DHIRES.getState() || start < 0x02000); + if (dres) { + type = "d" + type; + } + File outFile = new File("screen_" + type + "_a" + Integer.toHexString(start) + "_" + timestamp); + try (FileOutputStream out = new FileOutputStream(outFile)) { + RAM128k ram = (RAM128k) Computer.getComputer().memory; + Computer.pause(); + if (dres) { + for (int i = 0; i < len; i++) { + out.write(ram.getAuxVideoMemory().readByte(start + i)); + } + } + for (int i = 0; i < len; i++) { + out.write(ram.getMainMemory().readByte(start + i)); + } + } + System.out.println("Wrote screenshot to " + outFile.getAbsolutePath()); + } + + @InvokableAction( + name = "Save Screenshot", + category = "general", + description = "Save image of visible screen", + alternatives = "Save image,save framebuffer,screenshot") + public static void saveScreenshot() throws HeadlessException, IOException { + JFileChooser select = new JFileChooser(); + Computer.pause(); + BufferedImage i = Computer.getComputer().getVideo().getFrameBuffer(); + BufferedImage j = new BufferedImage(i.getWidth(), i.getHeight(), i.getType()); + j.getGraphics().drawImage(i, 0, 0, null); + select.showSaveDialog(Emulator.getFrame()); + File targetFile = select.getSelectedFile(); + if (targetFile == null) { + return; + } + String filename = targetFile.getName(); + System.out.println("Writing screenshot to " + filename); + String extension = filename.substring(filename.lastIndexOf(".") + 1); + ImageIO.write(j, extension, targetFile); + } + + public static final String CONFIGURATION_DIALOG_NAME = "Configuration"; + @InvokableAction( + name = "Configuration", + category = "general", + description = "Edit emulator configuraion", + alternatives = "Reconfigure,Preferences,Settings") + public static void showConfig() { + if (Emulator.getFrame().getModalDialogUI(CONFIGURATION_DIALOG_NAME) == null) { + JPanel ui = new ConfigurationPanel(); + Emulator.getFrame().registerModalDialog(ui, CONFIGURATION_DIALOG_NAME, null, false); + } + Emulator.getFrame().showDialog(CONFIGURATION_DIALOG_NAME); + } + + public static final String MEDIA_MANAGER_DIALOG_NAME = "Media Manager"; + public static final String MEDIA_MANAGER_EDIT_DIALOG_NAME = "Media Details"; + @InvokableAction( + name = "Media Manager", + category = "general", + description = "Show the media manager", + alternatives = "Insert disk;Eject disk;Browse;Download;Select") + public static void showMediaManager() { + if (Emulator.getFrame().getModalDialogUI(MEDIA_MANAGER_DIALOG_NAME) == null) { + Emulator.getFrame().registerModalDialog(MediaLibrary.getInstance().buildUserInterface(), MEDIA_MANAGER_DIALOG_NAME, null, false); + } + Emulator.getFrame().showDialog(MEDIA_MANAGER_DIALOG_NAME); + } + + public static boolean confirm(String message) { + return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(Emulator.getFrame(), message); + } +} \ No newline at end of file diff --git a/src/main/java/jace/JaceApplication.java b/src/main/java/jace/JaceApplication.java new file mode 100644 index 0000000..c7f86e6 --- /dev/null +++ b/src/main/java/jace/JaceApplication.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package jace; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * + * @author blurry + */ +public class JaceApplication extends Application { + + @Override + public void start(Stage stage) throws Exception { + Parent root = FXMLLoader.load(getClass().getResource("fxml/JaceUI.fxml")); + + Scene scene = new Scene(root); + + stage.setScene(scene); + stage.show(); + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + launch(args); + } + +} diff --git a/src/main/java/jace/JaceUIController.java b/src/main/java/jace/JaceUIController.java new file mode 100644 index 0000000..485b179 --- /dev/null +++ b/src/main/java/jace/JaceUIController.java @@ -0,0 +1,38 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package jace; + +import java.awt.Canvas; +import java.net.URL; +import java.util.ResourceBundle; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.layout.Region; + +/** + * + * @author blurry + */ +public class JaceUIController { + @FXML + private ResourceBundle resources; + + @FXML + private Canvas displayCanvas; + + @FXML + private Region notificationRegion; + + + @FXML + public void initialize() { + assert displayCanvas != null : "fx:id=\"displayCanvas\" was not injected: check your FXML file 'JaceUI.fxml'."; + assert notificationRegion != null : "fx:id=\"notificationRegion\" was not injected: check your FXML file 'JaceUI.fxml'."; + } + + +} diff --git a/src/main/java/jace/apple2e/Apple2e.java b/src/main/java/jace/apple2e/Apple2e.java new file mode 100644 index 0000000..be234ab --- /dev/null +++ b/src/main/java/jace/apple2e/Apple2e.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.Emulator; +import jace.cheat.Cheats; +import jace.config.ClassSelection; +import jace.config.ConfigurableField; +import jace.core.Card; +import jace.core.Computer; +import jace.core.Motherboard; +import jace.core.RAM; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.state.Stateful; +import jace.core.Video; +import jace.hardware.CardDiskII; +import jace.hardware.CardExt80Col; +import jace.hardware.ConsoleProbe; +import jace.hardware.Joystick; +import jace.hardware.massStorage.CardMassStorage; +import java.awt.Graphics; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Apple2e is a computer with a 65c02 CPU, 128k of bankswitched ram, + * double-hires graphics, and up to seven peripheral I/O cards installed. Pause + * and resume are implemented by the Motherboard class. This class provides + * overall configuration of the computer, but the actual operation of the + * computer and its timing characteristics are managed in the Motherboard class. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class Apple2e extends Computer { + + static int IRQ_VECTOR = 0x003F2; + public Motherboard motherboard; + @ConfigurableField(name = "Slot 1", shortName = "s1card") + public ClassSelection card1 = new ClassSelection(Card.class, null); + @ConfigurableField(name = "Slot 2", shortName = "s2card") +// public Class card2 = CardSSC.class; + public ClassSelection card2 = new ClassSelection(Card.class, null); + @ConfigurableField(name = "Slot 3", shortName = "s3card") + public ClassSelection card3 = new ClassSelection(Card.class, null); + @ConfigurableField(name = "Slot 4", shortName = "s4card") + public ClassSelection card4 = new ClassSelection(Card.class, null); + @ConfigurableField(name = "Slot 5", shortName = "s5card") + public ClassSelection card5 = new ClassSelection(Card.class, null); + @ConfigurableField(name = "Slot 6", shortName = "s6card") + public ClassSelection card6 = new ClassSelection(Card.class, CardDiskII.class); + @ConfigurableField(name = "Slot 7", shortName = "s7card") + public ClassSelection card7 = new ClassSelection(Card.class, CardMassStorage.class); + @ConfigurableField(name = "Debug rom", shortName = "debugRom", description = "Use debugger //e rom") + public boolean useDebugRom = false; + @ConfigurableField(name = "Console probe", description = "Enable console redirection (experimental!)") + public boolean useConsoleProbe = false; + private ConsoleProbe probe = new ConsoleProbe(); + @ConfigurableField(name = "Helpful hints", shortName = "hints") + public boolean enableHints = true; + @ConfigurableField(name = "Renderer", shortName = "video", description = "Video rendering implementation") + public ClassSelection videoRenderer = new ClassSelection(Video.class, VideoNTSC.class); + @ConfigurableField(name = "Aux Ram", shortName = "ram", description = "Aux ram card") + public ClassSelection ramCard = new ClassSelection(RAM128k.class, CardExt80Col.class); + public Joystick joystick1; + public Joystick joystick2; + @ConfigurableField(name = "Activate Cheats", shortName = "cheat", defaultValue = "") + public ClassSelection cheatEngine = new ClassSelection(Cheats.class, null); + public Cheats activeCheatEngine = null; + + /** + * Creates a new instance of Apple2e + */ + public Apple2e() { + super(); + try { + reconfigure(); + // Setup core resources + joystick1 = new Joystick(0); + joystick2 = new Joystick(1); + setCpu(new MOS65C02()); + reinitMotherboard(); + } catch (Throwable t) { + System.err.println("Unable to initalize virtual machine"); + t.printStackTrace(System.err); + } + } + + @Override + public String getName() { + return "Computer (Apple //e)"; + } + + private void reinitMotherboard() { + if (motherboard != null && motherboard.isRunning()) { + motherboard.suspend(); + } + motherboard = new Motherboard(); + motherboard.reconfigure(); + Motherboard.miscDevices.add(joystick1); + Motherboard.miscDevices.add(joystick2); + } + + @Override + public void coldStart() { + Computer.pause(); + reinitMotherboard(); + reboot(); + //getMemory().dump(); + for (SoftSwitches s : SoftSwitches.values()) { + s.getSwitch().reset(); + } + getMemory().configureActiveMemory(); + getVideo().configureVideoMode(); + getCpu().reset(); + for (Card c : getMemory().getAllCards()) { + if (c != null) { + c.reset(); + } + } + + Computer.resume(); + /* + getCpu().resume(); + getVideo().resume(); + */ + } + + public void reboot() { + RAM r = getMemory(); + r.write(IRQ_VECTOR, (byte) 0x00, false, true); + r.write(IRQ_VECTOR + 1, (byte) 0x00, false, true); + r.write(IRQ_VECTOR + 2, (byte) 0x00, false, true); + warmStart(); + } + + @Override + public void warmStart() { + boolean restart = Computer.pause(); + for (SoftSwitches s : SoftSwitches.values()) { + s.getSwitch().reset(); + } + getMemory().configureActiveMemory(); + getVideo().configureVideoMode(); + getCpu().reset(); + for (Card c : getMemory().getAllCards()) { + if (c != null) { + c.reset(); + } + } + getCpu().resume(); + Computer.resume(); + } + + private void insertCard(Class type, int slot) { + if (getMemory().getCard(slot) != null) { + if (getMemory().getCard(slot).getClass().equals(type)) { + return; + } + getMemory().removeCard(slot); + } + if (type != null) { + try { + getMemory().addCard(type.newInstance(), slot); + } catch (InstantiationException | IllegalAccessException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + @Override + public final void reconfigure() { + boolean restart = Computer.pause(); + + super.reconfigure(); + + RAM128k currentMemory = (RAM128k) getMemory(); + if (currentMemory != null && !(currentMemory.getClass().equals(ramCard.getValue()))) { + try { + RAM128k newMemory = (RAM128k) ramCard.getValue().newInstance(); + newMemory.copyFrom(currentMemory); + setMemory(newMemory); + } catch (InstantiationException | IllegalAccessException ex) { + } + } + if (getMemory() == null) { + try { + currentMemory = (RAM128k) ramCard.getValue().newInstance(); + } catch (InstantiationException | IllegalAccessException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + try { + setMemory(currentMemory); + for (SoftSwitches s : SoftSwitches.values()) { + s.getSwitch().register(); + } + } catch (Throwable ex) { + } + } + currentMemory.reconfigure(); + + try { + if (useConsoleProbe) { + probe.init(this); + } else { + probe.shutdown(); + } + + if (useDebugRom) { + loadRom("jace/data/apple2e_debug.rom"); + } else { + loadRom("jace/data/apple2e.rom"); + } + + if (getVideo() == null || getVideo().getClass() != videoRenderer.getValue()) { + Graphics g = null; + if (getVideo() != null) { + getVideo().suspend(); + g = getVideo().getScreen(); + } + try { + setVideo((Video) videoRenderer.getValue().newInstance()); + getVideo().configureVideoMode(); + getVideo().reconfigure(); + getVideo().setScreen(g); + Emulator.resizeVideo(); + getVideo().resume(); + } catch (InstantiationException | IllegalAccessException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + } + + // Add all new cards + insertCard(card1.getValue(), 1); + insertCard(card2.getValue(), 2); + insertCard(card3.getValue(), 3); + insertCard(card4.getValue(), 4); + insertCard(card5.getValue(), 5); + insertCard(card6.getValue(), 6); + insertCard(card7.getValue(), 7); + if (enableHints) { + enableHints(); + } else { + disableHints(); + } + getMemory().configureActiveMemory(); + + if (cheatEngine.getValue() == null) { + if (activeCheatEngine != null) { + activeCheatEngine.detach(); + } + activeCheatEngine = null; + } else { + boolean startCheats = true; + if (activeCheatEngine != null) { + if (activeCheatEngine.getClass().equals(cheatEngine.getValue())) { + startCheats = false; + } else { + activeCheatEngine.detach(); + activeCheatEngine = null; + } + } + if (startCheats) { + try { + activeCheatEngine = (Cheats) cheatEngine.getValue().newInstance(); + } catch (InstantiationException | IllegalAccessException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + activeCheatEngine.attach(); + } + } + } catch (IOException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + if (restart) { + Computer.resume(); + } + } + + @Override + protected void doPause() { + if (motherboard == null) { + return; + } + motherboard.pause(); + } + + @Override + protected void doResume() { + if (motherboard == null) { + return; + } + motherboard.resume(); + } + + @Override + protected boolean isRunning() { + if (motherboard == null) { + return false; + } + return motherboard.isRunning() && !motherboard.isPaused; + } + private List hints = new ArrayList<>(); + + private void enableHints() { + if (hints.isEmpty()) { + hints.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0FB63); + } + + @Override + protected void doEvent(RAMEvent e) { + if (getCpu().getProgramCounter() != getScopeStart()) { + return; + } + Thread t = new Thread(() -> { + try { + // Give the floppy drive time to start + Thread.sleep(1000); + if (getCpu().getProgramCounter() >> 8 != 0x0c6) { + return; + } + + int row = 2; + for (String s : new String[]{ + " Welcome to", + " _ __ ___ ____ ", + " | | / /\\ / / ` | |_ ", + " \\_|_| /_/--\\ \\_\\_, |_|__ ", + "", + " Java Apple Computer Emulator", + "", + " Presented by BLuRry", + " http://goo.gl/SnzqG", + "", + "Press F1 to insert disk in Slot 6, D1", + "Press F2 to insert disk in Slot 6, D2", + "Press F3 to insert HDV or 2MG in slot 7", + "Press F4 to open configuration", + "Press F5 to run raw binary program", + "Press F8 to correct the aspect ratio", + "Press F9 to toggle fullscreen", + "Press F10 to open/close the debugger", + "", + " If metacheat is enabled:", + "Press HOME to activate memory heatmap", + "Press END to activate metacheat search" + }) { + int addr = 0x0401 + VideoDHGR.calculateTextOffset(row++); + for (char c : s.toCharArray()) { + getMemory().write(addr++, (byte) (c | 0x080), false, true); + } + } + while (getCpu().getProgramCounter() >> 8 == 0x0c6) { + int x = (int) (Math.random() * 26.0) + 7; + int y = (int) (Math.random() * 4.0) + 3; + int addr = 0x0400 + VideoDHGR.calculateTextOffset(y) + x; + byte old = getMemory().readRaw(addr); + for (char c : "+xX*+".toCharArray()) { + if (getCpu().getProgramCounter() >> 8 != 0x0c6) { + break; + } + getMemory().write(addr, (byte) (c | 0x080), true, true); + Thread.sleep(100); + } + getMemory().write(addr, old, true, true); + } + } catch (InterruptedException ex) { + Logger.getLogger(Apple2e.class.getName()).log(Level.SEVERE, null, ex); + } + }); + t.setName("Startup Animation"); + t.start(); + } + }); + // Latch to the PRODOS SYNTAX CHECK parser + /* + hints.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() {setScopeStart(0x0a685);} + + @Override + protected void doEvent(RAMEvent e) { + String in = ""; + for (int i=0x0200; i < 0x0300; i++) { + char c = (char) (getMemory().readRaw(i) & 0x07f); + if (c == 0x0d) break; + in += c; + } + + System.err.println("Intercepted command: "+in); + } + }); + */ + } + hints.stream().forEach((hint) -> { + getMemory().addListener(hint); + }); + } + + private void disableHints() { + hints.stream().forEach((hint) -> { + getMemory().removeListener(hint); + }); + } + + @Override + public String getShortName() { + return "computer"; + } +} diff --git a/src/main/java/jace/apple2e/MOS65C02.java b/src/main/java/jace/apple2e/MOS65C02.java new file mode 100644 index 0000000..4e5712b --- /dev/null +++ b/src/main/java/jace/apple2e/MOS65C02.java @@ -0,0 +1,1313 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.config.ConfigurableField; +import jace.core.CPU; +import jace.core.Computer; +import jace.core.RAM; +import jace.core.RAMEvent.TYPE; +import jace.state.Stateful; + +/** + * This is a full implementation of a MOS-65c02 processor, including the BBR, + * BBS, RMB and SMB opcodes. It is possible that this will be later refactored + * into a core 6502 and a separate extended 65c02 so that undocumented 6502 + * opcodes could be supported but that's not on the table currently. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class MOS65C02 extends CPU { + + static public boolean readAddressTriggersEvent = true; + static private MOS65C02 cpu; + static int RESET_VECTOR = 0x00FFFC; + static int INT_VECTOR = 0x00FFFE; + @Stateful + public int A = 0x0FF; + @Stateful + public int X = 0x0FF; + @Stateful + public int Y = 0x0FF; + @Stateful + public int C = 1; + @Stateful + public boolean interruptSignalled = false; + @Stateful + public boolean Z = true; + @Stateful + public boolean I = true; + @Stateful + public boolean D = true; + @Stateful + public boolean B = true; + @Stateful + public boolean V = true; + @Stateful + public boolean N = true; + @Stateful + public int STACK = 0xFF; + @ConfigurableField(name = "BRK on bad opcode", description = "If on, unrecognized opcodes will be treated as BRK. Otherwise, they will be NOP") + public boolean breakOnBadOpcode = false; + @ConfigurableField(name = "Ext. opcode warnings", description = "If on, uses of 65c02 extended opcodes (or undocumented 6502 opcodes -- which will fail) will be logged to stdout for debugging purposes") + public boolean warnAboutExtendedOpcodes = false; + + private static RAM getMemory() { + return Computer.getComputer().getMemory(); + } + + @Override + public void reconfigure() { + } + + public enum OPCODE { + ADC_IMM(0x0069, COMMAND.ADC, MODE.IMMEDIATE, 2), + ADC_ZP(0x0065, COMMAND.ADC, MODE.ZEROPAGE, 3), + ADC_ZP_X(0x0075, COMMAND.ADC, MODE.ZEROPAGE_X, 4), + ADC_AB(0x006D, COMMAND.ADC, MODE.ABSOLUTE, 4), + ADC_IND_ZP(0x0072, COMMAND.ADC, MODE.INDIRECT_ZP, 5, true), + ADC_IND_ZP_X(0x0061, COMMAND.ADC, MODE.INDIRECT_ZP_X, 6), + ADC_AB_X(0x007D, COMMAND.ADC, MODE.ABSOLUTE_X, 4), + ADC_AB_Y(0x0079, COMMAND.ADC, MODE.ABSOLUTE_Y, 4), + ADC_IND_ZP_Y(0x0071, COMMAND.ADC, MODE.INDIRECT_ZP_Y, 5), + AND_IMM(0x0029, COMMAND.AND, MODE.IMMEDIATE, 2), + AND_ZP(0x0025, COMMAND.AND, MODE.ZEROPAGE, 3), + AND_ZP_X(0x0035, COMMAND.AND, MODE.ZEROPAGE_X, 4), + AND_AB(0x002D, COMMAND.AND, MODE.ABSOLUTE, 4), + AND_IND_ZP(0x0032, COMMAND.AND, MODE.INDIRECT_ZP, 5, true), + AND_IND_ZP_X(0x0021, COMMAND.AND, MODE.INDIRECT_ZP_X, 6), + AND_AB_X(0x003D, COMMAND.AND, MODE.ABSOLUTE_X, 4), + AND_AB_Y(0x0039, COMMAND.AND, MODE.ABSOLUTE_Y, 4), + AND_IND_ZP_Y(0x0031, COMMAND.AND, MODE.INDIRECT_ZP_Y, 5), + ASL(0x000A, COMMAND.ASL_A, MODE.IMPLIED, 2), + ASL_ZP(0x0006, COMMAND.ASL, MODE.ZEROPAGE, 5), + ASL_ZP_X(0x0016, COMMAND.ASL, MODE.ZEROPAGE_X, 6), + ASL_AB(0x000E, COMMAND.ASL, MODE.ABSOLUTE, 6), + ASL_AB_X(0x001E, COMMAND.ASL, MODE.ABSOLUTE_X, 7), + BCC_REL(0x0090, COMMAND.BCC, MODE.RELATIVE, 2), + BCS_REL(0x00B0, COMMAND.BCS, MODE.RELATIVE, 2), + BBR0(0x00f, COMMAND.BBR0, MODE.ZP_REL, 5, true), + BBR1(0x01f, COMMAND.BBR1, MODE.ZP_REL, 5, true), + BBR2(0x02f, COMMAND.BBR2, MODE.ZP_REL, 5, true), + BBR3(0x03f, COMMAND.BBR3, MODE.ZP_REL, 5, true), + BBR4(0x04f, COMMAND.BBR4, MODE.ZP_REL, 5, true), + BBR5(0x05f, COMMAND.BBR5, MODE.ZP_REL, 5, true), + BBR6(0x06f, COMMAND.BBR6, MODE.ZP_REL, 5, true), + BBR7(0x07f, COMMAND.BBR7, MODE.ZP_REL, 5, true), + BBS0(0x08f, COMMAND.BBS0, MODE.ZP_REL, 5, true), + BBS1(0x09f, COMMAND.BBS1, MODE.ZP_REL, 5, true), + BBS2(0x0af, COMMAND.BBS2, MODE.ZP_REL, 5, true), + BBS3(0x0bf, COMMAND.BBS3, MODE.ZP_REL, 5, true), + BBS4(0x0cf, COMMAND.BBS4, MODE.ZP_REL, 5, true), + BBS5(0x0df, COMMAND.BBS5, MODE.ZP_REL, 5, true), + BBS6(0x0ef, COMMAND.BBS6, MODE.ZP_REL, 5, true), + BBS7(0x0ff, COMMAND.BBS7, MODE.ZP_REL, 5, true), + BEQ_REL0(0x00F0, COMMAND.BEQ, MODE.RELATIVE, 2), + BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 3, true), + BIT_ZP(0x0024, COMMAND.BIT, MODE.ZEROPAGE, 3), + BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 3, true), + BIT_AB(0x002C, COMMAND.BIT, MODE.ABSOLUTE, 4), + BIT_AB_X(0x003C, COMMAND.BIT, MODE.ABSOLUTE_X, 4, true), + BMI_REL(0x0030, COMMAND.BMI, MODE.RELATIVE, 2), + BNE_REL(0x00D0, COMMAND.BNE, MODE.RELATIVE, 2), + BPL_REL(0x0010, COMMAND.BPL, MODE.RELATIVE, 2), + BRA_REL(0x0080, COMMAND.BRA, MODE.RELATIVE, 2, true), + // BRK(0x0000, COMMAND.BRK, MODE.IMPLIED, 7), + // Do this so that BRK is treated as a two-byte instruction + BRK(0x0000, COMMAND.BRK, MODE.IMMEDIATE, 7), + BVC_REL(0x0050, COMMAND.BVC, MODE.RELATIVE, 2), + BVS_REL(0x0070, COMMAND.BVS, MODE.RELATIVE, 2), + CLC(0x0018, COMMAND.CLC, MODE.IMPLIED, 2), + CLD(0x00D8, COMMAND.CLD, MODE.IMPLIED, 2), + CLI(0x0058, COMMAND.CLI, MODE.IMPLIED, 2), + CLV(0x00B8, COMMAND.CLV, MODE.IMPLIED, 2), + CMP_IMM(0x00C9, COMMAND.CMP, MODE.IMMEDIATE, 2), + CMP_ZP(0x00C5, COMMAND.CMP, MODE.ZEROPAGE, 3), + CMP_ZP_X(0x00D5, COMMAND.CMP, MODE.ZEROPAGE_X, 4), + CMP_AB(0x00CD, COMMAND.CMP, MODE.ABSOLUTE, 4), + CMP_IND_ZP_X(0x00C1, COMMAND.CMP, MODE.INDIRECT_ZP_X, 6), + CMP_AB_X(0x00DD, COMMAND.CMP, MODE.ABSOLUTE_X, 4), + CMP_AB_Y(0x00D9, COMMAND.CMP, MODE.ABSOLUTE_Y, 4), + CMP_IND_ZP_Y(0x00D1, COMMAND.CMP, MODE.INDIRECT_ZP_Y, 5), + CMP_IND_ZP(0x00D2, COMMAND.CMP, MODE.INDIRECT_ZP, 5, true), + CPX_IMM(0x00E0, COMMAND.CPX, MODE.IMMEDIATE, 2), + CPX_ZP(0x00E4, COMMAND.CPX, MODE.ZEROPAGE, 3), + CPX_AB(0x00EC, COMMAND.CPX, MODE.ABSOLUTE, 4), + CPY_IMM(0x00C0, COMMAND.CPY, MODE.IMMEDIATE, 2), + CPY_ZP(0x00C4, COMMAND.CPY, MODE.ZEROPAGE, 3), + CPY_AB(0x00CC, COMMAND.CPY, MODE.ABSOLUTE, 4), + DEC(0x003A, COMMAND.DEA, MODE.IMPLIED, 2, true), + DEC_ZP(0x00C6, COMMAND.DEC, MODE.ZEROPAGE, 5), + DEC_ZP_X(0x00D6, COMMAND.DEC, MODE.ZEROPAGE_X, 6), + DEC_AB(0x00CE, COMMAND.DEC, MODE.ABSOLUTE, 6), + DEC_AB_X(0x00DE, COMMAND.DEC, MODE.ABSOLUTE_X, 7), + DEX(0x00CA, COMMAND.DEX, MODE.IMPLIED, 2), + DEY(0x0088, COMMAND.DEY, MODE.IMPLIED, 2), + EOR_IMM(0x0049, COMMAND.EOR, MODE.IMMEDIATE, 2), + EOR_ZP(0x0045, COMMAND.EOR, MODE.ZEROPAGE, 3), + EOR_ZP_X(0x0055, COMMAND.EOR, MODE.ZEROPAGE_X, 4), + EOR_AB(0x004D, COMMAND.EOR, MODE.ABSOLUTE, 4), + EOR_IND_ZP(0x0052, COMMAND.EOR, MODE.INDIRECT_ZP, 5, true), + EOR_IND_ZP_X(0x0041, COMMAND.EOR, MODE.INDIRECT_ZP_X, 6), + EOR_AB_X(0x005D, COMMAND.EOR, MODE.ABSOLUTE_X, 4), + EOR_AB_Y(0x0059, COMMAND.EOR, MODE.ABSOLUTE_Y, 4), + EOR_IND_ZP_Y(0x0051, COMMAND.EOR, MODE.INDIRECT_ZP_Y, 5), + INC(0x001A, COMMAND.INA, MODE.IMPLIED, 2, true), + INC_ZP(0x00E6, COMMAND.INC, MODE.ZEROPAGE, 5), + INC_ZP_X(0x00F6, COMMAND.INC, MODE.ZEROPAGE_X, 6), + INC_AB(0x00EE, COMMAND.INC, MODE.ABSOLUTE, 6), + INC_AB_X(0x00FE, COMMAND.INC, MODE.ABSOLUTE_X, 7), + INX(0x00E8, COMMAND.INX, MODE.IMPLIED, 2), + INY(0x00C8, COMMAND.INY, MODE.IMPLIED, 2), + JMP_AB(0x004C, COMMAND.JMP, MODE.ABSOLUTE, 3), + JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 5), + JMP_IND_X(0x007C, COMMAND.JMP, MODE.INDIRECT_X, 6, true), + JSR_AB(0x0020, COMMAND.JSR, MODE.ABSOLUTE, 6), + LDA_IMM(0x00A9, COMMAND.LDA, MODE.IMMEDIATE, 2), + LDA_ZP(0x00A5, COMMAND.LDA, MODE.ZEROPAGE, 3), + LDA_ZP_X(0x00B5, COMMAND.LDA, MODE.ZEROPAGE_X, 4), + LDA_AB(0x00AD, COMMAND.LDA, MODE.ABSOLUTE, 4), + LDA_IND_ZP_X(0x00A1, COMMAND.LDA, MODE.INDIRECT_ZP_X, 6), + LDA_AB_X(0x00BD, COMMAND.LDA, MODE.ABSOLUTE_X, 4), + LDA_AB_Y(0x00B9, COMMAND.LDA, MODE.ABSOLUTE_Y, 4), + LDA_IND_ZP_Y(0x00B1, COMMAND.LDA, MODE.INDIRECT_ZP_Y, 5), + LDA_IND_ZP(0x00B2, COMMAND.LDA, MODE.INDIRECT_ZP, 5, true), + LDX_IMM(0x00A2, COMMAND.LDX, MODE.IMMEDIATE, 2), + LDX_ZP(0x00A6, COMMAND.LDX, MODE.ZEROPAGE, 3), + LDX_ZP_Y(0x00B6, COMMAND.LDX, MODE.ZEROPAGE_Y, 4), + LDX_AB(0x00AE, COMMAND.LDX, MODE.ABSOLUTE, 4), + LDX_AB_Y(0x00BE, COMMAND.LDX, MODE.ABSOLUTE_Y, 4), + LDY_IMM(0x00A0, COMMAND.LDY, MODE.IMMEDIATE, 2), + LDY_ZP(0x00A4, COMMAND.LDY, MODE.ZEROPAGE, 3), + LDY_ZP_X(0x00B4, COMMAND.LDY, MODE.ZEROPAGE_X, 4), + LDY_AB(0x00AC, COMMAND.LDY, MODE.ABSOLUTE, 4), + LDY_AB_X(0x00BC, COMMAND.LDY, MODE.ABSOLUTE_X, 4), + LSR(0x004A, COMMAND.LSR_A, MODE.IMPLIED, 2), + LSR_ZP(0x0046, COMMAND.LSR, MODE.ZEROPAGE, 5), + LSR_ZP_X(0x0056, COMMAND.LSR, MODE.ZEROPAGE_X, 6), + LSR_AB(0x004E, COMMAND.LSR, MODE.ABSOLUTE, 6), + LSR_AB_X(0x005E, COMMAND.LSR, MODE.ABSOLUTE_X, 7), + NOP(0x00EA, COMMAND.NOP, MODE.IMPLIED, 2), + ORA_IMM(0x0009, COMMAND.ORA, MODE.IMMEDIATE, 2), + ORA_ZP(0x0005, COMMAND.ORA, MODE.ZEROPAGE, 3), + ORA_ZP_X(0x0015, COMMAND.ORA, MODE.ZEROPAGE_X, 4), + ORA_AB(0x000D, COMMAND.ORA, MODE.ABSOLUTE, 4), + ORA_IND_ZP(0x0012, COMMAND.ORA, MODE.INDIRECT_ZP, 5, true), + ORA_IND_ZP_X(0x0001, COMMAND.ORA, MODE.INDIRECT_ZP_X, 6), + ORA_AB_X(0x001D, COMMAND.ORA, MODE.ABSOLUTE_X, 4), + ORA_AB_Y(0x0019, COMMAND.ORA, MODE.ABSOLUTE_Y, 4), + ORA_IND_ZP_Y(0x0011, COMMAND.ORA, MODE.INDIRECT_ZP_Y, 5), + PHA(0x0048, COMMAND.PHA, MODE.IMPLIED, 3), + PHP(0x0008, COMMAND.PHP, MODE.IMPLIED, 3), + PHX(0x00DA, COMMAND.PHX, MODE.IMPLIED, 3, true), + PHY(0x005A, COMMAND.PHY, MODE.IMPLIED, 3, true), + PLA(0x0068, COMMAND.PLA, MODE.IMPLIED, 4), + PLP(0x0028, COMMAND.PLP, MODE.IMPLIED, 4), + PLX(0x00FA, COMMAND.PLX, MODE.IMPLIED, 4, true), + PLY(0x007A, COMMAND.PLY, MODE.IMPLIED, 4, true), + RMB0(0x007, COMMAND.RMB0, MODE.ZEROPAGE, 5, true), + RMB1(0x017, COMMAND.RMB1, MODE.ZEROPAGE, 5, true), + RMB2(0x027, COMMAND.RMB2, MODE.ZEROPAGE, 5, true), + RMB3(0x037, COMMAND.RMB3, MODE.ZEROPAGE, 5, true), + RMB4(0x047, COMMAND.RMB4, MODE.ZEROPAGE, 5, true), + RMB5(0x057, COMMAND.RMB5, MODE.ZEROPAGE, 5, true), + RMB6(0x067, COMMAND.RMB6, MODE.ZEROPAGE, 5, true), + RMB7(0x077, COMMAND.RMB7, MODE.ZEROPAGE, 5, true), + ROL(0x002A, COMMAND.ROL_A, MODE.IMPLIED, 2), + ROL_ZP(0x0026, COMMAND.ROL, MODE.ZEROPAGE, 5), + ROL_ZP_X(0x0036, COMMAND.ROL, MODE.ZEROPAGE_X, 6), + ROL_AB(0x002E, COMMAND.ROL, MODE.ABSOLUTE, 6), + ROL_AB_X(0x003E, COMMAND.ROL, MODE.ABSOLUTE_X, 7), + ROR(0x006A, COMMAND.ROR_A, MODE.IMPLIED, 2), + ROR_ZP(0x0066, COMMAND.ROR, MODE.ZEROPAGE, 5), + ROR_ZP_X(0x0076, COMMAND.ROR, MODE.ZEROPAGE_X, 6), + ROR_AB(0x006E, COMMAND.ROR, MODE.ABSOLUTE, 6), + ROR_AB_X(0x007E, COMMAND.ROR, MODE.ABSOLUTE_X, 7), + RTI(0x0040, COMMAND.RTI, MODE.IMPLIED, 6), + RTS(0x0060, COMMAND.RTS, MODE.IMPLIED, 6), + SBC_IMM(0x00E9, COMMAND.SBC, MODE.IMMEDIATE, 2), + SBC_ZP(0x00E5, COMMAND.SBC, MODE.ZEROPAGE, 3), + SBC_ZP_X(0x00F5, COMMAND.SBC, MODE.ZEROPAGE_X, 4), + SBC_AB(0x00ED, COMMAND.SBC, MODE.ABSOLUTE, 4), + SBC_IND_ZP(0x00F2, COMMAND.SBC, MODE.INDIRECT_ZP, 5, true), + SBC_IND_ZP_X(0x00E1, COMMAND.SBC, MODE.INDIRECT_ZP_X, 6), + SBC_AB_X(0x00FD, COMMAND.SBC, MODE.ABSOLUTE_X, 4), + SBC_AB_Y(0x00F9, COMMAND.SBC, MODE.ABSOLUTE_Y, 4), + SBC_IND_ZP_Y(0x00F1, COMMAND.SBC, MODE.INDIRECT_ZP_Y, 5), + SEC(0x0038, COMMAND.SEC, MODE.IMPLIED, 2), + SED(0x00F8, COMMAND.SED, MODE.IMPLIED, 2), + SEI(0x0078, COMMAND.SEI, MODE.IMPLIED, 2), + SMB0(0x087, COMMAND.SMB0, MODE.ZEROPAGE, 5, true), + SMB1(0x097, COMMAND.SMB1, MODE.ZEROPAGE, 5, true), + SMB2(0x0a7, COMMAND.SMB2, MODE.ZEROPAGE, 5, true), + SMB3(0x0b7, COMMAND.SMB3, MODE.ZEROPAGE, 5, true), + SMB4(0x0c7, COMMAND.SMB4, MODE.ZEROPAGE, 5, true), + SMB5(0x0d7, COMMAND.SMB5, MODE.ZEROPAGE, 5, true), + SMB6(0x0e7, COMMAND.SMB6, MODE.ZEROPAGE, 5, true), + SMB7(0x0f7, COMMAND.SMB7, MODE.ZEROPAGE, 5, true), + STA_ZP(0x0085, COMMAND.STA, MODE.ZEROPAGE, 3), + STA_ZP_X(0x0095, COMMAND.STA, MODE.ZEROPAGE_X, 4), + STA_AB(0x008D, COMMAND.STA, MODE.ABSOLUTE, 4), + STA_AB_X(0x009D, COMMAND.STA, MODE.ABSOLUTE_X, 5), + STA_AB_Y(0x0099, COMMAND.STA, MODE.ABSOLUTE_Y, 5), + STA_IND_ZP(0x0092, COMMAND.STA, MODE.INDIRECT_ZP, 5, true), + STA_IND_ZP_X(0x0081, COMMAND.STA, MODE.INDIRECT_ZP_X, 6), + STA_IND_ZP_Y(0x0091, COMMAND.STA, MODE.INDIRECT_ZP_Y, 6), + STP(0x00DB, COMMAND.STP, MODE.IMPLIED, 3, true), + STX_ZP(0x0086, COMMAND.STX, MODE.ZEROPAGE, 3), + STX_ZP_Y(0x0096, COMMAND.STX, MODE.ZEROPAGE_Y, 4), + STX_AB(0x008E, COMMAND.STX, MODE.ABSOLUTE, 4), + STY_ZP(0x0084, COMMAND.STY, MODE.ZEROPAGE, 3), + STY_ZP_X(0x0094, COMMAND.STY, MODE.ZEROPAGE_X, 4), + STY_AB(0x008C, COMMAND.STY, MODE.ABSOLUTE, 4), + STZ_ZP(0x0064, COMMAND.STZ, MODE.ZEROPAGE, 3, true), + STZ_ZP_X(0x0074, COMMAND.STZ, MODE.ZEROPAGE_X, 4, true), + STZ_AB(0x009C, COMMAND.STZ, MODE.ABSOLUTE, 4, true), + STZ_AB_X(0x009E, COMMAND.STZ, MODE.ABSOLUTE_X, 5, true), + TAX(0x00AA, COMMAND.TAX, MODE.IMPLIED, 2), + TAY(0x00A8, COMMAND.TAY, MODE.IMPLIED, 2), + TRB_ZP(0x0014, COMMAND.TRB, MODE.ZEROPAGE, 5, true), + TRB_AB(0x001C, COMMAND.TRB, MODE.ABSOLUTE, 6, true), + TSB_ZP(0x0004, COMMAND.TSB, MODE.ZEROPAGE, 5, true), + TSB_AB(0x000C, COMMAND.TSB, MODE.ABSOLUTE, 6, true), + TSX(0x00BA, COMMAND.TSX, MODE.IMPLIED, 2), + TXA(0x008A, COMMAND.TXA, MODE.IMPLIED, 2), + TXS(0x009A, COMMAND.TXS, MODE.IMPLIED, 2), + TYA(0x0098, COMMAND.TYA, MODE.IMPLIED, 2), + WAI(0x00CB, COMMAND.WAI, MODE.IMPLIED, 3, true); + private int code; + private boolean isExtendedOpcode; + + public int getCode() { + return code; + } + private int waitCycles; + + public int getWaitCycles() { + return waitCycles; + } + private COMMAND command; + + public COMMAND getCommand() { + return command; + } + private MODE addressingMode; + + public MODE getMode() { + return addressingMode; + } + int address = 0; + int value = 0; + + private void fetch() { + address = getMode().calculator.calculateAddress(); + value = getMode().calculator.getValue(!command.isStoreOnly()); + } + + public void execute() { + command.getProcessor().processCommand(address, value, addressingMode); + } + + private OPCODE(int val, COMMAND c, MODE m, int wait) { + this(val, c, m, wait, false); + } + + private OPCODE(int val, COMMAND c, MODE m, int wait, boolean extended) { + code = val; + waitCycles = wait - 1; + command = c; + addressingMode = m; + isExtendedOpcode = extended; + } + } + + private static interface AddressCalculator { + + abstract int calculateAddress(); + + default int getValue(boolean isRead) { + int address = calculateAddress(); + return (address > -1) ? (0x0ff & getMemory().read(address, TYPE.READ_DATA, isRead, false)) : 0; + } + } + + private enum MODE { + + IMPLIED(1, "", () -> -1), + // RELATIVE(2, "#$~1 ($R)"), + RELATIVE(2, "$R", () -> { + int pc = cpu.getProgramCounter(); + int address = pc + 2 + getMemory().read(pc + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + // The wait cycles are not added unless the branch actually happens! + cpu.setPageBoundaryPenalty((address & 0x00ff00) != (pc & 0x00ff00)); + return address; + }), + IMMEDIATE(2, "#$~1", () -> cpu.getProgramCounter() + 1), + ZEROPAGE(2, "$~1", () -> getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false) & 0x00FF), + ZEROPAGE_X(2, "$~1,X", () -> 0x0FF & (getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false) + cpu.X)), + ZEROPAGE_Y(2, "$~1,Y", () -> 0x0FF & (getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false) + cpu.Y)), + INDIRECT(3, "$(~2~1)", () -> { + int address = getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + return getMemory().readWord(address, TYPE.READ_DATA, true, false); + }), + INDIRECT_X(3, "$(~2~1,X)", () -> { + int address = getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false) + cpu.X; + return getMemory().readWord(address & 0x0FFFF, TYPE.READ_DATA, true, false); + }), + INDIRECT_ZP(2, "$(~1)", () -> { + int address = getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + return getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false); + }), + INDIRECT_ZP_X(2, "$(~1,X)", () -> { + int address = getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false) + cpu.X; + return getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false); + }), + INDIRECT_ZP_Y(2, "$(~1),Y", () -> { + int address = 0x00FF & getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + address = getMemory().readWord(address, TYPE.READ_DATA, true, false) + cpu.Y; + if ((address & 0x00ff00) > 0) { + cpu.addWaitCycles(1); + } + return address; + }), + ABSOLUTE(3, "$~2~1", () -> getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false)), + ABSOLUTE_X(3, "$~2~1,X", () -> { + int address2 = getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + int address = 0x0FFFF & (address2 + cpu.X); + if ((address & 0x00FF00) != (address2 & 0x00FF00)) { + cpu.addWaitCycles(1); + } + return address; + }), + ABSOLUTE_Y(3, "$~2~1,Y", () -> { + int address2 = getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + int address = 0x0FFFF & (address2 + cpu.Y); + if ((address & 0x00FF00) != (address2 & 0x00FF00)) { + cpu.addWaitCycles(1); + } + return address; + }), + ZP_REL(2, "$~1,$R", new AddressCalculator() { + @Override + public int calculateAddress() { + // Note: This is two's compliment addition and the getMemory().read() returns a signed 8-bit value + int pc = cpu.getProgramCounter(); + int address = pc + 2 + getMemory().read(pc + 2, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + // The wait cycles are not added unless the branch actually happens! + cpu.setPageBoundaryPenalty((address & 0x00ff00) != (pc & 0x00ff00)); + return address; + } + + @Override + public int getValue(boolean isRead) { + int pc = cpu.getProgramCounter(); + int address = getMemory().read(pc + 1, TYPE.READ_OPERAND, readAddressTriggersEvent, false); + return getMemory().read(address, TYPE.READ_DATA, true, false); + } + }); + private int size; + + public int getSize() { + return this.size; + } +// private String format; +// +// public String getFormat() { +// return this.format; +// } + private AddressCalculator calculator; + + public int calcAddress() { + return calculator.calculateAddress(); + } + private boolean indirect; + + public boolean isIndirect() { + return indirect; + } + String f1; + String f2; + boolean twoByte = false; + boolean relative = false; + boolean implied = true; + + private MODE(int size, String fmt, AddressCalculator calc) { + this.size = size; + if (fmt.contains("~")) { + this.f1 = fmt.substring(0, fmt.indexOf('~')); + this.f2 = fmt.substring(fmt.indexOf("~1") + 2); + if (fmt.contains("~2")) { + twoByte = true; + } + implied = false; + } else if (fmt.contains("R")) { + this.f1 = fmt.substring(0, fmt.indexOf("R")); + f2 = ""; + relative = true; + implied = false; + } +// this.format = fmt; + + this.calculator = calc; + this.indirect = toString().startsWith("INDIRECT"); + } + + public MOS65C02.AddressCalculator getCalculator() { + return calculator; + } + + public String formatMode(int pc) { + if (implied) { + return ""; + } else { + int b1 = 0x00ff & getMemory().readRaw((pc + 1) & 0x0FFFF); + if (relative) { + String R = wordString(pc + 2 + (byte) b1); + return f1 + R; + } else if (twoByte) { + int b2 = 0x00ff & getMemory().readRaw((pc + 2) & 0x0FFFF); + return f1 + byte2(b2) + byte2(b1) + f2; + } else { + return f1 + byte2(b1) + f2; + } + } + } + } + + private static interface CommandProcessor { + + public void processCommand(int address, int value, MODE addressMode); + } + + private static class BBRCommand implements CommandProcessor { + + int bit; + + public BBRCommand(int bit) { + this.bit = bit; + } + + @Override + public void processCommand(int address, int value, MODE addressMode) { + if (((value >> bit) & 1) != 0) { + return; + } + if (cpu.C != 0) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + } + } + + private static class BBSCommand implements CommandProcessor { + + int bit; + + public BBSCommand(int bit) { + this.bit = bit; + } + + @Override + public void processCommand(int address, int value, MODE addressMode) { + if (((value >> bit) & 1) == 0) { + return; + } + if (cpu.C != 0) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + } + } + + private static class RMBCommand implements CommandProcessor { + + int bit; + + public RMBCommand(int bit) { + this.bit = bit; + } + + @Override + public void processCommand(int address, int value, MODE addressMode) { + int mask = 0x0ff ^ (1 << bit); + value &= mask; + getMemory().write(address, (byte) value, true, false); + } + } + + private static class SMBCommand implements CommandProcessor { + + int bit; + + public SMBCommand(int bit) { + this.bit = bit; + } + + @Override + public void processCommand(int address, int value, MODE addressMode) { + int mask = 1 << bit; + value |= mask; + getMemory().write(address, (byte) value, true, false); + } + } + + private enum COMMAND { + ADC((int address, int value, MODE addressMode) -> { + int w = 0; + cpu.V = ((cpu.A ^ value) & 0x080) == 0; + if (cpu.D) { + // Decimal Mode + w = (cpu.A & 0x0f) + (value & 0x0f) + cpu.C; + if (w >= 10) { + w = 0x010 | ((w + 6) & 0x0f); + } + w += (cpu.A & 0x0f0) + (value & 0x00f0); + if (w >= 0x0A0) { + cpu.C = 1; + if (cpu.V && w >= 0x0180) { + cpu.V = false; + } + w += 0x060; + } else { + cpu.C = 0; + if (cpu.V && w < 0x080) { + cpu.V = false; + } + } + } else { + // Binary Mode + w = cpu.A + value + cpu.C; + if (w >= 0x0100) { + cpu.C = 1; + if (cpu.V && w >= 0x0180) { + cpu.V = false; + } + } else { + cpu.C = 0; + if (cpu.V && w < 0x080) { + cpu.V = false; + } + } + } + cpu.A = w & 0x0ff; + cpu.setNZ(cpu.A); + }), + AND((int address, int value, MODE addressMode) -> { + cpu.A &= value; + cpu.setNZ(cpu.A); + }), + ASL((int address, int value, MODE addressMode) -> { + cpu.C = ((value & 0x080) != 0) ? 1 : 0; + value = 0x0FE & (value << 1); + cpu.setNZ(value); + // Emulate correct behavior of fetch-store-modify + // http://forum.6502.org/viewtopic.php?f=4&t=1617&view=previous + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + }), + ASL_A((int address, int value, MODE addressMode) -> { + cpu.C = cpu.A >> 7; + cpu.A = 0x0FE & (cpu.A << 1); + cpu.setNZ(cpu.A); + }), + BBR0(new BBRCommand(0)), + BBR1(new BBRCommand(1)), + BBR2(new BBRCommand(2)), + BBR3(new BBRCommand(3)), + BBR4(new BBRCommand(4)), + BBR5(new BBRCommand(5)), + BBR6(new BBRCommand(6)), + BBR7(new BBRCommand(7)), + BBS0(new BBSCommand(0)), + BBS1(new BBSCommand(1)), + BBS2(new BBSCommand(2)), + BBS3(new BBSCommand(3)), + BBS4(new BBSCommand(4)), + BBS5(new BBSCommand(5)), + BBS6(new BBSCommand(6)), + BBS7(new BBSCommand(7)), + BCC((int address, int value, MODE addressMode) -> { + if (cpu.C == 0) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BCS((int address, int value, MODE addressMode) -> { + if (cpu.C != 0) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BEQ((int address, int value, MODE addressMode) -> { + if (cpu.Z) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BIT((int address, int value, MODE addressMode) -> { + int result = (cpu.A & value); + cpu.Z = result == 0; + cpu.N = (value & 0x080) != 0; + // As per http://www.6502.org/tutorials/vflag.html + if (addressMode != MODE.IMMEDIATE) { + cpu.V = (value & 0x040) != 0; + } + }), + BMI((int address, int value, MODE addressMode) -> { + if (cpu.N) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BNE((int address, int value, MODE addressMode) -> { + if (!cpu.Z) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BPL((int address, int value, MODE addressMode) -> { + if (!cpu.N) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BRA((int address, int value, MODE addressMode) -> { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 1 : 0); + }), + BRK((int address, int value, MODE addressMode) -> { + cpu.BRK(); + }), + BVC((int address, int value, MODE addressMode) -> { + if (!cpu.V) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + BVS((int address, int value, MODE addressMode) -> { + if (cpu.V) { + cpu.setProgramCounter(address); + cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1); + } + }), + CLC((int address, int value, MODE addressMode) -> { + cpu.C = 0; + }), + CLD((int address, int value, MODE addressMode) -> { + cpu.D = false; + }), + CLI((int address, int value, MODE addressMode) -> { + cpu.I = false; + cpu.interruptSignalled = false; + }), + CLV((int address, int value, MODE addressMode) -> { + cpu.V = false; + }), + CMP((int address, int value, MODE addressMode) -> { + int val = cpu.A - value; + cpu.C = (val >= 0) ? 1 : 0; + cpu.setNZ(val); + }), + CPX((int address, int value, MODE addressMode) -> { + int val = cpu.X - value; + cpu.C = (val >= 0) ? 1 : 0; + cpu.setNZ(val); + }), + CPY((int address, int value, MODE addressMode) -> { + int val = cpu.Y - value; + cpu.C = (val >= 0) ? 1 : 0; + cpu.setNZ(val); + }), + DEC((int address, int value, MODE addressMode) -> { + value = 0x0FF & (value - 1); + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + cpu.setNZ(value); + }), + DEA((int address, int value, MODE addressMode) -> { + cpu.A = 0x0FF & (cpu.A - 1); + cpu.setNZ(cpu.A); + }), + DEX((int address, int value, MODE addressMode) -> { + cpu.X = 0x0FF & (cpu.X - 1); + cpu.setNZ(cpu.X); + }), + DEY((int address, int value, MODE addressMode) -> { + cpu.Y = 0x0FF & (cpu.Y - 1); + cpu.setNZ(cpu.Y); + }), + EOR((int address, int value, MODE addressMode) -> { + cpu.A = 0x0FF & (cpu.A ^ value); + cpu.setNZ(cpu.A); + }), + INC((int address, int value, MODE addressMode) -> { + value = 0x0ff & (value + 1); + // emulator correct fetch-modify-store behavior + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + cpu.setNZ(value); + }), + INA((int address, int value, MODE addressMode) -> { + cpu.A = 0x0FF & (cpu.A + 1); + cpu.setNZ(cpu.A); + }), + INX((int address, int value, MODE addressMode) -> { + cpu.X = 0x0FF & (cpu.X + 1); + cpu.setNZ(cpu.X); + }), + INY((int address, int value, MODE addressMode) -> { + cpu.Y = 0x0FF & (cpu.Y + 1); + cpu.setNZ(cpu.Y); + }), + JMP((int address, int value, MODE addressMode) -> { + cpu.setProgramCounter(address); + }), + JSR((int address, int value, MODE addressMode) -> { + cpu.pushWord(cpu.getProgramCounter() - 1); + cpu.setProgramCounter(address); + }), + LDA((int address, int value, MODE addressMode) -> { + cpu.A = value; + cpu.setNZ(cpu.A); + }), + LDX((int address, int value, MODE addressMode) -> { + cpu.X = value; + cpu.setNZ(cpu.X); + }), + LDY((int address, int value, MODE addressMode) -> { + cpu.Y = value; + cpu.setNZ(cpu.Y); + }), + LSR((int address, int value, MODE addressMode) -> { + cpu.C = (value & 1); + value = (value >> 1) & 0x07F; + cpu.setNZ(value); + // emulator correct fetch-modify-store behavior + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + }), + LSR_A((int address, int value, MODE addressMode) -> { + cpu.C = cpu.A & 1; + cpu.A = (cpu.A >> 1) & 0x07F; + cpu.setNZ(cpu.A); + }), + NOP((int address, int value, MODE addressMode) -> {}), + ORA((int address, int value, MODE addressMode) -> { + cpu.A |= value; + cpu.setNZ(cpu.A); + }), + PHA((int address, int value, MODE addressMode) -> { + cpu.push((byte) cpu.A); + }), + PHP((int address, int value, MODE addressMode) -> { + cpu.push((byte) (cpu.getStatus())); + }), + PHX((int address, int value, MODE addressMode) -> { + cpu.push((byte) cpu.X); + }), + PHY((int address, int value, MODE addressMode) -> { + cpu.push((byte) cpu.Y); + }), + PLA((int address, int value, MODE addressMode) -> { + cpu.A = 0x0FF & cpu.pop(); + cpu.setNZ(cpu.A); + }), + PLP((int address, int value, MODE addressMode) -> { + cpu.setStatus(cpu.pop()); + }), + PLX((int address, int value, MODE addressMode) -> { + cpu.X = 0x0FF & cpu.pop(); + cpu.setNZ(cpu.X); + }), + PLY((int address, int value, MODE addressMode) -> { + cpu.Y = 0x0FF & cpu.pop(); + cpu.setNZ(cpu.Y); + }), + RMB0(new RMBCommand(0)), + RMB1(new RMBCommand(1)), + RMB2(new RMBCommand(2)), + RMB3(new RMBCommand(3)), + RMB4(new RMBCommand(4)), + RMB5(new RMBCommand(5)), + RMB6(new RMBCommand(6)), + RMB7(new RMBCommand(7)), + ROL((int address, int value, MODE addressMode) -> { + int oldC = cpu.C; + cpu.C = value >> 7; + value = 0x0ff & ((value << 1) | oldC); + cpu.setNZ(value); + // emulator correct fetch-modify-store behavior + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + }), + ROL_A((int address, int value, MODE addressMode) -> { + int oldC = cpu.C; + cpu.C = cpu.A >> 7; + cpu.A = 0x0ff & ((cpu.A << 1) | oldC); + cpu.setNZ(cpu.A); + }), + ROR((int address, int value, MODE addressMode) -> { + int oldC = cpu.C << 7; + cpu.C = value & 1; + value = 0x0ff & ((value >> 1) | oldC); + cpu.setNZ(value); + // emulator correct fetch-modify-store behavior + getMemory().write(address, (byte) value, true, false); + getMemory().write(address, (byte) value, true, false); + }), + ROR_A((int address, int value, MODE addressMode) -> { + int oldC = cpu.C << 7; + cpu.C = cpu.A & 1; + cpu.A = 0x0ff & ((cpu.A >> 1) | oldC); + cpu.setNZ(cpu.A); + }), + RTI((int address, int value, MODE addressMode) -> { + cpu.returnFromInterrupt(); + }), + RTS((int address, int value, MODE addressMode) -> { + cpu.setProgramCounter(cpu.popWord() + 1); + }), + SBC((int address, int value, MODE addressMode) -> { + cpu.V = ((cpu.A ^ value) & 0x080) != 0; + int w = 0; + if (cpu.D) { + int temp = 0x0f + (cpu.A & 0x0f) - (value & 0x0f) + cpu.C; + if (temp < 0x10) { + w = 0; + temp -= 6; + } else { + w = 0x10; + temp -= 0x10; + } + w += 0x00f0 + (cpu.A & 0x00f0) - (value & 0x00f0); + if (w < 0x100) { + cpu.C = 0; + if (cpu.V && w < 0x080) { + cpu.V = false; + } + w -= 0x60; + } else { + cpu.C = 1; + if (cpu.V && w >= 0x180) { + cpu.V = false; + } + } + w += temp; + } else { + w = 0x0ff + cpu.A - value + cpu.C; + if (w < 0x100) { + cpu.C = 0; + if (cpu.V && (w < 0x080)) { + cpu.V = false; + } + } else { + cpu.C = 1; + if (cpu.V && (w >= 0x180)) { + cpu.V = false; + } + } + } + cpu.A = w & 0x0ff; + cpu.setNZ(cpu.A); + }), + SEC((int address, int value, MODE addressMode) -> { + cpu.C = 1; + }), + SED((int address, int value, MODE addressMode) -> { + cpu.D = true; + }), + SEI((int address, int value, MODE addressMode) -> { + cpu.I = true; + }), + SMB0(new SMBCommand(0)), + SMB1(new SMBCommand(1)), + SMB2(new SMBCommand(2)), + SMB3(new SMBCommand(3)), + SMB4(new SMBCommand(4)), + SMB5(new SMBCommand(5)), + SMB6(new SMBCommand(6)), + SMB7(new SMBCommand(7)), + STA(true, (int address, int value, MODE addressMode) -> { + getMemory().write(address, (byte) cpu.A, true, false); + }), + STP((int address, int value, MODE addressMode) -> { + cpu.suspend(); + }), + STX(true, (int address, int value, MODE addressMode) -> { + getMemory().write(address, (byte) cpu.X, true, false); + }), + STY(true, (int address, int value, MODE addressMode) -> { + getMemory().write(address, (byte) cpu.Y, true, false); + }), + STZ(true, (int address, int value, MODE addressMode) -> { + getMemory().write(address, (byte) 0, true, false); + }), + TAX((int address, int value, MODE addressMode) -> { + cpu.X = cpu.A; + cpu.setNZ(cpu.X); + }), + TAY((int address, int value, MODE addressMode) -> { + cpu.Y = cpu.A; + cpu.setNZ(cpu.Y); + }), + TRB((int address, int value, MODE addressMode) -> { + cpu.C = (value & cpu.A) != 0 ? 1 : 0; + value &= ~cpu.A; + getMemory().write(address, (byte) value, true, false); + }), + TSB((int address, int value, MODE addressMode) -> { + cpu.C = (value & cpu.A) != 0 ? 1 : 0; + value |= cpu.A; + getMemory().write(address, (byte) value, true, false); + }), + TSX((int address, int value, MODE addressMode) -> { + cpu.X = cpu.STACK; + cpu.setNZ(cpu.STACK); + }), + TXA((int address, int value, MODE addressMode) -> { + cpu.A = cpu.X; + cpu.setNZ(cpu.X); + }), + TXS((int address, int value, MODE addressMode) -> { + cpu.STACK = cpu.X; + }), + TYA((int address, int value, MODE addressMode) -> { + cpu.A = cpu.Y; + cpu.setNZ(cpu.Y); + }), + WAI((int address, int value, MODE addressMode) -> { + cpu.waitForInterrupt(); + }); + private CommandProcessor processor; + + public CommandProcessor getProcessor() { + return processor; + } + private boolean storeOnly; + + public boolean isStoreOnly() { + return storeOnly; + } + + private COMMAND(CommandProcessor processor) { + this(false, processor); + } + + private COMMAND(boolean storeOnly, CommandProcessor processor) { + this.storeOnly = storeOnly; + this.processor = processor; + } + } + static private OPCODE[] opcodes; + + static { + opcodes = new OPCODE[256]; + for (OPCODE o : OPCODE.values()) { + opcodes[o.getCode()] = o; + } + } + + /** + * Creates a new instance of MOS65C02 + */ + public MOS65C02() { + cpu = this; + } + + @Override + protected void executeOpcode() { + if (interruptSignalled) { + processInterrupt(); + } + int pc = getProgramCounter(); + +// RAM ram = getMemory(); +// int op = 0x00ff & getMemory().read(pc, false); + // This makes it possible to trap the memory read of an opcode, when PC == Address, you know it is executing that opcode. + int op = 0x00ff & getMemory().read(pc, TYPE.EXECUTE, true, false); + OPCODE opcode = opcodes[op]; + if (isTraceEnabled() || isLogEnabled() || (warnAboutExtendedOpcodes && opcode != null && opcode.isExtendedOpcode)) { + String t = getState().toUpperCase() + " " + Integer.toString(pc, 16) + " : " + disassemble(); + if (warnAboutExtendedOpcodes && opcode != null && opcode.isExtendedOpcode) { + System.out.println(">>EXTENDED OPCODE DETECTED "+Integer.toHexString(opcode.code)+"<<"); + System.out.println(t); + if (isLogEnabled()) { + log(">>EXTENDED OPCODE DETECTED "+Integer.toHexString(opcode.code)+"<<"); + log(t); + } + } else { + if (isTraceEnabled()) { + System.out.println(t); + } + if (isLogEnabled()) { + log(t); + } + } + } + if (opcode == null) { + // handle bad opcode as a NOP + int wait = 0; + int bytes = 2; + int n = op & 0x0f; + if (n == 2) { + wait = 2; + } else if (n == 3 || n == 7 || n == 0x0b || n == 0x0f) { + wait = 1; + bytes = 1; + } else if (n == 4) { + bytes = 2; + if ((op & 0x0f0) == 0x040) { + wait = 3; + } else { + wait = 4; + } + } else if (n == 0x0c) { + bytes = 3; + if ((op & 0x0f0) == 0x050) { + wait = 8; + } else { + wait = 4; + } + } + incrementProgramCounter(bytes); + addWaitCycles(wait); + + if (isLogEnabled() || breakOnBadOpcode) { + System.out.println("Unrecognized opcode " + + Integer.toHexString(op) + + " at " + Integer.toHexString(pc)); + } + if (isLogEnabled()) { + dumpTrace(); + } + if (breakOnBadOpcode) { + OPCODE.BRK.execute(); + } + } else { + opcode.fetch(); + incrementProgramCounter(opcode.getMode().getSize()); + opcode.execute(); + addWaitCycles(opcode.getWaitCycles()); + } + } + + private void setNZ(int value) { + N = (value & 0x080) != 0; + Z = (value & 0x0ff) == 0; + } + + public void pushWord(int val) { + push((byte) (val >> 8)); + push((byte) (val & 0x00ff)); + } + + public int popWord() { + return (0x0FF & pop()) | (0x0ff00 & (pop() << 8)); + } + + public void push(byte val) { + getMemory().write(0x0100 + STACK, val, true, false); + STACK = (STACK - 1) & 0x0FF; + //System.out.println("--> PUSH "+Integer.toString(0x0FF & val, 16)); + } + + public byte pop() { + STACK = (STACK + 1) & 0x0FF; + byte val = getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false); + //System.out.println("<-- POP "+Integer.toString(0x0FF & val, 16)); + return val; + } + + private byte getStatus() { + return (byte) ((N ? 0x080 : 0) + | (V ? 0x040 : 0) + | 0x020 + | (B ? 0x010 : 0) + | (D ? 0x08 : 0) + | (I ? 0x04 : 0) + | (Z ? 0x02 : 0) + | ((C > 0) ? 0x01 : 0)); + } + + private void setStatus(byte b) { + N = (b & 0x080) != 0; + V = (b & 0x040) != 0; + // B flag is unaffected in this way. + D = (b & 0x08) != 0; + I = (b & 0x04) != 0; + Z = (b & 0x02) != 0; + C = (char) (b & 0x01); + } + + private void returnFromInterrupt() { + setStatus(pop()); + setProgramCounter(popWord()); + } + + private void waitForInterrupt() { + I = true; + suspend(); + } + + public void BRK() { + if (isLogEnabled()) { + System.out.println("BRK at $" + Integer.toString(getProgramCounter(), 16)); + dumpTrace(); + } + B = true; + // 65c02 clears D flag on BRK + D = false; + interruptSignalled = true; + } + + // Hardware IRQ generated + @Override + public void generateInterrupt() { + B = false; + interruptSignalled = true; + resume(); + } + + private void processInterrupt() { + if (!interruptSignalled) { + return; + } + interruptSignalled = false; + if (!I || B) { + I = false; + pushWord(getProgramCounter()); + push(getStatus()); + I = true; + int newPC = getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false); +// System.out.println("Interrupt generated, setting PC to (" + Integer.toString(INT_VECTOR, 16) + ") = " + Integer.toString(newPC, 16)); + setProgramCounter(newPC); + } + } + + public int getSTACK() { + return STACK; + } + + // Cold/Warm boot procedure + public void reset() { + boolean restart = Computer.pause(); + pushWord(getProgramCounter()); + push(getStatus()); + // STACK = 0x0ff; +// B = false; + B = true; +// C = 1; + D = false; +// I = true; +// N = true; +// V = true; +// Z = true; + int newPC = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false); + System.out.println("Reset called, setting PC to (" + Integer.toString(RESET_VECTOR, 16) + ") = " + Integer.toString(newPC, 16)); + setProgramCounter(newPC); + if (restart) { + Computer.resume(); + } + } + + protected String getDeviceName() { + return "65C02 Processor"; + } + + private static String byte2(int b) { + String out = Integer.toString(b & 0x0FF, 16); + if (out.length() == 1) { + return "0" + out; + } + return out; + } + + private static String wordString(int w) { + String out = Integer.toHexString(w); + if (out.length() == 1) { + return "000" + out; + } + if (out.length() == 2) { + return "00" + out; + } + if (out.length() == 3) { + return "0" + out; + } + return out; + } + + public String getState() { + StringBuilder out = new StringBuilder(); + out.append(byte2(A)).append(" "); + out.append(byte2(X)).append(" "); + out.append(byte2(Y)).append(" "); + // out += "PC:"+wordString(getProgramCounter())+" "; + out.append("01").append(byte2(STACK)). append(" "); + out.append(getFlags()); + return out.toString(); + } + + public String getFlags() { + StringBuilder out = new StringBuilder(); + out.append(N ? "N" : "."); + out.append(V ? "V" : "."); + out.append("R"); + out.append(B ? "B" : "."); + out.append(D ? "D" : "."); + out.append(I ? "I" : "."); + out.append(Z ? "Z" : "."); + out.append((C != 0) ? "C" : "."); + return out.toString(); + } + + public String disassemble() { + int pc = getProgramCounter(); +// RAM ram = getMemory(); + int op = getMemory().readRaw(pc); + OPCODE o = opcodes[op & 0x0ff]; + if (o == null) { + return "???"; + } + String format = o.getMode().formatMode(pc); +// format = format.replaceAll("~1", byte2(b1)); +// format = format.replaceAll("~2", byte2(b2)); +// format = format.replaceAll("R", R); + /* + String mem = wordString(pc) + ":" + byte2(op) + " " + + ((o.getMode().getSize() > 1) ? + byte2(b1) : " " ) + " " + + ((o.getMode().getSize() > 2) ? + byte2(b2) : " " ) + " "; + */ + StringBuilder out = new StringBuilder(o.getCommand().toString()); + out.append(" ").append(format); + return out.toString(); + } + private boolean pageBoundaryPenalty = false; + + private void setPageBoundaryPenalty(boolean b) { + pageBoundaryPenalty = b; + } + + @Override + public void pushPC() { + cpu.pushWord(cpu.getProgramCounter() - 1); + } +} diff --git a/src/main/java/jace/apple2e/RAM128k.java b/src/main/java/jace/apple2e/RAM128k.java new file mode 100644 index 0000000..1860d9c --- /dev/null +++ b/src/main/java/jace/apple2e/RAM128k.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.core.Card; +import jace.core.Computer; +import jace.core.PagedMemory; +import jace.core.RAM; +import jace.state.Stateful; +import java.io.IOException; +import java.io.InputStream; + +/** + * Implementation of a 128k memory space and the MMU found in an Apple //e. The + * MMU behavior is mimicked by configureActiveMemory. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +abstract public class RAM128k extends RAM { + @Stateful + public PagedMemory mainMemory; + @Stateful + public PagedMemory languageCard; + @Stateful + public PagedMemory languageCard2; + public PagedMemory cPageRom; + public PagedMemory rom; + public PagedMemory blank; + + public RAM128k() { + super(); + mainMemory = new PagedMemory(0xc000, PagedMemory.Type.ram); + rom = new PagedMemory(0x3000, PagedMemory.Type.firmwareMain); + cPageRom = new PagedMemory(0x1000, PagedMemory.Type.slotRom); + languageCard = new PagedMemory(0x3000, PagedMemory.Type.languageCard); + languageCard2 = new PagedMemory(0x1000, PagedMemory.Type.languageCard); + activeRead = new PagedMemory(0x10000, PagedMemory.Type.ram); + activeWrite = new PagedMemory(0x10000, PagedMemory.Type.ram); + blank = new PagedMemory(0x100, PagedMemory.Type.ram); + + // Format memory with FF FF 00 00 pattern + for (int i = 0; i < 0x0100; i++) { + blank.get(0)[i] = (byte) 0x0FF; + } + initMemoryPattern(mainMemory); + } + + public final void initMemoryPattern(PagedMemory mem) { + // Format memory with FF FF 00 00 pattern + for (int i = 0; i < 0x0100; i++) { + for (int j = 0; j < 0x0c0; j++) { + byte use = (byte) ((i % 4) > 1 ? 0x0FF : 0x00); + mem.get(j)[i] = use; + } + } + } + + /** + * + */ + @Override + public void configureActiveMemory() { + log("MMU Switches"); + synchronized (this) { + // First off, set up read/write for main memory (might get changed later on) + activeRead.fillBanks(SoftSwitches.RAMRD.getState() ? getAuxMemory() : mainMemory); + activeWrite.fillBanks(SoftSwitches.RAMWRT.getState() ? getAuxMemory() : mainMemory); + + // Handle language card softswitches + activeRead.fillBanks(rom); + //activeRead.fillBanks(cPageRom); + for (int i = 0x0c0; i < 0x0d0; i++) { + activeWrite.set(i, null); + } + if (SoftSwitches.LCRAM.getState()) { + if (!SoftSwitches.AUXZP.getState()) { + activeRead.fillBanks(languageCard); + if (!SoftSwitches.LCBANK1.getState()) { + activeRead.fillBanks(languageCard2); + } + } else { + activeRead.fillBanks(getAuxLanguageCard()); + if (!SoftSwitches.LCBANK1.getState()) { + activeRead.fillBanks(getAuxLanguageCard2()); + } + } + } + + if (SoftSwitches.LCWRITE.getState()) { + if (!SoftSwitches.AUXZP.getState()) { + activeWrite.fillBanks(languageCard); + if (!SoftSwitches.LCBANK1.getState()) { + activeWrite.fillBanks(languageCard2); + } + } else { + activeWrite.fillBanks(getAuxLanguageCard()); + if (!SoftSwitches.LCBANK1.getState()) { + activeWrite.fillBanks(getAuxLanguageCard2()); + } + } + } else { + // Make 0xd000 - 0xffff non-writable! + for (int i = 0x0d0; i < 0x0100; i++) { + activeWrite.set(i, null); + } + } + + // Handle 80STORE logic for bankswitching video ram + if (SoftSwitches._80STORE.isOn()) { + activeRead.setBanks(0x04, 0x04, 0x04, + SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory); + activeWrite.setBanks(0x04, 0x04, 0x04, + SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory); + if (SoftSwitches.HIRES.isOn()) { + activeRead.setBanks(0x020, 0x020, 0x020, + SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory); + activeWrite.setBanks(0x020, 0x020, 0x020, + SoftSwitches.PAGE2.isOn() ? getAuxMemory() : mainMemory); + } + } + + // Handle zero-page bankswitching + if (SoftSwitches.AUXZP.getState()) { + // Aux pages 0 and 1 + activeRead.setBanks(0, 2, 0, getAuxMemory()); + activeWrite.setBanks(0, 2, 0, getAuxMemory()); + } else { + // Main pages 0 and 1 + activeRead.setBanks(0, 2, 0, mainMemory); + activeWrite.setBanks(0, 2, 0, mainMemory); + } + + /* + INTCXROM SLOTC3ROM C1,C2,C4-CF C3 + 0 0 slot rom + 0 1 slot slot + 1 - rom rom + */ + if (SoftSwitches.CXROM.getState()) { + // Enable C1-CF to point to rom + activeRead.setBanks(0, 0x0F, 0x0C1, cPageRom); + } else { + // Enable C1-CF to point to slots + for (int slot = 1; slot <= 7; slot++) { + Card c = getCard(slot); + if (c != null) { + // Enable card slot ROM + activeRead.setBanks(0, 1, 0x0C0 + slot, c.getCxRom()); + if (getActiveSlot() == slot) { + activeRead.setBanks(0, 8, 0x0C8, c.getC8Rom()); + } + } else { + // Disable card slot ROM (TODO: floating bus) + activeRead.set(0x0C0 + slot, blank.get(0)); + } + } + if (getActiveSlot() == 0) { + for (int i = 0x0C8; i < 0x0D0; i++) { + activeRead.set(i, blank.get(0)); + } + } + if (SoftSwitches.SLOTC3ROM.isOff()) { + // Enable C3 to point to internal ROM + activeRead.setBanks(2, 1, 0x0C3, cPageRom); + } + if (SoftSwitches.INTC8ROM.getState()) { + // Enable C8-CF to point to internal ROM + activeRead.setBanks(7, 8, 0x0C8, cPageRom); + } + } + } + // All ROM reads not intecepted will return 0xFF! (TODO: floating bus) + activeRead.set(0x0c0, blank.get(0)); + } + + public void log(String message) { + if (Computer.getComputer().cpu != null && Computer.getComputer().cpu.isLogEnabled()) { + String stack = ""; + for (StackTraceElement e : Thread.currentThread().getStackTrace()) { + stack += e.getClassName() + "." + e.getMethodName() + "(" + e.getLineNumber() + ");"; + } + Computer.getComputer().cpu.log(stack); + Computer.getComputer().cpu.log(message + ";" + SoftSwitches.RAMRD + ";" + SoftSwitches.RAMWRT + ";" + SoftSwitches.AUXZP + ";" + SoftSwitches._80STORE + ";" + SoftSwitches.HIRES + ";" + SoftSwitches.PAGE2 + ";" + SoftSwitches.LCBANK1 + ";" + SoftSwitches.LCRAM + ";" + SoftSwitches.LCWRITE); + } + } + + /** + * + * @param path + * @throws java.io.IOException + */ + @Override + protected void loadRom(String path) throws IOException { + // Remap writable ram to reflect rom file structure + byte[] ignore = new byte[256]; + activeWrite.set(0, ignore); // Ignore first bank of data + for (int i = 1; i < 17; i++) { + activeWrite.set(i, ignore); + } + activeWrite.setBanks(0, cPageRom.getMemory().length, 0x011, cPageRom); + activeWrite.setBanks(0, rom.getMemory().length, 0x020, rom); + //---------------------- + InputStream inputRom = getClass().getClassLoader().getResourceAsStream(path); + int read = 0; + int addr = 0; + byte[] in = new byte[1024]; + while (addr < 0x00FFFF && (read = inputRom.read(in)) > 0) { + for (int i = 0; i < read; i++) { + write(addr++, in[i], false, false); + } + } +// System.out.println("Finished reading rom with " + inputRom.available() + " bytes left unread!"); + //dump(); + configureActiveMemory(); + } + + /** + * @return the mainMemory + */ + public PagedMemory getMainMemory() { + return mainMemory; + } + + abstract public PagedMemory getAuxVideoMemory(); + abstract public PagedMemory getAuxMemory(); + abstract public PagedMemory getAuxLanguageCard(); + abstract public PagedMemory getAuxLanguageCard2(); + + /** + * @return the languageCard + */ + public PagedMemory getLanguageCard() { + return languageCard; + } + + /** + * @return the languageCard2 + */ + public PagedMemory getLanguageCard2() { + return languageCard2; + } + + /** + * @return the cPageRom + */ + public PagedMemory getcPageRom() { + return cPageRom; + } + + /** + * @return the rom + */ + public PagedMemory getRom() { + return rom; + } + + void copyFrom(RAM128k currentMemory) { + // This is really quick and dirty but should be sufficient to avoid most crashes... + blank = currentMemory.blank; + cPageRom = currentMemory.cPageRom; + rom = currentMemory.rom; + listeners = currentMemory.listeners; + mainMemory = currentMemory.mainMemory; + languageCard = currentMemory.languageCard; + languageCard2 = currentMemory.languageCard2; + cards = currentMemory.cards; + activeSlot = currentMemory.activeSlot; + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/SoftSwitches.java b/src/main/java/jace/apple2e/SoftSwitches.java new file mode 100644 index 0000000..1497863 --- /dev/null +++ b/src/main/java/jace/apple2e/SoftSwitches.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.apple2e.softswitch.IntC8SoftSwitch; +import jace.apple2e.softswitch.KeyboardSoftSwitch; +import jace.apple2e.softswitch.Memory2SoftSwitch; +import jace.apple2e.softswitch.MemorySoftSwitch; +import jace.apple2e.softswitch.VideoSoftSwitch; +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.SoftSwitch; + +/** + * Softswitches reside in the addresses C000-C07f and control everything from + * memory management to speaker sound and keyboard. Other I/O ports (c080-C0ff) + * are managed by any registered Cards. This enumeration serves as a convenient + * way to represent the different softswitches as well as provide a clean + * enumeration. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public enum SoftSwitches { + + _80STORE(new MemorySoftSwitch("80Store", 0x0c000, 0x0c001, 0x0c018, RAMEvent.TYPE.WRITE, false)), + RAMRD(new MemorySoftSwitch("AuxRead (RAMRD)", 0x0c002, 0x0c003, 0x0c013, RAMEvent.TYPE.WRITE, false)), + RAMWRT(new MemorySoftSwitch("AuxWrite (RAMWRT)", 0x0c004, 0x0c005, 0x0c014, RAMEvent.TYPE.WRITE, false)), + CXROM(new MemorySoftSwitch("IntCXROM", 0x0c006, 0x0c007, 0x0c015, RAMEvent.TYPE.WRITE, false)), + AUXZP(new MemorySoftSwitch("AuxZeroPage", 0x0c008, 0x0c009, 0x0c016, RAMEvent.TYPE.WRITE, false)), + SLOTC3ROM(new MemorySoftSwitch("C3ROM", 0x0c00a, 0x0c00b, 0x0c017, RAMEvent.TYPE.WRITE, false)), + INTC8ROM(new IntC8SoftSwitch()), + LCBANK1(new MemorySoftSwitch("LangCardBank1", + new int[]{0x0c088, 0x0c089, 0x0c08a, 0x0c08b, 0x0c08c, 0x0c08d, 0x0c08e, 0x0c08f}, + new int[]{0x0c080, 0x0c081, 0x0c082, 0x0c083, 0x0c084, 0x0c085, 0x0c086, 0x0c087}, + new int[]{0x0c011}, RAMEvent.TYPE.ANY, false)), + LCRAM(new MemorySoftSwitch("LangCardRam/HRAMRD'", + new int[]{0x0c081, 0x0c082, 0x0c085, 0x0c086, 0x0c089, 0x0c08a, 0x0c08d, 0x0c08e}, + new int[]{0x0c080, 0x0c083, 0x0c084, 0x0c087, 0x0c088, 0x0c08b, 0x0c08c, 0x0c08f}, + new int[]{0x0c012}, RAMEvent.TYPE.ANY, false)), + LCWRITE(new Memory2SoftSwitch("LangCardWrite", + new int[]{0x0c080, 0x0c082, 0x0c084, 0x0c086, 0x0c088, 0x0c08a, 0x0c08c, 0x0c08e}, + new int[]{0x0c081, 0x0c083, 0x0c085, 0x0c087, 0x0c089, 0x0c08b, 0x0c08d, 0x0c08f}, + null, RAMEvent.TYPE.ANY, true)), + //Renamed as per Sather 5-7 + _80COL(new VideoSoftSwitch("80ColumnVideo (80COL/80VID)", 0x0c00c, 0x0c00d, 0x0c01f, RAMEvent.TYPE.WRITE, false)), + ALTCH(new VideoSoftSwitch("Mousetext", 0x0c00e, 0x0c00f, 0x0c01e, RAMEvent.TYPE.WRITE, false)), + TEXT(new VideoSoftSwitch("Text", 0x0c050, 0x0c051, 0x0c01a, RAMEvent.TYPE.ANY, true)), + MIXED(new VideoSoftSwitch("Mixed", 0x0c052, 0x0c053, 0x0c01b, RAMEvent.TYPE.ANY, false)), + PAGE2(new VideoSoftSwitch("Page2", 0x0c054, 0x0c055, 0x0c01c, RAMEvent.TYPE.ANY, false) { + @Override + public void stateChanged() { +// if (Computer.getComputer() == null) { +// return; +// } +// if (Computer.getComputer() == null && Computer.getComputer().getMemory() == null) { +// return; +// } +// if (Computer.getComputer() == null && Computer.getComputer().getVideo() == null) { +// return; +// } + + // PAGE2 is a hybrid switch; 80STORE ? memory : video + if (_80STORE.isOn()) { + Computer.getComputer().getMemory().configureActiveMemory(); + } else { + Computer.getComputer().getVideo().configureVideoMode(); + } + } + }), + HIRES(new VideoSoftSwitch("Hires", 0x0c056, 0x0c057, 0x0c01d, RAMEvent.TYPE.ANY, false)), + DHIRES(new VideoSoftSwitch("Double-hires", 0x0c05f, 0x0c05e, 0x0c07f, RAMEvent.TYPE.ANY, false)), + PB0(new MemorySoftSwitch("Pushbutton0", -1, -1, 0x0c061, RAMEvent.TYPE.ANY, null)), + PB1(new MemorySoftSwitch("Pushbutton1", -1, -1, 0x0c062, RAMEvent.TYPE.ANY, null)), + PB2(new MemorySoftSwitch("Pushbutton2", -1, -1, 0x0c063, RAMEvent.TYPE.ANY, null)), + PDLTRIG(new MemorySoftSwitch( + "PaddleTrigger", + null, + new int[]{0x0c070, 0x0c071, 0x0c072, 0x0c073, 0x0c074, 0x0c075, 0x0c076, 0x0c077, + 0x0c078, 0x0c079, 0x0c07a, 0x0c07b, 0x0c07c, 0x0c07d, 0x0c07e, 0x0c07f}, + null, RAMEvent.TYPE.ANY, false)), + PDL0(new MemorySoftSwitch("Paddle0", -1, -1, 0x0c064, RAMEvent.TYPE.ANY, false)), + PDL1(new MemorySoftSwitch("Paddle1", -1, -1, 0x0c065, RAMEvent.TYPE.ANY, false)), + PDL2(new MemorySoftSwitch("Paddle2", -1, -1, 0x0c066, RAMEvent.TYPE.ANY, false)), + PDL3(new MemorySoftSwitch("Paddle3", -1, -1, 0x0c067, RAMEvent.TYPE.ANY, false)), + AN0(new MemorySoftSwitch("Annunciator0", 0x0c058, 0x0c059, -1, RAMEvent.TYPE.ANY, false)), + AN1(new MemorySoftSwitch("Annunciator1", 0x0c05a, 0x0c05b, -1, RAMEvent.TYPE.ANY, false)), + AN2(new MemorySoftSwitch("Annunciator2", 0x0c05c, 0x0c05d, -1, RAMEvent.TYPE.ANY, false)), + AN3(new MemorySoftSwitch("Annunciator3", 0x0c05e, 0x0c05f, -1, RAMEvent.TYPE.ANY, false)), + KEYBOARD(new KeyboardSoftSwitch( + "Keyboard", + new int[]{0x0c010, 0x0c11, 0x0c012, 0x0c013, 0x0c014, 0x0c015, 0x0c016, 0x0c017, + 0x0c018, 0x0c019, 0x0c01a, 0x0c01b, 0x0c01c, 0x0c01d, 0x0c01e, 0x0c01f}, + null, + new int[]{0x0c000, 0x0c001, 0x0c002, 0x0c003, 0x0c004, 0x0c005, 0x0c006, 0x0c007, + 0x0c008, 0x0c009, 0x0c00a, 0x0c00b, 0x0c00c, 0x0c00d, 0x0c00e, 0x0c00f, 0x0c010}, + RAMEvent.TYPE.WRITE, false)), + //C010 should clear keyboard strobe when read as well + KEYBOARD_STROBE_READ(new SoftSwitch("KeyStrobe_Read", 0x0c010, -1, -1, RAMEvent.TYPE.READ, false) { + @Override + protected byte readSwitch() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void stateChanged() { + KEYBOARD.getSwitch().setState(false); + } + }), + TAPEOUT(new MemorySoftSwitch("TapeOut", 0x0c020, 0x0c020, 0x0c060, RAMEvent.TYPE.ANY, false)), + VBL(new VideoSoftSwitch("VBL", -1, -1, 0x0c019, RAMEvent.TYPE.ANY, false)), + FLOATING_BUS(new SoftSwitch("FloatingBus", null, null, new int[]{0x0C050, 0x0C051, 0x0C052, 0x0C053, 0x0C054}, RAMEvent.TYPE.READ, null) { + @Override + protected byte readSwitch() { + if (Computer.getComputer().getVideo() == null) { + return 0; + } + return Computer.getComputer().getVideo().getFloatingBus(); + } + + @Override + public void stateChanged() { + } + }); + + /* + 2C:VBL (new MemorySoftSwitch(0x0c070, 0x0*, 0x0c041, RAMEvent.TYPE.ANY, false)), + 2C:VBLENABLE (new MemorySoftSwitch(0x0c05a, 0x0c05b, 0x0-, RAMEvent.TYPE.ANY, false)), + 2C:XINT (new MemorySoftSwitch(0x0c015 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)), + 2C:YINT (new MemorySoftSwitch(0x0c017 (r), c048-c04f (r/w), 0x0-, 0x0-, RAMEvent.TYPE.ANY, false)), + 2C:MBUTTON (new MemorySoftSwitch(0x0*, 0x0*, 0x0c063, RAMEvent.TYPE.ANY, false)), + 2C:80/40 switch (new MemorySoftSwitch(0x0*, 0x0*, 0x0c060, RAMEvent.TYPE.ANY, false)), + 2C:XDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c066, RAMEvent.TYPE.ANY, false)), + 2C:YDirection (new MemorySoftSwitch(0x0*, 0x0*, 0x0c067, RAMEvent.TYPE.ANY, false)), + */ + private final SoftSwitch softswitch; + + /** + * Creates a new instance of SoftSwitches + */ + private SoftSwitches(SoftSwitch softswitch) { + this.softswitch = softswitch; + } + + public SoftSwitch getSwitch() { + return softswitch; + } + + public boolean getState() { + return softswitch.getState(); + } + + public final boolean isOn() { + return softswitch.getState(); + } + + public final boolean isOff() { + return !softswitch.getState(); + } + + @Override + public String toString() { + return softswitch.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/Speaker.java b/src/main/java/jace/apple2e/Speaker.java new file mode 100644 index 0000000..9d481a8 --- /dev/null +++ b/src/main/java/jace/apple2e/Speaker.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.config.ConfigurableField; +import jace.core.Computer; +import jace.core.Device; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.core.SoundMixer; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import static jace.core.Utility.*; +import java.io.FileNotFoundException; + +/** + * Apple // Speaker Emulation Created on May 9, 2007, 9:55 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Speaker extends Device { + + static boolean fileOutputActive = false; + static OutputStream out; + + public static void toggleFileOutput() { + if (fileOutputActive) { + try { + out.close(); + } catch (IOException ex) { + Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex); + } + out = null; + fileOutputActive = false; + } else { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.showSaveDialog(null); + File f = fileChooser.getSelectedFile(); + if (f == null) { + return; + } + if (f.exists()) { + int i = JOptionPane.showConfirmDialog(null, "Overwrite existing file?"); + if (i != JOptionPane.OK_OPTION && i != JOptionPane.YES_OPTION) { + return; + } + } + try { + out = new FileOutputStream(f); + fileOutputActive = true; + } catch (FileNotFoundException ex) { + Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + /** + * Counter tracks the number of cycles between sampling + */ + private double counter = 0; + /** + * Level is the number of cycles the speaker has been on + */ + private int level = 0; + /** + * Idle cycles counts the number of cycles the speaker has not been changed + * (used to deactivate sound when not in use) + */ + private int idleCycles = 0; + /** + * Number of samples in buffer + */ + static int BUFFER_SIZE = (int) (((float) SoundMixer.RATE) * 0.4); + // Number of samples available in output stream before playback happens (avoid extra blocking) +// static int MIN_PLAYBACK_BUFFER = BUFFER_SIZE / 2; + static int MIN_PLAYBACK_BUFFER = 64; + // Number of samples in buffer to wait until playback (avoid underrun) + private int MIN_SAMPLE_PLAYBACK = 64; + /** + * Playback volume (should be < 1423) + */ + @ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400") + public static int VOLUME = 600; + /** + * Number of idle cycles until speaker playback is deactivated + */ + @ConfigurableField(name = "Idle cycles before sleep", shortName = "idle") + public static int MAX_IDLE_CYCLES = 100000; + /** + * Java sound output + */ + private SourceDataLine sdl; + /** + * Manifestation of the apple speaker softswitch + */ + private boolean speakerBit = false; + // + /** + * Locking semaphore to prevent race conditions when working with buffer or + * related variables + */ + private final Object bufferLock = new Object(); + /** + * Double-buffer used for playing processed sound -- as one is played the + * other fills up. + */ + byte[] soundBuffer1; + byte[] soundBuffer2; + int currentBuffer = 1; + int bufferPos = 0; + private double TICKS_PER_SAMPLE = ((double) Motherboard.SPEED) / ((double) SoundMixer.RATE); + private double TICKS_PER_SAMPLE_FLOOR = Math.floor(TICKS_PER_SAMPLE); + Thread playbackThread; + private final RAMListener listener + = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + + @Override + public boolean isRelevant(RAMEvent e) { + return true; + } + + @Override + protected void doConfig() { + setScopeStart(0x0C030); + setScopeEnd(0x0C03F); + } + + @Override + protected void doEvent(RAMEvent e) { + if (e.getType() == RAMEvent.TYPE.WRITE) { + level += 2; + } else { + speakerBit = !speakerBit; + } + resetIdle(); + } + }; + + /** + * Creates a new instance of Speaker + */ + public Speaker() { + configureListener(); + reconfigure(); + } + + /** + * Suspend playback of sound + * @return + */ + @Override + public boolean suspend() { + boolean result = super.suspend(); + speakerBit = false; + if (playbackThread != null && playbackThread.isAlive()) { + playbackThread = null; + } + return result; + } + + /** + * Start or resume playback of sound + */ + @Override + public void resume() { + sdl = null; + try { + sdl = Motherboard.mixer.getLine(this); + } catch (LineUnavailableException ex) { + System.out.println("ERROR: Could not output sound: " + ex.getMessage()); + } + if (sdl != null) { + setRun(true); + counter = 0; + idleCycles = 0; + level = 0; + bufferPos = 0; + if (playbackThread == null || !playbackThread.isAlive()) { + playbackThread = new Thread(new Runnable() { + + @Override + public void run() { + int len; + while (isRunning()) { +// Motherboard.requestSpeed(this); + len = bufferPos; + if (len >= MIN_SAMPLE_PLAYBACK) { + byte[] buffer; + synchronized (bufferLock) { + len = bufferPos; + buffer = (currentBuffer == 1) ? soundBuffer1 : soundBuffer2; + currentBuffer = (currentBuffer == 1) ? 2 : 1; + bufferPos = 0; + } + sdl.write(buffer, 0, len); + if (fileOutputActive && out != null) { + try { + out.write(buffer, 0, len); + } catch (IOException ex) { + Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex); + } + } + } else { + try { + // Wait 12.5 ms, which is 1/8 the total duration of the buffer + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + Motherboard.cancelSpeedRequest(this); + Motherboard.mixer.returnLine(this); + + } + }); + playbackThread.setName("Speaker playback"); + playbackThread.start(); + } + } + } + + /** + * Reset idle counter whenever sound playback occurs + */ + public void resetIdle() { + idleCycles = 0; + if (!isRunning()) { + resume(); + } + } + + /** + * Motherboard cycle tick Every 23 ticks a sample will be added to the + * buffer If the buffer is full, this will block until there is room in the + * buffer, thus keeping the emulation in sync with the sound + */ + @Override + public void tick() { + if (!isRunning() || playbackThread == null) { + return; + } + if (idleCycles++ >= MAX_IDLE_CYCLES) { + suspend(); + } + if (speakerBit) { + level++; + } + counter += 1.0d; + if (counter >= TICKS_PER_SAMPLE) { + int sample = level * VOLUME; + int bytes = SoundMixer.BITS >> 3; + int shift = SoundMixer.BITS; + + // Force emulator to wait until sound buffer has been processed + int wait = 0; + while (bufferPos >= BUFFER_SIZE) { + if (wait++ > 1000) { + Computer.pause(); + detach(); + Computer.resume(); + Motherboard.enableSpeaker = false; + gripe("Sound playback is not working properly. Check your configuration and sound system to ensure they are set up properly."); + return; + } + try { + // Yield to other threads (e.g. sound) so that the buffer can drain + Thread.sleep(5); + } catch (InterruptedException ex) { + + } + } + + byte[] buf; + synchronized (bufferLock) { + if (currentBuffer == 1) { + buf = soundBuffer1; + } else { + buf = soundBuffer2; + } + int index = bufferPos; + for (int i = 0; i < SoundMixer.BITS; i += 8, index++) { + shift -= 8; + buf[index] = buf[index + bytes] = (byte) ((sample >> shift) & 0x0ff); + } + + bufferPos += bytes * 2; + } + + // Set level back to 0 + level = 0; + // Set counter to 0 + counter -= TICKS_PER_SAMPLE_FLOOR; + } + } + + /** + * Add a memory event listener for C03x for capturing speaker events + */ + private void configureListener() { + Computer.getComputer().getMemory().addListener(listener); + } + + private void removeListener() { + Computer.getComputer().getMemory().removeListener(listener); + } + + /** + * Returns "Speaker" + * + * @return "Speaker" + */ + @Override + protected String getDeviceName() { + return "Speaker"; + } + + @Override + public String getShortName() { + return "spk"; + } + + @Override + public final void reconfigure() { + if (soundBuffer1 != null && soundBuffer2 != null) { + return; + } + BUFFER_SIZE = 10000 * (SoundMixer.BITS >> 3); + MIN_SAMPLE_PLAYBACK = SoundMixer.BITS * 8; + soundBuffer1 = new byte[BUFFER_SIZE]; + soundBuffer2 = new byte[BUFFER_SIZE]; + } + + @Override + public void attach() { + configureListener(); + resume(); + } + + @Override + public void detach() { + removeListener(); + suspend(); + } +} diff --git a/src/main/java/jace/apple2e/VideoDHGR.java b/src/main/java/jace/apple2e/VideoDHGR.java new file mode 100644 index 0000000..0e5b09d --- /dev/null +++ b/src/main/java/jace/apple2e/VideoDHGR.java @@ -0,0 +1,747 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.core.Computer; +import jace.core.Font; +import jace.core.Palette; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.core.Video; +import jace.core.VideoWriter; +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.util.logging.Logger; + +/** + * This is the primary video rendering class, which provides all necessary video + * writers for every display mode as well as managing the display mode (via + * configureVideoMode). The quality of the color rendering is sub-par compared + * to VideoNTSC. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class VideoDHGR extends Video { + // Reorder bits 3,2,1,0 -> 0,3,2,1 + // Fixes double-hires color palette + + public final static int flipNybble[] = { + 0, 2, 4, 6, + 8, 10, 12, 14, + 1, 3, 5, 7, + 9, 11, 13, 15 + }; + private static final boolean USE_GS_MOUSETEXT = false; + private VideoWriter textPage1; + private VideoWriter textPage2; + private VideoWriter loresPage1; + private VideoWriter loresPage2; + private VideoWriter hiresPage1; + private VideoWriter hiresPage2; + // Special 80-column modes + private VideoWriter text80Page1; + private VideoWriter text80Page2; + private VideoWriter dloresPage1; + private VideoWriter dloresPage2; + private VideoWriter dhiresPage1; + private VideoWriter dhiresPage2; + // Mixed mode + private VideoWriter mixed; + private VideoWriter currentGraphicsWriter = null; + private VideoWriter currentTextWriter = null; + + /** + * Creates a new instance of VideoDHGR + */ + public VideoDHGR() { + hiresPage1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (hiresOffset[y] + 0x02000); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayHires(screen, xOffset, y, yGraphicsOffset + 0x02000); + } + }; + hiresPage2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (hiresOffset[y] + 0x04000); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayHires(screen, xOffset, y, yGraphicsOffset + 0x04000); + } + }; + dhiresPage1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (hiresOffset[y] + 0x02000); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayDoubleHires(screen, xOffset, y, yGraphicsOffset + 0x02000); + } + + @Override + public VideoWriter actualWriter() { + return hiresPage1; + } + }; + dhiresPage2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (hiresOffset[y] + 0x04000); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayDoubleHires(screen, xOffset, y, yGraphicsOffset + 0x04000); + } + + @Override + public VideoWriter actualWriter() { + return hiresPage2; + } + }; + textPage1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0400); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayText(screen, xOffset, y, yTextOffset + 0x0400); + } + }; + textPage2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0800); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayText(screen, xOffset, y, yTextOffset + 0x0800); + } + }; + text80Page1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0400); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayText80(screen, xOffset, y, yTextOffset + 0x0400); + } + + @Override + public VideoWriter actualWriter() { + return textPage1; + } + }; + text80Page2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0800); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayText80(screen, xOffset, y, yTextOffset + 0x0800); + } + + @Override + public VideoWriter actualWriter() { + return textPage2; + } + }; + loresPage1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0400); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayLores(screen, xOffset, y, yTextOffset + 0x0400); + } + + @Override + public VideoWriter actualWriter() { + return textPage1; + } + }; + loresPage2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0800); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayLores(screen, xOffset, y, yTextOffset + 0x0800); + } + + @Override + public VideoWriter actualWriter() { + return textPage2; + } + }; + dloresPage1 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0400); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayDoubleLores(screen, xOffset, y, yTextOffset + 0x0400); + } + + @Override + public VideoWriter actualWriter() { + return textPage1; + } + }; + dloresPage2 = new VideoWriter() { + @Override + public int getYOffset(int y) { + return (textOffset[y] + 0x0800); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayDoubleLores(screen, xOffset, y, yTextOffset + 0x0800); + } + + @Override + public VideoWriter actualWriter() { + return textPage2; + } + }; + mixed = new VideoWriter() { + @Override + public int getYOffset(int y) { + return actualWriter().getYOffset(y); + } + + @Override + public void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset) { + displayMixed(screen, xOffset, y, yTextOffset, yGraphicsOffset); + } + + @Override + public void markDirty(int y) { + actualWriter().actualWriter().markDirty(y); + } + + @Override + public void clearDirty(int y) { + actualWriter().actualWriter().clearDirty(y); + } + + @Override + public boolean isRowDirty(int y) { + return actualWriter().actualWriter().isRowDirty(y); + } + + @Override + public VideoWriter actualWriter() { + if (y < 160) { + return currentGraphicsWriter; + } + return currentTextWriter; + } + + @Override + public boolean isMixed() { + return true; + } + }; + registerDirtyFlagChecks(); + } + // color burst per byte (chat mauve compatibility) + boolean[] useColor = new boolean[80]; + + protected void displayDoubleHires(BufferedImage screen, int xOffset, int y, int rowAddress) { + // Skip odd columns since this does two at once + if ((xOffset & 0x01) == 1) { + return; + } + int b1 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset); + int b2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset); + int b3 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1); + int b4 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1); + int useColOffset = xOffset << 1; + // This shouldn't be necessary but prevents an index bounds exception when graphics modes are flipped (Race condition?) + if (useColOffset >= 77) { + useColOffset = 76; + } + useColor[useColOffset] = (b1 & 0x80) != 0; + useColor[useColOffset + 1] = (b2 & 0x80) != 0; + useColor[useColOffset + 2] = (b3 & 0x80) != 0; + useColor[useColOffset + 3] = (b4 & 0x80) != 0; + int dhgrWord = 0x07f & b1; + dhgrWord |= (0x07f & b2) << 7; + dhgrWord |= (0x07f & b3) << 14; + dhgrWord |= (0x07f & b4) << 21; + showDhgr(screen, times14[xOffset], y, dhgrWord); + } + boolean extraHalfBit = false; + + protected void displayHires(BufferedImage screen, int xOffset, int y, int rowAddress) { + // Skip odd columns since this does two at once + if ((xOffset & 0x01) == 1) { + return; + } + int b1 = 0x0ff & ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset); + int b2 = 0x0ff & ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1); + int dhgrWord = hgrToDhgr[(extraHalfBit && xOffset > 0) ? b1 | 0x0100 : b1][b2]; + extraHalfBit = (dhgrWord & 0x10000000) != 0; + showDhgr(screen, times14[xOffset], y, dhgrWord & 0xfffffff); +// If you want monochrome, use this instead... +// showBW(screen, times14[xOffset], y, dhgrWord); + } + // Take two consecutive bytes and double them, taking hi-bit into account + // This should yield a 28-bit word of 7 color dhgr pixels + // This looks like crap on text... + static final int[][] hgrToDhgr; + // Take two consecutive bytes and double them, disregarding hi-bit + // Useful for text mode + static final int[][] hgrToDhgrBW; + static final int[] times14; + static final int[] flipBits; + + static { + // complete reverse of 8 bits + flipBits = new int[256]; + for (int i = 0; i < 256; i++) { + flipBits[i] = (((i * 0x0802 & 0x22110) | (i * 0x8020 & 0x88440)) * 0x10101 >> 16) & 0x0ff; + } + + times14 = new int[40]; + for (int i = 0; i < 40; i++) { + times14[i] = i * 14; + } + hgrToDhgr = new int[512][256]; + hgrToDhgrBW = new int[256][256]; + for (int bb1 = 0; bb1 < 512; bb1++) { + for (int bb2 = 0; bb2 < 256; bb2++) { + int value = ((bb1 & 0x0181) >= 0x0101) ? 1 : 0; + int b1 = byteDoubler((byte) (bb1 & 0x07f)); + if ((bb1 & 0x080) != 0) { + b1 <<= 1; + } + int b2 = byteDoubler((byte) (bb2 & 0x07f)); + if ((bb2 & 0x080) != 0) { + b2 <<= 1; + } + if ((bb1 & 0x040) == 0x040 && (bb2 & 1) != 0) { + b2 |= 1; + } + value |= b1 | (b2 << 14); + if ((bb2 & 0x040) != 0) { + value |= 0x10000000; + } + hgrToDhgr[bb1][bb2] = value; + hgrToDhgrBW[bb1 & 0x0ff][bb2] = + byteDoubler((byte) bb1) | (byteDoubler((byte) bb2) << 14); + } + } + } + + protected void displayLores(BufferedImage screen, int xOffset, int y, int rowAddress) { + int c1 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF; + if ((y & 7) < 4) { + c1 &= 15; + } else { + c1 >>= 4; + } + DataBuffer b = screen.getRaster().getDataBuffer(); + int yOffset = xyOffset[y][times14[xOffset]]; + int color = Palette.color[c1].getRGB(); + // Unrolled loop, faster + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + } + + private void displayDoubleLores(BufferedImage screen, int xOffset, int y, int rowAddress) { + int c1 = ((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset) & 0x0FF; + int c2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF; + if ((y & 7) < 4) { + c1 &= 15; + c2 &= 15; + } else { + c1 >>= 4; + c2 >>= 4; + } + DataBuffer b = screen.getRaster().getDataBuffer(); + int yOffset = xyOffset[y][times14[xOffset]]; + int color = Palette.color[c1].getRGB(); + int color2 = Palette.color[c2].getRGB(); + // Unrolled loop, faster + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + b.setElem(yOffset++, color2); + } + boolean flashInverse = false; + int flashTimer = 0; + int FLASH_SPEED = 16; // UTAIIe:8-13,P7 - FLASH toggles every 16 scans + int[] currentCharMap = CHAR_MAP1; + static final int[] CHAR_MAP1; + static final int[] CHAR_MAP2; + static final int[] CHAR_MAP3; + + static { + // Generate screen text lookup maps ahead of time + // ALTCHR clear + // 00-3F - Inverse characters (uppercase only) "@P 0" + // 40-7F - Flashing characters (uppercase only) "@P 0" + // 80-BF - Normal characters (uppercase only) "@P 0" + // C0-DF - Normal characters (repeat 80-9F) "@P" + // E0-FF - Normal characters (lowercase) "`p" + + // ALTCHR set + // 00-3f - Inverse characters (uppercase only) "@P 0" + // 40-5f - Mousetext (//gs alts are at 0x46 and 0x47, swap with 0x11 and 0x12 for //e and //c) + // 60-7f - Inverse characters (lowercase only) + // 80-BF - Normal characters (uppercase only) + // C0-DF - Normal characters (repeat 80-9F) + // E0-FF - Normal characters (lowercase) + + + // MAP1: Normal map, flash inverse = false + CHAR_MAP1 = new int[256]; + // MAP2: Normal map, flash inverse = true + CHAR_MAP2 = new int[256]; + // MAP3: Alt map, mousetext mode + CHAR_MAP3 = new int[256]; + for (int b = 0; b < 256; b++) { + int mod = b % 0x020; + // Inverse + if (b < 0x020) { + CHAR_MAP1[b] = mod + 0x0c0; + CHAR_MAP2[b] = mod + 0x0c0; + CHAR_MAP3[b] = mod + 0x0c0; + } else if (b < 0x040) { + CHAR_MAP1[b] = mod + 0x0a0; + CHAR_MAP2[b] = mod + 0x0a0; + CHAR_MAP3[b] = mod + 0x0a0; + } else if (b < 0x060) { + // Flash/Mouse + CHAR_MAP1[b] = mod + 0x0c0; + CHAR_MAP2[b] = mod + 0x040; + if (!USE_GS_MOUSETEXT && mod == 6) { + CHAR_MAP3[b] = 0x011; + } else if (!USE_GS_MOUSETEXT && mod == 7) { + CHAR_MAP3[b] = 0x012; + } else { + CHAR_MAP3[b] = mod + 0x080; + } + } else if (b < 0x080) { + // Flash/Inverse lowercase + CHAR_MAP1[b] = mod + 0x0a0; + CHAR_MAP2[b] = mod + 0x020; + CHAR_MAP3[b] = mod + 0x0e0; + } else if (b < 0x0a0) { + // Normal uppercase + CHAR_MAP1[b] = mod + 0x040; + CHAR_MAP2[b] = mod + 0x040; + CHAR_MAP3[b] = mod + 0x040; + } else if (b < 0x0c0) { + // Normal uppercase + CHAR_MAP1[b] = mod + 0x020; + CHAR_MAP2[b] = mod + 0x020; + CHAR_MAP3[b] = mod + 0x020; + } else if (b < 0x0e0) { + // Normal uppercase (repeat) + CHAR_MAP1[b] = mod + 0x040; + CHAR_MAP2[b] = mod + 0x040; + CHAR_MAP3[b] = mod + 0x040; + } else { + // Normal lowercase + CHAR_MAP1[b] = mod + 0x060; + CHAR_MAP2[b] = mod + 0x060; + CHAR_MAP3[b] = mod + 0x060; + } + } + } + + @Override + public void vblankStart() { + // ALTCHR set only affects character mapping and disables FLASH. + if (SoftSwitches.ALTCH.isOn()) { + currentCharMap = CHAR_MAP3; + } else { + flashTimer--; + if (flashTimer <= 0) { + markFlashDirtyBits(); + flashTimer = FLASH_SPEED; + flashInverse = !flashInverse; + if (flashInverse) { + currentCharMap = CHAR_MAP2; + } else { + currentCharMap = CHAR_MAP1; + } + } + } + super.vblankStart(); + } + + @Override + public void vblankEnd() { + } + + private int getFontChar(byte b) { + return currentCharMap[b & 0x0ff]; + } + + protected void displayText(BufferedImage screen, int xOffset, int y, int rowAddress) { + // Skip odd columns since this does two at once + if ((xOffset & 0x01) == 1) { + return; + } + int yOffset = y & 7; + byte byte2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1); + int c1 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset)); + int c2 = getFontChar(byte2); + int b1 = Font.getByte(c1, yOffset); + int b2 = Font.getByte(c2, yOffset); + // Why is this getting inversed now? Bug in hgrToDhgrBW? + // Nick says: are you getting confused because the //e video ROM is inverted? (1=black) + int out = hgrToDhgrBW[b1][b2]; + showBW(screen, times14[xOffset], y, out); + } + + protected void displayText80(BufferedImage screen, int xOffset, int y, int rowAddress) { + // Skip odd columns since this does two at once + if ((xOffset & 0x01) == 1) { + return; + } + int yOffset = y & 7; + int c1 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset)); + int c2 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset)); + int c3 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getAuxVideoMemory().readByte(rowAddress + xOffset + 1)); + int c4 = getFontChar(((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1)); + int bits = Font.getByte(c1, yOffset) | (Font.getByte(c2, yOffset) << 7) + | (Font.getByte(c3, yOffset) << 14) | (Font.getByte(c4, yOffset) << 21); + showBW(screen, times14[xOffset], y, bits); + } + + private void displayMixed(BufferedImage screen, int xOffset, int y, int textOffset, int graphicsOffset) { + mixed.actualWriter().displayByte(screen, xOffset, y, textOffset, graphicsOffset); + } + protected boolean hiresMode = false; + public boolean dhgrMode = false; + + @Override + public void configureVideoMode() { + boolean page2 = SoftSwitches.PAGE2.isOn() && SoftSwitches._80STORE.isOff(); + dhgrMode = SoftSwitches._80COL.getState() && SoftSwitches.DHIRES.getState() && SoftSwitches.HIRES.getState(); + currentTextWriter = + SoftSwitches._80COL.getState() + ? page2 + ? text80Page2 : text80Page1 + : page2 + ? textPage2 : textPage1; + currentGraphicsWriter = + SoftSwitches._80COL.getState() && SoftSwitches.DHIRES.getState() + ? SoftSwitches.HIRES.getState() + ? page2 + ? dhiresPage2 : dhiresPage1 + : page2 + ? dloresPage2 : dloresPage1 + : SoftSwitches.HIRES.getState() + ? page2 + ? hiresPage2 : hiresPage1 + : page2 + ? loresPage2 : loresPage1; + setCurrentWriter( + SoftSwitches.TEXT.getState() ? currentTextWriter + : SoftSwitches.MIXED.getState() ? mixed + : currentGraphicsWriter); + hiresMode = !SoftSwitches.DHIRES.getState(); + } + + protected void showDhgr(BufferedImage screen, int xOffset, int y, int dhgrWord) { + //Graphics2D g = (Graphics2D) screen.getGraphics(); + DataBuffer b = screen.getRaster().getDataBuffer(); + int yOffset = xyOffset[y][xOffset]; + try { + for (int i = 0; i < 7; i++) { + int color = Palette.color[flipNybble[dhgrWord & 15]].getRGB(); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + b.setElem(yOffset++, color); + dhgrWord >>= 4; + } + } catch (ArrayIndexOutOfBoundsException ex) { + Logger.getLogger(getClass().getName()).warning("Went out of bounds in video display"); + } + } + static final int BLACK = Color.BLACK.getRGB(); + static final int WHITE = Color.WHITE.getRGB(); + static final int[][] xyOffset; + + static { + xyOffset = new int[192][560]; + for (int y = 0; y < 192; y++) { + for (int x = 0; x < 560; x++) { + xyOffset[y][x] = y * 560 + x; + } + } + } + + protected void showBW(BufferedImage screen, int xOffset, int y, int dhgrWord) { + int color = 0; + // Using the data buffer directly is about 15 times faster than setRGB + // This is because setRGB does extra (useless) color model logic + // For that matter even Graphics.drawLine is faster than setRGB! + DataBuffer b = screen.getRaster().getDataBuffer(); + // This is equivilant to y*560 but is 5% faster + // Also, adding xOffset now makes it additionally 5% faster + //int yOffset = ((y << 4) + (y << 5) + (y << 9))+xOffset; + + //is this lookup faster? + int yOffset = xyOffset[y][xOffset]; + for (int i = 0; i < 28; i++) { + // yOffset++ is used instead of yOffset+i, because it is faster + b.setElem(yOffset++, (dhgrWord & 1) == 1 ? WHITE : BLACK); + dhgrWord >>= 1; + } + } + + /** + * + */ + @Override + public void doPostDraw() { + } + + /** + * + * @return + */ + @Override + protected String getDeviceName() { + return "DHGR-Capable Video"; + } + + private void markFlashDirtyBits() { + // TODO: Be smarter about detecting where flash is used... one day... + for (int row = 0; row < 192; row++) { + currentTextWriter.markDirty(row); + } + } + + private void registerDirtyFlagChecks() { + ((RAM128k) Computer.getComputer().getMemory()).addListener(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0400); + setScopeEnd(0x0bff); + } + + @Override + protected void doEvent(RAMEvent e) { + int row = textRowLookup[e.getAddress() & 0x03ff]; +// int row = identifyTextRow(e.getAddress() & 0x03ff); + if (row > 23) { + return; + } + VideoWriter tmark = (e.getAddress() < 0x0800) ? textPage1 : textPage2; + row <<= 3; + int yy = row + 8; + for (int y = row; y < yy; y++) { + tmark.markDirty(y); + } + } + }); + ((RAM128k) Computer.getComputer().getMemory()).addListener(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x2000); + setScopeEnd(0x5fff); + } + + @Override + protected void doEvent(RAMEvent e) { + int row = hiresRowLookup[e.getAddress() & 0x01fff]; +// int row = identifyHiresRow(e.getAddress() & 0x03fff); + if (row < 0 || row >= 192) { + return; + } + VideoWriter mark = (e.getAddress() < 0x04000) ? hiresPage1 : hiresPage2; + mark.markDirty(row); + } + }); + } + + @Override + public void reconfigure() { + // Do nothing (for now) + } + + @Override + public void attach() { + // Do nothing + } + + @Override + public void detach() { + // Do nothing + } + + @Override + public void hblankStart(BufferedImage screen, int y, boolean isDirty) { + // Do nothing + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/VideoNTSC.java b/src/main/java/jace/apple2e/VideoNTSC.java new file mode 100644 index 0000000..17a1b53 --- /dev/null +++ b/src/main/java/jace/apple2e/VideoNTSC.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e; + +import jace.config.ConfigurableField; +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.core.Video; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.util.HashSet; +import java.util.Set; + +/** + * Provides a clean color monitor simulation, complete with text-friendly + * palette and mixed color/bw (mode 7) rendering. This class extends the + * VideoDHGR class to provide all necessary video writers and other rendering + * mechanics, and then overrides the actual output routines (showBW, showDhgr) with more suitable + * (and much prettier) alternatives. Rather than draw to the video buffer every + * cycle, rendered screen info is pushed into a buffer with mask bits (to + * indicate B&W vs color) And the actual conversion happens at the end of the + * scanline during the HBLANK period. This video rendering was inspired by + * Blargg but was ultimately rewritten from scratch once the color palette was + * implemented. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class VideoNTSC extends VideoDHGR { + + @ConfigurableField(name = "Text palette", shortName = "textPalette", defaultValue = "false", description = "Use text-friendly color palette") + public static boolean useTextPalette = false; + static int activePalette[][]; + @ConfigurableField(name = "Video 7", shortName = "video7", defaultValue = "true", description = "Enable Video 7 RGB rendering support") + public static boolean enableVideo7 = false; + // Scanline represents 560 bits, divided up into 28-bit words + int[] scanline = new int[20]; + static int[] divBy28 = new int[560]; + + static { + for (int i = 0; i < 560; i++) { + divBy28[i] = i / 28; + } + } + int pos = 0; + int lastKnownY = -1; + boolean colorActive = false; + int rowStart = 0; + + @Override + protected void showBW(BufferedImage screen, int xOffset, int y, int dhgrWord) { + if (lastKnownY != y) { + lastKnownY = y; + pos = rowStart = divBy28[xOffset]; + colorActive = false; + } else { + if (pos > 20) pos-=20; + } + doDisplay(screen, xOffset, y, dhgrWord); + } + + @Override + protected void showDhgr(BufferedImage screen, int xOffset, int y, int dhgrWord) { + if (lastKnownY != y) { + lastKnownY = y; + pos = rowStart = divBy28[xOffset]; + colorActive = true; + } + doDisplay(screen, xOffset, y, dhgrWord); + } + + @Override + protected void displayLores(BufferedImage screen, int xOffset, int y, int rowAddress) { + // Skip odd columns since this does two at once + if ((xOffset & 0x01) == 1) { + return; + } + + if (lastKnownY != y) { + lastKnownY = y; + pos = rowStart = divBy28[xOffset]; + colorActive = true; + } + int c1 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset) & 0x0FF; + if ((y & 7) < 4) { + c1 &= 15; + } else { + c1 >>= 4; + } + int c2 = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory().readByte(rowAddress + xOffset + 1) & 0x0FF; + if ((y & 7) < 4) { + c2 &= 15; + } else { + c2 >>= 4; + } + int pat = c1 | c1 << 4 | c1 << 8 | (c1 & 3) << 12; + pat |= (c2 & 12) << 12 | c2 << 16 | c2 << 20 | c2 << 24; + scanline[pos++] = pat; + } + + private void doDisplay(BufferedImage screen, int xOffset, int y, int dhgrWord) { + if (pos >= 20) pos -= 20; + scanline[pos] = dhgrWord; + pos++; + } + + @Override + public void hblankStart(BufferedImage screen, int y, boolean isDirty) { + if (isDirty) { + renderScanline(screen, y); + } + lastKnownY = -1; + } + // Offset is based on location in graphics buffer that corresponds with the row and + // a number (0-20) that represents how much of the scanline was rendered + // This is based off the xyOffset but is different because of P + static int pyOffset[][]; + + static { + pyOffset = new int[192][21]; + for (int y = 0; y < 192; y++) { + for (int p = 0; p < 21; p++) { + pyOffset[y][p] = (y * 560) + (p * 28); + } + } + } + + private void renderScanline(BufferedImage screen, int y) { + DataBuffer b = screen.getRaster().getDataBuffer(); + try { + // This is equivilant to y*560 but is 5% faster + //int yOffset = ((y << 4) + (y << 5) + (y << 9))+xOffset; + + // For some reason this jumps up to 40 in the wayout title screen (?) + int p = pyOffset[y][rowStart]; + if (rowStart > 0) { + getCurrentWriter().markDirty(y); + } + // Reset scanline position + if (colorActive && (!dhgrMode || !enableVideo7 || graphicsMode.isColor())) { + int byteCounter = 0; + for (int s = rowStart; s < 20; s++) { + int add = 0; + int bits; + if (hiresMode) { + bits = scanline[s] << 2; + if (s > 0) { + bits |= (scanline[s - 1] >> 26) & 3; + } + } else { + bits = scanline[s] << 3; + if (s > 0) { + bits |= (scanline[s - 1] >> 25) & 7; + } + } + if (s < 19) { + add = (scanline[s + 1] & 7); + } + boolean isBW = false; + if (enableVideo7 && dhgrMode && graphicsMode == rgbMode.mix) { + for (int i = 0; i < 28; i++) { + if (i % 7 == 0) { + isBW = !hiresMode && !useColor[byteCounter]; + byteCounter++; + } + if (isBW) { + b.setElem(p++, ((bits & 0x8) == 0) ? BLACK : WHITE); + } else { + b.setElem(p++, activePalette[i % 4][bits & 0x07f]); + } + bits >>= 1; + if (i == 20) { + bits |= add << (hiresMode ? 9 : 10); + } + } + } else { + for (int i = 0; i < 28; i++) { + b.setElem(p++, activePalette[i % 4][bits & 0x07f]); + bits >>= 1; + if (i == 20) { + bits |= add << (hiresMode ? 9 : 10); + } + } + } + } + } else { + for (int s = rowStart; s < 20; s++) { + int bits = scanline[s]; + for (int i = 0; i < 28; i++) { + b.setElem(p++, ((bits & 1) == 0) ? BLACK : WHITE); + bits >>= 1; + } + } + } + } catch (ArrayIndexOutOfBoundsException ex) { + // Flag this scanline to be written again, something screwed up! + // This only happens during a race condition when the video + // mode changes at just the wrong time. + getCurrentWriter().markDirty(y); + } + } + // y Range [0,1] + public static final double MIN_Y = 0; + public static final double MAX_Y = 1; + // i Range [-0.5957, 0.5957] + public static final double MAX_I = 0.5957; + // q Range [-0.5226, 0.5226] + public static final double MAX_Q = 0.5226; + static final int solidPalette[][] = new int[4][128]; + static final int textPalette[][] = new int[4][128]; + static final double[][] yiq = { + {0.0, 0.0, 0.0}, //0000 0 + {0.25, 0.5, 0.5}, //0001 1 + {0.25, -0.5, 0.5}, //0010 2 + {0.5, 0.0, 1.0}, //0011 3 +Q + {0.25, -0.5, -0.5}, //0100 4 + {0.5, 0.0, 0.0}, //0101 5 + {0.5, -1.0, 0.0}, //0110 6 +I + {0.75, -0.5, 0.5}, //0111 7 + {0.25, 0.5, -0.5}, //1000 8 + {0.5, 1.0, 0.0}, //1001 9 -I + {0.5, 0.0, 0.0}, //1010 a + {0.75, 0.5, 0.5}, //1011 b + {0.5, 0.0, -1.0}, //1100 c -Q + {0.75, 0.5, -0.5}, //1101 d + {0.75, -0.5, -0.5}, //1110 e + {1.0, 0.0, 0.0}, //1111 f + }; + + static { + int maxLevel = 10; + for (int offset = 0; offset < 4; offset++) { + for (int pattern = 0; pattern < 128; pattern++) { + int level = (pattern & 1) + + ((pattern >> 1) & 1) * 1 + + ((pattern >> 2) & 1) * 2 + + ((pattern >> 3) & 1) * 4 + + ((pattern >> 4) & 1) * 2 + + ((pattern >> 5) & 1) * 1; + + int col = (pattern >> 2) & 0x0f; + for (int rot = 0; rot < offset; rot++) { + col = ((col & 8) >> 3) | ((col << 1) & 0x0f); + } + double y1 = yiq[col][0]; + double y2 = ((double) level / (double) maxLevel); + solidPalette[offset][pattern] = (255 << 24) | yiqToRgb(y1, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q); + textPalette[offset][pattern] = (255 << 24) | yiqToRgb(y2, yiq[col][1] * MAX_I, yiq[col][2] * MAX_Q); + } + } + // Avoid NPE just in case. + activePalette = solidPalette; + } + + static public int yiqToRgb(double y, double i, double q) { + int r = (int) (normalize((y + 0.956 * i + 0.621 * q), 0, 1) * 255); + int g = (int) (normalize((y - 0.272 * i - 0.647 * q), 0, 1) * 255); + int b = (int) (normalize((y - 1.105 * i + 1.702 * q), 0, 1) * 255); + return (r << 16) | (g << 8) | b; + } + + public static double normalize(double x, double minX, double maxX) { + if (x < minX) { + return minX; + } + if (x > maxX) { + return maxX; + } + return x; + } + + @Override + public void reconfigure() { + detach(); + activePalette = useTextPalette ? textPalette : solidPalette; + super.reconfigure(); + attach(); + } + // The following section captures changes to the RGB mode + // The details of this are in Brodener's patent application #4631692 + // http://www.freepatentsonline.com/4631692.pdf + // as well as the AppleColor adapter card manual + // http://apple2.info/download/Ext80ColumnAppleColorCardHR.pdf + rgbMode graphicsMode = rgbMode.color; + + public static enum rgbMode { + + color(true), mix(true), bw(false), _160col(false); + boolean colorMode = false; + + rgbMode(boolean c) { + this.colorMode = c; + } + + public boolean isColor() { + return colorMode; + } + } + + public static enum ModeStateChanges { + + SET_AN3, CLEAR_AN3, SET_80, CLEAR_80; + } + boolean f1 = true; + boolean f2 = true; + boolean an3 = true; + + public void rgbStateChange(ModeStateChanges state) { + switch (state) { + case CLEAR_80: + break; + case CLEAR_AN3: + an3 = false; + break; + case SET_80: + break; + case SET_AN3: + if (!an3) { + f2 = f1; + f1 = SoftSwitches._80COL.getState(); + } + an3 = true; + break; + } +// This is the more technically correct implementation except for two issues: +// 1) 160-column mode isn't implemented so it's not worth bothering to capture that state +// 2) A lot of programs are clueless about RGB modes so it's good to default to normal color mode +// graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode._160col : rgbMode.bw); + graphicsMode = f1 ? (f2 ? rgbMode.color : rgbMode.mix) : (f2 ? rgbMode.color : rgbMode.bw); +// System.out.println(state + ": "+ graphicsMode); + } + // These catch changes to the RGB mode to toggle between color, BW and mixed + static Set rgbStateListeners = new HashSet<>(); + + static { + rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0c05e); + } + + @Override + protected void doEvent(RAMEvent e) { + Video v = Computer.getComputer().getVideo(); + if (v instanceof VideoNTSC) { + ((VideoNTSC) v).rgbStateChange(ModeStateChanges.CLEAR_AN3); + } + } + }); + rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0c05f); + } + + @Override + protected void doEvent(RAMEvent e) { + Video v = Computer.getComputer().getVideo(); + if (v instanceof VideoNTSC) { + ((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_AN3); + } + } + }); + rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0fa62); + } + + @Override + protected void doEvent(RAMEvent e) { + Video v = Computer.getComputer().getVideo(); + if (v instanceof VideoNTSC) { + // When reset hook is called, reset the graphics mode + // This is useful in case a program is running that + // is totally clueless how to set the RGB state correctly. + ((VideoNTSC) v).f1 = true; + ((VideoNTSC) v).f2 = true; + ((VideoNTSC) v).an3 = false; + ((VideoNTSC) v).graphicsMode = rgbMode.color; + } + } + }); + rgbStateListeners.add(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0c00d); + } + + @Override + protected void doEvent(RAMEvent e) { + Video v = Computer.getComputer().getVideo(); + if (v instanceof VideoNTSC) { + ((VideoNTSC) v).rgbStateChange(ModeStateChanges.SET_80); + } + } + }); + + + } + + @Override + public void detach() { + super.detach(); + rgbStateListeners.stream().forEach((l) -> { + Computer.getComputer().getMemory().removeListener(l); + }); + } + + @Override + public void attach() { + super.attach(); + rgbStateListeners.stream().forEach((l) -> { + Computer.getComputer().getMemory().addListener(l); + }); + } +} diff --git a/src/main/java/jace/apple2e/softswitch/IntC8SoftSwitch.java b/src/main/java/jace/apple2e/softswitch/IntC8SoftSwitch.java new file mode 100644 index 0000000..950dfd2 --- /dev/null +++ b/src/main/java/jace/apple2e/softswitch/IntC8SoftSwitch.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e.softswitch; + +import jace.apple2e.SoftSwitches; +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.core.SoftSwitch; + +/** + * Very funky softswitch which controls Slot 3 / C8 ROM behavior (and is the + * reason why some cards don't like Slot 3) + * Created on February 1, 2007, 9:40 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class IntC8SoftSwitch extends SoftSwitch { + + /** + * Creates a new instance of IntC8SoftSwitch + */ + public IntC8SoftSwitch() { + super("InternalC8Rom", false); + // INTC8Rom should activate whenever C3xx memory is accessed and SLOTC3ROM is off + addListener( + new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0C300); + setScopeEnd(0x0C3FF); + } + + @Override + protected void doEvent(RAMEvent e) { + if (SoftSwitches.SLOTC3ROM.isOff()) { + setState(true); + } + } + }); + + // INTCXRom shoud deactivate whenever CFFF is accessed + addListener( + new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + protected void doConfig() { + setScopeStart(0x0CFFF); + } + + @Override + protected void doEvent(RAMEvent e) { + setState(false); + } + }); + } + + @Override + protected byte readSwitch() { + return 0; + } + + @Override + public void stateChanged() { + if (Computer.getComputer().getMemory() != null) { + Computer.getComputer().getMemory().configureActiveMemory(); + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/softswitch/KeyboardSoftSwitch.java b/src/main/java/jace/apple2e/softswitch/KeyboardSoftSwitch.java new file mode 100644 index 0000000..421cd49 --- /dev/null +++ b/src/main/java/jace/apple2e/softswitch/KeyboardSoftSwitch.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e.softswitch; + +import jace.core.Keyboard; +import jace.core.RAMEvent; +import jace.core.SoftSwitch; + +/** + * Keyboard keypress strobe -- on = key pressed + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class KeyboardSoftSwitch extends SoftSwitch { + public KeyboardSoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) { + super(name,offAddress,onAddress,queryAddress,changeType,initalState); + } + + public KeyboardSoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) { + super(name,offAddrs,onAddrs,queryAddrs,changeType,initalState); + } + + public void stateChanged() { + Keyboard.clearStrobe(); + } + + /** + * return current keypress (if high bit set, strobe is not cleared) + * @return + */ + @Override + public byte readSwitch() { + return Keyboard.readState(); + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/softswitch/Memory2SoftSwitch.java b/src/main/java/jace/apple2e/softswitch/Memory2SoftSwitch.java new file mode 100644 index 0000000..9780bb2 --- /dev/null +++ b/src/main/java/jace/apple2e/softswitch/Memory2SoftSwitch.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e.softswitch; + +import jace.core.RAMEvent.TYPE; + +/** + * A softswitch that requires two consecutive accesses to flip + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Memory2SoftSwitch extends MemorySoftSwitch { + public Memory2SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, TYPE changeType, Boolean initalState) { + super(name, offAddress, onAddress, queryAddress, changeType, initalState); + } + + public Memory2SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, TYPE changeType, Boolean initalState) { + super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState); + } + + // The switch must be set true two times in a row before it will actually be set. + int count = 0; + @Override + public void setState(boolean newState) { + if (!newState) { + count = 0; + super.setState(newState); + } else { + count++; + if (count >= 2) { + super.setState(newState); + count = 0; + } + } + } + + @Override + public String toString() { + return getName()+(getState()?":1":":0")+"~~"+count; + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/softswitch/MemorySoftSwitch.java b/src/main/java/jace/apple2e/softswitch/MemorySoftSwitch.java new file mode 100644 index 0000000..16a95f7 --- /dev/null +++ b/src/main/java/jace/apple2e/softswitch/MemorySoftSwitch.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e.softswitch; + +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.SoftSwitch; + +/** + * A memory softswitch is a softswitch which triggers a memory reconfiguration + * after its value is changed. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class MemorySoftSwitch extends SoftSwitch { + + public MemorySoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) { + super(name, offAddress, onAddress, queryAddress, changeType, initalState); + } + + public MemorySoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) { + super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState); + } + + public void stateChanged() { +// System.out.println(getName()+ " was switched to "+getState()); + if (Computer.getComputer().getMemory() != null) { + Computer.getComputer().getMemory().configureActiveMemory(); + } + } + + // Todo: Implement floating bus, maybe? + protected byte readSwitch() { + return (byte) (getState() ? 0x0A0 : 0x020); + } +} \ No newline at end of file diff --git a/src/main/java/jace/apple2e/softswitch/VideoSoftSwitch.java b/src/main/java/jace/apple2e/softswitch/VideoSoftSwitch.java new file mode 100644 index 0000000..36c81e2 --- /dev/null +++ b/src/main/java/jace/apple2e/softswitch/VideoSoftSwitch.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.apple2e.softswitch; + +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.SoftSwitch; + +/** + * A video softswitch is a softswitch which triggers a change in video mode when + * it is altered. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class VideoSoftSwitch extends SoftSwitch { + + public VideoSoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) { + super(name, offAddress, onAddress, queryAddress, changeType, initalState); + } + + public VideoSoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) { + super(name, offAddrs, onAddrs, queryAddrs, changeType, initalState); + } + + public void stateChanged() { +// System.out.println("Set "+getName()+" -> "+getState()); + if (Computer.getComputer().getVideo() != null) { + Computer.getComputer().getVideo().configureVideoMode(); + } + } + + protected byte readSwitch() { +// System.out.println("Read "+getName()+" = "+getState()); + return (byte) (getState() ? 0x080 : 0x000); + } +} diff --git a/src/main/java/jace/applesoft/Command.java b/src/main/java/jace/applesoft/Command.java new file mode 100755 index 0000000..d5759e3 --- /dev/null +++ b/src/main/java/jace/applesoft/Command.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.applesoft; + +import java.util.ArrayList; +import java.util.List; + +/** + * A command is a list of parts, either raw bytes (ascii text) or tokens. When + * put together they represent a single basic statement. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Command { + + public static enum TOKEN { + + END((byte) 0x080, "END"), + FOR((byte) 0x081, "FOR"), + NEXT((byte) 0x082, "NEXT"), + DATA((byte) 0x083, "DATA"), + INPUT((byte) 0x084, "INPUT"), + DEL((byte) 0x085, "DEL"), + DIM((byte) 0x086, "DIM"), + READ((byte) 0x087, "READ"), + GR((byte) 0x088, "GR"), + TEXT((byte) 0x089, "TEXT"), + PR((byte) 0x08A, "PR#"), + IN((byte) 0x08B, "IN#"), + CALL((byte) 0x08C, "CALL"), + PLOT((byte) 0x08D, "PLOT"), + HLIN((byte) 0x08E, "HLIN"), + VLIN((byte) 0x08F, "VLIN"), + HGR2((byte) 0x090, "HGR2"), + HGR((byte) 0x091, "HGR"), + HCOLOR((byte) 0x092, "HCOLOR="), + HPLOT((byte) 0x093, "HPLOT"), + DRAW((byte) 0x094, "DRAW"), + XDRAW((byte) 0x095, "XDRAW"), + HTAB((byte) 0x096, "HTAB"), + HOME((byte) 0x097, "HOME"), + ROT((byte) 0x098, "ROT="), + SCALE((byte) 0x099, "SCALE="), + SHLOAD((byte) 0x09A, "SHLOAD"), + TRACE((byte) 0x09B, "TRACE"), + NOTRACE((byte) 0x09C, "NOTRACE"), + NORMAL((byte) 0x09D, "NORMAL"), + INVERSE((byte) 0x09E, "INVERSE"), + FLASH((byte) 0x09F, "FLASH"), + COLOR((byte) 0x0A0, "COLOR="), + POP((byte) 0x0A1, "POP"), + VTAB((byte) 0x0A2, "VTAB"), + HIMEM((byte) 0x0A3, "HIMEM:"), + LOMEM((byte) 0x0A4, "LOMEM:"), + ONERR((byte) 0x0A5, "ONERR"), + RESUME((byte) 0x0A6, "RESUME"), + RECALL((byte) 0x0A7, "RECALL"), + STORE((byte) 0x0A8, "STORE"), + SPEED((byte) 0x0A9, "SPEED="), + LET((byte) 0x0AA, "LET"), + GOTO((byte) 0x0AB, "GOTO"), + RUN((byte) 0x0AC, "RUN"), + IF((byte) 0x0AD, "IF"), + RESTORE((byte) 0x0AE, "RESTORE"), + AMPERSAND((byte) 0x0AF, "&"), + GOSUB((byte) 0x0B0, "GOSUB"), + RETURN((byte) 0x0B1, "RETURN"), + REM((byte) 0x0B2, "REM"), + STOP((byte) 0x0B3, "STOP"), + ONGOTO((byte) 0x0B4, "ON"), + WAIT((byte) 0x0B5, "WAIT"), + LOAD((byte) 0x0B6, "LOAD"), + SAVE((byte) 0x0B7, "SAVE"), + DEF((byte) 0x0B8, "DEF"), + POKE((byte) 0x0B9, "POKE"), + PRINT((byte) 0x0BA, "PRINT"), + CONT((byte) 0x0BB, "CONT"), + LIST((byte) 0x0BC, "LIST"), + CLEAR((byte) 0x0BD, "CLEAR"), + GET((byte) 0x0BE, "GET"), + NEW((byte) 0x0BF, "NEW"), + TAB((byte) 0x0C0, "TAB("), + TO((byte) 0x0C1, "TO"), + FN((byte) 0x0C2, "FN"), + SPC((byte) 0x0c3, "SPC"), + THEN((byte) 0x0c4, "THEN"), + AT((byte) 0x0c5, "AT"), + NOT((byte) 0x0c6, "NOT"), + STEP((byte) 0x0c7, "STEP"), + PLUS((byte) 0x0c8, "+"), + MINUS((byte) 0x0c9, "-"), + MULTIPLY((byte) 0x0Ca, "*"), + DIVIDE((byte) 0x0Cb, "/"), + POWER((byte) 0x0Cc, "^"), + AND((byte) 0x0Cd, "AND"), + OR((byte) 0x0Ce, "OR"), + GREATER((byte) 0x0CF, ">"), + EQUAL((byte) 0x0d0, "="), + LESS((byte) 0x0d1, "<"), + SGN((byte) 0x0D2, "SGN"), + INT((byte) 0x0D3, "INT"), + ABS((byte) 0x0D4, "ABS"), + USR((byte) 0x0D5, "USR"), + FRE((byte) 0x0D6, "FRE"), + SCREEN((byte) 0x0D7, "SCRN("), + PDL((byte) 0x0D8, "PDL"), + POS((byte) 0x0D9, "POS"), + SQR((byte) 0x0DA, "SQR"), + RND((byte) 0x0DB, "RND"), + LOG((byte) 0x0DC, "LOG"), + EXP((byte) 0x0DD, "EXP"), + COS((byte) 0x0DE, "COS"), + SIN((byte) 0x0DF, "SIN"), + TAN((byte) 0x0E0, "TAN"), + ATN((byte) 0x0E1, "ATN"), + PEEK((byte) 0x0E2, "PEEK"), + LEN((byte) 0x0E3, "LEN"), + STR((byte) 0x0E4, "STR$"), + VAL((byte) 0x0E5, "VAL"), + ASC((byte) 0x0E6, "ASC"), + CHR((byte) 0x0E7, "CHR$"), + LEFT((byte) 0x0E8, "LEFT$"), + RIGHT((byte) 0x0E9, "RIGHT$"), + MID((byte) 0x0EA, "MID$"); + private String str; + private byte b; + + TOKEN(byte b, String str) { + this.b = b; + this.str = str; + } + + @Override + public String toString() { + return str; + } + + public static TOKEN fromByte(byte b) { + for (TOKEN t : values()) { + if (t.b == b) { + return t; + } + } + return null; + } + } + + public static class ByteOrToken { + + byte b; + TOKEN t; + boolean isToken = false; + + public ByteOrToken(byte b) { + TOKEN t = TOKEN.fromByte(b); + if (t != null) { + isToken = true; + this.t = t; + } else { + isToken = false; + this.b = b; + } + + } + + @Override + public String toString() { + return isToken + ? " " + t.toString() + " " + : String.valueOf((char) b); + } + } + List parts = new ArrayList(); + + @Override + public String toString() { + String out = ""; + for (ByteOrToken p : parts) { + out += p.toString(); + } + return out; + } +} diff --git a/src/main/java/jace/applesoft/Line.java b/src/main/java/jace/applesoft/Line.java new file mode 100755 index 0000000..78a0f97 --- /dev/null +++ b/src/main/java/jace/applesoft/Line.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.applesoft; + +import java.util.ArrayList; +import java.util.List; + +/** + * Representation of a line of applesoft basic, having a line number and a list of program commands. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Line { + + private static char STATEMENT_BREAK = ':'; // delimits multiple commands, the colon character + private int number; + private Line next; + private Line previous; + private List commands = new ArrayList(); + private int length; + + /** + * @return the number + */ + public int getNumber() { + return number; + } + + /** + * @param number the number to set + */ + public void setNumber(int number) { + this.number = number; + } + + /** + * @return the next + */ + public Line getNext() { + return next; + } + + /** + * @param next the next to set + */ + public void setNext(Line next) { + this.next = next; + } + + /** + * @return the previous + */ + public Line getPrevious() { + return previous; + } + + /** + * @param previous the previous to set + */ + public void setPrevious(Line previous) { + this.previous = previous; + } + + /** + * @return the commands + */ + public List getCommands() { + return commands; + } + + /** + * @param commands the commands to set + */ + public void setCommands(List commands) { + this.commands = commands; + } + + /** + * @return the length + */ + public int getLength() { + return length; + } + + /** + * @param length the length to set + */ + public void setLength(int length) { + this.length = length; + } + + @Override + public String toString() { + String out = String.valueOf(getNumber()); + boolean isFirst = true; + for (Command c : commands) { + if (!isFirst) out += STATEMENT_BREAK; + out += c.toString(); + isFirst = false; + } + return out; + } + + static Line fromBinary(byte[] binary, int pos) { + Line l = new Line(); + int lineNumber = (binary[pos+2] & 0x0ff) + ((binary[pos+3] & 0x0ff) << 8); + l.setNumber(lineNumber); + pos += 4; + Command c = new Command(); + int size = 5; + while (binary[pos] != 0) { + size++; + if (binary[pos] == STATEMENT_BREAK) { + l.commands.add(c); + c = new Command(); + } else { + Command.ByteOrToken bt = new Command.ByteOrToken(binary[pos]); + c.parts.add(bt); + } + pos++; + } + l.commands.add(c); + l.length = size; + return l; + } +} \ No newline at end of file diff --git a/src/main/java/jace/applesoft/Program.java b/src/main/java/jace/applesoft/Program.java new file mode 100755 index 0000000..0753b5c --- /dev/null +++ b/src/main/java/jace/applesoft/Program.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.applesoft; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Decode an applesoft program into a list of program lines + * Right now this is an example/test program but it successfully tokenized the + * souce of Lemonade Stand. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Program { + List lines = new ArrayList(); + int startingAddress = 0x0801; + + public static void main(String... args) { + byte[] source = null; + try { + File f = new File("/home/brobert/Documents/Personal/a2gameserver/lib/data/games/LEMONADE#fc0801"); + FileInputStream in = new FileInputStream(f); + source = new byte[(int) f.length()]; + in.read(source); + } catch (FileNotFoundException ex) { + Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex); + } + Program test = Program.fromBinary(source); + System.out.println(test.toString()); + } + + static Program fromBinary(byte[] binary) { + return fromBinary(binary, 0x0801); + } + + static Program fromBinary(byte[] binary, int startAddress) { + Program program = new Program(); + int currentAddress = startAddress; + int pos = 0; + while (pos < binary.length) { + int nextAddress = (binary[pos] & 0x0ff) + ((binary[pos+1] & 0x0ff) << 8); + if (nextAddress == 0) break; + int length = nextAddress - currentAddress; + Line l = Line.fromBinary(binary, pos); + if (l == null) break; + program.lines.add(l); + if (l.getLength() != length) { + System.out.println("Line "+l.getNumber()+" parsed as "+l.getLength()+" bytes long, but that leaves "+ + (length - l.getLength())+" bytes hidden behind next line"); + } + pos += length; + currentAddress = nextAddress; + } + return program; + } + + @Override + public String toString() { + String out = ""; + for (Line l : lines) + out += l.toString() + "\n"; + return out; + } +} \ No newline at end of file diff --git a/src/main/java/jace/cheat/Cheats.java b/src/main/java/jace/cheat/Cheats.java new file mode 100644 index 0000000..4e0a970 --- /dev/null +++ b/src/main/java/jace/cheat/Cheats.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.core.Computer; +import jace.core.Device; +import jace.core.RAMListener; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents some combination of hacks that can be enabled or disabled + * through the configuration interface. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class Cheats extends Device { + Set listeners = new HashSet(); + + public void addCheat(RAMListener l) { + listeners.add(l); + Computer.getComputer().getMemory().addListener(l); + } + + @Override + public void detach() { + for (RAMListener l : listeners) { + Computer.getComputer().getMemory().removeListener(l); + } + listeners.clear(); + } + + @Override + public void reconfigure() { + detach(); + attach(); + } + + public String getShortName() { + return "cheat"; + } +} \ No newline at end of file diff --git a/src/main/java/jace/cheat/MemorySpy.form b/src/main/java/jace/cheat/MemorySpy.form new file mode 100644 index 0000000..d76f970 --- /dev/null +++ b/src/main/java/jace/cheat/MemorySpy.form @@ -0,0 +1,295 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/jace/cheat/MemorySpy.java b/src/main/java/jace/cheat/MemorySpy.java new file mode 100644 index 0000000..9a2d893 --- /dev/null +++ b/src/main/java/jace/cheat/MemorySpy.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.core.Utility; +import static jace.core.Utility.gripe; + +/** + * This represents the window of the memory spy module. The memory view itself + * is implemented in MemorySpyGrid. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class MemorySpy extends javax.swing.JFrame { + + static MemorySpy singleton = null; + + /** + * Creates new form MemorySpy + */ + public MemorySpy() { + initComponents(); + setDefaultCloseOperation(HIDE_ON_CLOSE); + singleton = this; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + mapType = new javax.swing.ButtonGroup(); + viewScroll = new javax.swing.JScrollPane(); + memorySpyGrid = new jace.cheat.MemorySpyGrid(); + controlPanel = new javax.swing.JPanel(); + showLabel = new javax.swing.JLabel(); + zoomAmount = new javax.swing.JSlider(); + mapActivityMode = new javax.swing.JRadioButton(); + mapValueMode = new javax.swing.JRadioButton(); + zoomLabel = new javax.swing.JLabel(); + startAddress = new javax.swing.JTextField(); + startLabel = new javax.swing.JLabel(); + endAddress = new javax.swing.JTextField(); + statusLabel = new javax.swing.JLabel(); + endLabel = new javax.swing.JLabel(); + updateAddressButton = new javax.swing.JButton(); + useColorCheckbox = new javax.swing.JCheckBox(); + fastFadeCheckbox = new javax.swing.JCheckBox(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + addComponentListener(new java.awt.event.ComponentAdapter() { + public void componentResized(java.awt.event.ComponentEvent evt) { + formComponentResized(evt); + } + public void componentShown(java.awt.event.ComponentEvent evt) { + formComponentShown(evt); + } + public void componentHidden(java.awt.event.ComponentEvent evt) { + formComponentHidden(evt); + } + }); + addHierarchyBoundsListener(new java.awt.event.HierarchyBoundsListener() { + public void ancestorMoved(java.awt.event.HierarchyEvent evt) { + } + public void ancestorResized(java.awt.event.HierarchyEvent evt) { + formAncestorResized(evt); + } + }); + + viewScroll.setPreferredSize(new java.awt.Dimension(640, 480)); + + memorySpyGrid.setPreferredSize(new java.awt.Dimension(580, 480)); + + javax.swing.GroupLayout memorySpyGridLayout = new javax.swing.GroupLayout(memorySpyGrid); + memorySpyGrid.setLayout(memorySpyGridLayout); + memorySpyGridLayout.setHorizontalGroup( + memorySpyGridLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 631, Short.MAX_VALUE) + ); + memorySpyGridLayout.setVerticalGroup( + memorySpyGridLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 490, Short.MAX_VALUE) + ); + + viewScroll.setViewportView(memorySpyGrid); + + controlPanel.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(0, 0, 0), 1, true)); + + showLabel.setText("Show"); + + zoomAmount.setMaximum(15); + zoomAmount.setMinimum(1); + zoomAmount.setPaintTicks(true); + zoomAmount.setSnapToTicks(true); + zoomAmount.setValue(5); + zoomAmount.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + zoomAmountStateChanged(evt); + } + }); + + mapType.add(mapActivityMode); + mapActivityMode.setSelected(true); + mapActivityMode.setText("Activity"); + mapActivityMode.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + mapActivityModeActionPerformed(evt); + } + }); + + mapType.add(mapValueMode); + mapValueMode.setText("Value"); + mapValueMode.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + mapValueModeActionPerformed(evt); + } + }); + + zoomLabel.setText("Zoom"); + + startAddress.setText("$0000"); + + startLabel.setText("Start"); + + endAddress.setText("$FFFF"); + + statusLabel.setFont(new java.awt.Font("Courier New", 0, 14)); // NOI18N + statusLabel.setText(" "); + statusLabel.setBorder(javax.swing.BorderFactory.createEtchedBorder(javax.swing.border.EtchedBorder.RAISED)); + + endLabel.setText("End"); + + updateAddressButton.setText("Update"); + updateAddressButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + updateAddressButtonActionPerformed(evt); + } + }); + + useColorCheckbox.setText("Color Gradient"); + useColorCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + useColorCheckboxActionPerformed(evt); + } + }); + + fastFadeCheckbox.setText("Fast Fade"); + fastFadeCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fastFadeCheckboxActionPerformed(evt); + } + }); + + javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); + controlPanel.setLayout(controlPanelLayout); + controlPanelLayout.setHorizontalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGap(19, 19, 19) + .addComponent(showLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mapActivityMode) + .addComponent(fastFadeCheckbox)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(controlPanelLayout.createSequentialGroup() + .addComponent(mapValueMode) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(zoomLabel)) + .addComponent(useColorCheckbox)) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGap(144, 144, 144) + .addComponent(endLabel)) + .addGroup(controlPanelLayout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(zoomAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 107, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(startLabel))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(endAddress, javax.swing.GroupLayout.DEFAULT_SIZE, 60, Short.MAX_VALUE) + .addComponent(startAddress)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(updateAddressButton) + .addContainerGap(28, Short.MAX_VALUE)) + .addGroup(controlPanelLayout.createSequentialGroup() + .addContainerGap() + .addComponent(statusLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + ); + controlPanelLayout.setVerticalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(zoomAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(startLabel) + .addComponent(startAddress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(endAddress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(endLabel) + .addComponent(updateAddressButton) + .addComponent(useColorCheckbox) + .addComponent(fastFadeCheckbox))) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(mapActivityMode, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(showLabel)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(zoomLabel) + .addComponent(mapValueMode)))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(statusLabel) + .addContainerGap()) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(viewScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 633, Short.MAX_VALUE) + .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(viewScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 492, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pack(); + }// //GEN-END:initComponents + + private void mapActivityModeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mapActivityModeActionPerformed + memorySpyGrid.setDisplayMode(MemorySpyGrid.Modes.ACTIVITY); + }//GEN-LAST:event_mapActivityModeActionPerformed + + private void mapValueModeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mapValueModeActionPerformed + memorySpyGrid.setDisplayMode(MemorySpyGrid.Modes.VALUE); + }//GEN-LAST:event_mapValueModeActionPerformed + + private void zoomAmountStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_zoomAmountStateChanged + if (!zoomAmount.getValueIsAdjusting()) { + memorySpyGrid.setZoomAmount(zoomAmount.getValue()); + } + }//GEN-LAST:event_zoomAmountStateChanged + + private void updateAddressButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_updateAddressButtonActionPerformed + int startAddr = Utility.parseHexInt(startAddress.getText()); + int endAddr = Utility.parseHexInt(endAddress.getText()); + if (startAddr < 0 || startAddr > 0x0ffff) { + gripe("Start address is out of range!"); + return; + } + if (endAddr < 0 || endAddr > 0x0ffff) { + gripe("End address is out of range!"); + return; + } + if (startAddr > endAddr) { + gripe("Start address must be smaller than end address"); + return; + } + memorySpyGrid.updateRange(startAddr, endAddr); + }//GEN-LAST:event_updateAddressButtonActionPerformed + + @Override + public void validate() { + super.validate(); + memorySpyGrid.adjustSize(); + } + + private void formComponentHidden(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentHidden + memorySpyGrid.stopDisplay(); + }//GEN-LAST:event_formComponentHidden + + private void formComponentShown(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentShown + memorySpyGrid.startDisplay(); + }//GEN-LAST:event_formComponentShown + long lastRefresh = -1L; + long refreshDelay = 1000L; + private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized + }//GEN-LAST:event_formComponentResized + + private void formAncestorResized(java.awt.event.HierarchyEvent evt) {//GEN-FIRST:event_formAncestorResized +// java.awt.EventQueue.invokeLater(new Runnable() { +// @Override +// public void run() { +// lastRefresh = System.currentTimeMillis() + refreshDelay + 10; +// try { +// Thread.sleep(refreshDelay); +// } catch (InterruptedException ex) { +// Logger.getLogger(MemorySpy.class.getName()).log(Level.SEVERE, null, ex); +// } +// if (System.currentTimeMillis() >= lastRefresh) { +// int newWidth = getParent().getWidth(); +// } +// } +// }); +// memorySpyGrid.adjustSize(); + }//GEN-LAST:event_formAncestorResized + + private void useColorCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useColorCheckboxActionPerformed + memorySpyGrid.setColorGradient(useColorCheckbox.isSelected()); + }//GEN-LAST:event_useColorCheckboxActionPerformed + + private void fastFadeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fastFadeCheckboxActionPerformed + memorySpyGrid.setFastFade(fastFadeCheckbox.isSelected()); + }//GEN-LAST:event_fastFadeCheckboxActionPerformed + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + /* Set the Nimbus look and feel */ + // + /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel. + * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html + */ + try { + for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + javax.swing.UIManager.setLookAndFeel(info.getClassName()); + break; + } + } + } catch (ClassNotFoundException ex) { + java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (InstantiationException ex) { + java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (javax.swing.UnsupportedLookAndFeelException ex) { + java.util.logging.Logger.getLogger(MemorySpy.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } + // + + /* Create and display the form */ + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new MemorySpy().setVisible(true); + } + }); + } + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel controlPanel; + private javax.swing.JTextField endAddress; + private javax.swing.JLabel endLabel; + private javax.swing.JCheckBox fastFadeCheckbox; + private javax.swing.JRadioButton mapActivityMode; + private javax.swing.ButtonGroup mapType; + private javax.swing.JRadioButton mapValueMode; + private jace.cheat.MemorySpyGrid memorySpyGrid; + private javax.swing.JLabel showLabel; + private javax.swing.JTextField startAddress; + private javax.swing.JLabel startLabel; + public javax.swing.JLabel statusLabel; + private javax.swing.JButton updateAddressButton; + private javax.swing.JCheckBox useColorCheckbox; + private javax.swing.JScrollPane viewScroll; + private javax.swing.JSlider zoomAmount; + private javax.swing.JLabel zoomLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/jace/cheat/MemorySpyGrid.form b/src/main/java/jace/cheat/MemorySpyGrid.form new file mode 100644 index 0000000..be5d6ab --- /dev/null +++ b/src/main/java/jace/cheat/MemorySpyGrid.form @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/jace/cheat/MemorySpyGrid.java b/src/main/java/jace/cheat/MemorySpyGrid.java new file mode 100644 index 0000000..74ea72f --- /dev/null +++ b/src/main/java/jace/cheat/MemorySpyGrid.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.core.Computer; +import jace.core.RAM; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.RAMListener; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JLabel; +import javax.swing.JViewport; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * This is the memory view of the memory spy module. The window is defined by + * the MemorySpy class. This class registers memory listeners needed to provide + * the relevant memory views. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class MemorySpyGrid extends javax.swing.JPanel { + + /** + * Creates new form MemorySpyGrid + */ + public MemorySpyGrid() { + initComponents(); + gradient = bwGradient; + } + // This is used primarily during scroll events to repaint the whole area + ChangeListener viewportChangeListener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + repaint(); + } + }; + + @Override + public void validate() { + super.validate(); + if (Computer.getComputer() != null) { + adjustSize(); + } + if (getParent() != null) { + JViewport viewport = (JViewport) getParent(); + viewport.removeChangeListener(viewportChangeListener); + viewport.addChangeListener(viewportChangeListener); + } + } + + @Override + public void paint(Graphics g) { + super.paint(g); + if (mode == Modes.ACTIVITY) { + for (int a = startAddress; a <= endAddress; a++) { + if (!activeRam.contains(a)) { + drawActivity(a, 0, 0, 0, (Graphics2D) g); + } else { + drawActivity(a, + getInt(writeActivity.get(a)), + getInt(readActivity.get(a)), + getInt(pcActivity.get(a)), + (Graphics2D) g); + } + } + } else { + RAM ram = Computer.getComputer().getMemory(); + for (int a = startAddress; a <= endAddress; a++) { + drawValue(a, ram.readRaw(a) & 0x0ff, (Graphics2D) g); + } + } + } + Color[] gradient; + static Color[] colorGradient, bwGradient; + + static { + colorGradient = new Color[256]; + bwGradient = new Color[256]; + for (int i = 0; i < 256; i++) { + float hue = ((float) i) / 360.0f; + colorGradient[i] = Color.getHSBColor(0.67f - hue, 1, 1); + bwGradient[i] = new Color(i, i, i); + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + setBackground(new java.awt.Color(1, 1, 1)); + setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR)); + addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + formMouseClicked(evt); + } + public void mouseExited(java.awt.event.MouseEvent evt) { + formMouseExited(evt); + } + public void mouseEntered(java.awt.event.MouseEvent evt) { + formMouseEntered(evt); + } + }); + addMouseMotionListener(new java.awt.event.MouseMotionAdapter() { + public void mouseMoved(java.awt.event.MouseEvent evt) { + formMouseMoved(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 300, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + boolean mousePresent = false; + int mouseX; + int mouseY; + private void formMouseMoved(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseMoved + mouseX = evt.getX(); + mouseY = evt.getY(); + }//GEN-LAST:event_formMouseMoved + + private void formMouseEntered(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseEntered + mousePresent = true; + }//GEN-LAST:event_formMouseEntered + + private void formMouseExited(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseExited + mousePresent = false; + }//GEN-LAST:event_formMouseExited + + private void formMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_formMouseClicked + int addr = convertXYtoAddr(mouseX, mouseY); + if (addr >= 0) { + MetaCheats.singleton.addWatches(addr, addr); + } + }//GEN-LAST:event_formMouseClicked + + private int convertXYtoAddr(int x, int y) { + int offset = x / zoomAmount; + if (offset < 0 || offset > columns) { + return -1; + } + int row = y / zoomAmount; + int addr = startAddress + (row * columns) + offset; + if (addr > endAddress) { + return -1; + } + return addr; + } + + private String spaceFill(String s, int size) { + while (s.length() < size) { + s = s + " "; + } + return s; + } + + private void updateDetails() { + final JLabel status = MemorySpy.singleton.statusLabel; + if (mousePresent) { + final int addr = convertXYtoAddr(mouseX, mouseY); + if (addr >= 0) { + final int value = Computer.getComputer().getMemory().readRaw(addr) & 0x0ff; + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + Integer lastChange = setBy.get(addr); + Integer lastRead = readBy.get(addr); + char c1 = (char) Math.max(32, value & 0x07f); + char c2 = (char) (64 + (value % 32)); + char c3 = (char) (32 + (value % 96)); + + status.setText("$" + + spaceFill(Integer.toHexString(addr), 4) + ": " + + spaceFill(String.valueOf(value), 3) + " ($" + + spaceFill(Integer.toHexString(value), 2) + ") " + c1 + " " + c2 + " " + c3 + + (lastChange != null + ? " Written by $" + + spaceFill(Integer.toHexString(lastChange), 4) + : "") + + (lastRead != null + ? " Read by $" + + spaceFill(Integer.toHexString(lastRead), 4) + : "")); + } + }); + } + } else { + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + status.setText("Nothing selected"); + } + }); + } + } + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables + Modes mode = Modes.ACTIVITY; + + void setDisplayMode(Modes newMode) { + stopDisplay(); + mode = newMode; + repaint(); + startDisplay(); + } + int zoomAmount = 5; + + void setZoomAmount(int value) { + zoomAmount = value; + recalibrate(); + } + int startAddress = 0; + int endAddress = 0x0ffff; + + void updateRange(int startAddr, int endAddr) { + startAddress = startAddr; + endAddress = endAddr; + activeRam.clear(); + valueActivity.clear(); + readActivity.clear(); + writeActivity.clear(); + pcActivity.clear(); + recalibrate(); + } + int columns = 1; + int lastKnownWidth = 0; + int lastKnownHeight = 0; + + public void adjustSize() { + int newWidth = getParent().getWidth(); + if (newWidth == lastKnownWidth && getParent().getHeight() == lastKnownHeight) { + return; + } + lastKnownWidth = newWidth; + lastKnownHeight = getParent().getHeight(); + recalibrate(); + } + + public void recalibrate() { + java.awt.EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + stopDisplay(); + int size = endAddress - startAddress + 1; + int width = getParent().getWidth(); + columns = ((width / zoomAmount) / 16) * 16; + int height = ((size / columns) + 1) * zoomAmount; + setSize(width, height); + setPreferredSize(new Dimension(width, height)); + getParent().validate(); + repaint(); + startDisplay(); + } + }); + } + + protected void drawValue(int addr, int value, Graphics2D g) { + int offset = addr - startAddress; + int x = zoomAmount * (offset % columns); + int y = zoomAmount * (offset / columns); + value = value & 0x0ff; + g.setColor(gradient[value]); + g.fillRect(x, y, zoomAmount, zoomAmount); + } + + protected void drawActivity(int addr, int read, int write, int pc, Graphics2D g) { + int offset = addr - startAddress; + int x = zoomAmount * (offset % columns); + int y = zoomAmount * (offset / columns); + g.setColor(new Color(read & 0x0ff, write & 0x0ff, pc & 0x0ff)); + g.fillRect(x, y, zoomAmount, zoomAmount); + } + + private int getInt(Integer i) { + if (i != null) { + return i; + } + return 0; + } + boolean isDisplayActive = false; + Thread redrawThread = null; + // 30 fps or so + static int UPDATE_INTERVAL = 10; + static int ACTIVITY_INCREMENT = 150; + static int ACTIVITY_DECREMENT = 5; + static int ACTIVITY_MAX = 255; + RAMListener memoryListener = null; + Map readActivity = new ConcurrentHashMap(); + Map writeActivity = new ConcurrentHashMap(); + Map pcActivity = new ConcurrentHashMap(); + Set activeRam = new ConcurrentSkipListSet(); + Map valueActivity = new ConcurrentHashMap(); + Map setBy = new ConcurrentHashMap(); + Map readBy = new ConcurrentHashMap(); + + void startDisplay() { + if (memoryListener != null) { + Computer.getComputer().getMemory().removeListener(memoryListener); + memoryListener = null; + } + isDisplayActive = true; + memoryListener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(startAddress); + setScopeEnd(endAddress); + } + + @Override + protected void doEvent(RAMEvent e) { + int addr = e.getAddress(); + if (addr < startAddress || addr > endAddress) { + return; + } + int pc = Computer.getComputer().getCpu().programCounter; + if (e.getType() == TYPE.EXECUTE || e.getType() == TYPE.READ_OPERAND) { + if (pcActivity.containsKey(addr)) { + pcActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(pcActivity.get(addr)) + ACTIVITY_INCREMENT)); + } else { + pcActivity.put(addr, ACTIVITY_INCREMENT); + } + } else if (e.getType().isRead()) { + if (readActivity.containsKey(addr)) { + readActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(readActivity.get(addr)) + ACTIVITY_INCREMENT)); + } else { + readActivity.put(addr, ACTIVITY_INCREMENT); + } + readBy.put(addr, pc); + } else { + if (writeActivity.containsKey(addr)) { + writeActivity.put(addr, Math.min(ACTIVITY_MAX, getInt(writeActivity.get(addr)) + ACTIVITY_INCREMENT)); + } else { + writeActivity.put(addr, ACTIVITY_INCREMENT); + } + valueActivity.put(addr, e.getNewValue()); + setBy.put(addr, pc); + } + activeRam.add(addr); + } + }; + Computer.getComputer().getMemory().addListener(memoryListener); + redrawThread = new Thread(new Runnable() { + @Override + public void run() { + if (mode == Modes.ACTIVITY) { + // Activity heatmap mode + while (isDisplayActive) { + updateDetails(); + Graphics2D g = (Graphics2D) getGraphics(); + for (Iterator i = activeRam.iterator(); i.hasNext();) { + boolean remove = true; + int addr = i.next(); + int read = getInt(readActivity.get(addr)); + if (read <= 0) { + read = 0; + } else { + remove = false; + readActivity.put(addr, read - ACTIVITY_DECREMENT); + } + int write = getInt(writeActivity.get(addr)); + if (write <= 0) { + write = 0; + } else { + remove = false; + writeActivity.put(addr, write - ACTIVITY_DECREMENT); + } + int pc = getInt(pcActivity.get(addr)); + if (pc <= 0) { + pc = 0; + } else { + remove = false; + pcActivity.put(addr, pc - ACTIVITY_DECREMENT); + } + + if (remove) { + i.remove(); + pcActivity.remove(addr); + writeActivity.remove(addr); + readActivity.remove(addr); + } + + drawActivity(addr, write, read, pc, g); + } + g.dispose(); + try { + Thread.sleep(UPDATE_INTERVAL); + } catch (InterruptedException ex) { + Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex); + } + } + } else { + // Redraw value, no activity counts needed + while (isDisplayActive) { + updateDetails(); + Graphics2D g = (Graphics2D) getGraphics(); + for (Iterator i = valueActivity.keySet().iterator(); i.hasNext();) { + int addr = i.next(); + int value = getInt(valueActivity.get(addr)); + i.remove(); + drawValue(addr, value, g); + } + g.dispose(); + try { + Thread.sleep(UPDATE_INTERVAL); + } catch (InterruptedException ex) { + Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } + }); + redrawThread.setName("Memory spy redraw"); + redrawThread.start(); + } + + void stopDisplay() { + isDisplayActive = false; + if (memoryListener != null) { + Computer.getComputer().getMemory().removeListener(memoryListener); + memoryListener = null; + } + if (redrawThread != null && redrawThread.isAlive()) { + try { + redrawThread.join(); + } catch (InterruptedException ex) { + Logger.getLogger(MemorySpyGrid.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + void setColorGradient(boolean useColor) { + if (useColor) { + gradient = colorGradient; + } else { + gradient = bwGradient; + } + repaint(); + } + + void setFastFade(boolean fast) { + if (fast) { + ACTIVITY_DECREMENT = 20; + } else { + ACTIVITY_DECREMENT = 5; + } + } + + public static enum Modes { + + ACTIVITY, VALUE + } +} diff --git a/src/main/java/jace/cheat/MetaCheatForm.form b/src/main/java/jace/cheat/MetaCheatForm.form new file mode 100644 index 0000000..31e3f16 --- /dev/null +++ b/src/main/java/jace/cheat/MetaCheatForm.form @@ -0,0 +1,516 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + <Editor/> + <Renderer/> + </Column> + <Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="false"> + <Title/> + <Editor/> + <Renderer/> + </Column> + <Column maxWidth="-1" minWidth="-1" prefWidth="-1" resizable="false"> + <Title/> + <Editor/> + <Renderer/> + </Column> + </TableColumnModel> + </Property> + <Property name="tableHeader" type="javax.swing.table.JTableHeader" editor="org.netbeans.modules.form.editors2.JTableHeaderEditor"> + <TableHeader reorderingAllowed="true" resizingAllowed="false"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JButton" name="addSelected"> + <Properties> + <Property name="text" type="java.lang.String" value="Add Selected"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addSelectedActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JRadioButton" name="searchForByte"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="valueTypes"/> + </Property> + <Property name="selected" type="boolean" value="true"/> + <Property name="text" type="java.lang.String" value="Byte"/> + </Properties> + </Component> + <Component class="javax.swing.JRadioButton" name="searchForWord"> + <Properties> + <Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor"> + <ComponentRef name="valueTypes"/> + </Property> + <Property name="text" type="java.lang.String" value="Word"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="addWatchLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Add Watches:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="addStartLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Start"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="addStartNumber"> + </Component> + <Component class="javax.swing.JTextField" name="addEndNumber"> + </Component> + <Component class="javax.swing.JLabel" name="addEndLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="End"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="addWatchesButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Add"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addWatchesButtonActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/src/main/java/jace/cheat/MetaCheatForm.java b/src/main/java/jace/cheat/MetaCheatForm.java new file mode 100644 index 0000000..d323478 --- /dev/null +++ b/src/main/java/jace/cheat/MetaCheatForm.java @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.core.Utility; +import static jace.core.Utility.gripe; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This is the metacheat user interface. The actual logic of metacheat is in the + * Metacheats class. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class MetaCheatForm extends javax.swing.JFrame { + + /** + * Creates new form MetaCheatForm + */ + public MetaCheatForm() { + initComponents(); + this.setDefaultCloseOperation(HIDE_ON_CLOSE); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + searchTypes = new javax.swing.ButtonGroup(); + valueTypes = new javax.swing.ButtonGroup(); + tabs = new javax.swing.JTabbedPane(); + cheatPanel = new javax.swing.JPanel(); + activeCheatsLabel = new javax.swing.JLabel(); + activeCheatsScroll = new javax.swing.JScrollPane(); + activeCheatsTable = new javax.swing.JTable(); + removeSelectedButton = new javax.swing.JButton(); + addNewCheatLabel = new javax.swing.JLabel(); + jSeparator1 = new javax.swing.JSeparator(); + addValueLabel = new javax.swing.JLabel(); + addAddressLabel = new javax.swing.JLabel(); + addByteValueButton = new javax.swing.JButton(); + addWordValueButton = new javax.swing.JButton(); + addAddressField = new javax.swing.JTextField(); + addValueField = new javax.swing.JTextField(); + disableCheatButton = new javax.swing.JButton(); + enableCheatButton = new javax.swing.JButton(); + searchPanel = new javax.swing.JPanel(); + searchForValue = new javax.swing.JRadioButton(); + searchForLabel = new javax.swing.JLabel(); + searchForChange = new javax.swing.JRadioButton(); + searchNumber = new javax.swing.JTextField(); + valueLabel = new javax.swing.JLabel(); + resetButton = new javax.swing.JButton(); + searchButton = new javax.swing.JButton(); + resultsStatusLabel = new javax.swing.JLabel(); + resultsScroll = new javax.swing.JScrollPane(); + resultsTable = new javax.swing.JTable(); + addSelected = new javax.swing.JButton(); + searchForByte = new javax.swing.JRadioButton(); + searchForWord = new javax.swing.JRadioButton(); + addWatchLabel = new javax.swing.JLabel(); + addStartLabel = new javax.swing.JLabel(); + addStartNumber = new javax.swing.JTextField(); + addEndNumber = new javax.swing.JTextField(); + addEndLabel = new javax.swing.JLabel(); + addWatchesButton = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + + activeCheatsLabel.setText("Active Cheats"); + + activeCheatsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + "Address", "Value" + } + ) { + Class[] types = new Class [] { + java.lang.String.class, java.lang.String.class + }; + boolean[] canEdit = new boolean [] { + false, false + }; + + public Class getColumnClass(int columnIndex) { + return types [columnIndex]; + } + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return canEdit [columnIndex]; + } + }); + activeCheatsScroll.setViewportView(activeCheatsTable); + + removeSelectedButton.setText("Remove Selected"); + removeSelectedButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + removeSelectedButtonActionPerformed(evt); + } + }); + + addNewCheatLabel.setText("Add a new cheat:"); + + addValueLabel.setText("Value"); + + addAddressLabel.setText("Address"); + + addByteValueButton.setText("Add Byte Value"); + addByteValueButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addByteValueButtonActionPerformed(evt); + } + }); + + addWordValueButton.setText("Add Word Value"); + addWordValueButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addWordValueButtonActionPerformed(evt); + } + }); + + disableCheatButton.setText("Disable Selected"); + disableCheatButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + disableCheatButtonActionPerformed(evt); + } + }); + + enableCheatButton.setText("Enable Selected"); + enableCheatButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableCheatButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout cheatPanelLayout = new javax.swing.GroupLayout(cheatPanel); + cheatPanel.setLayout(cheatPanelLayout); + cheatPanelLayout.setHorizontalGroup( + cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(activeCheatsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 569, Short.MAX_VALUE) + .addComponent(jSeparator1) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(activeCheatsLabel) + .addComponent(addNewCheatLabel) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addComponent(addByteValueButton) + .addGap(18, 18, 18) + .addComponent(addWordValueButton)) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addComponent(addValueLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 56, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(addValueField, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addComponent(addAddressLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(addAddressField, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addComponent(disableCheatButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(enableCheatButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(removeSelectedButton))) + .addContainerGap()) + ); + cheatPanelLayout.setVerticalGroup( + cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(cheatPanelLayout.createSequentialGroup() + .addContainerGap() + .addComponent(activeCheatsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(activeCheatsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 118, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(removeSelectedButton) + .addComponent(disableCheatButton) + .addComponent(enableCheatButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(addNewCheatLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addAddressLabel) + .addComponent(addAddressField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addValueLabel) + .addComponent(addValueField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(cheatPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addByteValueButton) + .addComponent(addWordValueButton)) + .addContainerGap()) + ); + + tabs.addTab("Cheats", cheatPanel); + + searchTypes.add(searchForValue); + searchForValue.setSelected(true); + searchForValue.setText("Value"); + searchForValue.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchForValueActionPerformed(evt); + } + }); + + searchForLabel.setText("Search for:"); + + searchTypes.add(searchForChange); + searchForChange.setText("Change"); + searchForChange.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchForChangeActionPerformed(evt); + } + }); + + searchNumber.setText("0"); + searchNumber.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchNumberActionPerformed(evt); + } + }); + + valueLabel.setText("Value:"); + + resetButton.setText("Reset"); + resetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + resetButtonActionPerformed(evt); + } + }); + + searchButton.setText("Search"); + searchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchButtonActionPerformed(evt); + } + }); + + resultsStatusLabel.setText("No results"); + resultsStatusLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP); + resultsStatusLabel.setBorder(new javax.swing.border.SoftBevelBorder(javax.swing.border.BevelBorder.RAISED)); + + resultsTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + "Address", "Last Search", "Current Value" + } + ) { + Class[] types = new Class [] { + java.lang.String.class, java.lang.String.class, java.lang.String.class + }; + boolean[] canEdit = new boolean [] { + false, false, false + }; + + public Class getColumnClass(int columnIndex) { + return types [columnIndex]; + } + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return canEdit [columnIndex]; + } + }); + resultsTable.getTableHeader().setResizingAllowed(false); + resultsScroll.setViewportView(resultsTable); + resultsTable.getColumnModel().getColumn(0).setResizable(false); + resultsTable.getColumnModel().getColumn(1).setResizable(false); + resultsTable.getColumnModel().getColumn(2).setResizable(false); + + addSelected.setText("Add Selected"); + addSelected.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addSelectedActionPerformed(evt); + } + }); + + valueTypes.add(searchForByte); + searchForByte.setSelected(true); + searchForByte.setText("Byte"); + + valueTypes.add(searchForWord); + searchForWord.setText("Word"); + + addWatchLabel.setText("Add Watches:"); + + addStartLabel.setText("Start"); + + addEndLabel.setText("End"); + + addWatchesButton.setText("Add"); + addWatchesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addWatchesButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout searchPanelLayout = new javax.swing.GroupLayout(searchPanel); + searchPanel.setLayout(searchPanelLayout); + searchPanelLayout.setHorizontalGroup( + searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(searchPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(resultsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 569, Short.MAX_VALUE) + .addGroup(searchPanelLayout.createSequentialGroup() + .addComponent(addSelected) + .addGap(18, 18, 18) + .addComponent(resultsStatusLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, searchPanelLayout.createSequentialGroup() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(searchPanelLayout.createSequentialGroup() + .addComponent(searchForLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(searchForChange) + .addComponent(searchForValue)) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGap(3, 3, 3) + .addComponent(searchForByte)) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGap(6, 6, 6) + .addComponent(searchForWord)))) + .addGroup(searchPanelLayout.createSequentialGroup() + .addComponent(valueLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(searchNumber, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(resetButton, javax.swing.GroupLayout.PREFERRED_SIZE, 76, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(searchButton))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 158, Short.MAX_VALUE) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, searchPanelLayout.createSequentialGroup() + .addComponent(addWatchLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(addWatchesButton)) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(addStartLabel) + .addComponent(addEndLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(addEndNumber) + .addComponent(addStartNumber)))))) + .addContainerGap()) + ); + searchPanelLayout.setVerticalGroup( + searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(searchPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchForLabel) + .addComponent(searchForValue) + .addComponent(searchForByte)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchForChange) + .addComponent(searchForWord) + .addComponent(addStartLabel) + .addComponent(addStartNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(valueLabel) + .addComponent(resetButton) + .addComponent(searchButton) + .addComponent(addEndLabel) + .addComponent(addEndNumber, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(searchPanelLayout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addWatchesButton, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(addWatchLabel)))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(resultsScroll, javax.swing.GroupLayout.DEFAULT_SIZE, 194, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(searchPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addSelected) + .addComponent(resultsStatusLabel)) + .addContainerGap()) + ); + + tabs.addTab("Search", searchPanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(tabs) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(tabs) + .addContainerGap()) + ); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + private void searchForValueActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchForValueActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_searchForValueActionPerformed + + private void searchForChangeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchForChangeActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_searchForChangeActionPerformed + + private void searchNumberActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchNumberActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_searchNumberActionPerformed + + private void resetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_resetButtonActionPerformed + MetaCheats.singleton.resetSearch(); + resultsStatusLabel.setText("Results cleared."); + }//GEN-LAST:event_resetButtonActionPerformed + + private void addSelectedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addSelectedActionPerformed + List<Integer> addr = new ArrayList<Integer>(MetaCheats.singleton.results.keySet()); + int val = Utility.parseHexInt(searchNumber.getText()); + for (int i : resultsTable.getSelectedRows()) { + if (searchForByte.isSelected()) { + MetaCheats.singleton.addByteCheat(addr.get(i), val); + } else { + MetaCheats.singleton.addWordCheat(addr.get(i), val); + } + } + }//GEN-LAST:event_addSelectedActionPerformed + + private void removeSelectedButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeSelectedButtonActionPerformed + Set<Integer> remove = new HashSet<Integer>(); + for (int i : activeCheatsTable.getSelectedRows()) { + String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0)); + remove.add(Utility.parseHexInt(s)); + } + for (int i : remove) { + MetaCheats.singleton.removeCheat(i); + } + }//GEN-LAST:event_removeSelectedButtonActionPerformed + + private void addByteValueButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addByteValueButtonActionPerformed + try { + int addr = Utility.parseHexInt(addAddressField.getText()); + int val = Utility.parseHexInt(addValueField.getText()); + MetaCheats.singleton.addByteCheat(addr, val); + } catch (NullPointerException e) { + gripe("Please enure that the address and value fields are correctly filled in."); + } + }//GEN-LAST:event_addByteValueButtonActionPerformed + + private void addWordValueButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addWordValueButtonActionPerformed + try { + int addr = Utility.parseHexInt(addAddressField.getText()); + int val = Utility.parseHexInt(addValueField.getText()); + MetaCheats.singleton.addWordCheat(addr, val); + } catch (NullPointerException e) { + gripe("Please enure that the address and value fields are correctly filled in."); + } + }//GEN-LAST:event_addWordValueButtonActionPerformed + + private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed + if (searchForChange.isSelected() == searchForValue.isSelected()) { + gripe("Please select if you want to search for a fixed value or a delta change"); + return; + } + if (searchForByte.isSelected() == searchForWord.isSelected()) { + gripe("Please select if you want to search for a byte or a word value"); + return; + } + try { + int val = Utility.parseHexInt(searchNumber.getText()); + MetaCheats.singleton.performSearch(searchForChange.isSelected(), searchForByte.isSelected(), val); + } catch (NullPointerException e) { + gripe("Please enter a value"); + } + }//GEN-LAST:event_searchButtonActionPerformed + + private void disableCheatButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_disableCheatButtonActionPerformed + for (int i : activeCheatsTable.getSelectedRows()) { + String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0)); + MetaCheats.singleton.disableCheat(Utility.parseHexInt(s)); + } + + }//GEN-LAST:event_disableCheatButtonActionPerformed + + private void enableCheatButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableCheatButtonActionPerformed + for (int i : activeCheatsTable.getSelectedRows()) { + String s = String.valueOf(activeCheatsTable.getModel().getValueAt(i, 0)); + MetaCheats.singleton.enableCheat(Utility.parseHexInt(s)); + } + }//GEN-LAST:event_enableCheatButtonActionPerformed + + private void addWatchesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addWatchesButtonActionPerformed + try { + int addrStart = Utility.parseHexInt(addStartNumber.getText()); + int addrEnd = Utility.parseHexInt(addEndNumber.getText()); + if (addrStart > addrEnd) { + gripe("Start address must be smaller than end address!"); + return; + } + MetaCheats.singleton.addWatches(addrStart, addrEnd); + resultsStatusLabel.setText("Added range to watch list."); + } catch (NullPointerException e) { + gripe("Please enure that the start and end fields are correctly filled in."); + } + }//GEN-LAST:event_addWatchesButtonActionPerformed + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + /* Set the Nimbus look and feel */ + //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) "> + /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel. + * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html + */ + try { + for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + javax.swing.UIManager.setLookAndFeel(info.getClassName()); + break; + } + } + } catch (ClassNotFoundException ex) { + java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (InstantiationException ex) { + java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } catch (javax.swing.UnsupportedLookAndFeelException ex) { + java.util.logging.Logger.getLogger(MetaCheatForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); + } + //</editor-fold> + + /* Create and display the form */ + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new MetaCheatForm().setVisible(true); + } + }); + } + // Variables declaration - do not modify//GEN-BEGIN:variables + public javax.swing.JLabel activeCheatsLabel; + public javax.swing.JScrollPane activeCheatsScroll; + public javax.swing.JTable activeCheatsTable; + public javax.swing.JTextField addAddressField; + public javax.swing.JLabel addAddressLabel; + public javax.swing.JButton addByteValueButton; + public javax.swing.JLabel addEndLabel; + public javax.swing.JTextField addEndNumber; + public javax.swing.JLabel addNewCheatLabel; + public javax.swing.JButton addSelected; + public javax.swing.JLabel addStartLabel; + public javax.swing.JTextField addStartNumber; + public javax.swing.JTextField addValueField; + public javax.swing.JLabel addValueLabel; + public javax.swing.JLabel addWatchLabel; + public javax.swing.JButton addWatchesButton; + public javax.swing.JButton addWordValueButton; + public javax.swing.JPanel cheatPanel; + public javax.swing.JButton disableCheatButton; + public javax.swing.JButton enableCheatButton; + public javax.swing.JSeparator jSeparator1; + public javax.swing.JButton removeSelectedButton; + public javax.swing.JButton resetButton; + public javax.swing.JScrollPane resultsScroll; + public javax.swing.JLabel resultsStatusLabel; + public javax.swing.JTable resultsTable; + public javax.swing.JButton searchButton; + public javax.swing.JRadioButton searchForByte; + public javax.swing.JRadioButton searchForChange; + public javax.swing.JLabel searchForLabel; + public javax.swing.JRadioButton searchForValue; + public javax.swing.JRadioButton searchForWord; + public javax.swing.JTextField searchNumber; + public javax.swing.JPanel searchPanel; + public javax.swing.ButtonGroup searchTypes; + public javax.swing.JTabbedPane tabs; + public javax.swing.JLabel valueLabel; + public javax.swing.ButtonGroup valueTypes; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/jace/cheat/MetaCheats.java b/src/main/java/jace/cheat/MetaCheats.java new file mode 100644 index 0000000..592a24f --- /dev/null +++ b/src/main/java/jace/cheat/MetaCheats.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.apple2e.RAM128k; +import jace.core.Computer; +import jace.core.KeyHandler; +import jace.core.Keyboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.table.DefaultTableModel; + +/** + * Basic mame-style cheats. The user interface is in MetaCheatForm. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class MetaCheats extends Cheats { + static MetaCheats singleton = null; + // This is used to help the handler exit faster when there is nothing to do + boolean noCheats = true; + + public MetaCheats() { + super(); + singleton = this; + } + + + Map<Integer, Integer> holdBytes = new TreeMap<Integer,Integer>(); + Map<Integer, Integer> holdWords = new TreeMap<Integer,Integer>(); + Set<Integer> disabled = new HashSet<Integer>(); + Map<Integer, Integer> results = new TreeMap<Integer,Integer>(); + @Override + protected String getDeviceName() { + return "Meta-cheat engine"; + } + + @Override + public void tick() { + // Do nothing + } + + public static int MAX_RESULTS_SHOWN = 256; + + public MetaCheatForm form = null; + public boolean isDrawing = false; + public void redrawResults() { + if (isDrawing) { + return; + } + isDrawing = true; + + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(MetaCheats.class.getName()).log(Level.SEVERE, null, ex); + } + + RAM128k ram = (RAM128k) Computer.getComputer().getMemory(); + DefaultTableModel model = (DefaultTableModel) form.resultsTable.getModel(); + if (results.size() > MAX_RESULTS_SHOWN) { + model.setRowCount(0); + isDrawing = false; + return; + } + boolean useWord = form.searchForWord.isSelected(); + if (model.getRowCount() != results.size()) { + model.setRowCount(0); + List<Integer> iter = new ArrayList<Integer>(results.keySet()); + for (Integer i : iter) { + int val = results.get(i); + if (useWord) { + int current = ram.readWordRaw(i) & 0x0ffff; + model.addRow(new Object[]{hex(i, 4), val + " ("+hex(val,4)+")", current + " ("+hex(current,4)+")"}); + } else { + int current = ram.readRaw(i) & 0x0ff; + model.addRow(new Object[]{hex(i, 4), val + " ("+hex(val,2)+")", current + " ("+hex(current,2)+")"}); + } + } + } else { + List<Integer> iter = new ArrayList<Integer>(results.keySet()); + for (int i=0; i < iter.size(); i++) { + int val = results.get(iter.get(i)); + if (useWord) { + int current = ram.readWordRaw(iter.get(i)) & 0x0ffff; + model.setValueAt(val + " ("+hex(val,4)+")", i, 1); + model.setValueAt(current + " ("+hex(current,4)+")", i, 2); + } else { + int current = ram.readRaw(iter.get(i)) & 0x0ff; + model.setValueAt(val + " ("+hex(val,2)+")", i, 1); + model.setValueAt(current + " ("+hex(current,2)+")", i, 2); + } + } + } + isDrawing = false; + } + + public static String hex(int val, int size) { + String out = Integer.toHexString(val); + while (out.length() < size) { + out = "0"+out; + } + return "$"+out; + } + + public void redrawCheats() { + noCheats = holdBytes.isEmpty() && holdWords.isEmpty() && disabled.isEmpty(); + DefaultTableModel model = (DefaultTableModel) form.activeCheatsTable.getModel(); + model.setRowCount(0); + for (Integer i : holdBytes.keySet()) { + String loc = hex(i, 4); + if (disabled.contains(i)) loc += " (off)"; + int val = holdBytes.get(i); + model.addRow(new Object[]{loc, val + " ("+hex(val,2)+")"}); + } + for (Integer i : holdWords.keySet()) { + String loc = hex(i, 4); + if (disabled.contains(i)) loc += " (off)"; + int val = holdWords.get(i); + model.addRow(new Object[]{loc, val + " ("+hex(val,4)+")"}); + } + } + public void showCheatForm() { + if (form == null) { + form = new MetaCheatForm(); + } + form.setVisible(true); + } + + MemorySpy spy = null; + public void showMemorySpy() { + if (spy == null) { + spy = new MemorySpy(); + } + spy.setVisible(true); + } + + + @Override + public void attach() { + this.addCheat(new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.ANY, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + } + + @Override + protected void doEvent(RAMEvent e) { + if (noCheats) return; + if (disabled.contains(e.getAddress())) return; + if (holdBytes.containsKey(e.getAddress())) { + e.setNewValue(holdBytes.get(e.getAddress())); + } else if (holdWords.containsKey(e.getAddress())) { + e.setNewValue(holdWords.get(e.getAddress()) & 0x0ff); + } else if (holdWords.containsKey(e.getAddress()-1)) { + if (disabled.contains(e.getAddress()-1)) return; + e.setNewValue((holdWords.get(e.getAddress()-1)>>8) & 0x0ff); + } + } + }); + this.addCheat(new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ANY, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + } + + @Override + protected void doEvent(RAMEvent e) { + if (results.isEmpty()) return; + if (results.size() > MAX_RESULTS_SHOWN || isDrawing) return; + if (results.containsKey(e.getAddress()) || results.containsKey(e.getAddress()-1)) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + redrawResults(); + } + }); + t.setName("Metacheat results updater"); + t.start(); + } + } + }); + + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_END) { + @Override + public boolean handleKeyUp(KeyEvent e) { + showCheatForm(); + return false; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + return false; + } + }, this); + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_HOME) { + @Override + public boolean handleKeyUp(KeyEvent e) { + showMemorySpy(); + return false; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + return false; + } + }, this); + } + + @Override + public void detach() { + super.detach(); + Keyboard.unregisterAllHandlers(this); + } + + public void addByteCheat(int addr, int val) { + holdBytes.put(addr, val); + redrawCheats(); + } + + public void addWordCheat(int addr, int val) { + holdWords.put(addr, val); + redrawCheats(); + } + + void removeCheat(int i) { + holdBytes.remove(i); + holdWords.remove(i); + disabled.remove(i); + redrawCheats(); + } + + void resetSearch() { + results.clear(); + redrawResults(); + } + + void performSearch(boolean useDeltaSearch, boolean searchForByteValues, int val) { + RAM128k ram = (RAM128k) Computer.getComputer().getMemory(); + if (results.isEmpty()) { + int max = 0x010000; + if (!searchForByteValues) max--; + for (int i=0; i < max; i++) { + if (i >= 0x0c000 && i <= 0x0cfff) continue; + int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff; + if (useDeltaSearch) { + results.put(i, v); + } else if (v == val) { + results.put(i, v); + } + } + } else { + Set<Integer> remove = new HashSet<Integer>(); + for (Integer i : results.keySet()) { + int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff; + if (useDeltaSearch) { + if (v - results.get(i) != val) { + remove.add(i); + } else { + results.put(i,v); + } + } else { + if (v != val) { + remove.add(i); + } else { + results.put(i,v); + } + } + } + for (Integer i : remove) { + results.remove(i); + } + } + form.resultsStatusLabel.setText("Search found "+results.size()+" result(s)."); + redrawResults(); + } + + void enableCheat(int addr) { + disabled.remove(addr); + redrawCheats(); + } + + void disableCheat(int addr) { + disabled.add(addr); + redrawCheats(); + } + + void addWatches(int addrStart, int addrEnd) { + RAM128k ram = (RAM128k) Computer.getComputer().getMemory(); + if (form == null) return; + boolean searchForByteValues = form.searchForByte.isSelected(); + for (int i = addrStart; i <= addrEnd; i = i + (searchForByteValues ? 1 : 2)) { + int v = searchForByteValues ? ram.readRaw(i) & 0x0ff : ram.readWordRaw(i) & 0x0ffff; + results.put(i,v); + } + redrawResults(); + } +} diff --git a/src/main/java/jace/cheat/PrinceOfPersiaCheats.java b/src/main/java/jace/cheat/PrinceOfPersiaCheats.java new file mode 100644 index 0000000..ab2eece --- /dev/null +++ b/src/main/java/jace/cheat/PrinceOfPersiaCheats.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.cheat; + +import jace.Emulator; +import jace.apple2e.RAM128k; +import jace.apple2e.SoftSwitches; +import jace.config.ConfigurableField; +import jace.core.Computer; +import jace.core.PagedMemory; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import java.awt.Component; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +/** + * Prince of Persia game cheats. This would not have been possible without the + * source. I am eternally grateful to Jordan Mechner both for creating this + * game, and for being so kind to release the source code to it so that we can + * learn how it works. Where possible, I've indicated where I found the various + * game variables in the original source so that it might help anyone else + * trying to learn how this game works. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class PrinceOfPersiaCheats extends Cheats implements MouseListener { + + @ConfigurableField(category = "Hack", name = "Feather fall", defaultValue = "false", description = "Fall like a feather!") + public static boolean velocityHack; + // Game memory locations + // Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/GAMEEQ.S + @ConfigurableField(category = "Hack", name = "Invincibility", defaultValue = "false", description = "Warning: will crash game if you are impaled") + public static boolean invincibilityHack; + @ConfigurableField(category = "Hack", name = "Infinite Time", defaultValue = "false", description = "Freeze the clock") + public static boolean timeHack; + @ConfigurableField(category = "Hack", name = "Sleepy Time", defaultValue = "false", description = "Enemies won't react") + public static boolean sleepHack; + @ConfigurableField(category = "Hack", name = "Can haz sword?", defaultValue = "false", description = "Start with sword in level 1") + public static boolean swordHack; + @ConfigurableField(category = "Hack", name = "Mouse", defaultValue = "false", description = "Left click kills/opens, Right click teleports") + public static boolean mouseHack; + boolean mouseRegistered = false; + public static int PREV = 0x02b; + public static int SPREV = 0x02e; + public static int CharPosn = 0x040; + public static int CharX = 0x041; + public static int CharY = 0x042; + public static int CharFace = 0x043; + public static int CharBlockX = 0x44; + public static int CharBlockY = 0x45; + public static int CharAction = 0x46; + public static int CharXVel = 0x47; + public static int CharYVel = 0x48; + public static int CharSeq = 0x49; // Word + public static int CharScrn = 0x4b; + public static int CharRepeat = 0x4c; + public static int CharID = 0x4d; + public static int CharSword = 0x4e; + public static int CharLife = 0x4f; + public static int KidX = 0x051; + public static int KidY = 0x052; + public static int KidFace = 0x53; + public static int KidBlockX = 0x54; + public static int KidBlockY = 0x55; + public static int KidAction = 0x56; + public static int KidScrn = 0x5b; + public static int ShadBlockX = 0x64; + public static int ShadBlockY = 0x65; + public static int ShadLife = 0x06f; + // Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/02%20POP%20Disk%20Routines/CP.525/RYELLOW1.S + public static int deprotectCheckYellow = 0x07c; + public static int NumTrans = 0x096; + public static int OppStrength = 0x0cc; + public static int KidStrength = 0x0ce; + public static int EnemyAlert = 0x0d1; + public static int ChgOppStr = 0x0d2; + // Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/02%20POP%20Disk%20Routines/CP.525/PURPLE.MAIN.S + public static int deprotectCheckPurple = 0x0da; + public static int Heoric = 0x0d3; + public static int InEditor = 0x0202; + public static int MinLeft = 0x0300; + public static int hasSword = 0x030a; + public static int mobtables = 0x0b600; + public static int trloc = mobtables; + public static int trscrn = trloc + 0x020; + public static int trdirec = trscrn + 0x020; + // Blueprint (map level data)0 + public static int BlueSpec = 0x0b9d0; + public static int LinkLoc = 0x0bca0; + public static int LinkMap = 0x0bda0; + public static int Map = 0x0bea0; + public static int MapInfo = 0x0bf00; + public static int RedBufs = 0x05e00; + public static int RedBuf = RedBufs + 90; + // Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/EQ.S + public static int WipeBuf = RedBuf + 90; + public static int MoveBuf = WipeBuf + 30; + // Object types + // Source: https://github.com/jmechner/Prince-of-Persia-Apple-II/blob/master/01%20POP%20Source/Source/MOVEDATA.S + public static int space = 0; + public static int floor = 1; + public static int spikes = 2; + public static int posts = 3; + public static int gate = 4; + public static int dpressplate = 5; + public static int pressplate = 6; + public static int panelwif = 7; + public static int pillarbottom = 8; + public static int pillartop = 9; + public static int flask = 10; + public static int loose = 11; + public static int panelwof = 12; + public static int mirror = 13; + public static int rubble = 14; + public static int upressplate = 15; + public static int exit = 16; + public static int exit2 = 17; + public static int slicer = 18; + public static int torch = 19; + public static int block = 20; + public static int bones = 21; + public static int sword = 22; + public static int window = 23; + public static int window2 = 24; + public static int archbot = 25; + public static int archtop1 = 26; + public static int archtop2 = 27; + public static int archtop3 = 28; + public static int archtop4 = 29; + // This is the correct value for an open exit door. + public static int ExitOpen = 172; + + @Override + protected String getDeviceName() { + return ("Prince of Persia"); + } + + @Override + public void tick() { + // Do nothing + } + + @Override + public void attach() { + if (velocityHack) { + addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(CharYVel); + } + + @Override + protected void doEvent(RAMEvent e) { + registerMouse(); + if (!SoftSwitches.AUXZP.getState()) { + return; + } + int newVel = e.getNewValue(); + if (newVel > 5) { + newVel = 1; + } + e.setNewValue(newVel & 0x0ff); + } + }); + } + + if (invincibilityHack) { + addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(KidStrength); + } + + @Override + protected void doEvent(RAMEvent e) { + registerMouse(); + if (!SoftSwitches.AUXZP.getState()) { + return; + } + e.setNewValue(3); + } + }); + } + if (sleepHack) { + addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(EnemyAlert); + } + + @Override + protected void doEvent(RAMEvent e) { + registerMouse(); + if (!SoftSwitches.AUXZP.getState()) { + return; + } + e.setNewValue(0); + } + }); + } + if (swordHack) { + addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(hasSword); + } + + @Override + protected void doEvent(RAMEvent e) { + registerMouse(); + if (!SoftSwitches.AUXZP.getState()) { + return; + } + e.setNewValue(1); + } + }); + } + if (timeHack) { + addCheat(new RAMListener(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(MinLeft); + } + + @Override + protected void doEvent(RAMEvent e) { + registerMouse(); + if (!SoftSwitches.AUXZP.getState()) { + return; + } + e.setNewValue(0x069); + } + }); + } + if (mouseHack) { + if (Emulator.getScreen() != null) { + Emulator.getScreen().addMouseListener(this); + } else { + mouseRegistered = true; + } + } else { + if (Emulator.getScreen() != null) { + Emulator.getScreen().removeMouseListener(this); + } + } + } + + @Override + public void detach() { + super.detach(); + if (Emulator.getScreen() != null) { + Emulator.getScreen().removeMouseListener(this); + } + mouseRegistered = false; + } + public static int BlueType = 0x0b700; + + public void registerMouse() { + if (mouseRegistered) { + Component drawingArea = Emulator.getScreen(); + if (drawingArea == null) { + return; + } + Emulator.getScreen().addMouseListener(this); + mouseRegistered = false; + } + } + + @Override + public void mouseClicked(MouseEvent me) { + Component drawingArea = Emulator.getScreen(); + if (drawingArea == null) { + return; + } + Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation(); + Point topLeft = drawingArea.getLocationOnScreen(); + Double x = (currentMouseLocation.x - topLeft.x) / drawingArea.getSize().getWidth(); + // Offset y by three pixels to account for tiles above + Double y = (currentMouseLocation.y - topLeft.y) / drawingArea.getSize().getHeight() - 0.015625; + // Now we have the x and y coordinates ranging from 0 to 1.0, scale to POP values + int row = y < 0 ? -1 : (int) (y * 3); + int col = (int) (x * 10); + + // Do a check if we are at the bottom of the tile, the user might have been clicking on the tile to the right. + // This accounts for the isometric view and allows a little more flexibility, not to mention warping behind gates + // that are on the left edge of the screen! + int yCoor = ((int) (y * 192) % 63); + if (yCoor >= 47) { + double yOffset = 1.0 - (((double) yCoor - 47.0) / 16.0); + int xCoor = ((int) (x * 280) % 28); + double xOffset = ((double) xCoor) / 28.0; + if (xOffset <= yOffset) { + col--; + } + } + + // Note: POP uses a 255-pixel horizontal axis, Pixels 0-57 are offscreen to the left + // and 198-255 offscreen to the right. + +// System.out.println("Clicked on " + col + "," + row + " -- screen " + (x * 280) + "," + (y * 192)); + RAM128k mem = (RAM128k) Computer.getComputer().getMemory(); + PagedMemory auxMem = mem.getAuxMemory(); + + if (me.getButton() == MouseEvent.BUTTON1) { + // Left click hacks + // See if there is an opponent we can kill off. + int opponentX = auxMem.readByte(ShadBlockX); + int opponentY = auxMem.readByte(ShadBlockY); + int opponentLife = auxMem.readByte(ShadLife); + // If there is a guy near where the user clicked and he's alive, then kill 'em. + if (opponentLife != 0 && opponentY == row && Math.abs(col - opponentX) <= 1) { + // System.out.println("Enemy at " + opponentX + "," + opponentY + "; life=" + opponentLife); + // Occasionally, if the code is at the right spot this will cause the special effect of a hit to appear + auxMem.writeByte(ChgOppStr, (byte) -opponentLife); + // And this will kill the dude pretty much right away. + auxMem.writeByte(ShadLife, (byte) 0); + } else if (row >= 0 && col >= 0) { + // Try to perform actions on the block clicked as well as to the left and right of it. + // This opens gates and exits. + performAction(row, col, 1); + performAction(row, col - 1, 1); + performAction(row, col + 1, 1); + } + } else { + // Right/middle click == warp + byte warpX = (byte) (x * 140 + 58); + // This aliases the Y coordinate so the prince is on the floor at the correct spot. + byte warpY = (byte) ((row * 63) + 54); +// System.out.println("Warping to " + warpX + "," + warpY); + auxMem.writeByte(KidX, warpX); + auxMem.writeByte(KidY, warpY); + auxMem.writeByte(KidBlockX, (byte) col); + auxMem.writeByte(KidBlockY, (byte) row); + // Set action to bump into a wall so it can reset the kid's feet on the ground correctly. + // Not sure if this has any real effect but things seem to be working (so I'll just leave this here...) + auxMem.writeByte(KidAction, (byte) 5); + } + } + + /** + * + * @param row + * @param col + * @param direction + */ + public void performAction(int row, int col, int direction) { + RAM128k mem = (RAM128k) Computer.getComputer().getMemory(); + PagedMemory auxMem = mem.getAuxMemory(); + byte currentScrn = auxMem.readByte(KidScrn); + if (col < 0) { + col += 10; + int scrnLeft = auxMem.readByte(Map + ((currentScrn - 1) * 4)); + if (scrnLeft == 0) { + return; + } + currentScrn = (byte) scrnLeft; + byte prev = auxMem.readByte(PREV + row); + byte sprev = auxMem.readByte(SPREV + row); + // If the block to the left is gate, let's lie about it being open... for science + // This causes odd-looking screen behavior but it gets the job done. + if (prev == 4) { + // Update the temp variable that represents that object + auxMem.writeByte(SPREV + row, (byte) 255); + // And also update the blueprint + auxMem.writeByte(BlueSpec + ((scrnLeft - 1) * 30) + row * 10 + 9, (byte) 255); + } +// System.out.println("Looking at room to left, row "+row+": "+Integer.toHexString(prev)+","+Integer.toHexString(sprev)); + } else if (col >= 10) { + // This code will probably never be called but here just in case. + col -= 10; + int scrnRight = auxMem.readByte(Map + ((currentScrn - 1) * 4) + 1); + if (scrnRight == 0) { + return; + } + currentScrn = (byte) scrnRight; + } + int numTransition = auxMem.readByte(NumTrans); + byte clickedLoc = (byte) (row * 10 + col); + // Figure out what kind of block is there + int blockType = auxMem.readByte(BlueType + (currentScrn - 1) * 30 + row * 10 + col) & 0x01f; + if (blockType == exit2 || blockType == exit) { + // Open the exit by changing the map data and adding the tiles to the move buffer + auxMem.writeByte(BlueSpec + (currentScrn - 1) * 30 + row * 10 + col, (byte) ExitOpen); + direction = 1; + // Tell the graphics engine that this piece has moved. + auxMem.writeByte(MoveBuf + row * 10 + col, (byte) 2); + } + if (blockType == gate || blockType == exit2 || blockType == exit) { + // If the object in question can be opened (exit or gate) add it to the transitional animation buffer + //System.out.print("Triggering screen " + currentScrn + " at pos " + clickedLoc); + boolean addTransition = false; + if (numTransition == 0) { + addTransition = true; + } else { + addTransition = true; + for (int i = 1; i <= numTransition; i++) { + byte scrn = auxMem.readByte(trscrn + i); + byte loc = auxMem.readByte(trloc + i); + if (scrn == currentScrn && loc == clickedLoc) { + // Entry already exists, just change its direction + auxMem.writeByte(trdirec + i, (byte) direction); + addTransition = false; + break; + } + } + if (addTransition && numTransition >= 0x20) { + addTransition = false; + } + } + // If the object was not in the animation buffer, add it. + if (addTransition) { + numTransition++; + auxMem.writeByte(trdirec + numTransition, (byte) direction); + auxMem.writeByte(trscrn + numTransition, currentScrn); + auxMem.writeByte(trloc + numTransition, clickedLoc); + auxMem.writeByte(NumTrans, (byte) numTransition); + } + } + } + + @Override + public void mousePressed(MouseEvent me) { + } + + @Override + public void mouseReleased(MouseEvent me) { + } + + @Override + public void mouseEntered(MouseEvent me) { + } + + @Override + public void mouseExited(MouseEvent me) { + } +} \ No newline at end of file diff --git a/src/main/java/jace/config/BooleanComponent.java b/src/main/java/jace/config/BooleanComponent.java new file mode 100644 index 0000000..6a8e800 --- /dev/null +++ b/src/main/java/jace/config/BooleanComponent.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.config.Configuration.ConfigNode; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JCheckBox; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +class BooleanComponent extends JCheckBox implements ActionListener { + ConfigNode node; + String fieldName; + + public BooleanComponent(ConfigNode node, String fieldName) { + this.node = node; + this.fieldName = fieldName; + synchronizeValue(); + addActionListener(this); + } + + public void actionPerformed(ActionEvent e) { + Boolean value = Boolean.valueOf(isSelected()); + node.setFieldValue(fieldName, value); + } + + public void synchronizeValue() { + try { + + Object value = node.getFieldValue(fieldName); + if (value == null) { + setSelected(false); + } else { + setSelected(Boolean.valueOf(String.valueOf(value)).booleanValue()); + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(BooleanComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/config/ClassSelection.java b/src/main/java/jace/config/ClassSelection.java new file mode 100644 index 0000000..7678400 --- /dev/null +++ b/src/main/java/jace/config/ClassSelection.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.core.Utility; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ + +public class ClassSelection extends DynamicSelection<Class> { + + Class template = null; + + public ClassSelection(Class supertype, Class defaultValue) { + super(defaultValue); + template = supertype; + } + + @Override + public LinkedHashMap<Class, String> getSelections() { + LinkedHashMap<Class, String> selections = new LinkedHashMap<Class, String>(); + List<? extends Class> allClasses = (List<? extends Class>) Utility.findAllSubclasses(template); + if (!allClasses.contains(null)) { + allClasses.add(null); + } + List<Entry<Class, String>> values = new ArrayList<Map.Entry<Class, String>>(); + if (allowNull()) { + values.add(new Entry<Class, String>() { + + @Override + public Class getKey() { + return null; + } + + @Override + public String getValue() { + return "***Empty***"; + } + + @Override + public String setValue(String v) { + throw new UnsupportedOperationException("Not supported yet."); + } + }); + } + for (final Class c : allClasses) { + Entry<Class, String> entry = new Map.Entry<Class, String>() { + + public Class getKey() { + return c; + } + + public String getValue() { + if (c == null) { + return "**Empty**"; + } + if (c.isAnnotationPresent(Name.class)) { + return ((Name) c.getAnnotation(Name.class)).value(); + } + return c.getSimpleName(); + } + + public String setValue(String value) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String toString() { + return getValue(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) || obj == getKey() || getKey() != null && getKey().equals(obj); + } + }; + values.add(entry); + } + Collections.sort(values, new Comparator<Map.Entry<? extends Class, String>>() { + public int compare(Entry<? extends Class, String> o1, Entry<? extends Class, String> o2) { + if (o1.getKey() == null) { + return -1; + } + if (o2.getKey() == null) { + return 1; + } else { + return (o1.getValue().compareTo(o2.getValue())); + } + } + }); + for (Map.Entry<Class, String> entry : values) { + Class key = entry.getKey(); + selections.put(key, entry.getValue()); + } + return selections; + } + + @Override + public boolean allowNull() { + return false; + } + + @Override + public void setValue(Class value) { + Object v = value; + if (v != null && v instanceof String) { + super.setValueByMatch((String) v); + return; + } + super.setValue(value); + } +} \ No newline at end of file diff --git a/src/main/java/jace/config/ConfigurableField.java b/src/main/java/jace/config/ConfigurableField.java new file mode 100644 index 0000000..4e90457 --- /dev/null +++ b/src/main/java/jace/config/ConfigurableField.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A configurable field annotation means that an object property can be changed + * by the end-user. + * NOTE: Any field that implements this must be public and serializable! + * If a field is not serializable, it will result in a serialization error + * when the configuration is being saved. There is no way to offer a compiler + * warning to avoid this, unfortunately. + * One way you can work with this constraint when allowing large reconfiguration + * of functionality, such as Cards or other class implementations of hardware, + * is to store the class itself of the component as a configuration value + * and let the Reconfigure method generate a new instance. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigurableField { + public String name(); + public String shortName() default ""; + public String defaultValue() default ""; + public String description() default ""; + public String category() default "General"; +} \ No newline at end of file diff --git a/src/main/java/jace/config/Configuration.java b/src/main/java/jace/config/Configuration.java new file mode 100644 index 0000000..bced959 --- /dev/null +++ b/src/main/java/jace/config/Configuration.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.core.Computer; +import jace.core.Utility; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +/** + * Manages the configuration state of the emulator components. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Configuration implements Reconfigurable { + + public String getName() { + return "Configuration"; + } + + @Override + public String getShortName() { + return "cfg"; + } + + public void reconfigure() { + } + + public static class ConfigTreeModel implements TreeModel { + + public Object getRoot() { + return BASE; + } + + public Object getChild(Object parent, int index) { + if (parent instanceof ConfigNode) { + ConfigNode n = (ConfigNode) parent; + return n.children.values().toArray()[index]; + } else { + return null; + } + } + + public int getChildCount(Object parent) { + if (parent instanceof ConfigNode) { + ConfigNode n = (ConfigNode) parent; + return n.children.size(); + } else { + return 0; + } + } + + public boolean isLeaf(Object node) { + return getChildCount(node) == 0; + } + + public void valueForPathChanged(TreePath path, Object newValue) { + // Do nothing... + } + + public int getIndexOfChild(Object parent, Object child) { + if (parent instanceof ConfigNode) { + ConfigNode n = (ConfigNode) parent; + ConfigNode[] c = (ConfigNode[]) n.children.values().toArray(new ConfigNode[0]); + for (int i = 0; i < c.length; i++) { + if (c[i].equals(child)) { + return i; + } + } + } + return -1; + } + + public void addTreeModelListener(TreeModelListener l) { + // Do nothing... + } + + public void removeTreeModelListener(TreeModelListener l) { + // Do nothing... + } + } + + /** + * Represents a serializable configuration node as part of a tree. + * The root node should be a single instance (e.g. Computer) + * The child nodes should be all object instances that stem from each object + * The overall goal of this class is two-fold: + * 1) Provide a navigable manner to inspect configuration + * 2) Provide a simple persistence mechanism to load/store configuration + */ + public static class ConfigNode implements Serializable { + + public transient ConfigNode root; + public transient ConfigNode parent; + public transient Reconfigurable subject; + private Map<String, Serializable> settings; + protected Map<String, ConfigNode> children; + private boolean changed = false; + + @Override + public String toString() { + if (subject == null) { + return "???"; + } + return (changed ? "<html><i>" : "") + subject.getName(); + } + + public ConfigNode(Reconfigurable subject) { + this(null, subject); + this.root = null; + } + + public ConfigNode(ConfigNode parent, Reconfigurable subject) { + this.subject = subject; + this.settings = new TreeMap<String, Serializable>(); + this.children = new TreeMap<String, ConfigNode>(); + this.parent = parent; + if (this.parent != null) { + this.root = this.parent.root != null ? this.parent.root : this.parent; + } + } + + public void setFieldValue(String field, Serializable value) { + if (value != null) { + if (value.equals(getFieldValue(field))) { + return; + } + } else { + if (getFieldValue(field) == null) { + return; + } + } + changed = true; + setRawFieldValue(field, value); + } + + public void setRawFieldValue(String field, Serializable value) { + settings.put(field, value); + } + + public Serializable getFieldValue(String field) { + return settings.get(field); + } + + public Set<String> getAllSettingNames() { + return settings.keySet(); + } + } + public final static ConfigNode BASE; + public static Computer emulator = Computer.getComputer(); + @ConfigurableField(name = "Autosave Changes", description = "If unchecked, changes are only saved when the Save button is pressed.") + public static boolean saveAutomatically = false; + + static { + BASE = new ConfigNode(new Configuration()); + buildTree(); + } + + public static void buildTree() { + buildTree(BASE, new HashSet()); + } + + private static void buildTree(ConfigNode node, Set visited) { + if (node.subject == null) { + return; + } + for (Field f : node.subject.getClass().getFields()) { +// System.out.println("Evaluating field " + f.getName()); + try { + Object o = f.get(node.subject); + if (/*o == null ||*/visited.contains(o)) { + continue; + } +// System.out.println(o.getClass().getName()); + // If the object in question is not reconfigurable, + // skip over it and investigate its fields instead +// if (o.getClass().isAssignableFrom(Reconfigurable.class)) { +// if (Reconfigurable.class.isAssignableFrom(o.getClass())) { + if (f.isAnnotationPresent(ConfigurableField.class)) { + if (o != null && ISelection.class.isAssignableFrom(o.getClass())) { + ISelection selection = (ISelection) o; + node.setRawFieldValue(f.getName(), (Serializable) selection.getSelections().get(selection.getValue())); + } else { + node.setRawFieldValue(f.getName(), (Serializable) o); + } + continue; + } else if (o == null) { + continue; + } + + if (o instanceof Reconfigurable) { + Reconfigurable r = (Reconfigurable) o; + visited.add(r); + ConfigNode child = node.children.get(f.getName()); + if (child == null || !child.subject.equals(o)) { + child = new ConfigNode(node, r); + node.children.put(f.getName(), child); + } + buildTree(child, visited); + } else if (o.getClass().isArray()) { + String fieldName = f.getName(); + Class type = o.getClass().getComponentType(); + if (!Reconfigurable.class.isAssignableFrom(type)) { + continue; + } + Reconfigurable[] r = (Reconfigurable[]) o; + visited.add(r); + for (int i = 0; i < r.length; i++) { + String childName = fieldName + i; + if (r[i] == null) { + node.children.remove(childName); + continue; + } + ConfigNode grandchild = node.children.get(childName); + if (grandchild == null || !grandchild.subject.equals(r[i])) { + grandchild = new ConfigNode(node, r[i]); + node.children.put(childName, grandchild); + } + buildTree(grandchild, visited); + } + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + @InvokableAction( + name="Save settings", + description="Save all configuration settings as defaults", + category="general", + alternatives="save preferences;save defaults" + ) + public static void saveSettings() { + FileOutputStream fos = null; + { + ObjectOutputStream oos = null; + try { + applySettings(BASE); + oos = new ObjectOutputStream(new FileOutputStream(getSettingsFile())); + oos.writeObject(BASE); + } catch (FileNotFoundException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + if (oos != null) { + oos.close(); + } + } catch (IOException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } + + @InvokableAction( + name="Load settings", + description="Load all configuration settings previously saved", + category="general", + alternatives="load preferences;revert settings;revert preferences" + ) + public static void loadSettings() { + { + ObjectInputStream ois = null; + FileInputStream fis = null; + try { + ois = new ObjectInputStream(new FileInputStream(getSettingsFile())); + ConfigNode newRoot = (ConfigNode) ois.readObject(); + applyConfigTree(newRoot, BASE); + } catch (ClassNotFoundException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (FileNotFoundException ex) { +// Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + // This just means there are no settings to be saved -- just ignore it. + } catch (IOException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + if (ois != null) { + ois.close(); + } + } catch (IOException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } + + public static void resetToDefaults() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public static File getSettingsFile() { + return new File(System.getProperty("user.dir"), ".jace.conf"); + } + + /** + * Apply settings from node tree to the object model + * This also calls "reconfigure" on objects in sequence + * @param node + * @return True if any settings have changed in the node or any of its descendants + */ + public static boolean applySettings(ConfigNode node) { + boolean resume = false; + if (node == BASE) { + resume = Computer.pause(); + } + boolean hasChanged = false; + if (node.changed) { + doApply(node); + hasChanged = true; + } + + // Now that the object structure reflects the current configuration, + // process reconfiguration from the children, etc. + for (ConfigNode child : node.children.values()) { + hasChanged |= applySettings(child); + } + + if (node.equals(BASE) && hasChanged) { + buildTree(); + } + + if (resume) { + Computer.resume(); + } + + return hasChanged; + } + + private static void applyConfigTree(ConfigNode newRoot, ConfigNode oldRoot) { + if (oldRoot == null || newRoot == null) { + return; + } + oldRoot.settings = newRoot.settings; + if (oldRoot.subject != null) { + doApply(oldRoot); + buildTree(oldRoot, new HashSet()); + } + for (String childName : newRoot.children.keySet()) { +// System.out.println("Applying settings for " + childName); + applyConfigTree(newRoot.children.get(childName), oldRoot.children.get(childName)); + } + } + + private static void doApply(ConfigNode node) { + List<String> removeList = new ArrayList<String>(); + for (String f : node.settings.keySet()) { + try { + Field ff = node.subject.getClass().getField(f); +// System.out.println("Setting " + f + " to " + node.settings.get(f)); + Object val = node.settings.get(f); + Class valType = (val != null ? val.getClass() : null); + Class fieldType = ff.getType(); + if (ISelection.class.isAssignableFrom(fieldType)) { + ISelection selection = (ISelection) ff.get(node.subject); + try { + selection.setValue(val); + } catch (ClassCastException c) { + selection.setValueByMatch(String.valueOf(val)); + } + continue; + } + if (val == null || valType.equals(fieldType)) { + ff.set(node.subject, val); + continue; + } +// System.out.println(fieldType); + val = Utility.deserializeString(String.valueOf(val), fieldType, false); +// System.out.println("Setting "+node.subject.getName()+" property "+ff.getName()+" with value "+String.valueOf(val)); + ff.set(node.subject, val); + } catch (NoSuchFieldException ex) { + System.out.println("Setting "+f+" no longer exists, skipping."); + removeList.add(f); + } catch (SecurityException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalArgumentException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + } + for (String f : removeList) { + node.settings.remove(f); + } + + try { + // When settings are applied, this could very well change the object structure + // For example, if cards or other pieces of emulation are changed around +// System.out.println("Reconfiguring "+node.subject.getName()); + node.subject.reconfigure(); + } catch (Exception ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + + node.changed = false; + } + + public static void applySettings(Map<String, String> settings) { + for (Map.Entry<String, String> setting : settings.entrySet()) { + Map<String, ConfigNode> shortNames = new HashMap<String, ConfigNode>(); + buildNodeMap(BASE, shortNames); + + String settingName = setting.getKey(); + String value = setting.getValue(); + String[] parts = settingName.split("\\."); + if (parts.length != 2) { + System.err.println("Unable to parse settting, should be in the form of DEVICE.PROPERTYNAME "+settingName); + continue; + } + String deviceName = parts[0]; + String fieldName = parts[1]; + ConfigNode n = shortNames.get(deviceName.toLowerCase()); + if (n == null) { + System.err.println("Unable to find device named "+deviceName+", try one of these: "+Utility.join(shortNames.keySet(), ", ")); + continue; + } + + boolean found = false; + List<String> shortFieldNames = new ArrayList<String>(); + for (String longName : n.getAllSettingNames()) { + ConfigurableField f = null; + try { + f = n.subject.getClass().getField(longName).getAnnotation(ConfigurableField.class); + } catch (NoSuchFieldException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + String shortName = (f != null && !f.shortName().equals("")) ? f.shortName() : longName; + shortFieldNames.add(shortName); + + if (fieldName.equalsIgnoreCase(longName) || fieldName.equalsIgnoreCase(shortName)) { + found = true; + n.setFieldValue(longName, value); + applySettings(n); + n.subject.reconfigure(); + buildTree(); + System.out.println("Set property "+n.subject.getName()+"."+longName+" to "+value); + break; + } + } + if (!found) { + System.err.println("Unable to find property "+fieldName+" for device "+deviceName+". Try one of these :"+Utility.join(shortFieldNames, ", ")); + } + } + } + + private static void buildNodeMap(ConfigNode n, Map<String, ConfigNode> shortNames) { + shortNames.put(n.subject.getShortName().toLowerCase(), n); + for (Map.Entry<String, ConfigNode> c : n.children.entrySet()) { + buildNodeMap(c.getValue(), shortNames); + } + } + + private static void printTree(ConfigNode n, String prefix, int i) { + for (String setting : n.getAllSettingNames()) { + for (int j=0; j < i; j++) System.out.print(" "); + ConfigurableField f = null; + try { + f = n.subject.getClass().getField(setting).getAnnotation(ConfigurableField.class); + } catch (NoSuchFieldException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(Configuration.class.getName()).log(Level.SEVERE, null, ex); + } + String sn = (f != null && !f.shortName().equals("")) ? f.shortName() : setting; + System.out.println(prefix+">>"+setting+" ("+n.subject.getShortName()+"."+sn+")"); + } + for (Map.Entry<String, ConfigNode> c : n.children.entrySet()) { + printTree(c.getValue(), prefix+"."+c.getKey(), i+1); + } + } +} diff --git a/src/main/java/jace/config/ConfigurationPanel.form b/src/main/java/jace/config/ConfigurationPanel.form new file mode 100644 index 0000000..1d2b63f --- /dev/null +++ b/src/main/java/jace/config/ConfigurationPanel.form @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.7" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[650, 480]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="applyButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="saveButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="revertButton" min="-2" max="-2" attributes="0"/> + </Group> + <Component id="configTreeScrollPane" min="-2" pref="271" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Component id="settingsPanel" pref="438" max="32767" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Component id="configTreeScrollPane" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="applyButton" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="saveButton" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="revertButton" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + </Group> + <Component id="settingsPanel" pref="0" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="configTreeScrollPane"> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTree" name="configTree"> + <Properties> + <Property name="model" type="javax.swing.tree.TreeModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="new Configuration.ConfigTreeModel()" type="code"/> + </Property> + </Properties> + <Events> + <EventHandler event="valueChanged" listener="javax.swing.event.TreeSelectionListener" parameters="javax.swing.event.TreeSelectionEvent" handler="configTreeValueChanged"/> + </Events> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="settingsPanel"> + <Properties> + <Property name="alignmentX" type="float" value="0.0"/> + <Property name="alignmentY" type="float" value="0.0"/> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[375, 480]"/> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + </Container> + <Component class="javax.swing.JButton" name="applyButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Apply"/> + <Property name="toolTipText" type="java.lang.String" value="Apply current changes without saving"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="applyButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="saveButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Save"/> + <Property name="toolTipText" type="java.lang.String" value="Apply settings and save"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="saveButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="revertButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Revert"/> + <Property name="toolTipText" type="java.lang.String" value="Revert all settings to last saved values (hold SHIFT while clicking to revert to defaults)"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="revertButtonActionPerformed"/> + </Events> + </Component> + </SubComponents> +</Form> diff --git a/src/main/java/jace/config/ConfigurationPanel.java b/src/main/java/jace/config/ConfigurationPanel.java new file mode 100644 index 0000000..5891952 --- /dev/null +++ b/src/main/java/jace/config/ConfigurationPanel.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.apple2e.Apple2e; +import jace.config.Configuration.ConfigNode; +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.io.File; +import java.lang.reflect.Field; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JTextField; +import javax.swing.JTree; + +/** + * Configuration user interface. Not pretty but it works. + * Created on Dec 16, 2010, 3:23:13 PM + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ConfigurationPanel extends javax.swing.JPanel { + + public static void main(String... args) { + new Apple2e(); + Apple2e.getComputer().reconfigure(); + Configuration.loadSettings(); + JFrame f = new JFrame(); + f.setContentPane(new ConfigurationPanel()); + f.setSize(f.getContentPane().getPreferredSize()); + f.validate(); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.setVisible(true); + } + + /** Creates new form ConfigurationPanel */ + public ConfigurationPanel() { + initComponents(); + expandAll(configTree); + } + + /** This method is called from within the constructor to + * initialize the form. + * WARNING: Do NOT modify this code. The content of this method is + * always regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + configTreeScrollPane = new javax.swing.JScrollPane(); + configTree = new javax.swing.JTree(); + settingsPanel = new javax.swing.JPanel(); + applyButton = new javax.swing.JButton(); + saveButton = new javax.swing.JButton(); + revertButton = new javax.swing.JButton(); + + setPreferredSize(new java.awt.Dimension(650, 480)); + + configTree.setModel(new Configuration.ConfigTreeModel()); + configTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { + public void valueChanged(javax.swing.event.TreeSelectionEvent evt) { + configTreeValueChanged(evt); + } + }); + configTreeScrollPane.setViewportView(configTree); + + settingsPanel.setAlignmentX(0.0F); + settingsPanel.setAlignmentY(0.0F); + settingsPanel.setPreferredSize(new java.awt.Dimension(375, 480)); + settingsPanel.setLayout(new java.awt.GridBagLayout()); + + applyButton.setText("Apply"); + applyButton.setToolTipText("Apply current changes without saving"); + applyButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + applyButtonActionPerformed(evt); + } + }); + + saveButton.setText("Save"); + saveButton.setToolTipText("Apply settings and save"); + saveButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + saveButtonActionPerformed(evt); + } + }); + + revertButton.setText("Revert"); + revertButton.setToolTipText("Revert all settings to last saved values (hold SHIFT while clicking to revert to defaults)"); + revertButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + revertButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(applyButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(saveButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(revertButton)) + .addComponent(configTreeScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 271, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 438, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(configTreeScrollPane) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(applyButton) + .addComponent(saveButton) + .addComponent(revertButton)) + .addContainerGap()) + .addComponent(settingsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 0, Short.MAX_VALUE) + ); + }// </editor-fold>//GEN-END:initComponents + + ConfigNode currentNode = null; + private void configTreeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_configTreeValueChanged + settingsPanel.removeAll(); + ConfigNode node = (ConfigNode) evt.getPath().getLastPathComponent(); + if (currentNode == node) return; + currentNode = node; + if (node != null && node.subject != null && !node.getAllSettingNames().isEmpty()) { + GridBagLayout l = (GridBagLayout) settingsPanel.getLayout(); + GridBagConstraints c = new GridBagConstraints(); + int y = 0; + for (String s : node.getAllSettingNames()) { + try { + Field f = node.subject.getClass().getField(s); + ConfigurableField annotation = f.getAnnotation(ConfigurableField.class); + if (annotation == null) continue; + JLabel label = new JLabel(annotation.name()); + c.anchor = GridBagConstraints.FIRST_LINE_START; + c.fill = GridBagConstraints.HORIZONTAL; + c.gridwidth = 1; + c.gridy = y++; + c.gridx = 0; + c.weightx = 0.0; + c.insets = new Insets(2, 2, 2, 2); + c.ipady = 2; + settingsPanel.add(label,c); + Component edit = generateEditComponent(node, s); + edit.setSize(edit.getPreferredSize()); + c.gridx = 1; + c.weightx = 0.5; + c.ipady = 0; + c.insets = new Insets(0, 2, 2, 2); + c.gridwidth = GridBagConstraints.REMAINDER; + settingsPanel.add(edit,c); + if (!annotation.description().equals("")) { + c.gridy = y++; + c.gridx = 0; + c.gridwidth = 2; + c.weightx = 0.0; + JLabel desc = new JLabel("<html><i>" + annotation.description()); + c.insets = new Insets(1, 15, 7, 5); + settingsPanel.add(desc, c); + } + } catch (NoSuchFieldException ex) { + Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + settingsPanel.validate(); + settingsPanel.setVisible(true); + settingsPanel.repaint(); + + }//GEN-LAST:event_configTreeValueChanged + + private void applyButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_applyButtonActionPerformed + Configuration.applySettings(Configuration.BASE); + configTree.updateUI(); + expandAll(configTree); + }//GEN-LAST:event_applyButtonActionPerformed + + private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed + Configuration.saveSettings(); + configTree.updateUI(); + expandAll(configTree); + }//GEN-LAST:event_saveButtonActionPerformed + + private void revertButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_revertButtonActionPerformed + if ((evt.getModifiers() & evt.SHIFT_MASK) != 0) { + revertDefaultsActionPerformed(evt); + return; + } + Configuration.loadSettings(); + configTree.updateUI(); + expandAll(configTree); + }//GEN-LAST:event_revertButtonActionPerformed + + private void revertDefaultsActionPerformed(java.awt.event.ActionEvent evt) { + if (JOptionPane.showConfirmDialog(null, "Revert all settings to defaults?", "Revert to defaults?", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) + return; + Configuration.resetToDefaults(); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton applyButton; + private javax.swing.JTree configTree; + private javax.swing.JScrollPane configTreeScrollPane; + private javax.swing.JButton revertButton; + private javax.swing.JButton saveButton; + private javax.swing.JPanel settingsPanel; + // End of variables declaration//GEN-END:variables + + public void expandAll(JTree tree) { + for (int row = 0; row < tree.getRowCount(); tree.expandRow(row++)); + } + + private Component generateEditComponent(ConfigNode node, String s) { + try { + Field f = node.subject.getClass().getField(s); + + if (f.getType().isPrimitive()) { + if (f.getType().equals(Boolean.TYPE)) { + return new BooleanComponent(node, s); + } else if (f.getType().equals(Integer.TYPE)) { + return new IntegerComponent(node, s); + } else if (f.getType().equals(Short.TYPE)) { + return new IntegerComponent(node, s); + } else if (f.getType().equals(Byte.TYPE)) { + return new IntegerComponent(node, s); + } else if (f.getType().equals(Long.TYPE)) { + return new IntegerComponent(node, s); + } else { + return new StringComponent(node, s); + } + } else if (f.getType().equals(String.class)) { + return new StringComponent(node, s); + } else if (f.getType().equals(File.class)) { + return new FileComponent(node, s); + } else if (Class.class.isEnum()) { + // TODO: Add enumeration support! + } else if (ISelection.class.isAssignableFrom(f.getType())) { + return new DynamicSelectComponent(node, s); + } + return new JTextField(); + } catch (NoSuchFieldException ex) { + Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(ConfigurationPanel.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } +} diff --git a/src/main/java/jace/config/DynamicSelectComponent.java b/src/main/java/jace/config/DynamicSelectComponent.java new file mode 100644 index 0000000..e19abf2 --- /dev/null +++ b/src/main/java/jace/config/DynamicSelectComponent.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.config.Configuration.ConfigNode; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.event.ListDataListener; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +class DynamicSelectComponent extends JComboBox implements ActionListener { + + ConfigNode node; + String fieldName; + Serializable currentValue; + + @Override + public void actionPerformed(ActionEvent e) { + node.setFieldValue(fieldName, currentValue); + } + + public void synchronizeValue() { + try { + Object value = node.getFieldValue(fieldName); + if (value == null) { + getModel().setSelectedItem(null); + setSelectedItem(getModel().getSelectedItem()); + } else { + getModel().setSelectedItem(value); + setSelectedItem(getModel().getSelectedItem()); + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } + + public DynamicSelectComponent(ConfigNode node, String fieldName) { + try { + this.node = node; + this.fieldName = fieldName; + DynamicSelection sel; + try { + sel = (DynamicSelection) node.subject.getClass().getField(fieldName).get(node.subject); + } catch (IllegalArgumentException ex) { + Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex); + System.err.print("Couldn't get selections for field " + fieldName); + return; + } catch (IllegalAccessException ex) { + Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex); + System.err.print("Couldn't get selections for field " + fieldName); + return; + } catch (NoSuchFieldException ex) { + Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex); + System.err.print("Couldn't get selections for field " + fieldName); + return; + } catch (SecurityException ex) { + Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex); + System.err.print("Couldn't get selections for field " + fieldName); + return; + } + currentValue = node.getFieldValue(fieldName); + final LinkedHashMap selections = sel.getSelections(); + + addActionListener(this); + ComboBoxModel m; + + setModel(new ComboBoxModel() { + + Entry value; + + public void setSelectedItem(Object anItem) { + if (anItem != null && anItem instanceof Map.Entry) { + value = (Entry) anItem; + currentValue = (Serializable) ((Entry) anItem).getKey(); + } else { + for (Map.Entry entry : (Set<Map.Entry>) selections.entrySet()) { + if (entry.getValue().equals(anItem)) { + value = entry; + currentValue = (Serializable) entry.getKey(); + } + if (entry.getKey() == null && anItem == null) { + value = entry; + currentValue = (Serializable) entry.getKey(); + } + if (entry.getKey() != null && entry.equals(anItem)) { + value = entry; + currentValue = (Serializable) entry.getKey(); + } + } + } + + } + + public Object getSelectedItem() { + return selections.get(currentValue); + } + + public int getSize() { + return selections.size(); + } + + public Object getElementAt(int index) { + for (Map.Entry entry : (Set<Map.Entry>) selections.entrySet()) { + if (index == 0) { + return entry.getValue(); + } + index--; + } + return null; + } + + public void addListDataListener(ListDataListener l) { + } + + public void removeListDataListener(ListDataListener l) { + } + }); + synchronizeValue(); + } catch (SecurityException ex) { + Logger.getLogger(DynamicSelectComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } +} diff --git a/src/main/java/jace/config/DynamicSelection.java b/src/main/java/jace/config/DynamicSelection.java new file mode 100644 index 0000000..87d916f --- /dev/null +++ b/src/main/java/jace/config/DynamicSelection.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.core.Utility; +import java.util.Iterator; +import java.util.Map; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class DynamicSelection<T> implements ISelection<T> { + public DynamicSelection(T defaultValue) { + setValue(defaultValue); + } + abstract public boolean allowNull(); + T currentValue; + @Override + public T getValue() { + if (currentValue != null || !allowNull()) { + return currentValue; + } else { + Iterator<? extends T> i = getSelections().keySet().iterator(); + return i.next(); + } + } + + @Override + public void setValue(T value) {currentValue = value;} + + public void setValueByMatch(String search) { + Map<? extends T, String> selections = getSelections(); + String match = Utility.findBestMatch(search, selections.values()); + if (match != null) { + for (T key : selections.keySet()) { + if (selections.get(key).equals(match)) { + setValue(key); + return; + } + } + } else { + setValue(null); + } + } +} diff --git a/src/main/java/jace/config/FileComponent.java b/src/main/java/jace/config/FileComponent.java new file mode 100644 index 0000000..8aa56bc --- /dev/null +++ b/src/main/java/jace/config/FileComponent.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.config.Configuration.ConfigNode; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +/** + * This component provides a text field for the manual input of a file, as well + * as an associated 'Browse' button, allowing the user to browse for a file and + * have its location show up automatically in the text field. + * + * --borrowed and modified by Brendan Robert + * + * @author Eelke Spaak + * @see javax.swing.JFileChooser + */ +class FileComponent extends javax.swing.JPanel implements ActionListener, KeyListener { + ConfigNode node; + String fieldName; + + public void actionPerformed(ActionEvent e) { + textField.setBackground(Color.WHITE); + String value = textField.getText(); + if (value == null || value.equals("")) { + node.setFieldValue(fieldName, null); + } else { + File f = new File(value); + if (f.exists()) { + node.setFieldValue(fieldName, f); + } else { + textField.setBackground(Color.RED); + } + } + } + + public void synchronizeValue() { + try { + Object value = node.getFieldValue(fieldName); + if (value == null) { + setText(""); + } else { + setText(String.valueOf(value)); + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } + + private int TEXT_FIELD_WIDTH = 150; + + /** Creates new form JFileField */ + public FileComponent(ConfigNode node, String fieldName) { + this.node = node; + this.fieldName = fieldName; +// if (".".equals(type.value())) { +// fileSelectionMode = JFileChooser.DIRECTORIES_ONLY; +// } else { +// setFileTypeName(type.value()); +// setExtensionFilter(type.value()); +// } + initComponents(); + textField.addActionListener(this); + synchronizeValue(); + } + private String extensionFilter; + private String fileTypeName; + private int fileSelectionMode = JFileChooser.FILES_ONLY; + + /** This method is called from within the constructor to + * initialize the form. + */ + private void initComponents() { + textField = new javax.swing.JTextField(); + browseButton = new javax.swing.JButton(); + textField.setPreferredSize(new Dimension(150,20)); + textField.addKeyListener(this); + browseButton.setText("..."); + browseButton.setPreferredSize(new Dimension(25,20)); + browseButton.addActionListener(new java.awt.event.ActionListener() { + + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseButtonActionPerformed(evt); + } + }); + this.add(textField); + this.add(browseButton); + + FlowLayout layout = new FlowLayout(); + this.setLayout(layout); + this.validate(); + } + + private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) { + File currentDirectory = new File("."); + JFileChooser chooser = new JFileChooser(); + + chooser.setFileSelectionMode(fileSelectionMode); + + // Quick-n-dirty implementation of file extension filter since it's new in JDK 1.6 + if (extensionFilter != null && fileTypeName != null) { + FileFilter filter = new FileFilter() { + String[] extensions = extensionFilter.toLowerCase().split(","); + @Override + public boolean accept(File f) { + for (int i=0; i < extensions.length; i++) { + if (f.getPath().toLowerCase().endsWith(extensions[i])) + return true; + } + return false; + } + + @Override + public String getDescription() { + return fileTypeName; + } + }; + chooser.setFileFilter(filter); + } + + try { + File f = new File(textField.getText()); + if (f.exists()) { + if (f.isDirectory()) { + chooser.setCurrentDirectory(f); + } else { + chooser.setCurrentDirectory(f.getParentFile()); + chooser.setSelectedFile(f); + } + } else { + chooser.setCurrentDirectory(currentDirectory); + } + } catch (Exception ignore) { + } + + int returnVal = chooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + try { + File selectedFile = chooser.getSelectedFile(); + if (selectedFile.getCanonicalPath().startsWith(currentDirectory.getCanonicalPath())) { + String use = selectedFile.getCanonicalPath().substring(currentDirectory.getCanonicalPath().length() + 1); + textField.setText(use); + } else { + textField.setText(selectedFile.getPath()); + } + node.setFieldValue(fieldName, selectedFile); + + } catch (IOException ex) { + Logger.getLogger(FileComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + /** + * Returns the value of the text field. + * + * @return the value of the text field + */ + public String getText() { + return textField.getText(); + } + + /** + * Sets the value of the text field. + * + * @param the value to put in the text field + */ + public void setText(String text) { + textField.setText(text); + } + + /** + * Returns the extension filter (a comma-separated string of extensions) + * that the JFileChooser should use when browsing for a file. + * + * @return the extension filter + */ + public String getExtensionFilter() { + return extensionFilter; + } + + /** + * Sets the extension filter (a comma-separated string of extensions) + * that the JFileChooser should use when browsing for a file. + * + * @param extensionFilter the extension filter + */ + public void setExtensionFilter(String extensionFilter) { + this.extensionFilter = extensionFilter; + } + + /** + * Returns the description of the file types the JFileChooser should be + * browsing for. + * + * @return the file type description + */ + public String getFileTypeName() { + return fileTypeName; + } + + /** + * Sets the description of the file types the JFileChooser should be + * browsing for. + * + * @param fileTypeName the file type description + */ + public void setFileTypeName(String fileTypeName) { + this.fileTypeName = fileTypeName; + } + + /** + * Returns the file selection mode to be used by the JFileChooser. + * + * @return the type of files to be displayed + * @see javax.swing.JFileChooser#getFileSelectionMode() + */ + public int getFileSelectionMode() { + return fileSelectionMode; + } + + /** + * Sets the file selection mode to be used by the JFileChooser. + * + * @param fileSelectionMode the type of files to be displayed + * @see javax.swing.JFileChooser#setFileSelectionMode(int) + */ + public void setFileSelectionMode(int fileSelectionMode) { + this.fileSelectionMode = fileSelectionMode; + } + + /** + * Implemented to make layout managers align the JFileField on the baseline + * of the included text field, rather than on the absolute bottom of the + * JPanel. + * + * @param w + * @param h + * @return + */ +// @Override +// public int getBaseline(int w, int h) { +// return textField.getBaseline(w, h); +// } + // Variables declaration - do not modify + private javax.swing.JButton browseButton; + private javax.swing.JTextField textField; + + public void keyTyped(KeyEvent e) { + } + + public void keyPressed(KeyEvent e) { + } + + public void keyReleased(KeyEvent e) { + actionPerformed(null); + } + // End of variables declaration +} diff --git a/src/main/java/jace/config/ISelection.java b/src/main/java/jace/config/ISelection.java new file mode 100644 index 0000000..4c66b48 --- /dev/null +++ b/src/main/java/jace/config/ISelection.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import java.io.Serializable; +import java.util.LinkedHashMap; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public interface ISelection<T> extends Serializable { + + public LinkedHashMap<? extends T, String> getSelections(); + + public T getValue(); + + public void setValue(T value); + + public void setValueByMatch(String value); +} diff --git a/src/main/java/jace/config/IntegerComponent.java b/src/main/java/jace/config/IntegerComponent.java new file mode 100644 index 0000000..9e3d457 --- /dev/null +++ b/src/main/java/jace/config/IntegerComponent.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.config.Configuration.ConfigNode; +import java.awt.Color; +import java.awt.event.KeyEvent; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class IntegerComponent extends StringComponent { + + @Override + public void keyReleased(KeyEvent e) { + String t = getText(); + if (t == null || t.equals("")) { + try { + ConfigurableField f = node.subject.getClass().getField(fieldName).getAnnotation(ConfigurableField.class); + t = f.defaultValue(); + if (t == null || t.equals("")) { + t = "0"; + } +// setText(t); + } catch (NoSuchFieldException ex) { + Logger.getLogger(IntegerComponent.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(IntegerComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } + + try { + int i = Integer.parseInt(t); + node.setFieldValue(fieldName, i); + setBackground(Color.white); + } catch (NumberFormatException ex) { + setBackground(Color.red); + } + } + + + public IntegerComponent(ConfigNode node, String fieldName) { + super(node, fieldName); + setColumns(10); + } +} diff --git a/src/main/java/jace/config/InvokableAction.java b/src/main/java/jace/config/InvokableAction.java new file mode 100644 index 0000000..d95b4b3 --- /dev/null +++ b/src/main/java/jace/config/InvokableAction.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A invokable action annotation means that an object method can be called by the end-user. + * This serves as a hook for keybindings as well as semantic navigation potential. + * <br/> + * Name should be short, meaningful, and succinct. e.g. "Insert disk" + * <br/> + * Category can be used to group actions by overall topic, for example an automated table of contents + * <br/> + * Description is descriptive text which provides additional clarity, e.g. + * "This will present you with a file selection dialog to pick a floppy disk image. + * Currently, dos-ordered (DSK, DO), Prodos-ordered (PO), and Nibble (NIB) formats are supported. + * <br/> + * Alternatives should be delimited by semicolons) can provide more powerful search + * For "insert disk", alternatives might be "change disk;switch disk" and + * reboot might have alternatives as "warm start;cold start;boot;restart". + * <hr/> + * NOTE: Any method that implements this must be public and take no parameters! + * If a method signature is not correct, it will result in a runtime exception + * when the action is triggered. There is no way to offer a compiler + * warning to avoid this, unfortunately. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InvokableAction { + /* + * Should be short and meaningful name for action being invoked, e.g. "Insert disk" + */ + public String name(); + /* + * Can be used to group actions by overall topic, for example an automated table of contents + * To be determined... + */ + public String category() default "General"; + /* + * More descriptive text which provides additional clarity, e.g. + * "This will present you with a file selection dialog to pick a floppy disk image. + * Currently, dos-ordered (DSK, DO), Prodos-ordered (PO), and Nibble (NIB) formats are supported." + */ + public String description() default ""; + /* + * Alternatives should be delimited by semicolons) can provide more powerful search + * For "insert disk", alternatives might be "change disk;switch disk" and + * reboot might have alternatives as "warm start;cold start;boot;restart". + */ + public String alternatives() default ""; +} \ No newline at end of file diff --git a/src/main/java/jace/config/Name.java b/src/main/java/jace/config/Name.java new file mode 100644 index 0000000..69175e1 --- /dev/null +++ b/src/main/java/jace/config/Name.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Name { + public String value(); + public String description() default ""; +} diff --git a/src/main/java/jace/config/Reconfigurable.java b/src/main/java/jace/config/Reconfigurable.java new file mode 100644 index 0000000..dcbcfdd --- /dev/null +++ b/src/main/java/jace/config/Reconfigurable.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public interface Reconfigurable { + public String getName(); + public String getShortName(); + public void reconfigure(); +} diff --git a/src/main/java/jace/config/StringComponent.java b/src/main/java/jace/config/StringComponent.java new file mode 100644 index 0000000..2439a2e --- /dev/null +++ b/src/main/java/jace/config/StringComponent.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.config; + +import jace.config.Configuration.ConfigNode; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JTextField; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +class StringComponent extends JTextField implements KeyListener { + ConfigNode node; + String fieldName; + + public void keyTyped(KeyEvent e) { + } + + public void keyPressed(KeyEvent e) { + } + + public void keyReleased(KeyEvent e) { + node.setFieldValue(fieldName, getText()); + } + + public StringComponent(ConfigNode node, String fieldName) { + this.node = node; + this.fieldName = fieldName; + synchronizeValue(); + addKeyListener(this); + } + + public void synchronizeValue() { + try { + Object value = node.getFieldValue(fieldName); + if (value == null) { + setText(""); + } else { + setText(String.valueOf(value)); + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(StringComponent.class.getName()).log(Level.SEVERE, null, ex); + } + } +} diff --git a/src/main/java/jace/core/CPU.java b/src/main/java/jace/core/CPU.java new file mode 100644 index 0000000..577a063 --- /dev/null +++ b/src/main/java/jace/core/CPU.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.config.ConfigurableField; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * CPU is a vague abstraction of a CPU. It is defined as something which can be + * debugged or traced. It has a program counter which can be incremented or + * change. Most importantly, it is a device which does something on every clock tick. + * Subclasses should implement "executeOpcode" rather than override the tick method. + * Created on January 4, 2007, 7:27 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class CPU extends Device { + + /** + * Creates a new instance of CPU + */ + public CPU() { + } + + @Override + public String getShortName() { + return "cpu"; + } + private Debugger debugger = null; + @ConfigurableField(name = "Enable trace to STDOUT", shortName = "trace") + public boolean trace = false; + + public boolean isTraceEnabled() { + return trace; + } + + public void setTraceEnabled(boolean t) { + trace = t; + } + @ConfigurableField(name = "Trace length", shortName = "traceSize", description = "Number of most recent trace lines to keep for debugging errors. Zero == disabled") + public int traceLength = 0; + private ArrayList<String> traceLog = new ArrayList<>(); + + public boolean isLogEnabled() { + return (traceLength > 0); + } + + public void log(String line) { + if (!isLogEnabled()) { + return; + } + while (traceLog.size() >= traceLength) { + traceLog.remove(0); + } + traceLog.add(line); + } + + public void dumpTrace() { + Computer.pause(); + ArrayList<String> newLog = new ArrayList<>(); + ArrayList<String> log = traceLog; + traceLog = newLog; + Computer.resume(); + System.out.println("Most recent " + traceLength + " instructions:"); + log.stream().forEach((s) -> { + System.out.println(s); + }); + traceLog.clear(); + } + + public void setDebug(Debugger d) { + debugger = d; + suspend(); + } + + public void clearDebug() { + debugger = null; + resume(); + } + //@ConfigurableField(name="Program Counter") + public int programCounter = 0; + + public int getProgramCounter() { + return programCounter; + } + + public void setProgramCounter(int programCounter) { + this.programCounter = 0x00FFFF & programCounter; + } + + public void incrementProgramCounter(int amount) { + this.programCounter += amount; + this.programCounter = 0x00FFFF & this.programCounter; + } + + /** + * Process a single tick of the main processor clock. Either we're waiting + * to execute the next instruction, or the next instruction is ready to go + */ + @Override + public void tick() { + try { + if (debugger != null) { + if (!debugger.isActive() && debugger.hasBreakpoints()) { + debugger.getBreakpoints().stream().filter((i) -> (i == getProgramCounter())).forEach((_item) -> { + debugger.setActive(true); + }); + } + if (debugger.isActive()) { + debugger.updateStatus(); + if (!debugger.takeStep()) { + // If the debugger is active and we aren't ready for the next step, sleep and exit + // Without the sleep, this would constitute a very rapid-fire loop and would eat + // an unnecessary amount of CPU. + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + Logger.getLogger(CPU.class.getName()).log(Level.SEVERE, null, ex); + } + return; + } + } + } + } catch (Throwable t) { + // ignore + } + executeOpcode(); + } + /* + * Execute the current opcode at the current program counter + *@return number of cycles to wait until next command can be executed + */ + + protected abstract void executeOpcode(); + + public abstract void reset(); + + public abstract void generateInterrupt(); + + abstract public void pushPC(); + + @Override + public void attach() { + } + + @Override + public void detach() { + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/Card.java b/src/main/java/jace/core/Card.java new file mode 100644 index 0000000..17c8273 --- /dev/null +++ b/src/main/java/jace/core/Card.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.apple2e.SoftSwitches; + +/** + * Card is an abstraction of an Apple ][ hardware module which can carry its own + * ROM (both CX, a 256-byte ROM which loads into memory depending on what slot + * the card is in) and the C8 ROM is a 2K ROM loaded at $C800 when the card is + * active. + * + * This class mostly just stubs out common functionality used by many different + * cards and provides a consistent interface for more advanced features like VBL + * synchronization. + * Created on February 1, 2007, 5:35 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class Card extends Device { + + private PagedMemory cxRom; + private PagedMemory c8Rom; + private int slot; + private RAMListener ioListener; + private RAMListener firmwareListener; + private RAMListener c8firmwareListener; + + /** + * Creates a new instance of Card + */ + public Card() { + cxRom = new PagedMemory(0x0100, PagedMemory.Type.cardFirmware); + c8Rom = new PagedMemory(0x0800, PagedMemory.Type.cardFirmware); + } + + @Override + public String getShortName() { + return "s" + getSlot(); + } + + @Override + public String getName() { + return getDeviceName() + " (slot " + slot + ")"; + } + + abstract public void reset(); + + @Override + public void attach() { + ioListener = new RAMListener( + RAMEvent.TYPE.ANY, + RAMEvent.SCOPE.RANGE, + RAMEvent.VALUE.ANY) { + protected void doConfig() { + setScopeStart(slot * 16 + 0x00c080); + setScopeEnd(slot * 16 + 0x00c08F); + } + + protected void doEvent(RAMEvent e) { + int address = e.getAddress() & 0x0f; + handleIOAccess(address, e.getType(), e.getNewValue(), e); + } + }; + + firmwareListener = new RAMListener( + RAMEvent.TYPE.ANY, + RAMEvent.SCOPE.RANGE, + RAMEvent.VALUE.ANY) { + protected void doConfig() { + setScopeStart(slot * 256 + 0x00c000); + setScopeEnd(slot * 256 + 0x00c0ff); + } + + protected void doEvent(RAMEvent e) { + Computer.getComputer().getMemory().setActiveCard(slot); + if (SoftSwitches.CXROM.getState()) { + return; + } + handleFirmwareAccess(e.getAddress() & 0x0ff, e.getType(), e.getNewValue(), e); + } + }; + + c8firmwareListener = new RAMListener( + RAMEvent.TYPE.ANY, + RAMEvent.SCOPE.RANGE, + RAMEvent.VALUE.ANY) { + protected void doConfig() { + setScopeStart(slot * 256 + 0x00c800); + setScopeEnd(slot * 256 + 0x00cfff); + } + + protected void doEvent(RAMEvent e) { + if (SoftSwitches.CXROM.getState() + || Computer.getComputer().getMemory().getActiveSlot() != getSlot() + || SoftSwitches.INTC8ROM.getState()) { + return; + } + handleC8FirmwareAccess(e.getAddress() - 0x0c800, e.getType(), e.getNewValue(), e); + } + }; + + Computer.getComputer().getMemory().addListener(ioListener); + Computer.getComputer().getMemory().addListener(firmwareListener); + Computer.getComputer().getMemory().addListener(c8firmwareListener); + } + + @Override + public void detach() { + suspend(); + Computer.getComputer().getMemory().removeListener(ioListener); + Computer.getComputer().getMemory().removeListener(firmwareListener); + Computer.getComputer().getMemory().removeListener(c8firmwareListener); + } + + abstract protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e); + + abstract protected void handleFirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e); + + abstract protected void handleC8FirmwareAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e); + + public int getSlot() { + return slot; + } + + public void setSlot(int slot) { + this.slot = slot; + } + + public PagedMemory getCxRom() { + return cxRom; + } + + public PagedMemory getC8Rom() { + return c8Rom; + } + + @Override + public void reconfigure() { + boolean restart = suspend(); + detach(); + if (restart) { + resume(); + } + attach(); + } + + public void notifyVBLStateChanged(boolean state) { + // Do nothing unless overridden + } + + public boolean suspendWithCPU() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/Computer.java b/src/main/java/jace/core/Computer.java new file mode 100644 index 0000000..3fa411e --- /dev/null +++ b/src/main/java/jace/core/Computer.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.config.ConfigurableField; +import jace.config.InvokableAction; +import jace.config.Reconfigurable; +import jace.library.MediaLibrary; +import jace.state.StateManager; +import java.io.IOException; + +/** + * This is a very generic stub of a Computer and provides a generic set of + * overall functionality, namely boot, pause and resume features. What sort of + * memory, video and cpu get used are totally determined by fully-baked + * subclasses. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class Computer implements Reconfigurable { + + static private Computer theComputer; + public RAM memory; + public CPU cpu; + public Video video; + public Keyboard keyboard; + public StateManager stateManager; + public MediaLibrary mediaLibrary = MediaLibrary.getInstance(); + @ConfigurableField(category = "advanced", name = "State management", shortName = "rewind", description = "This enables rewind support, but consumes a lot of memory when active.") + public boolean enableStateManager; + + /** + * Creates a new instance of Computer + */ + public Computer() { + theComputer = this; + keyboard = new Keyboard(); + } + + public RAM getMemory() { + return memory; + } + + public void notifyVBLStateChanged(boolean state) { + for (Card c : getMemory().cards) { + if (c == null) { + continue; + } + c.notifyVBLStateChanged(state); + } + if (state && stateManager != null) { + stateManager.notifyVBLActive(); + } + } + + public void setMemory(RAM memory) { + if (this.memory != memory) { + if (this.memory != null) { + this.memory.detach(); + } + memory.attach(); + } + this.memory = memory; + } + + public void waitForNextCycle() { + //@TODO IMPLEMENT TIMER SLEEP CODE! + } + + public Video getVideo() { + return video; + } + + public void setVideo(Video video) { + this.video = video; + } + + public CPU getCpu() { + return cpu; + } + + public void setCpu(CPU cpu) { + this.cpu = cpu; + } + + public void loadRom(String path) throws IOException { + memory.loadRom(path); + } + + @InvokableAction( + name = "Cold boot", + description = "Process startup sequence from power-up", + category = "general", + alternatives = "Full reset;reset emulator") + public abstract void coldStart(); + + @InvokableAction( + name = "Warm boot", + description = "Process user-initatiated reboot (ctrl+apple+reset)", + category = "general", + alternatives = "reboot;reset;three-finger-salute") + public abstract void warmStart(); + + static public Computer getComputer() { + return theComputer; + } + + public Keyboard getKeyboard() { + return this.keyboard; + } + + protected abstract boolean isRunning(); + + protected abstract void doPause(); + + protected abstract void doResume(); + + @InvokableAction(name = "Pause", description = "Stops the computer, allowing reconfiguration of core elements", alternatives = "freeze;halt") + public static boolean pause() { + boolean result = false; + if (theComputer != null) { + result = theComputer.isRunning(); + theComputer.doPause(); + } + return result; + } + + @InvokableAction(name = "Resume", description = "Resumes the computer if it was previously paused", alternatives = "unpause;unfreeze;resume") + public static void resume() { + if (theComputer != null) { + theComputer.doResume(); + } + } + + public void reconfigure() { + if (enableStateManager) { + stateManager = StateManager.getInstance(); + } else { + stateManager = null; + StateManager.getInstance().invalidate(); + } + } +} diff --git a/src/main/java/jace/core/Debugger.java b/src/main/java/jace/core/Debugger.java new file mode 100644 index 0000000..a8fd5b6 --- /dev/null +++ b/src/main/java/jace/core/Debugger.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * A debugger has the ability to track a list of breakpoints and step a CPU one + * instruction at a time. This is a very generic abstraction used to describe + * the basic contract of what a debugger should do. EmulatorUILogic creates an + * anonymous subclass that hooks into the actual emulator. + * Created on April 16, 2007, 10:37 PM + * + * @author Administrator + */ +public abstract class Debugger { + + public abstract void updateStatus(); + private boolean active = false; + public boolean step = false; + + public void setActive(boolean state) { + active = state; + } + + public boolean isActive() { + return active; + } + private List<Integer> breakpoints = new ArrayList<Integer>(); + + public List<Integer> getBreakpoints() { + return breakpoints; + } + private boolean hasBreakpoints = false; + + boolean hasBreakpoints() { + return hasBreakpoints; + } + + public void updateBreakpoints() { + hasBreakpoints = false; + for (Integer i : breakpoints) { + if (i != null) { + hasBreakpoints = true; + } + } + } + + boolean takeStep() { + if (step) { + step = false; + return true; + } + return false; + } +} diff --git a/src/main/java/jace/core/Device.java b/src/main/java/jace/core/Device.java new file mode 100644 index 0000000..120d381 --- /dev/null +++ b/src/main/java/jace/core/Device.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.state.Stateful; +import jace.config.Reconfigurable; + +/** + * Device is a very simple abstraction of any emulation component. A device + * performs some sort of action on every clock tick, unless it is waiting for a + * number of cycles to elapse (waitCycles > 0). A device might also be paused or + * suspended. + * + * Depending on the type of device, some special work might be required to + * attach or detach it to the active emulation (such as what should happen when + * a card is inserted or removed from a slot?) + * Created on May 10, 2007, 5:46 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public abstract class Device implements Reconfigurable { + // Number of cycles to do nothing (for cpu/video cycle accuracy) + + @Stateful + private int waitCycles = 0; + @Stateful + private boolean run = true; + @Stateful + public boolean isPaused = false; + + public void addWaitCycles(int wait) { + waitCycles += wait; + } + + public void setWaitCycles(int wait) { + waitCycles = wait; + } + + public void doTick() { + /* + if (waitCycles <= 0) + tick(); + else + waitCycles--; + */ + + if (!run) { +// System.out.println("Device stopped: " + getName()); + isPaused = true; + return; + } + // The following might be as much as 7% faster than the above + // My guess is that the above results in a GOTO + // whereas the following pre-emptive return avoids that + if (waitCycles > 0) { + waitCycles--; + return; + } + // Implicit else... + tick(); + } + + public boolean isRunning() { + return run; + } + + public synchronized void setRun(boolean run) { +// System.out.println(Thread.currentThread().getName() + (run ? " resuming " : " suspending ")+ getDeviceName()); + isPaused = false; + this.run = run; + } + + protected abstract String getDeviceName(); + + @Override + public String getName() { + return getDeviceName(); + } + + public abstract void tick(); + + public boolean suspend() { + if (isRunning()) { + setRun(false); + return true; + } + return false; + } + + public void resume() { + setRun(true); + } + + public abstract void attach(); + + public abstract void detach(); +} diff --git a/src/main/java/jace/core/Font.java b/src/main/java/jace/core/Font.java new file mode 100644 index 0000000..67d57e5 --- /dev/null +++ b/src/main/java/jace/core/Font.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import javax.imageio.ImageIO; + +/** + * Represents the Apple ][ character font used in text modes. + * Created on January 16, 2007, 8:16 PM + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Font { + static public int[][] font; + static public int getByte(int c, int yOffset) { + return font[c][yOffset]; + } + static { + font = new int[256][8]; + try { + InputStream in = ClassLoader.getSystemResourceAsStream("jace/data/font.gif"); + BufferedImage fontImage = ImageIO.read(in); + for (int i=0; i < 256; i++) { + int x = (i >> 4)*13 + 2; + int y = (i & 15)*13 + 4; + for (int j=0; j < 8; j++) { + int row = 0; + for (int k=0; k < 7; k++) { + int color = fontImage.getRGB((7-k)+x, j+y); + row = (row<<1) | (1-(color&1)); +// row = (row<<1) | (color&1); + } + font[i][j]=row; + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + + /** Creates a new instance of Font */ + private Font() { + } + +} diff --git a/src/main/java/jace/core/KeyHandler.java b/src/main/java/jace/core/KeyHandler.java new file mode 100644 index 0000000..7b8c6a1 --- /dev/null +++ b/src/main/java/jace/core/KeyHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import java.awt.event.KeyEvent; + +/** + * Listen for a specific key or set of keys + * If there is a match, the handleKeyUp or handleKeyDown methods will be called. + * This is meant to save a lot of extra conditional logic elsewhere. + * + * The handler methods should return true if they have consumed the key event and do + * not want any other processing to continue for that keypress. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class KeyHandler { + public int key = 0; + public int modifiers = 0; + public KeyHandler(int key, int... modifiers) { + this.key = key; + this.modifiers = 0; + for (int m : modifiers) { + this.modifiers |= m; + } + } + + public abstract boolean handleKeyUp(KeyEvent e); + public abstract boolean handleKeyDown(KeyEvent e); +} diff --git a/src/main/java/jace/core/Keyboard.java b/src/main/java/jace/core/Keyboard.java new file mode 100644 index 0000000..61f5e64 --- /dev/null +++ b/src/main/java/jace/core/Keyboard.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.EmulatorUILogic; +import jace.apple2e.SoftSwitches; +import jace.apple2e.Speaker; +import jace.apple2e.softswitch.KeyboardSoftSwitch; +import jace.config.Reconfigurable; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Keyboard manages all keyboard-related activities. For now, all hotkeys are + * hard-coded. The eventual direction for this class is to only manage key + * handlers for all keys and provide remapping -- but it's not there yet. + * Created on March 29, 2007, 11:32 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Keyboard implements Reconfigurable { + + @Override + public String getShortName() { + return "kbd"; + } + static byte currentKey = 0; + + public static void clearStrobe() { + currentKey = (byte) (currentKey & 0x07f); + } + + public static void pressKey(byte key) { + currentKey = (byte) (0x0ff & (0x080 | key)); + } + + public static byte readState() { + // If strobe was cleared... + if ((currentKey & 0x080) == 0) { + // Call clipboard buffer paste routine + int newKey = Keyboard.getClipboardKeystroke(); + if (newKey >= 0) { + pressKey((byte) newKey); + } + } + return currentKey; + } + + /** + * Creates a new instance of Keyboard + */ + public Keyboard() { + } + private static Map<Integer, Set<KeyHandler>> keyHandlersByKey = new HashMap<Integer, Set<KeyHandler>>(); + private static Map<Object, Set<KeyHandler>> keyHandlersByOwner = new HashMap<Object, Set<KeyHandler>>(); + + public static void registerKeyHandler(KeyHandler l, Object owner) { + if (!keyHandlersByKey.containsKey(l.key)) { + keyHandlersByKey.put(l.key, new HashSet<KeyHandler>()); + } + keyHandlersByKey.get(l.key).add(l); + if (!keyHandlersByOwner.containsKey(owner)) { + keyHandlersByOwner.put(owner, new HashSet<KeyHandler>()); + } + keyHandlersByOwner.get(owner).add(l); + } + + public static void unregisterAllHandlers(Object owner) { + if (!keyHandlersByOwner.containsKey(owner)) { + return; + } + for (KeyHandler handler : keyHandlersByOwner.get(owner)) { + if (!keyHandlersByKey.containsKey(handler.key)) { + continue; + } + keyHandlersByKey.get(handler.key).remove(handler); + } + } + + public static void processKeyDownEvents(KeyEvent e) { + if (keyHandlersByKey.containsKey(e.getKeyCode())) { + for (KeyHandler h : keyHandlersByKey.get(e.getKeyCode())) { + if (h.modifiers != e.getModifiers() && h.modifiers != -1) { + continue; + } + boolean isHandled = h.handleKeyDown(e); + if (isHandled) { + e.consume(); + return; + } + } + } + } + + public static void processKeyUpEvents(KeyEvent e) { + if (keyHandlersByKey.containsKey(e.getKeyCode())) { + for (KeyHandler h : keyHandlersByKey.get(e.getKeyCode())) { + if (h.modifiers != e.getModifiers() && h.modifiers != -1) { + continue; + } + boolean isHandled = h.handleKeyUp(e); + if (isHandled) { + e.consume(); + return; + } + } + } + } + + public KeyListener getListener() { + return new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + processKeyDownEvents(e); + if (e.getKeyCode() == 0 || e.isConsumed()) { + return; + } + + KeyboardSoftSwitch key = + (KeyboardSoftSwitch) SoftSwitches.KEYBOARD.getSwitch(); + char c = e.getKeyChar(); + if ((e.getModifiers() & (KeyEvent.ALT_MASK|KeyEvent.META_MASK|KeyEvent.META_DOWN_MASK)) > 0) { + // explicit left and right here because other locations + // can be sent as well, e.g. KEY_LOCATION_STANDARD + if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT) { + pressOpenApple(); + } else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT) { + pressSolidApple(); + } + } + + int code = e.getKeyCode(); + + switch (code) { + case KeyEvent.VK_LEFT: + case KeyEvent.VK_KP_LEFT: + c = 8; + break; + case KeyEvent.VK_RIGHT: + case KeyEvent.VK_KP_RIGHT: + c = 21; + break; + case KeyEvent.VK_UP: + case KeyEvent.VK_KP_UP: + c = 11; + break; + case KeyEvent.VK_DOWN: + case KeyEvent.VK_KP_DOWN: + c = 10; + break; + case KeyEvent.VK_TAB: + c = 9; + break; + case KeyEvent.VK_ENTER: + c = 13; + break; + case KeyEvent.VK_BACK_SPACE: + c = 127; + break; + default: + if ((e.getModifiers() & KeyEvent.CTRL_DOWN_MASK) > 0) { + c = (char) (code - 'A' + 1); + } + } + + if (c < 128) { + pressKey((byte) c); + } + +// e.consume(); + } + + @Override + public void keyReleased(KeyEvent e) { + int code = e.getKeyCode(); + processKeyUpEvents(e); + if (code == 0 || e.isConsumed()) { + return; + } + if (code == KeyEvent.VK_INSERT && e.isShiftDown()) { + doPaste(); + } + if (code == KeyEvent.VK_F10) { + EmulatorUILogic.toggleDebugPanel(); + } + if ((code == KeyEvent.VK_F12 || code == KeyEvent.VK_PAGE_UP || code == KeyEvent.VK_BACK_SPACE || code == KeyEvent.VK_PAUSE) && ((e.getModifiers() & KeyEvent.CTRL_MASK) > 0)) { + Computer.getComputer().warmStart(); + } + if (code == KeyEvent.VK_F1) { + EmulatorUILogic.showMediaManager(); + } + if (code == KeyEvent.VK_F4) { + EmulatorUILogic.showConfig(); + } + if (code == KeyEvent.VK_F7) { + Speaker.toggleFileOutput(); + } + if (code == KeyEvent.VK_F8) { + EmulatorUILogic.scaleIntegerRatio(); + } + if (code == KeyEvent.VK_F9) { + EmulatorUILogic.toggleFullscreen(); + } + if (code == KeyEvent.VK_PRINTSCREEN || code == KeyEvent.VK_SCROLL_LOCK) { + try { + if (e.isShiftDown()) { + EmulatorUILogic.saveScreenshotRaw(); + } else { + EmulatorUILogic.saveScreenshot(); + } + } catch (IOException ex) { + Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex); + } + Computer.resume(); + } + if ((e.getModifiers() & (KeyEvent.ALT_MASK|KeyEvent.META_MASK|KeyEvent.META_DOWN_MASK)) > 0) { + // explicit left and right here because other locations + // can be sent as well, e.g. KEY_LOCATION_STANDARD + if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_LEFT) { + releaseOpenApple(); + } else if (e.getKeyLocation() == KeyEvent.KEY_LOCATION_RIGHT) { + releaseSolidApple(); + } + } + + e.consume(); +// e.setKeyChar((char) 0); +// e.setKeyCode(0); + } + + private void pressOpenApple() { + Computer.pause(); + SoftSwitches.PB0.getSwitch().setState(true); + Computer.resume(); + } + + private void pressSolidApple() { + Computer.pause(); + SoftSwitches.PB1.getSwitch().setState(true); + Computer.resume(); + } + + private void releaseOpenApple() { + Computer.pause(); + SoftSwitches.PB0.getSwitch().setState(false); + Computer.resume(); + } + + private void releaseSolidApple() { + Computer.pause(); + SoftSwitches.PB1.getSwitch().setState(false); + Computer.resume(); + } + }; + } + + public static void doPaste(String text) { + pasteBuffer = new StringReader(text); + } + + private static void doPaste() { + try { + Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard(); + String contents = (String) clip.getData(DataFlavor.stringFlavor); + if (contents != null && !"".equals(contents)) { + contents = contents.replaceAll("\\n(\\r)?", (char) 0x0d + ""); + pasteBuffer = new StringReader(contents); + } + } catch (UnsupportedFlavorException ex) { + Logger.getLogger(Keyboard.class + .getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(Keyboard.class + .getName()).log(Level.SEVERE, null, ex); + } + + } + static StringReader pasteBuffer = null; + + public static int getClipboardKeystroke() { + if (pasteBuffer == null) { + return -1; + } + + try { + int keypress = pasteBuffer.read(); + // Handle end of paste buffer + if (keypress == -1) { + pasteBuffer.close(); + pasteBuffer = null; + return -1; + } + + KeyboardSoftSwitch key = + (KeyboardSoftSwitch) SoftSwitches.KEYBOARD.getSwitch(); + return (keypress & 0x0ff); + + + } catch (IOException ex) { + Logger.getLogger(Keyboard.class + .getName()).log(Level.SEVERE, null, ex); + } + return -1; + } + + @Override + public String getName() { + return "Keyboard"; + } + + @Override + public void reconfigure() { + } +} diff --git a/src/main/java/jace/core/Motherboard.java b/src/main/java/jace/core/Motherboard.java new file mode 100644 index 0000000..fcfa13e --- /dev/null +++ b/src/main/java/jace/core/Motherboard.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.apple2e.SoftSwitches; +import jace.apple2e.Speaker; +import jace.config.ConfigurableField; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Motherboard is the heart of the computer. It can have a list of cards + * inserted (the behavior and number of cards is determined by the Memory class) + * as well as a speaker and any other miscellaneous devices (e.g. joysticks). + * This class provides the real main loop of the emulator, and is responsible + * for all timing as well as the pause/resume features used to prevent resource + * collisions between threads. Created on May 1, 2007, 11:22 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Motherboard extends TimedDevice { + + static final Computer computer = Computer.getComputer(); + static final CPU cpu = computer.getCpu(); + static Motherboard instance; + static final public Set<Device> miscDevices = new HashSet<Device>(); + @ConfigurableField(name = "Enable Speaker", shortName = "speaker", defaultValue = "true") + public static boolean enableSpeaker = true; + public static Speaker speaker; + public static SoundMixer mixer = new SoundMixer(); + + static void vblankEnd() { + SoftSwitches.VBL.getSwitch().setState(true); + computer.notifyVBLStateChanged(true); + } + + static void vblankStart() { + SoftSwitches.VBL.getSwitch().setState(false); + computer.notifyVBLStateChanged(false); + } + + /** + * Creates a new instance of Motherboard + */ + public Motherboard() { + instance = this; + } + + protected String getDeviceName() { + return "Motherboard"; + } + + @Override + public String getShortName() { + return "mb"; + } + @ConfigurableField(category = "advanced", name = "CPU per clock", defaultValue = "1", description = "Number of CPU cycles per clock cycle (normal = 1)") + public static int cpuPerClock = 1; + public int clockCounter = 1; + public Card[] cards; + + public void tick() { + try { + clockCounter--; + cpu.doTick(); + if (clockCounter > 0) { + return; + } + clockCounter = cpuPerClock; + Computer.getComputer().getVideo().doTick(); + // Unrolled loop since this happens so often + if (cards[0] != null) { + cards[0].doTick(); + } + if (cards[1] != null) { + cards[1].doTick(); + } + if (cards[2] != null) { + cards[2].doTick(); + } + if (cards[3] != null) { + cards[3].doTick(); + } + if (cards[4] != null) { + cards[4].doTick(); + } + if (cards[5] != null) { + cards[5].doTick(); + } + if (cards[6] != null) { + cards[6].doTick(); + } + for (Device m : miscDevices) { + m.doTick(); + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + // From the holy word of Sather 3:5 (Table 3.1) :-) + // This average speed averages in the "long" cycles + public static long SPEED = 1020484L; // (NTSC) + //public static long SPEED = 1015625L; // (PAL) + + public long defaultCyclesPerSecond() { + return SPEED; + } + + public synchronized void reconfigure() { + boolean startAgain = pause(); + accelorationRequestors.clear(); + super.reconfigure(); + Card[] cards = computer.getMemory().getAllCards(); + // Now create devices as needed, e.g. sound + Motherboard.miscDevices.add(mixer); + mixer.reconfigure(); + + if (enableSpeaker) { + try { + if (speaker == null) { + speaker = new Speaker(); + } else { + speaker.attach(); + } + if (mixer.lineAvailable) { + Motherboard.miscDevices.add(speaker); + } + } catch (Throwable t) { + System.out.println("Unable to initalize sound -- deactivating speaker out"); + speaker.detach(); + Motherboard.miscDevices.remove(speaker); + } + } else { + if (speaker != null) { + speaker.detach(); + Motherboard.miscDevices.remove(speaker); + } + } + if (startAgain && Computer.getComputer().getMemory() != null) { + resume(); + } + } + static HashSet<Object> accelorationRequestors = new HashSet<Object>(); + + static public void requestSpeed(Object requester) { + accelorationRequestors.add(requester); + if (instance != null) { + instance.enableTempMaxSpeed(); + } + } + + static public void cancelSpeedRequest(Object requester) { + accelorationRequestors.remove(requester); + if (instance != null && accelorationRequestors.isEmpty()) { + instance.disableTempMaxSpeed(); + } + } + + @Override + public void attach() { + } + Map<Card, Boolean> resume = new HashMap<Card, Boolean>(); + + @Override + public boolean suspend() { + synchronized (resume) { + resume.clear(); + for (Card c : cards) { + if (c == null || !c.suspendWithCPU() || !c.isRunning()) { + continue; + } + resume.put(c, c.suspend()); + } + } + return super.suspend(); + } + + @Override + public void resume() { + cards = computer.getMemory().getAllCards(); + super.resume(); + synchronized (resume) { + for (Card c : cards) { + if (Boolean.TRUE.equals(resume.get(c))) { + c.resume(); + } + } + } + } + + @Override + public void detach() { + for (Device d : miscDevices) { + d.suspend(); + } + miscDevices.clear(); +// halt(); + } +} diff --git a/src/main/java/jace/core/PagedMemory.java b/src/main/java/jace/core/PagedMemory.java new file mode 100644 index 0000000..9fb55bc --- /dev/null +++ b/src/main/java/jace/core/PagedMemory.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.state.StateManager; +import jace.state.Stateful; +import java.util.Arrays; + +/** + * This represents bank-switchable ram which can reside at fixed portions of the + * computer's memory. This makes it possible to switch out memory pages in a + * very efficient manner so that the MMU abstraction doesn't bury the rest of + * the emulator in messy conditionals. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class PagedMemory { + + public enum Type { + + cardFirmware(0x0c800), + languageCard(0x0d000), + firmwareMain(0x0d000), + firmware80column(0x0c300), + slotRom(0x0c100), + ram(0x0000); + protected int baseAddress; + + private Type(int newBase) { + baseAddress = newBase; + } + + public int getBaseAddress() { + return baseAddress; + } + } + // This is a fixed array, used for internal-only!! + @Stateful + public byte[][] internalMemory = new byte[0][]; + @Stateful + public Type type; + + /** + * Creates a new instance of PagedMemory + */ + public PagedMemory(int size, Type memType) { + type = memType; + internalMemory = new byte[size >> 8][256]; + for (int i = 0; i < size; i += 256) { + byte[] b = new byte[256]; + Arrays.fill(b, (byte) 0x00); + internalMemory[i >> 8] = b; + } + } + + public PagedMemory(byte[] romData, Type memType) { + type = memType; + loadData(romData); + } + + public void loadData(byte[] romData) { + for (int i = 0; i < romData.length; i += 256) { + byte[] b = new byte[256]; + for (int j = 0; j < 256; j++) { + b[j] = romData[i + j]; + } + internalMemory[i >> 8] = b; + } + } + + public void loadData(byte[] romData, int offset, int length) { + for (int i = 0; i < length; i += 256) { + byte[] b = new byte[256]; + for (int j = 0; j < 256; j++) { + b[j] = romData[offset + i + j]; + } + internalMemory[i >> 8] = b; + } + } + + public byte[][] getMemory() { + return internalMemory; + } + + public byte[] get(int pageNumber) { + return internalMemory[pageNumber]; + } + + public void set(int pageNumber, byte[] bank) { + internalMemory[pageNumber] = bank; + } + + public byte[] getMemoryPage(int memoryBase) { + int offset = memoryBase - type.baseAddress; +// int page = offset >> 8; + int page = (offset >> 8) & 0x0ff; +// return get(page); + return internalMemory[page]; + } + + public void setBanks(int sourceStart, int sourceLength, int targetStart, PagedMemory source) { + for (int i = 0; i < sourceLength; i++) { + set(targetStart + i, source.get(sourceStart + i)); + } + } + + public byte readByte(int address) { + return getMemoryPage(address)[address & 0x0ff]; + } + + public void writeByte(int address, byte value) { + byte[] page = getMemoryPage(address); + StateManager.markDirtyValue(page); + getMemoryPage(address)[address & 0x0ff] = value; + } + + public void fillBanks(PagedMemory source) { + byte[][] sourceMemory = source.getMemory(); + int sourceBase = source.type.getBaseAddress() >> 8; + int thisBase = type.getBaseAddress() >> 8; + int start = sourceBase > thisBase ? sourceBase : thisBase; + int sourceEnd = sourceBase + source.getMemory().length; + int thisEnd = thisBase + getMemory().length; + int end = sourceEnd < thisEnd ? sourceEnd : thisEnd; + for (int i = start; i < end; i++) { + set(i - thisBase, sourceMemory[i - sourceBase]); + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/Palette.java b/src/main/java/jace/core/Palette.java new file mode 100644 index 0000000..43f820f --- /dev/null +++ b/src/main/java/jace/core/Palette.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import java.awt.Color; + +/** + * Fixed color palette -- only used for the older DHGR renderer (the new NTSC renderer uses its own YUV conversion and builds its own palettes) + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Palette { + private Palette() {} + + static public final int BLACK = 0; + static public final int VIOLET = 3; + static public final int BLUE = 6; + static public final int ORANGE = 9; + static public final int GREEN = 12; + static public final int WHITE = 15; + + static public Color[] color; + static { + color = new Color[16]; + color[ 0] = new Color( 0, 0, 0); + color[ 1] = new Color(208, 0, 48); + color[ 2] = new Color( 0, 0,128); + color[ 3] = new Color(255, 0,255); + color[ 4] = new Color( 0,128, 0); + color[ 5] = new Color(128,128,128); + color[ 6] = new Color( 0, 0,255); + color[ 7] = new Color( 96,160,255); + color[ 8] = new Color(128, 80, 0); + color[ 9] = new Color(255,128, 0); + color[10] = new Color(192,192,192); + color[11] = new Color(255,144,128); + color[12] = new Color( 0,255, 0); + color[13] = new Color(255,255, 0); + color[14] = new Color( 64,255,144); + color[15] = new Color(255,255,255); + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/RAM.java b/src/main/java/jace/core/RAM.java new file mode 100644 index 0000000..8bbf364 --- /dev/null +++ b/src/main/java/jace/core/RAM.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.apple2e.SoftSwitches; +import jace.config.Reconfigurable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +/** + * RAM is a 64K address space of paged memory. It also manages sets of memory + * listeners, used by I/O as well as emulator add-ons (and cheats). RAM also + * manages cards in the emulator because they are tied into the MMU memory + * bankswitch logic. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class RAM implements Reconfigurable { + public PagedMemory activeRead; + public PagedMemory activeWrite; + public List<RAMListener> listeners; + public List<RAMListener>[] listenerMap; + public List<RAMListener>[] ioListenerMap; + protected Card[] cards; + // card 0 = 80 column card firmware / system rom + public int activeSlot = 0; + + /** + * Creates a new instance of RAM + */ + public RAM() { + listeners = new Vector<RAMListener>(); + cards = new Card[8]; + refreshListenerMap(); + } + + public void setActiveCard(int slot) { + if (activeSlot != slot) { + activeSlot = slot; + configureActiveMemory(); + } else if (!SoftSwitches.CXROM.getState()) { + configureActiveMemory(); + } + } + + public int getActiveSlot() { + return activeSlot; + } + + public Card[] getAllCards() { + return cards; + } + + public Card getCard(int slot) { + if (slot >= 1 && slot <= 7) { + return cards[slot]; + } + return null; + } + + public void addCard(Card c, int slot) { + cards[slot] = c; + c.setSlot(slot); + c.attach(); + } + + public void removeCard(Card c) { + c.suspend(); + c.detach(); + removeCard(c.getSlot()); + } + + public void removeCard(int slot) { + if (cards[slot] != null) { + cards[slot].suspend(); + cards[slot].detach(); + } + cards[slot] = null; + } + + abstract public void configureActiveMemory(); + + public byte write(int address, byte b, boolean generateEvent, boolean requireSynchronization) { + byte[] page = activeWrite.getMemoryPage(address); + byte old = 0; + if (page == null) { + if (generateEvent) { + callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization); + } + } else { + int offset = address & 0x0FF; + old = page[offset]; + if (generateEvent) { + b = callListener(RAMEvent.TYPE.WRITE, address, old, b, requireSynchronization); + } + page[offset] = b; + } + return old; + } + + public void writeWord(int address, int w, boolean generateEvent, boolean requireSynchronization) { + int lsb = write(address, (byte) (w & 0x0ff), generateEvent, requireSynchronization); + int msb = write(address + 1, (byte) (w >> 8), generateEvent, requireSynchronization); +// int oldValue = msb << 8 + lsb; + } + + public byte readRaw(int address) { + // if (address >= 65536) return 0; + return activeRead.getMemoryPage(address)[address & 0x0FF]; + } + + public byte read(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSyncronization) { + // if (address >= 65536) return 0; + byte value = activeRead.getMemoryPage(address)[address & 0x0FF]; +// if (triggerEvent || ((address & 0x0FF00) == 0x0C000)) { + if (triggerEvent || (address & 0x0FFF0) == 0x0c030) { + value = callListener(eventType, address, value, value, requireSyncronization); + } + return value; + } + + public int readWordRaw(int address) { + int lsb = 0x00ff & readRaw(address); + int msb = (0x00ff & readRaw(address + 1)) << 8; + return msb + lsb; + } + + public int readWord(int address, RAMEvent.TYPE eventType, boolean triggerEvent, boolean requireSynchronization) { + int lsb = 0x00ff & read(address, eventType, triggerEvent, requireSynchronization); + int msb = (0x00ff & read(address + 1, eventType, triggerEvent, requireSynchronization)) << 8; + int value = msb + lsb; +// if (generateEvent) { +// callListener(RAMEvent.TYPE.READ, address, value, value); +// } + return value; + } + + private void mapListener(RAMListener l, int address) { + if ((address & 0x0FF00) == 0x0C000) { + int index = address & 0x0FF; + List<RAMListener> ioListeners = ioListenerMap[index]; + if (ioListeners == null) { + ioListeners = new ArrayList<>(); + ioListenerMap[index] = ioListeners; + } + if (!ioListeners.contains(l)) { + ioListeners.add(l); + } + } else { + int index = address >> 8; + List<RAMListener> otherListeners = listenerMap[index]; + if (otherListeners == null) { + otherListeners = new ArrayList<>(); + listenerMap[index] = otherListeners; + } + if (!otherListeners.contains(l)) { + otherListeners.add(l); + } + } + } + + private void addListenerRange(RAMListener l) { + if (l.getScope() == RAMEvent.SCOPE.ADDRESS) { + mapListener(l, l.getScopeStart()); + } else { + int start = 0; + int end = 0x0ffff; + if (l.getScope() == RAMEvent.SCOPE.RANGE) { + start = l.getScopeStart(); + end = l.getScopeEnd(); + } + for (int i = start; i <= end; i++) { + mapListener(l, i); + } + } + } + + private void refreshListenerMap() { + listenerMap = new ArrayList[256]; + ioListenerMap = new ArrayList[256]; + for (RAMListener l : listeners) { + addListenerRange(l); + } + } + + public void addListener(final RAMListener l) { + boolean restart = Computer.pause(); + if (listeners.contains(l)) { + return; + } + listeners.add(l); + addListenerRange(l); + if (restart) { + Computer.resume(); + } + } + + public void removeListener(final RAMListener l) { + boolean restart = Computer.pause(); + listeners.remove(l); + refreshListenerMap(); + if (restart) { + Computer.resume(); + } + } + + public byte callListener(RAMEvent.TYPE t, int address, int oldValue, int newValue, boolean requireSyncronization) { + List<RAMListener> activeListeners = null; + if (requireSyncronization) { + Computer.getComputer().getCpu().suspend(); + } + if ((address & 0x0FF00) == 0x0C000) { + activeListeners = ioListenerMap[address & 0x0FF]; + if (activeListeners == null && t.isRead()) { + if (requireSyncronization) { + Computer.getComputer().getCpu().resume(); + } + return Computer.getComputer().getVideo().getFloatingBus(); + } + } else { + activeListeners = listenerMap[(address >> 8) & 0x0ff]; + } + if (activeListeners != null) { + RAMEvent e = new RAMEvent(t, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, address, oldValue, newValue); + for (RAMListener l : activeListeners) { + l.handleEvent(e); + } + if (requireSyncronization) { + Computer.getComputer().getCpu().resume(); + } + return (byte) e.getNewValue(); + } + if (requireSyncronization) { + Computer.getComputer().getCpu().resume(); + } + return (byte) newValue; + } + + abstract protected void loadRom(String path) throws IOException; + + public void dump() { + for (int i = 0; i < 0x0FFFF; i += 16) { + System.out.print(Integer.toString(i, 16)); + System.out.print(":"); + String part1 = ""; + String part2 = ""; + for (int j = 0; j < 16; j++) { + int a = i + j; + int br = 0x0FF & activeRead.getMemory()[i >> 8][i & 0x0ff]; + String s1 = Integer.toString(br, 16); + System.out.print(' '); + if (s1.length() == 1) { + System.out.print('0'); + } + System.out.print(s1); + + /* + try { + int bw = 0; + bw = 0x0FF & activeWrite.getMemory().get(a/256)[a%256]; + String s2 = (br == bw) ? "**" : Integer.toString(bw,16); + System.out.print(' '); + if (s2.length()==1) System.out.print('0'); + System.out.print(s2); + } catch (NullPointerException ex) { + System.out.print(" --"); + } + */ + } + System.out.println(); +// System.out.println(Integer.toString(i, 16)+":"+part1+" -> "+part2); + } + } + + abstract public void attach(); + abstract public void detach(); +} \ No newline at end of file diff --git a/src/main/java/jace/core/RAMEvent.java b/src/main/java/jace/core/RAMEvent.java new file mode 100644 index 0000000..3e44e2c --- /dev/null +++ b/src/main/java/jace/core/RAMEvent.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +/** + * A RAM event is defined as anything that causes a read or write to the + * mainboard RAM of the computer. This could be the result of an indirect + * address fetch (indirect addressing) as well as direct or indexed operator + * addressing modes. + * + * It is also possible to track if the read is an opcode read, indicating that + * the CPU is executing the given memory location at that moment. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class RAMEvent { + + public enum TYPE { + + READ(true), + READ_DATA(true), + EXECUTE(true), + READ_OPERAND(true), + WRITE(false), + ANY(false); + boolean read = false; + + TYPE(boolean r) { + this.read = r; + } + + public boolean isRead() { + return read; + } + }; + + public enum SCOPE { + + ADDRESS, + RANGE, + ANY + }; + + public enum VALUE { + + ANY, + RANGE, + EQUALS, + NOT_EQUALS, + CHANGE_BY + }; + private TYPE type; + private SCOPE scope; + private VALUE value; + private int address, oldValue, newValue; + + /** + * Creates a new instance of RAMEvent + */ + public RAMEvent(TYPE t, SCOPE s, VALUE v, int address, int oldValue, int newValue) { + setType(t); + setScope(s); + setValue(v); + this.setAddress(address); + this.setOldValue(oldValue); + this.setNewValue(newValue); + } + + public TYPE getType() { + return type; + } + + public final void setType(TYPE type) { + this.type = type; + } + + public SCOPE getScope() { + return scope; + } + + public final void setScope(SCOPE scope) { + this.scope = scope; + } + + public VALUE getValue() { + return value; + } + + public final void setValue(VALUE value) { + this.value = value; + } + + public int getAddress() { + return address; + } + + public final void setAddress(int address) { + this.address = address; + } + + public int getOldValue() { + return oldValue; + } + + public final void setOldValue(int oldValue) { + this.oldValue = oldValue; + } + + public int getNewValue() { + return newValue; + } + + public final void setNewValue(int newValue) { + this.newValue = newValue; + } +} diff --git a/src/main/java/jace/core/RAMListener.java b/src/main/java/jace/core/RAMListener.java new file mode 100644 index 0000000..e4eeec6 --- /dev/null +++ b/src/main/java/jace/core/RAMListener.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.core.RAMEvent.TYPE; + +/** + * A Ram Listener waits for a specific ram event, as specified by access type + * (read/write/execute, etc) or a memory address, or range of addresses. The + * subclass must define the address range (scope start/end) via the doConfig + * method. Ram listeners are used all over the emulator, but especially in cheat + * modules and the softswitch and I/O cards. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class RAMListener { + + private RAMEvent.TYPE type; + private RAMEvent.SCOPE scope; + private RAMEvent.VALUE value; + private int scopeStart; + private int scopeEnd; + private int valueStart; + private int valueEnd; + private int valueAmount; + + /** + * Creates a new instance of RAMListener + */ + public RAMListener(RAMEvent.TYPE t, RAMEvent.SCOPE s, RAMEvent.VALUE v) { + setType(t); + setScope(s); + setValue(v); + doConfig(); + } + + public RAMEvent.TYPE getType() { + return type; + } + + public final void setType(RAMEvent.TYPE type) { + this.type = type; + } + + public RAMEvent.SCOPE getScope() { + return scope; + } + + public final void setScope(RAMEvent.SCOPE scope) { + this.scope = scope; + } + + public RAMEvent.VALUE getValue() { + return value; + } + + public final void setValue(RAMEvent.VALUE value) { + this.value = value; + } + + public int getScopeStart() { + return scopeStart; + } + + public void setScopeStart(int scopeStart) { + this.scopeStart = scopeStart; + } + + public int getScopeEnd() { + return scopeEnd; + } + + public void setScopeEnd(int scopeEnd) { + this.scopeEnd = scopeEnd; + } + + public int getValueStart() { + return valueStart; + } + + public void setValueStart(int valueStart) { + this.valueStart = valueStart; + } + + public int getValueEnd() { + return valueEnd; + } + + public void setValueEnd(int valueEnd) { + this.valueEnd = valueEnd; + } + + public int getValueAmount() { + return valueAmount; + } + + public void setValueAmount(int valueAmount) { + this.valueAmount = valueAmount; + } + + public boolean isRelevant(RAMEvent e) { + // Skip event if it's not the right type + if (type != TYPE.ANY && e.getType() != TYPE.ANY) { + if ((type != e.getType())) { + if (type == TYPE.READ) { + if (!e.getType().isRead()) { + return false; + } + } else { + return false; + } + } + } + // Skip event if it's not in the scope we care about + if (scope != RAMEvent.SCOPE.ANY) { + if (scope == RAMEvent.SCOPE.ADDRESS && e.getAddress() != scopeStart) { + return false; + } else if (scope == RAMEvent.SCOPE.RANGE && (e.getAddress() < scopeStart || e.getAddress() > scopeEnd)) { + return false; + } + } + + // Skip event if the value modification is uninteresting + if (value != RAMEvent.VALUE.ANY) { + if (value == RAMEvent.VALUE.CHANGE_BY && e.getNewValue() - e.getOldValue() != valueAmount) { + return false; + } else if (value == RAMEvent.VALUE.EQUALS && e.getNewValue() != valueAmount) { + return false; + } else if (value == RAMEvent.VALUE.NOT_EQUALS && e.getNewValue() == valueAmount) { + return false; + } else if (value == RAMEvent.VALUE.RANGE && (e.getNewValue() < valueStart || e.getNewValue() > valueEnd)) { + return false; + } + } + + // Ok, so we've filtered out the uninteresting stuff + // If we've made it this far then the event is valid. + return true; + } + + public void handleEvent(RAMEvent e) { + if (isRelevant(e)) { + doEvent(e); + } + } + + abstract protected void doConfig(); + + abstract protected void doEvent(RAMEvent e); +} diff --git a/src/main/java/jace/core/SoftSwitch.java b/src/main/java/jace/core/SoftSwitch.java new file mode 100644 index 0000000..29434d6 --- /dev/null +++ b/src/main/java/jace/core/SoftSwitch.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.state.Stateful; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A softswitch is a hidden bit that lives in the MMU, it can be activated or + * deactivated to change operating characteristics of the computer such as video + * display mode or memory paging model. Other special softswitches access + * keyboard and speaker ports. The underlying mechanic of softswitches is + * managed by the RamListener/Ram model and, in the case of video modes, the + * Video classes. + * + * The implementation of softswitches is in jace.apple2e.SoftSwitches + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + * @see jace.apple2e.SoftSwitches + */ +public abstract class SoftSwitch { + + @Stateful + public Boolean state; + private Boolean initalState; + private List<RAMListener> listeners; + private final List<Integer> exclusionActivate = new ArrayList<>(); + private final List<Integer> exclusionDeactivate = new ArrayList<>(); + private final List<Integer> exclusionQuery = new ArrayList<>(); + private String name; + private boolean toggleType = false; + + /** + * Creates a new instance of SoftSwitch + * + * @param name + * @param initalState + */ + public SoftSwitch(String name, Boolean initalState) { + this.initalState = initalState; + this.state = initalState; + this.listeners = new ArrayList<>(); + this.name = name; + } + + public SoftSwitch(String name, int offAddress, int onAddress, int queryAddress, RAMEvent.TYPE changeType, Boolean initalState) { + if (onAddress == offAddress && onAddress != -1) { + toggleType = true; +// System.out.println("Switch " + name + " is a toggle type switch!"); + } + this.initalState = initalState; + this.state = initalState; + this.listeners = new ArrayList<>(); + this.name = name; + int[] onAddresses = null; + int[] offAddresses = null; + int[] queryAddressList = null; + if (onAddress >= 0) { + onAddresses = new int[]{onAddress}; + } + if (offAddress >= 0) { + offAddresses = new int[]{offAddress}; + } + if (queryAddress >= 0) { + queryAddressList = new int[]{queryAddress}; + } + init(offAddresses, onAddresses, queryAddressList, changeType); + } + + public SoftSwitch(String name, int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType, Boolean initalState) { + this(name, initalState); + init(offAddrs, onAddrs, queryAddrs, changeType); + } + + private void init(int[] offAddrs, int[] onAddrs, int[] queryAddrs, RAMEvent.TYPE changeType) { + if (toggleType) { + List<Integer> addrs = new ArrayList<>(); + for (int i : onAddrs) { + addrs.add(i); + } + Collections.sort(addrs); + final int beginAddr = addrs.get(0); + final int endAddr = addrs.get(addrs.size() - 1); + for (int i = beginAddr; i < endAddr; i++) { + if (!addrs.contains(i)) { + exclusionActivate.add(i); + } + } + RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(beginAddr); + setScopeEnd(endAddr); + } + + @Override + protected void doEvent(RAMEvent e) { + if (!exclusionActivate.contains(e.getAddress())) { + // System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName()); + setState(!getState()); + } + } + }; + addListener(l); + } else { + if (onAddrs != null) { + List<Integer> addrs = new ArrayList<>(); + for (int i : onAddrs) { + addrs.add(i); + } + Collections.sort(addrs); + final int beginAddr = addrs.get(0); + final int endAddr = addrs.get(addrs.size() - 1); + for (int i = beginAddr; i < endAddr; i++) { + if (!addrs.contains(i)) { + exclusionActivate.add(i); + } + } + RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(beginAddr); + setScopeEnd(endAddr); + } + + @Override + protected void doEvent(RAMEvent e) { + if (!exclusionActivate.contains(e.getAddress())) { + // System.out.println("Access to "+Integer.toHexString(e.getAddress())+" ENABLES switch "+getName()); + setState(true); + } + } + }; + addListener(l); + } + + if (offAddrs != null) { + List<Integer> addrs = new ArrayList<>(); + for (int i : offAddrs) { + addrs.add(i); + } + final int beginAddr = addrs.get(0); + final int endAddr = addrs.get(addrs.size() - 1); + for (int i = beginAddr; i < endAddr; i++) { + if (!addrs.contains(i)) { + exclusionDeactivate.add(i); + } + } + RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(beginAddr); + setScopeEnd(endAddr); + } + + @Override + protected void doEvent(RAMEvent e) { + if (!exclusionDeactivate.contains(e.getAddress())) { + setState(false); +// System.out.println("Access to "+Integer.toHexString(e.getAddress())+" disables switch "+getName()); + } + } + }; + addListener(l); + } + } + + if (queryAddrs != null) { + List<Integer> addrs = new ArrayList<>(); + for (int i : queryAddrs) { + addrs.add(i); + } + final int beginAddr = addrs.get(0); + final int endAddr = addrs.get(addrs.size() - 1); + for (int i = beginAddr; i < endAddr; i++) { + if (!addrs.contains(i)) { + exclusionQuery.add(i); + } + } +// RAMListener l = new RAMListener(changeType, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + RAMListener l = new RAMListener(RAMEvent.TYPE.READ, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(beginAddr); + setScopeEnd(endAddr); + } + + @Override + protected void doEvent(RAMEvent e) { + if (!exclusionQuery.contains(e.getAddress())) { + e.setNewValue(0x0ff & readSwitch()); +// System.out.println("Read from "+Integer.toHexString(e.getAddress())+" returns "+Integer.toHexString(e.getNewValue())); + } + } + }; + addListener(l); + } + } + + public boolean inhibit() { + return false; + } + + abstract protected byte readSwitch(); + + protected void addListener(RAMListener l) { + listeners.add(l); + } + + public String getName() { + return name; + } + + public void reset() { + if (initalState != null) { + setState(initalState); + } + } + + public void register() { + RAM m = Computer.getComputer().getMemory(); + listeners.stream().forEach((l) -> { + m.addListener(l); + }); + } + + public void unregister() { + RAM m = Computer.getComputer().getMemory(); + listeners.stream().forEach((l) -> { + m.removeListener(l); + }); + } + + public void setState(boolean newState) { + if (inhibit()) { + return; + } +// if (this != SoftSwitches.VBL.getSwitch() && +// this != SoftSwitches.KEYBOARD.getSwitch()) +// System.out.println("Switch "+name+" set to "+newState); + state = newState; + /* + if (queryAddresses != null) { + RAM m = Computer.getComputer().getMemory(); + for (int i:queryAddresses) { + byte old = m.read(i, false); + m.write(i, (byte) (old & 0x7f | (state ? 0x080:0x000)), false); + } + } + */ + stateChanged(); + } + + public final boolean getState() { + if (state == null) { + return false; + } + return state; + } + + abstract public void stateChanged(); + + @Override + public String toString() { + return getName() + (getState() ? ":1" : ":0"); + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/SoundMixer.java b/src/main/java/jace/core/SoundMixer.java new file mode 100644 index 0000000..35bd895 --- /dev/null +++ b/src/main/java/jace/core/SoundMixer.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.config.ConfigurableField; +import jace.config.DynamicSelection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.Mixer.Info; +import javax.sound.sampled.SourceDataLine; + +/** + * Manages sound resources used by various audio devices (such as speaker and + * mockingboard cards.) The plumbing is managed in this class so that the + * consumers do not have to do a lot of work to manage mixer lines or deal with + * how to reuse active lines if needed. It is possible that this class might be + * used to manage volume in the future, but that remains to be seen. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class SoundMixer extends Device { + + private final Set<SourceDataLine> availableLines = Collections.synchronizedSet(new HashSet<SourceDataLine>()); + private final Map<Object, SourceDataLine> activeLines = Collections.synchronizedMap(new HashMap<Object, SourceDataLine>()); + /** + * Bits per sample + */ + @ConfigurableField(name = "Bits per sample", shortName = "bits") + public static int BITS = 16; + /** + * Sample playback rate + */ + @ConfigurableField(name = "Playback Rate", shortName = "freq") + public static int RATE = 48000; + /** + * Sound format used for playback + */ + private AudioFormat af; + /** + * Is sound line available for playback at all? + */ + public boolean lineAvailable; + @ConfigurableField(name = "Audio device", description = "Audio output device") + public static DynamicSelection<String> preferredMixer = new DynamicSelection<String>(null) { + @Override + public boolean allowNull() { + return false; + } + + @Override + public LinkedHashMap<? extends String, String> getSelections() { + Info[] mixerInfo = AudioSystem.getMixerInfo(); + LinkedHashMap<String, String> out = new LinkedHashMap<>(); + for (Info i : mixerInfo) { + out.put(i.getName(), i.getName()); + } + return out; + } + }; + private Mixer theMixer; + + @Override + public String getDeviceName() { + return "Sound Output"; + } + + @Override + public String getShortName() { + return "mixer"; + } + + @Override + public synchronized void reconfigure() { + detach(); + try { + initMixer(); + if (lineAvailable) { + initAudio(); + System.out.println("Started sound"); + } else { + System.out.println("Sound not stared: Line not available"); + } + } catch (LineUnavailableException ex) { + System.out.println("Unable to start sound"); + Logger.getLogger(SoundMixer.class.getName()).log(Level.SEVERE, null, ex); + } + attach(); + } + + /** + * Obtain sound playback line if available + * + * @throws javax.sound.sampled.LineUnavailableException If there is no line + * available + */ + private void initAudio() throws LineUnavailableException { + af = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, RATE, BITS, 2, BITS / 4, RATE, true); +// af = new AudioFormat(RATE, BITS, 2, true, true); + DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af); + lineAvailable = AudioSystem.isLineSupported(dli); + } + + public synchronized SourceDataLine getLine(Object requester) throws LineUnavailableException { + if (activeLines.containsKey(requester)) { + return activeLines.get(requester); + } + SourceDataLine sdl = null; + if (availableLines.isEmpty()) { + sdl = getNewLine(); + } else { + sdl = availableLines.iterator().next(); + availableLines.remove(sdl); + } + activeLines.put(requester, sdl); + sdl.start(); + return sdl; + } + + public void returnLine(Object requester) { + if (activeLines.containsKey(requester)) { + SourceDataLine sdl = activeLines.remove(requester); +// Calling drain on pulse driver can cause it to freeze up (?) +// sdl.drain(); + sdl.stop(); + sdl.flush(); + availableLines.add(sdl); + } + } + + private SourceDataLine getNewLine() throws LineUnavailableException { + SourceDataLine l = null; +// Line.Info[] info = theMixer.getSourceLineInfo(); + DataLine.Info dli = new DataLine.Info(SourceDataLine.class, af); + System.out.println("Maximum output lines: " + theMixer.getMaxLines(dli)); + System.out.println("Allocated output lines: " + theMixer.getSourceLines().length); + System.out.println("Getting source line from " + theMixer.getMixerInfo().toString() + ": " + af.toString()); + try { + l = (SourceDataLine) theMixer.getLine(dli); + } catch (IllegalArgumentException e) { + lineAvailable = false; + throw new LineUnavailableException(e.getMessage()); + } catch (LineUnavailableException e) { + lineAvailable = false; + throw e; + } + if (!(l instanceof SourceDataLine)) { + lineAvailable = false; + throw new LineUnavailableException("Line is not an output line!"); + } + final SourceDataLine sdl = (SourceDataLine) l; +// sdl.open(af); +// if (false) { +// return sdl; +// } + sdl.open(); + sdl.start(); +// new Thread(new Runnable() { +// @Override +// public void run() { +// System.out.println("Going into an infinite loop!!!"); +// try { +// while (true) { +// sdl.write(new byte[]{randomByte(),randomByte(),randomByte(),randomByte()}, 0, 4); +// } +// } catch (Throwable t) { +// t.printStackTrace(); +// } +// System.out.println("Thread dying..."); +// } +// }).start(); + return sdl; + } + + public byte randomByte() { + return (byte) (Math.random() * 256); + } + + @Override + public void tick() { + } + + @Override + public void attach() { +// if (Motherboard.enableSpeaker) +// Motherboard.speaker.attach(); + } + + @Override + public void detach() { + availableLines.stream().forEach((line) -> { + line.close(); + }); + Set requesters = new HashSet(activeLines.keySet()); + requesters.stream().map((o) -> { + if (o instanceof Device) { + ((Device) o).detach(); + } + return o; + }).filter((o) -> (o instanceof Card)).forEach((o) -> { + ((Card) o).reconfigure(); + }); + if (theMixer != null) { + for (Line l : theMixer.getSourceLines()) { + l.close(); + } + } + availableLines.clear(); + activeLines.clear(); + } + + private void initMixer() { + Info selected = null; + Info[] mixerInfo = AudioSystem.getMixerInfo(); + + if (mixerInfo == null || mixerInfo.length == 0) { + theMixer = null; + lineAvailable = false; + System.out.println("No sound mixer is available!"); + return; + } + + String mixer = preferredMixer.getValue(); + selected = mixerInfo[0]; + for (Info i : mixerInfo) { + if (i.getName().equalsIgnoreCase(mixer)) { + selected = i; + break; + } + } + theMixer = AudioSystem.getMixer(selected); + for (Line l : theMixer.getSourceLines()) { + l.close(); + } + lineAvailable = true; + } +} diff --git a/src/main/java/jace/core/TimedDevice.java b/src/main/java/jace/core/TimedDevice.java new file mode 100644 index 0000000..a94fa94 --- /dev/null +++ b/src/main/java/jace/core/TimedDevice.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.config.ConfigurableField; + +/** + * A timed device is a device which executes so many ticks in a given time + * interval. This is the core of the emulator timing mechanics. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class TimedDevice extends Device { + + /** + * Creates a new instance of TimedDevice + */ + public TimedDevice() { + setSpeed(cyclesPerSecond); + } + @ConfigurableField(name = "Speed", description = "(in hertz)") + public long cyclesPerSecond = defaultCyclesPerSecond(); + @ConfigurableField(name = "Max speed") + public boolean maxspeed = false; + + @Override + public abstract void tick(); + private static final double NANOS_PER_SECOND = 1000000000.0; + // current cycle within the period + private int cycleTimer = 0; + // The actual worker that the device runs as + public Thread worker; + public static int TEMP_SPEED_MAX_DURATION = 1000000; + private int tempSpeedDuration = 0; + public boolean hasStopped = true; + + @Override + public boolean suspend() { + disableTempMaxSpeed(); + boolean result = super.suspend(); + if (worker != null && worker.isAlive()) { + try { + worker.interrupt(); + worker.join(1000); + } catch (InterruptedException ex) { + } + } + worker = null; + return result; + } + Thread timerThread; + + public boolean pause() { + if (!isRunning()) { + return false; + } + isPaused = true; + try { + // KLUDGE: Sleeping to wait for worker thread to hit paused state. We might be inside the worker (?) + Thread.sleep(10); + } catch (InterruptedException ex) { + } + return true; + } + + @Override + public void resume() { + super.resume(); + isPaused = false; + if (worker != null && worker.isAlive()) { + return; + } + worker = new Thread(() -> { + while (isRunning()) { + hasStopped = false; + doTick(); + while (isPaused) { + hasStopped = true; + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + return; + } + } + resync(); + } + hasStopped = true; + }); + worker.setDaemon(false); + worker.setPriority(Thread.MAX_PRIORITY); + worker.start(); + worker.setName("Timed device " + getDeviceName() + " worker"); + } + long nanosPerInterval; // How long to wait between pauses + long cyclesPerInterval; // How many cycles to wait until a pause interval + long nextSync; // When was the last pause? + + public final void setSpeed(long cyclesPerSecond) { + cyclesPerInterval = cyclesPerSecond / 100L; + nanosPerInterval = (long) ((double) cyclesPerInterval * NANOS_PER_SECOND / (double) cyclesPerSecond); +// System.out.println("Will pause " + nanosPerInterval + " nanos every " + cyclesPerInterval + " cycles"); + cycleTimer = 0; + resetSyncTimer(); + } + long skip = 0; + long wait = 0; + + public final void resetSyncTimer() { + nextSync = System.nanoTime() + nanosPerInterval; + cycleTimer = 0; + } + + public void enableTempMaxSpeed() { + tempSpeedDuration = TEMP_SPEED_MAX_DURATION; + } + + public void disableTempMaxSpeed() { + tempSpeedDuration = 0; + resetSyncTimer(); + } + + protected void resync() { + if (++cycleTimer >= cyclesPerInterval) { + if (maxspeed || tempSpeedDuration > 0) { + if (tempSpeedDuration > 0) { + tempSpeedDuration -= cyclesPerInterval; + } + resetSyncTimer(); + return; + } + long now = System.nanoTime(); + if (now < nextSync) { + cycleTimer = 0; + long currentSyncDiff = nextSync - now; + // Don't bother resynchronizing unless we're off by 10ms + if (currentSyncDiff > 10000000L) { + try { +// System.out.println("Sleeping for " + currentSyncDiff / 1000000 + " milliseconds"); + Thread.sleep(currentSyncDiff / 1000000L, (int) (currentSyncDiff % 1000000L)); + } catch (InterruptedException ex) { + System.err.println(getDeviceName() + " was trying to sleep for " + (currentSyncDiff / 1000000) + " millis but was woken up"); +// Logger.getLogger(TimedDevice.class.getName()).log(Level.SEVERE, null, ex); + } + } else { +// System.out.println("Sleeping for " + currentSyncDiff + " nanoseconds"); +// LockSupport.parkNanos(currentSyncDiff); + } + } + nextSync += nanosPerInterval; + } + } + + @Override + public void reconfigure() { + if (cyclesPerSecond == 0) { + cyclesPerSecond = defaultCyclesPerSecond(); + } + setSpeed(cyclesPerSecond); + } + + public abstract long defaultCyclesPerSecond(); +} diff --git a/src/main/java/jace/core/Utility.java b/src/main/java/jace/core/Utility.java new file mode 100644 index 0000000..806d8d1 --- /dev/null +++ b/src/main/java/jace/core/Utility.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.Emulator; +import java.awt.BorderLayout; +import java.awt.EventQueue; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +/** + * This is a set of helper functions which do not belong anywhere else. Functions vary from introspection, discovery, and string/pattern matching. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Utility { + + //--------------- Introspection utilities + private static Set<Class> findClasses(String pckgname, Class clazz) { + Set<Class> output = new HashSet<>(); + // Code from JWhich + // ====== + // Translate the package name into an absolute path + String name = pckgname; + if (!name.startsWith("/")) { + name = "/" + name; + } + name = name.replace('.', '/'); + + // Get a File object for the package + URL url = Utility.class.getResource(name); + if (url == null || url.getFile().contains("jre/lib")) { + return output; + } + if (url.getProtocol().equalsIgnoreCase("jar")) { + return findClassesInJar(url, clazz); + } + + File directory = new File(url.getFile()); + // New code + // ====== + if (directory.exists()) { + // Get the list of the files contained in the package + for (String filename : directory.list()) { + char firstLetter = filename.charAt(0); + if (firstLetter < 'A' || (firstLetter > 'Z' && firstLetter < 'a') || firstLetter > 'z') { + continue; + } + // we are only interested in .class files + if (filename.endsWith(".class")) { + // removes the .class extension + String classname = filename.substring(0, filename.length() - 6); + try { + // Try to create an instance of the object + String className = pckgname + "." + classname; +// System.out.println("Class: " + className); + Class c = Class.forName(className); + if (clazz.isAssignableFrom(c)) { + output.add(c); + } + } catch (ClassNotFoundException cnfex) { + System.err.println(cnfex); + } + } else { +// System.out.println("Skipping non class: " + filename); + } + } + } + return output; + } + + private static Set<Class> findClassesInJar(URL jarLocation, Class clazz) { + Set<Class> output = new HashSet<>(); + JarFile jarFile = null; + try { + JarURLConnection conn = (JarURLConnection) jarLocation.openConnection(); + jarFile = conn.getJarFile(); + Enumeration<JarEntry> entries = jarFile.entries(); + String last = ""; + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + if (jarEntry.getName().equals(last)) { + return output; + } + last = jarEntry.getName(); + if (jarEntry.getName().endsWith(".class")) { + String className = jarEntry.getName(); + className = className.substring(0, className.length() - 6); + className = className.replaceAll("/", "\\."); + if (className.startsWith("com.sun")) { + continue; + } + if (className.startsWith("java")) { + continue; + } + if (className.startsWith("javax")) { + continue; + } + if (className.startsWith("com.oracle")) { + continue; + } + // removes the .class extension + try { + // Try to create an instance of the object +// System.out.println("Class: " + className); + Class c = Class.forName(className); + if (clazz.isAssignableFrom(c)) { + output.add(c); + } + } catch (ClassNotFoundException cnfex) { + System.err.println(cnfex); + } catch (Throwable cnfex) { +// System.err.println(cnfex); + } + } else { +// System.out.println("Skipping non class: " + jarEntry.getName()); + } + } + } catch (IOException ex) { + Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + if (jarFile != null) { + jarFile.close(); + } + } catch (IOException ex) { + Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); + } + } + return output; + } + private static final Map<Class, Collection<Class>> classCache = new HashMap<>(); + + public static List<Class> findAllSubclasses(Class clazz) { + if (classCache.containsKey(clazz)) { + return (List<Class>) classCache.get(clazz); + } + TreeMap<String, Class> allClasses = new TreeMap<>(); + for (Package p : Package.getPackages()) { + if (p.getName().startsWith("java") + || p.getName().startsWith("com.sun") + || p.getName().startsWith("com.oracle")) { + continue; + } + findClasses(p.getName(), clazz).stream().filter((c) -> !(Modifier.isAbstract(c.getModifiers()))).forEach((c) -> { + allClasses.put(c.getSimpleName(), c); + }); + } + List<Class> values = new ArrayList(allClasses.values()); + classCache.put(clazz, values); + return values; + } + + //------------------------------ String comparators + /** + * Rank two strings similarity in terms of distance The lower the number, + * the more similar these strings are to each other See: + * http://en.wikipedia.org/wiki/Levenshtein_distance#Computing_Levenshtein_distance + * + * @param s + * @param t + * @return Distance (higher is better) + */ + public static int levenshteinDistance(String s, String t) { + if (s == null || t == null || s.length() == 0 || t.length() == 0) { + return -1; + } + + s = s.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", ""); + t = t.toLowerCase().replaceAll("[^a-zA-Z0-9\\s]", ""); + int m = s.length(); + int n = t.length(); + int[][] dist = new int[m + 1][n + 1]; + for (int i = 1; i <= m; i++) { + dist[i][0] = i; + } + for (int i = 1; i <= n; i++) { + dist[0][i] = i; + } + for (int j = 1; j <= n; j++) { + for (int i = 1; i <= m; i++) { + if (s.charAt(i - 1) == t.charAt(j - 1)) { + dist[i][j] = dist[i - 1][j - 1]; + } else { + int del = dist[i - 1][j] + 1; + int insert = dist[i][j - 1] + 1; + int sub = dist[i - 1][j - 1] + 1; + dist[i][j] = Math.min(Math.min(del, insert), sub); + } + } + } + return Math.max(m, n) - dist[m][n]; + } + + /** + * Compare strings based on a tally of similar patterns found, using a fixed + * search window The resulting score is heavily penalized if the strings + * differ greatly in length This is not as efficient as levenshtein, so it's + * only used as a tie-breaker. + * + * @param c1 + * @param c2 + * @param width Search window size + * @return Overall similarity score (higher is beter) + */ + public static double rankMatch(String c1, String c2, int width) { + double score = 0; + String s1 = c1.toLowerCase(); + String s2 = c2.toLowerCase(); + for (int i = 0; i < s1.length() + 1 - width; i++) { + String m = s1.substring(i, i + width); + int j = 0; + while ((j = s2.indexOf(m, j)) > -1) { + score += width; + j++; + } + } + double l1 = s1.length(); + double l2 = s2.length(); + // If the two strings are equivilent in length, the score is higher + // If the two strings are different in length, the score is adjusted lower depending on how large the difference is + // This is offset just a hair for tuning purposes + double adjustment = (Math.min(l1, l2) / Math.max(l1, l2)) + 0.1; + return score * adjustment * adjustment; + } + + public static String join(Collection c, String d) { + String result = ""; + boolean isFirst = true; + for (Object o : c) { + result += (isFirst ? "" : d) + o.toString(); + isFirst = false; + } + return result; + } + + public static ImageIcon loadIcon(String filename) { + URL imageUrl = Utility.class.getClassLoader().getResource("jace/data/" + filename); + ImageIcon i = new ImageIcon(imageUrl); + return i; + } + + public static void runModalProcess(String title, final Runnable runnable) { + final JDialog frame = new JDialog(Emulator.getFrame()); + final JProgressBar progressBar = new JProgressBar(); + progressBar.setIndeterminate(true); + final JPanel contentPane = new JPanel(); + contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + contentPane.setLayout(new BorderLayout()); + contentPane.add(new JLabel(title), BorderLayout.NORTH); + contentPane.add(progressBar, BorderLayout.CENTER); + frame.setContentPane(contentPane); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + new Thread(() -> { + runnable.run(); + frame.setVisible(false); + frame.dispose(); + }).start(); + } + + public static class RankingComparator implements Comparator<String> { + + String match; + + public RankingComparator(String match) { + // Adding a space helps respect word boundaries as part of the match + // In the case of very close matches this is another tie-breaker + // Especially for very small search terms + this.match = match + " "; + } + + @Override + public int compare(String o1, String o2) { + double s1 = levenshteinDistance(match, o1); + double s2 = levenshteinDistance(match, o2); + if (s2 == s1) { + s1 = rankMatch(o1, match, 3) + rankMatch(o1, match, 2); + s2 = rankMatch(o2, match, 3) + rankMatch(o2, match, 2); + if (s2 == s1) { + return (o1.compareTo(o2)); + } else { + // Normalize result to -1, 0 or 1 so there is no rounding issues! + return (int) Math.signum(s2 - s1); + } + } else { + return (int) (s2 - s1); + } + } + } + + /** + * Given a desired search string and a search space of recognized + * selections, identify the best match in the list + * + * @param match String to search for + * @param search Space of all valid results + * @return Best match found, or null if there was nothing close to a match + * found. + */ + public static String findBestMatch(String match, Collection<String> search) { + if (search == null || search.isEmpty()) { + return null; + } + RankingComparator r = new RankingComparator(match); + List<String> candidates = new ArrayList<>(search); + Collections.sort(candidates, r); +// for (String c : candidates) { +// double m2 = rankMatch(c, match, 2); +// double m3 = rankMatch(c, match, 3); +// double m4 = rankMatch(c, match, 4); +// double l = levenshteinDistance(match, c); +// System.out.println(match + "->" + c + ":" + l + " -- "+ m2 + "," + m3 + "," + "(" + (m2 + m3) + ")"); +// } +// double score = rankMatch(match, candidates.get(0), 2); + double score = levenshteinDistance(match, candidates.get(0)); + if (score > 1) { + return candidates.get(0); + } + return null; + } + + public static void printStackTrace() { + System.out.println("CURRENT STACK TRACE:"); + for (StackTraceElement s : Thread.currentThread().getStackTrace()) { + System.out.println(s.getClassName() + "." + s.getMethodName() + " (line " + s.getLineNumber() + ") " + (s.isNativeMethod() ? "NATIVE" : "")); + } + System.out.println("END OF STACK TRACE"); + } + + public static int parseHexInt(Object s) { + if (s == null) { + return -1; + } + if (s instanceof Integer) { + return (Integer) s; + } + String val = String.valueOf(s).trim(); + int base = 10; + if (val.startsWith("$")) { + base = 16; + val = val.contains(" ") ? val.substring(1, val.indexOf(' ')) : val.substring(1); + } else if (val.startsWith("0x")) { + base = 16; + val = val.contains(" ") ? val.substring(2, val.indexOf(' ')) : val.substring(2); + } + try { + return Integer.parseInt(val, base); + } catch (NumberFormatException ex) { + gripe("This isn't a valid number: " + val + ". If you put a $ in front of that then I'll know you meant it to be a hex number."); + throw ex; + } + } + + public static void gripe(final String message) { + EventQueue.invokeLater(() -> { + JOptionPane.showMessageDialog(Emulator.getFrame(), message, "Error", JOptionPane.ERROR_MESSAGE); + }); + } + + public static Object findChild(Object object, String fieldName) { + if (object instanceof Map) { + Map map = (Map) object; + for (Object key : map.keySet()) { + if (key.toString().equalsIgnoreCase(fieldName)) { + return map.get(key); + } + } + return null; + } + try { + Field f = object.getClass().getField(fieldName); + return f.get(object); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { + for (Method m : object.getClass().getMethods()) { + if (m.getName().equalsIgnoreCase("get" + fieldName) && m.getParameterTypes().length == 0) { + try { + return m.invoke(object, new Object[0]); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) { + } + } + } + } + return null; + } + + public static Object setChild(Object object, String fieldName, String value, boolean hex) { + if (object instanceof Map) { + Map map = (Map) object; + for (Object key : map.entrySet()) { + if (key.toString().equalsIgnoreCase(fieldName)) { + map.put(key, value); + return null; + } + } + return null; + } + Field f; + try { + f = object.getClass().getField(fieldName); + } catch (NoSuchFieldException ex) { + System.out.println("Object type " + object.getClass().getName() + " has no field named " + fieldName); + Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); + return null; + } catch (SecurityException ex) { + Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); + return null; + } + Object useValue = deserializeString(value, f.getType(), hex); + try { + f.set(object, useValue); + return useValue; + } catch (IllegalArgumentException | IllegalAccessException ex) { + for (Method m : object.getClass().getMethods()) { + if (m.getName().equalsIgnoreCase("set" + fieldName) && m.getParameterTypes().length == 0) { + try { + m.invoke(object, useValue); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex1) { + } + } + } + } + return useValue; + } + static Map<Class, Map<String, Object>> enumCache = new HashMap<>(); + + public static Object findClosestEnumConstant(String value, Class type) { + Map<String, Object> enumConstants = enumCache.get(type); + if (enumConstants == null) { + Object[] constants = type.getEnumConstants(); + enumConstants = new HashMap<>(); + for (Object o : constants) { + enumConstants.put(o.toString(), o); + } + enumCache.put(type, enumConstants); + } + + String key = findBestMatch(value, enumConstants.keySet()); + if (key == null) { + return null; + } + return enumConstants.get(key); + } + + public static Object deserializeString(String value, Class type, boolean hex) { + int radix = hex ? 16 : 10; + if (type.equals(Integer.TYPE) || type == Integer.class) { + value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", ""); + try { + return Integer.parseInt(value, radix); + } catch (NumberFormatException ex) { + return null; + } + } else if (type.equals(Short.TYPE) || type == Short.class) { + value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", ""); + try { + return Short.parseShort(value, radix); + } catch (NumberFormatException ex) { + return null; + } + } else if (type.equals(Long.TYPE) || type == Long.class) { + value = value.replaceAll(hex ? "[^0-9\\-\\.A-Fa-f]" : "[^0-9\\-\\.]", ""); + try { + return Long.parseLong(value, radix); + } catch (NumberFormatException ex) { + return null; + } + } else if (type.equals(Byte.TYPE) || type == Byte.class) { + try { + value = value.replaceAll(hex ? "[^0-9\\-A-Fa-f]" : "[^0-9\\-]", ""); + return Byte.parseByte(value, radix); + } catch (NumberFormatException ex) { + return null; + } + } else if (type.equals(Boolean.TYPE) || type == Boolean.class) { + return Boolean.valueOf(value); + } else if (type == File.class) { + return new File(String.valueOf(value)); + } else if (type.isEnum()) { + value = value.replaceAll("[\\.\\s\\-]", ""); + return findClosestEnumConstant(value, type); + } + return null; + } + + public static Object getProperty(Object object, String path) { + String[] paths = path.split("\\."); + for (String path1 : paths) { + object = findChild(object, path1); + if (object == null) { + return null; + } + } + return object; + } + + public static Object setProperty(Object object, String path, String value, boolean hex) { + String[] paths = path.split("\\."); + for (int i = 0; i < paths.length - 1; i++) { + object = findChild(object, paths[i]); + if (object == null) { + return null; + } + } + return setChild(object, paths[paths.length - 1], value, hex); + } +} \ No newline at end of file diff --git a/src/main/java/jace/core/Video.java b/src/main/java/jace/core/Video.java new file mode 100644 index 0000000..cf2457f --- /dev/null +++ b/src/main/java/jace/core/Video.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import jace.state.Stateful; +import jace.Emulator; +import jace.config.ConfigurableField; +import jace.config.InvokableAction; +import java.awt.Graphics; +import java.awt.image.BufferedImage; + +/** + * Generic abstraction of a 560x192 video output device which renders 40 columns + * per scanline. This also triggers VBL and updates the physical screen. + * Subclasses are used to manage actual rendering via ScreenWriter + * implementations. + * Created on November 10, 2006, 4:29 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public abstract class Video extends Device { + + @Stateful + BufferedImage video; + VideoWriter currentWriter; + Graphics screen; + private byte floatingBus = 0; + private int width = 560; + private int height = 192; + @Stateful + public int x = 0; + @Stateful + public int y = 0; + @Stateful + public int scannerAddress; + @Stateful + public int vPeriod = 0; + @Stateful + public int hPeriod = 0; + static final public int CYCLES_PER_LINE = 65; + static final public int TOTAL_LINES = 262; + static final public int APPLE_CYCLES_PER_LINE = 40; + static final public int APPLE_SCREEN_LINES = 192; + static final public int HBLANK = CYCLES_PER_LINE - APPLE_CYCLES_PER_LINE; + static final public int VBLANK = (TOTAL_LINES - APPLE_SCREEN_LINES) * CYCLES_PER_LINE; + static public int[] textOffset; + static public int[] hiresOffset; + static public int[] textRowLookup; + static public int[] hiresRowLookup; + private boolean screenDirty; + private boolean lineDirty; + private boolean isVblank = false; + static VideoWriter[][] writerCheck = new VideoWriter[40][192]; + + static { + textOffset = new int[192]; + hiresOffset = new int[192]; + textRowLookup = new int[0x0400]; + hiresRowLookup = new int[0x02000]; + for (int i = 0; i < 192; i++) { + textOffset[i] = calculateTextOffset(i >> 3); + hiresOffset[i] = calculateHiresOffset(i); + } + for (int i = 0; i < 0x0400; i++) { + textRowLookup[i] = identifyTextRow(i); + } + for (int i = 0; i < 0x2000; i++) { + hiresRowLookup[i] = identifyHiresRow(i); + } + } + private int forceRedrawRowCount = 0; + Thread updateThread; + + /** + * Creates a new instance of Video + */ + public Video() { + suspend(); + video = new BufferedImage(560, 192, BufferedImage.TYPE_INT_RGB); + vPeriod = 0; + hPeriod = 0; + forceRefresh(); + } + + public void setWidth(int w) { + width = w; + } + + public int getWidth() { + return width; + } + + public void setHeight(int h) { + height = h; + } + + public int getHeight() { + return height; + } + + public void setScreen(Graphics g) { + screen = g; + + } + + public Graphics getScreen() { + return screen; + } + + public VideoWriter getCurrentWriter() { + return currentWriter; + } + + public void setCurrentWriter(VideoWriter currentWriter) { + if (this.currentWriter != currentWriter || currentWriter.isMixed()) { + this.currentWriter = currentWriter; + forceRedrawRowCount = APPLE_SCREEN_LINES + 1; + } + } + @ConfigurableField(category = "video", name = "Min. Screen Refesh", defaultValue = "15", description = "Minimum number of miliseconds to wait before trying to redraw.") + public static int MIN_SCREEN_REFRESH = 15; + + public void redraw() { + if (screen == null || video == null) { + return; + } + screenDirty = false; + screen.drawImage(video, 0, 0, width, height, null); + if (Emulator.getFrame() != null) { + Emulator.getFrame().repaintIndicators(); + } + } + + public void vblankStart() { + if (screenDirty && isRunning()) { + redraw(); + } + } + + abstract public void vblankEnd(); + + abstract public void hblankStart(BufferedImage screen, int y, boolean isDirty); + + public void setScannerLocation(int loc) { + scannerAddress = loc; + } + + @Override + public void tick() { + setFloatingBus(Computer.getComputer().getMemory().readRaw(scannerAddress + x)); + if (hPeriod > 0) { + hPeriod--; + if (hPeriod == 0) { + x = -1; + setScannerLocation(currentWriter.getYOffset(y)); + } + } else { + if (!isVblank) { + draw(); + } + if (x >= APPLE_CYCLES_PER_LINE - 1) { + int yy = y + hblankOffsetY; + if (yy < 0) { + yy += APPLE_SCREEN_LINES; + } + if (yy >= APPLE_SCREEN_LINES) { + yy -= (TOTAL_LINES - APPLE_SCREEN_LINES); + } + setScannerLocation(currentWriter.getYOffset(yy) + hblankOffsetX + (yy < 64 ? 128 : 0)); + x = -1; + if (!isVblank) { + if (lineDirty) { + screenDirty = true; + currentWriter.clearDirty(y); + } + hblankStart(video, y, lineDirty); + lineDirty = false; + forceRedrawRowCount--; + } + hPeriod = HBLANK; + y++; + if (y >= APPLE_SCREEN_LINES) { + if (!isVblank) { + y = APPLE_SCREEN_LINES - (TOTAL_LINES - APPLE_SCREEN_LINES); + isVblank = true; + vblankStart(); + Motherboard.vblankStart(); + } else { + y = 0; + isVblank = false; + vblankEnd(); + Motherboard.vblankEnd(); + } + } + } + } + x++; + } + + abstract public void configureVideoMode(); + + protected static int byteDoubler(byte b) { + int num = + // Skip hi-bit because it's not used in display + // ((b&0x080)<<7) | + ((b & 0x040) << 6) + | ((b & 0x020) << 5) + | ((b & 0x010) << 4) + | ((b & 0x08) << 3) + | ((b & 0x04) << 2) + | ((b & 0x02) << 1) + | (b & 0x01); + return num | (num << 1); + } + @ConfigurableField(name = "Waits per cycle", category = "Advanced", description = "Adjust the delay for the scanner") + public static int waitsPerCycle = 0; + @ConfigurableField(name = "Hblank X offset", category = "Advanced", description = "Adjust where the hblank period starts relative to the start of the line") + public static int hblankOffsetX = -29; + @ConfigurableField(name = "Hblank Y offset", category = "Advanced", description = "Adjust which line the HBLANK starts on (0=current, 1=next, etc)") + public static int hblankOffsetY = 1; + + private void draw() { + if (lineDirty || forceRedrawRowCount > 0 || currentWriter.isRowDirty(y)) { + lineDirty = true; + currentWriter.displayByte(video, x, y, textOffset[y], hiresOffset[y]); + } + setWaitCycles(waitsPerCycle); + doPostDraw(); + } + + static public int calculateHiresOffset(int y) { + return calculateTextOffset(y >> 3) + ((y & 7) << 10); + } + + static public int calculateTextOffset(int y) { + return ((y & 7) << 7) + 40 * (y >> 3); + } + + static public int identifyTextRow(int y) { + //floor((x-1024)/128) + floor(((x-1024)%128)/40)*8 + // Caller must check result is <= 23, if so then they are in a screenhole! + return (y >> 7) + (((y & 0x7f) / 40) << 3); + } + + static public int identifyHiresRow(int y) { + int blockOffset = identifyTextRow(y & 0x03ff); + // Caller must check results is > 0, if not then they are in a screenhole! + if (blockOffset > 23) { + return -1; + } + return ((y >> 10) & 7) + (blockOffset << 3); + } + + public abstract void doPostDraw(); + + public byte getFloatingBus() { + return floatingBus; + } + + private void setFloatingBus(byte floatingBus) { + this.floatingBus = floatingBus; + } + + @InvokableAction(name = "Refresh screen", + category = "display", + description = "Marks screen contents as changed, forcing full screen redraw", + alternatives = "redraw") + public final void forceRefresh() { + lineDirty = true; + screenDirty = true; + forceRedrawRowCount = APPLE_SCREEN_LINES + 1; + } + + @Override + public String getShortName() { + return "vid"; + } + + public BufferedImage getFrameBuffer() { + return video; + } +} diff --git a/src/main/java/jace/core/VideoWriter.java b/src/main/java/jace/core/VideoWriter.java new file mode 100644 index 0000000..20de46f --- /dev/null +++ b/src/main/java/jace/core/VideoWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.core; + +import java.awt.image.BufferedImage; + +/** + * VideoWriter is an abstraction of a graphics display mode that knows how to + * render a scanline a certain way (lo-res, hi-res, text, etc) over a specific + * range of memory (as determined by getYOffset.) Dirty flags are used to mark + * scanlines that were altered and require redraw. This is the key to only + * updating the screen as needed instead of drawing all the time at the expense + * of CPU. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class VideoWriter { + + public abstract void displayByte(BufferedImage screen, int xOffset, int y, int yTextOffset, int yGraphicsOffset); + + // This is used to support composite mixed-mode writers so that we can talk to the writer being used for a scanline + public VideoWriter actualWriter() { + return this; + } + + public abstract int getYOffset(int y); + // Dirty flags allow us to know if a scanline has or has not changed + // Very useful for knowing if we should bother drawing changes + private final boolean[] dirtyFlags = new boolean[192]; + + public void markDirty(int y) { + actualWriter().dirtyFlags[y] = true; + } + + public void clearDirty(int y) { + actualWriter().dirtyFlags[y] = false; + } + + public boolean isRowDirty(int y) { + return actualWriter().dirtyFlags[y]; + } + + public boolean isMixed() { + return false; + } +} diff --git a/src/main/java/jace/hardware/CardAppleMouse.java b/src/main/java/jace/hardware/CardAppleMouse.java new file mode 100644 index 0000000..16b4eca --- /dev/null +++ b/src/main/java/jace/hardware/CardAppleMouse.java @@ -0,0 +1,637 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.Emulator; +import jace.apple2e.MOS65C02; +import jace.apple2e.RAM128k; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.core.Card; +import jace.core.Computer; +import jace.core.PagedMemory; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.state.Stateful; +import jace.core.Utility; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.ImageIcon; + +/** + * Apple Mouse interface implementation. This is fully compatible with several + * applications. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +@Name("Apple Mouse") +public class CardAppleMouse extends Card implements MouseListener { + + @Stateful + public int mode; + @Stateful + public boolean active; + @Stateful + public boolean interruptOnMove; + @Stateful + public boolean interruptOnPress; + @Stateful + public boolean interruptOnVBL; + @Stateful + public boolean button0press; + @Stateful + public boolean button1press; + @Stateful + public boolean button0pressLast; + @Stateful + public boolean button1pressLast; + @Stateful + public boolean isInterrupt; + @Stateful + public boolean isVBL; + @Stateful + public int statusByte; + @Stateful + public Point lastMouseLocation; + @Stateful + public Point clampMin = new Point(0, 0x03ff); + @Stateful + public Point clampMax = new Point(0, 0x03ff); + // By default, update 60 times a second -- roughly every VBL period (in theory) + @ConfigurableField(name = "Update frequency", shortName = "updateFreq", category = "Mouse", description = "# of CPU cycles between updates; affects polling and interrupt-based routines") + public static int CYCLES_PER_UPDATE = (int) (1020484L / 60L); + @ConfigurableField(name = "Fullscreen fix", shortName = "fsfix", category = "Mouse", description = "If the mouse pointer is a little off when in fullscreen, this should fix it.") + public boolean fullscreenFix = true; + ImageIcon mouseActive = Utility.loadIcon("input-mouse.png"); + + @Override + public String getDeviceName() { + return "Apple Mouse"; + } + + @Override + public void reset() { + mode = 0; + deactivateMouse(); + } + + /* + * Coded against this information + * http://stason.org/TULARC/pc/apple2/programmer/012-How-do-I-write-programs-which-use-the-mouse.html + */ + @Override + protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) { + /* + * Screen holes + * $0478 + slot Low byte of absolute X position + * $04F8 + slot Low byte of absolute Y position + * $0578 + slot High byte of absolute X position + * $05F8 + slot High byte of absolute Y position + * $0678 + slot Reserved and used by the firmware + * $06F8 + slot Reserved and used by the firmware + * $0778 + slot Button 0/1 interrupt status byte + * $07F8 + slot Mode byte + * + * Interrupt status byte: + * Set by READMOUSE + * Bit 7 6 5 4 3 2 1 0 + * | | | | | | | | + * | | | | | | | `--- Previously, button 1 was up (0) or down (1) + * | | | | | | `----- Movement interrupt + * | | | | | `------- Button 0/1 interrupt + * | | | | `--------- VBL interrupt + * | | | `----------- Currently, button 1 is up (0) or down (1) + * | | `------------- X/Y moved since last READMOUSE + * | `--------------- Previously, button 0 was up (0) or down (1) + * `----------------- Currently, button 0 is up (0) or down (1) + * + * Mode byte + * Valid after calling SERVEMOUSE, cleared with READMOUSE + * Bit 7 6 5 4 3 2 1 0 + * | | | | | | | | + * | | | | | | | `--- Mouse off (0) or on (1) + * | | | | | | `----- Interrupt if mouse is moved + * | | | | | `------- Interrupt if button is pressed + * | | | | `--------- Interrupt on VBL + * | | | `----------- Reserved + * | | `------------- Reserved + * | `--------------- Reserved + * `----------------- Reserved + */ + if (type == RAMEvent.TYPE.EXECUTE) { + // This means the CPU is calling firmware at this location + switch (offset - 0x080) { + case 0: + setMouse(); + break; + case 1: + serveMouse(); + break; + case 2: + readMouse(); + break; + case 3: + clearMouse(); + break; + case 4: + posMouse(); + break; + case 5: + clampMouse(); + break; + case 6: + homeMouse(); + break; + case 7: + initMouse(); + break; + } + // Always pass back RTS + e.setNewValue(0x060); + } else if (type.isRead()) { + /* Identification bytes + * $Cn05 = $38 $Cn07 = $18 $Cn0B = $01 $Cn0C = $20 $CnFB = $D6 + */ + switch (offset) { + case 0x05: + e.setNewValue(0x038); + break; + case 0x07: + e.setNewValue(0x018); + break; + case 0x0B: + e.setNewValue(0x01); + break; + case 0x0C: + e.setNewValue(0x020); + break; + case 0x0FB: + e.setNewValue(0x0D6); + break; + // As per the //gs firmware reference manual + case 0x08: + // Pascal signature byte + e.setNewValue(0x001); + case 0x011: + e.setNewValue(0x000); + break; + // Function call offsets + case 0x12: + e.setNewValue(0x080); + break; + case 0x13: + e.setNewValue(0x081); + break; + case 0x14: + e.setNewValue(0x082); + break; + case 0x15: + e.setNewValue(0x083); + break; + case 0x16: + e.setNewValue(0x084); + break; + case 0x17: + e.setNewValue(0x085); + break; + case 0x18: + e.setNewValue(0x086); + break; + case 0x19: + e.setNewValue(0x087); + break; + default: + e.setNewValue(0x069); + } +// System.out.println("Read mouse firmware at "+Integer.toHexString(e.getAddress())+" == "+Integer.toHexString(e.getNewValue())); + } + } + + private MOS65C02 getCPU() { + return (MOS65C02) Computer.getComputer().getCpu(); + } + + /* + * $Cn12 SETMOUSE Sets mouse mode + * A = mouse operation mode (0-f) + * C = 1 if illegal mode requested + * mode byte updated + */ + private void setMouse() { + mode = getCPU().A & 0x0ff; + if (mode > 0x0f) { + getCPU().C = 1; + return; + } else { + getCPU().C = 0; + } + //Mouse off (0) or on (1) + if ((mode & 1) == 0) { + deactivateMouse(); + return; + } + //Interrupt if mouse is moved + interruptOnMove = ((mode & 2) != 0); + //Interrupt if button is pressed + interruptOnPress = ((mode & 4) != 0); + //Interrupt on VBL + interruptOnVBL = ((mode & 8) != 0); + activateMouse(); + } + + /* + * $Cn13 SERVEMOUSE Services mouse interrupt + * Test for interupt and clear mouse interrupt line + * Return C=0 if mouse interrupt occurred + * Updates screen hole interrupt status bits + */ + private void serveMouse() { + // If any interrupts are registered then + updateMouseState(); + if (isInterrupt) { + getCPU().C = 0; + } else { + getCPU().C = 1; +// System.out.println("MOUSE TRIGGERED INTERRUPT!"); + } +// isInterrupt = false; +// isVBL=false; + } + + /* + * $Cn14 READMOUSE Reads mouse position + * Reads delta (X/Y) positions, updates abolute X/Y pos + * and reads button statuses + * Always returns C=0 + * Interrupt status bits cleared + * Screen hole positions for button/movement status bits updated + */ + private void readMouse() { + updateMouseState(); + isInterrupt = false; + isVBL = false; + // set screen holes + getCPU().C = 0; + } + + /* + * $Cn16 POSMOUSE Sets mouse position to a user-defined pos + * Caller puts new position in screenhole + * Always returns C=0 + */ + private void posMouse() { + // Ignore? + getCPU().C = 0; + } + + /* + * $Cn17 CLAMPMOUSE Sets mouse bounds in a window + * Sets up clamping window for mouse user + * Power up defaults are 0 - 1023 (0 - 3ff) + * Caller sets: + * A = 0 if setting X, 1 if setting Y + * $0478 = low byte of low clamp. + * $04F8 = low byte of high clamp. + * $0578 = high byte of low clamp. + * $05F8 = high byte of high clamp. + * //gs homes mouse to low address, but //c and //e do not + */ + private void clampMouse() { + RAM128k memory = (RAM128k) Computer.getComputer().memory; + byte clampMinLo = memory.getMainMemory().readByte(0x0478); + byte clampMaxLo = memory.getMainMemory().readByte(0x04F8); + byte clampMinHi = memory.getMainMemory().readByte(0x0578); + byte clampMaxHi = memory.getMainMemory().readByte(0x05F8); + int min = (clampMinLo & 0x0ff) | ((clampMinHi << 8) & 0x0FF00); + int max = (clampMaxLo & 0x0ff) | ((clampMaxHi << 8) & 0x0FF00); + if (getCPU().A == 0) { + setClampWindowX(min, max); + } else if (getCPU().A == 1) { + setClampWindowY(min, max); + } +// System.out.println("Set mouse clamping to:" + clampMin.toString() + ";" + clampMax.toString()); + } + + /* + * $Cn19 INITMOUSE Resets mouse clamps to default values; sets mouse position to 0,0 + * Sets screen holes to default values and sets clamping + * window to default value (000 - 3ff) for both X and Y + * Exit:C=0 + * Screen holes are updated + */ + private void initMouse() { + mouseActive.setDescription("Active"); + Emulator.getFrame().addIndicator(this, mouseActive, 2000); + setClampWindowX(0, 0x3ff); + setClampWindowY(0, 0x3ff); + clearMouse(); + } + + /* + * $Cn15 CLEARMOUSE Clears mouse position to 0 (for delta mode) + * Resets buttons, movement and interrupt status bits to 0 + * Intended to be used for delta mouse positioning instead of absolute positioning + * Always returns C=0 + * Interrupt status bits cleared + * Screen hole positions for button/movement status bits updated + */ + private void clearMouse() { + isVBL = false; + isInterrupt = false; + button0press = false; + button1press = false; + button0pressLast = false; + button1pressLast = false; + homeMouse(); + } + + /* + * $Cn18 HOMEMOUSE Sets absolute position to upper-left corner of clamping window + * Exit: c=0 + * Screen hole positions are updated + */ + private void homeMouse() { + lastMouseLocation = new Point(0, 0); + updateMouseState(); + getCPU().C = 0; + } + + /* + * This is called whenever the mouse firmware has been activated in software + */ + private void activateMouse() { + active = true; + Component drawingArea = Emulator.getScreen(); + if (drawingArea != null) { + drawingArea.addMouseListener(this); + } + } + + /* + * This is called whenever there is a hard reset or when the mouse is turned off + */ + private void deactivateMouse() { + active = false; + mode = 0; + interruptOnMove = false; + interruptOnPress = false; + interruptOnVBL = false; + Component drawingArea = Emulator.getScreen(); + if (drawingArea != null) { + drawingArea.removeMouseListener(this); + } + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + // No IO access necessary (is there?) + } + private int delay = CYCLES_PER_UPDATE; + + @Override + public void tick() { + if (!active) { + return; + } + delay--; + if (delay > 0) { + return; + } + delay = CYCLES_PER_UPDATE; + + // If interrupts not used, just move on + if (!interruptOnMove && !interruptOnPress) { + return; + } + + if (interruptOnPress) { + if (button0press != button0pressLast || button1press != button1pressLast) { + isInterrupt = true; + getCPU().generateInterrupt(); + return; + } + } + if (interruptOnMove) { + Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation(); + if (!currentMouseLocation.equals(lastMouseLocation)) { + isInterrupt = true; + getCPU().generateInterrupt(); + } + } + } + + @Override + public void notifyVBLStateChanged(boolean state) { + // VBL is false when it is the vertical blanking period + if (!state && interruptOnVBL && active) { + isVBL = true; + isInterrupt = true; + getCPU().generateInterrupt(); + } + } + + private void updateMouseState() { + Component drawingArea = Emulator.getScreen(); + if (drawingArea == null) { + return; + } + Graphics2D screen = (Graphics2D) Computer.getComputer().getVideo().getScreen(); +// Point currentMouseLocation = MouseInfo.getPointerInfo().getLocation(); +// Point topLeft = drawingArea.getLocationOnScreen(); + Point currentMouseLocation = Emulator.getFrame().getContentPane().getMousePosition(); + if (currentMouseLocation == null) return; +// Point topLeft = drawingArea.getLocationOnScreen(); + Point topLeft = new Point(0,0); + + Dimension d = drawingArea.getBounds().getSize(); + if (screen.getTransform() != null) { + d = new Dimension((int) (screen.getTransform().getScaleX() * d.width), + (int) (screen.getTransform().getScaleY() * d.height)); + topLeft.x += screen.getTransform().getTranslateX(); + topLeft.y += screen.getTransform().getTranslateY(); + } + if (fullscreenFix) { + if (Emulator.getFrame().isFullscreenActive()) { + Toolkit t = Toolkit.getDefaultToolkit(); + topLeft.y -= t.getScreenInsets(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration()).top; + } + } + + // Scale X and Y to the clamping range of the mouse (will this work for most software?) + double width = clampMax.x - clampMin.x; + double x = currentMouseLocation.getX() - topLeft.x; + x *= width; + x /= d.width; + x += clampMin.x; + if (x < clampMin.x) { + x = clampMin.x; + } + if (x > clampMax.x) { + x = clampMax.x; + } + + double height = clampMax.y - clampMin.y; + double y = currentMouseLocation.getY() - topLeft.y; + y *= height; + y /= d.height; + y += clampMin.y; + if (y < clampMin.y) { + y = clampMin.y; + } + if (y > clampMax.y) { + y = clampMax.y; + } + + PagedMemory m = ((RAM128k) Computer.getComputer().getMemory()).getMainMemory(); + int s = getSlot(); + /* + * $0478 + slot Low byte of absolute X position + * $04F8 + slot Low byte of absolute Y position + */ + m.writeByte(0x0478 + s, (byte) ((int) x & 0x0ff)); + m.writeByte(0x04F8 + s, (byte) ((int) y & 0x0ff)); + /* + * $0578 + slot High byte of absolute X position + * $05F8 + slot High byte of absolute Y position + */ + m.writeByte(0x0578 + s, (byte) (((int) x & 0x0ff00) >> 8)); + m.writeByte(0x05F8 + s, (byte) (((int) y & 0x0ff00) >> 8)); + /* + * $0678 + slot Reserved and used by the firmware + * $06F8 + slot Reserved and used by the firmware + * + * Interrupt status byte: + * Set by READMOUSE + * Bit 7 6 5 4 3 2 1 0 + * | | | | | | | | + * | | | | | | | `--- Previously, button 1 was up (0) or down (1) + * | | | | | | `----- Movement interrupt + * | | | | | `------- Button 0/1 interrupt + * | | | | `--------- VBL interrupt + * | | | `----------- Currently, button 1 is up (0) or down (1) + * | | `------------- X/Y moved since last READMOUSE + * | `--------------- Previously, button 0 was up (0) or down (1) + * `----------------- Currently, button 0 is up (0) or down (1) + */ + int status = 0; + boolean mouseMoved = !currentMouseLocation.equals(lastMouseLocation); + if (button1pressLast) { + status |= 1; + } + if (interruptOnMove && mouseMoved) { + status |= 2; + } + if (interruptOnPress && (button0press != button0pressLast || button1press != button1pressLast)) { + status |= 4; + } + if (isVBL) { + status |= 8; + } + if (button1press) { + status |= 16; + } + if (mouseMoved) { + status |= 32; + } + if (button0pressLast) { + status |= 64; + } + if (button0press) { + status |= 128; + } + /* + * $0778 + slot Button 0/1 interrupt status byte + */ + m.writeByte(0x0778 + s, (byte) (status)); + + /* + * $07F8 + slot Mode byte + */ + m.writeByte(0x07F8 + s, (byte) (mode)); + + lastMouseLocation = currentMouseLocation; + button0pressLast = button0press; + button1pressLast = button1press; + } + + @Override + public void mousePressed(MouseEvent me) { + int button = me.getButton(); + if (button == 1 || button == 2) { + button0press = true; + } + if (button == 2 || button == 3) { + button1press = true; + } + } + + @Override + public void mouseReleased(MouseEvent me) { + int button = me.getButton(); + if (button == 1 || button == 2) { + button0press = false; + } + if (button == 2 || button == 3) { + button1press = false; + } + } + + @Override + public void mouseClicked(MouseEvent me) { + } + + @Override + public void mouseEntered(MouseEvent me) { + } + + @Override + public void mouseExited(MouseEvent me) { + } + + private void setClampWindowX(int min, int max) { + // Fix for GEOS clamping funkiness + if (max == 32767) { + max = 560; + } + clampMin.x = min; + clampMax.x = max; + } + + private void setClampWindowY(int min, int max) { + // Fix for GEOS clamping funkiness + if (max == 32767) { + max = 192; + } + clampMin.y = min; + clampMax.y = max; + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // Do nothing, there is no need to emulate c8 rom + } +} diff --git a/src/main/java/jace/hardware/CardDiskII.java b/src/main/java/jace/hardware/CardDiskII.java new file mode 100644 index 0000000..f4d5720 --- /dev/null +++ b/src/main/java/jace/hardware/CardDiskII.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.Emulator; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.config.Reconfigurable; +import jace.core.Card; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.Utility; +import jace.library.MediaConsumer; +import jace.library.MediaConsumerParent; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Apple Disk ][ interface implementation. This card represents the interface + * side of the Disk ][ controller interface as well as the on-board "boot0" ROM. + * The behavior of the actual drive stepping, reading disk images, and so on is + * performed by DiskIIDrive and FloppyDisk, respectively. This class only serves + * as the I/O interface portion. + * Created on April 21, 2007 + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("Disk ][ Controller") +public class CardDiskII extends Card implements Reconfigurable, MediaConsumerParent { + + DiskIIDrive currentDrive; + DiskIIDrive drive1 = new DiskIIDrive(); + DiskIIDrive drive2 = new DiskIIDrive(); + @ConfigurableField(category = "Disk", defaultValue = "254", name = "Default volume", description = "Value to use for disk volume number") + static public int DEFAULT_VOLUME_NUMBER = 0x0FE; + @ConfigurableField(category = "Disk", defaultValue = "true", name = "Speed boost", description = "If enabled, emulator will run at max speed during disk access") + static public boolean USE_MAX_SPEED = true; + + public CardDiskII() { + try { + loadRom("jace/data/DiskII.rom"); + } catch (IOException ex) { + Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex); + } + drive1.setIcon(Utility.loadIcon("disk_ii.png")); + drive2.setIcon(Utility.loadIcon("disk_ii.png")); + reset(); + } + + @Override + public String getDeviceName() { + return "Disk ][ Controller"; + } + + @Override + public void reset() { + currentDrive = drive1; + drive1.reset(); + drive2.reset(); + if (Emulator.getFrame() != null) { + Emulator.getFrame().removeIndicators(this); + } +// Motherboard.cancelSpeedRequest(this); + } + + @SuppressWarnings("fallthrough") + @Override + protected void handleIOAccess(int register, RAMEvent.TYPE type, int value, RAMEvent e) { + // handle Disk ][ registers + switch (register) { + case 0x0: + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: + currentDrive.step(register); + break; + + case 0x8: + // drive off + currentDrive.setOn(false); +// Emulator.getFrame().removeIndicator(this, currentDrive == drive1 ? diskDrive1Icon : diskDrive2Icon, false); + break; + + case 0x9: + // drive on + currentDrive.setOn(true); + Emulator.getFrame().addIndicator(this, currentDrive.getIcon()); + break; + + case 0xA: + // drive 1 + currentDrive = drive1; + break; + + case 0xB: + // drive 2 + currentDrive = drive2; + break; + + case 0xC: + // read/write latch + currentDrive.write(); + e.setNewValue(currentDrive.readLatch()); + break; + + case 0xF: + // write mode + currentDrive.setWriteMode(); + case 0xD: + // set latch + if (e.getType() == RAMEvent.TYPE.WRITE) { + currentDrive.setLatchValue((byte) e.getNewValue()); + } + e.setNewValue(currentDrive.readLatch()); + break; + + case 0xE: + // read mode + currentDrive.setReadMode(); + if (currentDrive.disk != null && currentDrive.disk.writeProtected) { + e.setNewValue(0x080); + } else + { +// e.setNewValue((byte) (Math.random() * 256.0)); + e.setNewValue(0); + } + break; + } + // even addresses return the latch value +// if (e.getType() == RAMEvent.TYPE.READ) { +// if ((register & 0x1) == 0) { +// e.setNewValue(currentDrive.latch); +// } else { +// // return floating bus value (IIRC) +// } +// } + tweakTiming(); + } + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // Do nothing: The ROM does everything + return; + } + + public void loadRom(String path) throws IOException { + InputStream romFile = CardDiskII.class.getClassLoader().getResourceAsStream(path); + final int cxRomLength = 0x100; + byte[] romData = new byte[cxRomLength]; + try { + if (romFile.read(romData) != cxRomLength) { + throw new IOException("Bad Disk ][ ROM size"); + } + getCxRom().loadData(romData); + } catch (IOException ex) { + throw ex; + } + } + + @Override + public void tick() { + // Do nothing (if you want 1mhz timing control, you can do that here...) +// drive1.tick(); +// drive2.tick(); + } + + @Override + public void reconfigure() { + super.reconfigure(); + } + + private void tweakTiming() { + if (drive1.isOn() || drive2.isOn()) { + if (USE_MAX_SPEED) { + Motherboard.requestSpeed(this); + } + } else { + Motherboard.cancelSpeedRequest(this); + } + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // There is no special c8 rom for this card + } + + @Override + public void setSlot(int slot) { + super.setSlot(slot); + drive1.getIcon().setDescription("S" + slot + "D1"); + drive2.getIcon().setDescription("S" + slot + "D2"); + } + + public MediaConsumer[] getConsumers() { + return new MediaConsumer[] {drive1, drive2}; + } +} diff --git a/src/main/java/jace/hardware/CardExt80Col.java b/src/main/java/jace/hardware/CardExt80Col.java new file mode 100644 index 0000000..1ce052d --- /dev/null +++ b/src/main/java/jace/hardware/CardExt80Col.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.RAM128k; +import jace.core.PagedMemory; +import jace.state.Stateful; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class CardExt80Col extends RAM128k { + @Stateful + public PagedMemory auxMemory; + @Stateful + public PagedMemory auxLanguageCard; + @Stateful + public PagedMemory auxLanguageCard2; + + @Override + public String getName() { + return "Extended 80-col card (128kb)"; + } + + @Override + public String getShortName() { + return "128kb"; + } + + public CardExt80Col() { + super(); + auxMemory = new PagedMemory(0xc000, PagedMemory.Type.ram); + auxLanguageCard = new PagedMemory(0x3000, PagedMemory.Type.languageCard); + auxLanguageCard2 = new PagedMemory(0x1000, PagedMemory.Type.languageCard); + initMemoryPattern(auxMemory); + } + + // This is redundant here, but necessary for Ramworks + @Override + public PagedMemory getAuxVideoMemory() { + return auxMemory; + } + + /** + * @return the auxMemory + */ + @Override + public PagedMemory getAuxMemory() { + return auxMemory; + } + + /** + * @return the auxLanguageCard + */ + @Override + public PagedMemory getAuxLanguageCard() { + return auxLanguageCard; + } + + /** + * @return the auxLanguageCard2 + */ + @Override + public PagedMemory getAuxLanguageCard2() { + return auxLanguageCard2; + } + + @Override + public void reconfigure() { + // Do nothing + } + + @Override + public void attach() { + // Nothing to do... + } + + @Override + public void detach() { + // Nothing to do... + } +} diff --git a/src/main/java/jace/hardware/CardHayesMicromodem.java b/src/main/java/jace/hardware/CardHayesMicromodem.java new file mode 100644 index 0000000..0c6e74b --- /dev/null +++ b/src/main/java/jace/hardware/CardHayesMicromodem.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.config.Name; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Partial Hayes Micromodem II implementation, acting more as a bridge to + * provide something similar to the Super Serial support for applications which + * do not support the SSC card but do support Hayes, such as DiversiDial. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("Hayes Micromodem II") +public class CardHayesMicromodem extends CardSSC { + + @Override + public String getDeviceName() { + return "Hayes Micromodem"; + } + public int RING_INDICATOR_REG = 5; + private boolean ringIndicator = false; + + public CardHayesMicromodem() { + ACIA_Data = 7; + ACIA_Status = 6; + ACIA_Control = 5; + ACIA_Command = 6; + // set these to high values will essentially NO-OP them. + SW1 = 255; + SW1_SETTING = 255; + SW2_CTS = 255; + RECV_IRQ_ENABLED = false; + TRANS_IRQ_ENABLED = false; + } + + @Override + public void clientConnected() { + setRingIndicator(true); + super.clientConnected(); + } + + @Override + public void clientDisconnected() { + setRingIndicator(false); + super.clientDisconnected(); + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + if (register == ACIA_Data) { + super.handleIOAccess(register, type, value, e); + return; + } + if (type.isRead() && register == RING_INDICATOR_REG) { + e.setNewValue(isRingIndicator() ? 0 : 255); + } else if (type.isRead() && register == ACIA_Status) { + e.setNewValue(getStatusValue()); + } else if (type == TYPE.WRITE && register == ACIA_Control) { + if ((value & 0x080) == 0) { + System.out.println("Software triggered disconnect"); + try { + if (clientSocket != null) { + clientSocket.getOutputStream().write("Disconnected by host\n".getBytes()); + } + } catch (IOException ex) { + System.out.println("Client disconnected before host"); + // If there's an error, ignore it. That means the client disconnected first. + } + // Hang up + hangUp(); + setRingIndicator(false); + } else { + System.out.println("Software answered connect request"); + try { + if (clientSocket != null) { + clientSocket.getOutputStream().write("Connected to emulated Apple\n".getBytes()); + } + } catch (IOException ex) { + Logger.getLogger(CardHayesMicromodem.class.getName()).log(Level.SEVERE, null, ex); + } + setRingIndicator(false); + } + } + } + + @Override + public void loadRom(String path) throws IOException { + // Do nothing -- there is no rom for this card right now. + } + + /** + * @return the ringIndicator + */ + public boolean isRingIndicator() { + return ringIndicator; + } + + /** + * @param ringIndicator the ringIndicator to set + */ + public void setRingIndicator(boolean ringIndicator) { + this.ringIndicator = ringIndicator; + } + + private int getStatusValue() { + int status = 0; + try { + // 0 = receive register full + if (inputAvailable()) { + status |= 0x01; + } + // 1 = transmit register empty -- always :-) + status |= 0x02; + // 2 = No Carrier + if (isRingIndicator() || !isConnected()) { + status |= 0x04; + } + } catch (Throwable ex) { + Logger.getLogger(CardHayesMicromodem.class.getName()).log(Level.SEVERE, null, ex); + } + return status; + } +} diff --git a/src/main/java/jace/hardware/CardMockingboard.java b/src/main/java/jace/hardware/CardMockingboard.java new file mode 100644 index 0000000..7a05d8f --- /dev/null +++ b/src/main/java/jace/hardware/CardMockingboard.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.core.Card; +import jace.core.Computer; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.RAMListener; +import jace.core.SoundMixer; +import static jace.core.Utility.*; +import jace.hardware.mockingboard.PSG; +import jace.hardware.mockingboard.R6522; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; + +/** + * Mockingboard-C implementation (with partial Phasor support). This uses two + * 6522 chips to communicate to two respective AY PSG sound chips. This class + * manages the I/O access as well as the sound playback thread. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("Mockingboard") +public class CardMockingboard extends Card implements Runnable { + // If true, emulation will cover 4 AY chips. Otherwise, only 2 AY chips + + static final int[] AY_ADDRESSES = new int[]{0, 0x080, 0x010, 0x090}; + @ConfigurableField(name = "Volume", shortName = "vol", + category = "Sound", + description = "Mockingboard volume, 100=max, 0=silent") + public int volume = 100; + static public int MAX_AMPLITUDE = 0x007fff; + @ConfigurableField(name = "Phasor mode", + category = "Sound", + description = "If enabled, card will have 4 sound chips instead of 2") + public boolean phasorMode = false; + @ConfigurableField(name = "Clock Rate (hz)", + category = "Sound", + defaultValue = "1020484", + description = "Clock rate of AY oscillators") + public int CLOCK_SPEED = 1020484; + public int SAMPLE_RATE = 48000; + @ConfigurableField(name = "Buffer size", + category = "Sound", + description = "Number of samples to generate on each pass") + public int BUFFER_LENGTH = 64; + // The array of configured AY chips + public PSG[] chips; + // The 6522 controllr chips (always 2) + public R6522[] controllers; + private int ticksBeteenPlayback = 5000; + Lock timerSync = new ReentrantLock(); + Condition cpuCountReached = timerSync.newCondition(); + Condition playbackFinished = timerSync.newCondition(); + @ConfigurableField(name = "Idle sample threshold", description = "Number of samples to wait before suspending sound") + private int MAX_IDLE_SAMPLES = SAMPLE_RATE; + + @Override + public String getDeviceName() { + return "Mockingboard"; + } + + public CardMockingboard() { + controllers = new R6522[2]; + for (int i = 0; i < 2; i++) { + //don't ask... + final int j = i; + controllers[i] = new R6522() { + @Override + public void sendOutputA(int value) { + if (activeChip != null) { + activeChip.setBus(value); + } else { + System.out.println("No active AY chip!"); + } + } + + @Override + public void sendOutputB(int value) { + if (activeChip != null) { + activeChip.setControl(value & 0x07); + } else { + System.out.println("No active AY chip!"); + } + } + + @Override + public int receiveOutputA() { + return activeChip == null ? 0 : activeChip.bus; + } + + @Override + public int receiveOutputB() { + return 0; + } + + @Override + public String getShortName() { + return "timer" + j; + } + }; + } + } + + @Override + public void reset() { + // Reset PSG registers + suspend(); + if (chips != null) { + for (PSG psg : chips) { + psg.reset(); + } + } + } + RAMListener mainListener = null; + PSG activeChip = null; + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { +// System.out.println(e.getType().toString() + " event to mockingboard register "+Integer.toHexString(register)+", value "+e.getNewValue()); + activeChip = null; + resume(); + int chip = 0; + for (PSG psg : chips) { + if (psg.getBaseReg() == (register & 0x0f0)) { + activeChip = psg; + break; + } + chip++; + } + if (activeChip == null) { + System.err.println("Could not determine which PSG to communicate to"); + e.setNewValue(Computer.getComputer().getVideo().getFloatingBus()); + return; + } + R6522 controller = controllers[chip & 1]; + if (e.getType().isRead()) { + int val = controller.readRegister(register & 0x0f); +// System.out.println("Register returns "+Integer.toHexString(val)); + e.setNewValue(val); + } else { + controller.writeRegister(register & 0x0f, e.getNewValue()); + } + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + // Oddly, all IO is done at the firmware address bank. It's a strange card. + e.setNewValue(Computer.getComputer().getVideo().getFloatingBus()); + } + long ticksSinceLastPlayback = 0; + + @Override + public void tick() { + for (R6522 c : controllers) { + if (c == null || !c.isRunning()) { + continue; + } + c.tick(); + } + + if (isRunning() && !pause) { + timerSync.lock(); + try { + ticksSinceLastPlayback++; + if (ticksSinceLastPlayback >= ticksBeteenPlayback) { + cpuCountReached.signalAll(); + while (isRunning() && ticksSinceLastPlayback >= ticksBeteenPlayback) { + if (!playbackFinished.await(1, TimeUnit.SECONDS)) { + gripe("The mockingboard playback thread has stalled. Disabling mockingboard."); + suspend(); + } + } + } + } catch (InterruptedException ex) { + suspend(); + // Do nothing, probably suspending CPU + } finally { + timerSync.unlock(); + } + } + } + + @Override + public void reconfigure() { + boolean restart = suspend(); + initPSG(); + for (PSG chip : chips) { + chip.setRate(CLOCK_SPEED, SAMPLE_RATE); + chip.reset(); + } + super.reconfigure(); + if (restart) { + resume(); + } + } +/////////////////////////////////////////////////////////// + public static int[] VolTable; + int[][] buffers; + int bufferLength = -1; + + public void playSound(int[] left, int[] right) { + chips[0].update(left, true, left, false, left, false, BUFFER_LENGTH); + chips[1].update(right, true, right, false, right, false, BUFFER_LENGTH); + if (phasorMode) { + chips[2].update(left, false, left, false, left, false, BUFFER_LENGTH); + chips[3].update(right, false, right, false, right, false, BUFFER_LENGTH); + } + } + + public void buildMixerTable() { + VolTable = new int[16]; + int numChips = phasorMode ? 4 : 2; + + /* calculate the volume->voltage conversion table */ + /* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step) */ + /* The YM2149 still has 16 levels for the tone generators, but 32 for */ + /* the envelope generator (1.5dB per step). */ + double out = ((double) MAX_AMPLITUDE * (double) volume) / 100.0; + // Reduce max amplitude to reflect post-mixer values so we don't have to scale volume when mixing channels + out = out * 2.0 / 3.0 / numChips; + double delta = 1.15; + for (int i = 15; i > 0; i--) { + VolTable[i] = (int) Math.round(out); /* round to nearest */ // [TC: unsigned int cast] +// out /= 1.188502227; /* = 10 ^ (1.5/20) = 1.5dB */ +// out /= 1.15; /* = 10 ^ (3/20) = 3dB */ + delta += 0.0225; + out /= delta; // As per applewin's source, the levels don't scale as documented. + } + VolTable[0] = 0; + } + Thread playbackThread = null; + boolean pause = false; + + @Override + public void resume() { + pause = false; + if (!isRunning()) { + if (chips == null) { + initPSG(); + } + for (R6522 controller : controllers) { + controller.attach(); + controller.resume(); + } + } + super.resume(); + if (playbackThread == null || !playbackThread.isAlive()) { + playbackThread = new Thread(this, "Mockingboard sound playback"); + playbackThread.start(); + } + } + + @Override + public boolean suspend() { + super.suspend(); + for (R6522 controller : controllers) { + controller.suspend(); + controller.detach(); + } + if (playbackThread == null || !playbackThread.isAlive()) { + return false; + } + if (playbackThread != null) { + playbackThread.interrupt(); + try { + // Wait for thread to die + playbackThread.join(); + } catch (InterruptedException ex) { + } + } + playbackThread = null; + return true; + } + + @Override + /** + * This is the audio playback thread + */ + public void run() { + try { + SourceDataLine out = Motherboard.mixer.getLine(this); + int[] leftBuffer = new int[BUFFER_LENGTH]; + int[] rightBuffer = new int[BUFFER_LENGTH]; + int frameSize = out.getFormat().getFrameSize(); + byte[] buffer = new byte[BUFFER_LENGTH * frameSize]; + System.out.println("Mockingboard playback started"); + int bytesPerSample = frameSize / 2; + buildMixerTable(); + ticksBeteenPlayback = (int) ((Motherboard.SPEED * BUFFER_LENGTH) / SAMPLE_RATE); + ticksSinceLastPlayback = 0; + int zeroSamples = 0; + while (isRunning()) { + Motherboard.requestSpeed(this); + playSound(leftBuffer, rightBuffer); + int p = 0; + for (int idx = 0; idx < BUFFER_LENGTH; idx++) { + int sampleL = leftBuffer[idx]; + int sampleR = rightBuffer[idx]; + // Convert left + right samples into buffer format + if (sampleL == 0 && sampleR == 0) { + zeroSamples++; + } else { + zeroSamples = 0; + } + for (int shift = SoundMixer.BITS - 8, index = 0; shift >= 0; shift -= 8, index++) { + buffer[p + index] = (byte) (sampleR >> shift); + buffer[p + index + bytesPerSample] = (byte) (sampleL >> shift); + } + p += frameSize; + } + try { + timerSync.lock(); + ticksSinceLastPlayback -= ticksBeteenPlayback; + } finally { + timerSync.unlock(); + } + out.write(buffer, 0, buffer.length); + if (zeroSamples >= MAX_IDLE_SAMPLES) { + zeroSamples = 0; + pause = true; + Motherboard.cancelSpeedRequest(this); + while (pause && isRunning()) { + try { + Thread.sleep(50); + timerSync.lock(); + playbackFinished.signalAll(); + } catch (InterruptedException ex) { + return; + } catch (IllegalMonitorStateException ex) { + // Do nothing + } finally { + try { + timerSync.unlock(); + } catch (IllegalMonitorStateException ex) { + // Do nothing -- this is probably caused by a suspension event + } + } + } + } + try { + timerSync.lock(); + playbackFinished.signalAll(); + while (isRunning() && ticksSinceLastPlayback < ticksBeteenPlayback) { + cpuCountReached.await(); + } + } catch (InterruptedException ex) { + // Do nothing, probably killing playback thread on purpose + } finally { + timerSync.unlock(); + } + } + } catch (LineUnavailableException ex) { + Logger.getLogger(CardMockingboard.class + .getName()).log(Level.SEVERE, null, ex); + } finally { + Motherboard.cancelSpeedRequest(this); + System.out.println("Mockingboard playback stopped"); + Motherboard.mixer.returnLine(this); + } + } + + private void initPSG() { + int max = phasorMode ? 4 : 2; + chips = new PSG[max]; + for (int i = 0; i < max; i++) { + chips[i] = new PSG(AY_ADDRESSES[i], CLOCK_SPEED, SAMPLE_RATE, "AY" + i); + } + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // There is no c8 rom access to emulate + } + + // This fixes freezes when resizing the window, etc. + @Override + public boolean suspendWithCPU() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/CardRamFactor.java b/src/main/java/jace/hardware/CardRamFactor.java new file mode 100644 index 0000000..6dc0a41 --- /dev/null +++ b/src/main/java/jace/hardware/CardRamFactor.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.Emulator; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.core.Card; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.state.Stateful; +import jace.core.Utility; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.ImageIcon; + +/** + * This card strives to be a clone of the Applied Engineering RamFactor card + * http://www.downloads.reactivemicro.com/Public/Apple%20II%20Items/Hardware/RAMFactor/RAMFactor%20v1.5.pdf + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +@Name("RamFactor") +public class CardRamFactor extends Card { + int ADDR1 = 0; + int ADDR2 = 1; + int ADDR3 = 2; + int DATA = 3; + int BANK_SELECT = 0x0f; + @ConfigurableField(category = "memory", name = "Ram size", description = "Size of card ram in KB", shortName = "size", defaultValue = "8192") + public int RAM_SIZE = 8192; + int actualSize = RAM_SIZE * 1024; + // Important: pointer of current ram read/write for slinky access + @Stateful + int addressPointer = 0x0ffffff; + @Stateful + int firmwareBank = 0; + @ConfigurableField(category = "performance", name = "Speed Boost", description = "Boost emulator speed when RAM in use", shortName = "boostSpeed", defaultValue = "false") + public boolean speedBoost = false; + + @Override + public String getDeviceName() { + return "RamFactor"; + } + ImageIcon indicator; + public CardRamFactor() { + indicator=Utility.loadIcon("ram.png"); + try { + loadRom("jace/data/RAMFactor14.rom"); + } catch (IOException ex) { + Logger.getLogger(CardRamFactor.class.getName()).log(Level.SEVERE, null, ex); + } + allocateMemory(actualSize); + updateFirmwareMemory(); + } + + @Override + public void reset() { + firmwareBank = 0; + updateFirmwareMemory(); + } + + @Override + public void reconfigure() { + actualSize = RAM_SIZE * 1024; + allocateMemory(actualSize); + updateFirmwareMemory(); + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + Emulator.getFrame().addIndicator(this, indicator); + value &= 0x0ff; + switch (register) { + case 0: + case 4: + // Lo-byte of pointer + if (type.isRead()) { + e.setNewValue(addressPointer & 0x0ff); + } else { + addressPointer = (addressPointer & 0x0ffff00) | value; + if (RAM_SIZE <= 1024) { + addressPointer |= 0x0f00000; + } + } + break; + case 1: + case 5: + // Mid-byte of pointer + if (type.isRead()) { + e.setNewValue((addressPointer >> 8) & 0x0ff); + } else { + addressPointer = (addressPointer & 0x0ff00ff) | (value << 8); + if (RAM_SIZE <= 1024) { + addressPointer |= 0x0f00000; + } + } + break; + case 2: + case 6: + // Hi-byte of pointer + if (type.isRead()) { + if (RAM_SIZE <= 1024) { + e.setNewValue(0x0f0 | ((addressPointer >> 16) & 0x0ff)); + } else { + e.setNewValue((addressPointer >> 16) & 0x0ff); + } + } else { + addressPointer = (addressPointer & 0x00ffff) | (value << 16); + if (RAM_SIZE <= 1024) { + addressPointer |= 0x0f00000; + } + } + break; + case 3: + case 7: + if (type.isRead()) { + e.setNewValue(readMemory(addressPointer)); + } else { + writeMemory(addressPointer, (byte) value); + } + addressPointer++; + // Keep the pointer in range + addressPointer &= 0x0ffffff; + break; + case 15: { + // Firmware bank select + if (type == TYPE.WRITE) { + firmwareBank = value; + updateFirmwareMemory(); + } + } + default: + if (type.isRead()) { + e.setNewValue(0x0ff); + } + break; + } + } + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + if (speedBoost) { + Motherboard.requestSpeed(this); + } + } + + @Override + public void tick() { + // Do nothing + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + if (speedBoost) { + Motherboard.requestSpeed(this); + } + } + + @Stateful + public byte[] cardRam; + int ADDRESS_MASK = 0x07FFFFF; + private byte readMemory(int i) { + while (i >= cardRam.length) { + i -= cardRam.length; + } + return cardRam[i]; + } + + private void writeMemory(int i, byte newValue) { + while (i >= cardRam.length) { + i -= cardRam.length; + } + cardRam[i] = newValue; + } + + private void allocateMemory(int size) { + if (cardRam != null && cardRam.length == size) return; + cardRam = new byte[size]; + Arrays.fill(cardRam, (byte) 0); + } + + @Override + public void setSlot(int slot) { + super.setSlot(slot); + indicator.setDescription("Slot "+getSlot()); + // Rom has different images for each slot + updateFirmwareMemory(); + } + + final int cxRomLength = 0x02000; + byte[] romData = new byte[cxRomLength]; + public void loadRom(String path) throws IOException { + InputStream romFile = CardRamFactor.class.getClassLoader().getResourceAsStream(path); + try { + if (romFile.read(romData) != cxRomLength) { + throw new IOException("Bad RamFactor rom size"); + } + updateFirmwareMemory(); + } catch (IOException ex) { + throw ex; + } + } + + private void updateFirmwareMemory() { + int romOffset = 0; + if ((firmwareBank&1) == 1) { + romOffset = 0x01000; + } + getCxRom().loadData(romData, romOffset + getSlot()*0x0100, 256); + getC8Rom().loadData(romData, romOffset + 0x0800, 0x0800); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/CardRamworks.java b/src/main/java/jace/hardware/CardRamworks.java new file mode 100644 index 0000000..a22bb41 --- /dev/null +++ b/src/main/java/jace/hardware/CardRamworks.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.RAM128k; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.core.Computer; +import jace.core.PagedMemory; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.state.Stateful; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * Emulates the Ramworks Basic and Ramworks III cards + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +@Name("Ramworks III Memory Expansion") +public class CardRamworks extends RAM128k { + public static int BANK_SELECT = 0x0c073; + @Stateful + public int currentBank = 0; + @Stateful + public List<Map<BankType, PagedMemory>> memory; + public Map<BankType, PagedMemory> nullBank = generateBank(); + @ConfigurableField( + category = "memory", + defaultValue = "3072", + name = "Memory Size", + description = "Size in KB. Should be a multiple of 64 and not exceed 8192. The real card cannot support more than 3072k") + public int memorySize = 3072; + public int maxBank = memorySize / 64; + private Map<BankType, PagedMemory> generateBank() { + Map<BankType, PagedMemory> memoryBank = new EnumMap<BankType, PagedMemory>(BankType.class); + memoryBank.put(BankType.MAIN_MEMORY, new PagedMemory(0xc000, PagedMemory.Type.ram)); + memoryBank.put(BankType.LANGUAGE_CARD_1, new PagedMemory(0x3000, PagedMemory.Type.languageCard)); + memoryBank.put(BankType.LANGUAGE_CARD_2, new PagedMemory(0x1000, PagedMemory.Type.languageCard)); + return memoryBank; + } + + public static enum BankType { + MAIN_MEMORY, LANGUAGE_CARD_1, LANGUAGE_CARD_2 + }; + + public CardRamworks() { + super(); + memory = new ArrayList<Map<BankType, PagedMemory>>(maxBank); + reconfigure(); + } + + private PagedMemory getAuxBank(BankType type, int bank) { + if (bank >= maxBank) { + return nullBank.get(type); + } + Map<BankType, PagedMemory> memoryBank = memory.get(bank); + if (memoryBank == null) { + memoryBank = generateBank(); + memory.set(bank, memoryBank); + } + return memoryBank.get(type); + } + + @Override + public PagedMemory getAuxVideoMemory() { + return getAuxBank(BankType.MAIN_MEMORY, 0); + } + + PagedMemory lastAux = null; + @Override + public PagedMemory getAuxMemory() { + return getAuxBank(BankType.MAIN_MEMORY, currentBank); + } + + @Override + public PagedMemory getAuxLanguageCard() { + return getAuxBank(BankType.LANGUAGE_CARD_1, currentBank); + } + + @Override + public PagedMemory getAuxLanguageCard2() { + return getAuxBank(BankType.LANGUAGE_CARD_2, currentBank); + } + + @Override + public String getName() { + return "Ramworks III"; + } + + @Override + public String getShortName() { + return "Ramworks3"; + } + + @Override + public void reconfigure() { + boolean resume = Computer.pause(); + maxBank = memorySize / 64; + if (maxBank < 1) maxBank = 1; + if (maxBank > 128) maxBank = 128; + for (int i = memory.size(); i < maxBank; i++) { + memory.add(null); + } + configureActiveMemory(); + if (resume) { + Computer.resume(); + } + } + RAMListener bankSelectListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(BANK_SELECT); + setScopeEnd(BANK_SELECT); + } + + @Override + protected void doEvent(RAMEvent e) { + currentBank = e.getNewValue(); + configureActiveMemory(); + } + }; + + @Override + public void attach() { + addListener(bankSelectListener); + } + + @Override + public void detach() { + removeListener(bankSelectListener); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/CardSSC.java b/src/main/java/jace/hardware/CardSSC.java new file mode 100644 index 0000000..b8af34a --- /dev/null +++ b/src/main/java/jace/hardware/CardSSC.java @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.Emulator; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.config.Reconfigurable; +import jace.core.Card; +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.Utility; +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.ImageIcon; + +/** + * Super Serial Card with serial-over-tcp/ip support. This is fully compatible + * with the SSC ROM and supported applications. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("Super Serial Card") +public class CardSSC extends Card implements Reconfigurable, Runnable { + + @ConfigurableField(name = "TCP/IP Port", shortName = "port") + public static short IP_PORT = 1977; + protected ServerSocket socket; + protected Socket clientSocket; + protected Thread listenThread; + private int lastInputByte = 0; + private boolean FULL_ECHO = true; + private boolean RECV_ACTIVE = true; + private boolean TRANS_ACTIVE = true; +// private boolean RECV_STRIP_LF = true; +// private boolean TRANS_ADD_LF = true; + @ConfigurableField(name = "Strip LF (recv)", shortName = "stripLF", defaultValue = "false", description = "Strip incoming linefeeds") + public boolean RECV_STRIP_LF = false; + @ConfigurableField(name = "Add LF (send)", shortName = "addLF", defaultValue = "false", description = "Append linefeeds after outgoing carriage returns") + public boolean TRANS_ADD_LF = false; + private boolean DTR = true; + public int SW1 = 0x01; // Read = Jumper block SW1 + //Bit 0 = !SW1-6 + //Bit 1 = !SW1-5 + //Bit 4 = !SW1-4 + //Bit 5 = !SW1-3 + //Bit 6 = !SW1-2 + //Bit 7 = !SW1-1 + // 19200 baud (SW1-1,2,3,4 off) + // Communications mode (SW1-5,6 on) + public int SW1_SETTING = 0x0F0; + public int SW2_CTS = 0x02; // Read = Jumper block SW2 and CTS + //Bit 0 = !CTS + //SW2-6 = Allow interrupts (disable in ][, ][+) + //Bit 1 = !SW2-5 -- Generate LF after CR + //Bit 2 = !SW2-4 + //Bit 3 = !SW2-3 + //Bit 5 = !SW2-2 + //Bit 7 = !SW2-1 + // 1 stop bit (SW2-1 on) + // 8 data bits (SW2-2 on) + // No parity (SW2-3 don't care, SW2-4 off) + private static int SW2_SETTING = 0x04; + public int ACIA_Data = 0x08; // Read=Receive / Write=transmit + public int ACIA_Status = 0x09; // Read=Status / Write=Reset + public int ACIA_Command = 0x0A; + public int ACIA_Control = 0x0B; + public boolean PORT_CONNECTED = false; + public boolean RECV_IRQ_ENABLED = false; + public boolean TRANS_IRQ_ENABLED = false; + public boolean IRQ_TRIGGERED = false; + // Bitmask for stop bits (FF = 8, 7F = 7, etc) + private int DATA_BITS = 0x07F; + + public String getDeviceName() { + return "Super Serial Card"; + } + ImageIcon activityIndicator; + + @Override + public void setSlot(int slot) { + try { + loadRom("jace/data/SSC.rom"); + } catch (IOException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + } + super.setSlot(slot); + activityIndicator = Utility.loadIcon("network-wired.png"); + activityIndicator.setDescription("Slot " + slot); + } + + @Override + public void run() { + while (socket != null && !socket.isClosed()) { + try { + Logger.getLogger(CardSSC.class.getName()).log(Level.INFO, "Slot " + getSlot() + " listening on port " + IP_PORT, (Throwable) null); +// System.out.println("Waiting for connect"); + while ((clientSocket = socket.accept()) != null) { + clientConnected(); + clientSocket.setTcpNoDelay(true); + while (isConnected()) { + try { + Thread.sleep(livenessCheck / 2); + } catch (InterruptedException ex) { + // Do nothing + } + } + clientDisconnected(); + hangUp(); + } + } catch (SocketTimeoutException ex) { + // Do nothing + } catch (IOException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.FINE, null, ex); + } + } + socket = null; + } + + // Called when a client first connects via telnet + public void clientConnected() { + System.err.println("Client connected"); + } + + // Called when a client disconnects + public void clientDisconnected() { + System.out.println("Client disconnected"); + } + + public void loadRom(String path) throws IOException { + // Load rom file, first 0x0700 bytes are C8 rom, last 0x0100 bytes are CX rom + // CF00-CFFF are unused by the SSC + InputStream romFile = CardSSC.class.getClassLoader().getResourceAsStream(path); + final int cxRomLength = 0x0100; + final int c8RomLength = 0x0700; + byte[] romxData = new byte[cxRomLength]; + byte[] rom8Data = new byte[c8RomLength]; + try { + if (romFile.read(rom8Data) != c8RomLength) { + throw new IOException("Bad SSC rom size"); + } + getC8Rom().loadData(rom8Data); + if (romFile.read(romxData) != cxRomLength) { + throw new IOException("Bad SSC rom size"); + } + getCxRom().loadData(romxData); + } catch (IOException ex) { + throw ex; + } + } + + @Override + public void reset() { + java.awt.EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + suspend(); + resume(); + } + }); + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + try { + int newValue = -1; + switch (type) { + case EXECUTE: + case READ_OPERAND: + case READ_DATA: + case READ: + if (register == SW1) { + newValue = SW1_SETTING; + } + if (register == SW2_CTS) { + newValue = SW2_SETTING & 0x0FE; + // if port is connected and ready to send another byte, set CTS bit on + newValue |= (PORT_CONNECTED && inputAvailable()) ? 0x00 : 0x01; + } + if (register == ACIA_Data) { + Emulator.getFrame().addIndicator(this, activityIndicator); + newValue = getInputByte(); + if (RECV_IRQ_ENABLED) { + triggerIRQ(); + } + } + if (register == ACIA_Status) { + newValue = 0; + // 0 = Parity error (1) + // 1 = Framing error (1) + // 2 = Overrun error (1) + // 3 = ACIA Receive Register full (1) + if (inputAvailable()) { + newValue |= 0x08; + } + // 4 = ACIA Transmit Register empty (1) + if (true) { + newValue |= 0x010; + } + // 5 = Data Carrier Detect (DCD) true (0) + // 6 = Data Set Ready (DSR) true (0) + // 7 = Interrupt (IRQ) has occurred + if (IRQ_TRIGGERED) { + newValue |= 0x080; + } + IRQ_TRIGGERED = false; + } + if (register == ACIA_Command) { + newValue = 0; + // 0 = DTR Enable (1) / Disable (0) receiver and IRQ + // 1 = Allow IRQ (1) when status bit 3 is true + if (RECV_IRQ_ENABLED) { + newValue |= 2; + } + // 2,3 = Control transmit IRQ, RTS level and transmitter + newValue |= 12; + // 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0) + if (FULL_ECHO) { + newValue |= 16; + } + // 5 = Control parity + } + if (register == ACIA_Control) { + // 0-3 = Baud Rate + // 4 = Use baud rate generator (1) / Use external clock (0) + // 5-6 = Number of data bits (00 = 8, 10 = 7, 01 = 6, 11 = 5) + // 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2) + newValue = 0; + } + break; + case WRITE: + if (register == ACIA_Data) { + Emulator.getFrame().addIndicator(this, activityIndicator); + sendOutputByte(value & 0x0FF); + if (TRANS_IRQ_ENABLED) { + triggerIRQ(); + } + } + if (register == ACIA_Command) { + // 0 = DTR Enable (1) / Disable (0) receiver and IRQ + DTR = ((value & 1) == 0); + // 0 = Allow IRQ (0) when status bit 3 is true + if ((value & 2) == 0) { + RECV_IRQ_ENABLED = !DTR; + } else { + RECV_IRQ_ENABLED = false; + } + // 2,3 = Control transmit IRQ, RTS level and transmitter + // 0 0 = Transmit interrupt off, RTS high, Transmitter off + // 1 0 = Transmit interrupt ON, RTS low, Transmitter on + // 0 1 = Transmit interrupt off, RTS low, Transmitter on + // 1 1 = Transmit interrupt off, RTS low, Transmit BRK + switch ((value >> 2) & 3) { + case 0: + TRANS_IRQ_ENABLED = false; + TRANS_ACTIVE = false; + break; + case 1: + TRANS_IRQ_ENABLED = true; + TRANS_ACTIVE = true; + break; + case 2: + TRANS_IRQ_ENABLED = false; + TRANS_ACTIVE = true; + break; + case 3: + TRANS_IRQ_ENABLED = false; + TRANS_ACTIVE = true; + break; + } + // 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0) + FULL_ECHO = ((value & 16) > 0); + System.out.println("Echo set to " + FULL_ECHO); + // 5 = Control parity + } + if (register == ACIA_Control) { + // 0-3 = Baud Rate + // 4 = Use baud rate generator (1) / Use external clock (0) + // 5-6 = Number of data bits (00 = 8, 01 = 7, 10 = 6, 11 = 5) + // 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2) + int bits = (value & 127) >> 5; + System.out.println("Data bits set to " + (8 - bits)); + switch (bits) { + case 0: + DATA_BITS = 0x0FF; + break; + case 1: + DATA_BITS = 0x07F; + break; + case 2: + DATA_BITS = 0x03F; + break; + case 3: + DATA_BITS = 0x01F; + break; + } + } + break; + } + if (newValue > -1) { + e.setNewValue(newValue); + value = newValue; + } +// System.out.println("SSC I/O "+type+", register "+register+", value "+value); + } catch (IOException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + } + } + + @Override + public void tick() { + // Do nothing + } + + public boolean inputAvailable() throws IOException { + if (isConnected() && clientSocket != null && clientSocket.getInputStream() != null) { + return clientSocket.getInputStream().available() > 0; + } else { + return false; + } + } + + private int getInputByte() throws IOException { + if (inputAvailable()) { + int in = clientSocket.getInputStream().read() & DATA_BITS; +// System.out.write(in & 0x07f); + if (RECV_STRIP_LF && in == 10 && lastInputByte == 13) { + in = clientSocket.getInputStream().read() & DATA_BITS; +// System.out.write(in & 0x07f); + } +// System.out.flush(); + lastInputByte = in; + } + return lastInputByte; + } + long lastSuccessfulWrite = -1L; + + private void sendOutputByte(int i) throws IOException { + if (clientSocket != null && clientSocket.isConnected()) { + try { +// System.out.write(i & 0x07f); + clientSocket.getOutputStream().write(i & DATA_BITS); + if (TRANS_ADD_LF && (i & DATA_BITS) == 13) { +// System.out.write(10); + clientSocket.getOutputStream().write(10); + } + clientSocket.getOutputStream().flush(); + lastSuccessfulWrite = System.currentTimeMillis(); + } catch (IOException e) { + lastSuccessfulWrite = -1L; + hangUp(); + } + } else { + lastSuccessfulWrite = -1L; + } + } + + private void setCTS(boolean b) throws InterruptedException { + PORT_CONNECTED = b; + if (b == false) { + reset(); + } + } + + private boolean getCTS() throws InterruptedException { + return PORT_CONNECTED; + } + + private void triggerIRQ() { + IRQ_TRIGGERED = true; + Computer.getComputer().getCpu().generateInterrupt(); + } + + public void hangUp() { + lastInputByte = 0; + lastSuccessfulWrite = -1L; + if (clientSocket != null && clientSocket.isConnected()) { + try { + clientSocket.shutdownInput(); + clientSocket.shutdownOutput(); + clientSocket.close(); + } catch (IOException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + } + } + clientSocket = null; + } + + /** + * Detach from server socket port and ensure that the card's resources are + * no longer in use + */ + @Override + public boolean suspend() { + if (socket != null) { + try { + socket.close(); + } catch (IOException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + } + } + hangUp(); + if (listenThread != null && listenThread.isAlive()) { + try { + listenThread.join(); + } catch (InterruptedException ex) { + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + } + } + listenThread = null; + socket = null; + return super.suspend(); + } + + @Override + public void resume() { + if (!isRunning()) { + super.resume(); + RECV_IRQ_ENABLED = false; + TRANS_IRQ_ENABLED = false; + IRQ_TRIGGERED = false; + + try { + socket = new ServerSocket(IP_PORT); + socket.setReuseAddress(true); + socket.setSoTimeout(0); + //socket.setReuseAddress(true); + listenThread = new Thread(this); + listenThread.setDaemon(false); + listenThread.setName("SSC port listener"); + listenThread.start(); + } catch (IOException ex) { + suspend(); + Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); + ex.printStackTrace(); + } + } + } + @ConfigurableField(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life (in milliseconds)") + public int livenessCheck = 10000; + + public boolean isConnected() { + if (clientSocket == null || !clientSocket.isConnected()) { + return false; + } + if (lastSuccessfulWrite == -1 || System.currentTimeMillis() > (lastSuccessfulWrite + livenessCheck)) { + try { + sendOutputByte(0); + return true; + } catch (IOException e) { + return false; + } + } else { + return true; + } + } + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // Do nothing -- the card rom does everything + return; + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // There is no special c8 rom behavior for this card + } +} diff --git a/src/main/java/jace/hardware/CardThunderclock.java b/src/main/java/jace/hardware/CardThunderclock.java new file mode 100644 index 0000000..e506241 --- /dev/null +++ b/src/main/java/jace/hardware/CardThunderclock.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.Emulator; +import jace.apple2e.MOS65C02; +import jace.config.ConfigurableField; +import jace.config.Name; +import jace.core.Card; +import jace.core.Computer; +import jace.core.Motherboard; +import jace.core.PagedMemory; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.Utility; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.ImageIcon; + +/** + * Implementation of the Thunderclock Plus with some limitations: + * + * The apple cannot set time. The firmware will act like it is working but + * nothing will actually happen when a time set command is sent. + * + * Though the interrupt features are implemented, they have not been tested. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("ThunderClock Plus") +public class CardThunderclock extends Card { + + ImageIcon clockIcon; + ImageIcon clockFixIcon; + long lastShownIcon = -1; + // Only mention that the clock is read if it hasn't been checked for over 30 seconds + // This is to avoid showing it all the time in programs that poll it constantly + long MIN_WAIT = 30000; + @ConfigurableField(category = "OS", name = "Patch Prodos Year", description = "If enabled, the Prodos clock driver will be patched to use the current year.") + public boolean attemptYearPatch = true; + + public CardThunderclock() { + try { + loadRom("jace/data/thunderclock_plus.rom"); + } catch (IOException ex) { + Logger.getLogger(CardDiskII.class.getName()).log(Level.SEVERE, null, ex); + } + clockIcon = Utility.loadIcon("clock.png"); + clockFixIcon = Utility.loadIcon("clock_fix.png"); + clockFixIcon.setDescription("Fixed"); + } + + // Raw format: 40 bits, in BCD form (it actually streams out in the reverse order of this, bit 0 first) + // The data format is fully elaborated in the datasheet of the calendar/clock chip: NEC uPD1990AC + // month (1-12) -- hex + // day of week (0-6) + // day of month, tens digit (0-3) + // day of month, ones digit (0-9) + // hour, tens digit (0-2) + // hour, ones digit (0-9) + // minute, tens digit (0-5) + // minute, ones digit (0-9) + // second, tens digit (0-5) + // second, ones digit (0-9) + @Override + public void reset() { + irqAsserted = false; + irqEnabled = false; + ticks = 0; + timerRate = 0; + } + public boolean strobe = false; + public boolean clock = false; + public boolean shiftMode = false; + public boolean irqEnabled = false; + public boolean irqAsserted = false; + public boolean timerEnabled = false; + public int timerRate = 0; + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + // Data is read via bit-banging the status register + // Nibbles are sent lowest significant bit first. + // Commands are sent to the register followed by a strobe pulse on bit 2 on and off + // So senting the time read command would be a string of bytes: 0x018, 0x01c and then 0x018 again + // + // Time read is signaled by 0x018 followed by a register shift command 0x08 + // When register shift is active, a clock signal is used to move to the next bit. + // + // A bit is placed in data-in (bit 0) + // Then the clock is raised (bit 1 set) and then lowered (bit 1 unset) + // After this, the next time the register is read it will have the next bit + // of the register in the hibit (bit 7) + // + // Reg 0: Command register + // data in = 0x01 + // clock = 0x02 + // strobe = 0x04 + // register hold = 0x0 + // register shift = 0x08 + // time set = 0x010 + // time read = 0x018 + // Timer modes = 0x020 (64hz), 0x028 (256hz), 0x030 (2048hz) + // Interrupt enable = 0x040 (IRQ assert is read as 0x020 in the status register) + // data out = 0x080 + if (type.isRead() && register == 0) { + e.setNewValue((peekBit()) | (irqAsserted ? 0x020 : 0)); + return; + } + + if (register == 8) { + irqAsserted = false; + return; + } else if (register != 0) { + return; + } + + boolean isClock = (value & 0x02) != 0; + boolean isStrobe = (value & 0x04) != 0; + boolean isShift = (value & 0x08) != 0; + boolean isRead = (value & 0x18) != 0; + + if (!isClock && clock) { + if (buffer != null) { + buffer.pop(); + } + } + + if (!isStrobe && strobe) { + shiftMode = isShift; + if (isRead) { + if (attemptYearPatch) { + performProdosPatch(); + } + getTime(); + clockIcon.setDescription("Slot " + getSlot()); + long now = System.currentTimeMillis(); + if ((now - lastShownIcon) > MIN_WAIT) { + Emulator.getFrame().addIndicator(this, clockIcon, 3000); + } + lastShownIcon = now; + } + shiftMode = isShift; + } + + timerEnabled = (value & 0x020) != 0; + ticks = 0; + if (timerEnabled) { + switch (value & 0x038) { + case 0x020: + timerRate = (int) (Motherboard.SPEED / 64); + break; + case 0x028: + timerRate = (int) (Motherboard.SPEED / 256); + break; + case 0x030: + timerRate = (int) (Motherboard.SPEED / 2048); + break; + default: + timerEnabled = false; + timerRate = 0; + } + } else { + timerRate = 0; + } + + irqEnabled = (value & 0x040) != 0; + clock = isClock; + strobe = isStrobe; + } + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // Firmware ROM is used -- only I/O port was needed for proper emulation + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // C8 access is used to read the clock directly + } + + @Override + protected String getDeviceName() { + return "Thunderclock Plus"; + } + + int ticks = 0; + @Override + public void tick() { + if (timerEnabled) { + ticks++; + if (ticks >= timerRate) { + ticks = 0; + irqAsserted = true; + if (irqEnabled) { + Computer.getComputer().getCpu().generateInterrupt(); + } + } + } + } + + private void getTime() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(System.currentTimeMillis()); + clearBuffer(); + pushNibble(cal.get(Calendar.MONTH) + 1); + pushNibble(cal.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY); + pushNibble(cal.get(Calendar.DAY_OF_MONTH) / 10); + pushNibble(cal.get(Calendar.DAY_OF_MONTH) % 10); + pushNibble(cal.get(Calendar.HOUR_OF_DAY) / 10); + pushNibble(cal.get(Calendar.HOUR_OF_DAY) % 10); + pushNibble(cal.get(Calendar.MINUTE) / 10); + pushNibble(cal.get(Calendar.MINUTE) % 10); + pushNibble(cal.get(Calendar.SECOND) / 10); + pushNibble(cal.get(Calendar.SECOND) % 10); + } + Stack<Boolean> buffer; + + private void clearBuffer() { + if (buffer == null) { + buffer = new Stack<Boolean>(); + } else { + buffer.clear(); + } + } + + private void pushNibble(int value) { + for (int i = 0; i < 4; i++) { + boolean val = (value & 8) != 0; + buffer.push(val); + value <<= 1; + } + } + + private int peekBit() { + if (buffer == null || buffer.isEmpty()) { + return 0; + } + return buffer.peek() ? 0x080 : 0; + } + + public void loadRom(String path) throws IOException { + InputStream romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path); + final int cxRomLength = 0x0100; + final int c8RomLength = 0x0700; + byte[] romxData = new byte[cxRomLength]; + byte[] rom8Data = new byte[c8RomLength]; + try { + if (romFile.read(romxData) != cxRomLength) { + throw new IOException("Bad Thunderclock rom size"); + } + getCxRom().loadData(romxData); + romFile.close(); + romFile = CardThunderclock.class.getClassLoader().getResourceAsStream(path); + if (romFile.read(rom8Data) != c8RomLength) { + throw new IOException("Bad Thunderclock rom size"); + } + getC8Rom().loadData(rom8Data); + romFile.close(); + } catch (IOException ex) { + throw ex; + } + } + static byte[] DRIVER_PATTERN = { + (byte) 0x00, (byte) 0x01f, (byte) 0x03b, (byte) 0x05a, + (byte) 0x078, (byte) 0x097, (byte) 0x0b5, (byte) 0x0d3, + (byte) 0x0f2 + }; + static int DRIVER_OFFSET = -26; + int patchLoc = -1; + + /** + * Scan active memory for the Prodos clock driver and patch the internal + * code to use a fixed value for the present year. This means Prodos will + * always tell time correctly. + */ + private void performProdosPatch() { + PagedMemory ram = Computer.getComputer().getMemory().activeRead; + if (patchLoc > 0) { + // We've already patched, just validate + if (ram.readByte(patchLoc) == (byte) MOS65C02.OPCODE.LDA_IMM.getCode()) { + return; + } + } + int match = 0; + int matchStart = 0; + for (int addr = 0x08000; addr < 0x010000; addr++) { + if (ram.readByte(addr) == DRIVER_PATTERN[match]) { + match++; + if (match == DRIVER_PATTERN.length) { + break; + } + } else { + match = 0; + matchStart = addr; + } + } + if (match != DRIVER_PATTERN.length) { + return; + } + patchLoc = matchStart + DRIVER_OFFSET; + ram.writeByte(patchLoc, (byte) MOS65C02.OPCODE.LDA_IMM.getCode()); + int year = Calendar.getInstance().get(Calendar.YEAR) % 100; + ram.writeByte(patchLoc + 1, (byte) year); + ram.writeByte(patchLoc + 2, (byte) MOS65C02.OPCODE.NOP.getCode()); + ram.writeByte(patchLoc + 3, (byte) MOS65C02.OPCODE.NOP.getCode()); + Emulator.getFrame().addIndicator(this, clockFixIcon, 4000); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/ConsoleProbe.java b/src/main/java/jace/hardware/ConsoleProbe.java new file mode 100644 index 0000000..96128a9 --- /dev/null +++ b/src/main/java/jace/hardware/ConsoleProbe.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.SoftSwitches; +import jace.core.Computer; +import jace.core.Keyboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import java.awt.Rectangle; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Attempt at adding accessibility by redirecting screen/keyboard traffic to + * stdout/stdin of the console. Doesn't work well, unfortunately. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ConsoleProbe { + + public static boolean enabled = true; + public String[] lastScreen = new String[24]; + public List<Rectangle> regions = new ArrayList<Rectangle>(); + private RAMListener textListener; + public static long lastChange; + public static long updateDelay = 100L; + public static boolean readerActive = false; + public Computer computer; + private Thread keyReaderThread; + + public void init(final Computer c) { + computer = c; + enabled = true; + keyReaderThread = new Thread(new KeyReader()); + keyReaderThread.setName("Console probe key reader"); + keyReaderThread.start(); + textListener = new RAMListener(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0400); + setScopeEnd(0x0BFF); + } + + @Override + protected void doEvent(RAMEvent e) { + if (e.getAddress() < 0x0800 && SoftSwitches.PAGE2.isOn()) { + return; + } + if (SoftSwitches.TEXT.isOff()) { + if (SoftSwitches.MIXED.isOn()) { + handleMixedMode(); + } + } else { + handleTextMode(); + } + } + + private void handleMixedMode() { + handleTextMode(); + } + + private void handleTextMode() { + lastChange = System.currentTimeMillis(); + if (readerActive) { + return; + } + Thread t = new Thread(new ScreenReader()); + t.start(); + } + }; + c.getMemory().addListener(textListener); + } + + public static synchronized void performRead() { + } + + public void shutdown() { + enabled = false; + if (textListener != null) { + computer.getMemory().removeListener(textListener); + } + + if (keyReaderThread != null && keyReaderThread.isAlive()) { + try { + keyReaderThread.join(); + } catch (InterruptedException ex) { + Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + public static class ScreenReader implements Runnable { + + public void run() { + readerActive = true; + try { + // Keep sleeping until there have been no more screen changes during the specified delay period + // It is possible that the lastChange will keep being updated while in this loop + // That is both expected and the reason this is a loop! + long delay = 0; + while (System.currentTimeMillis() - lastChange <= updateDelay) { + delay = updateDelay - System.currentTimeMillis() - lastChange; + if (delay > 0) { + Thread.sleep(delay); + } + } + } catch (InterruptedException ex) { + Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex); + } + // Signal that we're off to go read the screen (and any additional update will need to spawn the thread again) + readerActive = false; + performRead(); + } + } + + public static class KeyReader implements Runnable { + + public Computer c; + + public void run() { + while (true) { + try { + while (enabled && (System.in.available() == 0 || Keyboard.readState() < 0)) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + System.out.println(ex.getMessage()); + Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex); + } + } + if (!enabled) { + return; + } + int ch = System.in.read(); + if (ch == 10) { + ch = 13; + } + Keyboard.pressKey((byte) ch); + } catch (IOException ex) { + System.out.println(ex.getMessage()); + Logger.getLogger(ConsoleProbe.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } +} diff --git a/src/main/java/jace/hardware/ConsoleProbeSimple.java b/src/main/java/jace/hardware/ConsoleProbeSimple.java new file mode 100644 index 0000000..3bc830f --- /dev/null +++ b/src/main/java/jace/hardware/ConsoleProbeSimple.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.MOS65C02; +import jace.core.Computer; +import jace.core.Keyboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import java.awt.Toolkit; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Attempt to simplify what ConsoleProbe was attempting. Still not ready for any + * real use. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ConsoleProbeSimple { + + RAMListener cout; + public static int COUT = 0xFDED; + + public void init(final Computer c) { + Thread t = new Thread(new KeyReader()); + t.start(); + + cout = new RAMListener(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(COUT); + } + + @Override + protected void doEvent(RAMEvent e) { + MOS65C02 cpu = (MOS65C02) c.getCpu(); + int ch = cpu.A & 0x07f; + if (ch == 13) { + System.out.println(); + } else if (ch < ' ') { + if (ch == 7) { + Toolkit.getDefaultToolkit().beep(); + } else { + System.out.println("CHR" + ch); + } + } else { + System.out.print((char) ch); + } + } + }; + c.getMemory().addListener(cout); + } + + public static class KeyReader implements Runnable { + + public Computer c; + + public void run() { + while (true) { + try { + while (System.in.available() == 0 || Keyboard.readState() < 0) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + System.out.println(ex.getMessage()); + Logger.getLogger(ConsoleProbeSimple.class.getName()).log(Level.SEVERE, null, ex); + } + } + int ch = System.in.read(); + if (ch == 10) { + ch = 13; + } + Keyboard.pressKey((byte) ch); + } catch (IOException ex) { + System.out.println(ex.getMessage()); + Logger.getLogger(ConsoleProbeSimple.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + } +} diff --git a/src/main/java/jace/hardware/DiskIIDrive.java b/src/main/java/jace/hardware/DiskIIDrive.java new file mode 100644 index 0000000..fd61906 --- /dev/null +++ b/src/main/java/jace/hardware/DiskIIDrive.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.library.MediaConsumer; +import jace.library.MediaEntry; +import jace.library.MediaEntry.MediaFile; +import jace.state.StateManager; +import jace.state.Stateful; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.LockSupport; +import javax.swing.ImageIcon; + +/** + * This implements the mechanical part of the disk drive and tracks changes to + * disk images. The actual handling of disk images is performed in the + * FloppyDisk class. The apple interface card portion is managed in the + * CardDiskII class. Useful reading: + * http://www.doc.ic.ac.uk/~ih/doc/stepper/others/example3/diskii_specs.html + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class DiskIIDrive implements MediaConsumer { + + FloppyDisk disk; + // Number of milliseconds to wait between last write and update to disk image + public static long WRITE_UPDATE_DELAY = 1000; + // Flag to halt if any writes to floopy occur when updating physical disk image + boolean diskUpdatePending = false; + // Last time of write operation + long lastWriteTime; + // Managed thread to update disk image in background + Thread writerThread; + private final byte[][] driveHeadStepDelta = { + {0, 0, 1, 1, 0, 0, 1, 1, -1, -1, 0, 0, -1, -1, 0, 0}, // phase 0 + {0, -1, 0, -1, 1, 0, 1, 0, 0, -1, 0, -1, 1, 0, 1, 0}, // phase 1 + {0, 0, -1, -1, 0, 0, -1, -1, 1, 1, 0, 0, 1, 1, 0, 0}, // phase 2 + {0, 1, 0, 1, -1, 0, -1, 0, 0, 1, 0, 1, -1, 0, -1, 0}}; // phase 3 + @Stateful + public int halfTrack; + @Stateful + public int trackStartOffset; + @Stateful + public int nibbleOffset; + @Stateful + public boolean writeMode; + @Stateful + public boolean driveOn; + @Stateful + public int magnets; + @Stateful + public byte latch; + @Stateful + public int spinCount; + Set<Integer> dirtyTracks; + + public void reset() { + driveOn = false; + magnets = 0; + dirtyTracks = new HashSet<Integer>(); + diskUpdatePending = false; + } + + void step(int register) { + // switch drive head stepper motor magnets on/off + int magnet = (register >> 1) & 0x3; + magnets &= ~(1 << magnet); + magnets |= ((register & 0x1) << magnet); + + // step the drive head according to stepper magnet changes + if (driveOn) { + int delta = driveHeadStepDelta[halfTrack & 0x3][magnets]; + if (delta != 0) { + int newHalfTrack = halfTrack + delta; + if (newHalfTrack < 0) { + newHalfTrack = 0; + } else if (newHalfTrack > FloppyDisk.HALF_TRACK_COUNT) { + newHalfTrack = FloppyDisk.HALF_TRACK_COUNT; + } + if (newHalfTrack != halfTrack) { + halfTrack = newHalfTrack; + trackStartOffset = (halfTrack >> 1) * FloppyDisk.TRACK_NIBBLE_LENGTH; + if (trackStartOffset >= FloppyDisk.DISK_NIBBLE_LENGTH) { + trackStartOffset = FloppyDisk.DISK_NIBBLE_LENGTH - FloppyDisk.TRACK_NIBBLE_LENGTH; + } + nibbleOffset = 0; + + //System.out.printf("new half track %d\n", currentHalfTrack); + } + } + } + } + + void setOn(boolean b) { + driveOn = b; + } + + boolean isOn() { + return driveOn; + } + + byte readLatch() { + byte result = 0x07f; + if (!writeMode) { + spinCount = (spinCount + 1) & 0x0F; + if (spinCount > 0) { + if (disk != null) { + result = disk.nibbles[trackStartOffset + nibbleOffset++]; + } else { + result = (byte) 0x0ff; + } + } + if (nibbleOffset >= FloppyDisk.TRACK_NIBBLE_LENGTH) { + nibbleOffset = 0; + } + } else { + spinCount = (spinCount + 1) & 0x0F; + if (spinCount > 0) { + result = (byte) 0x080; + } + } + return result; + } + + void write() { + if (writeMode) { + while (diskUpdatePending) { + // If another thread requested writes to block (e.g. because of disk activity), wait for it to finish! + LockSupport.parkNanos(1000); + } + if (disk != null) { + // Do nothing if write-protection is enabled! + if (getMediaEntry() == null || !getMediaEntry().writeProtected) { + dirtyTracks.add(trackStartOffset / FloppyDisk.TRACK_NIBBLE_LENGTH); + disk.nibbles[trackStartOffset + nibbleOffset++] = latch; + triggerDiskUpdate(); + StateManager.markDirtyValue(disk.nibbles); + } + } + + if (nibbleOffset >= FloppyDisk.TRACK_NIBBLE_LENGTH) { + nibbleOffset = 0; + } + } + } + + void setLatchValue(byte value) { + if (writeMode) { + latch = value; + } else { + latch = (byte) 0xFF; + } + } + + void setReadMode() { + writeMode = false; + } + + void setWriteMode() { + writeMode = true; + } + + private void updateDisk() { + + // Signal disk update is underway + diskUpdatePending = true; + // Update all tracks as necessary + if (disk != null) { + for (Integer track : dirtyTracks) { + disk.updateTrack(track); + } + } + // Empty out dirty list + dirtyTracks.clear(); + // Signal disk update is completed + diskUpdatePending = false; + } + + private void triggerDiskUpdate() { + lastWriteTime = System.currentTimeMillis(); + if (writerThread == null || !writerThread.isAlive()) { + writerThread = new Thread(new Runnable() { + @Override + public void run() { + long diff = 0; + // Wait until there have been no virtual writes for specified delay time + while ((diff = System.currentTimeMillis() - lastWriteTime) < WRITE_UPDATE_DELAY) { + // Sleep for difference of time + LockSupport.parkNanos(diff * 1000); + // Note: In the meantime, there could have been another disk write, + // in which case this loop will repeat again as needed. + } + updateDisk(); + } + }); + writerThread.start(); + } + } + + void insertDisk(File diskPath) throws IOException { + disk = new FloppyDisk(diskPath); + dirtyTracks = new HashSet<Integer>(); + // Emulator state has changed significantly, reset state manager + StateManager.getInstance().invalidate(); + } + private ImageIcon icon; + + public ImageIcon getIcon() { + return icon; + } + + public void setIcon(ImageIcon i) { + icon = i; + } + private MediaEntry currentMediaEntry; + private MediaFile currentMediaFile; + + public void eject() { + if (disk == null) { + return; + } + waitForPendingWrites(); + disk = null; + dirtyTracks = new HashSet<Integer>(); + // Emulator state has changed significantly, reset state manager + StateManager.getInstance().invalidate(); + } + + public void insertMedia(MediaEntry e, MediaFile f) throws IOException { + if (!isAccepted(e, f)) { + return; + } + eject(); + insertDisk(f.path); + currentMediaEntry = e; + currentMediaFile = f; + } + + public MediaEntry getMediaEntry() { + return currentMediaEntry; + } + + public MediaFile getMediaFile() { + return currentMediaFile; + } + + public boolean isAccepted(MediaEntry e, MediaFile f) { + if (f == null) return false; + System.out.println("Type is accepted: "+f.path+"; "+e.type.toString()+": "+e.type.is140kb); + return e.type.is140kb; + } + + private void waitForPendingWrites() { + while (diskUpdatePending || !dirtyTracks.isEmpty()) { + // If the current disk has unsaved changes, wait!!! + LockSupport.parkNanos(1000); + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/FloppyDisk.java b/src/main/java/jace/hardware/FloppyDisk.java new file mode 100644 index 0000000..1d5b87b --- /dev/null +++ b/src/main/java/jace/hardware/FloppyDisk.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.state.StateManager; +import jace.state.Stateful; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Representation of a 140kb floppy disk image. This also performs conversions as + * needed. Internally, the emulator will always use a "nibblized" disk + * representation during active use. So if any sort of dsk/do/po image is loaded + * it will be converted first. If changes are made to the disk then the tracks + * will be converted back into de-nibblized form prior to saving. The + * DiskIIDrive class managed disk changes, this class is more an interface to + * load/save various disk formats and hold the active disk image while it is in + * use. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class FloppyDisk { + + @Stateful + boolean writeProtected; + @Stateful + public int headerLength = 0; + @Stateful + public boolean isNibblizedImage; + @Stateful + public int volumeNumber; + static final public int TRACK_NIBBLE_LENGTH = 0x1A00; + static final public int TRACK_COUNT = 35; + static final public int SECTOR_COUNT = 16; + static final public int HALF_TRACK_COUNT = TRACK_COUNT * 2; + static final public int DISK_NIBBLE_LENGTH = TRACK_NIBBLE_LENGTH * TRACK_COUNT; + static final public int DISK_PLAIN_LENGTH = 143360; + static final public int DISK_2MG_NON_NIB_LENGTH = DISK_PLAIN_LENGTH + 0x040; + static final public int DISK_2MG_NIB_LENGTH = DISK_NIBBLE_LENGTH + 0x040; + @Stateful + public byte[] nibbles = new byte[DISK_NIBBLE_LENGTH]; + // Denotes the mapping of physical order (array index) to the dos 3.3 logical order (value) + public static int[] DOS_33_SECTOR_ORDER = { + 0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04, + 0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F + }; + // Denotes the mapping of physical order (array index) to the Prodos logical order (value) + // Borrowed from KEGS -- thanks KEGS team! + public static int[] PRODOS_SECTOR_ORDER = { + 0x00, 0x08, 0x01, 0x09, 0x02, 0x0a, 0x03, 0x0b, + 0x04, 0x0c, 0x05, 0x0d, 0x06, 0x0e, 0x07, 0x0f + }; + // Sector ordering used for current disk + @Stateful + public int[] currentSectorOrder; + // Location of image + @Stateful + public File diskPath; + static int[] NIBBLE_62 = { + 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, + 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc, + 0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3, + 0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, + 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec, + 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, + 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff}; + static int[] NIBBLE_62_REVERSE; + + static { + NIBBLE_62_REVERSE = new int[256]; + for (int i = 0; i < NIBBLE_62.length; i++) { + NIBBLE_62_REVERSE[NIBBLE_62[i] & 0x0ff] = 0x0ff & i; + } + } + private static boolean DEBUG = false; + + public FloppyDisk() throws IOException { + // This constructor is only used for disk conversion... + } + + /** + * + * @param diskFile + * @throws IOException + */ + public FloppyDisk(File diskFile) throws IOException { + FileInputStream input = new FileInputStream(diskFile); + String name = diskFile.getName().toUpperCase(); + readDisk(input, name.endsWith(".PO")); + writeProtected = !diskFile.canWrite(); + diskPath = diskFile; + } + + // brendanr: refactored to use input stream + public void readDisk(InputStream diskFile, boolean prodosOrder) throws IOException { + isNibblizedImage = true; + volumeNumber = CardDiskII.DEFAULT_VOLUME_NUMBER; + headerLength = 0; + try { + int bytesRead = diskFile.read(nibbles); + if (bytesRead == DISK_2MG_NIB_LENGTH) { + bytesRead -= 0x040; + // Try to pick up volume number from 2MG header. + volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254; + nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length); + + headerLength = 0x040; + } + if (bytesRead == DISK_2MG_NON_NIB_LENGTH) { + bytesRead -= 0x040; + // Try to pick up correct sector ordering and volume from 2MG header. + prodosOrder = (nibbles[12] == 01); + volumeNumber = ((nibbles[17] & 1) == 1) ? nibbles[16] : 254; + nibbles = Arrays.copyOfRange(nibbles, 0x040, nibbles.length); + + headerLength = 0x040; + } + currentSectorOrder = prodosOrder ? PRODOS_SECTOR_ORDER : DOS_33_SECTOR_ORDER; + if (bytesRead == DISK_PLAIN_LENGTH) { + isNibblizedImage = false; + nibbles = nibblize(nibbles); + if (nibbles.length != DISK_NIBBLE_LENGTH) { + throw new IOException("Nibblized version is wrong size (expected-actual = " + (DISK_NIBBLE_LENGTH - nibbles.length) + ")"); + } + } else if (bytesRead != DISK_NIBBLE_LENGTH) { + throw new IOException("Bad NIB size " + bytesRead + "; JACE only recognizes plain images " + DISK_PLAIN_LENGTH + " or nibble images " + DISK_NIBBLE_LENGTH + " sizes"); + } + } catch (IOException ex) { + throw ex; + } + StateManager.markDirtyValue(nibbles); + StateManager.markDirtyValue(currentSectorOrder); + } + + /* + * Convert a block-format disk to a 6-by-2 nibblized encoding scheme (raw NIB disk format) + */ + public byte[] nibblize(byte[] nibbles) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + for (int track = 0; track < TRACK_COUNT; track++) { + for (int sector = 0; sector < SECTOR_COUNT; sector++) { + // 15 junk bytes + writeJunkBytes(output, 15); + // Address block + writeAddressBlock(output, track, sector); + // 4 junk bytes + writeJunkBytes(output, 4); + // Data block + nibblizeBlock(output, track, currentSectorOrder[sector], nibbles); + // 34 junk bytes + writeJunkBytes(output, 34); + } + } + return output.toByteArray(); + } + + private void writeJunkBytes(ByteArrayOutputStream output, int i) { + for (int b = 0; b < i; b++) { + output.write(0x0FF); + } + } + + private void writeAddressBlock(ByteArrayOutputStream output, int track, int sector) throws IOException { + output.write(0x0d5); + output.write(0x0aa); + output.write(0x096); + + int checksum = 00; + // volume + checksum ^= volumeNumber; + output.write(getOddEven(volumeNumber)); + // track + checksum ^= track; + output.write(getOddEven(track)); + // sector + checksum ^= sector; + output.write(getOddEven(sector)); + // checksum + output.write(getOddEven(checksum & 0x0ff)); + output.write(0x0de); + output.write(0x0aa); + output.write(0x0eb); + } + + private byte[] getOddEven(int i) { + byte[] out = new byte[2]; + out[0] = (byte) (0xAA | (i >> 1)); + out[1] = (byte) (0xAA | i); + return out; + } + + private int decodeOddEven(byte b1, byte b2) { +// return (((b1 ^ 0x0AA) << 1) & 0x0ff) | ((b2 ^ 0x0AA) & 0x0ff); + int result = ((((b1 << 1) | 1) & b2) & 0x0ff); + return result; + } + + private void nibblizeBlock(ByteArrayOutputStream output, int track, int sector, byte[] nibbles) { + int offset = ((track * SECTOR_COUNT) + sector) * 256; + int[] temp = new int[342]; + for (int i = 0; i < 256; i++) { + temp[i] = (nibbles[offset + i] & 0x0ff) >> 2; + } + int hi = 0x001; + int med = 0x0AB; + int low = 0x055; + + for (int i = 0; i < 0x56; i++) { + int value = ((nibbles[offset + hi] & 1) << 5) + | ((nibbles[offset + hi] & 2) << 3) + | ((nibbles[offset + med] & 1) << 3) + | ((nibbles[offset + med] & 2) << 1) + | ((nibbles[offset + low] & 1) << 1) + | ((nibbles[offset + low] & 2) >> 1); + temp[i + 256] = value; + hi = (hi - 1) & 0x0ff; + med = (med - 1) & 0x0ff; + low = (low - 1) & 0x0ff; + } + output.write(0x0d5); + output.write(0x0aa); + output.write(0x0ad); + + int last = 0; + for (int i = temp.length - 1; i > 255; i--) { + int value = temp[i] ^ last; + output.write(NIBBLE_62[value]); + last = temp[i]; + } + for (int i = 0; i < 256; i++) { + int value = temp[i] ^ last; + output.write(NIBBLE_62[value]); + last = temp[i]; + } + // Last data byte used as checksum + output.write(NIBBLE_62[last]); + output.write(0x0de); + output.write(0x0aa); + output.write(0x0eb); + } + + public void updateTrack(Integer track) { + // If disk is nibble image, write nibbles directly + if (isNibblizedImage) { + updateNibblizedTrack(track); + } + // Otherwise denibblize and write out + if (!isNibblizedImage) { + updateDenibblizedTrack(track); + } + } + + void updateNibblizedTrack(Integer track) { + try { + RandomAccessFile disk = new RandomAccessFile(diskPath, "rws"); + // Locate start of track + disk.seek(headerLength + track * TRACK_NIBBLE_LENGTH); + // Update that section of the disk image + disk.write(nibbles, track * TRACK_NIBBLE_LENGTH, TRACK_NIBBLE_LENGTH); + disk.close(); + } catch (FileNotFoundException ex) { + Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + static public boolean CHECK_NIB_SECTOR_PATTERN_ON_WRITE = true; + + void updateDenibblizedTrack(Integer track) { + try { + byte[] trackNibbles = new byte[TRACK_NIBBLE_LENGTH]; + byte[] trackData = new byte[SECTOR_COUNT * 256]; + // Copy track into temporary buffer +// System.out.println("Nibblized track "+track); +// System.out.printf("%04d:",0); + for (int i = 0, pos = track * TRACK_NIBBLE_LENGTH; i < TRACK_NIBBLE_LENGTH; i++, pos++) { + trackNibbles[i] = nibbles[pos]; +// System.out.print(Integer.toString(nibbles[pos] & 0x0ff, 16)+" "); +// if (i % 16 == 15) { +// System.out.println(); +// System.out.printf("%04d:",i+1); +// } + } +// System.out.println(); + + int pos = 0; + for (int i = 0; i < SECTOR_COUNT; i++) { + // Loop through number of sectors + pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x096); + // Locate track number + int trackVerify = decodeOddEven(trackNibbles[pos + 5], trackNibbles[pos + 6]); + // Locate sector number + int sector = decodeOddEven(trackNibbles[pos + 7], trackNibbles[pos + 8]); +// System.out.println("Writing track " + track + ", getting address block for T" + trackVerify + ".S" + sector + " found at NIB offset "+pos); + // Skip to end of address block + pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa /*, 0x0eb this is sometimes being written as FF??*/); + // Locate start of sector data + pos = locatePattern(pos, trackNibbles, 0x0d5, 0x0aa, 0x0ad); + // Determine offset in output data for sector + //int offset = reverseLoopkup(currentSectorOrder, sector) * 256; + int offset = currentSectorOrder[sector] * 256; +// System.out.println("Sector "+sector+" maps to physical sector "+reverseLoopkup(currentSectorOrder, sector)); + // Decode sector data + denibblizeSector(trackNibbles, pos + 3, trackData, offset); + // Skip to end of sector + pos = locatePattern(pos, trackNibbles, 0x0de, 0x0aa, 0x0eb); + } + // Write track to disk + RandomAccessFile disk; + try { + disk = new RandomAccessFile(diskPath, "rws"); + disk.seek(headerLength + track * 256 * SECTOR_COUNT); + disk.write(trackData); + disk.close(); + } catch (FileNotFoundException ex) { + Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } catch (Throwable ex) { + Logger.getLogger(FloppyDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + + private int locatePattern(int pos, byte[] data, int... pattern) throws Throwable { + int max = data.length; + while (!matchPattern(pos, data, pattern)) { + pos = (pos + 1) % data.length; + max--; + if (max < 0) { + throw new Throwable("Could not match pattern!"); + } + } +// System.out.print("Found pattern at "+pos+": "); +// for (int i : pattern) {System.out.print(Integer.toString( i & 0x0ff, 16)+" ");} +// System.out.println(); + return pos; + } + + private boolean matchPattern(int pos, byte[] data, int... pattern) { + int matched = 0; + for (int i : pattern) { + int d = data[pos] & 0x0ff; + if (d != i) { + if (matched > 1) { + System.out.println("Warning: Issue when interpreting nibbilized disk data: at position " + pos + " pattern byte " + Integer.toString(i, 16) + " doesn't match " + Integer.toString(d, 16)); + } + return false; + } + pos = (pos + 1) % data.length; + matched++; + } + return true; + } + + private void denibblizeSector(byte[] source, int pos, byte[] trackData, int offset) { + int[] temp = new int[342]; + int current = pos; + int last = 0; + // Un-encode raw data, leaving with pre-nibblized bytes + for (int i = temp.length - 1; i > 255; i--) { + int t = NIBBLE_62_REVERSE[0x0ff & source[current++]]; + temp[i] = t ^ last; + last ^= t; + } + for (int i = 0; i < 256; i++) { + int t = NIBBLE_62_REVERSE[0x0ff & source[current++]]; + temp[i] = t ^ last; + last ^= t; + } + + // Now decode the pre-nibblized bytes + int p = temp.length - 1; + for (int i = 0; i < 256; i++) { + int a = (temp[i] << 2); + a = a + ((temp[p] & 1) << 1) + ((temp[p] & 2) >> 1); + trackData[i + offset] = (byte) a; + temp[p] = temp[p] >> 2; + p--; + if (p < 256) { + p = temp.length - 1; + } + } + } + + private int reverseLoopkup(int[] table, int value) { + for (int i = 0; i < table.length; i++) { + if (table[i] == value) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/jace/hardware/Joystick.java b/src/main/java/jace/hardware/Joystick.java new file mode 100644 index 0000000..50e0cbd --- /dev/null +++ b/src/main/java/jace/hardware/Joystick.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.SoftSwitches; +import jace.apple2e.softswitch.MemorySoftSwitch; +import jace.config.ConfigurableField; +import jace.core.Computer; +import jace.core.Device; +import jace.core.KeyHandler; +import jace.core.Keyboard; +import jace.core.RAMEvent; +import jace.core.RAMListener; +import jace.state.Stateful; +import java.awt.AWTException; +import java.awt.Dimension; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.Robot; +import java.awt.Toolkit; +import java.awt.event.KeyEvent; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Simple implementation of joystick support that supports mouse or keyboard. + * Actual joystick support isn't offered by Java at this moment in time + * unfortunately. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Stateful +public class Joystick extends Device { + + @ConfigurableField(name = "Enabled", shortName = "enabled", description = "If unchecked, then there is no joystick support.") + public boolean enabled; + @ConfigurableField(name = "Center Mouse", description = "Moves mouse back to the center of the screen, can get annoying.") + public boolean centerMouse; + @ConfigurableField(name = "Use keyboard", shortName = "useKeys", description = "Arrow keys will control joystick instead of the mouse.") + public boolean useKeyboard; + @ConfigurableField(name = "Hog keypresses", shortName = "hog", description = "Key presses will not be sent to emulator.") + public boolean hogKeyboard; + public int port; + @Stateful + public int x = 0; + @Stateful + public int y = 0; + private int joyX = 0; + private int joyY = 0; + MemorySoftSwitch xSwitch; + MemorySoftSwitch ySwitch; + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Point lastMouseLocation; + Robot robot; + Point centerPoint; + + public Joystick(int port) { + centerPoint = new Point(screenSize.width / 2, screenSize.height / 2); + this.port = port; + if (port == 0) { + xSwitch = (MemorySoftSwitch) SoftSwitches.PDL0.getSwitch(); + ySwitch = (MemorySoftSwitch) SoftSwitches.PDL1.getSwitch(); + } else { + xSwitch = (MemorySoftSwitch) SoftSwitches.PDL2.getSwitch(); + ySwitch = (MemorySoftSwitch) SoftSwitches.PDL3.getSwitch(); + } + lastMouseLocation = MouseInfo.getPointerInfo().getLocation(); + try { + robot = new Robot(); + } catch (AWTException ex) { + Logger.getLogger(Joystick.class.getName()).log(Level.SEVERE, null, ex); + } + } + public boolean leftPressed = false; + public boolean rightPressed = false; + public boolean upPressed = false; + public boolean downPressed = false; + + private void readJoystick() { + if (useKeyboard) { + joyX = leftPressed ? (rightPressed ? 128 : 0) : (rightPressed ? 255 : 128); + joyY = upPressed ? (downPressed ? 128 : 0) : (downPressed ? 255 : 128); + } else { + Point l = MouseInfo.getPointerInfo().getLocation(); + if (l.x < lastMouseLocation.x) { + joyX = 0; + } else if (l.x > lastMouseLocation.x) { + joyX = 255; + } else { + joyX = 128; + } + + if (l.y < lastMouseLocation.y) { + joyY = 0; + } else if (l.y > lastMouseLocation.y) { + joyY = 255; + } else { + joyY = 128; + } + + if (centerMouse) { + lastMouseLocation = centerPoint; + robot.mouseMove(centerPoint.x, centerPoint.y); + } else { + if (l.x <= 20) { + robot.mouseMove(20, l.y); + l = MouseInfo.getPointerInfo().getLocation(); + } + if ((l.x + 21) == screenSize.getWidth()) { + robot.mouseMove((int) (screenSize.getWidth() - 20), l.y); + l = MouseInfo.getPointerInfo().getLocation(); + } + if (l.y <= 20) { + robot.mouseMove(l.x, 20); + l = MouseInfo.getPointerInfo().getLocation(); + } + if ((l.y + 21) == screenSize.getHeight()) { + robot.mouseMove(l.x, (int) (screenSize.getHeight() - 20)); + l = MouseInfo.getPointerInfo().getLocation(); + } + + lastMouseLocation = l; + } + } + } + + @Override + protected String getDeviceName() { + return "Joystick (port " + port + ")"; + } + + @Override + public String getShortName() { + return "joy" + port; + } + + @Override + public void tick() { + boolean finished = true; + if (x > 0) { + if (--x == 0) { + xSwitch.setState(false); + } else { + finished = false; + } + } + if (y > 0) { + if (--y == 0) { + ySwitch.setState(false); + } else { + finished = false; + } + } + if (finished) { + setRun(false); + } + } + + @Override + public void attach() { + registerListeners(); + } + + @Override + public void detach() { + removeListeners(); + } + + public void reconfigure() { + x = 0; + y = 0; + if (enabled) { + registerListeners(); + } else { + removeListeners(); + } + } + RAMListener listener = new RAMListener(RAMEvent.TYPE.ANY, RAMEvent.SCOPE.RANGE, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0C070); + setScopeEnd(0x0C07f); + } + + @Override + protected void doEvent(RAMEvent e) { + setRun(true); + readJoystick(); +// if (x <= 0) { + xSwitch.setState(true); + x = 10 + joyX * 11; +// } +// if (y <= 0) { + ySwitch.setState(true); + y = 10 + joyY * 11; +// } + } + }; + + private void registerListeners() { + Computer.getComputer().getMemory().addListener(listener); + if (useKeyboard) { + System.out.println("Registering key handlers"); + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_LEFT, -1) { + @Override + public boolean handleKeyUp(KeyEvent e) { + leftPressed = false; + return hogKeyboard; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + leftPressed = true; + return hogKeyboard; + } + }, this); + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_RIGHT, -1) { + @Override + public boolean handleKeyUp(KeyEvent e) { + rightPressed = false; + return hogKeyboard; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + rightPressed = true; + return hogKeyboard; + } + }, this); + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_UP, -1) { + @Override + public boolean handleKeyUp(KeyEvent e) { + upPressed = false; + return hogKeyboard; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + upPressed = true; + return hogKeyboard; + } + }, this); + Keyboard.registerKeyHandler(new KeyHandler(KeyEvent.VK_DOWN, -1) { + @Override + public boolean handleKeyUp(KeyEvent e) { + downPressed = false; + return hogKeyboard; + } + + @Override + public boolean handleKeyDown(KeyEvent e) { + downPressed = true; + return hogKeyboard; + } + }, this); + } + } + + private void removeListeners() { + Computer.getComputer().getMemory().removeListener(listener); + Keyboard.unregisterAllHandlers(this); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/PassportMidiInterface.java b/src/main/java/jace/hardware/PassportMidiInterface.java new file mode 100644 index 0000000..ff880c2 --- /dev/null +++ b/src/main/java/jace/hardware/PassportMidiInterface.java @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.config.Name; +import jace.core.Card; +import jace.core.Computer; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.sound.midi.InvalidMidiDataException; +import javax.sound.midi.MidiDevice; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.ShortMessage; +import javax.sound.midi.Synthesizer; + +/** + * Partial implementation of Passport midi card, supporting midi output routed + * to the java midi synth for playback. Compatible with Ultima V. Card + * operational notes taken from the Passport MIDI interface manual + * ftp://ftp.apple.asimov.net/pub/apple_II/documentation/hardware/misc/passport_midi.pdf + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name(value = "Passport Midi Interface", description = "MIDI sound card") +public class PassportMidiInterface extends Card { + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // There is no rom on this card, so nothing to do here + } + + // MIDI timing: 31250 BPS, 8-N-1 (roughly 3472k per second) + public static enum TIMER_MODE { + + continuous, singleShot, freqComparison, pulseComparison + }; + + public static class PTMTimer { + // Configuration values + + public boolean prescaledTimer = false; // Only available on Timer 3 + public boolean enableClock = false; // False == use CX clock input + public boolean dual8BitMode = false; + public TIMER_MODE mode = TIMER_MODE.continuous; + public boolean irqEnabled = false; + public boolean counterOutputEnable = false; + // Set by data latches + public Long duration = 0L; + // Run values + public boolean irqRequested = true; + public Long value = 0L; + } + // I/O registers + // --- 6840 PTM + public static final int TIMER_CONTROL_1 = 0; + public static final int TIMER_CONTROL_2 = 1; + public static final int TIMER1_MSB = 2; + public static final int TIMER1_LSB = 3; + public static final int TIMER2_MSB = 4; + public static final int TIMER2_LSB = 5; + // (Most likely not used) + public static final int TIMER3_MSB = 6; + public static final int TIMER3_LSB = 7; + // --- 6850 ACIA registers (write) + public static final int ACIA_CONTROL = 8; + public static final int ACIA_SEND = 9; + // --- 6850 ACIA registers (read) + public static final int ACIA_STATUS = 8; + public static final int ACIA_RECV = 9; + // --- Drums + public static final int DRUM_SYNC_SET = 0x0e; + public static final int DRUM_SYNC_CLEAR = 0x0f; + //--------------------------------------------------------- + // PTM control values (register 1,2 and 3) + public static final int PTM_START_TIMERS = 0; + public static final int PTM_STOP_TIMERS = 1; + public static final int PTM_RESET = 67; + // PTM select values (register 2 only) -- modifies what Reg 1 points to + public static final int PTM_SELECT_REG_1 = 1; + public static final int PTM_SELECT_REG_3 = 0; + // PTM select values (register 3 only) + public static final int TIMER_3_PRESCALED = 1; + public static final int TIMER_3_NOT_PRESCALED = 0; + // PTM bit values + public static final int PTM_CLOCK_SOURCE = 2; // Bit 1 + // 0 = external, 2 = internal clock + public static final int PTM_LATCH_IS_16_BIT = 4; // Bit 2 + // 0 = 16-bit, 4 = dual 8-bit + // Bits 3-5 + // 5 4 3 + public static final int PTM_CONTINUOUS = 0; // 0 x 0 + public static final int PTM_SINGLE_SHOT = 32; // 1 x 0 + public static final int PTM_FREQ_COMP = 8; // x 0 1 + public static final int PTM_PULSE_COMP = 24; // x 1 1 + public static final int PTM_IRQ_ENABLED = 64; // Bit 6 + // 64 = IRQ Enabled, 0 = IRQ Masked + public static final int PTM_OUTPUT_ENABLED = 128; // Bit 7 + // 128 = Timer output enabled, 0 = disabled + // ACIA control values + // Reset == Master reset + even parity + 2 stop bits + 8 bit + No interrupts (??) + public static final int ACIA_RESET = 19; + public static final int ACIA_MASK_INTERRUPTS = 17; + public static final int ACIA_OFF = 21; + // Counter * 1 + RTS = low, transmit interrupt enabled + public static final int ACIA_INT_ON_SEND = 49; + // Counter * 1 + RTS = high, transmit interrupt disabled + Interrupt on receive + public static final int ACIA_INT_ON_RECV = 145; + // Counter * 1 + RTS = low, transmit interrupt enabled + Interrupt on receive + public static final int ACIA_INT_ON_SEND_AND_RECV = 177; + // ACIA control register values + // --- Bits 1 and 0 control counter divide select + public static final int ACIA_COUNTER_1 = 0; + public static final int ACIA_COUNTER_16 = 1; + public static final int ACIA_COUNTER_64 = 2; + public static final int ACIA_MASTER_RESET = 3; + // Midi is always transmitted 8-N-1 + public static final int ACIA_ODD_PARITY = 4; // 4 = odd, 0 = even + public static final int ACIA_STOP_BITS_1 = 8; // 8 = 1 stop bit, 0 = 2 stop bits + public static final int ACIA_WORD_LENGTH_8 = 16; // 16 = 8-bit, 0 = 7-bit + // --- Bits 5 and 6 control interrupts + // 6 5 + // 0 0 RTS = low, transmit interrupt disabled + // 0 1 RTS = low, transmit interrupt enabled + // 1 0 RTS = high, transmit interrupt disabled + // 1 1 RTS = low, Transmit break, trasmit interrupt disabled + public static final int ACIA_RECV_INTERRUPT = 128; // 128 = interrupt on receive, 0 = no interrupt + // PTM configuration + private boolean ptmTimer3Selected = false; // When true, reg 1 points at timer 3 + private boolean ptmTimersActive = false; // When true, timers run constantly + private PTMTimer[] ptmTimer = { + new PTMTimer(), + new PTMTimer(), + new PTMTimer() + }; + private boolean ptmStatusReadSinceIRQ = false; + // ---------------------- ACIA CONFIGURATION + private boolean aciaInterruptOnSend = false; + private boolean aciaInterruptOnReceive = false; + // ---------------------- ACIA STATUS BITS + // True when MIDI IN receives a byte + private boolean receivedACIAByte = false; + // True when data is not transmitting (always true because we aren't really doing wire transmission); + private boolean transmitACIAEmpty = true; + // True if another byte is received before the previous byte was processed + private boolean receiverACIAOverrun = false; + // True if ACIA generated interrupt request + private boolean irqRequestedACIA = false; + //--- the synth + private Synthesizer synth; + + @Override + public void reset() { + // TODO: Deactivate card + suspend(); + } + + @Override + public boolean suspend() { + // TODO: Deactivate card + suspendACIA(); + return super.suspend(); + } + + @Override + protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // No firmware, so do nothing + return; + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + switch (type) { + case READ_DATA: + int returnValue = 0; + switch (register) { + case ACIA_STATUS: + returnValue = getACIAStatus(); + break; + case ACIA_RECV: + returnValue = getACIARecieve(); + break; + //TODO: Implement PTM registers + case TIMER_CONTROL_1: + // Technically it's not supposed to return anything... + returnValue = getPTMStatus(); + break; + case TIMER_CONTROL_2: + returnValue = getPTMStatus(); + break; + case TIMER1_LSB: + returnValue = (int) (ptmTimer[0].value & 0x0ff); + if (ptmStatusReadSinceIRQ) { + ptmTimer[0].irqRequested = false; + } + break; + case TIMER1_MSB: + returnValue = (int) (ptmTimer[0].value >> 8) & 0x0ff; + if (ptmStatusReadSinceIRQ) { + ptmTimer[0].irqRequested = false; + } + break; + case TIMER2_LSB: + returnValue = (int) (ptmTimer[1].value & 0x0ff); + if (ptmStatusReadSinceIRQ) { + ptmTimer[1].irqRequested = false; + } + break; + case TIMER2_MSB: + returnValue = (int) (ptmTimer[1].value >> 8) & 0x0ff; + if (ptmStatusReadSinceIRQ) { + ptmTimer[1].irqRequested = false; + } + break; + case TIMER3_LSB: + returnValue = (int) (ptmTimer[2].value & 0x0ff); + if (ptmStatusReadSinceIRQ) { + ptmTimer[2].irqRequested = false; + } + break; + case TIMER3_MSB: + returnValue = (int) (ptmTimer[2].value >> 8) & 0x0ff; + if (ptmStatusReadSinceIRQ) { + ptmTimer[2].irqRequested = false; + } + break; + default: + System.out.println("Passport midi read unrecognized, port " + register); + } + + e.setNewValue(returnValue); +// System.out.println("Passport I/O read register " + register + " == " + returnValue); + break; + case WRITE: + int v = e.getNewValue() & 0x0ff; +// System.out.println("Passport I/O write register " + register + " == " + v); + switch (register) { + case ACIA_CONTROL: + processACIAControl(v); + break; + case ACIA_SEND: + processACIASend(v); + break; + case TIMER_CONTROL_1: + if (ptmTimer3Selected) { +// System.out.println("Configuring timer 3"); + ptmTimer[2].prescaledTimer = ((v & TIMER_3_PRESCALED) != 0); + processPTMConfiguration(ptmTimer[2], v); + } else { +// System.out.println("Configuring timer 1"); + if ((v & PTM_STOP_TIMERS) == 0) { + startPTM(); + } else { + stopPTM(); + } + processPTMConfiguration(ptmTimer[0], v); + } + break; + case TIMER_CONTROL_2: +// System.out.println("Configuring timer 2"); + ptmTimer3Selected = ((v & PTM_SELECT_REG_1) == 0); + processPTMConfiguration(ptmTimer[1], v); + break; + case TIMER1_LSB: + ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff00) | v; + break; + case TIMER1_MSB: + ptmTimer[0].duration = (ptmTimer[0].duration & 0x0ff) | (v << 8); + break; + case TIMER2_LSB: + ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff00) | v; + break; + case TIMER2_MSB: + ptmTimer[1].duration = (ptmTimer[1].duration & 0x0ff) | (v << 8); + break; + case TIMER3_LSB: + ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | v; + break; + case TIMER3_MSB: + ptmTimer[2].duration = (ptmTimer[2].duration & 0x0ff00) | (v << 8); + break; + default: + System.out.println("Passport midi write unrecognized, port " + register); + } + + break; + } + } + + @Override + public void tick() { + if (ptmTimersActive) { + for (PTMTimer t : ptmTimer) { +// if (t.duration == 0) { +// continue; +// } + t.value--; + if (t.value < 0) { + // TODO: interrupt dual 8-bit mode, whatver that is! + if (t.irqEnabled) { +// System.out.println("Timer generating interrupt!"); + t.irqRequested = true; + Computer.getComputer().getCpu().generateInterrupt(); + ptmStatusReadSinceIRQ = false; + } + if (t.mode == TIMER_MODE.continuous || t.mode == TIMER_MODE.freqComparison) { + t.value = t.duration; + } + } + } + } + } + + @Override + public String getDeviceName() { + return "Passport MIDI Controller"; + } + + //------------------------------------------------------ PTM + private void processPTMConfiguration(PTMTimer timer, int val) { + timer.enableClock = (val & PTM_CLOCK_SOURCE) != 0; + timer.dual8BitMode = (val & PTM_LATCH_IS_16_BIT) != 0; + switch (val & 56) { + // Evaluate bits 3, 4 and 5 to determine mode + case PTM_CONTINUOUS: + timer.mode = TIMER_MODE.continuous; + break; + case PTM_PULSE_COMP: + timer.mode = TIMER_MODE.pulseComparison; + break; + case PTM_FREQ_COMP: + timer.mode = TIMER_MODE.freqComparison; + break; + case PTM_SINGLE_SHOT: + timer.mode = TIMER_MODE.singleShot; + break; + default: + timer.mode = TIMER_MODE.continuous; + break; + } + timer.irqEnabled = (val & PTM_IRQ_ENABLED) != 0; + timer.counterOutputEnable = (val & PTM_OUTPUT_ENABLED) != 0; + } + + private void stopPTM() { +// System.out.println("Passport timers halted"); + ptmTimersActive = false; + } + + private void startPTM() { +// System.out.println("Passport timers started"); + ptmTimersActive = true; + ptmTimer[0].irqRequested = false; + ptmTimer[1].irqRequested = false; + ptmTimer[2].irqRequested = false; + ptmTimer[0].value = ptmTimer[0].duration; + ptmTimer[1].value = ptmTimer[1].duration; + ptmTimer[2].value = ptmTimer[2].duration; + + } + + // Bits 0, 1 and 2 == IRQ requested from timer 1, 2 or 3 + // Bit 7 = Any IRQ + private int getPTMStatus() { + int status = 0; + for (int i = 0; i < 3; i++) { + PTMTimer t = ptmTimer[i]; + if (t.irqRequested && t.irqEnabled) { + ptmStatusReadSinceIRQ = true; + status |= (1 << i); + status |= 128; + } + } + return status; + } + //------------------------------------------------------ ACIA + /* + ACIA status register + Bit 0 = Receive data register full + Bit 1 = Transmit data register empty + Bits 2 and 3 pertain to modem (DCD and CTS, so ignore) + Bit 4 = Framing error + Bit 5 = Receiver overrun + Bit 6 = Partity error (not used by MIDI) + Bit 7 = Interrupt request + */ + + private int getACIAStatus() { + int status = 0; + if (receivedACIAByte) { + status |= 1; + } + if (transmitACIAEmpty) { + status |= 2; + } + if (receiverACIAOverrun) { + status |= 32; + } + if (irqRequestedACIA) { + status |= 128; + } + return status; + } + + // TODO: Implement MIDI IN... some day + private int getACIARecieve() { + return 0; + } + + private void processACIAControl(int value) { + if ((value & 0x03) == ACIA_MASTER_RESET) { + resume(); + } + } + ShortMessage currentMessage; + int currentMessageStatus; + int currentMessageData1; + int currentMessageData2; + int messageSize = 255; + int currentMessageReceived = 0; + + private void processACIASend(int value) { + if (!isRunning()) { +// System.err.println("ACIA not active!"); + return; + } else { +// System.out.println("ACIA send "+value); + } + // First off try to finish off previous command already in play + boolean sendMessage = false; + if (currentMessage != null) { + if ((value & 0x080) > 0) { + // Any command byte received means we finished receiving another command + // and valid or not, process it as-is + if (currentMessage != null) { + sendMessage = true; + } + // If there is no current message, then we'll pick this up afterwards... + } else { + // If we receive a data byte ( < 128 ) then check if we have the right size + // if so, then the command was completely received, and it's time to send it. + currentMessageReceived++; + if (currentMessageReceived >= messageSize) { + sendMessage = true; + } + if (currentMessageReceived == 1) { + currentMessageData1 = value; + } else { + // Possibly redundant, but there's no reason a message should be longer than this... + currentMessageData2 = value; + sendMessage = true; + } + } + } + + // If we have a command to send, then do it + if (sendMessage == true) { + if (synth != null && synth.isOpen()) { + // Send message + try { +// System.out.println("Sending MIDI message "+currentMessageStatus+","+currentMessageData1+","+currentMessageData2); + currentMessage.setMessage(currentMessageStatus, currentMessageData1, currentMessageData2); + synth.getReceiver().send(currentMessage, -1L); + } catch (InvalidMidiDataException ex) { + Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex); + } catch (MidiUnavailableException ex) { + Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex); + } + } + currentMessage = null; + } + + // Do we have a new command byte? + if ((value & 0x080) > 0) { + // Start a new message + currentMessage = new ShortMessage(); + currentMessageStatus = value; + currentMessageData1 = 0; + currentMessageData2 = 0; + try { + currentMessage.setMessage(currentMessageStatus, 0, 0); + messageSize = currentMessage.getLength(); + } catch (InvalidMidiDataException ex) { + messageSize = 0; + } + currentMessageReceived = 0; + } + + } + + @Override + public void resume() { + if (isRunning() && synth != null && synth.isOpen()) { + return; + } + try { + MidiDevice.Info[] devices = MidiSystem.getMidiDeviceInfo(); + if (devices.length == 0) { + System.out.println("No MIDI devices found"); + } else { + for (MidiDevice.Info dev : devices) { + System.out.println("MIDI Device found: " + dev); + if (dev.getName().contains("Java Sound")) { + if (dev instanceof Synthesizer) { + synth = (Synthesizer) dev; + break; + } + } + } + } + if (synth == null) { + synth = MidiSystem.getSynthesizer(); + } + if (synth != null) { + System.out.println("Selected MIDI device: " + synth.getDeviceInfo().getName()); + synth.open(); + super.resume(); + } + } catch (MidiUnavailableException ex) { + System.out.println("Could not open MIDI synthesizer"); + ex.printStackTrace(); + Logger.getLogger(PassportMidiInterface.class.getName()).log(Level.SEVERE, null, ex); + } + } + + private void suspendACIA() { + // TODO: Stop ACIA thread... + if (synth != null && synth.isOpen()) { + synth.close(); + synth = null; + } + } +} diff --git a/src/main/java/jace/hardware/ProdosDriver.java b/src/main/java/jace/hardware/ProdosDriver.java new file mode 100644 index 0000000..4ff05d9 --- /dev/null +++ b/src/main/java/jace/hardware/ProdosDriver.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.MOS65C02; +import jace.core.Card; +import jace.core.Computer; +import jace.core.RAM; +import java.io.IOException; + +/** + * Helper functions for prodos drivers + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class ProdosDriver { + public static int MLI_COMMAND = 0x042; + public static int MLI_UNITNUMBER = 0x043; + public static int MLI_BUFFER_ADDRESS = 0x044; + public static int MLI_BLOCK_NUMBER = 0x046; + + public static enum MLI_RETURN { + NO_ERROR(0), IO_ERROR(0x027), NO_DEVICE(0x028), WRITE_PROTECTED(0x02B); + public int intValue; + + MLI_RETURN(int val) { + intValue = val; + } + } + + public static enum MLI_COMMAND_TYPE { + STATUS(0x0), READ(0x01), WRITE(0x02), FORMAT(0x03); + public int intValue; + + MLI_COMMAND_TYPE(int val) { + intValue = val; + } + + public static MLI_COMMAND_TYPE fromInt(int value) { + for (MLI_COMMAND_TYPE c : values()) { + if (c.intValue == value) { + return c; + } + } + return null; + } + } + + abstract public boolean changeUnit(int unitNumber); + abstract public int getSize(); + abstract public boolean isWriteProtected(); + abstract public void mliFormat() throws IOException; + abstract public void mliRead(int block, int bufferAddress) throws IOException; + abstract public void mliWrite(int block, int bufferAddress) throws IOException; + abstract public Card getOwner(); + + public void handleMLI() { + int returnCode = prodosMLI().intValue; + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); + cpu.A = returnCode; + // Clear carry flag if no error, otherwise set carry flag + cpu.C = (returnCode == 0x00) ? 00 : 01; + } + + private MLI_RETURN prodosMLI() { + try { + RAM memory = Computer.getComputer().getMemory(); + int cmd = memory.readRaw(MLI_COMMAND); + MLI_COMMAND_TYPE command = MLI_COMMAND_TYPE.fromInt(cmd); + int unit = (memory.readWordRaw(MLI_UNITNUMBER) & 0x080) > 0 ? 1 : 0; + if (changeUnit(unit) == false) { + return MLI_RETURN.NO_DEVICE; + } + int block = memory.readWordRaw(MLI_BLOCK_NUMBER); + int bufferAddress = memory.readWordRaw(MLI_BUFFER_ADDRESS); +// System.out.println(getOwner().getName()+" MLI Call "+command+", unit "+unit+" Block "+block+" --> "+Integer.toHexString(bufferAddress)); + if (command == null) { + System.out.println(getOwner().getName()+" Mass storage given bogus command (" + Integer.toHexString(cmd) + "), returning I/O error"); + return MLI_RETURN.IO_ERROR; + } + switch (command) { + case STATUS: + int blocks = getSize(); + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); + cpu.X = blocks & 0x0ff; + cpu.Y = (blocks >> 8) & 0x0ff; + if (isWriteProtected()) { + return MLI_RETURN.WRITE_PROTECTED; + } + break; + case FORMAT: + mliFormat(); + case READ: + mliRead(block, bufferAddress); + break; + case WRITE: + mliWrite(block, bufferAddress); + break; + default: + System.out.println(getOwner().getName()+" MLI given bogus command (" + Integer.toHexString(cmd) + " = " + command.name() + "), returning I/O error"); + return MLI_RETURN.IO_ERROR; + } + return MLI_RETURN.NO_ERROR; + } catch (UnsupportedOperationException ex) { + return MLI_RETURN.WRITE_PROTECTED; + } catch (IOException ex) { + System.out.println(getOwner().getName()+" Encountered IO Error, returning error: " + ex.getMessage()); + return MLI_RETURN.IO_ERROR; + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/SmartportDriver.java b/src/main/java/jace/hardware/SmartportDriver.java new file mode 100644 index 0000000..1d6f39a --- /dev/null +++ b/src/main/java/jace/hardware/SmartportDriver.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware; + +import jace.apple2e.MOS65C02; +import jace.core.Computer; +import jace.core.RAM; +import jace.hardware.massStorage.CardMassStorage; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Generic abstraction of a smartport device. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class SmartportDriver { + + public static enum ERROR_CODE { + NO_ERROR(0), INVALID_COMMAND(0x01), BAD_PARAM_COUNT(0x04), INVALID_UNIT(0x011), INVALID_CODE(0x021), BAD_BLOCK_NUMBER(0x02d); + int intValue; + ERROR_CODE(int c) { + intValue = c; + } + } + + public void handleSmartport() { + int returnCode = callSmartport().intValue; + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); + cpu.A = returnCode; + // Clear carry flag if no error, otherwise set carry flag + cpu.C = (returnCode == 0x00) ? 00 : 01; + } + + private ERROR_CODE callSmartport() { + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); + RAM ram = Computer.getComputer().getMemory(); + int callAddress = cpu.popWord() + 1; + int command = ram.readRaw(callAddress); + boolean extendedCall = command >= 0x040; +// command &= 0x0f; + // Modify stack so that RTS goes to the right place after the smartport device call + //cpu.pushWord(callAddress + (extendedCall ? 5 : 3)); + // Kludge due to the CPU not getting the faked RTS opcode + cpu.setProgramCounter(callAddress + (extendedCall ? 5 : 3)); + + // Calculate parameter address block + int parmAddr = 0; + if (!extendedCall) { + parmAddr = ram.readWordRaw(callAddress + 1); + } else { + // Extended calls -- not gonna happen on this platform anyway + int parmAddrLo = ram.readWordRaw(callAddress + 1); + int parmAddrHi = ram.readWordRaw(callAddress + 3); + parmAddr = parmAddrHi << 16 | parmAddrLo; + } + // Now process command + System.out.println("Received command " + command + " with address block " + Integer.toHexString(parmAddr)); + byte numParms = ram.readRaw(parmAddr); + int[] params = new int[16]; + for (int i = 0; i < 16; i++) { + int value = 0x0ff & ram.readRaw(parmAddr + i); + params[i] = value; + System.out.print(Integer.toHexString(value) + " "); + } + System.out.println(); + int unitNumber = params[1]; + if (!changeUnit(unitNumber)) { + System.out.println("Invalid unit: "+unitNumber); + return ERROR_CODE.INVALID_UNIT; + } + int dataBuffer = params[2] | (params[3] << 8); + + try { + switch (command) { + case 0: //Status + return returnStatus(dataBuffer, params); + case 1: //Read Block + int blockNum = params[4] | (params[5] << 8) | (params[6] << 16); + read(blockNum, dataBuffer); + return ERROR_CODE.NO_ERROR; + // System.out.println("reading "+blockNum+" to $"+Integer.toHexString(dataBuffer)); + case 2: //Write Block + blockNum = params[4] | (params[5] << 8) | (params[6] << 16); + write(blockNum, dataBuffer); + return ERROR_CODE.NO_ERROR; + case 3: //Format + case 4: //Control + case 5: //Init + case 6: //Open + case 7: //Close + case 8: //Read + case 9: //Write + default: + System.out.println("Unimplemented command "+command); + return ERROR_CODE.INVALID_COMMAND; + } + } catch (IOException ex) { + Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex); + return ERROR_CODE.INVALID_CODE; + } + } + + abstract public boolean changeUnit(int unitNumber); + abstract public void read(int blockNum, int buffer) throws IOException; + abstract public void write(int blockNum, int buffer) throws IOException; + abstract public ERROR_CODE returnStatus(int dataBuffer, int[] params); +} diff --git a/src/main/java/jace/hardware/massStorage/CardMassStorage.java b/src/main/java/jace/hardware/massStorage/CardMassStorage.java new file mode 100644 index 0000000..dc3da2c --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/CardMassStorage.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import jace.Emulator; +import jace.apple2e.MOS65C02; +import jace.config.Name; +import jace.core.Card; +import jace.core.Computer; +import jace.core.Motherboard; +import jace.core.RAMEvent; +import jace.core.RAMEvent.TYPE; +import jace.core.Utility; +import jace.hardware.ProdosDriver; +import jace.hardware.SmartportDriver; +import jace.library.MediaConsumer; +import jace.library.MediaConsumerParent; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Hard disk and 800k floppy (smartport) controller card. HDV and 2MG images are + * both supported. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Name("Mass Storage Device") +public class CardMassStorage extends Card implements MediaConsumerParent { + + MassStorageDrive drive1; + MassStorageDrive drive2; + + public CardMassStorage() { + drive1 = new MassStorageDrive(); + drive2 = new MassStorageDrive(); + drive1.setIcon(Utility.loadIcon("drive-harddisk.png")); + drive2.setIcon(Utility.loadIcon("drive-harddisk.png")); + currentDrive = drive1; + } + + @Override + public void setSlot(int slot) { + super.setSlot(slot); + drive1.getIcon().setDescription("S" + getSlot() + "D1"); + drive2.getIcon().setDescription("S" + getSlot() + "D2"); + } + + @Override + public String getDeviceName() { + return "Mass Storage Device"; + } + // boot0 stores cards*16 of boot device here + static int SLT16 = 0x02B; + // "rom" offset where device driver is called by MLI +// static int DEVICE_DRIVER_OFFSET = 0x042; + static int DEVICE_DRIVER_OFFSET = 0x0A; + byte[] cardSignature = new byte[]{ + (byte) 0x0a9 /*NOP*/, 0x020, (byte) 0x0a9, 0x00, + (byte) 0x0a9, 0x03 /*currentDisk cards*/, (byte) 0x0a9, 0x03c /*currentDisk cards*/, + (byte) 0xd0, 0x07, 0x60, (byte) 0x0b0, + 0x01 /*firmware cards*/, 0x18, (byte) 0x0b0, 0x5a + }; + Card theCard = this; + public MassStorageDrive currentDrive; + + public IDisk getCurrentDisk() { + if (currentDrive != null) { + return currentDrive.getCurrentDisk(); + } + return null; + } + ProdosDriver driver = new ProdosDriver() { + @Override + public boolean changeUnit(int unit) { + currentDrive = unit == 0 ? drive1 : drive2; + return getCurrentDisk() != null; + } + + @Override + public int getSize() { + return getCurrentDisk() != null ? getCurrentDisk().getSize() : 0; + } + + @Override + public boolean isWriteProtected() { + return getCurrentDisk() != null ? getCurrentDisk().isWriteProtected() : true; + } + + @Override + public void mliFormat() throws IOException { + getCurrentDisk().mliFormat(); + } + + @Override + public void mliRead(int block, int bufferAddress) throws IOException { + getCurrentDisk().mliRead(block, bufferAddress); + } + + @Override + public void mliWrite(int block, int bufferAddress) throws IOException { + getCurrentDisk().mliWrite(block, bufferAddress); + } + + @Override + public Card getOwner() { + return theCard; + } + }; + + @Override + public void reconfigure() { + try { + detach(); + + int pc = Computer.getComputer().getCpu().getProgramCounter(); + if (drive1.getCurrentDisk() != null && getSlot() == 7 && (pc == 0x0c65e || pc == 0x0c661)) { + // If the computer is in a loop trying to boot from cards 6, fast-boot from here instead + // This is a convenience to boot a hard-drive if the emulator has started waiting for a currentDisk + currentDrive = drive1; + getCurrentDisk().boot0(getSlot()); + Card[] cards = Computer.getComputer().getMemory().getAllCards(); + Motherboard.cancelSpeedRequest(cards[6]); + } + attach(); + } catch (IOException ex) { + Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex); + } + } + + @Override + public void reset() { + } + + @Override + protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { + // There is no c8 rom for this card + } + + @Override + protected void handleFirmwareAccess(int offset, TYPE type, int value, RAMEvent e) { + MOS65C02 cpu = (MOS65C02) Computer.getComputer().getCpu(); +// System.out.println(e.getType()+" "+Integer.toHexString(e.getAddress())+" from instruction at "+Integer.toHexString(cpu.getProgramCounter())); + if (type.isRead()) { + Emulator.getFrame().addIndicator(this, currentDrive.getIcon()); + if (drive1.getCurrentDisk() == null && drive2.getCurrentDisk() == null) { + e.setNewValue(0); + return; + } + if (type == TYPE.EXECUTE) { + // Virtual functions, handle accordingly + String error; + if (offset == 0x00) { + // NOP unless otherwise specified + e.setNewValue(0x0ea); + try { + if (drive1.getCurrentDisk() != null) { + currentDrive = drive1; + getCurrentDisk().boot0(getSlot()); + } else { + // Patch for crash on start when no image is mounted + e.setNewValue(0x060); + } + return; + } catch (IOException ex) { + Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex); + error = ex.getMessage(); + // Jump to the basic interpreter for now + cpu.setProgramCounter(0x0dfff); + int address = 0x0480; + for (char c : error.toCharArray()) { + Computer.getComputer().getMemory().write(address++, (byte) (c + 0x080), false, false); + } + } + } else { + if (offset == DEVICE_DRIVER_OFFSET) { + driver.handleMLI(); + } else if (offset == DEVICE_DRIVER_OFFSET + 3) { + smartport.handleSmartport(); + } else { + System.out.println("Call to unknown handler " + Integer.toString(e.getAddress(), 16) + "-- returning"); + } + /* act like RTS was called */ + e.setNewValue(0x060); + } + } + if (offset < 16) { + e.setNewValue(cardSignature[offset]); + } else { + switch (offset) { + // Disk capacity = 65536 blocks + case 0x0FC: + e.setNewValue(0x0ff); + break; + case 0x0FD: + e.setNewValue(0x07f); + break; + // Status bits + case 0x0FE: + e.setNewValue(0x0D7); + break; + case 0x0FF: + e.setNewValue(DEVICE_DRIVER_OFFSET); + } + } + } + } + + @Override + protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { + // Ignore IO registers + } + + @Override + public void tick() { + // Nothing is done per CPU cycle + } + SmartportDriver smartport = new SmartportDriver() { + @Override + public boolean changeUnit(int unitNumber) { + currentDrive = unitNumber == 1 ? drive1 : drive2; + return getCurrentDisk() != null; + } + + @Override + public void read(int blockNum, int buffer) throws IOException { + getCurrentDisk().mliRead(blockNum, buffer); + } + + @Override + public void write(int blockNum, int buffer) throws IOException { + getCurrentDisk().mliWrite(blockNum, buffer); + } + + @Override + public ERROR_CODE returnStatus(int dataBuffer, int[] params) { + return ERROR_CODE.NO_ERROR; + } + }; + + public MediaConsumer[] getConsumers() { + return new MediaConsumer[]{drive1, drive2}; + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/massStorage/DirectoryNode.java b/src/main/java/jace/hardware/massStorage/DirectoryNode.java new file mode 100644 index 0000000..064b6b3 --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/DirectoryNode.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Prodos directory node + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class DirectoryNode extends DiskNode implements FileFilter { +// public static int FILE_ENTRY_SIZE = 38; + public static int FILE_ENTRY_SIZE = 0x027; + public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock) throws IOException { + setBaseBlock(baseBlock); + init(ownerFilesystem, physicalDir); + } + + public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir) throws IOException { + init(ownerFilesystem, physicalDir); + } + + + private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile) throws IOException { + setPhysicalFile(physicalFile); + setType(EntryType.SUBDIRECTORY); + setName(physicalFile.getName()); + setOwnerFilesystem(ownerFilesystem); + } + + @Override + public void doDeallocate() { + } + + @Override + public void doAllocate() { + File[] files = physicalFile.listFiles(this); + int numEntries = files.length; + int numBlocks = 1; + // First block has 12 entries, subsequent blocks have 13 entries + if (numEntries > 12) { + numBlocks += (numEntries - 12) / 13; + } + + for (File f : files) { + addFile(f); + } + Collections.sort(children, new Comparator<DiskNode>() { + public int compare(DiskNode o1, DiskNode o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } + + @Override + public void doRefresh() { + } + + @Override + /** + * Checks contents of subdirectory for changes as well as directory itself (super class) + */ + public boolean checkFile() throws IOException { + boolean success = true; + if (!super.checkFile()) { + return false; + } + HashSet<String> realFiles = new HashSet<String>(); + File[] realFileList = physicalFile.listFiles(this); + for (File f : realFileList) { + realFiles.add(f.getName()); + } + for (Iterator<DiskNode> i = getChildren().iterator(); i.hasNext(); ) { + DiskNode node = i.next(); + if (realFiles.contains(node.getPhysicalFile().getName())) { + realFiles.remove(node.getPhysicalFile().getName()); + } else { + i.remove(); + success = false; + } + if (node.isAllocated()) { + if (!(node instanceof DirectoryNode) && !node.checkFile()) { + success = false; + } + } + } + if (!realFiles.isEmpty()) { + success = false; + // New files showed up -- deal with them! + for (String fileName : realFiles) { + addFile(new File(physicalFile, fileName)); + } + } + return success; + } + + @Override + public void readBlock(int block, byte[] buffer) throws IOException { + checkFile(); + if (block == 0) { + generateHeader(buffer); + for (int i=0; i < 12 && i < children.size(); i++) + generateFileEntry(buffer, 4 + (i+1) * FILE_ENTRY_SIZE, i); + } else { + int start = (block * 13) - 1; + int end = start + 13; + int offset = 4; + + for (int i=start; i < end && i < children.size(); i++) { + // TODO: Add any parts that are not file entries. + generateFileEntry(buffer, offset, i); + offset += FILE_ENTRY_SIZE; + } + } + } + + public boolean accept(File file) { + if (file.getName().endsWith("~")) return false; + char c = file.getName().charAt(0); + if (c == '.' || c == '~') { + return false; + } + if (file.isHidden()) { + return false; + } + return true; + } + + /** + * Generate the directory header found in the base block of a directory + * @param buffer where to write data + */ + @SuppressWarnings("static-access") + private void generateHeader(byte[] buffer) { +// System.out.println("Generating directory header"); + // Previous block = 0 + generateWord(buffer, 0,0); + // Next block + int nextBlock = 0; + if (!additionalNodes.isEmpty()) + nextBlock = additionalNodes.get(0).baseBlock; + generateWord(buffer, 0x02, nextBlock); + // Directory header + name length + // Volumme header = 0x0f0; Subdirectory header = 0x0e0 + buffer[4]= (byte) ((baseBlock == 0x02 ? 0x0f0 : 0x0E0) + getName().length()); + generateName(buffer, 5, this); + for (int i=0x014 ; i <= 0x01b; i++) + buffer[i] = 0; + generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified()); + // Prodos 1.9 + buffer[0x020] = 0x019; + // Minimum version = 0 (no min) + buffer[0x021] = 0x000; + // Directory may be read/written to, may not be destroyed or renamed + buffer[0x022] = 0x03; + // Entry size + buffer[0x023] = (byte) FILE_ENTRY_SIZE; + // Entries per block + buffer[0x024] = (byte) 0x0d; + // Directory items count + generateWord(buffer, 0x025, children.size()); + // Volume bitmap pointer + generateWord(buffer, 0x027, ownerFilesystem.freespaceBitmap.baseBlock); + // Total number of blocks + generateWord(buffer, 0x029, ownerFilesystem.MAX_BLOCK); + } + + /** + * Generate the entry of a directory + * @param buffer where to write data + * @param offset starting offset in buffer to write + * @param fileNumber number of file (indexed in Children array) to write + */ + private void generateFileEntry(byte[] buffer, int offset, int fileNumber) throws IOException { +// System.out.println("Generating entry for "+children.get(fileNumber).getName()); + DiskNode child = children.get(fileNumber); + // Entry Type and length + buffer[offset] = (byte) ((child.getType().code << 4) + child.getName().length()); + // Name + generateName(buffer, offset+1, child); + // File type + buffer[offset + 0x010] = (byte) ((child instanceof DirectoryNode) ? 0x0f : ((FileNode) child).fileType); + // Key pointer + generateWord(buffer, offset + 0x011, child.getBaseBlock()); + // Blocks used -- will report only one unless file is actually allocated +// child.allocate(); + generateWord(buffer, offset + 0x013, 1 + child.additionalNodes.size()); + // EOF + // TODO: Verify this is the right thing to do -- is EOF total length or a modulo? + int length = ((int) child.physicalFile.length()) & 0x0ffffff; + generateWord(buffer, offset + 0x015, length & 0x0ffff); + buffer[offset + 0x017] = (byte) ((length >> 16) & 0x0ff); + // Creation date + generateTimestamp(buffer, offset + 0x018, child.physicalFile.lastModified()); + // Version = 1.9 + buffer[offset + 0x01c] = 0x19; + // Minimum version = 0 + buffer[offset + 0x01d] = 0; + // Access = all granted + buffer[offset + 0x01e] = (byte) 0x0ff; + // AUX type + if (child instanceof FileNode) + generateWord(buffer, offset + 0x01f, ((FileNode) child).loadAddress); + // Modification date + generateTimestamp(buffer, offset + 0x021, child.physicalFile.lastModified()); + // Key pointer for directory + generateWord(buffer, offset + 0x025, getBaseBlock()); + } + + private void generateTimestamp(byte[] buffer, int offset, long date) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(date); + + // yyyyyyym mmmddddd - Byte 0,1 + // ---hhhhh --mmmmmm - Byte 2,3 +// buffer[offset+1] = (byte) (((c.get(Calendar.YEAR) - 1990) << 1) + ((c.get(Calendar.MONTH)>> 3) & 1)); + buffer[offset+0] = 0; + buffer[offset+1] = 0; + buffer[offset+2] = 0; + buffer[offset+3] = 0; +// buffer[offset+2] = (byte) ((c.get(Calendar.MONTH)>> 3) & 1); +// buffer[offset+3] = (byte) (((c.get(Calendar.MONTH)&7) + c.get(Calendar.DAY_OF_MONTH)) & 0x0ff); +// buffer[offset+0] = (byte) c.get(Calendar.HOUR_OF_DAY); +// buffer[offset+1] = (byte) c.get(Calendar.MINUTE); + } + + private void generateWord(byte[] buffer, int i, int value) { + // Little endian format + buffer[i] = (byte) (value & 0x0ff); + buffer[i+1] = (byte) ((value >> 8) & 0x0ff); + } + + private void generateName(byte[] buffer, int offset, DiskNode node) { + for (int i=0; i < node.getName().length(); i++) { + buffer[offset+i] = (byte) node.getName().charAt(i); + } + } + + private void addFile(File file) { + try { + if (file.isDirectory()) { + addChild(new DirectoryNode(getOwnerFilesystem(), file)); + } else { + addChild(new FileNode(getOwnerFilesystem(), file)); + } + } catch (IOException ex) { + Logger.getLogger(DirectoryNode.class.getName()).log(Level.SEVERE, null, ex); + } + } +} diff --git a/src/main/java/jace/hardware/massStorage/DiskNode.java b/src/main/java/jace/hardware/massStorage/DiskNode.java new file mode 100644 index 0000000..2584849 --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/DiskNode.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Prodos file/directory node abstraction. This provides a lot of the glue for + * maintaining some sort of state across the virtual prodos volume and the + * physical disk folder or file represented by this node. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class DiskNode { + + public enum EntryType { + + DELETED(0), + SEEDLING(1), + SAPLING(2), + TREE(3), + SUBDIRECTORY(0x0D), + SUBDIRECTORY_HEADER(0x0E), + VOLUME_HEADER(0x0F); + public int code; + + EntryType(int c) { + code = c; + } + } + boolean allocated = false; + long allocationTime = -1L; + long lastCheckTime = -1L; + int baseBlock = -1; + List<DiskNode> additionalNodes; + ProdosVirtualDisk ownerFilesystem; + File physicalFile; + DiskNode parent; + List<DiskNode> children; + private EntryType type; + private String name; + + public DiskNode() { + additionalNodes = new ArrayList<DiskNode>(); + children = new ArrayList<DiskNode>(); + } + + public boolean checkFile() throws IOException { + allocate(); + if (physicalFile == null) { + return false; + } + if (physicalFile.lastModified() != lastCheckTime) { + lastCheckTime = physicalFile.lastModified(); + refresh(); + return false; + } + return true; + } + + public void allocate() throws IOException { + if (!allocated) { + doAllocate(); + allocationTime = System.currentTimeMillis(); + allocated = true; + ownerFilesystem.allocateEntry(this); + } + } + + public void deallocate() { + if (allocated) { + ownerFilesystem.deallocateEntry(this); + doDeallocate(); + allocationTime = -1L; + allocated = false; + additionalNodes.clear(); + // NOTE: This is recursive! + for (DiskNode node : getChildren()) { + node.deallocate(); + } + } + } + + public void refresh() { + ownerFilesystem.deallocateEntry(this); + doRefresh(); + allocationTime = System.currentTimeMillis(); + allocated = true; + ownerFilesystem.allocateEntry(this); + } + + /** + * @return the allocated + */ + public boolean isAllocated() { + return allocated; + } + + /** + * @return the allocationTime + */ + public long getAllocationTime() { + return allocationTime; + } + + /** + * @return the lastCheckTime + */ + public long getLastCheckTime() { + return lastCheckTime; + } + + /** + * @return the baseBlock + */ + public int getBaseBlock() { + return baseBlock; + } + + /** + * @param baseBlock the baseBlock to set + */ + public void setBaseBlock(int baseBlock) { + this.baseBlock = baseBlock; + } + + /** + * @return the ownerFilesystem + */ + public ProdosVirtualDisk getOwnerFilesystem() { + return ownerFilesystem; + } + + /** + * @param ownerFilesystem the ownerFilesystem to set + * @throws IOException + */ + public void setOwnerFilesystem(ProdosVirtualDisk ownerFilesystem) throws IOException { + this.ownerFilesystem = ownerFilesystem; + if (baseBlock == -1) { + setBaseBlock(ownerFilesystem.getNextFreeBlock()); + } + ownerFilesystem.allocateEntry(this); + } + + /** + * @return the physicalFile + */ + public File getPhysicalFile() { + return physicalFile; + } + + /** + * @param physicalFile the physicalFile to set + */ + public void setPhysicalFile(File physicalFile) { + this.physicalFile = physicalFile; + setName(physicalFile.getName()); + } + + /** + * @return the parent + */ + public DiskNode getParent() { + return parent; + } + + /** + * @param parent the parent to set + */ + public void setParent(DiskNode parent) { + this.parent = parent; + } + + /** + * @return the children + */ + public List<DiskNode> getChildren() { + return children; + } + + /** + * @param children the children to set + */ + public void setChildren(List<DiskNode> children) { + this.children = children; + } + + public void addChild(DiskNode child) { + children.add(child); + } + + public void removeChild(DiskNode child) { + children.remove(child); + } + + /** + * @return the type + */ + public EntryType getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(EntryType type) { + this.type = type; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + if (name.length() > 15) { + name = name.substring(0, 15); + } + this.name = name.toUpperCase(); + } + + public abstract void doDeallocate(); + + public abstract void doAllocate() throws IOException; + + public abstract void doRefresh(); + + public abstract void readBlock(int sequence, byte[] buffer) throws IOException; + + public void readBlock(byte[] buffer) throws IOException { + checkFile(); + readBlock(0, buffer); + } +} diff --git a/src/main/java/jace/hardware/massStorage/FileNode.java b/src/main/java/jace/hardware/massStorage/FileNode.java new file mode 100644 index 0000000..580fe0d --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/FileNode.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * Representation of a prodos file with a known file type and having a known + * size (either seedling, sapling or tree) + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class FileNode extends DiskNode { + + public enum FileType { + + UNKNOWN(0x00, 0x0000), + ADB(0x019, 0x0000), + AWP(0x01a, 0x0000), + ASP(0x01b, 0x0000), + BAD(0x01, 0x0000), + BIN(0x06, 0x0300), + CLASS(0xED, 0x0000), + BAS(0xfc, 0x0801), + CMD(0x0f0, 0x0000), + INT(0xfa, 0x0801), + IVR(0xfb, 0x0000), + PAS(0xef, 0x0000), + REL(0x0Fe, 0x0000), + SHK(0x0e0, 0x08002), + SDK(0x0e0, 0x08002), + SYS(0x0ff, 0x02000), + SYSTEM(0x0ff, 0x02000), + TXT(0x04, 0x0000), + U01(0x0f1, 0x0000), + U02(0x0f2, 0x0000), + U03(0x0f3, 0x0000), + U04(0x0f4, 0x0000), + U05(0x0f5, 0x0000), + U06(0x0f6, 0x0000), + U07(0x0f7, 0x0000), + U08(0x0f8, 0x0000), + VAR(0x0FD, 0x0000); + public int code = 0; + public int defaultLoadAddress = 0; + + FileType(int code, int addr) { + this.code = code; + this.defaultLoadAddress = addr; + } + } + public int fileType = 0x00; + public int loadAddress = 0x00; + public static int SEEDLING_MAX_SIZE = ProdosVirtualDisk.BLOCK_SIZE; + public static int SAPLING_MAX_SIZE = ProdosVirtualDisk.BLOCK_SIZE * 128; + + @Override + public EntryType getType() { + long fileSize = getPhysicalFile().length(); + if (fileSize <= SEEDLING_MAX_SIZE) { + setType(EntryType.SEEDLING); + return EntryType.SEEDLING; + } else if (fileSize <= SAPLING_MAX_SIZE) { + setType(EntryType.SAPLING); + return EntryType.SAPLING; + } + setType(EntryType.TREE); + return EntryType.TREE; + } + + @Override + public void setName(String name) { + String[] parts = name.split("\\."); + FileType t = null; + int offset = 0; + if (parts.length > 1) { + String extension = parts[parts.length - 1].toUpperCase(); + String[] extParts = extension.split("#"); + if (extParts.length == 2) { + offset = Integer.parseInt(extParts[1], 16); + extension = extParts[0]; + } + try { + t = FileType.valueOf(extension); + } catch (IllegalArgumentException ex) { + System.out.println("Not sure what extension " + extension + " is!"); + } + name = ""; + for (int i = 0; i < parts.length - 1; i++) { + name += (i > 0 ? "." + parts[i] : parts[i]); + } + if (extParts[extParts.length - 1].equals("SYSTEM")) { + name += ".SYSTEM"; + } + } + if (t == null) { + t = FileType.UNKNOWN; + } + if (offset == 0) { + offset = t.defaultLoadAddress; + } + fileType = t.code; + loadAddress = offset; + + // Pass usable name (stripped of file extension and other type info) as name + super.setName(name); + } + + public FileNode(ProdosVirtualDisk ownerFilesystem, File file) throws IOException { + setOwnerFilesystem(ownerFilesystem); + setPhysicalFile(file); + } + + @Override + public void doDeallocate() { + } + + @Override + public void doAllocate() throws IOException { + int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1); + int treeBlocks = 0; + if (dataBlocks > 1 && dataBlocks < 257) { + treeBlocks = 1; + } else { + treeBlocks = 1 + (dataBlocks / 256); + } + for (int i = 1; i < dataBlocks + treeBlocks; i++) { + new SubNode(i, this); + } + } + + @Override + public void doRefresh() { + } + + @Override + public void readBlock(int block, byte[] buffer) throws IOException { +// System.out.println("Read block "+block+" of file "+getName()); + switch (this.getType()) { + case SEEDLING: + readFile(buffer, 0); + break; + case SAPLING: + if (block > 0) { + readFile(buffer, (block - 1)); + } else { + // Generate seedling index block + generateIndex(buffer, 0, 256); + } + break; + case TREE: + int dataBlocks = (int) ((getPhysicalFile().length() / ProdosVirtualDisk.BLOCK_SIZE) + 1); + int treeBlocks = (dataBlocks / 256); + if (block == 0) { + generateIndex(buffer, 0, treeBlocks); + } else if (block < treeBlocks) { + int start = treeBlocks + (block - 1 * 256); + int end = Math.min(start + 256, treeBlocks); + generateIndex(buffer, treeBlocks, end); + } else { + readFile(buffer, (block - treeBlocks)); + } + break; + } + } + + private void readFile(byte[] buffer, int start) throws IOException { + FileInputStream f = new FileInputStream(physicalFile); + f.skip(start * ProdosVirtualDisk.BLOCK_SIZE); + f.read(buffer, 0, ProdosVirtualDisk.BLOCK_SIZE); + f.close(); + } + + private void generateIndex(byte[] buffer, int indexStart, int indexLimit) { + int pos = 0; + for (int i = indexStart; pos < 256 && i < indexLimit && i < additionalNodes.size(); i++, pos++) { + buffer[pos] = (byte) (additionalNodes.get(i).baseBlock & 0x0ff); + buffer[pos + 256] = (byte) ((additionalNodes.get(i).baseBlock >> 8) & 0x0ff); + } + } +} diff --git a/src/main/java/jace/hardware/massStorage/FreespaceBitmap.java b/src/main/java/jace/hardware/massStorage/FreespaceBitmap.java new file mode 100644 index 0000000..25697d1 --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/FreespaceBitmap.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.IOException; + +/** + * Maintain freespace and node allocation + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class FreespaceBitmap extends DiskNode { + int size = (ProdosVirtualDisk.MAX_BLOCK + 1) / 8 / ProdosVirtualDisk.BLOCK_SIZE; + public FreespaceBitmap(ProdosVirtualDisk fs, int start) throws IOException { + setBaseBlock(start); + setOwnerFilesystem(fs); + + for (int i=1; i < size; i++) { + new SubNode(i, this, start+i); + } + } + @Override + public void doDeallocate() { +// + } + + @Override + public void doAllocate() { +/// + } + + @Override + public void doRefresh() { +// + } + + @Override + public void readBlock(int sequence, byte[] buffer) throws IOException { + int startBlock = sequence * ProdosVirtualDisk.BLOCK_SIZE * 8; + int endBlock = (sequence+1)* ProdosVirtualDisk.BLOCK_SIZE * 8; + int bitCounter=0; + int pos=0; + int value=0; + for (int i=startBlock; i < endBlock; i++) { + if (!getOwnerFilesystem().isAllocated(i)) { + value++; + } + bitCounter++; + if (bitCounter < 8) { + value *= 2; + } else { + bitCounter = 0; + buffer[pos++]=(byte) value; + value = 0; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/massStorage/IDisk.java b/src/main/java/jace/hardware/massStorage/IDisk.java new file mode 100644 index 0000000..cfb5dd5 --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/IDisk.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.IOException; + +/** + * Generic representation of a mass storage disk, either an image or a virtual volume. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public interface IDisk { + public static int BLOCK_SIZE = 512; + public static int MAX_BLOCK = 65535; + + public void mliFormat() throws IOException; + public void mliRead(int block, int bufferAddress) throws IOException; + public void mliWrite(int block, int bufferAddress) throws IOException; + public void boot0(int slot) throws IOException; + + // Return size in 512k blocks + public int getSize(); + + public void eject(); + public boolean isWriteProtected(); + +} diff --git a/src/main/java/jace/hardware/massStorage/LargeDisk.java b/src/main/java/jace/hardware/massStorage/LargeDisk.java new file mode 100644 index 0000000..1892564 --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/LargeDisk.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import jace.apple2e.MOS65C02; +import jace.core.Computer; +import jace.core.RAM; +import static jace.hardware.ProdosDriver.*; +import jace.hardware.ProdosDriver.MLI_COMMAND_TYPE; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Representation of a hard drive or 800k disk image used by CardMassStorage + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class LargeDisk implements IDisk { + RandomAccessFile diskImage; + File diskPath; + // Offset in input file where data can be found + private int dataOffset = 0; + private int physicalBlocks = 0; + private int logicalBlocks = 0; + + public LargeDisk(File f) { + try { + readDiskImage(f); + } catch (IOException ex) { + Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + + public void mliFormat() throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void mliRead(int block, int bufferAddress) throws IOException { + RAM memory = Computer.getComputer().getMemory(); + if (block < physicalBlocks) { + diskImage.seek((block * BLOCK_SIZE) + dataOffset); + for (int i = 0; i < BLOCK_SIZE; i++) { + memory.write(bufferAddress + i, diskImage.readByte(), true, false); + } + } else { + for (int i = 0; i < BLOCK_SIZE; i++) { + memory.write(bufferAddress + i, (byte) 0, true, false); + } + } + } + + public void mliWrite(int block, int bufferAddress) throws IOException { + if (block < physicalBlocks) { + RAM memory = Computer.getComputer().getMemory(); + diskImage.seek((block * BLOCK_SIZE) + dataOffset); + byte[] buf = new byte[BLOCK_SIZE]; + for (int i = 0; i < BLOCK_SIZE; i++) { + buf[i]=memory.readRaw(bufferAddress + i); + } + diskImage.write(buf); + } + } + + public void boot0(int slot) throws IOException { + Computer.getComputer().getCpu().suspend(); + mliRead(0, 0x0800); + byte slot16 = (byte) (slot << 4); + ((MOS65C02) Computer.getComputer().getCpu()).X = slot16; + RAM memory = Computer.getComputer().getMemory(); + memory.write(CardMassStorage.SLT16, slot16, false, false); + memory.write(MLI_COMMAND, (byte) MLI_COMMAND_TYPE.READ.intValue, false, false); + memory.write(MLI_UNITNUMBER, slot16, false, false); + // Write location to block read routine to zero page + memory.writeWord(0x048, 0x0c000 + CardMassStorage.DEVICE_DRIVER_OFFSET + (slot * 0x0100), false, false); + ((MOS65C02) Computer.getComputer().getCpu()).setProgramCounter(0x0800); + Computer.getComputer().getCpu().resume(); + } + + public File getPhysicalPath() { + return diskPath; + } + + public void setPhysicalPath(File f) throws IOException { + diskPath = f; + } + + private boolean read2mg(File f) { + boolean result = false; + FileInputStream fis = null; + try { + fis = new FileInputStream(getPhysicalPath()); + if (fis.read() == 0x32 && fis.read() == 0x49 && fis.read() == 0x4D && fis.read() == 0x47) { + System.out.println("Disk is 2MG"); + // todo: read header + dataOffset = 64; + physicalBlocks = (int) (f.length() / BLOCK_SIZE); + logicalBlocks = physicalBlocks; + result = true; + } + } catch (FileNotFoundException ex) { + Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + fis.close(); + } catch (IOException ex) { + Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + return result; + } + + private void readHdv(File f) { + System.out.println("Disk is HDV"); + dataOffset = 0; + physicalBlocks = (int) (f.length() / BLOCK_SIZE); + logicalBlocks = physicalBlocks; + } + + private void readDiskImage(File f) throws FileNotFoundException, IOException { + eject(); + setPhysicalPath(f); + if (!read2mg(f)) { + readHdv(f); + } + diskImage = new RandomAccessFile(f, "rwd"); + } + + @Override + public void eject() { + if (diskImage != null) { + try { + diskImage.close(); + diskImage = null; + setPhysicalPath(null); + } catch (IOException ex) { + Logger.getLogger(LargeDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + @Override + public boolean isWriteProtected() { + return diskPath == null || !diskPath.canWrite(); + } + + @Override + public int getSize() { + return physicalBlocks; + } +} diff --git a/src/main/java/jace/hardware/massStorage/MassStorageDrive.java b/src/main/java/jace/hardware/massStorage/MassStorageDrive.java new file mode 100644 index 0000000..5642b5c --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/MassStorageDrive.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import jace.library.MediaConsumer; +import jace.library.MediaEntry; +import jace.library.MediaEntry.MediaFile; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.ImageIcon; + +/** + * + * @author brobert + */ +public class MassStorageDrive implements MediaConsumer { + IDisk disk = null; + ImageIcon icon = null; + + public ImageIcon getIcon() { + return icon; + } + + public void setIcon(ImageIcon i) { + icon = i; + } + + MediaEntry currentEntry; + MediaFile currentFile; + + public void insertMedia(MediaEntry e, MediaFile f) throws IOException { + eject(); + currentEntry = e; + currentFile = f; + disk= readDisk(currentFile.path); + } + + public MediaEntry getMediaEntry() { + return currentEntry; + } + + public MediaFile getMediaFile() { + return currentFile; + } + + public boolean isAccepted(MediaEntry e, MediaFile f) { + return e.type.isProdosOrdered; + } + + public void eject() { + if (disk != null) { + disk.eject(); + disk = null; + } + } + + private IDisk readDisk(File f) { + if (f.isFile()) { + return new LargeDisk(f); + } else if (f.isDirectory()) { + try { + return new ProdosVirtualDisk(f); + } catch (IOException ex) { + System.out.println("Unable to open virtual disk: " + ex.getMessage()); + Logger.getLogger(CardMassStorage.class.getName()).log(Level.SEVERE, null, ex); + } + } + return null; + } + + public IDisk getCurrentDisk() { + return disk; + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/massStorage/ProdosVirtualDisk.java b/src/main/java/jace/hardware/massStorage/ProdosVirtualDisk.java new file mode 100644 index 0000000..2d02ecc --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/ProdosVirtualDisk.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import jace.EmulatorUILogic; +import jace.apple2e.MOS65C02; +import jace.core.Computer; +import jace.core.RAM; +import static jace.hardware.ProdosDriver.*; +import jace.hardware.ProdosDriver.MLI_COMMAND_TYPE; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Representation of a Prodos Volume which maps to a physical folder on the + * actual hard drive. This is used by CardMassStorage in the event the disk path + * is a folder and not a disk image. FreespaceBitmap and the various Node + * classes are used to represent the filesystem structure. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ProdosVirtualDisk implements IDisk { + + public static int VOLUME_START = 2; + public static int FREESPACE_BITMAP_START = 6; + byte[] ioBuffer; + File physicalRoot; + Map<Integer, DiskNode> physicalMap; + DirectoryNode rootDirectory; + FreespaceBitmap freespaceBitmap; + + public ProdosVirtualDisk(File rootPath) throws IOException { + ioBuffer = new byte[BLOCK_SIZE]; + setPhysicalPath(rootPath); + } + + public void mliRead(int block, int bufferAddress) throws IOException { +// System.out.println("Read block " + block + " to " + Integer.toHexString(bufferAddress)); + DiskNode node = physicalMap.get(block); + RAM memory = Computer.getComputer().getMemory(); + Arrays.fill(ioBuffer, (byte) (block & 0x0ff)); + if (node == null) { + System.out.println("Reading unknown block?!"); + for (int i = 0; i < BLOCK_SIZE; i++) { + memory.write(bufferAddress + i, (byte) 0, false, false); + } + } else { +// if (node.getPhysicalFile() == null) { +// System.out.println("reading block "+block+ " from directory structure to "+Integer.toHexString(bufferAddress)); +// } else { +// System.out.println("reading block "+block+ " from "+node.getPhysicalFile().getName()+" to "+Integer.toHexString(bufferAddress)); +// } + node.readBlock(ioBuffer); + for (int i = 0; i < BLOCK_SIZE; i++) { + memory.write(bufferAddress + i, ioBuffer[i], false, false); + } + } +// for (int i=0; i < 512; i++) { +// if (i % 32 == 0 && i > 0) System.out.println(); +// System.out.print(((ioBuffer[i]&0x0ff)<16 ? "0" : "") + Integer.toHexString(ioBuffer[i] & 0x0ff) + " "); +// } +// System.out.println(); + } + + public void mliWrite(int block, int bufferAddress) throws IOException { + System.out.println("Write block " + block + " to " + Integer.toHexString(bufferAddress)); + throw new IOException("Write not implemented yet!"); +// DiskNode node = physicalMap.get(block); +// RAM memory = Computer.getComputer().getMemory(); +// if (node == null) { +// // CAPTURE WRITES TO UNUSED BLOCKS +// } else { +// node.readBlock(block, ioBuffer); +// for (int i=0; i < BLOCK_SIZE; i++) { +// memory.write(bufferAddress+i, ioBuffer[i], false); +// } +// } + } + + public void mliFormat() { + throw new UnsupportedOperationException("Formatting for this type of media is not supported!"); + } + + public File locateFile(File rootPath, String string) { + File mostLikelyMatch = null; + for (File f : rootPath.listFiles()) { + if (f.getName().equalsIgnoreCase(string)) { + return f; + } + // This is not sufficient, a more deterministic approach should be taken + if (string.toUpperCase().startsWith(f.getName().toUpperCase())) { + if (mostLikelyMatch == null || f.getName().length() > mostLikelyMatch.getName().length()) { + mostLikelyMatch = f; + } + } + } + return mostLikelyMatch; + } + + public int getNextFreeBlock() throws IOException { + // Don't allocate Zero block for anything! + // for (int i = 0; i < MAX_BLOCK; i++) { + for (int i = 2; i < MAX_BLOCK; i++) { + if (!physicalMap.containsKey(i)) { + return i; + } + } + throw new IOException("Virtual Disk Full!"); + } + + // Mark space occupied by node + public void allocateEntry(DiskNode node) { + physicalMap.put(node.baseBlock, node); + for (DiskNode sub : node.additionalNodes) { + physicalMap.put(sub.getBaseBlock(), sub); + } + } + + // Mark space occupied by nodes as free (remove allocation mapping) + public void deallocateEntry(DiskNode node) { + // Only de-map nodes if the allocation table is actually pointing to the nodes! + if (physicalMap.get(node.baseBlock) != null && physicalMap.get(node.baseBlock).equals(node)) { + physicalMap.remove(node.baseBlock); + } + for (DiskNode sub : node.additionalNodes) { + if (physicalMap.get(sub.getBaseBlock()) != null && physicalMap.get(sub.baseBlock).equals(sub)) { + physicalMap.remove(sub.getBaseBlock()); + } + } + } + + // Is the specified block in use? + public boolean isAllocated(int i) { + return (physicalMap.containsKey(i)); + } + + @Override + public void boot0(int slot) throws IOException { + File prodos = locateFile(physicalRoot, "PRODOS.SYS"); + if (prodos == null || !prodos.exists()) { + throw new IOException("Unable to locate PRODOS.SYS"); + } + Computer.getComputer().getCpu().suspend(); + byte slot16 = (byte) (slot << 4); + ((MOS65C02) Computer.getComputer().getCpu()).X = slot16; + RAM memory = Computer.getComputer().getMemory(); + memory.write(CardMassStorage.SLT16, slot16, false, false); + memory.write(MLI_COMMAND, (byte) MLI_COMMAND_TYPE.READ.intValue, false, false); + memory.write(MLI_UNITNUMBER, slot16, false, false); + // Write location to block read routine to zero page + memory.writeWord(0x048, 0x0c000 + CardMassStorage.DEVICE_DRIVER_OFFSET + (slot * 0x0100), false, false); + EmulatorUILogic.brun(prodos, 0x02000); + Computer.getComputer().getCpu().resume(); + } + + public File getPhysicalPath() { + return physicalRoot; + } + + public void setPhysicalPath(File f) throws IOException { + if (physicalRoot != null && physicalRoot.equals(f)) { + return; + } + physicalRoot = f; + physicalMap = new HashMap<Integer, DiskNode>(); + if (!physicalRoot.exists() || !physicalRoot.isDirectory()) { + try { + throw new IOException("Root path must be a directory that exists!"); + } catch (IOException ex) { + Logger.getLogger(ProdosVirtualDisk.class.getName()).log(Level.SEVERE, null, ex); + } + } + // Root directory ALWAYS starts on block 2! + rootDirectory = new DirectoryNode(this, physicalRoot, VOLUME_START); + rootDirectory.setName("VIRTUAL"); + allocateEntry(rootDirectory); + freespaceBitmap = new FreespaceBitmap(this, FREESPACE_BITMAP_START); + allocateEntry(freespaceBitmap); + + + } + + public void eject() { + // Nothing to do here... + } + + @Override + public boolean isWriteProtected() { + return true; + } + + @Override + public int getSize() { + return 0x0ffff; + } +} diff --git a/src/main/java/jace/hardware/massStorage/SubNode.java b/src/main/java/jace/hardware/massStorage/SubNode.java new file mode 100644 index 0000000..4edde7f --- /dev/null +++ b/src/main/java/jace/hardware/massStorage/SubNode.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.massStorage; + +import java.io.IOException; + +/** + * Subnode is a generic node (block) used up by a file. Thes nodes do nothing + * more than just occupy space in the freespace bitmap. The file node itself + * keeps track of its subnodes and figures out what each subnode should + * "contain". The subnodes themselves don't track anything though. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class SubNode extends DiskNode { + + int sequenceNumber; + private int seq; + + public SubNode(int seq, DiskNode parent) throws IOException { + init(seq, parent); + } + + public SubNode(int seq, DiskNode parent, int baseBlock) throws IOException { + setBaseBlock(baseBlock); + init(seq, parent); + } + + private void init(int seq, DiskNode parent) throws IOException { + sequenceNumber = seq; + setParent(parent); + setOwnerFilesystem(parent.getOwnerFilesystem()); + parent.additionalNodes.add(this); + } + + @Override + public void doDeallocate() { + } + + @Override + public void doAllocate() { + } + + @Override + public void doRefresh() { + } + + @Override + public void readBlock(int sequence, byte[] buffer) throws IOException { + parent.readBlock(sequenceNumber, buffer); + } +} diff --git a/src/main/java/jace/hardware/mockingboard/AY8910_old.java b/src/main/java/jace/hardware/mockingboard/AY8910_old.java new file mode 100644 index 0000000..ef8d8a7 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/AY8910_old.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jace.hardware.CardMockingboard; + +// Port of AY code from AppleWin -- not used buy kept for reference. + +/*************************************************************************** + * + * ay8910.c + * + * + * Emulation of the AY-3-8910 / YM2149 sound chip. + * + * Based on various code snippets by Ville Hallik, Michael Cuddy, + * Tatsuyuki Satoh, Fabrice Frances, Nicola Salmoria. + * + ***************************************************************************/ + +// +// From mame.txt (http://www.mame.net/readme.html) +// +// VI. Reuse of Source Code +// -------------------------- +// This chapter might not apply to specific portions of MAME (e.g. CPU +// emulators) which bear different copyright notices. +// The source code cannot be used in a commercial product without the written +// authorization of the authors. Use in non-commercial products is allowed, and +// indeed encouraged. If you use portions of the MAME source code in your +// program, however, you must make the full source code freely available as +// well. +// Usage of the _information_ contained in the source code is free for any use. +// However, given the amount of time and energy it took to collect this +// information, if you find new information we would appreciate if you made it +// freely available as well. +// + +public class AY8910_old { + static final int MAX_OUTPUT = 0x007fff; + static final int MAX_AY8910 = 2; + static final int CLOCK = 1789770; + static final int SAMPLE_RATE = 44100; + +// See AY8910_set_clock() for definition of STEP + static final int STEP = 0x008000; + static int num = 0, ym_num = 0; + int SampleRate = 0; + + /* register id's */ + public enum Reg { + AFine(0, 255), + ACoarse(1, 15), + BFine(2, 255), + BCoarse(3, 15), + CFine(4, 255), + CCoarse(5, 15), + NoisePeriod(6, 31), + Enable(7, 255), + AVol(8, 31), + BVol(9, 31), + CVol(10, 31), + EnvFine(11, 255), + EnvCoarse(12, 255), + EnvShape(13, 15), + PortA(14, 255), + PortB(15, 255); + + public final int registerNumber; + public final int max; + Reg(int number, int maxValue) { + registerNumber = number; + max=maxValue; + } + static Reg get(int number) { + for (Reg r:Reg.values()) + if (r.registerNumber == number) return r; + return null; + } + static public Reg[] preferredOrder = new Reg[]{ + Enable,EnvShape,EnvCoarse,EnvFine,NoisePeriod,AVol,BVol,CVol, + AFine,ACoarse,BFine,BCoarse,CFine,CCoarse}; + } + + public AY8910_old() { + chips = new ArrayList<PSG>(); + for (int i=0; i < MAX_AY8910; i++) { + PSG chip = new PSG(); + chips.add(chip); + } + initAll(CLOCK, SAMPLE_RATE); + } + +/////////////////////////////////////////////////////////// + private List<PSG> chips; + + static int[] VolTable; + static { + buildMixerTable(); + } + + private class PSG { + int Channel; + int register_latch; + Map<Reg, Integer> registers; + + int lastEnable; + int UpdateStep; + int PeriodA,PeriodB,PeriodC,PeriodN,PeriodE; + int CountA,CountB,CountC,CountN,CountE; + int VolA,VolB,VolC,VolE; + int EnvelopeA,EnvelopeB,EnvelopeC; + int OutputA,OutputB,OutputC,OutputN; + int CountEnv; + int Hold,Alternate,Attack,Holding; + int RNG; + + public PSG() { + registers = new HashMap<Reg, Integer>(); + for (Reg r: Reg.values()) + setReg(r, 0); + } + + public void reset() { + register_latch = 0; + RNG = 1; + OutputA = 0; + OutputB = 0; + OutputC = 0; + OutputN = 0x00ff; + lastEnable = -1; /* force a write */ + for (Reg r: Reg.values()) + writeReg(r, 0); + } + + public void setClock(int clock) { + /* the step clock for the tone and noise generators is the chip clock */ + /* divided by 8; for the envelope generator of the AY-3-8910, it is half */ + /* that much (clock/16), but the envelope of the YM2149 goes twice as */ + /* fast, therefore again clock/8. */ + /* Here we calculate the number of steps which happen during one sample */ + /* at the given sample rate. No. of events = sample rate / (clock/8). */ + /* STEP is a multiplier used to turn the fraction into a fixed point */ + /* number. */ + double clk = clock; + double smprate = SampleRate; + UpdateStep = (int) (((double)STEP * smprate * 8.0 + clk/2.0) / clk); + } + + public void setReg(Reg r, int value) { + registers.put(r,value); + } + public int getReg(Reg r) { + return registers.get(r); + } + + public void writeReg(Reg r, int value) { + value &= r.max; + setReg(r, value); + int old; + /* A note about the period of tones, noise and envelope: for speed reasons,*/ + /* we count down from the period to 0, but careful studies of the chip */ + /* output prove that it instead counts up from 0 until the counter becomes */ + /* greater or equal to the period. This is an important difference when the*/ + /* program is rapidly changing the period to modulate the sound. */ + /* To compensate for the difference, when the period is changed we adjust */ + /* our internal counter. */ + /* Also, note that period = 0 is the same as period = 1. This is mentioned */ + /* in the YM2203 data sheets. However, this does NOT apply to the Envelope */ + /* period. In that case, period = 0 is half as period = 1. */ + switch(r) { + case ACoarse: + case AFine: + old = PeriodA; + PeriodA = (getReg(Reg.AFine) + 256 * getReg(Reg.ACoarse)) * UpdateStep; + if (PeriodA == 0) PeriodA = UpdateStep; + CountA += PeriodA - old; + if (CountA <= 0) CountA = 1; + break; + case BCoarse: + case BFine: + old = PeriodB; + PeriodB = (getReg(Reg.BFine) + 256 * getReg(Reg.BCoarse)) * UpdateStep; + if (PeriodB == 0) PeriodB = UpdateStep; + CountB += PeriodB - old; + if (CountB <= 0) CountB = 1; + break; + case CCoarse: + case CFine: + setReg(Reg.CCoarse, getReg(Reg.CCoarse) & 0x0f); + old = PeriodC; + PeriodA = (getReg(Reg.CFine) + 256 * getReg(Reg.CCoarse)) * UpdateStep; + if (PeriodC == 0) PeriodC = UpdateStep; + CountC += PeriodC - old; + if (CountC <= 0) CountC = 1; + break; + case NoisePeriod: + old = PeriodN; + PeriodN = getReg(Reg.NoisePeriod) * UpdateStep; + if (PeriodN == 0) PeriodN = UpdateStep; + CountN += PeriodN - old; + if (CountN <= 0) CountN = 1; + break; + case Enable: + lastEnable = value; + break; + case AVol: + EnvelopeA = value & 0x10; + if (EnvelopeA > 0) + VolA = VolE; + else { + if (value > 0) + VolA = CardMockingboard.VolTable[value]; + else + VolA = CardMockingboard.VolTable[0]; + } + break; + case BVol: + EnvelopeB = value & 0x10; + if (EnvelopeB > 0) + VolB = VolE; + else { + if (value > 0) + VolB = CardMockingboard.VolTable[value]; + else + VolB = CardMockingboard.VolTable[0]; + } + break; + case CVol: + EnvelopeC = value & 0x10; + if (EnvelopeC > 0) + VolC = VolE; + else { + if (value > 0) + VolC = CardMockingboard.VolTable[value]; + else + VolC = CardMockingboard.VolTable[0]; + } + break; + case EnvFine: + case EnvCoarse: + old = PeriodE; + PeriodE = ((getReg(Reg.EnvFine) + 256 * getReg(Reg.EnvCoarse))) * UpdateStep; + if (PeriodE == 0) PeriodE = UpdateStep / 2; + CountE += PeriodE - old; + if (CountE <= 0) CountE = 1; + if (PeriodE <= 0) PeriodE = 1; + break; + case EnvShape: + /* envelope shapes: + C AtAlH + 0 0 x x \___ + 0 1 x x /|__ + 1 0 0 0 \\\\ + 1 0 0 1 \___ + 1 0 1 0 \/\/ + __ + 1 0 1 1 \| + 1 1 0 0 //// + ___ + 1 1 0 1 / + 1 1 1 0 /\/\ + 1 1 1 1 /|__ + + The envelope counter on the AY-3-8910 has 16 steps. On the YM2149 it + has twice the steps, happening twice as fast. Since the end result is + just a smoother curve, we always use the YM2149 behaviour. + */ + Attack = (value & 0x04) != 0 ? 0x1f : 0x00; + if ( (value & 0x08) == 0) { + /* if Continue = 0, map the shape to the equivalent one which has Continue = 1 */ + Hold = 1; + Alternate = Attack; + } else { + Hold = value & 0x01; + Alternate = value & 0x02; + } + CountE = PeriodE; + CountEnv = 0x1f; + Holding = 0; + VolE = CardMockingboard.VolTable[CountEnv ^ Attack]; + if (EnvelopeA != 0) VolA = VolE; + if (EnvelopeB != 0) VolB = VolE; + if (EnvelopeC != 0) VolC = VolE; + break; + case PortA: + case PortB: + break; + } + } + + void update(int[][] buffer, int length) { + int[] buf1, buf2, buf3; + int outn; + + buf1 = buffer[0]; + buf2 = buffer[1]; + buf3 = buffer[2]; + + + /* The 8910 has three outputs, each output is the mix of one of the three */ + /* tone generators and of the (single) noise generator. The two are mixed */ + /* BEFORE going into the DAC. The formula to mix each channel is: */ + /* (ToneOn | ToneDisable) & (NoiseOn | NoiseDisable). */ + /* Note that this means that if both tone and noise are disabled, the output */ + /* is 1, not 0, and can be modulated changing the volume. */ + + + /* If the channels are disabled, set their output to 1, and increase the */ + /* counter, if necessary, so they will not be inverted during this update. */ + /* Setting the output to 1 is necessary because a disabled channel is locked */ + /* into the ON state (see above); and it has no effect if the volume is 0. */ + /* If the volume is 0, increase the counter, but don't touch the output. */ + if ( (getReg(Reg.Enable) & 0x01) != 0) { + if (CountA <= length*STEP) CountA += length*STEP; + OutputA = 1; + } else if (getReg(Reg.AVol) == 0) { + /* note that I do count += length, NOT count = length + 1. You might think */ + /* it's the same since the volume is 0, but doing the latter could cause */ + /* interferencies when the program is rapidly modulating the volume. */ + if (CountA <= length*STEP) CountA += length*STEP; + } + + if ( (getReg(Reg.Enable) & 0x02) != 0) { + if (CountB <= length*STEP) CountB += length*STEP; + OutputB = 1; + } else if (getReg(Reg.BVol) == 0) { + if (CountB <= length*STEP) CountB += length*STEP; + } + + if ( (getReg(Reg.Enable) & 0x04) != 0) { + if (CountC <= length*STEP) CountC += length*STEP; + OutputC = 1; + } else if (getReg(Reg.CVol) == 0) { + if (CountC <= length*STEP) CountC += length*STEP; + } + + /* for the noise channel we must not touch OutputN - it's also not necessary */ + /* since we use outn. */ + if ((getReg(Reg.Enable) & 0x38) == 0x38) /* all off */ + if (CountN <= length*STEP) CountN += length*STEP; + + outn = (OutputN | getReg(Reg.Enable)); + int index = 0; + //System.out.println("Length:"+length); + /* buffering loop */ + while (length != 0) { + int vola,volb,volc; + int left; + + /* vola, volb and volc keep track of how long each square wave stays */ + /* in the 1 position during the sample period. */ + vola = volb = volc = 0; + //System.out.println("STEP:"+STEP); + + left = STEP; + do { + int nextevent; + + if (CountN < left) nextevent = CountN; + else nextevent = left; + + if ( (outn & 0x08) != 0) { + if (OutputA != 0) vola += CountA; + CountA -= nextevent; + /* PeriodA is the half period of the square wave. Here, in each */ + /* loop I add PeriodA twice, so that at the end of the loop the */ + /* square wave is in the same status (0 or 1) it was at the start. */ + /* vola is also incremented by PeriodA, since the wave has been 1 */ + /* exactly half of the time, regardless of the initial position. */ + /* If we exit the loop in the middle, OutputA has to be inverted */ + /* and vola incremented only if the exit status of the square */ + /* wave is 1. */ + while (CountA <= 0 && PeriodA > 0) { + CountA += PeriodA; + if (CountA > 0) { + OutputA ^= 1; + if (OutputA != 0) vola += PeriodA; + break; + } + CountA += PeriodA; + vola += PeriodA; + } + if (OutputA != 0) vola -= CountA; + } else { + CountA -= nextevent; + while (CountA <= 0 && PeriodA > 0) { + CountA += PeriodA; + if (CountA > 0) { + OutputA ^= 1; + break; + } + CountA += PeriodA; + } + } + + if ((outn & 0x10) != 0) { + if (OutputB != 0) volb += CountB; + CountB -= nextevent; + while (CountB <= 0 && PeriodB > 0) { + CountB += PeriodB; + if (CountB > 0) { + OutputB ^= 1; + if (OutputB != 0) volb += PeriodB; + break; + } + CountB += PeriodB; + volb += PeriodB; + } + if (OutputB != 0) volb -= CountB; + } else { + CountB -= nextevent; + while (CountB <= 0 && PeriodB > 0) { + CountB += PeriodB; + if (CountB > 0) { + OutputB ^= 1; + break; + } + CountB += PeriodB; + } + } + + if ( (outn & 0x20) != 0) { + if (OutputC != 0) volc += CountC; + CountC -= nextevent; + while (CountC <= 0 && PeriodC > 0) { + CountC += PeriodC; + if (CountC > 0) { + OutputC ^= 1; + if (OutputC != 0) volc += PeriodC; + break; + } + CountC += PeriodC; + volc += PeriodC; + } + if (OutputC != 0) volc -= CountC; + } else { + CountC -= nextevent; + while (CountC <= 0 && PeriodC > 0) { + CountC += PeriodC; + if (CountC > 0) { + OutputC ^= 1; + break; + } + CountC += PeriodC; + } + } + + CountN -= nextevent; + if (CountN <= 0 && PeriodN > 0) { + /* Is noise output going to change? */ + /* (bit0^bit1)? */ + if (((RNG + 1) & 2) != 0) { + OutputN ^= 0x0FF; + outn = (OutputN | getReg(Reg.Enable)); + } + + /* The Random Number Generator of the 8910 is a 17-bit shift */ + /* register. The input to the shift register is bit0 XOR bit3 */ + /* (bit0 is the output). This was verified on AY-3-8910 and YM2149 chips. */ + + /* The following is a fast way to compute bit17 = bit0^bit3. */ + /* Instead of doing all the logic operations, we only check */ + /* bit0, relying on the fact that after three shifts of the */ + /* register, what now is bit3 will become bit0, and will */ + /* invert, if necessary, bit14, which previously was bit17. */ + if ((RNG & 1) != 0) RNG ^= 0x0024000; /* This version is called the "Galois configuration". */ + RNG >>= 1; + CountN += PeriodN; + } + + left -= nextevent; + } while (left > 0); + // System.out.println("End left loop"); + + /* update envelope */ + if (Holding == 0) { + CountE -= STEP; + if (CountE <= 0) { + do { + CountEnv--; + CountE += PeriodE; + } while (CountE <= 0); + + /* check envelope current position */ + if (CountEnv < 0) { + if (Hold != 0) { + if (Alternate != 0) + Attack ^= 0x1f; + Holding = 1; + CountEnv = 0; + } else { + /* if CountEnv has looped an odd number of times (usually 1), */ + /* invert the output. */ + if ( (Alternate != 0) && ((CountEnv & 0x20) != 0)) + Attack ^= 0x1f; + CountEnv &= 0x1f; + } + } + + VolE = VolTable[CountEnv ^ Attack]; + /* reload volume */ + if (EnvelopeA != 0) VolA = VolE; + if (EnvelopeB != 0) VolB = VolE; + if (EnvelopeC != 0) VolC = VolE; + } + } + + // Output PCM wave [-32768...32767] instead of MAME's voltage level [0...32767] + // - This allows for better s/w mixing +buf1[index] = (vola * VolA) / STEP; +buf2[index] = (volb * VolB) / STEP; +buf3[index] = (volc * VolC) / STEP; +/* + if(VolA != 0) { + if (vola != 0) buf1[index] = (vola * VolA) / STEP; + else buf1[index] = -VolA; + } else { + buf1[index] = 0; + } + + // + + if(VolB != 0) { + if (volb != 0) buf2[index] = (volb * VolB) / STEP; + else buf2[index] = -VolB; + } else + buf2[index] = 0; + + // + + if(VolC != 0) { + if (volc != 0) buf3[index] = (volc * VolC) / STEP; + else buf3[index] = -VolC; + } else + buf3[index] = 0; + */ + index++; + length--; + } + } + }; + + public void writeReg(int chipNumber, int register, int value) { + Reg r = Reg.get(register); + writeReg(chipNumber, r, value); + } + + public void writeReg(int chipNumber, Reg register, int value) { + chips.get(chipNumber).writeReg(register, value); + } + + // /length/ is the number of samples we require + // NB. This should be called at twice the 6522 IRQ rate or (eg) 60Hz if no IRQ. + public void update(int chipNumber,int[][] buffer,int length) { + chips.get(chipNumber).update(buffer, length); + } + + int[][] buffers; + int bufferLength = -1; + public int[][] getBuffers(int length) { + if (buffers == null || bufferLength != length) { + buffers = new int[3][length]; + bufferLength = length; + } + return buffers; + } + + public void playSound(int size, int[] left, int[] right) { + int[][] buffers = getBuffers(left.length); + update(0, buffers, size); + mixDown(left, buffers, size); + update(1, buffers, size); + mixDown(right, buffers, size); + } + + public void mixDown(int[] out, int[][] in, int size) { + for (int i=0; i < size; i++) { + int sample = (in[0][i] + in[1][i] + in[2][i]) / 3; + out[i] = sample; + } + } + + public void setClock(int chipNumber,int clock) { + chips.get(chipNumber).setClock(clock); + } + + public void reset(int chipNumber) { + chips.get(chipNumber).reset(); + } + + public void initAll(int nClock, int nSampleRate) { + SampleRate = nSampleRate; + for (PSG p:chips) { + p.setClock(nClock); + p.reset(); + } + } + + public void initClock(int nClock) { + for (PSG p:chips) p.setClock(nClock); + } + + static void buildMixerTable() { + VolTable = new int[32]; + int SampleRate; + + /* calculate the volume->voltage conversion table */ + /* The AY-3-8910 has 16 levels, in a logarithmic scale (3dB per step) */ + /* The YM2149 still has 16 levels for the tone generators, but 32 for */ + /* the envelope generator (1.5dB per step). */ + double out = MAX_OUTPUT; + for (int i = 31;i > 0;i--) { + VolTable[i] = (int) (out + 0.5); /* round to nearest */ // [TC: unsigned int cast] + out /= 1.188502227; /* = 10 ^ (1.5/20) = 1.5dB */ + } + VolTable[0] = 0; + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/mockingboard/EnvelopeGenerator.java b/src/main/java/jace/hardware/mockingboard/EnvelopeGenerator.java new file mode 100644 index 0000000..02f3db5 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/EnvelopeGenerator.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +/** + * Envelope generator of the PSG sound chip + * Created on April 18, 2006, 5:49 PM + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class EnvelopeGenerator extends TimedGenerator { + + boolean hold = false; + boolean attk = false; + boolean alt = false; + boolean cont = false; + int direction; + int amplitude; + + public EnvelopeGenerator(int _clock, int _sampleRate) { + super(_clock, _sampleRate); + } + + public int stepsPerCycle() { + return 8; + } + + @Override + public void setPeriod(int p) { + if (p > 0) { + super.setPeriod(p); + } else { + clocksPerPeriod = stepsPerCycle() / 2; + } + } + + public void step() { + int stateChanges = updateCounter(); + for (int i = 0; i < stateChanges; i++) { + if (amplitude == 0 && direction == -1) { + if (!cont) { + direction = 0; + } else if (hold) { + direction = 0; + if (alt) { + amplitude = 15; + } + } else if (alt) { + direction = 1; + } else { + amplitude = 15; + } + } + if (amplitude == 15 && direction == 1) { + if (!cont) { + direction = 0; + amplitude = 0; + } else if (hold) { + direction = 0; + if (alt) { + amplitude = 0; + } + } else if (alt) { + direction = -1; + } else { + amplitude = 0; + } + } + amplitude += direction; + } + } + + public void setShape(int shape) { + counter = 0; + cont = (shape & 8) != 0; + attk = (shape & 4) != 0; + alt = (shape & 2) != 0; + hold = (shape & 1) != 0; + if (attk) { + amplitude = 0; + direction = 1; + } else { + amplitude = 15; + direction = -1; + } + } + + public int getAmplitude() { + return amplitude; + } + + public void reset() { + super.reset(); + setShape(0); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/mockingboard/NoiseGenerator.java b/src/main/java/jace/hardware/mockingboard/NoiseGenerator.java new file mode 100644 index 0000000..4444164 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/NoiseGenerator.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +/** + * Noise generator of the PSG sound chip. + * Created on April 18, 2006, 5:47 PM + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class NoiseGenerator extends TimedGenerator { + int rng = 0x003333; + public NoiseGenerator(int _clock,int _sampleRate) { + super(_clock, _sampleRate); + } + public int stepsPerCycle() { + return 8; + } + public void step() { + int stateChanges = updateCounter(); + for (int i=0; i < stateChanges; i++) + updateRng(); + } + public static final int bit17 = 0x010000; + public void updateRng() { + int newBit17 = (rng & 0x04) > 0 == (rng & 0x01) > 0 ? bit17 : 0; + rng = newBit17 + (rng >> 1); + } + public boolean isOn() { + return ((rng & 1) == 1); + } +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/mockingboard/PSG.java b/src/main/java/jace/hardware/mockingboard/PSG.java new file mode 100644 index 0000000..d8c7bc2 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/PSG.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of the AY sound PSG chip. This class manages register values + * and mixes the channels together (in the update method) The work of + * maintaining the sound, envelope and noise generator states is provided by the + * respective generator classes. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class PSG { + + int baseReg; + /* register ids */ + + public enum Reg { + + AFine(0, 255), + ACoarse(1, 15), + BFine(2, 255), + BCoarse(3, 15), + CFine(4, 255), + CCoarse(5, 15), + NoisePeriod(6, 31), + Enable(7, 255), + AVol(8, 31), + BVol(9, 31), + CVol(10, 31), + EnvFine(11, 255), + EnvCoarse(12, 255), + EnvShape(13, 15), + PortA(14, 255), + PortB(15, 255); + public final int registerNumber; + public final int max; + + Reg(int number, int maxValue) { + registerNumber = number; + max = maxValue; + } + + static Reg get(int number) { + for (Reg r : Reg.values()) { + if (r.registerNumber == number) { + return r; + } + } + return null; + } + static public Reg[] preferredOrder = new Reg[]{ + Enable, EnvShape, EnvCoarse, EnvFine, NoisePeriod, AVol, BVol, CVol, + AFine, ACoarse, BFine, BCoarse, CFine, CCoarse}; + } + + public enum BusControl { + + inactive(4), + read(5), + write(6), + latch(7); + int val; + + BusControl(int v) { + val = v; + } + + public static BusControl fromInt(int i) { + for (BusControl b : BusControl.values()) { + if (b.val == i) { + return b; + } + } + return null; + } + } + List<SoundGenerator> channels; + EnvelopeGenerator envelopeGenerator; + NoiseGenerator noiseGenerator; + int CLOCK; + int SAMPLE_RATE; + public int bus; + int selectedReg; + String name; + Map<Reg, Integer> regValues; + + public PSG(int base, int clock, int sample_rate, String name) { + this.name = name; + baseReg = base; + channels = new ArrayList<SoundGenerator>(); + for (int i = 0; i < 3; i++) { + channels.add(new SoundGenerator(clock, sample_rate)); + } + envelopeGenerator = new EnvelopeGenerator(clock, sample_rate); + noiseGenerator = new NoiseGenerator(clock, sample_rate); + regValues = Collections.synchronizedMap(new EnumMap<Reg, Integer>(Reg.class)); + reset(); + } + + public void setBus(int b) { + bus = b; + } + + public void setControl(int c) { + BusControl cmd = BusControl.fromInt(c); + if (cmd == null) { +// System.out.println("Bad control param "+c); + return; + } + switch (cmd) { + case inactive: + break; + case latch: +// System.out.println("PSG latched register "+selectedReg); + selectedReg = bus & 0x0f; + break; + case read: + bus = getReg(Reg.get(selectedReg)); +// System.out.println("PSG read register "+selectedReg + " == "+bus); + break; + case write: +// System.out.println("PSG wrote register "+selectedReg + " == "+bus); + setReg(Reg.get(selectedReg), bus); + break; + } + } + + public int getBaseReg() { + return baseReg; + } + + public void setRate(int clock, int sample_rate) { + CLOCK = clock; + SAMPLE_RATE = sample_rate; + for (SoundGenerator c : channels) { + c.setRate(clock, sample_rate); + } + envelopeGenerator.setRate(clock, sample_rate); + noiseGenerator.setRate(clock, sample_rate); + reset(); + } + + public final void reset() { + for (Reg r : Reg.values()) { + // Don't reset the enable register with 0, that will turn everything on! + setReg(r, r == Reg.Enable ? 255 : 0); + } + envelopeGenerator.reset(); + noiseGenerator.reset(); + for (SoundGenerator c : channels) { + c.reset(); + } + } + + public void setReg(Reg r, int value) { + if (r != null) { + value &= r.max; + regValues.put(r, value); + writeReg(r, value & 0x0ff); + } + } + + public int getReg(Reg r) { + if (r == null) { + return -1; + } + Integer value = regValues.get(r); + if (value == null) { + return -1; + } + return value & 0x0ff; + } + + public void writeReg(Reg r, int value) { + /* A note about the period of tones, noise and envelope: for speed reasons,*/ + /* we count down from the period to 0, but careful studies of the chip */ + /* output prove that it instead counts up from 0 until the counter becomes */ + /* greater or equal to the period. This is an important difference when the*/ + /* program is rapidly changing the period to modulate the sound. */ + /* To compensate for the difference, when the period is changed we adjust */ + /* our internal counter. */ + /* Also, note that period = 0 is the same as period = 1. This is mentioned */ + /* in the YM2203 data sheets. However, this does NOT apply to the Envelope */ + /* period. In that case, period = 0 is half as period = 1. */ + switch (r) { + case ACoarse: + case AFine: + channels.get(0).setPeriod(getReg(Reg.AFine) + (getReg(Reg.ACoarse) << 8)); + break; + case BCoarse: + case BFine: + channels.get(1).setPeriod(getReg(Reg.BFine) + (getReg(Reg.BCoarse) << 8)); + break; + case CCoarse: + case CFine: + channels.get(2).setPeriod(getReg(Reg.CFine) + (getReg(Reg.CCoarse) << 8)); + break; + case NoisePeriod: + noiseGenerator.setPeriod(value); + noiseGenerator.counter = 0; + break; + case Enable: + channels.get(0).setActive((value & 1) == 0); + channels.get(0).setNoiseActive((value & 8) == 0); + channels.get(1).setActive((value & 2) == 0); + channels.get(1).setNoiseActive((value & 16) == 0); + channels.get(2).setActive((value & 4) == 0); + channels.get(2).setNoiseActive((value & 32) == 0); + break; + case AVol: + channels.get(0).setAmplitude(value); + break; + case BVol: + channels.get(1).setAmplitude(value); + break; + case CVol: + channels.get(2).setAmplitude(value); + break; + case EnvFine: + case EnvCoarse: + envelopeGenerator.setPeriod(getReg(Reg.EnvFine) + 256 * getReg(Reg.EnvCoarse)); + break; + case EnvShape: + envelopeGenerator.setShape(value); + break; + case PortA: + case PortB: + break; + } + } + + public void update(int[] bufA, boolean clearA, int[] bufB, boolean clearB, int[] bufC, boolean clearC, int length) { + for (int i = 0; i < length; i++) { + noiseGenerator.step(); + envelopeGenerator.step(); + if (clearA) { + bufA[i] = channels.get(0).step(noiseGenerator, envelopeGenerator); + } else { + bufA[i] += channels.get(0).step(noiseGenerator, envelopeGenerator); + } + if (clearB) { + bufB[i] = channels.get(1).step(noiseGenerator, envelopeGenerator); + } else { + bufB[i] += channels.get(1).step(noiseGenerator, envelopeGenerator); + } + if (clearC) { + bufC[i] = channels.get(2).step(noiseGenerator, envelopeGenerator); + } else { + bufC[i] += channels.get(2).step(noiseGenerator, envelopeGenerator); + } + } + } +}; diff --git a/src/main/java/jace/hardware/mockingboard/R6522.java b/src/main/java/jace/hardware/mockingboard/R6522.java new file mode 100644 index 0000000..ba27427 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/R6522.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +import jace.core.Computer; +import jace.core.Device; + +/** + * Implementation of 6522 VIA chip + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class R6522 extends Device { + + public R6522() { + } + + // 6522 VIA + // http://www.applevault.com/twiki/Main/Mockingboard/6522.pdf + + // I/O registers + public static enum Register { + ORB(0), // Output Register B + ORA(1), // Output Register A + DDRB(2),// Data direction reg B + DDRA(3),// Data direction reg A + T1CL(4),// T1 low-order latches (low-order counter for read operations) + T1CH(5),// T1 high-order counter + T1LL(6),// T1 low-order latches + T1LH(7),// T1 high-order latches + T2CL(8),// T2 low-order latches (low-order counter for read operations) + T2CH(9),// T2 high-order counter + SR(10),// Shift register + ACR(11),// Aux control register + PCR(12),// Perripheral control register + IFR(13),// Interrupt flag register + IER(14),// Interrupt enable register + ORAH(15);// Output Register A (no handshake) + + int val; + Register(int v) { + val = v; + } + + static public Register fromInt(int i) { + for (Register r : Register.values()) { + if (r.val == i) return r; + } + return null; + } + } + // state variables + public int oraReg = 0; + public int iraReg = 0; + public int orbReg = 0; + public int irbReg = 0; + // DDRA and DDRB must be set to output for mockingboard to do anything + // Common values for this are FF for DDRA and 7 for DDRB + // DDRB bits 0-2 are used to control AY chips but bits 3-7 are not connected. + // that's why it is common to see mockingboard drivers init the port with a 7 + public int dataDirectionA = 0; + public int dataDirectionB = 0; + + // Though this is necessary for a complete emulation of the 6522, it isn't needed by the mockingboard + // set by bit 0 of ACR +// public boolean latchEnabledA = false; + // set by bit 1 of ACR +// public boolean latchEnabledB = false; + //Bits 2,3,4 of ACR +// static public enum ShiftRegisterControl { +// interruptDisabled(0), +// shiftInT2(4), +// shiftIn02(8), +// shiftInExt(12), +// shiftOutFree(16), +// shiftOutT2(20), +// shiftOut02(24), +// shiftOutExt(28); +// +// int val; +// private ShiftRegisterControl(int v) { +// val = v; +// } +// +// public static ShiftRegisterControl fromBits(int b) { +// b=b&28; +// for (ShiftRegisterControl s : values()) { +// if (s.val == b) return s; +// } +// return null; +// } +// } +// public ShiftRegisterControl shiftMode = ShiftRegisterControl.interruptDisabled; +// //Bit 5 of ACR (false = timed interrupt, true = count down pulses on PB6) +// public boolean t2countPulses = false; +// //Bit 6 of ACR (true = continuous, false = one-shot) +// public boolean t1continuous = false; +// //Bit 7 of ACR (true = enable PB7, false = interruptDisabled) +// public boolean t1enablePB7 = false; +// // NOTE: Mockingboard did not use PB6 or PB7, they are not connected to anything + public boolean timer1interruptEnabled = true; + public boolean timer1IRQ = false; // True if timer interrupt flag is set + public int timer1latch = 0; + public int timer1counter = 0; + public boolean timer1freerun = false; + public boolean timer1running = false; + public boolean timer2interruptEnabled = true; + public boolean timer2IRQ = false; // True if timer interrupt flag is set + public int timer2latch = 0; + public int timer2counter = 0; + public boolean timer2running = false; + + @Override + protected String getDeviceName() { + return "6522 VIA Chip"; + } + + @Override + public void tick() { + if (timer1running) { + timer1counter --; + if (timer1counter < 0) { + timer1counter = timer1latch; + if (!timer1freerun) timer1running = false; + if (timer1interruptEnabled) { +// System.out.println("Timer 1 generated interrupt"); + timer1IRQ = true; + Computer.getComputer().getCpu().generateInterrupt(); + } + } + } + if (timer2running) { + timer2counter --; + if (timer2counter < 0) { + timer2running = false; + timer2counter = timer2latch; + if (timer2interruptEnabled) { + timer2IRQ = true; + Computer.getComputer().getCpu().generateInterrupt(); + } + } + } + if (!timer1running && !timer2running) + setRun(false); + } + + @Override + public void attach() { + // Start chip + } + + @Override + public void detach() { + // Reset + } + + @Override + public void reconfigure() { + // Reset + } + + public void writeRegister(int reg, int value) { + value &= 0x0ff; + Register r = Register.fromInt(reg); +// System.out.println("Writing "+(value&0x0ff)+" to register "+r.toString()); + switch (r) { + case ORB: + if (dataDirectionB == 0) break; + value = value & dataDirectionB; + sendOutputB(value); + break; + case ORA: +// case ORAH: + if (dataDirectionA == 0) break; + value = value & dataDirectionA; + sendOutputA(value); + break; + case DDRB: + dataDirectionB = value; + break; + case DDRA: + dataDirectionA = value; + break; + case T1CL: + case T1LL: + timer1latch = (timer1latch & 0x0ff00) | value; + break; + case T1CH: + timer1latch = (timer1latch & 0x0ff) | (value << 8); + timer1IRQ = false; + timer1counter = timer1latch; + timer1running = true; + setRun(true); + break; + case T1LH: + timer1latch = (timer1latch & 0x0ff) | (value << 8); + timer1IRQ = false; + break; + case T2CL: + timer2latch = (timer2latch & 0x0ff00) | value; + break; + case T2CH: + timer2latch = (timer2latch & 0x0ff) | (value << 8); + timer2IRQ = false; + timer2counter = timer2latch; + timer2running = true; + setRun(true); + break; + case SR: + // SHIFT REGISTER NOT IMPLEMENTED + break; + case ACR: + // SHIFT REGISTER NOT IMPLEMENTED + timer1freerun = (value & 64) != 0; + if (timer1freerun) timer1running = true; + break; + case PCR: + // TODO: Implement if Votrax (SSI) is to be supported + break; + case IFR: + if ((value & 64) != 0) timer1IRQ = false; + if ((value & 32) != 0) timer2IRQ = false; + break; + case IER: + boolean enable = (value & 128) != 0; + if ((value & 64) != 0) timer1interruptEnabled = enable; + if ((value & 32) != 0) timer2interruptEnabled = enable; + break; + default: + } + } + + // Whatever uses 6522 will want to know when it is outputting values + // So to hook that in, these abstract methods will be defined as appropriate + public abstract void sendOutputA(int value); + public abstract void sendOutputB(int value); + + public int readRegister(int reg) { + Register r = Register.fromInt(reg); +// System.out.println("Reading register "+r.toString()); + switch (r) { + case ORB: + if (dataDirectionB == 0x0ff) break; + return receiveOutputB() & (dataDirectionB ^ 0x0ff); + case ORA: + case ORAH: + if (dataDirectionA == 0x0ff) break; + return receiveOutputA() & (dataDirectionA ^ 0x0ff); + case DDRB: + return dataDirectionB; + case DDRA: + return dataDirectionA; + case T1CL: + timer1IRQ = false; +// int out = timer1counter & 0x0ff; + // Behavior to get SkyFox to detect mockingboard -- thanks to tom@snsys.com! + timer1counter -= 8; +// return out; + return timer1counter & 0x0ff; + case T1CH: + // Behavior to get SkyFox to detect mockingboard -- thanks to tom@snsys.com! + timer1counter -= 8; + return (timer1counter & 0x0ff00) >> 8; + case T1LL: + return timer1latch & 0x0ff; + case T1LH: + return (timer1latch & 0x0ff00) >> 8; + case T2CL: + timer2IRQ = false; + return timer2counter & 0x0ff; + case T2CH: + return (timer2counter & 0x0ff00) >> 8; + case SR: + // SHIFT REGISTER NOT IMPLEMENTED + return 0; + case ACR: + // SHIFT REGISTER NOT IMPLEMENTED + if (timer1freerun) return 64; + return 0; + case PCR: + break; + case IFR: + int val = 0; + if (timer1IRQ) val |= 64; + if (timer2IRQ) val |= 32; + if (val != 0) val |= 128; + return val; + case IER: + val = 128; + if (timer1interruptEnabled) val |= 64; + if (timer2interruptEnabled) val |= 32; + return val; + } + return 0; + } + public abstract int receiveOutputA(); + public abstract int receiveOutputB(); +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/mockingboard/SoundGenerator.java b/src/main/java/jace/hardware/mockingboard/SoundGenerator.java new file mode 100644 index 0000000..3a06080 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/SoundGenerator.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +import jace.hardware.CardMockingboard; + +/** + * Square-wave generator used in the PSG sound chip. + * Created on April 18, 2006, 5:48 PM + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class SoundGenerator extends TimedGenerator { + int amplitude; + boolean useEnvGen; + boolean active; + boolean noiseActive; + boolean inverted; + public SoundGenerator(int _clock, int _sampleRate) { + super(_clock, _sampleRate); + } + + @Override + public int stepsPerCycle() { + return 8; + } + public void setAmplitude(int _amp) { + amplitude = (_amp & 0x0F); + useEnvGen = (_amp & 0x010) != 0; + } + public void setActive(boolean _active) { + active = _active; + } + public void setNoiseActive(boolean _active) { + noiseActive = _active; + } + + @Override + public void setPeriod(int _period) { + super.setPeriod(_period); + } + + public int step(NoiseGenerator noiseGen, EnvelopeGenerator envGen) { + int stateChanges = updateCounter(); + if (((stateChanges & 1) == 1)) inverted = !inverted; + if (amplitude == 0 && !useEnvGen) return 0; + if (!active && !noiseActive) return 0; + boolean invert = false; + int vol = useEnvGen ? envGen.getAmplitude() : amplitude; + if (active) { + invert = noiseActive && noiseGen.isOn() ? false : inverted; + } else { + invert = noiseActive && !noiseGen.isOn(); + } + return invert ? -CardMockingboard.VolTable[vol] : CardMockingboard.VolTable[vol]; + } + + public void reset() { + super.reset(); + amplitude = 0; + useEnvGen = false; + active = false; + noiseActive = false; + inverted = false; + } +}; \ No newline at end of file diff --git a/src/main/java/jace/hardware/mockingboard/TimedGenerator.java b/src/main/java/jace/hardware/mockingboard/TimedGenerator.java new file mode 100644 index 0000000..57c0340 --- /dev/null +++ b/src/main/java/jace/hardware/mockingboard/TimedGenerator.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.hardware.mockingboard; + +/** + * Abstraction of the generators used in the PSG chip -- this manages the + * periodicity of each generator that is more or less the same. + * Created on April 18, 2006, 5:47 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class TimedGenerator { + + int sampleRate; + int clock; + // Default period to 1 so that this can be used as a regular interval timer right away + int period = 1; + public double counter; + double cyclesPerSample; + int clocksPerPeriod; + + private TimedGenerator() { + } + + public TimedGenerator(int _clock, int _sampleRate) { + setRate(clock, sampleRate); + reset(); + } + // In most cases a cycle is a step. The AY uses 16-cycle based periods for its oscillators + // Basically this works as a hard-coded multiplier if overridden. + + public int stepsPerCycle() { + return 1; + } + + public void setRate(int clock, int sample_rate) { + sampleRate = sample_rate; + this.clock = clock; + cyclesPerSample = ((double) clock) / ((double) sampleRate); + } + + public void setPeriod(int _period) { + period = _period > 0 ? _period : 1; + clocksPerPeriod = (int) (period * stepsPerCycle()); + // set counter back... necessary? +// while (clocksPerPeriod > period) { +// counter -= clocksPerPeriod; +// } + } + + protected int updateCounter() { + counter += cyclesPerSample; + int numStateChanges = 0; + while (counter >= clocksPerPeriod) { + counter -= clocksPerPeriod; + numStateChanges++; + } + return numStateChanges; + } + + public void reset() { + counter = 0; + period = 1; + } +}; \ No newline at end of file diff --git a/src/main/java/jace/library/DiskTransferHandler.java b/src/main/java/jace/library/DiskTransferHandler.java new file mode 100644 index 0000000..87dda07 --- /dev/null +++ b/src/main/java/jace/library/DiskTransferHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import java.awt.Image; +import java.awt.datatransfer.Transferable; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.TransferHandler; + +/** + * + * @author brobert + */ +class DiskTransferHandler extends TransferHandler { + + public DiskTransferHandler() { + } + + @Override + public int getSourceActions(JComponent c) { + return COPY; + } + MediaEntry currentEntry = null; + + @Override + protected Transferable createTransferable(JComponent c) { + JList list = (JList) c; + MediaEntry entry = (MediaEntry) list.getSelectedValue(); + System.out.println("Transferrable --> " + entry.name); + currentEntry = entry; + return new TransferableMediaEntry(entry); + } + + @Override + public Image getDragImage() { + if (currentEntry == null) { + return super.getDragImage(); + } else { + return currentEntry.type.diskIcon; + } + } +} diff --git a/src/main/java/jace/library/DiskType.java b/src/main/java/jace/library/DiskType.java new file mode 100644 index 0000000..6471dd5 --- /dev/null +++ b/src/main/java/jace/library/DiskType.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.core.Utility; +import jace.hardware.FloppyDisk; +import java.awt.Image; +import java.io.File; +import javax.swing.Icon; +import javax.swing.ImageIcon; + +/** + * + * @author brobert + */ +public enum DiskType { + SINGLELOAD("Single-load binary", false, false, "rom_image.png"), + FLOPPY140_NIB("140kb nibble disk image", true, false, "525_floppy.png"), + FLOPPY140_DO("140kb Dos-ordered disk", true, false, "525_floppy.png"), + FLOPPY140_PO("140kb Prodos-ordered disk", true, true, "525_floppy.png"), + FLOPPY140_2MG("140kb disk with 2MG header", true, false, "525_floppy.png"), + FLOPPY800("800kb disk (2mg or raw)", false, true, "35_floppy.png"), + LARGE("Hard drive image (2mg or raw)", false, true, "harddrive.png"), + VIRTUAL("Virtual Prodos Volume", false, true, "harddrive.png"); + + public boolean isProdosOrdered = false; + public boolean is140kb = false; + public String description; + public Image diskIcon; + DiskType(String desc, boolean is140, boolean po, String iconPath) { + description = desc; + is140kb = is140; + isProdosOrdered = po; + diskIcon = Utility.loadIcon(iconPath).getImage(); + } + + static public DiskType determineType(File file) { + if (!file.exists()) return null; + if (file.isDirectory()) return VIRTUAL; + if (file.getName().toLowerCase().endsWith("hdv")) { + return LARGE; + } + if (file.getName().toLowerCase().endsWith("nib")) { + return FLOPPY140_NIB; + } + if (file.getName().toLowerCase().endsWith("dsk")) { + return FLOPPY140_DO; + } + long length = file.length(); + if (length <= 64*1024) return SINGLELOAD; + if (length == FloppyDisk.DISK_2MG_NIB_LENGTH || length == FloppyDisk.DISK_2MG_NON_NIB_LENGTH) + return FLOPPY140_2MG; + if (length == FloppyDisk.DISK_NIBBLE_LENGTH) + return FLOPPY140_NIB; + if (length == FloppyDisk.DISK_PLAIN_LENGTH) { + if (file.getName().toLowerCase().endsWith(".po")) return FLOPPY140_PO; + return FLOPPY140_DO; + } + if (Math.abs( (800 * 1024) - length) <= 1024) { + return FLOPPY800; + } + return LARGE; + } +} diff --git a/src/main/java/jace/library/DriveIcon.java b/src/main/java/jace/library/DriveIcon.java new file mode 100644 index 0000000..e1c8a47 --- /dev/null +++ b/src/main/java/jace/library/DriveIcon.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.library.MediaEntry.MediaFile; +import jace.ui.OutlinedLabel; +import java.awt.datatransfer.DataFlavor; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDropEvent; + +/** + * + * @author brobert + */ +public class DriveIcon extends OutlinedLabel { + + final MediaConsumer target; + + public DriveIcon(MediaConsumer mediaTarget) { + super(mediaTarget.getIcon()); + target = mediaTarget; + setText(target.getIcon().getDescription()); + setDropTarget(new DropTarget() { + @Override + public synchronized void drop(DropTargetDropEvent dtde) { + try { + String data = dtde.getTransferable().getTransferData(DataFlavor.stringFlavor).toString(); + long id = Long.parseLong(data); + MediaEntry e = MediaCache.getLocalLibrary().mediaLookup.get(id); + // Once other libraries are implemented, make sure to alias locally! +// MediaEntry entry = MediaCache.getLocalLibrary().findLocalEntry(e); + System.out.println("Inserting "+e.name+" into "+target.toString()); + MediaFile f = MediaCache.getLocalLibrary().getCurrentFile(e, true); + target.isAccepted(e, f); + target.insertMedia(e, f); + } catch (Exception ex) { + ex.printStackTrace(System.err); + dtde.rejectDrop(); + } + } + + }); + } +} diff --git a/src/main/java/jace/library/MediaCache.java b/src/main/java/jace/library/MediaCache.java new file mode 100644 index 0000000..770e852 --- /dev/null +++ b/src/main/java/jace/library/MediaCache.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.core.Utility; +import jace.library.MediaEntry.MediaFile; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Holds all information about media titles, manages low-level operations of + * downloading images from online sources, and also manages the persistence of + * the media library + * + * @author brobert + */ +public class MediaCache implements Serializable { + + public static int DELAY_BEFORE_PERSISTING_LIBRARY = 2000; + public static MediaCache LOCAL_LIBRARY; + + public Set<Long> favorites; + public Map<String, Set<Long>> nameLookup; + public Map<String, Set<Long>> categoryLookup; + public Map<String, Set<Long>> keywordLookup; + public Map<Long, MediaEntry> mediaLookup; + public long lastDirtyMarker; + + public MediaCache() { + favorites = new HashSet<Long>(); + nameLookup = new HashMap<String, Set<Long>>(); + categoryLookup = new HashMap<String, Set<Long>>(); + keywordLookup = new HashMap<String, Set<Long>>(); + mediaLookup = new HashMap<Long, MediaEntry>(); + } + + public static MediaCache getLocalLibrary() { + if (LOCAL_LIBRARY == null) { + LOCAL_LIBRARY = new MediaCache(); + LOCAL_LIBRARY.readLibraryFromDisk(); + } + return LOCAL_LIBRARY; + + } + + private void cleanup() { + cleanup(nameLookup); + cleanup(categoryLookup); + cleanup(keywordLookup); + } + + private void cleanup(Map<String, Set<Long>> lookup) { + Set<String> remove = new HashSet<String>(); + for (Entry<String, Set<Long>> entry : lookup.entrySet()) { + if (entry.getValue() == null || entry.getValue().isEmpty()) { + remove.add(entry.getKey()); + } else { + boolean hasSomething = false; + for (Iterator<Long> l = entry.getValue().iterator(); l.hasNext();) { + if (mediaLookup.containsKey(l.next())) { + hasSomething = true; + } else { + l.remove(); + } + } + if (!hasSomething) { + remove.add(entry.getKey()); + } + } + } + lookup.keySet().removeAll(remove); + } + + public void add(MediaEntry e) { + e.isLocal = this.equals(LOCAL_LIBRARY); + // Randomize ID if it is not already set + // This is a combination of the string hash as well as a 8-bit sequence number + if (e.id == 0 || e.id == -1) { + e.id = e.name.hashCode(); + e.id <<= 8; + while (mediaLookup.containsKey(e.id)) { + e.id++; + } + } + mediaLookup.put(e.id, e); + cacheEntry(nameLookup, e.name, e.id); + cacheEntry(categoryLookup, e.category, e.id); + if (e.favorite) { + favorites.add(e.id); + } + for (String s : e.keywords) { + cacheEntry(keywordLookup, s, e.id); + } + markDirty(); + } + + public void remove(MediaEntry e) { + mediaLookup.remove(e.id); + removeFiles(e); + cleanup(); + markDirty(); + } + + public void update(MediaEntry e) { + remove(e); + add(e); + } + + private void cacheEntry(Map<String, Set<Long>> cache, String key, long id) { + Set<Long> ids = cache.get(key); + if (ids == null) { + ids = new HashSet<Long>(); + cache.put(key, ids); + } + ids.add(id); + } + + public static File getMediaLibraryFolder() { + String userHome = System.getProperty("user.home"); + if (userHome == null || userHome.equals("")) { + userHome = "."; + } + File f = new File(new File(userHome, ".jace"), "mediaLibrary"); + if (!f.exists()) { + f.mkdirs(); + } + return f; + } + + // Remove file(s) associated with media entry + private void removeFiles(MediaEntry e) { + if (e.files == null) { + return; + } + for (MediaEntry.MediaFile f : e.files) { + f.path.delete(); + } + Utility.gripe("All disk images for " + e.name + " have been deleted."); + } + + public MediaEntry.MediaFile getCurrentFile(MediaEntry e, boolean isPermanent) { + if (e == null) { + return null; + } + if (e.files == null || e.files.isEmpty()) { + e.files = new ArrayList<MediaFile>(); + getLocalLibrary().add(e); + getLocalLibrary().createBlankFile(e, "Initial", !isPermanent); + getLocalLibrary().downloadImage(e, e.files.get(0), true); + } + for (MediaEntry.MediaFile f : e.files) { + if (f.activeVersion) { + return f; + } + } + e.files.get(0).activeVersion = true; + return e.files.get(0); + } + + public void saveFile(MediaEntry e, InputStream data) { + saveFile(getCurrentFile(e, MediaLibrary.CREATE_LOCAL_ON_SAVE), data); + } + + public void saveFile(MediaFile f, InputStream data) { + // TODO: If file is temporary but is supposed to be created local when saved, then move the save to a permanent file!! + if (f.temporary && MediaLibrary.CREATE_LOCAL_ON_SAVE) { + f = convertTemporaryFileToLocal(f); + } + FileOutputStream fos = null; + f.lastWritten = System.currentTimeMillis(); + try { + fos = new FileOutputStream(f.path, false); + byte[] b = new byte[4096]; + while (data.available() > 0) { + int read = data.read(b); + fos.write(b, 0, read); + } + fos.close(); + } catch (FileNotFoundException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + Utility.gripe("Could not write disk for " + f.path + " -- File not found!"); + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + Utility.gripe("Could not write disk for " + f.path + " -- I/O Exception: " + ex.getMessage()); + } finally { + try { + fos.close(); + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + boolean disableWrites = false; + + public void readLibraryFromDisk() { + disableWrites = true; + ObjectInputStream in = null; + try { + File mediaCatalogFile = getMediaLibaryCatalog(); + if (!mediaCatalogFile.exists()) { + return; + } + FileInputStream fileStream = new FileInputStream(mediaCatalogFile); + in = new ObjectInputStream(fileStream); + while (fileStream.available() > 0) { + MediaEntry e = (MediaEntry) in.readObject(); + add(e); + } + } catch (FileNotFoundException ex) { + Utility.gripe(ex.getMessage()); + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Utility.gripe(ex.getMessage()); + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } catch (ClassNotFoundException ex) { + Utility.gripe(ex.getMessage()); + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } finally { + disableWrites = false; + try { + if (in != null) { + in.close(); + } + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + public void writeLibraryToDisk() { + ObjectOutputStream out = null; + try { + File mediaCatalogFile = getMediaLibaryCatalog(); + out = new ObjectOutputStream(new FileOutputStream(mediaCatalogFile)); + for (MediaEntry e : mediaLookup.values()) { + out.writeObject(e); + } + out.close(); + } catch (FileNotFoundException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + Thread writerWorker; + + public void markDirty() { + // We don't really care if a remote library is changing + if (disableWrites || !this.equals(getLocalLibrary())) { + return; + } + lastDirtyMarker = System.nanoTime(); + if (writerWorker == null || !writerWorker.isAlive()) { + writerWorker = new Thread(new Runnable() { + long timeCheck = 0; + + public void run() { + while (true) { + try { + Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2); + // Wait half the delay before capturing the value + // this will help avoid unnecessary delays when the + // library is updated a bunch in a small interval of time + timeCheck = lastDirtyMarker; + Thread.sleep(DELAY_BEFORE_PERSISTING_LIBRARY / 2); + } catch (InterruptedException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + continue; + } + // If the invalidation time stamp changed again wait longer + if (timeCheck != lastDirtyMarker) { + continue; + } + writeLibraryToDisk(); + // If the invalidation stamp wasn't changed then the worker can exit + if (timeCheck == lastDirtyMarker) { + break; + } + } + } + }); + writerWorker.start(); + } + } + + private MediaFile downloadTempCopy(MediaEntry e) { + downloadImage(e, getCurrentFile(e, false), isDownloading); + return getCurrentFile(e, false); + } + static boolean isDownloading = false; + + public void downloadImage(final MediaEntry e, final MediaFile target, boolean wait) { + isDownloading = true; + Utility.runModalProcess("Loading disk image...", new Runnable() { + public void run() { + InputStream in = null; + try { + URI uri = null; + try { + uri = new URI(e.source); + } catch (URISyntaxException ex) { + File f = new File(e.source); + if (f.exists()) { + uri = f.toURI(); + } + } + if (uri == null) { + Utility.gripe("Unable to resolve path: " + e.source); + return; + } + in = uri.toURL().openStream(); + saveFile(target, in); + } catch (MalformedURLException ex) { + Utility.gripe("Unable to resolve path: " + e.source); + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } catch (IOException ex) { + Utility.gripe("Unable to download file: " + ex.getMessage()); + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } finally { + isDownloading = false; + try { + in.close(); + } catch (IOException ex) { + Logger.getLogger(MediaCache.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + }); + if (wait) { + int timeout = 10000; + while (timeout > 0 && isDownloading) { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + return; + } + timeout -= 100; + } + } + } + + private void createBlankFile(MediaEntry e, String label, boolean isTemporary) { + MediaFile f = new MediaEntry.MediaFile(); + e.files.add(f); + f.activeVersion = true; + f.checksum = 0L; + f.label = label; + f.lastWritten = System.currentTimeMillis(); + f.temporary = isTemporary; + // Now generate new file path + File mediaFolder = isTemporary ? getTempDirectory() : getMediaLibraryFolder(); + String name = e.name.replaceAll("[^0-9A-Za-z]", ""); + String s1 = e.name.length() > 0 ? name.substring(0, 1) : "_"; + String s2 = e.name.length() > 1 ? name.substring(1, 2) : "_"; + File sub1 = new File(mediaFolder, "_" + s1); + File sub2 = new File(sub1, "_" + s2); + sub2.mkdirs(); + f.path = new File(sub2, name + "_" + System.nanoTime()); + if (isTemporary) { + sub1.deleteOnExit(); + sub2.deleteOnExit(); + f.path.deleteOnExit(); + } + } + + private MediaFile resolveLocalCopy(MediaEntry e) { + if (!e.isLocal) { + e = findLocalEntry(e); + } + // If this is a local entry, load the current file + if (e.isLocal) { + MediaFile f = getCurrentFile(e, true); + if (f != null && f.path.exists()) { + return f; + } + } + // If there is no current file, download it + if (MediaLibrary.CREATE_LOCAL_ON_LOAD) { + getLocalLibrary().add(e); + MediaFile f = getCurrentFile(e, true); + downloadImage(e, f, true); + return f; + } else { + return downloadTempCopy(e); + } + } + + public MediaEntry findLocalEntry(MediaEntry e) { + for (MediaEntry entry : getLocalLibrary().mediaLookup.values()) { + if (entry.source.equals(e.source)) + return entry; + } + return null; + } + + private File getTempDirectory() { + String temp = System.getProperty("java.io.tmpdir"); + File tempDir = null; + if (temp != null) { + tempDir = new File(temp); + if (tempDir.exists() && tempDir.isDirectory()) { + return tempDir; + } + tempDir = new File(getMediaLibraryFolder(), "temp"); + tempDir.mkdirs(); + tempDir.deleteOnExit(); + } + return tempDir; + } + + private MediaFile convertTemporaryFileToLocal(MediaFile f) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + private File getMediaLibaryCatalog() { + return new File(getMediaLibraryFolder(), "mediacache.db"); + } +} diff --git a/src/main/java/jace/library/MediaConsumer.java b/src/main/java/jace/library/MediaConsumer.java new file mode 100644 index 0000000..eb4deb7 --- /dev/null +++ b/src/main/java/jace/library/MediaConsumer.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.library.MediaEntry.MediaFile; +import java.io.IOException; +import javax.swing.ImageIcon; + +/** + * + * @author brobert + */ +public interface MediaConsumer { + public ImageIcon getIcon(); + public void setIcon(ImageIcon i); + public void insertMedia(MediaEntry e, MediaFile f) throws IOException; + public MediaEntry getMediaEntry(); + public MediaFile getMediaFile(); + public boolean isAccepted(MediaEntry e, MediaFile f); + public void eject(); +} diff --git a/src/main/java/jace/library/MediaConsumerParent.java b/src/main/java/jace/library/MediaConsumerParent.java new file mode 100644 index 0000000..2d747e5 --- /dev/null +++ b/src/main/java/jace/library/MediaConsumerParent.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +/** + * + * @author brobert + */ +public interface MediaConsumerParent { + public MediaConsumer[] getConsumers(); +} diff --git a/src/main/java/jace/library/MediaEditUI.form b/src/main/java/jace/library/MediaEditUI.form new file mode 100644 index 0000000..484637e --- /dev/null +++ b/src/main/java/jace/library/MediaEditUI.form @@ -0,0 +1,384 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[600, 400]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="NameLabel" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="KeywordsLabel" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="SourceLabel" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="DescriptionLabel" min="-2" max="-2" attributes="0"/> + <Component id="AuthorLabel" alignment="0" min="-2" max="-2" attributes="0"/> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace min="-2" pref="5" max="-2" attributes="0"/> + <Component id="CategoryLabel" min="-2" max="-2" attributes="0"/> + </Group> + <Component id="PublishedLabel" alignment="0" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="SourceField" max="32767" attributes="0"/> + <Component id="NameField" alignment="0" max="32767" attributes="0"/> + <Component id="DescriptionPane" pref="319" max="32767" attributes="0"/> + <Component id="CategoryField" alignment="1" max="32767" attributes="0"/> + <Group type="102" alignment="1" attributes="0"> + <Component id="YearField" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="PublisherLabel" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="PublisherField" max="32767" attributes="0"/> + </Group> + <Component id="KeywordsField" max="32767" attributes="0"/> + <Component id="AuthorField" alignment="0" max="32767" attributes="0"/> + </Group> + </Group> + <Group type="102" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Component id="ReplaceButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="CopyDiskButton" min="-2" max="-2" attributes="0"/> + </Group> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace min="-2" pref="112" max="-2" attributes="0"/> + <Component id="writeProtect" min="-2" pref="140" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="FavoriteField" min="-2" max="-2" attributes="0"/> + </Group> + </Group> + <EmptySpace min="0" pref="56" max="32767" attributes="0"/> + </Group> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" max="-2" attributes="0"> + <Group type="102" alignment="1" attributes="0"> + <Component id="SaveButton" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="CancellationButton" min="-2" max="-2" attributes="0"/> + </Group> + <Component id="ScreenshotCombo" alignment="1" max="32767" attributes="0"/> + <Component id="Screenshot" alignment="1" max="32767" attributes="0"/> + <Component id="InfoLabel" alignment="1" max="32767" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Component id="Screenshot" min="-2" pref="96" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="ScreenshotCombo" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="InfoLabel" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="CancellationButton" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="SaveButton" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="ReplaceButton" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="CopyDiskButton" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + </Group> + <Group type="102" attributes="0"> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="NameLabel" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="NameField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="SourceField" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="SourceLabel" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="DescriptionLabel" min="-2" max="-2" attributes="0"/> + <Component id="DescriptionPane" pref="125" max="32767" attributes="0"/> + </Group> + <EmptySpace min="-2" pref="8" max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="CategoryLabel" alignment="3" max="32767" attributes="0"/> + <Component id="CategoryField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="YearField" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="PublishedLabel" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="PublisherLabel" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="PublisherField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="AuthorField" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="AuthorLabel" alignment="3" min="-2" pref="30" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="KeywordsField" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="KeywordsLabel" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="writeProtect" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="FavoriteField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace min="-2" pref="36" max="-2" attributes="0"/> + </Group> + </Group> + <EmptySpace max="-2" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="javax.swing.JLabel" name="NameLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Name"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="Screenshot"> + <Properties> + <Property name="horizontalAlignment" type="int" value="0"/> + <Property name="text" type="java.lang.String" value="Screenshot"/> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo"> + <EtchetBorder/> + </Border> + </Property> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[140, 96]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[140, 96]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[140, 192]"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="SourceLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Source (URL)"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="DescriptionLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Description"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="KeywordsLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Keywords"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="CategoryLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Category"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="PublishedLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Published"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="AuthorLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Author"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="SourceField"> + <Properties> + <Property name="text" type="java.lang.String" value="http://website/diskImage.dsk"/> + <Property name="toolTipText" type="java.lang.String" value="URL of where disk came from, can also be a local file path"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="NameField"> + <Properties> + <Property name="text" type="java.lang.String" value="Disk name"/> + <Property name="toolTipText" type="java.lang.String" value="Name of disk -- if part of a series include side or disk number"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="CategoryField"> + <Properties> + <Property name="text" type="java.lang.String" value="Category > sub cat > sub cat"/> + <Property name="toolTipText" type="java.lang.String" value="Category (use > marks to separate sub-category)"/> + </Properties> + </Component> + <Container class="javax.swing.JScrollPane" name="DescriptionPane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTextArea" name="DescriptionField"> + <Properties> + <Property name="columns" type="int" value="20"/> + <Property name="rows" type="int" value="5"/> + <Property name="text" type="java.lang.String" value="Description of disk goes here"/> + </Properties> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JTextField" name="AuthorField"> + <Properties> + <Property name="text" type="java.lang.String" value="Smith, John"/> + </Properties> + </Component> + <Component class="javax.swing.JComboBox" name="YearField"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="40"> + <StringItem index="0" value="1976"/> + <StringItem index="1" value="1977"/> + <StringItem index="2" value="1978"/> + <StringItem index="3" value="1979"/> + <StringItem index="4" value="1980"/> + <StringItem index="5" value="1981"/> + <StringItem index="6" value="1982"/> + <StringItem index="7" value="1983"/> + <StringItem index="8" value="1984"/> + <StringItem index="9" value="1985"/> + <StringItem index="10" value="1986"/> + <StringItem index="11" value="1987"/> + <StringItem index="12" value="1988"/> + <StringItem index="13" value="1989"/> + <StringItem index="14" value="1990"/> + <StringItem index="15" value="1991"/> + <StringItem index="16" value="1992"/> + <StringItem index="17" value="1993"/> + <StringItem index="18" value="1994"/> + <StringItem index="19" value="1995"/> + <StringItem index="20" value="1996"/> + <StringItem index="21" value="1997"/> + <StringItem index="22" value="1998"/> + <StringItem index="23" value="1999"/> + <StringItem index="24" value="2000"/> + <StringItem index="25" value="2001"/> + <StringItem index="26" value="2002"/> + <StringItem index="27" value="2003"/> + <StringItem index="28" value="2004"/> + <StringItem index="29" value="2005"/> + <StringItem index="30" value="2006"/> + <StringItem index="31" value="2007"/> + <StringItem index="32" value="2008"/> + <StringItem index="33" value="2009"/> + <StringItem index="34" value="2010"/> + <StringItem index="35" value="2011"/> + <StringItem index="36" value="2012"/> + <StringItem index="37" value="2013"/> + <StringItem index="38" value="2014"/> + <StringItem index="39" value="2015"/> + </StringArray> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="KeywordsField"> + <Properties> + <Property name="text" type="java.lang.String" value="Keyword1, Keyword2, Keyword3"/> + <Property name="toolTipText" type="java.lang.String" value="Relevant keywords (use commas to separate multiple keywords)"/> + </Properties> + </Component> + <Component class="javax.swing.JCheckBox" name="FavoriteField"> + <Properties> + <Property name="text" type="java.lang.String" value="Show in favorites"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="SaveButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Save"/> + <Property name="toolTipText" type="java.lang.String" value="Save metadata changes"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="SaveButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="CancellationButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Close"/> + <Property name="toolTipText" type="java.lang.String" value="Abandon metadata changes and close"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="CancellationButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="ReplaceButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Replace Disk"/> + <Property name="toolTipText" type="java.lang.String" value="Abandon current disk image and download a new copy from the source URL"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="ReplaceButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JLabel" name="InfoLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Disk usage here"/> + <Property name="verticalAlignment" type="int" value="1"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="CopyDiskButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Save copy of disk"/> + <Property name="toolTipText" type="java.lang.String" value="Create a local copy of the disk image on your hard drive"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="CopyDiskButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JComboBox" name="ScreenshotCombo"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="3"> + <StringItem index="0" value="Screenshot"/> + <StringItem index="1" value="Box (front)"/> + <StringItem index="2" value="Box (back)"/> + </StringArray> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="ScreenshotComboActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JTextField" name="PublisherField"> + <Properties> + <Property name="text" type="java.lang.String" value="Publisher"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="PublisherLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="By"/> + </Properties> + </Component> + <Component class="javax.swing.JCheckBox" name="writeProtect"> + <Properties> + <Property name="text" type="java.lang.String" value="Write Protect"/> + </Properties> + </Component> + </SubComponents> +</Form> diff --git a/src/main/java/jace/library/MediaEditUI.java b/src/main/java/jace/library/MediaEditUI.java new file mode 100644 index 0000000..269a0bf --- /dev/null +++ b/src/main/java/jace/library/MediaEditUI.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.Emulator; +import jace.EmulatorUILogic; +import static jace.EmulatorUILogic.MEDIA_MANAGER_EDIT_DIALOG_NAME; +import jace.library.MediaEntry.MediaFile; + +/** + * + * @author brobert + */ +public class MediaEditUI extends javax.swing.JPanel { + + public static String CREATE_TITLE = "Create media entry"; + public static String EDIT_TITLE = "Edit media entry"; + public static String KEYWORD_DELIMITER = ","; + public static String BOX_BACK = "Box (back)"; + public static String BOX_FRONT = "Box (front)"; + public static String SCREENSHOT = "Screenshot"; + + /** + * Creates new form MediaEditUI + */ + public MediaEditUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + NameLabel = new javax.swing.JLabel(); + Screenshot = new javax.swing.JLabel(); + SourceLabel = new javax.swing.JLabel(); + DescriptionLabel = new javax.swing.JLabel(); + KeywordsLabel = new javax.swing.JLabel(); + CategoryLabel = new javax.swing.JLabel(); + PublishedLabel = new javax.swing.JLabel(); + AuthorLabel = new javax.swing.JLabel(); + SourceField = new javax.swing.JTextField(); + NameField = new javax.swing.JTextField(); + CategoryField = new javax.swing.JTextField(); + DescriptionPane = new javax.swing.JScrollPane(); + DescriptionField = new javax.swing.JTextArea(); + AuthorField = new javax.swing.JTextField(); + YearField = new javax.swing.JComboBox(); + KeywordsField = new javax.swing.JTextField(); + FavoriteField = new javax.swing.JCheckBox(); + SaveButton = new javax.swing.JButton(); + CancellationButton = new javax.swing.JButton(); + ReplaceButton = new javax.swing.JButton(); + InfoLabel = new javax.swing.JLabel(); + CopyDiskButton = new javax.swing.JButton(); + ScreenshotCombo = new javax.swing.JComboBox(); + PublisherField = new javax.swing.JTextField(); + PublisherLabel = new javax.swing.JLabel(); + writeProtect = new javax.swing.JCheckBox(); + + setPreferredSize(new java.awt.Dimension(600, 400)); + + NameLabel.setText("Name"); + + Screenshot.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + Screenshot.setText("Screenshot"); + Screenshot.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + Screenshot.setMaximumSize(new java.awt.Dimension(140, 96)); + Screenshot.setMinimumSize(new java.awt.Dimension(140, 96)); + Screenshot.setPreferredSize(new java.awt.Dimension(140, 192)); + + SourceLabel.setText("Source (URL)"); + + DescriptionLabel.setText("Description"); + + KeywordsLabel.setText("Keywords"); + + CategoryLabel.setText("Category"); + + PublishedLabel.setText("Published"); + + AuthorLabel.setText("Author"); + + SourceField.setText("http://website/diskImage.dsk"); + SourceField.setToolTipText("URL of where disk came from, can also be a local file path"); + + NameField.setText("Disk name"); + NameField.setToolTipText("Name of disk -- if part of a series include side or disk number"); + + CategoryField.setText("Category > sub cat > sub cat"); + CategoryField.setToolTipText("Category (use > marks to separate sub-category)"); + + DescriptionField.setColumns(20); + DescriptionField.setRows(5); + DescriptionField.setText("Description of disk goes here"); + DescriptionPane.setViewportView(DescriptionField); + + AuthorField.setText("Smith, John"); + + YearField.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "1976", "1977", "1978", "1979", "1980", "1981", "1982", "1983", "1984", "1985", "1986", "1987", "1988", "1989", "1990", "1991", "1992", "1993", "1994", "1995", "1996", "1997", "1998", "1999", "2000", "2001", "2002", "2003", "2004", "2005", "2006", "2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015" })); + + KeywordsField.setText("Keyword1, Keyword2, Keyword3"); + KeywordsField.setToolTipText("Relevant keywords (use commas to separate multiple keywords)"); + + FavoriteField.setText("Show in favorites"); + + SaveButton.setText("Save"); + SaveButton.setToolTipText("Save metadata changes"); + SaveButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + SaveButtonActionPerformed(evt); + } + }); + + CancellationButton.setText("Close"); + CancellationButton.setToolTipText("Abandon metadata changes and close"); + CancellationButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + CancellationButtonActionPerformed(evt); + } + }); + + ReplaceButton.setText("Replace Disk"); + ReplaceButton.setToolTipText("Abandon current disk image and download a new copy from the source URL"); + ReplaceButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + ReplaceButtonActionPerformed(evt); + } + }); + + InfoLabel.setText("Disk usage here"); + InfoLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP); + + CopyDiskButton.setText("Save copy of disk"); + CopyDiskButton.setToolTipText("Create a local copy of the disk image on your hard drive"); + CopyDiskButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + CopyDiskButtonActionPerformed(evt); + } + }); + + ScreenshotCombo.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Screenshot", "Box (front)", "Box (back)" })); + ScreenshotCombo.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + ScreenshotComboActionPerformed(evt); + } + }); + + PublisherField.setText("Publisher"); + + PublisherLabel.setText("By"); + + writeProtect.setText("Write Protect"); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(NameLabel) + .addComponent(KeywordsLabel) + .addComponent(SourceLabel) + .addComponent(DescriptionLabel) + .addComponent(AuthorLabel) + .addGroup(layout.createSequentialGroup() + .addGap(5, 5, 5) + .addComponent(CategoryLabel)) + .addComponent(PublishedLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(SourceField) + .addComponent(NameField) + .addComponent(DescriptionPane, javax.swing.GroupLayout.DEFAULT_SIZE, 319, Short.MAX_VALUE) + .addComponent(CategoryField, javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(YearField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(PublisherLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(PublisherField)) + .addComponent(KeywordsField) + .addComponent(AuthorField))) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(ReplaceButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(CopyDiskButton)) + .addGroup(layout.createSequentialGroup() + .addGap(112, 112, 112) + .addComponent(writeProtect, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(FavoriteField))) + .addGap(0, 56, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(SaveButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(CancellationButton)) + .addComponent(ScreenshotCombo, javax.swing.GroupLayout.Alignment.TRAILING, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(Screenshot, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(InfoLabel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(Screenshot, javax.swing.GroupLayout.PREFERRED_SIZE, 96, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(ScreenshotCombo, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(InfoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(CancellationButton) + .addComponent(SaveButton) + .addComponent(ReplaceButton) + .addComponent(CopyDiskButton))) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(NameLabel) + .addComponent(NameField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(SourceField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(SourceLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(DescriptionLabel) + .addComponent(DescriptionPane, javax.swing.GroupLayout.DEFAULT_SIZE, 125, Short.MAX_VALUE)) + .addGap(8, 8, 8) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(CategoryLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(CategoryField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(YearField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(PublishedLabel) + .addComponent(PublisherLabel) + .addComponent(PublisherField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(AuthorField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(AuthorLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(KeywordsField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(KeywordsLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(writeProtect) + .addComponent(FavoriteField)) + .addGap(36, 36, 36))) + .addContainerGap()) + ); + }// </editor-fold>//GEN-END:initComponents + + private void SaveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_SaveButtonActionPerformed + persist(); + library.refreshUI(); + Emulator.getFrame().closeDialog(MEDIA_MANAGER_EDIT_DIALOG_NAME); + }//GEN-LAST:event_SaveButtonActionPerformed + + /** + * If confirmed, the current disk image will be wiped out and reloaded from + * the source if the source can still be found + * + * @param evt ignored + */ + private void ReplaceButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_ReplaceButtonActionPerformed + if (!EmulatorUILogic.confirm("This will wipe out your local copy permanently. Only press OK if you are sure you want this to happen.")) { + return; + } + MediaFile f = library.cache.getCurrentFile(editingEntity, true); + library.cache.downloadImage(editingEntity, f, false); + }//GEN-LAST:event_ReplaceButtonActionPerformed + + private void CopyDiskButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_CopyDiskButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_CopyDiskButtonActionPerformed + + private void CancellationButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_CancellationButtonActionPerformed + if (EmulatorUILogic.confirm("Abandon unsaved changes? Are you sure?")) { + Emulator.getFrame().closeDialog(MEDIA_MANAGER_EDIT_DIALOG_NAME); + } + }//GEN-LAST:event_CancellationButtonActionPerformed + + /** + * Look at what type of screenshot to show and present it if available. + * + * @param evt passed in but actually just ignored + */ + private void ScreenshotComboActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_ScreenshotComboActionPerformed + }//GEN-LAST:event_ScreenshotComboActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField AuthorField; + private javax.swing.JLabel AuthorLabel; + private javax.swing.JButton CancellationButton; + private javax.swing.JTextField CategoryField; + private javax.swing.JLabel CategoryLabel; + private javax.swing.JButton CopyDiskButton; + private javax.swing.JTextArea DescriptionField; + private javax.swing.JLabel DescriptionLabel; + private javax.swing.JScrollPane DescriptionPane; + private javax.swing.JCheckBox FavoriteField; + private javax.swing.JLabel InfoLabel; + private javax.swing.JTextField KeywordsField; + private javax.swing.JLabel KeywordsLabel; + private javax.swing.JTextField NameField; + private javax.swing.JLabel NameLabel; + private javax.swing.JLabel PublishedLabel; + private javax.swing.JTextField PublisherField; + private javax.swing.JLabel PublisherLabel; + private javax.swing.JButton ReplaceButton; + private javax.swing.JButton SaveButton; + private javax.swing.JLabel Screenshot; + private javax.swing.JComboBox ScreenshotCombo; + private javax.swing.JTextField SourceField; + private javax.swing.JLabel SourceLabel; + private javax.swing.JComboBox YearField; + private javax.swing.JCheckBox writeProtect; + // End of variables declaration//GEN-END:variables + MediaEntry editingEntity; + + void populate(MediaEntry entity) { + // TODO: Implement all fields + AuthorField.setText(entity.author); +// entity.type +// entity.auxtype + CategoryField.setText(entity.category); + DescriptionField.setText(entity.description); + FavoriteField.setSelected(entity.favorite); + KeywordsField.setText(joinKeywords(entity.keywords)); + NameField.setText(entity.name); + PublisherField.setText(entity.publisher); + if (entity.boxBackURL == null) { + ScreenshotCombo.removeItem(BOX_BACK); + } + if (entity.boxFrontURL == null) { + ScreenshotCombo.removeItem(BOX_FRONT); + } + if (entity.screenshotURL == null) { + ScreenshotCombo.removeItem(SCREENSHOT); + } + if (ScreenshotCombo.getItemCount() == 0) { + Screenshot.setVisible(false); + ScreenshotCombo.setVisible(false); + } + SourceField.setText(entity.source); + writeProtect.setSelected(entity.writeProtected); + YearField.setSelectedItem(entity.year); + editingEntity = entity; + } + + void persist() { + editingEntity.author = AuthorField.getText(); +// entity.type +// entity.auxtype + editingEntity.category = CategoryField.getText(); + editingEntity.description = DescriptionField.getText(); + editingEntity.favorite = FavoriteField.isSelected(); + editingEntity.keywords = splitKeywords(KeywordsField.getText()); + editingEntity.name = NameField.getText(); + editingEntity.publisher = PublisherField.getText(); + editingEntity.source = SourceField.getText(); + editingEntity.writeProtected = writeProtect.isSelected(); + editingEntity.year = String.valueOf(YearField.getSelectedItem()); + + if (createMode) { + MediaCache.getLocalLibrary().add(editingEntity); + setCreate(false); + } else { + MediaCache.getLocalLibrary().update(editingEntity); + } + } + + boolean createMode = false; + + void setCreate(boolean b) { + createMode = b; + if (createMode) { + setName(CREATE_TITLE); + } else { + setName(EDIT_TITLE); + } + } + MediaLibraryUI library = null; + + void setParentLibary(MediaLibraryUI library) { + this.library = library; + } + + private String[] splitKeywords (String keywords) { + return keywords.split(KEYWORD_DELIMITER); + } + + private String joinKeywords(String[] keywords) { + if (keywords == null) { + return ""; + } + StringBuilder out = new StringBuilder(); + for (String keyword : keywords) { + if (keyword.length() == 0) { + continue; + } + if (out.length() > 0) { + out.append(KEYWORD_DELIMITER); + } + out.append(keyword); + } + return out.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/jace/library/MediaEntry.java b/src/main/java/jace/library/MediaEntry.java new file mode 100644 index 0000000..89bc8a2 --- /dev/null +++ b/src/main/java/jace/library/MediaEntry.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +/** + * + * @author brobert + */ +public class MediaEntry implements Serializable { + + @Override + public String toString() { + return (name == null || name.length() == 0) ? "No name" : name; + } + + public long id; + public boolean isLocal; + public String source; + public String name; + public String[] keywords = new String[0]; + public String category; + public String description; + public String year; + public String author; + public String publisher; + public String screenshotURL; + public String boxFrontURL; + public String boxBackURL; + public boolean favorite; + public DiskType type; + public String auxtype; + public boolean writeProtected; + public List<MediaFile> files; + + public static class MediaFile implements Serializable { + public long checksum; + public File path; + public boolean activeVersion; + public String label; + public long lastRead; + public long lastWritten; + volatile public boolean temporary = false; + } +} diff --git a/src/main/java/jace/library/MediaLibrary.java b/src/main/java/jace/library/MediaLibrary.java new file mode 100644 index 0000000..01e1fc0 --- /dev/null +++ b/src/main/java/jace/library/MediaLibrary.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.config.ConfigurableField; +import jace.config.Reconfigurable; +import jace.core.Card; +import jace.core.Computer; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import javax.swing.JPanel; + +/** + * Main entry point of the media library Serves as an interface between the UI + * and the media cache Also provides a touchpoint for emulator configuration + * options + * + * @author brobert + */ +public class MediaLibrary implements Reconfigurable { + + public static MediaLibrary instance; + + public static MediaLibrary getInstance() { + if (instance == null) { + instance = new MediaLibrary(); + } + return instance; + } + //-------------------------------------------------------------------------- + @ConfigurableField(category = "Library", defaultValue = "true", name = "Auto-add", description = "Automatically download and save local copies of disks when played.") + public static boolean CREATE_LOCAL_ON_LOAD = true; + @ConfigurableField(category = "Library", defaultValue = "true", name = "Keep local copy", description = "Automatically download and save local copies of disks when written.") + public static boolean CREATE_LOCAL_ON_SAVE = true; + + public String getName() { + return "Media Library"; + } + + public String getShortName() { + return "media"; + } + + public void reconfigure() { + rebuildDriveList(); + } + //-------------------------------------------------------------------------- + MediaManagerUI userInterface; + + public JPanel buildUserInterface() { + userInterface = new MediaManagerUI(); + rebuildDriveList(); + rebuildTabs(); + return userInterface; + } + + public void rebuildDriveList() { + if (userInterface == null) { + return; + } + userInterface.Drives.removeAll(); + for (Card card : Computer.getComputer().memory.getAllCards()) { + if (card == null || !(card instanceof MediaConsumerParent)) { + continue; + } + MediaConsumerParent parent = (MediaConsumerParent) card; + GridBagLayout layout = (GridBagLayout) userInterface.Drives.getLayout(); + GridBagConstraints c = new GridBagConstraints(); + for (MediaConsumer consumer : parent.getConsumers()) { + DriveIcon drive = new DriveIcon(consumer); + drive.setSize(100, 70); + drive.setPreferredSize(new Dimension(100, 70)); + c.gridwidth = GridBagConstraints.REMAINDER; + layout.setConstraints(drive, c); + userInterface.Drives.add(drive); + } + } + userInterface.Drives.revalidate(); + } + + private void rebuildTabs() { + userInterface.Libraries.removeAll(); + MediaCache localLibraryCache = MediaCache.getLocalLibrary(); + MediaLibraryUI localLibrary = new MediaLibraryUI(); + localLibrary.setName("Local"); + localLibrary.setCache(localLibraryCache); + localLibrary.setLocal(true); + userInterface.Libraries.add(localLibrary); + userInterface.Libraries.revalidate(); + } + + public MediaEditUI buildEditInstance(MediaLibraryUI library, MediaEntry entry) { + MediaEditUI form = new MediaEditUI(); + if (entry == null) { + // create form + form.setCreate(true); + form.populate(new MediaEntry()); + } else { + form.setCreate(false); + form.populate(entry); + } + form.setParentLibary(library); + return form; + } +} \ No newline at end of file diff --git a/src/main/java/jace/library/MediaLibraryUI.form b/src/main/java/jace/library/MediaLibraryUI.form new file mode 100644 index 0000000..f8a0bd7 --- /dev/null +++ b/src/main/java/jace/library/MediaLibraryUI.form @@ -0,0 +1,198 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.6" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="orderToolbar" min="-2" max="-2" attributes="0"/> + <Component id="tocPane" pref="207" max="32767" attributes="0"/> + </Group> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace min="-2" pref="3" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="actionToolbar" min="-2" max="-2" attributes="0"/> + <Component id="titleLabel" min="-2" pref="261" max="-2" attributes="0"/> + <Component id="descriptionLabel" min="-2" pref="261" max="-2" attributes="0"/> + </Group> + </Group> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="titlesScrollpane" pref="277" max="32767" attributes="0"/> + </Group> + </Group> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="orderToolbar" alignment="0" min="-2" pref="25" max="-2" attributes="0"/> + <Component id="actionToolbar" alignment="0" min="-2" pref="25" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="1" attributes="0"> + <Group type="102" attributes="0"> + <Component id="titlesScrollpane" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="titleLabel" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="descriptionLabel" min="-2" pref="110" max="-2" attributes="0"/> + </Group> + <Component id="tocPane" pref="0" max="32767" attributes="0"/> + </Group> + </Group> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="tocPane"> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTree" name="tocTree"> + <Events> + <EventHandler event="valueChanged" listener="javax.swing.event.TreeSelectionListener" parameters="javax.swing.event.TreeSelectionEvent" handler="tocTreeValueChanged"/> + </Events> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JToolBar" name="orderToolbar"> + <Properties> + <Property name="rollover" type="boolean" value="true"/> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="orderByLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Order By:"/> + </Properties> + </Component> + <Component class="javax.swing.JComboBox" name="orderByCombo"> + <Properties> + <Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor"> + <StringArray count="7"> + <StringItem index="0" value="Name"/> + <StringItem index="1" value="Favorites"/> + <StringItem index="2" value="Recently Used"/> + <StringItem index="3" value="Category"/> + <StringItem index="4" value="Source"/> + <StringItem index="5" value="Keyword"/> + <StringItem index="6" value="Author"/> + </StringArray> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="orderByComboActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JToolBar" name="actionToolbar"> + <Properties> + <Property name="rollover" type="boolean" value="true"/> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout"/> + <SubComponents> + <Component class="javax.swing.JButton" name="CreateNew"> + <Properties> + <Property name="text" type="java.lang.String" value="Create"/> + <Property name="focusable" type="boolean" value="false"/> + <Property name="horizontalTextPosition" type="int" value="0"/> + <Property name="verticalTextPosition" type="int" value="3"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="CreateNewActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="ViewEdit"> + <Properties> + <Property name="text" type="java.lang.String" value="View"/> + <Property name="focusable" type="boolean" value="false"/> + <Property name="horizontalTextPosition" type="int" value="0"/> + <Property name="verticalTextPosition" type="int" value="3"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="ViewEditActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="Remove"> + <Properties> + <Property name="text" type="java.lang.String" value="Remove"/> + <Property name="focusable" type="boolean" value="false"/> + <Property name="horizontalTextPosition" type="int" value="0"/> + <Property name="verticalTextPosition" type="int" value="3"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="RemoveActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JButton" name="Favorite"> + <Properties> + <Property name="text" type="java.lang.String" value="Favorite"/> + <Property name="focusable" type="boolean" value="false"/> + <Property name="horizontalTextPosition" type="int" value="0"/> + <Property name="verticalTextPosition" type="int" value="3"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="FavoriteActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JScrollPane" name="titlesScrollpane"> + <AuxValues> + <AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JList" name="titlesList"> + <Properties> + <Property name="model" type="javax.swing.ListModel" editor="org.netbeans.modules.form.editors2.ListModelEditor"> + <StringArray count="1"> + <StringItem index="0" value="Software titles listed here"/> + </StringArray> + </Property> + <Property name="selectionMode" type="int" value="0"/> + <Property name="dragEnabled" type="boolean" value="true"/> + </Properties> + </Component> + </SubComponents> + </Container> + <Component class="javax.swing.JLabel" name="titleLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="SoftwareTitle"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="descriptionLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.modules.form.editors2.FontEditor"> + <FontInfo relative="true"> + <Font component="descriptionLabel" italic="true" property="font" relativeSize="true" size="0"/> + </FontInfo> + </Property> + <Property name="horizontalAlignment" type="int" value="2"/> + <Property name="text" type="java.lang.String" value="Software Description Here"/> + <Property name="verticalAlignment" type="int" value="1"/> + </Properties> + </Component> + </SubComponents> +</Form> diff --git a/src/main/java/jace/library/MediaLibraryUI.java b/src/main/java/jace/library/MediaLibraryUI.java new file mode 100644 index 0000000..5d3c961 --- /dev/null +++ b/src/main/java/jace/library/MediaLibraryUI.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import jace.Emulator; +import jace.EmulatorUILogic; +import static jace.EmulatorUILogic.MEDIA_MANAGER_EDIT_DIALOG_NAME; +import static jace.EmulatorUILogic.MEDIA_MANAGER_DIALOG_NAME; +import jace.core.Utility; +import java.awt.datatransfer.DataFlavor; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.List; +import java.util.Set; +import java.util.Vector; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; + +/** + * + * @author brobert + */ +public class MediaLibraryUI extends javax.swing.JPanel { + + public static final String EMPTY_VALUE = "_EMPTY_"; + public static final String MISCELLANEOUS = "Misc."; + + /** + * Creates new form MediaLibraryUI + */ + public MediaLibraryUI() { + initComponents(); + titlesList.setTransferHandler(new DiskTransferHandler()); + titlesList.setDragEnabled(true); + titlesList.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + if (e.getClickCount() > 1) { + ViewEditActionPerformed(null); + } + } + }); + setDropTarget(new DropTarget() { + public synchronized void drop(DropTargetDropEvent evt) { + try { + evt.acceptDrop(DnDConstants.ACTION_COPY); + List<File> droppedFiles = null; + try { + droppedFiles = (List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + } catch (ClassCastException e) { + return; + } + boolean added = false; + for (File file : droppedFiles) { + MediaEntry entry = new MediaEntry(); + entry.name = file.getName(); + entry.source = file.toURI().toURL().toExternalForm(); + entry.type = DiskType.determineType(file); + entry.description = "Added via drag-and-drop to media library"; + if (null == MediaCache.getLocalLibrary().findLocalEntry(entry)) { + MediaCache.getLocalLibrary().add(entry); + added = true; + } + } + if (added) { + refreshUI(); + } + } catch (Exception ex) { + ex.printStackTrace(System.err); + Utility.gripe("Could not add file to library: " + ex.getMessage()); + } + } + }); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + tocPane = new javax.swing.JScrollPane(); + tocTree = new javax.swing.JTree(); + orderToolbar = new javax.swing.JToolBar(); + orderByLabel = new javax.swing.JLabel(); + orderByCombo = new javax.swing.JComboBox(); + actionToolbar = new javax.swing.JToolBar(); + CreateNew = new javax.swing.JButton(); + ViewEdit = new javax.swing.JButton(); + Remove = new javax.swing.JButton(); + Favorite = new javax.swing.JButton(); + titlesScrollpane = new javax.swing.JScrollPane(); + titlesList = new javax.swing.JList(); + titleLabel = new javax.swing.JLabel(); + descriptionLabel = new javax.swing.JLabel(); + + tocTree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { + public void valueChanged(javax.swing.event.TreeSelectionEvent evt) { + tocTreeValueChanged(evt); + } + }); + tocPane.setViewportView(tocTree); + + orderToolbar.setRollover(true); + + orderByLabel.setText("Order By:"); + orderToolbar.add(orderByLabel); + + orderByCombo.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Name", "Favorites", "Recently Used", "Category", "Source", "Keyword", "Author" })); + orderByCombo.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + orderByComboActionPerformed(evt); + } + }); + orderToolbar.add(orderByCombo); + + actionToolbar.setRollover(true); + + CreateNew.setText("Create"); + CreateNew.setFocusable(false); + CreateNew.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + CreateNew.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + CreateNew.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + CreateNewActionPerformed(evt); + } + }); + actionToolbar.add(CreateNew); + + ViewEdit.setText("View"); + ViewEdit.setFocusable(false); + ViewEdit.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + ViewEdit.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + ViewEdit.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + ViewEditActionPerformed(evt); + } + }); + actionToolbar.add(ViewEdit); + + Remove.setText("Remove"); + Remove.setFocusable(false); + Remove.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + Remove.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + Remove.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + RemoveActionPerformed(evt); + } + }); + actionToolbar.add(Remove); + + Favorite.setText("Favorite"); + Favorite.setFocusable(false); + Favorite.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + Favorite.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + Favorite.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + FavoriteActionPerformed(evt); + } + }); + actionToolbar.add(Favorite); + + titlesList.setModel(new javax.swing.AbstractListModel() { + String[] strings = { "Software titles listed here" }; + public int getSize() { return strings.length; } + public Object getElementAt(int i) { return strings[i]; } + }); + titlesList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + titlesList.setDragEnabled(true); + titlesScrollpane.setViewportView(titlesList); + + titleLabel.setText("SoftwareTitle"); + + descriptionLabel.setFont(descriptionLabel.getFont().deriveFont((descriptionLabel.getFont().getStyle() | java.awt.Font.ITALIC))); + descriptionLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + descriptionLabel.setText("Software Description Here"); + descriptionLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(orderToolbar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tocPane)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(3, 3, 3) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(actionToolbar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(titleLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 261, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 261, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(titlesScrollpane)))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(orderToolbar, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(actionToolbar, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addComponent(titlesScrollpane) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(titleLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 110, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(tocPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE))) + ); + }// </editor-fold>//GEN-END:initComponents + + private void ViewEditActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_ViewEditActionPerformed + if (getSelectedEntry() == null) { + return; + } + System.out.println(getSelectedEntry().name); + Emulator.getFrame().registerModalDialog( + MediaLibrary.getInstance().buildEditInstance(this, getSelectedEntry()), + MEDIA_MANAGER_EDIT_DIALOG_NAME, + MEDIA_MANAGER_DIALOG_NAME, true); + Emulator.getFrame().showDialog(MEDIA_MANAGER_EDIT_DIALOG_NAME); + }//GEN-LAST:event_ViewEditActionPerformed + + private void RemoveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_RemoveActionPerformed + MediaEntry selection = getSelectedEntry(); + if (selection == null) { + return; + } + String message = "Are you sure you want to remove " + selection.name + " from the library? This cannot be undone!"; + if (!EmulatorUILogic.confirm(message)) { + return; + } + cache.remove(selection); + refreshUI(); + }//GEN-LAST:event_RemoveActionPerformed + + private void CreateNewActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_CreateNewActionPerformed + Emulator.getFrame().registerModalDialog( + MediaLibrary.getInstance().buildEditInstance(this, null), + MEDIA_MANAGER_EDIT_DIALOG_NAME, + MEDIA_MANAGER_DIALOG_NAME, true); + Emulator.getFrame().showDialog(MEDIA_MANAGER_EDIT_DIALOG_NAME); + }//GEN-LAST:event_CreateNewActionPerformed + + private void FavoriteActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_FavoriteActionPerformed + MediaEntry selection = getSelectedEntry(); + if (selection == null) { + return; + } + selection.favorite = true; + cache.update(selection); + }//GEN-LAST:event_FavoriteActionPerformed + + private void orderByComboActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_orderByComboActionPerformed + updateTOCTree(); + }//GEN-LAST:event_orderByComboActionPerformed + + private void tocTreeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_tocTreeValueChanged + titlesList.removeAll(); + TreeModel model = tocTree.getModel(); + if (!(model instanceof TocTreeModel)) { + return; + } + TocTreeModel tocModel = (TocTreeModel) model; + Set<Long> allEntries = tocModel.getEntries(evt.getPath().getLastPathComponent()); + if (allEntries == null) { + return; + } + Vector v = new Vector(); + for (Long l : allEntries) { + v.add(cache.mediaLookup.get(l)); + } + titlesList.setListData(v); + }//GEN-LAST:event_tocTreeValueChanged + // Variables declaration - do not modify//GEN-BEGIN:variables + public javax.swing.JButton CreateNew; + public javax.swing.JButton Favorite; + public javax.swing.JButton Remove; + public javax.swing.JButton ViewEdit; + public javax.swing.JToolBar actionToolbar; + public javax.swing.JLabel descriptionLabel; + public javax.swing.JComboBox orderByCombo; + public javax.swing.JLabel orderByLabel; + public javax.swing.JToolBar orderToolbar; + public javax.swing.JLabel titleLabel; + public javax.swing.JList titlesList; + public javax.swing.JScrollPane titlesScrollpane; + public javax.swing.JScrollPane tocPane; + public javax.swing.JTree tocTree; + // End of variables declaration//GEN-END:variables + MediaCache cache; + + public void setCache(MediaCache libraryCache) { + cache = libraryCache; + updateTOCTree(); + } + boolean localLibrary; + + public void setLocal(boolean b) { + localLibrary = b; + } + + public boolean isLocal() { + return localLibrary; + } + + private MediaEntry getSelectedEntry() { + Object sel = titlesList.getSelectedValue(); + if (sel == null || !(sel instanceof MediaEntry)) { + return null; + } + return (MediaEntry) sel; + } + + public void refreshUI() { + //TODO: Preserve selections in toc tree and media title list! + updateTOCTree(); + } + + public static interface tocSortModel { + + public TreeModel buildModel(MediaCache cache); + } + + public static enum tocOptions { + + Name("Name", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + Set<String> names = cache.nameLookup.keySet(); + TocTreeModel model = new TocTreeModel(); + model.name = "All By Name"; + for (String name : names) { + if (name == null || name.length() == 0) { + name = EMPTY_VALUE; + } + String letter = name.toUpperCase().substring(0, 1); + model.addItems(letter, name, cache.nameLookup.get(name)); + } + model.twoLevel = false; + return model; + } + }), + Favorites( + "Favorites", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + Set<String> names = cache.nameLookup.keySet(); + TocTreeModel model = new TocTreeModel(); + model.name = "Favorites By Name"; + for (String name : names) { + // Filter out only the favorites + Set<Long> entries = cache.nameLookup.get(name); + entries.retainAll(cache.favorites); + if (entries.isEmpty()) { + continue; + } + if (name == null || name.length() == 0) { + name = EMPTY_VALUE; + } + String letter = name.toUpperCase().substring(0, 1); + model.addItems(letter, name, entries); + } + model.twoLevel = false; + return model; + } + }), + Recently_Used("Recently Used", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + TreeModel model = new DefaultTreeModel(null, true); + return model; + } + }), + Category("Category", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + Set<String> names = cache.categoryLookup.keySet(); + TocTreeModel model = new TocTreeModel(); + model.name = "All By Category"; + for (String name : names) { + if (name == null || name.length() == 0) { + name = EMPTY_VALUE; + } + String[] categories = name.split(":|/"); + String sub = (categories.length > 1 ? categories[1] : MISCELLANEOUS); + model.addItems(categories[0], sub, cache.nameLookup.get(name)); + } + return model; + } + }), + Source("Source", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + TreeModel model = new DefaultTreeModel(null, true); + return model; + } + }), + Keyword("Keyword", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + Set<String> names = cache.keywordLookup.keySet(); + TocTreeModel model = new TocTreeModel(); + model.name = "All By Keyword"; + for (String name : names) { + if (name == null || name.length() == 0) { + name = EMPTY_VALUE; + } + String letter = name.toUpperCase().substring(0, 1); + model.addItems(letter, name, cache.keywordLookup.get(name)); + } + return model; + } + }), + Author("Author", new tocSortModel() { + public TreeModel buildModel(MediaCache cache) { + TreeModel model = new DefaultTreeModel(null, true); + return model; + } + }); + String alternate; + public tocSortModel factory; + + tocOptions(String alt, tocSortModel modelFactory) { + alternate = alt; + factory = modelFactory; + } + + public static tocOptions fromString(String str) { + try { + return tocOptions.valueOf(str); + } catch (Throwable t) { + for (tocOptions option : tocOptions.values()) { + if (option.alternate.equalsIgnoreCase(str)) { + return option; + } + } + } + return null; + } + }; + + private void updateTOCTree() { + String order = String.valueOf(orderByCombo.getSelectedItem()); + tocOptions sortOption = tocOptions.fromString(order); + tocTree.setModel(sortOption.factory.buildModel(cache)); + } +} \ No newline at end of file diff --git a/src/main/java/jace/library/MediaManagerUI.form b/src/main/java/jace/library/MediaManagerUI.form new file mode 100644 index 0000000..f31a6d0 --- /dev/null +++ b/src/main/java/jace/library/MediaManagerUI.form @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.8" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="Libraries" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="Drives" min="-2" max="-2" attributes="0"/> + <EmptySpace min="-2" pref="4" max="-2" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="Libraries" pref="0" max="32767" attributes="0"/> + <Component id="Drives" alignment="0" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JTabbedPane" name="Libraries"> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout"/> + <SubComponents> + <Component class="jace.library.MediaLibraryUI" name="mediaLibraryUI2"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription"> + <JTabbedPaneConstraints tabName="Local"> + <Property name="tabTitle" type="java.lang.String" value="Local"/> + </JTabbedPaneConstraints> + </Constraint> + </Constraints> + </Component> + <Component class="jace.library.MediaLibraryUI" name="mediaLibraryUI3"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription"> + <JTabbedPaneConstraints tabName="Virtual II"> + <Property name="tabTitle" type="java.lang.String" value="Virtual II"/> + </JTabbedPaneConstraints> + </Constraint> + </Constraints> + </Component> + <Container class="javax.swing.JPanel" name="addLibraryTab"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout$JTabbedPaneConstraintsDescription"> + <JTabbedPaneConstraints tabName="Add New Library"> + <Property name="tabTitle" type="java.lang.String" value="Add New Library"/> + </JTabbedPaneConstraints> + </Constraint> + </Constraints> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="libraryAddInstructionsLabel" pref="0" max="32767" attributes="0"/> + <Group type="102" attributes="0"> + <Component id="librarySourceLabel" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="libraryPathField" pref="370" max="32767" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="addLibraryButton" min="-2" max="-2" attributes="0"/> + </Group> + </Group> + <EmptySpace max="-2" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="libraryAddInstructionsLabel" min="-2" pref="63" max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="libraryPathField" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="librarySourceLabel" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="addLibraryButton" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace pref="269" max="32767" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="javax.swing.JTextField" name="libraryPathField"> + <Properties> + <Property name="text" type="java.lang.String" value="jTextField1"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="libraryPathFieldActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JLabel" name="librarySourceLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="Source"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="libraryAddInstructionsLabel"> + <Properties> + <Property name="text" type="java.lang.String" value="<html><p>Either type in the URL or path to the source, or drag it from another window (file manager or web browser) -- The source should be the path to the XML catalog, not just the path to the main website.</p>"/> + </Properties> + </Component> + <Component class="javax.swing.JButton" name="addLibraryButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Add"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="addLibraryButtonActionPerformed"/> + </Events> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="Drives"> + <Properties> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 430]"/> + </Property> + </Properties> + <LayoutCode> + <CodeStatement> + <CodeExpression id="1_DrivesLayout"> + <CodeVariable name="DrivesLayout" type="4096" declaredType="java.awt.GridBagLayout"/> + <ExpressionOrigin> + <ExpressionProvider type="CodeConstructor"> + <CodeConstructor class="java.awt.GridBagLayout" parameterTypes=""/> + </ExpressionProvider> + </ExpressionOrigin> + </CodeExpression> + <StatementProvider type="CodeExpression"> + <CodeExpression id="1_DrivesLayout"/> + </StatementProvider> + </CodeStatement> + <CodeStatement> + <CodeExpression id="1_DrivesLayout"/> + <StatementProvider type="CodeField"> + <CodeField name="columnWidths" class="java.awt.GridBagLayout"/> + </StatementProvider> + <Parameters> + <CodeExpression id="2"> + <ExpressionOrigin> + <Value type="[I" editor="org.netbeans.modules.form.layoutsupport.delegates.GridBagLayoutSupport$IntArrayPropertyEditor"> + <PropertyValue value="[1]"/> + </Value> + </ExpressionOrigin> + </CodeExpression> + </Parameters> + </CodeStatement> + <CodeStatement> + <CodeExpression id="1_DrivesLayout"/> + <StatementProvider type="CodeField"> + <CodeField name="rowHeights" class="java.awt.GridBagLayout"/> + </StatementProvider> + <Parameters> + <CodeExpression id="3"> + <ExpressionOrigin> + <Value type="[I" editor="org.netbeans.modules.form.layoutsupport.delegates.GridBagLayoutSupport$IntArrayPropertyEditor"> + <PropertyValue value="[10]"/> + </Value> + </ExpressionOrigin> + </CodeExpression> + </Parameters> + </CodeStatement> + <CodeStatement> + <CodeExpression id="4_Drives"> + <CodeVariable name="Drives" type="8193" declaredType="javax.swing.JPanel"/> + <ExpressionOrigin> + <ExpressionProvider type="ComponentRef"> + <ComponentRef name="Drives"/> + </ExpressionProvider> + </ExpressionOrigin> + </CodeExpression> + <StatementProvider type="CodeMethod"> + <CodeMethod name="setLayout" class="java.awt.Container" parameterTypes="java.awt.LayoutManager"/> + </StatementProvider> + <Parameters> + <CodeExpression id="1_DrivesLayout"/> + </Parameters> + </CodeStatement> + </LayoutCode> + </Container> + </SubComponents> +</Form> diff --git a/src/main/java/jace/library/MediaManagerUI.java b/src/main/java/jace/library/MediaManagerUI.java new file mode 100644 index 0000000..fd6ee87 --- /dev/null +++ b/src/main/java/jace/library/MediaManagerUI.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +/** + * + * @author brobert + */ +public class MediaManagerUI extends javax.swing.JPanel { + + /** + * Creates new form MediaManagerUI + */ + public MediaManagerUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + Libraries = new javax.swing.JTabbedPane(); + mediaLibraryUI2 = new jace.library.MediaLibraryUI(); + mediaLibraryUI3 = new jace.library.MediaLibraryUI(); + addLibraryTab = new javax.swing.JPanel(); + libraryPathField = new javax.swing.JTextField(); + librarySourceLabel = new javax.swing.JLabel(); + libraryAddInstructionsLabel = new javax.swing.JLabel(); + addLibraryButton = new javax.swing.JButton(); + Drives = new javax.swing.JPanel(); + + Libraries.addTab("Local", mediaLibraryUI2); + Libraries.addTab("Virtual II", mediaLibraryUI3); + + libraryPathField.setText("jTextField1"); + libraryPathField.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + libraryPathFieldActionPerformed(evt); + } + }); + + librarySourceLabel.setText("Source"); + + libraryAddInstructionsLabel.setText("<html><p>Either type in the URL or path to the source, or drag it from another window (file manager or web browser) -- The source should be the path to the XML catalog, not just the path to the main website.</p>"); + + addLibraryButton.setText("Add"); + addLibraryButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addLibraryButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout addLibraryTabLayout = new javax.swing.GroupLayout(addLibraryTab); + addLibraryTab.setLayout(addLibraryTabLayout); + addLibraryTabLayout.setHorizontalGroup( + addLibraryTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(addLibraryTabLayout.createSequentialGroup() + .addContainerGap() + .addGroup(addLibraryTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(libraryAddInstructionsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(addLibraryTabLayout.createSequentialGroup() + .addComponent(librarySourceLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(libraryPathField, javax.swing.GroupLayout.DEFAULT_SIZE, 370, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(addLibraryButton))) + .addContainerGap()) + ); + addLibraryTabLayout.setVerticalGroup( + addLibraryTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(addLibraryTabLayout.createSequentialGroup() + .addContainerGap() + .addComponent(libraryAddInstructionsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addGroup(addLibraryTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(libraryPathField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(librarySourceLabel) + .addComponent(addLibraryButton)) + .addContainerGap(269, Short.MAX_VALUE)) + ); + + Libraries.addTab("Add New Library", addLibraryTab); + + Drives.setMinimumSize(new java.awt.Dimension(100, 430)); + java.awt.GridBagLayout DrivesLayout = new java.awt.GridBagLayout(); + DrivesLayout.columnWidths = new int[] {1}; + DrivesLayout.rowHeights = new int[] {10}; + Drives.setLayout(DrivesLayout); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(Libraries) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(Drives, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(4, 4, 4)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(Libraries, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addComponent(Drives, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + }// </editor-fold>//GEN-END:initComponents + + private void libraryPathFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_libraryPathFieldActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_libraryPathFieldActionPerformed + + private void addLibraryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addLibraryButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_addLibraryButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + public javax.swing.JPanel Drives; + public javax.swing.JTabbedPane Libraries; + public javax.swing.JButton addLibraryButton; + public javax.swing.JPanel addLibraryTab; + public javax.swing.JLabel libraryAddInstructionsLabel; + public javax.swing.JTextField libraryPathField; + public javax.swing.JLabel librarySourceLabel; + public jace.library.MediaLibraryUI mediaLibraryUI2; + public jace.library.MediaLibraryUI mediaLibraryUI3; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/jace/library/TocTreeModel.java b/src/main/java/jace/library/TocTreeModel.java new file mode 100644 index 0000000..649c6e3 --- /dev/null +++ b/src/main/java/jace/library/TocTreeModel.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +/** + * + * @author brobert + */ +public class TocTreeModel implements TreeModel { + String name; + boolean twoLevel = true; + Map<String, Map<String, Set<Long>>> tree = new TreeMap<String, Map<String, Set<Long>>>() { + @Override + public String toString() { + return name; + } + }; + + public void addItems(String parent, final String sub, Set<Long> entries) { + if (entries == null || entries.isEmpty()) return; + Map<String, Set<Long>> parentNode = tree.get(parent); + if (parentNode == null) { + parentNode = new TreeMap<String, Set<Long>>(); + tree.put(parent, parentNode); + } + Set<Long> allEntries = parentNode.get(sub); + if (allEntries == null) { + allEntries = new TreeSet<Long>() { + @Override + public String toString() { + return sub; + } + }; + parentNode.put(sub, allEntries); + } + allEntries.addAll(entries); + } + + public Object getRoot() { + return tree; + } + + public Object getChild(Object parent, int index) { + if (parent == getRoot()) { + return tree.keySet().toArray()[index]; + } + if (parent instanceof String) { + if (tree.get((String) parent) != null) { + return tree.get((String) parent).values().toArray()[index]; + } + } + return null; + } + + public int getChildCount(Object parent) { + if (parent == getRoot()) { + return tree.keySet().size(); + } + if (twoLevel && parent instanceof String) { + if (tree.get((String) parent) != null) { + return tree.get((String) parent).values().size(); + } + } + + return 0; + } + + public boolean isLeaf(Object node) { + return getChildCount(node) == 0; + } + + public void valueForPathChanged(TreePath path, Object newValue) { + // Do nothing... + } + + public int getIndexOfChild(Object parent, Object child) { + if (parent instanceof String) { + String n = (String) parent; + int index = 0; + for (String c : tree.get((String) parent).keySet()) { + if (c.equals(child)) { + return index; + } + index++; + } + } + return -1; + } + + public void addTreeModelListener(TreeModelListener l) { + // Do nothing... + } + + public void removeTreeModelListener(TreeModelListener l) { + // Do nothing... + } + + public Set<Long> getEntries(Object selection) { + if (selection.equals(this)) return getEntries(tree); + if (selection instanceof Set) return (Set<Long>) selection; + if (Map.class.isInstance(selection)) { + Set<Long> all = new LinkedHashSet<Long>(); + for (Object val : ((Map) selection).values()) { + Set<Long> entries = getEntries(val); + if (entries != null) all.addAll(getEntries(val)); + } + return all; + } + if (selection instanceof String) { + Set<Long> values = new LinkedHashSet<Long>(); + Map<String, Set<Long>> children = tree.get(String.valueOf(selection)); + for (Set<Long> val : children.values()) { + values.addAll(val); + } + return values; + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/jace/library/TransferableMediaEntry.java b/src/main/java/jace/library/TransferableMediaEntry.java new file mode 100644 index 0000000..e680a7d --- /dev/null +++ b/src/main/java/jace/library/TransferableMediaEntry.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 brobert. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.library; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +/** + * + * @author brobert + */ +class TransferableMediaEntry implements Transferable { + MediaEntry entry; + public TransferableMediaEntry(MediaEntry entry) { + this.entry = entry; + } + + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[]{DataFlavor.stringFlavor}; + } + + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor.isFlavorTextType(); + } + + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + return String.valueOf(entry.id); + } + +} diff --git a/src/main/java/jace/state/ObjectGraphNode.java b/src/main/java/jace/state/ObjectGraphNode.java new file mode 100644 index 0000000..3424c77 --- /dev/null +++ b/src/main/java/jace/state/ObjectGraphNode.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.state; + +import jace.Emulator; +import java.io.Serializable; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This represents an object graph in a serializable way. The emulator object + * tree is not preserved in a saved state (it would be too bulky) so it is + * important that state can be restored. This means that serialized object + * values need to be merged to a new object graph, and that graph needs to be + * traversed cleanly and correctly. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ObjectGraphNode<T> implements Serializable { + + public ObjectGraphNode parent; + public String name; + public int index; + public boolean isStateful; + public boolean forceCheck; // If true, object must always be inspected for changes -- more expensive + public List<ObjectGraphNode> children; + transient public WeakReference<T> source; + transient public DirtyFlag dirty; + transient public Class<T> type; + public boolean isPrimitive = false; + + public static enum DirtyFlag { + + UNKNOWN, CLEAN, DIRTY + }; + + public ObjectGraphNode(Class<T> clazz) { + children = new ArrayList<ObjectGraphNode>(); + type = clazz; + dirty = DirtyFlag.UNKNOWN; + forceCheck = true; + } + + public ObjectGraphNode(T obj) { + this((Class<T>) obj.getClass()); + source = (WeakReference<T>) new WeakReference(obj); + } + + public T getCurrentValue() { + if (isPrimitive || type.isPrimitive()) { + try { + return (T) parent.type.getField(name).get(parent.getCurrentValue()); + } catch (IllegalArgumentException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (NoSuchFieldException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } else { + return source.get(); + } + } + + public void markClean() { + dirty = DirtyFlag.CLEAN; + } + + public void markDirty() { + dirty = DirtyFlag.DIRTY; + } + + public boolean isDirty() { + return dirty == DirtyFlag.DIRTY; + } + + public void setCurrentValue(Object value) { + if (parent == null) { + return; + } + if (List.class.isAssignableFrom(parent.type)) { + // This is a list index + List p = (List) parent.getCurrentValue(); + p.set(index, value); + } else if (Map.class.isAssignableFrom(parent.type)) { + // this is a map entry + Map p = (Map) parent.getCurrentValue(); + // TODO: If keys are not strings then this will not work -- write something to resolve keys + p.put(name, value); + } else if (parent.type.isArray()) { + // This is an array index + Object[] p = (Object[]) parent.getCurrentValue(); + if (p != null) { + p[index] = value; + } + } else { + // Must be an object member + Object p = parent.getCurrentValue(); + try { + parent.type.getField(name).set(p, value); + } catch (NoSuchFieldException ex) { + System.out.println(parent.name + "." + name); + System.out.println("This = "+type); + System.out.println("Parent = " + parent.type); + System.out.println("this Is Array: " + type.isArray()); + System.out.println("parent Is Array: " + parent.type.isArray()); + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (SecurityException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalArgumentException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + Logger.getLogger(ObjectGraphNode.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + public ObjectGraphNode find(String path) { + String[] parts = path.split("\\."); + ObjectGraphNode current = this; + for (int i = 0; i < parts.length; i++) { + for (ObjectGraphNode child : (List<ObjectGraphNode>) current.children) { + if (child.name.equals(parts[i])) { + current = child; + break; + } else { + return null; + } + } + } + return current; + } + + public boolean valueChanged(State tail) { + if (!forceCheck || isDirty()) { + return isDirty(); + } + + //?? Let's be lazy and just say yes for now. + return true; + + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof ObjectGraphNode)) { + return false; + } + ObjectGraphNode node = (ObjectGraphNode) obj; + if (parent == null) { + if (node.parent != null) { + return false; + } + } else { + if (node.parent == null) { + return false; + } else if (!node.parent.equals(parent)) { + return false; + } + } + return (name.equals(node.name) && index == node.index); + } +} diff --git a/src/main/java/jace/state/State.java b/src/main/java/jace/state/State.java new file mode 100644 index 0000000..0f790c9 --- /dev/null +++ b/src/main/java/jace/state/State.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.state; + +import java.awt.image.BufferedImage; +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * A state is nothing more than a map of captured variable values, except that a + * state can also be a delta (set of changes) from a previous state. This allows + * states to occupy a lot less memory and be more efficiently managed. States + * are chained together in a linked list, using a special delete method to + * ensure that delta states are properly merged. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class State extends HashMap<ObjectGraphNode, StateValue> implements Serializable { + + boolean deltaState; + State previousState; + State nextState; + // Tail is only correct on the head node, everything else will likely be null + State tail; + BufferedImage screenshot; + + /** + * Removing the next state allows a LRU buffer of states -- but states can't + * simple be discarded, they have to be more carefully managed because most + * states only track deltas. Things like memory have to be merged correctly. + * + * This state will merge with the next state and then remove it from the + * linked list. + * + * @return + */ + public State deleteNext() { + if (nextState == null) { + return null; + } + if (nextState.deltaState) { + putAll(nextState); + + nextState = nextState.nextState; + + if (nextState == null) { + tail = this; + } + return this; + } else { + nextState.tail = tail; + nextState.previousState = previousState; + return nextState; + } + } + + public void addState(State newState) { + newState.previousState = this; + nextState = newState; + } + + public void apply() { + Set<ObjectGraphNode> applied = new HashSet<ObjectGraphNode>(); + State current = this; + while (current != null) { + for (StateValue val : current.values()) { + if (!applied.contains(val.node)) { + System.out.print(val.node.parent.parent != null ? val.node.parent.parent.name + "." : ""); + System.out.print(val.node.parent != null ? val.node.parent.name + "." : ""); + System.out.print(val.node.name); + System.out.print("=="); + System.out.println(val.value == null ? "NULL" : val.value.getClass().toString()); + val.node.setCurrentValue(val.value); + applied.add(val.node); + } + } + if (!current.deltaState) { + current = null; + } else { + current = current.previousState; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/state/StateManager.java b/src/main/java/jace/state/StateManager.java new file mode 100644 index 0000000..f072f10 --- /dev/null +++ b/src/main/java/jace/state/StateManager.java @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.state; + +import jace.Emulator; +import jace.apple2e.SoftSwitches; +import jace.config.ConfigurableField; +import jace.config.Reconfigurable; +import jace.core.Computer; +import jace.core.PagedMemory; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.WritableRaster; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class StateManager implements Reconfigurable { + + private static StateManager instance; + + public static StateManager getInstance() { + if (instance == null) { + instance = new StateManager(); + } + return instance; + } + State alphaState; + Set<ObjectGraphNode> allStateVariables; + WeakHashMap<Object, ObjectGraphNode> objectLookup; + long maxMemory = -1; + long freeRequired = -1; + @ConfigurableField(category = "Emulator", name = "Max states", description = "How many states can be captured, oldest states are automatically truncated.", defaultValue = "150") + public int maxStates = 100; + @ConfigurableField(category = "Emulator", name = "Capture frequency", description = "How often states are captured, in relation to each VBL (1 = 60 states/second, 2 = 30 states/second, 3 = 15 states/second, etc", defaultValue = "3") + public int captureFrequency = 3; + private ObjectGraphNode<BufferedImage> imageGraphNode; + + private void buildStateMap() { + allStateVariables = new HashSet<ObjectGraphNode>(); + objectLookup = new WeakHashMap<Object, ObjectGraphNode>(); + ObjectGraphNode emulator = new ObjectGraphNode(Emulator.instance); + emulator.name = "Emulator"; + Set visited = new HashSet(); + buildStateMap(emulator, visited); + + // Also track all softswitches + for (SoftSwitches s : SoftSwitches.values()) { + final SoftSwitches ss = s; + ObjectGraphNode switchNode = new ObjectGraphNode(s); + switchNode.name = s.toString(); + ObjectGraphNode switchVar = new ObjectGraphNode(s.getSwitch()) { + @Override + /** + * This is a more efficient way of updating the softswitch + * states And really in a way this works out much better to + * ensure that memory and graphics pages are set up correctly + * when resuming states. + */ + public void setCurrentValue(Object value) { + Boolean b = (Boolean) value; + ss.getSwitch().setState(b); + } + + @Override + public Object getCurrentValue() { + return Boolean.valueOf(ss.getSwitch().getState()); + } + }; + switchVar.name = "switch"; + switchVar.parent = switchNode; + allStateVariables.add(switchVar); + objectLookup.put(s, switchNode); + objectLookup.put(s.getSwitch(), switchVar); + } + } + + private void buildStateMap(ObjectGraphNode node, Set visited) { + if (visited.contains(node)) { + return; + } + Object currentValue = node.getCurrentValue(); + if (currentValue == null || visited.contains(currentValue)) { + return; + } + visited.add(node); + visited.add(currentValue); + objectLookup.put(node.getCurrentValue(), node); + for (Field f : node.getCurrentValue().getClass().getFields()) { + try { + Object o = f.get(node.getCurrentValue()); + if (o == null) { + continue; + } + Annotation a = f.getAnnotation(Stateful.class); + ObjectGraphNode child = new ObjectGraphNode(o); + child.name = f.getName(); + child.parent = node; + if (a != null) { + child.isStateful = true; + addStateVariable(child, f); + } + if (!f.getType().isPrimitive() && !f.getType().isArray()) { + // This is not stateful, but examine its children just in case + buildStateMap(child, visited); + } + } catch (IllegalArgumentException ex) { + Logger.getLogger(StateManager.class.getName()).log(Level.SEVERE, null, ex); + } catch (IllegalAccessException ex) { + Logger.getLogger(StateManager.class.getName()).log(Level.SEVERE, null, ex); + } + } + } + + /** + * The node and field correspond to an object member field that has a + * + * @Stateful annotation. This method has to make sense of this and plan out + * how states should be captured for this field. + * @param node + * @param f + */ + private void addStateVariable(ObjectGraphNode node, Field f) { +// if (f == null) { +// System.out.println("State var: "+node.parent.name+"."+node.name+ ">>" + node.index); +// } else { +// System.out.println("State var: "+node.parent.name+"."+node.name+ ">>" + f.getDeclaringClass().getName() + "." + f.getName()); +// } + // For paged memory, video and softswiches we might have to do something + // more sophosticated. + Class type = node.getCurrentValue().getClass(); + if (PagedMemory.class.isAssignableFrom(type)) { + addMemoryPages((ObjectGraphNode<PagedMemory>) node, f); + } else if (BufferedImage.class.isAssignableFrom(type)) { + addVideoFrame((ObjectGraphNode<BufferedImage>) node, f); + } else if (List.class.isAssignableFrom(type)) { + List l = (List) node.getCurrentValue(); + Type fieldGenericType = f.getGenericType(); +// Class genericType = Object.class; +// if (fieldGenericType instanceof ParameterizedType) { +// genericType = (Class) ((ParameterizedType) fieldGenericType).getActualTypeArguments()[0]; +// } else { +// System.out.println("NOT PARAMATERIZED!"); +// } + for (int i = 0; i < l.size(); i++) { + if (l.get(i) != null) { +// ObjectGraphNode inode = new ObjectGraphNode(genericType); + Object obj = l.get(i); + if (obj == null) { + continue; + } + ObjectGraphNode inode = new ObjectGraphNode(obj); +// inode.source = new WeakReference(); + inode.parent = node; + inode.name = String.valueOf(i); + inode.index = i; + // Build this recursively because it might be a nested type (e.g. a list of maps) + // If it isn't a nested type then the default case shoud apply. + addStateVariable(inode, null); + } + } + } else if (Map.class.isAssignableFrom(type)) { + // TODO: + // Walk through members of the map, etc. + // This will at least let RamWorks memory pages work with state management + } else { + // This is the default case, just capture the field and move on + allStateVariables.add(node); + node.isPrimitive = f.getType().isPrimitive(); + // Since we can only guess how state changes just assume we have to check for changes every time. + node.forceCheck = true; + } + } + + /** + * Track a stateful video framebuffer. + * + * @param objectGraphNode + * @param f + */ + private void addVideoFrame(ObjectGraphNode<BufferedImage> node, Field f) { + imageGraphNode = node; + } + + /** + * Track a stateful set of memory. + * + * @param objectGraphNode + * @param f + */ + private void addMemoryPages(ObjectGraphNode<PagedMemory> node, Field f) { + PagedMemory mem = node.getCurrentValue(); + ObjectGraphNode<byte[][]> internalmem = new ObjectGraphNode<byte[][]>(mem.internalMemory); + internalmem.parent = node; + internalmem.name = "internalMemory"; + for (int i = 0; i < mem.internalMemory.length; i++) { + byte[] memPage = mem.internalMemory[i]; + if (memPage == null) { + continue; + } + ObjectGraphNode<byte[]> page = new ObjectGraphNode<byte[]>(memPage); + page.parent = internalmem; + page.name = String.valueOf(i); + page.index = i; + page.forceCheck = false; + allStateVariables.add(page); + objectLookup.put(mem.internalMemory[i], page); + } + } + + public static void markDirtyValue(Object o) { + StateManager manager = getInstance(); + if (manager.objectLookup == null) { + return; + } + ObjectGraphNode node = manager.objectLookup.get(o); + if (node == null) { + return; + } + node.markDirty(); + } + + public String getName() { + return "State Manager"; + } + + public String getShortName() { + return "state"; + } + + /** + * If reconfigure is called, it means the emulator state has changed too + * greatly and we need to abandon captured states and start from scratch. + */ + public void reconfigure() { + boolean resume = Computer.pause(); + isValid = false; + + // Now figure out how much memory we're allowed to eat + maxMemory = Runtime.getRuntime().maxMemory(); + // If we have less than 2% heap remaining then states will be recycled + freeRequired = maxMemory / 50L; + frameCounter = captureFrequency; + if (resume) { + Computer.resume(); + } + } + boolean isValid = false; + + public void invalidate() { + isValid = false; + } + int stateCount = 0; + + public void captureState() { + // If the state graph is invalidated it means we have to abandon all + // previously captured states. This helps ensure that rewinding will + // not result in an unintended or invalid state. + if (!isValid) { + alphaState = null; + if (allStateVariables != null) { + allStateVariables.clear(); + } + allStateVariables = null; + // This will probably result in a lot of invalidated objects + // so it's a good idea to suggest to the JVM to reclaim memory now. + System.gc(); + + if (Emulator.instance == null) { + return; + } + + // Re-examine the object structure of the emulator in case it changed + buildStateMap(); + System.out.println(allStateVariables.size() + " variables tracked per state"); + System.out.println(objectLookup.entrySet().size() + " objects tracked in emulator model"); + isValid = true; + stateCount = 0; + } + if (alphaState == null) { + System.gc(); + alphaState = captureAlphaState(); + alphaState.tail = alphaState; + } else { + if (Runtime.getRuntime().freeMemory() <= freeRequired) { + invalidate(); + return; + } + while (stateCount >= maxStates) { + removeOldestState(); + stateCount--; + } + + State newState = captureDeltaState(alphaState.tail); +// State newState = (stateCount % 2 == 0) ? captureDeltaState(alphaState.tail) : captureAlphaState(); + alphaState.tail.addState(newState); + alphaState.tail = newState; + } + // Now capture the current screen + alphaState.tail.screenshot = getScreenshot(); + stateCount++; + } + + private BufferedImage getScreenshot() { + BufferedImage screen = Computer.getComputer().getVideo().getFrameBuffer(); + ColorModel cm = screen.getColorModel(); + boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); + WritableRaster raster = screen.copyData(null); + return new BufferedImage(cm, raster, isAlphaPremultiplied, null); + } + + private State captureAlphaState() { + State s = new State(); + s.deltaState = false; + for (ObjectGraphNode node : allStateVariables) { + s.put(node, new StateValue(node)); + node.markClean(); + } + return s; + } + + private State captureDeltaState(State tail) { + State s = new State(); + s.deltaState = true; + for (ObjectGraphNode node : allStateVariables) { + if (!node.valueChanged(tail)) { + // If there are no changes to this node value, don't waste memory on it. + continue; + } + s.put(node, new StateValue(node)); + node.markClean(); + } + return s; + + } + + private void removeOldestState() { + if (alphaState == null) { + return; + } + if (alphaState.nextState == null) { + alphaState = null; + } else { + alphaState = alphaState.deleteNext(); + } + } + // Don't capture for the first few seconds of emulation. This is sort of a + // hack but is also a very elegant way to help the emulator avoid wasting + // time at start since the emulator state changes a lot at first. + int frameCounter = 200; + + /** + * Every time the Apple reaches VBL there is a screen update event The rest + * of the emulator is notified after the video frame was redrawn, so this is + * the best time to capture the state. + */ + public void notifyVBLActive() { + frameCounter--; + if (frameCounter > 0) { + return; + } + frameCounter = captureFrequency; + captureState(); + } + + public void rewind(int numStates) { + boolean resume = Computer.pause(); + State state = alphaState.tail; + while (numStates > 0 && state.previousState != null) { + state = state.previousState; + numStates--; + } + state.apply(); + alphaState.tail = state; + state.nextState = null; + Computer.getComputer().getVideo().forceRefresh(); + System.gc(); + if (resume) { + Computer.resume(); + } + } +} diff --git a/src/main/java/jace/state/StateValue.java b/src/main/java/jace/state/StateValue.java new file mode 100644 index 0000000..02eb88c --- /dev/null +++ b/src/main/java/jace/state/StateValue.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.state; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Arrays; + +/** + * This represents a serializable value of an object + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class StateValue<T> implements Serializable { + + public ObjectGraphNode<T> node; + public Class<T> type; + public T value; + + public StateValue(ObjectGraphNode<T> node) { + this.node = node; + this.type = this.node.type; +// if (node.name.equals("Z")) { +// System.out.println("Z >>>> "+ String.valueOf(node.getCurrentValue())); +// System.out.println(String.valueOf(copyObject(node.getCurrentValue()))); +// } + value = copyObject(this.node.getCurrentValue()); +// System.out.print(node.parent != null ? node.parent.name + "." : ""); +// System.out.print(node.name); +// System.out.print("=="); +// System.out.println(value == null ? "NULL" : value.getClass().toString()); + } + + public void mergeValue(StateValue<T> previous) { + // Do nothing -- this is here in case it is necessary to implement partial changes + } + + private T copyObject(T currentValue) { + if (currentValue == null) return null; + if (type.isPrimitive()) { + return currentValue; + } + if (!type.isArray()) { + try { + return (T) type.getMethod("clone").invoke(currentValue); + } catch (Exception ex) { + try { + // Use serialization to build a deep copy + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(currentValue); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + return (T) ois.readObject(); + } catch (IOException e) { + return null; + } catch (ClassNotFoundException e) { + return null; + } + } + } else if (type.isArray()) { + // Cross fingers! I hope this works and I don't have to investigate primite types further... + if (currentValue instanceof byte[]) { + byte[] array = (byte[]) currentValue; + return (T) Arrays.copyOf(array, array.length); + } else { + Object[] array = (Object[]) currentValue; + return (T) Arrays.copyOf(array, array.length); + } + } + + return currentValue; + } +} diff --git a/src/main/java/jace/state/Stateful.java b/src/main/java/jace/state/Stateful.java new file mode 100644 index 0000000..931689d --- /dev/null +++ b/src/main/java/jace/state/Stateful.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.state; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates an object that possesses state which should be preserved Stateful + * variables are indicated by members annotated by Stateful This is used for + * saved states and rewind features. The StateManager uses this annotation to + * identify where to preserve and restore state as needed. + * + * Configurable fields are inherently stateful and do not necessarily have to be + * indicated. Configuration is captured as part of state. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Stateful { +} diff --git a/src/main/java/jace/tracker/Command.java b/src/main/java/jace/tracker/Command.java new file mode 100644 index 0000000..1408f12 --- /dev/null +++ b/src/main/java/jace/tracker/Command.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Command { + public static enum CommandScope {global, channel} + public static enum CommandType { + Rest(0, 0, 0, false), + Ay1(1, 1, 17, false), + Ay2(2, 1, 17, false), + Ay1and2(3, 2, 34, true), + PitchSlide(4, 2, 2, true), + Vibrato(5, 2, 2, true), + VolumeSlide(6, 2, 2, true), + Tremolo(7, 2, 2, true), + GlobalDetune(8, 2, 2, true), + Reserved(9, 0, 0, false), + AllOff(0x0a, 1, 1, false), + Reset(0x0b, 1, 1, false), + Playback(0x0c, 1, 2, false), + TicksPerBeat(0x0d, 1, 1, true), + Clock(0x0e, 2, 2, false), + Macro(0x0f, 1, 1, true); + + int number; + int maxParameters; + int minParameters; + boolean isChain; + CommandType(int num, int minParam, int maxParam, boolean chain) { + number = num; + isChain = chain; + maxParameters = maxParam; + minParameters = minParam; + } + } + Integer[] parameters = {}; + +} diff --git a/src/main/java/jace/tracker/EditableLabel.java b/src/main/java/jace/tracker/EditableLabel.java new file mode 100644 index 0000000..8f22731 --- /dev/null +++ b/src/main/java/jace/tracker/EditableLabel.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import jace.core.Utility; +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.EventQueue; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.ImageIcon; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class EditableLabel extends JPanel implements MouseListener, FocusListener { + + public static EditableLabel FOCUS; + public static int DEFAULT_GAP = 4; + Component editComponent; + JLabel labelComponent; + CardLayout layout; + boolean isEditing = false; + int width = 4; + Object ownerObject; + String objectProperty; + + private void showBlankValue() { + String s = "...................".substring(0, width); + labelComponent.setText(s); + } + + public static enum cards { + + label, edit + }; + + public EditableLabel(JLabel label, Component edit, int width, Object owner, String property) { + this(label, edit, width, DEFAULT_GAP, DEFAULT_GAP, owner, property); + } + + public EditableLabel(JLabel label, Component edit, int width, int horizontalPadding, int verticalPadding, Object owner, String property) { + ownerObject = owner; + this.width = width; + objectProperty = property; + addMouseListener(this); + edit.addFocusListener(this); + addFocusListener(this); + layout = new CardLayout(horizontalPadding, verticalPadding); + setLayout(layout); + add(label, cards.label.toString()); + add(edit, cards.edit.toString()); + labelComponent = label; + editComponent = edit; + deactivateEdit(); + setBackground(UserInterface.Theme.background.color); + label.setForeground(UserInterface.Theme.foreground.color); + label.setOpaque(false); + edit.setBackground(UserInterface.Theme.backgroundEdit.color); + edit.setForeground(UserInterface.Theme.foregroundEdit.color); + edit.setFocusTraversalKeysEnabled(false); + edit.addKeyListener(NAV_LISTENER); + label.addKeyListener(NAV_LISTENER); + this.addKeyListener(NAV_LISTENER); + } + + @Override + public void mouseClicked(MouseEvent e) { + mousePressed(e); + } + + @Override + public void mousePressed(MouseEvent e) { + if (isEditing) { + return; + } + activateEdit(); + // This next bit will generate a second mouse event and pass it on to the edit component so that + // the edit cursor appears under the mouse pointer, not a the start of the component. + final MouseEvent e2 = new MouseEvent(editComponent, e.getID(), e.getWhen(), e.getModifiers(), e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger(), e.getButton()); + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + editComponent.dispatchEvent(e2); + } + }); + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void focusGained(FocusEvent e) { + if (e.getComponent() == this || e.getComponent() == labelComponent || e.getComponent() == editComponent) { + activateEdit(); + } else { + deactivateEdit(); + } + } + + @Override + public void focusLost(FocusEvent e) { + deactivateEdit(); + } + + private void activateEdit() { + FOCUS = this; + isEditing = true; + layout.show(this, cards.edit.toString()); + editComponent.requestFocusInWindow(); + } + + private void deactivateEdit() { + isEditing = false; + if (editComponent instanceof JTextField) { + String value = ((JTextField) editComponent).getText(); + if (value != null) { + value = value.trim(); + if (value.length() > width) { + value = value.substring(0, width); + } + } + Object result = Utility.setProperty(ownerObject, objectProperty, value, true); + + value = (result == null) ? null : (result instanceof Integer ? Integer.toString((Integer) result, 16) : result.toString()); + if (value != null && value.length() < width) { + value = value.concat(" ".substring(0, width - value.length())); + } + if (value == null || value.equals("")) { + showBlankValue(); + ((JTextField) editComponent).setText(null); + } else { + labelComponent.setText(value); + ((JTextField) editComponent).setText(value.trim()); + } + } else if (editComponent instanceof JComboBox) { + ImageIcon selection = (ImageIcon) ((JComboBox) editComponent).getSelectedItem(); + labelComponent.setText(selection.getDescription()); + } + layout.show(this, cards.label.toString()); + } + + public static EditableLabel generateTextLabel(Object owner, String property, int width, KeyListener listener) { + EditableLabel label = generateTextLabel(owner, property, width); + label.editComponent.addKeyListener(listener); + return label; + } + + public static EditableLabel generateTextLabel(Object owner, String property, int width) { + Object value = Utility.getProperty(owner, property); + JLabel label = new JLabel(value != null ? value.toString() : null); + JTextField editor = new JTextField(""); + editor.setCaretColor(UserInterface.Theme.foregroundEdit.color); + editor.setBorder(new EmptyBorder(0, 0, 0, 0)); + label.setFont(UserInterface.EDITOR_FONT); + editor.setFont(UserInterface.EDITOR_FONT); + + EditableLabel output = new EditableLabel(label, editor, width, owner, property); + if (value == null || value.equals("")) { + output.showBlankValue(); + } + return output; + } + static KeyListener NAV_LISTENER = new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + switch (e.getKeyCode()) { + case KeyEvent.VK_TAB: + if (e.isShiftDown()) { + moveLeft(); + } else { + moveRight(); + } + e.consume(); + break; + case KeyEvent.VK_UP: + moveUp(); + e.consume(); + break; + case KeyEvent.VK_DOWN: + moveDown(); + e.consume(); + break; + default: + break; + } + } + + public int getIndex(Component c) { + System.out.println("Looking for " + c.getClass().getName() + " in parent " + c.getParent().getClass().getName()); + for (int i = 0; i < c.getParent().getComponentCount(); i++) { + if (c == c.getParent().getComponent(i)) { + return i; + } + } + return -1; + } + + public void focus(final Component c) { + Runnable r = new Runnable() { + @Override + public void run() { + if (c instanceof EditableLabel) { + ((EditableLabel) c).activateEdit(); + } else { + c.requestFocusInWindow(); + } + } + }; + EventQueue.invokeLater(r); + } + + public void moveDown() { + Component c = FOCUS; + int col = getIndex(c); + int row = getIndex(c.getParent()) + 1; + if (row < c.getParent().getParent().getComponentCount()) { + System.out.println("Trying to focus on col " + col + " row " + row); + focus(((Container) c.getParent().getParent().getComponent(row)).getComponent(col)); + } + } + + public void moveUp() { + Component c = FOCUS; + int col = getIndex(c); + int row = getIndex(c.getParent()) - 1; + if (row >= 0) { + System.out.println("Trying to focus on col " + col + " row " + row); + focus(((Container) c.getParent().getParent().getComponent(row)).getComponent(col)); + } + } + + public void moveLeft() { + Component c = FOCUS; + int col = getIndex(c) - 1; + System.out.println("Trying to focus on col " + col); + if (col >= 0) { + focus(c.getParent().getComponent(col)); + } + } + + public void moveRight() { + Component c = FOCUS; + int col = getIndex(c) + 1; + System.out.println("Trying to focus on col " + col); + if (col < c.getParent().getComponentCount()) { + focus(c.getParent().getComponent(col)); + } + } + }; +} diff --git a/src/main/java/jace/tracker/Pattern.java b/src/main/java/jace/tracker/Pattern.java new file mode 100644 index 0000000..152fc13 --- /dev/null +++ b/src/main/java/jace/tracker/Pattern.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import java.util.ArrayList; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Pattern extends ArrayList<Row> { + String description; // Not used currently + int id; +} \ No newline at end of file diff --git a/src/main/java/jace/tracker/PlaybackEngine.java b/src/main/java/jace/tracker/PlaybackEngine.java new file mode 100644 index 0000000..16689d6 --- /dev/null +++ b/src/main/java/jace/tracker/PlaybackEngine.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import jace.apple2e.MOS65C02; +import jace.core.Card; +import jace.core.Computer; +import jace.core.Motherboard; +import jace.hardware.CardExt80Col; +import jace.hardware.CardMockingboard; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class PlaybackEngine extends Computer { + + Motherboard motherboard = new Motherboard(); + CardMockingboard mockingboard = new CardMockingboard(); + + public PlaybackEngine() { + setMemory(new CardExt80Col()); + setCpu(new MOS65C02()); + getMemory().addCard(mockingboard, 5); + } + + @Override + public void coldStart() { + for (Card c : getMemory().getAllCards()) { + if (c != null) { + c.reset(); + } + } + } + + @Override + public void warmStart() { + for (Card c : getMemory().getAllCards()) { + if (c != null) { + c.reset(); + } + } + } + + @Override + protected boolean isRunning() { + return motherboard.isRunning(); + } + + @Override + protected void doPause() { + motherboard.suspend(); + } + + @Override + protected void doResume() { + motherboard.resume(); + } + + @Override + public String getName() { + return "Playback Computer"; + } + + @Override + public String getShortName() { + return "Computer"; + } + + @Override + public void reconfigure() { + // do nothing + } +} \ No newline at end of file diff --git a/src/main/java/jace/tracker/Row.java b/src/main/java/jace/tracker/Row.java new file mode 100644 index 0000000..1a5ecbd --- /dev/null +++ b/src/main/java/jace/tracker/Row.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import jace.core.Utility; +import java.awt.image.BufferedImage; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Set; +import javax.swing.ImageIcon; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Row { + + public static enum Note { + C0(3901), + CS0(3682), + D0(3476), + DS0(3279), + E0(3096), + F0(2922), + FS0(2759), + G0(2603), + GS0(2457), + A0(2319), + AS0(2189), + B0(2066), + C1(1950), + CS1(1841), + D1(1737), + DS1(1640), + E1(1548), + F1(1461), + FS1(1379), + G1(1302), + GS1(1229), + A1(1160), + AS1(1095), + B1(1033), + C2(975), + CS2(920), + D2(869), + DS2(820), + E2(774), + F2(731), + FS2(690), + G2(651), + GS2(614), + A2(580), + AS2(547), + B2(517), + C3(488), + CS3(460), + D3(434), + DS3(410), + E3(387), + F3(365), + FS3(345), + G3(325), + GS3(307), + A3(290), + AS3(274), + B3(258), + C4(244), + CS4(230), + D4(217), + DS4(205), + E4(193), + F4(183), + FS4(172), + G4(163), + GS4(154), + A4(145), + AS4(137), + B4(129), + C5(122), + CS5(115), + D5(109), + DS5(102), + E5(97), + F5(91), + FS5(86), + G5(81), + GS5(77), + A5(72), + AS5(68), + B5(65), + C6(61), + CS6(58), + D6(54), + DS6(51), + E6(48), + F6(46), + FS6(43), + G6(41), + GS6(38), + A6(36), + AS6(34), + B6(32), + C7(30), + CS7(29), + D7(27), + DS7(26), + E7(24), + F7(23), + FS7(22), + G7(20), + GS7(19), + A7(18), + AS7(17), + B7(16), + C8(15); + int freq; + + Note(int f) { + freq = f; + } + + @Override + public String toString() { + return super.toString().replace("S", "#"); + } + } + + static ImageIcon[] ENVELOPE_ICONS; + static { + ENVELOPE_ICONS = new ImageIcon[EnvelopeShape.values().length]; + int i = 0; + for (EnvelopeShape shape : EnvelopeShape.values()) { + ENVELOPE_ICONS[i++] = shape.getIcon(); + } + } + public static enum EnvelopeShape { + unspecified(-1, ""), + pulse(0, "|\\____"), + pulseinv(4, "/|____"), + saw(8, "|\\|\\|\\"), + triangle(10,"\\/\\/\\/"), + triangleinv(14,"/\\/\\/\\"), + holdinv(11,"|\\|^^^"), + hold(13,"/^^^^^"); + int value; + String pattern; + ImageIcon icon; + EnvelopeShape(int v, String p) { + value = v; + pattern = p; + } + ImageIcon getIcon() { + if (icon == null) { + if (value >= 0) { + icon = Utility.loadIcon("ayenvelope"+value+".png"); + } else { + icon = new ImageIcon(new BufferedImage(64, 12,BufferedImage.TYPE_4BYTE_ABGR)); + } + icon.setDescription(toString()); + } + return icon; + } + } + + public static enum Channel {A1, B1, C1, A2, B2, C2} + + public static class ChannelData { + public Note tone; + public Integer volume; // Range 0-F + public Boolean toneActive; + public Boolean noiseActive; + public Boolean envelopeActive; // Results in volume = 0x010 + public Set<Command> commands = new HashSet<Command>(); + public boolean isEmpty() { + if (!commands.isEmpty()) return false; + if (tone != null || volume != null || toneActive != null || noiseActive != null || envelopeActive != null) return false; + return true; + } + } + + public EnumMap<Channel, ChannelData> channels = new EnumMap<Channel, ChannelData>(Channel.class); + + public Row() { + for (Channel c : Channel.values()) { + channels.put(c, new ChannelData()); + } + } + + public Integer ay1noisePeriod, ay2noisePeriod; + public Integer ay1envelopePeriod, ay2envelopePeriod; + public EnvelopeShape ay1envelopeShape, ay2envelopeShape; + public Set<Command> globalCommands = new HashSet<Command>(); + + public boolean isEmpty() { + for (ChannelData d : channels.values()) { + if (d != null && !d.isEmpty()) return false; + } + if (ay1envelopePeriod != null || ay2envelopePeriod != null) return false; + if (ay1envelopeShape != null || ay2envelopeShape != null) return false; + if (ay1noisePeriod != null || ay2noisePeriod != null) return false; + return globalCommands.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/jace/tracker/Song.java b/src/main/java/jace/tracker/Song.java new file mode 100644 index 0000000..edc1197 --- /dev/null +++ b/src/main/java/jace/tracker/Song.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Song { + public static enum Format { + woz0 + } + Format format; + int fileSize; + String songName; + String authorName; + + Map<Integer, Pattern> patterns = new HashMap<Integer, Pattern>(); + Map<Integer, Pattern> macros = new HashMap<Integer, Pattern>(); + int defaultPatternLength = 64; + int defaultMacroLength = 16; + List<Integer> order = new ArrayList<Integer>(); +} \ No newline at end of file diff --git a/src/main/java/jace/tracker/SongPersistor.java b/src/main/java/jace/tracker/SongPersistor.java new file mode 100644 index 0000000..08da4e2 --- /dev/null +++ b/src/main/java/jace/tracker/SongPersistor.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class SongPersistor { + static void writeToBytes(OutputStream s) { + + } + + static Song readFromBytes(InputStream s) { + Song song = new Song(); + + + + return song; + } +} \ No newline at end of file diff --git a/src/main/java/jace/tracker/UserInterface.java b/src/main/java/jace/tracker/UserInterface.java new file mode 100644 index 0000000..63eede3 --- /dev/null +++ b/src/main/java/jace/tracker/UserInterface.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.tracker; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Font; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.HashMap; +import java.util.Map; +import javax.swing.BoxLayout; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.xml.transform.Source; + +/** + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class UserInterface { + + static Font EDITOR_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12); + + public static enum Theme { + + background(0x000000), + foreground(0xffffff), + backgroundEdit(0x000080), + foregroundEdit(0xffff80); + Color color; + + Theme(int col) { + color = new Color(col & 0x0ffffff); + } + } + public static int BASE_OCTAVE = 3; + + public static enum Note { + + C0("C", -1), + CS0("CS", -1), + D0("D", -1), + DS0("DS", -1), + E0("E", -1), + F0("F", -1), + FS0("FS", -1), + G0("G", -1), + GS0("GS", -1), + A0("A", -1), + AS0("AS", -1), + B0("B", -1), + C1("C", 0), + CS1("CS", 0), + D1("D", 0), + DS1("DS", 0), + E1("E", 0), + F1("F", 0), + FS1("FS", 0), + G1("G", 0), + GS1("GS", 0), + A1("A", 0), + AS1("AS", 0), + B1("B", 0), + C2("C", 1), + CS2("CS", 1), + D2("D", 1), + DS2("DS", 1), + E2("E", 1); + public String note; + public int octaveOffset; + + Note(String n, int offset) { + note = n; + octaveOffset = offset; + } + }; + public static final Map<Integer, Note> KEYBOARD_MAP = new HashMap<Integer, Note>(); + + static { + KEYBOARD_MAP.put(KeyEvent.VK_Z, Note.C0); + KEYBOARD_MAP.put(KeyEvent.VK_S, Note.CS0); + KEYBOARD_MAP.put(KeyEvent.VK_X, Note.D0); + KEYBOARD_MAP.put(KeyEvent.VK_D, Note.DS0); + KEYBOARD_MAP.put(KeyEvent.VK_C, Note.E0); + KEYBOARD_MAP.put(KeyEvent.VK_V, Note.F0); + KEYBOARD_MAP.put(KeyEvent.VK_G, Note.FS0); + KEYBOARD_MAP.put(KeyEvent.VK_B, Note.G0); + KEYBOARD_MAP.put(KeyEvent.VK_H, Note.GS0); + KEYBOARD_MAP.put(KeyEvent.VK_N, Note.A0); + KEYBOARD_MAP.put(KeyEvent.VK_J, Note.AS0); + KEYBOARD_MAP.put(KeyEvent.VK_M, Note.B0); + KEYBOARD_MAP.put(KeyEvent.VK_Q, Note.C1); + KEYBOARD_MAP.put(KeyEvent.VK_2, Note.CS1); + KEYBOARD_MAP.put(KeyEvent.VK_W, Note.D1); + KEYBOARD_MAP.put(KeyEvent.VK_3, Note.DS1); + KEYBOARD_MAP.put(KeyEvent.VK_E, Note.E1); + KEYBOARD_MAP.put(KeyEvent.VK_R, Note.F1); + KEYBOARD_MAP.put(KeyEvent.VK_5, Note.FS1); + KEYBOARD_MAP.put(KeyEvent.VK_T, Note.G1); + KEYBOARD_MAP.put(KeyEvent.VK_6, Note.GS1); + KEYBOARD_MAP.put(KeyEvent.VK_Y, Note.A1); + KEYBOARD_MAP.put(KeyEvent.VK_7, Note.AS1); + KEYBOARD_MAP.put(KeyEvent.VK_U, Note.B1); + KEYBOARD_MAP.put(KeyEvent.VK_I, Note.C2); + KEYBOARD_MAP.put(KeyEvent.VK_9, Note.CS2); + KEYBOARD_MAP.put(KeyEvent.VK_O, Note.D2); + KEYBOARD_MAP.put(KeyEvent.VK_0, Note.DS2); + KEYBOARD_MAP.put(KeyEvent.VK_P, Note.E2); + } + + public static void main(String... args) { + Row r = new Row(); + JFrame testWindow = new JFrame(); + testWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + testWindow.setSize(900, 600); + Container content = testWindow.getContentPane(); + content.setLayout(new BoxLayout(testWindow.getContentPane(), BoxLayout.Y_AXIS)); + content.setBackground(Theme.background.color); + content.setForeground(Theme.foreground.color); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); + content.add(createRowEditor(r)); +// testWindow.doLayout(); + testWindow.setVisible(true); + } + + public static KeyAdapter toneListner = new KeyAdapter() { + + @Override + public void keyReleased(KeyEvent e) { + e.consume(); + } + + @Override + public void keyTyped(KeyEvent e) { + e.consume(); + } + + + @Override + public void keyPressed(KeyEvent e) { + JTextField field = (JTextField) e.getSource(); + if (KEYBOARD_MAP.containsKey(e.getKeyCode())) { + Note n = KEYBOARD_MAP.get(e.getKeyCode()); + String noteval = n.note; + int octave = BASE_OCTAVE + n.octaveOffset; + noteval += octave; + try { + // Test the waters, is ths value ok? + Row.Note.valueOf(noteval); + // Looks like it worked -- use the value + field.setText(noteval); + } catch (Throwable t) { + // out of bounds or bad value + } + + } + e.consume(); + field.setFocusable(false); + field.setFocusable(true); + } + }; + + public static Component createRowEditor(Row r) { + JPanel rowEditor = new JPanel(); + rowEditor.setSize(800, 24); + rowEditor.setLayout(new BoxLayout(rowEditor, BoxLayout.X_AXIS)); + rowEditor.setBackground(Theme.background.color); + rowEditor.setOpaque(true); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A1.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A1.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A1.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B1.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B1.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B1.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C1.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C1.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C1.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "ay1noisePeriod", 4)); + rowEditor.add(EditableLabel.generateTextLabel(r, "ay1envelopePeriod", 4)); + rowEditor.add(generateEnvelopeEditor(r.ay1envelopeShape, r, "ay2envelopeShape")); + + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A2.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A2.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.A2.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B2.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B2.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.B2.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C2.tone", 3, toneListner)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C2.volume", 1)); + rowEditor.add(EditableLabel.generateTextLabel(r, "channels.C2.commands", 3)); + rowEditor.add(EditableLabel.generateTextLabel(r, "ay2noisePeriod", 4)); + rowEditor.add(EditableLabel.generateTextLabel(r, "ay2envelopePeriod", 4)); + rowEditor.add(generateEnvelopeEditor(r.ay2envelopeShape, r, "ay2envelopeShape")); + + rowEditor.add(EditableLabel.generateTextLabel(r, "globalCommands", 6)); + rowEditor.doLayout(); + return rowEditor; + } + + public static Component generateEnvelopeEditor(Row.EnvelopeShape envelope, Row row, String property) { + if (envelope == null) { + envelope = Row.EnvelopeShape.unspecified; + } + JLabel label = new JLabel(envelope.icon) { + @Override + public void setText(String text) { + Row.EnvelopeShape e; + try { + e = Row.EnvelopeShape.valueOf(text); + } catch (Throwable ex) { + e = Row.EnvelopeShape.unspecified; + } + setIcon(e.getIcon()); + } + }; + label.setText(envelope.toString()); + JComboBox editor = new JComboBox(Row.ENVELOPE_ICONS); + EditableLabel result = new EditableLabel(label, editor, 64, row, property); + return result; + } +} diff --git a/src/main/java/jace/ui/AbstractEmulatorFrame.form b/src/main/java/jace/ui/AbstractEmulatorFrame.form new file mode 100644 index 0000000..1e949f4 --- /dev/null +++ b/src/main/java/jace/ui/AbstractEmulatorFrame.form @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> + <Properties> + <Property name="defaultCloseOperation" type="int" value="3"/> + </Properties> + <SyntheticProperties> + <SyntheticProperty name="formSizePolicy" type="int" value="1"/> + <SyntheticProperty name="generateCenter" type="boolean" value="false"/> + </SyntheticProperties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="400" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="300" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> +</Form> diff --git a/src/main/java/jace/ui/AbstractEmulatorFrame.java b/src/main/java/jace/ui/AbstractEmulatorFrame.java new file mode 100644 index 0000000..a757574 --- /dev/null +++ b/src/main/java/jace/ui/AbstractEmulatorFrame.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import jace.config.ConfigurableField; +import jace.config.Reconfigurable; +import jace.core.Computer; +import java.awt.Color; +import java.awt.Component; +import java.awt.EventQueue; +import java.awt.Graphics2D; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.event.KeyListener; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JPanel; + +/** + * This is an abstraction of the emulator user interface. It defines a lot of + * the management of user interface elements (such as screen indicators) as well + * as window management (resize, fullscreen, etc) + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public abstract class AbstractEmulatorFrame extends javax.swing.JFrame implements Reconfigurable { + + /** + * Creates new form AbstractEmulatorFrame + */ + public AbstractEmulatorFrame() { + initComponents(); + } + + @Override + public synchronized void addKeyListener(KeyListener l) { + super.addKeyListener(l); + getScreen().addKeyListener(l); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 300, Short.MAX_VALUE) + ); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables + @Override + abstract public String getShortName(); + @ConfigurableField(name = "Show Debug Panel") + public Boolean showDebug = false; + + @Override + public void reconfigure() { + getDebuggerPanel().setVisible(isShowDebug()); + resizeVideo(); + } + + abstract public DebuggerPanel getDebuggerPanel(); + + /** + * @return the showDebug + */ + public boolean isShowDebug() { + return showDebug; + } + + /** + * @param showDebug the showDebug to set + */ + public void setShowDebug(boolean showDebug) { + this.showDebug = showDebug; + } + + public void addIndicator(Object owner, ImageIcon icon, int time) { + synchronized (indicators) { + Set<ImageIcon> ind = indicators.get(owner); + if (ind == null) { + ind = new HashSet<ImageIcon>(); + indicators.put(owner, ind); + } + removeTime.put(icon, System.currentTimeMillis() + time); + if (ind.contains(icon)) { + return; + } + ind.add(icon); + } + redrawIndicators(); + } + public static int DEFAULT_INDICATOR_TIME = 250; + + public void addIndicator(Object owner, ImageIcon icon) { + addIndicator(owner, icon, DEFAULT_INDICATOR_TIME); + } + + public void removeIndicator(Object owner, ImageIcon icon, boolean redraw) { + synchronized (indicators) { + Set<ImageIcon> ind = indicators.get(owner); + if (ind != null) { + ind.remove(icon); + } + removeTime.remove(icon); + } + if (redraw) { + redrawIndicators(); + } + } + + public void removeIndicator(Object owner, ImageIcon icon) { + removeIndicator(owner, icon, true); + } + + public void removeIndicators(Object owner) { + synchronized (indicators) { + Set<ImageIcon> ind = indicators.get(owner); + if (ind == null) { + return; + } + for (ImageIcon i : ind) { + removeTime.remove(i); + } + indicators.remove(owner); + } + redrawIndicators(); + } + Lock indicatorLock = new ReentrantLock(); + Thread indicatorThread; + + public void redrawIndicators() { + final HashSet<ImageIcon> i = new HashSet<ImageIcon>(); + HashSet<ImageIcon> removeList = new HashSet<ImageIcon>(); + long now = System.currentTimeMillis(); + long soonest = Long.MAX_VALUE; + synchronized (indicators) { + for (Object owner : indicators.keySet()) { + Set<ImageIcon> ind = indicators.get(owner); + for (ImageIcon icon : ind) { + long rt = removeTime.get(icon); + if (rt <= now) { + removeList.add(icon); + } else { + i.add(icon); + if (rt < soonest) { + soonest = rt; + } + } + } + for (ImageIcon remove : removeList) { + removeIndicator(owner, remove, false); + } + } + } + boolean changed = false; + if (visibleIndicators == null) { + changed = true; + } else if (i.size() != visibleIndicators.size() || !i.containsAll(visibleIndicators)) { + changed = true; + } + if (changed) { + visibleIndicators = i; + doRedrawIndicators(visibleIndicators); + } + resumeIndicatorLoop(); + } + Set<ImageIcon> visibleIndicators; + + private void resumeIndicatorLoop() { + try { + indicatorLock.lock(); + if (indicatorThread == null || !indicatorThread.isAlive()) { + indicatorThread = new Thread(new Runnable() { + @Override + public void run() { + while (visibleIndicators != null) { + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + return; + } + redrawIndicators(); + if (visibleIndicators.isEmpty()) { + visibleIndicators = null; + } + } + } + }); + indicatorThread.start(); + } + } finally { + indicatorLock.unlock(); + } + } + + private void suspendIndicatorLoop() { + try { + indicatorLock.lock(); + visibleIndicators.clear(); + indicatorThread.interrupt(); + indicatorThread = null; + } finally { + indicatorLock.unlock(); + } + } + + abstract public void doRedrawIndicators(Set<ImageIcon> indicators); + private Map<ImageIcon, Long> removeTime = new HashMap<ImageIcon, Long>(); + private Map<Object, Set<ImageIcon>> indicators = new HashMap<Object, Set<ImageIcon>>(); + + public void resizeVideo() { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + Computer.pause(); + Computer.getComputer().getVideo().suspend(); + JPanel debugger = getDebuggerPanel(); + Component screen = getScreen(); + Rectangle bounds = screen.getParent().getBounds(); + int width = (int) bounds.getWidth(); + int height = (int) bounds.getHeight(); + if (debugger.isVisible()) { + debugger.setBounds(width - debugger.getWidth(), 0, debugger.getWidth(), height); + width = (int) bounds.getWidth() - debugger.getWidth() + 1; + screen.setSize( + width, + height); + debugger.revalidate(); + } else { + screen.setSize( + width, + height); + } + Computer.getComputer().getVideo().setWidth(width); + Computer.getComputer().getVideo().setHeight(height); + if (!isFullscreen || !fullscreenEnforceRatio) { + Computer.getComputer().getVideo().setScreen(getScreenGraphics()); + } + Computer.getComputer().getVideo().forceRefresh(); + screen.validate(); + screen.requestFocusInWindow(); + Computer.resume(); + Computer.getComputer().getVideo().resume(); + } + }); + } + + abstract public Component getScreen(); + boolean fullscreenEnforceRatio = false; + + public void enforceIntegerRatio() { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + int ww = getWidth(); + int wh = getHeight(); + int w = getContentPane().getWidth(); + int h = getContentPane().getHeight(); + int bw = ww - w; + int bh = wh - h; + double dhscale = w / 560.0; + double dvscale = h / 384.0; + int hscale = (int) Math.round(dhscale); + int vscale = (int) Math.round(dvscale); + int scale = Math.min(hscale, vscale); + if (scale < 1) { + scale = 1; + } + Rectangle b = getBounds(); + if (!isFullscreen) { + b.setSize(bw + 560 * scale, bh + 384 * scale); + setBounds(b); + } else { + fullscreenEnforceRatio = !fullscreenEnforceRatio; + if (fullscreenEnforceRatio) { + int sw = getBounds().width; + int sh = getBounds().height; + while ((560 * scale) > sw || (384 * scale) > sh) { + scale--; + } + b.setSize(560 * scale, 384 * scale); + b.x = (w / 2) - (b.width / 2); + b.y = (h / 2) - (b.height / 2); + Computer.pause(); + Computer.getComputer().getVideo().suspend(); + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + } + Graphics2D g = (Graphics2D) getScreenGraphics(); + g.setColor(new Color(0, 0, 0x040)); + g.fill(getBounds()); + Graphics2D gg = (Graphics2D) g.create(b.x, b.y, b.width, b.height); + gg.scale((double) b.width / (double) sw, (double) b.height / (double) sh); + Computer.getComputer().getVideo().setScreen(gg); + Computer.getComputer().getVideo().resume(); + Computer.resume(); + } else { + b = getBounds(); + getScreen().setBounds(getBounds()); + } + } + resizeVideo(); + } + }); + } + + abstract public void repaintIndicators(); + + public Graphics2D getScreenGraphics() { + return (Graphics2D) getScreen().getGraphics(); + } + boolean isFullscreen = false; + + public boolean isFullscreenActive() { + return isFullscreen; + } + + public void toggleFullscreen() { + final JFrame window = this; + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + Computer.pause(); + Computer.getComputer().getVideo().suspend(); + isFullscreen = !isFullscreen; + GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + if (isFullscreen) { + removeNotify(); + setUndecorated(true); + validate(); + addNotify(); + device.setFullScreenWindow(window); + fullscreenEnforceRatio = false; + } else { + removeNotify(); + setUndecorated(false); + addNotify(); + device.setFullScreenWindow(null); + } + resizeVideo(); + Computer.getComputer().getVideo().resume(); + Computer.resume(); + } + }); + } + + private class Dialog { + + JPanel ui; + String name; + Dialog parent; + boolean disposeOnClose; + } + Dialog visibleDialog = null; + Map<String, Dialog> registeredModalDialogs = new HashMap<String, Dialog>(); + + public JPanel getModalDialogUI(String name) { + Dialog dialog = registeredModalDialogs.get(name); + return dialog != null ? dialog.ui : null; + } + + public void registerModalDialog(JPanel ui, String name, String parent, boolean disposeOnClose) { + Dialog dialog = new Dialog(); + dialog.ui = ui; + dialog.parent = registeredModalDialogs.get(parent); + dialog.name = name; + dialog.disposeOnClose = disposeOnClose; + registeredModalDialogs.put(name, dialog); + } + + public void closeDialog(String name) { + Dialog d = registeredModalDialogs.get(name); + if (d == null) return; + Dialog parent = d.parent; + if (d.disposeOnClose) { + removeDialogAndChildren(d); + disposeModalDialog(name); + } + // If this operation somehow affected the visible dialog, show its parent if possible + while (!registeredModalDialogs.containsValue(parent) && parent != null) { + parent = parent.parent; + } + showDialog(parent); + } + + // Close/hide the current dialog + // If the disposeOnClose flag is true then the dialog will be removed + // completely. Some dialogs such as configuration will have this set + // to false to avoid having to be regenerated unnecessarily. + public void closeDialog() { + if (visibleDialog == null) { + return; + } + if (visibleDialog.disposeOnClose) { + removeDialogAndChildren(visibleDialog); + } else { + hideModalDialog(visibleDialog.name); + } + showDialog(visibleDialog.parent); + } + + // Recursively remove a dialog and any children that were registered + // This is a depth-first operation out of necessity since the actual + // stoage structure is flat. + private void removeDialogAndChildren(Dialog d) { + if (d == null) { + return; + } + for (Dialog child : registeredModalDialogs.values()) { + if (child.parent == null || !child.parent.equals(d)) { + continue; + } + removeDialogAndChildren(child); + } + registeredModalDialogs.remove(d.name); + } + + public void showDialog(String name) { + Dialog d = registeredModalDialogs.get(name); + if (d == null) { + return; + } + showDialog(d); + } + + protected void showDialog(Dialog d) { + if (d == null) { + // Uh oh... why are we asked to show nothing? + System.err.println("WARNING: Asked to show a null modal dialog!"); + return; + } + if (d.parent != null && d.parent.equals(visibleDialog)) { + // The new dialog is a child of the visible dialog, preserve the parent dialog + } else if (visibleDialog != null) { + // The new dialog has no child relationship with the current dialog. + // This means that the old dialog should be considered closed. + if (visibleDialog.disposeOnClose) { + removeDialogAndChildren(d); + } + } + List<String> ancestors = new ArrayList<String>(); + for (Dialog parent = d.parent; parent != null; parent = parent.parent) { + ancestors.add(0, parent.name); + } + displayModalDialog(d.name, d.ui, ancestors); + visibleDialog = d; + } + + abstract protected void displayModalDialog(String name, JPanel ui, List<String> ancestors); + abstract protected void disposeModalDialog(String name); + abstract protected void hideModalDialog(String name); +} \ No newline at end of file diff --git a/src/main/java/jace/ui/DebuggerPanel.form b/src/main/java/jace/ui/DebuggerPanel.form new file mode 100644 index 0000000..09908b5 --- /dev/null +++ b/src/main/java/jace/ui/DebuggerPanel.form @@ -0,0 +1,598 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.3" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="28" green="0" red="0" type="rgb"/> + </Property> + <Property name="doubleBuffered" type="boolean" value="false"/> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 492]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="labelWatches" alignment="0" max="32767" attributes="2"/> + <Group type="102" alignment="1" attributes="0"> + <Group type="103" groupAlignment="1" max="-2" attributes="0"> + <Component id="textW4" alignment="0" max="32767" attributes="1"/> + <Component id="textW3" alignment="0" max="32767" attributes="1"/> + <Component id="textW2" alignment="0" max="32767" attributes="1"/> + <Component id="textW1" alignment="0" min="-2" pref="34" max="-2" attributes="1"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="valueW4" alignment="0" min="-2" max="-2" attributes="1"/> + <Group type="103" alignment="0" groupAlignment="0" attributes="0"> + <Component id="valueW3" alignment="0" max="32767" attributes="1"/> + <Component id="valueW2" alignment="0" min="-2" max="-2" attributes="1"/> + <Component id="valueW1" alignment="0" min="-2" max="-2" attributes="1"/> + </Group> + </Group> + <EmptySpace min="-2" pref="24" max="-2" attributes="0"/> + </Group> + <Component id="enableDebug" alignment="0" max="32767" attributes="2"/> + <Group type="102" alignment="0" attributes="0"> + <Component id="labelBreakPoints" min="-2" max="-2" attributes="2"/> + <EmptySpace min="0" pref="0" max="32767" attributes="0"/> + </Group> + <Component id="enableTrace" alignment="0" max="32767" attributes="2"/> + <Component id="stepForwardButton" alignment="0" max="32767" attributes="1"/> + </Group> + <EmptySpace min="51" pref="51" max="-2" attributes="0"/> + </Group> + <Group type="102" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="labelA" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="labelPC1" alignment="0" min="-2" max="-2" attributes="0"/> + <Group type="103" alignment="0" groupAlignment="1" max="-2" attributes="0"> + <Group type="102" alignment="0" attributes="1"> + <EmptySpace min="-2" pref="22" max="-2" attributes="0"/> + <Component id="valuePC2" pref="18" max="32767" attributes="1"/> + </Group> + <Group type="102" alignment="0" attributes="1"> + <Component id="labelPC" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="valuePC" max="32767" attributes="0"/> + </Group> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="labelSP" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="labelY" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="labelX" alignment="0" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" max="-2" attributes="0"> + <Component id="valueSP" alignment="0" max="32767" attributes="1"/> + <Component id="valueY" alignment="0" max="32767" attributes="1"/> + <Component id="valueA" alignment="0" max="32767" attributes="1"/> + <Component id="valueX" alignment="0" pref="18" max="32767" attributes="1"/> + </Group> + </Group> + </Group> + <Component id="labelINST" alignment="0" min="-2" max="-2" attributes="0"/> + <Group type="103" alignment="0" groupAlignment="1" max="-2" attributes="0"> + <Component id="textBP4" alignment="0" max="32767" attributes="1"/> + <Component id="textBP3" alignment="0" max="32767" attributes="1"/> + <Component id="textBP2" alignment="0" max="32767" attributes="1"/> + <Component id="textBP1" alignment="0" min="-2" pref="33" max="-2" attributes="1"/> + </Group> + <Component id="valueINST" alignment="0" min="-2" pref="60" max="-2" attributes="1"/> + </Group> + <EmptySpace max="32767" attributes="0"/> + </Group> + </Group> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelA" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueA" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelX" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueX" alignment="3" min="-2" pref="14" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelY" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueY" alignment="3" min="-2" pref="14" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelSP" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueSP" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelPC" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valuePC" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="labelPC1" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valuePC2" alignment="3" min="-2" pref="14" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Component id="labelINST" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="valueINST" min="-2" pref="13" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="labelBreakPoints" min="-2" pref="14" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="textBP1" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="textBP2" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="textBP3" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="textBP4" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="labelWatches" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="textW1" alignment="3" min="-2" pref="19" max="-2" attributes="0"/> + <Component id="valueW1" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="textW2" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueW2" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="textW3" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="valueW3" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="textW4" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="valueW4" alignment="0" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace min="-2" pref="6" max="-2" attributes="0"/> + <Component id="stepForwardButton" min="-2" pref="23" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="enableDebug" min="-2" max="-2" attributes="0"/> + <EmptySpace type="unrelated" max="-2" attributes="0"/> + <Component id="enableTrace" min="-2" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="javax.swing.JLabel" name="labelA"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="A:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelX"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="X:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelY"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Y:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelSP"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="SP:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelPC"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="PC:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelINST"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Instruction:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="valueA"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueX"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueY"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueSP"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valuePC"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueINST"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="BRK"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JCheckBox" name="enableDebug"> + <Properties> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="0" red="0" type="rgb"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Debug?"/> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo"> + <EmptyBorder bottom="0" left="0" right="0" top="0"/> + </Border> + </Property> + <Property name="contentAreaFilled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="enableDebugActionPerformed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="labelPC1"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="FL:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="valuePC2"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JButton" name="stepForwardButton"> + <Properties> + <Property name="text" type="java.lang.String" value="Step"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="stepForwardButtonActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JLabel" name="labelBreakPoints"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Breakpoints:"/> + </Properties> + </Component> + <Component class="javax.swing.JLabel" name="labelWatches"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="0"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Watches:"/> + </Properties> + </Component> + <Component class="javax.swing.JTextField" name="textBP1"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textBP1breakpointKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textBP2"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textBP2breakpointKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textBP3"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textBP3breakpointKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textBP4"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textBP4breakpointKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textW1"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textW1watchKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textW2"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textW2watchKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textW3"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textW3watchKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JTextField" name="textW4"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="10" style="0"/> + </Property> + </Properties> + <Events> + <EventHandler event="keyReleased" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="textW4watchKeyPressed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueW1"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueW2"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueW3"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JLabel" name="valueW4"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Arial" size="11" style="1"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="00"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="javax.swing.JCheckBox" name="enableTrace"> + <Properties> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="ff" green="0" red="0" type="rgb"/> + </Property> + <Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="66" green="ff" red="ff" type="rgb"/> + </Property> + <Property name="text" type="java.lang.String" value="Trace?"/> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo"> + <EmptyBorder bottom="0" left="0" right="0" top="0"/> + </Border> + </Property> + <Property name="contentAreaFilled" type="boolean" value="false"/> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="enableTraceActionPerformed"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + </SubComponents> +</Form> diff --git a/src/main/java/jace/ui/DebuggerPanel.java b/src/main/java/jace/ui/DebuggerPanel.java new file mode 100644 index 0000000..95d6437 --- /dev/null +++ b/src/main/java/jace/ui/DebuggerPanel.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import jace.EmulatorUILogic; + +/** + * Simple debugger panel user interface. It gets the job done, but only just. + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class DebuggerPanel extends javax.swing.JPanel { + + /** + * Creates new form DebuggerPanel + */ + public DebuggerPanel() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + labelA = new javax.swing.JLabel(); + labelX = new javax.swing.JLabel(); + labelY = new javax.swing.JLabel(); + labelSP = new javax.swing.JLabel(); + labelPC = new javax.swing.JLabel(); + labelINST = new javax.swing.JLabel(); + valueA = new javax.swing.JLabel(); + valueX = new javax.swing.JLabel(); + valueY = new javax.swing.JLabel(); + valueSP = new javax.swing.JLabel(); + valuePC = new javax.swing.JLabel(); + valueINST = new javax.swing.JLabel(); + enableDebug = new javax.swing.JCheckBox(); + labelPC1 = new javax.swing.JLabel(); + valuePC2 = new javax.swing.JLabel(); + stepForwardButton = new javax.swing.JButton(); + labelBreakPoints = new javax.swing.JLabel(); + labelWatches = new javax.swing.JLabel(); + textBP1 = new javax.swing.JTextField(); + textBP2 = new javax.swing.JTextField(); + textBP3 = new javax.swing.JTextField(); + textBP4 = new javax.swing.JTextField(); + textW1 = new javax.swing.JTextField(); + textW2 = new javax.swing.JTextField(); + textW3 = new javax.swing.JTextField(); + textW4 = new javax.swing.JTextField(); + valueW1 = new javax.swing.JLabel(); + valueW2 = new javax.swing.JLabel(); + valueW3 = new javax.swing.JLabel(); + valueW4 = new javax.swing.JLabel(); + enableTrace = new javax.swing.JCheckBox(); + + setBackground(new java.awt.Color(0, 0, 40)); + setDoubleBuffered(false); + setPreferredSize(new java.awt.Dimension(100, 492)); + + labelA.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelA.setForeground(new java.awt.Color(255, 255, 102)); + labelA.setText("A:"); + + labelX.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelX.setForeground(new java.awt.Color(255, 255, 102)); + labelX.setText("X:"); + + labelY.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelY.setForeground(new java.awt.Color(255, 255, 102)); + labelY.setText("Y:"); + + labelSP.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelSP.setForeground(new java.awt.Color(255, 255, 102)); + labelSP.setText("SP:"); + + labelPC.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelPC.setForeground(new java.awt.Color(255, 255, 102)); + labelPC.setText("PC:"); + + labelINST.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelINST.setForeground(new java.awt.Color(255, 255, 102)); + labelINST.setText("Instruction:"); + + valueA.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueA.setForeground(new java.awt.Color(255, 255, 255)); + valueA.setText("00"); + + valueX.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueX.setForeground(new java.awt.Color(255, 255, 255)); + valueX.setText("00"); + + valueY.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueY.setForeground(new java.awt.Color(255, 255, 255)); + valueY.setText("00"); + + valueSP.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueSP.setForeground(new java.awt.Color(255, 255, 255)); + valueSP.setText("00"); + + valuePC.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valuePC.setForeground(new java.awt.Color(255, 255, 255)); + valuePC.setText("00"); + + valueINST.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueINST.setForeground(new java.awt.Color(255, 255, 255)); + valueINST.setText("BRK"); + + enableDebug.setBackground(new java.awt.Color(0, 0, 255)); + enableDebug.setForeground(new java.awt.Color(255, 255, 102)); + enableDebug.setText("Debug?"); + enableDebug.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + enableDebug.setContentAreaFilled(false); + enableDebug.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableDebugActionPerformed(evt); + } + }); + + labelPC1.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelPC1.setForeground(new java.awt.Color(255, 255, 102)); + labelPC1.setText("FL:"); + + valuePC2.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valuePC2.setForeground(new java.awt.Color(255, 255, 255)); + valuePC2.setText("00"); + + stepForwardButton.setText("Step"); + stepForwardButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + stepForwardButtonActionPerformed(evt); + } + }); + + labelBreakPoints.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelBreakPoints.setForeground(new java.awt.Color(255, 255, 102)); + labelBreakPoints.setText("Breakpoints:"); + + labelWatches.setFont(new java.awt.Font("Arial", 0, 11)); // NOI18N + labelWatches.setForeground(new java.awt.Color(255, 255, 102)); + labelWatches.setText("Watches:"); + + textBP1.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textBP1.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textBP1breakpointKeyPressed(evt); + } + }); + + textBP2.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textBP2.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textBP2breakpointKeyPressed(evt); + } + }); + + textBP3.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textBP3.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textBP3breakpointKeyPressed(evt); + } + }); + + textBP4.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textBP4.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textBP4breakpointKeyPressed(evt); + } + }); + + textW1.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textW1.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textW1watchKeyPressed(evt); + } + }); + + textW2.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textW2.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textW2watchKeyPressed(evt); + } + }); + + textW3.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textW3.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textW3watchKeyPressed(evt); + } + }); + + textW4.setFont(new java.awt.Font("Arial", 0, 10)); // NOI18N + textW4.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + textW4watchKeyPressed(evt); + } + }); + + valueW1.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueW1.setForeground(new java.awt.Color(255, 255, 255)); + valueW1.setText("00"); + + valueW2.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueW2.setForeground(new java.awt.Color(255, 255, 255)); + valueW2.setText("00"); + + valueW3.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueW3.setForeground(new java.awt.Color(255, 255, 255)); + valueW3.setText("00"); + + valueW4.setFont(new java.awt.Font("Arial", 1, 11)); // NOI18N + valueW4.setForeground(new java.awt.Color(255, 255, 255)); + valueW4.setText("00"); + + enableTrace.setBackground(new java.awt.Color(0, 0, 255)); + enableTrace.setForeground(new java.awt.Color(255, 255, 102)); + enableTrace.setText("Trace?"); + enableTrace.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0)); + enableTrace.setContentAreaFilled(false); + enableTrace.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + enableTraceActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(labelWatches, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(textW4, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textW3, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textW2, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textW1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 34, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(valueW4) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(valueW3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(valueW2) + .addComponent(valueW1))) + .addGap(24, 24, 24)) + .addComponent(enableDebug, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(labelBreakPoints) + .addGap(0, 0, Short.MAX_VALUE)) + .addComponent(enableTrace, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(stepForwardButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGap(51, 51, 51)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(labelA) + .addComponent(labelPC1) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addGap(22, 22, 22) + .addComponent(valuePC2, javax.swing.GroupLayout.DEFAULT_SIZE, 18, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(labelPC) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(valuePC, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(labelSP) + .addComponent(labelY) + .addComponent(labelX)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(valueSP, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(valueY, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(valueA, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(valueX, javax.swing.GroupLayout.DEFAULT_SIZE, 18, Short.MAX_VALUE)))) + .addComponent(labelINST) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(textBP4, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textBP3, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textBP2, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textBP1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(valueINST, javax.swing.GroupLayout.PREFERRED_SIZE, 60, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelA) + .addComponent(valueA)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelX) + .addComponent(valueX, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelY) + .addComponent(valueY, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelSP) + .addComponent(valueSP)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelPC) + .addComponent(valuePC)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(labelPC1) + .addComponent(valuePC2, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelINST) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(valueINST, javax.swing.GroupLayout.PREFERRED_SIZE, 13, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelBreakPoints, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(textBP1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(textBP2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(textBP3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(textBP4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelWatches) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(textW1, javax.swing.GroupLayout.PREFERRED_SIZE, 19, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(valueW1)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(textW2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(valueW2)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(textW3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(valueW3)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(textW4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(valueW4)) + .addGap(6, 6, 6) + .addComponent(stepForwardButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(enableDebug) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(enableTrace) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// </editor-fold>//GEN-END:initComponents + + private void enableDebugActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableDebugActionPerformed + EmulatorUILogic.enableDebug(enableDebug.isSelected()); + }//GEN-LAST:event_enableDebugActionPerformed + + private void stepForwardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_stepForwardButtonActionPerformed + EmulatorUILogic.stepForward(); + }//GEN-LAST:event_stepForwardButtonActionPerformed + + private void textBP1breakpointKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textBP1breakpointKeyPressed + EmulatorUILogic.updateBreakpointList(this); + }//GEN-LAST:event_textBP1breakpointKeyPressed + + private void textBP2breakpointKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textBP2breakpointKeyPressed + EmulatorUILogic.updateBreakpointList(this); + }//GEN-LAST:event_textBP2breakpointKeyPressed + + private void textBP3breakpointKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textBP3breakpointKeyPressed + EmulatorUILogic.updateBreakpointList(this); + }//GEN-LAST:event_textBP3breakpointKeyPressed + + private void textBP4breakpointKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textBP4breakpointKeyPressed + EmulatorUILogic.updateBreakpointList(this); + }//GEN-LAST:event_textBP4breakpointKeyPressed + + private void textW1watchKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textW1watchKeyPressed + EmulatorUILogic.updateWatchList(this); + }//GEN-LAST:event_textW1watchKeyPressed + + private void textW2watchKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textW2watchKeyPressed + EmulatorUILogic.updateWatchList(this); + }//GEN-LAST:event_textW2watchKeyPressed + + private void textW3watchKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textW3watchKeyPressed + EmulatorUILogic.updateWatchList(this); + }//GEN-LAST:event_textW3watchKeyPressed + + private void textW4watchKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_textW4watchKeyPressed + EmulatorUILogic.updateWatchList(this); + }//GEN-LAST:event_textW4watchKeyPressed + + private void enableTraceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_enableTraceActionPerformed + EmulatorUILogic.enableTrace(enableTrace.isSelected()); + }//GEN-LAST:event_enableTraceActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + public javax.swing.JCheckBox enableDebug; + public javax.swing.JCheckBox enableTrace; + private javax.swing.JLabel labelA; + private javax.swing.JLabel labelBreakPoints; + private javax.swing.JLabel labelINST; + private javax.swing.JLabel labelPC; + private javax.swing.JLabel labelPC1; + private javax.swing.JLabel labelSP; + private javax.swing.JLabel labelWatches; + private javax.swing.JLabel labelX; + private javax.swing.JLabel labelY; + private javax.swing.JButton stepForwardButton; + public javax.swing.JTextField textBP1; + public javax.swing.JTextField textBP2; + public javax.swing.JTextField textBP3; + public javax.swing.JTextField textBP4; + public javax.swing.JTextField textW1; + public javax.swing.JTextField textW2; + public javax.swing.JTextField textW3; + public javax.swing.JTextField textW4; + public javax.swing.JLabel valueA; + public javax.swing.JLabel valueINST; + public javax.swing.JLabel valuePC; + public javax.swing.JLabel valuePC2; + public javax.swing.JLabel valueSP; + public javax.swing.JLabel valueW1; + public javax.swing.JLabel valueW2; + public javax.swing.JLabel valueW3; + public javax.swing.JLabel valueW4; + public javax.swing.JLabel valueX; + public javax.swing.JLabel valueY; + // End of variables declaration//GEN-END:variables +} diff --git a/src/main/java/jace/ui/EmulatorFrame.form b/src/main/java/jace/ui/EmulatorFrame.form new file mode 100644 index 0000000..109d3d4 --- /dev/null +++ b/src/main/java/jace/ui/EmulatorFrame.form @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.8" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> + <Properties> + <Property name="defaultCloseOperation" type="int" value="3"/> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="0" green="0" red="0" type="rgb"/> + </Property> + <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> + <Color id="Default Cursor"/> + </Property> + </Properties> + <SyntheticProperties> + <SyntheticProperty name="formSizePolicy" type="int" value="1"/> + <SyntheticProperty name="generateCenter" type="boolean" value="false"/> + </SyntheticProperties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="layers" alignment="0" pref="560" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="layers" alignment="0" pref="518" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JLayeredPane" name="layers"> + <Properties> + <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor"> + <Color blue="40" green="0" red="0" type="rgb"/> + </Property> + <Property name="opaque" type="boolean" value="true"/> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JLayeredPaneSupportLayout"/> + <SubComponents> + <Container class="jace.ui.ScreenPanel" name="screen"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JLayeredPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JLayeredPaneSupportLayout$JLayeredPaneConstraintsDescription"> + <JLayeredPaneConstraints x="0" y="0" width="560" height="520" layer="0" position="-1"/> + </Constraint> + </Constraints> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="560" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="520" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + </Container> + <Component class="jace.ui.DebuggerPanel" name="debuggerPanel"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JLayeredPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JLayeredPaneSupportLayout$JLayeredPaneConstraintsDescription"> + <JLayeredPaneConstraints x="460" y="0" width="-1" height="-1" layer="200" position="-1"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/src/main/java/jace/ui/EmulatorFrame.java b/src/main/java/jace/ui/EmulatorFrame.java new file mode 100644 index 0000000..f1d1964 --- /dev/null +++ b/src/main/java/jace/ui/EmulatorFrame.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import jace.core.Computer; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.event.KeyListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * A newer take on the emulator window, providing a layered user interface for + * on-screen indicator icons. Created on Mar 27, 2010, 6:40:16 PM + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class EmulatorFrame extends AbstractEmulatorFrame { + + @Override + public String getShortName() { + return "frame"; + } + + @Override + public DebuggerPanel getDebuggerPanel() { + return debuggerPanel; + } + + @Override + public Component getScreen() { + return screen; + } + + @Override + public void resizeVideo() { + super.resizeVideo(); + layers.setLayer(screen, layers.lowestLayer()); + layers.validate(); + // On some systems (Ubuntu + Gnome 3) the screen layout gets messed up + // when toggling between fullscreen. This moves the screen back to the + // top of the window each time so that issue doesn't happen anymore. + screen.setLocation(screen.getLocation().x, 0); + } + public static final long serialVersionUID = -1100; + Font labelFont; + + /** + * Creates new form EmulatorFrame + */ + public EmulatorFrame() { + initComponents(); + layers.setDoubleBuffered(true); + screen.setDoubleBuffered(true); + labelFont = Font.decode("Ubuntu-BOLD-14"); + if (labelFont == null) { + labelFont = Font.decode("Arial-BOLD-14"); + } + layers.setBounds(getContentPane().getBounds()); + screen.setBounds(layers.getBounds()); + debuggerPanel.setVisible(false); + layers.setPosition(screen, 10); + layers.setPosition(debuggerPanel, 2); + reconfigure(); + screen.setFocusTraversalKeysEnabled(false); + } + + public synchronized void addKeyListener(KeyListener l) { + super.addKeyListener(l); + layers.addKeyListener(l); + screen.addKeyListener(l); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + layers = new javax.swing.JLayeredPane(); + screen = new jace.ui.ScreenPanel(); + debuggerPanel = new jace.ui.DebuggerPanel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setBackground(new java.awt.Color(0, 0, 0)); + setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + + layers.setBackground(new java.awt.Color(0, 0, 64)); + layers.setOpaque(true); + + javax.swing.GroupLayout screenLayout = new javax.swing.GroupLayout(screen); + screen.setLayout(screenLayout); + screenLayout.setHorizontalGroup( + screenLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 560, Short.MAX_VALUE) + ); + screenLayout.setVerticalGroup( + screenLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 520, Short.MAX_VALUE) + ); + + screen.setBounds(0, 0, 560, 520); + layers.add(screen, javax.swing.JLayeredPane.DEFAULT_LAYER); + debuggerPanel.setBounds(460, 0, 100, 492); + layers.add(debuggerPanel, javax.swing.JLayeredPane.MODAL_LAYER); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(layers, javax.swing.GroupLayout.DEFAULT_SIZE, 560, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(layers, javax.swing.GroupLayout.DEFAULT_SIZE, 518, Short.MAX_VALUE) + ); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new EmulatorFrame().setVisible(true); + } + }); + } + // Variables declaration - do not modify//GEN-BEGIN:variables + public jace.ui.DebuggerPanel debuggerPanel; + public javax.swing.JLayeredPane layers; + public jace.ui.ScreenPanel screen; + // End of variables declaration//GEN-END:variables + Set<JLabel> previousIndicators = new HashSet<JLabel>(); + + @Override + public void doRedrawIndicators(Set<ImageIcon> ind) { + synchronized (previousIndicators) { + for (JLabel l : previousIndicators) { + l.setVisible(false); + layers.remove(l); + } + previousIndicators.clear(); + } + if (ind != null && !ind.isEmpty()) { + int x = layers.getWidth(); + int y = layers.getHeight(); + for (ImageIcon i : visibleIndicators) { + JLabel label = createIndicatorIcon(i); + x -= (i.getIconWidth() + 10); + layers.add(label); + layers.setLayer(label, layers.highestLayer()); + label.setBounds(x, y - i.getIconHeight() - 10, i.getIconWidth() + 5, i.getIconHeight() + 5); + if (x <= 80) { + x = layers.getWidth(); + y -= 140; + } + synchronized (previousIndicators) { + previousIndicators.add(label); + } + label.setVisible(true); + } + } else { + if (Computer.getComputer() != null && Computer.getComputer().video != null) { + Computer.getComputer().video.forceRefresh(); + } + // This was causing a whole screen flicker -- bad. + // screen.repaint(); + } + screen.requestFocusInWindow(); + } + + @Override + public void repaintIndicators() { + synchronized (previousIndicators) { + if (previousIndicators != null) { + for (JLabel l : previousIndicators) { + Graphics g = l.getGraphics(); + if (g != null) { + l.paint(g); + } + } + } + } + } + Map<ImageIcon, JLabel> indicatorCache = new HashMap<ImageIcon, JLabel>(); + + private JLabel createIndicatorIcon(ImageIcon i) { + if (indicatorCache.containsKey(i)) { + return indicatorCache.get(i); + } + JLabel label = new OutlinedLabel(i.getDescription()); + label.setIcon(i); + label.setHorizontalTextPosition(JLabel.CENTER); + label.setVerticalTextPosition(JLabel.CENTER); + label.setBackground(Color.BLACK); + label.setForeground(Color.WHITE); + label.setFont(labelFont); + label.setOpaque(false); + label.setFocusable(false); + label.setBounds(0, 0, i.getIconWidth(), i.getIconHeight()); + BufferedImage img = new BufferedImage(i.getIconWidth(), i.getIconHeight(), BufferedImage.TYPE_INT_ARGB); + label.paint(img.getGraphics()); + ImageIcon icon = new ImageIcon(img); + JLabel renderedLabel = new JLabel(icon); + indicatorCache.put(i, renderedLabel); + return renderedLabel; + } + Map<String, JFrame> modals = new HashMap<String, JFrame>(); + + @Override + protected void displayModalDialog(final String name, JPanel ui, List<String> ancestors) { + final JFrame modal = modals.get(name); + if (modal != null) { + modal.setVisible(true); + modal.setState(Frame.NORMAL); + modal.toFront(); + modal.requestFocus(); + return; + } + JFrame frame = new JFrame(); + modals.put(name, frame); + frame.addWindowStateListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + closeDialog(name); + } + }); + frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + frame.setTitle(name); + frame.setContentPane(ui); + frame.setSize(ui.getPreferredSize()); + frame.validate(); + frame.setVisible(true); + } + + @Override + protected void disposeModalDialog(String name) { + JFrame modal = modals.get(name); + if (modal != null) { + modals.remove(name); + modal.dispose(); + } + modals.remove(name); + } + + @Override + protected void hideModalDialog(String name) { + JFrame modal = modals.get(name); + if (modal != null) { + modal.setVisible(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/jace/ui/Library.java b/src/main/java/jace/ui/Library.java new file mode 100644 index 0000000..4142a12 --- /dev/null +++ b/src/main/java/jace/ui/Library.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Nothing yet, but will be a software librarian of sorts one day... + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class Library { + public static void main (String... args) { + try { + // Type: a = ascii, d = directory listing, i = image (binary) + URL url = new URL("ftp://anonymous:jaceuser@ftp.apple.asimov.net/pub/apple_II/images/;type=d"); + URLConnection urlc = url.openConnection(); + InputStream is = urlc.getInputStream(); // To upload + byte[] buffer = new byte[4096]; + int bytesRead = is.read(buffer); + while (bytesRead > 0) { + String s = new String(buffer, 0, bytesRead); + System.out.println(s); + bytesRead = is.read(buffer); + } + } catch (IOException ex) { + Logger.getLogger(Library.class.getName()).log(Level.SEVERE, null, ex); + } + } +} diff --git a/src/main/java/jace/ui/MainFrame.form b/src/main/java/jace/ui/MainFrame.form new file mode 100644 index 0000000..b42a4d4 --- /dev/null +++ b/src/main/java/jace/ui/MainFrame.form @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.4" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> + <Properties> + <Property name="defaultCloseOperation" type="int" value="3"/> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[560, 384]"/> + </Property> + </Properties> + <SyntheticProperties> + <SyntheticProperty name="formSizePolicy" type="int" value="1"/> + <SyntheticProperty name="generateCenter" type="boolean" value="false"/> + </SyntheticProperties> + <AuxValues> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="4"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <Component id="screen" max="32767" attributes="0"/> + <EmptySpace min="-2" pref="1" max="-2" attributes="0"/> + <Component id="debuggerPanel" min="-2" pref="109" max="-2" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="screen" max="32767" attributes="0"/> + <Component id="debuggerPanel" alignment="0" pref="504" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="jace.ui.DebuggerPanel" name="debuggerPanel"> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + <Component class="java.awt.Canvas" name="screen"> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="null"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new jace.ui.ScreenCanvas()"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="1"/> + </AuxValues> + </Component> + </SubComponents> +</Form> diff --git a/src/main/java/jace/ui/MainFrame.java b/src/main/java/jace/ui/MainFrame.java new file mode 100644 index 0000000..a3d7205 --- /dev/null +++ b/src/main/java/jace/ui/MainFrame.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import java.awt.Canvas; +import java.awt.Component; +import java.util.List; +import java.util.Set; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +/** + * The old user interface. It is not capable of displaying on-screen indicators. Much of its logic was pulled out into the AbstractEmulatorFrame class. + * @author Administrator + */ +public class MainFrame extends AbstractEmulatorFrame { + + @Override + public DebuggerPanel getDebuggerPanel() { + return debuggerPanel; + } + + public MainFrame(JPanel debugger, JCheckBox enableDebug, JCheckBox enableTrace, JLabel labelA, JLabel labelBreakPoints, JLabel labelINST, JLabel labelPC, JLabel labelPC1, JLabel labelSP, JLabel labelWatches, JLabel labelX, JLabel labelY, Canvas screen, JButton stepForwardButton, JTextField textBP1, JTextField textBP2, JTextField textBP3, JTextField textBP4, JTextField textW1, JTextField textW2, JTextField textW3, JTextField textW4, JLabel valueA, JLabel valueINST, JLabel valuePC, JLabel valuePC2, JLabel valueSP, JLabel valueW1, JLabel valueW2, JLabel valueW3, JLabel valueW4, JLabel valueX, JLabel valueY) { + } + + /** + * Creates new form MainFrame + */ + public MainFrame() { + initComponents(); + reconfigure(); + screen.setFocusTraversalKeysEnabled(false); + resizeVideo(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + debuggerPanel = new jace.ui.DebuggerPanel(); + screen = new ScreenCanvas(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setMinimumSize(new java.awt.Dimension(560, 384)); + + screen.setPreferredSize(null); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(screen, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(1, 1, 1) + .addComponent(debuggerPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 109, javax.swing.GroupLayout.PREFERRED_SIZE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(screen, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(debuggerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + /** + * @param args the command line arguments + */ + public static void main(String args[]) { + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new MainFrame().setVisible(true); + } + }); + } + // Variables declaration - do not modify//GEN-BEGIN:variables + public jace.ui.DebuggerPanel debuggerPanel; + public java.awt.Canvas screen; + // End of variables declaration//GEN-END:variables + + @Override + public String getShortName() { + return "frame"; + } + + @Override + public Component getScreen() { + return screen; + } + + @Override + public void doRedrawIndicators(Set<ImageIcon> indicators) { + } + + @Override + public void repaintIndicators() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + protected void displayModalDialog(String name, JPanel ui, List<String> ancestors) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected void disposeModalDialog(String name) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected void hideModalDialog(String name) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } +} diff --git a/src/main/java/jace/ui/OutlinedLabel.java b/src/main/java/jace/ui/OutlinedLabel.java new file mode 100644 index 0000000..7c2481d --- /dev/null +++ b/src/main/java/jace/ui/OutlinedLabel.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import java.awt.BasicStroke; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import javax.swing.Icon; +import javax.swing.JLabel; + +/** + * This renders label text in white with a black outline around the letters for + * enhanced readability. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class OutlinedLabel extends JLabel { + + public OutlinedLabel(Icon image) { + super(image); + } + + public OutlinedLabel(String text) { + super(text); + } + + public OutlinedLabel(Icon image, int horizontalAlignment) { + super(image, horizontalAlignment); + } + + public OutlinedLabel(String text, int horizontalAlignment) { + super(text, horizontalAlignment); + } + + public OutlinedLabel(String text, Icon icon, int horizontalAlignment) { + super(text, icon, horizontalAlignment); + } + + @Override + public void paint(Graphics g) { + String text = getText(); + Graphics2D gg = (Graphics2D) g; + int width = getWidth(); + int height = getHeight(); +// int width = g.getClipBounds().width; +// int height = g.getClipBounds().height; + TextLayout layout = new TextLayout(text, getFont(), ((Graphics2D) g).getFontRenderContext()); + Shape shape = layout.getOutline(null); + getIcon().paintIcon(this, g, (int) ((width - getIcon().getIconWidth()) / 2), 0); + int x = (int) ((width - layout.getBounds().getWidth()) / 2); + int y = (int) (((height - layout.getBounds().getHeight() + layout.getAscent()) / 2) + layout.getDescent()); + AffineTransform shift = AffineTransform.getTranslateInstance(x, y); + Shape shp = shift.createTransformedShape(shape); + gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gg.setColor(getBackground()); + gg.setStroke(new BasicStroke(3.0f)); + gg.draw(shp); + gg.setColor(getForeground()); + gg.fill(shp); + } +} diff --git a/src/main/java/jace/ui/ScreenCanvas.java b/src/main/java/jace/ui/ScreenCanvas.java new file mode 100644 index 0000000..9f0f56a --- /dev/null +++ b/src/main/java/jace/ui/ScreenCanvas.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import jace.core.Computer; +import java.awt.Canvas; +import java.awt.Color; +import java.awt.Graphics; + +/** + * Simple canvas with a navy blue background. Repaint requests are redirected to + * the video class. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ScreenCanvas extends Canvas { + + public ScreenCanvas() { + setBackground(new Color(0, 0, 64)); + setIgnoreRepaint(true); + } + + @Override + public void paint(Graphics g) { + if (Computer.getComputer() != null) { + Computer.getComputer().getVideo().forceRefresh(); + } + } +} diff --git a/src/main/java/jace/ui/ScreenPanel.java b/src/main/java/jace/ui/ScreenPanel.java new file mode 100644 index 0000000..5f0724f --- /dev/null +++ b/src/main/java/jace/ui/ScreenPanel.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package jace.ui; + +import jace.core.Computer; +import java.awt.Color; +import java.awt.Graphics; +import javax.swing.JPanel; + +/** + * Simple panel which has a navy blue background and uses the video class to + * render its contents. + * + * @author Brendan Robert (BLuRry) brendan.robert@gmail.com + */ +public class ScreenPanel extends JPanel { + + public ScreenPanel() { + setBackground(new Color(0, 0, 64)); + setOpaque(false); + } + + @Override + public void paint(Graphics g) { + if (Computer.getComputer() != null) { + Computer.getComputer().getVideo().forceRefresh(); + } + } +} diff --git a/src/main/resources/.DS_Store b/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..12fdec8875beda55ed6b7ce7b01ced4473c4ecaa GIT binary patch literal 6148 zcmeHKyG{c!5S)b*ij*cLrGJ4xI7Q(L_<;xl)u9vz5_DJPyZAI_A4Q@IQW_eVmDXdg zcWilz+gkv(e&5{zD*y|+BMv^y&G+4Bc2yBa^D}ni)9|?84_sVo&h7Am54>VH=HFn% z8$L1M8RPNBr6(&-3P=GdAO)m=6ga0qmFdOu+@@kuKnnb81^oNa=#G8ilo+254$%S- zJBGtJk6wb<JV5LVr$k0*mQ-R=ty&CAI^(VK`obwO>9Dw&=hV$s9g4;6jJHUK^@$p# zfD|}Y;5wHpumAV-ANv1Ol2%ec3j8SrtlK<n)_hXc*4g8{);9W<?m1s{H_n5?A<8i^ j$}tySj&CC=^P11O-xp4aL1#SZMEwl7E;1?b+X{RG<iQ#4 literal 0 HcmV?d00001 diff --git a/src/main/resources/fxml/JaceUI.fxml b/src/main/resources/fxml/JaceUI.fxml new file mode 100644 index 0000000..c6b866e --- /dev/null +++ b/src/main/resources/fxml/JaceUI.fxml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.canvas.*?> +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.scene.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + +<AnchorPane id="AnchorPane" prefHeight="384.0" prefWidth="560.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="jace.JaceUIController"> + <children> + <Canvas fx:id="displayCanvas" height="384.0" width="560.0" /> + <Region fx:id="notificationRegion" prefHeight="50.0" prefWidth="510.0" AnchorPane.bottomAnchor="5.0" AnchorPane.rightAnchor="5.0" /> + </children> +</AnchorPane> diff --git a/src/main/resources/jace/data/35_floppy.png b/src/main/resources/jace/data/35_floppy.png new file mode 100644 index 0000000000000000000000000000000000000000..54188b4976b1f38b80019c3527bf64186d0a233c GIT binary patch literal 1132 zcmV-y1e5!TP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS0000PbVXQnQ*UN;cVTj60C#tHE@^ISb7Ns}WiD@WXPfRk8UO$T{z*hZR9J<@ zmQQONM;M2n*<G!)n^+i(YGV^9h4j)>AkafkhMscEHN}`7@(FS@{s;-=&sQ+{1|cD7 zAUXR~9}K||2##Y6(c02#S39#a(}S~v)>=2FR+<+E#30T4%<uQk`$|}A`JUl@*X%$5 zAcW}jdcD8b*VnHD_V(@Dqh7Dqnq{K*@81_MU%u=bV|e)R;a_E0{tmzw3x2$H>z4h$ zVs37340HkCS@5lE!CH$krpn_1zX8iZ^#!FAgTVl;HQjC(&zo&|NGTZ(hlF8>wN?N> z0FeXGS~D07D2jqOj%l@KS^_Ag==b}?aa`MX07X%7a&khJWh^Z%5d^{1A(sg$B}tN? zl)@M@4WN`FNfNRwLu)<h*G!^P;Ox0IfaBw1(ln(kOSINAOAe<BeERf>r%#_ErJM$+ zBpMEfXss#Ba;5-jnzFgM$^86$ZNH{Kzu&JZFe?j+qF`@tkAs5)q9|$xkW%vb^Jhk* z5n5}MQkPCFrDQxFV~lZ@N-3LbAWc(tcXw&G+mvPbp8>SiNGVZDH7ZyXMKi!~ION&0 zXJlE%y?ghFq6pvj@qNG6lqP>{t#utvYyGdb2_c*UlTm9#RTyI!kH;6k_66dr0LB>C zAZGmp0r#rT)_~Ji2;roD@ZbSbN(Xh>m1P<C@85S9twlACTdkJubUGIJrYnjfD}+$M zI`Ffz!1Fv>tyZJ=p64+)H+Lbal~Pj{PBykM3~M=R08Inv?%lgQdGZ9$^IWBCt?_-| zNnFj2D$(oLuX+CbdF`Bqg#{ixdPEq8wSAY-_x;8Zyt=xIF@_f}Ua+yT0YInI;nk~G z1VMnc79oUN71-9+R_&a%wKV|R?KVOPSBPH)I5|1t-Me>u{P>ZbogDzKUcCyOHWj55 zN-5WTq?8;U9!{N;WtjtLRNy=dq?Dv-TJyEKL7wN7W$9XOwT_RE&jkp>(DekEc!uXG zAf<HkVG^Lz>EQc5QcChXM@s2FH?{FefJOz*15ip$DFCOH9Yv9woJCR8z%B~l6!@A2 zXB$Y8Bt%hETS5qoF*S*+$n)F*G+7|avSfLAxlw`9Xyk4Hr`>EY7+|d>j$;Q9$1y<= zO!fOo1*%t2MF3!}JrF`{<$3<cojZ4G9<Hpckfte%i;Fyd{FrXHOB}}-V@T7K`T6;p z6=hjAT5#>!HE!Iv!OF@CZ{EDoHGnVi=i9e$6W~`M;k0#<rs?9%n>Vi?9UX}<3`vrN zcDv2~{yyz?n=lLsf&itIdl4T#d`PZbxpFWGu)V!4N25{oe{84P7iZ_U5aK7`7a#_T ysjFN*JRkxTu-ydU0X;wf`8NRYIlY_#`u_mGxKpLuGFQ9+0000<MNUMnLSTZ1#0UNW literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/525_floppy.png b/src/main/resources/jace/data/525_floppy.png new file mode 100644 index 0000000000000000000000000000000000000000..09558f61ea35990fe29d4ffc7b185b5d6a927227 GIT binary patch literal 881 zcmV-%1CIQOP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS0000PbVXQnQ*UN;cVTj60C#tHE@^ISb7Ns}WiD@WXPfRk8UO$T14%?dR9J<@ zm(NQZK@`Wov$HY3WI;%60#f!Mwn|A4QcwxG<m|oGL(kqlc<~?5KSDeSR&TutJ>(!1 z5?TV%oXo*!YYa=MG2`xL_w|q;%`Y{x)%b<E4Ey$d=FOY;W*8CSIi)<ScK|8?FvbFb zK;TC_9)JDR4qewpHk<wGx^4;p7!iRnrt7+1*xud-=X{_cW9;4^*X#A6Ay!saY}>XY zMD$1PBqJg$FE2mntXDz^92^|Tm@oig0RI3S$AJ(6UXqfCP_0&_?SN(u0F4OLYBi}) zw&XaD6hP-9`v7d)esX|L^#NFxCA|%<>po5dgb*-IQyPFVHWWauRzo_ShT}N>0FL9} z;^G2^VIUX`N&%dopFaptEEbW;WMG<RH+-uOXqr|?Bob1Ndo}3z*6VegoSdLoEOt!; zctu3Ny0&1LFfzuVX&R!@D8k_|s?{nAg+kxyv27crQVC~gXML;gR2#suER;$m1cN~+ z%H1s@7K<Ssk3-Y6a-~u!Oixe0o12>(J3KtZ&CLxel?n{Q!1eX@P`T6vkk9AA7(*Zs zXb;XgR8@tdC{R@ui;Iikod4Y1-27l!R(@@5?aloB{P(G;DP@0uUpj%+-U%Xt5CTGo zzK=tp5GE!jaCLR{#Wc-ab2_)XyL&u2IhpWMMWxztt5VL)%)D*Q?d<HlP!#2Txm=cZ z4s@IX0EJ-;lF1}iS688`>M`g1o1!QmZ*Om-8yg!i3}aZo{ik49rPFD+u8UMEwG@d& zmQGJkvA4G;<GfqVebWKZ2!{<IMnvAPWZ`fau~^Kjd0U<U#>dC8zP^sFtt}*z$<YJM z&dx&Db%a78EG#UH9)NQWzuyneIeb3f=m8jG;GBbVes9?x34rVc_E7+A+itJk%N1@A zz}@|FfO5GUE-x=(Sr(3tj{XY(MARxqpCM697-O!_=c~)ubPN;CLeyD`KcP{f;s6Fe zk9xp8hMxdr(dY+_X4Dd6%n#rtfH5zBmXHAOn}{6t+|TpB%G<Qm7HZ~&00000NkvXX Hu0mjfa!-ZM literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/6502_functional_test.bin b/src/main/resources/jace/data/6502_functional_test.bin new file mode 100644 index 0000000000000000000000000000000000000000..f666e06740c5fbebfb6805dcb618f9d2989ca48b GIT binary patch literal 65536 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/DiskII.rom b/src/main/resources/jace/data/DiskII.rom new file mode 100644 index 0000000000000000000000000000000000000000..74c04b137a9d153e3e466b85080dfcb29f54f09a GIT binary patch literal 256 zcmZ3auz+C^bDK>Umx|2?0ahE&|C)6hIJ_?Wo-sF!`NRu>rwS4OckN|h<N|_L?NxjG z4(#nYu(#{L-p&IH0`@i>n4!tc#R?Q}Jg_osCBv%@f!D2St+uU>D>+)#B{+a;1%7*8 zz3?5(IJxS==T{Kcv=5vb6YhJi{h)f`<^twc2e7DCs|}b|wduI<M&qdMg?&pMjyhi0 zv0?jyuojy=AhRN`F)gv_n90C=;T^KbM70wa-oq4JXa{;J?3BUpjcRcW%vvA}l7IJ1 W{h94j+p`QD%d{ul_F?2;0097PfqNDJ literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/RAMFactor14.rom b/src/main/resources/jace/data/RAMFactor14.rom new file mode 100644 index 0000000000000000000000000000000000000000..50ce4ba3b29379986c118976966be9116eb8745c GIT binary patch literal 8192 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 aDLn(5V8=5S+dlt6*y3Mg^dmFl|NjL6jKY}! literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/SSC.rom b/src/main/resources/jace/data/SSC.rom new file mode 100644 index 0000000000000000000000000000000000000000..2c94c9b22d7becade3e41d619380cf7ce7c30ea7 GIT binary patch literal 2048 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/apple2e.rom b/src/main/resources/jace/data/apple2e.rom new file mode 100644 index 0000000000000000000000000000000000000000..28e95a953ba0823587c9830ca4951b2277cc1f9e GIT binary patch literal 20480 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 R?++b%ThXI<<=(wr{|B0iA{GDu literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/apple2e_debug.rom b/src/main/resources/jace/data/apple2e_debug.rom new file mode 100644 index 0000000000000000000000000000000000000000..367cc6c1424a4dfecb6904b4f9f3e466c9568830 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/apple2plus.rom b/src/main/resources/jace/data/apple2plus.rom new file mode 100644 index 0000000000000000000000000000000000000000..91f1d263dd48a8bee8f2548b3c2bd23f7192b36a GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope0.png b/src/main/resources/jace/data/ayenvelope0.png new file mode 100644 index 0000000000000000000000000000000000000000..30f0f2ece3b200cb775a2d9a6d9180a1978a0e58 GIT binary patch literal 747 zcmV<H0u=p;P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G4>r%3iC6#t02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Kiv zL_t(o!{ye^Y8z)1#_``fA5mfpf5a#srcig$Lg^*sI<hXI;Lt^ONeV5lET_9}vdJQW z5<-?;^<r`bAt4Yel2XaCG@g08m=U%q-P`KBnui(A%=w@5oHK#PUz2SU*2zm|16m(= zGvv=tmi#279g?qN{OvEwZ%Mylvcq>9?5_V00JJvAZbFO)zY}&?{Q}#pZ7w^6grbF2 z2%-Br#)B^94cwxAbJ-yz<jENsL+CxLJHd6Px5zuo3?U(_aMI`_;!z0uzr@&mPQFQU zdzm35Oc5IZ<(O(f7>2O-V~kH;l5fzuv8)i1di7PY(G%i;lRgF^?A?p8`wCmj3Ze0T zS~YTxzp46UAISzG^uAwa2+4UFO0pSo#PB0NPz=vQNN(5LVS9@Qoy!g&qlhGBGXqM} zg6T0wLpCN<70LyK5ccj-WaO)Cjrgg<wd#D>qbA-;dNI;md=IRYj4Rqx-XCWuNau|e z=JiIna?I1W>|du?*dwiH!jqRF^zL)AhACNr^85=|E?AxOczRtFv}YKx*3Zai7&mvv zby8JP%l%L6maHL1B81)#F?Rn5;nzo`=_PS`Q=l}~1vjQRq%EsS6o(C+L-JAG9*gs0 z#I92aLI}P4F&;d>?EVko=R1LiJG8f$wP?*4_xU`i6F+-)*X!w@cj|3$(ji;HHOg@M z+(qn)@;^Etf0xzCmUGf89M73d*q;77$cqtoQ@%tjxH@Xgo>QGkIt>>Oz~VorQUl0m db*GQ}{{TD0;Dq%N6nFpt002ovPDHLkV1l6?NooK9 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope10.png b/src/main/resources/jace/data/ayenvelope10.png new file mode 100644 index 0000000000000000000000000000000000000000..4bcb11fe55e61e6b3d307a86a663d3f82fb29cfc GIT binary patch literal 755 zcmV<P0u23$P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5h<3@yK(>k02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00K)% zL_t(o!_}8PPuoxwhM(i$7!n{~e6?lnFX(UR)Rlz=RVb`X9aCc^>ehjk3L%#6Ed3Fc z`WreRK_Lp{gC=pD<aD@DprTQ1D&pS7^0m)%Umc(G-fMxKWgrLs95@!x2I_FqguMol z$5saBb1*1CCIf*3whxU4WX|Klw}2#&R{ii5f@SC}14{<Jm3f>vdl6DEfK}DX7hu)g z`zvwbThJJQZ;5zsR4Yf2Tm)7OJc`J>;;d(2FQ~eQBJba+x`$vdT+hdXZ$UqR;ZNxK zBHp^H`xz*JvlL$BpW-C1>h6np+mPsqc-yM(K1e=V68=5kf<gyoT9CX1nj+qY$z<3h z+!g06ft^(?ABdES(60ei=+{I_Mb+{F*x5Mn<zYAlQ&!#vPy_!Qf+{pGA2ybe@eQB_ zeixi+U=GN{g)d_+G6FIxE~tZj2JH(-b)h{Csk|{;I~xbSb-U1B!#oVC=4EPsZ-=$K ziCN2!PTKGkuFl|l4R(&r_3F&bPm+Jz4ic{1;{Qd%R<ev|IR8&ihfRWv@47dTQW5CD z)DZd>Fr!-jG`_e=^KUr(0L~@+zFR&p`>q+P?ygAbE$D^my>Y}%f`2aw>(Hyh;KUjY zxND5VQ_u?$Z&TIXg+bDY^ERW2dusk~W4}J^B^|KR67l$%y>DDm#7%-fTYz2;B&Aw8 z67gP1c*sM4Ux`|DaZkyAJ`eRQBvOzZg40Tj5BTQ8YYX~AX!jxAg?bC7>M+}ck98h3 lo`U~UU{PTZfOf&|`~pe9t%*u7pM?Mb002ovPDHLkV1gI$N$~&x literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope11.png b/src/main/resources/jace/data/ayenvelope11.png new file mode 100644 index 0000000000000000000000000000000000000000..932fa746a5ad91344a5199e0381e52ddd1035ff8 GIT binary patch literal 897 zcmV-{1AhF8P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyr7 z5EeF6m>@s^000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0008qNkl<Z zSi{}d+lm}j6b9gL*P(k(!RZV+kbvl&@8GkD<R(G95)6tVGu;V-;I&AC5X2Yq5dzU9 zX3{g$ai&kzRojbQu{(q)c$2j2W*6*Lt7`x2U+Z5x(EkWmaE>Z6J0kmots(s*uUP*) zlgn&=h^x$dlsQc#Z@kg!Z2A08e*e|}Eq-0Mdl3~*sh(5*UNAdP{(g*iAB3=fla1%h zI;go`bAPlFX`B24uEuz9&zf5Z-+Wxq?cox{c<1{N_6KY?m{Z`o6%a}~JLshVgP(9l zeM&Ya)bHj|-^76Ktf1;rZl?ns{1W4xgT#b<%*Be+hJN0bLk9&hZzG@Ki2Nyw5ykZN zk5`d0yj=jA*sc-)<&1K~>9C+&sh=!{F!&)cVQ`~$ZjjO6P7J8>#0*mA^QU-7F=R4f zbEO8yE%Ft%^0x;-wZhD;Iwf^y!i#A^(Xd#&{A1!cg#N7<`}ab)eT^(808YkiZqUuB zTST*Q%KYLP-~GCdeIR3kxL9ViIVPNhaQm|u?>?a1#btIc5Z+_aB@0Vx#fpvTx;Da$ zvR>ARGvYCeA&x`1eJ#d=pIP)+Tp-&^a@lm2A<EhsTlT7UW^?>WGo(1AI1XX(X<~vN z#U7!T<dDRKD*qn<@V7G76f@$8lS4eEI!R2p`y+9etVh1n`WI|ka{#Ki)XBO|EqgL$ z_XLMDPg*A2h_U}IVGCRB`vr?^-5F4k&)ZU%V?k$1{}E28S|&IOVgF`~2cOfd=z7(x zIRND>O-8iqgr;g||07-;^Q;9Se-^_2AjZKb%cfb*($u)V3>c-ckaiuVA>pOnAGNCR zn6uxA4@0>1b=sR9aysI1wYK*GzWjilJ!W0HGfsyo>#tmb+MD3;BIhm<_6Q~M1@#j) zhHc5LofYY@^_;xJbk5lsS0=B!_K)~tgHD+kUv^W!D7#Ge`0CNSs|e$j=dHHVcktL> X)}cC$Jm=Ve00000NkvXXu0mjfAIqlg literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope13.png b/src/main/resources/jace/data/ayenvelope13.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3f906027eb7253791304faeeba1bc5bf709c66 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQY`6?zK#qG8~eHcB(eheY)Rhk zE)4%caKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmPar<8z@#dV((&Ojl_64!_l z=ltB<)VvY~=c3falGGH1^30M91$R&1fbd2>aRvs)5KkA!kch)ir`Y;21&Xxp=Qj3f z>Ui`+_+@NsbOEc`F}Lsqt(|^qdIt`1Nhdw#x-+R&>7o0FlQwPHhQSF>FEGA0(civ9 z|K~Z&olc6eIgWc7|H)0^J9wWpYeLw9n6#Hum)(pGJHVv8Vg7^tCzC!n3R?v9My+Xd z$?MyDbdq{VZ<JDb`09y&>UlZTf@I>#Lf+q2`l#w7e4ruf!{w*u2VVVQ_{Sn~V8v_E z6@TqEtd-j8ns?r@{lWLm{5_2I-^#yRn=jer!n?6pF*I1>?1Hp}ACE;F_(K<`PyE5N zh5w)Rg~oT{Hf3S|_(a;5b5G{qAS?0hLFNXr_6w;$ESc}LoaZeNmpjm$Cd<M2`~jm^ zf|+6_L+SMgpV_9EEf8Aarmyr-)1ZXa&Rk)}O1Je&A30ToSA0287Ta*X|G`(mkk>ae wcvNa7`hk4ugkK5AZ(3{>kgAY)*ipwghxItK#rrRkz(`>5boFyt=akR{0C5h-6#xJL literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope14.png b/src/main/resources/jace/data/ayenvelope14.png new file mode 100644 index 0000000000000000000000000000000000000000..12b8ec08149c7ff231b4e1ac8e3f364ac27aa058 GIT binary patch literal 787 zcmV+u1MK{XP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5(XA6)#v~K02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00L`C zL_t(o!`+w5PTNorhQG1nBo2g-ge0_uC+Mpnb;YI|gizVC>aNtWK;5$ip^8oS-Si=< zzC#yCT!M0gki-e`ba8?zL@RKjV$ma6mS@g<`e%Hu!lT<}4(9SODnc#?!xT6HwA*0U z;j#()Z6<C{!GAdit$DCBkPIPpWknO`lpym8SW?~n1}vFxr)2z_x#^Ps*L=#5sX)4- z`sq6`V?`5Zy#QxH)jJaT_+HgJ0%u`tpHSpe@>zsrNp<(Dh`$1XWknMw1y%1*#NPsY zAmVSSdWRr|$wWRSKUD^bs@@k7|1Asx2(Kk#@R7*&JD>-N5N<5MRoyud@z;Pl1T`35 zJUnOP;@@&O18Oosa2vRSpbx1ukOgvg;SY(7m47E*q6H6I;G9FZ0hvB@(~v2^umaBf z<3z^EzaM8st6X>ropbnEhw8C;cg~HSx`@AK7A}e={&vo;77Ty25HJ6J(PYxf{VyX% zrc3_$uNeZ>SzvrfU@>~~&wJOfpzcC13xhl`H(Byv{FhDBKd8a5C9=J)x^n=DB2a=v z(TLk#H^Vo6z6A*ftQknAejgH|CjTdq;VEcC#NSZ$_F$A$_4Y*k4bX=1$Iln2E(2v% z?}x~TH=tceHlZ`c1*BSl+B}p?&|3s2ue$Sj{AZfro<Z#b$}Oli;^YfdD`pJ{vXHWX zF3^IL%PDVP%aG5)r~p<DBmsH@VH@lQG_PR)IBtFd>LH~1U|T>4T$|@_$*;Qb+J$x+ za%t$=#-6e7_G>xdCZPi)0+0}N2m`=%A+Vu3GkF1`^1Gw4&JlD%nC-zy{}-8})+o<l R!-D_-002ovPDHLkV1hV9ROJ8w literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope4.png b/src/main/resources/jace/data/ayenvelope4.png new file mode 100644 index 0000000000000000000000000000000000000000..4245e30741a48fc3cda29b8be299504c6481fd19 GIT binary patch literal 810 zcmV+_1J(SAP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5FFWgP7nY902y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00M$Z zL_t(o!`;`<ZW~n;2JqjVKX#Kiah!-Ggf7|zNQfm%9)cAcHV6q-;sw}1WhX*nQMN4T z1|cN&m3S3iAyQl1{I}!Gj4z9s$fi}RED*&$(r89ApXT20yZ4;$+~C&npv(RS){vvX zWql1^SkTCB2m5S`{Y@r&w0jgO@s#tYxU#^J+-~JR+T`*c=?7FF#`ygclKXVF$nqr? z;6ay-9`;$gOSl)~;iG2r?AE)XduK@nxFO%!#U9m8jEDUY`oA@RHHtP`ODMn%`TQNm zd)SKc@KFc{Kas>4E>A3YByT34yi4ATvHx=jgKvo=j2d7`HeGZ9ve%Pu?_e{={x2as z{F*GGiU>6}mW&%mTnYhRA-^4C|JM*6eu*(9ObP88KW$QBoGgI=uOol(1=%UixlU-c zsj4P>0x>7F7F>Wq2OHSNOXQ0a@~51PSh+*FP1*=BT_x+1?J)S5@tBplnlP$n&#UR% zrTLG!R9sByj5)f>7_P9oPTHfjF(Yrt^jUo$6|~n$irJ6SPHm4+_E>qTBV0uh$-~=U z=X}n&ih|Arr)&(fx|2)RXtiVP{~p4@=Qv~XjN&=%GakR-oA=2lsAhctR4vlc-23@~ z|1W0O%q@-08RmlGFFG>1DW);@{|Mo`Piw1d9a9WBJLbpJW<{^KzNo(uWB;o~x4;m7 z_#`mcMUTlk?WFPf6OPV!d^&><wm5l@wfls9La(kNNmV;S`i7pMnsbWu7QMs2jj!f* zXL?OJB74r{C}TLKeMOqoHg|KxohJ`}j=4J|Z`FC^xE7!)DJ#<A)q#t&?iSOWvYBLA o5)1O`zr)Yh;Nm6?&Y52R4Q;3Lz%5DiwEzGB07*qoM6N<$f)3AetpET3 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ayenvelope8.png b/src/main/resources/jace/data/ayenvelope8.png new file mode 100644 index 0000000000000000000000000000000000000000..e653b1d8c62214055880bde63a331673ae359ac1 GIT binary patch literal 966 zcmV;%13CPOP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5du1GLNWjV02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00SOL zL_t(o!`0VKZ(Btahw<NBzviuR?4&PIS}KGpBzAn4E)ik_LWl*6u237NRRt1jgkXaZ z5@N|0K<byEYY-^Vw2mE`+Od6ohsE5GSnY-lt!H<xp8H=sI&)?w(0vozSYsL)ACf&{ zxkvZV-1>Nn<t>(W$T!lrNuTV1W{>Xg^L+OzCmkwRn6;=DFPHyhn|h0-HtKkmF>aGz zi*ffSvKuttAYMg__t)nGsx@Y9^6eP!{YY`0$`08ky_F>B{MtvpN^vE|&whCMJY*}3 zIx+74%=A)5V}t4?i1F?hgzK#Aq^_uT<|V8*SzSNl?|)A98rcq=O=fG{Y@hMlwZu2J zUJbrdr?JLTC)KshjA;Wa5W?>FG2Y!}b`h7kxW$DHx~Cy^EIrSM%`^VB3$HGp)k=P~ z{C_p0mYr*l7{cC{sS~tQNNp=^hwfF>$SaG>PnxJQt0#U|&(h$KmkC4SFof=>G46hy z23>XqWyrWmHDgvR9zSTY)J)PUrL;;VV}w`=m>pB}34;)Be;DJvZ>hDCNn3>VGN4#I zzE$#hgZOXPWYlI9v8<mF`{X_JL%983jJw|@llc}|hkQA;5mZKt$6KQ~-2z#;|4$8O z6FjBpQGHCcAHv?d$)t^RDufMYOQo=!>TvP+G&IRVHh)Hj8PzfIh{0q0#dMHN-usH! zBHN&{dTJ2U#9Kf<RI_yZo6X9ASqd1P&>EzW;?L6L2QhZ<6PB@Dc1}iK&^W;Y@~Wk= z%A^dKq&BLbl-I}s)6%5F5boTHarYyNaPD?kN?S}mqc+BISs!CWF)i=fhy2~=pbV)T zgm7ms#=UpWhNclFi^^v>V)`W2(<iB39i18d=BS*i`%Hc#{u;uaTjk7o#JJDV$&2}C zfBZN8|IZI}UuWD&dm|2M^w{sEAJ4CwZQg1zzMS3<(@GjP`TmPvj@>rH4#g!J?Q~St z2IULp;CcS#<X5;}XESFqp%_v-W^K%a$$Y@W5jP65A;nXgeMUW22K+MOVLs1481Z(6 o)`a6Jm8VSm43B6IUzPlS0HIKBo0v$(9{>OV07*qoM6N<$f~)GnZvX%Q literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/clock.png b/src/main/resources/jace/data/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..b0735bd86c3f21fac4bfed3ba7d8e4833b8989dc GIT binary patch literal 4657 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/clock_fix.png b/src/main/resources/jace/data/clock_fix.png new file mode 100644 index 0000000000000000000000000000000000000000..b75926d221e53823c1c7c3e4bfd46cc0f37eb6a3 GIT binary patch literal 5595 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/disk_ii.png b/src/main/resources/jace/data/disk_ii.png new file mode 100644 index 0000000000000000000000000000000000000000..89b79915213da9c373894a3c420fb857e5716129 GIT binary patch literal 13149 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 DuJ!(B literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/drive-harddisk.png b/src/main/resources/jace/data/drive-harddisk.png new file mode 100644 index 0000000000000000000000000000000000000000..68351d1b692cfc210fd6bbf3f3751f6c67c8d18c GIT binary patch literal 3317 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/envelopes.xcf b/src/main/resources/jace/data/envelopes.xcf new file mode 100644 index 0000000000000000000000000000000000000000..8ff5dd8c0b386d318908a02652520b690f100696 GIT binary patch literal 13334 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/font.gif b/src/main/resources/jace/data/font.gif new file mode 100644 index 0000000000000000000000000000000000000000..d34a36001c3fe787b364067a1fa5e10bfc7820c0 GIT binary patch literal 4527 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/harddrive.png b/src/main/resources/jace/data/harddrive.png new file mode 100644 index 0000000000000000000000000000000000000000..e732e9229da084156bc3c7c57ef02fafb8497c8e GIT binary patch literal 2183 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 JPDHLkV1gzBF75yT literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/input-mouse.png b/src/main/resources/jace/data/input-mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..ef449c39f6fb9b266923acd2e8057d9cd5fe1ff1 GIT binary patch literal 3497 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 X9NqU;9iOcG00000NkvXXu0mjf**uH- literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/network-wired.png b/src/main/resources/jace/data/network-wired.png new file mode 100644 index 0000000000000000000000000000000000000000..816a626fb0d831dfac172e895ded2ec7ee22e426 GIT binary patch literal 2661 zcmV-r3YztaP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF000UsNkl<Zc-rlo z2T&DB9>(R?yR~X9P^vsbv6xmZU2_(5_K7)R4y97&fH|Egm;+#3!x_Ql3@0W`SGr=v z96(D21QRM?L~%v<-}iM@R}Ifcuu8lUZ>+Dz>FJ(+zi+yGI+066zH9y8fdBOmxO(-f zL`FtRR8&+N-Me>Bw4$S<(<mk;##+{KjOjmlHuE%%0oSizm*>x)%ZnE;lwQ7kDX(6= zlGm?a%bPcEBr!2j>FwLMO7Gsilcc01YcX8M(P@~T&*Agbb9Elb(>w-Tx^zjNK7A@{ z*RGYhbLUEYe7qziB+!)f4AG!Q=yZYk96oQ|x^?RL&z?P#%a<>k&j4?4Z#j1CSXK?t zC4uKG8NlZsKYrZ22JnZ)#l>Zp0emi>pQ8+*zCQyF9y};hr%pABVL96XYVzdCGG)pX z89H>RlrCM`D2DMKrQyFx12QzBY}v9|QD$%7d<I;+cu}4_dGbjqm^g8w(xgd~l**Sc zpCz%R=rl}kZ2<Fu>E<-RXhNk*mA-_m;mv74Y;3Ht6ev}#TGghiRjVe|t5>%c!)?ab z@Y(?8gFMY=fVBxVYSc)hnl)=mty;CDcJ10yr%oMladFY&HN$zI@r>!L;hB%Q47hON zf;@iw*xCdFDQV!kb?ZvKdiA71g9g&DVMA*%oc9?|8$wybo6msr=g-TdM~|#c;3thR zO7-j4SCCDbG?AuFn@aQM&80<)7FxV!IPWta<1-zfVGXbHf$8Ql;NioE)+X?cDm7}< zNE$b8EX|rVla?)8O6%6G#nshS+O}=0#cPK1KI1Vy(=oj@yvhf8n#X|9&`=s+G=U<7 z23K#{VzEe@Hf^LshYr%IQzz-%xwCZb+LdQsGo1GskMXq;8eGFOA9ERS?%X+f@ZbTZ zO`uq$DFxH2RVxM8v13OC-MxEv>D8;3^zPl8XI?X$_Zg4zRk~@@rfF~uukwNE<}u*x z*|T#0{(YkfN;7865Dnkct+{%`g9fyhq3&IzfB*h6c2sZi7~Mw(4H_gPhjmxs-MV#? zzP&80650d}uHltnx_JyZbLI@Ev}l6S2o_~Z!A$jVQAOQrT2q-fv!%GZyNlm~j<RfF zCmAtfge>-Mqr!Xi=pp}@*g)E~Yo|>}1FrLc>E<#(&+{3LU{R*n$gNpaQMz>LA{+gx zi~p)RGHlo|`S+%#vS(`x88>d6{A*nU72dOFPg%dT3QG&qF?||vod@zXmjQa>(`ZEd z_U*L@L<0!O;;%-3<HwJenKNhd%xi}8KI1XIPHP0$d6>(9)2C1CSzx0PdPuA113p_i ztr1-3fjqyS0iK?ofAjbEcM1py$e$}$E<5Y{r%s*H)8e*_pa}8VGN!kl9!H*E$AFC+ zH|7C+8}z&$@7%dF2s5)zG=W;Va%J9m^XBD0dGaI;(8_#Wz_zloE}--HItCDa)22<X zn0$!GHAiJBK|w)>mn>QG^MVBnimYG1J|H+aIAZ_){c`8d9is^uFO+01oyHpcs~Nyz zp9coC27DZ@)qI_X2n-Ai!5YABOjLa2y+el%sYYqls#To2)<4pgAz78p^s85|=JT`P z0Kx;%f=LGrpXNv1ioPWeUOY$<!@)^Sc6^Q;Il?&EqS>=&%h97pv)2F(9}*IhU<*Dx zDqQ?x{A9($6_Sd7`bbSpm3{m6$%zvubalusWuF0rM=`b>K72SaEG$g!-o2X!J}fd! z!Fxx0OT@bf1^(`Xq%i>vrR>_ZOVBX#M`oMw8^5dpgkQdVxq=T54`)|X6}Ou=Zz}j3 z5jVv1s;5i|pCXY-k)n!yn267RJ~d&(h7A$u$mIKGmd6S2<KtsNW2WI%UEaNWw`|+C zO+v4PDtM2}9`eWSKhz9O@C^~qeaZym!O4q{9v&V67|Z81xdEBOqvInhR;*B6)LC0+ z5nh#se;$pM34w{q1a)BUUNAm?9vho7X3Ut~7}^$LKf&|{5PtFE#TK;S>B9Hx*H4Yt zuUxsp?x%tuz2!O2sw61*kP!LEA#p?mTj~%DVShHS0iO@AHzP=La<T;MJ5P9367FBS zCW-j`5k3YDrvBFBb9fk4K=l|+X#nBp&!2BWFD}dQJhTZc2{C|A2nv$8;9!Y1eD+`f zXK+$bZGtLSuAI+w1`vMEoH?1ob69!}khTt1hdpc7DEND~ZyVuNzkvtF36CpQtmtGa z0|-BB)-20`0|zpN=O!du2TKG?#()6><OYne1;1s>mUt{~xS|UD)hq@OKDU>bSBozJ z&#%H7lmLDr*-tRSZ{EB)9!p%V#flZn!+gzTfIUWUS8Ty!h|3unH7mP)`*xe~tPVza zrem*<-QQccZV3;J9OF>$TWfez88C9>$RDxn+<EKPt#7U2(E}8W>!nYhKDJ5#uNiI& zp1nnOdpS}|s8FGT1;t*$o5}#Z*&ilMm{1jK?SW&*j`ag{vJoDiL%~-;P(_R;WEGwb zUZN4+bOvAw+5u~9m++R^QF)Jgo60e~5nhYq?Y@2c5}MT^OYr5&l{3Jb&;Y#EpHKyS z0RJaV*s)_rDqC)R3e&)2mPZyXTEsIOM79Z;!ef7r0p5fL;IKmpa2_&b$f&`C2m8Z> zWWw_8a>$EWpEU4Gmo61QKR@yH^_Ahnho=Y6uSR0Yk|hoBCN%&pw>{o|2*5v}{y)PP zAcb|G(1csIY?(}-KHUh<sV#1MWviYpe9@vsO#x43)&M$S>FFYXQ(64EsK|zm;0WWw zgj$<}Iaz^cyI~XF%vInoSd;q|)!-%!AT%d63CmWW*W8JvR$x#~GJ>aRiG>OkGQykR z01Cs?zq`4){f0ik`-KY^3XLE%+60c=lnJON+>yc0;OD@b-vI2ma0I+Jy1+>U!sG`H zpjh;|9?$)FG=bNt*r=F_KEsD^1)j<f9_#I1ShyfOL0H$h9L;_AN~j*(hLVDQLNZL@ zvh@4n#f$sl-dn))Ymiu=KmpUjQ|ZEEmDL)aZ&|Ovz6T^bM%<b841IWiREs|uR;5an zz`}(KcOE!!;8jOQ$CeuYn=t@e%p5T*>jiLI@Z5Nigbj$l;d~RosTd=CzyM}csZynU zuuJPwks?K^0{Tb5+vA+u%<z;od_EN8*%`rOqQd)^Uw$b7{fdo97f}LE=FgwsxnRM9 zKjG{E=pW1uZIlr{FLqV9@7=pM$_6~Qp(SC^;^pMz<fuQ9B}$ZVAT<Bauy0WsJ`dim z1@ONEJ{0heGJ?-ZB7AOaMk>E&&z{kM_XYe;z=s0<k%q_TEl0vrZ1EicUmNglfFB2V zU%>AKd?<{GDpRJ+Yz?0y4FJ3y;GF^Q0(dvTj|03f;D1NU?hg2TIsQ*)0dEI*XTZAv z-VOWfMsv6d_`KiE-wp%b4)D%^uZ`YZdBCd<Oiq_|575rp+1bI~-d=ZLa=iWvi?Hs{ T4UI&G00000NkvXXu0mjf=7AGZ literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/ram.png b/src/main/resources/jace/data/ram.png new file mode 100644 index 0000000000000000000000000000000000000000..21e2b3b58952b9082e02fb65584bc299ee09674a GIT binary patch literal 5520 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�H>ax&o14H89F$}ClRmed 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 S@>4ee0000<MNUMnLSTa2)sA@p literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/rom_image.png b/src/main/resources/jace/data/rom_image.png new file mode 100644 index 0000000000000000000000000000000000000000..83e176b36c02632832b6c3f0a14258024aee86f7 GIT binary patch literal 954 zcmV;r14aCaP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800009a7bBm000Gv z000Gv0c~iV`Tzg`8FWQhbW?9;ba!ELWdK2BZ(?O2No`?gWm08fWO;GPWjp`?14l_j zK~#9!>{m-q6HydC^XO}e?GPX#EeT2@ERE?3#h_c$#I?q?1``v10$aC4O>_eb<7;Pv z#+{Xb1UDLYxPSt|gml^tVqY_J=X&mK%d>Dh9TFFMo0FN|xsNm7`M!J30Q_%%xk;h{ z6Zh^HL!po^D>4q2_>4k655vRfVQ6Rwg25nUvum)tybM~ji!Vs==ejP~mSsMEG}V35 z`l`qQH4xAvng)uZ?CbjlrBWFRg#z}gu(PuRk|cwM<C={*P@<p<*B3%0?Jd}BJw3gU z%NbBCnjp&(I1Yiht{)f!6mg)g_Yh1@PR4VFkxe8LdVhaEeEs$vx)9o3Pxdv+HpG6Z z#WUu>?@|d;7cYQe7_hOiZca@d4Ts#ytAHBN&kml0(a}rb5*G+=<PvhkD0|Cep0PR9 zC2)Cc42DNWARN(JhC?~rk;!CgsZ{C#AtY>?McCZ@i4LhqE2wYSuAu`p%R*-yeESW^ z=MD4A($YiED!6lZf<{>gI%p*f!Lz4NBwr+C#6mP$$N7ZDP&hzJKMu6B&~bBpt6*+! zf$O^y1j^+yTphpSQw0qKTx7I2tIzYTf|Zq3Q1P_G7xJtE%eL@k6n`qXdFwWfL^SXi z0q$_6Qihi=o=c}dLQ+peUL1fr3a6t2;)2KhnFstd^sR#T?>-=b!qC^-2exe^tQAZ? zj!<F@foi1+>GXBKDj*yIcCFQ_kW40c1{A8GF-N2Vxdj5-wr!c&>?$lSe%|E(hf9`u zP9Q8n39Ko_h532-fyD|E((I@rN~dqoa5xOHSQm!C!CIgPYk?eCm}o>Qs8*{mJNwqJ z3hIysgCT@o;Tv-f5HSZRCH_@F8!BK&G21vEk2e@jgeq_e_3pr76}*1+hHu8;WHB{O zlm8d00As+$O;68I-xU!vg;&J@(WnN*AuX$*gVtUy5UYUh$%@AxiO7mgcF3G$P44(s z!T9(!ir)n=Fp%VjQ0z<$wzjtTy?_E>)oPIW^wFOR2ojIrH^KVaI{(ikDBQWp0jvjw zBpgf*NVemsc+4tzl^x<Jn2IHYQN|cb2>Ui(<LnTU+D`*qJ@~z_jkn#+E~e^+k$v`$ cw?6_500n6(+V|Qu?f?J)07*qoM6N<$g3ey14FCWD literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/thunderclock_plus.rom b/src/main/resources/jace/data/thunderclock_plus.rom new file mode 100644 index 0000000000000000000000000000000000000000..6c5c1cb8403fefe67bae17316dfe5baf3af76f05 GIT binary patch literal 2048 zcmeHF&ubJ{9N)~&>}01|l|;f;Ed5HX>`JJ6@W+5bx^*|c4U=Tp>{=BGd+6cSQx6_; zQFBm`g@qh?*ajAc(aJtabg2brgNq7<=FJ{_haN;wJa{lUl$Ta<5lZ@<iP%g3gXY2e zeBbxY_xt&L=lj0>bE(lYo8N0@t1s#G&ygyrljY4PuxFLhm0C|XaMk8?N@<fW##NH4 zFU{$9dV2Z_v3Gbs9Tn=UW71&#*GzL^*s?kM<l0d-+wfFoG)$F68m3117xFwmAkXkI zS>h+8%}1ogXXQD*PfqZIvgiUIMNIK8rR8S$Avul$q~oa=kj20qH2t|@uJr|kvtopr zYuOC&;az)gcWPZr?V^Tth%<ouKI;!)AL`>x70==w<q0{&LFUps;fXJmpfEkTa0H!8 zrcPMAVaD)GUYIfQF8COu=x%wt0*0SgD6JDpS&b}WVfg<YFZ$fn#lUB#E_MEui2gYL z2>WxJrYe*rKbG-<nHw2}MMC}!Waf#`?U<TW`G}ZxA`~OkRddZCbvFXHe2ROir?*W_ z9AtN)=~G7y?t*<?>g+gPi9eWA)0FmKUHK`xtKuz6I2zmyqryn>r?u!~QHIg18V14A zQPXw!SW9u;Dk?So_Kof;8t#a<wAp_6uLCzj9=PW_QHqX;e$<b%6-=R4`a_NQx+*^* zY$v-spdgdNGU3pGvYb%TI}@Pr!HnYAJR(#!DB^r5Gu$)R2A{eE1U7wq71Tn!<-5C4 zua4ww5~12;H8j?KE)(%ge7+G;URT%>{-`51u;FnP0e<rfq4j-&PgEgHZFn&pJ5OuR zKQb<R8YStzAX-5v1VAs47hX)fq#aL7Ucgn^YE6&`c9#;c5DGmst^?m~fs=$v7dsea zKBLY9U;KStyvG?M3mPWLD4gw5Lu^!B8&jN=(A{Kk$hkvdXs?1}!vN*i*VhaGWqt9o z^6jhrH+TO~`u*aY!@sCkbH9#`owxQ5%#^2Ka(W6%<ve_o&q0ZlVZ2y?nQRI2#Ue~( zr(it)HB4rUkhP0w&%y-eOq4CimQTS<-iGOuC77B%4Y~YDxYKI2A|W`=<IU~0-GS{6 I{BJw(56L*isQ>@~ literal 0 HcmV?d00001 diff --git a/src/main/resources/jace/data/woz_figure.gif b/src/main/resources/jace/data/woz_figure.gif new file mode 100644 index 0000000000000000000000000000000000000000..dc4fa817db2ae5784be60e7d6c99142e075ea4ca GIT binary patch literal 3252 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 Z0``Lz`<Z&cbRPFjdC=2E3V{Ft{{^v7VT=F( literal 0 HcmV?d00001 diff --git a/src/main/resources/styles/Styles.css b/src/main/resources/styles/Styles.css new file mode 100644 index 0000000..3ab643a --- /dev/null +++ b/src/main/resources/styles/Styles.css @@ -0,0 +1,3 @@ +.button { + -fx-font-weight: bold; +} diff --git a/target/classes/.netbeans_automatic_build b/target/classes/.netbeans_automatic_build new file mode 100644 index 0000000..e69de29 diff --git a/target/classes/fxml/JaceUI.fxml b/target/classes/fxml/JaceUI.fxml new file mode 100644 index 0000000..c6b866e --- /dev/null +++ b/target/classes/fxml/JaceUI.fxml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.canvas.*?> +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.scene.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + +<AnchorPane id="AnchorPane" prefHeight="384.0" prefWidth="560.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="jace.JaceUIController"> + <children> + <Canvas fx:id="displayCanvas" height="384.0" width="560.0" /> + <Region fx:id="notificationRegion" prefHeight="50.0" prefWidth="510.0" AnchorPane.bottomAnchor="5.0" AnchorPane.rightAnchor="5.0" /> + </children> +</AnchorPane> diff --git a/target/classes/jace/data/35_floppy.png b/target/classes/jace/data/35_floppy.png new file mode 100644 index 0000000000000000000000000000000000000000..54188b4976b1f38b80019c3527bf64186d0a233c GIT binary patch literal 1132 zcmV-y1e5!TP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS0000PbVXQnQ*UN;cVTj60C#tHE@^ISb7Ns}WiD@WXPfRk8UO$T{z*hZR9J<@ zmQQONM;M2n*<G!)n^+i(YGV^9h4j)>AkafkhMscEHN}`7@(FS@{s;-=&sQ+{1|cD7 zAUXR~9}K||2##Y6(c02#S39#a(}S~v)>=2FR+<+E#30T4%<uQk`$|}A`JUl@*X%$5 zAcW}jdcD8b*VnHD_V(@Dqh7Dqnq{K*@81_MU%u=bV|e)R;a_E0{tmzw3x2$H>z4h$ zVs37340HkCS@5lE!CH$krpn_1zX8iZ^#!FAgTVl;HQjC(&zo&|NGTZ(hlF8>wN?N> z0FeXGS~D07D2jqOj%l@KS^_Ag==b}?aa`MX07X%7a&khJWh^Z%5d^{1A(sg$B}tN? zl)@M@4WN`FNfNRwLu)<h*G!^P;Ox0IfaBw1(ln(kOSINAOAe<BeERf>r%#_ErJM$+ zBpMEfXss#Ba;5-jnzFgM$^86$ZNH{Kzu&JZFe?j+qF`@tkAs5)q9|$xkW%vb^Jhk* z5n5}MQkPCFrDQxFV~lZ@N-3LbAWc(tcXw&G+mvPbp8>SiNGVZDH7ZyXMKi!~ION&0 zXJlE%y?ghFq6pvj@qNG6lqP>{t#utvYyGdb2_c*UlTm9#RTyI!kH;6k_66dr0LB>C zAZGmp0r#rT)_~Ji2;roD@ZbSbN(Xh>m1P<C@85S9twlACTdkJubUGIJrYnjfD}+$M zI`Ffz!1Fv>tyZJ=p64+)H+Lbal~Pj{PBykM3~M=R08Inv?%lgQdGZ9$^IWBCt?_-| zNnFj2D$(oLuX+CbdF`Bqg#{ixdPEq8wSAY-_x;8Zyt=xIF@_f}Ua+yT0YInI;nk~G z1VMnc79oUN71-9+R_&a%wKV|R?KVOPSBPH)I5|1t-Me>u{P>ZbogDzKUcCyOHWj55 zN-5WTq?8;U9!{N;WtjtLRNy=dq?Dv-TJyEKL7wN7W$9XOwT_RE&jkp>(DekEc!uXG zAf<HkVG^Lz>EQc5QcChXM@s2FH?{FefJOz*15ip$DFCOH9Yv9woJCR8z%B~l6!@A2 zXB$Y8Bt%hETS5qoF*S*+$n)F*G+7|avSfLAxlw`9Xyk4Hr`>EY7+|d>j$;Q9$1y<= zO!fOo1*%t2MF3!}JrF`{<$3<cojZ4G9<Hpckfte%i;Fyd{FrXHOB}}-V@T7K`T6;p z6=hjAT5#>!HE!Iv!OF@CZ{EDoHGnVi=i9e$6W~`M;k0#<rs?9%n>Vi?9UX}<3`vrN zcDv2~{yyz?n=lLsf&itIdl4T#d`PZbxpFWGu)V!4N25{oe{84P7iZ_U5aK7`7a#_T ysjFN*JRkxTu-ydU0X;wf`8NRYIlY_#`u_mGxKpLuGFQ9+0000<MNUMnLSTZ1#0UNW literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/525_floppy.png b/target/classes/jace/data/525_floppy.png new file mode 100644 index 0000000000000000000000000000000000000000..09558f61ea35990fe29d4ffc7b185b5d6a927227 GIT binary patch literal 881 zcmV-%1CIQOP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS0000PbVXQnQ*UN;cVTj60C#tHE@^ISb7Ns}WiD@WXPfRk8UO$T14%?dR9J<@ zm(NQZK@`Wov$HY3WI;%60#f!Mwn|A4QcwxG<m|oGL(kqlc<~?5KSDeSR&TutJ>(!1 z5?TV%oXo*!YYa=MG2`xL_w|q;%`Y{x)%b<E4Ey$d=FOY;W*8CSIi)<ScK|8?FvbFb zK;TC_9)JDR4qewpHk<wGx^4;p7!iRnrt7+1*xud-=X{_cW9;4^*X#A6Ay!saY}>XY zMD$1PBqJg$FE2mntXDz^92^|Tm@oig0RI3S$AJ(6UXqfCP_0&_?SN(u0F4OLYBi}) zw&XaD6hP-9`v7d)esX|L^#NFxCA|%<>po5dgb*-IQyPFVHWWauRzo_ShT}N>0FL9} z;^G2^VIUX`N&%dopFaptEEbW;WMG<RH+-uOXqr|?Bob1Ndo}3z*6VegoSdLoEOt!; zctu3Ny0&1LFfzuVX&R!@D8k_|s?{nAg+kxyv27crQVC~gXML;gR2#suER;$m1cN~+ z%H1s@7K<Ssk3-Y6a-~u!Oixe0o12>(J3KtZ&CLxel?n{Q!1eX@P`T6vkk9AA7(*Zs zXb;XgR8@tdC{R@ui;Iikod4Y1-27l!R(@@5?aloB{P(G;DP@0uUpj%+-U%Xt5CTGo zzK=tp5GE!jaCLR{#Wc-ab2_)XyL&u2IhpWMMWxztt5VL)%)D*Q?d<HlP!#2Txm=cZ z4s@IX0EJ-;lF1}iS688`>M`g1o1!QmZ*Om-8yg!i3}aZo{ik49rPFD+u8UMEwG@d& zmQGJkvA4G;<GfqVebWKZ2!{<IMnvAPWZ`fau~^Kjd0U<U#>dC8zP^sFtt}*z$<YJM z&dx&Db%a78EG#UH9)NQWzuyneIeb3f=m8jG;GBbVes9?x34rVc_E7+A+itJk%N1@A zz}@|FfO5GUE-x=(Sr(3tj{XY(MARxqpCM697-O!_=c~)ubPN;CLeyD`KcP{f;s6Fe zk9xp8hMxdr(dY+_X4Dd6%n#rtfH5zBmXHAOn}{6t+|TpB%G<Qm7HZ~&00000NkvXX Hu0mjfa!-ZM literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/6502_functional_test.bin b/target/classes/jace/data/6502_functional_test.bin new file mode 100644 index 0000000000000000000000000000000000000000..f666e06740c5fbebfb6805dcb618f9d2989ca48b GIT binary patch literal 65536 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/DiskII.rom b/target/classes/jace/data/DiskII.rom new file mode 100644 index 0000000000000000000000000000000000000000..74c04b137a9d153e3e466b85080dfcb29f54f09a GIT binary patch literal 256 zcmZ3auz+C^bDK>Umx|2?0ahE&|C)6hIJ_?Wo-sF!`NRu>rwS4OckN|h<N|_L?NxjG z4(#nYu(#{L-p&IH0`@i>n4!tc#R?Q}Jg_osCBv%@f!D2St+uU>D>+)#B{+a;1%7*8 zz3?5(IJxS==T{Kcv=5vb6YhJi{h)f`<^twc2e7DCs|}b|wduI<M&qdMg?&pMjyhi0 zv0?jyuojy=AhRN`F)gv_n90C=;T^KbM70wa-oq4JXa{;J?3BUpjcRcW%vvA}l7IJ1 W{h94j+p`QD%d{ul_F?2;0097PfqNDJ literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/RAMFactor14.rom b/target/classes/jace/data/RAMFactor14.rom new file mode 100644 index 0000000000000000000000000000000000000000..50ce4ba3b29379986c118976966be9116eb8745c GIT binary patch literal 8192 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 aDLn(5V8=5S+dlt6*y3Mg^dmFl|NjL6jKY}! literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/SSC.rom b/target/classes/jace/data/SSC.rom new file mode 100644 index 0000000000000000000000000000000000000000..2c94c9b22d7becade3e41d619380cf7ce7c30ea7 GIT binary patch literal 2048 zcmYLJZEPIX6`kFkZ~QUPCfY`7@`N^?oi(bA6gyLdi<Ll_U6#Zx5lZ?4O;uG)Nh|qh zQCn=<47C>-O&X#)Y`0w!Z`fzicK8u>HtmuL#bsu_Hji3rC6XpCK_P%318G(c{_1^e zr0$P-Gk4#;@11kcxu_W8tp;XF9$WGfB^{nzN#{!*U;Bk`_vUgAYia9-S9{jaxkAqE z!Z9+K!wENz#~|mPLmQP|4P_A|kI1MP{Bc=;3T6c|wL~?7<WZw`CWMmHJ(hODz*0%) ztdcHNdJU~S9M{Is%k${jc}wdUj$=laW$BKS1-~}#o2VF~nLrzTby;{FN}`R1%b|Ok zPRm5w8&=htsi<b8)jeL}p73~*Lf>jS+v;9I^P{ec8ZJ3v-D9O8xMeybWDfG3OX$vK zT8!RB?}T|0plx&RpDU_S*9FZdTPvw(Pbfxgs1R9-20cFcgOr_u`m?D>!(}L^3$n5U zkJ~E17DqZhf@RnP#Q7T`Ivb)Rb9pPYJkeGSWX(au%1+9J11@P9R_Uzkd?Y5<JRVOv z@1{qlp<v(Ezk0mf*ELmU^fjFimz~59)#}ky-V0t>7LNAN&Tyt?3?H>sO33%;@~4A} znib`U&3;Ox%K$$CCzzq{uB^jK!HCgTIB#0@UnHzFMtJ|%gwsZW2(thL%l2l~k?u0{ z+Wk;A-bCkCozG45kA|}X%$J#C_tVG?livDX95?VNdT)L@^;+6iv9R+1QKnP)$~$Qi zuc*h;*A84e02})xAlJf^wn|&&bifzTj7~T6y$#x$zmL4$Ch%^nT2cL2gxp<Wk?_Hk z0UN>#ju>8aSqa(d6lIUbiqU(a>&W3>T)tY98n#+yAW|fkVGWDF{r$yDS5{Wn);CTy z8e~oXFh*L?3v;@sHsKo@%H`~SOY06rht&i(Ug_uhBTfRLa+_PUkx*$v-U5)Nv9Lg- zDkTq*#FTC(=_acA0f43n5r<aib+e*sqyN@9!mWF({w390C|V^v9(7S6<#Lt-9^#WG zXg54YZo>=6>@)Pc=4L@>f6jTNEjl)Q67qiJ-W?vXjBHUGB}w;d^t2t)*k)H|g6wnT z6ENjvowaP0s!65-?~ABYp>O$*=HMw;A?PQgj$F?FYQ)2H8`-HPqRwh<eXfcTdb2T3 zmMf|pZX=*U;VDPJ1zXJ@nKNVJ5W3YMD^t=0bmip6dDECq(WYAA3DN;BLN1T4G!I{F zLR*uR9yXHx!1xL~srb5iZ9j=$I{>EioSV?^O>zdmWUjv~*N#IT^z(Gns*Wq4;TZbq z0@~G7R>JY<KY;Oc6Ub>3Os-SqfT48r+}{=#%d<^FE=<CK%2(*A@u-t-(<8oM;6E8A z+zDE?<nV_VoB7rn$mcf5=hoqWHJu1CONe0@pJA4KASRQ^z$AiMvFAzu;KOJ3{_4!P zANIcU)~nzB;AYlYe8jo((B83qSK0j4*rBW9!CwwNws+{!tBBdt`q^;nr3YKZeXV`@ z*3jP8eFy)!Yrk{-vBymYKTsBkg5NH4M8-GE3Buyla%|8`*qOS{YKi1inQ^kpWlCPc z)CB6dj6B;DbyEm$(J%|P)_GIwCIS^1{3ONB;3d1a_PoEdm&=@d-Z#goS0NSeARlZ8 zy}8QC2_GF_M-%IybBH9L0Y}#gz9DtM$iyE)pDlPXaT<!_v9Id_;qdyPE|he^4AV{* z4L?9W-L7-`GdgDiDcWd=h~y`tn2A~P<FJ$5g`QsU1^PV?dS;F39@Fl}3r4MP!Kn0` zuk8NbMB$Z*3F-p=vd8A*&9Kku4DWL#ts6v%<D@MbnDY6j7r>~GX8S_f?JUbb*V%FM z1#F47l)pg+Fk@!`2Bl=VO^FHaWBmss$$yC4*}i*rWApBqCySUGm)xp-+>HJ^3q}rt zfP5anr=0=E3U$zkLOzBimrG?_R$1zc4_~Te3{3#Z1s41%z-Bm|;(|B81;?3hfZ|3v zbSLQz^Rathc=0zc+3Ius1!Kw(e=@o8HafI%uFk|JmxY;jFM6u!VGZbeLg#>YZ3pms z198FoyU86`m>q~Xdm!nE?|+lr#+<FwOT|Sekv|^*OhZ<>&h%GQ7(q<jRsp-Eod+qT zBc4w6vq63}KumCoG4sVBKNIF)+`A&UGvbp@0%mjF<7UiLiL(PnL-~SIr}K7uI_XqD rzP-C(z+92W&?jp!EUf=u8h}&=p!cU3>%_qICDvJnZ1Vyk%~|n(5UGK5 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/apple2e.rom b/target/classes/jace/data/apple2e.rom new file mode 100644 index 0000000000000000000000000000000000000000..28e95a953ba0823587c9830ca4951b2277cc1f9e GIT binary patch literal 20480 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 R?++b%ThXI<<=(wr{|B0iA{GDu literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/apple2e_debug.rom b/target/classes/jace/data/apple2e_debug.rom new file mode 100644 index 0000000000000000000000000000000000000000..367cc6c1424a4dfecb6904b4f9f3e466c9568830 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/apple2plus.rom b/target/classes/jace/data/apple2plus.rom new file mode 100644 index 0000000000000000000000000000000000000000..91f1d263dd48a8bee8f2548b3c2bd23f7192b36a GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope0.png b/target/classes/jace/data/ayenvelope0.png new file mode 100644 index 0000000000000000000000000000000000000000..30f0f2ece3b200cb775a2d9a6d9180a1978a0e58 GIT binary patch literal 747 zcmV<H0u=p;P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G4>r%3iC6#t02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00Kiv zL_t(o!{ye^Y8z)1#_``fA5mfpf5a#srcig$Lg^*sI<hXI;Lt^ONeV5lET_9}vdJQW z5<-?;^<r`bAt4Yel2XaCG@g08m=U%q-P`KBnui(A%=w@5oHK#PUz2SU*2zm|16m(= zGvv=tmi#279g?qN{OvEwZ%Mylvcq>9?5_V00JJvAZbFO)zY}&?{Q}#pZ7w^6grbF2 z2%-Br#)B^94cwxAbJ-yz<jENsL+CxLJHd6Px5zuo3?U(_aMI`_;!z0uzr@&mPQFQU zdzm35Oc5IZ<(O(f7>2O-V~kH;l5fzuv8)i1di7PY(G%i;lRgF^?A?p8`wCmj3Ze0T zS~YTxzp46UAISzG^uAwa2+4UFO0pSo#PB0NPz=vQNN(5LVS9@Qoy!g&qlhGBGXqM} zg6T0wLpCN<70LyK5ccj-WaO)Cjrgg<wd#D>qbA-;dNI;md=IRYj4Rqx-XCWuNau|e z=JiIna?I1W>|du?*dwiH!jqRF^zL)AhACNr^85=|E?AxOczRtFv}YKx*3Zai7&mvv zby8JP%l%L6maHL1B81)#F?Rn5;nzo`=_PS`Q=l}~1vjQRq%EsS6o(C+L-JAG9*gs0 z#I92aLI}P4F&;d>?EVko=R1LiJG8f$wP?*4_xU`i6F+-)*X!w@cj|3$(ji;HHOg@M z+(qn)@;^Etf0xzCmUGf89M73d*q;77$cqtoQ@%tjxH@Xgo>QGkIt>>Oz~VorQUl0m db*GQ}{{TD0;Dq%N6nFpt002ovPDHLkV1l6?NooK9 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope10.png b/target/classes/jace/data/ayenvelope10.png new file mode 100644 index 0000000000000000000000000000000000000000..4bcb11fe55e61e6b3d307a86a663d3f82fb29cfc GIT binary patch literal 755 zcmV<P0u23$P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5h<3@yK(>k02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00K)% zL_t(o!_}8PPuoxwhM(i$7!n{~e6?lnFX(UR)Rlz=RVb`X9aCc^>ehjk3L%#6Ed3Fc z`WreRK_Lp{gC=pD<aD@DprTQ1D&pS7^0m)%Umc(G-fMxKWgrLs95@!x2I_FqguMol z$5saBb1*1CCIf*3whxU4WX|Klw}2#&R{ii5f@SC}14{<Jm3f>vdl6DEfK}DX7hu)g z`zvwbThJJQZ;5zsR4Yf2Tm)7OJc`J>;;d(2FQ~eQBJba+x`$vdT+hdXZ$UqR;ZNxK zBHp^H`xz*JvlL$BpW-C1>h6np+mPsqc-yM(K1e=V68=5kf<gyoT9CX1nj+qY$z<3h z+!g06ft^(?ABdES(60ei=+{I_Mb+{F*x5Mn<zYAlQ&!#vPy_!Qf+{pGA2ybe@eQB_ zeixi+U=GN{g)d_+G6FIxE~tZj2JH(-b)h{Csk|{;I~xbSb-U1B!#oVC=4EPsZ-=$K ziCN2!PTKGkuFl|l4R(&r_3F&bPm+Jz4ic{1;{Qd%R<ev|IR8&ihfRWv@47dTQW5CD z)DZd>Fr!-jG`_e=^KUr(0L~@+zFR&p`>q+P?ygAbE$D^my>Y}%f`2aw>(Hyh;KUjY zxND5VQ_u?$Z&TIXg+bDY^ERW2dusk~W4}J^B^|KR67l$%y>DDm#7%-fTYz2;B&Aw8 z67gP1c*sM4Ux`|DaZkyAJ`eRQBvOzZg40Tj5BTQ8YYX~AX!jxAg?bC7>M+}ck98h3 lo`U~UU{PTZfOf&|`~pe9t%*u7pM?Mb002ovPDHLkV1gI$N$~&x literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope11.png b/target/classes/jace/data/ayenvelope11.png new file mode 100644 index 0000000000000000000000000000000000000000..932fa746a5ad91344a5199e0381e52ddd1035ff8 GIT binary patch literal 897 zcmV-{1AhF8P)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00001b5ch_0Itp) z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyr7 z5EeF6m>@s^000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0008qNkl<Z zSi{}d+lm}j6b9gL*P(k(!RZV+kbvl&@8GkD<R(G95)6tVGu;V-;I&AC5X2Yq5dzU9 zX3{g$ai&kzRojbQu{(q)c$2j2W*6*Lt7`x2U+Z5x(EkWmaE>Z6J0kmots(s*uUP*) zlgn&=h^x$dlsQc#Z@kg!Z2A08e*e|}Eq-0Mdl3~*sh(5*UNAdP{(g*iAB3=fla1%h zI;go`bAPlFX`B24uEuz9&zf5Z-+Wxq?cox{c<1{N_6KY?m{Z`o6%a}~JLshVgP(9l zeM&Ya)bHj|-^76Ktf1;rZl?ns{1W4xgT#b<%*Be+hJN0bLk9&hZzG@Ki2Nyw5ykZN zk5`d0yj=jA*sc-)<&1K~>9C+&sh=!{F!&)cVQ`~$ZjjO6P7J8>#0*mA^QU-7F=R4f zbEO8yE%Ft%^0x;-wZhD;Iwf^y!i#A^(Xd#&{A1!cg#N7<`}ab)eT^(808YkiZqUuB zTST*Q%KYLP-~GCdeIR3kxL9ViIVPNhaQm|u?>?a1#btIc5Z+_aB@0Vx#fpvTx;Da$ zvR>ARGvYCeA&x`1eJ#d=pIP)+Tp-&^a@lm2A<EhsTlT7UW^?>WGo(1AI1XX(X<~vN z#U7!T<dDRKD*qn<@V7G76f@$8lS4eEI!R2p`y+9etVh1n`WI|ka{#Ki)XBO|EqgL$ z_XLMDPg*A2h_U}IVGCRB`vr?^-5F4k&)ZU%V?k$1{}E28S|&IOVgF`~2cOfd=z7(x zIRND>O-8iqgr;g||07-;^Q;9Se-^_2AjZKb%cfb*($u)V3>c-ckaiuVA>pOnAGNCR zn6uxA4@0>1b=sR9aysI1wYK*GzWjilJ!W0HGfsyo>#tmb+MD3;BIhm<_6Q~M1@#j) zhHc5LofYY@^_;xJbk5lsS0=B!_K)~tgHD+kUv^W!D7#Ge`0CNSs|e$j=dHHVcktL> X)}cC$Jm=Ve00000NkvXXu0mjfAIqlg literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope13.png b/target/classes/jace/data/ayenvelope13.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3f906027eb7253791304faeeba1bc5bf709c66 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^4nQox!3HFkJ+IURQY`6?zK#qG8~eHcB(eheY)Rhk zE)4%caKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmPar<8z@#dV((&Ojl_64!_l z=ltB<)VvY~=c3falGGH1^30M91$R&1fbd2>aRvs)5KkA!kch)ir`Y;21&Xxp=Qj3f z>Ui`+_+@NsbOEc`F}Lsqt(|^qdIt`1Nhdw#x-+R&>7o0FlQwPHhQSF>FEGA0(civ9 z|K~Z&olc6eIgWc7|H)0^J9wWpYeLw9n6#Hum)(pGJHVv8Vg7^tCzC!n3R?v9My+Xd z$?MyDbdq{VZ<JDb`09y&>UlZTf@I>#Lf+q2`l#w7e4ruf!{w*u2VVVQ_{Sn~V8v_E z6@TqEtd-j8ns?r@{lWLm{5_2I-^#yRn=jer!n?6pF*I1>?1Hp}ACE;F_(K<`PyE5N zh5w)Rg~oT{Hf3S|_(a;5b5G{qAS?0hLFNXr_6w;$ESc}LoaZeNmpjm$Cd<M2`~jm^ zf|+6_L+SMgpV_9EEf8Aarmyr-)1ZXa&Rk)}O1Je&A30ToSA0287Ta*X|G`(mkk>ae wcvNa7`hk4ugkK5AZ(3{>kgAY)*ipwghxItK#rrRkz(`>5boFyt=akR{0C5h-6#xJL literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope14.png b/target/classes/jace/data/ayenvelope14.png new file mode 100644 index 0000000000000000000000000000000000000000..12b8ec08149c7ff231b4e1ac8e3f364ac27aa058 GIT binary patch literal 787 zcmV+u1MK{XP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5(XA6)#v~K02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00L`C zL_t(o!`+w5PTNorhQG1nBo2g-ge0_uC+Mpnb;YI|gizVC>aNtWK;5$ip^8oS-Si=< zzC#yCT!M0gki-e`ba8?zL@RKjV$ma6mS@g<`e%Hu!lT<}4(9SODnc#?!xT6HwA*0U z;j#()Z6<C{!GAdit$DCBkPIPpWknO`lpym8SW?~n1}vFxr)2z_x#^Ps*L=#5sX)4- z`sq6`V?`5Zy#QxH)jJaT_+HgJ0%u`tpHSpe@>zsrNp<(Dh`$1XWknMw1y%1*#NPsY zAmVSSdWRr|$wWRSKUD^bs@@k7|1Asx2(Kk#@R7*&JD>-N5N<5MRoyud@z;Pl1T`35 zJUnOP;@@&O18Oosa2vRSpbx1ukOgvg;SY(7m47E*q6H6I;G9FZ0hvB@(~v2^umaBf z<3z^EzaM8st6X>ropbnEhw8C;cg~HSx`@AK7A}e={&vo;77Ty25HJ6J(PYxf{VyX% zrc3_$uNeZ>SzvrfU@>~~&wJOfpzcC13xhl`H(Byv{FhDBKd8a5C9=J)x^n=DB2a=v z(TLk#H^Vo6z6A*ftQknAejgH|CjTdq;VEcC#NSZ$_F$A$_4Y*k4bX=1$Iln2E(2v% z?}x~TH=tceHlZ`c1*BSl+B}p?&|3s2ue$Sj{AZfro<Z#b$}Oli;^YfdD`pJ{vXHWX zF3^IL%PDVP%aG5)r~p<DBmsH@VH@lQG_PR)IBtFd>LH~1U|T>4T$|@_$*;Qb+J$x+ za%t$=#-6e7_G>xdCZPi)0+0}N2m`=%A+Vu3GkF1`^1Gw4&JlD%nC-zy{}-8})+o<l R!-D_-002ovPDHLkV1hV9ROJ8w literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope4.png b/target/classes/jace/data/ayenvelope4.png new file mode 100644 index 0000000000000000000000000000000000000000..4245e30741a48fc3cda29b8be299504c6481fd19 GIT binary patch literal 810 zcmV+_1J(SAP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5FFWgP7nY902y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00M$Z zL_t(o!`;`<ZW~n;2JqjVKX#Kiah!-Ggf7|zNQfm%9)cAcHV6q-;sw}1WhX*nQMN4T z1|cN&m3S3iAyQl1{I}!Gj4z9s$fi}RED*&$(r89ApXT20yZ4;$+~C&npv(RS){vvX zWql1^SkTCB2m5S`{Y@r&w0jgO@s#tYxU#^J+-~JR+T`*c=?7FF#`ygclKXVF$nqr? z;6ay-9`;$gOSl)~;iG2r?AE)XduK@nxFO%!#U9m8jEDUY`oA@RHHtP`ODMn%`TQNm zd)SKc@KFc{Kas>4E>A3YByT34yi4ATvHx=jgKvo=j2d7`HeGZ9ve%Pu?_e{={x2as z{F*GGiU>6}mW&%mTnYhRA-^4C|JM*6eu*(9ObP88KW$QBoGgI=uOol(1=%UixlU-c zsj4P>0x>7F7F>Wq2OHSNOXQ0a@~51PSh+*FP1*=BT_x+1?J)S5@tBplnlP$n&#UR% zrTLG!R9sByj5)f>7_P9oPTHfjF(Yrt^jUo$6|~n$irJ6SPHm4+_E>qTBV0uh$-~=U z=X}n&ih|Arr)&(fx|2)RXtiVP{~p4@=Qv~XjN&=%GakR-oA=2lsAhctR4vlc-23@~ z|1W0O%q@-08RmlGFFG>1DW);@{|Mo`Piw1d9a9WBJLbpJW<{^KzNo(uWB;o~x4;m7 z_#`mcMUTlk?WFPf6OPV!d^&><wm5l@wfls9La(kNNmV;S`i7pMnsbWu7QMs2jj!f* zXL?OJB74r{C}TLKeMOqoHg|KxohJ`}j=4J|Z`FC^xE7!)DJ#<A)q#t&?iSOWvYBLA o5)1O`zr)Yh;Nm6?&Y52R4Q;3Lz%5DiwEzGB07*qoM6N<$f)3AetpET3 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ayenvelope8.png b/target/classes/jace/data/ayenvelope8.png new file mode 100644 index 0000000000000000000000000000000000000000..e653b1d8c62214055880bde63a331673ae359ac1 GIT binary patch literal 966 zcmV;%13CPOP)<h;3K|Lk000e1NJLTq002M$000mO1^@s6rssJn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+zA>G5du1GLNWjV02y>eSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00SOL zL_t(o!`0VKZ(Btahw<NBzviuR?4&PIS}KGpBzAn4E)ik_LWl*6u237NRRt1jgkXaZ z5@N|0K<byEYY-^Vw2mE`+Od6ohsE5GSnY-lt!H<xp8H=sI&)?w(0vozSYsL)ACf&{ zxkvZV-1>Nn<t>(W$T!lrNuTV1W{>Xg^L+OzCmkwRn6;=DFPHyhn|h0-HtKkmF>aGz zi*ffSvKuttAYMg__t)nGsx@Y9^6eP!{YY`0$`08ky_F>B{MtvpN^vE|&whCMJY*}3 zIx+74%=A)5V}t4?i1F?hgzK#Aq^_uT<|V8*SzSNl?|)A98rcq=O=fG{Y@hMlwZu2J zUJbrdr?JLTC)KshjA;Wa5W?>FG2Y!}b`h7kxW$DHx~Cy^EIrSM%`^VB3$HGp)k=P~ z{C_p0mYr*l7{cC{sS~tQNNp=^hwfF>$SaG>PnxJQt0#U|&(h$KmkC4SFof=>G46hy z23>XqWyrWmHDgvR9zSTY)J)PUrL;;VV}w`=m>pB}34;)Be;DJvZ>hDCNn3>VGN4#I zzE$#hgZOXPWYlI9v8<mF`{X_JL%983jJw|@llc}|hkQA;5mZKt$6KQ~-2z#;|4$8O z6FjBpQGHCcAHv?d$)t^RDufMYOQo=!>TvP+G&IRVHh)Hj8PzfIh{0q0#dMHN-usH! zBHN&{dTJ2U#9Kf<RI_yZo6X9ASqd1P&>EzW;?L6L2QhZ<6PB@Dc1}iK&^W;Y@~Wk= z%A^dKq&BLbl-I}s)6%5F5boTHarYyNaPD?kN?S}mqc+BISs!CWF)i=fhy2~=pbV)T zgm7ms#=UpWhNclFi^^v>V)`W2(<iB39i18d=BS*i`%Hc#{u;uaTjk7o#JJDV$&2}C zfBZN8|IZI}UuWD&dm|2M^w{sEAJ4CwZQg1zzMS3<(@GjP`TmPvj@>rH4#g!J?Q~St z2IULp;CcS#<X5;}XESFqp%_v-W^K%a$$Y@W5jP65A;nXgeMUW22K+MOVLs1481Z(6 o)`a6Jm8VSm43B6IUzPlS0HIKBo0v$(9{>OV07*qoM6N<$f~)GnZvX%Q literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/clock.png b/target/classes/jace/data/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..b0735bd86c3f21fac4bfed3ba7d8e4833b8989dc GIT binary patch literal 4657 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/clock_fix.png b/target/classes/jace/data/clock_fix.png new file mode 100644 index 0000000000000000000000000000000000000000..b75926d221e53823c1c7c3e4bfd46cc0f37eb6a3 GIT binary patch literal 5595 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/disk_ii.png b/target/classes/jace/data/disk_ii.png new file mode 100644 index 0000000000000000000000000000000000000000..89b79915213da9c373894a3c420fb857e5716129 GIT binary patch literal 13149 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 DuJ!(B literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/drive-harddisk.png b/target/classes/jace/data/drive-harddisk.png new file mode 100644 index 0000000000000000000000000000000000000000..68351d1b692cfc210fd6bbf3f3751f6c67c8d18c GIT binary patch literal 3317 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/envelopes.xcf b/target/classes/jace/data/envelopes.xcf new file mode 100644 index 0000000000000000000000000000000000000000..8ff5dd8c0b386d318908a02652520b690f100696 GIT binary patch literal 13334 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/font.gif b/target/classes/jace/data/font.gif new file mode 100644 index 0000000000000000000000000000000000000000..d34a36001c3fe787b364067a1fa5e10bfc7820c0 GIT binary patch literal 4527 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 literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/harddrive.png b/target/classes/jace/data/harddrive.png new file mode 100644 index 0000000000000000000000000000000000000000..e732e9229da084156bc3c7c57ef02fafb8497c8e GIT binary patch literal 2183 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 JPDHLkV1gzBF75yT literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/input-mouse.png b/target/classes/jace/data/input-mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..ef449c39f6fb9b266923acd2e8057d9cd5fe1ff1 GIT binary patch literal 3497 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 X9NqU;9iOcG00000NkvXXu0mjf**uH- literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/network-wired.png b/target/classes/jace/data/network-wired.png new file mode 100644 index 0000000000000000000000000000000000000000..816a626fb0d831dfac172e895ded2ec7ee22e426 GIT binary patch literal 2661 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 T4UI&G00000NkvXXu0mjf=7AGZ literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/ram.png b/target/classes/jace/data/ram.png new file mode 100644 index 0000000000000000000000000000000000000000..21e2b3b58952b9082e02fb65584bc299ee09674a GIT binary patch literal 5520 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�H>ax&o14H89F$}ClRmed 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 S@>4ee0000<MNUMnLSTa2)sA@p literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/rom_image.png b/target/classes/jace/data/rom_image.png new file mode 100644 index 0000000000000000000000000000000000000000..83e176b36c02632832b6c3f0a14258024aee86f7 GIT binary patch literal 954 zcmV;r14aCaP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800009a7bBm000Gv z000Gv0c~iV`Tzg`8FWQhbW?9;ba!ELWdK2BZ(?O2No`?gWm08fWO;GPWjp`?14l_j zK~#9!>{m-q6HydC^XO}e?GPX#EeT2@ERE?3#h_c$#I?q?1``v10$aC4O>_eb<7;Pv z#+{Xb1UDLYxPSt|gml^tVqY_J=X&mK%d>Dh9TFFMo0FN|xsNm7`M!J30Q_%%xk;h{ z6Zh^HL!po^D>4q2_>4k655vRfVQ6Rwg25nUvum)tybM~ji!Vs==ejP~mSsMEG}V35 z`l`qQH4xAvng)uZ?CbjlrBWFRg#z}gu(PuRk|cwM<C={*P@<p<*B3%0?Jd}BJw3gU z%NbBCnjp&(I1Yiht{)f!6mg)g_Yh1@PR4VFkxe8LdVhaEeEs$vx)9o3Pxdv+HpG6Z z#WUu>?@|d;7cYQe7_hOiZca@d4Ts#ytAHBN&kml0(a}rb5*G+=<PvhkD0|Cep0PR9 zC2)Cc42DNWARN(JhC?~rk;!CgsZ{C#AtY>?McCZ@i4LhqE2wYSuAu`p%R*-yeESW^ z=MD4A($YiED!6lZf<{>gI%p*f!Lz4NBwr+C#6mP$$N7ZDP&hzJKMu6B&~bBpt6*+! zf$O^y1j^+yTphpSQw0qKTx7I2tIzYTf|Zq3Q1P_G7xJtE%eL@k6n`qXdFwWfL^SXi z0q$_6Qihi=o=c}dLQ+peUL1fr3a6t2;)2KhnFstd^sR#T?>-=b!qC^-2exe^tQAZ? zj!<F@foi1+>GXBKDj*yIcCFQ_kW40c1{A8GF-N2Vxdj5-wr!c&>?$lSe%|E(hf9`u zP9Q8n39Ko_h532-fyD|E((I@rN~dqoa5xOHSQm!C!CIgPYk?eCm}o>Qs8*{mJNwqJ z3hIysgCT@o;Tv-f5HSZRCH_@F8!BK&G21vEk2e@jgeq_e_3pr76}*1+hHu8;WHB{O zlm8d00As+$O;68I-xU!vg;&J@(WnN*AuX$*gVtUy5UYUh$%@AxiO7mgcF3G$P44(s z!T9(!ir)n=Fp%VjQ0z<$wzjtTy?_E>)oPIW^wFOR2ojIrH^KVaI{(ikDBQWp0jvjw zBpgf*NVemsc+4tzl^x<Jn2IHYQN|cb2>Ui(<LnTU+D`*qJ@~z_jkn#+E~e^+k$v`$ cw?6_500n6(+V|Qu?f?J)07*qoM6N<$g3ey14FCWD literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/thunderclock_plus.rom b/target/classes/jace/data/thunderclock_plus.rom new file mode 100644 index 0000000000000000000000000000000000000000..6c5c1cb8403fefe67bae17316dfe5baf3af76f05 GIT binary patch literal 2048 zcmeHF&ubJ{9N)~&>}01|l|;f;Ed5HX>`JJ6@W+5bx^*|c4U=Tp>{=BGd+6cSQx6_; zQFBm`g@qh?*ajAc(aJtabg2brgNq7<=FJ{_haN;wJa{lUl$Ta<5lZ@<iP%g3gXY2e zeBbxY_xt&L=lj0>bE(lYo8N0@t1s#G&ygyrljY4PuxFLhm0C|XaMk8?N@<fW##NH4 zFU{$9dV2Z_v3Gbs9Tn=UW71&#*GzL^*s?kM<l0d-+wfFoG)$F68m3117xFwmAkXkI zS>h+8%}1ogXXQD*PfqZIvgiUIMNIK8rR8S$Avul$q~oa=kj20qH2t|@uJr|kvtopr zYuOC&;az)gcWPZr?V^Tth%<ouKI;!)AL`>x70==w<q0{&LFUps;fXJmpfEkTa0H!8 zrcPMAVaD)GUYIfQF8COu=x%wt0*0SgD6JDpS&b}WVfg<YFZ$fn#lUB#E_MEui2gYL z2>WxJrYe*rKbG-<nHw2}MMC}!Waf#`?U<TW`G}ZxA`~OkRddZCbvFXHe2ROir?*W_ z9AtN)=~G7y?t*<?>g+gPi9eWA)0FmKUHK`xtKuz6I2zmyqryn>r?u!~QHIg18V14A zQPXw!SW9u;Dk?So_Kof;8t#a<wAp_6uLCzj9=PW_QHqX;e$<b%6-=R4`a_NQx+*^* zY$v-spdgdNGU3pGvYb%TI}@Pr!HnYAJR(#!DB^r5Gu$)R2A{eE1U7wq71Tn!<-5C4 zua4ww5~12;H8j?KE)(%ge7+G;URT%>{-`51u;FnP0e<rfq4j-&PgEgHZFn&pJ5OuR zKQb<R8YStzAX-5v1VAs47hX)fq#aL7Ucgn^YE6&`c9#;c5DGmst^?m~fs=$v7dsea zKBLY9U;KStyvG?M3mPWLD4gw5Lu^!B8&jN=(A{Kk$hkvdXs?1}!vN*i*VhaGWqt9o z^6jhrH+TO~`u*aY!@sCkbH9#`owxQ5%#^2Ka(W6%<ve_o&q0ZlVZ2y?nQRI2#Ue~( zr(it)HB4rUkhP0w&%y-eOq4CimQTS<-iGOuC77B%4Y~YDxYKI2A|W`=<IU~0-GS{6 I{BJw(56L*isQ>@~ literal 0 HcmV?d00001 diff --git a/target/classes/jace/data/woz_figure.gif b/target/classes/jace/data/woz_figure.gif new file mode 100644 index 0000000000000000000000000000000000000000..dc4fa817db2ae5784be60e7d6c99142e075ea4ca GIT binary patch literal 3252 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 Z0``Lz`<Z&cbRPFjdC=2E3V{Ft{{^v7VT=F( literal 0 HcmV?d00001 diff --git a/target/classes/styles/Styles.css b/target/classes/styles/Styles.css new file mode 100644 index 0000000..3ab643a --- /dev/null +++ b/target/classes/styles/Styles.css @@ -0,0 +1,3 @@ +.button { + -fx-font-weight: bold; +} diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..387ad6d --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Sun Sep 07 16:06:31 CDT 2014 +version=2.0-SNAPSHOT +groupId=org.badvision +artifactId=jace diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..9d79e28 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,348 @@ +jace/apple2e/Speaker$1.class +jace/cheat/MemorySpy.class +jace/apple2e/MOS65C02$RMBCommand.class +jace/cheat/MetaCheatForm$3.class +jace/library/MediaLibrary.class +jace/EmulatorUILogic.class +jace/hardware/mockingboard/R6522$1.class +jace/apple2e/MOS65C02$MODE.class +jace/config/DynamicSelectComponent$1.class +jace/apple2e/Apple2e$1.class +jace/ui/AbstractEmulatorFrame$1.class +jace/tracker/UserInterface$Note.class +jace/tracker/UserInterface$2.class +jace/config/DynamicSelection.class +jace/hardware/Joystick$5.class +jace/ui/OutlinedLabel.class +jace/core/KeyHandler.class +jace/hardware/massStorage/DiskNode$EntryType.class +jace/hardware/ProdosDriver.class +jace/config/ConfigurationPanel$1.class +jace/tracker/Command$CommandType.class +jace/library/MediaManagerUI$2.class +jace/cheat/MetaCheatForm$13.class +jace/hardware/CardMockingboard$1.class +jace/library/MediaConsumer.class +jace/state/State.class +jace/ui/MainFrame.class +jace/cheat/MetaCheatForm$9.class +jace/ui/DebuggerPanel$2.class +jace/config/InvokableAction.class +jace/core/Keyboard.class +jace/cheat/MemorySpy$5.class +jace/tracker/EditableLabel$2.class +jace/library/MediaManagerUI.class +jace/ui/EmulatorFrame$1.class +jace/core/Utility$RankingComparator.class +jace/hardware/CardHayesMicromodem.class +jace/apple2e/VideoDHGR$4.class +jace/core/RAMEvent$TYPE.class +jace/library/MediaLibraryUI$4.class +jace/ui/DebuggerPanel$8.class +jace/library/MediaLibraryUI$tocOptions$2.class +jace/hardware/ConsoleProbeSimple$1.class +jace/hardware/massStorage/IDisk.class +jace/hardware/PassportMidiInterface$TIMER_MODE.class +jace/core/Font.class +jace/tracker/UserInterface$1.class +jace/library/MediaLibraryUI$tocOptions$7.class +jace/apple2e/VideoDHGR$3.class +jace/library/MediaManagerUI$1.class +jace/apple2e/MOS65C02$OPCODE.class +jace/library/DriveIcon$1.class +jace/library/MediaLibraryUI$5.class +jace/core/Debugger.class +jace/cheat/MetaCheatForm$8.class +jace/config/Configuration$ConfigTreeModel.class +jace/core/Card$1.class +jace/ui/DebuggerPanel$9.class +jace/hardware/mockingboard/AY8910_old.class +jace/library/MediaLibraryUI$tocSortModel.class +jace/hardware/massStorage/MassStorageDrive.class +jace/apple2e/Speaker$2.class +jace/cheat/MetaCheatForm.class +jace/library/MediaEditUI$1.class +jace/cheat/PrinceOfPersiaCheats$5.class +.netbeans_automatic_build +jace/apple2e/SoftSwitches$3.class +jace/library/MediaLibraryUI$tocOptions$1.class +jace/core/TimedDevice.class +jace/state/StateManager.class +jace/apple2e/VideoDHGR$9.class +jace/apple2e/VideoNTSC$ModeStateChanges.class +jace/hardware/mockingboard/AY8910_old$PSG.class +jace/state/StateManager$1.class +jace/hardware/Joystick$4.class +jace/tracker/Row$Note.class +jace/core/SoundMixer.class +jace/tracker/Song.class +jace/config/Name.class +jace/ui/DebuggerPanel$3.class +jace/config/Reconfigurable.class +jace/ui/Library.class +jace/cheat/PrinceOfPersiaCheats$4.class +jace/core/Utility.class +jace/apple2e/SoftSwitches$2.class +jace/config/ConfigurationPanel.class +jace/library/TransferableMediaEntry.class +jace/library/MediaLibraryUI$3.class +jace/ui/DebuggerPanel$1.class +jace/hardware/mockingboard/SoundGenerator.class +jace/core/CPU.class +jace/cheat/MemorySpy$7.class +jace/tracker/Song$Format.class +jace/cheat/MetaCheatForm$14.class +jace/cheat/MetaCheats$2.class +jace/hardware/massStorage/SubNode.class +jace/ui/DebuggerPanel.class +jace/apple2e/MOS65C02$COMMAND.class +jace/cheat/MemorySpy$4.class +jace/core/VideoWriter.class +jace/hardware/mockingboard/AY8910_old$Reg.class +jace/core/PagedMemory.class +jace/tracker/UserInterface$Theme.class +jace/core/RAMEvent$SCOPE.class +jace/hardware/PassportMidiInterface$1.class +jace/tracker/EditableLabel$1.class +jace/hardware/massStorage/CardMassStorage.class +jace/library/MediaLibraryUI$tocOptions$6.class +jace/cheat/PrinceOfPersiaCheats$3.class +jace/hardware/PassportMidiInterface.class +jace/library/MediaConsumerParent.class +jace/state/StateValue.class +jace/hardware/CardSSC$1.class +jace/hardware/massStorage/DirectoryNode.class +jace/cheat/MetaCheatForm$2.class +jace/hardware/ConsoleProbe$KeyReader.class +jace/hardware/massStorage/FreespaceBitmap.class +jace/apple2e/MOS65C02$SMBCommand.class +jace/hardware/massStorage/DiskNode.class +jace/hardware/mockingboard/TimedGenerator.class +jace/cheat/MetaCheatForm$12.class +jace/hardware/mockingboard/PSG.class +jace/library/MediaLibraryUI.class +jace/apple2e/VideoNTSC.class +jace/hardware/FloppyDisk.class +jace/cheat/MetaCheatForm$5.class +jace/hardware/mockingboard/PSG$BusControl.class +jace/state/ObjectGraphNode$DirtyFlag.class +jace/hardware/CardMockingboard.class +jace/library/TocTreeModel.class +jace/ui/AbstractEmulatorFrame$2.class +jace/library/MediaCache$1.class +jace/config/FileComponent$1.class +jace/apple2e/VideoDHGR$15.class +jace/cheat/PrinceOfPersiaCheats$2.class +jace/Emulator.class +jace/core/Card.class +jace/library/MediaLibraryUI$1.class +jace/cheat/MetaCheats$1.class +jace/config/ClassSelection$2.class +jace/tracker/Row.class +jace/cheat/Cheats.class +jace/library/MediaEditUI$4.class +jace/tracker/Row$EnvelopeShape.class +jace/library/MediaLibraryUI$tocOptions$3.class +jace/apple2e/Apple2e.class +jace/cheat/MemorySpy$6.class +jace/hardware/PassportMidiInterface$PTMTimer.class +jace/hardware/DiskIIDrive$1.class +jace/library/DriveIcon.class +jace/EmulatorUILogic$2.class +jace/cheat/MetaCheatForm$11.class +jace/hardware/mockingboard/PSG$Reg.class +jace/library/MediaLibraryUI$2.class +jace/tracker/SongPersistor.class +jace/ui/AbstractEmulatorFrame$3.class +jace/cheat/MetaCheatForm$4.class +jace/apple2e/SoftSwitches$1.class +jace/config/ClassSelection$1.class +jace/hardware/CardExt80Col.class +jace/ui/EmulatorFrame$2.class +jace/library/MediaLibraryUI$tocOptions$4.class +jace/hardware/ConsoleProbe$ScreenReader.class +jace/config/FileComponent$2.class +jace/tracker/Pattern.class +jace/cheat/PrinceOfPersiaCheats$1.class +jace/hardware/Joystick$1.class +jace/library/MediaEditUI$5.class +jace/tracker/EditableLabel.class +jace/apple2e/VideoDHGR.class +jace/ui/MainFrame$1.class +jace/ui/AbstractEmulatorFrame$4.class +jace/hardware/CardRamworks$1.class +jace/apple2e/VideoDHGR$1.class +jace/cheat/MetaCheatForm$10.class +jace/core/SoftSwitch$3.class +jace/hardware/mockingboard/AY8910_old$1.class +jace/core/SoundMixer$1.class +jace/JaceApplication.class +jace/tracker/EditableLabel$2$1.class +jace/applesoft/Command$ByteOrToken.class +jace/cheat/MemorySpyGrid$7.class +jace/config/Configuration$ConfigNode.class +jace/state/Stateful.class +jace/cheat/MetaCheatForm$6.class +jace/hardware/Joystick.class +jace/EmulatorUILogic$1.class +jace/apple2e/SoftSwitches.class +jace/cheat/MemorySpyGrid$Modes.class +jace/cheat/MemorySpy$8.class +jace/hardware/mockingboard/EnvelopeGenerator.class +jace/core/Card$3.class +jace/core/Palette.class +jace/hardware/Joystick$2.class +jace/core/RAMListener.class +jace/apple2e/MOS65C02$BBRCommand.class +jace/apple2e/VideoDHGR$7.class +jace/library/TocTreeModel$1.class +jace/ui/DebuggerPanel$11.class +jace/cheat/MetaCheats$2$1.class +jace/state/ObjectGraphNode.class +jace/hardware/massStorage/DirectoryNode$1.class +jace/JaceUIController.class +jace/library/MediaEditUI$3.class +jace/core/RAM.class +jace/apple2e/softswitch/IntC8SoftSwitch$2.class +jace/apple2e/RAM128k.class +jace/library/MediaLibraryUI$7.class +jace/library/MediaLibraryUI$tocOptions$5.class +jace/ui/DebuggerPanel$5.class +jace/apple2e/softswitch/KeyboardSoftSwitch.class +jace/core/Device.class +jace/cheat/MetaCheats$3.class +jace/hardware/CardThunderclock.class +jace/config/ClassSelection$3.class +jace/library/DiskType.class +jace/cheat/MetaCheats$4.class +jace/hardware/CardDiskII.class +jace/hardware/Joystick$3.class +jace/cheat/MemorySpy$2.class +jace/cheat/MemorySpy$9.class +jace/hardware/CardSSC$2.class +jace/cheat/MemorySpyGrid$8.class +jace/core/Computer.class +jace/hardware/massStorage/FileNode$1.class +jace/apple2e/softswitch/IntC8SoftSwitch$1.class +jace/config/ConfigurableField.class +jace/core/Video.class +jace/hardware/mockingboard/NoiseGenerator.class +jace/Emulator$1.class +jace/applesoft/Command$TOKEN.class +jace/core/RAMEvent$VALUE.class +jace/apple2e/softswitch/Memory2SoftSwitch.class +jace/core/SoftSwitch$4.class +jace/cheat/MetaCheatForm$1.class +jace/cheat/MetaCheatForm$15.class +jace/library/TocTreeModel$2.class +jace/cheat/MemorySpyGrid$2.class +jace/apple2e/VideoDHGR$14.class +jace/ui/ScreenPanel.class +jace/library/MediaCache$2.class +jace/hardware/massStorage/FileNode.class +jace/hardware/massStorage/ProdosVirtualDisk.class +jace/apple2e/VideoDHGR$2.class +jace/apple2e/softswitch/VideoSoftSwitch.class +jace/applesoft/Program.class +jace/hardware/ProdosDriver$MLI_COMMAND_TYPE.class +jace/apple2e/VideoNTSC$1.class +jace/cheat/MemorySpy$3.class +jace/library/MediaEditUI$2.class +jace/hardware/mockingboard/R6522.class +jace/library/MediaCache.class +jace/Emulator$2.class +jace/apple2e/VideoNTSC$rgbMode.class +jace/ConvertDiskImage.class +jace/config/ClassSelection.class +jace/applesoft/Line.class +jace/cheat/MetaCheatForm$7.class +jace/ui/AbstractEmulatorFrame$Dialog.class +jace/core/Card$2.class +jace/tracker/Row$Channel.class +jace/apple2e/VideoDHGR$13.class +jace/cheat/MemorySpyGrid$1.class +jace/config/Configuration.class +jace/apple2e/softswitch/IntC8SoftSwitch.class +jace/apple2e/VideoNTSC$5.class +jace/hardware/CardRamFactor.class +jace/core/SoftSwitch.class +jace/hardware/ConsoleProbe.class +jace/cheat/MemorySpyGrid$3.class +jace/ui/DebuggerPanel$4.class +jace/library/MediaLibraryUI$6.class +jace/cheat/MemorySpyGrid.class +jace/tracker/Row$ChannelData.class +jace/apple2e/VideoDHGR$5.class +jace/library/MediaEntry.class +jace/hardware/massStorage/CardMassStorage$2.class +jace/ui/EmulatorFrame.class +jace/hardware/SmartportDriver.class +jace/hardware/mockingboard/R6522$Register.class +jace/ui/DebuggerPanel$10.class +jace/cheat/MemorySpy$1.class +jace/apple2e/VideoDHGR$8.class +jace/hardware/ConsoleProbeSimple.class +jace/tracker/UserInterface.class +jace/cheat/PrinceOfPersiaCheats.class +jace/core/Keyboard$1.class +jace/core/SoftSwitch$2.class +jace/apple2e/VideoDHGR$12.class +jace/apple2e/MOS65C02$BBSCommand.class +jace/hardware/DiskIIDrive.class +jace/hardware/mockingboard/PSG$1.class +jace/hardware/CardRamworks$BankType.class +jace/tracker/PlaybackEngine.class +jace/apple2e/VideoNTSC$2.class +jace/cheat/MemorySpyGrid$6.class +jace/apple2e/Speaker.class +jace/ui/ScreenCanvas.class +jace/config/IntegerComponent.class +jace/config/StringComponent.class +jace/tracker/Command.class +jace/hardware/ConsoleProbeSimple$KeyReader.class +jace/config/ConfigurationPanel$2.class +jace/library/MediaEntry$MediaFile.class +jace/library/DiskTransferHandler.class +jace/hardware/SmartportDriver$ERROR_CODE.class +jace/config/BooleanComponent.class +jace/library/MediaEditUI.class +jace/tracker/EditableLabel$cards.class +jace/apple2e/MOS65C02.class +jace/hardware/massStorage/LargeDisk.class +jace/tracker/Command$CommandScope.class +jace/apple2e/MOS65C02$MODE$1.class +jace/core/RAMEvent.class +jace/hardware/massStorage/CardMassStorage$1.class +jace/apple2e/VideoDHGR$11.class +jace/apple2e/VideoDHGR$6.class +jace/ui/DebuggerPanel$6.class +jace/apple2e/MOS65C02$CommandProcessor.class +jace/config/ConfigurationPanel$3.class +jace/hardware/CardSSC.class +jace/core/PagedMemory$Type.class +jace/apple2e/VideoNTSC$3.class +jace/library/MediaLibraryUI$tocOptions.class +jace/hardware/ConsoleProbe$1.class +jace/hardware/ProdosDriver$MLI_RETURN.class +jace/cheat/MemorySpyGrid$5.class +jace/cheat/MetaCheats.class +jace/hardware/massStorage/FileNode$FileType.class +jace/config/DynamicSelectComponent.class +jace/core/SoftSwitch$1.class +jace/hardware/CardRamworks.class +jace/library/MediaLibraryUI$8.class +jace/ui/DebuggerPanel$7.class +jace/cheat/MemorySpyGrid$4.class +jace/config/ISelection.class +jace/apple2e/MOS65C02$AddressCalculator.class +jace/library/MediaLibraryUI$9.class +jace/apple2e/softswitch/MemorySoftSwitch.class +jace/config/ConfigurationPanel$4.class +jace/core/Motherboard.class +jace/config/FileComponent.class +jace/apple2e/VideoDHGR$10.class +jace/hardware/ProdosDriver$1.class +jace/ui/AbstractEmulatorFrame.class +jace/hardware/CardAppleMouse.class +jace/applesoft/Command.class +jace/apple2e/VideoNTSC$4.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..783fd95 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,128 @@ +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/RAM128k.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/FileNode.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/MOS65C02.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaManagerUI.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/DiskNode.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/ClassSelection.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardSSC.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/SongPersistor.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/AY8910_old.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/state/Stateful.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/ScreenPanel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/Cheats.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Debugger.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/NoiseGenerator.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Motherboard.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Font.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardDiskII.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/TimedGenerator.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/ProdosDriver.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/Song.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/Library.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/Reconfigurable.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/KeyHandler.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/PagedMemory.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/JaceUIController.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaEditUI.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/applesoft/Line.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/Emulator.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/FreespaceBitmap.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/DynamicSelection.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/EmulatorUILogic.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/RAM.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/DiskIIDrive.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/SoundMixer.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/ScreenCanvas.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardThunderclock.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/PlaybackEngine.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/RAMListener.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/ProdosVirtualDisk.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/softswitch/MemorySoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/InvokableAction.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/softswitch/Memory2SoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/SoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/applesoft/Program.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/LargeDisk.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/MassStorageDrive.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/DebuggerPanel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/Speaker.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/softswitch/VideoSoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardAppleMouse.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/CardMassStorage.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/UserInterface.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/R6522.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/MemorySpy.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/SmartportDriver.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/TocTreeModel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Video.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/ConfigurationPanel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ConvertDiskImage.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaLibraryUI.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/BooleanComponent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaEntry.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardRamworks.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/ConsoleProbeSimple.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/ConsoleProbe.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/Apple2e.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/EmulatorFrame.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaConsumer.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/Configuration.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaLibrary.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/state/State.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/PassportMidiInterface.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/SoundGenerator.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/RAMEvent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/Joystick.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Palette.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/DiskTransferHandler.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/softswitch/IntC8SoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/ConfigurableField.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/softswitch/KeyboardSoftSwitch.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/StringComponent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/DynamicSelectComponent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardHayesMicromodem.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/Row.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/MetaCheatForm.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardExt80Col.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/VideoNTSC.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/MemorySpyGrid.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/Pattern.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/DriveIcon.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/VideoWriter.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/PSG.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/state/StateValue.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/SubNode.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/TransferableMediaEntry.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardMockingboard.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/SoftSwitches.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaConsumerParent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/Command.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/OutlinedLabel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Utility.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/DirectoryNode.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Keyboard.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/ISelection.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/mockingboard/EnvelopeGenerator.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/MetaCheats.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/CPU.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/TimedDevice.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/apple2e/VideoDHGR.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Computer.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/FloppyDisk.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/MainFrame.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/state/StateManager.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Device.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/ui/AbstractEmulatorFrame.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/JaceApplication.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/DiskType.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/FileComponent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/Name.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/core/Card.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/state/ObjectGraphNode.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/massStorage/IDisk.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/library/MediaCache.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/tracker/EditableLabel.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/cheat/PrinceOfPersiaCheats.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/applesoft/Command.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/config/IntegerComponent.java +/Users/blurry/Documents/dev/jace/src/main/java/jace/hardware/CardRamFactor.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst new file mode 100644 index 0000000..e69de29 diff --git a/target/test-classes/.netbeans_automatic_build b/target/test-classes/.netbeans_automatic_build new file mode 100644 index 0000000..e69de29