An attempt at understanding internals of the Sega Toys’ Advanced Pico Beena by reversing the BIOS and a few games. (WIP)

TOC


Tooling

Debug features

JTAG

The BIOS dumping writeup has instructions on how to connect to this debug port, including interfacing with telnet. However, you can instead interface with GDB, which allows for more convenient features such as scripting.

On a Debian-based Linux distro, you can install gdb-multiarch which is compiled with support for other architectures beyond what your host uses. In our case, we want to connect to an ARMv4t via OpenOCD, which exposes a GDB server by default on port 3333:

gdb-multiarch \
    -ex='set architecture armv4t' \
    -ex='set endian big' \
    -ex='target remote localhost:3333'

Test mode

Most games contain a hidden test mode that can be activated by the same inputs. There are at least 3 variants, each identified by the ROM header’s library date (the release date is usually a few months later):

  1. Before 2005-10-19: Test results are encoded in the background color, by updating the first palette entry. In some cases there are 2 distinct palettes and tilesets for graphical marks (e.g. Kazoku Minna no Nouryoku Trainer uses addresses 0x80074dac and 0x80074dbe for test passed, 0x80074a1c and 0x80074a2c for test failed);
  2. 2005-10-19 up to 2007-05-28: In addition to the background color update, ROM header fields are read and displayed, followed by test results under “Test Cartridge” (e.g. Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana uses addresses 0x80748f14 and 0x80746892 for the font’s palette and tileset);
  3. 2007-05-28 and after: MIDI audio plays when test mode is successfully activated. Depending on the game, it can include a SD-Card test (e.g. Anpanman no Waku Waku Game Oekaki) or peripheral specific tests (e.g. Cars 2 Racing Beena: Mezase! World Champion!).

Anpanman no Waku Waku Game Oekaki

The following video capture shows how the SD-Card reader is tested:

  1. Sound plays at the start;
  2. Reader is disconnected, its status remains blank after cartridge checksum is computed;
  3. Reader is connected, its led flashes, status becomes OK;
  4. We can repeat the process by disconnecting it again, status is blanked;

On the card, a file is written under /BEENA/S100031/TESTDATA.BIN.

Hex dump:

00000000: 8000 0040 8000 00a0 8000 00b0 8000 0060  ...@...........`
00000010: 0000 0000 0080 0000                      ........

These appear to be ROM header fields displayed on screen (except for what we call “product_name”):

  • 0x80000040: product_id
  • 0x800000a0: app_date
  • 0x800000b0: lib_date
  • 0x80000060: product_name
  • 0x00000000: flash_size
  • 0x00800000: mask_size

Cars 2 Racing Beena: Mezase! World Champion!

Courtesy of André Coelho (@hitmanmcc), also described how the peripheral is tested:

As you interact with the buttons the table below shows its status and once you’ve tested all of the functionality of the wheel you get a green screen indicating everything is OK.

Instructions

To activate test mode:

  1. Insert a game on the console;
  2. Flip all pages of the Storyware to expose all page sensors, and cover the sensors for pages 1, 3, and 5 with some opaque objects. Here’s a crude diagram of how that should look:
    Page sensors covered to activate test mode
  3. Hold down the left red button;
  4. Power on / Reset the console;

If all goes well, instead of the game showing the logo animations, it will stay locked in test mode until the PSU connector is removed and then reconnected (assuming there’s no batteries connected).

Unfortunately, this method is somewhat unreliable, likely due to a very short time window between powering on the console, stabilizing the page sensors, and running test mode input checks.

This can be workaround by triggering a soft reset via the JTAG port, which involves pulling strong-low on pin 15 (nSRST). For example, you can short nSRST to one of the ground pins:

Alternatively, if you are using GDB: monitor soft_reset_halt.

Otherwise, make sure that pages are being identified as expected. For example, if you open page 3 (i.e. 3 sensors are exposed), and you cover the leftmost sensor with an opaque object, page 2 should be loaded. If you also cover the 2nd leftmost sensor, page 1 should be loaded. If you uncover the leftmost sensor, but keep the 2nd leftmost covered, then page 3 should be loaded.

Technical overview

All variants perform the same tests:

  1. CRC32 checksum computed over the game’s ROM data after the header, compared against the expected checksum stored in the ROM header at offset 0xd0;
  2. Memory integrity checks consisting of reading a string from RAM to a cache, then writing those cached bytes at the same RAM address, which is then compared with the expected string.

Decompilation example:

iVar1 = check_crc();
if (iVar1 != 1) {
  sVar2 = strlen("Nana");
  len_nana = sVar2 + 1;
  __s2 = (char *)((int)&DAT_c00c6d00 - len_nana);
  strncpy(__s2,"Nana",len_nana);
  iVar1 = check_memcache_read(__s2,0x800 - len_nana,len_nana);
  if ((iVar1 != 1) && (iVar1 = read_500300bc_pre1(__s2,0x800 - len_nana,len_nana), iVar1 != 1)) {
    uVar3 = strcmp("Nana",__s2);
    uVar3 = (-uVar3 | uVar3) >> 0x1f;
    goto ret;
  }
}

Input checks

All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.

Checks to enable test mode are done in FUN_800291a0() (here named is_test_mode_active). It is called at the beginning of FUN_80000f40() (main), and the result is used to conditionally call FUN_80020af8() (test_mode). These calls happen before the palettes and tilesets of the logo screens are loaded in FUN_8001ffec():

int main() {
  // ...
  iVar1 = is_test_mode_active();
  if (iVar1 == 0) {
    test_mode();
  }
  // ...
  FUN_8001ffec();
  // ...
}

Pad buttons are directly read from I/O port address 0x50020034 (here named IO_BUTTONS). Page sensors are then read by callbacks run in a loop until the effective page is set. Afterwards, each page state is checked as well:

undefined8 is_test_mode_active(void) {
  byte bVar1;
  int iVar2;
  int uVar3;
  PageState *iVar3;
  long in_lr;

  iVar3 = read_volatile_4(IO_BUTTONS);

  /*
   * Check if the left red button is held down.
   *
   * This value encodes bits `XXXX XXAB CDEF GHIJ`, where:
   * - `X`: unused,
   * - `ABCDE`: red + directional buttons of one pad, and
   * - `FGHIJ`: red + directional buttons of the other pad.
   * They can be pressed (= 0) or released (= 1).
   */
  if (iVar3 == 0x1ff) {
    /*
     * `PageState` is an 8 byte structure:
     * - `0x0`: effective_page: the last page open,
     *         which will be loaded by the game;
     * - `0x1`: is_effective_page_set: flag that indicates
     *         the value in `effective_page` is ready to be read (= 1);
     * - `0x2..0x7`: flags for pages 1..6 to indicate if
     *             they are closed (= 1) or open (= 0).
     */
    iVar3 = (PageState *)get_work_page_read();
    bVar1 = iVar3->is_effective_page_set;
    while (bVar1 == 0) {
      run_callbacks();
      bVar1 = iVar3->is_effective_page_set;
    }
    uVar3 = 0; // Activate test mode

    // Are pages 1, 3, and 5 not closed, or pages 2, 4, and 6 not open?
    if ((iVar3->page1 + iVar3->page3 + iVar3->page5 != 3) ||
        (iVar3->page2 + iVar3->page4 + iVar3->page6 != 0)) {
      uVar3 = 1; // Skip test mode
    }
  }
  else {
    uVar3 = 1; // Skip test mode
  }
  return CONCAT44(in_lr,uVar3);
}

TV Ocha-Ken

Plug and play TV game 「テレビとお茶札 お茶犬『ほっ』と生活」 also has a test mode: Hold down A B C, then power on the system, release all buttons, and press B 3 times.

Video update tints

All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.

An unreferenced variable at address 0xc00c7cc7 (named w_debug_tint) is used to toggle a debug feature, likely used to track different phases of a game’s video update callback:

undefined4 do_callbacks(void) {
  bx_r3_chain_bind_callback();
  read_40000010();

  // (1)
  if (w_debug_tint != '\0') {
    write_volatile_4(DAT_40000030,0x1f00); // green tint
    written_to_40000030 = 0x1f; // red tint
    w_video_regs_dirty = 1;
  }
  // (2)
  bx_r3(&w_wait_not_video_dirty_cb,&w_video_dirty,extraout_r1);
  if (w_debug_tint != '\0') {
    write_volatile_4(DAT_40000030,0x1f0000); // blue tint
    written_to_40000030 = 0x1f0000; // blue tint
  }
  bx_r3_chain_bind_callback();
  // (3)
  return in_lr;
}

The following GDB commands can be run to explore this logic:

set *(char**)(0xc00c7cc4) = 0x01000001
hbreak *0x8002c74a  # (1)
hbreak *0x8002c768  # (2)
hbreak *0x8002c784  # (3)

Shown in this video capture:

  1. Stopped at breakpoint (1);
  2. Debug variable is set;
  3. Execution continued at (2), video callback is run, green tint is set;
  4. Execution continued at (3), callbacks in bx_r3_chain_bind_callback() are run, blue tint is set;
  5. Another iteration is done, note that at the 2nd blue tint, a frame was advanced;
  6. Breakpoints are disabled, execution is continued. Note some red tints are applied for a short time at the beginning;

Red tints can also be observed when switching pages.

Intro skip

A few games check for an input sequence at boot that skips the initial warning screen and the Beena / Sega Toys logo animations screen. Hold down the left pad’s up + down buttons, then power on the console.

Decompilation example:

dVar1 = read_volatile_4(IO_BUTTONS);
if (dVar1 != 0x39f) { // 0b0_00000_11100_11111
    // Show first screens...
}

Confirmed games:

  • Oshare Majo Love and Berry - Cute ni Oshare
  • Shoku Iku Series 1 Soreike! Anpanman - Sukikirai Nai Ko Genki na Ko!
  • Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana - Gojuuon Board Kinou-tsuki

Input parsing

All addresses in this section are from Soreike! Anpanman Hajimete Kaketa yo! Oboeta yo! Hiragana Katakana.

Pad buttons

There are two functions involved: FUN_80029388 stores a negated reading from I/O port address 0x50020034 (here named IO_BUTTONS):

void read_io_buttons_prev(void) {
  uint uVar1 = read_volatile_4(IO_BUTTONS);
  work_io_btn_prev = work_io_btn_prev | ~uVar1;
  return;
}

Then FUN_800293a0 will use that value along with another reading, to distinguish between a button being held, pressed, or released. We see that the total bits read are 0x3ff, and each pad corresponds to 0x1f bits:

// Take read values and compute masks
btns = read_volatile_4(IO_BUTTONS);
btns_press_mask = (work_io_btns_prev | ~btns) & 0x3ff;
btns_press = (work_io_btns_prev_mask ^ btns_press_mask) & btns_press_mask;
btns_press_neg = (work_io_btns_prev_mask ^ btns_press_mask) & ~btns_press_mask;

// Store pad1 state
btns1_hold = (work_io_btns_prev | ~btns) & 0x1f;
btns1_press = btns_press & 0x1f;
btns1_release = CONCAT11((char)btns_press_neg,work_io_btns1_state.field3_0x3) & 0xffff1fff;
work_io_btns1_state = btns1_hold << 0x18 | btns1_press << 0x10 | btns1_release;

// Store pad2 state
btns2_press = (byte)(btns_press >> 5);
btns = (uint)CONCAT11((char)(btns_press_mask >> 5),btns2_press) << 0x10;
btns_press_neg = CONCAT11((char)(btns_press_neg >> 5),work_io_btns2_state.field3_0x3) & 0xffff1fff;
work_io_btns2_state = btns | btns_press_neg;

// Store both pad1 and pad2 state
work_io_btns1n2_state.hold = (byte)(btns >> 0x18) | (byte)btns1_hold;
work_io_btns1n2_state.press = btns2_press | (byte)btns1_press;
work_io_btns1n2_state.release = (byte)(btns_press_neg >> 8) | (byte)(btns1_release >> 8);

// Prepare previous values for next time
work_io_btns_prev_mask = btns_press_mask;
work_io_btns_prev = 0;

return in_lr;

Unfortunately the decompilation doesn’t come out very clean, so let’s look at some examples to better understand what gets stored in these state variables after finishing parsing. These variables are layed out in memory as follows:

0xc00d5448 = work_io_btns_prev
0xc00d544c = work_io_btns1_state
0xc00d5450 = work_io_btns2_state
0xc00d5458 = work_io_btns1n2_state

Let’s start by pressing the red button in pad1:

# read 0x1ff
0xc00d5448 00 00 00 00 10 10 00 00 00 00 00 00 10 10 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 00 00 00 00 10 00 00 00

Both fields “hold” and “press” were set, in both pad1 and pad1n2. On the second time, only “hold” was set. Let’s try one of the directional buttons in pad2:

# read 0x1fe
0xc00d5448 00 00 00 00 10 00 00 00 01 01 00 00 11 01 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 01 00 00 00 11 00 00 00

Ok, so pad1n2 now has the accumulated bits from both pads. If we press all directional buttons in pad2…

# read 0x1f0
0xc00d5448 00 00 00 00 10 00 00 00 0f 0e 00 00 1f 0e 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 0f 00 00 00 1f 00 00 00

Seems like the “hold” field in pad1n2 is saturated with 5 bits. If we also press the red button in pad2…

# read 0x1e0
0xc00d5448 00 00 00 00 10 00 00 00 1f 10 00 00 1f 10 00 00
# repeat
0xc00d5448 00 00 00 00 10 00 00 00 1f 00 00 00 1f 00 00 00

Same value, and if we press all buttons…

# read 0x000
0xc00d5448 00 00 00 00 1f 00 00 00 1f 00 00 00 1f 00 00 00
# repeat
0xc00d5448 00 00 00 00 1f 00 00 00 1f 00 00 00 1f 00 00 00

Now let’s try releasing one of the directional buttons in pad1:

# read 0x100
0xc00d5448 00 00 00 00 17 00 08 00 1f 00 00 00 1f 00 08 00
# repeat
0xc00d5448 00 00 00 00 17 00 00 00 1f 00 00 00 1f 00 00 00

Now field “release” gets updated in pad1 and pad1n2 instead of field “press”. If we press it again and release the red button:

# read 0x200
0xc00d5448 00 00 00 00 0f 08 10 00 1f 00 00 00 1f 08 10 00
# repeat
0xc00d5448 00 00 00 00 0f 00 00 00 1f 00 00 00 1f 00 00 00

That’s pretty much it. But how do we know that the red button is the 5th most significant bit? It’s an educated guess…

In page 0 (i.e. Storyware closed), an intro sequence controlled by FUN_800019ec() shows a title and 2 strings at a time while a song is playing. We see pointers to the title address 0x8002f144 and the strings table address 0x8002f18c.

The sequence happens in a while loop, which has these exit conditions:

while( true ) {
  uVar1 = get_page();
  if ((uVar1 & 0xff) - 1 < 6) {
    // ...
    return in_lr;
  }
  uVar12 = intro_seq_check_inputs();
  if ((int)uVar12 - 1U < 4) break;
  // ...
}

In intro_seq_check_inputs(), the pad state for pad1n2 is returned by get_io_set_hold(3), followed by a check for the held button at bit 5:

io_set_btns1n2 = (byte *)get_io_set_hold(3);
psVar1 = (short *)FUN_8002a114(3);
page = (char *)get_work_page_read();
iVar4 = -1;
iVar5 = -1;
if (((((io_set_btns1n2->hold & 0x10) != 0) && (*(char *)(psVar1 + 2) == '\x02')) &&
     (iVar5 = iVar4, *(char *)((int)psVar1 + 9) != '\0')) &&
     ((*page == '\0' && (page[1] != '\0')))) {
  pwVar3 = &WORD_80055d48;
  iVar2 = 0;
  do {
    if ((((int)(short)*pwVar3 <= (int)*psVar1) &&
         ((int)*psVar1 <= (int)(short)pwVar3[2] + (int)(short)*pwVar3)) &&
       (((int)(short)pwVar3[1] <= (int)psVar1[1] &&
         (iVar5 = iVar2, (int)psVar1[1] <= (int)(short)pwVar3[1] + (int)(short)pwVar3[3]))))
        break;
    iVar2 = iVar2 + 1;
    pwVar3 = pwVar3 + 4;
    iVar5 = iVar4;
  } while (iVar2 < 6);
}
return CONCAT44(in_lr,iVar5 + 1);

If the button at bit 5 is not hold (!= 0), we enter the if branch, otherwise intro_seq_check_inputs() returns 0, causing the while loop in the caller function to exit. There’s also a check for some other input, and if the page still remains at value 0. It makes sense that this intro sequence stop playing if the player flips to another page, or if some input is done. But it’s unlikely that a single specific directional button would cause this behaviour and not the others buttons. Therefore, bit 5 is likely the red button.

Page sensors

There are two functions involved: FUN_80029498 directly stores two 4-byte readings from I/O port addresses 0x5002002c and 0x50020030 (here named IO_PAGE_U4_1 and IO_PAGE_U4_2):

void read_io_page(void) {
  work_io_page_u4_1 = read_volatile_4(IO_PAGE_U4_1);
  work_io_page_u4_2 = read_volatile_4(IO_PAGE_U4_2);
  return;
}

We can already tell how it deviates from the Pico parsing logic, which directly read 6 bits that encoded each page on/off from a single address.

On FUN_800294bc 7 bytes are parsed, 6 are used. The last 0x24 read values are kept for each byte, ranged in 0x00..0xff. These seem to be raw readings from the photodiode sensors.

  // Constrain index to store up to 0x24 read values
  if (work_io_page_i < 0x23) {
    page_i = work_io_page_i + 1;
  }
  else {
    page_i = 0;
  }

  // Store each read byte from 1st address in the corresponding index
  io_page = work_io_page_u4_1;
  work_io_page_raw_val[0] = (byte)((uint)work_io_page_u4_1 >> 0x18);
  work_io_page_i = page_i;
  work_io_page_raw_0x24_vals[page_i] = work_io_page_raw_val[0];
  work_io_page_raw_val[1] = (byte)((uint)io_page >> 0x10);
  work_io_page_raw_0x24_vals[page_i + 0x24] = work_io_page_raw_val[1];
  work_io_page_raw_val[2] = (byte)((uint)io_page >> 8);
  work_io_page_raw_0x24_vals[page_i + 0x48] = work_io_page_raw_val[2];
  work_io_page_raw_val[3] = (byte)io_page;
  work_io_page_raw_0x24_vals[page_i + 0x6c] = work_io_page_raw_val[3];

  // Store each read byte from 2nd address in the corresponding index
  io_page = work_io_page_u4_2;
  work_io_page_raw_val[4] = (byte)((uint)work_io_page_u4_2 >> 0x10);
  work_io_page_raw_0x24_vals[page_i + 0x90] = work_io_page_raw_val[4];
  work_io_page_raw_val[5] = (byte)((uint)io_page >> 8);
  work_io_page_raw_0x24_vals[page_i + 0xb4] = work_io_page_raw_val[5];
  work_io_page_raw_val[6] = (byte)io_page;
  work_io_page_raw_0x24_vals[page_i + 0xd8] = work_io_page_raw_val[6];

Each val is normalized into range 0x0..0x7, then accumulated on work_io_page_parsed_val. It’s then compared to thresholds: val < 0x38 is page on (= 0), val > 0x48 is page off (= 1).

  // Accumulate normalized values
  puVar4 = work_io_page_raw_0x24_vals;
  page_acc_i = 0;
  do {
    acc = 0;
    j = 0;
    do {
      pbVar1 = puVar4 + j;
      j = j + 1;
      acc = acc + (uint)*pbVar1;
    } while (j < 0x24);
    bVar2 = ext_21c(i,0x24); // bVar2 = (int) i / 0x24;
    acc = page_acc_i + 1;
    (&work_io_page_parsed_val)[page_acc_i] = bVar2;
    puVar4 = puVar4 + 0x24;
    page_acc_i = acc;
  } while (acc < 7);

  // Compute thresholds based on the lowest accumulated page value
  page_val = 0xff;
  page_i = 0;
  do {
    if ((&work_io_page_parsed_val)[page_i] < page_val) {
      page_val = (uint)(&work_io_page_parsed_val)[page_i];
    }
    page_i = page_i + 1;
  } while (page_i < 7);
  page_val_added = page_val + 0x48;
  work_page_val_off = (byte)page_val_added;
  if (0xff < page_val_added) {
    work_page_val_off = 0xff;
  }
  work_page_val_on = (char)page_val + 0x38;
  if (0xff < page_val_added) {
    work_page_val_on = 0xff;
  }

This function is called multiple times, and after 0x12 iterations, a flag is set to finally update the effective work_page_read value.

  page_i = 0;
  do {
    // If a threshold was passed, then update page state
    if (work_io_page_read_val[page_i] == '\0') {
      if (work_page_val_off < (&work_io_page_parsed_val)[page_i]) {
        uVar3 = 1;
[page_i]=X:
        work_io_page_read_val[page_i] = uVar3;
      }
    }
    else if ((&work_io_page_parsed_val)[page_i] < work_page_val_on) {
      uVar3 = 0;
      goto [page_i]=X;
    }

    // Compute effective page = last open page
    page_i = page_i + 1;
    if (5 < page_i) {
      if (work_io_page_read_val[5] == '\0') {
        page_read = 6;
      }
      else if (work_io_page_read_val[4] == '\0') {
        page_read = 5;
      }
      else if (work_io_page_read_val[3] == '\0') {
        page_read = 4;
      }
      else if (work_io_page_read_val[2] == '\0') {
        page_read = 3;
      }
      else if (work_io_page_read_val[1] == '\0') {
        page_read = 2;
      }
      else if (work_io_page_read_val[0] == '\0') {
        page_read = 1;
      }
      else {
        page_read = 0;
      }
      work_is_page_read_set = false;
      if (work_prev_page_read == page_read) {
        if (work_prev_page_i < 0x12) {
          work_prev_page_i = work_prev_page_i + 1;
        }
        else {
          work_is_page_read_set = true;
        }
      }
      else {
        work_prev_page_i = 0;
        work_prev_page_read = page_read;
      }
      if (work_is_page_read_set) {
        // We have 0x12 readings, update effective page with computed value
        work_page_read = (byte)page_read;
      }
      return in_lr;
    }
  } while( true );

We can also look at some examples to see how page variables change. We can try the pattern used for the test mode (recall that the byte at 0xc00d5464 isn’t parsed, and the byte at 0xc00d5467 is parsed but unused when computing the effective page):

set *(char**)(0xc00d5460) = 0xff00ff00
set *(char**)(0xc00d5464) = 0x11ff0022

On the 1st iteration, we see the raw values stored starting at 0xc00d556c, with some temporary values for each page increasing with value 0x07:

0xc00d5460 ff 00 ff 00 11 ff 00 22 00 ff 00 00 00 00 00 00
...
0xc00d5560 00 00 00 00 00 00 00 00 00 00 00 00 ff 00 ff 00
0xc00d5570 ff 00 22 07 00 07 00 07 00 00 48 38 00 00 00 00

On the 2nd iteration, the values continue increasing, and the temporary value for the least significant byte at 0xc00d5467 is now 0x01:

0xc00d5460 ff 00 ff 00 11 ff 00 22 00 ff ff 00 00 00 00 00
...
0xc00d5560 00 00 00 00 00 00 00 00 00 00 00 00 ff 00 ff 00
0xc00d5570 ff 00 22 0e 00 0e 00 0e 00 01 48 38 00 00 00 00

After more than 0x12 iterations, we now have an effective page = 6, stored at 0xc00d5564:

0xc00d5460 ff 00 ff 00 11 ff 00 22 ff ff ff ff ff ff ff ff
...
0xc00d5560 22 22 22 22 06 01 01 00 01 00 01 00 ff 00 ff 00
0xc00d5570 ff 00 22 ff 00 ff 00 ff 00 22 48 38 00 00 00 00

Now let’s try closing page 6:

set *(char**)(0xc00d5464) = 0x11ffff22

After more than 0x12 iterations, we now have an effective page = 4, since page 5 was already closed:

0xc00d5460 ff 00 ff 00 11 ff ff 22 ff ff ff ff ff ff ff ff
...
0xc00d5560 22 22 22 22 04 01 01 00 01 00 01 01 ff 00 ff 00
0xc00d5570 ff ff 22 ff 00 ff 00 ff ff 22 48 38 00 00 00 00

Data formats

  • Palettes:
    • 0x0..0x2: header = number of entries
    • 0x2+2*i..0x4+2*i: entry i, BGR555
  • Bitmaps:
    • 0x0..0x4: header
      • 0x0..0x1: type = 0x80 for uncompressed, 0x90..0x9f for compressed (currently unknown algorithm)
      • 0x1..0x3: number of entries k, where 0x80 * k = bitmap size
      • 0x3..0x4: unknown
      • Validated as header[0] & 0xf = (header[1] + header[2] + header[3] - 1) & 0xf
    • 0x4..EOF: Palette indexes, where 0 is the 1st entry in a global palette, other values reference the bitmap’s corresponding palette