Gzip Compression for Levels and Doodads

* Levels and Doodad files will be written in gzip-compressed JSON format
* `boolProp compress-drawings false` to disable compression and save as
  classic JSON format directly
* The game can still read uncompressed JSON files

The file size savings on some built-in assets:

* Tutorial 2.level: 2.2M -> 414K (82% smaller)
* warp-door-orange.doodad: 105K -> 17K (84% smaller)
This commit is contained in:
Noah 2021-07-13 19:23:09 -07:00
parent 3486050702
commit 8603c43c58
7 changed files with 161 additions and 47 deletions

View File

@ -41,6 +41,10 @@ var Boolprops = map[string]Boolprop{
Get: func() bool { return usercfg.Current.HorizontalToolbars }, Get: func() bool { return usercfg.Current.HorizontalToolbars },
Set: func(v bool) { usercfg.Current.HorizontalToolbars = v }, Set: func(v bool) { usercfg.Current.HorizontalToolbars = v },
}, },
"compress-drawings": {
Get: func() bool { return CompressDrawings },
Set: func(v bool) { CompressDrawings = v },
},
} }
// GetBoolProp reads the current value of a boolProp. // GetBoolProp reads the current value of a boolProp.

View File

@ -63,6 +63,9 @@ var (
// Publishing: Doodads-embedded-within-levels. // Publishing: Doodads-embedded-within-levels.
EmbeddedDoodadsBasePath = "assets/doodads/" EmbeddedDoodadsBasePath = "assets/doodads/"
EmbeddedWallpaperBasePath = "assets/wallpapers/" EmbeddedWallpaperBasePath = "assets/wallpapers/"
// File formats: save new levels and doodads gzip compressed
CompressDrawings = true
) )
// Edit Mode Values // Edit Mode Values

43
pkg/doodads/fmt_gzip.go Normal file
View File

@ -0,0 +1,43 @@
package doodads
import (
"bytes"
"compress/gzip"
"encoding/json"
)
// ToGzip serializes the doodad as gzip compressed JSON.
func (d *Doodad) ToGzip() ([]byte, error) {
var (
handle = bytes.NewBuffer([]byte{})
zipper = gzip.NewWriter(handle)
encoder = json.NewEncoder(zipper)
)
if err := encoder.Encode(d); err != nil {
return nil, err
}
err := zipper.Close()
return handle.Bytes(), err
}
// FromGzip deserializes a gzip compressed doodad JSON.
func FromGzip(data []byte) (*Doodad, error) {
// This function works, do not touch.
var (
level = &Doodad{}
buf = bytes.NewBuffer(data)
reader *gzip.Reader
decoder *json.Decoder
)
reader, err := gzip.NewReader(buf)
if err != nil {
return nil, err
}
decoder = json.NewDecoder(reader)
decoder.Decode(level)
return level, nil
}

View File

