package level import ( "encoding/json" "fmt" "math" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/go/render" ) // Chunker is the data structure that manages the chunks of a level, and // provides the API to interact with the pixels using their absolute coordinates // while abstracting away the underlying details. type Chunker struct { Size int `json:"size"` Chunks ChunkMap `json:"chunks"` } // NewChunker creates a new chunk manager with a given chunk size. func NewChunker(size int) *Chunker { return &Chunker{ Size: size, Chunks: ChunkMap{}, } } // Inflate iterates over the pixels in the (loaded) chunks and expands any // Sparse Swatches (which have only their palette index, from the file format // on disk) to connect references to the swatches in the palette. func (c *Chunker) Inflate(pal *Palette) error { for coord, chunk := range c.Chunks { chunk.Point = coord chunk.Size = c.Size chunk.Inflate(pal) } return nil } // IterViewport returns a channel to iterate every point that exists within // the viewport rect. func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel { pipe := make(chan Pixel) go func() { // Get the chunk box coordinates. var ( topLeft = c.ChunkCoordinate(render.NewPoint(viewport.X, viewport.Y)) bottomRight = c.ChunkCoordinate(render.Point{ X: viewport.X + viewport.W, Y: viewport.Y + viewport.H, }) ) for cx := topLeft.X; cx <= bottomRight.X; cx++ { for cy := topLeft.Y; cy <= bottomRight.Y; cy++ { if chunk, ok := c.GetChunk(render.NewPoint(cx, cy)); ok { for px := range chunk.Iter() { // Verify this pixel is also in range. if px.Point().Inside(viewport) { pipe <- px } } } } } close(pipe) }() return pipe } // IterViewportChunks returns a channel to iterate over the Chunk objects that // appear within the viewport rect, instead of the pixels in each chunk. func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { pipe := make(chan render.Point) go func() { sent := make(map[render.Point]interface{}) for x := viewport.X; x < viewport.W; x += (c.Size / 4) { for y := viewport.Y; y < viewport.H; y += (c.Size / 4) { // Constrain this chunksize step to a point within the bounds // of the viewport. This can yield partial chunks on the edges // of the viewport. point := render.NewPoint(x, y) if point.X < viewport.X { point.X = viewport.X } else if point.X > viewport.X+viewport.W { point.X = viewport.X + viewport.W } if point.Y < viewport.Y { point.Y = viewport.Y } else if point.Y > viewport.Y+viewport.H { point.Y = viewport.Y + viewport.H } // Translate to a chunk coordinate, dedupe and send it. coord := c.ChunkCoordinate(render.NewPoint(x, y)) if _, ok := sent[coord]; ok { continue } sent[coord] = nil if _, ok := c.GetChunk(coord); ok { pipe <- coord } } } close(pipe) }() return pipe } // IterPixels returns a channel to iterate over every pixel in the entire // chunker. func (c *Chunker) IterPixels() <-chan Pixel { pipe := make(chan Pixel) go func() { for _, chunk := range c.Chunks { for px := range chunk.Iter() { pipe <- px } } close(pipe) }() return pipe } // WorldSize returns the bounding coordinates that the Chunker has chunks to // manage: the lowest pixels from the lowest chunks to the highest pixels of // the highest chunks. func (c *Chunker) WorldSize() render.Rect { chunkLowest, chunkHighest := c.Bounds() return render.Rect{ X: chunkLowest.X * c.Size, Y: chunkLowest.Y * c.Size, W: (chunkHighest.X * c.Size) + (c.Size - 1), H: (chunkHighest.Y * c.Size) + (c.Size - 1), } } // WorldSizePositive returns the WorldSize anchored to 0,0 with only positive // coordinates. func (c *Chunker) WorldSizePositive() render.Rect { S := c.WorldSize() return render.Rect{ X: 0, Y: 0, W: int(math.Abs(float64(S.X))) + S.W, H: int(math.Abs(float64(S.Y))) + S.H, } } // Bounds returns the boundary points of the lowest and highest chunk which // have any data in them. func (c *Chunker) Bounds() (low, high render.Point) { for coord := range c.Chunks { if coord.X < low.X { low.X = coord.X } if coord.Y < low.Y { low.Y = coord.Y } if coord.X > high.X { high.X = coord.X } if coord.Y > high.Y { high.Y = coord.Y } } return low, high } // GetChunk gets a chunk at a certain position. Returns false if not found. func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) { chunk, ok := c.Chunks[p] return chunk, ok } // Redraw marks every chunk as dirty and invalidates all their texture caches, // forcing the drawing to re-generate from scratch. func (c *Chunker) Redraw() { for _, chunk := range c.Chunks { chunk.SetDirty() } } // Prerender visits every chunk and fetches its texture, in order to pre-load // the whole drawing for smooth gameplay rather than chunks lazy rendering as // they enter the screen. func (c *Chunker) Prerender() { for _, chunk := range c.Chunks { _ = chunk.CachedBitmap(render.Invisible) } } // PrerenderN will pre-render the texture for N number of chunks and then // yield back to the caller. Returns the number of chunks that still need // textures rendered; zero when the last chunk has been prerendered. func (c *Chunker) PrerenderN(n int) (remaining int) { var ( total int // total no. of chunks available totalRendered int // no. of chunks with textures modified int // number modified this call ) for _, chunk := range c.Chunks { total++ if chunk.bitmap != nil { totalRendered++ continue } if modified < n { _ = chunk.CachedBitmap(render.Invisible) totalRendered++ modified++ } } remaining = total - totalRendered return } // Get a pixel at the given coordinate. Returns the Palette entry for that // pixel or else returns an error if not found. func (c *Chunker) Get(p render.Point) (*Swatch, error) { // Compute the chunk coordinate. coord := c.ChunkCoordinate(p) if chunk, ok := c.Chunks[coord]; ok { return chunk.Get(p) } return nil, fmt.Errorf("no chunk %s exists for point %s", coord, p) } // Set a pixel at the given coordinate. func (c *Chunker) Set(p render.Point, sw *Swatch) error { coord := c.ChunkCoordinate(p) chunk, ok := c.Chunks[coord] if !ok { chunk = NewChunk() c.Chunks[coord] = chunk chunk.Point = coord chunk.Size = c.Size } return chunk.Set(p, sw) } // SetRect sets a rectangle of pixels to a color all at once. func (c *Chunker) SetRect(r render.Rect, sw *Swatch) error { var ( xMin = r.X yMin = r.Y xMax = r.X + r.W yMax = r.Y + r.H ) for x := xMin; x < xMax; x++ { for y := yMin; y < yMax; y++ { c.Set(render.NewPoint(x, y), sw) } } return nil } // Delete a pixel at the given coordinate. func (c *Chunker) Delete(p render.Point) error { coord := c.ChunkCoordinate(p) defer c.pruneChunk(coord) if chunk, ok := c.Chunks[coord]; ok { return chunk.Delete(p) } return fmt.Errorf("no chunk %s exists for point %s", coord, p) } // DeleteRect deletes a rectangle of pixels between two points. // The rect is a relative one with a width and height, and the X,Y values are // an absolute world coordinate. func (c *Chunker) DeleteRect(r render.Rect) error { var ( xMin = r.X yMin = r.Y xMax = r.X + r.W yMax = r.Y + r.H ) for x := xMin; x < xMax; x++ { for y := yMin; y < yMax; y++ { c.Delete(render.NewPoint(x, y)) } } return nil } // pruneChunk will remove an empty chunk from the chunk map, called after // delete operations. func (c *Chunker) pruneChunk(coord render.Point) { if chunk, ok := c.Chunks[coord]; ok { if chunk.Len() == 0 { log.Info("Chunker.pruneChunk: %s has become empty", coord) delete(c.Chunks, coord) } } } // ChunkCoordinate computes a chunk coordinate from an absolute coordinate. func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point { if c.Size == 0 { return render.Point{} } size := float64(c.Size) return render.NewPoint( int(math.Floor(float64(abs.X)/size)), int(math.Floor(float64(abs.Y)/size)), ) } // ChunkMap maps a chunk coordinate to its chunk data. type ChunkMap map[render.Point]*Chunk // MarshalJSON to convert the chunk map to JSON. This is needed for writing so // the JSON encoder knows how to serializes a `map[Point]*Chunk` but the inverse // is not necessary to implement. func (c ChunkMap) MarshalJSON() ([]byte, error) { dict := map[string]*Chunk{} for point, chunk := range c { dict[point.String()] = chunk } out, err := json.Marshal(dict) return out, err }