From 75fa0c7e56a76e9341d62a70bc88e9de8b5d230f Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 2 May 2022 20:35:53 -0700 Subject: [PATCH] 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 ` can re-chunk a level to use a different chunk size than the default 128. Large chunk sizes 512+ lead to performance problems. --- cmd/doodad/commands/edit_level.go | 59 ++++++++++++++++++++++++++++++- pkg/editor_scene.go | 35 ++++++++++++------ pkg/editor_ui.go | 1 - pkg/level/chunk.go | 24 +++++++++++-- pkg/level/chunker.go | 14 ++++++-- pkg/level/chunker_zipfile.go | 10 ++++++ pkg/play_scene.go | 8 +++++ pkg/uix/canvas_editable.go | 17 +++++---- pkg/uix/canvas_link_tool.go | 48 +++++++++++++++++++++---- 9 files changed, 182 insertions(+), 34 deletions(-) diff --git a/cmd/doodad/commands/edit_level.go b/cmd/doodad/commands/edit_level.go index a1cffcc..37349b2 100644 --- a/cmd/doodad/commands/edit_level.go +++ b/cmd/doodad/commands/edit_level.go @@ -1,6 +1,7 @@ package commands import ( + "errors" "fmt" "git.kirsle.net/apps/doodle/pkg/level" @@ -23,6 +24,11 @@ func init() { Aliases: []string{"q"}, 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{ Name: "title", Usage: "set the level title", @@ -41,7 +47,11 @@ func init() { }, &cli.StringFlag{ 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{ Name: "wallpaper", @@ -94,6 +104,11 @@ func editLevel(c *cli.Context, filename string) error { 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 * ***************************/ @@ -199,3 +214,45 @@ func editLevel(c *cli.Context, filename string) error { 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 +} diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index b2c6382..9849596 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -521,25 +521,38 @@ func (s *EditorScene) SaveLevel(filename string) error { log.Error("Error publishing level: %s", err.Error()) } + s.lastAutosaveAt = time.Now() return m.WriteFile(filename) } // AutoSave takes an autosave snapshot of the level or drawing. func (s *EditorScene) AutoSave() error { - var filename = "_autosave.level" + var ( + filename = "_autosave.level" + err error + ) - switch s.DrawingType { - case enum.LevelDrawing: - s.d.Flash("Automatically saved level to %s", filename) - return s.Level.WriteFile(filename) - case enum.DoodadDrawing: - filename = "_autosave.doodad" + s.d.FlashError("Beginning AutoSave() in a background thread") - s.d.Flash("Automatically saved doodad to %s", filename) - return s.Doodad.WriteFile(filename) - } + // Trigger the auto-save in the background to not block the main thread. + go func() { + var err error + switch s.DrawingType { + case enum.LevelDrawing: + err = s.Level.WriteFile(filename) + s.d.Flash("Automatically saved level to %s", filename) + case enum.DoodadDrawing: + filename = "_autosave.doodad" + err = s.Doodad.WriteFile(filename) + s.d.Flash("Automatically saved doodad to %s", filename) + } - return nil + if err != nil { + s.d.FlashError("Error saving %s: %s", filename, err) + } + }() + + return err } // LoadDoodad loads a doodad from disk. diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 4571ed5..140e330 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -407,7 +407,6 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { for _, actor := range actors { u.Scene.Level.Actors.Remove(actor.Actor) } - u.Scene.Level.PruneLinks() drawing.InstallActors(u.Scene.Level.Actors) } } diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index f74c6b5..63d3b38 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -35,7 +35,9 @@ type Chunk struct { texture render.Texturer textureMasked render.Texturer 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 @@ -240,17 +242,35 @@ func (c *Chunk) Teardown() int { } // 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 { c.dirty = true + c.modified = true 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 { c.dirty = true + c.modified = true 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. func (c *Chunk) Rect() render.Rect { // Lowest and highest chunks. diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 58877c7..96b631b 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -432,9 +432,17 @@ func (c *Chunker) FreeChunk(p render.Point) bool { return false } - // Don't delete empty chunks, hang on until next zipfile save. - if chunk, ok := c.Chunks[p]; ok && chunk.Len() == 0 { - 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. + if chunk, ok := c.Chunks[p]; ok && chunk.Len() == 0 { + return false + } } delete(c.Chunks, p) diff --git a/pkg/level/chunker_zipfile.go b/pkg/level/chunker_zipfile.go index 7eff613..bb2fa5e 100644 --- a/pkg/level/chunker_zipfile.go +++ b/pkg/level/chunker_zipfile.go @@ -204,3 +204,13 @@ func ChunksInZipfile(zf *zip.Reader, layer int) []render.Point { 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 +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 2f1ddb3..3424c9a 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -395,6 +395,14 @@ func (s *PlayScene) setupPlayer(playerCharacterFilename string) { } 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. diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index c08309b..19c95d7 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -517,15 +517,6 @@ func (w *Canvas) loopEditable(ev *event.State) error { var WP = w.WorldIndexAt(cursor) 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 // visually appears. var scrollBias = render.Point{ @@ -567,6 +558,14 @@ func (w *Canvas) loopEditable(ev *event.State) error { actor.Canvas.SetBorderSize(0) 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), + }) + } } } diff --git a/pkg/uix/canvas_link_tool.go b/pkg/uix/canvas_link_tool.go index fd5b531..0818c79 100644 --- a/pkg/uix/canvas_link_tool.go +++ b/pkg/uix/canvas_link_tool.go @@ -4,16 +4,10 @@ import ( "errors" "sort" - "git.kirsle.net/apps/doodle/pkg/drawtool" + "git.kirsle.net/apps/doodle/pkg/log" "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. func (w *Canvas) LinkAdd(a *Actor) error { 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", 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 { // Second click, call the OnLinkActors handler with the two actors. if w.OnLinkActors != nil { @@ -66,3 +64,39 @@ func (w *Canvas) GetLinkedActors(a *Actor) []*Actor { } 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() ###") +}