gruesome 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|