* The "Story Mode" button on the MainScene opens the levelpacks window. * Levelpacks from all places are shown (built-in and user files), basic level picker works. * When playing a level out of a levelpack: the PlayScene gets the file data from the zipfile and plays it OK. * When a levelpack level is solved, the "Next Level" button appears on the success modal and hitting Return will advance to the next level in the pack. The final level doesn't show this button. * The user can edit levelpack levels! Clicking the "Edit" button on the Play Mode moves the loaded level over to the EditScene and the user could save it to disk or edit/playtest it perfectly OK! The link to the levelpack is lost upon opening in the editor, so the "Next Level" victory button doesn't appear.
191 lines
4.9 KiB
Go
191 lines
4.9 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"git.kirsle.net/apps/doodle/assets"
|
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
|
)
|
|
|
|
// Binary file format headers for Levels and Doodads.
|
|
//
|
|
// The header is 8 bytes long: "DOODLE" + file format version + file type number.
|
|
const (
|
|
BinMagic = "DOODLE"
|
|
BinVersion uint8 = 1 // version of the file format we support
|
|
BinLevelType uint8 = 1
|
|
BinDoodadType uint8 = 2
|
|
)
|
|
|
|
// Paths to system-level assets bundled with the application.
|
|
var (
|
|
SystemDoodadsPath = filepath.Join("assets", "doodads")
|
|
SystemLevelsPath = filepath.Join("assets", "levels")
|
|
SystemCampaignsPath = filepath.Join("assets", "campaigns")
|
|
SystemLevelPacksPath = filepath.Join("assets", "levelpacks")
|
|
)
|
|
|
|
// MakeHeader creates the binary file header.
|
|
func MakeHeader(filetype uint8) []byte {
|
|
header := make([]byte, len(BinMagic)+2)
|
|
for i := 0; i < len(BinMagic); i++ {
|
|
header[i] = BinMagic[i]
|
|
}
|
|
|
|
header[len(header)-2] = BinVersion
|
|
header[len(header)-1] = filetype
|
|
|
|
return header
|
|
}
|
|
|
|
// ReadHeader reads and verifies a header from a filehandle.
|
|
func ReadHeader(filetype uint8, fh io.Reader) error {
|
|
header := make([]byte, len(BinMagic)+2)
|
|
_, err := fh.Read(header)
|
|
if err != nil {
|
|
return fmt.Errorf("ReadHeader: %s", err)
|
|
}
|
|
|
|
if string(header[:len(BinMagic)]) != BinMagic {
|
|
return errors.New("not a doodle drawing (no magic number in header)")
|
|
}
|
|
|
|
// Verify the file format version and type.
|
|
var (
|
|
fileVersion = header[len(header)-2]
|
|
fileType = header[len(header)-1]
|
|
)
|
|
|
|
if fileVersion == 0 || fileVersion > BinVersion {
|
|
return errors.New("binary format was created using a newer version of the game")
|
|
} else if fileType != filetype {
|
|
return errors.New("drawing type is not the type we expected")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
FindFile looks for a file (level or doodad) in a few places.
|
|
|
|
The filename should already have a ".level" or ".doodad" file extension. If
|
|
neither is given, the exact filename will be searched in all places.
|
|
|
|
1. Check in the files built into the program binary.
|
|
2. Check for system files in the binary's assets/ folder.
|
|
3. Check the user folders.
|
|
|
|
Returns the file path and an error if not found anywhere.
|
|
*/
|
|
func FindFile(filename string) (string, error) {
|
|
var filetype string
|
|
|
|
// If the filename has path separators, return it as-is.
|
|
if strings.ContainsRune(filename, filepath.Separator) ||
|
|
strings.ContainsRune(filename, '/') {
|
|
return filename, nil
|
|
}
|
|
|
|
// Any hint on what type of file we're looking for?
|
|
if strings.HasSuffix(filename, enum.LevelExt) {
|
|
filetype = enum.LevelExt
|
|
} else if strings.HasSuffix(filename, enum.DoodadExt) {
|
|
filetype = enum.DoodadExt
|
|
} else if strings.HasSuffix(filename, enum.LevelPackExt) {
|
|
filetype = enum.LevelPackExt
|
|
}
|
|
|
|
// Search level directories.
|
|
if filetype == enum.LevelExt || filetype == "" {
|
|
// system levels
|
|
candidate := filepath.Join(SystemLevelsPath, filename)
|
|
|
|
// embedded system doodad?
|
|
if _, err := assets.Asset(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
|
|
// WASM: can't check the filesystem. Let the caller go ahead and try
|
|
// loading via ajax request.
|
|
if runtime.GOOS == "js" {
|
|
return filename, nil
|
|
}
|
|
|
|
// external system level?
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
|
|
// user levels
|
|
candidate = userdir.LevelPath(filename)
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
// Search doodad directories.
|
|
if filetype == enum.DoodadExt || filetype == "" {
|
|
// system doodads path
|
|
candidate := filepath.Join(SystemDoodadsPath, filename)
|
|
|
|
// embedded system doodad?
|
|
if _, err := assets.Asset(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
|
|
// WASM: can't check the filesystem. Let the caller go ahead and try
|
|
// loading via ajax request.
|
|
if runtime.GOOS == "js" {
|
|
return filename, nil
|
|
}
|
|
|
|
// external system doodad?
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
|
|
// user doodads
|
|
candidate = userdir.DoodadPath(filename)
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
// Search levelpack directories.
|
|
if filetype == enum.LevelPackExt || filetype == "" {
|
|
// system levelpacks path
|
|
candidate := filepath.Join(SystemLevelPacksPath, filename)
|
|
|
|
// embedded in binary?
|
|
if _, err := assets.Asset(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
|
|
// WASM: can't check the filesystem. Let the caller go ahead and try
|
|
// loading via ajax request.
|
|
if runtime.GOOS == "js" {
|
|
return filename, nil
|
|
}
|
|
|
|
// external system levelpack?
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
|
|
// user levelpacks
|
|
candidate = userdir.LevelPackPath(filename)
|
|
if _, err := os.Stat(candidate); !os.IsNotExist(err) {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
return filename, errors.New("file not found")
|
|
}
|