Noah Petherbridge
5654145fd8
Finally add a second option for Chunk MapAccessor implementation besides the MapAccessor. The RLEAccessor is basically a MapAccessor that will compress your drawing with Run Length Encoding (RLE) in the on-disk format in the ZIP file. This slashes the file sizes of most levels: * Shapeshifter: 21.8 MB -> 8.1 MB * Jungle: 10.4 MB -> 4.1 MB * Zoo: 2.8 MB -> 1.3 MB Implementation details: * The RLE binary format for Chunks is a stream of Uvarint pairs storing the palette index number and the number of pixels to repeat it (along the Y,X axis of the chunk). * Null colors are represented by a Uvarint that decodes to 0xFFFF or 65535 in decimal. * Gameplay logic currently limits maps to 256 colors. * The default for newly created chunks in-game will be RLE by default. * Its in-memory representation is still a MapAccessor (a map of absolute world coordinates to palette index). * The game can still open and play legacy MapAccessor maps. * On save in the editor, the game will upgrade/convert MapAccessor chunks over to RLEAccessors, improving on your level's file size with a simple re-save. Current Bugs * On every re-save to RLE, one pixel is lost in the bottom-right corner of each chunk. Each subsequent re-save loses one more pixel to the left, so what starts as a single pixel per chunk slowly evolves into a horizontal line. * Some pixels smear vertically as well. * Off-by-negative-one errors when some chunks Iter() their pixels but compute a relative coordinate of (-1,0)! Some mismatch between the stored world coords of a pixel inside the chunk vs. the chunk's assigned coordinate by the Chunker: certain combinations of chunk coord/abs coord. To Do * The `doodad touch` command should re-save existing levels to upgrade them.
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package level
|
|
|
|
import (
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level/rle"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/go/render"
|
|
)
|
|
|
|
// RLEAccessor implements a chunk accessor which stores its on-disk format using
|
|
// Run Length Encoding (RLE), but in memory behaves equivalently to the MapAccessor.
|
|
type RLEAccessor struct {
|
|
chunk *Chunk // parent Chunk, for its Size and Point
|
|
acc *MapAccessor
|
|
}
|
|
|
|
// NewRLEAccessor initializes a RLEAccessor.
|
|
func NewRLEAccessor(chunk *Chunk) *RLEAccessor {
|
|
return &RLEAccessor{
|
|
chunk: chunk,
|
|
acc: NewMapAccessor(chunk),
|
|
}
|
|
}
|
|
|
|
// Inflate the sparse swatches from their palette indexes.
|
|
func (a *RLEAccessor) Inflate(pal *Palette) error {
|
|
return a.acc.Inflate(pal)
|
|
}
|
|
|
|
// Len returns the current size of the map, or number of pixels registered.
|
|
func (a *RLEAccessor) Len() int {
|
|
return a.acc.Len()
|
|
}
|
|
|
|
// IterViewport returns a channel to loop over pixels in the viewport.
|
|
func (a *RLEAccessor) IterViewport(viewport render.Rect) <-chan Pixel {
|
|
return a.acc.IterViewport(viewport)
|
|
}
|
|
|
|
// Iter returns a channel to loop over all points in this chunk.
|
|
func (a *RLEAccessor) Iter() <-chan Pixel {
|
|
return a.acc.Iter()
|
|
}
|
|
|
|
// Get a pixel from the map.
|
|
func (a *RLEAccessor) Get(p render.Point) (*Swatch, error) {
|
|
return a.acc.Get(p)
|
|
}
|
|
|
|
// Set a pixel on the map.
|
|
func (a *RLEAccessor) Set(p render.Point, sw *Swatch) error {
|
|
return a.acc.Set(p, sw)
|
|
}
|
|
|
|
// Delete a pixel from the map.
|
|
func (a *RLEAccessor) Delete(p render.Point) error {
|
|
return a.acc.Delete(p)
|
|
}
|
|
|
|
/*
|
|
MarshalBinary converts the chunk data to a binary representation.
|
|
|
|
This accessor uses Run Length Encoding (RLE) in its binary format. Starting
|
|
with the top-left pixel of this chunk, the binary format is a stream of bytes
|
|
formatted as such:
|
|
|
|
- UVarint for the palette index number (0-255), with 0xFF meaning void
|
|
- UVarint for the length of repetition of that palette index
|
|
*/
|
|
func (a *RLEAccessor) MarshalBinary() ([]byte, error) {
|
|
// Flatten the chunk out into a full 2D array of all its points.
|
|
var (
|
|
size = int(a.chunk.Size)
|
|
grid, err = rle.NewGrid(size)
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Populate the dense 2D array of its pixels.
|
|
for px := range a.Iter() {
|
|
var (
|
|
point = render.NewPoint(px.X, px.Y)
|
|
relative = RelativeCoordinate(point, a.chunk.Point, a.chunk.Size)
|
|
ptr = uint64(px.Swatch.Index())
|
|
)
|
|
|
|
// TODO: sometimes we get a -1 value in X or Y, not sure why.
|
|
if relative.X < 0 || relative.Y < 0 {
|
|
continue
|
|
}
|
|
grid[relative.Y][relative.X] = &ptr
|
|
}
|
|
|
|
return grid.Compress()
|
|
}
|
|
|
|
// UnmarshalBinary will decode a compressed RLEAccessor byte stream.
|
|
func (a *RLEAccessor) UnmarshalBinary(compressed []byte) error {
|
|
a.acc.mu.Lock()
|
|
defer a.acc.mu.Unlock()
|
|
|
|
// New format: decompress the byte stream.
|
|
log.Debug("RLEAccessor.Unmarshal: Reading %d bytes of compressed chunk data", len(compressed))
|
|
|
|
grid, err := rle.NewGrid(int(a.chunk.Size))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := grid.Decompress(compressed); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load the grid into our MapAccessor.
|
|
a.acc.Reset()
|
|
for y, row := range grid {
|
|
for x, col := range row {
|
|
if col == nil {
|
|
continue
|
|
}
|
|
|
|
// TODO: x-1 to avoid the level creeping to the right every save,
|
|
// not sure on the root cause! RLEAccessor Decompress?
|
|
abs := FromRelativeCoordinate(render.NewPoint(x, y), a.chunk.Point, a.chunk.Size)
|
|
abs.X -= 1
|
|
a.acc.grid[abs] = NewSparseSwatch(int(*col))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
// Prepare the 2D grid to decompress the RLE stream into.
|
|
var (
|
|
size = int(a.chunk.Size)
|
|
_, err = rle.NewGrid(size)
|
|
x, y, cursor int
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var reader = bytes.NewBuffer(compressed)
|
|
|
|
for {
|
|
var (
|
|
paletteIndex, err1 = binary.ReadUvarint(reader)
|
|
repeatCount, err2 = binary.ReadUvarint(reader)
|
|
)
|
|
|
|
if err1 != nil || err2 != nil {
|
|
log.Error("reading Uvarints from compressed data: {%s, %s}", err1, err2)
|
|
break
|
|
}
|
|
|
|
log.Warn("RLE index %d for %dpx", paletteIndex, repeatCount)
|
|
|
|
for i := uint64(0); i < repeatCount; i++ {
|
|
cursor++
|
|
if cursor%size == 0 {
|
|
y++
|
|
x = 0
|
|
} else {
|
|
x++
|
|
}
|
|
|
|
point := render.NewPoint(int(x), int(y))
|
|
if paletteIndex != 0xFF {
|
|
a.acc.grid[point] = NewSparseSwatch(int(paletteIndex))
|
|
}
|
|
}
|
|
}
|
|
*/
|