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