From 9378eadf71cdde8fbff0724993c8445b953deb02 Mon Sep 17 00:00:00 2001 From: Sylvain Glaize Date: Wed, 21 Feb 2024 00:30:22 +0100 Subject: [PATCH] Unpletter initial implementation. --- .clang-format | 66 +++++++ CMakeLists.txt | 8 + license.txt | 14 ++ pletter.cpp | 413 ++++++++++++++++++++++++++++++++++++++++ pletter.txt | 57 ++++++ unpack.asm | 141 ++++++++++++++ unpletter/README.md | 5 + unpletter/unpletter.cpp | 145 ++++++++++++++ unpletter/unpletter_c.c | 146 ++++++++++++++ 9 files changed, 995 insertions(+) create mode 100644 .clang-format create mode 100644 CMakeLists.txt create mode 100644 license.txt create mode 100644 pletter.cpp create mode 100644 pletter.txt create mode 100644 unpack.asm create mode 100644 unpletter/README.md create mode 100644 unpletter/unpletter.cpp create mode 100644 unpletter/unpletter_c.c diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..aadc8d5 --- /dev/null +++ b/.clang-format @@ -0,0 +1,66 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: None +AlignOperands: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 8 +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Left +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 0 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cc78cd8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.27) +project(pletter_uncompress CXX C) + +set(CMAKE_CXX_STANDARD 23) + +add_executable(pletter pletter.cpp) +add_executable(unpletter unpletter/unpletter.cpp) +add_executable(unpletter_c unpletter/unpletter_c.c) diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..f4ca8e2 --- /dev/null +++ b/license.txt @@ -0,0 +1,14 @@ +Pletter v0.5b - XL2S Entertainment 2008 + +Have fun with it, just do not harm anyone :) + +Keep in mind pletter is based on software with the following license: + + +Copyright (c) 2002-2003 Team Bomba + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pletter.cpp b/pletter.cpp new file mode 100644 index 0000000..5d0e2f0 --- /dev/null +++ b/pletter.cpp @@ -0,0 +1,413 @@ +/* + Pletter v0.5c1 + + XL2S Entertainment +*/ + +#pragma warning(disable: 4996) + +#include +#include +#include +#include +using namespace std; + +unsigned char *inData; + +unsigned maxlen[7] = { 128, 128+128, 512+128, 1024+128, 2048+128, 4096+128, 8192+128 }; +unsigned varcost[65536]; + +struct metadata +{ + unsigned reeks; + unsigned cpos[7], clen[7]; +} *meta; + +struct pakdata +{ + unsigned cost, mode, mlen; +} *p[7]; + +string sourcefilename, destfilename; +bool savelength = false; +unsigned inLength, offset; + +struct saves +{ + unsigned char *outData; + int ep, dp, p, e; + void init() + { + ep = dp = p = e = 0; + outData = new unsigned char[inLength * 2]; + } + void add0() + { + if (p == 0) + claimevent(); + e *= 2; + ++p; + if (p == 8) + addevent(); + } + void add1() + { + if (p == 0) + claimevent(); + e *= 2; + ++p; + ++e; + if (p == 8) + addevent(); + } + void addbit(int b) + { + if (b) + add1(); + else + add0(); + } + void add3(int b) + { + addbit(b & 4); + addbit(b & 2); + addbit(b & 1); + } + void addvar(int i) + { + int j = 32768; + while (!(i & j)) + j /= 2; + do + { + if (j == 1) + { + add0(); + return; + } + j/=2; + add1(); + if (i & j) + add1(); + else + add0(); + } + while (1); + } + void adddata(unsigned char d) + { + outData[dp++] = d; + } + void addevent() + { + outData[ep] = e; + e = p = 0; + } + void claimevent() + { + ep = dp; + ++dp; + } + void done() + { + if (p != 0) + { + while (p != 8) + { + e *= 2; + ++p; + } + addevent(); + } + FILE *file; + if (!(file = fopen(destfilename.c_str(), "wb"))) + { + cout << "Error writing file!\n"; + exit(1); + } + fwrite(outData, 1, dp, file); + fclose(file); + cout << " " << destfilename << ": " << inLength << " -> " << dp << endl; + } +} s; + +void loadfile(string sourcefilename) +{ + FILE *file; + if ((file = fopen(sourcefilename.c_str(), "rb")) == NULL) + { + cout << "Error opening file:" << sourcefilename << endl; + exit(1); + } + if (!inLength) + { + fseek(file, 0, SEEK_END); + inLength = ftell(file) - offset; + } + fseek(file, offset, SEEK_SET); + inData = new unsigned char[inLength + 1]; + meta = new metadata[inLength + 1]; + if (!fread(inData, inLength, 1, file)) + { + cout << "Filesize error" << endl; + exit(1); + } + inData[inLength] = 0; + fclose(file); +} + +void initvarcost() +{ + int v=1, b=1, r=1; + while (r != 65536) + { + for (int j = 0; j != r; ++j) + varcost[v++] = b; + b += 2; + r *= 2; + } +} + +void createmetadata() +{ + unsigned i, j; + unsigned *last = new unsigned[65536]; + memset(last, -1, 65536 * sizeof(unsigned)); + unsigned *prev = new unsigned[inLength + 1]; + for (i = 0; i != inLength; ++i) + { + meta[i].cpos[0] = meta[i].clen[0] = 0; + prev[i] = last[inData[i] + inData[i + 1] * 256]; + last[inData[i] + inData[i + 1] * 256] = i; + } + unsigned r=-1, t=0; + for (i = inLength - 1; i != -1; --i) + if (inData[i] == r) + meta[i].reeks = ++t; + else + { + r = inData[i]; + meta[i].reeks = t = 1; + } + for (int bl = 0; bl != 7; ++bl) + { + for (i = 0; i < inLength; ++i) + { + unsigned l, p; + p = i; + if (bl) + { + meta[i].clen[bl] = meta[i].clen[bl - 1]; + meta[i].cpos[bl] = meta[i].cpos[bl - 1]; + p = i - meta[i].cpos[bl]; + } + while((p = prev[p]) != -1) + { + if (i - p > maxlen[bl]) + break; + l = 0; + while (inData[p + l] == inData[i + l] && (i + l) < inLength) + { + if (meta[i + l].reeks > 1) + { + if ((j = meta[i + l].reeks) > meta[p + l].reeks) + j = meta[p + l].reeks; + l+=j; + } + else + ++l; + } + if (l > meta[i].clen[bl]) + { + meta[i].clen[bl] = l; + meta[i].cpos[bl] = i - p; + } + } + } + cout << "."; + } + cout << " "; +// delete [] prev; +// delete [] last; +} + +int getlen(pakdata *p, unsigned q) +{ + unsigned i, j, cc, ccc, kc, kmode, kl; + p[inLength].cost=0; + for (i = inLength - 1; i != -1; --i) + { + kmode = 0; + kl = 0; + kc = 9 + p[i + 1].cost; + + j=meta[i].clen[0]; + while (j > 1) + { + cc = 9 + varcost[j - 1] + p[i + j].cost; + if (cc < kc) + { + kc = cc; + kmode = 1; + kl = j; + } + --j; + } + + j = meta[i].clen[q]; + if (q == 1) + ccc = 9; + else + ccc = 9 + q; + while (j > 1) + { + cc = ccc + varcost[j - 1] + p[i + j].cost; + if (cc < kc) + { + kc = cc; + kmode = 2; + kl = j; + } + --j; + } + + p[i].cost=kc; p[i].mode=kmode; p[i].mlen=kl; + } + return p[0].cost; +} + +void save(pakdata *p, unsigned q) +{ + s.init(); + unsigned i, j; + + if (savelength) + { + s.adddata(inLength & 255); + s.adddata(inLength >> 8); + } + + s.add3(q-1); + s.adddata(inData[0]); + + i=1; + while(i 127) + cout << "-j>128-"; + s.adddata(j); + i += p[i].mlen; + break; + case 2: + s.add1(); + s.addvar(p[i].mlen - 1); + j = meta[i].cpos[q] - 1; + if (j < 128) + cout << "-j<128-"; + j -= 128; + s.adddata(128 | j & 127); + switch (q) + { + case 6: s.addbit(j & 4096); + case 5: s.addbit(j & 2048); + case 4: s.addbit(j & 1024); + case 3: s.addbit(j & 512); + case 2: s.addbit(j & 256); + s.addbit(j & 128); + case 1: + break; + default: + cout << "-2-"; + break; + } + i += p[i].mlen; + break; + default: + cout << "-?-"; + break; + } + } + + for (i = 0; i != 34; ++i) + s.add1(); + s.done(); +} + +int main(int argc, char *argv[]) +{ + if (argc == 1) + cout << endl; + cout << "Pletter v0.5c2 - www.xl2s.tk" << endl; + if (argc == 1) + { + cout << "\nUsage:\npletter [-s[ave_length]] sourcefile [[offset [inLength]] [destinationfile]]\n"; + exit(0); + } + + offset = 0; + inLength = 0; + + int i = 1; + if (argv[i][0] == '-') + { + savelength = (argv[i][1] == 's') || (argv[i][1] == 'S'); + ++i; + } + if (argv[i]) + sourcefilename = argv[i++]; + if (argv[i] && isdigit(argv[i][0])) + { + offset=atoi(argv[i++]); + if (argv[i] && isdigit(argv[i][0])) + inLength = atoi(argv[i++]); + } + if (argv[i]) + destfilename = argv[i++]; + + if (!sourcefilename[0]) + { + cout << "No inputfile" << endl; + exit(1); + } + if (!destfilename[0]) + destfilename = sourcefilename + ".plet5"; + + loadfile(sourcefilename); + + initvarcost(); + createmetadata(); + + int minlen = inLength * 1000; + int minbl = 0; + for(i = 1; i != 7; ++i) + { + p[i] = new pakdata[inLength + 1]; + int l = getlen(p[i], i); + if (l < minlen && i) + { + minlen = l; + minbl = i; + } + cout << "."; + } + save(p[minbl], minbl); + +#ifdef _DEBUG + cin.get(); +#endif + + return 0; +} + +//eof diff --git a/pletter.txt b/pletter.txt new file mode 100644 index 0000000..ddcbc24 --- /dev/null +++ b/pletter.txt @@ -0,0 +1,57 @@ + Pletter v0.5c1 - XL2S Entertainment 2008 +-==============---==================------ + +Pletter is a PC based compressor with an MSX based decompressor. + + What has changed +-================- + +version 0.5c1 +- Now also with working pletter.exe. + +version 0.5c +- The size of the data can be saved in the file. +- Version 0.5c uses a smaller maximum look back buffer size. This means the new unpacker cannot unpack all files packed with version 0.5b; old 0.5 unpackers can unpack 0.5c files though. Unless you let Pletter store the filesize in the file. + +version 0.5a +- The size of the lookbackbuffer is stored in the compressed data. + + How to compress +-===============- +The compressor only works on PC (or any other system that is able to get the source compiled): +pletter [-s] [[offset [length]] []] + +The sourcefile is the file to compress. When the destination file is omitted, it will be constructed the following way: sourcefilename+.plet5 + +So: pletter xl2slogo.sc2 +Will (hopefully ;) result in a file called: xl2slogo.sc2.plet5 + +It is possible to compress a part of a file by specifying the start and, optionally, the length: + +pletter fun.sc5 7 +will skip the bin header and result in a file called fun.sc5.plet5. + +The �s option makes Pletter save the length of the original data at the beginning of the file. If you use this option, you have to modify the unpack routine. + + How to decompress +-=================- +- Include unpack.asm in your sourcefile. +- Include the compressed data in your sourcefile. +- Call unpack with in HL a pointer to the compressed data, and in DE the destination. + +Example: + + + + ld hl,data + ld de,8000h + call unpack + + + + include unpack.asm ; include the unpacker +data + incbin data.gfx.plet5 ; the data + + +If you used to option to include the length of the uncompressed data in the file, you should modify the unpack routine to skip it, or skip it yourself by using ld hl,data+2, or even just incbin "data.gfx.plet5",2. diff --git a/unpack.asm b/unpack.asm new file mode 100644 index 0000000..79e361f --- /dev/null +++ b/unpack.asm @@ -0,0 +1,141 @@ +; pletter v0.5c msx unpacker + +; call unpack with hl pointing to some pletter5 data, and de pointing to the destination. +; changes all registers + +; define lengthindata when the original size is written in the pletter data + +; define LENGTHINDATA + + module pletter + + macro GETBIT + add a,a + call z,getbit + endmacro + + macro GETBITEXX + add a,a + call z,getbitexx + endmacro + +@unpack + + ifdef LENGTHINDATA + inc hl + inc hl + endif + + ld a,(hl) + inc hl + exx + ld de,0 + add a,a + inc a + rl e + add a,a + rl e + add a,a + rl e + rl e + ld hl,modes + add hl,de + ld e,(hl) + ld ixl,e + inc hl + ld e,(hl) + ld ixh,e + ld e,1 + exx + ld iy,loop +literal + ldi +loop + GETBIT + jr nc,literal + exx + ld h,d + ld l,e +getlen + GETBITEXX + jr nc,.lenok +.lus + GETBITEXX + adc hl,hl + ret c + GETBITEXX + jr nc,.lenok + GETBITEXX + adc hl,hl + ret c + GETBITEXX + jp c,.lus +.lenok + inc hl + exx + ld c,(hl) + inc hl + ld b,0 + bit 7,c + jp z,offsok + jp ix + +mode6 + GETBIT + rl b +mode5 + GETBIT + rl b +mode4 + GETBIT + rl b +mode3 + GETBIT + rl b +mode2 + GETBIT + rl b + GETBIT + jr nc,offsok + or a + inc b + res 7,c +offsok + inc bc + push hl + exx + push hl + exx + ld l,e + ld h,d + sbc hl,bc + pop bc + ldir + pop hl + jp iy + +getbit + ld a,(hl) + inc hl + rla + ret + +getbitexx + exx + ld a,(hl) + inc hl + exx + rla + ret + +modes + word offsok + word mode2 + word mode3 + word mode4 + word mode5 + word mode6 + + endmodule + +;eof \ No newline at end of file diff --git a/unpletter/README.md b/unpletter/README.md new file mode 100644 index 0000000..a5fafd9 --- /dev/null +++ b/unpletter/README.md @@ -0,0 +1,5 @@ +Unpletter is a C and C++ version of the unpacker for the Pletter compression scheme. + +By Sylvain Glaize, 2024. + +Distributed as the same license as the Pletter packer. See license.txt on the root folder. diff --git a/unpletter/unpletter.cpp b/unpletter/unpletter.cpp new file mode 100644 index 0000000..94e1b18 --- /dev/null +++ b/unpletter/unpletter.cpp @@ -0,0 +1,145 @@ +#include +#include +#include + +using data_type = unsigned char; + +class Uncompressor +{ +public: + explicit Uncompressor(const std::vector& inData) + : compressedData(inData) + { + std::vector output; + + int q_value = (getBit() << 2 | getBit() << 1 | getBit()) + 1; + + data_type first_byte = getByte(); + output.push_back(first_byte); + dataPosition = 2; + + while (dataPosition < inData.size()) + { + if (getBit())// Back reference + { + data_type length = getInterlacedEliasGamma() + 1; + + if (length == 255) + { + break; + } + + int offset = getByte(); + if (offset & 0x80) + { + offset = offset & 0x7F; + switch(q_value) + { + case 6: + offset = offset | (getBit() << 12); + case 5: + offset = offset | (getBit() << 11); + case 4: + offset = offset | (getBit() << 10); + case 3: + offset = offset | (getBit() << 9); + case 2: + offset = offset | (getBit() << 8); + offset = offset | (getBit() << 7); + case 1: + break; + default: + std::cout << "Invalid mode: " << q_value << std::endl; + break; + } + offset += 128; + } + offset += 1; + + for (int i = 0; i < length; i++) + { + output.push_back(output[output.size() - offset]); + } + } + else// Literal data + { + output.push_back(getByte()); + } + } + + for (unsigned char byte: output) + { + std::cout << byte; + } + } + +private: + int dataPosition = 0; + int varPosition = 0; + int bitForVarPosition = 7; + + const std::vector& compressedData; + + data_type getByte() + { + return compressedData[dataPosition++]; + } + + data_type getBit() + { + if (bitForVarPosition == 7) + { + varPosition = dataPosition; + dataPosition = varPosition + 1; + } + + data_type bit = (compressedData[varPosition] >> bitForVarPosition) & 1; + bitForVarPosition--; + + if (bitForVarPosition == -1) + { + bitForVarPosition = 7; + } + return bit; + } + + data_type getInterlacedEliasGamma() + { + data_type value = 1; + while (getBit()) + { + value = (value << 1) | getBit(); + } + return value; + } +}; + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + std::cout << "Usage: " << argv[0] << " " << std::endl; + return 1; + } + + std::ifstream file(argv[1], std::ios::binary | std::ios::ate); + if (!file) + { + std::cout << "Error reading file!" << std::endl; + return 1; + } + + std::streamsize length = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector compressedData(length); + if (!file.read(reinterpret_cast(compressedData.data()), length)) + { + std::cout << "Error reading file!" << std::endl; + return 1; + } + + Uncompressor data(compressedData); + + return 0; +} diff --git a/unpletter/unpletter_c.c b/unpletter/unpletter_c.c new file mode 100644 index 0000000..d1d3aed --- /dev/null +++ b/unpletter/unpletter_c.c @@ -0,0 +1,146 @@ +#include +#include + +typedef unsigned char data_type; + +struct Uncompressor { + unsigned char* compressedData; + int dataPosition; + int varPosition; + int bitForVarPosition; +}; + +data_type getByte(struct Uncompressor* uncompressor) +{ + return uncompressor->compressedData[uncompressor->dataPosition++]; +} + +data_type getBit(struct Uncompressor* uncompressor) +{ + if (uncompressor->bitForVarPosition == 7) + { + uncompressor->varPosition = uncompressor->dataPosition; + uncompressor->dataPosition = uncompressor->varPosition + 1; + } + + data_type bit = (uncompressor->compressedData[uncompressor->varPosition] >> uncompressor->bitForVarPosition) & 1; + uncompressor->bitForVarPosition--; + + if (uncompressor->bitForVarPosition == -1) + { + uncompressor->bitForVarPosition = 7; + } + return bit; +} + +data_type getInterlacedEliasGamma(struct Uncompressor* uncompressor) +{ + data_type value = 1; + while (getBit(uncompressor)) + { + value = (value << 1) | getBit(uncompressor); + } + return value; +} + +void uncompress(struct Uncompressor* uncompressor, size_t in_data_length) +{ + unsigned char* output = (unsigned char*) malloc(in_data_length * 3);// * 3 is an arbitrary number... + size_t output_position = 0; + + int q_value = (getBit(uncompressor) << 2 | getBit(uncompressor) << 1 | getBit(uncompressor)) + 1; + + data_type first_byte = getByte(uncompressor); + output[output_position++] = first_byte; + + while (uncompressor->dataPosition < in_data_length) + { + if (getBit(uncompressor)) + { + data_type length = getInterlacedEliasGamma(uncompressor) + 1; + + if (length == 255) + { + break; + } + + int offset = getByte(uncompressor); + if (offset & 0x80) + { + offset = offset & 0x7F; + switch (q_value) + { + case 6: + offset = offset | (getBit(uncompressor) << 12); + case 5: + offset = offset | (getBit(uncompressor) << 11); + case 4: + offset = offset | (getBit(uncompressor) << 10); + case 3: + offset = offset | (getBit(uncompressor) << 9); + case 2: + offset = offset | (getBit(uncompressor) << 8); + offset = offset | (getBit(uncompressor) << 7); + case 1: + break; + default: + printf("Invalid mode: %d\n", q_value); + break; + } + offset += 128; + } + offset += 1; + + for (int i = 0; i < length; i++) + { + output[output_position] = output[output_position - offset]; + output_position += 1; + } + } + else + { + output[output_position++] = getByte(uncompressor); + } + } + + for (int i = 0; i < output_position; i++) + { + printf("%c", output[i]); + } + free(output); +} + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + printf("Usage: %s \n", argv[0]); + return 1; + } + + FILE* file = fopen(argv[1], "rb"); + if (!file) + { + printf("Error reading file!\n"); + return 1; + } + + fseek(file, 0, SEEK_END); + long length = ftell(file); + fseek(file, 0, SEEK_SET); + + unsigned char* compressedData = (unsigned char*) malloc(length); + if (!fread(compressedData, length, 1, file)) + { + printf("Error reading file!\n"); + return 1; + } + + struct Uncompressor uncompressor = {compressedData, 0, 0, 7}; + uncompress(&uncompressor, length); + + free(compressedData); + fclose(file); + + return 0; +} \ No newline at end of file