2018-06-17 17:29:57 +00:00
|
|
|
package level
|
|
|
|
|
|
|
|
import (
|
2022-04-30 03:34:59 +00:00
|
|
|
"archive/zip"
|
2018-06-17 17:29:57 +00:00
|
|
|
"bytes"
|
2021-07-14 02:23:09 +00:00
|
|
|
"compress/gzip"
|
2018-06-17 17:29:57 +00:00
|
|
|
"encoding/json"
|
2018-08-11 00:19:47 +00:00
|
|
|
"fmt"
|
2018-10-16 16:20:25 +00:00
|
|
|
"io/ioutil"
|
2022-04-30 03:34:59 +00:00
|
|
|
"net/http"
|
2019-05-05 21:03:20 +00:00
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
2021-06-20 05:14:41 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/usercfg"
|
2018-06-17 17:29:57 +00:00
|
|
|
)
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
// FromJSON loads a level from JSON string (gzip supported).
|
2019-06-27 22:07:34 +00:00
|
|
|
func FromJSON(filename string, data []byte) (*Level, error) {
|
2019-07-03 23:22:30 +00:00
|
|
|
var m = New()
|
2021-07-14 02:23:09 +00:00
|
|
|
|
|
|
|
// Inspect if this file is JSON or gzip compressed.
|
|
|
|
if len(data) > 0 && data[0] == '{' {
|
|
|
|
// Looks standard JSON.
|
|
|
|
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
|
|
|
|
}
|
2022-04-30 03:34:59 +00:00
|
|
|
} else if http.DetectContentType(data) == "application/zip" {
|
|
|
|
if zipmap, err := FromZipfile(data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else {
|
|
|
|
m = zipmap
|
|
|
|
}
|
2021-07-14 02:23:09 +00:00
|
|
|
} else {
|
2022-04-30 03:34:59 +00:00
|
|
|
return nil, fmt.Errorf("invalid file format")
|
2021-07-14 02:23:09 +00:00
|
|
|
}
|
2019-06-27 22:07:34 +00:00
|
|
|
|
|
|
|
// Fill in defaults.
|
|
|
|
if m.Wallpaper == "" {
|
|
|
|
m.Wallpaper = DefaultWallpaper
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
2021-09-12 04:18:22 +00:00
|
|
|
m.Inflate()
|
2019-06-27 22:07:34 +00:00
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
return m, nil
|
2019-06-27 22:07:34 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
// 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.
|
2018-06-17 17:29:57 +00:00
|
|
|
func (m *Level) ToJSON() ([]byte, error) {
|
2021-07-14 02:23:09 +00:00
|
|
|
// Gzip compressing?
|
2022-04-30 03:34:59 +00:00
|
|
|
if balance.DrawingFormat == balance.FormatGZip {
|
2021-07-14 02:23:09 +00:00
|
|
|
return m.ToGzip()
|
|
|
|
}
|
|
|
|
|
2022-04-30 03:34:59 +00:00
|
|
|
// Zipfile?
|
|
|
|
if balance.DrawingFormat == balance.FormatZipfile {
|
|
|
|
return m.ToZipfile()
|
|
|
|
}
|
|
|
|
|
|
|
|
return m.AsJSON()
|
|
|
|
}
|
|
|
|
|
|
|
|
// AsJSON returns it just as JSON without any fancy gzip/zip magic.
|
|
|
|
func (m *Level) AsJSON() ([]byte, error) {
|
2018-06-17 17:29:57 +00:00
|
|
|
out := bytes.NewBuffer([]byte{})
|
|
|
|
encoder := json.NewEncoder(out)
|
2021-06-20 05:14:41 +00:00
|
|
|
if usercfg.Current.JSONIndent {
|
2019-05-05 21:03:20 +00:00
|
|
|
encoder.SetIndent("", "\t")
|
|
|
|
}
|
2018-06-17 17:29:57 +00:00
|
|
|
err := encoder.Encode(m)
|
|
|
|
return out.Bytes(), err
|
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
// ToGzip serializes the level as gzip compressed JSON.
|
|
|
|
func (m *Level) ToGzip() ([]byte, error) {
|
|
|
|
var (
|
|
|
|
handle = bytes.NewBuffer([]byte{})
|
|
|
|
zipper = gzip.NewWriter(handle)
|
|
|
|
encoder = json.NewEncoder(zipper)
|
|
|
|
)
|
|
|
|
if err := encoder.Encode(m); err != nil {
|
2018-08-11 00:19:47 +00:00
|
|
|
return nil, err
|
2018-06-17 17:29:57 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
err := zipper.Close()
|
|
|
|
return handle.Bytes(), err
|
|
|
|
}
|
|
|
|
|
2022-04-30 03:34:59 +00:00
|
|
|
// ToZipfile serializes the level as a ZIP archive and also migrates
|
|
|
|
// data loaded from an older save into the new zip format.
|
|
|
|
func (m *Level) ToZipfile() ([]byte, error) {
|
|
|
|
// If we do not have a Zipfile yet, migrate legacy data into one.
|
|
|
|
// if m.Zipfile == nil {
|
|
|
|
fh := bytes.NewBuffer([]byte{})
|
|
|
|
zipper := zip.NewWriter(fh)
|
|
|
|
defer zipper.Close()
|
|
|
|
|
|
|
|
// Migrate any legacy Chunker data into external files in the zip.
|
|
|
|
if err := m.Chunker.MigrateZipfile(zipper); err != nil {
|
|
|
|
return nil, fmt.Errorf("MigrateZipfile: %s", err)
|
|
|
|
}
|
|
|
|
|
2022-04-30 19:47:35 +00:00
|
|
|
// Migrate attached files to ZIP.
|
|
|
|
if err := m.Files.MigrateZipfile(zipper); err != nil {
|
|
|
|
return nil, fmt.Errorf("FileSystem.MigrateZipfile: %s", err)
|
|
|
|
}
|
|
|
|
|
2022-04-30 03:34:59 +00:00
|
|
|
// Write the header json.
|
|
|
|
{
|
|
|
|
header, err := m.AsJSON()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
writer, err := zipper.Create("level.json")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("zipping index.js: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if n, err := writer.Write(header); err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else {
|
|
|
|
log.Debug("Written level.json to zipfile: %s bytes", n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
zipper.Close()
|
|
|
|
|
|
|
|
// Refresh our Zipfile reader from the zipper we just wrote.
|
|
|
|
bin := fh.Bytes()
|
|
|
|
if err := m.ReloadZipfile(bin); err != nil {
|
|
|
|
log.Error("ReloadZipfile: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fh.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
// 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)
|
2018-08-11 00:19:47 +00:00
|
|
|
if err != nil {
|
2021-07-14 02:23:09 +00:00
|
|
|
return nil, err
|
2018-08-11 00:19:47 +00:00
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
decoder = json.NewDecoder(reader)
|
|
|
|
decoder.Decode(level)
|
2018-10-28 05:22:13 +00:00
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
return level, nil
|
|
|
|
}
|
2018-09-23 22:20:45 +00:00
|
|
|
|
2022-04-30 03:34:59 +00:00
|
|
|
// FromZipfile reads a level in zipfile format.
|
|
|
|
func FromZipfile(data []byte) (*Level, error) {
|
|
|
|
var (
|
|
|
|
level = New()
|
|
|
|
err = level.populateFromZipfile(data)
|
|
|
|
)
|
|
|
|
return level, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReloadZipfile re-reads the level's zipfile after a write.
|
|
|
|
func (m *Level) ReloadZipfile(data []byte) error {
|
|
|
|
return m.populateFromZipfile(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Common function between FromZipfile and ReloadZipFile.
|
|
|
|
func (m *Level) populateFromZipfile(data []byte) error {
|
|
|
|
var (
|
|
|
|
buf = bytes.NewReader(data)
|
|
|
|
zf *zip.Reader
|
|
|
|
decoder *json.Decoder
|
|
|
|
)
|
|
|
|
|
|
|
|
zf, err := zip.NewReader(buf, buf.Size())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read the level.json.
|
|
|
|
file, err := zf.Open("level.json")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
decoder = json.NewDecoder(file)
|
|
|
|
err = decoder.Decode(m)
|
|
|
|
|
|
|
|
// Keep the zipfile reader handy.
|
|
|
|
m.Zipfile = zf
|
|
|
|
m.Chunker.Zipfile = zf
|
2022-04-30 19:47:35 +00:00
|
|
|
m.Files.Zipfile = zf
|
|
|
|
|
|
|
|
// Re-inflate the level: ensures Actor instances get their IDs
|
|
|
|
// and everything is reloaded after saving the level.
|
|
|
|
m.Inflate()
|
2022-04-30 03:34:59 +00:00
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-07-14 02:23:09 +00:00
|
|
|
// LoadJSON loads a map from JSON file (gzip supported).
|
|
|
|
func LoadJSON(filename string) (*Level, error) {
|
|
|
|
data, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return FromJSON(filename, data)
|
2018-06-17 17:29:57 +00:00
|
|
|
}
|
2019-05-05 21:03:20 +00:00
|
|
|
|
|
|
|
// WriteJSON writes a level to JSON on disk.
|
|
|
|
func (m *Level) WriteJSON(filename string) error {
|
|
|
|
json, err := m.ToJSON()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Level.WriteJSON: JSON encode error: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ioutil.WriteFile(filename, json, 0755)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Level.WriteJSON: WriteFile error: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-04-30 03:34:59 +00:00
|
|
|
|
|
|
|
// Loop may be called each loop to allow the level to maintain its
|
|
|
|
// memory usage, e.g., for chunks not requested recently from a zipfile
|
|
|
|
// level to free those from RAM.
|
|
|
|
func (m *Level) Loop() error {
|
|
|
|
m.Chunker.FreeCaches()
|
|
|
|
return nil
|
|
|
|
}
|