Skip to content

Calendar Time v2.1.0

Advanced: Game-Owned Schedules and World Rules

Calendar Time should answer when the game is. Your game should decide what that time means for shops, NPC routines, crops, quests, weather, and other world rules. text TimeHost ↓ drives GameClock ← o

Status
Current
Version
v2.1.0
Source updated
2026-08-09
Generated on
2026-09-01

Calendar Time should answer when the game is. Your game should decide what that time means for shops, NPC routines, crops, quests, weather, and other world rules.

TimeHost
   ↓ drives
GameClock  ← one calendar-time authority
   ↓ exposes date/time events
WorldScheduleService  ← game-owned policy
   ↓ updates
Shop, NPC, crop, quest, and presentation systems

This guide is intentionally advanced. Start with Getting Started if you only need a calendar and clock.

Ownership boundary

Concern Owner
Calendar structure, dates, time-of-day phases GameCalendar
Current calendar instant and clock events GameClock
Engine-to-calendar driving and pause/rate bridge TimeHost
Shop hours, NPC routines, crop rules, quests, weather policy Your game
UI and presentation choices Your game
Simulation tick order and fixed-step loop Your game

A schedule service may store domain state such as shop_open, opening hours, or the last evaluated date. It must not store a second elapsed-time counter or advance a second clock.

Minimal shop-hours example

Copy this small game-owned service into your own project and rename it for your domain. The example uses only the public Calendar Time API:

class_name WorldScheduleService
extends RefCounted

var clock :GameClock
var opening_hour :int = 9
var closing_hour :int = 17
var shop_open :bool = false:
    set(value):
        if shop_open== value:
            return
        shop_open= value
        shop_open_changed.emit(shop_open)

signal shop_open_changed(is_open :bool)

func _init(p_clock :GameClock)-> void:
    clock= p_clock
    clock.signal_bus.date_time_changed.connect(_on_date_time_changed)
    clock.signal_bus.clock_state_loaded.connect(_on_clock_state_loaded)
    refresh()

func _on_date_time_changed(p_new :DateTime, _p_old :DateTime)-> void:
    _refresh(p_new)

func _on_clock_state_loaded()-> void:
    refresh()

func refresh()-> void:
    _refresh(clock.date_time()if clock!= null else null)

func _refresh(p_date_time :DateTime)-> void:
    if p_date_time== null or p_date_time.time== null:
        shop_open= false
        return
    shop_open= p_date_time.time.hours>= opening_hour \\
        and p_date_time.time.hours< closing_hour

The service reacts to the clock's authoritative date_time_changed event. It owns the shop_open decision and emits a game-local signal; Calendar Time does not know that a shop exists.

The repository demo contains the same runnable example at demo/scripts/calendar_time_examples/world_schedule_service.gd, with focused coverage in test/demo/simulation/test_world_schedule_service.gd.

Pause and speed behavior

Do not use Engine.time_scale as the shop's gameplay-time authority. Choose the clock or host API according to the intended scope:

# Pause this clock while menus remain responsive.
clock.speed_multiplier= 0.0

# Resume the same clock.
clock.speed_multiplier= 1.0

# Change the rate for every clock driven by this host.
time_host.time_scale.delta_multiplier= 2.0

When a clock is paused, its calendar does not advance and the schedule does not change. When it resumes, the next canonical clock event reevaluates the rule. For a fixed-step game, set time_host.drive_mode to MANUAL and call time_host.drive_microseconds(step_us) from the game-owned simulation loop.

Use Engine.time_scale only when the whole engine session should change pace, including physics and presentation. It is not a substitute for calendar progression and should not be a second clock.

Save and load

Save Calendar Time through its existing authority:

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

Keep game-owned schedule state in the game save envelope only when it is truly domain state. Do not save a duplicate current time, elapsed counter, or derived shop_open flag. Restore in this order:

  1. Restore the Calendar Time clock through TimeHost.load_state().
  2. Restore any independent game-domain state.
  3. Re-evaluate schedules from clock.date_time() or the next clock_state_loaded callback.

A schedule that subscribes to clock_state_loaded handles a restored instant immediately, without waiting for another tick. Calendar Time's clock snapshot and group serializer remain responsible for clock/calendar state; the game remains responsible for its own save envelope and world policy.

Common mistakes

  • Creating a second WorldClock or incrementing elapsed_seconds in the game.
  • Calling Engine.time_scale to implement a shop's opening hours.
  • Putting shop/NPC/crop concepts into the Calendar Time addon runtime.
  • Persisting shop_open as if it were authoritative time. It is derived state.
  • Running both an automatic TimeHost and a game-owned manual clock driver.
  • Re-deriving day/night from wall-clock hours when the calendar has authored time-of-day phases. Use the clock/calendar contract instead.

For the distinction between Calendar Time, Godot delta, and a game-owned fixed step, see What Should Drive What?. For exact integer driving and pause/rate ownership, see TimeScale & DriveMode.