Skip to content
ct

Release Migration

1.x to 2.0 Migration

Move World Time and Calendar Time 1.x projects to the Calendar Time 2.0 runtime shape.

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

Calendar Time 2.0 is a major clean-break release. It replaces the older World Time / 1.x runtime shape with a final 2.0 architecture centered on:

  • GameClock: the runtime Resource that owns canonical integer microseconds, calendar state, the per-clock TimeSignalBus, per-clock save/load, and optional ageing.
  • TimeHost: the scene Node that advances one or more clocks from Godot process, physics process, or manual drive mode.
  • ClockGroupSerializer: the multi-clock save/load facade returned by time_host.get_group_serializer() and used by time_host.load_state(save_data).

Code and scenes need a one-time rename/wiring pass. Save files are designed to auto-migrate on load through the 2.0 save DTO path.

2.0.x patch change: ActionProgress renamed to TimeProgressBar

The ActionProgress UI node has been renamed to TimeProgressBar to better describe its role as a clock-driven progress UI component. It now advances from an explicit GameClock signal instead of engine _process(delta).

If your project uses ActionProgress:

  • Replace node/script references with TimeProgressBar.
  • Replace any ActionProgress.new() calls with TimeProgressBar.new().
  • The file path changed from addons/calendar_time/game_time/ui/action_progress.gd to addons/calendar_time/game_time/ui/time_progress_bar.gd.

