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:
parent
fc736abd5f
commit
75fa0c7e56
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
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 {
|
||||
case enum.LevelDrawing:
|
||||
err = s.Level.WriteFile(filename)
|
||||
s.d.Flash("Automatically saved level to %s", filename)
|
||||
return s.Level.WriteFile(filename)
|
||||
case enum.DoodadDrawing:
|
||||
filename = "_autosave.doodad"
|
||||
|
||||
err = s.Doodad.WriteFile(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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -432,10 +432,18 @@ func (c *Chunker) FreeChunk(p render.Point) bool {
|
|||
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)
|
||||
return true
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() ###")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user