Stability and Bugfixes

* Editor: Auto-save on a background goroutine so you don't randomly freeze
  the editor up during.
* Fix actor linking issues when you drag and re-place a linked doodad: the
  level was too eagerly calling PruneLinks() whenever a doodad was 'destroyed'
  (such as the one just picked up) breaking half of the link connection.
* Chunk unloader: do not unload a chunk that has been modified (Set or Delete
  called on), keep them in memory until the next ZIP file save to flush them
  out to disk.
* Link Tool: if you clicked an actor and don't want to connect a link, click
  the first actor again to de-select it.

Updates to the `doodad` tool:

* `doodad edit-level --resize <int>` can re-chunk a level to use a different
  chunk size than the default 128. Large chunk sizes 512+ lead to performance
  problems.
This commit is contained in:
Noah 2022-05-02 20:35:53 -07:00
parent fc736abd5f
commit 75fa0c7e56
9 changed files with 182 additions and 34 deletions

View File

@ -1,6 +1,7 @@
package commands package commands
import ( import (
"errors"
"fmt" "fmt"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
@ -23,6 +24,11 @@ func init() {
Aliases: []string{"q"}, Aliases: []string{"q"},
Usage: "limit output (don't show doodad data at the end)", Usage: "limit output (don't show doodad data at the end)",
}, },
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "write to a different output file than the input (especially for --resize)",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "title", Name: "title",
Usage: "set the level title", Usage: "set the level title",
@ -41,7 +47,11 @@ func init() {
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "max-size", Name: "max-size",
Usage: "set the page max size (WxH format, like 2550x3300)", Usage: "set the bounded level page max size (WxH format, like 2550x3300)",
},
&cli.IntFlag{
Name: "resize",
Usage: "change the chunk size, and re-encode the whole level into chunks of the new size",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "wallpaper", Name: "wallpaper",
@ -94,6 +104,11 @@ func editLevel(c *cli.Context, filename string) error {
log.Info("File: %s", filename) log.Info("File: %s", filename)
// Migrating it to a different chunk size?
if c.Int("resize") > 0 {
return rechunkLevel(c, filename, lvl)
}
/*************************** /***************************
* Update level properties * * Update level properties *
***************************/ ***************************/
@ -199,3 +214,45 @@ func editLevel(c *cli.Context, filename string) error {
return showLevel(c, filename) return showLevel(c, filename)
} }
// doodad edit-level --resize CHUNK_SIZE
//
// Handles the deep operation of re-copying the old level into a new level
// at the new chunk size.
func rechunkLevel(c *cli.Context, filename string, lvl *level.Level) error {
var chunkSize = c.Int("resize")
log.Info("Resizing the level's chunk size.")
log.Info("Current chunk size: %d", lvl.Chunker.Size)
log.Info("Target chunk size: %d", chunkSize)
if output := c.String("output"); output != "" {
filename = output
log.Info("Output file will be: %s", filename)
}
if chunkSize == lvl.Chunker.Size {
return errors.New("the level already has the target chunk size")
}
// Keep the level's current Chunker, and set a new one.
var oldChunker = lvl.Chunker
lvl.Chunker = level.NewChunker(chunkSize)
// Iterate all the Pixels of the old chunker.
log.Info("Copying pixels from old chunker into new chunker (this may take a while)...")
for pixel := range oldChunker.IterPixels() {
lvl.Chunker.Set(
pixel.Point(),
pixel.Swatch,
)
}
log.Info("Writing new data to filename: %s", filename)
if err := lvl.WriteFile(filename); err != nil {
log.Error(err.Error())
}
return showLevel(c, filename)
return nil
}

View File

@ -521,25 +521,38 @@ func (s *EditorScene) SaveLevel(filename string) error {
log.Error("Error publishing level: %s", err.Error()) log.Error("Error publishing level: %s", err.Error())
} }
s.lastAutosaveAt = time.Now()
return m.WriteFile(filename) return m.WriteFile(filename)
} }
// AutoSave takes an autosave snapshot of the level or drawing. // AutoSave takes an autosave snapshot of the level or drawing.
func (s *EditorScene) AutoSave() error { func (s *EditorScene) AutoSave() error {
var filename = "_autosave.level" var (
filename = "_autosave.level"
err error
)
s.d.FlashError("Beginning AutoSave() in a background thread")
// Trigger the auto-save in the background to not block the main thread.
go func() {
var err error
switch s.DrawingType { switch s.DrawingType {
case enum.LevelDrawing: case enum.LevelDrawing:
err = s.Level.WriteFile(filename)
s.d.Flash("Automatically saved level to %s", filename) s.d.Flash("Automatically saved level to %s", filename)
return s.Level.WriteFile(filename)
case enum.DoodadDrawing: case enum.DoodadDrawing:
filename = "_autosave.doodad" filename = "_autosave.doodad"
err = s.Doodad.WriteFile(filename)
s.d.Flash("Automatically saved doodad to %s", filename) s.d.Flash("Automatically saved doodad to %s", filename)
return s.Doodad.WriteFile(filename)
} }
return nil if err != nil {
s.d.FlashError("Error saving %s: %s", filename, err)
}
}()
return err
} }
// LoadDoodad loads a doodad from disk. // LoadDoodad loads a doodad from disk.

