Skip to content

Instantly share code, notes, and snippets.

@karmic64
Last active December 21, 2023 22:42
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/8a6d200cc6c84f57bd443e7b403bc7e8 to your computer and use it in GitHub Desktop.
Save karmic64/8a6d200cc6c84f57bd443e7b403bc7e8 to your computer and use it in GitHub Desktop.
Shizuku (PC-98) .LFG to .PNG converter
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <setjmp.h>
#include <png.h>
int fget16(FILE *f)
{
int v = 0;
for (int b = 0; b < 16; b+=8)
{
int c = fgetc(f);
if (c == EOF) return EOF;
v |= c << b;
}
return v;
}
int fget32(FILE *f)
{
int v = 0;
for (int b = 0; b < 32; b+=8)
{
int c = fgetc(f);
if (c == EOF) return EOF;
v |= c << b;
}
return v;
}
void png_warn(png_structp png, png_const_charp msg)
{
printf("libpng warning: %s\n", msg);
}
void png_err(png_structp png, png_const_charp msg)
{
printf("libpng error: %s\n", msg);
}
int main(int argc, char *argv[])
{
if (argc < 2)
{
puts("usage: lfg infile...");
return EXIT_FAILURE;
}
for (int arg = 1; arg < argc; arg++)
{
if (arg != 1)
puts("");
FILE *inf = NULL;
FILE *of = NULL;
char *outname = NULL;
uint8_t *depack = NULL;
png_byte *image = NULL;
png_byte **image_rows = NULL;
/************************** read lfg ***************************/
const char *inname = argv[arg];
const size_t innamelen = strlen(inname);
printf("Converting %s...\n", inname);
/** open **/
inf = fopen(inname,"rb");
of = NULL;
if (!inf)
{
printf("Can't open: %s\n",strerror(errno));
goto next_file;
}
/** check sig **/
char filesig[8];
fread(filesig,1,8,inf);
if (memcmp(filesig,"LEAFCODE",8))
{
puts("Bad signature");
goto next_file;
}
/** read palette **/
png_color palette[0x10];
for (int nib = 0; nib < 0x18*2; nib++)
{
static int b;
if (!(nib & 1))
{
b = fgetc(inf);
}
int index = nib / 3;
int component = nib % 3;
unsigned color;
if (nib & 1)
{
color = b & 0x0f;
}
else
{
color = b >> 4;
}
color |= color<<4;
switch (component)
{
case 0:
palette[index].red = color;
break;
case 1:
palette[index].green = color;
break;
case 2:
palette[index].blue = color;
break;
}
}
/** set up dimensions **/
int dim1_sub = fget16(inf);
int dim2_sub = fget16(inf);
int dim1 = fget16(inf) - dim1_sub;
int dim2 = fget16(inf) - dim2_sub;
int orientation = fgetc(inf);
int bitplanes = fgetc(inf);
/*int u5 =*/ fgetc(inf);
/*int u6 =*/ fgetc(inf);
unsigned width;
unsigned height;
unsigned byte_rows;
unsigned byte_row_size;
switch (orientation)
{
case 0:
{
byte_rows = (dim1 + 1) * 4;
byte_row_size = dim2 + 1;
width = byte_rows * 2;
height = byte_row_size;
break;
}
case 1:
{
byte_row_size = (dim1 + 1) * 4;
byte_rows = dim2 + 1;
width = byte_row_size * 2;
height = byte_rows;
break;
}
default:
{
printf("Bad orientation value $%02X\n",orientation);
goto next_file;
}
}
/** decompress image data **/
size_t required_depack_size = byte_row_size * byte_rows;
int reported_depack_size = fget32(inf);
depack = malloc(required_depack_size);
size_t depack_size = 0;
uint8_t depack_buf[0x1000];
memset(depack_buf,0,sizeof(depack_buf));
unsigned depack_index = 0xfee;
int block_bit = 0;
int block_flags = 0;
while (1)
{
if (--block_bit < 0)
{
block_flags = fgetc(inf);
if (block_flags == EOF) break;
block_bit = 7;
}
if (block_flags & (1 << block_bit))
{
int data = fgetc(inf);
if (data == EOF || depack_size >= required_depack_size) break;
depack[depack_size++] = data;
depack_buf[depack_index++] = data;
depack_index &= 0xfff;
}
else
{
int b1 = fgetc(inf);
int b2 = fgetc(inf);
if (b1 == EOF || b2 == EOF) break;
int copy_size = (b1 & 0x0f) + 3;
int depack_src = (b1 | (b2<<8)) >> 4;
while (copy_size--)
{
if (depack_size >= required_depack_size) break;
int data = depack_buf[depack_src++];
depack[depack_size++] = data;
depack_buf[depack_index++] = data;
depack_src &= 0xfff;
depack_index &= 0xfff;
}
if (depack_size >= required_depack_size) break;
}
}
fclose(inf);
inf = NULL;
if (depack_size < required_depack_size)
{
printf("Image does not have enough data (want $%04zX bytes, has $%04zX bytes)\n",required_depack_size,depack_size);
goto next_file;
}
else if (reported_depack_size > required_depack_size)
{
printf("note: Ignoring excess data (want $%04zX bytes, has $%04X bytes)\n",required_depack_size,reported_depack_size);
}
/** transform data to image **/
image = malloc(sizeof(*image) * width * height);
image_rows = malloc(sizeof(*image_rows) * height);
for (unsigned row = 0; row < height; row++)
{
image_rows[row] = image + (width * row);
}
for (unsigned byte_row = 0; byte_row < byte_rows; byte_row++)
{
for (unsigned byte_col = 0; byte_col < byte_row_size; byte_col++)
{
unsigned b = depack[(byte_row * byte_row_size) + byte_col];
unsigned c1 = 0;
unsigned c2 = 0;
for (unsigned bit = 0; bit < 8; bit++)
{
unsigned * v = (bit & 1) ? &c2 : &c1;
if (b & (1<<bit))
{
*v |= 1 << (bit>>1);
}
}
switch (orientation)
{
case 0:
{
image_rows[byte_col][byte_row*2] = c2;
image_rows[byte_col][(byte_row*2)+1] = c1;
break;
}
case 1:
{
image_rows[byte_row][byte_col*2] = c2;
image_rows[byte_row][(byte_col*2)+1] = c1;
break;
}
}
}
}
free(depack);
depack = NULL;
/******************************* write png ************************/
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, png_err, png_warn);
if (!png_ptr)
{
puts("Can't create PNG struct");
goto next_file;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
puts("Can't create PNG info");
png_destroy_write_struct(&png_ptr, NULL);
goto next_file;
}
outname = malloc(innamelen+5);
memcpy(outname, inname, innamelen);
memcpy(outname+innamelen, ".png", 5);
of = fopen(outname,"wb");
free(outname);
outname = NULL;
if (!of)
{
printf("Can't open output: %s\n",strerror(errno));
goto next_file;
}
if (setjmp(png_jmpbuf(png_ptr)))
{
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(of);
of = NULL;
remove(outname);
goto next_file;
}
else
{
png_init_io(png_ptr, of);
png_set_IHDR(png_ptr,info_ptr,width,height,4,PNG_COLOR_TYPE_PALETTE,PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT);
png_set_PLTE(png_ptr, info_ptr, palette, 0x10);
png_set_rows(png_ptr, info_ptr, image_rows);
png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_PACKING, NULL);
fclose(of);
of = NULL;
puts("Export OK.");
}
/******************************* next ****************************/
next_file:
if (outname) free(outname);
if (depack) free(depack);
if (image) free(image);
if (image_rows) free(image_rows);
if (inf) fclose(inf);
if (of) fclose(of);
}
return EXIT_SUCCESS;
}
------------------------------------------ MUSIC DATA
both FM and MD files are compressed using the same scheme
dword - uncompressed file size
!! from here on, all data is one's-complemented !!
for each 8 decompression blocks:
byte - block types for next 8 blocks (upper bit first)
for each decompression block:
if block type = 0:
word - backref
bits 0-3: data copy size - 3
bits 15-4: lookback buffer index
if block type = 1:
byte - data
--------------------------------------------- GRAPHICS DATA
8 bytes - id "LEAFCODE", this is checked
$18 bytes - 16-color palette
the data is spread across the nybbles. each color has 4-bit r -> g -> b components
within a byte, the high nybble is first
word - dimension 1 subtract value. these are subtracted FROM THE HEADER VALUE, BEFORE ANY CALCULATION
word - dimension 2 subtract value
word - image dimension 1
if orientation is 0:
(amount of byte rows in image data / 4) - 1
if orientation is 1:
(length of data in one byte row / 4) - 1
word - image dimension 2
if orientation is 0:
length of data in one byte row - 1
if orientation is 1:
amount of byte rows in image data - 1
byte - image byte orientation
0: column by column
1: row by row
both orientations are top to bottom, left to right
values 2 and greater are rejected
byte - active bitplanes (some images have the upper 4 bits set? for now ignore them)
2 bytes - ?
dword - decompressed data size
following data is the same as compressed music data, but NOT obfuscated
image data is 4bpp indexed. within each byte there are two pixels. the bits of each pixel pair's color are interleaved. starting from the lower bit, alternate, from right to left, between the lowest to highest bits of the pixels of the pair
---------------------------------------------- CODE
important strings (LEAFCODE etc.) start at 1a3f:0a1c
routine 0ee1:0006 does the whole song and dance of loading a file to memory
routine at 0923:02d7 reads data from a file handle to a buffer
midi music variables start at 216f:0700
midi song data starts at 2a35:0000
midi song data load/init routine is at 109f:0006
routine at 1093:000e does main decompression
0f2c:15b4 is the REAL decompressor:
zeroes out a $1011-byte decompression buffer (though the game always and's the index with $0fff)
data is decompressed from DS:SI to ES:DI
initial decompression buffer index is $0fee (in BP)
background graphics loader is at 140b:0000
routine at 0f2c:104c handles uploading a loaded LFG to vram
routine at 0f2c:13dc reads 4 byte-rows of buffered data and uploads it to vram
character graphics loader at 1507:000c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment