Skip to main content

Rando SeedInfo CLR0 Structure

This page describes a way of storing RGB colors in the TPRando seed GCI. The main purpose of these colors would be for recoloring things in the game (such as tunic color).

Intro

The simplistic approach to storing these colors would be to have an array of 4-byte structures as follows:

OffsetTypeName
0x00u8isEnabled
0x01u8[3]RGB

Each thing you want to recolor would be represented by an enum such as:

{
Tunic: 0,
ZoraArmorPrimary: 1,
ZoraArmorSecondary: 2,
ZoraArmorHelmet: 3,
OrdonShield: 4,
}

You would then have an array of the 4-byte structures such that the index in the array is the enum.

// Example byte array
[
TunicEnabled,
TunicR,
TunicG,
TunicB,
ZoraArmorPrimaryEnabled,
ZoraArmorPrimaryR,
ZoraArmorPrimaryG,
ZoraArmorPrimaryB,
...
]

ZoraArmorPrimary has a value of 1, so we would look at offset 1 in the array (starts at byte 0x04 since each structure is 4 bytes).

With this approach, the size of the CLR0 structure would be 4 bytes * numberOfSupportedEnums.

In order to future-proof and in order to simultaneously support a more space-efficient design, we will want a header.

This chunk in the seed data is named CLR0.

CLR is short for "color", and the 0 is essentially the version number.

This follows the scheme that is used by pretty much every file in the game (that is, sections start with a 4-char label).


Adding to the example from before, the byte array would look like:

// Example byte array
[
"C",
"L",
"R",
"0",
HeaderOffset_0x04,
HeaderOffset_0x05,
HeaderOffset_0x06,
HeaderOffset_0x07,
HeaderOffset_0x08,
HeaderOffset_0x09,
HeaderOffset_0x0a,
HeaderOffset_0x0b,
HeaderOffset_0x0c,
HeaderOffset_0x0d,
HeaderOffset_0x0e,
HeaderOffset_0x0f,
TunicEnabled,
TunicR,
TunicG,
TunicB,
ZoraArmorPrimaryEnabled,
ZoraArmorPrimaryR,
ZoraArmorPrimaryG,
ZoraArmorPrimaryB,
...
]

Space-efficient approach

The above works well if you only need to store a few colors. But what if we need to be able to store 75 colors?

This is not that farfetched when you consider that the Midna color customizations contain 7 distinct RGB values, and the Zora Armor contains 3.

Also consider the case in which we support 75 recoloring options, but the player decided the only thing they want to recolor is the tunic (which takes 1 RGB value). With the simplistic approach, we would have approximately 74 * 4 bytes of wasted data (296 or 0x128 bytes which is nearly 4% of a block).


Instead of including an array entry for every single enum, we would ideally only include entries for enums we are actually using. Then if the player only needed 2 colors, we would only need 6 bytes of RGB data instead of 3 * 75 bytes.

The problem then becomes how do we map an enum to its index in the RGB array (previously the enum and its index were always identical).

Mapping enum to rgb index

Imagine we have a bunch of enums which are either enabled or disabled.

EnumEnabled?
0x0000enabled
0x0001no
0x0002no
0x0003enabled
0x0004no
0x0005enabled
0x0006enabled
0x0007no
0x0008enabled
0x0009no
0x000aenabled
0x000bno
note

The enums are u16 (not u8) so that we are not limited to 256 colors. We might never hit that limit, but it is not so unlikely that we shouldn't plan for it.

The RGB array for this would look like:

// Example byte array
[
enum_0000_R, enum_0000_G, enum_0000_B, // offset 0x0
enum_0003_R, enum_0003_G, enum_0003_B, // offset 0x3
enum_0005_R, enum_0005_G, enum_0005_B, // offset 0x6
enum_0006_R, enum_0006_G, enum_0006_B, // offset 0x9
enum_0008_R, enum_0008_G, enum_0008_B, // offset 0xc
enum_000a_R, enum_000a_G, enum_000a_B, // offset 0xf
...
]

There are 2 things we need to be able to do given an enum:

  • Determine if it is enabled or not.
  • Find its index in the RGB data.

For example, using the above data, we want to find the RGB index of enum 0x8.

(Let's assume we magically know that enum 0x8 is enabled for the time being)

The solution is to count up how many of the enums 0 through 7 are enabled.

Since enums 0, 3, 5, and 6 are enabled (4 total), enum 8's RGB index is 4.

However, this way of determining the memory index is too slow and does not scale well.

Check enum is enabled

Let's go ahead and answer the question of how we can tell that an enum is enabled.

There is no reason to waste more than a single bit on this, so we only need one byte for every 8 enums (rounded up).

// pseudocode
function isThisEnumEnabled(u16 myEnum) {
u16 tableIndex = myEnum >> 3;
u8 bitMask = 1 << (myEnum & 0x7);

// Assume we have a reference to the bitTable in the CLR0 chunk.
return (bitTable[tableIndex] & bitMask) !== 0;
}
note

The above is slightly different than how it actually works in code We have to adjust the input based on the minRecolorId in the header. Always reference the code if you want to see exactly how something works.

Mapping enum to rgb index quickly

If we are looking up enum 0x3e (62 decimal), instead of iterating through enums 0 through 0x3d and keeping track of how many are enabled, we can instead keep track of the cummulative results for every 8 enums.

For example:

cummulativeSums IndexFor Enums X to YCummulative Count
0x000x0000 to 0x00070x0000
0x010x0008 to 0x000f0x0003
0x020x0010 to 0x00170x0005
0x030x0018 to 0x001f0x0005
0x040x0020 to 0x00270x0008
0x050x0028 to 0x002f0x000c
0x060x0030 to 0x00370x0010
0x070x0038 to 0x003f0x0012
note

The cummulative sum for index 0 will always be 0, so we will leave this off in the final structure.

enumNumber >> 3.

0x3e >> 3 gives 7.

We check the cummulative value at index 7. The value for this is 0x12.

So then we fetch the value from the bitTable at offset 7.
Let's assume it like the following bits:

0101 0110

note

From left to right, these bits are for enums 0x3f, 0x3e, 0x3d, 0x3c, 0x3b, 0x3a, 0x39, and 0x38.

So now all we need to do is count the number of set bits to the right of our bit in question (which is bit 6 in this case), then add this value to the cummulative value (which we determined was 0x12).

The number of set bits to the right of bit 6 is 3, so our RGB index for enum 0x3f is 0x15 (0x12 + 3).


So the final question to answer is how do we quickly count the number of bits to the right of bit 6.

Fortunately, this is a solved problem.

We can keep a small lookup table to quickly map from a bit pattern to the number of bits which are set.

For 8 bits, there are 256 possible patterns. If we instead process each half of the 8 bits individually and add the results, we only need to handle 16 patterns:

bit patternnum bits set
0b00000
0b00011
0b00101
0b00112
0b01001
0b01012
0b01102
0b01113
0b10001
0b10012
0b10102
0b10113
0b11002
0b11013
0b11103
0b11114
// pseudocode, from https://stackoverflow.com/questions/9949935/calculate-number-of-bits-set-in-byte
uint8_t NIBBLE_LOOKUP [16] =
{
0, 1, 1, 2, 1, 2, 2, 3,
1, 2, 2, 3, 2, 3, 3, 4
};

uint8_t count_ones (uint8_t byte)
{
return NIBBLE_LOOKUP[byte & 0x0F] + NIBBLE_LOOKUP[byte >> 4];
}

The above will return the total number of set bits in a byte, but we are only concerned with the bits to the right of a specific one, so we can adjust it to the following:

// pseudocode
uint8_t NIBBLE_LOOKUP [16] =
{
0, 1, 1, 2, 1, 2, 2, 3,
1, 2, 2, 3, 2, 3, 3, 4
};

uint8_t BITS_TO_RIGHT_MASK [8] =
{
0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f
};

// bitIndex is one of 0,1,2,3,4,5,6,7. 0 means the least significant bit.
// Given a bit index, returns the number of bits which have a lower index and are set.
int8_t countSetBitsToRight( uint8_t byte, uint8_t bitIndex )
{
if ( bitIndex == 0 )
{
return 0;
}

uint8_t maskedByte = byte & BITS_TO_RIGHT_MASK[bitIndex];

uint8_t result = NIBBLE_LOOKUP[maskedByte & 0x0f];

if ( bitIndex >= 5 )
{
result += NIBBLE_LOOKUP[maskedByte >> 4];
}

return result;
}

So we now have all of the tools we need.

Let's talk about the entire CLR0 structure now.

CLR0 structure

Header

Header is size 0x16 bytes.

OffsetTypeNameDescription
0x00char[4]magic"CLR0"
0x04u32totalByteSizeTotal number of bytes for chunk (including header and padding at end).
0x08u8reservedNot currently used. Always 0x00.
0x09u8bitTableOffsetOffset to bitTable section relative to start of header.
0x0au16minRecolorIdMust be equal to the smallest enum which has data.
0x0cu16maxRecolorIdMust be equal to the largest enum which has data.
0x0eu16cummulativeSumsOffsetOffset to cummulativeSums section relative to start of header.
0x10u32complexDataOffsetOffset to complexData section relative to start of header.
0x14u16basicDataOffsetOffset to basicData section relative to start of header.

Note that the sections of the CLR0 chunk are ordered as follows:

  • header
  • bitTable
  • cummulativeSums
  • basicData
  • complexData

Notes on certain header properties:

  • magic
    • CLR0 could change to CLR1 and so on if we ever needed an entire restructure of this chunk for some reason (though that doesn't seem likely).
  • maxEnum
    • This is used to future-proof. Let's assume at the current day we are using enums 0x0000 through 0x00ab. If someone generates a seed today, its bitTable and cummulativeSums will be a certain size. If someone then uses that seed with the newer randomizer 6 months later, that rando code might try to check "should I recolor enum 0xcc?" If we didn't have a bounds on the maximum we should check, we would start reading bytes from outside the table and misinterpreting them.
      • Note that this value should be the largest enum which the player actually elected to recolor, not the largest one that could have been supported at the time the seed was generated. The lower we can make this value, the smaller we can make both the bitTable and cummulativeSums sections.
        • bitTable byte length should be ((maxEnum >> 3) + 1) rounded up to multiple of 2.
        • cummulativeSums section byte length should be (bitTable length - 1) * 2.
  • bitTableOffset
    • The header is only 0x16 bytes long, so we don't need more than a byte for bitTableOffset.
    • This value must be set to 0 if there is no data for any enums (meaning the CLR0 chunk is empty).
  • cummulativeSumsOffset The max enum is 0xffff. (0xffff >> 3) is 0x1fff which is the maximum index of the bitTable. So the max bitTable length is one more than that, or 0x2000. Adding the 0x16 from the bitTableOffset, the largest value we could have for this is 0x2016, so u16 is enough for this field.
  • basicData
    • The max enum is 0xffff. (0xffff >> 3) is 0x1fff which is the maximum index of the cummulativeSums section. So the max cummulativeSums length is one more than that, or 0x2000. Since each entry is a u16 (2 bytes), the maximum byte length of the cummulativeSums section is 0x4000. Adding this to the maximum cummulativeSumsOffset which is 0x2016, the maximum possible basicDataOffset value is 0x6016. So a u16 is enough for basicData.
    • Note that the basicData should immediately follow the cummulativeSums section.

bitTable

bitTable section is an array of bytes which comes directly after the header.

It contains one byte for every 8 recolorIds.

If no recolorIds are enabled, the offset to the bitTable in the header must be 0.

cummulativeSums

cummulativeSums section is an array of u16 which comes directly after the bitTable section. We leave off the entry for index 0 of the bitTable since it would always be 0x0000.

If this section is empty, the offset to it in the header should be 0.

basicData

The basicData section comes directly after the cummulativeSums section, and it is an array of words.

For example, an entry might be:

00 63 82 A0

The first byte is an enum called the RecolorType.

In this case, 0x00 means the next 3 bytes (63 82 A0) are an rgb value (63 is R, 82 is G ,and A0 is B).

The last 3 bytes could have a different meaning depending on the first byte (the enum). For example, if the first byte was 0x01 meaning RgbArray, then the remaining 3 bytes would actually be an offset in the complexData section, and the RGB array would be stored in the complexData section.

complexData

The complexData section could very well be empty (in which case the offset to it in the header should be 0).

But assuming it is not empty, it is just an array of bytes with no structure.

A basicData entry which makes use of it will point to an offset in the complexData section. The bytes at that location in the complexData will be interpreted according to the enum that was in the basicData.

For example, recolorType 0x01 is an RgbArray.

Let's look at some example data:

43 4C 52 30 00 00 00 38 00 16 00 00 00 07 00 00  CLR0...8........
00 00 00 1F 00 17 81 00 63 82 A0 01 00 00 00 08 ........c, .....
FF FF FF FF A0 FF FF FF 40 00 E8 7B 00 F3 FF 00 .... ...@..{....
AA FF 60 78 FF 00 00 00 00 00 00 00 00 00 00 00 ..`x............

From offset 0x14 of the header, we get that the basicData offset is 0x0017.

The 2 basicData entries are as follows:

  • 00 63 82 A0
  • 01 00 00 00

Looking at the 2nd entry, we see the RecolorType is 0x01 which is RgbArray. This means the last 3 bytes (in this case, 00 00 00) is an offset in the complexData section.

We can get the offset to the complexData section from offset 0x10 of the header.

In this case, the complexData section is at offset 0x1F.

So the data we are looking at starts with 08 FF FF FF FF A0 ....

The structure of the data is entirely dependent on the recolorType, but in the case of RgbArray, the first byte is the array length, and it is followed by 3 byte RGB array entries.

So in this case:

  • length
    • 8
  • entries:
    • FF FF FF
    • FF A0 FF
    • FF FF 40
    • 00 E8 7B
    • 00 F3 FF
    • 00 AA FF
    • 60 78 FF
    • 00 00 00

The above data in this example are the colors for the rainbow hearts.

note

The rainbow hearts are always displayed in the game with an ABCABCABC pattern. If you wanted a ping-pong ABCBABCBA pattern, you could probably update the first byte to be: 0x80 mask gives 0 or 1 to indicate pattern, and the 0x7F mask gives the array length.

Other notes

Future splitting up of recoloring

Let's assume that the tunic recoloring is enum 0x0000.

It is possible that in the future, we will want people to be able to recolor the hat and body separately.

In that case, we can add 2 new enums (let's say 0x00cd and 0x00ce).

Then in the rando code, we can check if either of these is enabled.
If either is enabled, we will use the values from those enums and ignore enum 0x0000.

We still want to support 0x0000 for backwards compatibility if neither of the new enums are enabled but 0x0000 is enabled.