View File

@ -407,7 +407,6 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
for _, actor := range actors { for _, actor := range actors {
u.Scene.Level.Actors.Remove(actor.Actor) u.Scene.Level.Actors.Remove(actor.Actor)
} }
u.Scene.Level.PruneLinks()
drawing.InstallActors(u.Scene.Level.Actors) drawing.InstallActors(u.Scene.Level.Actors)
} }
} }

View File

@ -35,7 +35,9 @@ type Chunk struct {
texture render.Texturer texture render.Texturer
textureMasked render.Texturer textureMasked render.Texturer
textureMaskedColor render.Color textureMaskedColor render.Color
dirty bool
dirty bool // Chunk is changed and needs textures redrawn
modified bool // Chunk is changed and is held in memory til next Zipfile save
} }
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for // JSONChunk holds a lightweight (interface-free) copy of the Chunk for
@ -240,17 +242,35 @@ func (c *Chunk) Teardown() int {
} }
// Set proxies to the accessor and flags the texture as dirty. // Set proxies to the accessor and flags the texture as dirty.
//
// It also marks the chunk as "Modified" so it will be kept in memory until the drawing
// is next saved to disk and the chunk written out to the zipfile.
func (c *Chunk) Set(p render.Point, sw *Swatch) error { func (c *Chunk) Set(p render.Point, sw *Swatch) error {
c.dirty = true c.dirty = true
c.modified = true
return c.Accessor.Set(p, sw) return c.Accessor.Set(p, sw)
} }
// Delete proxies to the accessor and flags the texture as dirty. // Delete proxies to the accessor and flags the texture as dirty and marks the chunk "Modified".
func (c *Chunk) Delete(p render.Point) error { func (c *Chunk) Delete(p render.Point) error {
c.dirty = true c.dirty = true
c.modified = true
return c.Accessor.Delete(p) return c.Accessor.Delete(p)
} }
/*
IsModified returns the chunk's Modified flag. This is most likely to occur in the Editor when
the user is drawing onto the level. Modified chunks are not unloaded from memory ever, until
they can be saved back to disk in the Zipfile format. During regular gameplay, chunks are
loaded and unloaded as needed.
The modified flag is flipped on Set() or Delete() and is never unflipped. On file save,
the Chunker is reloaded from scratch to hold chunks cached from zipfile members.
*/
func (c *Chunk) IsModified() bool {
return c.modified
}
// Rect returns the bounding coordinates that the Chunk has pixels for. // Rect returns the bounding coordinates that the Chunk has pixels for.
func (c *Chunk) Rect() render.Rect { func (c *Chunk) Rect() render.Rect {
// Lowest and highest chunks. // Lowest and highest chunks.

View File

@ -432,10 +432,18 @@ func (c *Chunker) FreeChunk(p render.Point) bool {
return false return false
} }
// If this chunk has been modified since it was last loaded from ZIP, hang onto it
// in memory until the next save so we don't lose it.
if chunk, ok := c.Chunks[p]; ok {
if chunk.IsModified() {
return false
}
// Don't delete empty chunks, hang on until next zipfile save. // Don't delete empty chunks, hang on until next zipfile save.
if chunk, ok := c.Chunks[p]; ok && chunk.Len() == 0 { if chunk, ok := c.Chunks[p]; ok && chunk.Len() == 0 {
return false return false
} }
}
delete(c.Chunks, p) delete(c.Chunks, p)
return true return true

View File

@ -204,3 +204,13 @@ func ChunksInZipfile(zf *zip.Reader, layer int) []render.Point {
return result return result
} }
// ChunkInZipfile tests whether the chunk exists in the zipfile.
func ChunkInZipfile(zf *zip.Reader, layer int, coord render.Point) bool {
for _, chunk := range ChunksInZipfile(zf, layer) {
if chunk == coord {
return true
}
}
return false
}

