doodle/pkg/level/chunk_rle.go
Noah Petherbridge 4851730ccf Fix RLE Encoding Off-by-One Errors [PTO]
Levels can now be converted to RLE encoded chunk accessors and be re-saved
continuously without any loss of information.

Off-by-one errors resolved:

* The rle.NewGrid() was adding a +1 everywhere making the 2D grids have 129
  elements to a side for a 128 chunk size.
* In rle.Decompress() the cursor value and translation to X,Y coordinates is
  fixed to avoid a pixel going missing at the end of the first row (128,0)
* The abs.X-- hack in UnmarshalBinary is no longer needed to prevent the
  chunks from scooting a pixel to the right on every save.

Doodad tool updates:

* Remove unused CLI flags in `doodad resave` (actors, chunks, script,
  attachment, verbose) and add a `--output` flag to save to a different file
  name to the original.
* Update `doodad show` to allow debugging of RLE compressed chunks:
    * CLI flag `--chunk=1,2` to specify a single chunk coordinate to debug
    * CLI flag `--visualize-rle` will Visualize() RLE compressed chunks in
      their 2D grid form in your terminal window (VERY noisy for large
      levels! Use the --chunk option to narrow to one chunk).

Bug fixes and misc changes:

* Chunk.Usage() to return a better percentage of chunk utilization.
* Chunker.ChunkFromZipfile() was split out into two functions:
    * RawChunkFromZipfile retrieves the raw bytes of the chunk as well as the
      file extension discovered (.bin or .json) so the caller can interpret
      the bytes correctly.
    * ChunkFromZipfile calls the former function and then depending on file
      extension, unmarshals from binary or json.
    * The Raw function enables the `doodad show` command to debug and visualize
      the raw contents of the RLE compressed chunks.
* Updated the Visualize() function for the RLE encoder: instead of converting
  palette indexes to hex (0-F) which would begin causing problems for palette
  indexes above 16 (as they would use two+ characters), indexes are mapped to
  a wider range of symbols (0-9A-Z) and roll over if you have more than 36
  colors on your level. This at least keeps the Visualize() grid an easy to
  read 128x128 characters in your terminal.
2024-05-24 13:54:41 -07:00

175 lines
4.0 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 y, row := range grid {
for x := range row {
var (
relative = render.NewPoint(x, y)
absolute = FromRelativeCoordinate(relative, a.chunk.Point, a.chunk.Size)
swatch, err = a.Get(absolute)
)
if err != nil {
continue
}
var ptr = uint64(swatch.Index())
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
}
abs := FromRelativeCoordinate(render.NewPoint(x, y), a.chunk.Point, a.chunk.Size)
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))
}
}
}
*/