doodle/pkg/level/filesystem.go

317 lines
7.5 KiB
Go
Raw Normal View History

Draw Actors Embedded in Levels in Edit Mode Add the JSON format for embedding Actors (Doodad instances) inside of a Level. I made a test map that manually inserted a couple of actors. Actors are given to the Canvas responsible for the Level via the function `InstallActors()`. So it means you'll call LoadLevel and then InstallActors to hook everything up. The Canvas creates sub-Canvas widgets from each Actor. After drawing the main level geometry from the Canvas.Chunker, it calls the drawActors() function which does the same but for Actors. Levels keep a global map of all Actors that exist. For any Actors that are visible within the Viewport, their sub-Canvas widgets are presented appropriately on top of the parent Canvas. In case their sub-Canvas overlaps the parent's boundaries, their sub-Canvas is resized and moved appropriately. - Allow the MainWindow to be resized at run time, and the UI recalculates its sizing and position. - Made the in-game Shell properties editable via environment variables. The kirsle.env file sets a blue and pink color scheme. - Begin the ground work for Levels and Doodads to embed files inside their data via the level.FileSystem type. - UI: Labels can now contain line break characters. It will appropriately render multiple lines of render.Text and take into account the proper BoxSize to contain them all. - Add environment variable DOODLE_DEBUG_ALL=true that will turn on ALL debug overlay and visualization options. - Add debug overlay to "tag" each Canvas widget with some of its details, like its Name and World Position. Can be enabled with the environment variable DEBUG_CANVAS_LABEL=true - Improved the FPS debug overlay to show in labeled columns and multiple colors, with easy ability to add new data points to it.
2018-10-19 20:31:58 +00:00
package level
import (
"archive/zip"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"sort"
"strings"
2022-09-24 22:17:25 +00:00
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
)
/*
FileSystem embeds a map of files inside a parent drawing.
Old-style drawings this was a map of filenames to their data in the JSON.
New-style drawings this just holds the filenames and the data is read
from the zipfile on demand.
*/
type FileSystem struct {
filemap map[string]File `json:"-"` // Legacy JSON format
Zipfile *zip.Reader `json:"-"` // New Zipfile format accessor
}
Draw Actors Embedded in Levels in Edit Mode Add the JSON format for embedding Actors (Doodad instances) inside of a Level. I made a test map that manually inserted a couple of actors. Actors are given to the Canvas responsible for the Level via the function `InstallActors()`. So it means you'll call LoadLevel and then InstallActors to hook everything up. The Canvas creates sub-Canvas widgets from each Actor. After drawing the main level geometry from the Canvas.Chunker, it calls the drawActors() function which does the same but for Actors. Levels keep a global map of all Actors that exist. For any Actors that are visible within the Viewport, their sub-Canvas widgets are presented appropriately on top of the parent Canvas. In case their sub-Canvas overlaps the parent's boundaries, their sub-Canvas is resized and moved appropriately. - Allow the MainWindow to be resized at run time, and the UI recalculates its sizing and position. - Made the in-game Shell properties editable via environment variables. The kirsle.env file sets a blue and pink color scheme. - Begin the ground work for Levels and Doodads to embed files inside their data via the level.FileSystem type. - UI: Labels can now contain line break characters. It will appropriately render multiple lines of render.Text and take into account the proper BoxSize to contain them all. - Add environment variable DOODLE_DEBUG_ALL=true that will turn on ALL debug overlay and visualization options. - Add debug overlay to "tag" each Canvas widget with some of its details, like its Name and World Position. Can be enabled with the environment variable DEBUG_CANVAS_LABEL=true - Improved the FPS debug overlay to show in labeled columns and multiple colors, with easy ability to add new data points to it.
2018-10-19 20:31:58 +00:00
// File holds details about a file in the FileSystem.
type File struct {
Data []byte `json:"data,omitempty"`
Draw Actors Embedded in Levels in Edit Mode Add the JSON format for embedding Actors (Doodad instances) inside of a Level. I made a test map that manually inserted a couple of actors. Actors are given to the Canvas responsible for the Level via the function `InstallActors()`. So it means you'll call LoadLevel and then InstallActors to hook everything up. The Canvas creates sub-Canvas widgets from each Actor. After drawing the main level geometry from the Canvas.Chunker, it calls the drawActors() function which does the same but for Actors. Levels keep a global map of all Actors that exist. For any Actors that are visible within the Viewport, their sub-Canvas widgets are presented appropriately on top of the parent Canvas. In case their sub-Canvas overlaps the parent's boundaries, their sub-Canvas is resized and moved appropriately. - Allow the MainWindow to be resized at run time, and the UI recalculates its sizing and position. - Made the in-game Shell properties editable via environment variables. The kirsle.env file sets a blue and pink color scheme. - Begin the ground work for Levels and Doodads to embed files inside their data via the level.FileSystem type. - UI: Labels can now contain line break characters. It will appropriately render multiple lines of render.Text and take into account the proper BoxSize to contain them all. - Add environment variable DOODLE_DEBUG_ALL=true that will turn on ALL debug overlay and visualization options. - Add debug overlay to "tag" each Canvas widget with some of its details, like its Name and World Position. Can be enabled with the environment variable DEBUG_CANVAS_LABEL=true - Improved the FPS debug overlay to show in labeled columns and multiple colors, with easy ability to add new data points to it.
2018-10-19 20:31:58 +00:00
}
// NewFileSystem initializes the FileSystem struct.
func NewFileSystem() *FileSystem {
return &FileSystem{
filemap: map[string]File{},
}
}
// Exists checks if a file exists.
func (fs *FileSystem) Exists(filename string) bool {
if fs.filemap == nil {
fs.filemap = map[string]File{}
}
// Legacy file map.
if _, ok := fs.filemap[filename]; ok {
return ok
}
// Check in the zipfile.
if fs.Zipfile != nil {
file, err := fs.Zipfile.Open(filename)
if err == nil {
defer file.Close()
return true
}
}
return false
}
// Get a file from the FileSystem.
func (fs *FileSystem) Get(filename string) ([]byte, error) {
if fs.filemap == nil {
fs.filemap = map[string]File{}
}
// Legacy file map.
if file, ok := fs.filemap[filename]; ok {
if len(file.Data) > 0 {
return file.Data, nil
}
}
// Check in the zipfile.
if fs.Zipfile != nil {
file, err := fs.Zipfile.Open(filename)
if err != nil {
return []byte{}, fmt.Errorf("%s: not in zipfile: %s", filename, err)
}
bin, err := ioutil.ReadAll(file)
if err != nil {
return bin, fmt.Errorf("%s: couldn't read zipfile member: %s", filename, err)
}
return bin, nil
}
return []byte{}, fmt.Errorf("no such file")
}
// Set a file into the FileSystem. Note: it will go into the legacy map
// structure until the next save to disk, at which point queued files
// are committed to ZIP.
func (fs *FileSystem) Set(filename string, data []byte) {
if fs.filemap == nil {
fs.filemap = map[string]File{}
}
fs.filemap[filename] = File{
Data: data,
}
}
// Delete a file from the FileSystem. This will store zero bytes in the
// legacy file map structure to mark it for deletion. On the next save,
// filemap files with zero bytes skip the ZIP archive.
func (fs *FileSystem) Delete(filename string) {
if fs.filemap == nil {
fs.filemap = map[string]File{}
}
fs.filemap[filename] = File{
Data: []byte{},
}
}
// Checksum returns a SHA-256 checksum of a file's data.
func (fs *FileSystem) Checksum(filename string) (string, error) {
data, err := fs.Get(filename)
if err != nil {
return "", err
}
h := sha256.New()
h.Write(data)
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// List files in the FileSystem, including the ZIP file.
//
// In the ZIP file, attachments are under the "assets/" prefix so this
// function won't mistakenly return chunks or level.json/doodad.json.
func (fs *FileSystem) List() []string {
var (
result = []string{}
seen = map[string]interface{}{}
)
// List the legacy or recently modified files first.
if fs.filemap != nil {
for filename := range fs.filemap {
result = append(result, filename)
seen[filename] = nil
}
}
// List the zipfile members.
if fs.Zipfile != nil {
for _, file := range fs.Zipfile.File {
if !strings.HasPrefix(file.Name, "assets/") {
continue
}
if _, ok := seen[file.Name]; !ok {
result = append(result, file.Name)
seen[file.Name] = nil
}
}
}
sort.Strings(result)
return result
}
// ListPrefix returns a list of files starting with the prefix.
func (fs *FileSystem) ListPrefix(prefix string) []string {
var result = []string{}
for _, name := range fs.List() {
if strings.HasPrefix(name, prefix) {
result = append(result, name)
}
}
return result
}
// UnmarshalJSON reads in a FileSystem from its legacy JSON representation.
func (fs *FileSystem) UnmarshalJSON(text []byte) error {
// Legacy format was a simple map[string]File.
var legacy map[string]File
err := json.Unmarshal(text, &legacy)
if err != nil {
return err
}
fs.filemap = legacy
return nil
}
// MigrateZipfile is called on save to write attached files to the ZIP
// file format.
func (fs *FileSystem) MigrateZipfile(zf *zip.Writer) error {
// Identify the files that we have marked for deletion.
var (
filesDeleted = map[string]interface{}{}
filesZipped = map[string]interface{}{}
)
if fs.filemap != nil {
for filename, data := range fs.filemap {
if len(data.Data) == 0 {
log.Info("FileSystem.MigrateZipfile: %s has become empty, remove from zip", filename)
filesDeleted[filename] = nil
}
}
}
// Copy all COLD STORED files from the old Zipfile into the new Zipfile
// except for the ones marked for deletion OR the ones currently in the
// warm cache which will be written next.
if fs.Zipfile != nil {
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
log.Debug("FileSystem.MigrateZipfile: Copying files from old zip to new zip")
for _, file := range fs.Zipfile.File {
if !strings.HasPrefix(file.Name, "assets/") {
continue
}
if _, ok := filesDeleted[file.Name]; ok {
2023-12-02 20:33:14 +00:00
log.Debug("Skip copying attachment %s: was marked for deletion", file.Name)
continue
}
// Skip files currently in memory.
if fs.filemap != nil {
if _, ok := fs.filemap[file.Name]; ok {
2023-12-02 20:33:14 +00:00
log.Debug("Skip copying attachment %s: one is loaded in memory", file.Name)
continue
}
}
log.Debug("Copy zipfile attachment %s", file.Name)
filesZipped[file.Name] = nil
if err := zf.Copy(file); err != nil {
return err
}
}
}
// Export currently warmed up files to ZIP, these will be ones that
// were updated recently OR legacy files from an old level read.
if fs.filemap != nil {
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
log.Debug("FileSystem.MigrateZipfile: has %d files in memory to write to ZIP", len(fs.filemap))
for filename, file := range fs.filemap {
if _, ok := filesZipped[filename]; ok {
continue
}
writer, err := zf.Create(filename)
if err != nil {
return err
}
n, err := writer.Write(file.Data)
if err != nil {
return err
}
log.Debug("Exported file to zip: %s (%d bytes)", filename, n)
}
}
return nil
}
////////////////
// Level class methods for its filesystem access
////////////////
// SetFile sets a file's data in the level.
func (l *Level) SetFile(filename string, data []byte) {
l.Files.Set(filename, data)
}
// GetFile looks up an embedded file.
func (l *Level) GetFile(filename string) ([]byte, error) {
if l.Files == nil {
return []byte{}, errors.New("filesystem not initialized")
}
return l.Files.Get(filename)
}
// DeleteFile removes an embedded file.
func (l *Level) DeleteFile(filename string) bool {
l.Files.Delete(filename)
return true
}
// DeleteFiles removes all files beginning with the prefix.
func (l *Level) DeleteFiles(prefix string) int {
var count int
for _, filename := range l.Files.ListPrefix(prefix) {
l.Files.Delete(filename)
count++
}
return count
}
// ListFiles returns the list of all embedded file names, alphabetically.
func (l *Level) ListFiles() []string {
if l == nil {
log.Error("Level.ListFiles() was called on a nil Level??")
return []string{}
}
if l.Files == nil {
log.Error("Level(%s).ListFiles: FileSystem not initialized", l.Title)
return []string{}
}
return l.Files.List()
}
// ListFilesAt returns the list of files having a common prefix.
func (l *Level) ListFilesAt(prefix string) []string {
return l.Files.ListPrefix(prefix)
}