From d96d232fd3f95fcaea9d8299fe2b667ffb536ec9 Mon Sep 17 00:00:00 2001 From: Josh Dersch Date: Tue, 20 Mar 2018 14:16:07 -0700 Subject: [PATCH] Initial commit of changes for 1.2.3. This includes: - Scripting support: Allows for recording and playback of mouse/keyboard input and various system control actions. Simple (i.e. basic) scripting format. - Fix for stale packets left in ethernet input queue; packets received by pcap while Alto's receiver is off are discarded. - Mouse input made more accurate, and tweaked to avoid Alto microcode bug that causes erroneous mouse inputs under very rare circumstances on real hardware, but much more frequently under emulation. - Small code cleanup here and there. Moved many UI strings to resources, many more to go. --- .gitignore | 1 + Contralto/AltoSystem.cs | 54 +- Contralto/CPU/CPU.cs | 14 +- Contralto/CPU/Tasks/EmulatorTask.cs | 28 + Contralto/Configuration.cs | 6 +- Contralto/Contralto.csproj | 11 +- Contralto/Disk/allgames.dsk | Bin 2601648 -> 2601648 bytes Contralto/Display/DisplayController.cs | 12 +- Contralto/ExecutionController.cs | 97 ++- Contralto/IO/DiskController.cs | 8 +- Contralto/IO/DoverROS.cs | 2 +- Contralto/IO/EthernetController.cs | 16 +- Contralto/IO/Keyboard.cs | 21 +- Contralto/IO/MouseAndKeyset.cs | 245 ++++-- Contralto/IO/OrbitController.cs | 2 +- Contralto/IO/TridentController.cs | 6 +- Contralto/IO/TridentDrive.cs | 2 +- Contralto/Logging/Log.cs | 1 + Contralto/Memory/Memory.cs | 4 +- Contralto/Program.cs | 69 +- Contralto/Properties/AssemblyInfo.cs | 6 +- Contralto/Properties/Resources.Designer.cs | 261 +++++++ Contralto/Properties/Resources.resx | 87 +++ Contralto/Scheduler.cs | 36 +- .../CommandExecutor.cs} | 117 +-- Contralto/Scripting/ControlCommands.cs | 219 ++++++ .../DebuggerAttributes.cs | 2 +- Contralto/Scripting/ScriptAction.cs | 701 ++++++++++++++++++ Contralto/Scripting/ScriptManager.cs | 126 ++++ Contralto/Scripting/ScriptPlayback.cs | 108 +++ Contralto/Scripting/ScriptReader.cs | 66 ++ Contralto/Scripting/ScriptRecorder.cs | 123 +++ Contralto/Scripting/ScriptWriter.cs | 42 ++ Contralto/SdlUI/DebuggerPrompt.cs | 1 + Contralto/SdlUI/SdlAltoWindow.cs | 32 +- Contralto/SdlUI/SdlConsole.cs | 309 +++----- Contralto/UI/AltoWindow.Designer.cs | 68 +- Contralto/UI/AltoWindow.cs | 293 ++++++-- Contralto/UI/Debugger.cs | 47 +- 39 files changed, 2733 insertions(+), 510 deletions(-) rename Contralto/{SdlUI/ConsoleExecutor.cs => Scripting/CommandExecutor.cs} (90%) create mode 100644 Contralto/Scripting/ControlCommands.cs rename Contralto/{SdlUI => Scripting}/DebuggerAttributes.cs (98%) create mode 100644 Contralto/Scripting/ScriptAction.cs create mode 100644 Contralto/Scripting/ScriptManager.cs create mode 100644 Contralto/Scripting/ScriptPlayback.cs create mode 100644 Contralto/Scripting/ScriptReader.cs create mode 100644 Contralto/Scripting/ScriptRecorder.cs create mode 100644 Contralto/Scripting/ScriptWriter.cs diff --git a/.gitignore b/.gitignore index b817552..b7279d4 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,4 @@ _Pvt_Extensions/ ModelManifest.xml /Contralto.VC.VC.opendb /Contralto.VC.db +/.vs/Contralto/v15/Server/sqlite3 diff --git a/Contralto/AltoSystem.cs b/Contralto/AltoSystem.cs index a807c5d..21b41a5 100644 --- a/Contralto/AltoSystem.cs +++ b/Contralto/AltoSystem.cs @@ -23,6 +23,7 @@ using Contralto.Memory; using Contralto.Display; using System.IO; using System; +using Contralto.Scripting; namespace Contralto { @@ -41,7 +42,7 @@ namespace Contralto _keyboard = new Keyboard(); _diskController = new DiskController(this); _displayController = new DisplayController(this); - _mouseAndKeyset = new MouseAndKeyset(); + _mouseAndKeyset = new MouseAndKeyset(this); _ethernetController = new EthernetController(this); _orbitController = new OrbitController(this); _audioDAC = new AudioDAC(this); @@ -78,6 +79,11 @@ namespace Contralto _tridentController.Reset(); UCodeMemory.Reset(); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command("reset"); + } } /// @@ -137,6 +143,20 @@ namespace Contralto } } } + + // + // If we're recording, add a "quit" command to the script, and stop the recording. + // + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(commitDisks ? "quit" : "quit without saving"); + ScriptManager.StopRecording(); + } + + if (ScriptManager.IsPlaying) + { + ScriptManager.StopPlayback(); + } } public void SingleStep() @@ -187,13 +207,23 @@ namespace Contralto if (newImage) { newPack = InMemoryDiskPack.CreateEmpty(geometry, path); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("new disk {0} {1}", drive, path)); + } } else { newPack = InMemoryDiskPack.Load(geometry, path); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("load disk {0} {1}", drive, path)); + } } - _diskController.Drives[drive].LoadPack(newPack); + _diskController.Drives[drive].LoadPack(newPack); } public void UnloadDiabloDrive(int drive) @@ -209,6 +239,11 @@ namespace Contralto _diskController.CommitDisk(drive); _diskController.Drives[drive].UnloadPack(); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("unload disk {0}", drive)); + } } public void LoadTridentDrive(int drive, string path, bool newImage) @@ -249,10 +284,20 @@ namespace Contralto if (newImage) { newPack = FileBackedDiskPack.CreateEmpty(geometry, path); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("new trident {0} {1}", drive, path)); + } } else { newPack = FileBackedDiskPack.Load(geometry, path); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("load trident {0} {1}", drive, path)); + } } _tridentController.Drives[drive].LoadPack(newPack); @@ -271,6 +316,11 @@ namespace Contralto _tridentController.CommitDisk(drive); _tridentController.Drives[drive].UnloadPack(); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.Command(String.Format("unload trident {0}", drive)); + } } diff --git a/Contralto/CPU/CPU.cs b/Contralto/CPU/CPU.cs index 124ffda..2709f60 100644 --- a/Contralto/CPU/CPU.cs +++ b/Contralto/CPU/CPU.cs @@ -146,13 +146,7 @@ namespace Contralto.CPU { switch (_currentTask.ExecuteNext()) { - case InstructionCompletion.TaskSwitch: - // Invoke the task switch, this will take effect after - // the NEXT instruction completes, not this one. - TaskSwitch(); - break; - - case InstructionCompletion.Normal: + case InstructionCompletion.Normal: // If we have a new task, switch to it now. if (_currentTask != _nextTask) { @@ -162,6 +156,12 @@ namespace Contralto.CPU } break; + case InstructionCompletion.TaskSwitch: + // Invoke the task switch, this will take effect after + // the NEXT instruction completes, not this one. + TaskSwitch(); + break; + case InstructionCompletion.MemoryWait: // We were waiting for memory on this cycle, we do nothing // (no task switch even if one is pending) in this case. diff --git a/Contralto/CPU/Tasks/EmulatorTask.cs b/Contralto/CPU/Tasks/EmulatorTask.cs index 1b05edd..5c1936d 100644 --- a/Contralto/CPU/Tasks/EmulatorTask.cs +++ b/Contralto/CPU/Tasks/EmulatorTask.cs @@ -16,6 +16,7 @@ */ using Contralto.Logging; +using Contralto.Scripting; using System; namespace Contralto.CPU @@ -159,6 +160,33 @@ namespace Contralto.CPU _cpu._system.TridentController.STARTF(_busData); break; + // + // The following are not actual Alto STARTF functions, + // these are used to allow writing Alto programs that can + // alter behavior of the emulator. At the moment, these + // are all related to scripting, and are only enabled + // when a script is running. + // + case 0x2000: + // + // Unpause script. + // + if (ScriptManager.IsPlaying) + { + ScriptManager.CompleteWait(); + } + break; + + case 0x4000: + // + // Emulator exit, commit disks. + // + if (ScriptManager.IsPlaying) + { + throw new ShutdownException(true); + } + break; + default: Log.Write(Logging.LogType.Warning, Logging.LogComponent.EmulatorTask, "STARTF for unknown device (code {0})", Conversion.ToOctal(_busData)); diff --git a/Contralto/Configuration.cs b/Contralto/Configuration.cs index d805586..fdbbcf1 100644 --- a/Contralto/Configuration.cs +++ b/Contralto/Configuration.cs @@ -278,7 +278,7 @@ namespace Contralto public static void ReadConfiguration() { if (Configuration.Platform == PlatformType.Windows - && Program.StartupArgs.Length == 0) + && !string.IsNullOrWhiteSpace(StartupOptions.ConfigurationFile)) { // // By default, on Windows we use the app Settings functionality @@ -366,9 +366,9 @@ namespace Contralto { string configFilePath = null; - if (Program.StartupArgs.Length > 0) + if (!string.IsNullOrWhiteSpace(StartupOptions.ConfigurationFile)) { - configFilePath = Program.StartupArgs[0]; + configFilePath = StartupOptions.ConfigurationFile; } else { diff --git a/Contralto/Contralto.csproj b/Contralto/Contralto.csproj index b6fc8e0..01b2eaf 100644 --- a/Contralto/Contralto.csproj +++ b/Contralto/Contralto.csproj @@ -148,9 +148,16 @@ True Settings.settings + + + + + + + - - + + diff --git a/Contralto/Disk/allgames.dsk b/Contralto/Disk/allgames.dsk index eff0be8924b16447d14b6d291bb0a5df7fec392c..6b6eeaecd2ec61d4d5d5ced6f9b6fe3a3d6d8f1b 100644 GIT binary patch delta 30737 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 delta 61084 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 /// /// - private void VerticalBlankScanlineCallback(ulong timeNsec, ulong skewNsec, object context) + private void VerticalBlankScanlineCallback(ulong skewNsec, object context) { // End of VBlank scanline. _vblankScanlineCount++; @@ -189,7 +189,7 @@ namespace Contralto.Display /// /// /// - private void HorizontalBlankEndCallback(ulong timeNsec, ulong skewNsec, object context) + private void HorizontalBlankEndCallback(ulong skewNsec, object context) { // Reset scanline word counter _word = 0; @@ -220,7 +220,7 @@ namespace Contralto.Display /// /// /// - private void WordCallback(ulong timeNsec, ulong skewNsec, object context) + private void WordCallback(ulong skewNsec, object context) { if (_display == null) { @@ -254,11 +254,11 @@ namespace Contralto.Display _display.DrawCursorWord(_scanline, _cursorXLatched, _whiteOnBlack, _cursorRegLatched); } - _scanline += 2; + _scanline += 2; if (_scanline >= 808) { - // Done with field. + // Done with field. // Draw the completed field to the emulated display. _display.Render(); @@ -271,7 +271,7 @@ namespace Contralto.Display // More scanlines to do. // Run CURT and MRT at end of scanline - _system.CPU.WakeupTask(TaskType.Cursor); + _system.CPU.WakeupTask(TaskType.Cursor); _system.CPU.WakeupTask(TaskType.MemoryRefresh); // Schedule HBlank wakeup for end of next HBlank diff --git a/Contralto/ExecutionController.cs b/Contralto/ExecutionController.cs index b466acf..f3b3796 100644 --- a/Contralto/ExecutionController.cs +++ b/Contralto/ExecutionController.cs @@ -15,6 +15,7 @@ along with ContrAlto. If not, see . */ +using Contralto.Scripting; using System; using System.Threading; @@ -23,6 +24,22 @@ namespace Contralto public delegate bool StepCallbackDelegate(); public delegate void ErrorCallbackDelegate(Exception e); + public delegate void ShutdownCallbackDelegate(bool commitDisks); + + public class ShutdownException : Exception + { + public ShutdownException(bool commitDisks) : base() + { + _commitDisks = commitDisks; + } + + public bool CommitDisks + { + get { return _commitDisks; } + } + + private bool _commitDisks; + } public class ExecutionController @@ -46,29 +63,56 @@ namespace Contralto { _userAbort = true; - if (_execThread != null) + if (System.Threading.Thread.CurrentThread != + _execThread) { - _execThread.Join(); - _execThread = null; + // + // Call is asynchronous, we will wait for the + // execution thread to finish. + // + if (_execThread != null) + { + _execThread.Join(); + _execThread = null; + } } } public void Reset(AlternateBootType bootType) { - bool running = IsRunning; - - if (running) + if (System.Threading.Thread.CurrentThread == + _execThread) { - StopExecution(); + // + // Call is from within the execution thread + // so we can just reset the system without worrying + // about synchronization. + // + _system.Reset(); + _system.PressBootKeys(bootType); } - _system.Reset(); - _system.PressBootKeys(bootType); - - if (running) + else { - StartExecution(AlternateBootType.None); + // + // Call is asynchronous, we need to stop the + // execution thread and restart it after resetting + // the system. + // + bool running = IsRunning; + + if (running) + { + StopExecution(); + } + _system.Reset(); + _system.PressBootKeys(bootType); + + if (running) + { + StartExecution(AlternateBootType.None); + } } - } + } public bool IsRunning { @@ -87,6 +131,12 @@ namespace Contralto set { _errorCallback = value; } } + public ShutdownCallbackDelegate ShutdownCallback + { + get { return _shutdownCallback; } + set { _shutdownCallback = value; } + } + private void StartAltoExecutionThread() { if (_execThread != null && _execThread.IsAlive) @@ -104,11 +154,29 @@ namespace Contralto private void ExecuteProc() { while (true) - { + { // Execute a single microinstruction try { _system.SingleStep(); + + if (ScriptManager.IsPlaying || + ScriptManager.IsRecording) + { + ScriptManager.ScriptScheduler.Clock(); + } + } + catch(ShutdownException s) + { + // + // We will only actually shut down if someone + // is listening to this event. + // + if (_shutdownCallback != null) + { + _shutdownCallback(s.CommitDisks); + _execAbort = true; + } } catch (Exception e) { @@ -139,6 +207,7 @@ namespace Contralto private StepCallbackDelegate _stepCallback; private ErrorCallbackDelegate _errorCallback; + private ShutdownCallbackDelegate _shutdownCallback; private AltoSystem _system; } diff --git a/Contralto/IO/DiskController.cs b/Contralto/IO/DiskController.cs index e1e13e9..70c838e 100644 --- a/Contralto/IO/DiskController.cs +++ b/Contralto/IO/DiskController.cs @@ -295,7 +295,7 @@ namespace Contralto.IO /// /// /// - private void SectorCallback(ulong timeNsec, ulong skewNsec, object context) + private void SectorCallback(ulong skewNsec, object context) { // // Next sector; move to next sector and wake up Disk Sector task. @@ -360,7 +360,7 @@ namespace Contralto.IO /// /// /// - private void WordCallback(ulong timeNsec, ulong skewNsec, object context) + private void WordCallback(ulong skewNsec, object context) { SpinDisk(); @@ -385,7 +385,7 @@ namespace Contralto.IO /// /// /// - private void SeclateCallback(ulong timeNsec, ulong skewNsec, object context) + private void SeclateCallback(ulong skewNsec, object context) { if (_seclateEnable) { @@ -635,7 +635,7 @@ namespace Contralto.IO return ((_kAdr & 0x00c0) >> 6) == 2 || ((_kAdr & 0x00c0) >> 6) == 3; } - private void SeekCallback(ulong timeNsec, ulong skewNsec, object context) + private void SeekCallback(ulong skewNsec, object context) { if (SelectedDrive.Cylinder < _destCylinder) { diff --git a/Contralto/IO/DoverROS.cs b/Contralto/IO/DoverROS.cs index ffaa637..9dd30af 100644 --- a/Contralto/IO/DoverROS.cs +++ b/Contralto/IO/DoverROS.cs @@ -474,7 +474,7 @@ namespace Contralto.IO /// /// /// - private void PrintEngineCallback(ulong timestampNsec, ulong delta, object context) + private void PrintEngineCallback(ulong delta, object context) { Log.Write(LogComponent.DoverROS, "Scanline {0} (sendvideo {1})", _printEngineTimestep, _sendVideo); switch (_state) diff --git a/Contralto/IO/EthernetController.cs b/Contralto/IO/EthernetController.cs index 43613c1..0f1e5a2 100644 --- a/Contralto/IO/EthernetController.cs +++ b/Contralto/IO/EthernetController.cs @@ -291,7 +291,7 @@ namespace Contralto.IO _system.Scheduler.Schedule(_fifoTransmitWakeupEvent); } - private void OutputFifoCallback(ulong timeNsec, ulong skewNsec, object context) + private void OutputFifoCallback(ulong skewNsec, object context) { bool end = (bool)context; @@ -415,21 +415,21 @@ namespace Contralto.IO /// /// /// - private void InputHandler(ulong timeNsec, ulong skewNsec, object context) + private void InputHandler(ulong skewNsec, object context) { switch(_inputState) { case InputState.ReceiverOff: - // Receiver is off, if we have any incoming packets, they are ignored. - // TODO: would it make sense to expire really old packets (say more than a couple of seconds old) - // so that the receiver doesn't pick up ancient history the next time it runs? - // We already cycle out packets as new ones come in, so this would only be an issue on very quiet networks. - // (And even then I don't know if it's really an issue.) + // Receiver is off, if we have any incoming packets, they are dropped. + // (If we leave packets in the queue while the receiver is off, this can cause + // stale data to be picked up when the receiver is turned back on, which can in + // turn cause unexpected behavior.) _receiverLock.EnterReadLock(); if (_nextPackets.Count > 0) { - Log.Write(LogComponent.EthernetPacket, "Receiver is off, ignoring incoming packet from packet queue."); + Log.Write(LogComponent.EthernetPacket, "Receiver is off, dropping incoming packets from packet queue."); + _nextPackets.Clear(); } _receiverLock.ExitReadLock(); _inputPollActive = false; diff --git a/Contralto/IO/Keyboard.cs b/Contralto/IO/Keyboard.cs index 4a39971..f16e552 100644 --- a/Contralto/IO/Keyboard.cs +++ b/Contralto/IO/Keyboard.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using Contralto.Memory; using Contralto.CPU; +using Contralto.Scripting; namespace Contralto.IO { @@ -115,7 +116,7 @@ namespace Contralto.IO public Keyboard() { InitMap(); - Reset(); + Reset(); } public void Reset() @@ -125,8 +126,8 @@ namespace Contralto.IO } public ushort Read(int address, TaskType task, bool extendedMemoryReference) - { - // keyboard word is inverted + { + // keyboard word is inverted return (ushort)~_keyWords[address - 0xfe1c]; // TODO: move to constant. } @@ -141,21 +142,31 @@ namespace Contralto.IO // If we had been holding boot keys, release them now that a real user is pressing a key. if (_bootKeysPressed) { - Reset(); + Reset(); } AltoKeyBit bits = _keyMap[key]; _keyWords[bits.Word] |= bits.Bitmask; + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.KeyDown(key); + } } public void KeyUp(AltoKey key) { AltoKeyBit bits = _keyMap[key]; _keyWords[bits.Word] &= (ushort)~bits.Bitmask; + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.KeyUp(key); + } } public void PressBootKeys(ushort bootAddress, bool netBoot) - { + { for (int i = 0; i < 16; i++) { if ((bootAddress & (0x8000 >> i)) != 0) diff --git a/Contralto/IO/MouseAndKeyset.cs b/Contralto/IO/MouseAndKeyset.cs index cda100d..30fe536 100644 --- a/Contralto/IO/MouseAndKeyset.cs +++ b/Contralto/IO/MouseAndKeyset.cs @@ -17,7 +17,9 @@ using Contralto.CPU; using Contralto.Memory; +using Contralto.Scripting; using System; +using System.Collections.Generic; using System.Threading; namespace Contralto.IO @@ -51,9 +53,10 @@ namespace Contralto.IO /// public class MouseAndKeyset : IMemoryMappedDevice { - public MouseAndKeyset() + public MouseAndKeyset(AltoSystem system) { - _lock = new ReaderWriterLockSlim(); + _system = system; + _lock = new ReaderWriterLockSlim(); Reset(); } @@ -61,6 +64,9 @@ namespace Contralto.IO { _keyset = 0; _buttons = AltoMouseButton.None; + _moves = new Queue(); + _currentMove = null; + _pollCounter = 0; } public ushort Read(int address, TaskType task, bool extendedMemoryReference) @@ -75,26 +81,47 @@ namespace Contralto.IO public void MouseMove(int dx, int dy) { + // Calculate number of steps in x and y to be decremented every call to PollMouseBits + MouseMovement nextMove = new MouseMovement(Math.Abs(dx), Math.Abs(dy), Math.Sign(dx), Math.Sign(dy)); + _lock.EnterWriteLock(); - // Calculate number of steps in x and y to be decremented every call to PollMouseBits - _xSteps = Math.Abs(dx); - _xDir = Math.Sign(dx); + _moves.Enqueue(nextMove); + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.MouseMoveRelative(dx, dy); + } - _ySteps = Math.Abs(dy); - _yDir = Math.Sign(dy); - _lock.ExitWriteLock(); } public void MouseDown(AltoMouseButton button) { _buttons |= button; + + if (ScriptManager.IsRecording) + { + // + // Record the absolute position of the mouse (as held in MOUSELOC in system memory). + // All other mouse movements in the script will be recorded relative to this point. + // + //int x = _system.Memory.Read(0x114, CPU.TaskType.Ethernet, false); + //int y = _system.Memory.Read(0x115, CPU.TaskType.Ethernet, false); + //ScriptManager.Recorder.MouseMoveAbsolute(x, y); + + ScriptManager.Recorder.MouseDown(button); + } } public void MouseUp(AltoMouseButton button) { _buttons ^= button; + + if (ScriptManager.IsRecording) + { + ScriptManager.Recorder.MouseUp(button); + } } public void KeysetDown(AltoKeysetKey key) @@ -129,63 +156,110 @@ namespace Contralto.IO ushort bits = 0; _lock.EnterReadLock(); - // TODO: optimize this - if (_yDir == -1 && _xDir == 0) - { - bits = 1; - } - else if (_yDir == 1 && _xDir == 0) - { - bits = 2; - } - else if (_yDir == 0 && _xDir == -1) - { - bits = 3; - } - else if (_yDir == -1 && _xDir == -1) - { - bits = 4; - } - else if (_yDir == 1 && _xDir == -1) - { - bits = 5; - } - else if (_yDir == 0 && _xDir == 1) - { - bits = 6; - } - else if (_yDir == -1 && _xDir == 1) - { - bits = 7; - } - else if (_yDir == 1 && _xDir == 1) - { - bits = 8; - } - // Move the mouse closer to its destination - if (_xSteps > 0) + if (_currentMove == null && _moves.Count > 0) { - _mouseX += _xDir; - _xSteps--; + _currentMove = _moves.Dequeue(); + } - if (_xSteps == 0) + // + // <-MOUSE is invoked by the Memory Refresh Task once per scanline (including during vblank) which + // works out to about 13,000 times a second. To more realistically simulate the movement of a mouse + // across a desk, we return actual mouse movement data only periodically. + // + if (_currentMove != null && (_pollCounter % _currentMove.PollRate) == 0) + { + + // + // Choose a direction. We do not provide movements in both X and Y at the same time; + // this is solely to avoid a microcode bug that causes erroneous movements in such cases + // (which then plays havoc with scripting and absolute coordinates.) + // (It is also the case that on the real hardware, such movements are extremely rare due to + // the nature of the hardware involved). + // + int dx = _currentMove.DX; + int dy = _currentMove.DY; + + if (dx != 0 && dy != 0) { - _xDir = 0; - } - } - - if (_ySteps > 0) - { - _mouseY += _yDir; - _ySteps--; - - if (_ySteps == 0) - { - _yDir = 0; + // Choose just one of the two directions to move in. + if (_currentDirection) + { + dx = 0; + } + else + { + dy = 0; + } + + _currentDirection = !_currentDirection; + } + + + if (dy == -1 && dx == 0) + { + bits = 1; + } + else if (dy == 1 && dx == 0) + { + bits = 2; + } + else if (dy == 0 && dx == -1) + { + bits = 3; + } + else if (dy == -1 && dx == -1) + { + bits = 4; + } + else if (dy == 1 && dx == -1) + { + bits = 5; + } + else if (dy == 0 && dx == 1) + { + bits = 6; + } + else if (dy == -1 && dx == 1) + { + bits = 7; + } + else if (dy == 1 && dx == 1) + { + bits = 8; + } + + // + // Move the mouse closer to its destination in either X or Y + // (but not both) + if (_currentMove.XSteps > 0 && dx != 0) + { + _currentMove.XSteps--; + + if (_currentMove.XSteps == 0) + { + _currentMove.DX = 0; + } + } + + if (_currentMove.YSteps > 0 && dy != 0) + { + _currentMove.YSteps--; + + if (_currentMove.YSteps == 0) + { + _currentMove.DY = 0; + } + } + + if (_currentMove.XSteps == 0 && _currentMove.YSteps == 0) + { + _currentMove = null; } } + _lock.ExitReadLock(); + _pollCounter++; return bits; } @@ -200,27 +274,60 @@ namespace Contralto.IO new MemoryRange(0xfe18, 0xfe1b), // UTILIN: 177030-177033 }; + AltoSystem _system; + // Mouse buttons: AltoMouseButton _buttons; // Keyset switches: AltoKeysetKey _keyset; - /// - /// Where the mouse is currently reported to be - /// - private int _mouseX; - private int _mouseY; + private ReaderWriterLockSlim _lock; + + // Used to control the rate of mouse movement data + // + public int _pollCounter; /// /// Where the mouse is moving to every time PollMouseBits is called. - /// - private int _xSteps; - private int _xDir; - private double _ySteps; - private int _yDir; + /// + private Queue _moves; + private MouseMovement _currentMove; + private bool _currentDirection; + + private class MouseMovement + { + public MouseMovement(int xsteps, int ysteps, int dx, int dy) + { + XSteps = xsteps; + YSteps = ysteps; + DX = dx; + DY = dy; + + // + // Calculate the rate at which mouse data should be returned in PollMouseBits, + // this is a function of the distance moved in this movement. We assume that the + // movement occurred in 1/60th of a second; PollMouseBits is invoked (via <-MOUSE) + // by the MRT approximately every 1/13000th of a second. + // This is all approximate and not expected to be completely accurate. + // + double distance = Math.Sqrt(Math.Pow(xsteps, 2) + Math.Pow(ysteps, 2)); + + PollRate = (int)((13000.0 / 120.0) / (distance + 1)); + + if (PollRate == 0) + { + PollRate = 1; + } + } + + public int XSteps; + public int YSteps; + public int DX; + public int DY; + public int PollRate; + } - private ReaderWriterLockSlim _lock; } } diff --git a/Contralto/IO/OrbitController.cs b/Contralto/IO/OrbitController.cs index bde5f97..59ca781 100644 --- a/Contralto/IO/OrbitController.cs +++ b/Contralto/IO/OrbitController.cs @@ -596,7 +596,7 @@ namespace Contralto.IO _image[x, wordAddress] = (ushort)(inputWord | inkBit); } - private void RefreshCallback(ulong timeNsec, ulong skewNsec, object context) + private void RefreshCallback(ulong skewNsec, object context) { _refresh = true; diff --git a/Contralto/IO/TridentController.cs b/Contralto/IO/TridentController.cs index f65c012..1737eb8 100644 --- a/Contralto/IO/TridentController.cs +++ b/Contralto/IO/TridentController.cs @@ -305,7 +305,7 @@ namespace Contralto.IO } } - private void OutputFifoCallback(ulong timeNsec, ulong skewNsec, object context) + private void OutputFifoCallback(ulong skewNsec, object context) { switch (_commandState) { @@ -701,7 +701,7 @@ namespace Contralto.IO } } - private void ReadWordCallback(ulong timeNsec, ulong skewNsec, object context) + private void ReadWordCallback(ulong skewNsec, object context) { if (_readWordCount > 0) { @@ -862,7 +862,7 @@ namespace Contralto.IO } } - private void SectorCallback(ulong timeNsec, ulong skewNsec, object context) + private void SectorCallback(ulong skewNsec, object context) { // Move to the next sector if the controller is running // and the disk is ready. diff --git a/Contralto/IO/TridentDrive.cs b/Contralto/IO/TridentDrive.cs index e9e7788..136254e 100644 --- a/Contralto/IO/TridentDrive.cs +++ b/Contralto/IO/TridentDrive.cs @@ -271,7 +271,7 @@ namespace Contralto.IO } } - private void SeekCallback(ulong timeNsec, ulong skewNsec, object context) + private void SeekCallback(ulong skewNsec, object context) { Log.Write(LogComponent.TridentDisk, "Seek to {0} complete.", _destCylinder); diff --git a/Contralto/Logging/Log.cs b/Contralto/Logging/Log.cs index 744d050..405a0db 100644 --- a/Contralto/Logging/Log.cs +++ b/Contralto/Logging/Log.cs @@ -53,6 +53,7 @@ namespace Contralto.Logging TridentController = 0x200000, TridentDisk = 0x400000, + Scripting = 0x2000000, Debug = 0x40000000, All = 0x7fffffff } diff --git a/Contralto/Memory/Memory.cs b/Contralto/Memory/Memory.cs index 950bcd3..d5cd494 100644 --- a/Contralto/Memory/Memory.cs +++ b/Contralto/Memory/Memory.cs @@ -83,7 +83,7 @@ namespace Contralto.Memory // Check for XM registers; this occurs regardless of XM flag since it's in the I/O page. if (address >= _xmBanksStart && address < _xmBanksStart + 16) { - // NB: While not specified in documentatino, some code (IFS in particular) relies on the fact that + // NB: While not specified in documentation, some code (IFS in particular) relies on the fact that // the upper 12 bits of the bank registers are all 1s. return (ushort)(0xfff0 | _xmBanks[address - _xmBanksStart]); } @@ -113,7 +113,7 @@ namespace Contralto.Memory } else { - address += 0x10000 * GetBankNumber(task, extendedMemory); + address += 0x10000 * GetBankNumber(task, extendedMemory); _mem[address] = data; } } diff --git a/Contralto/Program.cs b/Contralto/Program.cs index 20cbf01..f77cbb3 100644 --- a/Contralto/Program.cs +++ b/Contralto/Program.cs @@ -15,30 +15,66 @@ along with ContrAlto. If not, see . */ +using Contralto.Scripting; using Contralto.SdlUI; using System; using System.Windows.Forms; namespace Contralto { + public static class StartupOptions + { + public static string ConfigurationFile; + + public static string ScriptFile; + } + class Program { [STAThread] static void Main(string[] args) { // - // Check for command-line arguments. - // We expect at most one argument, specifying a configuration file to load. - // - StartupArgs = args; - if (args.Length > 1) + // Check for command-line arguments. + // + if (args.Length > 0) { - Console.WriteLine("usage: Contralto "); - return; - } + for (int i = 0; i < args.Length; i++) + { + switch (args[i++].ToLowerInvariant()) + { + case "-config": + if (i < args.Length) + { + StartupOptions.ConfigurationFile = args[i]; + } + else + { + PrintUsage(); + return; + } + break; - // Handle command-line args - PrintHerald(); + case "-script": + if (i < args.Length) + { + StartupOptions.ScriptFile = args[i]; + } + else + { + PrintUsage(); + return; + } + break; + + default: + PrintUsage(); + return; + } + } + } + + PrintHerald(); _system = new AltoSystem(); @@ -122,8 +158,6 @@ namespace Contralto } } - public static string[] StartupArgs; - private static void OnProcessExit(object sender, EventArgs e) { Console.WriteLine("Exiting..."); @@ -138,11 +172,16 @@ namespace Contralto private static void PrintHerald() { - Console.WriteLine("ContrAlto v{0} (c) 2015-2017 Living Computers: Museum+Labs.", typeof(Program).Assembly.GetName().Version); + Console.WriteLine("ContrAlto v{0} (c) 2015-2018 Living Computers: Museum+Labs.", typeof(Program).Assembly.GetName().Version); Console.WriteLine("Bug reports to joshd@livingcomputers.org"); Console.WriteLine(); - } - + } + + private static void PrintUsage() + { + Console.WriteLine("Usage: ContrAlto [-config ] [-script ]"); + } + private static AltoSystem _system; } } diff --git a/Contralto/Properties/AssemblyInfo.cs b/Contralto/Properties/AssemblyInfo.cs index 95db7f9..e48733f 100644 --- a/Contralto/Properties/AssemblyInfo.cs +++ b/Contralto/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Living Computers: Museum+Labs")] [assembly: AssemblyProduct("ContrAlto")] -[assembly: AssemblyCopyright("Copyright © Living Computers: Museum+Labs 2015-2017")] +[assembly: AssemblyCopyright("Copyright © Living Computers: Museum+Labs 2015-2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.2")] -[assembly: AssemblyFileVersion("1.2.2.0")] +[assembly: AssemblyVersion("1.2.3")] +[assembly: AssemblyFileVersion("1.2.3.0")] diff --git a/Contralto/Properties/Resources.Designer.cs b/Contralto/Properties/Resources.Designer.cs index f1afe5c..8413905 100644 --- a/Contralto/Properties/Resources.Designer.cs +++ b/Contralto/Properties/Resources.Designer.cs @@ -60,6 +60,69 @@ namespace Contralto.Properties { } } + /// + /// Looks up a localized string similar to Alto Diablo Disk Images (*.dsk, *.dsk44)|*.dsk;*.dsk44|Diablo 31 Disk Images (*.dsk)|*.dsk|Diablo 44 Disk Images (*.dsk44)|*.dsk44|All Files (*.*)|*.*. + /// + internal static string DiabloFilter { + get { + return ResourceManager.GetString("DiabloFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while creating new disk image: {0}. + /// + internal static string DiskCreateErrorText { + get { + return ResourceManager.GetString("DiskCreateErrorText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image creation error. + /// + internal static string DiskCreateErrorTitle { + get { + return ResourceManager.GetString("DiskCreateErrorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An error occurred while loading image: {0}. + /// + internal static string DiskLoadErrorText { + get { + return ResourceManager.GetString("DiskLoadErrorText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image load error. + /// + internal static string DiskLoadErrorTitle { + get { + return ResourceManager.GetString("DiskLoadErrorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select image to load into {0} drive {1}. + /// + internal static string DiskLoadTitle { + get { + return ResourceManager.GetString("DiskLoadTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select path for new {0} image for drive {1}. + /// + internal static string DiskNewTitle { + get { + return ResourceManager.GetString("DiskNewTitle", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -80,6 +143,24 @@ namespace Contralto.Properties { } } + /// + /// Looks up a localized string similar to Unable to save {0} disk {1}'s contents during unload. Error {2]. Any changes have been lost.. + /// + internal static string DiskSaveErrorText { + get { + return ResourceManager.GetString("DiskSaveErrorText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image unload error. + /// + internal static string DiskSaveErrorTitle { + get { + return ResourceManager.GetString("DiskSaveErrorTitle", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -109,5 +190,185 @@ namespace Contralto.Properties { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized string similar to Alto Mouse/Keyboard captured. Press Alt to release.. + /// + internal static string MouseCaptureActiveText { + get { + return ResourceManager.GetString("MouseCaptureActiveText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Click on display to capture Alto Mouse/Keyboard.. + /// + internal static string MouseCaptureInactiveText { + get { + return ResourceManager.GetString("MouseCaptureInactiveText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <no image loaded>. + /// + internal static string NoImageLoadedText { + get { + return ResourceManager.GetString("NoImageLoadedText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Script playback in Progress.. + /// + internal static string PlaybackInProgressText { + get { + return ResourceManager.GetString("PlaybackInProgressText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Script recording in Progress.. + /// + internal static string RecordingInProgressText { + get { + return ResourceManager.GetString("RecordingInProgressText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Screenshot.png. + /// + internal static string ScreenshotDefaultFileName { + get { + return ResourceManager.GetString("ScreenshotDefaultFileName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not save screenshot. Check the specified filename and path and try again.. + /// + internal static string ScreenshotErrorText { + get { + return ResourceManager.GetString("ScreenshotErrorText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PNG Images (*.png)|*.png|All Files (*.*)|*.*. + /// + internal static string ScreenshotFilter { + get { + return ResourceManager.GetString("ScreenshotFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select destination for screenshot.. + /// + internal static string ScreenshotTitle { + get { + return ResourceManager.GetString("ScreenshotTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alto Script Files (*.script)|*.script|All Files (*.*)|*.*. + /// + internal static string ScriptFilter { + get { + return ResourceManager.GetString("ScriptFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select script file to play. + /// + internal static string ScriptPlaybackTitle { + get { + return ResourceManager.GetString("ScriptPlaybackTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select path for script file to record. + /// + internal static string ScriptRecordTitle { + get { + return ResourceManager.GetString("ScriptRecordTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play Script.... + /// + internal static string StartPlaybackText { + get { + return ResourceManager.GetString("StartPlaybackText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Record Script.... + /// + internal static string StartRecordingText { + get { + return ResourceManager.GetString("StartRecordingText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop Playback. + /// + internal static string StopPlaybackText { + get { + return ResourceManager.GetString("StopPlaybackText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Stop Recording. + /// + internal static string StopRecordingText { + get { + return ResourceManager.GetString("StopRecordingText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alto Stopped due to error. See Debugger.. + /// + internal static string SystemErrorText { + get { + return ResourceManager.GetString("SystemErrorText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alto Running.. + /// + internal static string SystemRunningText { + get { + return ResourceManager.GetString("SystemRunningText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alto Stopped.. + /// + internal static string SystemStoppedText { + get { + return ResourceManager.GetString("SystemStoppedText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alto Trident Disk Images (*.dsk80, *.dsk300)|*.dsk80;*.dsk300|Trident T80 Disk Images (*.dsk80)|*.dsk80|Trident T300 Disk Images (*.dsk300)|*.dsk300|All Files (*.*)|*.*. + /// + internal static string TridentFilter { + get { + return ResourceManager.GetString("TridentFilter", resourceCulture); + } + } } } diff --git a/Contralto/Properties/Resources.resx b/Contralto/Properties/Resources.resx index f616cb2..9aba521 100644 --- a/Contralto/Properties/Resources.resx +++ b/Contralto/Properties/Resources.resx @@ -117,6 +117,27 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Alto Diablo Disk Images (*.dsk, *.dsk44)|*.dsk;*.dsk44|Diablo 31 Disk Images (*.dsk)|*.dsk|Diablo 44 Disk Images (*.dsk44)|*.dsk44|All Files (*.*)|*.* + + + An error occurred while creating new disk image: {0} + + + Image creation error + + + An error occurred while loading image: {0} + + + Image load error + + + Select image to load into {0} drive {1} + + + Select path for new {0} image for drive {1} + ..\Resources\DiskNoAccess.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -124,6 +145,12 @@ ..\Resources\DiskRead.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + Unable to save {0} disk {1}'s contents during unload. Error {2]. Any changes have been lost. + + + Image unload error + ..\resources\diskseek.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -133,4 +160,64 @@ ..\Resources\dragon.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + Alto Mouse/Keyboard captured. Press Alt to release. + + + Click on display to capture Alto Mouse/Keyboard. + + + <no image loaded> + + + Script playback in Progress. + + + Script recording in Progress. + + + Screenshot.png + + + Could not save screenshot. Check the specified filename and path and try again. + + + PNG Images (*.png)|*.png|All Files (*.*)|*.* + + + Select destination for screenshot. + + + Alto Script Files (*.script)|*.script|All Files (*.*)|*.* + + + Select script file to play + + + Select path for script file to record + + + Play Script... + + + Record Script... + + + Stop Playback + + + Stop Recording + + + Alto Stopped due to error. See Debugger. + + + Alto Running. + + + Alto Stopped. + + + Alto Trident Disk Images (*.dsk80, *.dsk300)|*.dsk80;*.dsk300|Trident T80 Disk Images (*.dsk80)|*.dsk80|Trident T300 Disk Images (*.dsk300)|*.dsk300|All Files (*.*)|*.* + \ No newline at end of file diff --git a/Contralto/Scheduler.cs b/Contralto/Scheduler.cs index 86f26e2..9ec5107 100644 --- a/Contralto/Scheduler.cs +++ b/Contralto/Scheduler.cs @@ -18,17 +18,15 @@ using System; using System.Collections.Generic; - namespace Contralto { /// /// The SchedulerEventCallback describes a delegate that is invoked whenever a scheduled event has /// reached its due-date and is fired. - /// - /// The current Alto time (in nsec) + /// /// The delta between the requested exec time and the actual exec time (in nsec) /// An object containing context useful to the scheduler of the event - public delegate void SchedulerEventCallback(ulong timeNsec, ulong skewNsec, object context); + public delegate void SchedulerEventCallback(ulong skewNsec, object context); /// /// An Event encapsulates a callback and associated context that is scheduled for a future timestamp. @@ -77,12 +75,17 @@ namespace Contralto /// /// The Scheduler class provides infrastructure for scheduling Alto time-based hardware events /// (for example, sector marks, or video task wakeups). + /// + /// Note that the Scheduler is not thread-safe and must only be used from the emulation thread, + /// or else things will break. This is not optimal -- having a thread-safe scheduler would make + /// it easier/cleaner to deal with asynchronous things like ethernet packets and scripting events + /// but doing so incurs about a 10% performance penalty so it's been avoided. /// public class Scheduler { public Scheduler() { - Reset(); + Reset(); } public ulong CurrentTimeNsec @@ -105,13 +108,14 @@ namespace Contralto // // See if we have any events waiting to fire at this timestep. - // + // while (_schedule.Top != null && _currentTimeNsec >= _schedule.Top.TimestampNsec) { // Pop the top event and fire the callback. - Event e = _schedule.Pop(); - e.EventCallback(_currentTimeNsec, _currentTimeNsec - e.TimestampNsec, e.Context); - } + Event e = _schedule.Pop(); + + e.EventCallback(_currentTimeNsec - e.TimestampNsec, e.Context); + } } /// @@ -129,27 +133,19 @@ namespace Contralto #endif e.TimestampNsec += _currentTimeNsec; + _schedule.Push(e); return e; } - /// - /// Remove an event from the schedule. - /// - /// - public void CancelEvent(Event e) - { - _schedule.Remove(e); - } - private ulong _currentTimeNsec; private SchedulerQueue _schedule; // 170nsec is approximately one Alto system clock cycle and is the time-base for // the scheduler. - private const ulong _timeStepNsec = 170; + private const ulong _timeStepNsec = 170; } /// @@ -215,7 +211,7 @@ namespace Contralto Event e = _top; _queue.RemoveFirst(); - _top = _queue.First.Value; + _top = _queue.First != null ? _queue.First.Value : null; return e; } diff --git a/Contralto/SdlUI/ConsoleExecutor.cs b/Contralto/Scripting/CommandExecutor.cs similarity index 90% rename from Contralto/SdlUI/ConsoleExecutor.cs rename to Contralto/Scripting/CommandExecutor.cs index a996ad7..8f1743d 100644 --- a/Contralto/SdlUI/ConsoleExecutor.cs +++ b/Contralto/Scripting/CommandExecutor.cs @@ -21,34 +21,48 @@ using System.Reflection; using System.Text; using System.IO; -namespace Contralto.SdlUI +namespace Contralto.Scripting { + public class MethodInvokeInfo + { + public MethodInvokeInfo(MethodInfo method, object instance) + { + if (method == null || instance == null) + { + throw new ArgumentNullException("method and instance must be non-null"); + } + + Method = method; + Instance = instance; + } + + public MethodInfo Method; + public object Instance; + } /// /// Defines a node in the debug command tree. /// public class DebuggerCommand { - public DebuggerCommand(object instance, string name, String description, String usage, MethodInfo method) + public DebuggerCommand(string name, String description, String usage, MethodInvokeInfo methodInvoke) { - Instance = instance; Name = name.Trim().ToLower(); Description = description; Usage = usage; - Methods = new List(4); + Methods = new List(4); - if (method != null) + if (methodInvoke != null) { - Methods.Add(method); + Methods.Add(methodInvoke); } SubCommands = new List(); } - - public object Instance; + public string Name; public string Description; public string Usage; - public List Methods; + public List Methods; public List SubCommands; public override string ToString() @@ -63,7 +77,7 @@ namespace Contralto.SdlUI } } - public void AddSubNode(List words, MethodInfo method, object instance) + public void AddSubNode(List words, MethodInvokeInfo methodInfo) { // We should never hit this case. if (words.Count == 0) @@ -77,13 +91,13 @@ namespace Contralto.SdlUI if (subNode == null) { // No, it has not -- create one and add it now. - subNode = new DebuggerCommand(instance, words[0], null, null, null); + subNode = new DebuggerCommand(words[0], null, null, null); this.SubCommands.Add(subNode); if (words.Count == 1) { // This is the last stop -- set the method and be done with it now. - subNode.Methods.Add(method); + subNode.Methods.Add(methodInfo); // early return. return; @@ -98,10 +112,10 @@ namespace Contralto.SdlUI // If we're on the last word at this point then this is an overloaded command. // Check that we don't have any other commands with this number of arguments. // - int argCount = method.GetParameters().Length; - foreach (MethodInfo info in subNode.Methods) + int argCount = methodInfo.Method.GetParameters().Length; + foreach (MethodInvokeInfo info in subNode.Methods) { - if (info.GetParameters().Length == argCount) + if (info.Method.GetParameters().Length == argCount) { throw new InvalidOperationException("Duplicate overload for console command"); } @@ -110,7 +124,7 @@ namespace Contralto.SdlUI // // We're ok. Add it to the method list. // - subNode.Methods.Add(method); + subNode.Methods.Add(methodInfo); // and return early. return; @@ -119,7 +133,7 @@ namespace Contralto.SdlUI // We have more words to go. words.RemoveAt(0); - subNode.AddSubNode(words, method, instance); + subNode.AddSubNode(words, methodInfo); } public DebuggerCommand FindSubNodeByName(string name) @@ -138,37 +152,21 @@ namespace Contralto.SdlUI return found; } } - - public class ConsoleExecutor + + public enum CommandResult { - public ConsoleExecutor(params object[] commandObjects) + Normal, + Quit, + QuitNoSave, + } + + public class CommandExecutor + { + public CommandExecutor(params object[] commandObjects) { List commandList = new List(commandObjects); BuildCommandTree(commandList); - - _consolePrompt = new DebuggerPrompt(_commandRoot); - } - - public CommandResult Prompt() - { - CommandResult next = CommandResult.Normal; - try - { - // Get the command string from the prompt. - string command = _consolePrompt.Prompt().Trim(); - - if (command != String.Empty) - { - next = ExecuteLine(command); - } - } - catch (Exception e) - { - Console.WriteLine(e.Message); - } - - return next; - } + } public CommandResult ExecuteScript(string scriptFile) { @@ -191,6 +189,16 @@ namespace Contralto.SdlUI return state; } + public DebuggerCommand CommandTreeRoot + { + get { return _commandRoot; } + } + + public CommandResult ExecuteCommand(string line) + { + return ExecuteLine(line); + } + private CommandResult ExecuteLine(string line) { CommandResult next = CommandResult.Normal; @@ -226,17 +234,17 @@ namespace Contralto.SdlUI } private CommandResult InvokeConsoleMethod(DebuggerCommand command, string[] args) - { - MethodInfo method = null; + { + MethodInvokeInfo method = null; // // Find the method that matches the arg count we were passed // (i.e. handle overloaded commands). // That this only matches on argument count is somewhat of a kluge... // - foreach (MethodInfo m in command.Methods) + foreach (MethodInvokeInfo m in command.Methods) { - ParameterInfo[] paramInfo = m.GetParameters(); + ParameterInfo[] paramInfo = m.Method.GetParameters(); if (args == null && paramInfo.Length == 0 || paramInfo.Length == args.Length) @@ -253,7 +261,7 @@ namespace Contralto.SdlUI throw new ArgumentException(String.Format("Invalid argument count to command.")); } - ParameterInfo[] parameterInfo = method.GetParameters(); + ParameterInfo[] parameterInfo = method.Method.GetParameters(); object[] invokeParams; if (args == null) @@ -352,7 +360,7 @@ namespace Contralto.SdlUI // If we've made it THIS far, then we were able to parse all the commands into what they should be. // Invoke the method on the object instance associated with the command. // - return (CommandResult)method.Invoke(command.Instance, invokeParams); + return (CommandResult)method.Method.Invoke(method.Instance, invokeParams); } enum ParseState @@ -627,7 +635,7 @@ namespace Contralto.SdlUI // this cast should always succeed given that we're filtering for this type above. DebuggerFunction function = (DebuggerFunction)attribs[0]; - DebuggerCommand newCommand = new DebuggerCommand(commandObject, function.CommandName, function.Description, function.Usage, info); + DebuggerCommand newCommand = new DebuggerCommand(function.CommandName, function.Description, function.Usage, new MethodInvokeInfo(info, commandObject)); _commandList.Add(newCommand); } @@ -635,7 +643,7 @@ namespace Contralto.SdlUI } // Now actually build the command tree from the above list! - _commandRoot = new DebuggerCommand(null, "Root", null, null, null); + _commandRoot = new DebuggerCommand("Root", null, null, null); foreach (DebuggerCommand c in _commandList) { @@ -643,7 +651,7 @@ namespace Contralto.SdlUI // This is kind of ugly, we know that at this point every command built above have only // one method. When building the tree, overloaded commands may end up with more than one. - _commandRoot.AddSubNode(new List(commandWords), c.Methods[0], c.Instance); + _commandRoot.AddSubNode(new List(commandWords), c.Methods[0]); } } @@ -664,8 +672,7 @@ namespace Contralto.SdlUI return CommandResult.Normal; } - - private DebuggerPrompt _consolePrompt; + private DebuggerCommand _commandRoot; private List _commandList; } diff --git a/Contralto/Scripting/ControlCommands.cs b/Contralto/Scripting/ControlCommands.cs new file mode 100644 index 0000000..51192e7 --- /dev/null +++ b/Contralto/Scripting/ControlCommands.cs @@ -0,0 +1,219 @@ +using Contralto.SdlUI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + public class ControlCommands + { + public ControlCommands(AltoSystem system, ExecutionController controller) + { + _system = system; + _controller = controller; + } + + [DebuggerFunction("quit", "Exits ContrAlto.")] + private CommandResult Quit() + { + _controller.StopExecution(); + return CommandResult.Quit; + } + + [DebuggerFunction("quit without saving", "Exits ContrAlto without committing changes to Diablo disk packs.")] + private CommandResult QuitNoSave() + { + _controller.StopExecution(); + return CommandResult.QuitNoSave; + } + + [DebuggerFunction("start", "Starts the emulated Alto normally.")] + private CommandResult Start() + { + if (_controller.IsRunning) + { + Console.WriteLine("Alto is already running."); + } + else + { + _controller.StartExecution(AlternateBootType.None); + Console.WriteLine("Alto started."); + } + + return CommandResult.Normal; + } + + [DebuggerFunction("stop", "Stops the emulated Alto.")] + private CommandResult Stop() + { + _controller.StopExecution(); + Console.WriteLine("Alto stopped."); + + return CommandResult.Normal; + } + + [DebuggerFunction("reset", "Resets the emulated Alto.")] + private CommandResult Reset() + { + _controller.Reset(AlternateBootType.None); + Console.WriteLine("Alto reset."); + + return CommandResult.Normal; + } + + [DebuggerFunction("start with keyboard disk boot", "Starts the emulated Alto with the specified keyboard disk boot address.")] + private CommandResult StartDisk() + { + if (_controller.IsRunning) + { + _controller.Reset(AlternateBootType.Disk); + } + else + { + _controller.StartExecution(AlternateBootType.Disk); + } + + return CommandResult.Normal; + } + + [DebuggerFunction("start with keyboard net boot", "Starts the emulated Alto with the specified keyboard ethernet boot number.")] + private CommandResult StartNet() + { + if (_controller.IsRunning) + { + _controller.Reset(AlternateBootType.Ethernet); + } + else + { + _controller.StartExecution(AlternateBootType.Ethernet); + } + + return CommandResult.Normal; + } + + [DebuggerFunction("load disk", "Loads the specified drive with the requested disk image.", " ")] + private CommandResult LoadDisk(ushort drive, string path) + { + if (drive > 1) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Load the new pack. + _system.LoadDiabloDrive(drive, path, false); + Console.WriteLine("Drive {0} loaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("unload disk", "Unloads the specified drive.", "")] + private CommandResult UnloadDisk(ushort drive) + { + if (drive > 1) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Unload the current pack. + _system.UnloadDiabloDrive(drive); + Console.WriteLine("Drive {0} unloaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("new disk", "Creates and loads a new image for the specified drive.", "")] + private CommandResult NewDisk(ushort drive, string path) + { + if (drive > 1) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Unload the current pack. + _system.LoadDiabloDrive(drive, path, true); + Console.WriteLine("Drive {0} created and loaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("load trident", "Loads the specified trident drive with the requested disk image.", " ")] + private CommandResult LoadTrident(ushort drive, string path) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Load the new pack. + _system.LoadTridentDrive(drive, path, false); + Console.WriteLine("Trident {0} loaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("unload trident", "Unloads the specified trident drive.", "")] + private CommandResult UnloadTrident(ushort drive) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Unload the current pack. + _system.UnloadTridentDrive(drive); + Console.WriteLine("Trident {0} unloaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("new trident", "Creates and loads a new image for the specified drive.", "")] + private CommandResult NewTrident(ushort drive, string path) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Unload the current pack. + _system.LoadTridentDrive(drive, path, true); + Console.WriteLine("Trident {0} created and loaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("set ethernet address", "Sets the Alto's host Ethernet address.")] + private CommandResult SetEthernetAddress(byte address) + { + if (address == 0 || address == 0xff) + { + Console.WriteLine("Address {0} is invalid.", Conversion.ToOctal(address)); + } + else + { + Configuration.HostAddress = address; + } + + return CommandResult.Normal; + } + + [DebuggerFunction("set keyboard net boot file", "Sets the boot file used for net booting.")] + private CommandResult SetKeyboardBootFile(ushort file) + { + Configuration.BootFile = file; + return CommandResult.Normal; + } + + [DebuggerFunction("set keyboard disk boot address", "Sets the boot address used for disk booting.")] + private CommandResult SetKeyboardBootAddress(ushort address) + { + Configuration.BootFile = address; + return CommandResult.Normal; + } + + + private AltoSystem _system; + private ExecutionController _controller; + } +} diff --git a/Contralto/SdlUI/DebuggerAttributes.cs b/Contralto/Scripting/DebuggerAttributes.cs similarity index 98% rename from Contralto/SdlUI/DebuggerAttributes.cs rename to Contralto/Scripting/DebuggerAttributes.cs index 331f16e..1d384ff 100644 --- a/Contralto/SdlUI/DebuggerAttributes.cs +++ b/Contralto/Scripting/DebuggerAttributes.cs @@ -18,7 +18,7 @@ using System; -namespace Contralto.SdlUI +namespace Contralto.Scripting { public class DebuggerFunction : Attribute diff --git a/Contralto/Scripting/ScriptAction.cs b/Contralto/Scripting/ScriptAction.cs new file mode 100644 index 0000000..50aee28 --- /dev/null +++ b/Contralto/Scripting/ScriptAction.cs @@ -0,0 +1,701 @@ +using Contralto.IO; +using Contralto.SdlUI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + + /// + /// Base class for scripting actions. + /// "Timestamp" provides a relative timestamp (in nsec) for the action. + /// "Completed" indicates whether the action completed during the last execution. + /// Actions can run multiple times by leaving Completed = false and adjusting the + /// Timestamp appropriately; the playback engine will reschedule it in this case. + /// + public abstract class ScriptAction + { + public ScriptAction(ulong timestamp) + { + _timestamp = timestamp; + } + + /// + /// Relative timestamp for this action. + /// + public ulong Timestamp + { + get { return _timestamp; } + } + + /// + /// Whether the action has completed after the last + /// Replay action + /// + public bool Completed + { + get { return _completed; } + } + + /// + /// Replays a single step of the action. If the action is completed, + /// Completed will be true afterwards. + /// + /// + /// + public abstract void Replay(AltoSystem system, ExecutionController controller); + + /// + /// Constructs the proper ScriptAction from a given line of text + /// + /// + /// + public static ScriptAction Parse(string line) + { + // + // An Action consists of a line in the format: + // [args] + // + // specifies a time relative to the last action, and may be: + // - a 64-bit integer indicating a time in nanoseconds + // - a double-precision floating point integer ending with "ms" indicating time in milliseconds + // - a "-", indicating a relative time of zero. (a "0" also works). + // + string[] tokens = line.Split(new char[] { ' ', ',' }); + + if (tokens.Length < 2) + { + throw new InvalidOperationException("Invalid Action format."); + } + + ulong timestamp = 0; + + if (tokens[0] != "-") + { + if (tokens[0].ToLowerInvariant().EndsWith("ms")) + { + // timestamp in msec + double fstamp = double.Parse(tokens[0].Substring(0, tokens[0].Length - 2)); + + timestamp = (ulong)(fstamp * Conversion.MsecToNsec); + } + else + { + // assume timestamp in nsec + timestamp = ulong.Parse(tokens[0]); + } + } + + switch(tokens[1]) + { + case "KeyDown": + return KeyAction.Parse(timestamp, true, tokens); + + case "KeyUp": + return KeyAction.Parse(timestamp, false, tokens); + + case "MouseDown": + return MouseButtonAction.Parse(timestamp, true, tokens); + + case "MouseUp": + return MouseButtonAction.Parse(timestamp, false, tokens); + + case "MouseMove": + return MouseMoveAction.Parse(timestamp, false, tokens); + + case "MouseMoveAbsolute": + return MouseMoveAction.Parse(timestamp, true, tokens); + + case "Command": + return CommandAction.Parse(timestamp, tokens); + + case "KeyStroke": + return KeyStrokeAction.Parse(timestamp, tokens); + + case "Type": + return TypeAction.Parse(timestamp, false, tokens); + + case "TypeLine": + return TypeAction.Parse(timestamp, true, tokens); + + case "Wait": + return WaitAction.Parse(timestamp, true, tokens); + + default: + throw new InvalidOperationException("Invalid Action"); + + } + } + + protected ulong _timestamp; + protected bool _completed; + } + + + /// + /// Injects a single key action (up or down) into the Alto's keyboard. + /// + public class KeyAction : ScriptAction + { + public KeyAction(ulong timestamp, AltoKey key, bool keyDown) : base(timestamp) + { + _key = key; + _keyDown = keyDown; + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + if (_keyDown) + { + system.Keyboard.KeyDown(_key); + } + else + { + system.Keyboard.KeyUp(_key); + } + + _completed = true; + } + + public override string ToString() + { + return String.Format("{0} {1} {2}", _timestamp, _keyDown ? "KeyDown" : "KeyUp", _key); + } + + public static KeyAction Parse(ulong timestamp, bool keyDown, string[] tokens) + { + if (tokens.Length != 3) + { + throw new InvalidOperationException("Invalid KeyAction syntax."); + } + + AltoKey key = (AltoKey)Enum.Parse(typeof(AltoKey), tokens[2]); + + return new KeyAction(timestamp, key, keyDown); + + } + + private AltoKey _key; + private bool _keyDown; + } + + /// + /// Injects a single mouse button action (up or down) into the Alto's Mouse. + /// + public class MouseButtonAction : ScriptAction + { + public MouseButtonAction(ulong timestamp, AltoMouseButton buttons, bool mouseDown) : base(timestamp) + { + _buttons = buttons; + _mouseDown = mouseDown; + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + if (_mouseDown) + { + system.MouseAndKeyset.MouseDown(_buttons); + } + else + { + system.MouseAndKeyset.MouseUp(_buttons); + } + + _completed = true; + } + + public override string ToString() + { + return String.Format("{0} {1} {2}", _timestamp, _mouseDown ? "MouseDown" : "MouseUp", _buttons); + } + + public static MouseButtonAction Parse(ulong timestamp, bool mouseDown, string[] tokens) + { + if (tokens.Length != 3) + { + throw new InvalidOperationException("Invalid MouseButtonAction syntax."); + } + + AltoMouseButton button = (AltoMouseButton)Enum.Parse(typeof(AltoMouseButton), tokens[2]); + + return new MouseButtonAction(timestamp, button, mouseDown); + + } + + private AltoMouseButton _buttons; + private bool _mouseDown; + } + + /// + /// Injects a mouse movement into the Alto's mouse. + /// + public class MouseMoveAction : ScriptAction + { + public MouseMoveAction(ulong timestamp, int dx, int dy, bool absolute) : base(timestamp) + { + _dx = dx; + _dy = dy; + _absolute = absolute; + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + if (_absolute) + { + // + // We stuff the x/y coordinates into the well-defined memory locations for the mouse coordinates. + // + system.Memory.Load(0x114, (ushort)_dx, CPU.TaskType.Emulator, false); + system.Memory.Load(0x115, (ushort)_dy, CPU.TaskType.Emulator, false); + } + else + { + system.MouseAndKeyset.MouseMove(_dx, _dy); + } + + _completed = true; + } + + public override string ToString() + { + return String.Format("{0} {1} {2},{3}", _timestamp, _absolute ? "MouseMoveAbsolute" : "MouseMove", _dx, _dy); + } + + public static MouseMoveAction Parse(ulong timestamp, bool absolute, string[] tokens) + { + if (tokens.Length != 4) + { + throw new InvalidOperationException("Invalid MouseMoveAction syntax."); + } + + int dx = int.Parse(tokens[2]); + int dy = int.Parse(tokens[3]); + + return new MouseMoveAction(timestamp, dx, dy, absolute); + } + + private int _dx; + private int _dy; + private bool _absolute; + } + + /// + /// Injects a command execution to control the Alto system. See ControlCommands for + /// the actual commands. + /// + public class CommandAction : ScriptAction + { + public CommandAction(ulong timestamp, string commandString) : base(timestamp) + { + _commandString = commandString; + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + // + // Execute the command. + // + // TODO: recreating these objects each time through is uncool. + // + ControlCommands controlCommands = new ControlCommands(system, controller); + CommandExecutor executor = new CommandExecutor(controlCommands); + + CommandResult res = executor.ExecuteCommand(_commandString); + + if (res == CommandResult.Quit || + res == CommandResult.QuitNoSave) + { + // + // Force an exit, commit disks if result was Quit. + // + throw new ShutdownException(res == CommandResult.Quit); + } + + _completed = true; + } + + public override string ToString() + { + return String.Format("{0} Command {1}", _timestamp, _commandString); + } + + public static CommandAction Parse(ulong timestamp, string[] tokens) + { + if (tokens.Length < 3) + { + throw new InvalidOperationException("Invalid Command syntax."); + } + + StringBuilder commandString = new StringBuilder(); + + for (int i = 2; i < tokens.Length; i++) + { + commandString.AppendFormat("{0} ", tokens[i]); + } + + return new CommandAction(timestamp, commandString.ToString()); + + } + + private string _commandString; + } + + /// + /// Injects one or more simultaneous keystrokes (keydown followed by keyup) into the + /// Alto's keyboard. + /// + public class KeyStrokeAction : ScriptAction + { + public KeyStrokeAction(ulong timestamp, AltoKey[] keys) : base(timestamp) + { + _keys = keys; + _keyDown = true; + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + // + // Press all requested keys simultaneously, then release them. + // + foreach(AltoKey key in _keys) + { + if (_keyDown) + { + system.Keyboard.KeyDown(key); + } + else + { + system.Keyboard.KeyUp(key); + } + } + + if (_keyDown) + { + // Delay 50ms, then repeat for keyup + _keyDown = false; + _completed = false; + _timestamp = 50 * Conversion.MsecToNsec; + } + else + { + _completed = true; + } + } + + public override string ToString() + { + StringBuilder keyString = new StringBuilder(); + + foreach(AltoKey key in _keys) + { + keyString.AppendFormat("{0} ", key); + } + + return String.Format("{0} KeyStroke {1}", _timestamp, keyString.ToString()); + } + + public static KeyStrokeAction Parse(ulong timestamp, string[] tokens) + { + if (tokens.Length < 3) + { + throw new InvalidOperationException("Invalid KeyStroke syntax."); + } + + AltoKey[] keys = new AltoKey[tokens.Length - 2]; + + for (int i = 2; i < tokens.Length; i++) + { + keys[i - 2] = (AltoKey)Enum.Parse(typeof(AltoKey), tokens[i]); + } + + return new KeyStrokeAction(timestamp, keys); + } + + private AltoKey[] _keys; + private bool _keyDown; + } + + /// + /// Injects a sequence of keystrokes corresponding to the keystrokes needed to + /// type the provided string. + /// + public class TypeAction : ScriptAction + { + static TypeAction() + { + BuildKeyMap(); + } + + public TypeAction(ulong timestamp, string text, bool cr) : base(timestamp) + { + _text = text; + _cr = cr; + _currentStroke = 0; + + BuildStrokeList(text); + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + if (_currentStroke >= _strokes.Count) + { + _completed = true; + } + else + { + Keystroke stroke = _strokes[_currentStroke++]; + + if (stroke.Type == StrokeType.KeyDown) + { + system.Keyboard.KeyDown(stroke.Key); + } + else + { + system.Keyboard.KeyUp(stroke.Key); + } + + // Delay 50ms before the next key + _timestamp = 50 * Conversion.MsecToNsec; + } + } + + public override string ToString() + { + return String.Format("{0} {1} {2}", _timestamp, _cr ? "TypeLine" : "Type", _text); + } + + public static TypeAction Parse(ulong timestamp, bool cr, string[] tokens) + { + if (tokens.Length < 2) + { + throw new InvalidOperationException("Invalid TypeAction syntax."); + } + + StringBuilder commandString = new StringBuilder(); + + for (int i = 2; i < tokens.Length; i++) + { + commandString.AppendFormat(i == tokens.Length - 1 ? "{0}" : "{0} ", tokens[i]); + } + + return new TypeAction(timestamp, commandString.ToString(), cr); + + } + + private void BuildStrokeList(string text) + { + _strokes = new List(); + + foreach (char c in text) + { + // + // For capital letters or shifted symbols, we need to depress Shift first + // (and release it when done). + // + bool shifted = _shiftedKeyMap.ContainsKey(c); + AltoKey charKey; + if (shifted) + { + Keystroke shift = new Keystroke(StrokeType.KeyDown, AltoKey.RShift); + _strokes.Add(shift); + + charKey = _shiftedKeyMap[c]; + } + else + { + if (_unmodifiedKeyMap.ContainsKey(c)) + { + charKey = _unmodifiedKeyMap[c]; + } + else + { + // Ignore this keystroke. + continue; + } + } + + _strokes.Add(new Keystroke(StrokeType.KeyDown, charKey)); + _strokes.Add(new Keystroke(StrokeType.KeyUp, charKey)); + + if (shifted) + { + Keystroke unshift = new Keystroke(StrokeType.KeyUp, AltoKey.RShift); + _strokes.Add(unshift); + } + } + + if (_cr) + { + // Add a Return keystroke to the end + _strokes.Add(new Keystroke(StrokeType.KeyDown, AltoKey.Return)); + _strokes.Add(new Keystroke(StrokeType.KeyUp, AltoKey.Return)); + } + } + + private enum StrokeType + { + KeyDown, + KeyUp + } + + private struct Keystroke + { + public Keystroke(StrokeType type, AltoKey key) + { + Type = type; + Key = key; + } + + public StrokeType Type; + public AltoKey Key; + } + + + private static void BuildKeyMap() + { + _unmodifiedKeyMap = new Dictionary(); + _shiftedKeyMap = new Dictionary(); + + // characters requiring no modifiers + _unmodifiedKeyMap.Add('1', AltoKey.D1); + _unmodifiedKeyMap.Add('2', AltoKey.D2); + _unmodifiedKeyMap.Add('3', AltoKey.D3); + _unmodifiedKeyMap.Add('4', AltoKey.D4); + _unmodifiedKeyMap.Add('5', AltoKey.D5); + _unmodifiedKeyMap.Add('6', AltoKey.D6); + _unmodifiedKeyMap.Add('7', AltoKey.D7); + _unmodifiedKeyMap.Add('8', AltoKey.D8); + _unmodifiedKeyMap.Add('9', AltoKey.D9); + _unmodifiedKeyMap.Add('0', AltoKey.D0); + _unmodifiedKeyMap.Add('-', AltoKey.Minus); + _unmodifiedKeyMap.Add('=', AltoKey.Plus); + _unmodifiedKeyMap.Add('\\', AltoKey.BSlash); + _unmodifiedKeyMap.Add('q', AltoKey.Q); + _unmodifiedKeyMap.Add('w', AltoKey.W); + _unmodifiedKeyMap.Add('e', AltoKey.E); + _unmodifiedKeyMap.Add('r', AltoKey.R); + _unmodifiedKeyMap.Add('t', AltoKey.T); + _unmodifiedKeyMap.Add('y', AltoKey.Y); + _unmodifiedKeyMap.Add('u', AltoKey.U); + _unmodifiedKeyMap.Add('i', AltoKey.I); + _unmodifiedKeyMap.Add('o', AltoKey.O); + _unmodifiedKeyMap.Add('p', AltoKey.P); + _unmodifiedKeyMap.Add('[', AltoKey.LBracket); + _unmodifiedKeyMap.Add(']', AltoKey.RBracket); + _unmodifiedKeyMap.Add('_', AltoKey.Arrow); + _unmodifiedKeyMap.Add('a', AltoKey.A); + _unmodifiedKeyMap.Add('s', AltoKey.S); + _unmodifiedKeyMap.Add('d', AltoKey.D); + _unmodifiedKeyMap.Add('f', AltoKey.F); + _unmodifiedKeyMap.Add('g', AltoKey.G); + _unmodifiedKeyMap.Add('h', AltoKey.H); + _unmodifiedKeyMap.Add('j', AltoKey.J); + _unmodifiedKeyMap.Add('k', AltoKey.K); + _unmodifiedKeyMap.Add('l', AltoKey.L); + _unmodifiedKeyMap.Add(';', AltoKey.Semicolon); + _unmodifiedKeyMap.Add('\'', AltoKey.Quote); + _unmodifiedKeyMap.Add('z', AltoKey.Z); + _unmodifiedKeyMap.Add('x', AltoKey.X); + _unmodifiedKeyMap.Add('c', AltoKey.C); + _unmodifiedKeyMap.Add('v', AltoKey.V); + _unmodifiedKeyMap.Add('b', AltoKey.B); + _unmodifiedKeyMap.Add('n', AltoKey.N); + _unmodifiedKeyMap.Add('m', AltoKey.M); + _unmodifiedKeyMap.Add(',', AltoKey.Comma); + _unmodifiedKeyMap.Add('.', AltoKey.Period); + _unmodifiedKeyMap.Add('/', AltoKey.FSlash); + _unmodifiedKeyMap.Add(' ', AltoKey.Space); + + // characters requiring a shift modifier + _shiftedKeyMap.Add('!', AltoKey.D1); + _shiftedKeyMap.Add('@', AltoKey.D2); + _shiftedKeyMap.Add('#', AltoKey.D3); + _shiftedKeyMap.Add('$', AltoKey.D4); + _shiftedKeyMap.Add('%', AltoKey.D5); + _shiftedKeyMap.Add('~', AltoKey.D6); + _shiftedKeyMap.Add('&', AltoKey.D7); + _shiftedKeyMap.Add('*', AltoKey.D8); + _shiftedKeyMap.Add('(', AltoKey.D9); + _shiftedKeyMap.Add(')', AltoKey.D0); + _shiftedKeyMap.Add('|', AltoKey.BSlash); + _shiftedKeyMap.Add('Q', AltoKey.Q); + _shiftedKeyMap.Add('W', AltoKey.W); + _shiftedKeyMap.Add('E', AltoKey.E); + _shiftedKeyMap.Add('R', AltoKey.R); + _shiftedKeyMap.Add('T', AltoKey.T); + _shiftedKeyMap.Add('Y', AltoKey.Y); + _shiftedKeyMap.Add('U', AltoKey.U); + _shiftedKeyMap.Add('I', AltoKey.I); + _shiftedKeyMap.Add('O', AltoKey.O); + _shiftedKeyMap.Add('P', AltoKey.P); + _shiftedKeyMap.Add('{', AltoKey.LBracket); + _shiftedKeyMap.Add('}', AltoKey.RBracket); + _shiftedKeyMap.Add('^', AltoKey.Arrow); + _shiftedKeyMap.Add('A', AltoKey.A); + _shiftedKeyMap.Add('S', AltoKey.S); + _shiftedKeyMap.Add('D', AltoKey.D); + _shiftedKeyMap.Add('F', AltoKey.F); + _shiftedKeyMap.Add('G', AltoKey.G); + _shiftedKeyMap.Add('H', AltoKey.H); + _shiftedKeyMap.Add('J', AltoKey.J); + _shiftedKeyMap.Add('K', AltoKey.K); + _shiftedKeyMap.Add('L', AltoKey.L); + _shiftedKeyMap.Add(':', AltoKey.Semicolon); + _shiftedKeyMap.Add('"', AltoKey.Quote); + _shiftedKeyMap.Add('Z', AltoKey.Z); + _shiftedKeyMap.Add('X', AltoKey.X); + _shiftedKeyMap.Add('C', AltoKey.C); + _shiftedKeyMap.Add('V', AltoKey.V); + _shiftedKeyMap.Add('B', AltoKey.B); + _shiftedKeyMap.Add('N', AltoKey.N); + _shiftedKeyMap.Add('M', AltoKey.M); + _shiftedKeyMap.Add('<', AltoKey.Comma); + _shiftedKeyMap.Add('>', AltoKey.Period); + _shiftedKeyMap.Add('?', AltoKey.FSlash); + } + + + private string _text; + private List _strokes; + private int _currentStroke; + private bool _cr; + + private static Dictionary _unmodifiedKeyMap; + private static Dictionary _shiftedKeyMap; + } + + /// + /// Causes the Playback engine to wait until the Alto executes a wakeup STARTF. + /// + public class WaitAction : ScriptAction + { + public WaitAction(ulong timestamp) : base(timestamp) + { + + } + + public override void Replay(AltoSystem system, ExecutionController controller) + { + // This is a no-op. + _completed = true; + } + + public override string ToString() + { + return String.Format("{0} Wait", _timestamp); + } + + public static WaitAction Parse(ulong timestamp, bool keyDown, string[] tokens) + { + if (tokens.Length != 2) + { + throw new InvalidOperationException("Invalid WaitAction syntax."); + } + + return new WaitAction(timestamp); + } + } +} diff --git a/Contralto/Scripting/ScriptManager.cs b/Contralto/Scripting/ScriptManager.cs new file mode 100644 index 0000000..d2b7054 --- /dev/null +++ b/Contralto/Scripting/ScriptManager.cs @@ -0,0 +1,126 @@ +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + public static class ScriptManager + { + static ScriptManager() + { + _scheduler = new Scheduler(); + } + + public static Scheduler ScriptScheduler + { + get { return _scheduler; } + } + + /// + /// Fired when playback of a script has completed or is stopped. + /// + public static event EventHandler PlaybackCompleted; + + public static void StartRecording(AltoSystem system, string scriptPath) + { + // Stop any pending actions + StopRecording(); + StopPlayback(); + + _scriptRecorder = new ScriptRecorder(system, scriptPath); + + Log.Write(LogComponent.Scripting, "Starting recording to {0}", scriptPath); + + // + // Record the absolute position of the mouse (as held in MOUSELOC in system memory). + // All other mouse movements in the script will be recorded relative to this point. + // + int x = system.Memory.Read(0x114, CPU.TaskType.Ethernet, false); + int y = system.Memory.Read(0x115, CPU.TaskType.Ethernet, false); + _scriptRecorder.MouseMoveAbsolute(x, y); + } + + public static void StopRecording() + { + if (IsRecording) + { + _scriptRecorder.End(); + _scriptRecorder = null; + } + + Log.Write(LogComponent.Scripting, "Stopped recording."); + } + + public static void StartPlayback(AltoSystem system, ExecutionController controller, string scriptPath) + { + // Stop any pending actions + StopRecording(); + StopPlayback(); + + _scheduler.Reset(); + + _scriptPlayback = new ScriptPlayback(scriptPath, system, controller); + _scriptPlayback.PlaybackCompleted += OnPlaybackCompleted; + _scriptPlayback.Start(); + + Log.Write(LogComponent.Scripting, "Starting playback of {0}", scriptPath); + } + + public static void StopPlayback() + { + if (IsPlaying) + { + _scriptPlayback.Stop(); + _scriptPlayback = null; + + PlaybackCompleted(null, null); + } + + Log.Write(LogComponent.Scripting, "Stopped playback."); + } + + public static void CompleteWait() + { + if (IsPlaying) + { + _scriptPlayback.Start(); + + Log.Write(LogComponent.Scripting, "Playback resumed after Wait."); + } + } + + public static ScriptRecorder Recorder + { + get { return _scriptRecorder; } + } + + public static ScriptPlayback Playback + { + get { return _scriptPlayback; } + } + + public static bool IsRecording + { + get { return _scriptRecorder != null; } + } + + public static bool IsPlaying + { + get { return _scriptPlayback != null; } + } + + private static void OnPlaybackCompleted(object sender, EventArgs e) + { + _scriptPlayback = null; + PlaybackCompleted(null, null); + } + + private static ScriptRecorder _scriptRecorder; + private static ScriptPlayback _scriptPlayback; + + private static Scheduler _scheduler; + } +} diff --git a/Contralto/Scripting/ScriptPlayback.cs b/Contralto/Scripting/ScriptPlayback.cs new file mode 100644 index 0000000..55b832d --- /dev/null +++ b/Contralto/Scripting/ScriptPlayback.cs @@ -0,0 +1,108 @@ +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + public class ScriptPlayback + { + public ScriptPlayback(string scriptFile, AltoSystem system, ExecutionController controller) + { + _scriptReader = new ScriptReader(scriptFile); + _system = system; + _controller = controller; + + _currentAction = null; + + _stopPlayback = false; + } + + /// + /// Fired when playback of the script has completed or is stopped. + /// + public event EventHandler PlaybackCompleted; + + public void Start() + { + _stopPlayback = false; + + // Schedule first event. + ScheduleNextEvent(0); + } + + public void Stop() + { + // We will stop after the next event is fired (if any) + _stopPlayback = true; + } + + private void ScheduleNextEvent(ulong skewNsec) + { + // + // Grab the next action if the current one is done. + // + if (_currentAction == null || _currentAction.Completed) + { + _currentAction = _scriptReader.ReadNext(); + } + + if (_currentAction != null) + { + // We have another action to queue up. + Event scriptEvent = new Event(_currentAction.Timestamp, _currentAction, OnEvent); + ScriptManager.ScriptScheduler.Schedule(scriptEvent); + + Log.Write(LogComponent.Scripting, "Queueing script action {0}", _currentAction); + } + else + { + // + // Playback is complete. + // + Log.Write(LogComponent.Scripting, "Playback completed."); + PlaybackCompleted(this, null); + } + } + + private void OnEvent(ulong skewNsec, object context) + { + // Replay the action. + if (!_stopPlayback) + { + ScriptAction action = (ScriptAction)context; + Log.Write(LogComponent.Scripting, "Invoking action {0}", action); + + action.Replay(_system, _controller); + + // Special case for Wait -- this causes the script to stop here until the + // Alto itself tells things to start up again. + // + if (action is WaitAction) + { + Log.Write(LogComponent.Scripting, "Playback paused, awaiting wakeup from Alto."); + } + else + { + // Kick off the next action in the script. + ScheduleNextEvent(skewNsec); + } + } + else + { + Log.Write(LogComponent.Scripting, "Playback stopped."); + PlaybackCompleted(this, null); + } + } + + private AltoSystem _system; + private ExecutionController _controller; + private ScriptReader _scriptReader; + + private ScriptAction _currentAction; + + private bool _stopPlayback; + } +} diff --git a/Contralto/Scripting/ScriptReader.cs b/Contralto/Scripting/ScriptReader.cs new file mode 100644 index 0000000..0b2b185 --- /dev/null +++ b/Contralto/Scripting/ScriptReader.cs @@ -0,0 +1,66 @@ +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + public class ScriptReader + { + public ScriptReader(string scriptPath) + { + _scriptReader = new StreamReader(scriptPath); + } + + public ScriptAction ReadNext() + { + if (_scriptReader == null) + { + return null; + } + + // + // Read the next action from the script file, + // skipping over comments and empty lines. + // + while (true) + { + if (_scriptReader.EndOfStream) + { + // End of the stream, return null to indicate this, + // and close the stream. + _scriptReader.Close(); + _scriptReader = null; + return null; + } + + string line = _scriptReader.ReadLine().Trim(); + + // Skip empty or comment lines. + if (string.IsNullOrWhiteSpace(line) || + line.StartsWith("#")) + { + continue; + } + + try + { + return ScriptAction.Parse(line); + } + catch(Exception e) + { + Log.Write(LogComponent.Scripting, "Invalid script; error: {0}.", e.Message); + _scriptReader.Close(); + _scriptReader = null; + return null; + } + } + } + + private StreamReader _scriptReader; + + } +} diff --git a/Contralto/Scripting/ScriptRecorder.cs b/Contralto/Scripting/ScriptRecorder.cs new file mode 100644 index 0000000..a727a4c --- /dev/null +++ b/Contralto/Scripting/ScriptRecorder.cs @@ -0,0 +1,123 @@ +using Contralto.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + /// + /// Records actions. + /// + public class ScriptRecorder + { + public ScriptRecorder(AltoSystem system, string scriptFile) + { + _script = new ScriptWriter(scriptFile); + _system = system; + _lastTimestamp = 0; + + _firstTime = true; + } + + public void End() + { + _script.End(); + } + + public void KeyDown(AltoKey key) + { + _script.AppendAction( + new KeyAction( + GetRelativeTimestamp(), + key, + true)); + } + + public void KeyUp(AltoKey key) + { + _script.AppendAction( + new KeyAction( + GetRelativeTimestamp(), + key, + false)); + } + + public void MouseDown(AltoMouseButton button) + { + _script.AppendAction( + new MouseButtonAction( + GetRelativeTimestamp(), + button, + true)); + } + + public void MouseUp(AltoMouseButton button) + { + _script.AppendAction( + new MouseButtonAction( + GetRelativeTimestamp(), + button, + false)); + } + + public void MouseMoveRelative(int dx, int dy) + { + _script.AppendAction( + new MouseMoveAction( + GetRelativeTimestamp(), + dx, + dy, + false)); + } + + public void MouseMoveAbsolute(int dx, int dy) + { + _script.AppendAction( + new MouseMoveAction( + GetRelativeTimestamp(), + dx, + dy, + true)); + } + + public void Command(string command) + { + _script.AppendAction( + new CommandAction( + GetRelativeTimestamp(), + command)); + } + + private ulong GetRelativeTimestamp() + { + if (_firstTime) + { + _firstTime = false; + // + // First item recorded, occurs at relative timestamp 0. + // + _lastTimestamp = ScriptManager.ScriptScheduler.CurrentTimeNsec; + return 0; + } + else + { + // + // relative time is delta between current system timestamp and the last + // recorded entry. + ulong relativeTimestamp = ScriptManager.ScriptScheduler.CurrentTimeNsec - _lastTimestamp; + _lastTimestamp = ScriptManager.ScriptScheduler.CurrentTimeNsec; + + return relativeTimestamp; + } + } + + private bool _enabled; + + private AltoSystem _system; + private ulong _lastTimestamp; + private bool _firstTime; + private ScriptWriter _script; + } +} diff --git a/Contralto/Scripting/ScriptWriter.cs b/Contralto/Scripting/ScriptWriter.cs new file mode 100644 index 0000000..e69a5a3 --- /dev/null +++ b/Contralto/Scripting/ScriptWriter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.Scripting +{ + /// + /// + /// + public class ScriptWriter + { + public ScriptWriter(string scriptPath) + { + _scriptWriter = new StreamWriter(scriptPath); + } + + /// + /// Adds a new ScriptAction to the queue + /// + /// + public void AppendAction(ScriptAction action) + { + if (_scriptWriter == null) + { + throw new InvalidOperationException("Cannot write to closed ScriptWriter."); + } + + _scriptWriter.WriteLine(action.ToString()); + } + + public void End() + { + _scriptWriter.Close(); + _scriptWriter = null; + } + + private StreamWriter _scriptWriter; + } +} diff --git a/Contralto/SdlUI/DebuggerPrompt.cs b/Contralto/SdlUI/DebuggerPrompt.cs index 4d96408..90e05d1 100644 --- a/Contralto/SdlUI/DebuggerPrompt.cs +++ b/Contralto/SdlUI/DebuggerPrompt.cs @@ -16,6 +16,7 @@ */ +using Contralto.Scripting; using System; using System.Collections.Generic; using System.Text; diff --git a/Contralto/SdlUI/SdlAltoWindow.cs b/Contralto/SdlUI/SdlAltoWindow.cs index 6017f28..b9b1e46 100644 --- a/Contralto/SdlUI/SdlAltoWindow.cs +++ b/Contralto/SdlUI/SdlAltoWindow.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using SDL2; using Contralto.Display; using Contralto.IO; +using Contralto.Scripting; namespace Contralto.SdlUI { @@ -84,23 +85,38 @@ namespace Contralto.SdlUI break; case SDL.SDL_EventType.SDL_MOUSEMOTION: - MouseMove(e.motion.x, e.motion.y); + if (!ScriptManager.IsPlaying) + { + MouseMove(e.motion.x, e.motion.y); + } break; case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN: - MouseDown(e.button.button, e.button.x, e.button.y); + if (!ScriptManager.IsPlaying) + { + MouseDown(e.button.button, e.button.x, e.button.y); + } break; case SDL.SDL_EventType.SDL_MOUSEBUTTONUP: - MouseUp(e.button.button); + if (!ScriptManager.IsPlaying) + { + MouseUp(e.button.button); + } break; case SDL.SDL_EventType.SDL_KEYDOWN: - KeyDown(e.key.keysym.sym); + if (!ScriptManager.IsPlaying) + { + KeyDown(e.key.keysym.sym); + } break; case SDL.SDL_EventType.SDL_KEYUP: - KeyUp(e.key.keysym.sym); + if (!ScriptManager.IsPlaying) + { + KeyUp(e.key.keysym.sym); + } break; default: @@ -243,9 +259,9 @@ namespace Contralto.SdlUI byte b = _1bppDisplayBuffer[i]; for (int bit = 7; bit >= 0; bit--) { - byte color = (byte)((b & (1 << bit)) == 0 ? 0x00 : 0xff); + uint color = (b & (1 << bit)) == 0 ? 0xff000000 : 0xffffffff; - _32bppDisplayBuffer[rgbIndex++] = (int)((color == 0) ? 0xff000000 : 0xffffffff); + _32bppDisplayBuffer[rgbIndex++] = (int)(color); } } } @@ -312,7 +328,7 @@ namespace Contralto.SdlUI int dy = y - my; if (dx != 0 || dy != 0) - { + { _system.MouseAndKeyset.MouseMove(dx, dy); // Don't handle the very next Mouse Move event (which will just be the motion we caused in the diff --git a/Contralto/SdlUI/SdlConsole.cs b/Contralto/SdlUI/SdlConsole.cs index c67a408..2aaa243 100644 --- a/Contralto/SdlUI/SdlConsole.cs +++ b/Contralto/SdlUI/SdlConsole.cs @@ -15,17 +15,12 @@ along with ContrAlto. If not, see . */ +using Contralto.Scripting; using System; using System.Threading; namespace Contralto.SdlUI -{ - public enum CommandResult - { - Normal, - Quit - } - +{ /// /// Provides a command-line interface to ContrAlto controls, /// as a substitute for the GUI interface of the Windows version. @@ -37,9 +32,13 @@ namespace Contralto.SdlUI _system = system; _controller = new ExecutionController(_system); - _controller.ErrorCallback += OnExecutionError; - } + _controller.ErrorCallback += OnExecutionError; + _controller.ShutdownCallback += OnShutdown; + _commitDisksAtShutdown = true; + + ScriptManager.PlaybackCompleted += OnScriptPlaybackCompleted; + } /// /// Invoke the CLI loop in a separate thread. @@ -54,6 +53,13 @@ namespace Contralto.SdlUI _cliThread = new Thread(RunCliThread); _cliThread.Start(); + + if (!string.IsNullOrWhiteSpace(StartupOptions.ScriptFile)) + { + Console.WriteLine("Starting playback of script {0}", StartupOptions.ScriptFile); + ScriptManager.StartPlayback(_system, _controller, StartupOptions.ScriptFile); + _controller.StartExecution(AlternateBootType.None); + } } /// @@ -61,25 +67,42 @@ namespace Contralto.SdlUI /// private void RunCliThread() { - ConsoleExecutor executor = new ConsoleExecutor(this); + ControlCommands controlCommands = new ControlCommands(_system, _controller); + CommandExecutor executor = new CommandExecutor(this, controlCommands); + DebuggerPrompt prompt = new DebuggerPrompt(executor.CommandTreeRoot); + CommandResult state = CommandResult.Normal; while (state != CommandResult.Quit) { - state = executor.Prompt(); + state = CommandResult.Normal; + try + { + // Get the command string from the prompt. + string command = prompt.Prompt().Trim(); + + if (command != String.Empty) + { + state = executor.ExecuteCommand(command); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } } // // Ensure the emulator is stopped. // - _controller.StopExecution(); + _controller.StopExecution(); // // Ensure the main window is closed. // _mainWindow.Close(); } - + private void OnMainWindowClosed(object sender, EventArgs e) { // @@ -87,7 +110,7 @@ namespace Contralto.SdlUI // _controller.StopExecution(); - _system.Shutdown(true /* commit disk contents */); + _system.Shutdown(_commitDisksAtShutdown); // // The Alto window was closed, shut down the CLI. @@ -95,6 +118,14 @@ namespace Contralto.SdlUI _cliThread.Abort(); } + private void OnShutdown(bool commitDisks) + { + _commitDisksAtShutdown = commitDisks; + + // Close the main window, this will cause everything else to shut down. + _mainWindow.Close(); + } + /// /// Error handling /// @@ -105,125 +136,14 @@ namespace Contralto.SdlUI System.Diagnostics.Debugger.Break(); } - [DebuggerFunction("quit", "Exits ContrAlto.")] - private CommandResult Quit() + private void OnScriptPlaybackCompleted(object sender, EventArgs e) { - _controller.StopExecution(); - return CommandResult.Quit; + Console.WriteLine("Script playback completed."); } // // Console commands // - [DebuggerFunction("start", "Starts the emulated Alto normally.")] - private CommandResult Start() - { - if (_controller.IsRunning) - { - Console.WriteLine("Alto is already running."); - } - else - { - _controller.StartExecution(AlternateBootType.None); - Console.WriteLine("Alto started."); - } - - return CommandResult.Normal; - } - - [DebuggerFunction("stop", "Stops the emulated Alto.")] - private CommandResult Stop() - { - _controller.StopExecution(); - Console.WriteLine("Alto stopped."); - - return CommandResult.Normal; - } - - [DebuggerFunction("reset", "Resets the emulated Alto.")] - private CommandResult Reset() - { - _controller.Reset(AlternateBootType.None); - Console.WriteLine("Alto reset."); - - return CommandResult.Normal; - } - - [DebuggerFunction("start with keyboard disk boot", "Starts the emulated Alto with the specified keyboard disk boot address.")] - private CommandResult StartDisk() - { - if (_controller.IsRunning) - { - _controller.Reset(AlternateBootType.Disk); - } - else - { - _controller.StartExecution(AlternateBootType.Disk); - } - - return CommandResult.Normal; - } - - [DebuggerFunction("start with keyboard net boot", "Starts the emulated Alto with the specified keyboard ethernet boot number.")] - private CommandResult StartNet() - { - if (_controller.IsRunning) - { - _controller.Reset(AlternateBootType.Ethernet); - } - else - { - _controller.StartExecution(AlternateBootType.Ethernet); - } - - return CommandResult.Normal; - } - - [DebuggerFunction("load disk", "Loads the specified drive with the requested disk image.", " ")] - private CommandResult LoadDisk(ushort drive, string path) - { - if (drive > 1) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Load the new pack. - _system.LoadDiabloDrive(drive, path, false); - Console.WriteLine("Drive {0} loaded.", drive); - - return CommandResult.Normal; - } - - [DebuggerFunction("unload disk", "Unloads the specified drive.", "")] - private CommandResult UnloadDisk(ushort drive) - { - if (drive > 1) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Unload the current pack. - _system.UnloadDiabloDrive(drive); - Console.WriteLine("Drive {0} unloaded.", drive); - - return CommandResult.Normal; - } - - [DebuggerFunction("new disk", "Creates and loads a new image for the specified drive.", "")] - private CommandResult NewDisk(ushort drive, string path) - { - if (drive > 1) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Unload the current pack. - _system.LoadDiabloDrive(drive, path, true); - Console.WriteLine("Drive {0} created and loaded.", drive); - - return CommandResult.Normal; - } - [DebuggerFunction("show disk", "Displays the contents of the specified drive.", "")] private CommandResult ShowDisk(ushort drive) { @@ -245,52 +165,7 @@ namespace Contralto.SdlUI } return CommandResult.Normal; - } - - [DebuggerFunction("load trident", "Loads the specified trident drive with the requested disk image.", " ")] - private CommandResult LoadTrident(ushort drive, string path) - { - if (drive > 7) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Load the new pack. - _system.LoadTridentDrive(drive, path, false); - Console.WriteLine("Trident {0} loaded.", drive); - - return CommandResult.Normal; - } - - [DebuggerFunction("unload trident", "Unloads the specified trident drive.", "")] - private CommandResult UnloadTrident(ushort drive) - { - if (drive > 7) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Unload the current pack. - _system.UnloadTridentDrive(drive); - Console.WriteLine("Trident {0} unloaded.", drive); - - return CommandResult.Normal; - } - - [DebuggerFunction("new trident", "Creates and loads a new image for the specified drive.", "")] - private CommandResult NewTrident(ushort drive, string path) - { - if (drive > 7) - { - throw new InvalidOperationException("Drive specification out of range."); - } - - // Unload the current pack. - _system.LoadTridentDrive(drive, path, true); - Console.WriteLine("Trident {0} created and loaded.", drive); - - return CommandResult.Normal; - } + } [DebuggerFunction("show trident", "Displays the contents of the specified trident drive.", "")] private CommandResult ShowTrident(ushort drive) @@ -320,22 +195,7 @@ namespace Contralto.SdlUI { Console.WriteLine("System type is {0}", Configuration.SystemType); return CommandResult.Normal; - } - - [DebuggerFunction("set ethernet address", "Sets the Alto's host Ethernet address.")] - private CommandResult SetEthernetAddress(byte address) - { - if (address == 0 || address == 0xff) - { - Console.WriteLine("Address {0} is invalid.", Conversion.ToOctal(address)); - } - else - { - Configuration.HostAddress = address; - } - - return CommandResult.Normal; - } + } [DebuggerFunction("show ethernet address", "Displays the Alto's host Ethernet address.")] private CommandResult ShowEthernetAddress() @@ -358,20 +218,78 @@ namespace Contralto.SdlUI return CommandResult.Normal; } - [DebuggerFunction("set keyboard net boot file", "Sets the boot file used for net booting.")] - private CommandResult SetKeyboardBootFile(ushort file) + [DebuggerFunction("start recording", "Starts recording of inputs to script file")] + private CommandResult StartRecording(string scriptPath) { - Configuration.BootFile = file; + if (ScriptManager.IsPlaying || + ScriptManager.IsRecording) + { + Console.WriteLine("{0} is already in progress.", ScriptManager.IsPlaying ? "Playback" : "Recording"); + } + else + { + Console.WriteLine("Recording to {0} starting.", scriptPath); + ScriptManager.StartRecording(_system, scriptPath); + } return CommandResult.Normal; } - [DebuggerFunction("set keyboard disk boot address", "Sets the boot address used for disk booting.")] - private CommandResult SetKeyboardBootAddress(ushort address) + [DebuggerFunction("stop recording", "Stops script recording")] + private CommandResult StopRecording() { - Configuration.BootFile = address; + if (!ScriptManager.IsRecording) + { + Console.WriteLine("No recording currently in progress."); + } + else + { + ScriptManager.StopRecording(); + Console.WriteLine("Recording stopped."); + } + return CommandResult.Normal; } + [DebuggerFunction("start playback", "Starts playback of script file")] + private CommandResult StartPlayback(string scriptPath) + { + if (ScriptManager.IsPlaying || + ScriptManager.IsRecording) + { + Console.WriteLine("{0} is already in progress.", ScriptManager.IsPlaying ? "Playback" : "Recording"); + } + else + { + Console.WriteLine("Playback of {0} starting.", scriptPath); + // + // Start the script. We need to pause the emulation while doing so, + // in order to avoid concurrency issues with the Scheduler (which is + // not thread-safe). + // + _controller.StopExecution(); + ScriptManager.StartPlayback(_system, _controller, scriptPath); + _controller.StartExecution(AlternateBootType.None); + } + return CommandResult.Normal; + } + + [DebuggerFunction("stop playback", "Stops script playback")] + private CommandResult StopPlayback() + { + if (!ScriptManager.IsPlaying) + { + Console.WriteLine("No playback currently in progress."); + } + else + { + ScriptManager.StopPlayback(); + Console.WriteLine("Playback stopped."); + } + + return CommandResult.Normal; + } + + // Not yet supported on non-Windows platforms /* [DebuggerFunction("enable display interlacing", "Enables interlaced display.")] @@ -442,5 +360,6 @@ namespace Contralto.SdlUI private ExecutionController _controller; private SdlAltoWindow _mainWindow; private Thread _cliThread; + private bool _commitDisksAtShutdown; } } diff --git a/Contralto/UI/AltoWindow.Designer.cs b/Contralto/UI/AltoWindow.Designer.cs index 32078f5..23d150c 100644 --- a/Contralto/UI/AltoWindow.Designer.cs +++ b/Contralto/UI/AltoWindow.Designer.cs @@ -39,11 +39,14 @@ this.drive0ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.loadToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); this.unloadToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.newToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); this.Drive0ImageName = new System.Windows.Forms.ToolStripMenuItem(); this.drive1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.loadToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); this.unloadToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); + this.newToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); this.Drive1ImageName = new System.Windows.Forms.ToolStripMenuItem(); + this.TridentToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.AlternateBootToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SystemEthernetBootMenu = new System.Windows.Forms.ToolStripMenuItem(); this.optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -57,9 +60,9 @@ this.CaptureStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.SystemStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.DisplayBox = new System.Windows.Forms.PictureBox(); - this.newToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.newToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); - this.TridentToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.scriptToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.RecordScriptMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.PlayScriptMenu = new System.Windows.Forms.ToolStripMenuItem(); this._mainMenu.SuspendLayout(); this.StatusLine.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.DisplayBox)).BeginInit(); @@ -81,6 +84,7 @@ // this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.saveScreenshotToolStripMenuItem, + this.scriptToolStripMenuItem, this.exitToolStripMenuItem}); this.fileToolStripMenuItem.Name = "fileToolStripMenuItem"; this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20); @@ -165,6 +169,13 @@ this.unloadToolStripMenuItem1.Text = "Unload"; this.unloadToolStripMenuItem1.Click += new System.EventHandler(this.OnSystemDrive0UnloadClick); // + // newToolStripMenuItem1 + // + this.newToolStripMenuItem1.Name = "newToolStripMenuItem1"; + this.newToolStripMenuItem1.Size = new System.Drawing.Size(172, 22); + this.newToolStripMenuItem1.Text = "New..."; + this.newToolStripMenuItem1.Click += new System.EventHandler(this.OnSystemDrive0NewClick); + // // Drive0ImageName // this.Drive0ImageName.Enabled = false; @@ -197,6 +208,13 @@ this.unloadToolStripMenuItem2.Text = "Unload"; this.unloadToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1UnloadClick); // + // newToolStripMenuItem2 + // + this.newToolStripMenuItem2.Name = "newToolStripMenuItem2"; + this.newToolStripMenuItem2.Size = new System.Drawing.Size(152, 22); + this.newToolStripMenuItem2.Text = "New..."; + this.newToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1NewClick); + // // Drive1ImageName // this.Drive1ImageName.Enabled = false; @@ -204,6 +222,12 @@ this.Drive1ImageName.Size = new System.Drawing.Size(152, 22); this.Drive1ImageName.Text = "Image Name"; // + // TridentToolStripMenuItem + // + this.TridentToolStripMenuItem.Name = "TridentToolStripMenuItem"; + this.TridentToolStripMenuItem.Size = new System.Drawing.Size(223, 22); + this.TridentToolStripMenuItem.Text = "Trident Drives"; + // // AlternateBootToolStripMenuItem // this.AlternateBootToolStripMenuItem.Name = "AlternateBootToolStripMenuItem"; @@ -322,25 +346,32 @@ this.DisplayBox.MouseMove += new System.Windows.Forms.MouseEventHandler(this.OnDisplayMouseMove); this.DisplayBox.MouseUp += new System.Windows.Forms.MouseEventHandler(this.OnDisplayMouseUp); // - // newToolStripMenuItem1 + // scriptToolStripMenuItem // - this.newToolStripMenuItem1.Name = "newToolStripMenuItem1"; - this.newToolStripMenuItem1.Size = new System.Drawing.Size(172, 22); - this.newToolStripMenuItem1.Text = "New..."; - this.newToolStripMenuItem1.Click += new System.EventHandler(this.OnSystemDrive0NewClick); + this.scriptToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.RecordScriptMenu, + this.PlayScriptMenu}); + this.scriptToolStripMenuItem.Name = "scriptToolStripMenuItem"; + this.scriptToolStripMenuItem.Size = new System.Drawing.Size(232, 22); + this.scriptToolStripMenuItem.Text = "Script"; // - // newToolStripMenuItem2 + // RecordScriptMenu // - this.newToolStripMenuItem2.Name = "newToolStripMenuItem2"; - this.newToolStripMenuItem2.Size = new System.Drawing.Size(152, 22); - this.newToolStripMenuItem2.Text = "New..."; - this.newToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1NewClick); + this.RecordScriptMenu.Name = "RecordScriptMenu"; + this.RecordScriptMenu.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Alt) + | System.Windows.Forms.Keys.Q))); + this.RecordScriptMenu.Size = new System.Drawing.Size(219, 22); + this.RecordScriptMenu.Text = "Record Script..."; + this.RecordScriptMenu.Click += new System.EventHandler(this.OnFileRecordScriptClick); // - // TridentToolStripMenuItem + // PlayScriptMenu // - this.TridentToolStripMenuItem.Name = "TridentToolStripMenuItem"; - this.TridentToolStripMenuItem.Size = new System.Drawing.Size(223, 22); - this.TridentToolStripMenuItem.Text = "Trident Drives"; + this.PlayScriptMenu.Name = "PlayScriptMenu"; + this.PlayScriptMenu.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Alt) + | System.Windows.Forms.Keys.V))); + this.PlayScriptMenu.Size = new System.Drawing.Size(219, 22); + this.PlayScriptMenu.Text = "Play Script..."; + this.PlayScriptMenu.Click += new System.EventHandler(this.OnFilePlayScriptClick); // // AltoWindow // @@ -407,5 +438,8 @@ private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem1; private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem2; private System.Windows.Forms.ToolStripMenuItem TridentToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem scriptToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem RecordScriptMenu; + private System.Windows.Forms.ToolStripMenuItem PlayScriptMenu; } } \ No newline at end of file diff --git a/Contralto/UI/AltoWindow.cs b/Contralto/UI/AltoWindow.cs index bd8f282..5dbcfd5 100644 --- a/Contralto/UI/AltoWindow.cs +++ b/Contralto/UI/AltoWindow.cs @@ -19,6 +19,7 @@ using Contralto.CPU; using Contralto.Display; using Contralto.IO; using Contralto.Properties; +using Contralto.Scripting; using Contralto.UI; using System; using System.Collections.Generic; @@ -35,7 +36,7 @@ namespace Contralto public AltoWindow() { InitializeComponent(); - InitKeymap(); + InitKeymap(); _mouseCaptured = false; _currentCursorState = true; @@ -47,6 +48,8 @@ namespace Contralto _lastBuffer = _currentBuffer = _displayData0; _frame = 0; + _commitDisksAtShutdown = true; + try { _frameTimer = new FrameTimer(60.0); @@ -64,7 +67,7 @@ namespace Contralto CreateTridentMenu(); - SystemStatusLabel.Text = _systemStoppedText; + SystemStatusLabel.Text = Resources.SystemStoppedText; DiskStatusLabel.Text = String.Empty; _diskIdleImage = Resources.DiskNoAccess; @@ -85,7 +88,9 @@ namespace Contralto _diskAccessTimer.Interval = 25; _diskAccessTimer.Elapsed += OnDiskTimerElapsed; _diskAccessTimer.Start(); - } + + ScriptManager.PlaybackCompleted += OnScriptPlaybackCompleted; + } public void AttachSystem(AltoSystem system) { @@ -95,17 +100,29 @@ namespace Contralto _controller = new ExecutionController(_system); _controller.ErrorCallback += OnExecutionError; + _controller.ShutdownCallback += OnShutdown; // Update disk image UI info // Diablo disks: - Drive0ImageName.Text = _system.DiskController.Drives[0].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[0].Pack.PackName) : _noImageLoadedText; - Drive1ImageName.Text = _system.DiskController.Drives[1].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[1].Pack.PackName) : _noImageLoadedText; + Drive0ImageName.Text = _system.DiskController.Drives[0].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[0].Pack.PackName) : Resources.NoImageLoadedText; + Drive1ImageName.Text = _system.DiskController.Drives[1].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[1].Pack.PackName) : Resources.NoImageLoadedText; // Trident disks for (int i = 0; i < _tridentImageNames.Count; i++) { TridentDrive drive = _system.TridentController.Drives[i]; - _tridentImageNames[i].Text = drive.IsLoaded ? Path.GetFileName(drive.Pack.PackName) : _noImageLoadedText; + _tridentImageNames[i].Text = drive.IsLoaded ? Path.GetFileName(drive.Pack.PackName) : Resources.NoImageLoadedText; + } + + // + // If a startup script was specified, start it running now -- + // tell the script manager to start the script, and start the + // Alto system running so that the script actually executes. + // + if (!string.IsNullOrWhiteSpace(StartupOptions.ScriptFile)) + { + StartScriptPlayback(StartupOptions.ScriptFile); + _controller.StartExecution(AlternateBootType.None); } } @@ -155,8 +172,8 @@ namespace Contralto catch(Exception ex) { MessageBox.Show( - String.Format("An error occurred while loading image: {0}", ex.Message), - "Image load error", MessageBoxButtons.OK); + String.Format(Resources.DiskLoadErrorText, ex.Message), + Resources.DiskLoadErrorTitle, MessageBoxButtons.OK); } } @@ -169,12 +186,12 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("Unable to save Diablo disk 0's contents during unload. Error {0}. Any changes have been lost.", ex.Message), - "Image unload error", MessageBoxButtons.OK); + String.Format(Resources.DiskSaveErrorText, "Diablo", 0, ex.Message), + Resources.DiskSaveErrorTitle, MessageBoxButtons.OK); } finally { - Drive1ImageName.Text = _noImageLoadedText; + Drive1ImageName.Text = Resources.NoImageLoadedText; Configuration.Drive1Image = String.Empty; } } @@ -197,8 +214,8 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("An error occurred while creating new disk image: {0}", ex.Message), - "Image creation error", MessageBoxButtons.OK); + String.Format(Resources.DiskCreateErrorText, ex.Message), + Resources.DiskCreateErrorTitle, MessageBoxButtons.OK); } } @@ -220,8 +237,8 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("An error occurred while loading image: {0}", ex.Message), - "Image load error", MessageBoxButtons.OK); + String.Format(Resources.DiskLoadErrorText, ex.Message), + Resources.DiskLoadErrorTitle, MessageBoxButtons.OK); } } @@ -234,12 +251,12 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("Unable to save Diablo disk 1's contents during unload. Error {0}. Any changes have been lost.", ex.Message), - "Image unload error", MessageBoxButtons.OK); + String.Format(Resources.DiskSaveErrorText, "Diablo", 1, ex.Message), + Resources.DiskSaveErrorTitle, MessageBoxButtons.OK); } finally { - Drive1ImageName.Text = _noImageLoadedText; + Drive1ImageName.Text = Resources.NoImageLoadedText; Configuration.Drive1Image = String.Empty; } } @@ -263,8 +280,8 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("An error occurred while creating new disk image: {0}", ex.Message), - "Image creation error", MessageBoxButtons.OK); + String.Format(Resources.DiskCreateErrorText, ex.Message), + Resources.DiskCreateErrorTitle, MessageBoxButtons.OK); } } @@ -287,8 +304,8 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("An error occurred while loading Trident image: {0}", ex.Message), - "Image load error", MessageBoxButtons.OK); + String.Format(Resources.DiskLoadErrorText, ex.Message), + Resources.DiskLoadErrorTitle, MessageBoxButtons.OK); } } @@ -303,12 +320,12 @@ namespace Contralto catch(Exception ex) { MessageBox.Show( - String.Format("Unable to save Trident disk {0}'s contents during unload. Error {1}. Any changes have been lost.", drive, ex.Message), - "Image unload error", MessageBoxButtons.OK); + String.Format(Resources.DiskSaveErrorText, "Trident", drive, ex.Message), + Resources.DiskSaveErrorTitle, MessageBoxButtons.OK); } finally { - _tridentImageNames[drive].Text = _noImageLoadedText; + _tridentImageNames[drive].Text = Resources.NoImageLoadedText; Configuration.TridentImages[drive] = String.Empty; } } @@ -332,8 +349,8 @@ namespace Contralto catch (Exception ex) { MessageBox.Show( - String.Format("An error occurred while creating new Trident image: {0}", ex.Message), - "Image creation error", MessageBoxButtons.OK); + String.Format(Resources.DiskCreateErrorText, ex.Message), + Resources.DiskCreateErrorTitle, MessageBoxButtons.OK); } } @@ -400,10 +417,10 @@ namespace Contralto SaveFileDialog fileDialog = new SaveFileDialog(); fileDialog.DefaultExt = "png"; - fileDialog.Filter = "PNG Images (*.png)|*.png|All Files (*.*)|*.*"; - fileDialog.Title = String.Format("Select destination for screenshot."); + fileDialog.Filter = Resources.ScreenshotFilter; + fileDialog.Title = Resources.ScreenshotTitle; fileDialog.CheckPathExists = true; - fileDialog.FileName = "Screenshot.png"; + fileDialog.FileName = Resources.ScreenshotDefaultFileName; DialogResult res = fileDialog.ShowDialog(); @@ -418,7 +435,7 @@ namespace Contralto } catch { - MessageBox.Show("Could not save screenshot. Check the specified filename and path and try again."); + MessageBox.Show(Resources.ScreenshotErrorText); } } @@ -428,10 +445,63 @@ namespace Contralto } } + private void OnFileRecordScriptClick(object sender, EventArgs e) + { + if (!ScriptManager.IsRecording) + { + SaveFileDialog fileDialog = new SaveFileDialog(); + + fileDialog.DefaultExt = "script"; + fileDialog.Filter = Resources.ScriptFilter; + fileDialog.CheckFileExists = false; + fileDialog.CheckPathExists = true; + fileDialog.OverwritePrompt = true; + fileDialog.ValidateNames = true; + fileDialog.Title = Resources.ScriptRecordTitle; + + DialogResult res = fileDialog.ShowDialog(); + + if (res == DialogResult.OK) + { + StartScriptRecording(fileDialog.FileName); + } + } + else + { + StopScriptRecording(); + } + } + + private void OnFilePlayScriptClick(object sender, EventArgs e) + { + if (!ScriptManager.IsPlaying) + { + OpenFileDialog fileDialog = new OpenFileDialog(); + + fileDialog.DefaultExt = "script"; + fileDialog.Filter = Resources.ScriptFilter; + fileDialog.Multiselect = false; + fileDialog.CheckFileExists = true; + fileDialog.CheckPathExists = true; + fileDialog.Title = Resources.ScriptPlaybackTitle; + + DialogResult res = fileDialog.ShowDialog(); + + if (res == DialogResult.OK) + { + StartScriptPlayback(fileDialog.FileName); + } + } + else + { + StopScriptPlayback(); + } + } + private void OnFileExitClick(object sender, EventArgs e) { _controller.StopExecution(); - this.Close(); + this.Close(); } private void OnAltoWindowClosed(object sender, FormClosedEventArgs e) @@ -442,17 +512,17 @@ namespace Contralto _fpsTimer.Stop(); _diskAccessTimer.Stop(); - // Halt the system and detach our display + // Halt the system and detach our display _controller.StopExecution(); _system.DetachDisplay(); - _system.Shutdown(true /* commit disk contents */); + _system.Shutdown(_commitDisksAtShutdown); // // Commit current configuration to disk // - Configuration.WriteConfiguration(); + Configuration.WriteConfiguration(); - DialogResult = DialogResult.OK; + DialogResult = DialogResult.OK; } private string ShowImageLoadDialog(int drive, bool trident) @@ -460,11 +530,11 @@ namespace Contralto OpenFileDialog fileDialog = new OpenFileDialog(); fileDialog.DefaultExt = trident ? "dsk80" : "dsk"; - fileDialog.Filter = trident ? _tridentFilter : _diabloFilter; + fileDialog.Filter = trident ? Resources.TridentFilter : Resources.DiabloFilter; fileDialog.Multiselect = false; fileDialog.CheckFileExists = true; fileDialog.CheckPathExists = true; - fileDialog.Title = String.Format("Select image to load into {0} drive {1}", trident ? "Trident" : "Diablo", drive); + fileDialog.Title = String.Format(Resources.DiskLoadTitle, trident ? "Trident" : "Diablo", drive); DialogResult res = fileDialog.ShowDialog(); @@ -483,12 +553,12 @@ namespace Contralto SaveFileDialog fileDialog = new SaveFileDialog(); fileDialog.DefaultExt = trident ? "dsk80" : "dsk"; - fileDialog.Filter = trident ? _tridentFilter : _diabloFilter; + fileDialog.Filter = trident ? Resources.TridentFilter : Resources.DiabloFilter; fileDialog.CheckFileExists = false; fileDialog.CheckPathExists = true; fileDialog.OverwritePrompt = true; fileDialog.ValidateNames = true; - fileDialog.Title = String.Format("Select path for new {0} image for drive {1}", trident ? "Trident" : "Diablo", drive); + fileDialog.Title = String.Format(Resources.DiskNewTitle, trident ? "Trident" : "Diablo", drive); DialogResult res = fileDialog.ShowDialog(); @@ -510,13 +580,21 @@ namespace Contralto { // TODO: invoke the debugger when an error is hit //OnDebuggerShowClick(null, null); - SystemStatusLabel.Text = _systemErrorText; + SystemStatusLabel.Text = Resources.SystemErrorText; Console.WriteLine("Execution error: {0} - {1}", e.Message, e.StackTrace); System.Diagnostics.Debugger.Break(); } + /// + /// Handle an internal shutdown of the emulator. + /// + private void OnShutdown(bool commitDisks) + { + BeginInvoke(new ShutdownCallbackDelegate(InternalShutdown), commitDisks); + } + private void StartSystem(AlternateBootType bootType) { // Disable "Start" menu item @@ -527,7 +605,7 @@ namespace Contralto _controller.StartExecution(bootType); - SystemStatusLabel.Text = _systemRunningText; + SystemStatusLabel.Text = Resources.SystemRunningText; } // @@ -560,7 +638,7 @@ namespace Contralto } else { - _lastBuffer = _currentBuffer; + _lastBuffer = _currentBuffer; } // Asynchronously render this frame. @@ -702,6 +780,12 @@ namespace Contralto /// protected override bool ProcessKeyEventArgs(ref Message m) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return base.ProcessKeyEventArgs(ref m); + } + // Grab the scancode from the message int scanCode = (int)((m.LParam.ToInt64() >> 16) & 0x1ff); bool down = false; @@ -729,7 +813,7 @@ namespace Contralto { case 0x2a: // LShift modifierKey = AltoKey.LShift; - break; + break; case 0x36: modifierKey = AltoKey.RShift; @@ -758,9 +842,15 @@ namespace Contralto return base.ProcessKeyEventArgs(ref m); } - // Hacky initial implementation of keyboard input. + private void OnKeyDown(object sender, KeyEventArgs e) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return; + } + if (!_mouseCaptured) { return; @@ -786,6 +876,12 @@ namespace Contralto private void OnKeyUp(object sender, KeyEventArgs e) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return; + } + if (!_mouseCaptured) { return; @@ -811,6 +907,12 @@ namespace Contralto private void OnDisplayMouseMove(object sender, MouseEventArgs e) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return; + } + if (!_mouseCaptured) { // We do nothing with mouse input unless we have capture. @@ -844,17 +946,16 @@ namespace Contralto Cursor.Position = DisplayBox.PointToScreen(middle); } - } - - private void HackMouseMove() - { - Point middle = new Point(DisplayBox.Width / 2, DisplayBox.Height / 2); - // Force (invisible) cursor to middle of window - Cursor.Position = DisplayBox.PointToScreen(middle); - } + } private void OnDisplayMouseDown(object sender, MouseEventArgs e) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return; + } + if (!_mouseCaptured) { return; @@ -883,6 +984,12 @@ namespace Contralto private void OnDisplayMouseUp(object sender, MouseEventArgs e) { + // Short-circuit if a script is playing. + if (ScriptManager.IsPlaying) + { + return; + } + if (!_mouseCaptured) { // On mouse-up, capture the mouse if the system is running. @@ -922,7 +1029,7 @@ namespace Contralto _mouseCaptured = true; ShowCursor(false); - CaptureStatusLabel.Text = "Alto Mouse/Keyboard captured. Press Alt to release."; + CaptureStatusLabel.Text = Resources.MouseCaptureActiveText; } private void ReleaseMouse() @@ -930,7 +1037,7 @@ namespace Contralto _mouseCaptured = false; ShowCursor(true); - CaptureStatusLabel.Text = "Click on display to capture Alto Mouse/Keyboard."; + CaptureStatusLabel.Text = Resources.MouseCaptureInactiveText; } /// @@ -1000,6 +1107,15 @@ namespace Contralto BeginInvoke(new DisplayDelegate(RefreshDiskStatus)); } + private void InternalShutdown(bool commitDisks) + { + // Determine how to exit. + _commitDisksAtShutdown = commitDisks; + + // Close our window and be done. + this.Close(); + } + private void RefreshDiskStatus() { if (_lastActivity != _system.DiskController.LastDiskActivity) @@ -1061,6 +1177,59 @@ namespace Contralto } } + private void StartScriptPlayback(string fileName) + { + // + // Start the script. We need to pause the emulation while doing so, + // in order to avoid concurrency issues with the Scheduler (which is + // not thread-safe). + // + _controller.StopExecution(); + ScriptManager.StartPlayback(_system, _controller, fileName); + _controller.StartExecution(AlternateBootType.None); + + PlayScriptMenu.Text = Resources.StopPlaybackText; + RecordScriptMenu.Enabled = false; + + SystemStatusLabel.Text = Resources.PlaybackInProgressText; + } + + private void StopScriptPlayback() + { + ScriptManager.StopPlayback(); + PlayScriptMenu.Text = Resources.StartPlaybackText; + RecordScriptMenu.Enabled = true; + + SystemStatusLabel.Text = _controller.IsRunning ? Resources.SystemRunningText : Resources.SystemStoppedText; + } + + + private void StartScriptRecording(string fileName) + { + ScriptManager.StartRecording(_system, fileName); + RecordScriptMenu.Text = Resources.StopRecordingText; + PlayScriptMenu.Enabled = false; + + SystemStatusLabel.Text = Resources.RecordingInProgressText; + } + + private void StopScriptRecording() + { + ScriptManager.StopRecording(); + RecordScriptMenu.Text = Resources.StartRecordingText; + PlayScriptMenu.Enabled = true; + + SystemStatusLabel.Text = _controller.IsRunning ? Resources.SystemRunningText : Resources.SystemStoppedText; + } + + private void OnScriptPlaybackCompleted(object sender, EventArgs e) + { + PlayScriptMenu.Text = Resources.StartPlaybackText; + RecordScriptMenu.Enabled = true; + + SystemStatusLabel.Text = _controller.IsRunning ? Resources.SystemRunningText : Resources.SystemStoppedText; + } + private void InitKeymap() { _keyMap = new Dictionary(); @@ -1179,7 +1348,7 @@ namespace Contralto ToolStripMenuItem newMenu = new ToolStripMenuItem("New...", null, OnTridentNewClick); newMenu.Tag = i; - ToolStripMenuItem imageMenu = new ToolStripMenuItem(_noImageLoadedText); + ToolStripMenuItem imageMenu = new ToolStripMenuItem(Resources.NoImageLoadedText); imageMenu.Tag = i; imageMenu.Enabled = false; _tridentImageNames.Add(imageMenu); @@ -1249,14 +1418,8 @@ namespace Contralto // Trident menu items for disk names private List _tridentImageNames; - // strings. TODO: move to resource - private const string _noImageLoadedText = ""; - private const string _systemStoppedText = "Alto Stopped."; - private const string _systemRunningText = "Alto Running."; - private const string _systemErrorText = "Alto Stopped due to error. See Debugger."; - private const string _diabloFilter = "Alto Diablo Disk Images (*.dsk, *.dsk44)|*.dsk;*.dsk44|Diablo 31 Disk Images (*.dsk)|*.dsk|Diablo 44 Disk Images (*.dsk44)|*.dsk44|All Files (*.*)|*.*"; - private const string _tridentFilter = "Alto Trident Disk Images (*.dsk80, *.dsk300)|*.dsk80;*.dsk300|Trident T80 Disk Images (*.dsk80)|*.dsk80|Trident T300 Disk Images (*.dsk300)|*.dsk300|All Files (*.*)|*.*"; + // Whether to commit disk images back to disk at shutdown + private bool _commitDisksAtShutdown; - - } + } } diff --git a/Contralto/UI/Debugger.cs b/Contralto/UI/Debugger.cs index 4821d56..a857e4d 100644 --- a/Contralto/UI/Debugger.cs +++ b/Contralto/UI/Debugger.cs @@ -741,6 +741,7 @@ namespace Contralto { // Belongs to a task, so we can grab the address out as well Address = sourceText.Substring(2, 4); + annotated = true; } catch { @@ -748,8 +749,14 @@ namespace Contralto annotated = false; } - Text = sourceText.Substring(tokens[0].Length + 1, sourceText.Length - tokens[0].Length - 1); - annotated = true; + string sourceCode = sourceText.Substring(tokens[0].Length, sourceText.Length - tokens[0].Length); + // Remove leading space if present + if (sourceCode.StartsWith(" ")) + { + sourceCode = sourceCode.Substring(1); + } + + Text = UnTabify(sourceCode); } else { @@ -760,7 +767,7 @@ namespace Contralto if (!annotated) { - Text = sourceText; + Text = UnTabify(sourceText); Address = String.Empty; Task = TaskType.Invalid; } @@ -770,6 +777,40 @@ namespace Contralto public string Address; public TaskType Task; + /// + /// Converts tabs in the given string to 8 space tabulation. As it should be. + /// + /// + /// + private string UnTabify(string tabified) + { + StringBuilder untabified = new StringBuilder(); + + int column = 0; + + foreach (char c in tabified) + { + if (c == '\t') + { + untabified.Append(" "); + column++; + while ((column % 8) != 0) + { + untabified.Append(" "); + column++; + } + } + else + { + untabified.Append(c); + column++; + } + + + } + + return untabified.ToString(); + } } private void OnTabChanged(object sender, EventArgs e)