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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +35 -0
- data/Rakefile +15 -0
- data/bin/gruesome +9 -0
- data/gruesome.gemspec +25 -0
- data/lib/gruesome/cli.rb +70 -0
- data/lib/gruesome/logo.rb +28 -0
- data/lib/gruesome/machine.rb +17 -0
- data/lib/gruesome/version.rb +3 -0
- data/lib/gruesome/z/abbreviation_table.rb +30 -0
- data/lib/gruesome/z/decoder.rb +292 -0
- data/lib/gruesome/z/dictionary.rb +156 -0
- data/lib/gruesome/z/header.rb +46 -0
- data/lib/gruesome/z/instruction.rb +50 -0
- data/lib/gruesome/z/machine.rb +71 -0
- data/lib/gruesome/z/memory.rb +268 -0
- data/lib/gruesome/z/object_table.rb +430 -0
- data/lib/gruesome/z/opcode.rb +519 -0
- data/lib/gruesome/z/opcode_class.rb +15 -0
- data/lib/gruesome/z/operand_type.rb +15 -0
- data/lib/gruesome/z/processor.rb +399 -0
- data/lib/gruesome/z/zscii.rb +337 -0
- data/lib/gruesome.rb +7 -0
- data/spec/z/memory_spec.rb +90 -0
- data/spec/z/processor_spec.rb +1956 -0
- data/test/logo.txt +77 -0
- metadata +118 -0
@@ -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
|