Class: Sabrina::Rom

Inherits:
Object
  • Object
show all
Defined in:
lib/sabrina/rom.rb

Overview

A class for handling low-level read and write operations upon a ROM file. Beyond creating a new Rom from filename for passing to Bytestream-family objects, you should not need to deal with it or any of its methods directly.

Constant Summary collapse

ID_OFFSET =

The position in the ROM file from which to read the 4-byte identifier string. This is relied upon to pull the ROM type data from the Config.

172

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rom_file) ⇒ Rom

Creates a new Rom object from the supplied ROM image file.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/sabrina/rom.rb', line 53

def initialize(rom_file)
  @path = rom_file
  @file = File.new(rom_file, 'r+b')

  @filename = @path.rpartition('/').last.rpartition('.').first
  @id = load_id

  @params = Config.rom_params(@id)

  @params.each_key do |key|
    m = key.downcase.to_sym
    define_singleton_method(m) { @params[key] } unless respond_to?(m)
  end
end

Instance Attribute Details

#fileFile (readonly)

The ROM file object.

Returns:

  • (File)


31
32
33
# File 'lib/sabrina/rom.rb', line 31

def file
  @file
end

#filenameString (readonly)

Just the filename of the ROM file.

Returns:

  • (String)


26
27
28
# File 'lib/sabrina/rom.rb', line 26

def filename
  @filename
end

#idString (readonly)

The 4-byte ID of the ROM.

Returns:

  • (String)

See Also:



16
17
18
# File 'lib/sabrina/rom.rb', line 16

def id
  @id
end

#pathString (readonly)

The full path and filename of the ROM file.

Returns:

  • (String)


21
22
23
# File 'lib/sabrina/rom.rb', line 21

def path
  @path
end

Class Method Details

.offset_to_pointer(offset) ⇒ Integer

Converts a numerical offset to a GBA-compliant, reverse 3-byte pointer.

Returns:

  • (Integer)


38
39
40
# File 'lib/sabrina/rom.rb', line 38

def offset_to_pointer(offset)
  format('%06X', offset).scan(/../).reverse.map { |x| x.hex.chr }.join('')
end

.pointer_to_offset(pointer) ⇒ Integer

Converts a reverse 3-byte pointer to a numerical offset.

Returns:

  • (Integer)


45
46
47
# File 'lib/sabrina/rom.rb', line 45

def pointer_to_offset(pointer)
  Bytestream.from_bytes(pointer.reverse).to_i
end

Instance Method Details

#close0

Closes the ROM file.

Returns:

  • (0)


281
282
283
284
# File 'lib/sabrina/rom.rb', line 281

def close
  @file.close
  0
end

#find_free(length, start = nil) ⇒ Integer

Returns the position of the first occurence of length 0xFF bytes, assumed to be free space available for writing. If start is nil, the search will begin at the :free_space_start offset specified in the ROM Config.

Parameters:

  • length (Integer)

Returns:

  • (Integer)

    the first found offset.



172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/sabrina/rom.rb', line 172

def find_free(length, start = nil)
  query = ("\xFF" * length).force_encoding('ASCII-8BIT')
  start ||= Bytestream.parse_offset(free_space_start)

  @file.seek(start)
  match = start + @file.read.index(query)

  return match if match % 4 == 0 || !match

  match += 1 until match % 4 == 0
  find_free(length, match)
end

#monster_name(real_index) ⇒ String

Gets the name of the monster identified by real_index from the ROM file.

Parameters:

  • real_index (Integer)

Returns:

  • (String)

See Also:



92
93
94
# File 'lib/sabrina/rom.rb', line 92

def monster_name(real_index)
  read_string_from_table(:name_table, real_index, name_length)
end

#param(p) ⇒ Object

Fetches a specific key from the ROM Config data.

Parameters:

  • p (String, Symbol)

    the key to look for.

See Also:



81
82
83
84
# File 'lib/sabrina/rom.rb', line 81

def param(p)
  s = p.to_sym
  @params.fetch(s) { fail "No parameter #{s} for ROM type #{@id}." }
end

#read(offset, length = nil) ⇒ String

Reads length bytes from offset, or the entire rest of the file if length is not specified.

Parameters:

  • offset (Integer)
  • length (Integer) (defaults to: nil)

Returns:

  • (String)


213
214
215
216
# File 'lib/sabrina/rom.rb', line 213

def read(offset, length = nil)
  @file.seek(offset)
  length ? @file.read(length) : @file.read
end

#read_lz77(offset) ⇒ Hash

Reads the data from offset, assuming it to be Lz77-compressed.

Parameters:

  • offset (Integer)

Returns:

  • (Hash)

    contains the uncompressed data as :stream and the estimated original compressed length as :original_length.

See Also:



191
192
193
# File 'lib/sabrina/rom.rb', line 191

def read_lz77(offset)
  Lz77.uncompress(self, offset)
end

#read_offset_from_table(name, index) ⇒ Integer

Reads a numerical offset associated with index from the table name. This assumes the table contains 3-byte GBA pointers.

Parameters:

  • name (String, Symbol)

    the name of the table as specified in the ROM Config data.

  • index (Integer)

    in the case of a monster, the real index of the monster.

Returns:

  • (Integer)

See Also:



121
122
123
124
# File 'lib/sabrina/rom.rb', line 121

def read_offset_from_table(name, index)
  pointer = read_table(name, index, 8, 3)
  self.class.pointer_to_offset(pointer)
end

#read_string(offset) ⇒ String

Reads a stream expected to be an 0xFF-terminated GBA string from offset.

