SaveManager¶
save, load and migrate persisted Properties.
▶ Run in browserTags: 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())