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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/sabrina.rb +46 -0
- data/lib/sabrina/bytestream.rb +266 -0
- data/lib/sabrina/bytestream/byte_input.rb +126 -0
- data/lib/sabrina/bytestream/byte_output.rb +112 -0
- data/lib/sabrina/bytestream/rom_operations.rb +138 -0
- data/lib/sabrina/children_manager.rb +60 -0
- data/lib/sabrina/config.rb +112 -0
- data/lib/sabrina/config/charmap_in.rb +81 -0
- data/lib/sabrina/config/charmap_out.rb +144 -0
- data/lib/sabrina/config/charmap_out_special.rb +28 -0
- data/lib/sabrina/config/main.rb +105 -0
- data/lib/sabrina/gba_string.rb +156 -0
- data/lib/sabrina/lz77.rb +161 -0
- data/lib/sabrina/meta.rb +33 -0
- data/lib/sabrina/monster.rb +147 -0
- data/lib/sabrina/palette.rb +216 -0
- data/lib/sabrina/plugin.rb +145 -0
- data/lib/sabrina/plugin/load.rb +43 -0
- data/lib/sabrina/plugin/register.rb +32 -0
- data/lib/sabrina/plugins/spritesheet.rb +196 -0
- data/lib/sabrina/plugins/stats.rb +257 -0
- data/lib/sabrina/rom.rb +302 -0
- data/lib/sabrina/sprite.rb +312 -0
- metadata +113 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Sabrina
|
2
|
+
class Plugin
|
3
|
+
# Allows to load plugins. Add +load_plugins+ at the end of your class's
|
4
|
+
# +initialize+ call.
|
5
|
+
module Load
|
6
|
+
class << self
|
7
|
+
# Exposes +#append_features+.
|
8
|
+
def include_in(obj)
|
9
|
+
append_features(obj) unless obj.include?(self)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :plugins
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Generates plugin instances. This should be called from
|
18
|
+
# the target class's init.
|
19
|
+
#
|
20
|
+
# @return [0]
|
21
|
+
def load_plugins
|
22
|
+
self.class.plugins.each do |plugin|
|
23
|
+
n = plugin.short_name
|
24
|
+
|
25
|
+
plugin.features.each do |f|
|
26
|
+
feature_all_sym = f.to_sym
|
27
|
+
feature_this_sym = "#{f}_#{n}"
|
28
|
+
unless respond_to?(feature_all_sym)
|
29
|
+
define_singleton_method(feature_all_sym, plugin.feature_all(f))
|
30
|
+
end
|
31
|
+
unless respond_to?(feature_this_sym)
|
32
|
+
define_singleton_method(feature_this_sym, plugin.feature_this(f, n))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
define_singleton_method(n, -> { @plugins[n] }) unless respond_to?(n)
|
37
|
+
@plugins[n] = plugin.new(self)
|
38
|
+
end
|
39
|
+
0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sabrina
|
2
|
+
class Plugin
|
3
|
+
# Allows to register plugins.
|
4
|
+
module Register
|
5
|
+
# Lists all currently registered plugins.
|
6
|
+
#
|
7
|
+
# @return [Set]
|
8
|
+
def plugins
|
9
|
+
@plugins.to_a
|
10
|
+
end
|
11
|
+
|
12
|
+
# Registers a new plugin for handling a specific subset of monster
|
13
|
+
# data.
|
14
|
+
#
|
15
|
+
# @return [0]
|
16
|
+
# @see Plugin
|
17
|
+
def register_plugin(plugin)
|
18
|
+
@plugins ||= Set.new
|
19
|
+
@plugins << plugin
|
20
|
+
end
|
21
|
+
|
22
|
+
# Lists all currently available features.
|
23
|
+
#
|
24
|
+
# @return [Set]
|
25
|
+
def features
|
26
|
+
s = Set.new
|
27
|
+
@plugins.each { |x| s += x.features }
|
28
|
+
s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Sabrina
|
2
|
+
module Plugins
|
3
|
+
# This {Plugin} provides an abstraction for manipulating all
|
4
|
+
# {Sprite Sprites} and {Palette Palettes} associated with a {Monster}.
|
5
|
+
#
|
6
|
+
# @see Plugin
|
7
|
+
class Spritesheet < Plugin
|
8
|
+
include ChildrenManager
|
9
|
+
|
10
|
+
# @see Plugin::ENHANCES
|
11
|
+
ENHANCES = Monster
|
12
|
+
|
13
|
+
# @see Plugin::PLUGIN_NAME
|
14
|
+
PLUGIN_NAME = 'Spritesheet'
|
15
|
+
|
16
|
+
# @see Plugin::SHORT_NAME
|
17
|
+
SHORT_NAME = 'spritesheet'
|
18
|
+
|
19
|
+
# @see Plugin::FEATURES
|
20
|
+
FEATURES = Set.new [:reread, :write, :save, :load]
|
21
|
+
|
22
|
+
# @!attribute rom
|
23
|
+
# The current working ROM file.
|
24
|
+
# @return [Rom]
|
25
|
+
# @!method rom=(r)
|
26
|
+
# The current working ROM file.
|
27
|
+
# @param [Rom] r
|
28
|
+
# @return [Rom]
|
29
|
+
# @!attribute index
|
30
|
+
# The real index of the monster.
|
31
|
+
# @return [Integer]
|
32
|
+
# @!method index=(i)
|
33
|
+
# The real index of the monster.
|
34
|
+
# @param [Integer] i
|
35
|
+
# @return [Integer]
|
36
|
+
attr_children :rom, :index
|
37
|
+
|
38
|
+
# The palettes used by the sprites.
|
39
|
+
#
|
40
|
+
# @return [Array]
|
41
|
+
attr_reader :palettes
|
42
|
+
|
43
|
+
# The sprites.
|
44
|
+
#
|
45
|
+
# @return [Array]
|
46
|
+
attr_reader :sprites
|
47
|
+
|
48
|
+
# Generates a new Spritesheet object.
|
49
|
+
#
|
50
|
+
# @return [Spritesheet]
|
51
|
+
def initialize(monster)
|
52
|
+
@monster = monster
|
53
|
+
@rom = monster.rom
|
54
|
+
@index = monster.index
|
55
|
+
|
56
|
+
@palettes = [
|
57
|
+
Palette.from_table(@monster.rom, :palette_table, @monster.index),
|
58
|
+
Palette.from_table(@monster.rom, :shinypal_table, @monster.index)
|
59
|
+
]
|
60
|
+
|
61
|
+
@sprites = [
|
62
|
+
Sprite.from_table(@monster.rom, :front_table, @monster.index),
|
63
|
+
Sprite.from_table(@monster.rom, :back_table, @monster.index)
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Array]
|
68
|
+
# @see ChildrenManager
|
69
|
+
def children
|
70
|
+
@sprites + @palettes
|
71
|
+
end
|
72
|
+
|
73
|
+
# Justify sprites to the target ROM count as per ROM {Config}, assuming
|
74
|
+
# 64x64 frame size.
|
75
|
+
def justify
|
76
|
+
@sprites.map(&:justify)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Load data from a file.
|
80
|
+
#
|
81
|
+
# @return [Array] Any return data from child methods.
|
82
|
+
def load(file = @monster.filename, dir = @monster.work_dir, *_args)
|
83
|
+
path = get_path(file, dir)
|
84
|
+
a = split_canvas(ChunkyPNG::Canvas.from_file(path))
|
85
|
+
h = { rom: @rom, index: @index }
|
86
|
+
|
87
|
+
normal_rgb = a[0].to_rgb_stream + a[2].to_rgb_stream
|
88
|
+
shiny_rgb = a[1].to_rgb_stream + a[3].to_rgb_stream
|
89
|
+
|
90
|
+
@palettes = Palette.create_synced_palettes(
|
91
|
+
normal_rgb,
|
92
|
+
shiny_rgb,
|
93
|
+
h.merge(table: :palette_table),
|
94
|
+
h.merge(table: :shinypal_table)
|
95
|
+
)
|
96
|
+
|
97
|
+
frames = @rom.special_frames.fetch(@index, @rom.frames)
|
98
|
+
front = crop_canvas(a[0], frames.first * 64)
|
99
|
+
back = crop_canvas(a[2], frames.last * 64)
|
100
|
+
|
101
|
+
@sprites = [
|
102
|
+
Sprite.from_canvas(
|
103
|
+
front,
|
104
|
+
@palettes.first,
|
105
|
+
h.merge(table: :front_table)
|
106
|
+
),
|
107
|
+
Sprite.from_canvas(
|
108
|
+
back,
|
109
|
+
@palettes.first,
|
110
|
+
h.merge(table: :back_table)
|
111
|
+
)
|
112
|
+
]
|
113
|
+
|
114
|
+
justify
|
115
|
+
children
|
116
|
+
end
|
117
|
+
|
118
|
+
# Save data to a file.
|
119
|
+
#
|
120
|
+
# @return [Array] Any return data from child methods.
|
121
|
+
def save(file = @monster.filename, dir = @monster.work_dir, *_args)
|
122
|
+
a = []
|
123
|
+
@sprites.product(@palettes).each do |x|
|
124
|
+
a << x.first.to_canvas(x.last)
|
125
|
+
end
|
126
|
+
|
127
|
+
path = get_path(file, dir, mkdir: true)
|
128
|
+
|
129
|
+
combine_canvas(a).save(path)
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Crops the canvas to the specified height if taller.
|
135
|
+
def crop_canvas(c, h)
|
136
|
+
return c.crop(0, 0, c.width, h) if c.height > h
|
137
|
+
c
|
138
|
+
end
|
139
|
+
|
140
|
+
# Horizontally combine a one-dimensional array of
|
141
|
+
# {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
|
142
|
+
# objects into a single, wide canvas.
|
143
|
+
#
|
144
|
+
# @param [Array] a an array of Canvas objects.
|
145
|
+
# @return [Canvas] the combined canvas.
|
146
|
+
def combine_canvas(a)
|
147
|
+
out_c = ChunkyPNG::Canvas.new(a.first.width * a.length, a.first.height)
|
148
|
+
a.each_index { |i| out_c.replace!(a[i], a.first.width * i) }
|
149
|
+
out_c
|
150
|
+
end
|
151
|
+
|
152
|
+
# Vertically combine a one-dimensional array of
|
153
|
+
# {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
|
154
|
+
# objects into a single, tall canvas.
|
155
|
+
#
|
156
|
+
# @param [Array] a an array of Canvas objects.
|
157
|
+
# @return [Canvas] the combined canvas.
|
158
|
+
def combine_canvas_vert(a)
|
159
|
+
out_c = ChunkyPNG::Canvas.new(a.first.width, a.first.height * a.length)
|
160
|
+
a.each_index { |i| out_c.replace!(a[i], 0, a.first.height * i) }
|
161
|
+
out_c
|
162
|
+
end
|
163
|
+
|
164
|
+
# Split a {http://rdoc.info/gems/chunky_png/1.2.0/ChunkyPNG/Canvas Canvas}
|
165
|
+
# into an array of canvases horizontally.
|
166
|
+
#
|
167
|
+
# @param [Canvas] c
|
168
|
+
# @param [Integer] w tile width.
|
169
|
+
# @return [Array] an array of Canvas objects.
|
170
|
+
def split_canvas(c, w = 64)
|
171
|
+
out_a = []
|
172
|
+
(c.width / w).times { |i| out_a << c.crop(i * w, 0, w, c.height) }
|
173
|
+
out_a
|
174
|
+
end
|
175
|
+
|
176
|
+
# Concatenate the file name and directory into a full path, optionally
|
177
|
+
# creating the directory if it doesn't exist.
|
178
|
+
#
|
179
|
+
# @param [String] file
|
180
|
+
# @param [String] dir
|
181
|
+
# @param [Hash] h
|
182
|
+
# @option h [Boolean] :mkdir If +true+, create +dir+ if it doesn't
|
183
|
+
# exist.
|
184
|
+
# @return [String]
|
185
|
+
def get_path(file, dir, h = {})
|
186
|
+
f, d = file.dup, dir.dup
|
187
|
+
d << '/' unless d.empty? || d.end_with?('/')
|
188
|
+
|
189
|
+
FileUtils.mkpath(d) if h.fetch(:mkdir, false) && !Dir.exist?(d)
|
190
|
+
|
191
|
+
path = d << f
|
192
|
+
path << '.png' unless path.downcase.end_with?('.png')
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module Sabrina
|
2
|
+
module Plugins
|
3
|
+
# @todo Finish this class.
|
4
|
+
# This {Plugin} aids in manipulating the basic stats of any {Monster}.
|
5
|
+
#
|
6
|
+
# @see Plugin
|
7
|
+
class Stats < Plugin
|
8
|
+
# @see Plugin::ENHANCES
|
9
|
+
ENHANCES = Monster
|
10
|
+
|
11
|
+
# @see Plugin::PLUGIN_NAME
|
12
|
+
PLUGIN_NAME = 'Stats'
|
13
|
+
|
14
|
+
# @see Plugin::SHORT_NAME
|
15
|
+
SHORT_NAME = 'stats'
|
16
|
+
|
17
|
+
# @see Plugin::FEATURES
|
18
|
+
FEATURES = Set.new [:reread, :write]
|
19
|
+
|
20
|
+
# Describes the order in which various stats appear in the byte data.
|
21
|
+
# All of these will also be converted into attributes on runtime and
|
22
|
+
# can be modified directly.
|
23
|
+
STRUCTURE = [
|
24
|
+
:hp, :attack, :defense, :speed, :sp_atk, :sp_def, :type_1, :type_2,
|
25
|
+
:catch_rate, :exp_yield, :ev_yield, :item_1, :item_2, :gender,
|
26
|
+
:egg_cycles, :friendship, :level_curve, :egg_group_1, :egg_group_2,
|
27
|
+
:ability_1, :ability_2, :safari_rate, :color_flip
|
28
|
+
]
|
29
|
+
|
30
|
+
# Code names for level up types.
|
31
|
+
LEVEL_CURVES = [
|
32
|
+
'Medium-Fast', 'Erratic', 'Fluctuating', 'Medium-Slow', 'Fast', 'Slow'
|
33
|
+
]
|
34
|
+
|
35
|
+
# Code names for egg groups.
|
36
|
+
EGG_GROUPS = [
|
37
|
+
'None', 'Monster', 'Water 1', 'Bug', 'Flying', 'Field', 'Fairy',
|
38
|
+
'Grass', 'Human-Like', 'Water 3', 'Mineral', 'Amorphous', 'Water 2',
|
39
|
+
'Ditto', 'Dragon', 'Undiscovered'
|
40
|
+
]
|
41
|
+
|
42
|
+
# Where to pull descriptive names from if applicable, these can be arrays
|
43
|
+
# or ROM tables.
|
44
|
+
NAMES = {
|
45
|
+
type_1: :type_table,
|
46
|
+
type_2: :type_table,
|
47
|
+
item_1: :item_table,
|
48
|
+
item_2: :item_table,
|
49
|
+
level_curve: LEVEL_CURVES,
|
50
|
+
egg_group_1: EGG_GROUPS,
|
51
|
+
egg_group_2: EGG_GROUPS,
|
52
|
+
ability_1: :ability_table,
|
53
|
+
ability_2: :ability_table
|
54
|
+
}
|
55
|
+
|
56
|
+
# @!attribute [rw] rom
|
57
|
+
# The current working ROM file.
|
58
|
+
# @return [Rom]
|
59
|
+
# @!attribute [rw] index
|
60
|
+
# The real index of the monster.
|
61
|
+
# @return [Integer]
|
62
|
+
attr_accessor(:rom, :index, *STRUCTURE)
|
63
|
+
|
64
|
+
# Generates a new Stats object.
|
65
|
+
#
|
66
|
+
# @return [Stats]
|
67
|
+
def initialize(monster)
|
68
|
+
@monster = monster
|
69
|
+
@rom = monster.rom
|
70
|
+
@index = monster.index
|
71
|
+
|
72
|
+
@stream = Bytestream.from_table(
|
73
|
+
@rom,
|
74
|
+
:stats_table,
|
75
|
+
@monster.index,
|
76
|
+
@rom.stats_length
|
77
|
+
)
|
78
|
+
|
79
|
+
parse_stats
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the base stats total.
|
83
|
+
#
|
84
|
+
# @return [Integer]
|
85
|
+
def total
|
86
|
+
@hp + @attack + @defense + @speed + @sp_atk + @sp_def
|
87
|
+
end
|
88
|
+
|
89
|
+
# Reloads the data from a ROM, dropping any changes.
|
90
|
+
#
|
91
|
+
# @return [self]
|
92
|
+
def reread
|
93
|
+
parse_stats
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Write data to the ROM.
|
98
|
+
def write
|
99
|
+
stream.write_to_rom
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns a {Bytestream} object representing the stats, ready to be
|
103
|
+
# written to the ROM.
|
104
|
+
#
|
105
|
+
# @return [Bytestream]
|
106
|
+
def stream
|
107
|
+
Bytestream.from_bytes(
|
108
|
+
unparse_stats,
|
109
|
+
rom: @rom,
|
110
|
+
table: :stats_table,
|
111
|
+
index: @index
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns a hash representation of the stats.
|
116
|
+
#
|
117
|
+
# @return [Hash]
|
118
|
+
def to_h
|
119
|
+
h = {}
|
120
|
+
|
121
|
+
STRUCTURE.each do |entry|
|
122
|
+
h[entry] = instance_variable_get('@' << entry.to_s)
|
123
|
+
end
|
124
|
+
|
125
|
+
{ @index => { stats: h } }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a pretty hexadecimal representation of the stats byte data.
|
129
|
+
#
|
130
|
+
# @return [String]
|
131
|
+
def to_hex
|
132
|
+
stream.to_hex(true)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns a pretty JSON representation of the stats.
|
136
|
+
#
|
137
|
+
# @return [String]
|
138
|
+
def to_json
|
139
|
+
JSON.pretty_generate(to_h)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns a blurb containing the base stats total.
|
143
|
+
#
|
144
|
+
# @return [String]
|
145
|
+
def to_s
|
146
|
+
"<Stats (#{total})>"
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Reads stats data from the ROM.
|
152
|
+
def parse_stats
|
153
|
+
b = @stream.to_bytes.dup
|
154
|
+
|
155
|
+
STRUCTURE.each do |entry|
|
156
|
+
value =
|
157
|
+
case entry
|
158
|
+
when :ev_yield
|
159
|
+
parse_evs(b.slice!(0, 2))
|
160
|
+
when :item_1, :item_2
|
161
|
+
b.slice!(0, 2).unpack('S').first
|
162
|
+
when :color_flip
|
163
|
+
parse_color_flip(b.slice!(0))
|
164
|
+
else
|
165
|
+
b.slice!(0).unpack('C').first
|
166
|
+
end
|
167
|
+
|
168
|
+
pretty_value = prettify_stat(entry, value)
|
169
|
+
instance_variable_set('@' << entry.to_s, pretty_value)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Converts stats data to a GBA-compatible byte string.
|
174
|
+
def unparse_stats
|
175
|
+
b = ''
|
176
|
+
STRUCTURE.each do |entry|
|
177
|
+
val = instance_variable_get('@' << entry.to_s)
|
178
|
+
case entry
|
179
|
+
when :ev_yield
|
180
|
+
b << unparse_evs(val)
|
181
|
+
when :item_1, :item_2
|
182
|
+
b << [val.to_i].pack('S')
|
183
|
+
when :color_flip
|
184
|
+
b << unparse_color_flip(val)
|
185
|
+
else
|
186
|
+
b << [val.to_i].pack('C')
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
b.ljust(28, "\x00")
|
191
|
+
end
|
192
|
+
|
193
|
+
# Attempts to annotate a numeric value with useful information.
|
194
|
+
def prettify_stat(entry, value)
|
195
|
+
return "#{value} (#{gender_info(value)})" if entry == :gender
|
196
|
+
|
197
|
+
return value unless NAMES.key?(entry) && value.is_a?(Numeric)
|
198
|
+
|
199
|
+
zero_is_valid = [:type_1, :type_2, :level_curve]
|
200
|
+
return value if value == 0 && !zero_is_valid.include?(entry)
|
201
|
+
|
202
|
+
return "#{value} (#{NAMES[entry][value]})" if NAMES[entry].is_a?(Array)
|
203
|
+
|
204
|
+
"#{value} (#{ @rom.read_string_from_table(NAMES[entry], value) })"
|
205
|
+
end
|
206
|
+
|
207
|
+
# Displays readable info about the gender distribution defined by the
|
208
|
+
# provided gender value.
|
209
|
+
def gender_info(i)
|
210
|
+
return 'Genderless' if i > 254
|
211
|
+
return 'Always Female' if i == 254
|
212
|
+
return 'Always Male' if i == 0
|
213
|
+
"#{(100.0 * i / 255).round}% Female"
|
214
|
+
end
|
215
|
+
|
216
|
+
# Converts a word (two bytes) into a hash of EV yield data.
|
217
|
+
def parse_evs(b)
|
218
|
+
a = b.unpack('b*').first.scan(/../).map { |x| x.reverse.to_i(2) }
|
219
|
+
h = {}
|
220
|
+
|
221
|
+
a.take(6).each_index do |i|
|
222
|
+
next if a[i] < 1
|
223
|
+
h[STRUCTURE[i]] = a[i]
|
224
|
+
end
|
225
|
+
|
226
|
+
h
|
227
|
+
end
|
228
|
+
|
229
|
+
# Converts a hash of EV yield data into a word (two bytes).
|
230
|
+
def unparse_evs(h)
|
231
|
+
a = []
|
232
|
+
|
233
|
+
STRUCTURE.take(6).each do |stat|
|
234
|
+
ev = h.fetch(stat, 0)
|
235
|
+
a << ev.to_s(2).rjust(2, '0').reverse
|
236
|
+
end
|
237
|
+
|
238
|
+
[a.join.ljust(16, '0')].pack 'b*'
|
239
|
+
end
|
240
|
+
|
241
|
+
# Converts a byte into an array of dex color and flip.
|
242
|
+
def parse_color_flip(b)
|
243
|
+
s = b.unpack('C').first.to_s(16).rjust(2, '0')
|
244
|
+
|
245
|
+
[
|
246
|
+
s[1].to_i,
|
247
|
+
s[0] == '8' ? true : false
|
248
|
+
]
|
249
|
+
end
|
250
|
+
|
251
|
+
# Converts an array of dex color and flip into a byte.
|
252
|
+
def unparse_color_flip(a)
|
253
|
+
((a.last ? '8' : '0') << a.first.to_s).hex.chr
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|