sabrina 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,112 @@
1
+ module Sabrina
2
+ class Bytestream
3
+ # Methods that allow {Bytestream} to output the raw byte data
4
+ # or export it to various formats. Subclasses should override {#to_bytes}
5
+ # to allow generation from an internal representation if present,
6
+ # and add to the list of output formats if necessary.
7
+ module ByteOutput
8
+ # Outputs a raw byte string. ROM and either table and index (recommended)
9
+ # or offset should have been specified, otherwise the return string will
10
+ # be empty. This method is relied on for write and save operations.
11
+ #
12
+ # This method uses an internal cache. The cache should be wiped
13
+ # automatically on changes to ROM or internal data, otherwise it can
14
+ # be wiped manually with {RomOperations#clear_cache}.
15
+ #
16
+ # Subclasses should define {#generate_bytes} to create the bytestream
17
+ # from the internal representation instead and only read from the
18
+ # ROM if the internal representation is absent.
19
+ #
20
+ # @return [String]
21
+ def to_bytes
22
+ return @bytes_cache if @bytes_cache
23
+ return @bytes_cache = generate_bytes if @representation
24
+
25
+ l_offset = offset
26
+ if @rom && offset
27
+ if @lz77
28
+ data = @rom.read_lz77(l_offset)
29
+ @length_cache = data[:original_length]
30
+ @lz77_cache = data[:original_stream]
31
+ return @bytes_cache = data[:stream]
32
+ end
33
+ return @bytes_cache = @rom.read_string(l_offset) if @is_gba_string
34
+ return @bytes_cache = @rom.read(l_offset, @length) if @length
35
+ end
36
+
37
+ return ''
38
+ end
39
+
40
+ # Subclasses should override this to return a string of bytes generated
41
+ # from the representation.
42
+ def generate_bytes
43
+ @bytes_cache = present
44
+ end
45
+
46
+ # Subclasses should override this to update the representation from
47
+ # byte data.
48
+ def present
49
+ @representation ||= to_bytes
50
+ end
51
+
52
+ # Same as {#to_bytes}.
53
+ def to_b
54
+ to_bytes
55
+ end
56
+
57
+ # Returns a hexadecimal representation of the byte data, optionally
58
+ # grouping it into quartets if passed +true+ as a parameter.
59
+ #
60
+ # @return [String]
61
+ def to_hex(pretty = false)
62
+ h = to_bytes.each_byte.to_a.map { |x| format('%02x', x) }.join('')
63
+ return h unless pretty
64
+
65
+ h.scan(/......../).map { |x| x.scan(/../).join(':') }.join(' ')
66
+ end
67
+
68
+ # Same as {#to_hex}, but reverses the bytes before conversion.
69
+ #
70
+ # @return [String]
71
+ def to_hex_reverse
72
+ to_bytes.each_byte.to_a.map { |x| format('%02x', x) }.reverse.join('')
73
+ end
74
+
75
+ # Returns the byte data converted to a base-16 integer.
76
+ # @return [Integer]
77
+ def to_i
78
+ to_hex.hex
79
+ end
80
+
81
+ # Outputs the byte data as a GBA-compatible {Lz77}-compressed
82
+ # stream, raising an error when the data is empty.
83
+ # {RomOperations#write_to_rom} relies on this
84
+ # method when :lz77 is set to true.
85
+ #
86
+ # This method uses an internal cache. The cache should be wiped
87
+ # automatically on changes to ROM or internal data, otherwise it can
88
+ # be wiped manually with {RomOperations#clear_cache}.
89
+ #
90
+ # Subclasses should clear the internal cache by calling
91
+ # {RomOperations#clear_cache} whenever the internal
92
+ # representation has changed.
93
+ # @return [String]
94
+ def to_lz77
95
+ return @lz77_cache if @lz77_cache
96
+ b = to_bytes
97
+ fail 'Cannot compress empty data.' if b.empty?
98
+ @lz77_cache = Lz77.compress(b)
99
+ end
100
+
101
+ # Returns the output of {#to_hex}.
102
+ #
103
+ # Subclasses should override this to provide a concise textual
104
+ # representation of the internal data.
105
+ #
106
+ # @return [String]
107
+ def to_s
108
+ "#{to_hex(true)}"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,138 @@
1
+ module Sabrina
2
+ class Bytestream
3
+ # Methods related to writing and retrieving various ROM data.
4
+ module RomOperations
5
+ # @todo Return key +:original_length+ for {Lz77.uncompress} is currently
6
+ # unreliable. Wipe and overwrite in place for table data is disabled.
7
+ #
8
+ # Writes the byte data to the ROM, trying to use first the
9
+ # table index and then the offset (and failing if neither is
10
+ # known).
11
+ #
12
+ # If the table index is known, the data will be written to an
13
+ # available free space on the ROM and the table will be updated.
14
+ #
15
+ # If the table data is unknown, the data will be written in place
16
+ # at the offset.
17
+ #
18
+ # This method will call {#reload_from_rom} and update and return
19
+ # the {#last_write} array.
20
+ #
21
+ # @return [Array] see {#last_write}.
22
+ def write_to_rom
23
+ old_length = calculate_length
24
+ b = (@lz77 ? to_lz77 : to_bytes)
25
+ new_length = b.length
26
+ old_offset = offset
27
+
28
+ unless @rom && old_offset
29
+ fail 'Rom and offset or table/index must be set before writing.'
30
+ end
31
+ fail 'Byte string is empty. Aborting.' if b.empty?
32
+ fail "Byte string too long #{new_length}. Aborting." if new_length > 10_000
33
+
34
+ @last_write = []
35
+
36
+ new_offset =
37
+ if !@pointer_mode || !(@table && @index)
38
+ @last_write << 'Bytestream#repoint: Overwriting in place.' \
39
+ " (#{old_offset})"
40
+ old_offset
41
+ # Wipe and overwrite disabled due to
42
+ # Lz77.uncompress[:original_length] inaccuracy.
43
+ #
44
+ # elsif new_length <= old_length
45
+ # @last_write << 'Repoint: New length less than or equal to old' \
46
+ # 'length, overwriting in place.'
47
+ # @last_write << @rom.wipe(old_offset, old_length, true)
48
+ # old_offset
49
+ else
50
+ o = repoint
51
+ @last_write << 'Bytestream#repoint: Wiping disabled in this' \
52
+ "version. Leaving #{old_length} bytes at #{old_offset}" \
53
+ " (#{ format('%06X', old_offset) }) in #{@rom} intact."
54
+ @last_write << "Bytestream#repoint: Repointed to #{o}" \
55
+ " (#{ format('%06X', o) })."
56
+ # @last_write << @rom.wipe(old_offset, old_length)
57
+ o
58
+ end
59
+
60
+ @last_write << @rom.write(new_offset, b)
61
+ clear_cache(lz77: false)
62
+
63
+ @last_write
64
+ end
65
+
66
+ # Wipes the internal cache AND the representation so that the data
67
+ # may be synced from ROM if present.
68
+ #
69
+ # @return [self]
70
+ def reload_from_rom
71
+ @representation = nil
72
+ clear_cache
73
+ present
74
+ self
75
+ end
76
+
77
+ # Wipes the internal cache so that the data may be reloaded from ROM if
78
+ # present.
79
+ #
80
+ # Subclasses should call this whenever the internal representation of
81
+ # data is changed, and define {ByteOutput#generate_bytes} to
82
+ # regenerate the byte data from the internal representation.
83
+ #
84
+ # @param [Hash] h
85
+ # @option h [Boolean] :lz77 whether to clear the internal representation
86
+ # as well (defaults to +true+).
87
+ # @return [self]
88
+ def clear_cache(h = { lz77: true })
89
+ @lz77_cache = nil if h.fetch(:lz77, true)
90
+ @length_cache = nil if h.fetch(:lz77, true) && !@lz77
91
+ @bytes_cache = nil
92
+ self
93
+ end
94
+
95
+ # @todo Return key +:original_length+ for {Lz77.uncompress} is currently
96
+ # unreliable. Wipe and overwrite in place for table data is disabled.
97
+ #
98
+ # Returns the length of the {Lz77} data stored on the ROM if +:lz77+
99
+ # mode is on and a table of offset is provided. Otherwise, returns
100
+ # the length of the byte data.
101
+ #
102
+ # @return [Integer]
103
+ def calculate_length
104
+ return @length_cache if @length_cache
105
+ if @lz77 && offset
106
+ return @length_cache = @rom.read_lz77(offset)[:original_length]
107
+ end
108
+ to_bytes.length
109
+ end
110
+
111
+ private
112
+
113
+ # Assigns the data to a suitable free space on the ROM and updates
114
+ # the table accordingly.
115
+ #
116
+ # This will clear the internal cache.
117
+ #
118
+ # @return [Integer] The offset found.
119
+ def repoint
120
+ unless @rom && @table && @index
121
+ fail 'Rom, table and index must be set before repointing.'
122
+ end
123
+
124
+ l = (@lz77 ? to_lz77.length : to_bytes.length)
125
+ f_offset = @rom.find_free(l)
126
+
127
+ unless f_offset
128
+ fail "Bytestream#repoint: Could not find #{l} bytes of free data in" \
129
+ " #{@rom}. Consider using a clean rombase."
130
+ end
131
+
132
+ @rom.write_offset_to_table(@table, @index, f_offset)
133
+ clear_cache
134
+ f_offset
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,60 @@
1
+ module Sabrina
2
+ # Implements +attr_children+, an +attr_accessor+-like macro that allows
3
+ # passing down attribute write calls to an arbitrary array of objects.
4
+ # Obviously, the child objects should support writing to the specified
5
+ # attributes.
6
+ #
7
+ # Classes should override +#children+ to return a meaningful array of
8
+ # child objects.
9
+ module ChildrenManager
10
+ # Adds a macro for attribute with children update.
11
+ module AttrChildren
12
+ private
13
+
14
+ # Takes any number of symbols and creates writers that will also
15
+ # pass the value down to each value in +children+.
16
+ def attr_writer_children(*args)
17
+ args.each do |x|
18
+ x_var = "@#{x}".to_sym
19
+ x_setter = "#{x}=".to_sym
20
+
21
+ lamb = generate_writer_children(x_setter, x_var)
22
+
23
+ define_method(x_setter, lamb) unless respond_to?(x_setter)
24
+ end
25
+ end
26
+
27
+ # Takes any number of symbols and creates accessors that will also
28
+ # pass the value down to each value in +children+.
29
+ def attr_children(*args)
30
+ attr_reader(*args)
31
+ attr_writer_children(*args)
32
+ end
33
+
34
+ def generate_writer_children(name, variable)
35
+ lambda do |value|
36
+ children.each do |c|
37
+ next c.method(name).call(value) if c.respond_to?(name)
38
+ fail "Child #{c} has no method #{name}, yet attr_children called."
39
+ end
40
+ instance_variable_set(variable, value)
41
+ end
42
+ end
43
+ end
44
+
45
+ class << self
46
+ # Magic for adding class methods when included.
47
+ def included(mod)
48
+ mod.extend(AttrChildren)
49
+ end
50
+ end
51
+
52
+ # Classes should override this to provide a meaningful array
53
+ # of children.
54
+ #
55
+ # @return [Array]
56
+ def children
57
+ instance_variable_defined?(:@children) ? @children : []
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,112 @@
1
+ module Sabrina
2
+ # This utility module handles the {Sabrina} configuration. You should not
3
+ # need to deal with it directly, except perhaps for ad-hoc runtime config
4
+ # loading.
5
+ #
6
+ # Each key present in the config can also be called as a module method,
7
+ # for example: +Sabrina::Config.rom_defaults+.
8
+ #
9
+ # Upon the first load of the library, a directory called +.sabrina+ should
10
+ # have been created in your home directory (a level above +My+ +Documents+ in
11
+ # Windows XP, or your Cygwin home if you are running Cygwin). This directory
12
+ # should contain a +sample.json+ config file.
13
+ #
14
+ # Any +.json+ files in the directory except +sample.json+ will be
15
+ # automatically loaded and merged into the default config. Any config file
16
+ # may contain only some of the necessary keys as long as the key hierarchy
17
+ # tree is preserved.
18
+ #
19
+ # The most obvious use of the above would likely be to supply the library with
20
+ # easily distributable config files for specific ROMs.
21
+ #
22
+ # Keep in mind that user config files will not be auto-updated on library
23
+ # updates. New versions might break support for old features or add new ones.
24
+ # If you run into errors, try moving the files away from +.sabrina+ and let
25
+ # the library generate a new sample config, then look at it to see what has
26
+ # changed.
27
+ module Config
28
+ # The user config directory.
29
+ USER_CONFIG_DIR = Dir.home + '/.sabrina/'
30
+
31
+ # A sample config file to create. This will not actually be loaded and
32
+ # should not be modified.
33
+ USER_CONFIG_SAMPLE = 'sample.json'
34
+
35
+ class << self
36
+ # A simple function for recursive merging of nested hashes.
37
+ #
38
+ # @param [Hash] h1
39
+ # @param [Hash] h2
40
+ # @return [Hash]
41
+ def deep_merge(h1, h2)
42
+ h1.merge(h2) do |_key, x, y|
43
+ x.is_a?(Hash) && y.is_a?(Hash) ? deep_merge(x, y) : y
44
+ end
45
+ end
46
+
47
+ # Loads a hash into the internal config.
48
+ #
49
+ # @param [Hash] h the option hash to be merged.
50
+ # @return [0]
51
+ def load(h)
52
+ @sabrina_config ||= {}
53
+ @sabrina_config = deep_merge(@sabrina_config, h)
54
+
55
+ @sabrina_config.each_key do |key|
56
+ m = key.downcase.to_sym
57
+ next if respond_to?(m)
58
+ define_singleton_method(m) { @sabrina_config[key] }
59
+ end
60
+
61
+ 0
62
+ end
63
+
64
+ # Creates {USER_CONFIG_DIR}, and {USER_CONFIG_SAMPLE} if the directory
65
+ # is empty.
66
+ #
67
+ # @return [0]
68
+ def create_user_config
69
+ FileUtils.mkpath(USER_CONFIG_DIR) unless Dir.exist?(USER_CONFIG_DIR)
70
+
71
+ c = @sabrina_config
72
+ c.delete_if { |key, _val| key.to_s.start_with?('charmap') }
73
+
74
+ f = File.new(USER_CONFIG_DIR + USER_CONFIG_SAMPLE, 'w+b')
75
+ f.write(JSON.pretty_generate(c), symbolize_names: true)
76
+ f.close
77
+ 0
78
+ end
79
+
80
+ # Loads all .json files from {USER_CONFIG_DIR}, exempting
81
+ # {USER_CONFIG_SAMPLE}.
82
+ #
83
+ # @return [0]
84
+ def load_user_config
85
+ files = Dir[USER_CONFIG_DIR + '*.json']
86
+
87
+ # create_user_config if files.empty?
88
+
89
+ files.each do |x|
90
+ next if x.end_with?(USER_CONFIG_SAMPLE)
91
+ load(JSON.parse(File.read(x)))
92
+ end
93
+ 0
94
+ end
95
+
96
+ # Returns a hash of all config keys for a ROM type identified by the
97
+ # 4-byte +id+.
98
+ #
99
+ # @param [String] id
100
+ # @return [Hash]
101
+ def rom_params(id)
102
+ defs = @sabrina_config[:rom_defaults]
103
+
104
+ params = @sabrina_config[:rom_data].fetch(id.to_sym) do
105
+ fail "Unsupported ROM type: \'#{id}\'."
106
+ end
107
+
108
+ defs.merge(params)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,81 @@
1
+ Sabrina::Config.load(
2
+ charmap_in: {
3
+ ' ' => '00',
4
+ '<' => '85',
5
+ '>' => '86',
6
+ '0' => 'A1',
7
+ '1' => 'A2',
8
+ '2' => 'A3',
9
+ '3' => 'A4',
10
+ '4' => 'A5',
11
+ '5' => 'A6',
12
+ '6' => 'A7',
13
+ '7' => 'A8',
14
+ '8' => 'A9',
15
+ '9' => 'AA',
16
+ '!' => 'AB',
17
+ '?' => 'B6',
18
+ '.' => 'AD',
19
+ '-' => 'AE',
20
+ "\'" => 'B4',
21
+ '♂' => 'B5',
22
+ '♀' => 'B6',
23
+ '$' => 'B7',
24
+ ',' => 'B8',
25
+ '*' => 'B9',
26
+ '/' => 'BA',
27
+ 'A' => 'BB',
28
+ 'B' => 'BC',
29
+ 'C' => 'BD',
30
+ 'D' => 'BE',
31
+ 'E' => 'BF',
32
+ 'F' => 'C0',
33
+ 'G' => 'C1',
34
+ 'H' => 'C2',
35
+ 'I' => 'C3',
36
+ 'J' => 'C4',
37
+ 'K' => 'C5',
38
+ 'L' => 'C6',
39
+ 'M' => 'C7',
40
+ 'N' => 'C8',
41
+ 'O' => 'C9',
42
+ 'P' => 'CA',
43
+ 'Q' => 'CB',
44
+ 'R' => 'CC',
45
+ 'S' => 'CD',
46
+ 'T' => 'CE',
47
+ 'U' => 'CF',
48
+ 'V' => 'D0',
49
+ 'W' => 'D1',
50
+ 'X' => 'D2',
51
+ 'Y' => 'D3',
52
+ 'Z' => 'D4',
53
+ 'a' => 'D5',
54
+ 'b' => 'D6',
55
+ 'c' => 'D7',
56
+ 'd' => 'D8',
57
+ 'e' => 'D9',
58
+ 'f' => 'DA',
59
+ 'g' => 'DB',
60
+ 'h' => 'DC',
61
+ 'i' => 'DD',
62
+ 'j' => 'DE',
63
+ 'k' => 'DF',
64
+ 'l' => 'E0',
65
+ 'm' => 'E1',
66
+ 'n' => 'E2',
67
+ 'o' => 'E3',
68
+ 'p' => 'E4',
69
+ 'q' => 'E5',
70
+ 'r' => 'E6',
71
+ 's' => 'E7',
72
+ 't' => 'E8',
73
+ 'u' => 'E9',
74
+ 'v' => 'EA',
75
+ 'w' => 'EB',
76
+ 'x' => 'EC',
77
+ 'y' => 'ED',
78
+ 'z' => 'EE',
79
+ "\n" => 'FE'
80
+ }
81
+ )