mirror of
https://github.com/livingcomputermuseum/ContrAlto.git
synced 2026-02-28 17:19:41 +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:
679
Contralto/Scripting/CommandExecutor.cs
Normal file
679
Contralto/Scripting/CommandExecutor.cs
Normal file
@@ -0,0 +1,679 @@
|
||||
/*
|
||||
This file is part of ContrAlto.
|
||||
|
||||
ContrAlto is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ContrAlto is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with ContrAlto. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Defines a node in the debug command tree.
|
||||
/// </summary>
|
||||
public class DebuggerCommand
|
||||
{
|
||||
public DebuggerCommand(string name, String description, String usage, MethodInvokeInfo methodInvoke)
|
||||
{
|
||||
Name = name.Trim().ToLower();
|
||||
Description = description;
|
||||
Usage = usage;
|
||||
Methods = new List<MethodInvokeInfo>(4);
|
||||
|
||||
if (methodInvoke != null)
|
||||
{
|
||||
Methods.Add(methodInvoke);
|
||||
}
|
||||
|
||||
SubCommands = new List<DebuggerCommand>();
|
||||
}
|
||||
|
||||
public string Name;
|
||||
public string Description;
|
||||
public string Usage;
|
||||
public List<MethodInvokeInfo> Methods;
|
||||
public List<DebuggerCommand> SubCommands;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (this.Methods.Count == 0)
|
||||
{
|
||||
return String.Format("{0}... ({1})", this.Name, this.SubCommands.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddSubNode(List<string> words, MethodInvokeInfo methodInfo)
|
||||
{
|
||||
// We should never hit this case.
|
||||
if (words.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Out of words building command node.");
|
||||
}
|
||||
|
||||
// Check the root to see if a node for the first incoming word has already been added
|
||||
DebuggerCommand subNode = FindSubNodeByName(words[0]);
|
||||
|
||||
if (subNode == null)
|
||||
{
|
||||
// No, it has not -- create one and add it now.
|
||||
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(methodInfo);
|
||||
|
||||
// early return.
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The node already exists, we will be adding a subnode, hopefully.
|
||||
if (words.Count == 1)
|
||||
{
|
||||
//
|
||||
// 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 = methodInfo.Method.GetParameters().Length;
|
||||
foreach (MethodInvokeInfo info in subNode.Methods)
|
||||
{
|
||||
if (info.Method.GetParameters().Length == argCount)
|
||||
{
|
||||
throw new InvalidOperationException("Duplicate overload for console command");
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We're ok. Add it to the method list.
|
||||
//
|
||||
subNode.Methods.Add(methodInfo);
|
||||
|
||||
// and return early.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We have more words to go.
|
||||
words.RemoveAt(0);
|
||||
subNode.AddSubNode(words, methodInfo);
|
||||
}
|
||||
|
||||
public DebuggerCommand FindSubNodeByName(string name)
|
||||
{
|
||||
DebuggerCommand found = null;
|
||||
|
||||
foreach (DebuggerCommand sub in SubCommands)
|
||||
{
|
||||
if (sub.Name == name)
|
||||
{
|
||||
found = sub;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CommandResult
|
||||
{
|
||||
Normal,
|
||||
Quit,
|
||||
QuitNoSave,
|
||||
}
|
||||
|
||||
public class CommandExecutor
|
||||
{
|
||||
public CommandExecutor(params object[] commandObjects)
|
||||
{
|
||||
List<object> commandList = new List<object>(commandObjects);
|
||||
BuildCommandTree(commandList);
|
||||
}
|
||||
|
||||
public CommandResult ExecuteScript(string scriptFile)
|
||||
{
|
||||
CommandResult state = CommandResult.Normal;
|
||||
|
||||
using (StreamReader sr = new StreamReader(scriptFile))
|
||||
{
|
||||
while (!sr.EndOfStream)
|
||||
{
|
||||
string line = sr.ReadLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
state = ExecuteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (line.StartsWith("#"))
|
||||
{
|
||||
// Comments start with "#", just ignore them
|
||||
}
|
||||
else if(line.StartsWith("@"))
|
||||
{
|
||||
// A line beginning with an "@" indicates a script to execute.
|
||||
string scriptFile = line.Substring(1);
|
||||
|
||||
next = ExecuteScript(scriptFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] args = null;
|
||||
DebuggerCommand command = GetDebuggerCommandFromCommandString(line, out args);
|
||||
|
||||
if (command == null)
|
||||
{
|
||||
// Not a command.
|
||||
Console.WriteLine("Invalid command.");
|
||||
}
|
||||
else
|
||||
{
|
||||
next = InvokeConsoleMethod(command, args);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private CommandResult InvokeConsoleMethod(DebuggerCommand command, string[] args)
|
||||
{
|
||||
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 (MethodInvokeInfo m in command.Methods)
|
||||
{
|
||||
ParameterInfo[] paramInfo = m.Method.GetParameters();
|
||||
|
||||
if (args == null && paramInfo.Length == 0 ||
|
||||
paramInfo.Length == args.Length)
|
||||
{
|
||||
// found a match
|
||||
method = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
// invalid argument count.
|
||||
throw new ArgumentException(String.Format("Invalid argument count to command."));
|
||||
}
|
||||
|
||||
ParameterInfo[] parameterInfo = method.Method.GetParameters();
|
||||
object[] invokeParams;
|
||||
|
||||
if (args == null)
|
||||
{
|
||||
invokeParams = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
invokeParams = new object[parameterInfo.Length];
|
||||
}
|
||||
|
||||
int argIndex = 0;
|
||||
for (int paramIndex = 0; paramIndex < parameterInfo.Length; paramIndex++)
|
||||
{
|
||||
ParameterInfo p = parameterInfo[paramIndex];
|
||||
|
||||
if (p.ParameterType.IsEnum)
|
||||
{
|
||||
//
|
||||
// This is an enumeration type.
|
||||
// See if we can find an enumerant that matches the argument.
|
||||
//
|
||||
FieldInfo[] fields = p.ParameterType.GetFields();
|
||||
|
||||
foreach (FieldInfo f in fields)
|
||||
{
|
||||
if (!f.IsSpecialName && args[argIndex].ToLower() == f.Name.ToLower())
|
||||
{
|
||||
invokeParams[paramIndex] = f.GetRawConstantValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (invokeParams[paramIndex] == null)
|
||||
{
|
||||
// no match, provide possible values
|
||||
StringBuilder sb = new StringBuilder(String.Format("Invalid value for parameter {0}. Possible values are:", paramIndex));
|
||||
|
||||
foreach (FieldInfo f in fields)
|
||||
{
|
||||
if (!f.IsSpecialName)
|
||||
{
|
||||
sb.AppendFormat("{0} ", f.Name);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
throw new ArgumentException(sb.ToString());
|
||||
}
|
||||
|
||||
argIndex++;
|
||||
|
||||
}
|
||||
else if (p.ParameterType.IsArray)
|
||||
{
|
||||
//
|
||||
// If a function takes an array type, i should do something here, yeah.
|
||||
//
|
||||
argIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (p.ParameterType == typeof(bool))
|
||||
{
|
||||
invokeParams[paramIndex] = bool.Parse(args[argIndex++]);
|
||||
}
|
||||
else if (p.ParameterType == typeof(uint))
|
||||
{
|
||||
invokeParams[paramIndex] = TryParseUint(args[argIndex++]);
|
||||
}
|
||||
else if (p.ParameterType == typeof(ushort))
|
||||
{
|
||||
invokeParams[paramIndex] = TryParseUshort(args[argIndex++]);
|
||||
}
|
||||
else if (p.ParameterType == typeof(string))
|
||||
{
|
||||
invokeParams[paramIndex] = args[argIndex++];
|
||||
}
|
||||
else if (p.ParameterType == typeof(char))
|
||||
{
|
||||
invokeParams[paramIndex] = (char)args[argIndex++][0];
|
||||
}
|
||||
else if (p.ParameterType == typeof(float))
|
||||
{
|
||||
invokeParams[paramIndex] = float.Parse(args[argIndex++]);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException(String.Format("Unhandled type for parameter {0}, type {1}", paramIndex, p.ParameterType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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.Method.Invoke(method.Instance, invokeParams);
|
||||
}
|
||||
|
||||
enum ParseState
|
||||
{
|
||||
NonWhiteSpace = 0,
|
||||
WhiteSpace = 1,
|
||||
QuotedString = 2,
|
||||
}
|
||||
|
||||
private List<string> SplitArgs(string commandString)
|
||||
{
|
||||
// We split on whitespace and specially handle quoted strings (quoted strings count as a single arg)
|
||||
//
|
||||
List<string> args = new List<string>();
|
||||
|
||||
commandString = commandString.Trim();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
ParseState state = ParseState.NonWhiteSpace;
|
||||
|
||||
foreach(char c in commandString)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ParseState.NonWhiteSpace:
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
// End of token
|
||||
args.Add(sb.ToString());
|
||||
sb.Clear();
|
||||
state = ParseState.WhiteSpace;
|
||||
}
|
||||
else if (c == '\"')
|
||||
{
|
||||
// Start of quoted string
|
||||
state = ParseState.QuotedString;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Character in token
|
||||
sb.Append(c);
|
||||
}
|
||||
break;
|
||||
|
||||
case ParseState.WhiteSpace:
|
||||
if (!char.IsWhiteSpace(c))
|
||||
{
|
||||
// Start of new token
|
||||
if (c != '\"')
|
||||
{
|
||||
sb.Append(c);
|
||||
state = ParseState.NonWhiteSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Start of quoted string
|
||||
state = ParseState.QuotedString;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ParseState.QuotedString:
|
||||
if (c == '\"')
|
||||
{
|
||||
// End of quoted string.
|
||||
args.Add(sb.ToString());
|
||||
sb.Clear();
|
||||
state = ParseState.WhiteSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Character in quoted string
|
||||
sb.Append(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
// Add the last token to the args list
|
||||
args.Add(sb.ToString());
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private DebuggerCommand GetDebuggerCommandFromCommandString(string command, out string[] args)
|
||||
{
|
||||
args = null;
|
||||
|
||||
List<string> cmdArgs = SplitArgs(command);
|
||||
|
||||
DebuggerCommand current = _commandRoot;
|
||||
int commandIndex = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// If this node has an executor and no subnodes, or if this node has an executor
|
||||
// and there are no further arguments, then we're done.
|
||||
if ((current.Methods.Count > 0 && current.SubCommands.Count == 0) ||
|
||||
(current.Methods.Count > 0 && commandIndex > cmdArgs.Count -1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (commandIndex > cmdArgs.Count - 1)
|
||||
{
|
||||
// Out of args with no match.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise we continue down the tree.
|
||||
DebuggerCommand next = current.FindSubNodeByName(cmdArgs[commandIndex]);
|
||||
|
||||
commandIndex++;
|
||||
|
||||
if (next == null)
|
||||
{
|
||||
//
|
||||
// If a matching subcommand was not found, then if we had a previous node with an
|
||||
// executor, use that; otherwise the command is invalid.
|
||||
//
|
||||
if (current.Methods.Count > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
// Now current should point to the command with the executor
|
||||
// and commandIndex should point to the first argument to the command.
|
||||
|
||||
cmdArgs.RemoveRange(0, commandIndex);
|
||||
|
||||
args = cmdArgs.ToArray();
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
private enum Radix
|
||||
{
|
||||
Binary = 2,
|
||||
Octal = 8,
|
||||
Decimal = 10,
|
||||
Hexadecimal = 16,
|
||||
}
|
||||
|
||||
private static uint TryParseUint(string arg)
|
||||
{
|
||||
uint result = 0;
|
||||
Radix radix = Radix.Decimal;
|
||||
|
||||
switch (arg[0])
|
||||
{
|
||||
case 'b':
|
||||
radix = Radix.Binary;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
radix = Radix.Octal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'd':
|
||||
radix = Radix.Decimal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'x':
|
||||
radix = Radix.Hexadecimal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
radix = Radix.Octal;
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = Convert.ToUInt32(arg, (int)radix);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("{0} is not a valid 32-bit value.", arg);
|
||||
throw;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ushort TryParseUshort(string arg)
|
||||
{
|
||||
ushort result = 0;
|
||||
Radix radix = Radix.Decimal;
|
||||
|
||||
switch (arg[0])
|
||||
{
|
||||
case 'b':
|
||||
radix = Radix.Binary;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
radix = Radix.Octal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'd':
|
||||
radix = Radix.Decimal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
case 'x':
|
||||
radix = Radix.Hexadecimal;
|
||||
arg = arg.Remove(0, 1);
|
||||
break;
|
||||
|
||||
default:
|
||||
radix = Radix.Octal;
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = Convert.ToUInt16(arg, (int)radix);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("{0} is not a valid 16-bit value.", arg);
|
||||
throw;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the debugger command tree.
|
||||
/// </summary>
|
||||
private void BuildCommandTree(List<object> commandObjects)
|
||||
{
|
||||
// Build the flat list which will be built into the tree, by walking
|
||||
// the classes that provide the methods
|
||||
_commandList = new List<DebuggerCommand>();
|
||||
|
||||
// Add ourself to the list
|
||||
commandObjects.Add(this);
|
||||
|
||||
foreach (object commandObject in commandObjects)
|
||||
{
|
||||
Type type = commandObject.GetType();
|
||||
foreach (MethodInfo info in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
object[] attribs = info.GetCustomAttributes(typeof(DebuggerFunction), true);
|
||||
|
||||
if (attribs.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException(String.Format("More than one ConsoleFunction attribute set on {0}", info.Name));
|
||||
}
|
||||
else if (attribs.Length == 1)
|
||||
{
|
||||
// we have a debugger attribute set on this method
|
||||
// this cast should always succeed given that we're filtering for this type above.
|
||||
DebuggerFunction function = (DebuggerFunction)attribs[0];
|
||||
|
||||
DebuggerCommand newCommand = new DebuggerCommand(function.CommandName, function.Description, function.Usage, new MethodInvokeInfo(info, commandObject));
|
||||
|
||||
_commandList.Add(newCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now actually build the command tree from the above list!
|
||||
_commandRoot = new DebuggerCommand("Root", null, null, null);
|
||||
|
||||
foreach (DebuggerCommand c in _commandList)
|
||||
{
|
||||
string[] commandWords = c.Name.Split(' ');
|
||||
|
||||
// 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<string>(commandWords), c.Methods[0]);
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerFunction("show commands", "Shows debugger commands and their descriptions.")]
|
||||
private CommandResult ShowCommands()
|
||||
{
|
||||
foreach (DebuggerCommand cmd in _commandList)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(cmd.Usage))
|
||||
{
|
||||
Console.WriteLine("{0} {2} - {1}", cmd.Name, cmd.Description, cmd.Usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("{0} - {1}", cmd.Name, cmd.Description);
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult.Normal;
|
||||
}
|
||||
|
||||
private DebuggerCommand _commandRoot;
|
||||
private List<DebuggerCommand> _commandList;
|
||||
}
|
||||
}
|
||||
219
Contralto/Scripting/ControlCommands.cs
Normal file
219
Contralto/Scripting/ControlCommands.cs
Normal file
@@ -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.", "<drive> <path>")]
|
||||
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.", "<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.", "<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.", "<drive> <path>")]
|
||||
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.", "<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.", "<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;
|
||||
}
|
||||
}
|
||||
64
Contralto/Scripting/DebuggerAttributes.cs
Normal file
64
Contralto/Scripting/DebuggerAttributes.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of ContrAlto.
|
||||
|
||||
ContrAlto is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ContrAlto is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with ContrAlto. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
|
||||
namespace Contralto.Scripting
|
||||
|
||||
{
|
||||
public class DebuggerFunction : Attribute
|
||||
{
|
||||
public DebuggerFunction(string commandName)
|
||||
{
|
||||
_commandName = commandName;
|
||||
_usage = "<No help available>";
|
||||
}
|
||||
|
||||
public DebuggerFunction(string commandName, string description)
|
||||
{
|
||||
_commandName = commandName;
|
||||
_description = description;
|
||||
}
|
||||
|
||||
public DebuggerFunction(string commandName, string description, string usage)
|
||||
{
|
||||
_commandName = commandName;
|
||||
_description = description;
|
||||
_usage = usage;
|
||||
}
|
||||
|
||||
public string CommandName
|
||||
{
|
||||
get { return _commandName; }
|
||||
}
|
||||
|
||||
public string Usage
|
||||
{
|
||||
get { return _usage; }
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return _description; }
|
||||
}
|
||||
|
||||
private string _commandName;
|
||||
private string _description;
|
||||
private string _usage;
|
||||
}
|
||||
}
|
||||
701
Contralto/Scripting/ScriptAction.cs
Normal file
701
Contralto/Scripting/ScriptAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Contralto/Scripting/ScriptManager.cs
Normal file
126
Contralto/Scripting/ScriptManager.cs
Normal file
@@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when playback of a script has completed or is stopped.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
108
Contralto/Scripting/ScriptPlayback.cs
Normal file
108
Contralto/Scripting/ScriptPlayback.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when playback of the script has completed or is stopped.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
66
Contralto/Scripting/ScriptReader.cs
Normal file
66
Contralto/Scripting/ScriptReader.cs
Normal file
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
123
Contralto/Scripting/ScriptRecorder.cs
Normal file
123
Contralto/Scripting/ScriptRecorder.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Records actions.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
42
Contralto/Scripting/ScriptWriter.cs
Normal file
42
Contralto/Scripting/ScriptWriter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ScriptWriter
|
||||
{
|
||||
public ScriptWriter(string scriptPath)
|
||||
{
|
||||
_scriptWriter = new StreamWriter(scriptPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new ScriptAction to the queue
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user