Files
questshock/modding/hacked/ss1/edit/ProjectService.go
T

395 lines
11 KiB
Go

package edit
import (
"bytes"
"os"
"path/filepath"
"time"
"github.com/inkyblackness/hacked/ss1"
"github.com/inkyblackness/hacked/ss1/content/object"
"github.com/inkyblackness/hacked/ss1/content/texture"
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/resource/lgres"
"github.com/inkyblackness/hacked/ss1/serial"
"github.com/inkyblackness/hacked/ss1/world"
"github.com/inkyblackness/hacked/ss1/world/ids"
)
const (
errNoResourcesFound ss1.StringError = "no resources found"
errNoStorageLocationSet ss1.StringError = "no storage location set"
)
// ProjectSettings describe the properties of a project.
type ProjectSettings struct {
ModFiles []string
Manifest []ManifestEntrySettings
}
// ManifestEntrySettings describe the properties of one manifest entry in a project.
type ManifestEntrySettings struct {
Origin []string
}
const autosaveTimeoutSec = 5
// SaveStatus describes the current change state.
type SaveStatus struct {
mod *world.Mod
// FilesModified is the count of how many files are affected by the current pending change.
FilesModified int
// SavePending is set if the mod has a storage location and changes are to be saved.
SavePending bool
// SaveIn is the duration after which an auto-save should happen.
SaveIn time.Duration
}
// ConfirmPendingSave marks that the recent auto-save status has been acknowledged and should no longer be notified.
// A new change will re-start the auto-save timer.
func (status SaveStatus) ConfirmPendingSave() {
if status.mod == nil {
return
}
status.mod.ResetLastChangeTime()
}
// ProjectService handles the overall information about the active project.
type ProjectService struct {
commander cmd.Registry
mod *world.Mod
modPath string
stateFilename string
}
// NewProjectService returns a new instance of a service for given mod.
func NewProjectService(commander cmd.Registry, mod *world.Mod) *ProjectService {
return &ProjectService{
commander: commander,
mod: mod,
}
}
// SaveStatus returns the current pending save information.
func (service ProjectService) SaveStatus() SaveStatus {
var status SaveStatus
status.mod = service.mod
status.FilesModified = len(service.mod.ModifiedFilenames())
if status.FilesModified > 0 {
lastChangeTime := service.mod.LastChangeTime()
if service.ModHasStorageLocation() && !lastChangeTime.IsZero() {
status.SavePending = true
saveAt := lastChangeTime.Add(time.Duration(autosaveTimeoutSec) * time.Second)
status.SaveIn = time.Until(saveAt)
if status.SaveIn <= 0 {
status.SaveIn = 0
}
}
}
return status
}
// CurrentStateFilename returns the name for the state of the project.
func (service ProjectService) CurrentStateFilename() string {
return service.stateFilename
}
// SetCurrentStateFilename updates the current filename.
func (service *ProjectService) SetCurrentStateFilename(value string) {
service.stateFilename = value
}
// CurrentSettings returns the snapshot of the project.
func (service ProjectService) CurrentSettings() ProjectSettings {
manifest := service.mod.World()
settings := ProjectSettings{Manifest: make([]ManifestEntrySettings, manifest.EntryCount())}
for i := 0; i < manifest.EntryCount(); i++ {
entry, _ := manifest.Entry(i)
settings.Manifest[i].Origin = entry.Origin
}
settings.ModFiles = service.relativeToSettings(service.mod.AllAbsoluteFilenames(service.modPath)...)
return settings
}
func (service *ProjectService) relativeToSettings(filenames ...string) []string {
loc := world.FileLocationFrom(service.stateFilename)
relatives := make([]string, 0, len(filenames))
for _, filename := range filenames {
relatives = append(relatives, world.FileLocationFrom(filename).NestedRelativeTo(loc.DirPath))
}
return relatives
}
func (service *ProjectService) absoluteFromSettings(filenames ...string) []string {
loc := world.FileLocationFrom(service.stateFilename)
absolutes := make([]string, 0, len(filenames))
for _, filename := range filenames {
absolutes = append(absolutes, world.FileLocationFrom(filename).AbsolutePathFrom(loc.DirPath))
}
return absolutes
}
// RestoreProject sets internal data based on the given settings.
func (service *ProjectService) RestoreProject(settings ProjectSettings, stateFilename string) {
service.ResetProject()
service.stateFilename = stateFilename
manifest := service.mod.World()
for _, entrySettings := range settings.Manifest {
entry, err := world.NewManifestEntryFrom(entrySettings.Origin)
if err != nil {
continue
}
err = manifest.InsertEntry(manifest.EntryCount(), entry)
if err != nil {
continue
}
}
_ = service.TryLoadModFrom(service.absoluteFromSettings(settings.ModFiles...))
}
// ResetProject clears the project and returns it to initial state.
func (service *ProjectService) ResetProject() {
service.setActiveMod("", nil, nil, nil)
service.mod.World().Reset()
service.stateFilename = ""
}
// AddManifestEntry attempts to insert the given manifest entry at given index.
func (service *ProjectService) AddManifestEntry(at int, entry *world.ManifestEntry) error {
return service.commander.Register(
cmd.Named("AddManifestEntry"),
cmd.Forward(func(modder world.Modder) error {
return service.mod.World().InsertEntry(at, entry)
}),
cmd.Reverse(func(modder world.Modder) error {
return service.mod.World().RemoveEntry(at)
}),
)
}
// RemoveManifestEntry attempts to remove the manifest entry at given index.
func (service *ProjectService) RemoveManifestEntry(at int) error {
manifest := service.mod.World()
entry, err := manifest.Entry(at)
if err != nil {
return err
}
return service.commander.Register(
cmd.Named("RemoveManifestEntry"),
cmd.Forward(func(modder world.Modder) error {
return manifest.RemoveEntry(at)
}),
cmd.Reverse(func(modder world.Modder) error {
return manifest.InsertEntry(at, entry)
}),
)
}
// MoveManifestEntry attempts to remove the manifest entry at given from index and re-insert it at given to index.
func (service *ProjectService) MoveManifestEntry(to, from int) error {
return service.commander.Register(
cmd.Named("MoveManifestEntry"),
cmd.Forward(func(modder world.Modder) error {
return service.mod.World().MoveEntry(to, from)
}),
cmd.Reverse(func(modder world.Modder) error {
return service.mod.World().MoveEntry(from, to)
}),
)
}
// Mod returns the currently active mod in the project.
func (service ProjectService) Mod() *world.Mod {
return service.mod
}
// NewMod resets the mod to a new state.
func (service *ProjectService) NewMod() {
service.setActiveMod("", nil, nil, nil)
}
// TryLoadModFrom attempts to set the active mod from given filenames.
func (service *ProjectService) TryLoadModFrom(names []string) error {
loaded := world.LoadFiles(false, names)
resourcesToTake := loaded.Resources
isSavegame := false
if (len(resourcesToTake) == 0) && (len(loaded.Savegames) == 1) {
resourcesToTake = loaded.Savegames
isSavegame = true
}
if len(resourcesToTake) == 0 {
return errNoResourcesFound
}
var locs []*world.LocalizedResources
modPath := ""
for location := range resourcesToTake {
if (len(modPath) == 0) || (len(location.DirPath) < len(modPath)) {
modPath = location.DirPath
}
}
for location, viewer := range resourcesToTake {
lang := ids.LocalizeFilename(location.Name)
template := location.Name
if isSavegame {
template = string(ids.Archive)
}
loc := &world.LocalizedResources{
File: location,
Template: template,
Language: lang,
}
for _, id := range viewer.IDs() {
view, err := viewer.View(id)
if err != nil {
// optimistic ignore
continue
}
_ = loc.Store.Put(id, view)
}
locs = append(locs, loc)
}
service.setActiveMod(modPath, locs, loaded.ObjectProperties, loaded.TextureProperties)
return nil
}
func (service *ProjectService) setActiveMod(modPath string, resources []*world.LocalizedResources,
objectProperties object.PropertiesTable, textureProperties texture.PropertiesList) {
service.setModPath(modPath)
service.mod.Reset(resources, objectProperties, textureProperties)
// fix list resources for any "old" mod.
service.mod.FixListResources()
}
// ModifyModWith runs a function with the intent to alter the current mod.
func (service *ProjectService) ModifyModWith(modifier func(world.Modder) error) (err error) {
service.mod.Modify(func(modder world.Modder) {
err = modifier(modder)
})
return
}
// ModHasStorageLocation returns whether the mod has a place to be stored.
func (service ProjectService) ModHasStorageLocation() bool {
return len(service.modPath) > 0
}
// ModPath returns the base path of the mod in the project.
func (service ProjectService) ModPath() string {
return service.modPath
}
func (service *ProjectService) setModPath(value string) {
service.modPath = value
}
// SaveMod will store the currently active mod in its current path.
func (service *ProjectService) SaveMod() error {
if !service.ModHasStorageLocation() {
return errNoStorageLocationSet
}
return service.SaveModUnder(service.modPath)
}
// SaveModUnder will store the currently active mod in the given path.
func (service *ProjectService) SaveModUnder(modPath string) error {
service.mod.FixListResources()
err := service.saveModResourcesTo(modPath)
if err != nil {
return err
}
service.setModPath(modPath)
service.mod.MarkSave()
return nil
}
func (service *ProjectService) saveModResourcesTo(modPath string) error {
localized := service.mod.ModifiedResources()
filenamesToSave := service.mod.ModifiedFilenames()
shallBeSaved := func(filename string) bool {
for _, toSave := range filenamesToSave {
if toSave == filename {
return true
}
}
return false
}
for _, loc := range localized {
if shallBeSaved(loc.File.Name) {
err := saveResourcesTo(loc.Store, loc.File.AbsolutePathFrom(modPath))
if err != nil {
return err
}
}
}
if shallBeSaved(world.TexturePropertiesFilename) {
err := saveTexturePropertiesTo(service.mod.TextureProperties(), filepath.Join(modPath, world.TexturePropertiesFilename))
if err != nil {
return err
}
}
if shallBeSaved(world.ObjectPropertiesFilename) {
err := saveObjectPropertiesTo(service.mod.ObjectProperties(), filepath.Join(modPath, world.ObjectPropertiesFilename))
if err != nil {
return err
}
}
return nil
}
func saveResourcesTo(viewer resource.Viewer, absFilename string) error {
file, err := os.Create(absFilename)
if err != nil {
return err
}
defer func() {
_ = file.Close() // nolint: gas
}()
err = lgres.Write(file, viewer)
return err
}
func saveTexturePropertiesTo(list texture.PropertiesList, absFilename string) error {
return saveCodableTo(list, absFilename)
}
func saveObjectPropertiesTo(list object.PropertiesTable, absFilename string) error {
return saveCodableTo(list, absFilename)
}
func saveCodableTo(codable serial.Codable, absFilename string) error {
buffer := bytes.NewBuffer(nil)
encoder := serial.NewEncoder(buffer)
codable.Code(encoder)
err := encoder.FirstError()
if err != nil {
return err
}
file, err := os.Create(absFilename)
if err != nil {
return err
}
defer func() {
_ = file.Close() // nolint: gas
}()
_, err = file.Write(buffer.Bytes())
return err
}