doodle/pkg/levelpack/levelpack.go

198 lines
4.5 KiB
Go

// Package levelpack handles ZIP archives for level packs.
package levelpack
import (
"archive/zip"
"encoding/json"
"errors"
"io/ioutil"
"os"
"runtime"
"sort"
"strings"
"time"
"git.kirsle.net/apps/doodle/assets"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/filesystem"
"git.kirsle.net/apps/doodle/pkg/userdir"
)
// LevelPack describes the contents of a levelpack file.
type LevelPack struct {
Title string `json:"title`
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:"-"`
}
// 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.
func LoadFile(filename string) (LevelPack, error) {
stat, err := os.Stat(filename)
if err != nil {
return LevelPack{}, err
}
fh, err := os.Open(filename)
if err != nil {
return LevelPack{}, err
}
reader, err := zip.NewReader(fh, stat.Size())
if err != nil {
return LevelPack{}, err
}
lp := LevelPack{
Zipfile: reader,
}
// Read the index.json.
lp.GetJSON(&lp, "index.json")
return lp, nil
}
// 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.
func LoadAllAvailable() ([]string, map[string]LevelPack, error) {
filenames, err := ListFiles()
if err != nil {
return filenames, nil, err
}
var dictionary = map[string]LevelPack{}
for _, filename := range filenames {
// Resolve the filename to a definite path on disk.
path, err := filesystem.FindFile(filename)
if err != nil {
return filenames, nil, err
}
lp, err := LoadFile(path)
if err != nil {
return filenames, nil, err
}
dictionary[filename] = lp
}
return filenames, dictionary, nil
}
// 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)
}
}
// 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
}
// 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)
}
// GetData returns file data from inside the loaded zipfile of a levelpack.
func (l LevelPack) GetData(filename string) ([]byte, error) {
if l.Zipfile == nil {
return []byte{}, errors.New("zipfile not loaded")
}
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 {
data, err := l.GetData(filename)
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
}