Class: Sabrina::Palette

Inherits:
Bytestream show all
Defined in:
lib/sabrina/palette.rb

Overview

A class dedicated to handling color palette data inside a ROM file. This must be used alongside sprites to display the correct colors in game or when exported to files.

While a palette will function in this and some other programs even if smaller than 16 colors, it must have exactly 16 colors to work in-game. To ensure this, use the #pad method to fill the remaining slots with a default color. This will, however, make it impossible to add further colors.

Parts adapted from Gen III Hacking Suite by thekaratekid552.

See Also:

  • Sprite#set_palette

Instance Attribute Summary

Attributes inherited from Bytestream

#filename, #index, #last_write, #rom, #table, #work_dir

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Bytestream

#lz77_mode, #offset, #offset=, parse_offset, #pointer, #string_mode

Methods included from Bytestream::ByteInput

#from_bytes, #from_hex, #from_rom, #from_rom_as_lz77, #from_table, #from_table_as_pointer

Methods included from Bytestream::RomOperations

#calculate_length, #clear_cache, #reload_from_rom, #write_to_rom

Methods included from Bytestream::ByteOutput

#to_b, #to_bytes, #to_hex, #to_hex_reverse, #to_i, #to_lz77

Constructor Details

#initialize(h = {}) ⇒ Palette

Same as Bytestream#initialize, but with :lz77 and :pointer_mode set to true by default.



101
102
103
104
105
106
# File 'lib/sabrina/palette.rb', line 101

def initialize(h = {})
  @lz77 = true
  @pointer_mode = true

  super
end

Class Method Details

.create_synced_palettes(rgb1, rgb2, h1 = {}, h2 = {}) ⇒ Array

Generates an array of two palettes from two 0xRRGGBB-format streams: One containing every color from rgb1, and another where each color is replaced with its spatial equivalent from rgb2. This assumes palette 1 does not contain duplicate entries, but palette 2 might.

Parameters:

  • rgb1 (String)

    a string of 0xRRGGBB values.

  • rgb2 (String)

    a string of 0xRRGGBB values.

  • h1 (Hash) (defaults to: {})
  • h2 (Hash) (defaults to: {})

Returns:

  • (Array)

    an array of the two resulting palettes.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/sabrina/palette.rb', line 29

def create_synced_palettes(rgb1, rgb2, h1 = {}, h2 = {})
  unless rgb1.length % 3 == 0 && rgb2.length % 3 == 0
    fail 'RGB stream length must divide by 3.'
  end

  a1, a2 = rgb1.scan(/.../), rgb2.scan(/.../)
  pal1, pal2 = Palette.empty(h1), Palette.empty(h2)

  a1.each_index do |i|
    pix1 = a1[i].unpack('CCC')
    next if pal1.index_of(pix1)

    pix2 = a2[i].unpack('CCC')
    pal1.add_color(pix1)
    pal2.add_color(pix2, force: true)
  end

  [pal1.pad, pal2.pad]
end

.empty(h = {}) ⇒ Palette

Returns an object representing an empty palette.

Parameters:

Returns:



80
81
82
83
# File 'lib/sabrina/palette.rb', line 80

def empty(h = {})
  h.merge!(representation: [])
  new(h)
end

.from_array(a = [], h = {}) ⇒ Palette

Returns a palette object represented by the given array of [R,G,B] values. Caution is advised as there is no validation.

Parameters:

  • a (Array) (defaults to: [])

Returns:



91
92
93
94
# File 'lib/sabrina/palette.rb', line 91

def from_array(a = [], h = {})
  h.merge!(representation: a)
  new(h)
end

.from_rgb(rgb, h = {}) ⇒ Sprite

Generates a palette from a stream of bytes following the 0xRRGGBB format, failing if the total number of colors in the palette exceeds 16.

Parameters:

Returns:



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/sabrina/palette.rb', line 64

