How ClockGroupSerializer.to_dict() / from_dict() identifies a
clock across save → load cycles, and why we use a stable
clock_id rather than array position.
TL;DR
Identity is clock_id, not array index.
Saves key by clock_id from resource_path or a runtime counter.
Reordering clocks between save and load is safe.
How clock_id is derived
A GameClock exposes a clock_id getter. The resolution chain:
clock_id_override— durable across sessions. Designer-chosen explicit label.persistent_id— durable across file renames and project restructuring. It also survives process restarts and cross-machine use. Duplicating a resource copies its UUID. Duplicate IDs are detected and can be regenerated.resource_pathhash — durable when the same.tresis shipped.- Runtime counter — not durable; session seed changes.
clock_id_override takes priority over persistent_id so that clocks with
an existing explicit override keep their identity unchanged when a
persistent_id is later assigned. Set persistent_id once via
clock.assign_persistent_id() or the editor "Calendar Time: Assign
Persistent ID" menu action. Leave it stable — changing it silently breaks
the link to existing saves.
Authored persistent ID (issue #424)
persistent_id is a @export var String on GameClock. When non-empty, it is
second in the clock_id resolution chain (after clock_id_override but
before resource_path and the runtime counter).
Authoring workflow
- File-backed
.tresclocks: Select the clock resource in the FileSystem dock, then run Project → Tools → Calendar Time: Assign Persistent ID. The UUID is written to the resource and saved to disk automatically. - Runtime clocks (created via
GameClock.new()): Callclock.assign_persistent_id()in code. This generates a UUID v4-like string usingCalendarTimeUtils.generate_uuid()and assigns it if the field is empty. Safe to call repeatedly — it's idempotent. - Regenerate: Call
clock.regenerate_persistent_id()or run the Project → Tools → Calendar Time: Regenerate Persistent ID action. Regeneration replaces the existing UUID. Use it after duplicating a resource. The Assign action is idempotent. The Regenerate action always overwrites.
Save format
ClockGroupSerializer._clock_to_dict() stores persistent_id in each
per-clock save entry. It stores the field when the value is non-empty. On load,
the serializer tries clock_id first. If that fails, it uses a unique
persistent_id from the save entry. A shared UUID is marked ambiguous. The
serializer refuses fallback instead of choosing an arbitrary clock. This handles
clocks whose paths changed or whose IDs were assigned after saving.
Legacy path-key migration (2.0.3)
Saves made before persistent_id existed have no persistent_id field.
If a clock later receives one, its clock_id changes from the path hash to the
UUID. The old save entry key no longer matches. To preserve compatibility,
from_dict() builds a legacy-ID alias table with _derive_clock_id_legacy().
Legacy path-hash entries still match through this alias.
Persistence safety by identity type
Persistence by identity type:
persistent_id(e.g."a1b2c3d4-...") — durable across renames, restarts, and machines. Duplicating a resource copies its UUID; duplicate IDs are detected and the regeneration workflow provides the repair path. This is the recommended identity for any clock that participates in long-lived saves.clock_id_override(e.g.&"world_clock") — durable across sessions.resource_pathhash (e.g.path:world_clock.tres:a3f2b1c0) — durable; same file, same id. Changes on file rename/restructure.- Runtime counter (e.g.
runtime:1a2b3c4d:1) — not durable; session seed changes.
For durable persistence of runtime clocks, set persistent_id:
func _ready()-> void:
var clock:= GameClock.new(my_calendar)
clock.assign_persistent_id()
$TimeHost.clocks= [clock]Runtime-only clocks
Clocks created at runtime via GameClock.new() that have neither a
persistent_id nor a clock_id_override receive a session-local
runtime:<seed>:<counter> id. These ids cannot match after a process
restart — the session seed changes between processes. ClockGroupSerializer
warns when a save contains anonymous runtime clocks and recommends setting
persistent_id for durable persistence.
What the contract gives you
Reordered saves restore by clock_id match.
When clocks are added or removed:
- More clocks at load than save: new clocks keep initial values.
- Fewer clocks at load than save: unknown saved entries are skipped.
Explicit host binding
GameClock instances are not registered in a process-wide static list.
TimeHost drives only the clocks assigned to its clocks export array.
Bind clocks explicitly:
@export var world_clock :GameClock
@export var festival_clock :GameClock
func _ready()-> void:
var host:= TimeHost.new()
host.clocks= [world_clock, festival_clock]
add_child(host)Multi-host setups (e.g. two unrelated scenes with separate clocks that happen to share a process) work correctly because each host tracks only the clocks it explicitly lists.
Related
- TimeScale and DriveModes — TimeScale ownership and the DriveMode enum (related ownership-contract pattern).