Files
aego/modules/savegame/module.go
T
sanspie f22242e5aa
ci / release (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
Stage 11g2–11g3: registry texts, CLI and the site in Russian (D348)
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
2026-10-25 15:31:22 +03:00

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("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)
}
}