View File

@ -395,6 +395,14 @@ func (s *PlayScene) setupPlayer(playerCharacterFilename string) {
} }
s.installPlayerDoodad(playerCharacterFilename, spawn, centerIn) s.installPlayerDoodad(playerCharacterFilename, spawn, centerIn)
// Scroll the level canvas to center on the start point.
scroll := render.Point{
X: -(spawn.X - (s.d.width / 2)),
Y: -(spawn.Y - (s.d.height / 2)),
}
log.Info("Scrolling level viewport to spawn (%s) location: %s", spawn, scroll)
s.drawing.ScrollTo(scroll)
} }
// Load and install the player doodad onto the level. // Load and install the player doodad onto the level.

View File

@ -517,15 +517,6 @@ func (w *Canvas) loopEditable(ev *event.State) error {
var WP = w.WorldIndexAt(cursor) var WP = w.WorldIndexAt(cursor)
for _, actor := range w.actors { for _, actor := range w.actors {
// Permanently color the actor if it's the current subject of the
// Link Tool (after 1st click, until 2nd click of other actor)
if w.linkFirst == actor {
actor.Canvas.Configure(ui.Config{
Background: render.RGBA(255, 153, 255, 153),
})
continue
}
// Compute the bounding box on screen where this doodad // Compute the bounding box on screen where this doodad
// visually appears. // visually appears.
var scrollBias = render.Point{ var scrollBias = render.Point{
@ -567,6 +558,14 @@ func (w *Canvas) loopEditable(ev *event.State) error {
actor.Canvas.SetBorderSize(0) actor.Canvas.SetBorderSize(0)
actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO
} }
// Permanently color the actor if it's the current subject of the
// Link Tool (after 1st click, until 2nd click of other actor)
if w.linkFirst == actor {
actor.Canvas.Configure(ui.Config{
Background: render.RGBA(255, 153, 255, 153),
})
}
} }
} }

View File

@ -4,16 +4,10 @@ import (
"errors" "errors"
"sort" "sort"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
) )
// LinkStart initializes the Link tool.
func (w *Canvas) LinkStart() {
w.Tool = drawtool.LinkTool
w.linkFirst = nil
}
// LinkAdd adds an actor to be linked in the Link tool. // LinkAdd adds an actor to be linked in the Link tool.
func (w *Canvas) LinkAdd(a *Actor) error { func (w *Canvas) LinkAdd(a *Actor) error {
if w.linkFirst == nil { if w.linkFirst == nil {
@ -22,6 +16,10 @@ func (w *Canvas) LinkAdd(a *Actor) error {
shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to", shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to",
a.Doodad().Title, a.Doodad().Title,
) )
} else if w.linkFirst == a {
// Clicked the same doodad twice, deselect it.
shmem.Flash("De-selected the doodad for linking.")
w.linkFirst = nil
} else { } else {
// Second click, call the OnLinkActors handler with the two actors. // Second click, call the OnLinkActors handler with the two actors.
if w.OnLinkActors != nil { if w.OnLinkActors != nil {
@ -66,3 +64,39 @@ func (w *Canvas) GetLinkedActors(a *Actor) []*Actor {
} }
return result return result
} }
// PrintLinks is a debug function that describes all actor links to console.
func (w *Canvas) PrintLinks() {
log.Error("### BEGIN Canvas.PrintLinks() ###")
// Map all of the actors by their IDs so we can look them up from their links
var actors = map[string]*Actor{}
for _, actor := range w.actors {
actors[actor.ID()] = actor
}
for _, actor := range w.actors {
var id = actor.ID()
if len(id) > 8 {
id = id[:8]
}
if len(actor.Actor.Links) > 0 {
log.Info("Actor %s (%s) at %s has %d links:", id, actor.Actor.Filename, actor.Position(), len(actor.Actor.Links))
for _, link := range actor.Actor.Links {
if otherActor, ok := actors[link]; ok {
var linkId = link
if len(linkId) > 8 {
linkId = linkId[:8]
}
log.Info("\tTo %s (%s) at %s", linkId, otherActor.Actor.Filename, otherActor.Position())
} else {
log.Error("\tTo unknown actor ID %s!", link)
}
}
}
}
log.Error("### END Canvas.PrintLinks() ###")
}