A compatibility alias kept scenes and scripts referencing ActionProgress loading through 2.0.3. The alias was removed in 2.0.4 (#446). Replace any remaining ActionProgress references with TimeProgressBar before upgrading to 2.0.4.

2.0.2: TimeScale split into CalendarUnits + ClockPacing

The combined TimeScale resource has been split into two distinct types (issue #318):

  • CalendarUnits — the calendar-unit structure: seconds_per_minute, minutes_per_hour, hours_per_day. Owned by GameCalendar.
  • ClockPacing — the runtime pacing: delta_multiplier. Owned by TimeHost.

TimeScale still exists as a compatibility shell — it extends CalendarUnits and retains delta_multiplier, so existing .tres resources load unchanged. No immediate migration is required.

For new code: author CalendarUnits on the calendar and ClockPacing on the host instead of a combined TimeScale. Use time_scale.to_clock_pacing() to extract the pacing half from an existing TimeScale resource.

2.0.3: Authored persistent clock identity (issue #424)

A persistent_id (UUID v4) field has been added to GameClock. When set, it provides a durable save/load identity across file renames, project restructuring, and process restarts. Duplicating a resource copies its UUID. The serializer detects duplicate IDs, and the regeneration workflow fixes them. The path-hash clock_id changes when files move.

Identity priority chain (corrected): clock_id_overridepersistent_idresource_path hash → runtime counter.

clock_id_override takes priority over persistent_id so that clocks with an existing explicit override keep their identity when a persistent_id is later assigned.

Authoring

  • File-backed clocks: Select the .tres in the FileSystem dock, then run Project → Tools → Calendar Time: Assign Persistent ID.
  • Runtime clocks: Call clock.assign_persistent_id() in code.
  • Regenerate: Call clock.regenerate_persistent_id() or use the editor Project → Tools → Calendar Time: Regenerate Persistent ID action.

Legacy save migration

Saves made before 2.0.3 have no persistent_id field. After a clock gains a persistent_id, its clock_id changes from the path hash to the UUID. To preserve backward compatibility, ClockGroupSerializer.from_dict() builds a legacy-id alias table from _derive_clock_id_legacy() (the id the clock would have without persistent_id). Legacy entries keyed by path hash still match via this alias.

ClockGroupSerializer.to_dict() now stores persistent_id in each per-clock save entry (when non-empty):

{
    "clocks": {
        "<clock_id>": {
            "clock_id":"<clock_id>",
            "persistent_id":"<uuid>",# present only when non-empty
            "time_state": { ... },
            "age_states": { ... },
            "age_service_state": { ... }
        }
    }
}

Duplicate persistent_id values within a clock group are detected at save time and surfaced via a persistent_id_duplicates_detected flag on the serialized result.

2.0.2: internal types de-globalized

Several internal implementation types had their class_name removed (issue #319). They are now loaded via explicit preload() paths and are no longer globally discoverable:

  • Internal calendar coordinators
  • DayNightCycleServiceLogic
  • Ageing internals

If your code referenced any of these by class_name, switch to an explicit preload:

const Logic := preload("res://addons/calendar_time/day_night_cycle/service/logic/day_night_cycle_service_logic.gd")

See docs/PUBLIC-API.md for the full classification of every shipped type into stable / advanced / internal tiers.

Quick Start

  1. Back up your project and any user:// save files.
  2. Replace the old plugin folder with the 2.0 addons/calendar_time/ folder.
  3. Open and re-save scenes/resources that used World Time / Calendar Time nodes or resources so Godot refreshes Inspector data.
  4. Update scripts using the rename tables below.
  5. Add or update a TimeHost and assign one or more GameClock resources to TimeHost.clocks.
  6. Route app-level group loads through time_host.load_state(save_data) so each clock publishes clock_state_loaded after state restore.
  7. Run a save/load smoke test and confirm clocks reload to the exact saved time.

Core Rename Table

Old name (World Time / 1.x) New name (2.0) Notes
World Time Calendar Time Product/plugin branding.
addons/world_time/ addons/calendar_time/ Plugin folder.
WorldTimePlugin CalendarTimePlugin Editor plugin class.
GameTimeSystem TimeHost Scene Node that drives assigned GameClock resources.
TimeState GameClock / TimeSnapshot Runtime state lives on GameClock; save DTO data uses TimeSnapshot.
WorldTimeSerializer ClockGroupSerializer Multi-clock facade via time_host.get_group_serializer().
WorldAgeSystem AgeService Optional per-clock ageing service, usually assigned on GameClock.
AgeingComponent AgeComponent Node attached to ageable game objects.
AgeingSettings AgeSettings Ageing configuration Resource.
AgeingSceneReplacement AgeSceneReplacement Age-threshold scene replacement node.
AddWhenAgeing AddWhenAged Age-threshold resource addition node.
WorldTimeClock removed Bind consumers to GameClock and clock.signal_bus directly.

Common Property Renames

Old property New property Notes
time_state clock / clocks UI/components bind one GameClock; TimeHost drives clocks[].
calendar on old system nodes calendar on GameClock Calendar schema belongs to the clock Resource.
ageing_component age_component Node-path exports.
ageing_settings age_settings Ageing resources and components.
world_age_system age_service Save/load and ageing setup.
DateChangeEvent.new DateChangeEvent.new_date Avoids collision with GDScript constructor naming.
DateChangeEvent.old DateChangeEvent.old_date Paired with new_date.
TimeOfDay.color TimeOfDay.tint_color Time-of-day resources.
TimeOfDayLightSettings.color TimeOfDayLightSettings.light_color Lighting resources.
EventDay.color EventDay.accent_color Event day resources.
TimeOfDayToggle.target TimeOfDayToggle.target_node Demo example script, no longer core plugin API.
TimeOfDayToggle.property TimeOfDayToggle.target_property Demo example script, no longer core plugin API.
CloseButton.target CloseButton.target_node Demo example script, no longer core plugin API.
AddAfterGameSeconds.add_target AddAfterGameSeconds.target_node Age/timer helper nodes.
AddAfterGameSeconds.amount AddAfterGameSeconds.seconds_amount Age/timer helper nodes.
AddWhenAged.target_resource AddWhenAged.target_age_state_resource Age trigger resources.
AddWhenAged.amount AddWhenAged.age_amount Age trigger resources.

Scene Wiring Changes

1.x style

Older projects often had a time system node that owned time state directly. Consumers reached into that node or a state object for current time and signals.

2.0 style

Create or duplicate a GameClock Resource, assign its calendar, then wire it explicitly:

@export var clock:GameClock
@export var time_host:TimeHost

func _ready()-> void:
    time_host.clocks= [clock]
    clock.signal_bus.game_seconds_advanced.connect(_on_game_seconds_advanced)
    clock.signal_bus.clock_state_loaded.connect(_on_clock_state_loaded)

For designer-authored scenes, assign TimeHost.clocks in the Inspector. auto_attach_unbound_clocks is a deprecated no-op kept only so existing scenes with the serialized property still load; it no longer discovers clocks automatically.

Signals

Signals are consumed through clock.signal_bus.

Signal mapping:

  • Time advanced by seconds — clock.signal_bus.game_seconds_advanced(amount, total).
  • Date/time changed — clock.signal_bus.date_time_changed(new, old).
  • Date changed — clock.signal_bus.date_changed(event).
  • Day finished — clock.signal_bus.day_finished(finished_date).
  • Time of day changed — clock.signal_bus.time_of_day_changed(current, previous).
  • Day/night transition progress — clock.signal_bus.day_night_transition_progress_changed(progress).
  • Event day started — clock.signal_bus.event_day_started(event_day).
  • Clock speed changed — clock.signal_bus.clock_speed_changed(speed_multiplier).
  • State loaded — clock.signal_bus.clock_state_loaded().

Example:

func _ready()-> void:
    clock.signal_bus.game_seconds_advanced.connect(_on_game_seconds_advanced)
    clock.signal_bus.day_night_transition_progress_changed.connect(_on_day_night_progress)
    clock.signal_bus.clock_state_loaded.connect(_refresh_from_loaded_clock)

clock_state_loaded is emitted by time_host.load_state(save_data) after the group serializer restores state. Use it to refresh UI, day/night visuals, and any derived state that should update immediately after load.

Saving And Loading

Per-clock save

Use this when you only own one clock and do not need group composition:

var save_data:Dictionary = clock.to_dict()
clock.from_dict(save_data)

clock.from_dict(save_data) publishes clock_state_loaded after restoring the clock.

Multi-clock save

Use the host facade for normal app-level saves:

var save_data:Dictionary = time_host.get_group_serializer().to_dict()
time_host.load_state(save_data)

The current group shape is:

{
    "clocks": {
        "<clock_id>": {
            "clock_id":"<clock_id>",
            "persistent_id":"<uuid>",# issue #424: present only when non-empty
            "time_state": { ... },
            "age_states": { ... },
            "age_service_state": { ... }
        }
    }
}

age_states and age_service_state are present only when the clock has an age_service.

Each GameClock has a stable clock_id. Resource-backed clocks derive it from resource_path. Runtime-only clocks get a session-unique runtime id, which is useful inside the current process but not stable across separate game launches. Set clock_id_override for runtime clocks that must load across separate game sessions.

For the full resolution chain and reorder-safe save details, see Multi-clock Save Identity.

Pre-2.0 save data that used the top-level 1.x WorldTimeSerializer shape migrates when loaded through time_host.load_state(save_data).

That legacy shape used time_state, age_states, and world_age_system_state at the save root.

Calendar Time loads it into the first bound clock positionally.

Early grouped pre-release saves under clocks are still tolerated.

Entries with clock_id load by id.

Entries without clock_id fall back to positional matching.

Unknown ids are skipped.

Float game_seconds and legacy game_ticks are migrated by the TimeSnapshot DTO path. The save key time_state remains for compatibility even though the current runtime object is GameClock and the DTO is TimeSnapshot.

Serialization keys vs inspector class_name

Calendar Time 2.0 dict saves use property names, never Godot global class names:

Serialization keys (property names, not class names):

  • GameDate — keys day, month, year. Safe: rename class_name or @export_range. Needs migration: rename properties.
  • HoursTime — keys hours, minutes, seconds. Same rules.
  • DateTime — nested date / time dicts. Same rules.

Inspector / editor front-end types (class_name, script_class labels) may rename for clarity. Resources still load by script path; dict saves do not embed class strings. After any property rename, re-save designer .tres / .tscn files (clean-break stance in §0) and update this table.

JSON reloads may deliver numbers as floats.

GameDate.from_dict and HoursTime.from_dict coerce to property types.

That keeps one-based day/month validation from breaking otherwise-valid saves.

Time Driving

TimeHost.drive_mode controls how clocks advance:

Mode Use
PROCESS Normal frame-driven game time.
PHYSICS_PROCESS Physics-step-driven game time.
MANUAL Tests, replay, fixed-step, ECS, or deterministic-friendly integrations.

For deterministic-friendly simulation, use integer microseconds:

time_host.drive_mode= TimeHost.DriveMode.MANUAL
time_host.drive_microseconds(16_666)

auto_increment_time is preserved as a backward-compatible alias for drive_mode:

  • auto_increment_time = true is equivalent to drive_mode = PROCESS.
  • auto_increment_time = false is equivalent to drive_mode = MANUAL.
  • drive_mode = PHYSICS_PROCESS reads back as auto_increment_time == true because it is still an auto-driven mode.

GameClock.current_microseconds is the canonical value. game_seconds() and date_time() are derived views.

Ageing Migration

Ageing is optional in 2.0 and is per-clock.

  • Replace WorldAgeSystem with AgeService.
  • Replace AgeingComponent with AgeComponent.
  • Assign the same GameClock used by the relevant gameplay system.
  • Bind each AgeComponent.clock to that GameClock; components resolve their registry from clock.age_registry.
  • Use clock.age_registry instead of the old process-wide singleton registry.

Most projects can migrate object ageing by assigning age_service on the relevant GameClock, updating renamed node/resource classes, binding each AgeComponent.clock, and re-saving affected scenes. Template clock resources show the supported .tres shape: a GameClock can include both calendar = ExtResource(...) and age_service = ExtResource(...).

File And Template Paths

Path renames:

  • addons/world_time/addons/calendar_time/
  • template world_time_systems.tscntemplates/calendar_time/default_time_host.tscn
  • ageing/ageing_component.gdageing/age_component.gd
  • ageing/ageing_settings.gdageing/age_settings.gd
  • ageing/add_when_ageing.gdageing/add_when_aged.gd
  • ageing/ageing_scene_replacement.gdageing/age_scene_replacement.gd

If you use uid:// references dragged through the Inspector, Godot may keep many links intact. If you use load() / preload() paths, update them manually.

Minimal Migration Checklist

  • Back up your project and saves.
  • Replace the addon folder with addons/calendar_time/.
  • Replace old time system nodes with TimeHost.
  • Create/assign GameClock resources and bind them to TimeHost.clocks.
  • Move calendar assignment onto each GameClock.
  • Move signal connections to clock.signal_bus.
  • Update app-level load paths to time_host.load_state(save_data).
  • Update ageing class/property names if your project uses ageing.
  • Bind each AgeComponent.clock to the same GameClock whose age registry it should use.
  • Remove any old AgeStateRegistry.get_singleton() usage and replace it with clock.age_registry.
  • Re-save scenes and resources in the Godot editor.
  • Run your save/load smoke test and confirm clocks reload to the exact saved time.

Demo Example Scripts (No Migration Required Unless You Used Them Directly)

Three small helper scripts used to live in the addon. They are now example scripts in the demo because they are conveniences, not core runtime. Consumers who need the behavior can copy the script from the demo into their own project.

Demo script Purpose Replace with
res://demo/scripts/calendar_time_examples/close_button.gd Button that hides a target Control on press. Inline pressed signal handler: button.pressed.connect(func(): target.hide()).
res://demo/scripts/calendar_time_examples/time_of_day_animated_sprite_2d.gd AnimatedSprite2D that switches its animation on time_of_day_changed. Plain AnimatedSprite2D + subscribe to clock.signal_bus.time_of_day_changed and call play("...").
res://demo/scripts/calendar_time_examples/time_of_day_toggle.gd Node that toggles a target property on time_of_day_changed. Plain Node + subscribe to clock.signal_bus.time_of_day_changed and set(...) the target property yourself.

If you used these classes in your own scenes, copy the example script into your project.

Update the scene script reference.

The class_name identifiers were removed so two projects can include the examples without global name collisions.

These example scripts are part of the Calendar Time demo (demo.zip) and the test scene (test/calendar_time/scenes/test_calendar_display.tscn) references them through the demo path. The plugin payload (plugin.zip) no longer contains them.

Troubleshooting

Time does not advance

Check that TimeHost.clocks contains at least one GameClock, drive_mode is not MANUAL unless you call drive_microseconds() / drive_seconds(), and time_scale.delta_multiplier is not zero.

UI does not refresh after load

Use time_host.load_state(save_data) instead of calling the group serializer's from_dict() directly. The host method publishes clock_state_loaded after restore.

A runtime clock loads as a different clock next session

Set clock.clock_id_override to a stable StringName before saving.

Old Inspector fields show missing property warnings

Open the scene/resource, reassign renamed exports if needed, and save. This is expected for clean-break renamed fields.