SaveManager

save, load and migrate persisted Properties.

▶ Run in browser

Tags: basics save load persistence property

A save in SimVX is a snapshot of the node tree’s persisted Properties. Mark a value with Property(..., persist=True) (or Colour(..., persist=True)) and SaveManager picks it up; everything else is left out. A Node2D’s position, rotation and scale persist by default, so the player here saves where it stands without declaring anything for it.

SaveManager(save_dir) writes one file per slot. save(root, "slot1") snapshots every node under root into slot1.sav, load("slot1") reads it back (raising FileNotFoundError when there is no such save) and apply(root, data) sets each stored value on the node at the same path, through the Property, so validation and change hooks run.

Saved data outlives the code that wrote it. A class states its format with __save_version__; when apply meets an entry written at an older version it passes the stored values through the class’s __migrate_save__(values, from_v, to_v) first. Here version 1 stored the score as points and version 2 renamed it, so the hook renames the key, and tint, which version 1 never had, keeps its current value.

Saves go to $XDG_DATA_HOME/simvx/save_load/ (~/.local/share when unset), or to the directory named by $SIMVX_SAVE_LOAD_DIR, so a save survives quitting and a copy of the example installed read-only still works.

Controls: arrow keys or the left stick move the disc, and a click or tap places it. Space scores, C cycles the colour, S saves and L loads; each has a button too.

Source

  1# /// simvx
  2# tags = ["basics", "save", "load", "persistence", "property"]
  3# web = { width = 960, height = 540, responsive = true }
  4# ///
  5"""SaveManager: save, load and migrate persisted Properties.
  6
  7A save in SimVX is a snapshot of the node tree's persisted Properties. Mark a value with
  8``Property(..., persist=True)`` (or ``Colour(..., persist=True)``) and ``SaveManager`` picks it
  9up; everything else is left out. A ``Node2D``'s ``position``, ``rotation`` and ``scale`` persist
 10by default, so the player here saves where it stands without declaring anything for it.
 11
 12``SaveManager(save_dir)`` writes one file per slot. ``save(root, "slot1")`` snapshots every
 13node under ``root`` into ``slot1.sav``, ``load("slot1")`` reads it back (raising
 14``FileNotFoundError`` when there is no such save) and ``apply(root, data)`` sets each stored
 15value on the node at the same path, through the Property, so validation and change hooks run.
 16
 17Saved data outlives the code that wrote it. A class states its format with
 18``__save_version__``; when ``apply`` meets an entry written at an older version it passes the
 19stored values through the class's ``__migrate_save__(values, from_v, to_v)`` first. Here
 20version 1 stored the score as ``points`` and version 2 renamed it, so the hook renames the key,
 21and ``tint``, which version 1 never had, keeps its current value.
 22
 23Saves go to ``$XDG_DATA_HOME/simvx/save_load/`` (``~/.local/share`` when unset), or to the
 24directory named by ``$SIMVX_SAVE_LOAD_DIR``, so a save survives quitting and a copy of the
 25example installed read-only still works.
 26
 27Controls: arrow keys or the left stick move the disc, and a click or tap places it. Space
 28scores, C cycles the colour, S saves and L loads; each has a button too.
 29"""
 30
 31import os
 32from pathlib import Path
 33
 34from simvx.core import (
 35    Anchor,
 36    Axis2D,
 37    Button,
 38    CanvasLayer,
 39    Colour,
 40    Controller,
 41    HBoxContainer,
 42    Input,
 43    Key,
 44    Label,
 45    Node2D,
 46    Pointer,
 47    Property,
 48    SaveManager,
 49    on_input,
 50)
 51from simvx.graphics import App
 52
 53RADIUS = 24.0
 54SPEED = 320.0
 55PALETTE = [(0.4, 0.8, 1.0, 1.0), (1.0, 0.65, 0.25, 1.0), (0.55, 0.95, 0.45, 1.0), (0.95, 0.45, 0.75, 1.0)]
 56
 57
 58def save_dir() -> Path:
 59    if override := os.environ.get("SIMVX_SAVE_LOAD_DIR"):
 60        return Path(override)
 61    return Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) / "simvx" / "save_load"
 62
 63
 64class Player(Node2D):
 65    __save_version__ = 2
 66
 67    score = Property(0, persist=True)
 68    tint = Colour(PALETTE[0], persist=True)
 69
 70    @classmethod
 71    def __migrate_save__(cls, values, from_v, to_v):
 72        if from_v == 1:
 73            values["score"] = values.pop("points", 0)
 74        return values
 75
 76    def on_draw(self, renderer):
 77        renderer.circle((0, 0), RADIUS, fill=self.tint)
 78        renderer.circle((0, 0), RADIUS, stroke=(1, 1, 1, 0.6))
 79
 80
 81class SaveLoadDemo(Node2D):
 82    input_context = {
 83        "move": [Axis2D.ARROWS, Controller.LEFT_STICK],
 84        "score": [Key.SPACE],
 85        "colour": [Key.C],
 86        "save": [Key.S],
 87        "load": [Key.L],
 88    }
 89
 90    def on_ready(self):
 91        self.saves = SaveManager(save_dir())
 92        self.player = self.add_child(Player(name="Player", position=self.viewport_size / 2))
 93
 94        # Example help overlay: not part of the technique shown.
 95        hud = self.add_child(CanvasLayer())
 96        self.info = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=10, font_size=20))
 97        self.status = "No save loaded"
 98        buttons = hud.add_child(HBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=10))
 99        for action in ("score", "colour", "save", "load"):
100            buttons.add_child(Button(action=action, text=action.capitalize()))
101
102    @on_input(Pointer.PRIMARY)
103    def place(self, event):
104        self.player.position = event.position
105
106    @on_input("score")
107    def add_score(self, event):
108        self.player.score += 10
109
110    @on_input("colour")
111    def next_colour(self, event):
112        self.player.tint = PALETTE[(PALETTE.index(self.player.tint) + 1) % len(PALETTE)]
113
114    @on_input("save")
115    def save(self, event):
116        self.status = f"Saved to {self.saves.save(self, 'slot1')}"
117
118    @on_input("load")
119    def load(self, event):
120        try:
121            data = self.saves.load("slot1")
122        except FileNotFoundError:
123            self.status = "No save in slot1 yet: press S first"
124            return
125        self.saves.apply(self, data)
126        self.status = f"Loaded slot1, saved at {data['saved_at'][:19]}"
127
128    def on_update(self, dt):
129        position = self.player.position + Input.value("move") * SPEED * dt
130        self.player.position = position.clip(RADIUS, self.viewport_size - RADIUS)
131        self.info.text = f"Score: {self.player.score}\n{self.status}"
132
133
134if __name__ == "__main__":
135    App(title="Save / Load", width=960, height=540).run(SaveLoadDemo())