Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user