Parameters:

  • offset (Integer)

Returns:

  • (String)

See Also:



200
201
202
203
204
205
# File 'lib/sabrina/rom.rb', line 200

def read_string(offset)
  term = "\xFF".force_encoding('ASCII-8BIT')

  @file.seek(offset)
  @file.gets(term)
end

#read_string_from_table(name, index, index_length = nil) ⇒ String

Reads a stream expected to be an 0xFF-terminated GBA string from the table. This assumes the entries before index are each index_length long.

Parameters:

  • name (String, Symbol)

    the name of the table as specified in the ROM Config data.

  • index (Integer)

    in the case of a monster, the real index of the monster.

  • index_length (Integer) (defaults to: nil)

    The number of bytes occupied by each index in table. If absent, will search Config for a _length param associated with the table.

Returns:

  • (String)

See Also:



139
140
141
142
143
# File 'lib/sabrina/rom.rb', line 139

def read_string_from_table(name, index, index_length = nil)
  index_length ||= param(name.to_s.sub('_table', '_length'))
  s = read_string(table(name) + index * index_length)
  GBAString.from_bytes(s).to_s
end

#read_table(name, index, index_length = nil, length = nil) ⇒ String

Reads length bytes of data from the table. This assumes the entries before index are each index_length long.

Parameters:

  • name (String, Symbol)

    the name of the table as specified in the ROM Config data.

  • index (Integer)

    in the case of a monster, the real index of the monster.

  • index_length (Integer) (defaults to: nil)

    The number of bytes occupied by each index in table. If absent, will search Config for a _length param associated with the table.

  • length (Integer) (defaults to: nil)

    how many bytes to read. Will assume index_length if absent.

Returns:

  • (String)

See Also:



159
160
161
162
163
# File 'lib/sabrina/rom.rb', line 159

def read_table(name, index, index_length = nil, length = nil)
  index_length ||= param(name.to_s.sub('_table', '_length'))
  length ||= index_length
  read(table(name) + index * index_length, length)
end

#table(name) ⇒ Integer

Returns the numerical offset associated with the name in the current ROM Config data.

Parameters:

  • name (String, Symbol)

Returns:

  • (Integer)


73
74
75
# File 'lib/sabrina/rom.rb', line 73

def table(name)
  Bytestream.parse_offset(param(name))
end

#table_to_offset(name, index, index_length = nil) ⇒ String

Takes a table name and index and returns a byte offset.

Parameters:

  • name (String, Symbol)

    the name of the table as specified in the ROM Config data.

  • index (Integer)

    in the case of a monster, the real index of the monster.

  • index_length (Integer) (defaults to: nil)

    The number of bytes occupied by each index in table. If absent, will search Config for a _length param associated with the table.

Returns:

  • (String)


106
107
108
109
110
# File 'lib/sabrina/rom.rb', line 106

def table_to_offset(name, index, index_length = nil)
  index_length ||= param(name.to_s.sub('_table', '_length'))

  table(name) + index * index_length
end

#to_sString

Returns a blurb consisting of the ROM title and ID.

Returns:

  • (String)


289
290
291
# File 'lib/sabrina/rom.rb', line 289

def to_s
  "#{ param(:title) } [#{@id}]"
end

#wipe(offset, length, force = false) ⇒ String

Writes a stream of length 0xFF bytes at the provided offset. This ought to be recognized as free space available for writing. Unless force is set to true, the method will do nothing if there may be multiple pointers referencing the given offset within the ROM file.

Parameters:

  • offset (Integer)
  • length (Integer)
  • force (Boolean) (defaults to: false)

    whether to force wiping even if the offset appears to be pointed at multiple times.

Returns:

  • (String)

    a debug message.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/sabrina/rom.rb', line 259

def wipe(offset, length, force = false)
  unless force
    pointer = self.class.offset_to_pointer(offset)

    @file.rewind
    hits = @file.read.scan(pointer).length
    if hits > 1
      return "Rom.wipe: Offset #{offset} (#{ format('%06X', offset) })" \
        " appears to be referenced by multiple pointers (#{hits})," \
        ' not wiping. Use wipe(offset, length, true) to override.'
    end
  end

  write(offset, "\xFF" * length)

  "Rom#wipe: Wiped #{length} bytes at #{offset}" \
    "(#{ format('%06X', offset) })."
end

#write(offset, b) ⇒ String

Writes a stream of bytes at the provided offset.

Parameters:

  • offset (Integer)

Returns:

  • (String)

    a debug message.



222
223
224
225
226
227
228
# File 'lib/sabrina/rom.rb', line 222

def write(offset, b)
  @file.seek(offset)
  @file.write(b.force_encoding('ASCII-8BIT'))

  "Rom#write: Wrote #{b.length} bytes at #{offset}" \
    " (#{ format('%06X', offset) })."
end

#write_offset_to_table(name, index, offset) ⇒ String

Writes the offset associated with index to the table name. This assumes the table contains 3-byte GBA pointers.

Parameters:

  • offset (Integer)

    the offset. It will be converted to a GBA-compliant 3-byte pointer before writing.

  • name (String, Symbol)

    the name of the table as specified in the ROM Config data.

  • index (Integer)

    in the case of a monster, the real index of the monster.

Returns:

  • (String)

    a debug message.

See Also:



241
242
243
244
245
246
# File 'lib/sabrina/rom.rb', line 241

def write_offset_to_table(name, index, offset)
  write(
    table(name) + index * 8,
    self.class.offset_to_pointer(offset)
  )
end