sabrina 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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