Last active
December 21, 2023 22:42
-
-
Save karmic64/8a6d200cc6c84f57bd443e7b403bc7e8 to your computer and use it in GitHub Desktop.
Shizuku (PC-98) .LFG to .PNG converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
------------------------------------------ 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