Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -0,0 +1,98 @@
package bitmap
import (
"fmt"
"io"
"github.com/inkyblackness/hacked/ss1"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/serial"
)
const (
animationEndTag = 0x0C
animationEndTagData = 0x01
animationEntryTag = 0x04
errEndTagDataWrong ss1.StringError = "end tag data wrong"
)
type unknownEntryTagError struct {
tag byte
}
func (err unknownEntryTagError) Error() string {
return fmt.Sprintf("unknown entry tag 0x%02X", int(err.tag))
}
// Animation describes a sequence of bitmaps to form a small movie.
type Animation struct {
Width int16
Height int16
ResourceID resource.ID
IntroFlag uint16
Entries []AnimationEntry
}
// AnimationEntry describes one part of an animation with common frame time.
type AnimationEntry struct {
FirstFrame byte
LastFrame byte
FrameTime int16
}
// ReadAnimation extracts an animation from a serialized stream.
func ReadAnimation(reader io.Reader) (anim Animation, err error) {
decoder := serial.NewDecoder(reader)
codeAnimationBase(decoder, &anim)
done := false
err = decoder.FirstError()
for !done && (err == nil) {
var tag byte
decoder.Code(&tag)
err = decoder.FirstError()
switch tag {
case animationEntryTag:
var entry AnimationEntry
decoder.Code(&entry)
anim.Entries = append(anim.Entries, entry)
err = decoder.FirstError()
case animationEndTag:
var endTagData byte
decoder.Code(&endTagData)
if endTagData != animationEndTagData {
err = errEndTagDataWrong
}
done = true
default:
if err == nil {
err = unknownEntryTagError{tag: tag}
}
}
}
return
}
// WriteAnimation serializes an animation to a writer.
func WriteAnimation(writer io.Writer, anim Animation) error {
encoder := serial.NewEncoder(writer)
codeAnimationBase(encoder, &anim)
for _, entry := range anim.Entries {
encoder.Code([]byte{animationEntryTag})
encoder.Code(&entry) // nolint: scopelint
}
encoder.Code([]byte{animationEndTag, animationEndTagData})
return encoder.FirstError()
}
func codeAnimationBase(coder serial.Coder, anim *Animation) {
coder.Code(&anim.Width)
coder.Code(&anim.Height)
coder.Code(&anim.ResourceID)
var unknown [6]byte
coder.Code(&unknown)
coder.Code(&anim.IntroFlag)
}
@@ -0,0 +1,58 @@
package bitmap
import (
"github.com/inkyblackness/hacked/ss1/resource"
)
// AnimationCache retrieves animations from a localizer and keeps them decoded until they are invalidated.
type AnimationCache struct {
localizer resource.Localizer
animations map[resource.Key]Animation
}
// NewAnimationCache returns a new instance.
func NewAnimationCache(localizer resource.Localizer) *AnimationCache {
cache := &AnimationCache{
localizer: localizer,
animations: make(map[resource.Key]Animation),
}
return cache
}
// InvalidateResources lets the cache remove any animations from resources that are specified in the given slice.
func (cache *AnimationCache) InvalidateResources(ids []resource.ID) {
for _, id := range ids {
for key := range cache.animations {
if key.ID == id {
delete(cache.animations, key)
}
}
}
}
// Animation tries to look up given animation.
func (cache *AnimationCache) Animation(key resource.Key) (anim Animation, err error) {
anim, existing := cache.animations[key]
if existing {
return anim, nil
}
selector := cache.localizer.LocalizedResources(key.Lang)
view, err := selector.Select(key.ID)
if err != nil {
return
}
if (view.ContentType() != resource.Animation) || (view.BlockCount() != 1) {
return anim, resource.ErrWrongType(key, resource.Animation)
}
reader, err := view.Block(0)
if err != nil {
return
}
anim, err = ReadAnimation(reader)
if err != nil {
return
}
cache.animations[key] = anim
return anim, nil
}
@@ -0,0 +1,176 @@
package bitmap_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/resource"
)
const (
baseAnimationResourceID = 0x0A4C
)
type AnimationCacheSuite struct {
suite.Suite
localizedResources resource.LocalizedResourcesList
instance *bitmap.AnimationCache
}
func TestAnimationCacheSuite(t *testing.T) {
suite.Run(t, new(AnimationCacheSuite))
}
func (suite *AnimationCacheSuite) SetupTest() {
suite.instance = nil
}
func (suite *AnimationCacheSuite) TestAnimationReturnsValueIfOK() {
anim := suite.someAnimation(0)
suite.givenAnInstance()
suite.whenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, anim)))
suite.thenAnimationShouldReturn(anim, 0)
}
func (suite *AnimationCacheSuite) TestAnimationReturnsErrorIfResourceNotExisting() {
suite.givenAnInstance()
suite.whenResourcesAre()
suite.thenAnimationShouldReturnError(0)
}
func (suite *AnimationCacheSuite) TestTextTriesToReloadWhenCacheInvalidated() {
suite.givenAnInstance()
suite.givenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, suite.someAnimation(0))))
suite.givenAnimationWasRetrieved(0)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(baseAnimationResourceID)
suite.thenAnimationShouldReturnError(0)
}
func (suite *AnimationCacheSuite) TestInvalidationConsidersOnlyAffectedIDs() {
suite.givenAnInstance()
suite.givenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, suite.someAnimation(0)), suite.storing(1, suite.someAnimation(1))))
suite.givenAnimationWasRetrieved(0)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x02BD)
suite.thenAnimationShouldReturn(suite.someAnimation(0), 0)
}
func (suite *AnimationCacheSuite) TestAnimationReturnsErrorIfResourceIsNotAnAnimation() {
suite.givenAnInstance()
suite.whenResourcesAre(suite.someLocalizedResources(suite.storingNonAnimation(0)))
suite.thenAnimationShouldReturnError(0)
}
func (suite *AnimationCacheSuite) givenAnInstance() {
suite.instance = bitmap.NewAnimationCache(suite)
}
func (suite *AnimationCacheSuite) givenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *AnimationCacheSuite) givenAnimationWasRetrieved(index int) {
_, err := suite.instance.Animation(suite.keyed(index))
require.Nil(suite.T(), err, "No error expected setting up")
}
func (suite *AnimationCacheSuite) whenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *AnimationCacheSuite) whenCacheResourcesAreInvalidated(ids ...resource.ID) {
suite.instance.InvalidateResources(ids)
}
func (suite *AnimationCacheSuite) thenAnimationShouldReturn(expected bitmap.Animation, index int) {
result, err := suite.instance.Animation(suite.keyed(index))
require.Nil(suite.T(), err, "No error expected")
assert.Equal(suite.T(), expected, result, "Animations don't match")
}
func (suite *AnimationCacheSuite) thenAnimationShouldReturnError(index int) {
_, err := suite.instance.Animation(suite.keyed(index))
require.NotNil(suite.T(), err, "Error expected")
}
func (suite AnimationCacheSuite) keyed(index int) resource.Key {
return resource.KeyOf(baseAnimationResourceID, resource.LangAny, index)
}
func (suite *AnimationCacheSuite) someLocalizedResources(modifiers ...func(*resource.Store)) resource.LocalizedResources {
var store resource.Store
for _, modifier := range modifiers {
modifier(&store)
}
return resource.LocalizedResources{
ID: "unnamed",
Language: resource.LangAny,
Viewer: store,
}
}
func (suite *AnimationCacheSuite) storing(id int, anim bitmap.Animation) func(*resource.Store) {
buf := bytes.NewBuffer(nil)
_ = bitmap.WriteAnimation(buf, anim)
return func(store *resource.Store) {
_ = store.Put(resource.ID(baseAnimationResourceID).Plus(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Animation,
Compound: true,
},
Blocks: resource.BlocksFrom([][]byte{buf.Bytes()}),
})
}
}
func (suite *AnimationCacheSuite) someAnimation(seed uint8) bitmap.Animation {
anim := bitmap.Animation{
Width: int16(seed) * 4,
Height: int16(seed) * 3,
IntroFlag: uint16(seed % 2),
ResourceID: resource.ID(seed),
Entries: make([]bitmap.AnimationEntry, seed+1),
}
for i := 0; i < len(anim.Entries); i++ {
anim.Entries[i] = bitmap.AnimationEntry{
FirstFrame: byte(i),
LastFrame: byte(i),
FrameTime: 100,
}
}
return anim
}
func (suite *AnimationCacheSuite) storingNonAnimation(id int) func(*resource.Store) {
return func(store *resource.Store) {
_ = store.Put(resource.ID(baseAnimationResourceID).Plus(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Sound,
Compound: true,
},
Blocks: resource.BlocksFrom([][]byte{{}}),
})
}
}
func (suite *AnimationCacheSuite) LocalizedResources(lang resource.Language) resource.Selector {
return resource.Selector{
From: suite.localizedResources,
Lang: lang,
}
}
@@ -0,0 +1,130 @@
package bitmap_test
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/resource"
)
func TestReadAnimationErrorsLength(t *testing.T) {
for i := 0; i < 20; i++ {
data := make([]byte, i)
_, err := bitmap.ReadAnimation(bytes.NewReader(data))
assert.Error(t, err, fmt.Sprintf("Error expected for %d bytes", i))
}
}
func TestReadAnimationErrorsTags(t *testing.T) {
tt := []struct {
info string
data []byte
}{
{
info: "end tag data missing",
data: []byte{
0x00, 0x00,
0x02, 0x02,
0x04, 0x04,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
0x0C, 0x0C,
0x0C,
},
},
{
info: "end tag data wrong",
data: []byte{
0x00, 0x00,
0x02, 0x02,
0x04, 0x04,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
0x0C, 0x0C,
0x0C, 0x00,
},
},
{
info: "unknown tag",
data: []byte{
0x00, 0x00,
0x02, 0x02,
0x04, 0x04,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
0x0C, 0x0C,
0x0B,
},
},
}
for _, tc := range tt {
_, err := bitmap.ReadAnimation(bytes.NewReader(tc.data))
assert.Error(t, err, fmt.Sprintf("Error expected for %s", tc.info))
}
}
func TestReadWriteAnimationEntries(t *testing.T) {
tt := []struct {
info string
data []byte
expected bitmap.Animation
}{
{
info: "empty list of entries",
data: []byte{
0x12, 0x34,
0x56, 0x78,
0xAA, 0xBB,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xCC, 0xDD,
0x0C, 0x01,
},
expected: bitmap.Animation{
Width: 0x3412,
Height: 0x7856,
ResourceID: resource.ID(0xBBAA),
IntroFlag: 0xDDCC,
Entries: nil,
},
},
{
info: "two entries",
data: []byte{
0x12, 0x34,
0x56, 0x78,
0xAA, 0xBB,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xCC, 0xDD,
0x04, 0x00, 0x0A, 0x10, 0x20,
0x04, 0x0B, 0x10, 0x30, 0x40,
0x0C, 0x01,
},
expected: bitmap.Animation{
Width: 0x3412,
Height: 0x7856,
ResourceID: resource.ID(0xBBAA),
IntroFlag: 0xDDCC,
Entries: []bitmap.AnimationEntry{
{FirstFrame: 0x00, LastFrame: 0x0A, FrameTime: 0x2010},
{FirstFrame: 0x0B, LastFrame: 0x10, FrameTime: 0x4030},
},
},
},
}
for _, tc := range tt {
td := tc
t.Run(tc.info, func(t *testing.T) {
anim, err := bitmap.ReadAnimation(bytes.NewReader(td.data))
require.Nil(t, err, "No error expected")
assert.Equal(t, td.expected, anim, "Animation data not as expected after reading")
output := bytes.NewBuffer(nil)
err = bitmap.WriteAnimation(output, anim)
require.Nil(t, err, "No error expected")
assert.Equal(t, td.data, output.Bytes(), "Data mismatch after re-encoding")
})
}
}
+101
View File
@@ -0,0 +1,101 @@
package bitmap
import (
"bytes"
"encoding/binary"
"io"
"github.com/inkyblackness/hacked/ss1"
"github.com/inkyblackness/hacked/ss1/serial/rle"
)
const (
errReaderIsNil ss1.StringError = "reader is nil"
errStrideNotValid ss1.StringError = "stride not equal to width for compressed bitmap"
errDataCouldNotBeRead ss1.StringError = "data could not be read"
errPaletteCouldNotBeRead ss1.StringError = "palette could not be read"
)
// Bitmap describes a palette based image.
type Bitmap struct {
Header Header
Pixels []byte
Palette *Palette
}
// Decode tries to read a bitmap from given reader.
// Should the bitmap be a compressed bitmap, then a reference image with pixel data all 0x00 is assumed.
func Decode(reader io.Reader) (*Bitmap, error) {
return DecodeReferenced(reader, func(width, height int16) ([]byte, error) {
return make([]byte, int(width)*int(height)), nil
})
}
// DecodeReferenced tries to read a bitmap from given reader.
// If the serialized bitmap describes a compressed bitmap, then the pixels from the reference are used as a basis for the result.
// The returned byte array from the provider will be used as pixel buffer for the new bitmap.
func DecodeReferenced(reader io.Reader, provider func(width, height int16) ([]byte, error)) (*Bitmap, error) {
if reader == nil {
return nil, errReaderIsNil
}
var bmp Bitmap
err := binary.Read(reader, binary.LittleEndian, &bmp.Header)
if err != nil {
return nil, err
}
if bmp.Header.Type == TypeCompressed8Bit {
if bmp.Header.Stride != uint16(bmp.Header.Width) {
return nil, errStrideNotValid
}
bmp.Pixels, err = provider(bmp.Header.Width, bmp.Header.Height)
if err != nil {
return nil, err
}
err = rle.Decompress(reader, bmp.Pixels)
} else {
bmp.Pixels = make([]byte, int(bmp.Header.Height)*int(bmp.Header.Stride))
_, err = reader.Read(bmp.Pixels)
}
if err != nil {
return nil, errDataCouldNotBeRead
}
if bmp.Header.PaletteOffset != 0 {
paletteFlag := uint32(0)
_ = binary.Read(reader, binary.LittleEndian, &paletteFlag)
bmp.Palette = new(Palette)
err = binary.Read(reader, binary.LittleEndian, bmp.Palette)
if err != nil {
return nil, errPaletteCouldNotBeRead
}
}
return &bmp, nil
}
// Encode writes the bitmap to a byte array and returns it.
// Compressed bitmaps will be compressed with a reference image with pixel data all 0x00.
func Encode(bmp *Bitmap, offsetBase int) []byte {
rawData := bmp.Pixels
if bmp.Header.Type == TypeCompressed8Bit {
buf := bytes.NewBuffer(nil)
_ = rle.Compress(buf, rawData, nil)
rawData = buf.Bytes()
}
header := bmp.Header
if bmp.Palette != nil {
header.PaletteOffset = int32(offsetBase + HeaderSize + len(rawData))
}
buf := bytes.NewBuffer(nil)
_ = binary.Write(buf, binary.LittleEndian, &header)
_ = binary.Write(buf, binary.LittleEndian, rawData)
if bmp.Palette != nil {
_ = binary.Write(buf, binary.LittleEndian, paletteMarker)
_ = binary.Write(buf, binary.LittleEndian, bmp.Palette)
}
return buf.Bytes()
}
@@ -0,0 +1,114 @@
package bitmap_test
import (
"bytes"
"encoding/binary"
"testing"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecodeReturnsErrorOnNilSource(t *testing.T) {
_, err := bitmap.Decode(nil)
assert.Error(t, err, "error expected")
}
func TestDecodeOfUncompressedDataReturnsBitmap(t *testing.T) {
data := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
bmp, err := bitmap.Decode(bytes.NewReader(data))
require.Nil(t, err, "no error expected")
require.NotNil(t, bmp, "bitmap expected")
assert.Equal(t, int16(1), bmp.Header.Width)
assert.Equal(t, int16(1), bmp.Header.Height)
assert.Equal(t, []byte{0xAA}, bmp.Pixels)
}
func TestDecodeOfCompressedDataReturnsBitmap(t *testing.T) {
data := getTestData(bitmap.TypeCompressed8Bit, []byte{0x00, 0x01, 0xBB, 0x80, 0x00, 0x00}, false)
bmp, err := bitmap.Decode(bytes.NewReader(data))
require.Nil(t, err, "no error expected")
require.NotNil(t, bmp, "bitmap expected")
assert.Equal(t, int16(1), bmp.Header.Width)
assert.Equal(t, int16(1), bmp.Header.Height)
assert.Equal(t, []byte{0xBB}, bmp.Pixels)
}
func TestDecodeWithPrivatePaletteReturnsPalette(t *testing.T) {
data := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, true)
bmp, err := bitmap.Decode(bytes.NewReader(data))
require.Nil(t, err, "no error expected")
require.NotNil(t, bmp, "bitmap expected")
assert.NotNil(t, bmp.Palette, "palette expected")
}
func TestWriteUncompressedWithoutPalette(t *testing.T) {
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
result := bitmap.Encode(bmp, 0)
assert.Equal(t, sourceData, result)
}
func TestWriteCompressedWithoutPalette(t *testing.T) {
sourceData := getTestData(bitmap.TypeCompressed8Bit, []byte{0x01, 0xBB, 0x80, 0x00, 0x00}, false)
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
result := bitmap.Encode(bmp, 0)
assert.Equal(t, sourceData, result)
}
func TestWriteUncompressedWithPalette(t *testing.T) {
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xCC}, true)
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
result := bitmap.Encode(bmp, 0)
assert.Equal(t, sourceData, result)
}
func TestWriteCompressedWithPalette(t *testing.T) {
sourceData := getTestData(bitmap.TypeCompressed8Bit, []byte{0x01, 0xEE, 0x80, 0x00, 0x00}, true)
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
result := bitmap.Encode(bmp, 0)
assert.Equal(t, sourceData, result)
}
func TestWriteWithForceTransparency(t *testing.T) {
sourceData := getTestData(bitmap.TypeFlat8Bit, []byte{0xAA}, false)
sourceData[6] = 1
bmp, _ := bitmap.Decode(bytes.NewReader(sourceData))
result := bitmap.Encode(bmp, 0)
assert.Equal(t, sourceData, result)
}
func getTestData(bmpType bitmap.Type, data []byte, withPalette bool) []byte {
var header bitmap.Header
buf := bytes.NewBuffer(nil)
header.Type = bmpType
header.Width = 1
header.Stride = 1
header.Height = 1
header.WidthFactor = 1
header.HeightFactor = 1
if withPalette {
header.PaletteOffset = int32(binary.Size(header) + len(data))
}
_ = binary.Write(buf, binary.LittleEndian, &header)
buf.Write(data)
if withPalette {
var pal bitmap.Palette
_ = binary.Write(buf, binary.LittleEndian, uint32(0x01000000))
_ = binary.Write(buf, binary.LittleEndian, pal)
}
return buf.Bytes()
}
@@ -0,0 +1,105 @@
package bitmap
import (
"image"
"image/color"
"math"
)
// d64 is the reference white point.
var d65 = [3]float64{0.95047, 1.00000, 1.08883}
func labF(t float64) float64 {
if t > 6.0/29.0*6.0/29.0*6.0/29.0 {
return math.Cbrt(t)
}
return t/3.0*29.0/6.0*29.0/6.0 + 4.0/29.0
}
func square(value float64) float64 {
return value * value
}
type labEntry struct {
l float64
a float64
b float64
}
func labEntryFromColor(clr color.Color) labEntry {
rLinear, gLinear, bLinear, _ := clr.RGBA()
r, g, b := float64(rLinear)/float64(0xFFFF), float64(gLinear)/float64(0xFFFF), float64(bLinear)/float64(0xFFFF)
x := 0.4124564*r + 0.3575761*g + 0.1804375*b
y := 0.2126729*r + 0.7151522*g + 0.0721750*b
z := 0.0193339*r + 0.1191920*g + 0.9503041*b
whiteRef := d65
fy := labF(y / whiteRef[1])
entry := labEntry{
l: 1.16*fy - 0.16,
a: 5.0 * (labF(x/whiteRef[0]) - fy),
b: 2.0 * (fy - labF(z/whiteRef[2]))}
return entry
}
func (entry labEntry) distanceTo(other labEntry) float64 {
return math.Sqrt(square(entry.l-other.l) + square(entry.a-other.a) + square(entry.b-other.b))
}
// Bitmapper creates bitmap images from generic images.
type Bitmapper struct {
pal []labEntry
}
// NewBitmapper returns a new bitmapper instance based on the given palette.
func NewBitmapper(palette *Palette) *Bitmapper {
bitmapper := &Bitmapper{}
for _, clr := range palette {
bitmapper.pal = append(bitmapper.pal, labEntryFromColor(clr.Color(0xFF)))
}
return bitmapper
}
// Map maps the provided image to a bitmap based on the internal palette.
func (bitmapper *Bitmapper) Map(img image.Image) Bitmap {
var bmp Bitmap
bounds := img.Bounds()
bmp.Header.Width = int16(math.Max(0, math.Min(float64(bounds.Dx()), math.MaxInt16)))
bmp.Header.Height = int16(math.Max(0, math.Min(float64(bounds.Dy()), math.MaxInt16)))
bmp.Pixels = make([]byte, int(bmp.Header.Width)*int(bmp.Header.Height))
for row := 0; row < int(bmp.Header.Height); row++ {
for column := 0; column < int(bmp.Header.Width); column++ {
bmp.Pixels[row*int(bmp.Header.Width)+column] = bitmapper.MapColor(img.At(column, row))
}
}
return bmp
}
// MapColor maps the provided color to the nearest index in the palette.
func (bitmapper *Bitmapper) MapColor(clr color.Color) (palIndex byte) {
_, _, _, a := clr.RGBA() // nolint:dogsled
indexWithin := func(index, from, to int) bool {
return (index >= from) && (index <= to)
}
if a > 0 {
clrEntry := labEntryFromColor(clr)
palDistance := 1000.0
for colorIndex, palEntry := range bitmapper.pal {
isRegularColor := indexWithin(colorIndex, 0x01, 0x02) || indexWithin(colorIndex, 0x08, 0x0A) || indexWithin(colorIndex, 0x20, 0xFF)
if isRegularColor {
distance := palEntry.distanceTo(clrEntry)
if distance < palDistance {
palDistance = distance
palIndex = byte(colorIndex)
}
}
}
}
return
}
@@ -0,0 +1,5 @@
package bitmap
const (
paletteMarker uint32 = 0x01000000
)
@@ -0,0 +1,43 @@
package bitmap
// Type describes the data layout of a bitmap.
type Type byte
// Type constants are listed below.
const (
// TypeFlat8Bit bitmaps are 8-bit paletted bitmaps that have their pixel stored in a flat layout.
TypeFlat8Bit Type = 2
// TypeCompressed8Bit bitmaps are 8-bit paletted bitmaps that have their pixel compressed in storage.
// Compression is using run-length-encoding (RLE); See package rle.
TypeCompressed8Bit Type = 4
)
// Flag adds further properties.
type Flag uint16
// Flag constants are listed below.
const (
// FlagTransparent is set for bitmaps that shall treat palette index 0x00 as fully transparent.
FlagTransparent Flag = 0x0001
)
// HeaderSize is the size of the Header structure, in bytes.
const HeaderSize = 28
// Area is a placeholder for either a rectangle, or an anchoring point (first two entries).
type Area [4]int16
// Header contains the meta information for a bitmap.
type Header struct {
_ [4]byte
Type Type
_ byte
Flags Flag
Width int16
Height int16
Stride uint16
WidthFactor byte
HeightFactor byte
Area Area
PaletteOffset int32
}
@@ -0,0 +1,65 @@
package bitmap
import (
"bytes"
"image/color"
"github.com/lucasb-eyer/go-colorful"
)
// RGB is describing Red, Green, and Blue intensities with 8-bit resolution.
type RGB struct {
Red uint8
Green uint8
Blue uint8
}
// Color returns the RGB data as a regular Color entry.
func (col RGB) Color(alpha byte) color.Color {
return color.RGBA{
R: col.Red,
G: col.Green,
B: col.Blue,
A: alpha,
}
}
// PaletteSize indicates how many colors a palette can hold with a byte index.
const PaletteSize = 256
// Palette describes a list of colors to be used by bitmaps.
// It is an array of 256 RGB values.
type Palette [PaletteSize]RGB
// ColorPalette returns a palette usable for the image packages.
func (pal Palette) ColorPalette(firstIndexTransparent bool) color.Palette {
result := make(color.Palette, len(pal))
for index, col := range pal {
alpha := byte(0xFF)
if (index == 0) && firstIndexTransparent {
alpha = 0x00
}
result[index] = col.Color(alpha)
}
return result
}
// IndexClosestTo returns the index into this palette that matches the given color the closest.
// This search excludes the provided indices.
func (pal Palette) IndexClosestTo(rgb RGB, excluding []byte) byte {
targetColor, _ := colorful.MakeColor(rgb.Color(0xFF))
closestDist := float64(-1)
closestIndex := byte(0)
for index, other := range pal {
if bytes.IndexByte(excluding, byte(index)) >= 0 {
continue
}
otherColor, _ := colorful.MakeColor(other.Color(0xFF))
dist := targetColor.DistanceLab(otherColor)
if (closestDist < 0) || (dist < closestDist) {
closestDist = dist
closestIndex = byte(index)
}
}
return closestIndex
}
@@ -0,0 +1,67 @@
package bitmap
import (
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/serial"
)
// PaletteCache retrieves palettes from a localizer and keeps them decoded until they are invalidated.
type PaletteCache struct {
localizer resource.Localizer
defaultPalette Palette
palettes map[resource.Key]Palette
}
// NewPaletteCache returns a new instance.
func NewPaletteCache(localizer resource.Localizer) *PaletteCache {
cache := &PaletteCache{
localizer: localizer,
palettes: make(map[resource.Key]Palette),
}
for i := 0; i < len(cache.defaultPalette); i++ {
col := &cache.defaultPalette[i]
col.Red = uint8(i)
col.Green = uint8(i)
col.Blue = uint8(i)
}
return cache
}
// InvalidateResources lets the cache remove any palettes from resources that are specified in the given slice.
func (cache *PaletteCache) InvalidateResources(ids []resource.ID) {
for _, id := range ids {
for key := range cache.palettes {
if key.ID == id {
delete(cache.palettes, key)
}
}
}
}
// Palette tries to look up given palette.
func (cache *PaletteCache) Palette(key resource.Key) (pal Palette, err error) {
pal, existing := cache.palettes[key]
if existing {
return pal, nil
}
selector := cache.localizer.LocalizedResources(key.Lang)
view, err := selector.Select(key.ID)
if err != nil {
return cache.defaultPalette, err
}
if (view.ContentType() != resource.Palette) || (view.BlockCount() != 1) {
return cache.defaultPalette, resource.ErrWrongType(key, resource.Palette)
}
reader, err := view.Block(0)
if err != nil {
return cache.defaultPalette, err
}
decoder := serial.NewDecoder(reader)
decoder.Code(&pal)
if decoder.FirstError() != nil {
return cache.defaultPalette, decoder.FirstError()
}
cache.palettes[key] = pal
return pal, nil
}
@@ -0,0 +1,169 @@
package bitmap_test
import (
"bytes"
"testing"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/serial"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
const (
basePaletteResourceID = 0x02BC
)
type PaletteCacheSuite struct {
suite.Suite
localizedResources resource.LocalizedResourcesList
instance *bitmap.PaletteCache
}
func TestPaletteCacheSuite(t *testing.T) {
suite.Run(t, new(PaletteCacheSuite))
}
func (suite *PaletteCacheSuite) SetupTest() {
suite.instance = nil
}
func (suite *PaletteCacheSuite) TestPaletteReturnsValueIfOK() {
pal := suite.somePalette(0)
suite.givenAnInstance()
suite.whenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, pal)))
suite.thenPaletteShouldReturn(pal, 0)
}
func (suite *PaletteCacheSuite) TestPaletteReturnsErrorIfResourceNotExisting() {
suite.givenAnInstance()
suite.whenResourcesAre()
suite.thenPaletteShouldReturnError(0)
}
func (suite *PaletteCacheSuite) TestTextTriesToReloadWhenCacheInvalidated() {
suite.givenAnInstance()
suite.givenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, suite.somePalette(0))))
suite.givenPaletteWasRetrieved(0)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(basePaletteResourceID)
suite.thenPaletteShouldReturnError(0)
}
func (suite *PaletteCacheSuite) TestInvalidationConsidersOnlyAffectedIDs() {
suite.givenAnInstance()
suite.givenResourcesAre(
suite.someLocalizedResources(
suite.storing(0, suite.somePalette(0)), suite.storing(1, suite.somePalette(1))))
suite.givenPaletteWasRetrieved(0)
suite.givenResourcesAre()
suite.whenCacheResourcesAreInvalidated(0x02BD)
suite.thenPaletteShouldReturn(suite.somePalette(0), 0)
}
func (suite *PaletteCacheSuite) TestPaletteReturnsErrorIfResourceIsNotAPalette() {
suite.givenAnInstance()
suite.whenResourcesAre(suite.someLocalizedResources(suite.storingNonPalette(0)))
suite.thenPaletteShouldReturnError(0)
}
func (suite *PaletteCacheSuite) givenAnInstance() {
suite.instance = bitmap.NewPaletteCache(suite)
}
func (suite *PaletteCacheSuite) givenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *PaletteCacheSuite) givenPaletteWasRetrieved(index int) {
_, err := suite.instance.Palette(suite.keyed(index))
require.Nil(suite.T(), err, "No error expected setting up")
}
func (suite *PaletteCacheSuite) whenResourcesAre(resources ...resource.LocalizedResources) {
suite.localizedResources = resources
}
func (suite *PaletteCacheSuite) whenCacheResourcesAreInvalidated(ids ...resource.ID) {
suite.instance.InvalidateResources(ids)
}
func (suite *PaletteCacheSuite) thenPaletteShouldReturn(expected bitmap.Palette, index int) {
result, err := suite.instance.Palette(suite.keyed(index))
require.Nil(suite.T(), err, "No error expected")
assert.Equal(suite.T(), expected, result, "Palettes don't match")
}
func (suite *PaletteCacheSuite) thenPaletteShouldReturnError(index int) {
_, err := suite.instance.Palette(suite.keyed(index))
require.NotNil(suite.T(), err, "Error expected")
}
func (suite PaletteCacheSuite) keyed(index int) resource.Key {
return resource.KeyOf(basePaletteResourceID, resource.LangAny, index)
}
func (suite *PaletteCacheSuite) someLocalizedResources(modifiers ...func(*resource.Store)) resource.LocalizedResources {
var store resource.Store
for _, modifier := range modifiers {
modifier(&store)
}
return resource.LocalizedResources{
ID: "unnamed",
Language: resource.LangAny,
Viewer: store,
}
}
func (suite *PaletteCacheSuite) storing(id int, palette bitmap.Palette) func(*resource.Store) {
buf := bytes.NewBuffer(nil)
encoder := serial.NewEncoder(buf)
encoder.Code(palette)
return func(store *resource.Store) {
_ = store.Put(resource.ID(basePaletteResourceID).Plus(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Palette,
Compound: false,
},
Blocks: resource.BlocksFrom([][]byte{buf.Bytes()}),
})
}
}
func (suite *PaletteCacheSuite) somePalette(seed uint8) bitmap.Palette {
var pal bitmap.Palette
for i := 0; i < len(pal); i++ {
pal[i].Red = seed + uint8(i)
pal[i].Green = seed + uint8(255-i)
pal[i].Blue = seed + uint8(i*2)
}
return pal
}
func (suite *PaletteCacheSuite) storingNonPalette(id int) func(*resource.Store) {
return func(store *resource.Store) {
_ = store.Put(resource.ID(basePaletteResourceID).Plus(id), resource.Resource{
Properties: resource.Properties{
ContentType: resource.Sound,
Compound: false,
},
Blocks: resource.BlocksFrom([][]byte{{}}),
})
}
}
func (suite *PaletteCacheSuite) LocalizedResources(lang resource.Language) resource.Selector {
return resource.Selector{
From: suite.localizedResources,
Lang: lang,
}
}