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 eff0be8..6b6eeae 100644 Binary files a/Contralto/Disk/allgames.dsk and b/Contralto/Disk/allgames.dsk differ diff --git a/Contralto/Display/DisplayController.cs b/Contralto/Display/DisplayController.cs index 69886fe..96faabd 100644 --- a/Contralto/Display/DisplayController.cs +++ b/Contralto/Display/DisplayController.cs @@ -143,7 +143,7 @@ namespace Contralto.Display /// /// /// - 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)