If anyone is interested, here's the C program to handle/extract the lb5/idx files and process the images as well.
[code:1]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <math.h>
typedef unsigned int uint;
typedef signed long long int64;
typedef unsigned long long uint64;
typedef signed int int32;
typedef unsigned int uint32;
typedef signed short int16;
typedef unsigned short uint16;
typedef signed char int8;
typedef unsigned char uint8;
const uint8 encryptKey[4]={0x88, 0x88, 0x88, 0x88};
#define mathMinimum2(a, b) ((a) < (b)? (a):(b))
#define mathMinimum3(a,b,c) (mathMinimum2(mathMinimum2((a),(b)), (c)))
#define mathMaximum2(a, b) ((a) > (b)? (a):(b))
#define mathMaximum3(a,b,c) (mathMaximum2(mathMaximum2((a),(b)), (c)))
//Utility function obtained from:
//http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
void colorConvertRgbToHsl(float r, float g, float b, float *oh, float *os, float *ol){
r /= 255.0f;
g /= 255.0f;
b /= 255.0f;
float max = mathMaximum3(r, g, b);
float min = mathMinimum3(r, g, b);
float h=0;
float s=0;
float l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
float d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if(max == r) {
h = (g - b) / d + (g < b ? 6 : 0);
}
else if(max == g) {
h = (b - r) / d + 2;
}
else if(max == b) {
h = (r - g) / d + 4;
}
h /= 6;
}
*oh=h * 255.0f;
*os=s * 255.0f;
*ol=l * 255.0f;
}
//Utility function obtained from:
//http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
float colorConvertHue2rgb(float p, float q, float t){
if(t < 0.0f) t += 1;
if(t > 1.0f) t -= 1;
if(t < 1.0f/6.0f) return p + (q - p) * 6.0f * t;
if(t < 1.0f/2.0f) return q;
if(t < 2.0f/3.0f) return p + (q - p) * (2.0f/3.0f - t) * 6.0f;
return p;
}
//Utility function obtained from:
//http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
void colorConvertHslToRgb(float h, float s, float l, float *or, float *og, float *ob){
float r;
float g;
float b;
h/=255.0f;
s/=255.0f;
l/=255.0f;
if(s == 0){
r = g = b = l; // achromatic
}else{
float q = l < 0.5 ? l * (1 + s) : l + s - l * s;
float p = 2 * l - q;
r = colorConvertHue2rgb(p, q, h + 1.0f/3.0f);
g = colorConvertHue2rgb(p, q, h);
b = colorConvertHue2rgb(p, q, h - 1.0f/3.0f);
}
*or=r * 255.0f;
*og=g * 255.0f;
*ob=b * 255.0f;
}
int main(int argc, const char* argv[] )
{
if(argc < 2)
{
printf("ungos <idx or lb5 file>\n");
return 0;
}
//Get length of file name
uint nameLength=strlen(argv[1]);
//Check if length is valid
if(nameLength < 4) {
printf("error: file name length is too short to be idx/lb5 file format\n");
return 0;
}
//Check for file format
if(strcmp(&argv[1][nameLength-4], ".idx") && strcmp(&argv[1][nameLength-4], ".lb5")) {
printf("error: file name is not idx nor lb5 file\n");
return 0;
}
//Create a copy of file name
uint8 *nameString=malloc(nameLength+1);
if(nameString == NULL) {
printf("error: out of memory... what is this, 1994?\n");
return 0;
}
memcpy(nameString, argv[1], nameLength+1);
//Set pointer to file format in string
uint8 *nameFormat=&nameString[nameLength-3];
//Create the directory
uint8 *dirString=malloc((nameLength-4) + 1 + 15 + 1);
if(dirString == NULL) {
printf("error: out of memory... what is this, 1994?\n");
return 0;
}
memcpy(dirString, argv[1], nameLength-4);
dirString[nameLength-4]=0;
mkdir(dirString);
//Set pointer to file name in directory string
dirString[nameLength-4]='/';
uint8 *dirFile=&dirString[nameLength-3];
//Open the idx file
strcpy(nameFormat, "idx");
FILE *fidx=fopen(nameString, "rb");
if(fidx == NULL) {
//Clean up
free(nameString);
free(dirString);
printf("error: could not open idx file\n");
return 0;
}
//Open the lb5 file
strcpy(nameFormat, "lb5");
FILE *flb5=fopen(nameString, "rb");
if(flb5 == NULL) {
//Clean up
free(nameString);
free(dirString);
fclose(fidx);
printf("error: could not open lb5 file\n");
return 0;
}
//Get the extract count from the idx file
uint extractCount=0;
fread(&extractCount, 1, 4, fidx);
//Extract the files
while(extractCount--) {
uint32 offset;
uint32 size;
//Read the offset
fread(&offset, 1, 4, fidx);
//Read size
fread(&size, 1, 4, fidx);
//Skip unknown byte
fgetc(fidx);
//Read name
fread(dirFile, 1, 15, fidx);
dirFile[15]=0;
//Skip non-test files
//if(strcmp(dirFile, "c013.bmp")) continue; ///////////////////////////////////////////////////////////////////
//if(strcmp(dirFile, "FN002.BMP")) continue;
//if(strcmp(dirFile, "c052.bmp")) continue;
//Open output file
FILE *file=fopen(dirString, "wb");
if(file == NULL) {
printf("warning: couldn't extract %s; skipping\n", dirFile);
continue;
}
//Allocate buffer to load file into memory
uint8 *data=malloc(size);
if(data == NULL) {
//Clean up
free(nameString);
free(dirString);
fclose(fidx);
fclose(flb5);
fclose(file);
printf("error: out of memory... what is this, 1994?\n");
return 0;
}
//Load file into memory
fseek(flb5, offset, SEEK_SET);
fread(data, 1, size, flb5);
//Check if file compressed
if((1==1) && (size >= 16) && (!memcmp(data, encryptKey, 4))) {
printf("decompressing & extracting: %s\n", dirFile);
uint32 imgWidth;
uint32 imgHeight;
uint imgX=0;
uint imgY=0;
uint32 decompressOffset;
uint32 decompressSize;
memcpy(&imgWidth, &data[4], 4);
memcpy(&imgHeight, &data[8], 4);
memcpy(&decompressSize, &data[12], 4);
//Calculate tile map size
uint tileWidth=(imgWidth+7)/8;
uint tileHeight=(imgHeight+7)/8;
uint tileSize=tileWidth * tileHeight;
uint tileCounter=0;
uint paletteOffset=16 + 54 + tileSize;
//Calculate starting offset
decompressOffset=16 + 54 + tileSize + 3*tileSize;
//Check if image can be represented
if(size < 16+54) {
//Free memory
free(data);
//Close output file
fclose(file);
printf("warning: can't decompress, bad data; skipping\n");
continue;
}
//Write header
fwrite(&data[16], 1, 54, file);
//Allocate image
uint8 *image=malloc(imgWidth*imgHeight*3);
memset(image, 0, imgWidth*imgHeight*3);
if(image == NULL) {
//Free memory
free(data);
free(image);
//Close file handles
fclose(fidx);
fclose(flb5);
fclose(file);
printf("error: out of memory... what is this, 1994?\n");
return 0;
}
//Decompress
while(tileCounter < tileSize) {
//Calculate tile limit
uint8 limitX=8;
uint8 limitY=8;
if((imgHeight - imgY) < 8) limitY=imgHeight - imgY;
if((imgWidth - imgX) < 8) limitX=imgWidth - imgX;
//Get dictionary tile type
uint8 tileType=data[16 + 54 + tileCounter];
switch(tileType) {
case 0: //Fill in with color from palette (correct)
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 r=data[paletteOffset + 3*(tileCounter) + 0];
uint8 g=data[paletteOffset + 3*(tileCounter) + 1];
uint8 b=data[paletteOffset + 3*(tileCounter) + 2];
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
break;
case 1: //Palette + 4bpp sat/light (basic)? //[TODO]
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 satlight=data[decompressOffset + tileY*limitX + tileX];
uint lightness=satlight & 0xF0;
uint saturation=(satlight & 0x0F) << 4;
int16 r=data[paletteOffset + 3*(tileCounter) + 0];// + colorOffset;
int16 g=data[paletteOffset + 3*(tileCounter) + 1];// + colorOffset;
int16 b=data[paletteOffset + 3*(tileCounter) + 2];// + colorOffset;
/*if(r<0) r=0;
if(g<0) g=0;
if(b<0) b=0;
if(r>255) r=255;
if(g>255) g=255;
if(b>255) b=255;*/
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=limitX*limitY;
break;
case 2: //Palette? //[TODO]
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 r=data[paletteOffset + 3*(tileCounter) + 0];
uint8 g=data[paletteOffset + 3*(tileCounter) + 1];
uint8 b=data[paletteOffset + 3*(tileCounter) + 2];
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=limitX*limitY;
break;
case 3: //Palette? //[TODO]
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 r=data[paletteOffset + 3*(tileCounter) + 0];
uint8 g=data[paletteOffset + 3*(tileCounter) + 1];
uint8 b=data[paletteOffset + 3*(tileCounter) + 2];
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=limitX*limitY;
break;
case 4: //Palette + 4bpp color offset (correct)
{
uint8 byteBuffer=0;
uint8 byteCount=0;
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
if(byteCount == 0) {
byteBuffer=data[decompressOffset + tileY*limitX/2 + tileX/2];
byteCount=2;
}
uint8 colorOffset=(byteBuffer & 0x0F); //Correct orientation checked, low nibble is leftmost pixel
byteBuffer>>=4;
byteCount--;
uint16 r=data[paletteOffset + 3*(tileCounter) + 0] + colorOffset;
uint16 g=data[paletteOffset + 3*(tileCounter) + 1] + colorOffset;
uint16 b=data[paletteOffset + 3*(tileCounter) + 2] + colorOffset;
if(r>255) r=255;
if(g>255) g=255;
if(b>255) b=255;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=limitX*limitY/2;
break;
case 5: //8bpp grayscale (correct)
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 grayscale=data[decompressOffset + tileY*limitX + tileX];
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=grayscale;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=grayscale;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=grayscale;
tileX++;
}
tileY++;
}
}
decompressOffset+=limitX*limitY;
break;
case 6: //HSL (incorrect) //[TODO]
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
float saturation=data[decompressOffset + 2*(tileY*limitX + tileX) + 0];
float lightness=data[decompressOffset + 2*(tileY*limitX + tileX) + 1];
float r=data[paletteOffset + 3*(tileCounter) + 0];
float g=data[paletteOffset + 3*(tileCounter) + 1];
float b=data[paletteOffset + 3*(tileCounter) + 2];
/*float h,s,l;
colorConvertRgbToHsl(r,g,b, &h, &s, &l);
s+=(saturation - lightness)/2.0f;
l+=2.0f*(255.0f - saturation);
colorConvertHslToRgb(h,s,l, &r, &g, &b);
//printf("%f %f %f\n", r, g, b);
if(r<0) r=0;
if(g<0) g=0;
if(b<0) b=0;
if(r>255) r=255;
if(g>255) g=255;
if(b>255) b=255;*/
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=2*limitX*limitY;
break;
case 7: //RGB (correct)
{
uint8 tileY=0;
while(tileY < limitY) {
uint8 tileX=0;
while(tileX < limitX) {
uint8 r=data[decompressOffset + 3*(tileY*limitX + tileX) + 0];
uint8 g=data[decompressOffset + 3*(tileY*limitX + tileX) + 1];
uint8 b=data[decompressOffset + 3*(tileY*limitX + tileX) + 2];
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+0]=r;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+1]=g;
image[3 * ((imgY + tileY) * imgWidth + (imgX + tileX))+2]=b;
tileX++;
}
tileY++;
}
}
decompressOffset+=3*limitX*limitY;
break;
}
//Move ahead one tile in image
imgX+=8;
if(imgX >= imgWidth) {
imgX=0;
imgY+=8;
}
//Next tile
tileCounter++;
}
//Write image
uint lineCounter=0;
while(lineCounter < imgHeight) {
fwrite(&image[3 * lineCounter * imgWidth], 1, imgWidth*3, file);
uint padding=4-((3*imgWidth) % 4);
if(padding == 4) padding=0;
fseek(file, padding, SEEK_CUR);
lineCounter++;
}
//Debug line
fwrite(&data[70], tileSize+3*tileSize + 8*8*3,1,file);
//Free memory
free(image);
}
else {
//Write file
printf("extracting: %s\n", dirFile);
fwrite(data, 1, size, file);
}
//Free memory
free(data);
//Close output file
fclose(file);
}
//Close the file handles
fclose(fidx);
fclose(flb5);
//Free file name
free(nameString);
free(dirString);
system("pause");
return -1;
}
[/code:1]
One of the game archives after unpacking seems to contain a bunch of text files.
I assume the text files are script files that control the scene animations and dialog that appears.
Though, I have no idea if they're using a standard Japanese text encoding, or a custom one.
If you want to experiment with one of the text files to try to find the right encoding, here's its hex data:
The game also stores some of the characters as separate files from the backgrounds, so it's kind of nice to be able to take out the sprites and post them places, like:
Though I still can't believe you sat there print-screening the game little by little!