diff --git a/tools/sc1/sc1-decomp/Makefile b/tools/sc1/sc1-decomp/Makefile new file mode 100644 index 000000000..ae08b44b8 --- /dev/null +++ b/tools/sc1/sc1-decomp/Makefile @@ -0,0 +1,9 @@ +TARGET := decomp +CFILES := ../../shared/util.c huff.c decomp.c lztfb.c +HFILES := ../../shared/cbytesex.h ../../shared/util.h dostypes.h huff.h lztfb.h getbit.h +CFLAGS := -std=c99 +DEBUG := 1 +#ERROR := 1 + +include ../../shared/Makefile.default + diff --git a/tools/sc1/sc1-decomp/decomp.c b/tools/sc1/sc1-decomp/decomp.c new file mode 100644 index 000000000..1c202a8bb --- /dev/null +++ b/tools/sc1/sc1-decomp/decomp.c @@ -0,0 +1,156 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "huff.h" +#include "lztfb.h" +#include "../../shared/util.h" + +#include +#include +#include +#include +#include +#include + + +struct options { + char *inFile; + char *outFile; + bool testOnly; +}; + +void usage(FILE *out); +void parse_arguments(int argc, char *argv[], struct options *opts); +bool testCompressed(const char *fileName); + +int +main(int argc, char *argv[]) { + void *buf; + size_t size; + bool compressed; + FILE *out; + + struct options opts; + parse_arguments(argc, argv, &opts); + + if (opts.testOnly) { + return testCompressed(opts.inFile) ? EXIT_SUCCESS : EXIT_FAILURE; + } else if (opts.outFile == NULL) { + logError(false, "Either -o or -t needs to be specified.\n"); + exit(EXIT_FAILURE); + } + + if (mmapOpen(opts.inFile, O_RDONLY, &buf, &size) == -1) + fatal(true, "mmapOpen() failed.\n"); + + if (((char *) buf)[0] == 6 && (((char *) buf)[1] & ~0x6) == 0) { + compressed = true; + } else { + compressed = false; + if (getU32BE((char *) buf + 2) != size - 6) + fatal(false, "File '%s' is not in a recognised format.\n", + opts.inFile); + } + + out = fopen(opts.outFile, "w"); + if (out == NULL) + fatal(true, "fopen() failed.\n"); + + if (compressed) { + LZTFB *lztfb = LZTFB_new(buf, size); + if (lztfb == NULL) + fatal(false, "LZTFB_new() failed.\n"); + + if (LZTFB_output(lztfb, out) == -1) + fatal(true, "LZTFB_output() failed.\n"); + + LZTFB_delete(lztfb); + } else { + if (fwrite((char *) buf + 6, 1, size - 6, out) != size - 6) + fatal(true, "fwrite() failed.\n"); + } + + (void) fclose(out); + munmap(buf, size); + + (void) argc; + (void) argv; + return EXIT_SUCCESS; +} + +void +usage(FILE *out) { + fprintf(out, "Syntax:\n" + "decomp -o \n" + "decomp -t \n" + "\t-o decompress to outfile\n" + "\t-t only test whether the file is compressed.\n" + "\t returns 0 if compressed, and 1 if not compressed\n"); +} + +void +parse_arguments(int argc, char *argv[], struct options *opts) { + char ch; + + memset(opts, '\0', sizeof (struct options)); + while (1) { + ch = getopt(argc, argv, "ho:t"); + if (ch == -1) + break; + switch(ch) { + case 'o': + opts->outFile = optarg; + break; + case '?': + case 'h': + usage(stdout); + exit(EXIT_SUCCESS); + case 't': + opts->testOnly = true; + break; + default: + usage(stderr); + exit(EXIT_FAILURE); + } + } + argc -= optind; + argv += optind; + if (argc != 1) { + usage(stderr); + exit(EXIT_FAILURE); + } + opts->inFile = argv[0]; +} + +bool +testCompressed(const char *fileName) { + FILE *file; + + file = fopen(fileName, "rb"); + if (file == NULL) + return false; + + uint8_t buf[6]; + if (fread(buf, 1, 6, file) != 6) { + fclose(file); + return false; + } + fclose(file); + + return buf[0] == 6 && (buf[1] & ~0x6) == 0; +} + + diff --git a/tools/sc1/sc1-decomp/decomp.sh b/tools/sc1/sc1-decomp/decomp.sh new file mode 100755 index 000000000..03a65f8ff --- /dev/null +++ b/tools/sc1/sc1-decomp/decomp.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +DECOMP=./decomp + +for FILE in *[23459a]; do + $DECOMP -o "$FILE.out" "$FILE" +done + diff --git a/tools/sc1/sc1-decomp/dostypes.h b/tools/sc1/sc1-decomp/dostypes.h new file mode 100644 index 000000000..78e8ce62c --- /dev/null +++ b/tools/sc1/sc1-decomp/dostypes.h @@ -0,0 +1,31 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _DOSTYPES_H +#define _DOSTYPES_H + +#include +#include + +typedef uint8_t BYTE; +typedef int8_t SBYTE; +typedef uint16_t WORD; +typedef int16_t SWORD; +typedef uint32_t DWORD; +typedef int32_t SDWORD; + +#endif /* _DOSTYPES_H */ + diff --git a/tools/sc1/sc1-decomp/getbit.h b/tools/sc1/sc1-decomp/getbit.h new file mode 100644 index 000000000..7118b3971 --- /dev/null +++ b/tools/sc1/sc1-decomp/getbit.h @@ -0,0 +1,50 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _GETBIT_H +#define _GETBIT_H + +static inline WORD +haveBit(BitStreamContext *bsc) { + return BSC_haveBit_OPAB(bsc); +} + +static inline WORD +getBit(BitStreamContext *bsc) { + if (haveBit(bsc)) + return BSC_getBit_OPAB(bsc); + + bsc->eof = true; + return 0; +} + +static inline WORD +haveBits(BitStreamContext *bsc, BYTE count) { + return BSC_haveBits_OPAB(bsc, count); +} + +// On EOF, this outputs garbage; this is how the original TFB code worked. +static inline WORD +getBits(BitStreamContext *bsc, BYTE count) { + if (haveBits(bsc, count)) + return BSC_getBits_OPAB(bsc, count); + + bsc->eof = true; + return BSC_getBits_OPAB(bsc, BSC_bitsLeft_OPAB(bsc)); +} + +#endif /* _GETBIT_H */ + diff --git a/tools/sc1/sc1-decomp/huff.c b/tools/sc1/sc1-decomp/huff.c new file mode 100644 index 000000000..de780d8e2 --- /dev/null +++ b/tools/sc1/sc1-decomp/huff.c @@ -0,0 +1,344 @@ +// Reverse engineered from the Star Control executable. + +#include "huff.h" +#include "../../shared/cbytesex.h" +#include "getbit.h" + +#include +#include + +static bool Huff_compareCodes(const Huff_Code *srcCodePtr, + const Huff_Code *destCodePtr); +static void Huff_sortAndStuff(int flag, Huff_Context *ctx); +static void Huff_sub_22334(Huff_Context *ctx); +static void Huff_reverseCodeBits(Huff_Context *ctx); +static bool Huff_readTable(Huff_Context *ctx); +static bool Huff_lookupCodeLarge(Huff_Context *ctx, DWORD *result); +static bool Huff_lookupCodeSmall(Huff_Context *ctx, DWORD *result); + +Huff_Context * +Huff_new(BitStreamContext *bsc, WORD codeCount) { + Huff_Context *result = malloc(sizeof (Huff_Context)); + + result->codeCount = codeCount; + result->bsc = bsc; + + if (!Huff_readTable(result)) + goto err; + Huff_sortAndStuff(0, result); + Huff_sub_22334(result); + Huff_reverseCodeBits(result); + Huff_sortAndStuff(1, result); + + return result; + +err: + free (result); + return NULL; +} + +void +Huff_delete(Huff_Context *ctx) { + free(ctx); +} + +// Returns true if we need to swap. +static bool +Huff_compareCodes(const Huff_Code *srcCodePtr, const Huff_Code *destCodePtr) { + // 2203:016a + + if (destCodePtr->len < srcCodePtr->len) + return true; + + if (destCodePtr->len > srcCodePtr->len) + return false; + + if (destCodePtr->field_0 < srcCodePtr->field_0) + return true; + + if (destCodePtr->field_0 > srcCodePtr->field_0) + return false; + + if (destCodePtr->value < srcCodePtr->value) + return true; + + return false; +} + +static void +Huff_sortAndStuff(int flag, Huff_Context *ctx) { + // 2203:00c6 + if (flag != 0 && ctx->maxCodeLen <= 8) { + // ctx->codes is reordered so that + // ctx->codes[i] == ctx->codes[i]->field_0 + + // 2203:00d3 + ctx->u.topCodeIndexLen = ctx->codes[0].len; + + WORD codeI = 0; + if (ctx->codeCount == 0) + return; + + // 2203:00e9 + Huff_Code *codePtr = &ctx->codes[0]; + + do { + // 2203:011a + // Put *codePtr in its place by swapping it with the Huff_Code + // structure where it needs to go. + // We continue doing this as long as the new value of *codePtr + // is not yet in its place. + while (codePtr->field_0 != codeI && + /* 2203:00ed */ codePtr->len != 0) { + // 2203:00f3 + // Swap ctx->codes[codePtr->field_0] and *codePtr + Huff_Code *otherCode = &ctx->codes[codePtr->field_0]; + Huff_Code tempCode = *codePtr; + *codePtr = *otherCode; + *otherCode = tempCode; + // 2203:011a + } + + // 2203:0121 + codePtr++; + codeI++; + } while (codeI < ctx->codeCount); + + return; + } else { + // Sorting the array of codes (using comb sort). + // The codes with the smaller code length are put first. + + // 2203:0139 + WORD si = ctx->codeCount / 2; + + for (;;) { + // 2203:0143 + bool noSwapThisRound = true; + + if (si >= ctx->codeCount) + goto loc_22200; + + // 2203:0158 + Huff_Code *srcCodePtr = &ctx->codes[0]; + Huff_Code *destCodePtr = &ctx->codes[si]; + + WORD cx = 0; + do { + // 2203:016a + if (Huff_compareCodes(srcCodePtr, destCodePtr)) { + // 2203:018e + Huff_Code tempCode = *srcCodePtr; + *srcCodePtr = *destCodePtr; + *destCodePtr = tempCode; + + noSwapThisRound = false; + } + + // 2203:01b3 + srcCodePtr++; + destCodePtr++; + cx++; + } while (cx < ctx->codeCount - si); + +loc_22200: + // 2203:01d0 + if (!noSwapThisRound) + continue; + + si /= 2; + if (si <= 0) + break; + } + + // 2203:01e2 + if (flag == 0) + return; + + si = 0; + for (WORD cx = 0; cx < ctx->codeCount; cx++) { + // Keep si from previous round. + while (si < ctx->codes[cx].len) { + // 2203:0200 + ctx->u.codeIndices[si] = cx; + si++; + } + } + } +} + +// Pre: codes are sorted on code length (smallest first). +static void +Huff_sub_22334(Huff_Context *ctx) { + // 2203:030f + WORD dx = 0; + WORD var_6 = 0; + WORD prevCodeLen = 0; + + WORD codeI = ctx->codeCount; + Huff_Code *codePtr = &ctx->codes[codeI - 1]; + + while (codeI != 0) { + // 2203:032f + dx += var_6; + if (codePtr->len != prevCodeLen) { + // 2203:033d + prevCodeLen = codePtr->len; + var_6 = 1 << (16 - codePtr->len); + } + + codePtr->field_0 = dx; + + codePtr--; + codeI--; + } +} + +static void +Huff_reverseCodeBits(Huff_Context *ctx) { + Huff_Code *codePtr = &ctx->codes[0]; + + for (WORD codeI = ctx->codeCount; codeI != 0; codeI--) { + // 2203:0376 + WORD wordToReverse = codePtr->field_0; + WORD rightBit = 1; + WORD leftBit = 0x8000; + WORD reversedWord = 0; + + BYTE bitI = 16; + do { + // 2203:0390 + if (wordToReverse & rightBit) + reversedWord |= leftBit; + + leftBit >>= 1; + rightBit <<= 1; + bitI--; + } while (bitI != 0); + + codePtr->field_0 = reversedWord; + codePtr++; + } +} + +static bool +Huff_readTable(Huff_Context *ctx) { + // 2203:022b + if (!haveBits(ctx->bsc, 8)) + return false; + WORD lengthCount = getBits(ctx->bsc, 8) + 1; + // Number of lengths for codes in this table. + + // 2203:025d + WORD index = 0; + ctx->maxCodeLen = 0; + + // 2203:026a + while (lengthCount != 0) { + // 2203:0271 + if (index >= 0x100 || !haveBits(ctx->bsc, 8)) + return false; + WORD dx = getBits(ctx->bsc, 8); + + WORD thisCodeLenCount = highU4(dx) + 1; + // Number of codes with this length + WORD codeLen = lowU4(dx) + 1; + + // 2203:02af (changed order; no reason for this to be in the while + // loop) + if (codeLen > ctx->maxCodeLen) { + // 2203:02b9 + ctx->maxCodeLen = codeLen; + } + + // 2203:02ab + // Prepare 'thisCodeLenCount' codes with the specified code length. + while (thisCodeLenCount != 0) { + Huff_Code *codePtr = &ctx->codes[index]; + codePtr->len = codeLen; + codePtr->value = index; + codePtr->field_0 = 0; + + index++; + thisCodeLenCount--; + }; + + // 2203:02e1 + lengthCount--; + } + + // 2203: 02e6 + if (ctx->maxCodeLen <= 8) { + ctx->lookupCodeFunc = Huff_lookupCodeSmall; + } else + ctx->lookupCodeFunc = Huff_lookupCodeLarge; + + return true; +} + +// Huffman decoder for codes of arbitrary length. +static bool +Huff_lookupCodeLarge(Huff_Context *ctx, DWORD *result) { + // 2203:03f2 + Huff_Code *codePtr = &ctx->codes[0]; + + // 2203:03f8 + WORD si = getBits(ctx->bsc, codePtr->len); + BYTE ch = codePtr->len; + BYTE cl; + + // 2203:0429 + while (codePtr->field_0 != si) { + if (codePtr->field_0 < si) { + // 2203:042f + codePtr++; + cl = codePtr->len - ch; + if (cl == 0) + continue; + } else { + // 2203:0459 + WORD ax = (WORD) ctx->u.codeIndices[ch]; + codePtr = &ctx->codes[ax]; + cl = codePtr->len - ch; + } + + // 2203:043d and 2203:0479 + WORD ax = getBits(ctx->bsc, cl); + + // 2203:049cd + si |= (ax << ch); + ch += cl; + } + + *result = codePtr->value; + return true; +} + +// Table-based huffman decoding. +// Code lengths are no longer than 8 bits. +// ctx->codes[]->value contains the value produced. +static bool +Huff_lookupCodeSmall(Huff_Context *ctx, DWORD *result) { + // 2203:04b4 + WORD codeI = getBits(ctx->bsc, ctx->u.topCodeIndexLen); + WORD haveCodeBits = ctx->u.topCodeIndexLen; + + // 2203:04f1 + Huff_Code *codePtr; + for (;;) { + codePtr = &ctx->codes[codeI]; + if (haveCodeBits == codePtr->len) + break; + + WORD ax = getBit(ctx->bsc); + + // 2203:051c + codeI |= (ax << haveCodeBits); + haveCodeBits++; + } + + *result = codePtr->value; + return true; +} + + diff --git a/tools/sc1/sc1-decomp/huff.h b/tools/sc1/sc1-decomp/huff.h new file mode 100644 index 000000000..23551fc2b --- /dev/null +++ b/tools/sc1/sc1-decomp/huff.h @@ -0,0 +1,44 @@ +// Reverse engineered from the Star Control executable. + +#ifndef _HUFF_H +#define _HUFF_H + +typedef struct Huff_Code Huff_Code; +typedef struct Huff_Context Huff_Context; + +#include "dostypes.h" +#include "../../shared/cbytesex.h" + + +struct Huff_Code { + WORD field_0; + BYTE value; + BYTE len; +}; + +struct Huff_Context { + Huff_Code codes[0x100]; + WORD codeCount; + WORD maxCodeLen; + bool (*lookupCodeFunc)(Huff_Context *ctx, DWORD *result); + + BitStreamContext *bsc; + + union { + BYTE topCodeIndexLen; + // Number of bits in the index of the top code. + BYTE codeIndices[16]; + // Index into 'codes' to the first code with some length. + } u; +}; + +Huff_Context *Huff_new(BitStreamContext *bsc, WORD codeCount); +void Huff_delete(Huff_Context *ctx); + +static inline bool +Huff_getCode(Huff_Context *ctx, DWORD *result) { + return ctx->lookupCodeFunc(ctx, result); +} + +#endif /* _HUFF_H */ + diff --git a/tools/sc1/sc1-decomp/lztfb.c b/tools/sc1/sc1-decomp/lztfb.c new file mode 100644 index 000000000..117b4faeb --- /dev/null +++ b/tools/sc1/sc1-decomp/lztfb.c @@ -0,0 +1,336 @@ +// Reverse engineered from the Star Control executable. + +#include "lztfb.h" + +#include "../../shared/cbytesex.h" +#include "../../shared/util.h" +#include "getbit.h" + +#include +#include +#include +#include +#include +#include + +static bool LZTFB_processMore(LZTFB *lztfb); + + +LZTFB * +LZTFB_new(const char *buf, size_t len) { + LZTFB *result = NULL; + + if (len < 6) + goto err; + + if (buf[0] != 0x06) { + // Not a compressed file. + goto err; + } + + result = malloc(sizeof (LZTFB)); + result->huffTable[0] = NULL; + result->huffTable[1] = NULL; + result->huffTable[2] = NULL; + + result->state = 0; + result->stateData.offset = 0; + result->stateData.count = 0; + + result->compressedSize = len; + result->uncompressedSize = getU32BE(buf + 2); + + if ((buf[1] & 2) != 0) { + result->table2Shift = 7; + } else + result->table2Shift = 6; + + BSC_init(&result->bsc, buf + 6, len); + + if ((buf[1] & 4) != 0) { + // Literal bytes are huffman encoded. + result->lengthBias = 3; + result->literalsEncoded = true; + } else { + // Literal bytes are read directly from the input. + result->lengthBias = 2; + result->literalsEncoded = false; + result->huffTable[0] = NULL; + } + + memset(result->buf, '\0', LZTFB_BUF_SIZE); + result->bufPtr = result->buf; + result->bufFill = 0; + result->lastBufFill = 0; + + return result; +err: + if (result != NULL) + LZTFB_delete(result); + return NULL; +} + +void +LZTFB_delete(LZTFB *lztfb) { + for (int tableI = 0; tableI < 3; tableI++) { + if (lztfb->huffTable[0] != NULL) + Huff_delete(lztfb->huffTable[0]); + } + + free(lztfb); +} + +int +LZTFB_output(LZTFB *lztfb, FILE *out) { + // 21e0:00aa + size_t toOutput = lztfb->uncompressedSize; + + while (toOutput > 0) { + lztfb->bufPtr = lztfb->buf; + lztfb->bufFill = 0; + + if (!LZTFB_processMore(lztfb)) { + logError(false, "LZTFB_processMore failed.\n"); + errno = EIO; + return -1; + } + lztfb->lastBufFill = lztfb->bufFill; + + size_t toWrite = lztfb->bufFill; + if (toWrite > toOutput) + toWrite = toOutput; + size_t written = fwrite(lztfb->buf, 1, toWrite, out); + if (written != toWrite) { + assert(ferror(out)); + logError(true, "fwrite() failed.\n"); + return -1; + } + toOutput -= written; + } + + return 0; +} + +// Memcopy from left to right. (memcpy doesn't guarantee this, and memmove +// does it differently) +static inline void +memcpyLtr(void *dest, void *src, size_t size) { + while (size--) { + *((uint8_t *) dest) = *((uint8_t *) src); + src = ((uint8_t *) src) + 1; + dest = ((uint8_t *) dest) + 1; + } +} + +static bool +LZTFB_processMore(LZTFB *lztfb) { + // 2203:0555 + int32_t offset = lztfb->stateData.offset; + uint32_t count = lztfb->stateData.count; + lztfb->stateData.count = 0; + uint8_t table2Shift = lztfb->table2Shift; + uint8_t lengthBias = lztfb->lengthBias; + + if (lztfb->state == 0) { + if (lztfb->literalsEncoded) { + lztfb->huffTable[0] = Huff_new(&lztfb->bsc, 0x100); + if (lztfb->huffTable[0] == NULL) + goto err; + } + + lztfb->huffTable[1] = Huff_new(&lztfb->bsc, 0x40); + if (lztfb->huffTable[1] == NULL) + goto err; + + lztfb->huffTable[2] = Huff_new(&lztfb->bsc, 0x40); + if (lztfb->huffTable[2] == NULL) + goto err; + + goto state1; + } + + if (lztfb->state == 1) { +state1: + // State 1: read more data and decide what to do. + + // The original code flagged EOF when it needed to read a new + // byte, and this wasn't possible. But while EOF it would + // still return undefined bytes. + // Because there may be a couple of bits left at the end of the + // stream (as the stream contains bytes) we can't know whether + // the end of the stream has been reached as long as we still + // have a couple of bits left. So we handle EOF the same way. + if (lztfb->bsc.eof) + return true; + + int bit = getBit(&lztfb->bsc); + if (bit != 0) { + // Literal byte. + if (lztfb->huffTable[0] == NULL) { + // Literal byte is read directly from the input. + *lztfb->bufPtr = getBits(&lztfb->bsc, 8); + } else { + // Literal byte is Huffman-encoded. + uint32_t dummy; + if (!Huff_getCode(lztfb->huffTable[0], &dummy)) + goto err; + *lztfb->bufPtr = (uint8_t) dummy; + } + + lztfb->bufPtr++; + lztfb->bufFill++; + if (lztfb->bufFill == LZTFB_BUF_SIZE) { + lztfb->state = 1; + return true; + } + goto state1; + } + + offset = getBits(&lztfb->bsc, table2Shift); + + uint32_t ax; + if (!Huff_getCode(lztfb->huffTable[2], &ax)) + goto err; + ax = (ax << table2Shift) | offset; + offset = lztfb->lastBufFill + lztfb->bufFill - ax; + // This looks like a bug; if lztfb->bufFill != + // LZTFB_BUF_SIZE, the offset is wrong. + + if (!Huff_getCode(lztfb->huffTable[1], &ax)) + goto err; + ax += lengthBias; + count = ax; + + if (ax == lengthBias + 0x3fU) { + // 6 bits was not enough; read 8 more. + count += getBits(&lztfb->bsc, 8); + } + + offset--; + if (offset < 0) { + goto state2; + } else + goto state3; + } + + if (lztfb->state == 2) { +state2: + // State 2: repeat '\0' a number of times. + // offset == -repeatCount + + // 2203:0588 + // Never more than count bytes. + if (offset < (int32_t) -count) + offset = -count; + // This means count will become 0 in the next line. + + count += offset; + // offset is negative; count gets smaller + + do { + uint32_t repeatCount = -offset; + if (repeatCount + lztfb->bufFill <= LZTFB_BUF_SIZE) { + // Enough room for repeatCount more characters. + memset(lztfb->bufPtr, '\0', repeatCount); + lztfb->bufFill += repeatCount; + lztfb->bufPtr += repeatCount; + offset = 0; + break; + } + + lztfb->stateData.count = repeatCount - + (LZTFB_BUF_SIZE - lztfb->bufFill); + offset += (LZTFB_BUF_SIZE - lztfb->bufFill); + // offset was negative; it now contains the negation of + // the number of bytes that still fit in the buffer. + } while (offset != 0); + + if (lztfb->bufFill != LZTFB_BUF_SIZE) { + if (count == 0) + goto state1; + + // count bytes will be copied from the start of the buffer. + // I don't get it. + goto state3; + } + // Buffer is full + + if (count != 0 || lztfb->stateData.count != 0) { + lztfb->stateData.offset = -lztfb->stateData.count; + lztfb->stateData.count += count; + lztfb->state = 2; + // Next time, continue where we left off. + return true; + } + + lztfb->state = 1; + // Next time, start by reading more data. + return true; + } + +state3: + // State 3: Backreference to an earlier piece of data + // offset is the offset of the earlier data + // count is the length. + + offset &= LZTFB_BUF_SIZE - 1; + // offset %= LZTFB_BUF_SIZE + + for (;;) { + if (count + lztfb->bufFill > LZTFB_BUF_SIZE) { + // There's no room in the buffer for this many bytes. Adjust + // the size, and save the rest for later. + + lztfb->stateData.count = + count - (LZTFB_BUF_SIZE - lztfb->bufFill); + count = LZTFB_BUF_SIZE - lztfb->bufFill; + if (count == 0) { + // Buffer was completely full. + break; + } + } + + if (count + offset <= LZTFB_BUF_SIZE) { + // All the data that is referenced comes from the previous + // call to this function. + // Copy lztfb[offset..(offset + count)] to bufPtr. + memcpyLtr(lztfb->bufPtr, &lztfb->buf[offset], count); + lztfb->bufPtr += count; + lztfb->bufFill += count; + break; + } + + // Copy lztfb[offset..] to bufPtr. The rest of the data to be copied + // comes from the data we wrote this call. + memcpyLtr(lztfb->bufPtr, &lztfb->buf[offset], + LZTFB_BUF_SIZE - offset); + lztfb->bufPtr += LZTFB_BUF_SIZE - offset; + lztfb->bufFill += LZTFB_BUF_SIZE - offset; + count -= LZTFB_BUF_SIZE - offset; + offset = 0; + } + // count contains the number of bytes written since offset was last + // adjusted. + + if (lztfb->bufFill != LZTFB_BUF_SIZE) + goto state1; + + if (lztfb->stateData.count == 0) { + lztfb->state = 1; + // Next time, start by reading new data. + return true; + } else { + lztfb->stateData.offset = offset + count; + // offset was not yet adjusted with count after the last write + lztfb->state = 3; + // Next time, continue where we left off. + return true; + } + +err: + return false; +} + + + + diff --git a/tools/sc1/sc1-decomp/lztfb.h b/tools/sc1/sc1-decomp/lztfb.h new file mode 100644 index 000000000..b273837b3 --- /dev/null +++ b/tools/sc1/sc1-decomp/lztfb.h @@ -0,0 +1,44 @@ +// Reverse engineered from the Star Control executable. + +#ifndef _LZTFB +#define _LZTFB + +#include +#include + +typedef struct LZTFB LZTFB; + +#include "../../shared/cbytesex.h" +#include "huff.h" + +#define LZTFB_BUF_SIZE 0x2000 + +struct LZTFB { + uint8_t buf[LZTFB_BUF_SIZE]; + uint8_t *bufPtr; + uint32_t bufFill; + uint32_t lastBufFill; + + BitStreamContext bsc; + + bool literalsEncoded; + Huff_Context *huffTable[3]; + int state; + struct { + int32_t offset; + uint32_t count; + } stateData; + + size_t compressedSize; + size_t uncompressedSize; + + uint8_t table2Shift; + uint8_t lengthBias; +}; + +LZTFB *LZTFB_new(const char *buf, size_t len); +int LZTFB_output(LZTFB *lztfb, FILE *out); +void LZTFB_delete(LZTFB *lztfb); + +#endif /* _LZTFB */ + diff --git a/tools/sc1/sc1-ianm/Makefile b/tools/sc1/sc1-ianm/Makefile new file mode 100644 index 000000000..78f3b31da --- /dev/null +++ b/tools/sc1/sc1-ianm/Makefile @@ -0,0 +1,10 @@ +TARGET := unianm +CFILES := unianm.c ../../shared/util.c +HFILES := unianm.h ../../shared/cbytesex.h ../../shared/util.h +CPPFLAGS := `pkg-config --cflags libpng` +LDFLAGS := `pkg-config --libs libpng` +DEBUG := 1 +#ERROR := 1 + +include ../../shared/Makefile.default + diff --git a/tools/sc1/sc1-ianm/unianm.c b/tools/sc1/sc1-ianm/unianm.c new file mode 100644 index 000000000..07dfab167 --- /dev/null +++ b/tools/sc1/sc1-ianm/unianm.c @@ -0,0 +1,507 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "unianm.h" +#include "../../shared/cbytesex.h" +#include "../../shared/util.h" + +struct options { + const char *inFile; + const char *outDir; + FILE *out; + int resType; + bool verbose; + const char *paletteFile; + int paletteNr; +}; + +void usage(FILE *out); +void parse_arguments(int argc, char *argv[], struct options *opts); +void verbose(const struct options *opts, const char *format, ...) + __attribute__((format(printf, 2, 3))); +bool processFile(const struct options *opts, uint8_t *buf, size_t size, + Anim **retAnim); +MasterPaletteColor *readPalette(struct options *opts); +bool writePngFile(const struct options *opts, const char *fileName, + const Anim *anim, const Frame *frame, + const MasterPaletteColor *palette); +int getDigitCount(uint32_t num); +bool outputFrames(struct options *opts, Anim *anim, + const MasterPaletteColor *masterPalette); + + +int +main(int argc, char *argv[]) { + void *buf = NULL; + size_t size = 0; + char *outDirBuf = NULL; + uint8_t *paletteBuf = NULL; + + struct options opts; + parse_arguments(argc, argv, &opts); + + if (opts.outDir != NULL && opts.paletteFile == NULL) { + logError(false, "Need to specify a palette file.\n"); + goto err; + } + + if (opts.outDir == NULL && opts.paletteFile != NULL) { + fprintf(stderr, "Warning: -p is meaningless without -o.\n"); + goto err; + } + + if (mmapOpen(opts.inFile, O_RDONLY, &buf, &size) == -1) + fatal(true, "mmapOpen() failed.\n"); + + if (size < 8 || memcmp(buf, "IANM", 4) != 0) { + logError(false, "File '%s' is not an ianm file.\n", + opts.inFile); + goto err; + } + + if (opts.outDir != NULL) { + size_t outDirLen = strlen(opts.outDir); + if (outDirLen == 0) { + logError(false, "Invalid output directory.\n"); + goto err; + } + + // Add a terminating '/', if necessary. + if (opts.outDir[outDirLen - 1] != '/') { + outDirBuf = malloc(outDirLen + 2); + memcpy(outDirBuf, opts.outDir, outDirLen); + outDirBuf[outDirLen] = '/'; + outDirBuf[outDirLen + 1] = '\0'; + opts.outDir = outDirBuf; + } + + if (access(opts.outDir, W_OK) != 0) { + logError(true, "Cannot write to output path.\n"); + goto err; + } + } + + Anim *anim; + if (!processFile(&opts, (uint8_t *) buf, size, &anim)) + goto err; + + if (opts.outDir != NULL) { + MasterPaletteColor *palette = readPalette(&opts); + if (palette == NULL) { + logError(false, "Failed to read palette.\n"); + goto err; + } + + outputFrames(&opts, anim, palette); + } + + munmap(buf, size); + + (void) argc; + (void) argv; + return EXIT_SUCCESS; + +err: + if (paletteBuf != NULL) + free(paletteBuf); + if (outDirBuf != NULL) + free(outDirBuf); + if (buf != NULL) + munmap(buf, size); + return EXIT_FAILURE; +} + +void +usage(FILE *out) { + fprintf(out, "Syntax:\t" + "unianm [-v] [-c ] -p -t -o \n" + "unianm [-v] [-c ] -t \n" + "\t-c specifies the palette to use (0, 1, or 2 (default))\n" + "\t-p specifies the file to load the palette from\n" + "\t-t specifies the resource type (2, 3, 4, or 5)\n" + "\t (ignored for now)\n" + "\t-v verbose\n"); +} + +void +parse_arguments(int argc, char *argv[], struct options *opts) { + char ch; + + memset(opts, '\0', sizeof (struct options)); + opts->out = stdout; + opts->resType = 2; + opts->paletteNr = 2; + while (1) { + ch = getopt(argc, argv, "c:ho:p:t:v"); + if (ch == -1) + break; + switch(ch) { + case 'o': + opts->outDir = optarg; + break; + case '?': + case 'h': + usage(stdout); + exit(EXIT_SUCCESS); + case 'c': + if (optarg[0] < '0' || optarg[0] > 2 || optarg[1] != '\0') { + logError(false, "Invalid argument to '-p'\n"); + exit(EXIT_FAILURE); + } + opts->paletteNr = optarg[0] - '\0'; + break; + case 'p': + opts->paletteFile = optarg; + break; + case 't': + opts->resType = atoi(optarg); + if (opts->resType < 2 || opts->resType > 5) { + logError(false, "Invalid argument to '-t'\n"); + exit(EXIT_FAILURE); + } + break; + case 'v': + opts->verbose = true; + break; + default: + usage(stderr); + exit(EXIT_FAILURE); + } + } + argc -= optind; + argv += optind; + if (argc != 1) { + usage(stderr); + exit(EXIT_FAILURE); + } + opts->inFile = argv[0]; +} + +void +verbose(const struct options *opts, const char *format, ...) { + if (!opts->verbose) + return; + + va_list args; + va_start(args, format); + vfprintf(opts->out, format, args); + va_end(args); +} + +// Pre: size >= 8 +bool +processFile(const struct options *opts, uint8_t *buf, size_t bufLen, + Anim **retAnim) { + assert(bufLen >= 8); + uint8_t *end = buf + bufLen; + uint8_t *bufPtr = buf + 8; + + Anim *anim = malloc(sizeof (Anim)); + verbose(opts, "0x00000000 \"IANM\"\n"); + + anim->frameCount = getU16BE(buf + 4); + verbose(opts, "0x00000004 Number of frames: %d\n", anim->frameCount); + anim->bpp = getU16BE(buf + 6) & 0xff; + verbose(opts, "0x00000006 Bits per pixel: %d\n", anim->bpp); + + anim->frames = malloc(anim->frameCount * sizeof (Frame)); + + for (uint16_t frameI = 0; frameI < anim->frameCount; frameI++) { + if (bufPtr + 4 > end) + goto fileTooSmall; + + Frame *frame = &anim->frames[frameI]; + frame->width = getU16BE(bufPtr); + frame->height = getU16BE(bufPtr + 2); + verbose(opts, "0x%08x Frame %2d: %3d x %3d pixels\n", + bufPtr - buf, frameI, frame->width, frame->height); + bufPtr += 4; + } + + for (uint16_t frameI = 0; frameI < anim->frameCount; frameI++) { + Frame *frame = &anim->frames[frameI]; + + if (bufPtr + 6 > end) + goto fileTooSmall; + + verbose(opts, "0x%08x Frame %d:\n", + bufPtr - buf, frameI); + frame->hotX = getS16BE(bufPtr); + frame->hotY = getS16BE(bufPtr + 2); + verbose(opts, "0x%08x Hotspot: (%3d, %3d)\n", bufPtr - buf, + frame->hotX, frame->hotY); + bufPtr += 4; + frame->hasPalette = getU8(bufPtr) != 0; + verbose(opts, "0x%08x %s (0x%02x)\n", bufPtr - buf, + frame->hasPalette ? "paletted" : "not paletted", + getU8(bufPtr)); + bufPtr++; + frame->transIndex = getU8(bufPtr) & 0x0f; + verbose(opts, "0x%08x Transparent palette index: 0x%02x\n", + bufPtr - buf, frame->transIndex); + bufPtr++; + + if (frame->hasPalette) { + if (bufPtr + 0x30 > end) + goto fileTooSmall; + for (int paletteI = 0; paletteI < 3; paletteI++) { + frame->palettes[paletteI] = bufPtr; + verbose(opts, "0x%08x Palette %d:", bufPtr - buf, + paletteI); + for (int i = 0; i < 0x10; i++) + verbose(opts, " %02x", bufPtr[i]); + verbose(opts, "\n"); + bufPtr += 0x10; + } + } else { + // No palette with this frame. Copy from the previous frame, + // if there is one. + if (frameI == 0) { + for (int paletteI = 0; paletteI < 3; paletteI++) + frame->palettes[paletteI] = NULL; + } else { + Frame *lastFrame = &anim->frames[frameI - 1]; + for (int paletteI = 0; paletteI < 3; paletteI++) + frame->palettes[paletteI] = + lastFrame->palettes[paletteI]; + frame->hasPalette = lastFrame->hasPalette; + } + } + + uint8_t bitDepth = 8 / anim->bpp; + frame->bytesPerLine = ((frame->width + (bitDepth - 1)) / bitDepth); + frame->pixelDataSize = frame->bytesPerLine * frame->height; + frame->pixelData = bufPtr; + + if (bufPtr + frame->pixelDataSize > end) + goto fileTooSmall; + verbose(opts, "0x%08x Pixel data: %d bytes\n", bufPtr - buf, + frame->pixelDataSize); + bufPtr += frame->pixelDataSize; + } + + if ((size_t) (bufPtr - buf) != bufLen) { + fprintf(stderr, "\nWarning: Extra data at the end of the file:\n" + "\tdata length: %d bytes\n\tfile length: %d bytes\n", + bufPtr - buf, bufLen); + } + + *retAnim = anim; + return true; + +fileTooSmall: + logError(false, "\nInput file is too small.\n"); + free(anim->frames); + free(anim); + return false; +} + +MasterPaletteColor * +readPalette(struct options *opts) { + FILE *file = NULL; + uint32_t *buf = malloc(0x300); + + file = fopen(opts->paletteFile, "rb"); + if (file == NULL) { + logError(true, "Could not open palette file.\n"); + goto err; + } + + if (fread(buf, 1, 0x300, file) != 0x300) { + logError(ferror(file), "Could not read palette file.\n"); + goto err; + } + + fclose(file); + return (MasterPaletteColor *) buf; + +err: + if (file != NULL) + fclose(file); + return NULL; +} + +bool +writePngFile(const struct options *opts, const char *fileName, + const Anim *anim, const Frame *frame, + const MasterPaletteColor *masterPalette) { + FILE *file = NULL; + + file = fopen(fileName, "wb"); + if (file == NULL) { + logError(true, "Could not open file '%s' for writing.\n", fileName); + return false; + } + + png_structp png_ptr; + png_infop info_ptr; + + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, + (png_voidp) NULL /* user_error_ptr */, + NULL /* user_error_fn */, NULL /* user_warning_fn */); + if (!png_ptr) { + fprintf(stderr, "png_create_write_struct failed.\n"); + goto err; + } + + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_write_struct(&png_ptr, (png_infopp) NULL); + fprintf(stderr, "png_create_info_struct failed.\n"); + goto err; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + fprintf(stderr, "png error.\n"); + png_destroy_write_struct(&png_ptr, &info_ptr); + goto err; + } + + png_init_io(png_ptr, file); + png_set_IHDR(png_ptr, info_ptr, frame->width, frame->height, + anim->bpp /* bit_depth per channel */, PNG_COLOR_TYPE_PALETTE, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_set_compression_level(png_ptr, 9); + + png_set_oFFs(png_ptr, info_ptr, -frame->hotX, -frame->hotY, + PNG_OFFSET_PIXEL); + + png_color_8 sig_bit; + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + + png_color pngPalette[16]; + if (frame->hasPalette) { + uint8_t *palette = frame->palettes[opts->paletteNr]; + for (int i = 0; i < 16; i++) { + uint8_t entry = palette[i]; + pngPalette[i].red = masterPalette[entry].red; + pngPalette[i].green = masterPalette[entry].green; + pngPalette[i].blue = masterPalette[entry].blue; + } + } else { + for (int i = 0; i < 16; i++) { + pngPalette[i].red = masterPalette[i].red; + pngPalette[i].green = masterPalette[i].green; + pngPalette[i].blue = masterPalette[i].blue; + } + } + png_set_PLTE(png_ptr, info_ptr, pngPalette, 16); + + { + // Generate transparency chunk + // for indexed PNGs, tRNS chunk contains an array of alpha values + // corresponding to palette entries + png_byte trans[0x100]; + // set all palette alpha values to 0xff (fully opaque) initially + memset(trans, 0xff, 0x100 * sizeof (png_byte)); + if (frame->transIndex < 0x10) + trans[frame->transIndex] = 0; + // the only one + // only need to write out upto and including transparentPixel + png_set_tRNS(png_ptr, info_ptr, trans, frame->transIndex + 1, NULL); + } + + png_write_info(png_ptr, info_ptr); + + { + uint8_t *linePtr = frame->pixelData; + for (uint32_t lineI = 0; lineI < frame->height; lineI++) { + png_write_row(png_ptr, (png_byte *) linePtr); + linePtr += frame->bytesPerLine; + } + } + + png_write_end(png_ptr, info_ptr); + png_destroy_write_struct(&png_ptr, &info_ptr); + + fclose(file); + return true; + +err: + if (file != NULL) + fclose(file); + return false; +} + +int +getDigitCount(uint32_t num) { + if (num == 0) + return 1; + + int result = 0; + while (num > 0) { + num /= 10; + result++; + } + return result; +} + +// Pre: opts.outDir already has a terminating '/'. +bool +outputFrames(struct options *opts, Anim *anim, + const MasterPaletteColor *palette) { + if (anim->frameCount == 0) { + fprintf(stderr, "Warning: no frames in file '%s'.\n", + opts->inFile); + return true; + } + + // Get just the last component of opts->inFile. + const char *inFileName = strrchr(opts->inFile, '/'); + if (inFileName == NULL) { + inFileName = opts->inFile; + } else + inFileName++; + + size_t outDirLen = strlen(opts->outDir); + size_t inFileLen = strlen(inFileName); + size_t maxPathLen = outDirLen + inFileLen + sizeof ".65535.png"; + char path[maxPathLen]; + + char *pathPtr = path + + sprintf(path, "%s%s.", opts->outDir, inFileName); + int digitCount = getDigitCount(anim->frameCount - 1); + + for (uint16_t frameI = 0; frameI < anim->frameCount; frameI++) { + Frame *frame = &anim->frames[frameI]; + + sprintf(pathPtr, "%0*d.png", digitCount, frameI); + + if (!writePngFile(opts, path, anim, frame, palette)) + logError(false, "Writing file '%s' failed.\n", path); + } + + return true; +} + + diff --git a/tools/sc1/sc1-ianm/unianm.h b/tools/sc1/sc1-ianm/unianm.h new file mode 100644 index 000000000..cb143b463 --- /dev/null +++ b/tools/sc1/sc1-ianm/unianm.h @@ -0,0 +1,50 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _UNIANM_H +#define _UNIANM_H + +typedef struct Frame Frame; +typedef struct Anim Anim; +typedef struct MasterPaletteColor MasterPaletteColor; + +struct Frame { + uint16_t width; + uint16_t height; + int16_t hotX; + int16_t hotY; + bool hasPalette; + uint8_t transIndex; + uint8_t *palettes[3]; + uint32_t bytesPerLine; + uint32_t pixelDataSize; + uint8_t *pixelData; +}; + +struct Anim { + uint16_t frameCount; + uint8_t bpp; + struct Frame *frames; +}; + +struct MasterPaletteColor { + uint8_t red; + uint8_t green; + uint8_t blue; +} __attribute__((packed)); + +#endif /* _UNIANM_H */ + diff --git a/tools/sc1/sc1-ianm/unianm.sh b/tools/sc1/sc1-ianm/unianm.sh new file mode 100755 index 000000000..e4e472d4c --- /dev/null +++ b/tools/sc1/sc1-ianm/unianm.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +DECOMP=/home/svdb/cvs/sc2/tools/sc1-decomp/decomp +UNIANM=/home/svdb/cvs/sc2/tools/sc1-ianm/unianm + +usage() { + echo "Syntax: unanim.sh " +} + +if [ $# -lt 1 ]; then + usage >&2 + exit 0 +fi + +processFile() { + local INPATH INDIR INNAME OUTDIR TMPFILE + + INPATH=$1 + if [ "x$INPATH" = "x" ]; then + INDIR="" + else + INDIR=${INPATH%%/*} + fi + INNAME=${INPATH##*/} + OUTDIR=${2%/}/$INNAME + TMPFILE=${2%/}/$INNAME.tmp + + case "$INNAME" in + *02) + TYPE=2 + ;; + *03) + TYPE=3 + ;; + *04) + TYPE=4 + ;; + *05) + TYPE=5 + ;; + *) + echo "Skipping file '$INNAME' -- not of a recognised graphics type." + return 1 + esac + + mkdir -- "$OUTDIR" + OUTDIR=$OUTDIR/ + + "$DECOMP" -o "$TMPFILE" -- "$INPATH" + "$UNIANM" -o "$OUTDIR" -t "$TYPE" -p 00200008 -- "$TMPFILE" + + rm -f -- "$TMPFILE" +} + +OUTDIR=$1 +shift +while [ "$#" -gt 0 ]; do + FILE=$1 + processFile "$FILE" "$OUTDIR" + shift +done + + +