gruesome 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ require 'bit-struct'
2
+
3
+ module Gruesome
4
+ module Z
5
+
6
+ # Z-Story File Header
7
+ class Header < BitStruct
8
+ default_options :endian => :big
9
+
10
+ unsigned :version, 8, "Z-Machine Version"
11
+ unsigned :availablity_flags, 8, "Availability Flags"
12
+ unsigned :reservedw1, 16, "Reserved Word 1"
13
+ unsigned :high_mem_base, 16, "High Memory Base"
14
+ unsigned :entry, 16, "Entry Point"
15
+ unsigned :dictionary_addr, 16, "Address of Dictionary"
16
+ unsigned :object_tbl_addr, 16, "Address of Object Table"
17
+ unsigned :global_var_addr, 16, "Address of Global Variables Table"
18
+ unsigned :static_mem_base, 16, "Static Memory Base"
19
+ unsigned :capability_flags, 8, "Desired Capability Flags"
20
+ unsigned :reservedb1, 8, "Reserved Byte 1"
21
+ unsigned :reservedw2, 16, "Reserved Word 2"
22
+ unsigned :reservedw3, 16, "Reserved Word 3"
23
+ unsigned :reservedw4, 16, "Reserved Word 4"
24
+ unsigned :abbrev_tbl_addr, 16, "Address of Abbreviations Table"
25
+ unsigned :file_length, 16, "Length of File"
26
+ unsigned :checksum, 16, "Checksum of File"
27
+ unsigned :interpreter_number, 8, "Interpreter Number"
28
+ unsigned :interpreter_version, 8, "Interpreter Version"
29
+ unsigned :screen_height, 8, "Screen Height (in Lines)"
30
+ unsigned :screen_width, 8, "Screen Height (in Characters)"
31
+ unsigned :screen_width_units, 16, "Screen Width (in Units)"
32
+ unsigned :screen_height_units, 16, "Screen Height (in Units)"
33
+ unsigned :font_width, 8, "Font Width (in Units)"
34
+ unsigned :font_height, 8, "Font Height (in Units)"
35
+ unsigned :routines_addr, 16, "Offset to Routines (Divided by 8)"
36
+ unsigned :static_str_addr, 16, "Offset to Static Strings (Divided by 8)"
37
+ unsigned :background_color, 8, "Default Background Color"
38
+ unsigned :foreground_color, 8, "Default Foreground Color"
39
+ unsigned :terminating_chars_tbl_addr, 16, "Address of Terminating Characters Table"
40
+ unsigned :output_stream_3_width, 16, "Total Width of Characters Send to Output Stream 3"
41
+ unsigned :revision_number, 16, "Standard Revision Number"
42
+ unsigned :alphabet_tbl_addr, 16, "Address of Alphabet Table"
43
+ unsigned :header_ext_tbl_addr, 16, "Address of Header Extension Table"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ module Gruesome
2
+ module Z
3
+
4
+ # A Z-Machine instruction
5
+ class Instruction
6
+ attr_reader :opcode # the opcode
7
+ attr_reader :types # the types of the operands
8
+ attr_reader :operands # the operands given to the instruction
9
+ attr_reader :destination # the destination variable to place the result
10
+ attr_reader :branch_to # the address to set the pc when branch is taken
11
+ # also... if 0, return true from routine
12
+ # if 1, return false from routine
13
+ attr_reader :branch_on # the condition is matched against this
14
+ attr_reader :length # instruction size in number of bytes
15
+
16
+ def initialize(opcode, types, operands, destination, branch_destination, branch_condition, length)
17
+ @opcode = opcode
18
+ @types = types
19
+ @operands = operands
20
+ @destination = destination
21
+ @branch_to = branch_destination
22
+ @branch_on = branch_condition
23
+ @length = length
24
+ end
25
+
26
+ def to_s(version)
27
+ line = Opcode.name(@opcode, version)
28
+ idx = -1
29
+ line = line + @operands.inject("") do |result, element|
30
+ idx += 1
31
+ if @types[idx] == OperandType::VARIABLE
32
+ result + " %" + sprintf("%02x", element)
33
+ else
34
+ result + " " + element.to_s
35
+ end
36
+ end
37
+
38
+ if @destination != nil
39
+ line = line + " -> %" + sprintf("%02x", @destination)
40
+ end
41
+
42
+ if @branch_to != nil
43
+ line = line + " goto $" + sprintf("%04x", @branch_to) + " on " + @branch_on.to_s
44
+ end
45
+
46
+ line
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'header'
2
+ require_relative 'memory'
3
+ require_relative 'decoder'
4
+ require_relative 'processor'
5
+ require_relative 'abbreviation_table'
6
+ require_relative 'object_table'
7
+
8
+ module Gruesome
9
+ module Z
10
+
11
+ # The class that initializes and maintains a Z-Machine
12
+ class Machine
13
+
14
+ # Will create a new virtual machine for the game file
15
+ def initialize(game_file)
16
+ file = File.open(game_file, "r")
17
+
18
+ # I. Create memory space
19
+
20
+ memory_size = file.size
21
+ @memory = Memory.new(file.read(memory_size))
22
+
23
+ # Set flags
24
+ flags = @memory.force_readb(0x01)
25
+ flags &= ~(0b1110000)
26
+ @memory.force_writeb(0x01, flags)
27
+
28
+ # Set flags 2
29
+ flags = @memory.force_readb(0x10)
30
+ flags &= ~(0b11111100)
31
+ @memory.force_writeb(0x10, flags)
32
+
33
+ # II. Read header (at address 0x0000) and associated tables
34
+ @header = Header.new(@memory.contents)
35
+ @object_table = ObjectTable.new(@memory)
36
+
37
+ # III. Instantiate CPU
38
+ @decoder = Decoder.new(@memory)
39
+ @processor = Processor.new(@memory)
40
+ end
41
+
42
+ def execute
43
+ while true do
44
+ i = @decoder.fetch
45
+ #var = @memory.readv(0)
46
+ #if var != nil
47
+ # puts "var %00 = " + sprintf("%04x", var)
48
+ # @memory.writev(0, var)
49
+ #end
50
+ #var = @memory.readv(1)
51
+ #if var != nil
52
+ # puts "var %01 = " + sprintf("%04x", @memory.readv(0x01))
53
+ #end
54
+ #puts "at $" + sprintf("%04x", @memory.program_counter) + ": " + i.to_s(@header.version)
55
+ @memory.program_counter += i.length
56
+
57
+ if i.opcode == Opcode::QUIT
58
+ break
59
+ end
60
+
61
+ begin
62
+ @processor.execute(i)
63
+ rescue RuntimeError => fuh
64
+ "error at $" + sprintf("%04x", @memory.program_counter) + ": " + i.to_s(@header.version)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
@@ -0,0 +1,268 @@
1
+ require_relative 'header'
2
+
3
+ # The Z-Machine Memory is a simple array of bytes
4
+ #
5
+ # There are three regions: Dynamic, Static, and High
6
+ #
7
+ # Dynamic Memory can be read and written to by a program
8
+ # Static Memory can only be read
9
+ # High Memory cannot be accessed by load/store instructions
10
+ #
11
+ # High Memory can overlap Static, but never Dynamic
12
+ #
13
+ # Memory is stored in big endian
14
+
15
+ # Also included as memory space, yet separated from the
16
+ # RAM itself: the stack, program counter, and a routine
17
+ # call stack which holds the stacks of the currently
18
+ # invocated routines
19
+
20
+ # The stack is weird. Every function call starts with an empty stack
21
+ # and any work left in the stack upon a return is lost. So there are
22
+ # actually many stacks... one stack to hold the stacks in play, and
23
+ # a stack for each active function.
24
+ #
25
+ # The stack holds the return address and the (up to) 15 local variables
26
+ # for the routine as accessed by variables %01 to %0f.
27
+ #
28
+ # Variable %00 is the top of the stack, writing to it pushes, reading
29
+ # from it pulls.
30
+ #
31
+ # Illegal access to variables will halt the machine. Such as illegally
32
+ # accessing local variables that do not exist as the routine header
33
+ # will specify an exact number.
34
+
35
+ module Gruesome
36
+ module Z
37
+
38
+ # This class holds the memory for the virtual machine
39
+ class Memory
40
+ attr_accessor :program_counter
41
+ attr_reader :num_locals
42
+
43
+ def initialize(contents)
44
+ @call_stack = []
45
+ @stack = []
46
+ @memory = contents
47
+ @num_locals = 0
48
+
49
+ # Get the header information
50
+ @header = Header.new(@memory)
51
+ @program_counter = @header.entry
52
+
53
+ # With the header info, discover the bounds of each memory region
54
+ @dyn_base = 0x0
55
+ @dyn_limit = @header.static_mem_base
56
+
57
+ # Cannot Write to Static Memory
58
+ @static_base = @header.static_mem_base
59
+ @static_limit = @memory.length
60
+
61
+ # Cannot Access High Memory
62
+ @high_base = @header.high_mem_base
63
+ @high_limit = @memory.length
64
+
65
+ # Error if high memory overlaps dynamic memory
66
+ if @high_base < @dyn_limit
67
+ # XXX: ERROR
68
+ end
69
+
70
+ # Check machine endianess
71
+ @endian = [1].pack('S')[0] == 1 ? 'little' : 'big'
72
+ end
73
+
74
+ def packed_address_to_byte_address(address)
75
+ if @header.version <=3
76
+ address * 2
77
+ else
78
+ end
79
+ end
80
+
81
+ # Sets up the environment for a new routine
82
+ def push_routine(return_addr, num_locals, destination)
83
+ # pushes the stack onto the call stack
84
+ @call_stack.push @num_locals
85
+ @call_stack.push destination
86
+ @call_stack.push @stack
87
+
88
+ # empties the current stack
89
+ @stack = Array.new()
90
+
91
+ # pushes the return address onto the stack
92
+ @stack.push(return_addr)
93
+
94
+ # push locals
95
+ num_locals.times do
96
+ @stack.push 0
97
+ end
98
+
99
+ @num_locals = num_locals
100
+ end
101
+
102
+ # Tears down the environment for the current routine
103
+ def pop_routine()
104
+ # return the return address
105
+ return_addr = @stack[0]
106
+ @stack = @call_stack.pop
107
+ destination = @call_stack.pop
108
+ @num_locals = @call_stack.pop
109
+
110
+ {:destination => destination, :return_address => return_addr}
111
+ end
112
+
113
+ def readb(address)
114
+ if address < @high_base
115
+ force_readb(address)
116
+ else
117
+ # XXX: Access violation
118
+ raise "Access Violation accessing $" + sprintf("%04x", address)
119
+ nil
120
+ end
121
+ end
122
+
123
+ def readw(address)
124
+ if (address + 1) < @high_base
125
+ force_readw(address)
126
+ else
127
+ # XXX: Access violation
128
+ raise "Access Violation accessing $" + sprintf("%04x", address)
129
+ nil
130
+ end
131
+ end
132
+
133
+ def writeb(address, value)
134
+ if address < @static_base
135
+ force_writeb(address, value)
136
+ else
137
+ # XXX: Access violation
138
+ raise "Access Violation (W) accessing $" + sprintf("%04x", address)
139
+ nil
140
+ end
141
+ end
142
+
143
+ def writew(address, value)
144
+ if (address + 1) < @static_base
145
+ force_writew(address, value)
146
+ else
147
+ # XXX: Access violation
148
+ raise "Access Violation (W) accessing $" + sprintf("%04x", address)
149
+ nil
150
+ end
151
+ end
152
+
153
+ def force_readb(address)
154
+ if address < @memory.size
155
+ @memory.getbyte(address)
156
+ else
157
+ # XXX: Access Violation
158
+ raise "Major Access Violation accessing $" + sprintf("%04x", address)
159
+ nil
160
+ end
161
+ end
162
+
163
+ def force_readw(address)
164
+ if (address + 1) < @memory.size
165
+ if @endian == 'little'
166
+ (@memory.getbyte(address+1) << 8) | @memory.getbyte(address)
167
+ else
168
+ (@memory.getbyte(address) << 8) | @memory.getbyte(address+1)
169
+ end
170
+ else
171
+ # XXX: Access Violation
172
+ raise "Major Access Violation accessing $" + sprintf("%04x", address)
173
+ nil
174
+ end
175
+ end
176
+
177
+ def force_writeb(address, value)
178
+ if address < @memory.size
179
+ @memory.setbyte(address, (value & 255))
180
+ else
181
+ # XXX: Access Violation
182
+ raise "Major Access (W) Violation accessing $" + sprintf("%04x", address)
183
+ nil
184
+ end
185
+ end
186
+
187
+ def force_writew(address, value)
188
+ if (address + 1) < @memory.size
189
+ low_byte = value & 255
190
+ high_byte = (value >> 8) & 255
191
+
192
+ if @endian == 'little'
193
+ tmp = high_byte
194
+ high_byte = low_byte
195
+ low_byte = tmp
196
+ end
197
+
198
+ @memory.setbyte(address, high_byte)
199
+ @memory.setbyte(address+1, low_byte)
200
+ else
201
+ # XXX: Access Violation
202
+ raise "Major Access (W) Violation accessing $" + sprintf("%04x", address)
203
+ nil
204
+ end
205
+ end
206
+
207
+ def contents
208
+ @memory
209
+ end
210
+
211
+ # Read from variable number index
212
+ def readv(index)
213
+ if index == 0
214
+ # pop from stack
215
+ @stack.pop
216
+ elsif index >= 16
217
+ index -= 16
218
+ readw(@header.global_var_addr + (index*2))
219
+ elsif index <= @num_locals
220
+ @stack[index]
221
+ else
222
+ # XXX: Error
223
+ end
224
+ end
225
+
226
+ # Write value to variable number index
227
+ def writev(index, value)
228
+ value &= 65535
229
+ if index == 0
230
+ # push to stack
231
+ @stack.push value
232
+ elsif index >= 16
233
+ index -= 16
234
+ writew(@header.global_var_addr + (index*2), value)
235
+ elsif index <= @num_locals
236
+ @stack[index] = value
237
+ else
238
+ # XXX: Error
239
+ end
240
+ end
241
+
242
+ def force_readzstr(index, max_len = -1)
243
+ chrs = []
244
+ continue = true
245
+ orig_index = index
246
+
247
+ until continue == false do
248
+ if max_len != -1 and (index + 2 - orig_index) > max_len
249
+ break
250
+ end
251
+
252
+ byte1 = force_readb(index)
253
+ byte2 = force_readb(index+1)
254
+
255
+ index += 2
256
+
257
+ chrs << ((byte1 >> 2) & 0b11111)
258
+ chrs << (((byte1 & 0b11) << 3) | (byte2 >> 5))
259
+ chrs << (byte2 & 0b11111)
260
+
261
+ continue = (byte1 & 0b10000000) == 0
262
+ end
263
+
264
+ return [index - orig_index, chrs]
265
+ end
266
+ end
267
+ end
268
+ end