From f1c792cf2c08258ff3c33f33becddbb8fa51cfdd Mon Sep 17 00:00:00 2001 From: David Cornelson Date: Sat, 6 Sep 2014 13:01:13 -0500 Subject: [PATCH] Standard Windows Implementation with Zifmia Wrapper This is the standard working version of FyreVM with the Zifmia wrapper. --- BigEndian.cs | 161 ++++ COPYRIGHT | 21 + Engine.cs | 1584 ++++++++++++++++++++++++++++++++++++++ EngineWrapper.cs | 367 +++++++++ GlkWrapper.cs | 440 +++++++++++ HeapAllocator.cs | 312 ++++++++ Opcodes.cs | 1885 ++++++++++++++++++++++++++++++++++++++++++++++ Output.cs | 775 +++++++++++++++++++ OutputBuffer.cs | 114 +++ Profiler.cs | 660 ++++++++++++++++ Quetzal.cs | 243 ++++++ UlxImage.cs | 297 ++++++++ VMException.cs | 29 + Veneer.cs | 544 +++++++++++++ 14 files changed, 7432 insertions(+) create mode 100644 BigEndian.cs create mode 100644 COPYRIGHT create mode 100644 Engine.cs create mode 100644 EngineWrapper.cs create mode 100644 GlkWrapper.cs create mode 100644 HeapAllocator.cs create mode 100644 Opcodes.cs create mode 100644 Output.cs create mode 100644 OutputBuffer.cs create mode 100644 Profiler.cs create mode 100644 Quetzal.cs create mode 100644 UlxImage.cs create mode 100644 VMException.cs create mode 100644 Veneer.cs diff --git a/BigEndian.cs b/BigEndian.cs new file mode 100644 index 0000000..f2e3c6b --- /dev/null +++ b/BigEndian.cs @@ -0,0 +1,161 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace FyreVM +{ + /// + /// Provides utility functions for working with big-endian numbers. + /// + internal static class BigEndian + { + /// + /// Reads an unsigned, big-endian, 16-bit number from a byte array. + /// + /// The array to read from. + /// The index within the array where the number starts. + /// The number read. + public static ushort ReadInt16(byte[] array, uint offset) + { + return (ushort)((array[offset] << 8) + array[offset + 1]); + } + + /// + /// Reads an unsigned, big-endian, 16-bit number from a byte array. + /// + /// The array to read from. + /// The index within the array where the number starts. + /// The number read. + public static ushort ReadInt16(byte[] array, int offset) + { + return ReadInt16(array, (uint)offset); + } + + /// + /// Reads an unsigned, big-endian, 32-bit number from a byte array. + /// + /// The array to read from. + /// The index within the array where the number starts. + /// The number read. + public static uint ReadInt32(byte[] array, uint offset) + { + return (uint)((array[offset] << 24) + (array[offset + 1] << 16) + + (array[offset + 2] << 8) + array[offset + 3]); + } + + /// + /// Reads an unsigned, big-endian, 32-bit number from a byte array. + /// + /// The array to read from. + /// The index within the array where the number starts. + /// The number read. + public static uint ReadInt32(byte[] array, int offset) + { + return ReadInt32(array, (uint)offset); + } + + /// + /// Writes an unsigned, big-endian, 16-bit number into a byte array. + /// + /// The array to write into. + /// The index within the array where the number will start. + /// The number to write. + public static void WriteInt16(byte[] array, uint offset, ushort value) + { + array[offset] = (byte)(value >> 8); + array[offset + 1] = (byte)value; + } + + /// + /// Writes an unsigned, big-endian, 16-bit number into a byte array. + /// + /// The array to write into. + /// The index within the array where the number will start. + /// The number to write. + public static void WriteInt16(byte[] array, int offset, ushort value) + { + WriteInt16(array, (uint)offset, value); + } + + /// + /// Writes an unsigned, big-endian, 32-bit number into a byte array. + /// + /// The array to write into. + /// The index within the array where the number will start. + /// The number to write. + public static void WriteInt32(byte[] array, uint offset, uint value) + { + array[offset] = (byte)(value >> 24); + array[offset + 1] = (byte)(value >> 16); + array[offset + 2] = (byte)(value >> 8); + array[offset + 3] = (byte)value; + } + + /// + /// Writes an unsigned, big-endian, 32-bit number into a byte array. + /// + /// The array to write into. + /// The index within the array where the number will start. + /// The number to write. + public static void WriteInt32(byte[] array, int offset, uint value) + { + WriteInt32(array, (uint)offset, value); + } + + /// + /// Writes an unsigned, big-endian, 16-bit number to a stream. + /// + /// The stream to write to. + /// The number to write. + public static void WriteInt16(Stream stream, ushort value) + { + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + /// + /// Reads an unsigned, big-endian, 16-bit number from a stream. + /// + /// The stream to read from. + /// The number read. + public static ushort ReadInt16(Stream stream) + { + int a = stream.ReadByte(); + int b = stream.ReadByte(); + return (ushort)((a << 8) + b); + } + + /// + /// Writes an unsigned, big-endian, 32-bit number to a stream. + /// + /// The stream to write to. + /// The number to write. + public static void WriteInt32(Stream stream, uint value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + /// + /// Reads an unsigned, big-endian, 32-bit number from a stream. + /// + /// The stream to read from. + /// The number read. + public static uint ReadInt32(Stream stream) + { + int a = stream.ReadByte(); + int b = stream.ReadByte(); + int c = stream.ReadByte(); + int d = stream.ReadByte(); + return (uint)((a << 24) + (b << 16) + (c << 8) + d); + } + } +} diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..7af23b3 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2014 Textfyre, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Engine.cs b/Engine.cs new file mode 100644 index 0000000..e394cc7 --- /dev/null +++ b/Engine.cs @@ -0,0 +1,1584 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Diagnostics; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace FyreVM +{ + /// + /// Provides data for an input line request event. + /// + public class LineWantedEventArgs : EventArgs + { + private string line; + + /// + /// Gets or sets the line of input that was read, or null to cancel. + /// + public string Line + { + get { return line; } + set { line = value; } + } + } + + /// + /// A delegate that handles the event. + /// + /// The raising the event. + /// The event arguments. + public delegate void LineWantedEventHandler(object sender, LineWantedEventArgs e); + + /// + /// Provides data for an input character request event. + /// + public class KeyWantedEventArgs : EventArgs + { + private char ch; + + /// + /// Gets or sets the character that was read, or '\0' to cancel. + /// + public char Char + { + get { return ch; } + set { ch = value; } + } + } + + /// + /// A delegate that handles the event. + /// + /// The raising the event. + /// The event arguments. + public delegate void KeyWantedEventHandler(object sender, KeyWantedEventArgs e); + + /// + /// Provides data for an output event. + /// + public class OutputReadyEventArgs : EventArgs + { + private IDictionary package; + + /// + /// Gets or sets a dictionary containing the text that has been + /// captured on each output channel since the last output delivery. + /// + public IDictionary Package + { + get { return package; } + set { package = value; } + } + } + + /// + /// A delegate that handles the event. + /// + /// The raising the event. + /// The event arguments. + public delegate void OutputReadyEventHandler(object sender, OutputReadyEventArgs e); + + /// + /// Provides data for a save or restore event. + /// + public class SaveRestoreEventArgs : EventArgs + { + private Stream stream; + + /// + /// Gets or sets the stream to use for saving or restoring the game + /// state. This stream will be closed by the interpreter after the + /// save or load process finishes. (A value of + /// indicates that the save/load process was aborted.) + /// + public Stream Stream + { + get { return stream; } + set { stream = value; } + } + } + + /// + /// A delegate that handles the or + /// event. + /// + /// The raising the event. + /// The event arguments. + public delegate void SaveRestoreEventHandler(object sender, SaveRestoreEventArgs e); + + public class TransitionEventArgs : EventArgs + { + private string message; + + public string Message { get { return message; } set { message = value; } } + } + + public delegate void TransitionRequestedEventHandler(object sender, TransitionEventArgs e); + + /// + /// Describes the type of Glk support offered by the interpreter. + /// + public enum GlkMode + { + /// + /// No Glk support. + /// + None, + /// + /// A minimal Glk implementation, with I/O functions mapped to the channel system. + /// + Wrapper, + } + + /// + /// The main FyreVM class, which implements a modified Glulx interpreter. + /// + public partial class Engine + { + public enum VMRequestType + { + StartGame, + EnterCommand + } + /// + /// Describes the task that the interpreter is currently performing. + /// + private enum ExecutionMode + { + /// + /// We are running function code. PC points to the next instruction. + /// + Code, + /// + /// We are printing a null-terminated string (E0). PC points to the + /// next character. + /// + CString, + /// + /// We are printing a compressed string (E1). PC points to the next + /// compressed byte, and printingDigit is the bit position (0-7). + /// + CompressedString, + /// + /// We are printing a Unicode string (E2). PC points to the next + /// character. + /// + UnicodeString, + /// + /// We are printing a decimal number. PC contains the number, and + /// printingDigit is the next digit, starting at 0 (for the first + /// digit or minus sign). + /// + Number, + /// + /// We are returning control to + /// after engine code has called a Glulx function. + /// + Return, + } + + /// + /// Represents a Glulx call stub, which describes the VM's state at + /// the time of a function call or string printing task. + /// + private struct CallStub + { + /// + /// The type of storage location (for function calls) or the + /// previous task (for string printing). + /// + public uint DestType; + /// + /// The storage address (for function calls) or the digit + /// being examined (for string printing). + /// + public uint DestAddr; + /// + /// The address of the opcode or character at which to resume. + /// + public uint PC; + /// + /// The stack frame in which the function call or string printing + /// was performed. + /// + public uint FramePtr; + + /// + /// Initializes a new call stub. + /// + /// The stub type. + /// The storage address or printing digit. + /// The address of the opcode or character at which to resume. + /// The stack frame pointer. + public CallStub(uint destType, uint destAddr, uint pc, uint framePtr) + { + this.DestType = destType; + this.DestAddr = destAddr; + this.PC = pc; + this.FramePtr = framePtr; + } + } + + #region Glulx constants + + // Header size and field offsets + public const int GLULX_HDR_SIZE = 36; + public const int GLULX_HDR_MAGIC_OFFSET = 0; + public const int GLULX_HDR_VERSION_OFFSET = 4; + public const int GLULX_HDR_RAMSTART_OFFSET = 8; + public const int GLULX_HDR_EXTSTART_OFFSET = 12; + public const int GLULX_HDR_ENDMEM_OFFSET = 16; + public const int GLULX_HDR_STACKSIZE_OFFSET = 20; + public const int GLULX_HDR_STARTFUNC_OFFSET = 24; + public const int GLULX_HDR_DECODINGTBL_OFFSET = 28; + public const int GLULX_HDR_CHECKSUM_OFFSET = 32; + + // Call stub: DestType values for function calls + public const int GLULX_STUB_STORE_NULL = 0; + public const int GLULX_STUB_STORE_MEM = 1; + public const int GLULX_STUB_STORE_LOCAL = 2; + public const int GLULX_STUB_STORE_STACK = 3; + + // Call stub: DestType values for string printing + public const int GLULX_STUB_RESUME_HUFFSTR = 10; + public const int GLULX_STUB_RESUME_FUNC = 11; + public const int GLULX_STUB_RESUME_NUMBER = 12; + public const int GLULX_STUB_RESUME_CSTR = 13; + public const int GLULX_STUB_RESUME_UNISTR = 14; + + // FyreVM addition: DestType value for nested calls + public const int FYREVM_STUB_RESUME_NATIVE = 99; + + // String decoding table: header field offsets + public const int GLULX_HUFF_TABLESIZE_OFFSET = 0; + public const int GLULX_HUFF_NODECOUNT_OFFSET = 4; + public const int GLULX_HUFF_ROOTNODE_OFFSET = 8; + + // String decoding table: node types + public const int GLULX_HUFF_NODE_BRANCH = 0; + public const int GLULX_HUFF_NODE_END = 1; + public const int GLULX_HUFF_NODE_CHAR = 2; + public const int GLULX_HUFF_NODE_CSTR = 3; + public const int GLULX_HUFF_NODE_UNICHAR = 4; + public const int GLULX_HUFF_NODE_UNISTR = 5; + public const int GLULX_HUFF_NODE_INDIRECT = 8; + public const int GLULX_HUFF_NODE_DBLINDIRECT = 9; + public const int GLULX_HUFF_NODE_INDIRECT_ARGS = 10; + public const int GLULX_HUFF_NODE_DBLINDIRECT_ARGS = 11; + + #endregion + + private const string LATIN1_CODEPAGE = "latin1";//28591; + + #region Dictionary of opcodes + + private readonly Dictionary opcodeDict = new Dictionary(); + private readonly Opcode[] quickOpcodes = new Opcode[0x80]; + + private void InitOpcodeDict() + { + MethodInfo[] methods = typeof(Engine).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); + foreach (MethodInfo mi in methods) + { + object[] attrs = mi.GetCustomAttributes(typeof(OpcodeAttribute), false); + if (attrs.Length > 0) + { + OpcodeAttribute attr = (OpcodeAttribute)(attrs[0]); + Delegate handler = Delegate.CreateDelegate(typeof(OpcodeHandler), this, mi); + opcodeDict.Add(attr.Number, new Opcode(attr, (OpcodeHandler)handler)); + } + } + } + + #endregion + + private const uint FIRST_MAJOR_VERSION = 2; + private const uint FIRST_MINOR_VERSION = 0; + private const uint LAST_MAJOR_VERSION = 3; + private const uint LAST_MINOR_VERSION = 1; + + private const int MAX_UNDO_LEVEL = 3; + + // persistent machine state (written to save file) + private UlxImage image; + private byte[] stack; + private uint pc, sp, fp; // program counter, stack ptr, call-frame ptr + private HeapAllocator heap; + + // transient state + private uint frameLen, localsPos; // updated along with FP + private IOSystem outputSystem; + private GlkMode glkMode = GlkMode.None; + private OutputBuffer outputBuffer; + private uint filterAddress; + private uint decodingTable; + private StrNode nativeDecodingTable; + private ExecutionMode execMode; + private byte printingDigit; // bit number for compressed strings, digit for numbers + private Random randomGenerator = new Random(); + private List undoBuffers = new List(); + private uint protectionStart, protectionLength; // relative to start of RAM! + private bool running; + private uint nestedResult; + private int nestingLevel; + private Veneer veneer = new Veneer(); + private uint maxHeapSize; + + /// + /// Initializes a new instance of the VM from a game file. + /// + /// A stream containing the ROM and + /// initial RAM. + public Engine(Stream gameFile) + { + image = new UlxImage(gameFile); + outputBuffer = new OutputBuffer(); + + uint version = (uint)image.ReadInt32(GLULX_HDR_VERSION_OFFSET); + uint major = version >> 16; + uint minor = (version >> 8) & 0xFF; + + if (major < FIRST_MAJOR_VERSION || + (major == FIRST_MAJOR_VERSION && minor < FIRST_MINOR_VERSION) || + major > LAST_MAJOR_VERSION || + (major == LAST_MAJOR_VERSION && minor > LAST_MINOR_VERSION)) + throw new ArgumentException("Game version is out of the supported range"); + + uint stacksize = (uint)image.ReadInt32(GLULX_HDR_STACKSIZE_OFFSET); + stack = new byte[stacksize]; + + InitOpcodeDict(); + } + + + /// + /// Initializes a new instance of the VM from a saved state and the + /// associated game file. + /// + /// A stream containing the ROM and + /// initial RAM. + /// A stream containing a + /// state that was saved by the specified game file. + public Engine(Stream gameFile, Stream saveFile) + : this(gameFile) + { + LoadFromStream(saveFile); + } + + /// + /// Raised when the VM wants to read a line of input. The handler may + /// return a string or indicate that input was canceled. + /// + public event LineWantedEventHandler LineWanted; + + /// + /// Raised when the VM wants to read a single character of input. + /// The handler may return a character or indicate that input was + /// canceled. + /// + public event KeyWantedEventHandler KeyWanted; + + /// + /// Raised when queued output is being delivered, i.e. before + /// requesting input or terminating. + /// + public event OutputReadyEventHandler OutputReady; + + /// + /// Raised when the VM needs a stream to use for saving the current + /// state. + /// + public event SaveRestoreEventHandler SaveRequested; + + /// + /// Raised when the VM needs a stream to use for restoring a previous + /// state. + /// + public event SaveRestoreEventHandler LoadRequested; + + /// + /// Raised when the game requests a physical device transition. The host device can handle in a native manner. + /// This happens instead of fusging for a keypress. + /// + public event TransitionRequestedEventHandler TransitionRequested; + + /// + /// Gets or sets a value limiting the maximum size of the Glulx heap, + /// in bytes, or zero to indicate an unlimited heap size. + /// + public uint MaxHeapSize + { + get { return maxHeapSize; } + set { maxHeapSize = value; if (heap != null) heap.MaxSize = value; } + } + + /// + /// Gets or sets a value indicating what type of Glk support will be offered. + /// + public GlkMode GlkMode + { + get { return glkMode; } + set { glkMode = value; } + } + + private void Push(uint value) + { + BigEndian.WriteInt32(stack, sp, value); + sp += 4; + } + + private void WriteToStack(uint position, uint value) + { + BigEndian.WriteInt32(stack, position, value); + } + + private uint Pop() + { + sp -= 4; + return BigEndian.ReadInt32(stack, sp); + } + + private uint ReadFromStack(uint position) + { + return BigEndian.ReadInt32(stack, position); + } + + private void PushCallStub(CallStub stub) + { + Push(stub.DestType); + Push(stub.DestAddr); + Push(stub.PC); + Push(stub.FramePtr); + } + + private CallStub PopCallStub() + { + CallStub stub; + + stub.FramePtr = Pop(); + stub.PC = Pop(); + stub.DestAddr = Pop(); + stub.DestType = Pop(); + + return stub; + } + + private static StringBuilder debugBuffer = new StringBuilder(); + + [Conditional("TRACEOPS")] + private static void WriteTrace(string str) + { + lock (debugBuffer) + { + debugBuffer.Append(str); + + if (str.Contains("\n")) + { + string x = debugBuffer.ToString(); + + do + { + int pos = x.IndexOf('\n'); + string line = x.Substring(0, pos).TrimEnd(); + + Debug.WriteLine(line); + + if (pos == x.Length - 1) + x = ""; + else + x = x.Substring(pos + 1); + } while (x.Contains("\n")); + + //Debug.WriteLine(debugBuffer.ToString()); + debugBuffer.Length = 0; + debugBuffer.Append(x); + } + } + } + + /// + /// Clears the stack and initializes VM registers from values found in RAM. + /// + private void Bootstrap() + { + uint mainfunc = image.ReadInt32(GLULX_HDR_STARTFUNC_OFFSET); + decodingTable = image.ReadInt32(GLULX_HDR_DECODINGTBL_OFFSET); + + sp = fp = frameLen = localsPos = 0; + outputSystem = IOSystem.Null; + execMode = ExecutionMode.Code; + EnterFunction(mainfunc); + } + + /// + /// Starts the interpreter. + /// + /// + /// This method does not return until the game finishes, either by + /// returning from the main function or with the quit opcode. + /// + /// + public Boolean _IsCurrentRestore = false; + public void Run() + { + running = true; + +#if PROFILING + cycles = 0; +#endif + + // initialize registers and stack + Bootstrap(); + CacheDecodingTable(); + + // run the game + if (_IsCurrentRestore == false) + { + InterpreterLoop(); + } + + + // send any output that may be left + DeliverOutput(); + + } + + public void Continue() + { + running = true; + +#if PROFILING + cycles = 0; +#endif + // run the game + InterpreterLoop(); + + // send any output that may be left + DeliverOutput(); + + } + + private uint NestedCall(uint address) + { + return NestedCall(address, null); + } + + private uint NestedCall(uint address, uint arg0) + { + funcargs1[0] = arg0; + return NestedCall(address, funcargs1); + } + + private uint NestedCall(uint address, uint arg0, uint arg1) + { + funcargs2[0] = arg0; + funcargs2[1] = arg1; + return NestedCall(address, funcargs2); + } + + private uint NestedCall(uint address, uint arg0, uint arg1, uint arg2) + { + funcargs3[0] = arg0; + funcargs3[1] = arg1; + funcargs3[2] = arg2; + return NestedCall(address, funcargs3); + } + + /// + /// Executes a Glulx function and returns its result. + /// + /// The address of the function. + /// The list of arguments, or + /// if no arguments need to be passed in. + /// The function's return value. + private uint NestedCall(uint address, params uint[] args) + { + ExecutionMode oldMode = execMode; + byte oldDigit = printingDigit; + + PerformCall(address, args, FYREVM_STUB_RESUME_NATIVE, 0); + nestingLevel++; + try + { + InterpreterLoop(); + } + finally + { + nestingLevel--; + execMode = oldMode; + printingDigit = oldDigit; + } + + return nestedResult; + } + + /// + /// Runs the main interpreter loop. + /// + private void InterpreterLoop() + { + try + { + + + const int MAX_OPERANDS = 8; + uint[] operands = new uint[MAX_OPERANDS]; + uint[] resultAddrs = new uint[MAX_OPERANDS]; + byte[] resultTypes = new byte[MAX_OPERANDS]; + + while (running) + { + switch (execMode) + { + case ExecutionMode.Code: + // decode opcode number + + uint opnum = image.ReadByte(pc); + + if (opnum >= 0xC0) + { + opnum = image.ReadInt32(pc) - 0xC0000000; + pc += 4; + } + else if (opnum >= 0x80) + { + opnum = (uint)(image.ReadInt16(pc) - 0x8000); + pc += 2; + } + else + { + pc++; + } + + // look up opcode info + Opcode opcode; + try + { + opcode = opcodeDict[opnum]; + WriteTrace("[" + opcode.ToString()); + } + catch (KeyNotFoundException) + { + throw new VMException(string.Format("Unrecognized opcode {0:X}h", opnum)); + } + + // decode load-operands + uint opcount = (uint)(opcode.Attr.LoadArgs + opcode.Attr.StoreArgs); + if (opcode.Attr.Rule == OpcodeRule.DelayedStore) + opcount++; + else if (opcode.Attr.Rule == OpcodeRule.Catch) + opcount += 2; + uint operandPos = pc + (opcount + 1) / 2; + + for (int i = 0; i < opcode.Attr.LoadArgs; i++) + { + byte type; + if (i % 2 == 0) + { + type = (byte)(image.ReadByte(pc) & 0xF); + } + else + { + type = (byte)((image.ReadByte(pc) >> 4) & 0xF); + pc++; + } + + WriteTrace(" "); + operands[i] = DecodeLoadOperand(opcode, type, ref operandPos); + } + + uint storePos = pc; + + // decode store-operands + for (int i = 0; i < opcode.Attr.StoreArgs; i++) + { + byte type; + if ((opcode.Attr.LoadArgs + i) % 2 == 0) + { + type = (byte)(image.ReadByte(pc) & 0xF); + } + else + { + type = (byte)((image.ReadByte(pc) >> 4) & 0xF); + pc++; + } + + resultTypes[i] = type; + WriteTrace(" -> "); + resultAddrs[i] = DecodeStoreOperand(opcode, type, ref operandPos); + } + + if (opcode.Attr.Rule == OpcodeRule.DelayedStore || + opcode.Attr.Rule == OpcodeRule.Catch) + { + // decode delayed store operand + byte type; + if ((opcode.Attr.LoadArgs + opcode.Attr.StoreArgs) % 2 == 0) + { + type = (byte)(image.ReadByte(pc) & 0xF); + } + else + { + type = (byte)((image.ReadByte(pc) >> 4) & 0xF); + pc++; + } + + WriteTrace(" -> "); + DecodeDelayedStoreOperand(type, ref operandPos, + operands, opcode.Attr.LoadArgs + opcode.Attr.StoreArgs); + } + + if (opcode.Attr.Rule == OpcodeRule.Catch) + { + // decode final load operand for @catch + byte type; + if ((opcode.Attr.LoadArgs + opcode.Attr.StoreArgs + 1) % 2 == 0) + { + type = (byte)(image.ReadByte(pc) & 0xF); + } + else + { + type = (byte)((image.ReadByte(pc) >> 4) & 0xF); + pc++; + } + + WriteTrace(" ?"); + operands[opcode.Attr.LoadArgs + opcode.Attr.StoreArgs + 2] = + DecodeLoadOperand(opcode, type, ref operandPos); + } + + WriteTrace("]\r\n"); + + // call opcode implementation + pc = operandPos; // after the last operand + opcode.Handler(operands); + + // store results + for (int i = 0; i < opcode.Attr.StoreArgs; i++) + StoreResult(opcode.Attr.Rule, resultTypes[i], resultAddrs[i], + operands[opcode.Attr.LoadArgs + i]); + break; + + case ExecutionMode.CString: + NextCStringChar(); + break; + + case ExecutionMode.UnicodeString: + NextUniStringChar(); + break; + + case ExecutionMode.Number: + NextDigit(); + break; + + case ExecutionMode.CompressedString: + if (nativeDecodingTable != null) + nativeDecodingTable.HandleNextChar(this); + else + NextCompressedChar(); + break; + + case ExecutionMode.Return: + return; + } + +#if PROFILING + cycles++; +#endif + } + } + catch (Exception ex) + { + string s = ex.Message; + throw; + } + } + + private uint DecodeLoadOperand(Opcode opcode, byte type, ref uint operandPos) + { + uint address, value; + switch (type) + { + case 0: + WriteTrace("zero"); + return 0; + case 1: + value = (uint)(sbyte)image.ReadByte(operandPos++); + WriteTrace("byte_" + value.ToString()); + return value; + case 2: + operandPos += 2; + value = (uint)(short)image.ReadInt16(operandPos - 2); + WriteTrace("short_" + value.ToString()); + return value; + case 3: + operandPos += 4; + value = image.ReadInt32(operandPos - 4); + WriteTrace("int_" + value.ToString()); + return value; + + // case 4: unused + + case 5: + address = image.ReadByte(operandPos++); + WriteTrace("ptr"); + goto LoadIndirect; + case 6: + address = image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ptr"); + goto LoadIndirect; + case 7: + address = image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ptr"); + LoadIndirect: + WriteTrace("_" + address.ToString() + "("); + switch (opcode.Attr.Rule) + { + case OpcodeRule.Indirect8Bit: + value = image.ReadByte(address); + break; + case OpcodeRule.Indirect16Bit: + value = image.ReadInt16(address); + break; + default: + value = image.ReadInt32(address); + break; + } + WriteTrace(value.ToString() + ")"); + return value; + + case 8: + if (sp <= fp + frameLen) + throw new VMException("Stack underflow"); + value = Pop(); + WriteTrace("sp(" + value.ToString() + ")"); + return value; + + case 9: + address = image.ReadByte(operandPos++); + goto LoadLocal; + case 10: + address = image.ReadInt16(operandPos); + operandPos += 2; + goto LoadLocal; + case 11: + address = image.ReadInt32(operandPos); + operandPos += 4; + LoadLocal: + WriteTrace("local_" + address.ToString() + "("); + address += fp + localsPos; + switch (opcode.Attr.Rule) + { + case OpcodeRule.Indirect8Bit: + if (address >= fp + frameLen) + throw new VMException("Reading outside local storage bounds"); + else + value = stack[address]; + break; + case OpcodeRule.Indirect16Bit: + if (address + 1 >= fp + frameLen) + throw new VMException("Reading outside local storage bounds"); + else + value = BigEndian.ReadInt16(stack, address); + break; + default: + if (address + 3 >= fp + frameLen) + throw new VMException("Reading outside local storage bounds"); + else + value = ReadFromStack(address); + break; + } + WriteTrace(value.ToString() + ")"); + return value; + + // case 12: unused + + case 13: + address = image.RamStart + image.ReadByte(operandPos++); + WriteTrace("ram"); + goto LoadIndirect; + case 14: + address = image.RamStart + image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ram"); + goto LoadIndirect; + case 15: + address = image.RamStart + image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ram"); + goto LoadIndirect; + + default: + throw new ArgumentException("Invalid operand type"); + } + } + + private uint DecodeStoreOperand(Opcode opcode, byte type, ref uint operandPos) + { + uint address; + switch (type) + { + case 0: + // discard result + WriteTrace("discard"); + return 0; + + // case 1..4: unused + + case 5: + address = image.ReadByte(operandPos++); + WriteTrace("ptr_" + address.ToString()); + break; + case 6: + address = image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ptr_" + address.ToString()); + break; + case 7: + address = image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ptr_" + address.ToString()); + break; + + // case 8: push onto stack + case 8: + // push onto stack + WriteTrace("sp"); + return 0; + + case 9: + address = image.ReadByte(operandPos++); + WriteTrace("local_" + address.ToString()); + break; + case 10: + address = image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("local_" + address.ToString()); + break; + case 11: + address = image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("local_" + address.ToString()); + break; + + // case 12: unused + + case 13: + address = image.RamStart + image.ReadByte(operandPos++); + WriteTrace("ram_" + (address - image.RamStart).ToString()); + break; + case 14: + address = image.RamStart + image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ram_" + (address - image.RamStart).ToString()); + break; + case 15: + address = image.RamStart + image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ram_" + (address - image.RamStart).ToString()); + break; + + default: + throw new ArgumentException("Invalid operand type"); + } + return address; + } + + private void StoreResult(OpcodeRule rule, byte type, uint address, uint value) + { + switch (type) + { + case 5: + case 6: + case 7: + case 13: + case 14: + case 15: + // write to memory + switch (rule) + { + case OpcodeRule.Indirect8Bit: + image.WriteByte(address, (byte)value); + break; + case OpcodeRule.Indirect16Bit: + image.WriteInt16(address, (ushort)value); + break; + default: + image.WriteInt32(address, value); + break; + } + break; + + case 9: + case 10: + case 11: + // write to local storage + address += fp + localsPos; + switch (rule) + { + case OpcodeRule.Indirect8Bit: + if (address >= fp + frameLen) + throw new VMException("Writing outside local storage bounds"); + else + stack[address] = (byte)value; + break; + case OpcodeRule.Indirect16Bit: + if (address + 1 >= fp + frameLen) + throw new VMException("Writing outside local storage bounds"); + else + BigEndian.WriteInt16(stack, address, (ushort)value); + break; + default: + if (address + 3 >= fp + frameLen) + throw new VMException("Writing outside local storage bounds"); + else + WriteToStack(address, value); + break; + } + break; + + case 8: + // push onto stack + Push(value); + break; + } + } + + private void DecodeDelayedStoreOperand(byte type, ref uint operandPos, + uint[] resultArray, int resultIndex) + { + switch (type) + { + case 0: + // discard result + resultArray[resultIndex] = GLULX_STUB_STORE_NULL; + resultArray[resultIndex + 1] = 0; + WriteTrace("discard"); + break; + + // case 1..4: unused + + case 5: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.ReadByte(operandPos++); + WriteTrace("ptr_" + (resultArray[resultIndex + 1]).ToString()); + break; + case 6: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ptr_" + (resultArray[resultIndex + 1]).ToString()); + break; + case 7: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ptr_" + (resultArray[resultIndex + 1]).ToString()); + break; + + // case 8: push onto stack + case 8: + // push onto stack + resultArray[resultIndex] = GLULX_STUB_STORE_STACK; + resultArray[resultIndex + 1] = 0; + WriteTrace("sp"); + break; + + case 9: + resultArray[resultIndex] = GLULX_STUB_STORE_LOCAL; + resultArray[resultIndex + 1] = image.ReadByte(operandPos++); + WriteTrace("local_" + (resultArray[resultIndex + 1]).ToString()); + break; + case 10: + resultArray[resultIndex] = GLULX_STUB_STORE_LOCAL; + resultArray[resultIndex + 1] = image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("local_" + (resultArray[resultIndex + 1]).ToString()); + break; + case 11: + resultArray[resultIndex] = GLULX_STUB_STORE_LOCAL; + resultArray[resultIndex + 1] = image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("local_" + (resultArray[resultIndex + 1]).ToString()); + break; + + // case 12: unused + + case 13: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.RamStart + image.ReadByte(operandPos++); + WriteTrace("ram_" + (resultArray[resultIndex + 1] - image.RamStart).ToString()); + break; + case 14: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.RamStart + image.ReadInt16(operandPos); + operandPos += 2; + WriteTrace("ram_" + (resultArray[resultIndex + 1] - image.RamStart).ToString()); + break; + case 15: + resultArray[resultIndex] = GLULX_STUB_STORE_MEM; + resultArray[resultIndex + 1] = image.RamStart + image.ReadInt32(operandPos); + operandPos += 4; + WriteTrace("ram_" + (resultArray[resultIndex + 1] - image.RamStart).ToString()); + break; + + default: + throw new ArgumentException("Invalid operand type"); + } + } + + private void PerformDelayedStore(uint type, uint address, uint value) + { + switch (type) + { + case GLULX_STUB_STORE_NULL: + // discard + break; + case GLULX_STUB_STORE_MEM: + // store in main memory + image.WriteInt32(address, value); + break; + case GLULX_STUB_STORE_LOCAL: + // store in local storage + WriteToStack(fp + localsPos + address, value); + break; + case GLULX_STUB_STORE_STACK: + // push onto stack + Push(value); + break; + } + } + + /// + /// Pushes a frame for a function call, updating FP, SP, and PC. + /// (A call stub should have already been pushed.) + /// + /// The address of the function being called. + private void EnterFunction(uint address) + { + EnterFunction(address, null); + } + + /// + /// Pushes a frame for a function call, updating FP, SP, and PC. + /// (A call stub should have already been pushed.) + /// + /// The address of the function being called. + /// The argument values to load into local storage, + /// or if local storage should all be zeroed. + private void EnterFunction(uint address, uint[] args) + { +#if PROFILING + profiler.Enter(address, cycles); +#endif + execMode = ExecutionMode.Code; + + // push a call frame + fp = sp; + Push(0); // temporary FrameLen + Push(0); // temporary LocalsPos + + // copy locals info into the frame... + uint localSize = 0; + + for (uint i = address + 1; true; i += 2) + { + byte type, count; + stack[sp++] = type = image.ReadByte(i); + stack[sp++] = count = image.ReadByte(i + 1); + if (type == 0 || count == 0) + { + pc = i + 2; + break; + } + if (localSize % type > 0) + localSize += (type - (localSize % type)); + localSize += (uint)(type * count); + } + // padding + while (sp % 4 > 0) + stack[sp++] = 0; + + localsPos = sp - fp; + WriteToStack(fp + 4, localsPos); // fill in LocalsPos + + if (args == null || args.Length == 0) + { + // initialize space for locals + for (uint i = 0; i < localSize; i++) + stack[sp + i] = 0; + } + else + { + // copy initial values as appropriate + uint offset = 0, lastOffset = 0; + byte size = 0, count = 0; + address++; + for (uint argnum = 0; argnum < args.Length; argnum++) + { + if (count == 0) + { + size = image.ReadByte(address++); + count = image.ReadByte(address++); + if (size == 0 || count == 0) + break; + if (offset % size > 0) + offset += (size - (offset % size)); + } + + // zero any padding space between locals + for (uint i = lastOffset; i < offset; i++) + stack[sp + i] = 0; + + switch (size) + { + case 1: + stack[sp + offset] = (byte)args[argnum]; + break; + case 2: + BigEndian.WriteInt16(stack, sp + offset, (ushort)args[argnum]); + break; + case 4: + WriteToStack(sp + offset, args[argnum]); + break; + } + + offset += size; + lastOffset = offset; + count--; + } + + // zero any remaining local space + for (uint i = lastOffset; i < localSize; i++) + stack[sp + i] = 0; + } + sp += localSize; + // padding + while (sp % 4 > 0) + stack[sp++] = 0; + + frameLen = sp - fp; + WriteToStack(fp, frameLen); // fill in FrameLen + } + + private void LeaveFunction(uint result) + { +#if PROFILING + profiler.Leave(cycles); +#endif + if (fp == 0) + { + // top-level function + running = false; + } + else + { + System.Diagnostics.Debug.Assert(sp >= fp); + sp = fp; + ResumeFromCallStub(result); + } + } + + private void ResumeFromCallStub(uint result) + { + CallStub stub = PopCallStub(); + + pc = stub.PC; + execMode = ExecutionMode.Code; + + uint newFP = stub.FramePtr; + uint newFrameLen = ReadFromStack(newFP); + uint newLocalsPos = ReadFromStack(newFP + 4); + + switch (stub.DestType) + { + case GLULX_STUB_STORE_NULL: + // discard + break; + case GLULX_STUB_STORE_MEM: + // store in main memory + image.WriteInt32(stub.DestAddr, result); + break; + case GLULX_STUB_STORE_LOCAL: + // store in local storage + WriteToStack(newFP + newLocalsPos + stub.DestAddr, result); + break; + case GLULX_STUB_STORE_STACK: + // push onto stack + Push(result); + break; + + case GLULX_STUB_RESUME_FUNC: + // resume executing in the same call frame + // return to avoid changing FP + return; + + case GLULX_STUB_RESUME_CSTR: + // resume printing a C-string + execMode = ExecutionMode.CString; + break; + case GLULX_STUB_RESUME_UNISTR: + // resume printing a Unicode string + execMode = ExecutionMode.UnicodeString; + break; + case GLULX_STUB_RESUME_NUMBER: + // resume printing a decimal number + execMode = ExecutionMode.Number; + printingDigit = (byte)stub.DestAddr; + break; + case GLULX_STUB_RESUME_HUFFSTR: + // resume printing a compressed string + execMode = ExecutionMode.CompressedString; + printingDigit = (byte)stub.DestAddr; + break; + + case FYREVM_STUB_RESUME_NATIVE: + // exit the interpreter loop and return via NestedCall() + nestedResult = result; + execMode = ExecutionMode.Return; + break; + } + + fp = newFP; + frameLen = newFrameLen; + localsPos = newLocalsPos; + return; + } + + private void InputLine(uint address, uint bufSize) + { + string input = null; + + // we need at least 4 bytes to do anything useful + if (bufSize < 4) + return; + + // can't do anything without this event handler + if (LineWanted == null) + { + image.WriteInt32(address, 0); + return; + } + + LineWantedEventArgs lineArgs = new LineWantedEventArgs(); + // CancelEventArgs waitArgs = new CancelEventArgs(); + + // ask the application to read a line + LineWanted(this, lineArgs); + input = lineArgs.Line; + + if (input == null) + { + image.WriteInt32(address, 0); + } + else + { + byte[] bytes = null; + // write the length first + try + { + bytes = StringToLatin1(input); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + } + image.WriteInt32(address, (uint)bytes.Length); + // followed by the character data, truncated to fit the buffer + uint max = Math.Min(bufSize, (uint)bytes.Length); + for (uint i = 0; i < max; i++) + image.WriteByte(address + 4 + i, bytes[i]); + } + } + + // quick 'n dirty translation, because Silverlight doesn't support ISO-8859-1 encoding + private static byte[] StringToLatin1(string str) + { + byte[] result = new byte[str.Length]; + + for (int i = 0; i < str.Length; i++) + { + ushort value = (ushort)str[i]; + if (value > 255) + result[i] = (byte)'?'; + else + result[i] = (byte)value; + } + + return result; + } + + private char InputChar() + { + // can't do anything without this event handler + if (KeyWanted == null) + return '\0'; + + KeyWantedEventArgs keyArgs = new KeyWantedEventArgs(); + //CancelEventArgs waitArgs = new CancelEventArgs(); + + // ask the application to read a character + KeyWanted(this, keyArgs); + return keyArgs.Char; + } + + private void SaveToStream(Stream stream, uint destType, uint destAddr) + { + if (stream == null) + { + return; + } + + Quetzal quetzal = new Quetzal(); + + // 'IFhd' identifies the first 128 bytes of the game file + quetzal["IFhd"] = image.GetOriginalIFHD(); + + // 'CMem' or 'UMem' are the compressed/uncompressed contents of RAM + byte[] origRam = image.GetOriginalRAM(); + byte[] newRomRam = image.GetMemory(); + int ramSize = (int)(image.EndMem - image.RamStart); +#if !SAVE_UNCOMPRESSED + quetzal["CMem"] = Quetzal.CompressMemory( + origRam, 0, origRam.Length, + newRomRam, (int)image.RamStart, ramSize); +#else + byte[] umem = new byte[ramSize + 4]; + BigEndian.WriteInt32(umem, 0, (uint)ramSize); + Array.Copy(newRomRam, (int)image.RamStart, umem, 4, ramSize); + quetzal["UMem"] = umem; +#endif + + // 'Stks' is the contents of the stack, with a stub on top + // identifying the destination of the save opcode. + PushCallStub(new CallStub(destType, destAddr, pc, fp)); + byte[] trimmed = new byte[sp]; + Array.Copy(stack, trimmed, (int)sp); + //for (uint bt=0; bt < sp; bt++) + //{ + // trimmed[bt] = stack[bt]; + //} + quetzal["Stks"] = trimmed; + PopCallStub(); + + // 'MAll' is the list of heap blocks + if (heap != null) + quetzal["MAll"] = heap.Save(); + else + { + + } + + quetzal.WriteToStream(stream); + } + + private void LoadFromStream(Stream stream) + { + Quetzal quetzal = Quetzal.FromStream(stream); + + // make sure the save file matches the game file + byte[] ifhd1 = quetzal["IFhd"]; + byte[] ifhd2 = image.GetOriginalIFHD(); + if (ifhd1 == null || ifhd1.Length != ifhd2.Length) + throw new ArgumentException("Missing or invalid IFhd block"); + + for (int i = 0; i < ifhd1.Length; i++) + if (ifhd1[i] != ifhd2[i]) + throw new ArgumentException("Saved game doesn't match this story file"); + + // load the stack + byte[] newStack = quetzal["Stks"]; + if (newStack == null) + throw new ArgumentException("Missing Stks block"); + + Array.Copy(newStack, stack, newStack.Length); + sp = (uint)newStack.Length; + + // save the protected area of RAM + byte[] protectedRam = new byte[protectionLength]; + image.ReadRAM(protectionStart, protectionLength, protectedRam); + + // load the contents of RAM, preferring a compressed chunk + byte[] origRam = image.GetOriginalRAM(); + byte[] delta = quetzal["CMem"]; + if (delta != null) + { + byte[] newRam = Quetzal.DecompressMemory(origRam, delta); + image.SetRAM(newRam, false); + } + else + { + // look for an uncompressed chunk + byte[] newRam = quetzal["UMem"]; + if (newRam == null) + throw new ArgumentException("Missing CMem/UMem blocks"); + else + image.SetRAM(newRam, true); + } + + // restore protected RAM + image.WriteRAM(protectionStart, protectedRam); + + // pop a call stub to restore registers + CallStub stub = PopCallStub(); + pc = stub.PC; + fp = stub.FramePtr; + frameLen = ReadFromStack(fp); + localsPos = ReadFromStack(fp + 4); + execMode = ExecutionMode.Code; + + // restore the heap if available + if (quetzal.Contains("MAll")) + { + heap = new HeapAllocator(quetzal["MAll"], HandleHeapMemoryRequest); + if (heap.BlockCount == 0) + heap = null; + else + heap.MaxSize = maxHeapSize; + } + + // give the original save opcode a result of -1 to show that it's been restored + PerformDelayedStore(stub.DestType, stub.DestAddr, 0xFFFFFFFF); + } + + /// + /// Reloads the initial contents of memory (except the protected area) + /// and starts the game over from the top of the main function. + /// + private void Restart() + { + // save the protected area of RAM + byte[] protectedRam = new byte[protectionLength]; + image.ReadRAM(protectionStart, protectionLength, protectedRam); + + // reload memory, reinitialize registers and stacks + image.Revert(); + Bootstrap(); + + // restore protected RAM + image.WriteRAM(protectionStart, protectedRam); + CacheDecodingTable(); + } + + /// + /// Terminates the interpreter loop, causing the + /// method to return. + /// + public void Stop() + { + running = false; + } + + } +} diff --git a/EngineWrapper.cs b/EngineWrapper.cs new file mode 100644 index 0000000..150e188 --- /dev/null +++ b/EngineWrapper.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Text; +using System.Diagnostics; +using Newtonsoft.Json; + +using FyreVM; + +namespace Zifmia.FyreVM.Service +{ + /// + /// The EngineWrapper is a state machine around the FyreVM Engine. It needs to be able handle the following + /// scenarios: + /// + /// 1. Start Game: start engine, load game, store output, save, return stored output, add output to saved-data + /// 2. Send Command: execute command, store output, save, return stored output, add output to saved-data + /// 3. Save: save, report saved + /// 4. Restore: load game, return output + /// 5. + /// + public class EngineWrapper + { + private Engine vm; + string entry = ""; + string saveCommand; + Stream saveFileData; + + //string outputXML; + Dictionary outputHash; + string outputJSON; + byte[] outSaveFile; + MemoryStream saveStream; + MemoryStream restoreStream; + + // The default is to load a game and return with any prologue data... + VMWrapperState wrapperState = VMWrapperState.LoadGame; + VMRequestType requestType = VMRequestType.StartGame; + + // + // Operation 1 (engine is not loaded, start game) Load the game file, store output, save game, store save file, stop vm. + // Operation 2 (engine is loaded, execute command) Set command to execute, start vm, set line input to command, store output, save game, store save file, stop vm. + // Operation 3 (engine is loaded, restore game) Set loadstream, start vm, store output, stop vm. + // Operation 4 (engine is not loaded, execute command) Load the game file, set loadstream, restore game, store output, stop vm. + // + + public enum VMRequestType + { + StartGame, + StartExistingGame, + ExecuteCommand, + NoCommand + } + + public enum VMWrapperState + { + LoadGame, + RunCommand, + RequestRestore, + RequestSave, + Completed + } + + public EngineWrapper() { } + + /// + /// Assume we're running a command and have save game data. + /// + /// + /// + /// + /// + Boolean _isCurrentRestore = false; + public EngineWrapper(byte[] gameFile, byte[] saveFile) + { + if (gameFile == null) + throw new Exception("Missing game data."); + + if (saveFile == null) + throw new Exception("Missing required save file."); + + MemoryStream gameData = new MemoryStream(gameFile); + saveFileData = new MemoryStream(saveFile); + + vm = new Engine(gameData); + } + + /// + /// Load the game and return data. + /// + /// + public EngineWrapper(byte[] gameFile) + { + if (gameFile == null) + throw new Exception("Missing game file."); + + MemoryStream gameData = new MemoryStream(gameFile); + + vm = new Engine(gameData); + + requestType = VMRequestType.StartGame; + wrapperState = VMWrapperState.LoadGame; + + Run(); + } + + public void SendCommand(string command) + { + wrapperState = VMWrapperState.RunCommand; + requestType = VMRequestType.ExecuteCommand; + + saveCommand = command; + + vm.Continue(); + } + + public void Restore(byte[] restoreData) + { + restoreStream = new MemoryStream(restoreData); + + wrapperState = VMWrapperState.RequestRestore; + requestType = VMRequestType.ExecuteCommand; + + needLine = true; + + vm.Continue(); + } + + private void Run() + { + vm.OutputReady += new OutputReadyEventHandler(vm_OutputReady); + vm.LineWanted += new LineWantedEventHandler(vm_LineWanted); + vm.KeyWanted += new KeyWantedEventHandler(vm_KeyWanted); + vm.SaveRequested += new SaveRestoreEventHandler(vm_SaveRequested); + vm.LoadRequested += new SaveRestoreEventHandler(vm_LoadRequested); + vm._IsCurrentRestore = _isCurrentRestore; + vm.Run(); + } + + /// + /// Starting game + /// - retrieves output (startup) + /// - ignore output (save) + /// + /// Entering a command + /// - ignore output (startup and load) + /// - retrieves output (command) + /// - ignore output (save) + /// + /// + /// + private void HandleOutput(Dictionary package) + { + // Reset hashtable + outputHash = package; + + //XmlWriterSettings settings = new XmlWriterSettings(); + //settings.OmitXmlDeclaration = true; + StringWriter sWriter = new StringWriter(); + + //using (XmlWriter writer = XmlWriter.Create(sWriter, settings)) + //{ + // // Open XML stream + // writer.WriteStartDocument(); + // writer.WriteStartElement("fyrevm"); + // writer.WriteStartElement("channels"); + + // // loop through results and output to xml + // foreach (string channel in package.Keys) + // { + // SetChannelData(channel, package, writer); + // } + + // writer.WriteEndElement(); + // writer.WriteEndElement(); + // writer.WriteEndDocument(); + + // writer.Flush(); + // outputXML = sWriter.ToString(); + //} + + StringBuilder data = sWriter.GetStringBuilder(); + + // Open JSON stream + sWriter = new StringWriter(); + JsonTextWriter jWriter = new JsonTextWriter(sWriter); + + jWriter.WriteStartObject(); + jWriter.WritePropertyName("channels"); + jWriter.WriteStartArray(); + + foreach (string channel in package.Keys) + { + jWriter.WriteStartObject(); + SetChannelDataJSON(channel, package, jWriter); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + jWriter.Close(); + + data = sWriter.GetStringBuilder(); + outputJSON = data.ToString(); + } + + //private void SetChannelData(string channel, Dictionary package, XmlWriter writer) + //{ + // string text = ""; + // string channelName = channel; + + // if (package.TryGetValue(channel, out text)) + // { + // WriteElementCDATA(writer, channelName, text.Trim()); + // } + // else + // { + // WriteElementCDATA(writer, channelName, ""); + // } + //} + + private void SetChannelDataJSON(string channel, Dictionary package, JsonTextWriter writer) + { + string text = ""; + string channelName = channel; + + if (package.TryGetValue(channel, out text)) + { + writer.WritePropertyName(channel); + writer.WriteValue(text); + } + else + { + writer.WritePropertyName(channel); + writer.WriteValue(""); + } + } + + //private void WriteElementCDATA(XmlWriter xWriter, string elementName, string text) + //{ + // xWriter.WriteStartElement(elementName); + // xWriter.WriteCData(text); + // xWriter.WriteEndElement(); + //} + + private bool needLine = true; + private void vm_OutputReady(object sender, OutputReadyEventArgs e) + { + // ----------- DECIDE TO STORE OUTPUT -------------- + + if (!needLine || wrapperState == VMWrapperState.LoadGame) + { + if ((wrapperState == VMWrapperState.LoadGame && requestType == VMRequestType.StartGame) || wrapperState == VMWrapperState.RunCommand) + { + HandleOutput((Dictionary)e.Package); + } + + // ----------- DETERMINE STATE ------------- + + if (wrapperState == VMWrapperState.RequestSave) + { + outSaveFile = saveStream.ToArray(); + wrapperState = VMWrapperState.Completed; + vm.Stop(); + } + + if (wrapperState == VMWrapperState.RunCommand || (wrapperState == VMWrapperState.LoadGame && requestType == VMRequestType.StartGame)) + wrapperState = VMWrapperState.RequestSave; + + if (wrapperState == VMWrapperState.RequestRestore && requestType == VMRequestType.ExecuteCommand) + wrapperState = VMWrapperState.RunCommand; + + if (wrapperState == VMWrapperState.LoadGame && requestType == VMRequestType.ExecuteCommand) + wrapperState = VMWrapperState.RequestRestore; + + needLine = true; + } + } + + private void vm_LineWanted(object sender, LineWantedEventArgs e) + { + if (wrapperState == VMWrapperState.RequestRestore) + entry = "restore"; + + if (wrapperState == VMWrapperState.RunCommand) + entry = saveCommand; + + if (wrapperState == VMWrapperState.RequestSave) + entry = "save"; + + if (wrapperState == VMWrapperState.Completed) + entry = null; + + needLine = false; + e.Line = entry; + } + + private void vm_KeyWanted(object sender, KeyWantedEventArgs e) + { + e.Char = entry[0]; + } + + private void vm_SaveRequested(object sender, SaveRestoreEventArgs e) + { + saveStream = new MemoryStream(); + e.Stream = saveStream; + } + + private void vm_LoadRequested(object sender, SaveRestoreEventArgs e) + { + e.Stream = restoreStream; + } + + public string ToJSON + { + get + { + return outputJSON; + } + } + + //public string ToXML + //{ + // get + // { + // return outputXML; + // } + //} + + public string FromHash(string channelName) + { + if (outputHash.ContainsKey(channelName)) + return (string)outputHash[channelName]; + else + return ""; + } + + public Dictionary FromHash() + { + return outputHash; + } + + public byte[] SaveFile + { + get + { + return outSaveFile; + } + } + + public MemoryStream SaveStream { + get { + return saveStream; + } + } + + private VMWrapperState WrapperState + { + get + { + return wrapperState; + } + } + } +} diff --git a/GlkWrapper.cs b/GlkWrapper.cs new file mode 100644 index 0000000..7e9070b --- /dev/null +++ b/GlkWrapper.cs @@ -0,0 +1,440 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.IO; + +namespace FyreVM +{ + partial class Engine + { + private const int MAX_GLK_ARGS = 8; + private uint[] glkArgs = new uint[MAX_GLK_ARGS]; + private Dictionary> glkHandlers; + private bool glkWindowOpen, glkWantLineInput, glkWantCharInput; + private uint glkLineInputBuffer, glkLineInputBufSize; + private Dictionary glkStreams; + private uint glkNextStreamID = 100; + private GlkStream glkCurrentStream; + + private static class GlkConst + { + public const uint wintype_TextBuffer = 3; + + public const uint evtype_None = 0; + public const uint evtype_CharInput = 2; + public const uint evtype_LineInput = 3; + + public const uint gestalt_CharInput = 1; + public const uint gestalt_CharOutput = 3; + public const uint gestalt_CharOutput_ApproxPrint = 1; + public const uint gestalt_CharOutput_CannotPrint = 0; + public const uint gestalt_CharOutput_ExactPrint = 2; + public const uint gestalt_LineInput = 2; + public const uint gestalt_Version = 0; + } + + private abstract class GlkStream + { + public readonly uint ID; + + public GlkStream(uint id) + { + this.ID = id; + } + + public abstract void PutChar(uint c); + + public abstract bool Close(out uint read, out uint written); + } + + private class GlkWindowStream : GlkStream + { + private readonly Engine engine; + + public GlkWindowStream(uint id, Engine engine) + : base(id) + { + this.engine = engine; + } + + public override void PutChar(uint c) + { + if (c > 0xffff) + c = '?'; + + engine.outputBuffer.Write((char)c); + } + + public override bool Close(out uint read, out uint written) + { + written = 0; + read = 0; + return false; + } + } + + private class GlkMemoryStream : GlkStream + { + private readonly Engine engine; + private readonly uint address; + private readonly byte[] buffer; + private uint position, written, read; + + public GlkMemoryStream(uint id, Engine engine, uint address, uint size) + : base(id) + { + this.engine = engine; + this.address = address; + + if (address != 0 && size != 0) + buffer = new byte[size]; + + position = written = read = 0; + } + + public override void PutChar(uint c) + { + if (c > 0xff) + c = '?'; + + written++; + if (position < buffer.Length) + buffer[position++] = (byte)c; + } + + public override bool Close(out uint read, out uint written) + { + written = this.written; + read = this.read; + + if (buffer != null) + { + uint max = (uint)Math.Min(written, buffer.Length); + for (uint i = 0; i < max; i++) + engine.image.WriteByte(address + i, buffer[i]); + } + + return true; + } + } + + private class GlkMemoryUniStream : GlkStream + { + private readonly Engine engine; + private readonly uint address; + private readonly uint[] buffer; + private uint position, written, read; + + public GlkMemoryUniStream(uint id, Engine engine, uint address, uint size) + : base(id) + { + this.engine = engine; + this.address = address; + + if (address != 0 && size != 0) + buffer = new uint[size]; + + position = written = read = 0; + } + + public override void PutChar(uint c) + { + written++; + if (position < buffer.Length) + buffer[position++] = c; + } + + public override bool Close(out uint read, out uint written) + { + written = this.written; + read = this.read; + + if (buffer != null) + { + uint max = (uint)Math.Min(written, buffer.Length); + for (uint i = 0; i < max; i++) + engine.image.WriteInt32(address + i * 4, buffer[i]); + } + + return true; + } + } + + private void GlkWrapperCall(uint[] args) + { + System.Diagnostics.Debug.WriteLine(string.Format("glk(0x{0:X4}, {1})", args[0], args[1])); + + if (glkHandlers == null) + InitGlkHandlers(); + + int gargc = (int)args[1]; + if (gargc > MAX_GLK_ARGS) + throw new ArgumentException("Too many stack arguments for @glk"); + + for (int i = 0; i < gargc; i++) + glkArgs[i] = Pop(); + + Func handler; + if (glkHandlers.TryGetValue(args[0], out handler)) + { + System.Diagnostics.Debug.WriteLine(" // " + handler.Target.GetType().Name); + args[2] = handler(glkArgs); + } + else + { + System.Diagnostics.Debug.WriteLine(" // unimplemented"); + args[2] = 0; + } + } + + private void GlkWrapperWrite(uint ch) + { + if (glkCurrentStream != null) + glkCurrentStream.PutChar(ch); + } + + private void GlkWrapperWrite(string str) + { + if (glkCurrentStream != null) + foreach (char c in str) + glkCurrentStream.PutChar(c); + } + + private uint GlkReadReference(uint reference) + { + if (reference == 0xffffffff) + return Pop(); + + return image.ReadInt32(reference); + } + + private void GlkWriteReference(uint reference, uint value) + { + if (reference == 0xffffffff) + Push(value); + else + image.WriteInt32(reference, value); + } + + private void GlkWriteReference(uint reference, params uint[] values) + { + if (reference == 0xffffffff) + { + foreach (uint v in values) + Push(v); + } + else + { + foreach (uint v in values) + { + image.WriteInt32(reference, v); + reference += 4; + } + } + } + + private void InitGlkHandlers() + { + glkHandlers = new Dictionary>(); + + glkHandlers.Add(0x0040, glk_stream_iterate); + glkHandlers.Add(0x0020, glk_window_iterate); + glkHandlers.Add(0x0064, glk_fileref_iterate); + glkHandlers.Add(0x0023, glk_window_open); + glkHandlers.Add(0x002F, glk_set_window); + glkHandlers.Add(0x0086, glk_set_style); + glkHandlers.Add(0x00D0, glk_request_line_event); + glkHandlers.Add(0x00C0, glk_select); + glkHandlers.Add(0x00A0, glk_char_to_lower); + glkHandlers.Add(0x00A1, glk_char_to_upper); + glkHandlers.Add(0x0043, glk_stream_open_memory); + glkHandlers.Add(0x0048, glk_stream_get_current); + glkHandlers.Add(0x0139, glk_stream_open_memory_uni); + glkHandlers.Add(0x0047, glk_stream_set_current); + glkHandlers.Add(0x0044, glk_stream_close); + + glkStreams = new Dictionary(); + } + + private uint glk_stream_iterate(uint[] args) + { + return 0; + } + + private uint glk_window_iterate(uint[] args) + { + if (glkWindowOpen && args[0] == 0) + return 1; + + return 0; + } + + private uint glk_fileref_iterate(uint[] args) + { + return 0; + } + + private uint glk_window_open(uint[] args) + { + if (glkWindowOpen) + return 0; + + glkWindowOpen = true; + glkStreams[1] = new GlkWindowStream(1, this); + return 1; + } + + private uint glk_set_window(uint[] args) + { + if (glkWindowOpen) + glkStreams.TryGetValue(1, out glkCurrentStream); + + return 0; + } + + private uint glk_set_style(uint[] args) + { + return 0; + } + + private uint glk_request_line_event(uint[] args) + { + glkWantLineInput = true; + glkLineInputBuffer = args[1]; + glkLineInputBufSize = args[2]; + return 0; + } + + private uint glk_select(uint[] args) + { + DeliverOutput(); + + if (glkWantLineInput) + { + string line; + if (LineWanted == null) + { + line = ""; + } + else + { + LineWantedEventArgs e = new LineWantedEventArgs(); + LineWanted(this, e); + line = e.Line; + } + + byte[] bytes = StringToLatin1(line); + uint max = Math.Min(glkLineInputBufSize, (uint)bytes.Length); + for (uint i = 0; i < max; i++) + image.WriteByte(glkLineInputBuffer + i, bytes[i]); + + // return event + GlkWriteReference( + args[0], + GlkConst.evtype_LineInput, 1, max, 0); + + glkWantLineInput = false; + } + else if (glkWantCharInput) + { + char ch; + if (KeyWanted == null) + { + ch = '\0'; + } + else + { + KeyWantedEventArgs e = new KeyWantedEventArgs(); + KeyWanted(this, e); + ch = e.Char; + } + + // return event + GlkWriteReference( + args[0], + GlkConst.evtype_CharInput, 1, ch, 0); + + glkWantCharInput = false; + } + else + { + // no event + GlkWriteReference( + args[0], + GlkConst.evtype_None, 0, 0, 0); + } + + return 0; + } + + private uint glk_char_to_lower(uint[] args) + { + char ch = (char)args[0]; + return (uint)char.ToLower(ch); + } + + private uint glk_char_to_upper(uint[] args) + { + char ch = (char)args[0]; + return (uint)char.ToUpper(ch); + } + + private uint glk_stream_open_memory(uint[] args) + { + uint id = glkNextStreamID++; + GlkStream stream = new GlkMemoryStream(id, this, args[0], args[1]); + glkStreams[id] = stream; + return id; + } + + private uint glk_stream_open_memory_uni(uint[] args) + { + uint id = glkNextStreamID++; + GlkStream stream = new GlkMemoryUniStream(id, this, args[0], args[1]); + glkStreams[id] = stream; + return id; + } + + private uint glk_stream_get_current(uint[] args) + { + if (glkCurrentStream == null) + return 0; + + return glkCurrentStream.ID; + } + + private uint glk_stream_set_current(uint[] args) + { + glkStreams.TryGetValue(args[0], out glkCurrentStream); + return 0; + } + + private uint glk_stream_close(uint[] args) + { + GlkStream stream; + if (glkStreams.TryGetValue(args[0], out stream)) + { + uint read, written; + bool closed = stream.Close(out read, out written); + if (args[1] != 0) + { + GlkWriteReference( + args[1], + read, written); + } + + if (closed) + { + glkStreams.Remove(args[0]); + if (glkCurrentStream == stream) + glkCurrentStream = null; + } + } + + return 0; + } + } +} \ No newline at end of file diff --git a/HeapAllocator.cs b/HeapAllocator.cs new file mode 100644 index 0000000..9c19f46 --- /dev/null +++ b/HeapAllocator.cs @@ -0,0 +1,312 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace FyreVM +{ + internal struct HeapEntry + { + public uint Start, Length; + + public HeapEntry(uint start, uint length) + { + this.Start = start; + this.Length = length; + } + + public override string ToString() + { + return string.Format("Start={0}, Length={1}", Start, Length); + } + } + + internal delegate bool MemoryRequester(uint newEndMem); + + /// + /// Manages the heap size and block allocation for the malloc/mfree opcodes. + /// + /// + /// If Inform ever starts using the malloc opcode directly, instead of + /// its own heap allocator, this should be made a little smarter. + /// Currently we make no attempt to avoid heap fragmentation. + /// + internal class HeapAllocator + { + private class EntryComparer : IComparer + { + public int Compare(HeapEntry x, HeapEntry y) + { + return x.Start.CompareTo(y.Start); + } + } + + private static readonly EntryComparer entryComparer = new EntryComparer(); + + private readonly uint heapAddress; + private readonly MemoryRequester setEndMem; + private readonly List blocks; // sorted + private readonly List freeList; // sorted + + private uint endMem; + private uint heapExtent; + private uint maxHeapExtent; + + /// + /// Initializes a new allocator with an empty heap. + /// + /// The address where the heap will start. + /// A delegate to request more memory. + public HeapAllocator(uint heapAddress, MemoryRequester requester) + { + this.heapAddress = heapAddress; + this.setEndMem = requester; + this.blocks = new List(); + this.freeList = new List(); + + endMem = heapAddress; + heapExtent = 0; + } + + /// + /// Initializes a new allocator from a previous saved heap state. + /// + /// A byte array describing the heap state, + /// as returned by the method. + /// A delegate to request more memory. + public HeapAllocator(byte[] savedHeap, MemoryRequester requester) + { + this.heapAddress = BigEndian.ReadInt32(savedHeap, 0); + this.setEndMem = requester; + this.blocks = new List(); + this.freeList = new List(); + + uint numBlocks = BigEndian.ReadInt32(savedHeap, 4); + blocks.Capacity = (int)numBlocks; + uint nextAddress = heapAddress; + + for (uint i = 0; i < numBlocks; i++) + { + uint start = BigEndian.ReadInt32(savedHeap, 8 * i + 8); + uint length = BigEndian.ReadInt32(savedHeap, 8 * i + 12); + blocks.Add(new HeapEntry(start, length)); + + if (nextAddress < start) + freeList.Add(new HeapEntry(nextAddress, start - nextAddress)); + + nextAddress = start + length; + } + + endMem = nextAddress; + heapExtent = nextAddress - heapAddress; + + if (setEndMem(endMem) == false) + throw new ArgumentException("Can't allocate VM memory to fit saved heap"); + + blocks.Sort(entryComparer); + freeList.Sort(entryComparer); + } + + /// + /// Gets the address where the heap starts. + /// + public uint Address + { + get { return heapAddress; } + } + + /// + /// Gets the size of the heap, in bytes. + /// + public uint Size + { + get { return heapExtent; } + } + + /// + /// Gets or sets the maximum allowed size of the heap, in bytes, or 0 to + /// allow an unlimited heap. + /// + /// + /// When a maximum size is set, memory allocations will be refused if they + /// would cause the heap to grow past the maximum size. Setting the maximum + /// size to less than the current is allowed, but such a + /// value will have no effect until deallocations cause the heap to shrink + /// below the new maximum size. + /// + public uint MaxSize + { + get { return maxHeapExtent; } + set { maxHeapExtent = value; } + } + + /// + /// Gets the number of blocks that the allocator is managing. + /// + public int BlockCount + { + get { return blocks.Count; } + } + + /// + /// Saves the heap state to a byte array. + /// + /// A byte array describing the current heap state. + public byte[] Save() + { + byte[] result = new byte[8 + blocks.Count * 8]; + + BigEndian.WriteInt32(result, 0, heapAddress); + BigEndian.WriteInt32(result, 4, (uint)blocks.Count); + for (int i = 0; i < blocks.Count; i++) + { + BigEndian.WriteInt32(result, 8 * i + 8, blocks[i].Start); + BigEndian.WriteInt32(result, 8 * i + 12, blocks[i].Length); + } + + return result; + } + + /// + /// Allocates a new block on the heap. + /// + /// The size of the new block, in bytes. + /// The address of the new block, or 0 if allocation failed. + public uint Alloc(uint size) + { + HeapEntry result = new HeapEntry(0, size); + + // look for a free block + if (freeList != null) + { + for (int i = 0; i < freeList.Count; i++) + { + HeapEntry entry = freeList[i]; + if (entry.Length >= size) + { + result.Start = entry.Start; + + if (entry.Length > size) + { + // shrink the free block + entry.Start += size; + entry.Length -= size; + freeList[i] = entry; + } + else + freeList.RemoveAt(i); + + break; + } + } + } + + if (result.Start == 0) + { + // enforce maximum heap size + if (maxHeapExtent != 0 && heapExtent + size > maxHeapExtent) + return 0; + + // add a new block at the end + result = new HeapEntry(heapAddress + heapExtent, size); + + if (heapAddress + heapExtent + size > endMem) + { + // grow the heap + uint newHeapAllocation = Math.Max( + heapExtent * 5 / 4, + heapExtent + size); + + if (maxHeapExtent != 0) + newHeapAllocation = Math.Min(newHeapAllocation, maxHeapExtent); + + if (setEndMem(heapAddress + newHeapAllocation)) + endMem = heapAddress + newHeapAllocation; + else + return 0; + } + + heapExtent += size; + } + + // add the new block to the list + int index = ~blocks.BinarySearch(result, entryComparer); + System.Diagnostics.Debug.Assert(index >= 0); + blocks.Insert(index, result); + + return result.Start; + } + + /// + /// Deallocates a previously allocated block. + /// + /// The address of the block to deallocate. + public void Free(uint address) + { + HeapEntry entry = new HeapEntry(address, 0); + int index = blocks.BinarySearch(entry, entryComparer); + + if (index >= 0) + { + // delete the block + entry = blocks[index]; + blocks.RemoveAt(index); + + // adjust the heap extent if necessary + if (entry.Start + entry.Length - heapAddress == heapExtent) + { + if (index == 0) + { + heapExtent = 0; + } + else + { + HeapEntry prev = blocks[index - 1]; + heapExtent = prev.Start + prev.Length - heapAddress; + } + } + + // add the block to the free list + index = ~freeList.BinarySearch(entry, entryComparer); + System.Diagnostics.Debug.Assert(index >= 0); + freeList.Insert(index, entry); + + if (index < freeList.Count - 1) + Coalesce(index, index + 1); + if (index > 0) + Coalesce(index - 1, index); + + // shrink the heap if necessary + if (blocks.Count > 0 && heapExtent <= (endMem - heapAddress) / 2) + { + if (setEndMem(heapAddress + heapExtent)) + { + endMem = heapAddress + heapExtent; + + for (int i = freeList.Count - 1; i >= 0; i--) { + if (freeList[i].Start >= endMem) + freeList.RemoveAt(i); + } + } + } + } + } + + private void Coalesce(int index1, int index2) + { + HeapEntry first = freeList[index1]; + HeapEntry second = freeList[index2]; + + if (first.Start + first.Length >= second.Start) + { + first.Length = second.Start + second.Length - first.Start; + freeList[index1] = first; + freeList.RemoveAt(index2); + } + } + } +} diff --git a/Opcodes.cs b/Opcodes.cs new file mode 100644 index 0000000..fd21a55 --- /dev/null +++ b/Opcodes.cs @@ -0,0 +1,1885 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.IO; + +namespace FyreVM +{ + /// + /// A delegate type for methods that implement Glulx opcodes. + /// + /// The array of operand values, passed in and out of + /// the method. + /// Elements of the array that correspond to + /// load operands will be filled with the loaded values before the method is called. + /// Elements corresponding to store operands must be filled in by the method; + /// after the method returns, those values will be read from the array and stored + /// in their destinations. + /// Note that "delayed store" operands take up two entries in the array. + internal delegate void OpcodeHandler(uint[] operands); + + /// + /// Describes exceptions to the typical operand order and meaning + /// for certain opcodes that don't fit the pattern. + /// + internal enum OpcodeRule : byte + { + /// + /// No special treatment. + /// + None, + /// + /// Indirect operands work with single bytes. + /// + Indirect8Bit, + /// + /// Indirect operands work with 16-bit words. + /// + Indirect16Bit, + /// + /// Has an additional operand that resembles a store, but which + /// is not actually passed out by the opcode handler. Instead, the + /// handler receives two values, DestType and DestAddr, which may + /// be written into a call stub so the result can be stored later. + /// + DelayedStore, + /// + /// Special case for op_catch. This opcode has a load operand + /// (the branch offset) and a delayed store, but the store comes first. + /// args[0] and [1] are the delayed store, and args[2] is the load. + /// + Catch, + } + + internal class Opcode + { + private readonly OpcodeAttribute attr; + private readonly OpcodeHandler handler; + + public Opcode(OpcodeAttribute attr, OpcodeHandler handler) + { + this.attr = attr; + this.handler = handler; + } + + public OpcodeAttribute Attr + { + get { return attr; } + } + + public OpcodeHandler Handler + { + get { return handler; } + } + + public override string ToString() + { + return attr.Name; + } + } + + /// + /// Describes a method that implements a Glulx opcode. The method must + /// fit the pattern of the delegate. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + class OpcodeAttribute : Attribute + { + private uint number; + private string name; + private byte loadArgs, storeArgs; + private OpcodeRule rule; + + public OpcodeAttribute(uint num, string name, byte loadArgs) + { + this.number = num; + this.name = name; + this.loadArgs = loadArgs; + } + + public OpcodeAttribute(uint num, string name, byte loadArgs, byte storeArgs) + : this(num, name, loadArgs) + { + this.storeArgs = storeArgs; + } + + /// + /// Gets the opcode number. + /// + public uint Number + { + get { return number; } + } + + /// + /// Gets the opcode's mnemonic name. + /// + public string Name + { + get { return name; } + } + + /// + /// Gets the number of load operands, which appear before any store operands. + /// + /// + /// If is set to , + /// the branch offset is not included in this count. + /// + public byte LoadArgs + { + get { return loadArgs; } + } + + /// + /// Gets the number of store operands, which appear after the load operands. + /// + public byte StoreArgs + { + get { return storeArgs; } + } + + /// + /// Gets a value describing anything exceptional about this opcode. + /// + public OpcodeRule Rule + { + get { return rule; } + set { rule = value; } + } + } + + public partial class Engine + { + [Opcode(0x00, "nop", 0)] + private void op_nop(uint[] args) + { + // do nothing! + } + + #region Arithmetic + + [Opcode(0x10, "add", 2, 1)] + private void op_add(uint[] args) + { + args[2] = args[0] + args[1]; + } + + [Opcode(0x11, "sub", 2, 1)] + private void op_sub(uint[] args) + { + args[2] = args[0] - args[1]; + } + + [Opcode(0x12, "mul", 2, 1)] + private void op_mul(uint[] args) + { + args[2] = args[0] * args[1]; + } + + [Opcode(0x13, "div", 2, 1)] + private void op_div(uint[] args) + { + args[2] = (uint)((int)args[0] / (int)args[1]); + } + + [Opcode(0x14, "mod", 2, 1)] + private void op_mod(uint[] args) + { + args[2] = (uint)((int)args[0] % (int)args[1]); + } + + [Opcode(0x15, "neg", 1, 1)] + private void op_neg(uint[] args) + { + args[1] = (uint)(-(int)args[0]); + } + + [Opcode(0x18, "bitand", 2, 1)] + private void op_bitand(uint[] args) + { + args[2] = args[0] & args[1]; + } + + [Opcode(0x19, "bitor", 2, 1)] + private void op_bitor(uint[] args) + { + args[2] = args[0] | args[1]; + } + + [Opcode(0x1A, "bitxor", 2, 1)] + private void op_bitxor(uint[] args) + { + args[2] = args[0] ^ args[1]; + } + + [Opcode(0x1B, "bitnot", 1, 1)] + private void op_bitnot(uint[] args) + { + args[1] = ~args[0]; + } + + [Opcode(0x1C, "shiftl", 2, 1)] + private void op_shiftl(uint[] args) + { + if (args[1] >= 32) + args[2] = 0; + else + args[2] = args[0] << (int)args[1]; + } + + [Opcode(0x1D, "sshiftr", 2, 1)] + private void op_sshiftr(uint[] args) + { + if (args[1] >= 32) + args[2] = ((args[0] & 0x80000000) == 0) ? 0 : 0xFFFFFFFF; + else + args[2] = (uint)((int)args[0] >> (int)args[1]); + } + + [Opcode(0x1E, "ushiftr", 2, 1)] + private void op_ushiftr(uint[] args) + { + if (args[1] >= 32) + args[2] = 0; + else + args[2] = args[0] >> (int)args[1]; + } + + #endregion + + #region Branching + + private void TakeBranch(uint target) + { + if (target == 0) + LeaveFunction(0); + else if (target == 1) + LeaveFunction(1); + else + pc += target - 2; + } + + [Opcode(0x20, "jump", 1)] + private void op_jump(uint[] args) + { + TakeBranch(args[0]); + } + + [Opcode(0x22, "jz", 2)] + private void op_jz(uint[] args) + { + if (args[0] == 0) + TakeBranch(args[1]); + } + + [Opcode(0x23, "jnz", 2)] + private void op_jnz(uint[] args) + { + if (args[0] != 0) + TakeBranch(args[1]); + } + + [Opcode(0x24, "jeq", 3)] + private void op_jeq(uint[] args) + { + if (args[0] == args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x25, "jne", 3)] + private void op_jne(uint[] args) + { + if (args[0] != args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x26, "jlt", 3)] + private void op_jlt(uint[] args) + { + if ((int)args[0] < (int)args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x27, "jge", 3)] + private void op_jge(uint[] args) + { + if ((int)args[0] >= (int)args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x28, "jgt", 3)] + private void op_jgt(uint[] args) + { + if ((int)args[0] > (int)args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x29, "jle", 3)] + private void op_jle(uint[] args) + { + if ((int)args[0] <= (int)args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x2A, "jltu", 3)] + private void op_jltu(uint[] args) + { + if (args[0] < args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x2B, "jgeu", 3)] + private void op_jgeu(uint[] args) + { + if (args[0] >= args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x2C, "jgtu", 3)] + private void op_jgtu(uint[] args) + { + if (args[0] > args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x2D, "jleu", 3)] + private void op_jleu(uint[] args) + { + if (args[0] <= args[1]) + TakeBranch(args[2]); + } + + [Opcode(0x104, "jumpabs", 1)] + private void op_jumpabs(uint[] args) + { + pc = args[0]; + } + + #endregion + + #region Functions + + private uint[] funcargs1 = new uint[1]; + private uint[] funcargs2 = new uint[2]; + private uint[] funcargs3 = new uint[3]; + + [Opcode(0x30, "call", 2, Rule = OpcodeRule.DelayedStore)] + private void op_call(uint[] args) + { + int count = (int)args[1]; + uint[] funcargs = new uint[count]; + + for (int i = 0; i < count; i++) + funcargs[i] = Pop(); + + PerformCall(args[0], funcargs, args[2], args[3]); + } + + [Opcode(0x160, "callf", 1, Rule = OpcodeRule.DelayedStore)] + private void op_callf(uint[] args) + { + PerformCall(args[0], null, args[1], args[2]); + } + + [Opcode(0x161, "callfi", 2, Rule = OpcodeRule.DelayedStore)] + private void op_callfi(uint[] args) + { + funcargs1[0] = args[1]; + PerformCall(args[0], funcargs1, args[2], args[3]); + } + + [Opcode(0x162, "callfii", 3, Rule = OpcodeRule.DelayedStore)] + private void op_callfii(uint[] args) + { + funcargs2[0] = args[1]; + funcargs2[1] = args[2]; + PerformCall(args[0], funcargs2, args[3], args[4]); + } + + [Opcode(0x163, "callfiii", 4, Rule = OpcodeRule.DelayedStore)] + private void op_callfiii(uint[] args) + { + funcargs3[0] = args[1]; + funcargs3[1] = args[2]; + funcargs3[2] = args[3]; + PerformCall(args[0], funcargs3, args[4], args[5]); + } + + private void PerformCall(uint address, uint[] args, uint destType, uint destAddr) + { + PerformCall(address, args, destType, destAddr, pc); + } + + private void PerformCall(uint address, uint[] args, uint destType, uint destAddr, uint stubPC) + { + PerformCall(address, args, destType, destAddr, stubPC, false); + } + + /// + /// Enters a function, pushing a call stub first if necessary. + /// + /// The address of the function to call. + /// The function's arguments, or null to call without arguments. + /// The DestType for the call stub. Ignored for tail calls. + /// The DestAddr for the call stub. Ignored for tail calls. + /// The PC value for the call stub. Ignored for tail calls. + /// true to perform a tail call, reusing the current call stub + /// and frame instead of pushing a new stub and creating a new frame. + private void PerformCall(uint address, uint[] args, uint destType, uint destAddr, uint stubPC, bool tailCall) + { + uint result; + if (veneer.InterceptCall(this, address, args, out result)) + { + PerformDelayedStore(destType, destAddr, result); + return; + } + + if (tailCall) + { + // pop the current frame and use the call stub below it + sp = fp; + } + else + { + // use a new call stub + PushCallStub(new CallStub(destType, destAddr, stubPC, fp)); + } + + byte type = image.ReadByte(address); + if (type == 0xC0) + { + // arguments are passed in on the stack + EnterFunction(address); + if (args == null) + { + Push(0); + } + else + { + for (int i = args.Length - 1; i >= 0; i--) + Push(args[i]); + Push((uint)args.Length); + } + } + else if (type == 0xC1) + { + // arguments are passed in local storage + EnterFunction(address, args); + } + else + throw new VMException(string.Format("Invalid function type {0:X}h", type)); + } + + [Opcode(0x31, "return", 1)] + private void op_return(uint[] args) + { + LeaveFunction(args[0]); + } + + [Opcode(0x32, "catch", 0, Rule = OpcodeRule.Catch)] + private void op_catch(uint[] args) + { + PushCallStub(new CallStub(args[0], args[1], pc, fp)); + // the catch token is the value of sp after pushing that stub + PerformDelayedStore(args[0], args[1], sp); + TakeBranch(args[2]); + } + + [Opcode(0x33, "throw", 2)] + private void op_throw(uint[] args) + { + if (args[1] > sp) + throw new VMException("Invalid catch token"); + + // pop the stack back down to the stub pushed by catch + sp = args[1]; + + // restore from the stub + CallStub stub = PopCallStub(); + pc = stub.PC; + fp = stub.FramePtr; + frameLen = ReadFromStack(fp); + localsPos = ReadFromStack(fp + 4); + + // store the thrown value and resume after the catch opcode + PerformDelayedStore(stub.DestType, stub.DestAddr, args[0]); + } + + [Opcode(0x34, "tailcall", 2)] + private void op_tailcall(uint[] args) + { + int count = (int)args[1]; + uint[] funcargs = new uint[count]; + + for (int i = 0; i < count; i++) + funcargs[i] = Pop(); + + PerformCall(args[0], funcargs, 0, 0, 0, true); + } + + [Opcode(0x180, "accelfunc", 2)] + private void op_accelfunc(uint[] args) + { + veneer.SetSlotGlulx(this, false, args[0], args[1]); + } + + [Opcode(0x181, "accelparam", 2)] + private void op_accelparam(uint[] args) + { + veneer.SetSlotGlulx(this, true, args[0], args[1]); + } + + #endregion + + #region Variables and Arrays + + [Opcode(0x40, "copy", 1, 1)] + private void op_copy(uint[] args) + { + args[1] = args[0]; + } + + [Opcode(0x41, "copys", 1, 1, Rule = OpcodeRule.Indirect16Bit)] + private void op_copys(uint[] args) + { + args[1] = (ushort)args[0]; + } + + [Opcode(0x42, "copyb", 1, 1, Rule = OpcodeRule.Indirect8Bit)] + private void op_copyb(uint[] args) + { + args[1] = (byte)args[0]; + } + + [Opcode(0x44, "sexs", 1, 1)] + private void op_sexs(uint[] args) + { + args[1] = (uint)(int)(short)args[0]; + } + + [Opcode(0x45, "sexb", 1, 1)] + private void op_sexb(uint[] args) + { + args[1] = (uint)(int)(sbyte)args[0]; + } + + [Opcode(0x48, "aload", 2, 1)] + private void op_aload(uint[] args) + { + args[2] = image.ReadInt32(args[0] + 4 * args[1]); + } + + [Opcode(0x49, "aloads", 2, 1)] + private void op_aloads(uint[] args) + { + args[2] = image.ReadInt16(args[0] + 2 * args[1]); + } + + [Opcode(0x4A, "aloadb", 2, 1)] + private void op_aloadb(uint[] args) + { + args[2] = image.ReadByte(args[0] + args[1]); + } + + [Opcode(0x4B, "aloadbit", 2, 1)] + private void op_aloadbit(uint[] args) + { + int bit = (int)args[1]; + uint address = (uint)(args[0] + bit / 8); + bit %= 8; + if (bit < 0) + { + address--; + bit += 8; + } + + byte value = image.ReadByte(address); + args[2] = (value & (1 << bit)) == 0 ? (uint)0 : (uint)1; + } + + [Opcode(0x4C, "astore", 3)] + private void op_astore(uint[] args) + { + image.WriteInt32(args[0] + 4 * args[1], args[2]); + } + + [Opcode(0x4D, "astores", 3)] + private void op_astores(uint[] args) + { + image.WriteInt16(args[0] + 2 * args[1], (ushort)args[2]); + } + + [Opcode(0x4E, "astoreb", 3)] + private void op_astoreb(uint[] args) + { + image.WriteByte(args[0] + args[1], (byte)args[2]); + } + + [Opcode(0x4F, "astorebit", 3)] + private void op_astorebit(uint[] args) + { + int bit = (int)args[1]; + uint address = (uint)(args[0] + bit / 8); + bit %= 8; + if (bit < 0) + { + address--; + bit += 8; + } + + byte value = image.ReadByte(address); + if (args[2] == 0) + value &= (byte)(~(1 << bit)); + else + value |= (byte)(1 << bit); + image.WriteByte(address, value); + } + + #endregion + + #region Output + + [Opcode(0x70, "streamchar", 1)] + private void op_streamchar(uint[] args) + { + StreamCharCore((byte)args[0]); + } + + [Opcode(0x73, "streamunichar", 1)] + private void op_streamunichar(uint[] args) + { + StreamCharCore(args[0]); + } + + private void StreamCharCore(uint value) + { + if (outputSystem == IOSystem.Filter) + { + PerformCall(filterAddress, new uint[] { value }, GLULX_STUB_STORE_NULL, 0); + } + else + { + SendCharToOutput(value); + } + } + + [Opcode(0x71, "streamnum", 1)] + private void op_streamnum(uint[] args) + { + if (outputSystem == IOSystem.Filter) + { + PushCallStub(new CallStub(GLULX_STUB_RESUME_FUNC, 0, pc, fp)); + string num = ((int)args[0]).ToString(); + PerformCall(filterAddress, new uint[] { (uint)num[0] }, + GLULX_STUB_RESUME_NUMBER, 1, args[0]); + } + else + { + string num = ((int)args[0]).ToString(); + SendStringToOutput(num); + } + } + + [Opcode(0x72, "streamstr", 1)] + private void op_streamstr(uint[] args) + { + if (outputSystem == IOSystem.Null) + return; + + uint address = args[0]; + byte type = image.ReadByte(address); + + // for retrying a compressed string after we discover it needs a call stub + byte savedDigit = 0; + StrNode savedNode = null; + + /* if we're not using the userland output filter, and the string is + * uncompressed (or contains no indirect references), we can just print it + * right here. */ + if (outputSystem != IOSystem.Filter) + { + switch (type) + { + case 0xE0: + // C string + SendStringToOutput(ReadCString(address + 1)); + return; + case 0xE2: + // Unicode string + SendStringToOutput(ReadUniString(address + 4)); + return; + case 0xE1: + // compressed string + if (nativeDecodingTable != null) + { + uint oldPC = pc; + + pc = address + 1; + printingDigit = 0; + + StrNode node = nativeDecodingTable.GetHandlingNode(this); + while (!node.NeedsCallStub) + { + if (node.IsTerminator) + { + pc = oldPC; + return; + } + + node.HandleNextChar(this); + + node = nativeDecodingTable.GetHandlingNode(this); + } + + savedDigit = printingDigit; + savedNode = node; + address = pc - 1; + pc = oldPC; + } + break; + } + } + + // can't decompress anything without a decoding table + if (type == 0xE1 && decodingTable == 0) + throw new VMException("No string decoding table is set"); + + /* otherwise, we have to push a call stub and let the main + * interpreter loop take care of printing the string. */ + PushCallStub(new CallStub(GLULX_STUB_RESUME_FUNC, 0, pc, fp)); + + switch (type) + { + case 0xE0: + execMode = ExecutionMode.CString; + pc = address + 1; + break; + case 0xE1: + execMode = ExecutionMode.CompressedString; + pc = address + 1; + printingDigit = savedDigit; + // this won't read a bit, since savedNode can't be a branch... + if (savedNode != null) + savedNode.HandleNextChar(this); + break; + case 0xE2: + execMode = ExecutionMode.UnicodeString; + pc = address + 4; + break; + default: + throw new VMException(string.Format("Invalid string type {0:X}h", type)); + } + } + + [Opcode(0x130, "glk", 2, 1)] + private void op_glk(uint[] args) + { + switch (glkMode) + { + case GlkMode.None: + // not really supported, just clear the stack + for (uint i = 0; i < args[1]; i++) + Pop(); + args[2] = 0; + break; + + case GlkMode.Wrapper: + GlkWrapperCall(args); + break; + } + } + + [Opcode(0x140, "getstringtbl", 0, 1)] + private void op_getstringtbl(uint[] args) + { + args[0] = decodingTable; + } + + [Opcode(0x141, "setstringtbl", 1)] + private void op_setstringtbl(uint[] args) + { + decodingTable = args[0]; + CacheDecodingTable(); + } + + [Opcode(0x148, "getiosys", 0, 2)] + private void op_getiosys(uint[] args) + { + switch (outputSystem) + { + case IOSystem.Null: + args[0] = 0; + args[1] = 0; + break; + + case IOSystem.Filter: + args[0] = 1; + args[1] = filterAddress; + break; + + case IOSystem.Channels: + args[0] = 20; + args[1] = 0; + break; + } + } + + [Opcode(0x149, "setiosys", 2, 0)] + private void op_setiosys(uint[] args) + { + SelectOutputSystem(args[0], args[1]); + } + + #endregion + + #region Memory Management + + [Opcode(0x102, "getmemsize", 0, 1)] + private void op_getmemsize(uint[] args) + { + args[0] = image.EndMem; + } + + [Opcode(0x103, "setmemsize", 1, 1)] + private void op_setmemsize(uint[] args) + { + if (heap != null) + throw new VMException("setmemsize is not allowed while the heap is active"); + + try + { + image.EndMem = args[0]; + args[1] = 0; + } + catch + { + args[1] = 1; + } + } + + [Opcode(0x170, "mzero", 2)] + private void op_mzero(uint[] args) + { + for (uint i = 0; i < args[0]; i++) + image.WriteByte(args[1] + i, 0); + } + + [Opcode(0x171, "mcopy", 3)] + private void op_mcopy(uint[] args) + { + if (args[2] < args[1]) + { + for (uint i = 0; i < args[0]; i++) + image.WriteByte(args[2] + i, image.ReadByte(args[1] + i)); + } + else + { + for (uint i = args[0] - 1; i >= 0; i--) + image.WriteByte(args[2] + i, image.ReadByte(args[1] + i)); + } + } + + private bool HandleHeapMemoryRequest(uint newEndMem) + { + try + { + image.EndMem = newEndMem; + return true; + } + catch + { + return false; + } + } + + [Opcode(0x178, "malloc", 1, 1)] + private void op_malloc(uint[] args) + { + uint size = args[0]; + if ((int)size <= 0) + { + args[1] = 0; + return; + } + + if (heap == null) + { + uint oldEndMem = image.EndMem; + heap = new HeapAllocator(oldEndMem, HandleHeapMemoryRequest); + heap.MaxSize = maxHeapSize; + args[1] = heap.Alloc(size); + if (args[1] == 0) + { + heap = null; + image.EndMem = oldEndMem; + } + } + else + { + args[1] = heap.Alloc(size); + } + } + + [Opcode(0x179, "mfree", 1)] + private void op_mfree(uint[] args) + { + if (heap != null) + { + heap.Free(args[0]); + if (heap.BlockCount == 0) + { + image.EndMem = heap.Address; + heap = null; + } + } + } + + #endregion + + #region Searching + + [Flags] + private enum SearchOptions + { + None = 0, + + KeyIndirect = 1, + ZeroKeyTerminates = 2, + ReturnIndex = 4, + } + + private bool KeyIsZero(uint address, uint size) + { + for (uint i = 0; i < size; i++) + if (image.ReadByte(address + i) != 0) + return false; + + return true; + } + + /// + /// Performs key comparison for the various search opcodes. + /// + /// The search key, if is + /// not set; or the address of the search key, if it is set. + /// The address of the candidate key which is to be + /// checked against the search key. + /// The length of the keys, in bytes. + /// The options passed into the search opcode. + /// A negative value if the search key is less than the candidate key, + /// a positive value if the search key is greater than the candidate key, or + /// 0 if the keys match. + private int CompareKeys(uint query, uint candidateAddr, uint keySize, SearchOptions options) + { + if ((options & SearchOptions.KeyIndirect) == 0) + { + // KeyIndirect is *not* set + // mask query to the appropriate size and compare it against the value stored at candidateAddr + uint ckey; + switch (keySize) + { + case 1: + ckey = image.ReadByte(candidateAddr); + query &= 0xFF; + break; + case 2: + ckey = image.ReadInt16(candidateAddr); + query &= 0xFFFF; + break; + case 3: + ckey = (uint)(image.ReadByte(candidateAddr) << 24 + image.ReadInt16(candidateAddr + 1)); + query &= 0xFFFFFF; + break; + default: + ckey = image.ReadInt32(candidateAddr); + break; + } + + return query.CompareTo(ckey); + } + + // KeyIndirect *is* set + // compare the bytes stored at query vs. candidateAddr + for (uint i = 0; i < keySize; i++) + { + byte b1 = image.ReadByte(query++); + byte b2 = image.ReadByte(candidateAddr++); + if (b1 < b2) + return -1; + else if (b1 > b2) + return 1; + } + + return 0; + } + + [Opcode(0x150, "linearsearch", 7, 1)] + private void op_linearsearch(uint[] args) + { + uint key = args[0]; + uint keySize = args[1]; + uint start = args[2]; + uint structSize = args[3]; + uint numStructs = args[4]; + uint keyOffset = args[5]; + SearchOptions options = (SearchOptions)args[6]; + + if (keySize > 4 && (options & SearchOptions.KeyIndirect) == 0) + throw new VMException("KeyIndirect option must be used when searching for a >4 byte key"); + + uint result = (options & SearchOptions.ReturnIndex) == 0 ? 0 : 0xFFFFFFFF; + + for (uint index = 0; index < numStructs; index++) + { + int cmp = CompareKeys(key, start + index * structSize + keyOffset, keySize, options); + if (cmp == 0) + { + // found it + if ((options & SearchOptions.ReturnIndex) == 0) + result = start + index * structSize; + else + result = index; + break; + } + + if ((options & SearchOptions.ZeroKeyTerminates) != 0 && + KeyIsZero(start + index * structSize + keyOffset, keySize)) + { + // stop searching + break; + } + } + + args[7] = result; + } + + [Opcode(0x151, "binarysearch", 7, 1)] + private void op_binarysearch(uint[] args) + { + uint key = args[0]; + uint keySize = args[1]; + uint start = args[2]; + uint structSize = args[3]; + uint numStructs = args[4]; + uint keyOffset = args[5]; + SearchOptions options = (SearchOptions)args[6]; + + args[7] = PerformBinarySearch(key, keySize, start, structSize, numStructs, keyOffset, options); + } + + // this is a separate method because it's also used by Veneer.CP__Tab + private uint PerformBinarySearch(uint key, uint keySize, uint start, uint structSize, uint numStructs, uint keyOffset, SearchOptions options) + { + if ((options & SearchOptions.ZeroKeyTerminates) != 0) + throw new VMException("ZeroKeyTerminates option may not be used with binarysearch"); + if (keySize > 4 && (options & SearchOptions.KeyIndirect) == 0) + throw new VMException("KeyIndirect option must be used when searching for a >4 byte key"); + + uint result = (options & SearchOptions.ReturnIndex) == 0 ? 0 : 0xFFFFFFFF; + uint low = 0, high = numStructs; + + while (low < high) + { + uint index = (low + high) / 2; + int cmp = CompareKeys(key, start + index * structSize + keyOffset, keySize, options); + if (cmp == 0) + { + // found it + if ((options & SearchOptions.ReturnIndex) == 0) + result = start + index * structSize; + else + result = index; + break; + } + else if (cmp < 0) + { + high = index; + } + else + { + low = index + 1; + } + } + return result; + } + + [Opcode(0x152, "linkedsearch", 6, 1)] + private void op_linkedsearch(uint[] args) + { + uint key = args[0]; + uint keySize = args[1]; + uint start = args[2]; + uint keyOffset = args[3]; + uint nextOffset = args[4]; + SearchOptions options = (SearchOptions)args[5]; + + if ((options & SearchOptions.ReturnIndex) != 0) + throw new VMException("ReturnIndex option may not be used with linkedsearch"); + + uint result = 0; + uint node = start; + + while (node != 0) + { + int cmp = CompareKeys(key, node + keyOffset, keySize, options); + if (cmp == 0) + { + // found it + result = node; + break; + } + + if ((options & SearchOptions.ZeroKeyTerminates) != 0 && + KeyIsZero(node + keyOffset, keySize)) + { + // stop searching + break; + } + + // advance to next item + node = image.ReadInt32(node + nextOffset); + } + + args[6] = result; + } + + #endregion + + #region Stack Manipulation + + [Opcode(0x50, "stkcount", 0, 1)] + private void op_stkcount(uint[] args) + { + args[0] = (sp - (fp + frameLen)) / 4; + } + + [Opcode(0x51, "stkpeek", 1, 1)] + private void op_stkpeek(uint[] args) + { + uint position = sp - 4 * (1 + args[0]); + if (position < (fp + frameLen)) + throw new VMException("Stack underflow"); + + args[1] = ReadFromStack(position); + } + + [Opcode(0x52, "stkswap", 0)] + private void op_stkswap(uint[] args) + { + if (sp - (fp + frameLen) < 8) + throw new VMException("Stack underflow"); + + uint a = Pop(); + uint b = Pop(); + Push(a); + Push(b); + } + + [Opcode(0x53, "stkroll", 2)] + private void op_stkroll(uint[] args) + { + int items = (int)args[0]; + int distance = (int)args[1]; + + if (items != 0) + { + distance %= items; + + if (distance != 0) + { + // rolling X items down Y slots == rolling X items up X-Y slots + if (distance < 0) + distance += items; + + if (sp - (fp + frameLen) < 4 * items) + throw new VMException("Stack underflow"); + + Stack temp1 = new Stack(distance); + Stack temp2 = new Stack(items - distance); + + for (int i = 0; i < distance; i++) + temp1.Push(Pop()); + for (int i = distance; i < items; i++) + temp2.Push(Pop()); + while (temp1.Count > 0) + Push(temp1.Pop()); + while (temp2.Count > 0) + Push(temp2.Pop()); + } + } + } + + [Opcode(0x54, "stkcopy", 1)] + private void op_stkcopy(uint[] args) + { + uint bytes = args[0] * 4; + if (bytes > sp - (fp + frameLen)) + throw new VMException("Stack underflow"); + + uint start = sp - bytes; + while (bytes-- > 0) + stack[sp++] = stack[start++]; + } + + #endregion + + private enum Gestalt + { + GlulxVersion = 0, + TerpVersion = 1, + ResizeMem = 2, + Undo = 3, + IOSystem = 4, + Unicode = 5, + MemCopy = 6, + MAlloc = 7, + MAllocHeap = 8, + Acceleration = 9, + AccelFunc = 10, + Float = 11, + } + + [Opcode(0x100, "gestalt", 2, 1)] + private void op_gestalt(uint[] args) + { + Gestalt selector = (Gestalt)args[0]; + switch (selector) + { + case Gestalt.GlulxVersion: + args[2] = 0x00030102; + break; + + case Gestalt.TerpVersion: + args[2] = 0x00000900; + break; + + case Gestalt.ResizeMem: + case Gestalt.Undo: + case Gestalt.Unicode: + case Gestalt.MemCopy: + case Gestalt.MAlloc: + case Gestalt.Acceleration: + case Gestalt.Float: + args[2] = 1; + break; + + case Gestalt.IOSystem: + if (args[1] == 0 || args[1] == 1 || args[1] == 20) + args[2] = 1; + else if (args[1] == 2 && glkMode != GlkMode.None) + args[2] = 1; + else + args[2] = 0; + break; + + case Gestalt.MAllocHeap: + if (heap == null) + args[2] = 0; + else + args[2] = heap.Address; + break; + + case Gestalt.AccelFunc: + args[2] = veneer.ImplementsFuncGlulx((uint)args[1]) ? (uint)1 : 0; + break; + + default: + // unrecognized gestalt selector + args[2] = 0; + break; + } + } + + [Opcode(0x101, "debugtrap", 1)] + private void op_debugtrap(uint[] args) + { + uint status = args[0]; + System.Diagnostics.Debugger.Break(); + } + + #region Game State + + [Opcode(0x120, "quit", 0)] + private void op_quit(uint[] args) + { + // end execution + running = false; + } + + [Opcode(0x121, "verify", 0, 1)] + private void op_verify(uint[] args) + { + // we already verified the game when it was loaded + args[0] = 0; + } + + [Opcode(0x122, "restart", 0)] + private void op_restart(uint[] args) + { + Restart(); + } + + [Opcode(0x123, "save", 1, Rule = OpcodeRule.DelayedStore)] + private void op_save(uint[] args) + { + if (nestingLevel == 0 && SaveRequested != null) + { + SaveRestoreEventArgs e = new SaveRestoreEventArgs(); + SaveRequested(this, e); + if (e.Stream != null) + { + try + { + SaveToStream(e.Stream, args[1], args[2]); + } + finally + { + e.Stream.Close(); + } + PerformDelayedStore(args[1], args[2], 0); + return; + } + } + + // failed + PerformDelayedStore(args[1], args[2], 1); + } + + + [Opcode(0x124, "restore", 1, Rule = OpcodeRule.DelayedStore)] + private void op_restore(uint[] args) + { + if (LoadRequested != null) + { + SaveRestoreEventArgs e = new SaveRestoreEventArgs(); + LoadRequested(this, e); + if (e.Stream != null) + { + try + { + LoadFromStream(e.Stream); + } + finally + { + e.Stream.Close(); + } + return; + } + } + + // failed + PerformDelayedStore(args[1], args[2], 1); + } + + [Opcode(0x125, "saveundo", 0, 0, Rule = OpcodeRule.DelayedStore)] + private void op_saveundo(uint[] args) + { + if (nestingLevel != 0) + { + // can't save if there's native code on the call stack + PerformDelayedStore(args[0], args[1], 1); + return; + } + + MemoryStream buffer = new MemoryStream(); + SaveToStream(buffer, args[0], args[1]); + + if (undoBuffers.Count >= MAX_UNDO_LEVEL) + undoBuffers.RemoveAt(0); + + undoBuffers.Add(buffer); + + PerformDelayedStore(args[0], args[1], 0); + } + + [Opcode(0x126, "restoreundo", 0, 0, Rule = OpcodeRule.DelayedStore)] + private void op_restoreundo(uint[] args) + { + if (undoBuffers.Count == 0) + { + PerformDelayedStore(args[0], args[1], 1); + } + else + { + MemoryStream buffer = undoBuffers[undoBuffers.Count - 1]; + undoBuffers.RemoveAt(undoBuffers.Count - 1); + + buffer.Seek(0, System.IO.SeekOrigin.Begin); + LoadFromStream(buffer); + } + } + + [Opcode(0x127, "protect", 2)] + private void op_protect(uint[] args) + { + if (args[0] < image.EndMem) + { + protectionStart = args[0]; + protectionLength = args[1]; + + if (protectionStart >= image.RamStart) + { + protectionStart -= image.RamStart; + } + else + { + protectionStart = 0; + protectionLength -= image.RamStart - protectionStart; + } + + if (protectionStart + protectionLength > image.EndMem) + protectionLength = image.EndMem - protectionStart; + } + } + + #endregion + + #region Random Number Generator + + [Opcode(0x110, "random", 1, 1)] + private void op_random(uint[] args) + { + if (args[0] == 0) + { + // 32 random bits + byte[] buffer = new byte[4]; + randomGenerator.NextBytes(buffer); + args[1] = (uint)(buffer[0] << 24 + buffer[1] << 16 + buffer[2] << 8 + buffer[3]); + } + else if ((int)args[0] > 0) + { + // range: 0 to args[0] - 1 + args[1] = (uint)randomGenerator.Next((int)args[0]); + } + else + { + // range: args[0] + 1 to 0 + args[1] = (uint)(-randomGenerator.Next(-(int)args[0])); + } + } + + [Opcode(0x111, "setrandom", 1)] + private void op_setrandom(uint[] args) + { + if (args[0] == 0) + randomGenerator = new Random(); + else + randomGenerator = new Random((int)args[0]); + } + + #endregion + + #region Floating Point + private static double Truncate(double d) { return d > 0.0 ? Math.Floor(d) : Math.Ceiling(d); } + +#if !ALLOW_UNSAFE + private static uint EncodeFloat(float x) + { + byte[] bytes = BitConverter.GetBytes(x); + return BitConverter.ToUInt32(bytes, 0); + } + + private static float DecodeFloat(uint x) + { + byte[] bytes = BitConverter.GetBytes(x); + return BitConverter.ToSingle(bytes, 0); + } +#else + private static unsafe uint EncodeFloat(float x) + { + return *((uint*)&x); + } + + private static unsafe float DecodeFloat(uint x) + { + return *((float*)&x); + } +#endif + + [Opcode(0x190, "numtof", 1, 1)] + private void op_numtof(uint[] args) + { + args[1] = EncodeFloat((int)args[0]); + } + + [Opcode(0x191, "ftonumz", 1, 1)] + private void op_ftonumz(uint[] args) + { + double f = (double)DecodeFloat((uint)Truncate(args[0])); + if (double.IsNaN(f)) + { + if ((args[0] & 0x80000000) != 0) + args[1] = 0x80000000; + else + args[1] = 0x7fffffff; + } + else if (f < int.MinValue) + args[1] = 0x80000000; + else if (f > int.MaxValue) + args[1] = 0x7fffffff; + else + args[1] = (uint)(int)f; + } + + [Opcode(0x192, "ftonumn", 1, 1)] + private void op_ftonumn(uint[] args) + { + double f = Math.Round(DecodeFloat(args[0])); + if (double.IsNaN(f)) + { + if ((args[0] & 0x80000000) != 0) + args[1] = 0x80000000; + else + args[1] = 0x7fffffff; + } + else if (f < int.MinValue) + args[1] = 0x80000000; + else if (f > int.MaxValue) + args[1] = 0x7fffffff; + else + args[1] = (uint)(int)f; + } + + [Opcode(0x1a0, "fadd", 2, 1)] + private void op_fadd(uint[] args) + { + args[2] = EncodeFloat(DecodeFloat(args[0]) + DecodeFloat(args[1])); + } + + [Opcode(0x1a1, "fsub", 2, 1)] + private void op_fsub(uint[] args) + { + args[2] = EncodeFloat(DecodeFloat(args[0]) - DecodeFloat(args[1])); + } + + [Opcode(0x1a2, "fmul", 2, 1)] + private void op_fmul(uint[] args) + { + args[2] = EncodeFloat(DecodeFloat(args[0]) * DecodeFloat(args[1])); + } + + [Opcode(0x1a3, "fdiv", 2, 1)] + private void op_fdiv(uint[] args) + { + args[2] = EncodeFloat(DecodeFloat(args[0]) / DecodeFloat(args[1])); + } + + [Opcode(0x1a4, "fmod", 2, 2)] + private void op_fmod(uint[] args) + { + float f1 = DecodeFloat(args[0]); + float f2 = DecodeFloat(args[1]); + + if (float.IsNaN(f1)) + { + args[2] = args[3] = args[0]; + } + else if (float.IsNaN(f2)) + { + args[2] = args[3] = args[1]; + } + else if (float.IsInfinity(f1) || f2 == 0f) + { + args[2] = args[3] = EncodeFloat(float.NaN); + } + else if (float.IsInfinity(f2)) + { + args[2] = args[0]; + args[3] = ((args[0] & 0x80000000) != (args[1] & 0x80000000)) ? 0x80000000 : 0; + } + else if (f1 == 0f) + { + args[2] = args[0]; + if ((args[0] & 0x80000000) == (args[1] & 0x80000000)) + args[3] = 0; + else + args[3] = 0x80000000; + } + else + { + double quo = Truncate((double)f1 / f2); + args[2] = EncodeFloat((float)(f1 % f2)); + if (args[2] == 0 && (args[0] & 0x80000000) != 0) + args[2] = 0x80000000; + args[3] = EncodeFloat((float)quo); + } + } + + [Opcode(0x198, "ceil", 1, 1)] + private void op_ceil(uint[] args) + { + args[1] = EncodeFloat((float)Math.Ceiling(DecodeFloat(args[0]))); + if (args[1] == 0 && (args[0] & 0x80000000) != 0) + args[1] = 0x80000000; + } + + [Opcode(0x199, "floor", 1, 1)] + private void op_floor(uint[] args) + { + args[1] = EncodeFloat((float)Math.Floor(DecodeFloat(args[0]))); + } + + [Opcode(0x1a8, "sqrt", 1, 1)] + private void op_sqrt(uint[] args) + { + args[1] = EncodeFloat((float)Math.Sqrt(DecodeFloat(args[0]))); + } + + [Opcode(0x1a9, "exp", 1, 1)] + private void op_exp(uint[] args) + { + args[1] = EncodeFloat((float)Math.Exp(DecodeFloat(args[0]))); + } + + [Opcode(0x1aa, "log", 1, 1)] + private void op_log(uint[] args) + { + args[1] = EncodeFloat((float)Math.Log(DecodeFloat(args[0]))); + } + + [Opcode(0x1ab, "pow", 2, 1)] + private void op_pow(uint[] args) + { + float f1 = DecodeFloat(args[0]); + float f2 = DecodeFloat(args[1]); + + if (f1 == 1f || (f1 == -1f && float.IsInfinity(f2)) || f2 == 0f) + { + args[2] = EncodeFloat(1f); + } + else + { + args[2] = EncodeFloat((float)Math.Pow(f1, f2)); + } + } + + [Opcode(0x1b0, "sin", 1, 1)] + private void op_sin(uint[] args) + { + args[1] = EncodeFloat((float)Math.Sin(DecodeFloat(args[0]))); + } + + [Opcode(0x1b1, "cos", 1, 1)] + private void op_cos(uint[] args) + { + args[1] = EncodeFloat((float)Math.Cos(DecodeFloat(args[0]))); + } + + [Opcode(0x1b2, "tan", 1, 1)] + private void op_tan(uint[] args) + { + args[1] = EncodeFloat((float)Math.Tan(DecodeFloat(args[0]))); + } + + [Opcode(0x1b3, "asin", 1, 1)] + private void op_asin(uint[] args) + { + args[1] = EncodeFloat((float)Math.Asin(DecodeFloat(args[0]))); + } + + [Opcode(0x1b4, "acos", 1, 1)] + private void op_acos(uint[] args) + { + args[1] = EncodeFloat((float)Math.Acos(DecodeFloat(args[0]))); + } + + [Opcode(0x1b5, "atan", 1, 1)] + private void op_atan(uint[] args) + { + args[1] = EncodeFloat((float)Math.Atan(DecodeFloat(args[0]))); + } + + [Opcode(0x1b6, "atan2", 2, 1)] + private void op_atan2(uint[] args) + { + float f1 = DecodeFloat(args[0]); + float f2 = DecodeFloat(args[1]); + if (float.IsInfinity(f1) && float.IsInfinity(f2)) + { + float rv; + if (float.IsNegativeInfinity(f2)) + rv = (float)(3 * Math.PI / 4); + else + rv = (float)(Math.PI / 4); + if (float.IsNegativeInfinity(f1)) + rv = -rv; + args[2] = EncodeFloat(rv); + } + else + { + args[2] = EncodeFloat((float)Math.Atan2(f1, f2)); + } + } + + [Opcode(0x1c0, "jfeq", 4)] + private void op_jfeq(uint[] args) + { + float f1 = DecodeFloat(args[0]); + float f2 = DecodeFloat(args[1]); + float f3 = DecodeFloat(args[2]); + if (FloatEqual(f1, f2, f3)) + TakeBranch(args[3]); + } + + private static bool FloatEqual(float f1, float f2, float tolerance) + { + if (float.IsNaN(f1) || float.IsNaN(f2) || float.IsNaN(tolerance)) + return false; + if (float.IsInfinity(f1) && float.IsInfinity(f2)) + return float.IsNegativeInfinity(f1) == float.IsNegativeInfinity(f2); + if (float.IsInfinity(tolerance)) + return true; + + return Math.Abs(f1 - f2) <= Math.Abs(tolerance); + } + + [Opcode(0x1c1, "jfne", 4)] + private void op_jfne(uint[] args) + { + float f1 = DecodeFloat(args[0]); + float f2 = DecodeFloat(args[1]); + float f3 = DecodeFloat(args[2]); + if (!FloatEqual(f1, f2, f3)) + TakeBranch(args[3]); + } + + [Opcode(0x1c2, "jflt", 3)] + private void op_jflt(uint[] args) + { + if (DecodeFloat(args[0]) < DecodeFloat(args[1])) + TakeBranch(args[2]); + } + + [Opcode(0x1c3, "jfle", 3)] + private void op_jfle(uint[] args) + { + if (DecodeFloat(args[0]) <= DecodeFloat(args[1])) + TakeBranch(args[2]); + } + + [Opcode(0x1c4, "jfgt", 3)] + private void op_jfgt(uint[] args) + { + if (DecodeFloat(args[0]) > DecodeFloat(args[1])) + TakeBranch(args[2]); + } + + [Opcode(0x1c5, "jfge", 3)] + private void op_jfge(uint[] args) + { + if (DecodeFloat(args[0]) >= DecodeFloat(args[1])) + TakeBranch(args[2]); + } + + [Opcode(0x1c8, "jisnan", 2)] + private void op_jisnan(uint[] args) + { + if (float.IsNaN(DecodeFloat(args[0]))) + TakeBranch(args[1]); + } + + [Opcode(0x1c9, "jisinf", 2)] + private void op_jisinf(uint[] args) + { + if (float.IsInfinity(DecodeFloat(args[0]))) + TakeBranch(args[1]); + } + + #endregion + + #region FyreVM Specific + + /// + /// Selects a function for the FyreVM system call opcode. + /// + private enum FyreCall + { + /// + /// Reads a line from the user: args[1] = buffer, args[2] = buffer size. + /// + ReadLine = 1, + /// + /// Converts a character to lowercase: args[1] = the character, + /// result = the lowercased character. + /// + ToLower = 2, + /// + /// Converts a character to uppercase: args[1] = the character, + /// result = the uppercased character. + /// + ToUpper = 3, + /// + /// Selects an output channel: args[1] = an OutputChannel value (see Output.cs). + /// + Channel = 4, + /// + /// Reads a character from the user: result = the 16-bit Unicode value. + /// + ReadKey = 5, + /// + /// Registers a veneer function address or constant value: args[1] = a + /// VeneerSlot value (see Veneer.cs), args[2] = the function address or + /// constant value, result = nonzero if the value was accepted. + /// + SetVeneer = 6, + /// + /// Tells the UI a device handled transition is requested. (press a button, touch screen, etc). + /// + RequestTransition = 7 + } + + [Opcode(0x1000, "fyrecall", 3, 1)] + private void op_fyrecall(uint[] args) + { + args[3] = 0; + + FyreCall call = (FyreCall)args[0]; + switch (call) + { + case FyreCall.ReadLine: + DeliverOutput(); + InputLine(args[1], args[2]); + break; + + case FyreCall.ReadKey: + DeliverOutput(); + args[3] = (uint)InputChar(); + break; + + case FyreCall.ToLower: + case FyreCall.ToUpper: + byte[] bytes = new byte[] { (byte)args[1] }; + char[] chars = System.Text.Encoding.GetEncoding(LATIN1_CODEPAGE).GetChars(bytes); + if (call == FyreCall.ToLower) + chars[0] = char.ToLower(chars[0]); + else + chars[0] = char.ToUpper(chars[0]); + bytes = System.Text.Encoding.GetEncoding(LATIN1_CODEPAGE).GetBytes(chars); + args[3] = bytes[0]; + break; + + case FyreCall.Channel: + outputBuffer.Channel = args[1]; + break; + + case FyreCall.SetVeneer: + args[3] = (uint)(veneer.SetSlotFyre(args[1], args[2]) ? 1 : 0); + break; + + case FyreCall.RequestTransition: + if (TransitionRequested != null) + TransitionRequested(this, new TransitionEventArgs()); + break; + + default: + throw new VMException("Unrecognized FyreVM system call #" + args[0].ToString()); + } + } + + #endregion + } +} diff --git a/Output.cs b/Output.cs new file mode 100644 index 0000000..c66259f --- /dev/null +++ b/Output.cs @@ -0,0 +1,775 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Serialization; + +namespace FyreVM +{ + public partial class Engine + { + /// + /// Identifies an output system for use with @setiosys. + /// + private enum IOSystem + { + /// + /// Output is discarded. + /// + Null, + /// + /// Output is filtered through a Glulx function. + /// + Filter, + /// + /// Output is sent through FyreVM's channel system. + /// + Channels, + /// + /// Output is sent through Glk. + /// + /// + Glk, + } + + /// + /// Sends a single character to the output system (other than + /// . + /// + /// The character to send. + private void SendCharToOutput(uint ch) + { + switch (outputSystem) + { + case IOSystem.Channels: + // TODO: need to handle Unicode characters larger than 16 bits? + outputBuffer.Write((char)ch); + break; + + case IOSystem.Glk: + if (glkMode == GlkMode.Wrapper) + GlkWrapperWrite(ch); + break; + } + } + + /// + /// Sends a string to the output system (other than + /// . + /// + /// The string to send. + private void SendStringToOutput(string str) + { + switch (outputSystem) + { + case IOSystem.Channels: + outputBuffer.Write(str); + break; + + case IOSystem.Glk: + if (glkMode == GlkMode.Wrapper) + GlkWrapperWrite(str); + break; + } + } + + /// + /// Sends the queued output to the event handler. + /// + private void DeliverOutput() + { + if (OutputReady != null) + { + OutputReadyEventArgs args = new OutputReadyEventArgs(); + args.Package = outputBuffer.Flush(); + OutputReady(this, args); + } + } + + private void SelectOutputSystem(uint number, uint rock) + { + switch (number) + { + case 0: + outputSystem = IOSystem.Null; + break; + case 1: + outputSystem = IOSystem.Filter; + filterAddress = rock; + break; + case 2: + if (glkMode == GlkMode.None) + throw new VMException("Glk support is not enabled"); + outputSystem = IOSystem.Glk; + break; + case 20: // T is the 20th letter + outputSystem = IOSystem.Channels; + break; + default: + throw new VMException("Unrecognized output system " + number.ToString()); + } + } + + private void NextCStringChar() + { + byte ch = image.ReadByte(pc); + pc++; + + if (ch == 0) + { + DonePrinting(); + return; + } + + if (outputSystem == IOSystem.Filter) + PerformCall(filterAddress, new uint[] { ch }, GLULX_STUB_RESUME_CSTR, 0, pc); + else + SendCharToOutput(ch); + } + + private void NextUniStringChar() + { + uint ch = image.ReadInt32(pc); + pc += 4; + + if (ch == 0) + { + DonePrinting(); + return; + } + + if (outputSystem == IOSystem.Filter) + PerformCall(filterAddress, new uint[] { ch }, GLULX_STUB_RESUME_UNISTR, 0, pc); + else + SendCharToOutput(ch); + } + + private void NextDigit() + { + string num = pc.ToString(); + if (printingDigit < num.Length) + { + if (outputSystem == IOSystem.Filter) + { + PerformCall(filterAddress, new uint[] { (uint)num[printingDigit] }, + GLULX_STUB_RESUME_NUMBER, (uint)(printingDigit + 1), pc); + } + else + { + // there's no reason to be here if we're not filtering output... + System.Diagnostics.Debug.Assert(false); + + SendCharToOutput(num[printingDigit]); + printingDigit++; + } + } + else + DonePrinting(); + } + + private bool NextCompressedStringBit() + { + bool result = (image.ReadByte(pc) & (1 << printingDigit)) != 0; + + printingDigit++; + if (printingDigit == 8) + { + printingDigit = 0; + pc++; + } + + return result; + } + + #region Native String Decoding Table + + private abstract class StrNode + { + /// + /// Performs the action associated with this string node: printing + /// a character or string, terminating output, or reading a bit and + /// delegating to another node. + /// + /// The that is printing. + /// When called on a branch node, this will consume one or + /// more compressed string bits. + public abstract void HandleNextChar(Engine e); + + /// + /// Returns the non-branch node that will handle the next string action. + /// + /// The that is printing. + /// A non-branch string node. + /// When called on a branch node, this will consume one or + /// more compressed string bits. + public virtual StrNode GetHandlingNode(Engine e) + { + return this; + } + + /// + /// Gets a value indicating whether this node requires a call stub to be + /// pushed. + /// + public virtual bool NeedsCallStub + { + get { return false; } + } + + /// + /// Gets a value indicating whether this node terminates the string. + /// + public virtual bool IsTerminator + { + get { return false; } + } + + protected void EmitChar(Engine e, char ch) + { + if (e.outputSystem == IOSystem.Filter) + { + e.PerformCall(e.filterAddress, new uint[] { (uint)ch }, + GLULX_STUB_RESUME_HUFFSTR, e.printingDigit, e.pc); + } + else + { + e.SendCharToOutput(ch); + } + } + + protected void EmitChar(Engine e, uint ch) + { + if (e.outputSystem == IOSystem.Filter) + { + e.PerformCall(e.filterAddress, new uint[] { ch }, + GLULX_STUB_RESUME_HUFFSTR, e.printingDigit, e.pc); + } + else + { + e.SendCharToOutput(ch); + } + } + } + + private class EndStrNode : StrNode + { + public override void HandleNextChar(Engine e) + { + e.DonePrinting(); + } + + public override bool IsTerminator + { + get { return true; } + } + } + + private class BranchStrNode : StrNode + { + private readonly StrNode left, right; + + public BranchStrNode(StrNode left, StrNode right) + { + this.left = left; + this.right = right; + } + + public StrNode Left + { + get { return left; } + } + + public StrNode Right + { + get { return right; } + } + + public override void HandleNextChar(Engine e) + { + if (e.NextCompressedStringBit() == true) + right.HandleNextChar(e); + else + left.HandleNextChar(e); + } + + public override StrNode GetHandlingNode(Engine e) + { + if (e.NextCompressedStringBit() == true) + return right.GetHandlingNode(e); + else + return left.GetHandlingNode(e); + } + } + + private class CharStrNode : StrNode + { + private readonly char ch; + + public CharStrNode(char ch) + { + this.ch = ch; + } + + public char Char + { + get { return ch; } + } + + public override void HandleNextChar(Engine e) + { + EmitChar(e, ch); + } + + public override string ToString() + { + return "CharStrNode: '" + ch + "'"; + } + } + + private class UniCharStrNode : StrNode + { + private readonly uint ch; + + public UniCharStrNode(uint ch) + { + this.ch = ch; + } + + public uint Char + { + get { return ch; } + } + + public override void HandleNextChar(Engine e) + { + EmitChar(e, ch); + } + + public override string ToString() + { + return string.Format("UniCharStrNode: '{0}' ({1})", (char)ch, ch); + } + } + + private class StringStrNode : StrNode + { + private readonly uint address; + private readonly ExecutionMode mode; + private readonly string str; + + public StringStrNode(uint address, ExecutionMode mode, string str) + { + this.address = address; + this.mode = mode; + this.str = str; + } + + public uint Address + { + get { return address; } + } + + public ExecutionMode Mode + { + get { return mode; } + } + + public override void HandleNextChar(Engine e) + { + if (e.outputSystem == IOSystem.Filter) + { + e.PushCallStub( + new CallStub(GLULX_STUB_RESUME_HUFFSTR, e.printingDigit, e.pc, e.fp)); + e.pc = address; + e.execMode = mode; + } + else + { + e.SendStringToOutput(str); + } + } + + public override string ToString() + { + return "StringStrNode: \"" + str + "\""; + } + } + + private class IndirectStrNode : StrNode + { + private readonly uint address; + private readonly bool dblIndirect; + private readonly uint argCount, argsAt; + + public IndirectStrNode(uint address, bool dblIndirect, + uint argCount, uint argsAt) + { + this.address = address; + this.dblIndirect = dblIndirect; + this.argCount = argCount; + this.argsAt = argsAt; + } + + public uint Address + { + get { return address; } + } + + public bool DoubleIndirect + { + get { return DoubleIndirect; } + } + + public uint ArgCount + { + get { return argCount; } + } + + public uint ArgsAt + { + get { return argsAt; } + } + + public override void HandleNextChar(Engine e) + { + e.PrintIndirect( + dblIndirect ? e.image.ReadInt32(address) : address, + argCount, argsAt); + } + + public override bool NeedsCallStub + { + get { return true; } + } + } + + /// + /// Builds a native version of the string decoding table if the table + /// is entirely in ROM, or verifies the table's current state if the + /// table is in RAM. + /// + private void CacheDecodingTable() + { + if (decodingTable == 0) + { + nativeDecodingTable = null; + return; + } + + uint size = image.ReadInt32(decodingTable + GLULX_HUFF_TABLESIZE_OFFSET); + if (decodingTable + size - 1 >= image.RamStart) + { + // if the table is in RAM, don't cache it. just verify it now + // and then process it directly from RAM when the time comes. + nativeDecodingTable = null; + VerifyDecodingTable(); + return; + } + + uint root = image.ReadInt32(decodingTable + GLULX_HUFF_ROOTNODE_OFFSET); + nativeDecodingTable = CacheDecodingTableNode(root); + } + + private StrNode CacheDecodingTableNode(uint node) + { + if (node == 0) + return null; + + byte nodeType = image.ReadByte(node++); + + switch (nodeType) + { + case GLULX_HUFF_NODE_END: + return new EndStrNode(); + + case GLULX_HUFF_NODE_BRANCH: + return new BranchStrNode( + CacheDecodingTableNode(image.ReadInt32(node)), + CacheDecodingTableNode(image.ReadInt32(node + 4))); + + case GLULX_HUFF_NODE_CHAR: + return new CharStrNode((char)image.ReadByte(node)); + + case GLULX_HUFF_NODE_UNICHAR: + return new UniCharStrNode(image.ReadInt32(node)); + + case GLULX_HUFF_NODE_CSTR: + return new StringStrNode(node, ExecutionMode.CString, + ReadCString(node)); + + case GLULX_HUFF_NODE_UNISTR: + return new StringStrNode(node, ExecutionMode.UnicodeString, + ReadUniString(node)); + + case GLULX_HUFF_NODE_INDIRECT: + return new IndirectStrNode(image.ReadInt32(node), false, 0, 0); + + case GLULX_HUFF_NODE_INDIRECT_ARGS: + return new IndirectStrNode(image.ReadInt32(node), false, + image.ReadInt32(node + 4), node + 8); + + case GLULX_HUFF_NODE_DBLINDIRECT: + return new IndirectStrNode(image.ReadInt32(node), true, 0, 0); + + case GLULX_HUFF_NODE_DBLINDIRECT_ARGS: + return new IndirectStrNode(image.ReadInt32(node), true, + image.ReadInt32(node + 4), node + 8); + + default: + throw new VMException("Unrecognized compressed string node type " + nodeType.ToString()); + } + } + + private string ReadCString(uint address) + { + StringBuilder sb = new StringBuilder(); + + byte b = image.ReadByte(address); + while (b != 0) + { + sb.Append((char)b); + b = image.ReadByte(++address); + } + + return sb.ToString(); + } + + private string ReadUniString(uint address) + { + StringBuilder sb = new StringBuilder(); + + uint ch = image.ReadInt32(address); + while (ch != 0) + { + sb.Append((char)ch); + address += 4; + ch = image.ReadInt32(address); + } + + return sb.ToString(); + } + + #endregion + + /// + /// Checks that the string decoding table is well-formed, i.e., that it + /// contains at least one branch, one end marker, and no unrecognized + /// node types. + /// + /// + /// The string decoding table is malformed. + /// + private void VerifyDecodingTable() + { + if (decodingTable == 0) + return; + + Stack nodesToCheck = new Stack(); + + uint rootNode = image.ReadInt32(decodingTable + GLULX_HUFF_ROOTNODE_OFFSET); + nodesToCheck.Push(rootNode); + + bool foundBranch = false, foundEnd = false; + + while (nodesToCheck.Count > 0) + { + uint node = nodesToCheck.Pop(); + byte nodeType = image.ReadByte(node++); + + switch (nodeType) + { + case GLULX_HUFF_NODE_BRANCH: + nodesToCheck.Push(image.ReadInt32(node)); // left child + nodesToCheck.Push(image.ReadInt32(node + 4)); // right child + foundBranch = true; + break; + + case GLULX_HUFF_NODE_END: + foundEnd = true; + break; + + case GLULX_HUFF_NODE_CHAR: + case GLULX_HUFF_NODE_UNICHAR: + case GLULX_HUFF_NODE_CSTR: + case GLULX_HUFF_NODE_UNISTR: + case GLULX_HUFF_NODE_INDIRECT: + case GLULX_HUFF_NODE_INDIRECT_ARGS: + case GLULX_HUFF_NODE_DBLINDIRECT: + case GLULX_HUFF_NODE_DBLINDIRECT_ARGS: + // OK + break; + + default: + throw new VMException("Unrecognized compressed string node type " + nodeType.ToString()); + } + } + + if (!foundBranch) + throw new VMException("String decoding table contains no branches"); + if (!foundEnd) + throw new VMException("String decoding table contains no end markers"); + } + + /// + /// Prints the next character of a compressed string, consuming one or + /// more bits. + /// + /// This is only used when the string decoding table is in RAM. + private void NextCompressedChar() + { + uint node = image.ReadInt32(decodingTable + GLULX_HUFF_ROOTNODE_OFFSET); + + while (true) + { + byte nodeType = image.ReadByte(node++); + + switch (nodeType) + { + case GLULX_HUFF_NODE_BRANCH: + if (NextCompressedStringBit() == true) + node = image.ReadInt32(node + 4); // go right + else + node = image.ReadInt32(node); // go left + break; + + case GLULX_HUFF_NODE_END: + DonePrinting(); + return; + + case GLULX_HUFF_NODE_CHAR: + case GLULX_HUFF_NODE_UNICHAR: + uint singleChar = (nodeType == GLULX_HUFF_NODE_UNICHAR) ? + image.ReadInt32(node) : image.ReadByte(node); + if (outputSystem == IOSystem.Filter) + { + PerformCall(filterAddress, new uint[] { singleChar }, + GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc); + } + else + { + SendCharToOutput(singleChar); + } + return; + + case GLULX_HUFF_NODE_CSTR: + if (outputSystem == IOSystem.Filter) + { + PushCallStub(new CallStub(GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc, fp)); + pc = node; + execMode = ExecutionMode.CString; + } + else + { + for (byte ch = image.ReadByte(node); ch != 0; ch = image.ReadByte(++node)) + SendCharToOutput(ch); + } + return; + + case GLULX_HUFF_NODE_UNISTR: + if (outputSystem == IOSystem.Filter) + { + PushCallStub(new CallStub(GLULX_STUB_RESUME_UNISTR, printingDigit, pc, fp)); + pc = node; + execMode = ExecutionMode.UnicodeString; + } + else + { + for (uint ch = image.ReadInt32(node); ch != 0; node += 4, ch = image.ReadInt32(node)) + SendCharToOutput(ch); + } + return; + + case GLULX_HUFF_NODE_INDIRECT: + PrintIndirect(image.ReadInt32(node), 0, 0); + return; + + case GLULX_HUFF_NODE_INDIRECT_ARGS: + PrintIndirect(image.ReadInt32(node), image.ReadInt32(node + 4), node + 8); + return; + + case GLULX_HUFF_NODE_DBLINDIRECT: + PrintIndirect(image.ReadInt32(image.ReadInt32(node)), 0, 0); + return; + + case GLULX_HUFF_NODE_DBLINDIRECT_ARGS: + PrintIndirect(image.ReadInt32(image.ReadInt32(node)), image.ReadInt32(node + 4), node + 8); + return; + + default: + throw new VMException("Unrecognized compressed string node type " + nodeType.ToString()); + } + } + } + + /// + /// Prints a string, or calls a routine, when an indirect node is + /// encountered in a compressed string. + /// + /// The address of the string or routine. + /// The number of arguments passed in. + /// The address where the argument array is stored. + private void PrintIndirect(uint address, uint argCount, uint argsAt) + { + byte type = image.ReadByte(address); + + switch (type) + { + case 0xC0: + case 0xC1: + uint[] args = new uint[argCount]; + for (uint i = 0; i < argCount; i++) + args[i] = image.ReadInt32(argsAt + 4 * i); + PerformCall(address, args, GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc); + break; + + case 0xE0: + if (outputSystem == IOSystem.Filter) + { + PushCallStub(new CallStub(GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc, fp)); + execMode = ExecutionMode.CString; + pc = address + 1; + } + else + { + address++; + for (byte ch = image.ReadByte(address); ch != 0; ch = image.ReadByte(++address)) + SendCharToOutput(ch); + } + break; + + case 0xE1: + PushCallStub(new CallStub(GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc, fp)); + execMode = ExecutionMode.CompressedString; + pc = address + 1; + printingDigit = 0; + break; + + case 0xE2: + if (outputSystem == IOSystem.Filter) + { + PushCallStub(new CallStub(GLULX_STUB_RESUME_HUFFSTR, printingDigit, pc, fp)); + execMode = ExecutionMode.UnicodeString; + pc = address + 4; + } + else + { + address += 4; + for (uint ch = image.ReadInt32(address); ch != 0; address += 4, ch = image.ReadInt32(address)) + SendCharToOutput(ch); + } + break; + + default: + throw new VMException(string.Format("Invalid type for indirect printing: {0:X}h", type)); + } + } + + private void DonePrinting() + { + ResumeFromCallStub(0); + } + } +} \ No newline at end of file diff --git a/OutputBuffer.cs b/OutputBuffer.cs new file mode 100644 index 0000000..d6f691a --- /dev/null +++ b/OutputBuffer.cs @@ -0,0 +1,114 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; + +namespace FyreVM +{ + /// + /// Collects output from the game file, on various output channels, to be + /// delivered all at once. + /// + internal class OutputBuffer + { + private const uint DEFAULT_CHANNEL = ('M' << 24) | ('A' << 16) | ('I' << 8) | 'N'; + private uint channel = ('M' << 24) | ('A' << 16) | ('I' << 8) | 'N'; + private Dictionary channelData; + + /// + /// Initializes a new output buffer and adds the main channel. + /// + public OutputBuffer() + { + channelData = new Dictionary(); + channelData.Add(DEFAULT_CHANNEL, new StringBuilder()); + } + + /// + /// Gets or sets the current output channel. + /// + /// + /// If the output channel is changed to any channel other than + /// , the channel's contents will be + /// cleared first. + /// + public uint Channel + { + get { return channel; } + set + { + if (channel != value) + { + channel = value; + if (value != DEFAULT_CHANNEL) + if (!channelData.ContainsKey(channel)) + channelData.Add(channel, new StringBuilder()); + else + channelData[channel].Length = 0; + } + } + } + + /// + /// Writes a string to the buffer for the currently selected + /// output channel. + /// + /// The string to write. + public void Write(string s) + { + if (!channelData.ContainsKey(channel)) + channelData.Add(channel, new StringBuilder(s)); + else + channelData[channel].Append(s); + } + + /// + /// Writes a single character to the buffer for the currently selected + /// output channel. + /// + /// The character to write. + public void Write(char c) + { + if (!channelData.ContainsKey(channel)) + channelData.Add(channel, new StringBuilder(c)); + else + channelData[channel].Append(c); + } + + + /// + /// Packages all the output that has been stored so far, returns it, + /// and empties the buffer. + /// + /// A dictionary mapping each active output channel to the + /// string of text that has been sent to it since the last flush. + public IDictionary Flush() + { + Dictionary result = new Dictionary(); + + foreach (KeyValuePair pair in channelData) + { + string channelName = GetChannelName(pair.Key); + + if (pair.Value.Length > 0) + { + result.Add(channelName, pair.Value.ToString()); + pair.Value.Length = 0; + } + } + + return result; + } + + private string GetChannelName(uint channelNumber) + { + return String.Concat((char)((channelNumber >> 24) & 0xff), (char)((channelNumber >> 16) & 0xff), (char)((channelNumber >> 8) & 0xff), (char)(channelNumber & 0xff)); + } + } +} diff --git a/Profiler.cs b/Profiler.cs new file mode 100644 index 0000000..e7a8467 --- /dev/null +++ b/Profiler.cs @@ -0,0 +1,660 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace FyreVM.Profiling +{ + /// + /// Describes a routine and collects performance statistics about it. + /// + public class ProfiledRoutine + { + private readonly uint address; + private string name, source, desc; + private long cycles; + private TimeSpan time; + private uint hitCount; + + internal ProfiledRoutine(uint address) + { + this.address = address; + } + + /// + /// Gets the routine's address. + /// + public uint Address + { + get { return address; } + } + + /// + /// Gets the routine's name, or null if the name is unknown. + /// + public string Name + { + get { return name; } + internal set { name = value; } + } + + /// + /// Gets the source code reference where the routine was defined, or + /// null if the definition point is unknown. + /// + public string Source + { + get { return source; } + internal set { source = value; } + } + + /// + /// Gets a human-readable description for the routine, or null + /// if no description is available. + /// + public string Description + { + get { return desc; } + internal set { desc = value; } + } + + /// + /// Gets the number of opcodes that have been executed for this routine. + /// + public long Cycles + { + get { return cycles; } + internal set { cycles = value; } + } + + /// + /// Gets the length of time that has been spent executing this routine. + /// + public TimeSpan Time + { + get { return time; } + internal set { time = value; } + } + + /// + /// Gets the number of times this routine has been called. + /// + public uint HitCount + { + get { return hitCount; } + internal set { hitCount = value; } + } + } + + /// + /// Tracks and analyzes a game's performance. + /// + public class Profiler + { + private class SymbolInfo + { + public string Name; + public string Source; + public string Description; + + public SymbolInfo(string name, string source) + { + this.Name = name; + this.Source = source; + this.Description = ""; + } + } + + private struct SourceLine + { + public int FileNum; + public int LineNum; + public int CharPos; + } + + private readonly Dictionary dict = new Dictionary(); + private uint currentRoutine; + private long entryCycles; + private DateTime entryTime; + private readonly Stack routineStack = new Stack(); + private Dictionary symbols; + private Dictionary symbolsByName; + private bool meterRunning = true; + + public Profiler() + { + } + + /// + /// Clears the performance information that has been collected so far. + /// + public void ResetStats() + { + dict.Clear(); + } + + /// + /// Gets or sets a value indicating whether performance is currently + /// being tracked. + /// + public bool MeterRunning + { + get { return meterRunning; } + set { meterRunning = value; } + } + + /// + /// Records that the VM is entering a routine. + /// + /// The address of the routine. + /// The current cycle count. + internal void Enter(uint address, long cycles) + { + routineStack.Push(currentRoutine); + + if (currentRoutine != 0) + { + long elapsedCycles = cycles - entryCycles; + TimeSpan elapsedTime = DateTime.Now - entryTime; + + BillCurrentRoutine(elapsedCycles, elapsedTime); + } + + currentRoutine = address; + BumpHitCount(); + entryCycles = cycles; + entryTime = DateTime.Now; + } + + /// + /// Records that the VM is leaving the most recently entered routine. + /// + /// The current cycle count. + internal void Leave(long cycles) + { + long elapsedCycles = cycles - entryCycles; + TimeSpan elapsedTime = DateTime.Now - entryTime; + + BillCurrentRoutine(elapsedCycles, elapsedTime); + + currentRoutine = routineStack.Pop(); + entryCycles = cycles; + entryTime = DateTime.Now; + } + + private ProfiledRoutine GetRoutineRecord() + { + ProfiledRoutine rec; + if (dict.TryGetValue(currentRoutine, out rec) == false) + { + rec = new ProfiledRoutine(currentRoutine); + dict.Add(currentRoutine, rec); + + SymbolInfo info; + if (symbols != null && symbols.TryGetValue(currentRoutine, out info)) + { + rec.Name = info.Name; + rec.Source = info.Source; + rec.Description = info.Description; + } + } + return rec; + } + + private void BillCurrentRoutine(long elapsedCycles, TimeSpan elapsedTime) + { + if (meterRunning) + { + ProfiledRoutine rec = GetRoutineRecord(); + + rec.Cycles += elapsedCycles; + rec.Time += elapsedTime; + } + } + + private void BumpHitCount() + { + if (meterRunning) + { + ProfiledRoutine rec = GetRoutineRecord(); + + rec.HitCount++; + } + } + + /// + /// Reads function names and addresses from a game debugging file. + /// + /// The game being profiled. + /// The Inform debugging file (gameinfo.dbg). + public void ReadDebugSymbols(Stream gameFile, Stream debugFile) + { + debugFile.Seek(0, SeekOrigin.Begin); + if (debugFile.ReadByte() != 0xDE || debugFile.ReadByte() != 0xBF) + throw new ArgumentException("Not an Inform debug file", "debugFile"); + + ushort version = BigEndian.ReadInt16(debugFile); + if (version != 0) + throw new ArgumentException("Unsupported debug file version", "debugFile"); + + BigEndian.ReadInt16(debugFile); // discard Inform version + + byte[] header = null; + uint codeOffset = 0; + List routineAddrs = new List(); + List routineNames = new List(); + List routineSources = new List(); + + Dictionary fileNames = new Dictionary(); + + while (debugFile.Position < debugFile.Length) + { + int type = debugFile.ReadByte(); + switch (type) + { + case 0: // EOF_DBR + debugFile.Seek(0, SeekOrigin.End); + break; + + case 1: // FILE_DBR + int fileNum = debugFile.ReadByte(); + SkipString(debugFile); + string fileName = ReadString(debugFile); + fileNames.Add(fileNum, Path.GetFileName(fileName)); + break; + + case 2: // CLASS_DBR + SkipString(debugFile); + debugFile.Seek(8, SeekOrigin.Current); + break; + + case 3: // OBJECT_DBR + debugFile.Seek(2, SeekOrigin.Current); + SkipString(debugFile); + debugFile.Seek(8, SeekOrigin.Current); + break; + + case 4: // GLOBAL_DBR + debugFile.ReadByte(); + SkipString(debugFile); + break; + + case 12: // ARRAY_DBR + case 5: // ATTR_DBR + case 6: // PROP_DBR + case 7: // FAKE_ACTION_DBR + case 8: // ACTION_DBR + debugFile.Seek(2, SeekOrigin.Current); + SkipString(debugFile); + break; + + case 9: // HEADER_DBR + header = new byte[64]; + debugFile.Read(header, 0, 64); + break; + + case 11: // ROUTINE_DBR + debugFile.Seek(2, SeekOrigin.Current); + SourceLine line = ReadLine(debugFile); + switch (line.FileNum) + { + case 0: + routineSources.Add(""); + break; + + case 255: + routineSources.Add(""); + break; + + default: + routineSources.Add(string.Format("{0}:{1}", + fileNames[line.FileNum], line.LineNum)); + break; + } + routineAddrs.Add(ReadAddress(debugFile)); + routineNames.Add(ReadString(debugFile)); + while (SkipString(debugFile) == true) + { + // keep skipping local variable names + } + break; + + case 10: // LINEREF_DBR + debugFile.Seek(2, SeekOrigin.Current); + ushort numSeqPts = BigEndian.ReadInt16(debugFile); + debugFile.Seek(numSeqPts * 6, SeekOrigin.Current); + break; + + case 14: // ROUTINE_END_DBR + debugFile.Seek(9, SeekOrigin.Current); + break; + + case 13: // MAP_DBR + while (true) + { + string key = ReadString(debugFile); + if (key.Length == 0) + break; + uint value = ReadAddress(debugFile); + if (key == "code area") + codeOffset = value; + } + break; + } + } + + // verify header + if (header != null) + { + byte[] origHeader = new byte[64]; + gameFile.Seek(0, SeekOrigin.Begin); + gameFile.Read(origHeader, 0, 64); + for (int i = 0; i < 64; i++) + if (header[i] != origHeader[i]) + throw new ArgumentException("Debug file header does not match game", "debugFile"); + } + + // store routine addresses + if (symbols == null) + { + symbols = new Dictionary(); + symbolsByName = new Dictionary(); + } + for (int i = 0; i < routineAddrs.Count; i++) + { + SymbolInfo info = new SymbolInfo(routineNames[i], routineSources[i]); + symbols[codeOffset + routineAddrs[i]] = info; + symbolsByName[info.Name] = info; + } + + UpdateRecsFromSymbols(); + } + + /// + /// Reads routine descriptions and definition points from a source file + /// generated by Inform 7 (auto.inf). + /// + /// + /// This will probably break when the layout of auto.inf changes. + public void ReadDescriptions(string autoInfFile) + { + if (symbolsByName == null) + throw new InvalidOperationException("Call ReadDebugSymbols first"); + + int lineNum = 1; + string currentObjID = ""; + List routineNames = new List(); + List routineDescs = new List(); + char[] spaceDelim = { ' ' }; + string lastLine = "", lastRoutine = ""; + + using (FileStream stream = new FileStream(autoInfFile, FileMode.Open, FileAccess.Read)) + { + autoInfFile = Path.GetFileName(autoInfFile); + using (StreamReader rdr = new StreamReader(stream)) + { + while (!rdr.EndOfStream) + { + string line = rdr.ReadLine(); + if (line.StartsWith("Class ") || line.StartsWith("Object ")) + { + string tempLine = line.Replace("->", ""); + string[] parts = tempLine.Split(spaceDelim, StringSplitOptions.RemoveEmptyEntries); + currentObjID = parts[1]; + } + else if (line.StartsWith(" with parse_name ")) + { + string[] parts = line.Split(spaceDelim, StringSplitOptions.RemoveEmptyEntries); + routineNames.Add(parts[2]); + routineDescs.Add("parses the name of " + currentObjID); + } + else if (line.StartsWith(" Relation_")) + { + // an entry in the relations table + string[] parts = line.Split(spaceDelim, 2, StringSplitOptions.RemoveEmptyEntries); + routineNames.Add(parts[0]); + int quote = line.IndexOf('"'), unquote = line.LastIndexOf('"'); + string relDesc = line.Substring(quote + 1, unquote - quote - 2); + int relates = relDesc.IndexOf(" relates "); + routineDescs.Add("implements the \"" + + relDesc.Substring(0, relates) + "\" relation"); + } + else if (line.StartsWith("[ Adj_")) + { + // an adjective routine definition + string[] parts = line.Split(spaceDelim); + routineNames.Add(parts[1]); + int bang = line.IndexOf('!'); + routineDescs.Add(line.Substring(bang + 2)); + } + else if (line.StartsWith("[ ")) + { + // any other routine definition + int endPos = line.IndexOfAny(routineDelims, 2); + if (endPos == -1) + endPos = line.Length; + string routine = line.Substring(2, endPos - 2); + + if (lastLine.StartsWith("! ")) + { + routineNames.Add(routine); + string desc = lastLine.Substring(2); + if (desc.EndsWith(":")) + desc = desc.Substring(0, desc.Length - 1); + routineDescs.Add(desc); + } + else if (routine.StartsWith("R_SHELL_") && lastRoutine.StartsWith("R_")) + { + routineNames.Add(routine); + routineDescs.Add("the main part of " + lastRoutine); + } + + lastRoutine = routine; + } + lineNum++; + if (line.StartsWith("! ") && + (lastLine.StartsWith("! From ") || lastLine.StartsWith("! Find ") || + lastLine.StartsWith("! True or ") || + lastLine.StartsWith("! How many ") || + lastLine.StartsWith("! Make everything "))) + lastLine = "! [" + lastLine.Substring(2) + "] " + line.Substring(2); + else + lastLine = line; + } + } + } + + // label the routines we just found + for (int i = 0; i < routineNames.Count; i++) + { + SymbolInfo info; + if (symbolsByName.TryGetValue(routineNames[i], out info)) + info.Description = routineDescs[i]; + } + + // change temporary file name to auto.inf for all routines + foreach (SymbolInfo info in symbols.Values) + { + if (info.Source.StartsWith(autoInfFile)) + { + int colon = info.Source.LastIndexOf(':'); + info.Source = "Inform 7 (auto.inf" + info.Source.Substring(colon) + ")"; + } + } + + UpdateRecsFromSymbols(); + } + + /// + /// Naively assigns descriptions to routines based on their names. + /// + public void SetDefaultDescriptions() + { + foreach (SymbolInfo info in symbols.Values) + if (info.Description == "") + info.Description = GetDefaultDescription(info.Name); + + UpdateRecsFromSymbols(); + } + + private string GetDefaultDescription(string routine) + { + if (routine.StartsWith("Resolver_")) + return "dispatch routine for an overload phrase"; + + if (routine.StartsWith("PHR_")) + return "a phrase"; + + if (routine.StartsWith("R_")) + return "a rule"; + + if (routine.StartsWith("R_SHELL_")) + return "the main part of a rule or phrase which uses dynamic blocks"; + + if (routine.StartsWith("Prop_")) + return "a set of objects or a query about objects)"; + + if (routine.StartsWith("Consult_Grammar_")) + return "a topic"; + + if (routine.StartsWith("GPR_Line_")) + return "assists with parsing topics"; + + if (routine.StartsWith("Parse_Name_")) + return "parses a complicated object or kind name"; + + if (routine.StartsWith("Cond_Token_")) + return "the condition for an understand-when line"; + + if (routine.StartsWith("text_routine_")) + return "text with bracketed substitutions"; + + if (routine.StartsWith("LOS_")) + return "tests \"in the presence of\""; + + if (routine.StartsWith("NAP_")) + return "a named category of actions"; + + if (routine.StartsWith("PAPR_")) + return "a chronology test event"; + + if (routine.EndsWith("found_in")) + return "backdrop placement"; + + // no match + return ""; + } + + private static readonly char[] routineDelims = { ' ', ';' }; + + /// + /// Loads routine names and definition points from an Inform source + /// file that wasn't generated by Inform 7 (for example, the .i6 template layer). + /// + /// + public void ReadSourceSymbols(string sourceFile) + { + if (symbolsByName == null) + throw new InvalidOperationException("Call ReadDebugSymbols first"); + + using (FileStream stream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)) + { + sourceFile = Path.GetFileName(sourceFile); + using (StreamReader rdr = new StreamReader(stream)) + { + int lineNum = 1; + while (!rdr.EndOfStream) + { + string line = rdr.ReadLine(); + if (line.StartsWith("[ ")) + { + int endPos = line.IndexOfAny(routineDelims, 2); + if (endPos == -1) + endPos = line.Length; + string routine = line.Substring(2, endPos - 2); + + SymbolInfo info; + if (symbolsByName.TryGetValue(routine, out info)) + info.Source = string.Format("{0}:{1}", sourceFile, lineNum); + } + lineNum++; + } + } + } + + UpdateRecsFromSymbols(); + } + + private void UpdateRecsFromSymbols() + { + // update any profiling records that already exist + foreach (ProfiledRoutine rec in dict.Values) + { + SymbolInfo info; + if (symbols.TryGetValue(rec.Address, out info)) + { + rec.Name = info.Name; + rec.Source = info.Source; + rec.Description = info.Description; + } + } + } + + private static SourceLine ReadLine(Stream debugFile) + { + SourceLine result; + + result.FileNum = debugFile.ReadByte(); + result.LineNum = BigEndian.ReadInt16(debugFile); + result.CharPos = debugFile.ReadByte(); + + return result; + } + + private static string ReadString(Stream debugFile) + { + StringBuilder sb = new StringBuilder(); + + int ch = debugFile.ReadByte(); + while (ch > 0) + { + sb.Append((char)ch); + ch = debugFile.ReadByte(); + } + + return sb.ToString(); + } + + private static bool SkipString(Stream debugFile) + { + int ch = debugFile.ReadByte(); + if (ch == 0) + return false; + + do { ch = debugFile.ReadByte(); } while (ch > 0); + return true; + } + + private static uint ReadAddress(Stream debugFile) + { + int a = debugFile.ReadByte(); + int b = debugFile.ReadByte(); + int c = debugFile.ReadByte(); + return (uint)((a << 16) + (b << 8) + c); + } + + /// + /// Gets the current set of profiler results. + /// + /// An array of records. + public ProfiledRoutine[] GetResults() + { + return dict.Values.ToArray(); + } + } +} diff --git a/Quetzal.cs b/Quetzal.cs new file mode 100644 index 0000000..326af61 --- /dev/null +++ b/Quetzal.cs @@ -0,0 +1,243 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace FyreVM +{ + /// + /// Implements the Quetzal saved-game file specification by holding a list of + /// typed data chunks which can be read from or written to streams. + /// + /// + /// http://www.ifarchive.org/if-archive/infocom/interpreters/specification/savefile_14.txt + /// + internal class Quetzal + { + private Dictionary chunks = new Dictionary(); + + private static readonly uint FORM = StrToID("FORM"); + private static readonly uint LIST = StrToID("LIST"); + private static readonly uint CAT_ = StrToID("CAT "); + private static readonly uint IFZS = StrToID("IFZS"); + + /// + /// Initializes a new chunk collection. + /// + public Quetzal() + { + } + + /// + /// Loads a collection of chunks from a Quetzal file. + /// + /// The stream to read from. + /// A new instance initialized + /// from the stream. + /// + /// Duplicate chunks are not supported by this class. Only the last + /// chunk of a given type will be available. + /// + public static Quetzal FromStream(Stream stream) + { + Quetzal result = new Quetzal(); + + uint type = BigEndian.ReadInt32(stream); + if (type != FORM && type != LIST && type != CAT_) + throw new ArgumentException("Invalid IFF type"); + + int length = (int)BigEndian.ReadInt32(stream); + byte[] buffer = new byte[length]; + int amountRead = stream.Read(buffer, 0, (int)length); + if (amountRead < length) + throw new ArgumentException("Quetzal file is too short"); + + stream = new MemoryStream(buffer); + type = BigEndian.ReadInt32(stream); + if (type != IFZS) + throw new ArgumentException("Wrong IFF sub-type: not a Quetzal file"); + + while (stream.Position < stream.Length) + { + type = BigEndian.ReadInt32(stream); + length = (int)BigEndian.ReadInt32(stream); + byte[] chunk = new byte[length]; + amountRead = stream.Read(chunk, 0, length); + if (amountRead < length) + throw new ArgumentException("Chunk extends past end of file"); + + result.chunks[type] = chunk; + } + + return result; + } + + /// + /// Gets or sets typed data chunks. + /// + /// The 4-character type identifier. + /// The contents of the chunk. + public byte[] this[string type] + { + get + { + byte[] result = null; + chunks.TryGetValue(StrToID(type), out result); + return result; + } + set + { + chunks[StrToID(type)] = value; + } + } + + /// + /// Checks whether the Quetzal file contains a given chunk type. + /// + /// The 4-character type identifier. + /// if the chunk is present. + public bool Contains(string type) + { + return chunks.ContainsKey(StrToID(type)); + } + + private static uint StrToID(string type) + { + byte a = (byte)type[0]; + byte b = (byte)type[1]; + byte c = (byte)type[2]; + byte d = (byte)type[3]; + return (uint)((a << 24) + (b << 16) + (c << 8) + d); + } + + /// + /// Writes the chunks to a Quetzal file. + /// + /// The stream to write to. + public void WriteToStream(Stream stream) + { + BigEndian.WriteInt32(stream, FORM); // IFF tag + BigEndian.WriteInt32(stream, 0); // file length (filled in later) + BigEndian.WriteInt32(stream, IFZS); // FORM sub-ID for Quetzal + + uint totalSize = 4; // includes sub-ID + foreach (KeyValuePair pair in chunks) + { + BigEndian.WriteInt32(stream, pair.Key); // chunk type + BigEndian.WriteInt32(stream, (uint)pair.Value.Length); // chunk length + stream.Write(pair.Value, 0, pair.Value.Length); // chunk data + totalSize += 8 + (uint)(pair.Value.Length); + } + + if (totalSize % 2 == 1) + stream.WriteByte(0); // padding (not counted in file length) + + stream.Seek(4, SeekOrigin.Begin); + BigEndian.WriteInt32(stream, totalSize); + //stream.SetLength(totalSize); + } + + /// + /// Compresses a block of memory by comparing it with the original + /// version from the game file. + /// + /// An array containing the original block of memory. + /// The offset within the array where the original block + /// starts. + /// The length of the original block, in bytes. + /// An array containing the changed block to be compressed. + /// The offset within the array where the changed + /// block starts. + /// The length of the changed block. This may be + /// greater than , but may not be less. + /// The RLE-compressed set of differences between the old and new + /// blocks, prefixed with a 4-byte length. + public static byte[] CompressMemory(byte[] original, int origStart, int origLength, + byte[] changed, int changedStart, int changedLength) + { + if (origStart + origLength > original.Length) + throw new ArgumentException("Original array is too small"); + if (changedStart + changedLength > changed.Length) + throw new ArgumentException("Changed array is too small"); + if (changedLength < origLength) + throw new ArgumentException("New block must be no smaller than old block"); + + MemoryStream mstr = new MemoryStream(); + BigEndian.WriteInt32(mstr, (uint)changedLength); + + for (int i = 0; i < origLength; i++) + { + byte b = (byte)(original[origStart+i] ^ changed[changedStart+i]); + if (b == 0) + { + int runLength; + for (runLength = 1; i + runLength < origLength; runLength++) + { + if (runLength == 256) + break; + if (original[origStart + i + runLength] != changed[changedStart + i + runLength]) + break; + } + mstr.WriteByte(0); + mstr.WriteByte((byte)(runLength - 1)); + i += runLength - 1; + } + else + mstr.WriteByte(b); + } + + return mstr.ToArray(); + } + + /// + /// Reconstitutes a changed block of memory by applying a compressed + /// set of differences to the original block from the game file. + /// + /// The original block of memory. + /// The RLE-compressed set of differences, + /// prefixed with a 4-byte length. This length may be larger than + /// the original block, but not smaller. + /// The changed block of memory. The length of this array is + /// specified at the beginning of . + public static byte[] DecompressMemory(byte[] original, byte[] delta) + { + MemoryStream mstr = new MemoryStream(delta); + uint length = BigEndian.ReadInt32(mstr); + if (length < original.Length) + throw new ArgumentException("Compressed block's length tag must be no less than original block's size"); + + byte[] result = new byte[length]; + int rp = 0; + + for (int i = 4; i < delta.Length; i++) + { + byte b = delta[i]; + if (b == 0) + { + int repeats = delta[++i] + 1; + Array.Copy(original, rp, result, rp, repeats); + rp += repeats; + } + else + { + result[rp] = (byte)(original[rp] ^ b); + rp++; + } + } + + while (rp < original.Length) + { + result[rp] = original[rp]; + rp++; + } + + return result; + } + + } +} diff --git a/UlxImage.cs b/UlxImage.cs new file mode 100644 index 0000000..ce2c657 --- /dev/null +++ b/UlxImage.cs @@ -0,0 +1,297 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace FyreVM +{ + /// + /// Represents the ROM and RAM of a Glulx game image. + /// + internal class UlxImage + { + private byte[] memory; + private uint ramstart; + private Stream originalStream; + private byte[] originalRam, originalHeader; + + /// + /// Initializes a new instance from the specified stream. + /// + /// A stream containing the Glulx image. + public UlxImage(Stream stream) + { + originalStream = stream; + LoadFromStream(stream); + } + + private void LoadFromStream(Stream stream) + { + if (stream.Length > int.MaxValue) + throw new ArgumentException(".ulx file is too big"); + + if (stream.Length < Engine.GLULX_HDR_SIZE) + throw new ArgumentException(".ulx file is too small"); + + // read just the header, to find out how much memory we need + memory = new byte[Engine.GLULX_HDR_SIZE]; + stream.Seek(0, SeekOrigin.Begin); + stream.Read(memory, 0, Engine.GLULX_HDR_SIZE); + + if (memory[0] != (byte)'G' || memory[1] != (byte)'l' || + memory[2] != (byte)'u' || memory[3] != (byte)'l') + throw new ArgumentException(".ulx file has wrong magic number"); + + uint endmem = ReadInt32(Engine.GLULX_HDR_ENDMEM_OFFSET); + + // now read the whole thing + memory = new byte[endmem]; + stream.Seek(0, SeekOrigin.Begin); + stream.Read(memory, 0, (int)stream.Length); + + // verify checksum + uint checksum = CalculateChecksum(); + if (checksum != ReadInt32(Engine.GLULX_HDR_CHECKSUM_OFFSET)) + throw new ArgumentException(".ulx file has incorrect checksum"); + + ramstart = ReadInt32(Engine.GLULX_HDR_RAMSTART_OFFSET); + } + + /// + /// Gets the address at which RAM begins. + /// + /// + /// The region of memory below RamStart is considered ROM. Addresses + /// below RamStart are readable but unwritable. + /// + public uint RamStart + { + get { return ramstart; } + } + + /// + /// Gets or sets the address at which memory ends. + /// + /// + /// This can be changed by the game with @setmemsize (or managed + /// automatically by the heap allocator). Addresses above EndMem are + /// neither readable nor writable. + /// + public uint EndMem + { + get + { + return (uint)memory.Length; + } + set + { + // round up to the next multiple of 256 + if (value % 256 != 0) + value = (value + 255) & 0xFFFFFF00; + + if ((uint)memory.Length != value) + { + byte[] newMem = new byte[value]; + Array.Copy(memory, newMem, (int)Math.Min((uint)memory.Length, (int)value)); + memory = newMem; + } + } + } + + /// + /// Reads a single byte from memory. + /// + /// The address to read from. + /// The byte at the specified address. + public byte ReadByte(uint offset) + { + return memory[offset]; + } + + /// + /// Reads a big-endian word from memory. + /// + /// The address to read from + /// The word at the specified address. + public ushort ReadInt16(uint offset) + { + return BigEndian.ReadInt16(memory, offset); + } + + /// + /// Reads a big-endian double word from memory. + /// + /// The address to read from. + /// The 32-bit value at the specified address. + public uint ReadInt32(uint offset) + { + return BigEndian.ReadInt32(memory, offset); + } + + /// + /// Writes a single byte into memory. + /// + /// The address to write to. + /// The value to write. + /// The address is below RamStart. + public void WriteByte(uint offset, byte value) + { + if (offset < ramstart) + throw new VMException("Writing into ROM"); + + memory[offset] = value; + } + + /// + /// Writes a big-endian 16-bit word into memory. + /// + /// The address to write to. + /// The value to write. + /// The address is below RamStart. + public void WriteInt16(uint offset, ushort value) + { + if (offset < ramstart) + throw new VMException("Writing into ROM"); + + BigEndian.WriteInt16(memory, offset, value); + } + + /// + /// Writes a big-endian 32-bit word into memory. + /// + /// The address to write to. + /// The value to write. + /// The address is below RamStart. + public void WriteInt32(uint offset, uint value) + { + if (offset < ramstart) + throw new VMException("Writing into ROM"); + + BigEndian.WriteInt32(memory, offset, value); + } + + /// + /// Calculates the checksum of the image. + /// + /// The sum of the entire image, taken as an array of + /// 32-bit words. + public uint CalculateChecksum() + { + uint end = ReadInt32(Engine.GLULX_HDR_EXTSTART_OFFSET); + // negative checksum here cancels out the one we'll add inside the loop + uint sum = (uint)(-ReadInt32(Engine.GLULX_HDR_CHECKSUM_OFFSET)); + + System.Diagnostics.Debug.Assert(end % 4 == 0); // Glulx spec 1.2 says ENDMEM % 256 == 0 + + for (uint i = 0; i < end; i += 4) + sum += ReadInt32(i); + + return sum; + } + + /// + /// Gets the entire contents of memory. + /// + /// An array containing all VM memory, ROM and RAM. + public byte[] GetMemory() + { + return memory; + } + + /// + /// Sets the entire contents of RAM, changing the size if necessary. + /// + /// The new contents of RAM. + /// If true, indicates that + /// is prefixed with a 32-bit word giving the new size of RAM, which may be + /// more than the number of bytes actually contained in the rest of the array. + public void SetRAM(byte[] newBlock, bool embeddedLength) + { + uint length; + int offset; + + if (embeddedLength) + { + offset = 4; + length = (uint)((newBlock[0] << 24) + (newBlock[1] << 16) + (newBlock[2] << 8) + newBlock[3]); + } + else + { + offset = 0; + length = (uint)newBlock.Length; + } + + EndMem = ramstart + length; + Array.Copy(newBlock, offset, memory, (int)ramstart, newBlock.Length - offset); + } + + /// + /// Obtains the initial contents of RAM from the game file. + /// + /// The initial contents of RAM. + public byte[] GetOriginalRAM() + { + if (originalRam == null) + { + int length = (int)(ReadInt32(Engine.GLULX_HDR_ENDMEM_OFFSET) - ramstart); + originalRam = new byte[length]; + originalStream.Seek(ramstart, SeekOrigin.Begin); + originalStream.Read(originalRam, 0, length); + } + return originalRam; + } + + /// + /// Obtains the header from the game file. + /// + /// The first 128 bytes of the game file. + public byte[] GetOriginalIFHD() + { + if (originalHeader == null) + { + originalHeader = new byte[128]; + originalStream.Seek(0, SeekOrigin.Begin); + originalStream.Read(originalHeader, 0, 128); + } + return originalHeader; + } + + /// + /// Copies a block of data out of RAM. + /// + /// The address, based at , + /// at which to start copying. + /// The number of bytes to copy. + /// The destination array. + public void ReadRAM(uint address, uint length, byte[] dest) + { + Array.Copy(memory, (int)(ramstart + address), dest, 0, (int)length); + } + + /// + /// Copies a block of data into RAM, expanding the memory map if needed. + /// + /// The address, based at , + /// at which to start copying. + /// The source array. + public void WriteRAM(uint address, byte[] src) + { + EndMem = Math.Max(EndMem, ramstart + (uint)src.Length); + Array.Copy(src, 0, memory, (int)(ramstart + address), src.Length); + } + + /// + /// Reloads the game file, discarding all changes that have been made + /// to RAM and restoring the memory map to its original size. + /// + public void Revert() + { + LoadFromStream(originalStream); + } + } +} diff --git a/VMException.cs b/VMException.cs new file mode 100644 index 0000000..2f2cb76 --- /dev/null +++ b/VMException.cs @@ -0,0 +1,29 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing resstrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace FyreVM +{ + // TODO: include Glulx backtrace in VM exceptions? + + /// + /// An exception that is thrown by FyreVM when the game misbehaves. + /// + public class VMException : Exception + { + public VMException(string message) + : base(message) + { + } + + public VMException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Veneer.cs b/Veneer.cs new file mode 100644 index 0000000..1876ac2 --- /dev/null +++ b/Veneer.cs @@ -0,0 +1,544 @@ +/* + * Copyright © 2008, Textfyre, Inc. - All Rights Reserved + * Please read the accompanying COPYRIGHT file for licensing restrictions. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace FyreVM +{ + public partial class Engine + { + /// + /// Identifies a veneer routine that is intercepted, or a constant that + /// the replacement routine needs to use. + /// + private enum VeneerSlot + { + // routine addresses + Z__Region = 1, + CP__Tab = 2, + OC__Cl = 3, + RA__Pr = 4, + RT__ChLDW = 5, + Unsigned__Compare = 6, + RL__Pr = 7, + RV__Pr = 8, + OP__Pr = 9, + RT__ChSTW = 10, + RT__ChLDB = 11, + Meta__class = 12, + + // object numbers and compiler constants + String = 1001, + Routine = 1002, + Class = 1003, + Object = 1004, + RT__Err = 1005, + NUM_ATTR_BYTES = 1006, + classes_table = 1007, + INDIV_PROP_START = 1008, + cpv__start = 1009, + ofclass_err = 1010, + readprop_err = 1011, + } + + /// + /// Provides hardcoded versions of some commonly used veneer routines (low-level + /// functions that are automatically compiled into every Inform game). + /// + /// + /// Inform games rely heavily on these routines, and substituting our C# versions + /// for the Glulx versions in the story file can increase performance significantly. + /// + private class Veneer + { + private uint zregion_fn, cp_tab_fn, oc_cl_fn, ra_pr_fn, rt_chldw_fn; + private uint unsigned_compare_fn, rl_pr_fn, rv_pr_fn, op_pr_fn; + private uint rt_chstw_fn, rt_chldb_fn, meta_class_fn; + + private uint string_mc, routine_mc, class_mc, object_mc; + private uint rt_err_fn, num_attr_bytes, classes_table; + private uint indiv_prop_start, cpv_start; + private uint ofclass_err, readprop_err; + + // RAM addresses of compiler-generated global variables + private const uint SELF_OFFSET = 16; + private const uint SENDER_OFFSET = 20; + // offsets of compiler-generated property numbers from INDIV_PROP_START + private const uint CALL_PROP = 5; + private const uint PRINT_PROP = 6; + private const uint PRINT_TO_ARRAY_PROP = 7; + + private static readonly Dictionary funcSlotMap, paramSlotMap; + + static Veneer() + { + funcSlotMap = new Dictionary(); + funcSlotMap.Add(1, VeneerSlot.Z__Region); + funcSlotMap.Add(2, VeneerSlot.CP__Tab); + funcSlotMap.Add(3, VeneerSlot.RA__Pr); + funcSlotMap.Add(4, VeneerSlot.RL__Pr); + funcSlotMap.Add(5, VeneerSlot.OC__Cl); + funcSlotMap.Add(6, VeneerSlot.RV__Pr); + funcSlotMap.Add(7, VeneerSlot.OP__Pr); + + paramSlotMap = new Dictionary(); + paramSlotMap.Add(0, VeneerSlot.classes_table); + paramSlotMap.Add(1, VeneerSlot.INDIV_PROP_START); + paramSlotMap.Add(2, VeneerSlot.Class); + paramSlotMap.Add(3, VeneerSlot.Object); + paramSlotMap.Add(4, VeneerSlot.Routine); + paramSlotMap.Add(5, VeneerSlot.String); + //paramSlotMap.Add(6, VeneerSlot.self) + paramSlotMap.Add(7, VeneerSlot.NUM_ATTR_BYTES); + paramSlotMap.Add(8, VeneerSlot.cpv__start); + } + + /// + /// Registers a routine address or constant value, using the traditional + /// FyreVM slot codes. + /// + /// Identifies the address or constant being registered. + /// The address of the routine or value of the constant. + /// if registration was successful. + public bool SetSlotFyre(uint slot, uint value) + { + switch ((VeneerSlot)slot) + { + case VeneerSlot.Z__Region: zregion_fn = value; break; + case VeneerSlot.CP__Tab: cp_tab_fn = value; break; + case VeneerSlot.OC__Cl: oc_cl_fn = value; break; + case VeneerSlot.RA__Pr: ra_pr_fn = value; break; + case VeneerSlot.RT__ChLDW: rt_chldw_fn = value; break; + case VeneerSlot.Unsigned__Compare: unsigned_compare_fn = value; break; + case VeneerSlot.RL__Pr: rl_pr_fn = value; break; + case VeneerSlot.RV__Pr: rv_pr_fn = value; break; + case VeneerSlot.OP__Pr: op_pr_fn = value; break; + case VeneerSlot.RT__ChSTW: rt_chstw_fn = value; break; + case VeneerSlot.RT__ChLDB: rt_chldb_fn = value; break; + case VeneerSlot.Meta__class: meta_class_fn = value; break; + + case VeneerSlot.String: string_mc = value; break; + case VeneerSlot.Routine: routine_mc = value; break; + case VeneerSlot.Class: class_mc = value; break; + case VeneerSlot.Object: object_mc = value; break; + case VeneerSlot.RT__Err: rt_err_fn = value; break; + case VeneerSlot.NUM_ATTR_BYTES: num_attr_bytes = value; break; + case VeneerSlot.classes_table: classes_table = value; break; + case VeneerSlot.INDIV_PROP_START: indiv_prop_start = value; break; + case VeneerSlot.cpv__start: cpv_start = value; break; + case VeneerSlot.ofclass_err: ofclass_err = value; break; + case VeneerSlot.readprop_err: readprop_err = value; break; + + default: + // not recognized + return false; + } + + // recognized + return true; + } + + /// + /// Registers a routine address or constant value, using the acceleration + /// codes defined in the Glulx specification. + /// + /// The for which the value is being set. + /// to set a constant value; + /// false to set a routine address. + /// The routine or constant index to set. + /// The address of the routine or value of the constant. + /// if registration was successful. + public bool SetSlotGlulx(Engine e, bool isParam, uint slot, uint value) + { + if (isParam && slot == 6) + { + if (value != e.image.RamStart + SELF_OFFSET) + throw new ArgumentException("Unexpected value for acceleration parameter 6"); + return true; + } + + Dictionary dict = isParam ? paramSlotMap : funcSlotMap; + VeneerSlot fyreSlot; + if (dict.TryGetValue(slot, out fyreSlot)) + return SetSlotFyre((uint)fyreSlot, value); + else + return false; + } + + /// + /// Tests whether a particular function is supported for acceleration, + /// using the codes defined in the Glulx specification. + /// + /// The routine index. + /// if the function code is supported. + public bool ImplementsFuncGlulx(uint slot) + { + return funcSlotMap.ContainsKey(slot); + } + + /// + /// Intercepts a routine call if its address has previously been registered. + /// + /// The attempting to call the routine. + /// The address of the routine. + /// The routine's arguments. + /// The routine's return value. + /// if the call was intercepted. + /// + /// matches a registered veneer routine, but + /// is too short for that routine. + /// + public bool InterceptCall(Engine e, uint address, uint[] args, out uint result) + { + if (address != 0) + { + if (address == zregion_fn) + { + result = Z__Region(e, args[0]); + return true; + } + + if (address == cp_tab_fn) + { + result = CP__Tab(e, args[0], args[1]); + return true; + } + + if (address == oc_cl_fn) + { + result = OC__Cl(e, args[0], args[1]); + return true; + } + + if (address == ra_pr_fn) + { + result = RA__Pr(e, args[0], args[1]); + return true; + } + + if (address == rt_chldw_fn) + { + result = RT__ChLDW(e, args[0], args[1]); + return true; + } + + if (address == unsigned_compare_fn) + { + result = (uint)args[0].CompareTo(args[1]); + return true; + } + + if (address == rl_pr_fn) + { + result = RL__Pr(e, args[0], args[1]); + return true; + } + + if (address == rv_pr_fn) + { + result = RV__Pr(e, args[0], args[1]); + return true; + } + + if (address == op_pr_fn) + { + result = OP__Pr(e, args[0], args[1]); + return true; + } + + if (address == rt_chstw_fn) + { + result = RT__ChSTW(e, args[0], args[1], args[2]); + return true; + } + + if (address == rt_chldb_fn) + { + result = RT__ChLDB(e, args[0], args[1]); + return true; + } + + if (address == meta_class_fn) + { + result = Meta__class(e, args[0]); + return true; + } + } + + result = 0; + return false; + } + + // distinguishes between strings, routines, and objects + private uint Z__Region(Engine e, uint address) + { + if (address < 36 || address >= e.image.EndMem) + return 0; + + byte type = e.image.ReadByte(address); + if (type >= 0xE0) + return 3; + if (type >= 0xC0) + return 2; + if (type >= 0x70 && type <= 0x7F && address >= e.image.RamStart) + return 1; + + return 0; + } + + // finds an object's common property table + private uint CP__Tab(Engine e, uint obj, uint id) + { + if (Z__Region(e, obj) != 1) + { + e.NestedCall(rt_err_fn, 23, obj); + return 0; + } + + uint otab = e.image.ReadInt32(obj + 16); + if (otab == 0) + return 0; + uint max = e.image.ReadInt32(otab); + otab += 4; + return e.PerformBinarySearch(id, 2, otab, 10, max, 0, SearchOptions.None); + } + + // finds the location of an object ("parent()" function) + private uint Parent(Engine e, uint obj) + { + return e.image.ReadInt32(obj + 1 + num_attr_bytes + 12); + } + + // determines whether an object is a member of a given class ("ofclass" operator) + private uint OC__Cl(Engine e, uint obj, uint cla) + { + switch (Z__Region(e, obj)) + { + case 3: + return (uint)(cla == string_mc ? 1 : 0); + + case 2: + return (uint)(cla == routine_mc ? 1 : 0); + + case 1: + if (cla == class_mc) + { + if (Parent(e, obj) == class_mc) + return 1; + if (obj == class_mc || obj == string_mc || + obj == routine_mc || obj == object_mc) + return 1; + return 0; + } + + if (cla == object_mc) + { + if (Parent(e, obj) == class_mc) + return 0; + if (obj == class_mc || obj == string_mc || + obj == routine_mc || obj == object_mc) + return 0; + return 1; + } + + if (cla == string_mc || cla == routine_mc) + return 0; + + if (Parent(e, cla) != class_mc) + { + e.NestedCall(rt_err_fn, ofclass_err, cla, 0xFFFFFFFF); + return 0; + } + + uint inlist = RA__Pr(e, obj, 2); + if (inlist == 0) + return 0; + + uint inlistlen = RL__Pr(e, obj, 2) / 4; + for (uint jx = 0; jx < inlistlen; jx++) + if (e.image.ReadInt32(inlist + jx * 4) == cla) + return 1; + + return 0; + + default: + return 0; + } + } + + // finds the address of an object's property (".&" operator) + private uint RA__Pr(Engine e, uint obj, uint id) + { + uint cla = 0; + if ((id & 0xFFFF0000) != 0) + { + cla = e.image.ReadInt32(classes_table + 4 * (id & 0xFFFF)); + if (OC__Cl(e, obj, cla) == 0) + return 0; + + id >>= 16; + obj = cla; + } + + uint prop = CP__Tab(e, obj, id); + if (prop == 0) + return 0; + + if (Parent(e, obj) == class_mc && cla == 0) + if (id < indiv_prop_start || id >= indiv_prop_start + 8) + return 0; + + if (e.image.ReadInt32(e.image.RamStart + SELF_OFFSET) != obj) + { + int ix = (e.image.ReadByte(prop + 9) & 1); + if (ix != 0) + return 0; + } + + return e.image.ReadInt32(prop + 4); + } + + // finds the length of an object's property (".#" operator) + private uint RL__Pr(Engine e, uint obj, uint id) + { + uint cla = 0; + if ((id & 0xFFFF0000) != 0) + { + cla = e.image.ReadInt32(classes_table + 4 * (id & 0xFFFF)); + if (OC__Cl(e, obj, cla) == 0) + return 0; + + id >>= 16; + obj = cla; + } + + uint prop = CP__Tab(e, obj, id); + if (prop == 0) + return 0; + + if (Parent(e, obj) == class_mc && cla == 0) + if (id < indiv_prop_start || id >= indiv_prop_start + 8) + return 0; + + if (e.image.ReadInt32(e.image.RamStart + SELF_OFFSET) != obj) + { + int ix = (e.image.ReadByte(prop + 9) & 1); + if (ix != 0) + return 0; + } + + return (uint)(4 * e.image.ReadInt16(prop + 2)); + } + + // performs bounds checking when reading from a word array ("-->" operator) + private uint RT__ChLDW(Engine e, uint array, uint offset) + { + uint address = array + 4 * offset; + if (address >= e.image.EndMem) + { + return e.NestedCall(rt_err_fn, 25); + } + return e.image.ReadInt32(address); + } + + // reads the value of an object's property ("." operator) + private uint RV__Pr(Engine e, uint obj, uint id) + { + uint addr = RA__Pr(e, obj, id); + if (addr == 0) + { + if (id > 0 && id < indiv_prop_start) + return e.image.ReadInt32(cpv_start + 4 * id); + + e.NestedCall(rt_err_fn, readprop_err, obj, id); + return 0; + } + + return e.image.ReadInt32(addr); + } + + // determines whether an object provides a given property ("provides" operator) + private uint OP__Pr(Engine e, uint obj, uint id) + { + switch (Z__Region(e, obj)) + { + case 3: + if (id == indiv_prop_start + PRINT_PROP || + id == indiv_prop_start + PRINT_TO_ARRAY_PROP) + return 1; + else + return 0; + + case 2: + if (id == indiv_prop_start + CALL_PROP) + return 1; + else + return 0; + + case 1: + if (id >= indiv_prop_start && id < indiv_prop_start + 8) + if (Parent(e, obj) == class_mc) + return 1; + + if (RA__Pr(e, obj, id) != 0) + return 1; + else + return 0; + + default: + return 0; + } + } + + // performs bounds checking when writing to a word array ("-->" operator) + private uint RT__ChSTW(Engine e, uint array, uint offset, uint val) + { + uint address = array + 4 * offset; + if (address >= e.image.EndMem || address < e.image.RamStart) + { + return e.NestedCall(rt_err_fn, 27); + } + else + { + e.image.WriteInt32(address, val); + return 0; + } + } + + // performs bounds checking when reading from a byte array ("->" operator) + private uint RT__ChLDB(Engine e, uint array, uint offset) + { + uint address = array + offset; + if (address >= e.image.EndMem) + return e.NestedCall(rt_err_fn, 24); + + return e.image.ReadByte(address); + } + + // determines the metaclass of a routine, string, or object ("metaclass()" function) + private uint Meta__class(Engine e, uint obj) + { + switch (Z__Region(e, obj)) + { + case 2: + return routine_mc; + case 3: + return string_mc; + case 1: + if (Parent(e, obj) == class_mc) + return class_mc; + if (obj == class_mc || obj == string_mc || + obj == routine_mc || obj == object_mc) + return class_mc; + return object_mc; + default: + return 0; + } + } + } + } +}