2021-12-24 03:15:32 +00:00
|
|
|
// Package levelpack handles ZIP archives for level packs.
|
|
|
|
package levelpack
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
2021-12-31 02:39:11 +00:00
|
|
|
"bytes"
|
2021-12-24 03:15:32 +00:00
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2021-12-31 02:39:11 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2021-12-24 03:15:32 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2021-12-24 05:11:45 +00:00
|
|
|
"runtime"
|
2021-12-27 04:48:29 +00:00
|
|
|
"sort"
|
2021-12-24 03:15:32 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2021-12-24 05:11:45 +00:00
|
|
|
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/assets"
|
2023-02-19 01:37:54 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/filesystem"
|
2023-02-19 01:37:54 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
|
2021-12-24 03:15:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// LevelPack describes the contents of a levelpack file.
|
|
|
|
type LevelPack struct {
|
2023-02-19 01:37:54 +00:00
|
|
|
Title string `json:"title"`
|
2021-12-24 03:15:32 +00:00
|
|
|
Description string `json:"description"`
|
|
|
|
Author string `json:"author"`
|
|
|
|
Created time.Time `json:"created"`
|
|
|
|
|
|
|
|
// Cached metadata about the (displayed) levels.
|
|
|
|
Levels []Level `json:"levels"`
|
|
|
|
|
|
|
|
// Number of levels unlocked by default.
|
|
|
|
// 0 = all levels unlocked
|
|
|
|
FreeLevels int `json:"freeLevels"`
|
|
|
|
|
|
|
|
// The loaded zip file for reading an existing levelpack.
|
|
|
|
Zipfile *zip.Reader `json:"-"`
|
2022-01-03 00:28:43 +00:00
|
|
|
|
|
|
|
// A reference to the original filename, not stored in json.
|
|
|
|
Filename string `json:"-"`
|
2023-02-19 01:37:54 +00:00
|
|
|
|
|
|
|
// Signature to allow free versions of the game to load embedded
|
|
|
|
// custom doodads inside this levelpack for its levels.
|
|
|
|
Signature []byte `json:"signature,omitempty"`
|
2021-12-24 03:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Level holds metadata about the levels in the levelpack.
|
|
|
|
type Level struct {
|
|
|
|
Title string `json:"title"`
|
|
|
|
Author string `json:"author"`
|
|
|
|
Filename string `json:"filename"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadFile reads a .levelpack zip file.
|
2023-02-19 01:37:54 +00:00
|
|
|
func LoadFile(filename string) (*LevelPack, error) {
|
2021-12-31 02:39:11 +00:00
|
|
|
var (
|
|
|
|
fh io.ReaderAt
|
|
|
|
filesize int64
|
|
|
|
)
|
|
|
|
|
|
|
|
// Look in embedded bindata.
|
|
|
|
if data, err := assets.Asset(filename); err == nil {
|
|
|
|
filesize = int64(len(data))
|
|
|
|
fh = bytes.NewReader(data)
|
2021-12-24 03:15:32 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 02:39:11 +00:00
|
|
|
// Try the filesystem.
|
|
|
|
if fh == nil {
|
|
|
|
stat, err := os.Stat(filename)
|
|
|
|
if err != nil {
|
2023-02-19 01:37:54 +00:00
|
|
|
return nil, err
|
2021-12-31 02:39:11 +00:00
|
|
|
}
|
|
|
|
filesize = stat.Size()
|
|
|
|
|
|
|
|
fh, err = os.Open(filename)
|
|
|
|
if err != nil {
|
2023-02-19 01:37:54 +00:00
|
|
|
return nil, err
|
2021-12-31 02:39:11 +00:00
|
|
|
}
|
2021-12-24 03:15:32 +00:00
|
|
|
}
|
|
|
|
|
2021-12-31 02:39:11 +00:00
|
|
|
// No luck?
|
|
|
|
if fh == nil {
|
2023-02-19 01:37:54 +00:00
|
|
|
return nil, errors.New("no file found")
|
2021-12-31 02:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
reader, err := zip.NewReader(fh, filesize)
|
2021-12-24 03:15:32 +00:00
|
|
|
if err != nil {
|
2023-02-19 01:37:54 +00:00
|
|
|
return nil, err
|
2021-12-24 03:15:32 +00:00
|
|
|
}
|
|
|
|
|
2023-02-19 01:37:54 +00:00
|
|
|
lp := &LevelPack{
|
2022-01-03 00:28:43 +00:00
|
|
|
Filename: filename,
|
|
|
|
Zipfile: reader,
|
2021-12-24 03:15:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Read the index.json.
|
2023-02-19 01:37:54 +00:00
|
|
|
lp.GetJSON(lp, "index.json")
|
2021-12-24 03:15:32 +00:00
|
|
|
|
|
|
|
return lp, nil
|
|
|
|
}
|
|
|
|
|
2021-12-27 04:48:29 +00:00
|
|
|
// LoadAllAvailable loads every levelpack visible to the game. Returns
|
|
|
|
// the sorted list of filenames as from ListFiles, plus a deeply loaded
|
|
|
|
// hash map associating the filenames with their data.
|
2023-02-19 01:37:54 +00:00
|
|
|
func LoadAllAvailable() ([]string, map[string]*LevelPack, error) {
|
2021-12-27 04:48:29 +00:00
|
|
|
filenames, err := ListFiles()
|
|
|
|
if err != nil {
|
|
|
|
return filenames, nil, err
|
|
|
|
}
|
|
|
|
|
2023-02-19 01:37:54 +00:00
|
|
|
var dictionary = map[string]*LevelPack{}
|
2021-12-27 04:48:29 +00:00
|
|
|
for _, filename := range filenames {
|
|
|
|
// Resolve the filename to a definite path on disk.
|
|
|
|
path, err := filesystem.FindFile(filename)
|
|
|
|
if err != nil {
|
2023-02-19 01:37:54 +00:00
|
|
|
log.Error("LoadAllAvailable: FindFile(%s): %s", path, err)
|
2021-12-27 04:48:29 +00:00
|
|
|
return filenames, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
lp, err := LoadFile(path)
|
|
|
|
if err != nil {
|
2021-12-31 02:39:11 +00:00
|
|
|
return filenames, nil, fmt.Errorf("LoadAllAvailable: LoadFile(%s): %s", path, err)
|
2021-12-27 04:48:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
dictionary[filename] = lp
|
|
|
|
}
|
|
|
|
|
|
|
|
return filenames, dictionary, nil
|
|
|
|
}
|
|
|
|
|
2021-12-24 05:11:45 +00:00
|
|
|
// ListFiles lists all the discoverable levelpack files, starting from
|
|
|
|
// the game's built-ins all the way to user levelpacks.
|
|
|
|
func ListFiles() ([]string, error) {
|
|
|
|
var names []string
|
|
|
|
|
|
|
|
// List levelpacks embedded into the binary.
|
|
|
|
if files, err := assets.AssetDir("assets/levelpacks"); err == nil {
|
|
|
|
names = append(names, files...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WASM stops here, no filesystem access.
|
|
|
|
if runtime.GOOS == "js" {
|
|
|
|
return names, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read system-level levelpacks.
|
|
|
|
files, _ := ioutil.ReadDir(filesystem.SystemLevelPacksPath)
|
|
|
|
for _, file := range files {
|
|
|
|
name := file.Name()
|
|
|
|
if strings.HasSuffix(name, enum.LevelPackExt) {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append user levelpacks.
|
|
|
|
files, _ = ioutil.ReadDir(userdir.LevelPackDirectory)
|
|
|
|
for _, file := range files {
|
|
|
|
name := file.Name()
|
|
|
|
if strings.HasSuffix(name, enum.LevelPackExt) {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-27 04:48:29 +00:00
|
|
|
// Deduplicate strings. Can happen e.g. because assets/ is baked
|
|
|
|
// in to bindata but files also exist there locally.
|
|
|
|
var (
|
|
|
|
dedupe []string
|
|
|
|
seen = map[string]interface{}{}
|
|
|
|
)
|
|
|
|
for _, value := range names {
|
|
|
|
if _, ok := seen[value]; !ok {
|
|
|
|
seen[value] = nil
|
|
|
|
dedupe = append(dedupe, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Strings(dedupe)
|
|
|
|
return dedupe, nil
|
2021-12-24 05:11:45 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 03:15:32 +00:00
|
|
|
// WriteFile saves the metadata to a .json file on disk.
|
|
|
|
func (l LevelPack) WriteFile(filename string) error {
|
|
|
|
out, err := json.Marshal(l)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return ioutil.WriteFile(filename, out, 0655)
|
|
|
|
}
|
|
|
|
|
2023-02-19 01:37:54 +00:00
|
|
|
// WriteZipfile saves a levelpack back into a zip file.
|
|
|
|
func (l LevelPack) WriteZipfile(filename string) error {
|
|
|
|
fh, err := os.Create(filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer fh.Close()
|
|
|
|
|
|
|
|
// Copy all of the levels and other files from the old zip to new zip.
|
|
|
|
zf := zip.NewWriter(fh)
|
|
|
|
defer zf.Close()
|
|
|
|
|
|
|
|
// Copy attached doodads and levels.
|
|
|
|
for _, file := range l.Zipfile.File {
|
|
|
|
if !strings.HasPrefix(file.Name, "doodads/") &&
|
|
|
|
!strings.HasPrefix(file.Name, "levels/") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := zf.Copy(file); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the index.json metadata.
|
|
|
|
meta, err := json.Marshal(l)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
writer, err := zf.Create("index.json")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = writer.Write(meta)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetFile returns file data from inside the loaded zipfile of a levelpack.
|
|
|
|
//
|
|
|
|
// This also implements the Embeddable interface.
|
|
|
|
func (l LevelPack) GetFile(filename string) ([]byte, error) {
|
2021-12-24 03:15:32 +00:00
|
|
|
if l.Zipfile == nil {
|
|
|
|
return []byte{}, errors.New("zipfile not loaded")
|
|
|
|
}
|
|
|
|
|
2023-02-19 01:37:54 +00:00
|
|
|
// NOTE: levelpacks don't have an "assets/" prefix but the game
|
|
|
|
// might come looking for "assets/doodads"
|
|
|
|
if strings.HasPrefix(filename, balance.EmbeddedDoodadsBasePath) {
|
|
|
|
filename = strings.Replace(filename, balance.EmbeddedDoodadsBasePath, "doodads/", 1)
|
|
|
|
}
|
|
|
|
|
2021-12-24 03:15:32 +00:00
|
|
|
file, err := l.Zipfile.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return []byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ioutil.ReadAll(file)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetJSON loads a JSON file from the zipfile and marshals it into your struct.
|
|
|
|
func (l LevelPack) GetJSON(v interface{}, filename string) error {
|
2023-02-19 01:37:54 +00:00
|
|
|
data, err := l.GetFile(filename)
|
2021-12-24 03:15:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return json.Unmarshal(data, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListFiles returns the files in the zipfile that match the prefix given.
|
|
|
|
func (l LevelPack) ListFiles(prefix string) []string {
|
|
|
|
var result []string
|
|
|
|
|
|
|
|
if l.Zipfile != nil {
|
|
|
|
for _, file := range l.Zipfile.File {
|
|
|
|
if strings.HasPrefix(file.Name, prefix) {
|
|
|
|
result = append(result, file.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|