@camoto/gamecomp
Advanced tools
Comparing version 3.0.0 to 4.0.0
@@ -30,2 +30,3 @@ /* | ||
import { RecordBuffer, RecordType } from '@camoto/record-io-buffer'; | ||
import { BitStream, BitView } from 'bit-buffer'; | ||
import Debug from '../util/debug.js'; | ||
@@ -48,5 +49,5 @@ const g_debug = Debug.extend(FORMAT_ID); | ||
* | ||
* The length/offset code is read as a single 16-bit integer, which is then | ||
* split into the separate length and offset values. How this split happens is | ||
* controlled by multiple options. | ||
* In byte mode (bitstream = false), the length/offset code is read as a single | ||
* 16-bit integer, which is then split into the separate length and offset | ||
* values. How this split happens is controlled by multiple options. | ||
* | ||
@@ -56,4 +57,4 @@ * Assuming the two bytes are AB followed by CD (i.e. a big-endian value 0xABCD) | ||
* | ||
* Split | lengthHigh | bigEndian | offsetRotate | ||
* ------+------------+-----------+------------- | ||
* Split | lengthHigh | bigEndian | rotateDistance | ||
* ------+------------+-----------+--------------- | ||
* ABC+D | false | true | 0 | ||
@@ -77,3 +78,4 @@ * A+BCD | true | true | 0 | ||
{ | ||
static metadata() { | ||
static metadata() | ||
{ | ||
return { | ||
@@ -83,14 +85,25 @@ id: FORMAT_ID, | ||
options: { | ||
sizeLength: 'Size of the backreference length field, in bits [4]', | ||
minLen: 'Minimum length of a backreference string, in bytes [3]', | ||
prefillByte: 'Byte used to prefill the sliding window buffer [0x00]', | ||
windowStartAt0: 'true to start at the beginning of the window, false ' | ||
+ 'to start at the end of the window (false)', | ||
littleEndian: 'Endian of 16-bit offset+length value: ' | ||
+ 'true = little, false = big [false]', | ||
bitstream: 'Embed flag bits in the stream (true) or group them into ' | ||
+ 'an initial byte. [false]', | ||
invertFlag: 'If true, flag=0 is codeword, if false flag=1 is codeword ' | ||
+ '[false]', | ||
lengthHigh: 'Whether the backreference length field is stored in the ' | ||
+ 'upper (most significant) bits of the code (true) or the lower ' | ||
+ 'bits. [false]', | ||
offsetRotate: 'How many bits to rotate the backreference offset ' | ||
littleEndian: 'Endian of 16-bit offset+length value: ' | ||
+ 'true = little, false = big [false]', | ||
minDistance: 'Minimum distance of a backreference string (actual ' | ||
+ 'distance when value is 0), in bytes [0]', | ||
minLength: 'Minimum length of a backreference string (actual length when ' | ||
+ 'value is 0), in bytes [3]', | ||
rotateDistance: 'How many bits to rotate the backreference distance ' | ||
+ 'field [0]', | ||
prefillByte: 'Byte used to prefill the sliding window buffer [0x00]', | ||
relativeDistance: 'Backreference distance is relative to current ' | ||
+ 'window position (true) or to the start of the sliding window ' | ||
+ '[false]', | ||
sizeDistance: 'Size of the backreference distance field, in bits [12]', | ||
sizeLength: 'Size of the backreference length field, in bits [4]', | ||
windowStartAt0: 'true to start at the beginning of the window, false ' | ||
+ 'to start at the end of the window (false)', | ||
}, | ||
@@ -100,25 +113,40 @@ }; | ||
static reveal(content, options = {}) { | ||
static reveal(content, options = {}) | ||
{ | ||
const debug = g_debug.extend('reveal'); | ||
options.bitstream = parseBool(options.bitstream); | ||
options.invertFlag = parseBool(options.invertFlag); | ||
options.lengthHigh = parseBool(options.lengthHigh); | ||
options.littleEndian = parseBool(options.littleEndian); | ||
options.minDistance = parseInt(options.minDistance || 0); | ||
// minLength can't be 0. | ||
options.minLength = parseInt(options.minLength || 3); | ||
options.rotateDistance = parseInt(options.rotateDistance || 0); | ||
options.prefillByte = parseInt(options.prefillByte || 0x00); | ||
options.relativeDistance = parseBool(options.relativeDistance); | ||
// sizeDistance can't be 0. | ||
options.sizeDistance = parseInt(options.sizeDistance || 12); | ||
// sizeLength can't be 0. | ||
options.sizeLength = parseInt(options.sizeLength || 4); | ||
options.minLen = parseInt(options.minLen || 3); | ||
options.prefillByte = parseInt(options.prefillByte || 0x00); | ||
options.windowStartAt0 = parseBool(options.windowStartAt0); | ||
options.littleEndian = parseBool(options.littleEndian); | ||
options.lengthHigh = parseBool(options.lengthHigh); | ||
options.offsetRotate = parseInt(options.offsetRotate || 0); | ||
if (options.sizeLength > 8) { | ||
throw new Error('Backreference length fields longer than 8 bits are not supported.'); | ||
if (!options.bitstream) { // byte mode | ||
if (options.sizeLength > 8) { | ||
throw new Error('Backreference length fields longer than 8 bits are ' | ||
+ 'not supported in byte mode.'); | ||
} | ||
if (options.sizeLength + options.sizeDistance != 16) { | ||
throw new Error('Backreference length + distance fields must total ' | ||
+ '16 bits in byte mode.'); | ||
} | ||
} | ||
const sizeOffset = 16 - options.sizeLength; | ||
const windowSize = (1 << sizeOffset); | ||
const maxBackrefSize = (1 << options.sizeLength) + options.minLen - 1; | ||
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefSize; | ||
const windowSize = (1 << options.sizeDistance); | ||
const maxBackrefLength = (1 << options.sizeLength) + options.minLength - 1; | ||
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefLength; | ||
// 2 bytes for each of 8 backrefs, plus flag byte | ||
const minEncodedBytesPer8Chunks = (8 * 2) + 1; | ||
const maxDecodedBytesPer8Chunks = (8 * maxBackrefSize); | ||
const maxDecodedBytesPer8Chunks = (8 * maxBackrefLength); | ||
@@ -128,83 +156,148 @@ const maxTheoreticalCompressionRatio = | ||
let output = new RecordBuffer(content.length * maxTheoreticalCompressionRatio); | ||
// Calculate the shifts and masks needed to extract the length and | ||
// offset values from the word we just read. | ||
let sizeShift, offShift; | ||
let shiftLength, shiftDistance; | ||
if (options.lengthHigh) { | ||
sizeShift = sizeOffset; | ||
offShift = 0; | ||
shiftLength = options.sizeDistance; | ||
shiftDistance = 0; | ||
} else { | ||
sizeShift = 0; | ||
offShift = options.sizeLength; | ||
shiftLength = 0; | ||
shiftDistance = options.sizeLength; | ||
} | ||
const sizeMask = (1 << options.sizeLength) - 1; | ||
const offMask = (1 << sizeOffset) - 1; | ||
const maskLength = (1 << options.sizeLength) - 1; | ||
const maskDistance = (1 << options.sizeDistance) - 1; | ||
let input = new RecordBuffer(content); | ||
let output = new RecordBuffer(content.length * maxTheoreticalCompressionRatio); | ||
let windowPos = windowStartPos; | ||
let slidingWindow = new Array(windowSize).fill(options.prefillByte); | ||
const fnReadWord = input.read.bind( | ||
input, | ||
options.littleEndian ? RecordType.int.u16le : RecordType.int.u16be | ||
); | ||
const invert = options.invertFlag ? 1 : 0; | ||
while (input.distFromEnd() > 0) { | ||
let fnNextFlag, fnReadByte, fnReadLengthDistance, fnBitsRemaining; | ||
if (options.bitstream) { | ||
// Support functions for bit-level LZSS. | ||
let bs = new BitStream( | ||
new BitView(content.buffer, content.byteOffset, content.byteLength) | ||
); | ||
bs.bigEndian = !options.littleEndian; | ||
let flagBitPos = 0; | ||
const flagByte = input.read(RecordType.int.u8); | ||
fnNextFlag = () => bs.readBits(1, false) ^ invert; | ||
while (flagBitPos < 8) { | ||
fnReadByte = () => bs.readBits(8, false); | ||
// if the flag indicates that this chunk is a literal... | ||
if ((flagByte & (1 << flagBitPos)) != 0) { | ||
if (input.distFromEnd() <= 0) break; | ||
fnReadLengthDistance = () => { | ||
let sizeA, sizeB; | ||
if (options.lengthHigh) { | ||
sizeA = options.sizeLength; | ||
sizeB = options.sizeDistance; | ||
} else { | ||
sizeA = options.sizeDistance; | ||
sizeB = options.sizeLength; | ||
} | ||
return [ | ||
bs.readBits(sizeA, false), | ||
bs.readBits(sizeB, false), | ||
]; | ||
}; | ||
const literal = input.read(RecordType.int.u8); | ||
output.write(RecordType.int.u8, literal); | ||
fnBitsRemaining = () => bs.bitsLeft; | ||
slidingWindow[windowPos++] = literal; | ||
windowPos %= windowSize; | ||
} else { | ||
// Support functions for byte-level LZSS. | ||
let input = new RecordBuffer(content); | ||
} else { // otherwise, this chunk is a backreference | ||
let flagBitPos = 8; | ||
let flagByte = 0; | ||
fnNextFlag = () => { | ||
if (flagBitPos >= 8) { | ||
flagByte = input.read(RecordType.int.u8); | ||
flagBitPos = 0; | ||
} | ||
const f = flagByte & 1; | ||
flagByte >>= 1; | ||
flagBitPos++; | ||
return f ^ invert; | ||
}; | ||
// verify that there are enough bytes left in the input stream to | ||
// describe the backreference | ||
if (input.distFromEnd() < 2) break; | ||
fnReadByte = () => { | ||
return input.read(RecordType.int.u8); | ||
}; | ||
const brWord = fnReadWord(); | ||
// Put this outside fnReadLengthDistance() so we avoid a conditional on | ||
// each read operation. | ||
const fnReadWord = input.read.bind( | ||
input, | ||
options.littleEndian ? RecordType.int.u16le : RecordType.int.u16be | ||
); | ||
let backrefOffset = (brWord >> offShift) & offMask; | ||
let backrefSize = (brWord >> sizeShift) & sizeMask; | ||
fnReadLengthDistance = () => { | ||
const brWord = fnReadWord(); | ||
backrefSize += options.minLen; | ||
let backrefDistance = (brWord >> shiftDistance) & maskDistance; | ||
let backrefLength = (brWord >> shiftLength) & maskLength; | ||
// Rotate the offset by the given number of bits, e.g. 0xABC rotated | ||
// by 4 = BCA. | ||
if (options.offsetRotate) { | ||
backrefOffset = | ||
( | ||
(backrefOffset << options.offsetRotate) | ||
| (backrefOffset >> (sizeOffset - options.offsetRotate)) | ||
) & offMask | ||
; | ||
} | ||
return [ | ||
backrefLength, | ||
backrefDistance, | ||
]; | ||
}; | ||
// copy the bytes from the sliding window to the output, and also | ||
// to the end of the sliding window | ||
for (let brByteIdx = 0; brByteIdx < backrefSize; brByteIdx++) { | ||
fnBitsRemaining = () => input.distFromEnd() * 8; | ||
} | ||
const curByte = slidingWindow[backrefOffset]; | ||
output.write(RecordType.int.u8, curByte); | ||
while (fnBitsRemaining() > 0) { | ||
slidingWindow[windowPos] = curByte; | ||
const flag = fnNextFlag(); | ||
++backrefOffset; backrefOffset %= windowSize; | ||
++windowPos; windowPos %= windowSize; | ||
} | ||
// if the flag indicates that this chunk is a backreference... | ||
if (flag) { | ||
// verify that there are enough bytes left in the input stream to | ||
// describe the backreference | ||
if (fnBitsRemaining() < 16) break; | ||
let [ backrefLength, backrefDistance ] = fnReadLengthDistance(); | ||
// Rotate the offset by the given number of bits, e.g. 0xABC rotated | ||
// by 4 = BCA. | ||
if (options.rotateDistance) { | ||
backrefDistance = | ||
( | ||
(backrefDistance << options.rotateDistance) | ||
| (backrefDistance >> (options.sizeDistance - options.rotateDistance)) | ||
) & maskDistance | ||
; | ||
} | ||
flagBitPos++; | ||
backrefLength += options.minLength; | ||
backrefDistance += options.minDistance; | ||
if (options.relativeDistance) { | ||
backrefDistance = windowSize + windowPos - backrefDistance; | ||
} | ||
// copy the bytes from the sliding window to the output, and also | ||
// to the end of the sliding window | ||
for (let brByteIdx = 0; brByteIdx < backrefLength; brByteIdx++) { | ||
backrefDistance %= windowSize; | ||
const curByte = slidingWindow[backrefDistance]; | ||
output.write(RecordType.int.u8, curByte); | ||
slidingWindow[windowPos] = curByte; | ||
++backrefDistance; | ||
++windowPos; windowPos %= windowSize; | ||
} | ||
} else { // otherwise, this chunk is a literal | ||
if (fnBitsRemaining() < 8) break; | ||
const literal = fnReadByte(); | ||
output.write(RecordType.int.u8, literal); | ||
slidingWindow[windowPos++] = literal; | ||
windowPos %= windowSize; | ||
} | ||
} | ||
@@ -215,3 +308,5 @@ | ||
static obscure(content, options = {}) { | ||
static obscure(content, options = {}) | ||
{ | ||
const debug = g_debug.extend('obscure'); | ||
@@ -266,16 +361,21 @@ function findMatch(r_inputPos, buf, bufSize, maxMatchLength) { | ||
const debug = g_debug.extend('obscure'); | ||
options.bitstream = parseBool(options.bitstream); | ||
options.invertFlag = parseBool(options.invertFlag); | ||
options.lengthHigh = parseBool(options.lengthHigh); | ||
options.littleEndian = parseBool(options.littleEndian); | ||
options.minDistance = parseInt(options.minDistance || 0); | ||
// minLength can't be 0 (infinite loop). | ||
options.minLength = parseInt(options.minLength || 3); | ||
options.rotateDistance = parseInt(options.rotateDistance || 0); | ||
options.prefillByte = parseInt(options.prefillByte || 0x00); | ||
options.relativeDistance = parseBool(options.relativeDistance); | ||
// sizeDistance can't be 0. | ||
options.sizeDistance = parseInt(options.sizeDistance || 12); | ||
// sizeLength can't be 0. | ||
options.sizeLength = parseInt(options.sizeLength || 4); | ||
options.minLen = parseInt(options.minLen || 3); | ||
options.prefillByte = parseInt(options.prefillByte || 0x00); | ||
options.windowStartAt0 = parseBool(options.windowStartAt0); | ||
options.littleEndian = parseBool(options.littleEndian); | ||
options.lengthHigh = parseBool(options.lengthHigh); | ||
options.offsetRotate = parseInt(options.offsetRotate || 0); | ||
const sizeOffset = 16 - options.sizeLength; | ||
const windowSize = (1 << sizeOffset); | ||
const maxBackrefSize = (1 << options.sizeLength) + options.minLen - 1; | ||
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefSize; | ||
const windowSize = (1 << options.sizeDistance); | ||
const maxBackrefLength = (1 << options.sizeLength) + options.minLength - 1; | ||
const windowStartPos = options.windowStartAt0 ? 0 : windowSize - maxBackrefLength; | ||
@@ -285,36 +385,136 @@ // worst case is an increase in content size by 12.5% (negative compression) | ||
let textBuf = new Uint8Array(windowSize + maxBackrefSize - 1).fill(options.prefillByte); | ||
let textBuf = new Uint8Array(windowSize + maxBackrefLength - 1).fill(options.prefillByte); | ||
let r_windowInputPos = windowStartPos; | ||
let inputPos = 0; | ||
let chunkIndex = 0; | ||
let inputStringLen = 0; | ||
let i = 0; | ||
// s starts maxBackrefSize bytes after windowStartPos, wrapping around to | ||
// s starts maxBackrefLength bytes after windowStartPos, wrapping around to | ||
// the start of the window if needed. | ||
let s = (windowStartPos + maxBackrefSize + windowSize) % windowSize; | ||
let s = (windowStartPos + maxBackrefLength + windowSize) % windowSize; | ||
let sizeShift, offShift; | ||
let shiftLength, shiftDistance; | ||
if (options.lengthHigh) { | ||
sizeShift = sizeOffset; | ||
offShift = 0; | ||
shiftLength = options.sizeDistance; | ||
shiftDistance = 0; | ||
} else { | ||
sizeShift = 0; | ||
offShift = options.sizeLength; | ||
shiftLength = 0; | ||
shiftDistance = options.sizeLength; | ||
} | ||
const offMask = (1 << sizeOffset) - 1; | ||
const maskDistance = (1 << options.sizeDistance) - 1; | ||
// we buffer up 8 chunks at a time (a mixture of literals and backreferences) | ||
// because the flags that indicate the chunk type are packed together into | ||
// a single prefix byte | ||
let chunkBuf = new Array(); | ||
const invert = options.invertFlag ? 1 : 0; | ||
// init the flag byte with all the flags clear; | ||
// we'll OR-in individual flags as literals are added | ||
chunkBuf.push(0); | ||
let fnWriteLiteral, fnWriteLengthDistance, fnFlush; | ||
if (options.bitstream) { | ||
// Support functions for bit-level LZSS. | ||
let bs = new BitStream( | ||
new BitView(output.buffer, output.byteOffset, output.byteLength) | ||
); | ||
bs.bigEndian = !options.littleEndian; | ||
fnWriteLiteral = literalByte => { | ||
bs.writeBits(0 ^ invert, 1); | ||
bs.writeBits(literalByte, 8); | ||
}; | ||
fnWriteLengthDistance = (backrefLength, backrefDistance) => { | ||
bs.writeBits(1 ^ invert, 1); | ||
if (options.lengthHigh) { | ||
bs.writeBits(backrefLength, options.sizeLength); | ||
bs.writeBits(backrefDistance, options.sizeDistance); | ||
} else { | ||
bs.writeBits(backrefDistance, options.sizeDistance); | ||
bs.writeBits(backrefLength, options.sizeLength); | ||
} | ||
}; | ||
fnFlush = () => { | ||
// Write zero bits until the next byte boundary. | ||
const bitsLeft = (8 - (bs.index % 8)) % 8; | ||
if (bitsLeft) bs.writeBits(0, bitsLeft); | ||
return new Uint8Array(output.buffer, 0, bs.byteIndex); | ||
}; | ||
} else { | ||
// Support functions for byte-level LZSS. | ||
let chunkIndex = 0; | ||
// we buffer up 8 chunks at a time (a mixture of literals and | ||
// backreferences) because the flags that indicate the chunk type are | ||
// packed together into a single prefix byte | ||
let chunkBuf = new Array(); | ||
// init the flag byte with all the flags clear; | ||
// we'll OR-in individual flags as literals are added | ||
chunkBuf.push(0); | ||
function flushChunks() { | ||
// once we've finished 8 chunks, write them to the output | ||
if (++chunkIndex >= 8) { | ||
const binaryChunkBuf = new Uint8Array(chunkBuf); | ||
output.put(binaryChunkBuf); | ||
chunkIndex = 0; | ||
chunkBuf.length = 0; | ||
chunkBuf.push(0); | ||
} | ||
} | ||
fnWriteLiteral = literalByte => { | ||
// set the flag indicating that this chunk is a literal | ||
chunkBuf[0] |= ((0 ^ invert) << chunkIndex); | ||
chunkBuf.push(literalByte); | ||
flushChunks(); | ||
}; | ||
fnWriteLengthDistance = (backrefLength, backrefDistance) => { | ||
chunkBuf[0] |= ((1 ^ invert) << chunkIndex); | ||
// Rotate the offset by the given number of bits, e.g. 0xABC rotated | ||
// by 4 = CAB. This is the opposite direction to when we read it. | ||
if (options.rotateDistance) { | ||
backrefDistance = | ||
( | ||
(backrefDistance >> options.rotateDistance) | ||
| (backrefDistance << (options.sizeDistance - options.rotateDistance)) | ||
) & maskDistance | ||
; | ||
} | ||
backrefLength <<= shiftLength; | ||
backrefDistance <<= shiftDistance; | ||
const word = backrefDistance | backrefLength; | ||
if (options.littleEndian) { | ||
chunkBuf.push(word & 0xFF); | ||
chunkBuf.push(word >> 8); | ||
} else { | ||
chunkBuf.push(word >> 8); | ||
chunkBuf.push(word & 0xFF); | ||
} | ||
flushChunks(); | ||
}; | ||
fnFlush = () => { | ||
// if there's a partial set of eight chunks, write it to the output | ||
if (chunkIndex > 0) { | ||
const binaryChunkBuf = new Uint8Array(chunkBuf); | ||
output.put(binaryChunkBuf); | ||
} | ||
return output.getU8(); | ||
}; | ||
} | ||
// Fill the read buffer until it is full or the end of the data is reached. | ||
const lenRead = Math.min( | ||
maxBackrefSize - inputStringLen, | ||
maxBackrefLength - inputStringLen, | ||
content.length - inputPos | ||
@@ -335,3 +535,3 @@ ); | ||
let { bestMatchStartPos, bestMatchLen } = | ||
findMatch(r_windowInputPos, textBuf, windowSize, maxBackrefSize); | ||
findMatch(r_windowInputPos, textBuf, windowSize, maxBackrefLength); | ||
@@ -343,47 +543,21 @@ if (bestMatchLen > inputStringLen) { | ||
// if a match was found, produce a backreference in the output stream | ||
if (bestMatchLen >= options.minLen) { | ||
if ( | ||
(bestMatchLen >= options.minLength) | ||
&& (bestMatchStartPos >= options.minDistance) | ||
) { | ||
let backrefOffset = bestMatchStartPos; | ||
let backrefSize = (bestMatchLen - options.minLen) << sizeShift; | ||
// Rotate the offset by the given number of bits, e.g. 0xABC rotated | ||
// by 4 = CAB. This is the opposite direction to when we read it. | ||
if (options.offsetRotate) { | ||
backrefOffset = | ||
( | ||
(backrefOffset >> options.offsetRotate) | ||
| (backrefOffset << (sizeOffset - options.offsetRotate)) | ||
) & offMask | ||
; | ||
if (options.relativeDistance) { | ||
bestMatchStartPos = (windowSize + r_windowInputPos - bestMatchStartPos) % windowSize; | ||
} | ||
backrefOffset <<= offShift; | ||
let backrefDistance = bestMatchStartPos - options.minDistance; | ||
let backrefLength = bestMatchLen - options.minLength; | ||
const word = backrefOffset | backrefSize; | ||
if (options.littleEndian) { | ||
chunkBuf.push(word & 0xFF); | ||
chunkBuf.push(word >> 8); | ||
} else { | ||
chunkBuf.push(word >> 8); | ||
chunkBuf.push(word & 0xFF); | ||
} | ||
fnWriteLengthDistance(backrefLength, backrefDistance); | ||
} else { // otherwise, produce a literal | ||
bestMatchLen = 1; | ||
// set the flag indicating that this chunk is a literal | ||
chunkBuf[0] |= (1 << chunkIndex); | ||
chunkBuf.push(textBuf[r_windowInputPos]); | ||
fnWriteLiteral(textBuf[r_windowInputPos]); | ||
} | ||
// once we've finished 8 chunks, write them to the output | ||
if (++chunkIndex >= 8) { | ||
const binaryChunkBuf = new Uint8Array(chunkBuf); | ||
output.put(binaryChunkBuf); | ||
chunkIndex = 0; | ||
chunkBuf.length = 0; | ||
chunkBuf.push(0); | ||
} | ||
// replace the matched parts of the string | ||
@@ -394,3 +568,3 @@ i = 0; | ||
textBuf[s] = content[inputPos]; | ||
if (s < (maxBackrefSize - 1)) { | ||
if (s < (maxBackrefLength - 1)) { | ||
textBuf[s + windowSize] = content[inputPos]; | ||
@@ -413,10 +587,4 @@ } | ||
// if there's a partial set of eight chunks, write it to the output | ||
if (chunkIndex > 0) { | ||
const binaryChunkBuf = new Uint8Array(chunkBuf); | ||
output.put(binaryChunkBuf); | ||
} | ||
return output.getU8(); | ||
return fnFlush(); | ||
} | ||
} |
{ | ||
"name": "@camoto/gamecomp", | ||
"version": "3.0.0", | ||
"version": "4.0.0", | ||
"description": "Apply and remove compression and encryption algorithms used by DOS games", | ||
@@ -5,0 +5,0 @@ "bin": { |
@@ -14,2 +14,4 @@ # gamecomp.js | ||
* cmp-lzss: Generic LZSS (Lempel-Ziv-Storer-Szymanski) | ||
* Byte mode (8 flag bits stored upfront in a single byte) | ||
* Bit mode (each flag bit stored before each literal or length+distance code) | ||
* cmp-lzw: Generic LZW (Lempel-Ziv-Welch) | ||
@@ -16,0 +18,0 @@ * cmp-rle-bash: Monster Bash *.DAT run-length-encoding |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
151
93485
20
2519