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;
+ }
+ }
+ }
+ }
+}