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-clockTimeSignalBus, 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 bytime_host.get_group_serializer()and used bytime_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 withTimeProgressBar.new(). - The file path changed from
addons/calendar_time/game_time/ui/action_progress.gdtoaddons/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 byGameCalendar.ClockPacing— the runtime pacing:delta_multiplier. Owned byTimeHost.
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_override → persistent_id
→ resource_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
.tresin the FileSystem dock, then runProject → Tools → Calendar Time: Assign Persistent ID. - Runtime clocks: Call
clock.assign_persistent_id()in code. - Regenerate: Call
clock.regenerate_persistent_id()or use the editorProject → Tools → Calendar Time: Regenerate Persistent IDaction.
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
- Back up your project and any
user://save files. - Replace the old plugin folder with the 2.0
addons/calendar_time/folder. - Open and re-save scenes/resources that used World Time / Calendar Time nodes or resources so Godot refreshes Inspector data.
- Update scripts using the rename tables below.
- Add or update a
TimeHostand assign one or moreGameClockresources toTimeHost.clocks. - Route app-level group loads through
time_host.load_state(save_data)so each clock publishesclock_state_loadedafter state restore. - 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— keysday,month,year. Safe: renameclass_nameor@export_range. Needs migration: rename properties.HoursTime— keyshours,minutes,seconds. Same rules.DateTime— nesteddate/timedicts. 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 = trueis equivalent todrive_mode = PROCESS.auto_increment_time = falseis equivalent todrive_mode = MANUAL.drive_mode = PHYSICS_PROCESSreads back asauto_increment_time == truebecause 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
WorldAgeSystemwithAgeService. - Replace
AgeingComponentwithAgeComponent. - Assign the same
GameClockused by the relevant gameplay system. - Bind each
AgeComponent.clockto thatGameClock; components resolve their registry fromclock.age_registry. - Use
clock.age_registryinstead 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.tscn→templates/calendar_time/default_time_host.tscn ageing/ageing_component.gd→ageing/age_component.gdageing/ageing_settings.gd→ageing/age_settings.gdageing/add_when_ageing.gd→ageing/add_when_aged.gdageing/ageing_scene_replacement.gd→ageing/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
GameClockresources and bind them toTimeHost.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.clockto the sameGameClockwhose age registry it should use. - Remove any old
AgeStateRegistry.get_singleton()usage and replace it withclock.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.