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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
<pre>
|
2
|
+
▄▄▄▄▄ ▄▄▄▄▄
|
3
|
+
▀████▄ ▄████▀
|
4
|
+
▀████▄ ▄████▀
|
5
|
+
▄▄▄▄▄▄ ██████▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄ ▄████▀ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄
|
6
|
+
▄████████▄ ██▓▀████▄ ██▓ ███ ▄███████ ▄███████▄ ▄████▀██▄ ████▄████ ▄███████
|
7
|
+
██▓ ███ ██▓ ▀███ ██▓ ███ ██▓ ██▓ ███ ███▀ ███ █████████ ██▓
|
8
|
+
██▓ ██▓ ███ ██▓ ███ ██▓ ██▓ ██▓ ███ ███▀█▀███ ██▓
|
9
|
+
██▓ █████ ████████ ███ ███ ████████ ▀██████▄ ██▓ ███ ██▓ ███ ████████
|
10
|
+
██▓ ▀▀███ ▀██▓▀▀▀█▄▄ ███ ███ ███▀▀▀▀▀ ▀▀▀▀███ ██▓ ███ ██▓ ███ ███▀▀▀▀▀
|
11
|
+
██▓ ███ ██▓ ███ ██▓ ███ ██▓ ██▓ ███ ██▓ ███ ███ ███ ██▓
|
12
|
+
▀████████▀ ██▓ ███ ▀███████▀ ▀███████ ▀███████▀ ▀███████▀ ██▓ ███ ▀███████
|
13
|
+
▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀
|
14
|
+
</pre>
|
15
|
+
|
16
|
+
# Gruesome
|
17
|
+
|
18
|
+
The Ruby Z-Code Emulator and IF Manager (Currently in progress). It current can
|
19
|
+
play version 3 games such as the ZORK trilogy.
|
20
|
+
|
21
|
+
## Currently implemented
|
22
|
+
|
23
|
+
* Majority of version 3 Z-Machine (Enough to play most games)
|
24
|
+
|
25
|
+
## Next
|
26
|
+
|
27
|
+
* Versions 4+ (Goal: Run Photopia)
|
28
|
+
|
29
|
+
## Overall Goals
|
30
|
+
|
31
|
+
There are several goals:
|
32
|
+
|
33
|
+
* Emulate the Z-Machine accurately for all versions (text-only first)
|
34
|
+
* Provide detailed information to IF designers and programmers
|
35
|
+
* Make it easy to pull down and play new and old IF and text adventure games
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
# rake spec
|
4
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
5
|
+
spec.pattern = 'spec/*/*_spec.rb'
|
6
|
+
end
|
7
|
+
|
8
|
+
# rake doc
|
9
|
+
RSpec::Core::RakeTask.new(:doc) do |spec|
|
10
|
+
spec.pattern = 'spec/*/*_spec.rb'
|
11
|
+
spec.rspec_opts = ['--format documentation']
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'bundler'
|
15
|
+
Bundler::GemHelper.install_tasks
|
data/bin/gruesome
ADDED
data/gruesome.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gruesome/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gruesome"
|
7
|
+
s.version = Grue::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Dave Wilkinson"]
|
10
|
+
s.email = ["wilkie05@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/wilkie/gruesome"
|
12
|
+
s.summary = %q{An Interactive Fiction client that can play/read interactive stories}
|
13
|
+
s.description = %q{Reads and executes various interactive fiction technologies and helps easily download new stories from other sources on the Internet.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "gruesome"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
|
24
|
+
s.add_dependency "bit-struct"
|
25
|
+
end
|
data/lib/gruesome/cli.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require_relative '../gruesome'
|
4
|
+
require_relative 'machine'
|
5
|
+
require_relative 'logo'
|
6
|
+
|
7
|
+
module Gruesome
|
8
|
+
class CLI
|
9
|
+
BANNER = <<-USAGE
|
10
|
+
Usage:
|
11
|
+
gruesome play STORY_FILE
|
12
|
+
|
13
|
+
Description:
|
14
|
+
The 'play' command will start a session of the story given as STORY_FILE
|
15
|
+
|
16
|
+
Example:
|
17
|
+
gruesome play zork1.z3
|
18
|
+
|
19
|
+
USAGE
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def parse_options
|
23
|
+
@opts = OptionParser.new do |opts|
|
24
|
+
opts.banner = BANNER.gsub(/^\t{2}/, '')
|
25
|
+
|
26
|
+
opts.separator ''
|
27
|
+
opts.separator 'Options:'
|
28
|
+
|
29
|
+
opts.on('-h', '--help', 'Display this help') do
|
30
|
+
puts opts
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@opts.parse!
|
36
|
+
end
|
37
|
+
|
38
|
+
def CLI.run
|
39
|
+
begin
|
40
|
+
parse_options
|
41
|
+
rescue OptionParser::InvalidOption => e
|
42
|
+
warn e
|
43
|
+
exit -1
|
44
|
+
end
|
45
|
+
|
46
|
+
def fail
|
47
|
+
puts @opts
|
48
|
+
exit -1
|
49
|
+
end
|
50
|
+
|
51
|
+
if ARGV.empty?
|
52
|
+
fail
|
53
|
+
end
|
54
|
+
|
55
|
+
case ARGV.first
|
56
|
+
when 'play'
|
57
|
+
fail unless ARGV[1]
|
58
|
+
|
59
|
+
Gruesome::Logo.print
|
60
|
+
|
61
|
+
puts
|
62
|
+
puts "--------------------------------------------------------------------------------"
|
63
|
+
puts
|
64
|
+
|
65
|
+
Gruesome::Machine.new(ARGV[1]).execute
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# DO NOT REMOVE ABOVE COMMENT! IT IS MAGIC!
|
4
|
+
|
5
|
+
module Gruesome
|
6
|
+
module Logo
|
7
|
+
# I'm just making sure this works regardless of the unicode format this
|
8
|
+
# source file is saved as...
|
9
|
+
LOGO = <<ENDLOGO
|
10
|
+
▄▄▄▄▄ ▄▄▄▄▄
|
11
|
+
▀████▄ ▄████▀
|
12
|
+
▀████▄ ▄████▀
|
13
|
+
▄▄▄▄▄▄ ██████▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄████▀ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄
|
14
|
+
▄████████▄ ██▓▀████▄ ██▓ ███ ▄███████ ▄████████▄ ▄████▀██▄ ████▄████ ▄███████
|
15
|
+
██▓ ███ ██▓ ▀███ ██▓ ███ ██▓ ██▓ ███ ███▀ ███ █████████ ██▓
|
16
|
+
██▓ ██▓ ███ ██▓ ███ ██▓ ██▓ ██▓ ███ ███▀█▀███ ██▓
|
17
|
+
██▓ █████ ████████ ███ ███ ████████ ▀███████▄ ██▓ ███ ██▓ ███ ████████
|
18
|
+
██▓ ▀▀███ ▀██▓▀▀▀█▄▄ ███ ███ ███▀▀▀▀▀ ▀▀▀▀▀███ ██▓ ███ ██▓ ███ ███▀▀▀▀▀
|
19
|
+
██▓ ███ ██▓ ███ ██▓ ███ ██▓ ██▓ ███ ██▓ ███ ███ ███ ██▓
|
20
|
+
▀████████▀ ██▓ ███ ▀███████▀ ▀███████ ▀████████▀ ▀███████▀ ██▓ ███ ▀███████
|
21
|
+
▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀▀ ▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀
|
22
|
+
ENDLOGO
|
23
|
+
|
24
|
+
def Logo.print
|
25
|
+
puts LOGO
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'z/machine'
|
2
|
+
|
3
|
+
module Gruesome
|
4
|
+
class Machine
|
5
|
+
def initialize(story_file)
|
6
|
+
# Later, detect the type
|
7
|
+
#
|
8
|
+
# For now, assume Z-Machine
|
9
|
+
|
10
|
+
@machine = Z::Machine.new(story_file)
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
@machine.execute
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# The Z-Machine, in order to conserve precious space, often encoded in various
|
2
|
+
# strings a codeword that would inject the word given at a particular place in
|
3
|
+
# the abbreviation table.
|
4
|
+
|
5
|
+
require_relative 'memory'
|
6
|
+
require_relative 'header'
|
7
|
+
require_relative 'zscii'
|
8
|
+
|
9
|
+
module Gruesome
|
10
|
+
module Z
|
11
|
+
class AbbreviationTable
|
12
|
+
def initialize(memory)
|
13
|
+
@memory = memory
|
14
|
+
@header = Header.new(@memory.contents)
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup(alphabet, index, translation_alphabet)
|
18
|
+
abbrev_index = (32 * (alphabet)) + index
|
19
|
+
addr = @header.abbrev_tbl_addr + abbrev_index*2
|
20
|
+
|
21
|
+
# this will yield a word address, which we multiply by 2
|
22
|
+
# to get into a byte address
|
23
|
+
str_addr = @memory.force_readw(addr)
|
24
|
+
str_addr *= 2
|
25
|
+
|
26
|
+
ZSCII.translate(translation_alphabet, @header.version, @memory.force_readzstr(str_addr)[1], nil)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
# The Z-Machine has a RISC-like instruction set with 1 byte opcodes with
|
2
|
+
# the exception of several extension opcodes of 2 bytes.
|
3
|
+
#
|
4
|
+
# The operands are variable length, however, mostly for size constraint reasons.
|
5
|
+
#
|
6
|
+
# The format:
|
7
|
+
#
|
8
|
+
# OPCODE :: 1 or 2 bytes
|
9
|
+
# OPCODE_TYPE (opt) :: 1 to 2 bytes, divided into 2-bit fields
|
10
|
+
# OPERANDS :: 0 to 8 given, each 1 or 2 bytes, or an unlimited length string
|
11
|
+
|
12
|
+
require_relative 'opcode'
|
13
|
+
require_relative 'operand_type'
|
14
|
+
require_relative 'opcode_class'
|
15
|
+
require_relative 'instruction'
|
16
|
+
require_relative 'zscii'
|
17
|
+
|
18
|
+
module Gruesome
|
19
|
+
module Z
|
20
|
+
|
21
|
+
# This is the instruction decoder
|
22
|
+
class Decoder
|
23
|
+
def initialize(memory)
|
24
|
+
@memory = memory
|
25
|
+
@instruction_cache = {}
|
26
|
+
@header = Header.new(@memory.contents)
|
27
|
+
@abbreviation_table = AbbreviationTable.new(@memory)
|
28
|
+
|
29
|
+
# For versions 1 and 2, there is a permanent alphabet
|
30
|
+
@alphabet = 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch
|
34
|
+
pc = @memory.program_counter
|
35
|
+
orig_pc = pc
|
36
|
+
|
37
|
+
# Determine type and form of the operand
|
38
|
+
# along with the number of operands
|
39
|
+
|
40
|
+
# read first byte to get opcode
|
41
|
+
opcode = @memory.force_readb(pc)
|
42
|
+
pc = pc + 1
|
43
|
+
|
44
|
+
# opcode form is top 2 bits
|
45
|
+
opcode_form = (opcode >> 6) & 3
|
46
|
+
operand_count = 0
|
47
|
+
operand_types = Array.new(8) { OperandType::OMITTED }
|
48
|
+
operand_values = []
|
49
|
+
|
50
|
+
# SHORT
|
51
|
+
if opcode_form == 2
|
52
|
+
# operand count is determined by bits 4 and 5
|
53
|
+
if ((opcode >> 4) & 3) == 3
|
54
|
+
operand_count = 0
|
55
|
+
opcode_class = OpcodeClass::OP0
|
56
|
+
else
|
57
|
+
operand_count = 1
|
58
|
+
opcode_class = OpcodeClass::OP1
|
59
|
+
end
|
60
|
+
|
61
|
+
operand_types[0] = (opcode >> 4) & 3
|
62
|
+
|
63
|
+
# opcode is given as bottom 4 bits
|
64
|
+
opcode = opcode & 0b1111
|
65
|
+
|
66
|
+
# VARIABLE
|
67
|
+
elsif opcode_form == 3
|
68
|
+
if (opcode & 0b100000) == 0
|
69
|
+
# when bit 5 is clear, there are two operands
|
70
|
+
operand_count = 2
|
71
|
+
opcode_class = OpcodeClass::OP2
|
72
|
+
else
|
73
|
+
# otherwise, there are VAR number
|
74
|
+
operand_count = 8
|
75
|
+
opcode_class = OpcodeClass::VAR
|
76
|
+
end
|
77
|
+
|
78
|
+
# opcode is given as bottom 5 bits
|
79
|
+
opcode = opcode & 0b11111
|
80
|
+
|
81
|
+
# EXTENDED
|
82
|
+
elsif opcode == 190 # extended form
|
83
|
+
opcode_class = OpcodeClass::EXT
|
84
|
+
|
85
|
+
# VAR number
|
86
|
+
operand_count = 8
|
87
|
+
|
88
|
+
# opcode is given as the next byte
|
89
|
+
opcode = @memory.force_readb(pc)
|
90
|
+
pc = pc + 1
|
91
|
+
|
92
|
+
# LONG
|
93
|
+
else
|
94
|
+
|
95
|
+
# there are always 2 operands
|
96
|
+
operand_count = 2
|
97
|
+
opcode_class = OpcodeClass::OP2
|
98
|
+
|
99
|
+
# bit 6 of opcode is type of operand 1
|
100
|
+
type = opcode & 0b1000000
|
101
|
+
if type == 0 # 0 means small constant
|
102
|
+
type = OperandType::SMALL
|
103
|
+
else # 1 means variable
|
104
|
+
type = OperandType::VARIABLE
|
105
|
+
end
|
106
|
+
operand_types[0] = type
|
107
|
+
|
108
|
+
# bit 5 of opcode is type of operand 2
|
109
|
+
type = opcode & 0b100000
|
110
|
+
if type == 0 # 0 means small constant
|
111
|
+
type = OperandType::SMALL
|
112
|
+
else # 1 means variable
|
113
|
+
type = OperandType::VARIABLE
|
114
|
+
end
|
115
|
+
operand_types[1] = type
|
116
|
+
|
117
|
+
# opcode is given as bottom 5 bits
|
118
|
+
opcode = opcode & 0b11111
|
119
|
+
end
|
120
|
+
|
121
|
+
# We need the opcode and opcode_class to be combined
|
122
|
+
opcode = (opcode << 3) | opcode_class
|
123
|
+
|
124
|
+
# convert some moved opcodes
|
125
|
+
if (@header.version <= 4)
|
126
|
+
if opcode == Opcode::CALL_1N
|
127
|
+
opcode = Opcode::NOT
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# handle VAR operands
|
132
|
+
if opcode_form == 3 or opcode_class == OpcodeClass::VAR or opcode_class == OpcodeClass::EXT
|
133
|
+
# each type for the operands is given by reading
|
134
|
+
# the next 1 or 2 bytes.
|
135
|
+
#
|
136
|
+
# This byte contains 4 type descriptions where
|
137
|
+
# the most significant 2 bits are the 0th type
|
138
|
+
# and the least 2 are the 3rd type
|
139
|
+
#
|
140
|
+
# If a type is deemed omitted, every subsequent
|
141
|
+
# type must also be omitted
|
142
|
+
|
143
|
+
byte = @memory.force_readb(pc)
|
144
|
+
pc = pc + 1
|
145
|
+
|
146
|
+
operand_types[0] = (byte >> 6) & 3
|
147
|
+
operand_types[1] = (byte >> 4) & 3
|
148
|
+
operand_types[2] = (byte >> 2) & 3
|
149
|
+
operand_types[3] = byte & 3
|
150
|
+
|
151
|
+
# Get the number of operands
|
152
|
+
idx = -1
|
153
|
+
first_omitted = -1
|
154
|
+
operand_count = operand_types.inject(0) do |result, element|
|
155
|
+
idx = idx + 1
|
156
|
+
if element == OperandType::OMITTED
|
157
|
+
first_omitted = idx
|
158
|
+
result
|
159
|
+
elsif first_omitted == -1
|
160
|
+
result + 1
|
161
|
+
else
|
162
|
+
# Error, OMITTED was found, but another type
|
163
|
+
# was defined as not omitted
|
164
|
+
# We will ignore
|
165
|
+
result
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
if opcode == Opcode::CALL_VS2 or opcode == Opcode::CALL_VN2
|
170
|
+
# Certain opcodes can have up to 8 operands!
|
171
|
+
# These are given by a second byte
|
172
|
+
byte = @memory.force_readb(pc)
|
173
|
+
pc = pc + 1
|
174
|
+
|
175
|
+
operand_types[4] = (byte >> 6) & 3
|
176
|
+
operand_types[5] = (byte >> 4) & 3
|
177
|
+
operand_types[6] = (byte >> 2) & 3
|
178
|
+
operand_types[7] = byte & 3
|
179
|
+
|
180
|
+
# update operand_count once more
|
181
|
+
operand_count = operand_types.inject(operand_count) do |result, element|
|
182
|
+
idx = idx + 1
|
183
|
+
if element == OperandType::OMITTED
|
184
|
+
first_omitted = idx
|
185
|
+
result
|
186
|
+
elsif first_omitted == -1
|
187
|
+
result + 1
|
188
|
+
else
|
189
|
+
# Error, OMITTED was found, but another type
|
190
|
+
# was defined as not omitted
|
191
|
+
# We will ignore
|
192
|
+
result
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Retrieve the operand values
|
199
|
+
operand_types = operand_types.slice(0, operand_count)
|
200
|
+
operand_types.each do |i|
|
201
|
+
if i == OperandType::SMALL or i == OperandType::VARIABLE
|
202
|
+
operand_values << @memory.force_readb(pc)
|
203
|
+
pc = pc + 1
|
204
|
+
elsif i == OperandType::LARGE
|
205
|
+
operand_values << @memory.force_readw(pc)
|
206
|
+
pc = pc + 2
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# If the opcode stores, we need to pull the next byte to get the
|
211
|
+
# destination of the result
|
212
|
+
destination = nil
|
213
|
+
if Opcode.is_store?(opcode, @header.version)
|
214
|
+
destination = @memory.force_readb(pc)
|
215
|
+
pc = pc + 1
|
216
|
+
end
|
217
|
+
|
218
|
+
# The opcode may indicate that it's argument is a string literal
|
219
|
+
if Opcode.has_string?(opcode, @header.version)
|
220
|
+
str = ""
|
221
|
+
# Now we read in 2-byte words until the most significant bit is set
|
222
|
+
# We unencode them from the ZSCII encoding
|
223
|
+
|
224
|
+
continue = true
|
225
|
+
|
226
|
+
alphabet = 0
|
227
|
+
if (@header.version < 3)
|
228
|
+
alphabet = @alphabet
|
229
|
+
end
|
230
|
+
|
231
|
+
result = @memory.force_readzstr(pc)
|
232
|
+
pc = pc + result[0]
|
233
|
+
chrs = result[1]
|
234
|
+
|
235
|
+
# convert the string from ZSCII to UTF8
|
236
|
+
operand_types << OperandType::STRING
|
237
|
+
operand_values << ZSCII.translate(@alphabet, @header.version, chrs, @abbreviation_table)
|
238
|
+
|
239
|
+
# determine shift locks
|
240
|
+
if (@header.version < 3)
|
241
|
+
@alphabet = ZSCII.eval_alphabet(@alphabet, @header.version, chrs, @abbreviation_table)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# If the opcode is a branch, we need to pull the offset info
|
246
|
+
branch_destination = nil
|
247
|
+
branch_condition = false
|
248
|
+
if Opcode.is_branch?(opcode, @header.version)
|
249
|
+
branch_offset = @memory.force_readb(pc)
|
250
|
+
pc = pc + 1
|
251
|
+
|
252
|
+
# if bit 7 is set, the branch occurs on a true condition
|
253
|
+
# false otherwise
|
254
|
+
if (branch_offset & 0b10000000) != 0
|
255
|
+
branch_condition = true
|
256
|
+
end
|
257
|
+
|
258
|
+
# if bit 6 is clear, the branch offset is 14 bits (6 from first byte
|
259
|
+
# and the 8 from the next byte) This is _signed_
|
260
|
+
if (branch_offset & 0b01000000) == 0
|
261
|
+
branch_offset = branch_offset & 0b111111
|
262
|
+
negative = (branch_offset & 0b100000) > 0
|
263
|
+
branch_offset = branch_offset << 8
|
264
|
+
branch_offset = branch_offset | @memory.force_readb(pc)
|
265
|
+
if (negative)
|
266
|
+
branch_offset = -(16384 - branch_offset)
|
267
|
+
end
|
268
|
+
pc = pc + 1
|
269
|
+
else # otherwise, the offset is simply the remaining 6 bits _unsigned_
|
270
|
+
branch_offset = branch_offset & 0b111111
|
271
|
+
end
|
272
|
+
|
273
|
+
# calculate actual destination from the offset
|
274
|
+
if branch_offset == 0 or branch_offset == 1
|
275
|
+
# a return
|
276
|
+
branch_destination = branch_offset
|
277
|
+
else
|
278
|
+
branch_destination = pc + branch_offset - 2
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Create an Instruction class to hold this metadata
|
283
|
+
inst = Instruction.new(opcode, operand_types, operand_values, destination, branch_destination, branch_condition, pc - orig_pc)
|
284
|
+
|
285
|
+
# Store in the instruction cache
|
286
|
+
@instruction_cache[orig_pc] = inst
|
287
|
+
|
288
|
+
inst
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# The Z-Machine has a dictionary which it uses to parse input and
|
2
|
+
# perform lexical analysis.
|
3
|
+
|
4
|
+
# The header is located at the address given in the Header at
|
5
|
+
# address 0x08
|
6
|
+
|
7
|
+
require_relative 'memory'
|
8
|
+
require_relative 'header'
|
9
|
+
require_relative 'zscii'
|
10
|
+
|
11
|
+
module Gruesome
|
12
|
+
module Z
|
13
|
+
class Dictionary
|
14
|
+
def initialize(memory)
|
15
|
+
@memory = memory
|
16
|
+
@header = Header.new(@memory.contents)
|
17
|
+
|
18
|
+
@abbreviation_table = AbbreviationTable.new(@memory)
|
19
|
+
|
20
|
+
@lookup_cache = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def address
|
24
|
+
@header.dictionary_addr
|
25
|
+
end
|
26
|
+
|
27
|
+
def address_of_entries
|
28
|
+
addr = self.address
|
29
|
+
num_input_codes = @memory.force_readb(addr)
|
30
|
+
addr += 1
|
31
|
+
addr += num_input_codes
|
32
|
+
addr += 3
|
33
|
+
|
34
|
+
addr
|
35
|
+
end
|
36
|
+
|
37
|
+
def entry_length
|
38
|
+
addr = self.address_of_entries - 3
|
39
|
+
return @memory.force_readb(addr)
|
40
|
+
end
|
41
|
+
|
42
|
+
def number_of_words
|
43
|
+
addr = self.address_of_entries - 2
|
44
|
+
return @memory.force_readw(addr)
|
45
|
+
end
|
46
|
+
|
47
|
+
def word_address(index)
|
48
|
+
addr = address_of_entries
|
49
|
+
addr += entry_length * index
|
50
|
+
|
51
|
+
return addr
|
52
|
+
end
|
53
|
+
|
54
|
+
def word(index)
|
55
|
+
addr = word_address(index)
|
56
|
+
|
57
|
+
codes = nil
|
58
|
+
|
59
|
+
if @header.version <= 3
|
60
|
+
codes = @memory.force_readzstr(addr, 4)[1]
|
61
|
+
else
|
62
|
+
codes = @memory.force_readzstr(addr, 6)[1]
|
63
|
+
end
|
64
|
+
|
65
|
+
str = ZSCII.translate(0, @header.version, codes, @abbreviation_table)
|
66
|
+
@lookup_cache[str] = index
|
67
|
+
return str
|
68
|
+
end
|
69
|
+
|
70
|
+
def lookup_word(token)
|
71
|
+
if token.length > 6 and @header.version <= 3
|
72
|
+
token = token[0..5]
|
73
|
+
elsif token.length > 9
|
74
|
+
token = token[0..8]
|
75
|
+
end
|
76
|
+
cached_index = @lookup_cache[token]
|
77
|
+
if cached_index != nil
|
78
|
+
if self.word(cached_index) == token
|
79
|
+
return cached_index
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
number_of_words.times do |i|
|
84
|
+
if word(i) == token
|
85
|
+
cached_index = i
|
86
|
+
break
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
cached_index
|
91
|
+
end
|
92
|
+
|
93
|
+
def word_separators
|
94
|
+
addr = self.address
|
95
|
+
|
96
|
+
# the number of word separators
|
97
|
+
num_input_codes = @memory.force_readb(addr)
|
98
|
+
addr += 1
|
99
|
+
|
100
|
+
codes = []
|
101
|
+
num_input_codes.times do
|
102
|
+
codes << @memory.force_readb(addr)
|
103
|
+
addr += 1
|
104
|
+
end
|
105
|
+
|
106
|
+
codes = codes.map do |i|
|
107
|
+
ZSCII.translate_Zchar(i)
|
108
|
+
end
|
109
|
+
|
110
|
+
return codes
|
111
|
+
end
|
112
|
+
|
113
|
+
def tokenize(line)
|
114
|
+
orig_line = line
|
115
|
+
|
116
|
+
# I. ensure that word separators are surrounded by spaces
|
117
|
+
self.word_separators.each do |sep|
|
118
|
+
line = line.gsub(sep, " " + sep + " ")
|
119
|
+
end
|
120
|
+
|
121
|
+
# II. break up the line into words delimited by spaces
|
122
|
+
tokens = line.split(' ')
|
123
|
+
|
124
|
+
line = orig_line
|
125
|
+
last_pos = 0
|
126
|
+
ret = tokens.map do |token|
|
127
|
+
index = line.index(token)
|
128
|
+
line = line[index+token.size..-1]
|
129
|
+
|
130
|
+
index = index + last_pos
|
131
|
+
last_pos = index + token.size
|
132
|
+
|
133
|
+
index
|
134
|
+
{ :position => index, :string => token }
|
135
|
+
end
|
136
|
+
|
137
|
+
ret
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse(tokens)
|
141
|
+
ret = []
|
142
|
+
tokens.each do |token|
|
143
|
+
index = lookup_word(token[:string])
|
144
|
+
if index != nil
|
145
|
+
addr = word_address(index)
|
146
|
+
else
|
147
|
+
addr = 0
|
148
|
+
end
|
149
|
+
ret << { :size => token[:string].size, :address => addr, :position => token[:position] }
|
150
|
+
end
|
151
|
+
|
152
|
+
ret
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|