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.

Original code license

The MIT License (MIT)

Copyright (c) 2014 karatekid552

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

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.



125
126
127
128
129
130
# File 'lib/sabrina/palette.rb', line 125

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.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/sabrina/palette.rb', line 53

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:



104
105
106
107
# File 'lib/sabrina/palette.rb', line 104

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:



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

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:



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/sabrina/palette.rb', line 88

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:



78
79
80
# File 'lib/sabrina/palette.rb', line 78

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)


153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/sabrina/palette.rb', line 153

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:



219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/sabrina/palette.rb', line 219

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)


184
185
186
# File 'lib/sabrina/palette.rb', line 184

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)


138
139
140
141
# File 'lib/sabrina/palette.rb', line 138

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)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/sabrina/palette.rb', line 191

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)


236
237
238
# File 'lib/sabrina/palette.rb', line 236

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