1
0
mirror of https://github.com/livingcomputermuseum/ContrAlto.git synced 2026-02-28 09:17:53 +00:00

Initial commit of changes for 1.2.3. This includes:

- Scripting support: Allows for recording and playback of mouse/keyboard input and various system control actions.  Simple (i.e. basic) scripting format.

- Fix for stale packets left in ethernet input queue; packets received by pcap while Alto's receiver is off are discarded.

- Mouse input made more accurate, and tweaked to avoid Alto microcode bug that causes erroneous mouse inputs under very rare circumstances on real hardware, but much more frequently under emulation.

- Small code cleanup here and there.  Moved many UI strings to resources, many more to go.
This commit is contained in:
Josh Dersch
2018-03-20 14:16:07 -07:00
parent 6f20ebbfe7
commit d96d232fd3
39 changed files with 2733 additions and 510 deletions

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
public abstract class ScriptAction
{
public ScriptAction(ulong timestamp)
{
_timestamp = timestamp;
}
/// <summary>
/// Relative timestamp for this action.
/// </summary>
public ulong Timestamp
{
get { return _timestamp; }
}
/// <summary>
/// Whether the action has completed after the last
/// Replay action
/// </summary>
public bool Completed
{
get { return _completed; }
}
/// <summary>
/// Replays a single step of the action. If the action is completed,
/// Completed will be true afterwards.
/// </summary>
/// <param name="system"></param>
/// <param name="controller"></param>
public abstract void Replay(AltoSystem system, ExecutionController controller);
/// <summary>
/// Constructs the proper ScriptAction from a given line of text
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public static ScriptAction Parse(string line)
{
//
// An Action consists of a line in the format:
// <timestamp> <Action Type> [args]
//
// <timestamp> 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;
}
/// <summary>
/// Injects a single key action (up or down) into the Alto's keyboard.
/// </summary>
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;
}
/// <summary>
/// Injects a single mouse button action (up or down) into the Alto's Mouse.
/// </summary>
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;
}
/// <summary>
/// Injects a mouse movement into the Alto's mouse.
/// </summary>
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;
}
/// <summary>
/// Injects a command execution to control the Alto system. See ControlCommands for
/// the actual commands.
/// </summary>
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;
}
/// <summary>
/// Injects one or more simultaneous keystrokes (keydown followed by keyup) into the
/// Alto's keyboard.
/// </summary>
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;
}
/// <summary>
/// Injects a sequence of keystrokes corresponding to the keystrokes needed to
/// type the provided string.
/// </summary>
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<Keystroke>();
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<char, AltoKey>();
_shiftedKeyMap = new Dictionary<char, AltoKey>();
// 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<Keystroke> _strokes;
private int _currentStroke;
private bool _cr;
private static Dictionary<char, AltoKey> _unmodifiedKeyMap;
private static Dictionary<char, AltoKey> _shiftedKeyMap;
}
/// <summary>
/// Causes the Playback engine to wait until the Alto executes a wakeup STARTF.
/// </summary>
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);
}
}
}