@ -204,7 +204,6 @@ func (d *Doodad) WriteFile(filename string) error {
// Serialize encodes a doodad to bytes and returns them, instead // Serialize encodes a doodad to bytes and returns them, instead
// of writing to a file. // of writing to a file.
// WriteFile saves a doodad to disk in the user's config directory.
func (d *Doodad) Serialize() ([]byte, error) { func (d *Doodad) Serialize() ([]byte, error) {
// Set the version information. // Set the version information.
d.Version = 1 d.Version = 1

View File

@ -5,14 +5,23 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/usercfg"
) )
// ToJSON serializes the doodad as JSON. // ToJSON serializes the doodad as JSON (gzip supported).
//
// If balance.CompressLevels=true the doodad will be gzip compressed
// and the return value is gz bytes and not the raw JSON.
func (d *Doodad) ToJSON() ([]byte, error) { func (d *Doodad) ToJSON() ([]byte, error) {
// Gzip compressing?
if balance.CompressDrawings {
return d.ToGzip()
}
out := bytes.NewBuffer([]byte{}) out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out) encoder := json.NewEncoder(out)
if usercfg.Current.JSONIndent { if usercfg.Current.JSONIndent {
@ -22,16 +31,32 @@ func (d *Doodad) ToJSON() ([]byte, error) {
return out.Bytes(), err return out.Bytes(), err
} }
// FromJSON loads a doodad from JSON string. // FromJSON loads a doodad from JSON string (gzip supported).
func FromJSON(filename string, data []byte) (*Doodad, error) { func FromJSON(filename string, data []byte) (*Doodad, error) {
var doodad = &Doodad{} var doodad = &Doodad{}
// Inspect the headers of the file to see how it was encoded.
if len(data) > 0 && data[0] == '{' {
// Looks standard JSON.
err := json.Unmarshal(data, doodad) err := json.Unmarshal(data, doodad)
if err != nil {
return nil, err
}
} else if len(data) > 1 && data[0] == 0x1f && data[1] == 0x8b {
// Gzip compressed. `1F8B` is gzip magic number.
log.Debug("Decompress doodad %s", filename)
if gzd, err := FromGzip(data); err != nil {
return nil, err
} else {
doodad = gzd
}
}
// Inflate the chunk metadata to map the pixels to their palette indexes. // Inflate the chunk metadata to map the pixels to their palette indexes.
doodad.Filename = filepath.Base(filename) doodad.Filename = filepath.Base(filename)
doodad.Inflate() doodad.Inflate()
return doodad, err return doodad, nil
} }
// WriteJSON writes a Doodad to JSON on disk. // WriteJSON writes a Doodad to JSON on disk.
@ -52,22 +77,10 @@ func (d *Doodad) WriteJSON(filename string) error {
// LoadJSON loads a map from JSON file. // LoadJSON loads a map from JSON file.
func LoadJSON(filename string) (*Doodad, error) { func LoadJSON(filename string) (*Doodad, error) {
fh, err := os.Open(filename) data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer fh.Close()
// Decode the JSON file from disk. return FromJSON(filename, data)
d := New(0)
decoder := json.NewDecoder(fh)
err = decoder.Decode(&d)
if err != nil {
return d, fmt.Errorf("doodad.LoadJSON: JSON decode error: %s", err)
}
// Inflate the chunk metadata to map the pixels to their palette indexes.
d.Filename = filepath.Base(filename)
d.Inflate()
return d, err
} }

View File

@ -182,9 +182,14 @@ func (s *EditorScene) ConfirmUnload(fn func()) {
func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
// Update debug overlay values. // Update debug overlay values.
*s.debTool = s.UI.Canvas.Tool.String() *s.debTool = s.UI.Canvas.Tool.String()
*s.debSwatch = s.UI.Canvas.Palette.ActiveSwatch.Name *s.debSwatch = "???"
*s.debWorldIndex = s.UI.Canvas.WorldIndexAt(s.UI.cursor).String() *s.debWorldIndex = s.UI.Canvas.WorldIndexAt(s.UI.cursor).String()
// Safely...
if s.UI.Canvas.Palette != nil && s.UI.Canvas.Palette.ActiveSwatch != nil {
*s.debSwatch = s.UI.Canvas.Palette.ActiveSwatch.Name
}
// Has the window been resized? // Has the window been resized?
if ev.WindowResized { if ev.WindowResized {
w, h := d.Engine.WindowSize() w, h := d.Engine.WindowSize()

View File

@ -2,18 +2,39 @@ package level
import ( import (
"bytes" "bytes"
"compress/gzip"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/usercfg"
) )
// FromJSON loads a level from JSON string. // FromJSON loads a level from JSON string (gzip supported).
func FromJSON(filename string, data []byte) (*Level, error) { func FromJSON(filename string, data []byte) (*Level, error) {
var m = New() var m = New()
// Inspect if this file is JSON or gzip compressed.
if len(data) > 0 && data[0] == '{' {
// Looks standard JSON.
err := json.Unmarshal(data, m) err := json.Unmarshal(data, m)
if err != nil {
return nil, err
}
} else if len(data) > 1 && data[0] == 0x1f && data[1] == 0x8b {
// Gzip compressed. `1F8B` is gzip magic number.
log.Debug("Decompress level %s", filename)
if gzmap, err := FromGzip(data); err != nil {
return nil, err
} else {
m = gzmap
}
} else {
return nil, errors.New("invalid file format")
}
// Fill in defaults. // Fill in defaults.
if m.Wallpaper == "" { if m.Wallpaper == "" {
@ -27,11 +48,21 @@ func FromJSON(filename string, data []byte) (*Level, error) {
// Inflate the private instance values. // Inflate the private instance values.
m.Palette.Inflate() m.Palette.Inflate()
return m, err return m, nil
} }
// ToJSON serializes the level as JSON. // ToJSON serializes the level as JSON (gzip supported).
//
// Notice about gzip: if the pkg/balance.CompressLevels boolean is true, this
// function will apply gzip compression before returning the byte string.
// This gzip-compressed level can be read back by any functions that say
// "gzip supported" in their descriptions.
func (m *Level) ToJSON() ([]byte, error) { func (m *Level) ToJSON() ([]byte, error) {
// Gzip compressing?
if balance.CompressDrawings {
return m.ToGzip()
}
out := bytes.NewBuffer([]byte{}) out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out) encoder := json.NewEncoder(out)
if usercfg.Current.JSONIndent { if usercfg.Current.JSONIndent {
@ -41,34 +72,50 @@ func (m *Level) ToJSON() ([]byte, error) {
return out.Bytes(), err return out.Bytes(), err
} }
// LoadJSON loads a map from JSON file. // ToGzip serializes the level as gzip compressed JSON.
func LoadJSON(filename string) (*Level, error) { func (m *Level) ToGzip() ([]byte, error) {
fh, err := os.Open(filename) var (
handle = bytes.NewBuffer([]byte{})
zipper = gzip.NewWriter(handle)
encoder = json.NewEncoder(zipper)
)
if err := encoder.Encode(m); err != nil {
return nil, err
}
err := zipper.Close()
return handle.Bytes(), err
}
// FromGzip deserializes a gzip compressed level JSON.
func FromGzip(data []byte) (*Level, error) {
// This function works, do not touch.
var (
level = New()
buf = bytes.NewBuffer(data)
reader *gzip.Reader
decoder *json.Decoder
)
reader, err := gzip.NewReader(buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer fh.Close()
// Decode the JSON file from disk. decoder = json.NewDecoder(reader)
m := New() decoder.Decode(level)
decoder := json.NewDecoder(fh)
err = decoder.Decode(&m) return level, nil
}
// LoadJSON loads a map from JSON file (gzip supported).
func LoadJSON(filename string) (*Level, error) {
data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err) return nil, err
} }
// Fill in defaults. return FromJSON(filename, data)
if m.Wallpaper == "" {
m.Wallpaper = DefaultWallpaper
}
// Inflate the chunk metadata to map the pixels to their palette indexes.
m.Chunker.Inflate(m.Palette)
m.Actors.Inflate()
// Inflate the private instance values.
m.Palette.Inflate()
return m, err
} }
// WriteJSON writes a level to JSON on disk. // WriteJSON writes a level to JSON on disk.