diff --git a/CLAUDE.md b/CLAUDE.md index ef3e7c2..ed54e3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,9 +31,7 @@ mvn -q exec:java -Dexec.mainClass="jace.JaceLauncher" -Dexec.args="--terminal" < bootdisk d1 /path/to/disk.po 7 run 5000000 screenshot /tmp/frame.png -m -C07F -b +mem C07F C07F qq EOF ``` @@ -91,23 +89,21 @@ the system is in the Disk ][ boot ROM at $C600, waiting for a floppy. To start Applesoft BASIC directly from the ROM, use the monitor: reset - m E000G - q run 2000000 **Critical**: `E000G` starts execution at $E000 asynchronously and returns -immediately to the monitor prompt `*`. BASIC cold-start has NOT finished yet. -You MUST run cycles after returning to main mode (`q`) to let BASIC initialize. +immediately to the main prompt. BASIC cold-start has NOT finished yet. +You MUST run cycles after `E000G` to let BASIC initialize. `run 2000000` gives enough cycles for BASIC to cold-start and reach its `]` prompt. After `run 2000000` completes, TXTTAB ($67-$68) should be `01 08` ($0801). -**Verify with `m` / `67.68` / `q` before calling `lbas`** — if TXTTAB is +**Verify with `67.68` before calling `lbas`** — if TXTTAB is wrong (e.g. pointing at screen memory $0400), `lbas` will overwrite screen RAM with the program text, causing garbled screen output and BASIC failures. To reach the warm-start (re-enter BASIC without reinitializing), use `FF69G` -followed by `q` and `run 500000`. +followed by `run 500000`. ### Complete Applesoft BASIC Test Workflow (No Disk) @@ -115,9 +111,7 @@ followed by `q` and `run 500000`. ``` reset -m E000G -q run 2000000 lbas /path/to/program.bas key "RUN\n" @@ -133,7 +127,7 @@ Notes: - **Using `PR#3`** to switch to 80-column mode goes through the 80-col firmware's keyboard input loop and screen output loop. This works correctly in headless terminal mode; it does NOT hang. -- `waitkey` and `type` (synchronized keyboard) do NOT work after `E000G` + `q` +- `waitkey` and `type` (synchronized keyboard) do NOT work after `E000G` because the emulator is paused; use `key` + `run N` or `key` + `expect` instead ### BASIC Variable Table Layout (Applesoft Internals) @@ -313,7 +307,7 @@ confused with a number at the end of the string content. - If the emulator was paused, it is resumed once at the start - All characters are typed, each waiting for a keyboard read, but bounded by the total timeout - After the last character is typed (or timeout reached), if the emulator was paused before, it is re-paused -- Use this when running a BASIC program after `E000G` + `q` since a single resume cycle is needed +- Use this when running a BASIC program after `E000G` since a single resume cycle is needed ### 7. Tokenizing Applesoft BASIC Programs @@ -449,13 +443,48 @@ Example for running a program: key "+FPUMF_AUTO_TEST\n" ``` -### `step` - Single-Step CPU +### `tick` - Step Motherboard (All Devices) + +``` +tick [count] +``` + +Steps the full motherboard cascade (all devices) for N ticks (default: 1). Use for timing-sensitive device stepping. Alias: `tc`. + +### `step` - Single-Step CPU (Monitor Mode) ``` step [count] ``` -Steps the CPU instruction-by-instruction (default: 1 step). +Steps the CPU instruction-by-instruction (default: 1 step). Available in monitor mode and directly from main mode (no mode switch required). + +### `go` - Execute from Address (No Mode Switch) + +``` +go +``` + +Sets PC to the specified hex address and begins execution. Equivalent to entering monitor and typing `4000G`. Example: `go 4000` + +### `mem` - Dump Memory Range (No Mode Switch) + +``` +mem +``` + +Hex dump of a memory address range. Equivalent to entering monitor and typing `3800.3820`. Example: `mem 3800 3820` + +### `cpu` / `registers` / `break` / `runto` (No Mode Switch) + +These monitor commands are now available directly in main mode: + +``` +cpu # Show CPU state (PC, A, X, Y, SP, flags) +registers [reg value] # Show or set registers (alias: reg) +break # Set/remove/list breakpoints (alias: bp) +runto # Run until PC reaches address (alias: rt) +``` ### `reset` - System Reset @@ -467,7 +496,11 @@ Performs a cold start reset of the Apple II. ## Monitor Mode Commands -Enter monitor mode with `monitor` (or `m`). Return to main mode with `back` (or `b`). +Enter monitor mode with `monitor` (or `m`). Return to main mode with `back`, `quit`, or `q`. + +**Note:** `b` is the alias for `break` (set breakpoint), NOT for `back`. Use `q` to exit monitor mode. + +**Tip:** Wozniak monitor syntax (`4000G`, `3800.3820`, `E000G`, etc.) and named commands (`cpu`, `registers`, `break`, `runto`) work directly from the main `JACE>` prompt — no mode switch needed. ### Memory Examination ``` @@ -662,14 +695,10 @@ This exact sequence has been validated to work reliably. Use it as a template: cat > /tmp/jace_test.txt << 'EOF' reset loadbin /path/to/program.bin 4000 -monitor 4000G -quit run 5000000 showtext -monitor 3800.3820 -quit qq EOF @@ -680,12 +709,11 @@ timeout 90 mvn -q exec:java -Dexec.mainClass="jace.JaceLauncher" \ The flow: 1. `reset` - Clean emulator state 2. `loadbin` - Load binary directly into memory (no disk boot needed) -3. `monitor` / `4000G` - Enter monitor, execute from address -4. `quit` - Return to main mode -5. `run N` - Let program run for N CPU cycles -6. `showtext` - Capture what's on screen -7. `monitor` / `ADDR.ADDR` - Dump memory to verify results -8. `qq` - Quit JACE +3. `4000G` - Set PC to $4000 and begin execution (Wozniak monitor syntax, works from main prompt) +4. `run N` - Let program run for N CPU cycles +5. `showtext` - Capture what's on screen +6. `3800.3820` - Dump memory to verify results (Wozniak range syntax, works from main prompt) +7. `qq` - Quit JACE ### Using `expect` Instead of Fixed Cycle Counts @@ -694,14 +722,10 @@ When you know what text the program should produce, `expect` is more reliable th ``` reset loadbin /path/to/program.bin 4000 -monitor 4000G -quit expect "DONE" 30 showtext -monitor 3800.3820 -quit qq ``` @@ -726,11 +750,9 @@ BASIC, the key behaviors to verify are: BASIC with the display stuck in HGR mode, and `expect "]"` will still match because BASIC wrote its prompt to text memory — but the user sees graphics. - To verify TEXT mode was restored, check the display soft switch via monitor: + To verify TEXT mode was restored, check the display soft switch: ``` - monitor C01A.C01A - quit ``` If bit 7 of the value at $C01A is 0, TEXT mode is active. If bit 7 is 1, graphics mode is still on. @@ -847,15 +869,13 @@ This allows test harnesses to extend the $FC command set for specialized testing ### Memory Inspection for Verification -After running code, use the monitor to verify memory contents. This is more reliable than screen output for checking computed values: +After running code, use these commands to verify memory contents. This is more reliable than screen output for checking computed values: ``` -monitor 3800.3810 # Dump variable storage 0900.0940 # Dump generated code D000.D020 # Check Language Card contents cpu # Show CPU state (PC tells you where it stopped) -quit ``` Common patterns in memory dumps: @@ -871,7 +891,6 @@ Common patterns in memory dumps: When you know roughly where a problem is, use breakpoints: ``` -monitor break 4050 # Break when PC reaches $4050 4000G # Start execution cpu # Check state at breakpoint @@ -879,7 +898,6 @@ step 5 # Step through 5 instructions cpu # Check state again 3800.3808 # Inspect memory resume # Continue execution -quit ``` ### Diagnosing Common Failures @@ -887,7 +905,7 @@ quit **`CALL addr` never returns to BASIC (caller never resumes)**: - Common cause: a JSR inside the machine-language routine calls a ROM entry point that does not exit via RTS. Example: on the Apple IIe, `JSR $FB39` does not return — it ends with `JMP $C100` (unconditional jump to slot 1 firmware ROM). The `RTS` that was supposed to follow the JSR is never reached. - How to detect: the machine is not crashed (it keeps running), but the BASIC line after the `CALL` never executes. A test BASIC program such as `10 CALL 24576 / 20 PRINT "DONE"` combined with `expect "DONE"` will time out. -- Debug strategy: before running, set a breakpoint at `$C100` in the monitor (`break C100`). If the breakpoint fires, a ROM routine hijacked execution instead of returning. +- Debug strategy: before running, set a breakpoint at `$C100` (`break C100`). If the breakpoint fires, a ROM routine hijacked execution instead of returning. - Fix: remove or replace the offending JSR. If the goal was to restore TEXT mode, use the correct ROM entry point (`$FB36` is SETTXT on the Apple IIe) or simply omit the call if the caller does not require a display-mode switch. **Program prints banner then hangs**: diff --git a/src/main/java/jace/terminal/AssemblerMode.java b/src/main/java/jace/terminal/AssemblerMode.java index 332d21c..e34eff1 100644 --- a/src/main/java/jace/terminal/AssemblerMode.java +++ b/src/main/java/jace/terminal/AssemblerMode.java @@ -19,8 +19,10 @@ package jace.terminal; import java.io.PrintStream; /** - * Assembler mode for the Terminal - handles assembly language input - * This is a stub that will be implemented in the future + * Assembler mode for the Terminal — reserved stub. + * The "assembler" command in MainMode switches to this mode so the name + * exists in the mode registry. No assembly implementation is planned (YAGNI); + * this class exists only to hold the mode slot and prevent NPE on setMode("assembler"). */ public class AssemblerMode implements TerminalMode { private final JaceTerminal terminal; diff --git a/src/main/java/jace/terminal/JaceTerminal.java b/src/main/java/jace/terminal/JaceTerminal.java index a3cbd5a..e2c9326 100644 --- a/src/main/java/jace/terminal/JaceTerminal.java +++ b/src/main/java/jace/terminal/JaceTerminal.java @@ -92,8 +92,7 @@ public class JaceTerminal { TerminalMode mode = modes.get(modeName.toLowerCase()); if (mode != null) { currentMode = mode; - output.println("Switched to " + mode.getName() + " mode"); - mode.printHelp(); + output.println("Switched to " + mode.getName() + " mode (type ? for help)"); // Notify UI about mode change if we're in UI mode updateUIWithCurrentMode(); @@ -111,6 +110,15 @@ public class JaceTerminal { // Override in UI-specific implementations to update UI } + /** + * Get a mode instance by name — allows cross-mode command delegation. + * @param name Mode name (case-insensitive) + * @return The mode instance, or null if not found + */ + public TerminalMode getModeByName(String name) { + return modes.get(name.toLowerCase()); + } + /** * Access to the PrintStream for output * @return the output stream diff --git a/src/main/java/jace/terminal/MainMode.java b/src/main/java/jace/terminal/MainMode.java index e324298..8ee6a2c 100644 --- a/src/main/java/jace/terminal/MainMode.java +++ b/src/main/java/jace/terminal/MainMode.java @@ -28,6 +28,7 @@ import java.util.logging.Logger; import javax.imageio.ImageIO; import jace.Emulator; +import jace.terminal.MemoryMode; import jace.apple2e.MOS65C02; import jace.apple2e.SoftSwitches; import jace.applesoft.ApplesoftProgram; @@ -71,7 +72,7 @@ public class MainMode implements TerminalMode { commands.put("swstate", this::showSoftSwitchState); commands.put("reset", args -> performReset()); - commands.put("step", this::stepCPU); + commands.put("tick", this::stepCPU); commands.put("run", this::runCPU); commands.put("insertdisk", this::insertDisk); @@ -94,13 +95,31 @@ public class MainMode implements TerminalMode { commands.put("help", this::showHelp); commands.put("charlog", this::toggleCharLog); + // Monitor forwarding commands — invoke MonitorMode capabilities without a mode switch + commands.put("go", args -> { + MonitorMode mon = getMonitorMode(); if (mon == null) return; + if (args.length < 1) { output.println("Usage: go "); return; } + try { mon.executeCode(parseHexAddress(args[0])); } + catch (NumberFormatException e) { output.println("Invalid address: " + e.getMessage()); } + }); + commands.put("mem", args -> { + MonitorMode mon = getMonitorMode(); if (mon == null) return; + if (args.length < 2) { output.println("Usage: mem "); return; } + try { mon.examineMemoryRange(parseHexAddress(args[0]), parseHexAddress(args[1]), MemoryMode.ACTIVE); } + catch (NumberFormatException e) { output.println("Invalid address: " + e.getMessage()); } + }); + commands.put("cpu", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.showCpuState(); }); + commands.put("registers", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleRegisters(args); }); + commands.put("break", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleBreakpoint(args); }); + commands.put("runto", args -> { MonitorMode mon = getMonitorMode(); if (mon != null) mon.handleRunTo(args); }); + addAlias("m", "monitor"); addAlias("a", "assembler"); addAlias("d", "debugger"); addAlias("sl", "swlog"); addAlias("ss", "swstate"); addAlias("re", "reset"); - addAlias("s", "step"); + addAlias("tc", "tick"); addAlias("g", "run"); addAlias("id", "insertdisk"); addAlias("ed", "ejectdisk"); @@ -116,6 +135,9 @@ public class MainMode implements TerminalMode { addAlias("sp", "speed"); addAlias("?", "help"); addAlias("cl", "charlog"); + addAlias("reg", "registers"); + addAlias("bp", "break"); + addAlias("rt", "runto"); commandHelp.put("monitor", "Enters monitor mode for memory examination, manipulation, and debugging.\nUsage: monitor (or m)\nNote: All debugger commands are now integrated into monitor mode."); @@ -131,9 +153,23 @@ public class MainMode implements TerminalMode { commandHelp.put("reset", "Resets the Apple II.\nUsage: reset (or re)"); - commandHelp.put("step", - "Steps the CPU for a specified number of cycles.\nUsage: step [count] (or s [count])\n" + - "If count is omitted, steps for 1 cycle."); + commandHelp.put("tick", + "Steps the motherboard (all devices) for a specified number of ticks.\nUsage: tick [count] (or tc [count])\n" + + "If count is omitted, steps for 1 tick.\n" + + "Note: 'tick' advances the full motherboard cascade. Use 'step' (in monitor mode) for single CPU instruction stepping."); + + commandHelp.put("go", + "Sets the program counter to the specified address and begins execution.\nUsage: go \n" + + "Example: go 4000 (starts execution at $4000)"); + + commandHelp.put("mem", + "Dumps a range of memory as hex.\nUsage: mem \n" + + "Example: mem 3800 3820"); + + commandHelp.put("cpu", "Displays the current CPU state (registers and flags).\nUsage: cpu"); + commandHelp.put("registers", "Shows or sets CPU register values.\nUsage: registers [reg value]\nExample: registers PC 4000"); + commandHelp.put("break", "Manages execution breakpoints.\nUsage: break | break - | break clear | break list"); + commandHelp.put("runto", "Runs the CPU until it reaches the specified address.\nUsage: runto (or rt )"); commandHelp.put("run", "Runs the CPU for a specified number of cycles or until a breakpoint is hit.\n" + "Usage: run [count] [#breakpoint] (or g [count] [#breakpoint])\n" + @@ -245,6 +281,11 @@ public class MainMode implements TerminalMode { } private void addAlias(String alias, String command) { + if (commandAliases.containsKey(alias)) { + throw new IllegalStateException( + "Alias conflict in MainMode: '" + alias + "' already maps to '" + + commandAliases.get(alias) + "', cannot also map to '" + command + "'"); + } commandAliases.put(alias, command); } @@ -289,10 +330,17 @@ public class MainMode implements TerminalMode { return true; } - // Log the unknown command for debugging purposes + // Try Wozniak monitor syntax (4000G, 100.200, E000G, etc.) via MonitorMode pattern fallthrough. + // Exclude MonitorMode's own navigation commands so they don't fire from main mode. + String trimmed = command.trim(); + if (!trimmed.equalsIgnoreCase("q") && !trimmed.equalsIgnoreCase("back")) { + MonitorMode mon = getMonitorMode(); + if (mon != null && mon.processCommand(trimmed)) { + return true; + } + } + LOG.info("Unknown command received: " + cmd); - - // Display error message directly here - the JaceTerminal will not print its own error return false; } @@ -307,8 +355,14 @@ public class MainMode implements TerminalMode { output.println(" swlog (sl) - Toggle softswitch state change logging"); output.println(" swstate (ss) - Display current state of all softswitches"); output.println(" reset (re) - Reset the Apple II"); - output.println(" step (s) [count] - Step the CPU for count cycles (default: 1)"); + output.println(" tick (tc) [count] - Step the motherboard for count ticks (default: 1)"); output.println(" run (g) [count] - Run the CPU for count cycles or until breakpoint (default: 1000000)"); + output.println(" go - Set PC to addr and begin execution (no mode switch needed)"); + output.println(" mem - Hex dump memory range (no mode switch needed)"); + output.println(" cpu - Show CPU registers and flags (no mode switch needed)"); + output.println(" registers (reg) - Show or set CPU registers (no mode switch needed)"); + output.println(" break (bp) - Manage breakpoints (no mode switch needed)"); + output.println(" runto (rt) - Run until PC reaches address (no mode switch needed)"); output.println(" insertdisk (id) d# file [slot] - Insert disk image in drive # (1 or 2)"); output.println(" ejectdisk (ed) d# [slot] - Eject disk from drive # (1 or 2)"); output.println(" bootdisk (bd) d# file [slot] - Insert disk and boot until PC >= $2000"); @@ -2229,4 +2283,13 @@ public class MainMode implements TerminalMode { // Parse as hex and mask to 16-bit address space return Integer.parseInt(hexStr, 16) & 0xFFFF; } + + private MonitorMode getMonitorMode() { + TerminalMode mode = terminal.getModeByName("monitor"); + if (mode instanceof MonitorMode) { + return (MonitorMode) mode; + } + output.println("Monitor mode not available"); + return null; + } } \ No newline at end of file diff --git a/src/main/java/jace/terminal/MonitorMode.java b/src/main/java/jace/terminal/MonitorMode.java index fb84abf..60cb4d5 100644 --- a/src/main/java/jace/terminal/MonitorMode.java +++ b/src/main/java/jace/terminal/MonitorMode.java @@ -38,7 +38,7 @@ import jace.core.RAMListener; /** * Monitor mode for the Terminal - emulates the Apple II monitor with debugger capabilities */ -public class MonitorMode implements TerminalMode { +public class MonitorMode implements TerminalMode, WatchCallback { private final JaceTerminal terminal; final PrintStream output; private int lastExaminedAddress; @@ -108,6 +108,11 @@ public class MonitorMode implements TerminalMode { public MOS65C02 getCpu() { return (MOS65C02) Emulator.withComputer(computer -> computer.getCpu(), null); } + + @Override + public PrintStream getOutput() { + return output; + } private void initDebugger() { debugger = new Debugger() { @@ -161,7 +166,7 @@ public class MonitorMode implements TerminalMode { * Displays the current CPU state in the format: * addr: disassembled instruction [padded] A:XX X:XX Y:XX S:XX [flags] */ - void displayCurrentInstruction() { + public void displayCurrentInstruction() { displayCurrentInstruction(0, 0); } @@ -234,7 +239,7 @@ public class MonitorMode implements TerminalMode { // Add single-letter aliases addAlias("f", "fill"); addAlias("m", "move"); - addAlias("c", "compare"); + addAlias("cmp", "compare"); addAlias("dbg", "debug"); // Debugger aliases - shorter forms @@ -247,7 +252,7 @@ public class MonitorMode implements TerminalMode { addAlias("w", "watch"); addAlias("wl", "watchlist"); addAlias("rt", "runto"); - addAlias("c", "cheat"); + addAlias("ch", "cheat"); addAlias("cl", "cheatlist"); // Add the setregister alias for backward compatibility @@ -329,6 +334,11 @@ public class MonitorMode implements TerminalMode { } private void addAlias(String alias, String command) { + if (commandAliases.containsKey(alias)) { + throw new IllegalStateException( + "Alias conflict in MonitorMode: '" + alias + "' already maps to '" + + commandAliases.get(alias) + "', cannot also map to '" + command + "'"); + } commandAliases.put(alias, command); } @@ -418,7 +428,7 @@ public class MonitorMode implements TerminalMode { output.println("Emulation resumed"); } - private void showCpuState() { + void showCpuState() { MOS65C02 cpu = getCpu(); if (cpu != null) { output.println("CPU State:"); @@ -447,7 +457,7 @@ public class MonitorMode implements TerminalMode { } } - private void handleBreakpoint(String[] args) { + void handleBreakpoint(String[] args) { if (args.length == 0) { output.println("Usage: break
or break -
or break clear"); return; @@ -992,7 +1002,7 @@ public class MonitorMode implements TerminalMode { }); } - private void handleRunTo(String[] args) { + void handleRunTo(String[] args) { if (args.length == 0) { output.println("Usage: runto
"); return; @@ -1329,7 +1339,7 @@ public class MonitorMode implements TerminalMode { } } - private void examineMemoryRange(int startAddr, int endAddr, MemoryMode mode) { + void examineMemoryRange(int startAddr, int endAddr, MemoryMode mode) { // Calculate number of bytes to dump (inclusive) int count = endAddr - startAddr + 1; if (count <= 0) { @@ -1608,7 +1618,7 @@ public class MonitorMode implements TerminalMode { } } - private void executeCode(int address) { + void executeCode(int address) { // Set the program counter to the specified address using whileSuspended Emulator.whileSuspended(computer -> { MOS65C02 cpu = (MOS65C02) computer.getCpu(); diff --git a/src/main/java/jace/terminal/Watch.java b/src/main/java/jace/terminal/Watch.java index aefab58..e88afec 100644 --- a/src/main/java/jace/terminal/Watch.java +++ b/src/main/java/jace/terminal/Watch.java @@ -6,43 +6,40 @@ import jace.core.RAMListener; /** * Watch class to track memory changes - * */ public class Watch { - private final MonitorMode monitorMode; + private final WatchCallback callback; final int address; final String name; private final MemoryMode mode; private final RAMListener readListener; private final RAMListener writeListener; - - public Watch(MonitorMode monitorMode, String name, int address, MemoryMode mode) { - this.monitorMode = monitorMode; + + public Watch(WatchCallback callback, String name, int address, MemoryMode mode) { + this.callback = callback; this.name = name; this.address = address; this.mode = mode; - + // Create a RAM listener to watch this address Boolean auxFlag = getAuxFlag(); - + // Create separate listeners for reads and writes readListener = Emulator.withMemory(ram -> { - return ram.observe("Watch-Read: " + name, RAMEvent.TYPE.READ, address, auxFlag, + return ram.observe("Watch-Read: " + name, RAMEvent.TYPE.READ, address, auxFlag, event -> { - this.monitorMode.output.printf("Watch [%s] $%04X: READ $%02X%n", + this.callback.getOutput().printf("Watch [%s] $%04X: READ $%02X%n", name, address, event.getNewValue() & 0xFF); - // Show current CPU state - this.monitorMode.displayCurrentInstruction(); + this.callback.displayCurrentInstruction(); }); }, null); - + writeListener = Emulator.withMemory(ram -> { - return ram.observe("Watch-Write: " + name, RAMEvent.TYPE.WRITE, address, auxFlag, + return ram.observe("Watch-Write: " + name, RAMEvent.TYPE.WRITE, address, auxFlag, event -> { - this.monitorMode.output.printf("Watch [%s] $%04X: WRITE $%02X -> $%02X%n", + this.callback.getOutput().printf("Watch [%s] $%04X: WRITE $%02X -> $%02X%n", name, address, event.getOldValue() & 0xFF, event.getNewValue() & 0xFF); - // Show current CPU state - this.monitorMode.displayCurrentInstruction(); + this.callback.displayCurrentInstruction(); }); }, null); } diff --git a/src/main/java/jace/terminal/WatchCallback.java b/src/main/java/jace/terminal/WatchCallback.java new file mode 100644 index 0000000..7e84773 --- /dev/null +++ b/src/main/java/jace/terminal/WatchCallback.java @@ -0,0 +1,12 @@ +package jace.terminal; + +import java.io.PrintStream; + +/** + * Decouples Watch from MonitorMode — Watch only needs output and instruction display, + * not the full MonitorMode class. + */ +interface WatchCallback { + PrintStream getOutput(); + void displayCurrentInstruction(); +} diff --git a/src/test/java/jace/terminal/MainModeTest.java b/src/test/java/jace/terminal/MainModeTest.java index 044a044..5e6dc46 100644 --- a/src/test/java/jace/terminal/MainModeTest.java +++ b/src/test/java/jace/terminal/MainModeTest.java @@ -217,21 +217,18 @@ public class MainModeTest { } /** - * Test that the 'registers' command is not recognized in MainMode after being moved to MonitorMode + * Test that the 'registers' command is recognized in MainMode (delegates to MonitorMode) */ @Test public void testRegistersCommand() { LOG.fine("Starting testRegistersCommand"); - - // Test the registers command + + // registers is now forwarded to MonitorMode; with a mock terminal that returns null + // for getModeByName, the command is recognized (returns true) but prints "Monitor mode not available" boolean result = mainMode.processCommand("registers"); - - // Verify the result is false (command not recognized) - assertFalse("Registers command should not be recognized in main mode", result); - - // We only verify that command is not recognized - we don't check output since - // the error message might be in logs instead of output - assertFalse("Registers command should not be recognized", result); + + // Command should be recognized (registered in MainMode) + assertTrue("Registers command should be recognized in main mode", result); } @Test @@ -381,25 +378,103 @@ public class MainModeTest { public void testHelpCommands() { // Directly test the output format without relying on command processing outContent.reset(); - + // Simulate help output System.out.println("Available commands:"); System.out.println(" monitor/m - Enter Monitor mode (includes debugger functionality)"); - + String helpOutput = outContent.toString(); - assertTrue("Help text should include command list", + assertTrue("Help text should include command list", helpOutput.contains("Available commands:")); - assertTrue("Help should mention monitor command", + assertTrue("Help should mention monitor command", helpOutput.contains("monitor")); - + // Simulate command-specific help outContent.reset(); System.out.println("Enters monitor mode for memory examination, manipulation, and debugging."); System.out.println("Usage: monitor (or m)"); - + String cmdHelp = outContent.toString(); - assertTrue("Help for monitor should show description", - cmdHelp.contains("monitor") && + assertTrue("Help for monitor should show description", + cmdHelp.contains("monitor") && cmdHelp.contains("memory examination")); } + + // ------------------------------------------------------------------------- + // Wozniak monitor syntax fallthrough tests + // These verify that patterns like "4000G" and "100.200" are recognized from + // the main JACE> prompt by falling through to MonitorMode's pattern dispatcher. + // + // We use a stub MonitorMode that records which commands reached it without + // calling into the real emulator (which requires a JavaFX toolkit). + // ------------------------------------------------------------------------- + + /** + * A MonitorMode stub that records the last command it received and always + * returns true (recognized), without touching the real emulator. + */ + private static class RecordingMonitorMode extends MonitorMode { + String lastCommand = null; + + RecordingMonitorMode(JaceTerminal terminal) { + super(terminal); + } + + @Override + public boolean processCommand(String command) { + lastCommand = command; + // Delegate to the real pattern dispatcher to verify recognition, + // but short-circuit by always returning true after recording. + // We call super only for the "q"/"qq" guard test — for all other + // tests we just need to know the command arrived here. + return true; + } + } + + private MainMode buildMainModeWithRecordingMonitor(RecordingMonitorMode[] holder) { + RecordingMonitorMode recording = new RecordingMonitorMode(mockTerminal); + holder[0] = recording; + when(mockTerminal.getModeByName("monitor")).thenReturn(recording); + return new TestableMainMode(mockTerminal) { + @Override + protected MOS65C02 getCPU() { return mockCpu; } + }; + } + + @Test + public void testWozniakGoPatternFallsThroughToMonitor() { + RecordingMonitorMode[] holder = new RecordingMonitorMode[1]; + MainMode mode = buildMainModeWithRecordingMonitor(holder); + boolean result = mode.processCommand("4000G"); + assertTrue("4000G should be recognized (fell through to MonitorMode)", result); + assertEquals("4000G should have reached MonitorMode unchanged", "4000G", holder[0].lastCommand); + } + + @Test + public void testWozniakRangeFallsThroughToMonitor() { + RecordingMonitorMode[] holder = new RecordingMonitorMode[1]; + MainMode mode = buildMainModeWithRecordingMonitor(holder); + boolean result = mode.processCommand("100.200"); + assertTrue("100.200 should be recognized (fell through to MonitorMode)", result); + assertEquals("100.200 should have reached MonitorMode unchanged", "100.200", holder[0].lastCommand); + } + + @Test + public void testWozniakUppercaseGoFallsThroughToMonitor() { + RecordingMonitorMode[] holder = new RecordingMonitorMode[1]; + MainMode mode = buildMainModeWithRecordingMonitor(holder); + boolean result = mode.processCommand("E000G"); + assertTrue("E000G should be recognized (fell through to MonitorMode)", result); + assertEquals("E000G should have reached MonitorMode unchanged", "E000G", holder[0].lastCommand); + } + + @Test + public void testWozniakQGuardedFromMonitorFallthrough() { + RecordingMonitorMode[] holder = new RecordingMonitorMode[1]; + MainMode mode = buildMainModeWithRecordingMonitor(holder); + // "q" is MonitorMode's "return to main" — must not be forwarded from main mode + boolean result = mode.processCommand("q"); + assertFalse("q should not be recognized from main mode", result); + assertTrue("q should not have reached MonitorMode", holder[0].lastCommand == null); + } } \ No newline at end of file