Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
type compressor struct {
|
||||
coder serial.Coder
|
||||
writer *WordWriter
|
||||
|
||||
dictBuffer dictEntryBuffer
|
||||
overtime int
|
||||
dictionary *dictEntry
|
||||
dictionarySize int
|
||||
curEntry *dictEntry
|
||||
}
|
||||
|
||||
// NewCompressor creates a new compressor instance over a writer.
|
||||
func NewCompressor(target io.Writer) io.WriteCloser {
|
||||
coder := serial.NewEncoder(target)
|
||||
obj := &compressor{
|
||||
coder: coder,
|
||||
writer: NewWordWriter(coder),
|
||||
dictionary: rootDictEntry(),
|
||||
dictionarySize: 0,
|
||||
overtime: 0}
|
||||
|
||||
obj.resetDictionary()
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func (obj *compressor) resetDictionary() {
|
||||
obj.dictionarySize = 0
|
||||
for i := 0; i < 0x100; i++ {
|
||||
obj.dictionary.Add(byte(i), Word(i), obj.dictBuffer.entry(Word(i)))
|
||||
}
|
||||
obj.curEntry = obj.dictionary
|
||||
}
|
||||
|
||||
func (obj *compressor) Close() error {
|
||||
obj.writer.Write(obj.curEntry.key)
|
||||
obj.writer.Close()
|
||||
|
||||
return obj.coder.FirstError()
|
||||
}
|
||||
|
||||
func (obj *compressor) Write(p []byte) (n int, err error) {
|
||||
for _, input := range p {
|
||||
obj.addByte(input)
|
||||
}
|
||||
|
||||
return len(p), obj.coder.FirstError()
|
||||
}
|
||||
|
||||
func (obj *compressor) addByte(value byte) {
|
||||
nextEntry := obj.curEntry.next[int(value)]
|
||||
if nextEntry != nil {
|
||||
obj.curEntry = nextEntry
|
||||
} else {
|
||||
obj.writer.Write(obj.curEntry.key)
|
||||
|
||||
key := Word(int(literalLimit) + obj.dictionarySize)
|
||||
if key < Reset {
|
||||
obj.curEntry.Add(value, key, obj.dictBuffer.entry(key))
|
||||
obj.dictionarySize++
|
||||
} else {
|
||||
obj.onKeySaturation()
|
||||
}
|
||||
|
||||
obj.curEntry = obj.dictionary.next[value]
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *compressor) onKeySaturation() {
|
||||
obj.overtime++
|
||||
if obj.overtime > 1000 {
|
||||
obj.writer.Write(Reset)
|
||||
obj.resetDictionary()
|
||||
obj.overtime = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CompressorSuite struct {
|
||||
suite.Suite
|
||||
store *serial.ByteStore
|
||||
compressor io.WriteCloser
|
||||
}
|
||||
|
||||
func TestCompressorSuite(t *testing.T) {
|
||||
suite.Run(t, new(CompressorSuite))
|
||||
}
|
||||
|
||||
func (suite *CompressorSuite) SetupTest() {
|
||||
suite.store = serial.NewByteStore()
|
||||
suite.compressor = compression.NewCompressor(suite.store)
|
||||
}
|
||||
|
||||
func (suite *CompressorSuite) TestWriteCompressesFirstReocurrence() {
|
||||
suite.givenWrittenData([]byte{0x00, 0x01})
|
||||
suite.givenWrittenData([]byte{0x00, 0x01})
|
||||
|
||||
err := suite.compressor.Close()
|
||||
assert.Nil(suite.T(), err, "no error expected")
|
||||
|
||||
suite.thenWordsShouldBe(compression.Word(0x0000), compression.Word(0x0001), compression.Word(0x0100))
|
||||
}
|
||||
|
||||
func (suite *CompressorSuite) TestWriteCompressesTest1() {
|
||||
suite.givenWrittenData([]byte{0x00, 0x01, 0x00, 0x02, 0x01, 0x00, 0x01})
|
||||
|
||||
err := suite.compressor.Close()
|
||||
assert.Nil(suite.T(), err, "no error expected")
|
||||
|
||||
suite.thenWordsShouldBe(
|
||||
compression.Word(0x0000), compression.Word(0x0001), compression.Word(0x0000),
|
||||
compression.Word(0x0002), compression.Word(0x0101), compression.Word(0x0001))
|
||||
}
|
||||
|
||||
func (suite *CompressorSuite) givenWrittenData(data []byte) {
|
||||
written, err := suite.compressor.Write(data)
|
||||
require.Nil(suite.T(), err, "no error expected writing")
|
||||
require.Equal(suite.T(), len(data), written, "expected all bytes to be written")
|
||||
}
|
||||
|
||||
func (suite *CompressorSuite) thenWordsShouldBe(expected ...compression.Word) {
|
||||
source := bytes.NewReader(suite.store.Data())
|
||||
reader := compression.NewWordReader(serial.NewDecoder(source))
|
||||
var words []compression.Word
|
||||
|
||||
for read := reader.Read(); read != compression.EndOfStream; read = reader.Read() {
|
||||
words = append(words, read)
|
||||
}
|
||||
|
||||
assert.Equal(suite.T(), expected, words)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
type decompressor struct {
|
||||
coder serial.Coder
|
||||
reader *WordReader
|
||||
|
||||
isEndOfStream bool
|
||||
dictBuffer dictEntryBuffer
|
||||
dictionary *dictEntry
|
||||
dictionarySize int
|
||||
lastEntry *dictEntry
|
||||
lookup []*dictEntry
|
||||
|
||||
scratch []byte
|
||||
leftover []byte
|
||||
}
|
||||
|
||||
// NewDecompressor creates a new decompressor instance over a reader.
|
||||
func NewDecompressor(source io.Reader) io.Reader {
|
||||
coder := serial.NewDecoder(source)
|
||||
obj := &decompressor{
|
||||
coder: coder,
|
||||
reader: NewWordReader(coder),
|
||||
dictionary: rootDictEntry(),
|
||||
scratch: make([]byte, 1024)}
|
||||
obj.resetDictionary()
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
func (obj *decompressor) resetDictionary() {
|
||||
obj.dictionarySize = 0
|
||||
obj.lookup = make([]*dictEntry, 1024)
|
||||
obj.dictionary = rootDictEntry()
|
||||
for i := 0; i < 0x100; i++ {
|
||||
entry := obj.dictionary.Add(byte(i), Word(i), obj.dictBuffer.entry(Word(i)))
|
||||
obj.lookup[Word(i)] = entry
|
||||
}
|
||||
obj.lastEntry = obj.dictionary
|
||||
}
|
||||
|
||||
func (obj *decompressor) Read(p []byte) (n int, err error) {
|
||||
requested := len(p)
|
||||
|
||||
for n < requested && !obj.isEndOfStream && obj.coder.FirstError() == nil {
|
||||
n += obj.takeFromLeftover(p[n:])
|
||||
if n < requested {
|
||||
obj.readNextWord()
|
||||
n += obj.takeFromLeftover(p[n:])
|
||||
}
|
||||
}
|
||||
err = obj.coder.FirstError()
|
||||
if err == nil && obj.isEndOfStream {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (obj *decompressor) takeFromLeftover(target []byte) (provided int) {
|
||||
requested := len(target)
|
||||
available := len(obj.leftover)
|
||||
|
||||
if available > 0 && requested > 0 {
|
||||
provided = available
|
||||
if provided > requested {
|
||||
provided = requested
|
||||
}
|
||||
copy(target[0:provided], obj.leftover)
|
||||
obj.leftover = obj.leftover[provided:]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (obj *decompressor) readNextWord() {
|
||||
nextWord := obj.reader.Read()
|
||||
|
||||
if obj.lastEntry.depth > len(obj.scratch) {
|
||||
obj.scratch = make([]byte, len(obj.scratch)+1024)
|
||||
}
|
||||
obj.leftover = obj.scratch[:obj.lastEntry.depth]
|
||||
obj.lastEntry.Data(obj.leftover)
|
||||
switch nextWord {
|
||||
case EndOfStream:
|
||||
obj.isEndOfStream = true
|
||||
case Reset:
|
||||
obj.resetDictionary()
|
||||
default:
|
||||
var nextEntry *dictEntry
|
||||
if int(nextWord) < len(obj.lookup) {
|
||||
nextEntry = obj.lookup[int(nextWord)]
|
||||
}
|
||||
|
||||
switch {
|
||||
case nextEntry != nil:
|
||||
if obj.lastEntry.depth > 0 {
|
||||
obj.addToDictionary(nextEntry.FirstByte())
|
||||
}
|
||||
obj.lastEntry = nextEntry
|
||||
case nextWord >= literalLimit:
|
||||
nextValue := obj.lastEntry.FirstByte()
|
||||
obj.addToDictionary(nextValue)
|
||||
obj.lastEntry = obj.lastEntry.next[nextValue]
|
||||
default:
|
||||
nextValue := byte(nextWord)
|
||||
obj.addToDictionary(nextValue)
|
||||
obj.lastEntry = obj.dictionary.next[nextValue]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *decompressor) addToDictionary(value byte) {
|
||||
key := Word(int(literalLimit) + obj.dictionarySize)
|
||||
nextEntry := obj.lastEntry.Add(value, key, obj.dictBuffer.entry(key))
|
||||
if int(key) >= len(obj.lookup) {
|
||||
newLookup := make([]*dictEntry, len(obj.lookup)+1024)
|
||||
copy(newLookup, obj.lookup)
|
||||
obj.lookup = newLookup
|
||||
}
|
||||
obj.lookup[key] = nextEntry
|
||||
obj.dictionarySize++
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
type DecompressorSuite struct {
|
||||
suite.Suite
|
||||
store *serial.ByteStore
|
||||
compressor io.WriteCloser
|
||||
}
|
||||
|
||||
func TestDecompressorSuite(t *testing.T) {
|
||||
suite.Run(t, new(DecompressorSuite))
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTest1() {
|
||||
input := []byte{0x00, 0x01, 0x00, 0x01}
|
||||
|
||||
suite.verify(input)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTest2() {
|
||||
input := []byte{0x00, 0x01, 0x00, 0x01, 0x00, 0x01}
|
||||
|
||||
suite.verify(input)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTest3() {
|
||||
suite.verify([]byte{})
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTest4() {
|
||||
input := []byte{0x00, 0x01, 0x00, 0x02, 0x01, 0x00, 0x01}
|
||||
|
||||
suite.verify(input)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTest5() {
|
||||
input := []byte{0x00, 0x01, 0x00, 0x02, 0x01, 0x00, 0x01, 0x02, 0x01, 0x02, 0x01, 0x00, 0x01, 0x02}
|
||||
|
||||
suite.verify(input)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressTestRandom() {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for testCase := 0; testCase < 100; testCase++ {
|
||||
input := make([]byte, r.Intn(1024))
|
||||
for i := 0; i < len(input); i++ {
|
||||
input[i] = byte(r.Intn(256))
|
||||
}
|
||||
suite.verify(input)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressHandlesDictionaryResets() {
|
||||
suite.writeWords(0x0001, 0x0002, 0x0100, compression.Reset, 0x0003, 0x0004, 0x0100, compression.EndOfStream)
|
||||
|
||||
suite.verifyOutput([]byte{0x01, 0x02, 0x01, 0x02, 0x03, 0x04, 0x03, 0x04})
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) TestDecompressHandlesSelfReferencingWords() {
|
||||
suite.writeWords(0x0001, 0x0002, 0x0101, compression.EndOfStream)
|
||||
|
||||
suite.verifyOutput([]byte{0x01, 0x02, 0x02, 0x02})
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) writeWords(values ...compression.Word) {
|
||||
suite.store = serial.NewByteStore()
|
||||
coder := serial.NewEncoder(suite.store)
|
||||
writer := compression.NewWordWriter(coder)
|
||||
|
||||
for _, value := range values {
|
||||
writer.Write(value)
|
||||
}
|
||||
writer.Close()
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) verify(input []byte) {
|
||||
suite.store = serial.NewByteStore()
|
||||
suite.compressor = compression.NewCompressor(serial.NewEncoder(suite.store))
|
||||
|
||||
n, err := suite.compressor.Write(input)
|
||||
assert.Nil(suite.T(), err, "no error expected writing")
|
||||
assert.Equal(suite.T(), len(input), n, "invalid number written")
|
||||
err = suite.compressor.Close()
|
||||
assert.Nil(suite.T(), err, "no error expected closing")
|
||||
|
||||
suite.verifyOutput(input)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) verifyOutput(expected []byte) {
|
||||
output := suite.buffer(len(expected))
|
||||
source := bytes.NewReader(suite.store.Data())
|
||||
decompressor := compression.NewDecompressor(source)
|
||||
read, err := decompressor.Read(output)
|
||||
|
||||
assert.True(suite.T(), (err == nil) || errors.Is(err, io.EOF), fmt.Sprintf("unexpected error: %v", err))
|
||||
assert.Equal(suite.T(), len(output), read, "unexpected number of bytes read")
|
||||
assert.Equal(suite.T(), expected, output)
|
||||
}
|
||||
|
||||
func (suite *DecompressorSuite) buffer(byteCount int) []byte {
|
||||
result := make([]byte, byteCount)
|
||||
for i := range result {
|
||||
result[i] = 0xFF
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package compression
|
||||
|
||||
type dictEntry struct {
|
||||
prev *dictEntry
|
||||
depth int
|
||||
|
||||
next [256]*dictEntry
|
||||
|
||||
key Word
|
||||
value byte
|
||||
first byte
|
||||
used bool
|
||||
}
|
||||
|
||||
func rootDictEntry() *dictEntry {
|
||||
return &dictEntry{prev: nil, depth: 0, value: 0x00, key: Reset}
|
||||
}
|
||||
|
||||
var emptyList [256]*dictEntry
|
||||
|
||||
func (entry *dictEntry) Add(value byte, key Word, newEntry *dictEntry) *dictEntry {
|
||||
newEntry.prev = entry
|
||||
newEntry.depth = entry.depth + 1
|
||||
newEntry.value = value
|
||||
newEntry.key = key
|
||||
newEntry.first = entry.first
|
||||
if newEntry.used {
|
||||
copy(newEntry.next[:], emptyList[:])
|
||||
}
|
||||
newEntry.used = true
|
||||
|
||||
entry.next[value] = newEntry
|
||||
if entry.depth == 0 {
|
||||
newEntry.first = value
|
||||
}
|
||||
|
||||
return newEntry
|
||||
}
|
||||
|
||||
func (entry *dictEntry) Data(bytes []byte) {
|
||||
cur := entry
|
||||
for i := entry.depth - 1; i >= 0; i-- {
|
||||
bytes[i] = cur.value
|
||||
cur = cur.prev
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *dictEntry) FirstByte() byte {
|
||||
return entry.first
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package compression
|
||||
|
||||
// dictEntryBucketSizeBits governs how big one bucket in the buffer is.
|
||||
const dictEntryBucketSizeBits = 8
|
||||
|
||||
type dictEntryBucket [1 << dictEntryBucketSizeBits]dictEntry
|
||||
|
||||
type dictEntryBuffer struct {
|
||||
buckets []*dictEntryBucket
|
||||
}
|
||||
|
||||
func (buffer *dictEntryBuffer) entry(key Word) *dictEntry {
|
||||
bucketIndex := int(key) >> dictEntryBucketSizeBits
|
||||
index := int(key & ((1 << dictEntryBucketSizeBits) - 1))
|
||||
|
||||
for bucketIndex >= len(buffer.buckets) {
|
||||
buffer.buckets = append(buffer.buckets, new(dictEntryBucket))
|
||||
}
|
||||
|
||||
return &buffer.buckets[bucketIndex][index]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package compression
|
||||
|
||||
// Word is a compression stream entry.
|
||||
type Word uint16
|
||||
|
||||
const (
|
||||
bitsPerWord = uint(14)
|
||||
|
||||
// EndOfStream indicates the last word in the compressed stream.
|
||||
EndOfStream = Word(0x3FFF)
|
||||
// Reset indicates a reset of the dictionary.
|
||||
Reset = Word(0x3FFE)
|
||||
literalLimit = Word(0x0100)
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
package compression
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
// WordReader provides word instances from a serialized stream.
|
||||
type WordReader struct {
|
||||
coder serial.Coder
|
||||
|
||||
buffer byte
|
||||
bufferBitsAvailable uint
|
||||
}
|
||||
|
||||
// NewWordReader returns a new instance.
|
||||
func NewWordReader(coder serial.Coder) *WordReader {
|
||||
reader := &WordReader{coder: coder, buffer: 0x00, bufferBitsAvailable: 0}
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
// Read returns the next word from the stream.
|
||||
func (reader *WordReader) Read() (value Word) {
|
||||
remaining := bitsPerWord
|
||||
|
||||
for remaining > reader.bufferBitsAvailable {
|
||||
value = Word((uint(value) << reader.bufferBitsAvailable) | uint(reader.buffer))
|
||||
remaining -= reader.bufferBitsAvailable
|
||||
|
||||
reader.bufferByte()
|
||||
}
|
||||
|
||||
value = Word((uint(value) << remaining) | uint(reader.buffer)>>(8-remaining))
|
||||
reader.buffer &= 1<<(8-remaining) - 1
|
||||
reader.bufferBitsAvailable -= remaining
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (reader *WordReader) bufferByte() {
|
||||
reader.coder.Code(&reader.buffer)
|
||||
reader.bufferBitsAvailable = 8
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type WordReaderSuite struct {
|
||||
suite.Suite
|
||||
reader *compression.WordReader
|
||||
}
|
||||
|
||||
func TestWordReaderSuite(t *testing.T) {
|
||||
suite.Run(t, new(WordReaderSuite))
|
||||
}
|
||||
|
||||
func (suite *WordReaderSuite) TestReadCanReadFirstWord() {
|
||||
suite.givenReaderFrom()
|
||||
|
||||
assert.Equal(suite.T(), compression.EndOfStream, suite.reader.Read())
|
||||
}
|
||||
|
||||
func (suite *WordReaderSuite) TestReadCanReadSeveralWords() {
|
||||
input := []compression.Word{
|
||||
compression.Word(0x3FFF), compression.Word(0x0000), compression.Word(0x3FFF),
|
||||
compression.Word(0x0000), compression.Word(0x2001), compression.Word(0x1234),
|
||||
}
|
||||
suite.givenReaderFrom(input...)
|
||||
|
||||
output := make([]compression.Word, len(input)+1)
|
||||
for i := 0; i < len(output); i++ {
|
||||
output[i] = suite.reader.Read()
|
||||
}
|
||||
|
||||
assert.Equal(suite.T(), append(input, compression.EndOfStream), output)
|
||||
}
|
||||
|
||||
func (suite *WordReaderSuite) givenReaderFrom(words ...compression.Word) {
|
||||
store := serial.NewByteStore()
|
||||
encoder := serial.NewEncoder(store)
|
||||
writer := compression.NewWordWriter(encoder)
|
||||
|
||||
for _, value := range words {
|
||||
writer.Write(value)
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
source := bytes.NewReader(store.Data())
|
||||
suite.reader = compression.NewWordReader(serial.NewDecoder(source))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package compression
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
// WordWriter serializes a stream of word entries.
|
||||
type WordWriter struct {
|
||||
coder serial.Coder
|
||||
|
||||
bufferedBits uint
|
||||
scratch uint32
|
||||
|
||||
outBuffer [1024]byte
|
||||
outUsed int
|
||||
}
|
||||
|
||||
// NewWordWriter returns a new instance.
|
||||
func NewWordWriter(coder serial.Coder) *WordWriter {
|
||||
writer := &WordWriter{coder: coder, bufferedBits: 0, scratch: 0}
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
// Close finishes the stream of words.
|
||||
func (writer *WordWriter) Close() {
|
||||
writer.Write(EndOfStream)
|
||||
if writer.bufferedBits > 0 {
|
||||
writer.writeByte(byte(writer.scratch >> 16))
|
||||
}
|
||||
writer.writeByte(byte(0x00))
|
||||
writer.flushBuffer()
|
||||
}
|
||||
|
||||
// Write adds the given word to the stream.
|
||||
func (writer *WordWriter) Write(value Word) {
|
||||
writer.scratch |= uint32(value) << ((16 - bitsPerWord) + (8 - writer.bufferedBits))
|
||||
writer.bufferedBits += bitsPerWord
|
||||
writer.writeByte(byte(writer.scratch >> 16))
|
||||
writer.scratch <<= 8
|
||||
writer.bufferedBits -= 8
|
||||
|
||||
if writer.bufferedBits >= 8 {
|
||||
writer.writeByte(byte(writer.scratch >> 16))
|
||||
writer.scratch <<= 8
|
||||
writer.bufferedBits -= 8
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *WordWriter) writeByte(value byte) {
|
||||
writer.outBuffer[writer.outUsed] = value
|
||||
writer.outUsed++
|
||||
if writer.outUsed >= len(writer.outBuffer) {
|
||||
writer.flushBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *WordWriter) flushBuffer() {
|
||||
writer.coder.Code(writer.outBuffer[:writer.outUsed])
|
||||
writer.outUsed = 0
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type WordWriterSuite struct {
|
||||
suite.Suite
|
||||
writer *compression.WordWriter
|
||||
store *serial.ByteStore
|
||||
}
|
||||
|
||||
func TestWordWriterSuite(t *testing.T) {
|
||||
suite.Run(t, new(WordWriterSuite))
|
||||
}
|
||||
|
||||
func (suite *WordWriterSuite) SetupTest() {
|
||||
suite.store = serial.NewByteStore()
|
||||
coder := serial.NewEncoder(suite.store)
|
||||
suite.writer = compression.NewWordWriter(coder)
|
||||
}
|
||||
|
||||
func (suite *WordWriterSuite) TestCloseWritesEndOfStreamMarkerAndTrailingZeroByte() {
|
||||
suite.writer.Close()
|
||||
|
||||
assert.Equal(suite.T(), []byte{0xFF, 0xFC, 0x00}, suite.store.Data())
|
||||
}
|
||||
|
||||
func (suite *WordWriterSuite) TestCloseWritesRemainderOnlyIfNotEmpty() {
|
||||
suite.writer.Write(compression.Word(0x0000))
|
||||
suite.writer.Write(compression.Word(0x0000))
|
||||
suite.writer.Write(compression.Word(0x0000))
|
||||
suite.writer.Close()
|
||||
|
||||
assert.Equal(suite.T(), []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x00}, suite.store.Data())
|
||||
}
|
||||
|
||||
func (suite *WordWriterSuite) TestWriteAndCloseLinesUpBits() {
|
||||
suite.writer.Write(compression.Word(0x1FFE)) // 0111111 1111110
|
||||
suite.writer.Close() // 1111111 1111111
|
||||
|
||||
assert.Equal(suite.T(), []byte{0x7F, 0xFB, 0xFF, 0xF0, 0x00}, suite.store.Data())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource/lgres/internal/compression"
|
||||
"github.com/inkyblackness/hacked/ss1/serial"
|
||||
)
|
||||
|
||||
// To run a profile, run it with the following command:
|
||||
// go test -bench . -args -cpuprofile=prof
|
||||
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
func rawData(size int) []byte {
|
||||
data := make([]byte, size)
|
||||
rand.Read(data) // nolint:gas
|
||||
return data
|
||||
}
|
||||
|
||||
func initProfiling(b *testing.B, nameSuffix string) func() {
|
||||
b.Helper()
|
||||
flag.Parse()
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile + "-" + b.Name() + nameSuffix)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = pprof.StartCPUProfile(f)
|
||||
return func() { pprof.StopCPUProfile() }
|
||||
}
|
||||
return func() {}
|
||||
}
|
||||
|
||||
func BenchmarkRawDataStorage(b *testing.B) {
|
||||
profileStop := initProfiling(b, "")
|
||||
defer profileStop()
|
||||
data := rawData(1024 * 1024)
|
||||
b.ResetTimer()
|
||||
for run := 0; run < b.N; run++ {
|
||||
encoder := serial.NewEncoder(serial.NewByteStore())
|
||||
_, _ = encoder.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkCompression(b *testing.B, size int, nameSuffix string) {
|
||||
b.Helper()
|
||||
profileStop := initProfiling(b, nameSuffix)
|
||||
defer profileStop()
|
||||
data := rawData(size)
|
||||
b.ResetTimer()
|
||||
for run := 0; run < b.N; run++ {
|
||||
compressor := compression.NewCompressor(serial.NewByteStore())
|
||||
_, _ = compressor.Write(data)
|
||||
_ = compressor.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompression1KB(b *testing.B) {
|
||||
benchmarkCompression(b, 1024, "1KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompression16KB(b *testing.B) {
|
||||
benchmarkCompression(b, 1024*16, "16KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompression128KB(b *testing.B) {
|
||||
benchmarkCompression(b, 1024*128, "128KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompression512KB(b *testing.B) {
|
||||
benchmarkCompression(b, 1024*512, "512KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompression1024KB(b *testing.B) {
|
||||
benchmarkCompression(b, 1024*1024, "1024KB")
|
||||
}
|
||||
|
||||
func benchmarkCompressionDecompression(b *testing.B, size int, nameSuffix string) {
|
||||
b.Helper()
|
||||
profileStop := initProfiling(b, nameSuffix)
|
||||
defer profileStop()
|
||||
data := rawData(size)
|
||||
output := make([]byte, len(data))
|
||||
b.ResetTimer()
|
||||
for run := 0; run < b.N; run++ {
|
||||
store := serial.NewByteStore()
|
||||
compressor := compression.NewCompressor(store)
|
||||
_, _ = compressor.Write(data)
|
||||
_ = compressor.Close()
|
||||
_, _ = store.Seek(0, io.SeekStart)
|
||||
decompressor := compression.NewDecompressor(store)
|
||||
_, _ = decompressor.Read(output)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompressionDecompression1KB(b *testing.B) {
|
||||
benchmarkCompressionDecompression(b, 1024, "1KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompressionDecompression16KB(b *testing.B) {
|
||||
benchmarkCompressionDecompression(b, 1024*16, "16KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompressionDecompression128KB(b *testing.B) {
|
||||
benchmarkCompressionDecompression(b, 1024*128, "128KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompressionDecompression512KB(b *testing.B) {
|
||||
benchmarkCompressionDecompression(b, 1024*512, "512KB")
|
||||
}
|
||||
|
||||
func BenchmarkCompressionDecompression1024KB(b *testing.B) {
|
||||
benchmarkCompressionDecompression(b, 1024*1024, "1024KB")
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package compression implements the LG resource file compression algorithm.
|
||||
*/
|
||||
package compression
|
||||
@@ -0,0 +1,17 @@
|
||||
package format
|
||||
|
||||
const (
|
||||
// HeaderString is the start of an LG Res file.
|
||||
HeaderString = "LG Res File v2\r\n"
|
||||
// CommentTerminator terminates the header string.
|
||||
CommentTerminator = byte(0x1A)
|
||||
// ResourceDirectoryFileOffsetPos is the byte offset for the pointer to the directory.
|
||||
ResourceDirectoryFileOffsetPos = 0x7C
|
||||
// BoundarySize is the byte interval at which entries must be placed.
|
||||
BoundarySize = 4
|
||||
|
||||
// ResourceTypeFlagCompressed indicates a compressed resource.
|
||||
ResourceTypeFlagCompressed = byte(0x01)
|
||||
// ResourceTypeFlagCompound indicates a compound resource.
|
||||
ResourceTypeFlagCompound = byte(0x02)
|
||||
)
|
||||
Reference in New Issue
Block a user