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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() ###")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user