def from_rgb(rgb, h = {})
  fail 'RGB stream length must divide by 3.' unless rgb.length % 3 == 0
  out_pal = empty(h)

  until rgb.empty?
    pixel = rgb.slice!(0, 3).unpack('CCC')
    out_pal.add(pixel) unless out_pal.index_of(pixel)
  end

  out_pal
end

.from_table(rom, table, index, h = {}) ⇒ Palette

Parameters:

Returns:

See Also:



54
55
56
# File 'lib/sabrina/palette.rb', line 54

def from_table(rom, table, index, h = {})
  from_table_as_pointer(rom, table, index, h)
end

Instance Method Details

#add_color(color, h = {}) ⇒ self Also known as: add

Adds a color to the array. The color must be a [R, G, B] array. Will fail on malformed color or 16 coors exceeded.

This will clear the internal cache.

Parameters:

  • color (Array)

    the color in [255, 255, 255] format.

  • h (Hash) (defaults to: {})

    @option h [Boolean] :force if true, add colors even if already

    present in the palette.
    

Returns:

  • (self)


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/sabrina/palette.rb', line 129

def add_color(color, h = {})
  unless color.is_a?(Array) && color.length == 3
    fail "Color must be [R, G, B]. (#{color})"
  end

  color.each do |i|
    next if i.between?(0, 255)
    fail "Color component out of bounds. (#{color})"
  end

  @representation ||= []

  if @representation.index(color) && !h.fetch(:force, false)
    return clear_cache
  end
  @representation << color

  if present.length > 16
    fail "Palette must be smaller than 16. (#{present.length}, #{color})"
  end

  clear_cache
end

#generate_bytesString

Converts the internal representation to a GBA-compatible stream of bytes.

Returns:

  • (String)

See Also:



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/sabrina/palette.rb', line 195

def generate_bytes
  pal = ''
  @representation.each do |c|
    red = c[0] >> 3
    green = c[1] >> 3 << 5
    blue = c[2] >> 3 << 10

    pal <<
      Bytestream.from_hex(format('%04X', (blue | green | red))).to_b.reverse
  end

  pal.rjust(2 * @representation.length, "\x00")
end

#index_of(color) ⇒ Integer

Returns the index of the [R, G, B] color in the palette, or nil if absent.

Parameters:

  • color (Array)

    the color in [255, 255, 255] format.

Returns:

  • (Integer)


160
161
162
# File 'lib/sabrina/palette.rb', line 160

def index_of(color)
  present.index(color)
end

#pad(l = 16, c = [16, 16, 16]) ⇒ self

Pads the palette until it has the specified number of colors. This is a mandatory step for the palette to actually work in-game.

Parameters:

  • l (Integer) (defaults to: 16)

    target size.

  • c (Array) (defaults to: [16, 16, 16])

    the color to pad with, following the [R, G, B] format.

Returns:

  • (self)


114
115
116
117
# File 'lib/sabrina/palette.rb', line 114

def pad(l = 16, c = [16, 16, 16])
  add_color(c, force: true) until present.length >= l
  clear_cache
end

#presentArray Also known as: to_a

Returns the palette as an array of [R, G, B] values.

Returns:

  • (Array)


167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/sabrina/palette.rb', line 167

def present
  return @representation if @representation

  red_mask, green_mask, blue_mask = 0x1f, 0x3e0, 0x7c00

  in_bytes = to_bytes.dup
  out_array = []

  until in_bytes.empty?
    color = Bytestream.from_bytes(in_bytes.slice!(0, 2).reverse).to_i

    out_array << [
      (color & red_mask) << 3,
      (color & green_mask) >> 5 << 3,
      (color & blue_mask) >> 10 << 3
    ]
  end

  @representation = out_array
end

#to_sString

A blurb showing the color count of the palette.

Returns:

  • (String)


212
213
214
# File 'lib/sabrina/palette.rb', line 212

def to_s
  "Palette (#{present.length})"
end