Skip to content
ct

Calendar Time v2.0.4

Multi-clock Save Identity

How ClockGroupSerializer.todict() / fromdict() identifies a clock across save → load cycles, and why we use a stable clockid rather than array position.

Status
Current
Version
v2.0.4
Updated
2026-08-09

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_path hash — durable when the same .tres is 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 .tres clocks: 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()): Call clock.assign_persistent_id() in code. This generates a UUID v4-like string using CalendarTimeUtils.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_path hash (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