Skip to content

Instantly share code, notes, and snippets.

@karmic64
Created December 12, 2023 15:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save karmic64/08b32d1e9348ca4e9f1c9b53000fe850 to your computer and use it in GitHub Desktop.
Save karmic64/08b32d1e9348ca4e9f1c9b53000fe850 to your computer and use it in GitHub Desktop.
Cardcaptor Sakura - Itsumo Sakura-chan to Issho (GBC) graphics ripper
/*
Cardcaptor Sakura - Itsumo Sakura-chan to Issho (GBC) graphics ripper by karmic, June 18 2022
Requires libpng (compile with -lpng)
ROM file should be V1.1 (CRC32 43F28E22)
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <sys/stat.h>
#include <setjmp.h>
#include <png.h>
/*
full call that sets graphic is at $29c6 (-> $12ab, graphic id in A, saved to $ca43)
graphic table is at 7:$4000, each entry is $14 bytes long, first 8 bytes are copied to $ca45+
the game supports DMG mono graphics! CGB flag at $c1a0 (=$11 if CGB)
graphic table entry format:
$00: graphic data usage markers
see $12dd data type list for the bit meanings
$01-$02: tile data base offset
if this is $0000-$07ff, the destination address is x + $9000
if this is $0800-$0fff, the destination address is x + $8000
if this is $1000+, the destination address is x + $7000
$03: image X(?) offset onscreen
$04: image Y(?) offset onscreen
$05: image width
$06: image height
$07: palette usage markers, lower bit is read first
$08: tile data bank
$09-$0a: tile data pointer
$0b: tilemap bank
$0c-$0d: tilemap pointer
$0e: cgb extended tilemap bank
$0f-$10: cgb extended tilemap pointer
$11: palette data bank (both obj and bg palettes use the same pointer)
$12-$13: palette data pointer
the main routine that copies graphic data elements is at $12dd
immediately following the first call instruction is the data type jump table
graphic id in A, data type in C:
0 - bg palettes
1 - cgb extended tilemap
2 - tilemap
3 - tiles
4 - obj palettes
palettes are copied to $c9c3
palette data for each palette (8 bytes long, 8 palettes) is ONLY used if the relevant bit of the palette usage marker is on
otherwise, it is SKIPPED OVER while copying. the unused palette data is STILL PRESENT
tile copying is special, if the destination address hits $9800, it will wrap to $8800
tilemaps and tile data are stored compressed, depacked to $ca51
$ff marks the end of the data
$80-$fe is a compressed run, where the data length is $xx-$7e and the data is the following byte
$00-$7f is unpacked, where the data length is $xx+1 and the data follows
*/
#define AMT_IMAGES 0x75
inline uint16_t get16(uint8_t *p) { return (*p) | (*(p+1) << 8); }
size_t decompress(void *dest, void *src)
{
uint8_t *sp = src;
uint8_t *dp = dest;
uint8_t c;
while ((c = *(sp++)) != 0xff)
{
if (c >= 0x80)
{
c -= 0x7e;
uint8_t d = *(sp++);
memset(dp, d, c);
}
else
{
c++;
memcpy(dp, sp, c);
sp += c;
}
dp += c;
}
return (void*)dp - dest;
}
void *get_rom_ptr(void *rom, unsigned bank, unsigned ptr)
{
if (ptr < 0x4000) return rom + ptr;
else return rom + (bank * 0x4000) + (ptr & 0x3fff);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
puts("usage: convert rom outdir");
return EXIT_FAILURE;
}
char *romname = argv[1];
char *outdirname = argv[2];
size_t outdirnamelen = strlen(outdirname);
struct stat st;
if (stat(romname, &st))
{
printf("couldn't stat rom file: %s\n",strerror(errno));
return EXIT_FAILURE;
}
size_t romsize = st.st_size;
FILE *f = fopen(romname,"rb");
if (!f)
{
printf("couldn't open rom file: %s\n",strerror(errno));
return EXIT_FAILURE;
}
uint8_t *rom = malloc(romsize);
if (fread(rom,1,romsize,f) != romsize)
{
printf("error reading rom file: %s\n",strerror(errno));
return EXIT_FAILURE;
}
fclose(f);
char *outname = malloc(outdirnamelen+16);
/******************************************/
uint8_t *img_tbl = &rom[7*0x4000];
png_byte bitmap[0x12*8][0x14*8];
png_byte *bitmap_rows[0x12*8];
for (unsigned i = 0; i < 0x12*8; i++)
bitmap_rows[i] = bitmap[i];
for (unsigned img_id = 0; img_id < AMT_IMAGES; img_id++)
{
uint8_t *img = &img_tbl[img_id * 0x14];
uint16_t tile_base_offs = get16(img+1);
uint8_t first_tile = tile_base_offs / 16;
uint8_t img_width = img[5];
uint8_t img_height = img[6];
uint8_t img_tile_bank = img[8];
uint16_t img_tile_ptr = get16(img+9);
uint8_t *tile_src = get_rom_ptr(rom,img_tile_bank,img_tile_ptr);
uint8_t tiles[256][8][2];
uint8_t img_map_bank = img[0x0b];
uint16_t img_map_ptr = get16(img+0x0c);
uint8_t *map_src = get_rom_ptr(rom,img_map_bank,img_map_ptr);
uint8_t map[0x12*0x14];
uint8_t img_cgb_map_bank = img[0x0e];
uint16_t img_cgb_map_ptr = get16(img+0x0f);
uint8_t *cgb_map_src = get_rom_ptr(rom,img_cgb_map_bank,img_cgb_map_ptr);
uint8_t cgb_map[0x12*0x14];
uint8_t img_pal_bank = img[0x11];
uint16_t img_pal_ptr = get16(img+0x12);
uint8_t *pal_src = get_rom_ptr(rom,img_pal_bank,img_pal_ptr);
png_color pal[8*4];
printf("writing %04u.png (base $%05lX, %ux%u, tiles $%05lX, map $%05lX, cgb map $%05lX, pal $%05lX)...", img_id, img-rom
,img_width,img_height, tile_src-rom, map_src-rom, cgb_map_src-rom, pal_src-rom);
/*******************************************/
/* some images assume tile id 0 is blank */
memset(tiles[(-first_tile) & 0xff],0,16);
size_t tiles_size = decompress(tiles, tile_src);
size_t map_size = decompress(map, map_src);
size_t cgb_map_size = decompress(cgb_map, cgb_map_src);
for (unsigned i = 0; i < 8*4; i++)
{
uint16_t c = get16(pal_src + (i*2));
uint8_t r = (c & 0x1f);
uint8_t g = ((c>>5) & 0x1f);
uint8_t b = ((c>>10) & 0x1f);
pal[i].red = (r<<3) | (r>>2);
pal[i].green = (g<<3) | (g>>2);
pal[i].blue = (b<<3) | (b>>2);
}
/*********************************************/
for (unsigned m_y = 0; m_y < img_height; m_y++)
{
for (unsigned m_x = 0; m_x < img_width; m_x++)
{
uint8_t mv = map[(m_y*img_width) + m_x];
uint8_t cmv = cgb_map[(m_y*img_width) + m_x];
uint8_t tile_id = mv - first_tile;
uint8_t tile_pal = cmv & 7;
for (unsigned t_y = 0; t_y < 8; t_y++)
{
unsigned t_eff_y = t_y;
if (cmv & 0x40) t_eff_y ^= 7;
uint8_t tbp0 = tiles[tile_id][t_y][0];
uint8_t tbp1 = tiles[tile_id][t_y][1];
for (unsigned t_x = 0; t_x < 8; t_x++)
{
unsigned t_eff_x = t_x;
if (cmv & 0x20) t_eff_x ^= 7;
unsigned c = ((tbp0 & (0x80 >> t_eff_x)) != 0) | ((tbp1 & (0x80 >> t_eff_x)) != 0)<<1;
unsigned b_x = (m_x*8)+t_x;
unsigned b_y = (m_y*8)+t_y;
bitmap[b_y][b_x] = (tile_pal<<2) | c;
}
}
}
}
/******************************************/
if (0)
{
FILE *f;
sprintf(outname,"%s/%04u.map",outdirname,img_id);
f = fopen(outname,"wb");
fwrite(map,1,map_size,f);
fclose(f);
sprintf(outname,"%s/%04u.cgb",outdirname,img_id);
f = fopen(outname,"wb");
fwrite(cgb_map,1,cgb_map_size,f);
fclose(f);
sprintf(outname,"%s/%04u.til",outdirname,img_id);
f = fopen(outname,"wb");
fwrite(tiles,1,tiles_size,f);
fclose(f);
}
sprintf(outname,"%s/%04u.png",outdirname,img_id);
FILE *f = fopen(outname,"wb");
if (!f)
{
printf("can't open: %s\n",strerror(errno));
continue;
}
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
if (!png_ptr)
{
puts("libpng init error");
fclose(f);
remove(outname);
continue;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
puts("libpng info error");
png_destroy_write_struct(&png_ptr,NULL);
fclose(f);
remove(outname);
continue;
}
if (setjmp(png_jmpbuf(png_ptr)))
{
png_destroy_write_struct(&png_ptr,&info_ptr);
fclose(f);
remove(outname);
continue;
}
png_init_io(png_ptr,f);
png_set_IHDR(png_ptr,info_ptr,
img_width*8,img_height*8, 8,PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
png_set_PLTE(png_ptr,info_ptr,
pal,8*4);
png_set_rows(png_ptr,info_ptr, bitmap_rows);
png_write_png(png_ptr,info_ptr,0,NULL);
png_destroy_write_struct(&png_ptr,&info_ptr);
fclose(f);
puts("OK");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment