299 lines
7.2 KiB
Go
299 lines
7.2 KiB
Go
package level
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/go/render"
|
|
)
|
|
|
|
// MapAccessor implements a chunk accessor by using a map of points to their
|
|
// palette indexes. This is the simplest accessor and is best for sparse chunks.
|
|
type MapAccessor struct {
|
|
grid map[render.Point]*Swatch
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewMapAccessor initializes a MapAccessor.
|
|
func NewMapAccessor() *MapAccessor {
|
|
return &MapAccessor{
|
|
grid: map[render.Point]*Swatch{},
|
|
}
|
|
}
|
|
|
|
// Inflate the sparse swatches from their palette indexes.
|
|
func (a *MapAccessor) Inflate(pal *Palette) error {
|
|
for point, swatch := range a.grid {
|
|
if swatch.IsSparse() {
|
|
// Replace this with the correct swatch from the palette.
|
|
if swatch.paletteIndex >= len(pal.Swatches) {
|
|
return fmt.Errorf("MapAccessor.Inflate: swatch for point %s has paletteIndex %d but palette has only %d colors",
|
|
point,
|
|
swatch.paletteIndex,
|
|
len(pal.Swatches),
|
|
)
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.grid[point] = pal.Swatches[swatch.paletteIndex] // <- concurrent write
|
|
a.mu.Unlock()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Len returns the current size of the map, or number of pixels registered.
|
|
func (a *MapAccessor) Len() int {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
return len(a.grid)
|
|
}
|
|
|
|
// IterViewport returns a channel to loop over pixels in the viewport.
|
|
func (a *MapAccessor) IterViewport(viewport render.Rect) <-chan Pixel {
|
|
pipe := make(chan Pixel)
|
|
go func() {
|
|
for px := range a.Iter() {
|
|
if px.Point().Inside(viewport) {
|
|
pipe <- px
|
|
}
|
|
}
|
|
close(pipe)
|
|
}()
|
|
return pipe
|
|
}
|
|
|
|
// Iter returns a channel to loop over all points in this chunk.
|
|
func (a *MapAccessor) Iter() <-chan Pixel {
|
|
pipe := make(chan Pixel)
|
|
go func() {
|
|
a.mu.Lock()
|
|
for point, swatch := range a.grid {
|
|
pipe <- Pixel{
|
|
X: point.X,
|
|
Y: point.Y,
|
|
Swatch: swatch,
|
|
}
|
|
}
|
|
a.mu.Unlock()
|
|
close(pipe)
|
|
}()
|
|
return pipe
|
|
}
|
|
|
|
// Get a pixel from the map.
|
|
func (a *MapAccessor) Get(p render.Point) (*Swatch, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
pixel, ok := a.grid[p] // <- concurrent read and write
|
|
if !ok {
|
|
return nil, errors.New("no pixel")
|
|
}
|
|
return pixel, nil
|
|
}
|
|
|
|
// Set a pixel on the map.
|
|
func (a *MapAccessor) Set(p render.Point, sw *Swatch) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
a.grid[p] = sw
|
|
return nil
|
|
}
|
|
|
|
// Delete a pixel from the map.
|
|
func (a *MapAccessor) Delete(p render.Point) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if _, ok := a.grid[p]; ok {
|
|
delete(a.grid, p)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("pixel was not there")
|
|
}
|
|
|
|
// MarshalJSON to convert the chunk map to JSON.
|
|
//
|
|
// When serialized, the key is the "X,Y" coordinate and the value is the
|
|
// swatch index of the Palette, rather than redundantly serializing out the
|
|
// Swatch object for every pixel.
|
|
//
|
|
// DEPRECATED: in the Zipfile format chunks will be saved as binary files
|
|
// instead of with their JSON wrappers, so MarshalJSON will be phased out.
|
|
func (a *MapAccessor) MarshalJSON() ([]byte, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// Write in the new compressed format.
|
|
if balance.CompressMapAccessor {
|
|
var compressed []byte
|
|
for point, sw := range a.grid {
|
|
var (
|
|
x = int64(point.X)
|
|
y = int64(point.Y)
|
|
sw = uint64(sw.index)
|
|
entry = []byte{}
|
|
)
|
|
|
|
entry = binary.AppendVarint(entry, x)
|
|
entry = binary.AppendVarint(entry, y)
|
|
entry = binary.AppendUvarint(entry, sw)
|
|
|
|
compressed = append(compressed, entry...)
|
|
}
|
|
|
|
out, err := json.Marshal(compressed)
|
|
return out, err
|
|
}
|
|
|
|
dict := map[string]int{}
|
|
for point, sw := range a.grid {
|
|
dict[point.String()] = sw.Index()
|
|
}
|
|
|
|
out, err := json.Marshal(dict)
|
|
return out, err
|
|
}
|
|
|
|
// UnmarshalJSON to convert the chunk map back from JSON.
|
|
//
|
|
// DEPRECATED: in the Zipfile format chunks will be saved as binary files
|
|
// instead of with their JSON wrappers, so MarshalJSON will be phased out.
|
|
func (a *MapAccessor) UnmarshalJSON(b []byte) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// Transparently upgrade the compression algorithm for this level.
|
|
// - Old style was a map[string]int like {"123,456": 4} mapping
|
|
// a coordinate to a palette index.
|
|
// - Now, coords and palettes are uint8 constrained so we can
|
|
// really tighten this up.
|
|
// For transparent upgrade, try and parse it the old way first.
|
|
var (
|
|
dict map[string]int // old-style
|
|
compressed []byte // new-style
|
|
)
|
|
err := json.Unmarshal(b, &dict)
|
|
if err != nil {
|
|
// Now try the new way.
|
|
err = json.Unmarshal(b, &compressed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// New format: decompress the byte stream.
|
|
if compressed != nil {
|
|
// log.Debug("MapAccessor.Unmarshal: Reading %d bytes of compressed chunk data", len(compressed))
|
|
|
|
var (
|
|
reader = bytes.NewBuffer(compressed)
|
|
)
|
|
|
|
for {
|
|
var (
|
|
x, err1 = binary.ReadVarint(reader)
|
|
y, err2 = binary.ReadVarint(reader)
|
|
sw, err3 = binary.ReadUvarint(reader)
|
|
)
|
|
|
|
point := render.NewPoint(int(x), int(y))
|
|
a.grid[point] = NewSparseSwatch(int(sw))
|
|
|
|
if err1 != nil || err2 != nil || err3 != nil {
|
|
// log.Error("Break read loop: %s; %s; %s", err1, err2, err3)
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Old format: read the dict in.
|
|
for coord, index := range dict {
|
|
point, err := render.ParsePoint(coord)
|
|
if err != nil {
|
|
return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err)
|
|
}
|
|
a.grid[point] = NewSparseSwatch(index)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
MarshalBinary converts the chunk data to a binary representation, for
|
|
better compression compared to JSON.
|
|
|
|
In the binary format each chunk begins with one Varint (the chunk Type)
|
|
followed by whatever wire format the chunk needs given its type.
|
|
|
|
This function is related to the CompressMapAccessor config constant:
|
|
the MapAccessor compression boils down each point to a series if packed
|
|
varints: the X, Y coord (varint) followed by palette index (Uvarint).
|
|
|
|
The output of this function is just the compressed MapAccessor stream.
|
|
*/
|
|
func (a *MapAccessor) MarshalBinary() ([]byte, error) {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// Write in the new compressed format.
|
|
var compressed []byte
|
|
for point, sw := range a.grid {
|
|
var (
|
|
x = int64(point.X)
|
|
y = int64(point.Y)
|
|
sw = uint64(sw.index)
|
|
entry = []byte{}
|
|
)
|
|
|
|
entry = binary.AppendVarint(entry, x)
|
|
entry = binary.AppendVarint(entry, y)
|
|
entry = binary.AppendUvarint(entry, sw)
|
|
|
|
compressed = append(compressed, entry...)
|
|
}
|
|
|
|
return compressed, nil
|
|
}
|
|
|
|
// UnmarshalBinary will decode a compressed MapAccessor byte stream.
|
|
func (a *MapAccessor) UnmarshalBinary(compressed []byte) error {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
// New format: decompress the byte stream.
|
|
//log.Debug("MapAccessor.Unmarshal: Reading %d bytes of compressed chunk data", len(compressed))
|
|
|
|
var reader = bytes.NewBuffer(compressed)
|
|
|
|
for {
|
|
var (
|
|
x, err1 = binary.ReadVarint(reader)
|
|
y, err2 = binary.ReadVarint(reader)
|
|
sw, err3 = binary.ReadUvarint(reader)
|
|
)
|
|
|
|
// We expect all 3 errors to be EOF together if the binary is formed correctly.
|
|
if err1 != nil || err2 != nil || err3 != nil {
|
|
if err1 == nil || err2 == nil || err3 == nil {
|
|
log.Error("MapAccessor.UnmarshalBinary: found odd number of varints!")
|
|
}
|
|
break
|
|
}
|
|
|
|
point := render.NewPoint(int(x), int(y))
|
|
a.grid[point] = NewSparseSwatch(int(sw))
|
|
}
|
|
|
|
return nil
|
|
}
|