198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
package savegame
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"aego/contracts/persistence"
|
|
"aego/contracts/scriptevents"
|
|
"aego/core/log"
|
|
"aego/engine"
|
|
"aego/engine/module"
|
|
"aego/format/aetext"
|
|
"aego/save"
|
|
"aego/schema"
|
|
)
|
|
|
|
const (
|
|
ID = "save"
|
|
SystemStep = ID + ".step"
|
|
ScriptModule = "aego/save"
|
|
EventSave = "OnSave"
|
|
EventLoad = "OnLoad"
|
|
MarkerName = "save.Persist"
|
|
SlotCheckpoint = "checkpoint"
|
|
SlotAutosave = "autosave"
|
|
)
|
|
|
|
type Persist struct {
|
|
NoTransform bool `aego:"noTransform"`
|
|
NoEnabled bool `aego:"noEnabled"`
|
|
}
|
|
|
|
type Events struct {
|
|
Save scriptevents.EventID
|
|
Load scriptevents.EventID
|
|
}
|
|
|
|
type Result struct {
|
|
Slot string
|
|
Load bool
|
|
Err error
|
|
}
|
|
|
|
type Module struct {
|
|
ev Events
|
|
sections []module.OwnedSection
|
|
game string
|
|
eng *engine.Engine
|
|
log *log.ChannelLogger
|
|
global aetext.Node
|
|
pending *save.Doc
|
|
unknown []save.Section
|
|
playtime float64
|
|
autosave float64
|
|
last Result
|
|
rev uint64
|
|
}
|
|
|
|
func New() *Module {
|
|
return &Module{ev: Events{Save: scriptevents.NoEvent, Load: scriptevents.NoEvent}}
|
|
}
|
|
|
|
func (*Module) Info() module.Info {
|
|
return module.Info{ID: ID, Version: engine.Version, Requires: []string{"script", "spawn"}, Builtin: true, DisplayName: "Saving", DocsOrder: 80, DocsIcon: "comp.persist"}
|
|
}
|
|
|
|
func (m *Module) Register(r *module.Registry) error {
|
|
registerDocs(r)
|
|
if err := module.Component[Persist](r, schema.Name(MarkerName),
|
|
schema.Doc("Persist saves the node's transform and enabled flag in save games."),
|
|
schema.FieldDoc("noTransform", "Leaves the transform out of save games, for nodes whose position comes from elsewhere."),
|
|
schema.FieldDoc("noEnabled", "Leaves the enabled flag out of save games.")).
|
|
Category("Engine").Display("Persist").Icon("comp.persist").Err(); err != nil {
|
|
return err
|
|
}
|
|
m.ev.Save = r.ScriptEvent(scriptevents.Event{Name: EventSave,
|
|
Doc: "OnSave runs on every script that has it while a game is saved; write values with save.Put*."})
|
|
m.ev.Load = r.ScriptEvent(scriptevents.Event{Name: EventLoad,
|
|
Doc: "OnLoad runs after Ready on every script that has it when a saved game is loaded; read values with save.Get*."})
|
|
r.System(engine.SystemDesc{ID: SystemStep, Phase: engine.PhaseAnimation, Scope: engine.ScopePlay, Pause: engine.PauseAlways, System: step{}})
|
|
if r.Bindings() != nil {
|
|
installScript(r.ScriptModule(ScriptModule), m)
|
|
}
|
|
r.Finalize(func(r *module.Registry) error {
|
|
m.sections = r.SaveSections()
|
|
var games []string
|
|
for _, info := range r.GameModules() {
|
|
games = append(games, info.ID+"@"+info.Version)
|
|
}
|
|
m.game = strings.Join(games, ",")
|
|
return nil
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (m *Module) Init(ctx *module.InitContext) error {
|
|
m.eng = ctx.Engine
|
|
lg := ctx.Log
|
|
if lg == nil {
|
|
lg = log.Nop()
|
|
}
|
|
m.log = lg.With(log.Channel(ID))
|
|
return nil
|
|
}
|
|
|
|
func (m *Module) logger() *log.ChannelLogger {
|
|
if m.log == nil {
|
|
m.log = log.Nop().With(log.Channel(ID))
|
|
}
|
|
return m.log
|
|
}
|
|
|
|
func (m *Module) AttachWorld(ctx *module.AttachContext) error {
|
|
if !ctx.Play || ctx.World == nil || ctx.Scene == nil {
|
|
return nil
|
|
}
|
|
s := NewService(m, ctx.World, ctx.Scene)
|
|
ctx.World.SetService(persistence.ServiceID, s)
|
|
if doc := m.pending; doc != nil {
|
|
m.pending = nil
|
|
m.finish(Result{Slot: doc.Meta.Name, Load: true, Err: s.Apply(doc)})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (*Module) DetachWorld(ctx *module.AttachContext) {
|
|
if ctx.Play && ctx.World != nil {
|
|
ctx.World.RemoveService(persistence.ServiceID)
|
|
}
|
|
}
|
|
|
|
func (m *Module) finish(r Result) {
|
|
m.last = r
|
|
m.rev++
|
|
what := "saved"
|
|
if r.Load {
|
|
what = "loaded"
|
|
}
|
|
if r.Err != nil {
|
|
m.logger().Error("cannot "+strings.TrimSuffix(what, "d")+" the game", log.F("slot", r.Slot), log.F("err", r.Err.Error()))
|
|
return
|
|
}
|
|
m.logger().Info("game "+what, log.F("slot", r.Slot))
|
|
}
|
|
|
|
func (m *Module) Last() (Result, uint64) {
|
|
return m.last, m.rev
|
|
}
|
|
|
|
func (m *Module) Playtime() float64 {
|
|
return m.playtime
|
|
}
|
|
|
|
func (m *Module) Global(key string) (aetext.Value, bool) {
|
|
return m.global.Field(key)
|
|
}
|
|
|
|
func (m *Module) SetGlobal(key string, v aetext.Value) {
|
|
m.global.SetField(key, v)
|
|
}
|
|
|
|
func (m *Module) store() *save.Store {
|
|
if m.eng == nil {
|
|
return nil
|
|
}
|
|
st, _ := engine.Service[*save.Store](m.eng, persistence.StoreID)
|
|
return st
|
|
}
|
|
|
|
func (m *Module) encode(st *save.Store, slot string, doc *save.Doc) {
|
|
run := func() {
|
|
err := st.Write(slot, save.Encode(doc), nil)
|
|
done := func() { m.finish(Result{Slot: slot, Err: err}) }
|
|
if m.eng != nil {
|
|
m.eng.Jobs().OnMain(done)
|
|
return
|
|
}
|
|
done()
|
|
}
|
|
if m.eng != nil {
|
|
m.eng.Jobs().Submit(run)
|
|
return
|
|
}
|
|
run()
|
|
}
|
|
|
|
func Of(w *engine.World) *Service {
|
|
s, _ := engine.ServiceOf[*Service](w, persistence.ServiceID)
|
|
return s
|
|
}
|
|
|
|
type step struct{}
|
|
|
|
func (step) Run(ctx *engine.Context) {
|
|
if s := Of(ctx.World); s != nil {
|
|
s.Step(ctx.Dt, ctx.Unscaled)
|
|
}
|
|
}
|