sabrina 0.5.5

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.
@@ -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
+ )