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,33 @@
1
+ module Sabrina
2
+ # Current software version.
3
+ #
4
+ # @return [String]
5
+ VERSION = '0.5.5'
6
+
7
+ # Date of the current version.
8
+ #
9
+ # @return [String]
10
+ DATE = '2014-10-12'
11
+
12
+ # A short description.
13
+ #
14
+ # @return [String]
15
+ ABOUT = 'Hack GBA ROMs of a popular monster collection RPG series.'
16
+
17
+ class << self
18
+ # Gets the PNG handler info.
19
+ def png
20
+ PNG
21
+ end
22
+
23
+ # @see VERSION
24
+ def version
25
+ "#{VERSION}/#{PNG}"
26
+ end
27
+
28
+ # @see ABOUT
29
+ def about
30
+ ABOUT
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,147 @@
1
+ module Sabrina
2
+ # An abstract class for dealing with further abstractions of data related
3
+ # to a specific, numbered monster.
4
+ #
5
+ # See {Sabrina::Plugins} for extensions that might enhance this class
6
+ # with added functionality.
7
+ class Monster
8
+ include ChildrenManager
9
+
10
+ # @!attribute rom
11
+ # The current working ROM file.
12
+ # @return [Rom]
13
+ # @!method rom=(r)
14
+ # The current working ROM file.
15
+ # @param [Rom] r
16
+ # @return [Rom]
17
+ # @!attribute index
18
+ # The real index of the monster.
19
+ # @return [Integer]
20
+ # @!method index=(i)
21
+ # The real index of the monster.
22
+ # @param [Integer] i
23
+ # @return [Integer]
24
+ attr_children :rom, :index
25
+
26
+ # The filename for saving data to a file, sans the path and extension.
27
+ # Refer to {#work_dir=} for setting the path, and child +#save+
28
+ # methods should take care of adding the right extension.
29
+ #
30
+ # @return [String]
31
+ attr_reader :filename
32
+
33
+ # The directory for reading and saving file data.
34
+ # Defaults to the current ROM's file name affixed with +_files+.
35
+ #
36
+ # @return [String]
37
+ # @see #work_dir=
38
+ attr_reader :work_dir
39
+
40
+ # The effective dex number of the monster, depending on any blanks
41
+ # in the ROM file.
42
+ attr_reader :dex_number
43
+
44
+ class << self
45
+ # Calculates the real index by parsing the provided index either
46
+ # as a dex number (when integer) or as a string. A number string
47
+ # prefixed with an exclamation mark +"!"+ will be interpreted as
48
+ # the real index.
49
+ #
50
+ # @param [Integer, String] index either the dex number, or the
51
+ # real index prepended with "!".
52
+ # @param [Rom] rom the rom file to pull the dex data from.
53
+ # @return [Integer] the real index of the monster in the ROM file,
54
+ # after accounting for blank spaces between dex numbers 251 and 252.
55
+ def parse_index(index, rom)
56
+ in_index = index.to_s
57
+ if /[^0-9\!]/ =~ in_index
58
+ fail "\'#{in_index}\' does not look like a valid index."
59
+ end
60
+
61
+ out_index =
62
+ if in_index['!']
63
+ in_index.rpartition('!').last.to_i
64
+ else
65
+ i = in_index.to_i
66
+ i < rom.dex_blank_start ? i : i + rom.dex_blank_length
67
+ end
68
+
69
+ unless out_index < rom.dex_length
70
+ fail "Real index #{out_index} out of bounds;" \
71
+ " #{rom.id} has #{rom.dex_length} monsters."
72
+ end
73
+
74
+ out_index
75
+ end
76
+ end
77
+
78
+ # Generates a new instance of Monster.
79
+ #
80
+ # @param [Rom] rom the ROM to be associated with the monster.
81
+ # @param [Integer] index either the dex number of the monster,
82
+ # or the real (ROM) index prefixed with "!". See {parse_index}
83
+ # for details.
84
+ # @param [String] dir the working directory.
85
+ # @return [Monster]
86
+ def initialize(rom, index, dir = nil)
87
+ @plugins = {}
88
+
89
+ @rom = rom
90
+ @index = self.class.parse_index(index, @rom)
91
+ @dex_number = index
92
+
93
+ @filename = format('%03d', @index)
94
+ @work_dir = dir || @rom.filename.dup << '_files/'
95
+
96
+ load_plugins
97
+ end
98
+
99
+ # @return [Array]
100
+ # @see ChildrenManager
101
+ def children
102
+ @plugins.values
103
+ end
104
+
105
+ # Sets the path for saving data to a file.
106
+ #
107
+ # @param [String] path
108
+ # @return [String]
109
+ def work_dir=(path)
110
+ path << '/' unless path.end_with?('/')
111
+ @work_dir = path
112
+ end
113
+
114
+ alias_method :dir=, :work_dir=
115
+
116
+ # Sets the dex number of the monster, dependent on any blanks in the ROM.
117
+ #
118
+ # @return [Integer]
119
+ # @see parse_index
120
+ def dex_number=(n)
121
+ @dex_number = n
122
+ self.index = self.class.parse_index(n, @rom)
123
+ end
124
+
125
+ # Reads the monster name from the ROM.
126
+ #
127
+ # @return [String]
128
+ def name
129
+ @rom.monster_name(@index)
130
+ end
131
+
132
+ # Closes the ROM file.
133
+ #
134
+ # @return [0]
135
+ def close
136
+ @rom.close
137
+ 0
138
+ end
139
+
140
+ # Prints a blurb consisting of the monster's dex number and name.
141
+ #
142
+ # @return [String]
143
+ def to_s
144
+ "#{@index}. #{name}"
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,216 @@
1
+ module Sabrina
2
+ # A class dedicated to handling color palette data inside a ROM file.
3
+ # This must be used alongside sprites to display the correct colors
4
+ # in game or when exported to files.
5
+ #
6
+ # While a palette will function in this and some other programs even if
7
+ # smaller than 16 colors, it must have exactly 16 colors to work in-game.
8
+ # To ensure this, use the {#pad} method to fill the remaining slots with
9
+ # a default color. This will, however, make it impossible to add further
10
+ # colors.
11
+ #
12
+ # Parts adapted from
13
+ # {https://github.com/thekaratekid552/Secret-Tool Gen III Hacking Suite}
14
+ # by thekaratekid552.
15
+ #
16
+ # @see Sprite#set_palette
17
+ class Palette < Bytestream
18
+ class << self
19
+ # Generates an array of two palettes from two +0xRRGGBB+-format streams:
20
+ # One containing every color from +rgb1+, and another where each color is
21
+ # replaced with its spatial equivalent from +rgb2+. This assumes
22
+ # palette 1 does not contain duplicate entries, but palette 2 might.
23
+ #
24
+ # @param [String] rgb1 a string of +0xRRGGBB+ values.
25
+ # @param [String] rgb2 a string of +0xRRGGBB+ values.
26
+ # @param [Hash] h1 see {Bytestream#initialize}
27
+ # @param [Hash] h2 see {Bytestream#initialize}
28
+ # @return [Array] an array of the two resulting palettes.
29
+ def create_synced_palettes(rgb1, rgb2, h1 = {}, h2 = {})
30
+ unless rgb1.length % 3 == 0 && rgb2.length % 3 == 0
31
+ fail 'RGB stream length must divide by 3.'
32
+ end
33
+
34
+ a1, a2 = rgb1.scan(/.../), rgb2.scan(/.../)
35
+ pal1, pal2 = Palette.empty(h1), Palette.empty(h2)
36
+
37
+ a1.each_index do |i|
38
+ pix1 = a1[i].unpack('CCC')
39
+ next if pal1.index_of(pix1)
40
+
41
+ pix2 = a2[i].unpack('CCC')
42
+ pal1.add_color(pix1)
43
+ pal2.add_color(pix2, force: true)
44
+ end
45
+
46
+ [pal1.pad, pal2.pad]
47
+ end
48
+
49
+ # Same as {ByteInput#from_table_as_pointer}.
50
+ #
51
+ # @param [Hash] h see {Bytestream#initialize}
52
+ # @return [Palette]
53
+ # @see ByteInput#from_table_as_pointer
54
+ def from_table(rom, table, index, h = {})
55
+ from_table_as_pointer(rom, table, index, h)
56
+ end
57
+
58
+ # Generates a palette from a stream of bytes following the +0xRRGGBB+
59
+ # format, failing if the total number of colors in the palette exceeds 16.
60
+ #
61
+ # @param [String] rgb
62
+ # @param [Hash] h see {Bytestream#initialize}
63
+ # @return [Sprite]
64
+ def from_rgb(rgb, h = {})
65
+ fail 'RGB stream length must divide by 3.' unless rgb.length % 3 == 0
66
+ out_pal = empty(h)
67
+
68
+ until rgb.empty?
69
+ pixel = rgb.slice!(0, 3).unpack('CCC')
70
+ out_pal.add(pixel) unless out_pal.index_of(pixel)
71
+ end
72
+
73
+ out_pal
74
+ end
75
+
76
+ # Returns an object representing an empty palette.
77
+ #
78
+ # @param [Hash] h see {Bytestream#initialize}
79
+ # @return [Palette]
80
+ def empty(h = {})
81
+ h.merge!(representation: [])
82
+ new(h)
83
+ end
84
+
85
+ # Returns a palette object represented by the given array
86
+ # of [R,G,B] values. Caution is advised as there is no
87
+ # validation.
88
+ #
89
+ # @param [Array] a
90
+ # @return [Palette]
91
+ def from_array(a = [], h = {})
92
+ h.merge!(representation: a)
93
+ new(h)
94
+ end
95
+ end
96
+
97
+ # Same as {Bytestream#initialize}, but with +:lz77+
98
+ # and +:pointer_mode+ set to true by default.
99
+ #
100
+ # @see Bytestream#initialize
101
+ def initialize(h = {})
102
+ @lz77 = true
103
+ @pointer_mode = true
104
+
105
+ super
106
+ end
107
+
108
+ # Pads the palette until it has the specified number of colors.
109
+ # This is a mandatory step for the palette to actually work in-game.
110
+ #
111
+ # @param [Integer] l target size.
112
+ # @param [Array] c the color to pad with, following the [R, G, B] format.
113
+ # @return [self]
114
+ def pad(l = 16, c = [16, 16, 16])
115
+ add_color(c, force: true) until present.length >= l
116
+ clear_cache
117
+ end
118
+
119
+ # Adds a color to the array. The color must be a [R, G, B] array.
120
+ # Will fail on malformed color or 16 coors exceeded.
121
+ #
122
+ # This will clear the internal cache.
123
+ #
124
+ # @param [Array] color the color in [255, 255, 255] format.
125
+ # @param [Hash] h
126
+ # @option h [Boolean] :force if +true+, add colors even if already
127
+ # present in the palette.
128
+ # @return [self]
129
+ def add_color(color, h = {})
130
+ unless color.is_a?(Array) && color.length == 3
131
+ fail "Color must be [R, G, B]. (#{color})"
132
+ end
133
+
134
+ color.each do |i|
135
+ next if i.between?(0, 255)
136
+ fail "Color component out of bounds. (#{color})"
137
+ end
138
+
139
+ @representation ||= []
140
+
141
+ if @representation.index(color) && !h.fetch(:force, false)
142
+ return clear_cache
143
+ end
144
+ @representation << color
145
+
146
+ if present.length > 16
147
+ fail "Palette must be smaller than 16. (#{present.length}, #{color})"
148
+ end
149
+
150
+ clear_cache
151
+ end
152
+
153
+ alias_method :add, :add_color
154
+
155
+ # Returns the index of the [R, G, B] color in the palette,
156
+ # or +nil+ if absent.
157
+ #
158
+ # @param [Array] color the color in [255, 255, 255] format.
159
+ # @return [Integer]
160
+ def index_of(color)
161
+ present.index(color)
162
+ end
163
+
164
+ # Returns the palette as an array of [R, G, B] values.
165
+ #
166
+ # @return [Array]
167
+ def present
168
+ return @representation if @representation
169
+
170
+ red_mask, green_mask, blue_mask = 0x1f, 0x3e0, 0x7c00
171
+
172
+ in_bytes = to_bytes.dup
173
+ out_array = []
174
+
175
+ until in_bytes.empty?
176
+ color = Bytestream.from_bytes(in_bytes.slice!(0, 2).reverse).to_i
177
+
178
+ out_array << [
179
+ (color & red_mask) << 3,
180
+ (color & green_mask) >> 5 << 3,
181
+ (color & blue_mask) >> 10 << 3
182
+ ]
183
+ end
184
+
185
+ @representation = out_array
186
+ end
187
+
188
+ alias_method :to_a, :present
189
+
190
+ # Converts the internal representation to a GBA-compatible
191
+ # stream of bytes.
192
+ #
193
+ # @return [String]
194
+ # @see ByteOutput#generate_bytes
195
+ def generate_bytes
196
+ pal = ''
197
+ @representation.each do |c|
198
+ red = c[0] >> 3
199
+ green = c[1] >> 3 << 5
200
+ blue = c[2] >> 3 << 10
201
+
202
+ pal <<
203
+ Bytestream.from_hex(format('%04X', (blue | green | red))).to_b.reverse
204
+ end
205
+
206
+ pal.rjust(2 * @representation.length, "\x00")
207
+ end
208
+
209
+ # A blurb showing the color count of the palette.
210
+ #
211
+ # @return [String]
212
+ def to_s
213
+ "Palette (#{present.length})"
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,145 @@
1
+ module Sabrina
2
+ # This is the recommended wrapper namespace for {Plugin Plugins} that enhance
3
+ # {Sabrina} with new functionality.
4
+ #
5
+ # @see Plugin
6
+ module Plugins; end
7
+
8
+ # The base class for plugins that improve classes with the ability
9
+ # to handle additional data. The +#initialize+ method for every
10
+ # plugin should accept a target class instance as the parameter.
11
+ # The +#initialize+ method of the target class should contain
12
+ # a call to +load_plugins+.
13
+ #
14
+ # When a class has been enhanced by a plugin, it will gain a read-only
15
+ # attribute +#plugins+ that will contain a set of all plugins registered
16
+ # with that class. Class instances will gain a read-only attribute
17
+ # +#plugins+ that will contain a hash of SHORT_NAME => (plugin instance)
18
+ # pairs.
19
+ #
20
+ # Each plugin will also expose a set of {FEATURES}. For every +feature+, two
21
+ # instance methods will be added to the target object: +#feature+, which
22
+ # will call +#feature+ on every plugin instance that supports it, and
23
+ # +#feature_shortname+, which will call +#feature+ on the instance of the
24
+ # plugin identified by {SHORT_NAME} (assuming it supports that feature).
25
+ #
26
+ # For clarity, plugins should be contained within the {Plugins}
27
+ # namespace.
28
+ class Plugin
29
+ # What class this plugin enhances. This should be required before
30
+ # the plugin.
31
+ #
32
+ # The target class should have +load_plugins+ at the end of the init.
33
+ ENHANCES = Monster
34
+
35
+ # Plugin name goes here.
36
+ PLUGIN_NAME = 'Generic Plugin'
37
+
38
+ # Short name should contain only a-z, 0-9, and underscores.
39
+ # This will be the suffix for target instance methods.
40
+ SHORT_NAME = 'genericplugin'
41
+
42
+ # Tells the enhanced class what public instance methods the plugin
43
+ # should expose.
44
+ #
45
+ # This should be a set of symbols.
46
+ #
47
+ # Common features include +:reread+, +:write+, +:save+, +:load+.
48
+ FEATURES = Set.new [:reread, :write, :save, :load]
49
+
50
+ class << self
51
+ # @see PLUGIN_NAME
52
+ def plugin_name
53
+ self::PLUGIN_NAME
54
+ end
55
+
56
+ # @see ENHANCES
57
+ def target
58
+ self::ENHANCES
59
+ end
60
+
61
+ # @see SHORT_NAME
62
+ def short_name
63
+ self::SHORT_NAME.downcase.gsub(/[^a-z0-9_]/, '')
64
+ end
65
+
66
+ # @see FEATURES
67
+ def features
68
+ self::FEATURES
69
+ end
70
+
71
+ # Automagically registers new plugins with target.
72
+ def inherited(subclass)
73
+ target.extend(Plugin::Register)
74
+ Plugin::Load.include_in(target)
75
+
76
+ target.register_plugin(subclass)
77
+ super
78
+ end
79
+
80
+ # Generate a +#feature+ method.
81
+ #
82
+ # @param [Symbol] f feature.
83
+ # @return [Proc]
84
+ def feature_all(f)
85
+ proc do |*args|
86
+ targets = @plugins.select { |_key, val| val.feature?(f) }
87
+ targets.values.map { |val| val.method(f).call(*args) }
88
+ end
89
+ end
90
+
91
+ # Generate a +#feature_shortname+ method.
92
+ #
93
+ # @param [Symbol] f feature.
94
+ # @return [Proc]
95
+ def feature_this(f, n)
96
+ proc do |*args|
97
+ @plugins.fetch(n).method(f).call(*args)
98
+ end
99
+ end
100
+
101
+ # Provides a short description of the plugin's functionality.
102
+ #
103
+ # @return [String]
104
+ def to_s
105
+ "\'#{short_name}\': enhances #{target.name}, supports #{features.to_a}"
106
+ end
107
+ end
108
+
109
+ # Generates a new {Plugin} object.
110
+ #
111
+ # @param [Object] parent
112
+ # @return [Plugin]
113
+ def initialize(parent, *_args)
114
+ @parent = parent
115
+ end
116
+
117
+ # Drop internal data and force reloading from ROM.
118
+ #
119
+ # @return [Array] Any return data from child methods.
120
+ def reread(*_args)
121
+ children.map(&:reload_from_rom)
122
+ end
123
+
124
+ # Write data to ROM.
125
+ #
126
+ # @return [Array] Any return data from child methods.
127
+ def write(*_args)
128
+ children.map(&:write_to_rom)
129
+ end
130
+
131
+ # Whether a plugin instance supports a feature.
132
+ #
133
+ # @return [Boolean]
134
+ def feature?(f)
135
+ self.class.features.include?(f)
136
+ end
137
+
138
+ # Subclasses should override this to provide a useful textual
139
+ # representation of instance data.
140
+ # @return [String]
141
+ def to_s
142
+ "<#{self.class.plugin_name}>"
143
+ end
144
+ end
145
+ end