Files
TheTree_mock/autoload/simulation_runtime.gd
T

13985 lines
622 KiB
GDScript

class_name SimulationRuntimeService
extends Node
signal command_completed(result: CommandResult)
signal snapshot_published(snapshot: Dictionary, digest: String)
# UI subscribers only need notification; exact snapshots remain available.
signal state_updated(snapshot: Dictionary)
signal definition_validation_failed(report: Dictionary)
signal save_operation_completed(result: Dictionary)
# Optional, read-only UI contribution collected at the actual checkpoint.
signal checkpoint_presentation_requested(presentation: Dictionary)
const DEFAULT_WORLD_SEED := 20260827
const STARTER_MANA_OUTPUT := 100
const STARTER_MANA_SOURCE := &"world_tree.mana_source"
const STARTER_MANA_BUS := &"world_tree.mana_bus"
const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH"
## The settlement.funds report definition (see _daily_fund_report).
const FUND_REPORT_VERSION := 2
const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE"
const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE"
const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE"
const WAGE_EXPENSE_ACCOUNT := &"WAGE_EXPENSE"
const RUNTIME_SNAPSHOT_VERSION := 14
const PeepingRooms := preload("res://scripts/special/peeping_room_service.gd")
const AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 14
const EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION := 13
const MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 15
const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd")
const AutoPurchasePlansType := preload("res://scripts/supply/supply_auto_purchase_quote_plans.gd")
const CLEANING_RUNTIME_SNAPSHOT_VERSION := 12
const WEATHER_RUNTIME_SNAPSHOT_VERSION := 11
const FacilityCleanliness = preload("res://scripts/environment/facility_cleanliness_service.gd")
const ManaReserveCycle = preload("res://scripts/mana/mana_reserve_cycle.gd")
const WarpService = preload("res://scripts/transport/warp_service.gd")
const CleaningBridge = preload("res://scripts/environment/facility_cleaning_bridge.gd")
const CleaningPolicy = preload("res://scripts/environment/facility_cleaning_policy.gd")
const UPKEEP_RUNTIME_SNAPSHOT_VERSION := 10
const WeatherTimeline = preload("res://scripts/environment/weather_timeline.gd")
const STAIRS_RUNTIME_SNAPSHOT_VERSION := 9
const TRANSPORT_RUNTIME_SNAPSHOT_VERSION := 8
const SUPPLY_RUNTIME_SNAPSHOT_VERSION := 7
const INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION := 6
const LEGACY_RUNTIME_SNAPSHOT_VERSION := 1
const DEFENSE_RUNTIME_SNAPSHOT_VERSION := 2
const PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION := 3
const CONTENT_RUNTIME_SNAPSHOT_VERSION := 4
const OPERATIONS_RUNTIME_SNAPSHOT_VERSION := 5
const MVP_CONTENT_MANIFEST_VERSION := 2
const OPERATIONS_RUNTIME_WRAPPER_VERSION := 1
const OPERATIONS_START_MINUTE := 360
const OPERATIONS_COORDINATOR_ORIGIN_MINUTE := 359
const MAX_PERSONAL_ACTION_POINTS := 4
const OPERATIONS_TEST_PREPARE_PHASE: StringName = &"PREPARE"
const OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE: StringName = &"AFTER_FIRST_MUTATION"
const DEFAULT_OPERATIONS_DEMAND_PROFILES := [
{&"facility_tag": &"hygiene", &"guest_class": &"cleric", &"time_band": &"MORNING"},
{&"facility_tag": &"service", &"guest_class": &"diplomat", &"time_band": &"MORNING"},
{&"facility_tag": &"lodging", &"guest_class": &"mage", &"time_band": &"DAY"},
{&"facility_tag": &"service", &"guest_class": &"merchant", &"time_band": &"DAY"},
{&"facility_tag": &"lodging", &"guest_class": &"noble", &"time_band": &"EVENING"},
{&"facility_tag": &"dining", &"guest_class": &"warrior", &"time_band": &"EVENING"},
{&"facility_tag": &"leisure", &"guest_class": &"outsider", &"time_band": &"NIGHT"},
{&"facility_tag": &"underground", &"guest_class": &"rogue", &"time_band": &"NIGHT"},
]
const MOVEMENT_CREDIT_SCALE := 1_000
const STAIR_MOVEMENT_CREDIT := 1_500
const MIN_MOVEMENT_CREDIT_PER_MINUTE := 250
const DEFAULT_AUTOSAVE_PATH := "user://saves/autosave"
const SAVE_LIFECYCLE_VERSION := 1
const DEFAULT_SAVE_POLICY := {
&"enabled": false,
&"daily_checkpoint_enabled": true,
&"autosave_path": DEFAULT_AUTOSAVE_PATH,
&"autosave_generations": SaveService.DEFAULT_AUTOSAVE_GENERATIONS,
&"auto_migrate": true,
}
const RNG_STREAM_IDS: Array[StringName] = [
&"world",
&"guest_spawn",
&"guest_decision",
&"staff",
&"relationship",
&"authority",
&"defense",
&"event",
&"loot",
]
const DEFENSE_COMMAND_TYPES: Array[StringName] = [
&"create_defense_raid",
&"register_defense_combatant",
&"register_defense_asset",
&"start_defense_raid",
&"set_defense_retreat_policy",
&"queue_defense_skill",
&"resolve_defense_round",
&"begin_next_defense_wave",
&"retreat_defenders",
&"cancel_defense_raid",
]
var definition_registry := DefinitionRegistry.new()
var command_queue := CommandQueue.new()
var id_generator := StableIdGenerator.new()
var peeping_room_service := PeepingRooms.new()
var domain_events := DomainEventStream.new()
var world_service := WorldService.new()
var path_service := PathService.new()
var mana_network_service := ManaNetworkService.new()
var weather := WeatherTimeline.new()
var facility_cleanliness := FacilityCleanliness.new()
var cleaning_policy := CleaningPolicy.new()
var cleaning_bridge := CleaningBridge.new()
var facility_upkeep := FacilityUpkeepAccrual.new()
var mana_reserve := ManaReserveCycle.new()
var upkeep_settlement := FacilityUpkeepSettlement.new()
var economy_service: EconomyService
var construction_resource_state: LedgerBackedResourceState
var operating_resource_state: LedgerBackedResourceState
var inventory_service := InventoryService.new()
var logistics_dispatcher := LogisticsDispatcher.new()
var logistics_requests: Dictionary = {}
var kitchen_production := KitchenProductionService.new()
var supply_management := SupplyManagementService.new()
var auto_purchase_plans := AutoPurchasePlansType.new()
var supply_logistics := SupplyLogisticsBridge.new()
var elevators := ElevatorService.new()
var legacy_stairs := LegacyStairService.new()
var staff_commute := StaffCommuteService.new()
var facility_service := FacilityService.new()
var guest_service := GuestService.new()
var staff_service := StaffService.new()
var mvp_content_catalog := MvpContentCatalog.new()
var mvp_content_report: Dictionary = {}
# Cache only catalog-derived immutable metadata, never GuestDefinition objects.
var _mvp_manifest_cache_catalog: MvpContentCatalog
var _mvp_manifest_cache_generation := -1
var _mvp_manifest_cache: Dictionary = {}
var progression_service := ProgressionService.new()
var operations_coordinator: OperationsCoordinator
var relationship_service := RelationshipService.new()
var event_service := EventService.new()
var authority_service := AuthorityService.new()
var defense_service := DefenseService.new()
var construction_service: ConstructionService
var excavation_service: ExcavationService
var spatial_subslot_service: SpatialSubslotService
var route_subslot_path_adapter: RouteSubslotPathAdapter
var facility_infrastructure_projector: FacilityInfrastructureProjector
var latest_snapshot: Dictionary = {}
var _latest_digest := ""
var _latest_digest_dirty := false
var latest_digest: String:
get:
if _latest_digest_dirty:
_latest_digest = SimulationDigest.compute(latest_snapshot)
_latest_digest_dirty = false
return _latest_digest
var latest_save_operation_result: Dictionary = {}
var latest_restore_safety_report: Dictionary = {}
var world_seed := DEFAULT_WORLD_SEED
var save_migration_registry := SaveMigrationRegistry.new()
var _clock: SimulationKernel
var _state: Variant
var _rng_streams: Dictionary = {}
var _executors: Dictionary = {}
var _next_command_id := 1
var _staffing_projection_enabled := false
var _movement_credits: Dictionary = {}
var _hired_staff_candidate_ids: Dictionary = {}
var _save_policy: Dictionary = DEFAULT_SAVE_POLICY.duplicate(true)
var _last_daily_checkpoint_day := -1
var _pending_checkpoints: Array[Dictionary] = []
var _next_checkpoint_request_id := 1
var _initialized := false
var _active_command_minute := -1
var _operations_enabled := false
# Transient in-tick execution guards. Persisted timing lives inside Defense.
var _defense_round_attempt_minute := -1
var _defense_round_attempted_raids: Dictionary = {}
var _defense_automatic_failed_minute := -1
var _operations_tick_blocked := false
var _operations_runtime_failure_latched := false
var _operations_runtime_failure_report: Dictionary = {}
var _operations_deferred_runtime_minute := -1
var _operations_resume_publish_pending := false
var _personal_action_points := 0
var _operations_test_boundary_hooks: Dictionary = {}
var _operations_transaction_active := false
var _operations_queued_domain_events: Array[SimulationDomainEvent] = []
var _operations_queued_guest_spawn_notifications: Array[StringName] = []
var _operations_flushing_guest_spawn_notifications: Dictionary = {}
var _operations_queued_economy_notifications: Array[StringName] = []
var _operations_flushing_economy_notifications: Dictionary = {}
var _infrastructure_failure_latched := false
var _infrastructure_failure_report: Dictionary = {}
var _construction_completion_transaction_active := false
var _construction_committing_notifications := false
var _construction_queued_domain_events: Array[SimulationDomainEvent] = []
func _ready() -> void:
# The autoload is also present in headless test SceneTrees, so merely entering
# the tree must never opt into disk writes. The playable main scene enables
# the production policy explicitly.
if not _initialized:
initialize(GameClock, GameState, DEFAULT_WORLD_SEED)
func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_SEED) -> Dictionary:
if _initialized and _clock != null:
_clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather")
_clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep")
_clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.operations")
_clock.unregister_tick_handler(_clock.TickPhase.APPLY_COMMANDS, &"simulation_runtime.commands")
_clock.unregister_tick_handler(_clock.TickPhase.PUBLISH_SNAPSHOT, &"simulation_runtime.snapshot")
_clock.unregister_tick_handler(_clock.TickPhase.CONSTRUCTION, &"simulation_runtime.construction")
_clock.unregister_tick_handler(_clock.TickPhase.MANA, &"simulation_runtime.mana")
_clock.unregister_tick_handler(_clock.TickPhase.LOGISTICS, &"simulation_runtime.logistics")
_clock.unregister_tick_handler(_clock.TickPhase.FACILITY_COMPLETION, &"simulation_runtime.facilities")
_clock.unregister_tick_handler(_clock.TickPhase.VITALS_AND_ENVIRONMENT, &"simulation_runtime.guest_vitals")
_clock.unregister_tick_handler(_clock.TickPhase.FACILITY_QUEUES, &"simulation_runtime.guest_queues")
_clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.authority_incidents")
_clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.defense")
_clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.relationship_events")
_clock.unregister_tick_handler(_clock.TickPhase.AI_DECISIONS, &"simulation_runtime.staff_ai")
_clock.unregister_tick_handler(_clock.TickPhase.PUBLISH_SNAPSHOT, &"simulation_runtime.zz_save_checkpoints")
_detach_defense_service(defense_service)
_clear_mvp_content_manifest_cache()
_clock = clock
_state = state
world_seed = seed
mvp_content_catalog = MvpContentCatalog.new()
mvp_content_report = mvp_content_catalog.rebuild()
progression_service = ProgressionService.new()
var initial_progression_ok := progression_service.configure(1, {
&"asset_gold": maxi(int(_state.get_resource(&"gold")), 0),
&"faith_lifetime": maxi(int(_state.get_resource(&"faith")), 0),
})
_hired_staff_candidate_ids.clear()
command_queue = CommandQueue.new()
id_generator = StableIdGenerator.new()
peeping_room_service = PeepingRooms.new()
domain_events = DomainEventStream.new()
world_service = WorldService.new()
path_service = PathService.new()
path_service.set_vertical_walk_requires_connector(true)
legacy_stairs = LegacyStairService.new()
legacy_stairs.configure(path_service)
_movement_credits.clear()
_connect_path_performance_signal()
mana_network_service = ManaNetworkService.new()
_initialize_world(maxi(int(_state.world_tree_rank), 1))
_initialize_mana_network()
_initialize_economy()
_initialize_s2_operations()
_staffing_projection_enabled = false
_initialize_s3_actors()
_initialize_s4_relationship_events()
_initialize_s5_authority()
_initialize_s5_defense()
construction_service = ConstructionService.new(world_service, construction_resource_state)
construction_service.current_minute = _clock.absolute_minute
peeping_room_service.configure(world_service, facility_service, construction_service, path_service, id_generator)
_connect_construction_signals()
var initial_infrastructure_ok := _initialize_infrastructure_services()
_next_command_id = 1
_last_daily_checkpoint_day = -1
_pending_checkpoints.clear()
_next_checkpoint_request_id = 1
_active_command_minute = -1
operations_coordinator = null
_operations_enabled = false
_operations_tick_blocked = false
_operations_runtime_failure_latched = false
_operations_runtime_failure_report.clear()
_operations_deferred_runtime_minute = -1
_operations_resume_publish_pending = false
_personal_action_points = 0
_operations_test_boundary_hooks.clear()
_operations_transaction_active = false
_operations_queued_domain_events.clear()
_operations_queued_guest_spawn_notifications.clear()
_operations_flushing_guest_spawn_notifications.clear()
_operations_queued_economy_notifications.clear()
_operations_flushing_economy_notifications.clear()
_infrastructure_failure_latched = false
_infrastructure_failure_report.clear()
_construction_completion_transaction_active = false
_construction_committing_notifications = false
_construction_queued_domain_events.clear()
latest_save_operation_result.clear()
latest_restore_safety_report.clear()
_initialize_rng_streams()
supply_logistics = SupplyLogisticsBridge.new()
supply_logistics.configure(self)
cleaning_bridge.configure(self)
elevators = ElevatorService.new()
elevators.configure(path_service)
elevators.synchronize([], _clock.absolute_minute)
staff_commute = StaffCommuteService.new()
staff_commute.configure(self)
_executors.clear()
register_command_executor(&"change_resources", Callable(self, "_execute_change_resources"))
register_command_executor(&"spend_resources", Callable(self, "_execute_spend_resources"))
register_command_executor(&"repay_loan_principal", Callable(self, "_execute_repay_loan_principal"))
register_command_executor(&"build_facility", Callable(self, "_execute_build_facility"))
register_command_executor(&"cancel_construction", Callable(self, "_execute_cancel_construction"))
register_command_executor(&"demolish_facility", Callable(self, "_execute_demolish_facility"))
for command_type: StringName in [&"peeping_room_validate", &"peeping_room_place", &"peeping_room_relink", &"peeping_room_record_evidence"]:
register_command_executor(command_type, Callable(self, "_execute_peeping_room"))
register_command_executor(&"set_elevator_operation", Callable(self, "_execute_set_elevator_operation"))
register_command_executor(&"start_excavation", Callable(self, "_execute_start_excavation"))
register_command_executor(&"pause_excavation", Callable(self, "_execute_pause_excavation"))
register_command_executor(&"resume_excavation", Callable(self, "_execute_resume_excavation"))
register_command_executor(&"set_world_tree_rank", Callable(self, "_execute_set_world_tree_rank"))
register_command_executor(&"update_progression_metrics", Callable(self, "_execute_update_progression_metrics"))
register_command_executor(&"complete_progression_quest", Callable(self, "_execute_complete_progression_quest"))
register_command_executor(&"acknowledge_operations_pause", Callable(self, "_execute_acknowledge_operations_pause"))
register_command_executor(&"purchase_supplies", Callable(self, "_execute_purchase_supplies"))
register_command_executor(&"set_supply_auto_purchase", Callable(self, "_execute_set_supply_auto_purchase"))
register_command_executor(&"set_kitchen_production", Callable(self, "_execute_set_kitchen_production"))
register_command_executor(&"add_inventory_stack", Callable(self, "_execute_add_inventory_stack"))
register_command_executor(&"create_logistics_request", Callable(self, "_execute_create_logistics_request"))
register_command_executor(&"dispatch_logistics", Callable(self, "_execute_dispatch_logistics"))
register_command_executor(&"advance_logistics_request", Callable(self, "_execute_advance_logistics_request"))
register_command_executor(&"register_guest_wallet", Callable(self, "_execute_register_guest_wallet"))
register_command_executor(&"create_service_request", Callable(self, "_execute_create_service_request"))
register_command_executor(&"set_facility_conditions", Callable(self, "_execute_set_facility_conditions"))
register_command_executor(&"set_facility_policy", Callable(self, "_execute_set_facility_policy"))
register_command_executor(&"leave_facility_queue", Callable(self, "_execute_leave_facility_queue"))
register_command_executor(&"spawn_guest", Callable(self, "_execute_spawn_guest"))
register_command_executor(&"transition_guest", Callable(self, "_execute_transition_guest"))
register_command_executor(&"decide_guest", Callable(self, "_execute_decide_guest"))
register_command_executor(&"register_staff", Callable(self, "_execute_register_staff"))
register_command_executor(&"hire_staff_candidate", Callable(self, "_execute_hire_staff_candidate"))
register_command_executor(&"create_staff_shift", Callable(self, "_execute_create_staff_shift"))
register_command_executor(&"set_staff_schedule_override", Callable(self, "_execute_set_staff_schedule_override"))
register_command_executor(&"set_staff_work_assignment", Callable(self, "_execute_set_staff_work_assignment"))
register_command_executor(&"create_staff_task", Callable(self, "_execute_create_staff_task"))
register_command_executor(&"begin_staff_task", Callable(self, "_execute_begin_staff_task"))
register_command_executor(&"progress_staff_task", Callable(self, "_execute_progress_staff_task"))
register_command_executor(&"create_relationship", Callable(self, "_execute_create_relationship"))
register_command_executor(&"register_original_relationship", Callable(self, "_execute_register_original_relationship"))
register_command_executor(&"change_relationship", Callable(self, "_execute_change_relationship"))
register_command_executor(&"attack_relationship_shield", Callable(self, "_execute_attack_relationship_shield"))
register_command_executor(&"request_route_lock", Callable(self, "_execute_request_route_lock"))
register_command_executor(&"confirm_route_lock", Callable(self, "_execute_confirm_route_lock"))
register_command_executor(&"register_event_definition", Callable(self, "_execute_register_event_definition"))
register_command_executor(&"enqueue_event", Callable(self, "_execute_enqueue_event"))
register_command_executor(&"reserve_event_choice_cost", Callable(self, "_execute_reserve_event_choice_cost"))
register_command_executor(&"submit_event_choice", Callable(self, "_execute_submit_event_choice"))
register_command_executor(&"report_authority_incident", Callable(self, "_execute_report_authority_incident"))
register_command_executor(&"register_authority_evidence", Callable(self, "_execute_register_authority_evidence"))
register_command_executor(&"discover_authority_evidence", Callable(self, "_execute_discover_authority_evidence"))
register_command_executor(&"evaluate_authority_inspection", Callable(self, "_execute_evaluate_authority_inspection"))
register_command_executor(&"schedule_authority_inspection", Callable(self, "_execute_schedule_authority_inspection"))
register_command_executor(&"start_authority_inspection", Callable(self, "_execute_start_authority_inspection"))
register_command_executor(&"arrive_authority_inspection", Callable(self, "_execute_arrive_authority_inspection"))
register_command_executor(&"plan_authority_inspection", Callable(self, "_execute_plan_authority_inspection"))
register_command_executor(&"advance_authority_inspection", Callable(self, "_execute_advance_authority_inspection"))
register_command_executor(&"inspect_authority_waypoint", Callable(self, "_execute_inspect_authority_waypoint"))
register_command_executor(&"resolve_authority_inspection", Callable(self, "_execute_resolve_authority_inspection"))
register_command_executor(&"depart_authority_inspection", Callable(self, "_execute_depart_authority_inspection"))
register_command_executor(&"create_defense_raid", Callable(self, "_execute_create_defense_raid"))
register_command_executor(&"register_defense_combatant", Callable(self, "_execute_register_defense_combatant"))
register_command_executor(&"register_defense_asset", Callable(self, "_execute_register_defense_asset"))
register_command_executor(&"start_defense_raid", Callable(self, "_execute_start_defense_raid"))
register_command_executor(&"set_defense_retreat_policy", Callable(self, "_execute_set_defense_retreat_policy"))
register_command_executor(&"queue_defense_skill", Callable(self, "_execute_queue_defense_skill"))
register_command_executor(&"resolve_defense_round", Callable(self, "_execute_resolve_defense_round"))
register_command_executor(&"begin_next_defense_wave", Callable(self, "_execute_begin_next_defense_wave"))
register_command_executor(&"retreat_defenders", Callable(self, "_execute_retreat_defenders"))
register_command_executor(&"cancel_defense_raid", Callable(self, "_execute_cancel_defense_raid"))
# Weather commits before upkeep and arrivals, including same-minute retries.
_clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather")
_clock.register_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather", Callable(self, "_advance_weather"))
_clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep")
_clock.register_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep", Callable(self, "_advance_facility_upkeep"))
_clock.register_tick_handler(
_clock.TickPhase.APPLY_COMMANDS,
&"simulation_runtime.commands",
Callable(self, "_process_tick_commands")
)
_clock.register_tick_handler(
_clock.TickPhase.PUBLISH_SNAPSHOT,
&"simulation_runtime.snapshot",
Callable(self, "_publish_snapshot")
)
_clock.register_tick_handler(
_clock.TickPhase.MANA,
&"simulation_runtime.mana",
Callable(self, "_advance_mana")
)
_clock.register_tick_handler(
_clock.TickPhase.CONSTRUCTION,
&"simulation_runtime.construction",
Callable(self, "_advance_construction")
)
_clock.register_tick_handler(
_clock.TickPhase.LOGISTICS,
&"simulation_runtime.logistics",
Callable(self, "_advance_inventory_and_logistics")
)
_clock.register_tick_handler(
_clock.TickPhase.FACILITY_COMPLETION,
&"simulation_runtime.facilities",
Callable(self, "_advance_facilities")
)
_clock.register_tick_handler(
_clock.TickPhase.VITALS_AND_ENVIRONMENT,
&"simulation_runtime.guest_vitals",
Callable(self, "_advance_guest_vitals")
)
_clock.register_tick_handler(
_clock.TickPhase.FACILITY_QUEUES,
&"simulation_runtime.guest_queues",
Callable(self, "_advance_guest_queues")
)
_clock.register_tick_handler(
_clock.TickPhase.INCIDENTS_AND_EVENTS,
&"simulation_runtime.authority_incidents",
Callable(self, "_advance_authority_incidents")
)
_clock.register_tick_handler(
_clock.TickPhase.INCIDENTS_AND_EVENTS,
&"simulation_runtime.defense",
Callable(self, "_advance_defense")
)
_clock.register_tick_handler(
_clock.TickPhase.INCIDENTS_AND_EVENTS,
&"simulation_runtime.relationship_events",
Callable(self, "_advance_relationship_events")
)
_clock.register_tick_handler(
_clock.TickPhase.AI_DECISIONS,
&"simulation_runtime.staff_ai",
Callable(self, "_advance_staff_ai")
)
# The `zz` ID deliberately orders persistence after the published simulation
# snapshot at the final phase of a fully completed minute.
_clock.register_tick_handler(
_clock.TickPhase.PUBLISH_SNAPSHOT,
&"simulation_runtime.zz_save_checkpoints",
Callable(self, "_process_save_checkpoints")
)
_initialized = true
var report := definition_registry.rebuild_from_paths(
FacilityCatalog.FACILITY_PATHS + FacilityCatalog.INFRASTRUCTURE_FACILITY_PATHS + FacilityCatalog.RESCUE_FACILITY_PATHS + FacilityCatalog.ORIGINAL_FACILITY_PATHS + FacilityCatalog.TRANSPORT_FACILITY_PATHS)
var catalog_registration_ok := _catalog_definitions_are_registered(guest_service)
report[&"content_catalog"] = mvp_content_report.duplicate(true)
report[&"progression_initialized"] = initial_progression_ok
report[&"catalog_guest_definitions_registered"] = catalog_registration_ok
report[&"infrastructure_initialized"] = initial_infrastructure_ok
report[&"cleaning_policy_valid"] = not cleaning_policy.create_snapshot().is_empty()
report[&"valid"] = bool(report[&"valid"]) \
and bool(mvp_content_report.get(&"valid", false)) \
and catalog_registration_ok \
and initial_progression_ok \
and initial_infrastructure_ok \
and bool(report[&"cleaning_policy_valid"]) \
and int(_state.world_tree_rank) == ProgressionService.MIN_RANK
if not report[&"valid"]:
_clock.set_paused(true)
definition_validation_failed.emit(report)
_publish_snapshot(_clock.absolute_minute)
return report
func register_command_executor(command_type: StringName, executor: Callable) -> bool:
if command_type.is_empty() or not executor.is_valid() or _executors.has(command_type):
return false
_executors[command_type] = executor
return true
func unregister_command_executor(command_type: StringName) -> void:
_executors.erase(command_type)
func submit_command(
command_type: StringName,
payload: Dictionary = {},
actor_id: StringName = &"player",
target_id: StringName = &"",
priority := 0,
execute_at := -1,
idempotency_key: StringName = &""
) -> CommandResult:
if not _initialized:
return CommandResult.failed(CommandResult.Code.EXECUTOR_FAILED, 0, "Simulation runtime is not initialized.")
if _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED, 0,
"ゲーム終了後は操作できません。帳簿の確認・保存・読み込みは利用できます。",
{&"reason": &"BANKRUPT"}
)
var scheduled_tick := _clock.absolute_minute if execute_at < 0 else execute_at
var command := SimulationCommand.new(
_next_command_id,
_clock.absolute_minute,
scheduled_tick,
priority,
command_type,
actor_id,
target_id,
payload,
idempotency_key
)
var enqueue_result := command_queue.enqueue(command)
if not enqueue_result.is_success():
return enqueue_result
_next_command_id += 1
if _clock.paused and scheduled_tick <= _clock.absolute_minute:
var resume_snapshot_published := _process_commands(_clock.absolute_minute)
if not resume_snapshot_published:
_publish_snapshot(_clock.absolute_minute)
var completed := command_queue.get_processed_result(command.command_id)
if completed != null:
return completed
return enqueue_result
func get_rng_stream(stream_id: StringName) -> DeterministicRng:
return _rng_streams.get(stream_id) as DeterministicRng
func create_snapshot() -> Dictionary:
var rng_data := {}
for stream_id: StringName in _sorted_name_keys(_rng_streams):
var stream: DeterministicRng = _rng_streams[stream_id]
rng_data[String(stream_id)] = stream.snapshot()
var owned_systems := {
&"runtime": {
&"runtime_schema_version": RUNTIME_SNAPSHOT_VERSION,
&"weather": weather.create_snapshot(),
&"world_seed": world_seed,
&"next_command_id": _next_command_id,
&"staffing_projection_enabled": _staffing_projection_enabled,
&"save_lifecycle": {
&"version": SAVE_LIFECYCLE_VERSION,
&"last_daily_checkpoint_day": _last_daily_checkpoint_day,
},
&"command_queue": command_queue.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"domain_events": domain_events.create_snapshot(),
&"rng_streams": rng_data,
&"world": world_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"movement_credits": _serialize_movement_credits(),
&"mana": mana_network_service.create_snapshot(),
&"mana_reserve": mana_reserve.create_snapshot(),
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"inventory": inventory_service.create_snapshot(),
&"kitchen_production": kitchen_production.create_snapshot(),
&"auto_purchase_plans": auto_purchase_plans.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"logistics_dispatcher": logistics_dispatcher.create_snapshot(),
&"logistics_requests": _serialize_logistics_requests(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"guests": guest_service.create_snapshot(),
&"staff": staff_service.create_snapshot(),
&"catalog_manifest": _create_mvp_content_manifest(mvp_content_catalog),
&"progression": progression_service.create_snapshot(),
&"hired_staff_candidate_ids": _serialize_hired_staff_candidate_ids(),
&"operations": _create_operations_runtime_snapshot(),
&"relationships": relationship_service.create_snapshot(),
&"event_definitions": _serialize_event_definitions(),
&"events": event_service.create_snapshot(),
&"authority": authority_service.create_snapshot(),
&"defense": defense_service.create_snapshot(),
&"construction": construction_service.create_snapshot(),
&"excavation": excavation_service.create_snapshot(),
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
&"route_projection": route_subslot_path_adapter.create_snapshot(),
&"facility_infrastructure": facility_infrastructure_projector.create_snapshot(),
},
}
if PurchaseQuoteV2.has_history(owned_systems[&"runtime"]):
owned_systems[&"runtime"][&"runtime_schema_version"] = MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION
if not peeping_room_service.is_empty():
owned_systems[&"runtime"][&"special_peeping_rooms"] = peeping_room_service.create_snapshot()
# Every component above returns detached containers. Transfer this fresh
# tree into the envelope instead of deep-copying all component snapshots a
# second time. Keep generic capture's caller-owned-extra contract unchanged.
var snapshot := SimulationSnapshot.capture(_clock, _state)
snapshot[&"systems"] = owned_systems
return snapshot
func restore_snapshot(snapshot: Dictionary) -> bool:
return _restore_snapshot_internal(snapshot, false)
func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bool:
var active_filter_profile := event_service.get_content_filter_profile()
var staff_repair_report := {
&"automatic_repairs_applied": false,
&"reopened_task_ids": [],
&"released_staff_ids": [],
&"release_failed_task_ids": [],
}
var event_lock_repair_report := {
&"automatic_repairs_applied": false,
&"released_actor_ids": [],
&"released_foreground_instance_id": &"",
&"reason": &"NONE",
}
var validation := SimulationSnapshot.validate(snapshot)
if not validation[&"ok"]:
return false
var systems: Variant = snapshot.get(&"systems", {})
if not systems is Dictionary or not systems.has(&"runtime") or not systems[&"runtime"] is Dictionary:
return false
var runtime_data: Dictionary = systems[&"runtime"]
var restored_minute := int(snapshot[&"clock"].get(&"absolute_minute", -1))
if restored_minute < 0:
return false
var runtime_schema_version_value: Variant = runtime_data.get(
&"runtime_schema_version",
runtime_data.get("runtime_schema_version", LEGACY_RUNTIME_SNAPSHOT_VERSION)
)
if typeof(runtime_schema_version_value) != TYPE_INT:
return false
var runtime_schema_version := int(runtime_schema_version_value)
if runtime_schema_version not in [
LEGACY_RUNTIME_SNAPSHOT_VERSION,
DEFENSE_RUNTIME_SNAPSHOT_VERSION,
PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION,
CONTENT_RUNTIME_SNAPSHOT_VERSION,
OPERATIONS_RUNTIME_SNAPSHOT_VERSION,
INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION,
SUPPLY_RUNTIME_SNAPSHOT_VERSION,
TRANSPORT_RUNTIME_SNAPSHOT_VERSION,
STAIRS_RUNTIME_SNAPSHOT_VERSION,
UPKEEP_RUNTIME_SNAPSHOT_VERSION,
WEATHER_RUNTIME_SNAPSHOT_VERSION,
CLEANING_RUNTIME_SNAPSHOT_VERSION,
EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION,
RUNTIME_SNAPSHOT_VERSION,
MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION,
]:
return false
var has_purchase_v2_history := PurchaseQuoteV2.has_history(runtime_data)
if (runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION) != has_purchase_v2_history:
return false
if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION \
and not _legacy_operations_history_is_absent(runtime_data):
return false
if runtime_schema_version < INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION \
and not _legacy_infrastructure_history_is_absent(runtime_data):
return false
if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION:
if not _runtime_v5_outer_snapshot_shape_is_valid(snapshot):
return false
var runtime_current_keys: Array[StringName] = [
&"runtime_schema_version", &"world_seed", &"next_command_id",
&"staffing_projection_enabled", &"save_lifecycle", &"command_queue",
&"id_generator", &"domain_events", &"rng_streams", &"world", &"path",
&"movement_credits", &"mana", &"economy", &"inventory",
&"logistics_dispatcher", &"logistics_requests", &"facilities", &"guests",
&"staff", &"catalog_manifest", &"progression", &"hired_staff_candidate_ids",
&"operations", &"relationships", &"event_definitions", &"events",
&"authority", &"defense", &"construction",
]
if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append_array([
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure",
])
if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"kitchen_production")
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"elevators")
if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"legacy_stairs")
if runtime_schema_version >= UPKEEP_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append_array([&"facility_upkeep", &"upkeep_settlement"])
if runtime_schema_version >= WEATHER_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"weather")
if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"facility_cleanliness")
if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION:
runtime_current_keys.append(&"auto_purchase_plans")
if runtime_data.has(&"special_peeping_rooms"):
runtime_current_keys.append(&"special_peeping_rooms")
if runtime_data.has(&"mana_reserve"):
runtime_current_keys.append(&"mana_reserve")
if not _dictionary_has_exact_canonical_keys(runtime_data, runtime_current_keys) \
or typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \
or typeof(runtime_data.get(&"world_seed", null)) != TYPE_INT \
or typeof(runtime_data.get(&"next_command_id", null)) != TYPE_INT \
or typeof(runtime_data.get(&"staffing_projection_enabled", null)) != TYPE_BOOL:
return false
var has_defense_snapshot := runtime_data.has(&"defense") or runtime_data.has("defense")
if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION and not has_defense_snapshot:
return false
if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \
and not runtime_data.get(&"movement_credits", null) is Array:
return false
if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION \
and (not runtime_data.get(&"catalog_manifest", null) is Dictionary \
or not runtime_data.get(&"progression", null) is Dictionary \
or not runtime_data.get(&"hired_staff_candidate_ids", null) is Array):
return false
if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \
and not runtime_data.get(&"operations", null) is Dictionary:
return false
if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION:
for infrastructure_key: StringName in [
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure",
]:
if not runtime_data.get(infrastructure_key, null) is Dictionary:
return false
if runtime_schema_version < SUPPLY_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"kitchen_production"):
return false
if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"kitchen_production", null) is Dictionary:
return false
if runtime_schema_version < TRANSPORT_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"elevators"):
return false
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"elevators", null) is Dictionary:
return false
if runtime_schema_version < STAIRS_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"legacy_stairs"):
return false
if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"legacy_stairs", null) is Dictionary:
return false
if runtime_schema_version < AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"auto_purchase_plans"):
return false
if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"auto_purchase_plans", null) is Dictionary:
return false
var restored_catalog := MvpContentCatalog.new()
var restored_catalog_report := restored_catalog.rebuild()
if not bool(restored_catalog_report.get(&"valid", false)):
return false
if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION \
and not _saved_content_manifest_matches(restored_catalog, runtime_data):
return false
var restored_last_daily_checkpoint_day := -1
var save_lifecycle_value: Variant = runtime_data.get(&"save_lifecycle", {})
if not save_lifecycle_value is Dictionary:
return false
var save_lifecycle: Dictionary = save_lifecycle_value
if not save_lifecycle.is_empty():
if (runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \
and (not _dictionary_has_exact_canonical_keys(
save_lifecycle, [&"version", &"last_daily_checkpoint_day"]
) \
or typeof(save_lifecycle.get(&"version", null)) != TYPE_INT \
or typeof(save_lifecycle.get(&"last_daily_checkpoint_day", null)) != TYPE_INT)) \
or int(save_lifecycle.get(&"version", -1)) != SAVE_LIFECYCLE_VERSION:
return false
restored_last_daily_checkpoint_day = int(
save_lifecycle.get(&"last_daily_checkpoint_day", -1)
)
if restored_last_daily_checkpoint_day < -1:
return false
if not runtime_data.get(&"command_queue", null) is Dictionary \
or not runtime_data.get(&"id_generator", null) is Dictionary \
or not runtime_data.get(&"domain_events", null) is Dictionary \
or not runtime_data.get(&"rng_streams", null) is Dictionary \
or not runtime_data.get(&"world", null) is Dictionary \
or not runtime_data.get(&"path", null) is Dictionary \
or not runtime_data.get(&"mana", null) is Dictionary \
or not runtime_data.get(&"economy", null) is Dictionary \
or not runtime_data.get(&"inventory", null) is Dictionary \
or not runtime_data.get(&"logistics_dispatcher", null) is Dictionary \
or not runtime_data.get(&"logistics_requests", null) is Array \
or not runtime_data.get(&"facilities", null) is Dictionary \
or not runtime_data.get(&"guests", null) is Dictionary \
or not runtime_data.get(&"staff", null) is Dictionary \
or not runtime_data.get(&"relationships", null) is Dictionary \
or not runtime_data.get(&"event_definitions", null) is Array \
or not runtime_data.get(&"events", null) is Dictionary \
or not runtime_data.get(&"authority", null) is Dictionary \
or (has_defense_snapshot and not runtime_data.get(&"defense", null) is Dictionary) \
or not runtime_data.get(&"construction", null) is Dictionary:
return false
var restored_queue := CommandQueue.new()
if not restored_queue.restore_snapshot(runtime_data[&"command_queue"]):
return false
if runtime_schema_version < AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION:
# Old saves must not reinterpret an unsupported queued action as a new
# paid feature when loading into this release. Failed historical actions
# remain ordinary recorded history; only resumable pending work is gated.
for row: Dictionary in restored_queue.create_snapshot()["pending"]:
if row["command_type"] == "set_supply_auto_purchase":
return false
var restored_id_generator := StableIdGenerator.new()
if not restored_id_generator.restore_snapshot(runtime_data[&"id_generator"]):
return false
var restored_domain_events := DomainEventStream.new()
if not restored_domain_events.restore_snapshot(runtime_data[&"domain_events"]):
return false
if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \
and not _runtime_command_history_crosslinks_are_valid(
runtime_data[&"command_queue"],
int(runtime_data[&"next_command_id"]),
restored_domain_events
):
return false
var restored_world := WorldService.new()
if not restored_world.restore_snapshot(runtime_data[&"world"]):
return false
var restored_path := PathService.new()
var restored_spatial := SpatialSubslotService.new()
var restored_route_adapter := RouteSubslotPathAdapter.new()
var restored_infrastructure_projector := FacilityInfrastructureProjector.new()
if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION:
# Configure against an empty Spatial source before any managed namespace is
# present. Then restore the source, rebuild the base+ROUTE topology, and
# finally restore Path traffic/actors and the two metadata owners.
if not restored_path.rebuild_graph(restored_world.get_walkable_cells()) \
or not restored_route_adapter.configure(
restored_spatial, restored_path, restored_world.get_walkable_cells()
) \
or not restored_infrastructure_projector.configure(
restored_route_adapter, restored_spatial, restored_path
) \
or not restored_spatial.restore_snapshot(runtime_data[&"spatial_subslots"]):
return false
var prepared_projection := restored_route_adapter.synchronize_projection(
&"runtime.v6.restore.prepare",
restored_world.get_walkable_cells()
)
if not prepared_projection.is_success() \
or not _path_snapshot_cells_match_world(
runtime_data[&"path"],
restored_route_adapter.get_projected_walkable_cells()
) \
or not restored_path.restore_snapshot(runtime_data[&"path"]) \
or not restored_route_adapter.restore_snapshot(runtime_data[&"route_projection"]) \
or not restored_infrastructure_projector.restore_snapshot(
runtime_data[&"facility_infrastructure"]
):
return false
else:
if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \
and not _path_snapshot_cells_match_world(
runtime_data[&"path"],
restored_world.get_walkable_cells()
):
return false
if not restored_path.restore_snapshot(runtime_data[&"path"]) \
or not restored_path.rebuild_graph(restored_world.get_walkable_cells()) \
or not restored_route_adapter.configure(
restored_spatial, restored_path, restored_world.get_walkable_cells()
) \
or not restored_infrastructure_projector.configure(
restored_route_adapter, restored_spatial, restored_path
):
return false
if runtime_schema_version < STAIRS_RUNTIME_SNAPSHOT_VERSION:
var legacy_path_data: Dictionary = runtime_data[&"path"]
if legacy_path_data.has(&"vertical_walk_requires_connector") or legacy_path_data.has(&"transit_actor_ids"):
return false
var restored_legacy_stairs := LegacyStairService.new()
restored_legacy_stairs.configure(restored_path)
if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION:
if runtime_data[&"path"].get(&"schema_version", -1) != PathService.SNAPSHOT_VERSION \
or not restored_path.get_vertical_walk_requires_connector() \
or not restored_legacy_stairs.restore_snapshot(runtime_data[&"legacy_stairs"]):
return false
else:
if restored_path.get_vertical_walk_requires_connector() or not restored_legacy_stairs.migrate_existing_vertical_walks():
return false
var restored_mana := ManaNetworkService.new()
if not restored_mana.restore_snapshot(runtime_data[&"mana"]):
return false
# A06: saves without the finite reserve keep an explicit stored amount
# (0 stays stopped) only within capacity; nothing is silently truncated.
var restored_mana_reserve := ManaReserveCycle.new()
var saved_game_resources: Variant = snapshot[&"game_state"].get(&"resources", null)
if not saved_game_resources is Dictionary:
return false
var saved_mana: Variant = saved_game_resources.get(&"mana", saved_game_resources.get("mana", null))
if runtime_data.has(&"mana_reserve"):
# Between boundaries Mana may exceed capacity; the next boundary records
# and discards the excess, so only a valid integer amount is required.
if not ManaReserveCycle.stored_amount_is_valid(saved_mana) \
or not restored_mana_reserve.restore_snapshot(runtime_data[&"mana_reserve"]) \
or restored_mana_reserve.get_minute() > restored_minute:
return false
elif not ManaReserveCycle.amount_is_valid(saved_mana) \
or not restored_mana_reserve.start_at(restored_minute):
return false
var restored_economy := EconomyService.new()
if not restored_economy.restore_snapshot(runtime_data[&"economy"]):
return false
if not _loan_principal_ledger_is_valid(restored_economy):
return false
if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION:
if not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT) \
and not restored_economy.register_account(WAGE_EXPENSE_ACCOUNT):
return false
elif not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT):
return false
var restored_upkeep := FacilityUpkeepAccrual.new()
var restored_upkeep_settlement := FacilityUpkeepSettlement.new()
if runtime_schema_version >= UPKEEP_RUNTIME_SNAPSHOT_VERSION:
if not runtime_data.get(&"facility_upkeep") is Dictionary or not runtime_data.get(&"upkeep_settlement") is Dictionary:
return false
if not restored_upkeep.restore_snapshot(runtime_data[&"facility_upkeep"]) \
or restored_upkeep.get_current_minute() != restored_minute \
or not restored_upkeep_settlement.restore_snapshot(runtime_data[&"upkeep_settlement"], restored_economy):
return false
if not _upkeep_accrual_crosslinks_are_valid(restored_upkeep, restored_upkeep_settlement):
return false
else:
if runtime_data.has(&"facility_upkeep") or runtime_data.has(&"upkeep_settlement") \
or restored_economy.has_account(FacilityUpkeepSettlement.EXPENSE) \
or restored_economy.has_account(FacilityUpkeepSettlement.PAYABLE):
return false
if not FacilityUpkeepSettlement.prepare_accounts(restored_economy) or not restored_upkeep.start_at(restored_minute)[&"ok"]:
return false
# A12: a save made before the funds were split reads every Gold as front funds.
if not FinancePolicy.prepare_accounts(restored_economy):
return false
var saved_resources: Variant = snapshot[&"game_state"].get(&"resources", null)
if not saved_resources is Dictionary \
or int(saved_resources.get(&"gold", saved_resources.get("gold", -1))) \
!= restored_economy.get_balance(PLAYER_CASH_ACCOUNT):
return false
var restored_inventory := InventoryService.new()
if not restored_inventory.restore_snapshot(runtime_data[&"inventory"]):
return false
var restored_dispatcher := LogisticsDispatcher.new()
if not restored_dispatcher.restore_snapshot(runtime_data[&"logistics_dispatcher"]):
return false
var restored_logistics: Variant = _deserialize_logistics_requests(runtime_data[&"logistics_requests"])
if restored_logistics == null:
return false
if restored_dispatcher.restored_snapshot_version == LogisticsDispatcher.LEGACY_SNAPSHOT_VERSION:
if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \
or not restored_dispatcher.migrate_legacy_assignment_ownership(
_derive_legacy_logistics_ownership(
restored_dispatcher,
restored_logistics,
restored_minute
)
):
return false
if not _validate_logistics_dispatcher_crosslinks(
restored_dispatcher,
restored_logistics,
restored_minute
):
return false
var restored_facilities := FacilityService.new()
if not restored_facilities.restore_snapshot(runtime_data[&"facilities"]):
return false
var restored_guests := GuestService.new()
if not restored_guests.restore_snapshot(runtime_data[&"guests"]):
return false
if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION:
if not _saved_catalog_guest_definitions_match(
runtime_data[&"guests"],
restored_catalog
):
return false
elif not _register_or_validate_legacy_catalog_definitions(
restored_guests,
restored_catalog
):
return false
var restored_staff := StaffService.new()
if recovery_mode:
restored_staff.configure_callbacks(
Callable(),
func(
reservation_ids: Array[StringName],
task: StaffTask,
absolute_minute: int
) -> Dictionary:
return _release_recovery_task_items(
restored_inventory,
reservation_ids,
task,
absolute_minute
),
Callable()
)
var restored_staff_ok := (
restored_staff.restore_snapshot_with_orphan_recovery(runtime_data[&"staff"])
if recovery_mode
else restored_staff.restore_snapshot(runtime_data[&"staff"])
)
if not restored_staff_ok:
return false
if recovery_mode:
staff_repair_report = restored_staff.last_recovery_repair_report.duplicate(true)
if not staff_repair_report[&"release_failed_task_ids"].is_empty():
return false
var saved_rank_value: Variant = snapshot[&"game_state"].get(&"world_tree_rank", null)
if not saved_rank_value is int:
return false
var saved_rank := int(saved_rank_value)
var restored_progression := ProgressionService.new()
if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION:
if not restored_progression.restore_snapshot(runtime_data[&"progression"]):
return false
else:
# Pre-v4 saves never recorded promotion evidence. Rank 1 can be migrated
# from authoritative live facts; a later rank would require guessing which
# requirements and quests were actually satisfied, so it is rejected.
if saved_rank != ProgressionService.MIN_RANK:
return false
var legacy_guest_lifetime := restored_guests.guest_count() \
+ int((runtime_data[&"guests"] as Dictionary)[&"persistent_records"].size())
if not restored_progression.configure(ProgressionService.MIN_RANK, {
&"active_above_ground_facilities": _count_active_facilities(restored_facilities, false),
&"active_underground_facilities": _count_active_facilities(restored_facilities, true),
&"asset_gold": restored_economy.get_balance(PLAYER_CASH_ACCOUNT),
&"faith_lifetime": maxi(int(saved_resources.get(&"faith", saved_resources.get("faith", 0))), 0),
&"guest_lifetime": legacy_guest_lifetime,
}):
return false
if saved_rank != restored_progression.current_rank \
or not _strict_variants_equal(
restored_progression.get_rank_bounds(saved_rank),
restored_world.get_bounds()
) \
or not _progression_guest_lifetime_matches(
restored_progression, restored_guests
):
return false
var restored_hired_staff_candidates: Variant
if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION:
restored_hired_staff_candidates = _deserialize_hired_staff_candidate_ids(
runtime_data[&"hired_staff_candidate_ids"],
restored_catalog
)
if restored_hired_staff_candidates == null:
return false
else:
restored_hired_staff_candidates = _derive_legacy_hired_staff_candidate_ids(
restored_staff,
runtime_data[&"staff"],
restored_catalog
)
if not _validate_catalog_staff_crosslinks(
restored_staff,
restored_hired_staff_candidates,
restored_catalog
):
return false
var restored_operations_state := _parse_operations_runtime_snapshot(
runtime_data.get(&"operations", {}),
runtime_schema_version,
restored_minute,
restored_guests,
restored_staff,
restored_economy,
int(runtime_data.get(&"world_seed", DEFAULT_WORLD_SEED)),
snapshot[&"clock"].get(&"paused", false),
restored_upkeep_settlement
)
if not bool(restored_operations_state.get(&"ok", false)):
return false
if bool(restored_operations_state.get(&"enabled", false)):
var restored_coordinator: OperationsCoordinator = restored_operations_state[&"coordinator"]
var expected_minimum := 0
var expected_maximum := 0
for period: Dictionary in restored_upkeep.get_report()[&"closed_periods"]:
if int(period[&"period_end"]) <= restored_coordinator.current_minute:
expected_minimum += 1
if int(period[&"period_end"]) <= restored_minute:
expected_maximum += 1
var actual_count := restored_upkeep_settlement.get_settled_period_count()
if actual_count < expected_minimum or actual_count > expected_maximum:
return false
var restored_movement_credits: Variant
if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION:
restored_movement_credits = _deserialize_movement_credits(
runtime_data[&"movement_credits"],
restored_minute
)
if restored_movement_credits == null:
return false
else:
restored_movement_credits = _rebuild_legacy_path_actor_projection(
restored_path,
restored_guests,
restored_staff,
restored_minute
)
if restored_movement_credits == null:
return false
if not _validate_runtime_path_crosslinks(
restored_path,
restored_guests,
restored_staff,
restored_movement_credits,
restored_minute
):
return false
var restored_relationships := RelationshipService.new()
restored_relationships.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, restored_staff, restored_catalog)
)
if not restored_relationships.restore_snapshot(runtime_data[&"relationships"]):
return false
var restored_events := EventService.new()
for raw_definition: Variant in runtime_data[&"event_definitions"]:
var definition := EventDefinition.from_variant(raw_definition)
if definition == null or not restored_events.register_definition(definition).is_success():
return false
if not bool(restored_events.validate_definitions().get(&"ok", false)):
return false
var restored_events_ok := (
restored_events.restore_snapshot_with_lock_recovery(runtime_data[&"events"])
if recovery_mode
else restored_events.restore_snapshot(runtime_data[&"events"])
)
if not restored_events_ok:
return false
if recovery_mode:
event_lock_repair_report = restored_events.last_recovery_repair_report.duplicate(true)
var saved_filter_profile := restored_events.get_content_filter_profile()
var effective_filter_profile := _merge_content_filter_profiles(
active_filter_profile,
saved_filter_profile
)
restored_events.configure_content_filter(
effective_filter_profile[&"filter_mode"],
effective_filter_profile[&"maximum_content_rating"],
effective_filter_profile[&"disabled_content_tags"]
)
var restored_authority := AuthorityService.new()
if not restored_authority.restore_snapshot(runtime_data[&"authority"]):
return false
var restored_defense := DefenseService.new()
if has_defense_snapshot:
if not restored_defense.restore_snapshot(runtime_data[&"defense"]):
return false
else:
# Runtime snapshots predating the Defense aggregate migrate to an empty
# aggregate at the saved clock. No external entity or consequence is
# synthesized by this compatibility path.
if not restored_defense.advance_time(restored_minute).is_success():
return false
var restored_staffing_projection := bool(
runtime_data.get(&"staffing_projection_enabled", false)
)
var restored_runtime_domain_minute := restored_minute
if bool(restored_operations_state.get(&"enabled", false)):
var restored_operations_coordinator: OperationsCoordinator = (
restored_operations_state.get(&"coordinator", null)
)
if int(restored_operations_state.get(&"deferred_runtime_minute", -1)) \
== restored_minute \
or (restored_operations_coordinator != null \
and restored_operations_coordinator.is_terminal()):
restored_runtime_domain_minute = restored_minute - 1
if not _validate_runtime_crosslinks(
restored_facilities,
restored_guests,
restored_economy,
restored_inventory,
restored_staff,
restored_runtime_domain_minute,
restored_staffing_projection
):
return false
if restored_authority.current_minute < restored_runtime_domain_minute \
or restored_authority.current_minute > restored_minute \
or restored_defense.current_minute < restored_runtime_domain_minute \
or restored_defense.current_minute > restored_minute:
return false
if not _validate_authority_economy_crosslinks(
restored_authority,
restored_economy,
restored_authority.current_minute
):
return false
if not _validate_defense_crosslinks(
restored_defense,
restored_staff,
restored_facilities,
restored_defense.current_minute
):
return false
if not _validate_event_defense_crosslinks(restored_events, restored_defense):
return false
var restored_construction_resource := _create_resource_adapter(
restored_economy,
CONSTRUCTION_EXPENSE_ACCOUNT,
&"CONSTRUCTION",
&"CONSTRUCTION_REFUND",
&"construction_service"
)
var restored_construction := ConstructionService.new(restored_world, restored_construction_resource)
if not restored_construction.restore_snapshot(runtime_data[&"construction"]):
return false
var restored_excavation := ExcavationService.new(
restored_world, restored_construction_resource
)
if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION:
if not restored_excavation.restore_snapshot(runtime_data[&"excavation"]):
return false
else:
restored_excavation.current_minute = restored_construction.current_minute
if not _migrate_legacy_infrastructure_proxies(
restored_construction,
restored_facilities,
restored_infrastructure_projector
) or not restored_progression.update_metrics({
&"active_above_ground_facilities": _count_active_facilities(
restored_facilities, false
),
&"active_underground_facilities": _count_active_facilities(
restored_facilities, true
),
&"asset_gold": restored_economy.get_balance(PLAYER_CASH_ACCOUNT),
}) or not _validate_runtime_crosslinks(
restored_facilities,
restored_guests,
restored_economy,
restored_inventory,
restored_staff,
restored_runtime_domain_minute,
restored_staffing_projection
):
return false
if not _validate_construction_facility_crosslinks(
restored_world,
restored_construction,
restored_facilities,
restored_infrastructure_projector
) or not _validate_infrastructure_crosslinks(
restored_world,
restored_path,
restored_construction,
restored_construction_resource,
restored_excavation,
restored_spatial,
restored_route_adapter,
restored_infrastructure_projector
):
return false
# Older saves used logical attendance. Migrate their staff capacity once;
# new saves must already agree with physical workplace positions.
for facility: FacilityInstance in restored_facilities.get_instances():
var present_ids: Array[StringName] = []
for staff_id: StringName in facility.active_staff_ids:
var worker := restored_staff.get_staff(staff_id)
if not StringName(restored_staff.get_work_assignment(staff_id).get(&"facility_id", &"")).is_empty() and not StaffCommuteService.is_at_facility(worker, facility) and not CleaningBridge.is_deployed(worker, facility, restored_staff):
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION:
return false
if worker.role_id != &"porter" and worker.current_task_id.is_empty():
worker.state = StaffRuntime.State.COMMUTING
else:
present_ids.append(staff_id)
if present_ids != facility.active_staff_ids:
restored_facilities.set_operational_status(facility.instance_id,
{&"active_staff_ids": present_ids}, restored_runtime_domain_minute)
var restored_elevators := ElevatorService.new()
restored_elevators.configure(restored_path)
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION:
if not restored_elevators.restore_snapshot(runtime_data[&"elevators"]):
return false
else:
if not bool(restored_elevators.synchronize([], restored_runtime_domain_minute).get(&"ok", false)):
return false
if not _validate_elevator_crosslinks(restored_elevators, restored_path, restored_construction,
restored_guests, restored_staff, restored_runtime_domain_minute):
return false
if not WarpService.connectors_match(restored_path, restored_construction.get_facilities()):
return false
for facility: FacilityInstance in restored_facilities.get_instances():
for staff_id: StringName in facility.active_staff_ids:
if not restored_elevators.get_actor_trip(_staff_path_actor_id(staff_id)).is_empty() and not CleaningBridge.is_deployed(restored_staff.get_staff(staff_id), facility, restored_staff):
return false
var restored_kitchen := KitchenProductionService.new()
restored_kitchen.configure(restored_inventory, restored_facilities, definition_registry, restored_runtime_domain_minute)
if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION:
if not restored_kitchen.restore_snapshot(runtime_data[&"kitchen_production"]):
return false
if restored_kitchen.current_minute != restored_runtime_domain_minute or not restored_kitchen.validate_crosslinks():
return false
if runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION \
and not PurchaseQuoteV2.valid_history(runtime_data, restored_economy):
return false
var restored_auto_purchase := AutoPurchasePlansType.new()
if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION:
if not restored_auto_purchase.restore_snapshot(runtime_data[&"auto_purchase_plans"], restored_facilities, restored_economy, restored_runtime_domain_minute):
return false
if not SupplyLogisticsBridge.validate_crosslinks(restored_logistics, restored_staff, restored_inventory, restored_facilities):
return false
var restored_weather := WeatherTimeline.new()
if runtime_schema_version >= WEATHER_RUNTIME_SNAPSHOT_VERSION:
if not runtime_data.get(&"weather") is Dictionary or not restored_weather.restore_snapshot(runtime_data[&"weather"]) or restored_weather.get_minute() != restored_minute:
return false
elif runtime_data.has(&"weather") or not restored_weather.initialize(&"clear", restored_minute):
return false
var restored_cleanliness := FacilityCleanliness.new()
if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION:
if not runtime_data.get(&"facility_cleanliness") is Dictionary or not restored_cleanliness.restore_snapshot(runtime_data[&"facility_cleanliness"]):
return false
else:
if runtime_data.has(&"facility_cleanliness"):
return false
var policy_values := cleaning_policy.create_snapshot()
for facility: FacilityInstance in restored_facilities.get_instances():
if not restored_cleanliness.register_facility(facility.instance_id, policy_values["initial_cleanliness"], policy_values["maximum_cleanliness"]):
return false
if not _validate_cleanliness_crosslinks(restored_cleanliness, restored_facilities, restored_staff, restored_path):
return false
var restored_world_seed := int(runtime_data.get(&"world_seed", DEFAULT_WORLD_SEED))
var restored_streams := {}
var saved_streams: Dictionary = runtime_data[&"rng_streams"]
for stream_key: Variant in saved_streams:
if not saved_streams[stream_key] is Dictionary:
return false
var normalized_stream_id := StringName(stream_key)
if normalized_stream_id.is_empty() or restored_streams.has(normalized_stream_id):
return false
var stream := DeterministicRng.from_root_seed(0, normalized_stream_id) as DeterministicRng
if stream == null or not stream.restore(saved_streams[stream_key]) \
or stream.get_stream_id() != normalized_stream_id \
or stream.get_root_seed() != restored_world_seed:
return false
restored_streams[normalized_stream_id] = stream
if not restored_streams.has(&"defense"):
if has_defense_snapshot:
return false
restored_streams[&"defense"] = DeterministicRng.from_root_seed(restored_world_seed, &"defense")
if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION:
for required_stream_id: StringName in RNG_STREAM_IDS:
if not restored_streams.has(required_stream_id):
return false
var restored_next_id := int(runtime_data.get(&"next_command_id", 0))
if restored_next_id <= 0:
return false
var restored_peeping_rooms := PeepingRooms.new()
restored_peeping_rooms.configure(restored_world, restored_facilities, restored_construction, restored_path, restored_id_generator)
if runtime_data.has(&"special_peeping_rooms"):
if not runtime_data[&"special_peeping_rooms"] is Dictionary:
return false
for raw_key: Variant in runtime_data:
if raw_key == &"special_peeping_rooms" and typeof(raw_key) != TYPE_STRING_NAME:
return false
if not restored_peeping_rooms.restore_section(runtime_data.get(&"special_peeping_rooms", null), restored_minute):
return false
if not SimulationSnapshot.restore(snapshot, _clock, _state):
return false
world_seed = restored_world_seed
_next_command_id = restored_next_id
_staffing_projection_enabled = restored_staffing_projection
command_queue = restored_queue
id_generator = restored_id_generator
peeping_room_service = restored_peeping_rooms
domain_events = restored_domain_events
_rng_streams = restored_streams
world_service = restored_world
path_service = restored_path
_movement_credits = (restored_movement_credits as Dictionary).duplicate(true)
_connect_path_performance_signal()
mana_network_service = restored_mana
mana_reserve = restored_mana_reserve
weather = restored_weather
facility_cleanliness = restored_cleanliness
facility_upkeep = restored_upkeep
upkeep_settlement = restored_upkeep_settlement
economy_service = restored_economy
inventory_service = restored_inventory
kitchen_production = restored_kitchen
auto_purchase_plans = restored_auto_purchase
logistics_dispatcher = restored_dispatcher
logistics_requests = restored_logistics
facility_service = restored_facilities
guest_service = restored_guests
staff_service = restored_staff
elevators = restored_elevators
legacy_stairs = restored_legacy_stairs
staff_commute.configure(self)
supply_logistics.configure(self)
cleaning_bridge.configure(self)
mvp_content_catalog = restored_catalog
_clear_mvp_content_manifest_cache()
mvp_content_report = restored_catalog_report.duplicate(true)
progression_service = restored_progression
_hired_staff_candidate_ids = (restored_hired_staff_candidates as Dictionary).duplicate(true)
operations_coordinator = restored_operations_state.get(&"coordinator", null)
_operations_enabled = bool(restored_operations_state[&"enabled"])
_personal_action_points = int(restored_operations_state[&"personal_action_points"])
_operations_runtime_failure_latched = bool(restored_operations_state[&"runtime_failure_latched"])
_operations_runtime_failure_report = (
restored_operations_state[&"runtime_failure_report"] as Dictionary
).duplicate(true)
_operations_tick_blocked = _operations_should_block_tick()
_operations_deferred_runtime_minute = int(
restored_operations_state[&"deferred_runtime_minute"]
)
_operations_resume_publish_pending = false
_operations_test_boundary_hooks.clear()
_operations_transaction_active = false
_operations_queued_domain_events.clear()
_operations_queued_guest_spawn_notifications.clear()
_operations_flushing_guest_spawn_notifications.clear()
_operations_queued_economy_notifications.clear()
_operations_flushing_economy_notifications.clear()
relationship_service = restored_relationships
event_service = restored_events
authority_service = restored_authority
_detach_defense_service(defense_service)
defense_service = restored_defense
_reset_defense_round_attempts()
construction_resource_state = restored_construction_resource
operating_resource_state = _create_resource_adapter(
economy_service,
OPERATING_EXPENSE_ACCOUNT,
&"OPERATING_EXPENSE",
&"OPERATING_REFUND",
&"simulation_runtime"
)
construction_service = restored_construction
excavation_service = restored_excavation
spatial_subslot_service = restored_spatial
route_subslot_path_adapter = restored_route_adapter
facility_infrastructure_projector = restored_infrastructure_projector
_infrastructure_failure_latched = false
_infrastructure_failure_report.clear()
_construction_completion_transaction_active = false
_construction_committing_notifications = false
_construction_queued_domain_events.clear()
_last_daily_checkpoint_day = restored_last_daily_checkpoint_day
_pending_checkpoints.clear()
_next_checkpoint_request_id = 1
_connect_economy_signals()
_configure_facility_boundaries()
_connect_facility_signals()
_configure_staff_boundaries()
_connect_guest_signals()
_connect_staff_signals()
_configure_relationship_event_boundaries()
_connect_relationship_signals()
_connect_event_signals()
_configure_authority_boundaries()
_connect_authority_signals()
_configure_defense_boundaries()
_connect_defense_signals()
_connect_construction_signals()
var excavation_callback := Callable(self, "_on_excavation_completed")
if not excavation_service.excavation_completed.is_connected(excavation_callback):
excavation_service.excavation_completed.connect(excavation_callback)
if _operations_enabled:
_configure_operations_callbacks()
_register_operations_tick_handler()
else:
_clock.unregister_tick_handler(
_clock.TickPhase.TIME_BOUNDARIES,
&"simulation_runtime.operations"
)
latest_restore_safety_report = {
&"report_version": 1,
&"recovery_mode": bool(recovery_mode),
&"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY",
&"automatic_repairs_applied": (
bool(staff_repair_report[&"automatic_repairs_applied"])
or bool(event_lock_repair_report[&"automatic_repairs_applied"])
),
&"repairs": {
&"orphan_tasks": staff_repair_report,
&"orphan_event_locks": event_lock_repair_report,
},
&"content_filter": _content_filter_restore_report(
active_filter_profile,
saved_filter_profile,
effective_filter_profile
),
&"forbidden_repairs_applied": false,
&"forbidden_repair_categories": [
&"synthesize_gold",
&"delete_facilities",
&"rewrite_ledger",
&"guess_route_locks",
],
}
_publish_snapshot(_clock.absolute_minute)
return true
func configure_save_policy(overrides: Dictionary) -> Dictionary:
var configured := _save_policy.duplicate(true)
if overrides.has(&"enabled"):
configured[&"enabled"] = bool(overrides[&"enabled"])
if overrides.has(&"daily_checkpoint_enabled"):
configured[&"daily_checkpoint_enabled"] = bool(overrides[&"daily_checkpoint_enabled"])
if overrides.has(&"auto_migrate"):
configured[&"auto_migrate"] = bool(overrides[&"auto_migrate"])
if overrides.has(&"autosave_path"):
var autosave_path := str(overrides[&"autosave_path"])
if not _save_policy_path_is_safe(autosave_path):
return {
&"ok": false,
&"code": &"INVALID_AUTOSAVE_PATH",
&"message": autosave_path,
}
configured[&"autosave_path"] = autosave_path
if overrides.has(&"autosave_generations"):
var generations := int(overrides[&"autosave_generations"])
if generations < 1 or generations > SaveService.MAX_AUTOSAVE_GENERATIONS:
return {
&"ok": false,
&"code": &"INVALID_AUTOSAVE_GENERATIONS",
&"message": str(generations),
}
configured[&"autosave_generations"] = generations
_save_policy = configured
return {
&"ok": true,
&"code": &"OK",
&"policy": get_save_policy(),
}
func enable_production_save_policy() -> Dictionary:
return configure_save_policy({
&"enabled": true,
&"daily_checkpoint_enabled": true,
&"autosave_path": DEFAULT_AUTOSAVE_PATH,
})
func get_save_policy() -> Dictionary:
return _save_policy.duplicate(true)
func get_last_daily_checkpoint_day() -> int:
return _last_daily_checkpoint_day
func get_pending_checkpoint_count() -> int:
return _pending_checkpoints.size()
func get_latest_restore_safety_report() -> Dictionary:
return latest_restore_safety_report.duplicate(true)
func enable_production_operations() -> Dictionary:
if not _initialized or _clock == null or _state == null:
return _operations_api_failure(&"RUNTIME_NOT_INITIALIZED", "Runtime is not initialized.")
if _operations_enabled:
return _operations_api_failure(&"ALREADY_ENABLED", "Production operations are already enabled.")
if _clock.absolute_minute != OPERATIONS_START_MINUTE \
or _clock.day != 1 or _clock.minute_of_day != OPERATIONS_START_MINUTE:
return _operations_api_failure(
&"INVALID_START_TIME",
"Production operations must start at Day 1 06:00."
)
if not bool(mvp_content_report.get(&"valid", false)):
return _operations_api_failure(&"CONTENT_INVALID", "MVP content is unavailable.")
if _staff_payroll_arrears_total(staff_service) != 0:
return _operations_api_failure(
&"INITIAL_ARREARS_UNSUPPORTED",
"Production operations cannot infer pre-existing payroll arrears."
)
if not (staff_service.create_snapshot().get(&"payroll_settlements", []) as Array).is_empty():
return _operations_api_failure(
&"INITIAL_PAYROLL_HISTORY_UNSUPPORTED",
"Production operations require a fresh Day 1 payroll history."
)
if _guest_aggregate_has_reserved_operations_id(guest_service):
return _operations_api_failure(
&"RESERVED_GUEST_ID_PRESENT",
"Production operations require the guest-auto namespace to be unused."
)
var coordinator := OperationsCoordinator.new({
&"start_minute": OPERATIONS_COORDINATOR_ORIGIN_MINUTE,
&"world_seed": world_seed,
})
coordinator.configure_callbacks(
Callable(self, "_operations_daily_boundary"),
Callable(self, "_operations_arrival_boundary"),
Callable(self, "_operations_morning_boundary"),
Callable(self, "_operations_midnight_upkeep_boundary")
)
var demand_configuration := _configure_default_operations_demand(coordinator)
if not demand_configuration.is_success():
return _operations_api_failure(
&"DEMAND_CONFIGURATION_FAILED",
demand_configuration.message,
demand_configuration.details
)
var population := coordinator.set_population_count(guest_service.guest_count())
if not population.is_success():
return _operations_api_failure(
&"INITIAL_POPULATION_INVALID",
population.message,
population.details
)
var previous_coordinator := operations_coordinator
var previous_ap := _personal_action_points
var enable_external_before := _capture_operations_external_state()
if enable_external_before.is_empty():
return _operations_api_failure(
&"INITIAL_CAPTURE_FAILED",
"Production Operations could not capture the initial Runtime state."
)
operations_coordinator = coordinator
_personal_action_points = 0
var advanced := coordinator.advance_to(OPERATIONS_START_MINUTE, true)
if not advanced.is_success() \
or coordinator.current_minute != _clock.absolute_minute \
or coordinator.get_population_count() != guest_service.guest_count() \
or coordinator.get_arrears_total() != _operations_closed_arrears_total() \
or not _operations_notifications_are_committable() \
or not _commit_operations_domain_events() \
or not _commit_operations_notifications():
coordinator.discard_deferred_notifications()
_restore_operations_external_state(enable_external_before)
_discard_operations_domain_events()
operations_coordinator = previous_coordinator
_personal_action_points = previous_ap
return _operations_api_failure(
&"INITIAL_BOUNDARY_FAILED",
advanced.message if not advanced.is_success() else "Initial Operations cross-links failed.",
advanced.details
)
_operations_enabled = true
_operations_tick_blocked = false
_operations_runtime_failure_latched = false
_operations_runtime_failure_report.clear()
_register_operations_tick_handler()
_pause_for_waiting_foreground_event()
_publish_snapshot(_clock.absolute_minute)
return {
&"ok": true,
&"code": &"OK",
&"enabled": true,
&"current_minute": coordinator.current_minute,
&"personal_action_points": _personal_action_points,
}
func acknowledge_operations_pause() -> Dictionary:
if not _operations_enabled or operations_coordinator == null:
return _operations_api_failure(&"OPERATIONS_DISABLED", "Production operations are disabled.")
if operations_coordinator.is_terminal():
return _operations_api_failure(&"TERMINAL_STATE", "Bankruptcy cannot be acknowledged away.")
if operations_coordinator.is_external_quarantined():
return _operations_api_failure(
&"EXTERNAL_ROLLBACK_FAILED",
"A verified snapshot restore is required."
)
if _operations_deferred_runtime_minute < 0:
return _operations_api_failure(
&"NO_DEFERRED_RUNTIME_MINUTE",
"Operations have no deferred Runtime phase suffix."
)
if _operations_deferred_runtime_minute != _clock.absolute_minute:
return _operations_api_failure(
&"DEFERRED_MINUTE_MISMATCH",
"The deferred Operations minute no longer matches GameClock."
)
var had_runtime_failure := _operations_runtime_failure_latched
var acknowledged := operations_coordinator.acknowledge_pause()
if not acknowledged.is_success():
return _operations_api_failure(&"ACKNOWLEDGE_FAILED", acknowledged.message, acknowledged.details)
var deferred_minute := _operations_deferred_runtime_minute
var deferred := operations_coordinator.advance_to(deferred_minute, true)
if not deferred.is_success():
var partial_commit_ok := _commit_operations_domain_events() \
and _commit_operations_notifications()
if not partial_commit_ok:
operations_coordinator.discard_deferred_notifications()
_discard_operations_domain_events()
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = deferred.to_dictionary()
_operations_tick_blocked = true
_clock.set_paused(true)
return _operations_api_failure(&"RETRY_FAILED", deferred.message, deferred.details)
if operations_coordinator.current_minute != deferred_minute \
or operations_coordinator.get_population_count() != guest_service.guest_count() \
or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total() \
or not _operations_notifications_are_committable() \
or not _commit_operations_domain_events() \
or not _commit_operations_notifications():
operations_coordinator.discard_deferred_notifications()
_discard_operations_domain_events()
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = {
&"code": &"OPERATIONS_CROSSLINK_MISMATCH",
&"minute": deferred_minute,
}
_operations_tick_blocked = true
_clock.set_paused(true)
return _operations_api_failure(
&"RETRY_CROSSLINK_FAILED",
"Operations retry did not restore Runtime cross-links."
)
_operations_runtime_failure_latched = false
_operations_runtime_failure_report.clear()
if operations_coordinator.is_terminal():
_operations_deferred_runtime_minute = -1
_operations_tick_blocked = true
_clock.set_paused(true)
_publish_snapshot(deferred_minute)
return {
&"ok": true,
&"code": &"TERMINAL_STATE",
&"had_runtime_failure": had_runtime_failure,
&"suffix_replayed": false,
}
if operations_coordinator.pause_requested:
_operations_tick_blocked = true
_clock.set_paused(true)
return _operations_api_failure(
&"PAUSE_REMAINS",
"Operations still require acknowledgement before Runtime phases can resume."
)
_operations_deferred_runtime_minute = -1
_operations_tick_blocked = false
_clock.set_paused(false)
_run_operations_deferred_suffix(deferred_minute)
_pause_for_waiting_foreground_event()
if operations_coordinator.get_population_count() != guest_service.guest_count() \
or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total():
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = {
&"code": &"DEFERRED_SUFFIX_CROSSLINK_MISMATCH",
&"minute": deferred_minute,
}
_operations_tick_blocked = true
_clock.set_paused(true)
return _operations_api_failure(
&"SUFFIX_CROSSLINK_FAILED",
"Deferred Runtime phases ended with invalid Operations cross-links."
)
return {
&"ok": true,
&"code": &"OK",
&"had_runtime_failure": had_runtime_failure,
&"coordinator": acknowledged.to_dictionary(),
&"suffix_replayed": true,
&"deferred_runtime_minute": deferred_minute,
}
func _run_operations_deferred_suffix(absolute_minute: int) -> void:
_advance_mana(absolute_minute)
_advance_construction(absolute_minute)
_advance_inventory_and_logistics(absolute_minute)
_advance_facilities(absolute_minute)
_advance_guest_vitals(absolute_minute)
_advance_authority_incidents(absolute_minute)
_advance_defense(absolute_minute)
_advance_relationship_events(absolute_minute)
_advance_guest_queues(absolute_minute)
_advance_staff_ai(absolute_minute)
if _active_command_minute >= 0:
_operations_resume_publish_pending = true
else:
_publish_snapshot(absolute_minute)
_process_save_checkpoints(absolute_minute)
func get_operations_dashboard_report() -> Dictionary:
return {&"enabled":_operations_enabled, &"arrears_total":_operations_total_arrears(),
&"tick_blocked":_operations_tick_blocked, &"runtime_failure_latched":_operations_runtime_failure_latched,
&"personal_action_points":_personal_action_points,
&"deferred_runtime_minute":_operations_deferred_runtime_minute,
&"coordinator":operations_coordinator.get_dashboard_summary() if operations_coordinator != null else {}}
func get_operations_report() -> Dictionary:
return {
&"enabled": _operations_enabled,
&"arrears_total": _operations_total_arrears(),
&"tick_blocked": _operations_tick_blocked,
&"runtime_failure_latched": _operations_runtime_failure_latched,
&"runtime_failure_report": _operations_runtime_failure_report.duplicate(true),
&"deferred_runtime_minute": _operations_deferred_runtime_minute,
&"personal_action_points": _personal_action_points,
&"coordinator": (
operations_coordinator.create_snapshot()
if operations_coordinator != null else {}
),
}
func set_operations_test_boundary_hook(operation: StringName, callback: Callable) -> bool:
# Test-only fault injection is invoked at PREPARE and, for arrivals, once at
# AFTER_FIRST_MUTATION. The latter proves staged service notifications are
# discarded when a real partial apply is rolled back.
if operation not in [&"DAILY_CLOSE", &"ARRIVAL_PLAN", &"MORNING_BOUNDARY", &"MIDNIGHT_UPKEEP"]:
return false
if callback.is_valid():
_operations_test_boundary_hooks[operation] = callback
else:
_operations_test_boundary_hooks.erase(operation)
return true
func _run_operations_test_boundary_hook(
operation: StringName,
payload: Dictionary,
phase: StringName,
token: Variant
) -> Dictionary:
var callback: Callable = _operations_test_boundary_hooks.get(operation, Callable())
if not callback.is_valid():
return {&"ok": true}
var returned: Variant = callback.call(payload.duplicate(true), phase, token)
if returned is bool:
return {&"ok": bool(returned), &"reason": &"TEST_HOOK_REJECTED"}
if returned is Dictionary:
var result: Dictionary = returned
if typeof(result.get(&"ok", null)) == TYPE_BOOL:
return result.duplicate(true)
return {&"ok": false, &"reason": &"TEST_HOOK_INVALID_RESULT"}
func _register_operations_tick_handler() -> void:
_clock.unregister_tick_handler(
_clock.TickPhase.TIME_BOUNDARIES,
&"simulation_runtime.operations"
)
_clock.register_tick_handler(
_clock.TickPhase.TIME_BOUNDARIES,
&"simulation_runtime.operations",
Callable(self, "_advance_operations_time")
)
func _configure_operations_callbacks() -> void:
if operations_coordinator == null:
return
operations_coordinator.configure_callbacks(
Callable(self, "_operations_daily_boundary"),
Callable(self, "_operations_arrival_boundary"),
Callable(self, "_operations_morning_boundary"),
Callable(self, "_operations_midnight_upkeep_boundary")
)
func _configure_default_operations_demand(coordinator: OperationsCoordinator) -> CommandResult:
if coordinator == null or DEFAULT_OPERATIONS_DEMAND_PROFILES.size() != 8:
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
0,
"The canonical Operations demand profile set is unavailable."
)
var allowed_classes: Dictionary = {}
for class_id: StringName in mvp_content_catalog.get_guest_class_ids():
allowed_classes[class_id] = true
var seen_classes: Dictionary = {}
for raw_profile: Variant in DEFAULT_OPERATIONS_DEMAND_PROFILES:
if not raw_profile is Dictionary:
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED, 0,
"A canonical Operations demand profile is malformed."
)
var profile: Dictionary = raw_profile
var guest_class := StringName(profile[&"guest_class"])
if not allowed_classes.has(guest_class) or seen_classes.has(guest_class):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED, 0,
"The canonical Operations demand classes are invalid."
)
seen_classes[guest_class] = true
var configured := coordinator.set_demand_input(
StringName(profile[&"facility_tag"]),
guest_class,
StringName(profile[&"time_band"]),
{
&"base": 100,
&"reputation": 0,
&"unmet_need": 0,
&"event": 0,
&"recent_price_penalty": 0,
&"wait_penalty": 0,
}
)
if not configured.is_success():
return configured
if seen_classes.size() != allowed_classes.size():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED, 0,
"Every MVP Guest class must have exactly one demand profile."
)
return CommandResult.succeeded(0, {&"profile_count": seen_classes.size()})
func get_facility_upkeep_report(facility_id: StringName) -> Dictionary:
var definition_id: StringName = &""
var facility := facility_service.get_instance(facility_id)
if facility != null:
definition_id = facility.definition_id
elif construction_service != null:
for built: Dictionary in construction_service.get_facilities():
if StringName(built[&"instance_id"]) == facility_id:
definition_id = StringName(built[&"definition_id"])
break
var definition := definition_registry.get_facility(definition_id)
var balance := upkeep_settlement.get_facility_balance(facility_id)
var units := facility_upkeep.get_unsettled_units(facility_id, upkeep_settlement.get_settled_period_count())
return {
&"facility_id": facility_id,
&"owned": definition != null,
&"daily_rate": definition.maintenance_gold if definition != null else 0,
&"unsettled_units": units + int(balance[&"fraction_units"]),
&"gold_denominator": FacilityUpkeepAccrual.MINUTES_PER_DAY,
&"arrears": balance[&"arrears"],
}
func _advance_facility_upkeep(minute: int) -> void:
if _operations_deferred_runtime_minute >= 0:
return
var rates: Dictionary = {}
for facility: FacilityInstance in facility_service.get_instances():
var definition := definition_registry.get_facility(facility.definition_id)
if definition != null:
rates[String(facility.instance_id)] = definition.maintenance_gold
# Infrastructure modules are completed assets even when they have no service instance.
for facility: Dictionary in construction_service.get_facilities():
var definition := definition_registry.get_facility(StringName(facility[&"definition_id"]))
if definition != null:
rates[String(facility[&"instance_id"])] = definition.maintenance_gold
var advanced := facility_upkeep.advance_to(minute, rates)
if not advanced[&"ok"]:
_clock.set_paused(true)
push_error("Facility upkeep accrual failed: %s" % advanced)
func _upkeep_accrual_crosslinks_are_valid(accrual: FacilityUpkeepAccrual, settlement: FacilityUpkeepSettlement) -> bool:
var periods: Array = accrual.get_report()[&"closed_periods"]
var receipts: Array = settlement.get_report()[&"receipts"]
if receipts.size() > periods.size():
return false
for index in receipts.size():
if receipts[index][&"period"] != periods[index]:
return false
return true
func _operations_closed_arrears_total() -> int:
# Coordinator arrears describe its last financial close. Interim calendar
# payments during a legacy 06:00 period must not rewrite that closed record.
var last_close := 0
if operations_coordinator != null and operations_coordinator.get_open_financial_day() > 1:
last_close = operations_coordinator.get_daily_close_minute(operations_coordinator.get_open_financial_day() - 1)
return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_through(last_close) \
+ _finance_arrears_through(economy_service, last_close)
func _operations_total_arrears() -> int:
return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_total() \
- economy_service.get_balance(FinancePolicy.RANK_TAX_PAYABLE) \
- economy_service.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE)
func _advance_operations_time(absolute_minute: int) -> void:
if not _operations_enabled or operations_coordinator == null:
return
if _operations_deferred_runtime_minute >= 0:
_operations_tick_blocked = true
_clock.set_paused(true)
return
_operations_tick_blocked = false
var advanced := operations_coordinator.advance_to(absolute_minute, true)
if not advanced.is_success():
# Earlier boundaries in the same minute can already be validly committed
# (for example demand before a failed arrival). Publish only those staged
# effects and notifications; the failing callback rolled itself back.
if not _commit_operations_domain_events() \
or not _commit_operations_notifications():
operations_coordinator.discard_deferred_notifications()
_discard_operations_domain_events()
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = advanced.to_dictionary()
_operations_deferred_runtime_minute = absolute_minute
_operations_tick_blocked = true
_clock.set_paused(true)
return
if operations_coordinator.current_minute != absolute_minute \
or operations_coordinator.get_population_count() != guest_service.guest_count() \
or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total() \
or not _operations_notifications_are_committable() \
or not _commit_operations_domain_events() \
or not _commit_operations_notifications():
operations_coordinator.discard_deferred_notifications()
_discard_operations_domain_events()
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = {
&"code": &"OPERATIONS_CROSSLINK_MISMATCH",
&"minute": absolute_minute,
}
_operations_deferred_runtime_minute = absolute_minute
_operations_tick_blocked = true
_clock.set_paused(true)
return
_operations_runtime_failure_latched = false
_operations_runtime_failure_report.clear()
if operations_coordinator.is_terminal():
_operations_deferred_runtime_minute = -1
_operations_tick_blocked = true
_clock.set_paused(true)
return
if operations_coordinator.pause_requested:
_operations_deferred_runtime_minute = absolute_minute
_operations_tick_blocked = true
_clock.set_paused(true)
return
_operations_deferred_runtime_minute = -1
_pause_for_waiting_foreground_event()
func _operations_should_block_tick() -> bool:
if not _operations_enabled or operations_coordinator == null:
return false
return _operations_deferred_runtime_minute >= 0 \
or _operations_runtime_failure_latched \
or operations_coordinator.pause_requested \
or operations_coordinator.is_terminal() \
or operations_coordinator.is_external_quarantined()
func _pause_for_waiting_foreground_event() -> void:
if event_service == null or _clock == null:
return
var foreground := event_service.get_foreground_instance()
if foreground != null and foreground.state == EventInstance.State.WAITING_CHOICE:
_clock.set_paused(true)
func _capture_operations_external_state() -> Dictionary:
# This is deliberately a composed transaction token, not a Runtime snapshot.
# It contains every aggregate that an Operations callback can reach, while
# excluding clock/coordinator state owned by the caller of the callback.
if not _begin_operations_external_transaction():
return {}
return _build_operations_external_state()
func _build_operations_external_state() -> Dictionary:
var committed_minute := _runtime_aggregate_committed_minute()
if committed_minute < 0:
_discard_operations_domain_events()
return {}
var rng_snapshots: Dictionary = {}
for stream_id: StringName in _sorted_name_keys(_rng_streams):
var stream := _rng_streams.get(stream_id) as DeterministicRng
if stream == null:
_discard_operations_domain_events()
return {}
rng_snapshots[stream_id] = stream.snapshot()
return {
# TIME_BOUNDARIES runs before the Runtime suffix. During a normal
# Operations callback the Clock is already at X while minute-driven
# aggregates are still committed through X - 1. Capture that committed
# participant minute so rollback validates the state it actually saved.
&"captured_minute": committed_minute,
&"captured_clock_minute": _clock.absolute_minute,
&"captured_authority_minute": authority_service.current_minute,
&"captured_defense_minute": defense_service.current_minute,
&"game_state": _state.create_snapshot(),
&"command_queue": command_queue.create_snapshot(),
&"next_command_id": _next_command_id,
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"inventory": inventory_service.create_snapshot(),
&"kitchen_production": kitchen_production.create_snapshot(),
&"auto_purchase_plans": auto_purchase_plans.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"guests": guest_service.create_snapshot(),
&"staff": staff_service.create_snapshot(),
&"relationships": relationship_service.create_snapshot(),
&"events": event_service.create_snapshot(),
&"authority": authority_service.create_snapshot(),
&"world": world_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"excavation": excavation_service.create_snapshot(),
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
&"route_projection": route_subslot_path_adapter.create_snapshot(),
&"facility_infrastructure": facility_infrastructure_projector.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"rng_streams": rng_snapshots,
&"staffing_projection_enabled": _staffing_projection_enabled,
&"progression": progression_service.create_snapshot(),
&"hired_staff_candidate_ids": _hired_staff_candidate_ids.duplicate(true),
&"personal_action_points": _personal_action_points,
# The remaining aggregates are reachable through Event actions or signals.
&"mana": mana_network_service.create_snapshot(),
&"mana_reserve": mana_reserve.create_snapshot(),
&"logistics_dispatcher": logistics_dispatcher.create_snapshot(),
&"logistics_requests": _serialize_logistics_requests(),
&"defense": defense_service.create_snapshot(),
&"construction": construction_service.create_snapshot(),
}
func _operations_external_committed_minute() -> int:
return _runtime_aggregate_committed_minute()
func _runtime_aggregate_committed_minute() -> int:
if facility_service == null or guest_service == null or inventory_service == null \
or staff_service == null or kitchen_production == null:
return -1
var minute := facility_service.current_minute
if minute < 0 or guest_service.current_minute != minute \
or inventory_service.current_minute != minute \
or staff_service.current_minute != minute \
or kitchen_production.current_minute != minute \
or elevators.current_minute != minute:
return -1
return minute
func _restore_operations_external_state(token: Variant) -> bool:
if not token is Dictionary:
_discard_operations_domain_events()
return false
var saved: Dictionary = token
var required_dictionary_keys: Array[StringName] = [
&"facility_upkeep", &"upkeep_settlement", &"facility_cleanliness",
&"game_state", &"command_queue", &"economy", &"inventory", &"kitchen_production", &"auto_purchase_plans", &"facilities", &"guests",
&"staff", &"relationships", &"events", &"authority", &"world", &"path", &"elevators", &"legacy_stairs",
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure",
&"movement_credits", &"domain_events", &"id_generator", &"rng_streams",
&"progression", &"hired_staff_candidate_ids", &"mana", &"mana_reserve",
&"logistics_dispatcher", &"defense", &"construction",
]
for key: StringName in required_dictionary_keys:
if not saved.get(key, null) is Dictionary:
_discard_operations_domain_events()
return false
if not saved.get(&"logistics_requests", null) is Array \
or typeof(saved.get(&"staffing_projection_enabled", null)) != TYPE_BOOL \
or typeof(saved.get(&"personal_action_points", null)) != TYPE_INT \
or typeof(saved.get(&"captured_minute", null)) != TYPE_INT \
or int(saved[&"captured_minute"]) < 0 \
or typeof(saved.get(&"captured_clock_minute", null)) != TYPE_INT \
or int(saved[&"captured_clock_minute"]) < int(saved[&"captured_minute"]) \
or typeof(saved.get(&"captured_authority_minute", null)) != TYPE_INT \
or int(saved[&"captured_authority_minute"]) < 0 \
or typeof(saved.get(&"captured_defense_minute", null)) != TYPE_INT \
or int(saved[&"captured_defense_minute"]) < 0 \
or typeof(saved.get(&"next_command_id", null)) != TYPE_INT \
or int(saved[&"next_command_id"]) <= 0:
_discard_operations_domain_events()
return false
# Validation/PREPARE failure did not mutate a participant. Avoid restore APIs
# here because GameState.restore_snapshot intentionally emits reset signals.
if _operations_external_state_matches(saved):
_discard_operations_domain_events()
return true
# Ask every participant to restore even when an earlier participant rejects
# its snapshot. This avoids knowingly leaving a half-restored external world.
var state_ok: bool = _state.restore_snapshot(saved[&"game_state"])
var command_queue_ok := command_queue.restore_snapshot(saved[&"command_queue"])
var economy_ok := economy_service.restore_snapshot(saved[&"economy"])
var upkeep_ok := facility_upkeep.restore_snapshot(saved[&"facility_upkeep"]) and upkeep_settlement.restore_snapshot(saved[&"upkeep_settlement"], economy_service)
var inventory_ok := inventory_service.restore_snapshot(saved[&"inventory"])
var facilities_ok := facility_service.restore_snapshot(saved[&"facilities"])
facilities_ok = facility_cleanliness.restore_snapshot(saved.get(&"facility_cleanliness", {})) and facilities_ok
var kitchen_ok := kitchen_production.restore_snapshot(saved[&"kitchen_production"])
var auto_purchase_ok := auto_purchase_plans.restore_snapshot(saved[&"auto_purchase_plans"], facility_service, economy_service, int(saved[&"captured_minute"]))
var guests_ok := guest_service.restore_snapshot(saved[&"guests"])
var staff_ok := staff_service.restore_snapshot(saved[&"staff"])
var relationships_ok := relationship_service.restore_snapshot(saved[&"relationships"])
var events_ok := event_service.restore_snapshot(saved[&"events"])
var authority_ok := authority_service.restore_snapshot(saved[&"authority"])
var world_ok := world_service.restore_snapshot(saved[&"world"])
var spatial_ok := spatial_subslot_service.restore_snapshot(saved[&"spatial_subslots"])
var route_snapshot: Dictionary = saved[&"route_projection"]
var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null)
var topology_ok := projected_walkable is Array \
and path_service.rebuild_graph(projected_walkable)
var path_ok := topology_ok and path_service.restore_snapshot(saved[&"path"])
path_ok = elevators.restore_snapshot(saved[&"elevators"]) and path_ok
path_ok = legacy_stairs.restore_snapshot(saved[&"legacy_stairs"]) and path_ok
var route_ok := path_ok and route_subslot_path_adapter.restore_snapshot(route_snapshot)
var infrastructure_ok := route_ok and facility_infrastructure_projector.restore_snapshot(
saved[&"facility_infrastructure"]
)
var domain_events_ok := domain_events.restore_snapshot(saved[&"domain_events"])
var ids_ok := id_generator.restore_snapshot(saved[&"id_generator"])
var progression_ok := progression_service.restore_snapshot(saved[&"progression"])
var mana_ok := mana_network_service.restore_snapshot(saved[&"mana"])
mana_ok = mana_reserve.restore_snapshot(saved[&"mana_reserve"]) and mana_ok
var dispatcher_ok := logistics_dispatcher.restore_snapshot(saved[&"logistics_dispatcher"])
var defense_ok := defense_service.restore_snapshot(saved[&"defense"])
var construction_ok := construction_service.restore_snapshot(saved[&"construction"])
var excavation_ok := excavation_service.restore_snapshot(saved[&"excavation"])
var restored_requests: Variant = _deserialize_logistics_requests(saved[&"logistics_requests"])
var requests_ok := restored_requests is Dictionary
if requests_ok:
logistics_requests = restored_requests
_movement_credits = (saved[&"movement_credits"] as Dictionary).duplicate(true)
_staffing_projection_enabled = bool(saved[&"staffing_projection_enabled"])
_hired_staff_candidate_ids = (
saved[&"hired_staff_candidate_ids"] as Dictionary
).duplicate(true)
_personal_action_points = int(saved[&"personal_action_points"])
_next_command_id = int(saved[&"next_command_id"])
var rng_ok := true
var saved_rngs: Dictionary = saved[&"rng_streams"]
if saved_rngs.size() != _rng_streams.size():
rng_ok = false
for raw_stream_id: Variant in saved_rngs:
var stream_id := StringName(raw_stream_id)
var stream := _rng_streams.get(stream_id) as DeterministicRng
if stream == null or not saved_rngs[raw_stream_id] is Dictionary \
or not stream.restore(saved_rngs[raw_stream_id]):
rng_ok = false
_discard_operations_domain_events()
var participants_ok := upkeep_ok and state_ok and command_queue_ok and economy_ok and inventory_ok and kitchen_ok and auto_purchase_ok \
and facilities_ok and guests_ok and staff_ok and relationships_ok \
and events_ok and authority_ok and world_ok and spatial_ok and path_ok \
and route_ok and infrastructure_ok and excavation_ok \
and domain_events_ok and ids_ok \
and progression_ok and mana_ok and dispatcher_ok and defense_ok \
and construction_ok and requests_ok and rng_ok
if not participants_ok:
return false
var validation_minute := int(saved[&"captured_minute"])
var clock_validation_minute := int(saved[&"captured_clock_minute"])
return _operations_external_state_matches(saved) \
and bool(economy_service.validate_ledger().get(&"ok", false)) \
and _validate_logistics_dispatcher_crosslinks(
logistics_dispatcher, logistics_requests, clock_validation_minute
) \
and _validate_runtime_crosslinks(
facility_service, guest_service, economy_service, inventory_service,
staff_service, validation_minute, _staffing_projection_enabled
) \
and _validate_runtime_path_crosslinks(
path_service, guest_service, staff_service, _movement_credits,
clock_validation_minute
) \
and _validate_authority_economy_crosslinks(
authority_service, economy_service,
int(saved[&"captured_authority_minute"])
) \
and _validate_defense_crosslinks(
defense_service, staff_service, facility_service,
int(saved[&"captured_defense_minute"])
) \
and _validate_construction_facility_crosslinks(
world_service, construction_service, facility_service,
facility_infrastructure_projector
) \
and _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
)
func _operations_external_state_matches(saved: Dictionary) -> bool:
if not _strict_variants_equal(_state.create_snapshot(), saved[&"game_state"]) \
or not _strict_variants_equal(command_queue.create_snapshot(), saved[&"command_queue"]) \
or _next_command_id != int(saved[&"next_command_id"]) \
or not _strict_variants_equal(facility_upkeep.create_snapshot(), saved[&"facility_upkeep"]) \
or not _strict_variants_equal(upkeep_settlement.create_snapshot(), saved[&"upkeep_settlement"]) \
or not _strict_variants_equal(economy_service.create_snapshot(), saved[&"economy"]) \
or not _strict_variants_equal(inventory_service.create_snapshot(), saved[&"inventory"]) \
or not _strict_variants_equal(kitchen_production.create_snapshot(), saved[&"kitchen_production"]) \
or not _strict_variants_equal(auto_purchase_plans.create_snapshot(), saved[&"auto_purchase_plans"]) \
or not _strict_variants_equal(facility_service.create_snapshot(), saved[&"facilities"]) \
or not _strict_variants_equal(facility_cleanliness.create_snapshot(), saved.get(&"facility_cleanliness", {})) \
or not _strict_variants_equal(guest_service.create_snapshot(), saved[&"guests"]) \
or not _strict_variants_equal(staff_service.create_snapshot(), saved[&"staff"]) \
or not _strict_variants_equal(relationship_service.create_snapshot(), saved[&"relationships"]) \
or not _strict_variants_equal(event_service.create_snapshot(), saved[&"events"]) \
or not _strict_variants_equal(authority_service.create_snapshot(), saved[&"authority"]) \
or not _strict_variants_equal(world_service.create_snapshot(), saved[&"world"]) \
or not _strict_variants_equal(path_service.create_snapshot(), saved[&"path"]) \
or not _strict_variants_equal(elevators.create_snapshot(), saved[&"elevators"]) \
or not _strict_variants_equal(legacy_stairs.create_snapshot(), saved[&"legacy_stairs"]) \
or not _strict_variants_equal(excavation_service.create_snapshot(), saved[&"excavation"]) \
or not _strict_variants_equal(spatial_subslot_service.create_snapshot(), saved[&"spatial_subslots"]) \
or not _strict_variants_equal(route_subslot_path_adapter.create_snapshot(), saved[&"route_projection"]) \
or not _strict_variants_equal(facility_infrastructure_projector.create_snapshot(), saved[&"facility_infrastructure"]) \
or not _strict_variants_equal(_movement_credits, saved[&"movement_credits"]) \
or not _strict_variants_equal(domain_events.create_snapshot(), saved[&"domain_events"]) \
or not _strict_variants_equal(id_generator.create_snapshot(), saved[&"id_generator"]) \
or not _strict_variants_equal(progression_service.create_snapshot(), saved[&"progression"]) \
or not _strict_variants_equal(mana_network_service.create_snapshot(), saved[&"mana"]) \
or not _strict_variants_equal(mana_reserve.create_snapshot(), saved[&"mana_reserve"]) \
or not _strict_variants_equal(logistics_dispatcher.create_snapshot(), saved[&"logistics_dispatcher"]) \
or not _strict_variants_equal(_serialize_logistics_requests(), saved[&"logistics_requests"]) \
or not _strict_variants_equal(defense_service.create_snapshot(), saved[&"defense"]) \
or not _strict_variants_equal(construction_service.create_snapshot(), saved[&"construction"]) \
or _staffing_projection_enabled != bool(saved[&"staffing_projection_enabled"]) \
or not _strict_variants_equal(_hired_staff_candidate_ids, saved[&"hired_staff_candidate_ids"]) \
or _personal_action_points != int(saved[&"personal_action_points"]):
return false
var saved_rngs: Dictionary = saved[&"rng_streams"]
if saved_rngs.size() != _rng_streams.size():
return false
for raw_stream_id: Variant in saved_rngs:
var stream := _rng_streams.get(StringName(raw_stream_id)) as DeterministicRng
if stream == null or not _strict_variants_equal(
stream.snapshot(), saved_rngs[raw_stream_id]
):
return false
return true
func _begin_operations_external_transaction() -> bool:
if _operations_transaction_active:
# One Coordinator minute can contain daily, demand and arrival boundaries.
# Keep their Runtime side effects in one notification/event transaction
# until the Coordinator has committed the complete minute.
return true
_operations_transaction_active = true
_operations_queued_domain_events.clear()
_operations_queued_guest_spawn_notifications.clear()
_operations_queued_economy_notifications.clear()
return true
func _commit_operations_domain_events() -> bool:
if not _operations_transaction_active:
return true
var seen: Dictionary = {}
for event: SimulationDomainEvent in _operations_queued_domain_events:
if event == null or not event.is_valid() or seen.has(event.event_id) \
or domain_events.has_published(event.event_id):
return false
seen[event.event_id] = true
var committed := domain_events.publish_batch(_operations_queued_domain_events)
if not committed:
return false
_operations_queued_domain_events.clear()
_operations_transaction_active = false
return true
func _operations_notifications_are_committable() -> bool:
if operations_coordinator == null or guest_service == null:
return false
var seen_guest_ids: Dictionary = {}
for guest_id: StringName in _operations_queued_guest_spawn_notifications:
if guest_id.is_empty() or seen_guest_ids.has(guest_id) \
or not guest_service.has_guest(guest_id) \
or not guest_service.has_pending_guest_spawned_notification(guest_id):
return false
seen_guest_ids[guest_id] = true
var seen_transaction_ids: Dictionary = {}
for transaction_id: StringName in _operations_queued_economy_notifications:
if transaction_id.is_empty() or seen_transaction_ids.has(transaction_id) \
or not economy_service.has_pending_transaction_notification(transaction_id):
return false
seen_transaction_ids[transaction_id] = true
return true
func _stage_operations_economy_notification(result: CommandResult) -> bool:
if result == null or not result.is_success():
return false
var transaction_id := StringName(result.details.get(&"transaction_id", &""))
if transaction_id.is_empty() \
or not economy_service.has_pending_transaction_notification(transaction_id) \
or _operations_queued_economy_notifications.has(transaction_id):
return false
_operations_queued_economy_notifications.append(transaction_id)
# Preserve Runtime's authoritative GameState projection now; the matching raw
# Economy signal is delivered to external observers only after commit.
_on_ledger_transaction_posted(
transaction_id, economy_service.get_transaction_entries(transaction_id)
)
return true
func _discard_operations_domain_events() -> void:
for guest_id: StringName in _operations_queued_guest_spawn_notifications:
guest_service.discard_guest_spawned_notification(guest_id)
for transaction_id: StringName in _operations_queued_economy_notifications:
economy_service.discard_transaction_notification(transaction_id)
_operations_queued_guest_spawn_notifications.clear()
_operations_flushing_guest_spawn_notifications.clear()
_operations_queued_economy_notifications.clear()
_operations_flushing_economy_notifications.clear()
_operations_queued_domain_events.clear()
_operations_transaction_active = false
func _commit_operations_notifications() -> bool:
if operations_coordinator == null:
return false
guest_service.set_external_mutation_blocked(true)
var coordinator_notifications_ok := operations_coordinator.commit_deferred_notifications()
var guest_notifications_ok := true
if coordinator_notifications_ok:
for guest_id: StringName in _operations_queued_guest_spawn_notifications:
_operations_flushing_guest_spawn_notifications[guest_id] = true
if not guest_service.commit_guest_spawned_notification(guest_id):
guest_notifications_ok = false
_operations_flushing_guest_spawn_notifications.erase(guest_id)
else:
guest_notifications_ok = false
var economy_notifications_ok := coordinator_notifications_ok and guest_notifications_ok
if economy_notifications_ok:
for transaction_id: StringName in _operations_queued_economy_notifications:
_operations_flushing_economy_notifications[transaction_id] = true
if not economy_service.commit_transaction_notification(transaction_id):
economy_notifications_ok = false
_operations_flushing_economy_notifications.erase(transaction_id)
guest_service.set_external_mutation_blocked(false)
_operations_queued_guest_spawn_notifications.clear()
_operations_flushing_guest_spawn_notifications.clear()
_operations_queued_economy_notifications.clear()
_operations_flushing_economy_notifications.clear()
return coordinator_notifications_ok and guest_notifications_ok and economy_notifications_ok
func _publish_domain_event(event: SimulationDomainEvent) -> bool:
if _construction_completion_transaction_active:
if event == null or not event.is_valid() or domain_events.has_published(event.event_id):
return false
for queued: SimulationDomainEvent in _construction_queued_domain_events:
if queued.event_id == event.event_id:
return false
_construction_queued_domain_events.append(event.copy())
return true
if not _operations_transaction_active:
return domain_events.publish(event)
if event == null or not event.is_valid() or domain_events.has_published(event.event_id):
return false
for queued: SimulationDomainEvent in _operations_queued_domain_events:
if queued.event_id == event.event_id:
return false
_operations_queued_domain_events.append(event.copy())
return true
func _operations_daily_boundary(
payload: Dictionary,
phase: StringName,
token: Variant
) -> Dictionary:
if phase == OperationsCoordinator.PHASE_CAPTURE:
var captured := _capture_operations_external_state()
return {&"ok": not captured.is_empty(), &"token": captured}
if phase == OperationsCoordinator.PHASE_VALIDATE:
return {
&"ok": token is Dictionary and not token.is_empty() \
and _operations_daily_payload_is_valid(payload),
&"token": token,
}
if phase == OperationsCoordinator.PHASE_APPLY:
return _apply_operations_daily_close(payload, token)
if phase == OperationsCoordinator.PHASE_ROLLBACK:
return {&"ok": _restore_operations_external_state(token)}
return {&"ok": false}
func _operations_daily_payload_is_valid(payload: Dictionary) -> bool:
for key: StringName in [
&"day", &"boundary_minute", &"period_start_minute",
&"period_end_minute_exclusive",
]:
if typeof(payload.get(key, null)) != TYPE_INT:
return false
var day := int(payload[&"day"])
var boundary_minute := int(payload[&"boundary_minute"])
return day > 0 \
and boundary_minute == _clock.absolute_minute \
and boundary_minute == operations_coordinator.get_daily_close_minute(day) \
and int(payload[&"period_start_minute"]) == operations_coordinator.get_financial_period_start(day) \
and int(payload[&"period_end_minute_exclusive"]) == boundary_minute
func _settle_upkeep_through(boundary_minute: int) -> Dictionary:
var upkeep_paid := 0
for period: Dictionary in facility_upkeep.get_closed_periods_from(upkeep_settlement.get_settled_period_count()):
if int(period[&"period_end"]) > boundary_minute:
continue
var upkeep_result := upkeep_settlement.settle(period, economy_service)
if not upkeep_result[&"ok"]:
return {&"ok": false, &"reason": &"UPKEEP_SETTLEMENT_FAILED"}
if not upkeep_result[&"replayed"]:
for row: Dictionary in upkeep_result[&"receipt"][&"rows"]:
upkeep_paid += int(row[&"paid"])
if not String(upkeep_result[&"transaction_id"]).is_empty() and not _stage_operations_economy_notification(CommandResult.succeeded(0, {&"transaction_id": upkeep_result[&"transaction_id"]})):
return {&"ok": false, &"reason": &"UPKEEP_NOTIFICATION_STAGE_FAILED"}
return {&"ok": true, &"paid_total": upkeep_paid}
func _operations_midnight_upkeep_boundary(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary:
if phase == OperationsCoordinator.PHASE_CAPTURE:
var captured := _capture_operations_external_state()
return {&"ok": not captured.is_empty(), &"token": captured}
if phase == OperationsCoordinator.PHASE_ROLLBACK:
return {&"ok": _restore_operations_external_state(token)}
var minute: Variant = payload.get(&"boundary_minute")
var valid: bool = token is Dictionary and not token.is_empty() and typeof(minute) == TYPE_INT \
and int(minute) == _clock.absolute_minute and int(minute) % 1440 == 0
if phase == OperationsCoordinator.PHASE_VALIDATE:
return {&"ok": valid, &"token": token}
if phase != OperationsCoordinator.PHASE_APPLY or not valid:
return {&"ok": false}
var prepared := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_PREPARE_PHASE, token)
if not bool(prepared.get(&"ok", false)):
return prepared
var midnight_due := _upkeep_debt_through(int(minute))
if midnight_due < 0:
return {&"ok": false, &"reason": &"UPKEEP_DUE_PREVIEW_FAILED"}
var midnight_top_up := top_up_front_fund(midnight_due, int(minute), StringName("finance.back_top_up.midnight.%d" % int(minute)))
if not midnight_top_up.is_success() or (int(midnight_top_up.details.get(&"amount", 0)) > 0 \
and not _stage_operations_economy_notification(midnight_top_up)):
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_FAILED"}
var result := _settle_upkeep_through(int(minute))
if not bool(result[&"ok"]):
return result
var after := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE, token)
if not bool(after.get(&"ok", false)):
return after
return {&"ok": upkeep_settlement.validate_ledger(economy_service) \
and int(_state.get_resource(&"gold")) == economy_service.get_balance(PLAYER_CASH_ACCOUNT)}
func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dictionary:
if not token is Dictionary or token.is_empty() \
or not _operations_daily_payload_is_valid(payload):
return {&"ok": false, &"reason": &"DAILY_CLOSE_PRECONDITION_FAILED"}
var prepare_hook := _run_operations_test_boundary_hook(
&"DAILY_CLOSE", payload, OPERATIONS_TEST_PREPARE_PHASE, token
)
if not bool(prepare_hook.get(&"ok", false)):
return prepare_hook
var day := int(payload[&"day"])
var boundary_minute := int(payload[&"boundary_minute"])
var preview := staff_service.preview_daily_payroll(day)
if not bool(preview.get(&"ok", false)) \
or not preview.get(&"rows", null) is Array:
return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_FAILED"}
# A09: the hidden fund first covers what the front fund cannot pay of the day's
# wages, upkeep, rank tax and loan interest (arrears included).
var finance_due := _daily_finance_due(economy_service, int(payload[&"period_start_minute"]), boundary_minute)
if not bool(finance_due[&"ok"]):
return finance_due
var payroll_due := 0
for raw_due: Variant in preview[&"rows"]:
if raw_due is Dictionary and typeof((raw_due as Dictionary).get(&"total_due", null)) == TYPE_INT:
payroll_due += maxi(int(raw_due[&"total_due"]), 0)
var upkeep_due := _upkeep_debt_through(boundary_minute)
if upkeep_due < 0:
return {&"ok": false, &"reason": &"UPKEEP_DUE_PREVIEW_FAILED"}
var top_up := top_up_front_fund(payroll_due + upkeep_due + int(finance_due[&"due_total"]), boundary_minute,
StringName("finance.back_top_up.day.%08d" % day))
if not top_up.is_success():
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_FAILED", &"result": top_up.to_dictionary()}
if int(top_up.details.get(&"amount", 0)) > 0 and not _stage_operations_economy_notification(top_up):
return {&"ok": false, &"reason": &"BACK_FUND_TOP_UP_NOTIFICATION_STAGE_FAILED"}
var available_gold := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var remaining_gold := available_gold
var payments: Dictionary = {}
var expected_paid_total := 0
var previous_staff_id := ""
for raw_row: Variant in preview[&"rows"]:
if not raw_row is Dictionary:
return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_INVALID"}
var row: Dictionary = raw_row
var staff_id_value: Variant = row.get(&"staff_id", null)
var total_due_value: Variant = row.get(&"total_due", null)
if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(staff_id_value).is_empty() \
or typeof(total_due_value) != TYPE_INT or int(total_due_value) < 0:
return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_INVALID"}
var staff_text := String(staff_id_value)
if not previous_staff_id.is_empty() and staff_text <= previous_staff_id:
return {&"ok": false, &"reason": &"PAYROLL_ORDER_INVALID"}
previous_staff_id = staff_text
var payment := mini(int(total_due_value), remaining_gold)
payments[StringName(staff_id_value)] = payment
remaining_gold -= payment
expected_paid_total += payment
var settlement_id := StringName("payroll-day-%08d" % day)
var settlement := staff_service.apply_daily_payroll_settlement(
settlement_id, day, payments
)
if not settlement.is_success() \
or int(settlement.details.get(&"paid_total", -1)) != expected_paid_total:
return {
&"ok": false,
&"reason": &"PAYROLL_SETTLEMENT_FAILED",
&"result": settlement.to_dictionary(),
}
if expected_paid_total > 0:
var wage_payment := economy_service.transfer(
PLAYER_CASH_ACCOUNT,
WAGE_EXPENSE_ACCOUNT,
expected_paid_total,
int(payload[&"boundary_minute"]),
&"WAGE",
settlement_id,
StringName("operations.wage.day.%08d" % day),
&"operations.payroll.settlement",
&"",
false
)
if not wage_payment.is_success():
return {
&"ok": false,
&"reason": &"WAGE_LEDGER_POST_FAILED",
&"result": wage_payment.to_dictionary(),
}
if not _stage_operations_economy_notification(wage_payment):
return {
&"ok": false,
&"reason": &"WAGE_NOTIFICATION_STAGE_FAILED",
&"result": wage_payment.to_dictionary(),
}
var upkeep_result := _settle_upkeep_through(int(payload[&"boundary_minute"]))
if not bool(upkeep_result[&"ok"]):
return upkeep_result
var upkeep_paid := int(upkeep_result[&"paid_total"])
var rank_tax := _post_daily_finance_charge(day, boundary_minute, int(finance_due[&"rank_tax"]),
FinancePolicy.RANK_TAX_EXPENSE, FinancePolicy.RANK_TAX_PAYABLE,
FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY, "rank_tax")
if not bool(rank_tax[&"ok"]):
return rank_tax
var loan_interest := _post_daily_finance_charge(day, boundary_minute, int(finance_due[&"loan_interest"]),
FinancePolicy.LOAN_INTEREST_EXPENSE, FinancePolicy.LOAN_INTEREST_PAYABLE,
FinancePolicy.LOAN_INTEREST_CATEGORY, FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY, "loan_interest")
if not bool(loan_interest[&"ok"]):
return loan_interest
var finance_paid := int(rank_tax[&"paid"]) + int(loan_interest[&"paid"])
var financials := _build_operations_daily_financials(
int(payload[&"period_start_minute"]),
int(payload[&"period_end_minute_exclusive"]),
int(preview.get(&"new_wages_total", -1)),
preview[&"rows"]
)
if not bool(financials.get(&"ok", false)):
return financials
var arrears_total := int(settlement.details.get(&"unpaid_after_total", -1))
if arrears_total < 0 \
or arrears_total != _staff_payroll_arrears_total(staff_service) \
or economy_service.get_balance(PLAYER_CASH_ACCOUNT) != available_gold - expected_paid_total - upkeep_paid - finance_paid \
or int(_state.get_resource(&"gold")) != economy_service.get_balance(PLAYER_CASH_ACCOUNT) \
or not bool(economy_service.validate_ledger().get(&"ok", false)) \
or not bool(staff_service.validate_invariants().get(&"ok", false)):
return {&"ok": false, &"reason": &"DAILY_CLOSE_POSTCONDITION_FAILED"}
var funds := _daily_fund_report(economy_service, day, int(payload[&"period_start_minute"]), boundary_minute)
if funds.is_empty():
return {&"ok": false, &"reason": &"DAILY_FUND_REPORT_FAILED"}
var settlement_report := settlement.details.duplicate(true)
settlement_report[&"upkeep_arrears_total"] = upkeep_settlement.get_arrears_total()
settlement_report[&"funds"] = funds
settlement_report[&"arrears_total"] = arrears_total + int(settlement_report[&"upkeep_arrears_total"]) \
+ int(funds[&"rank_tax_arrears"]) + int(funds[&"loan_interest_arrears"])
return {
&"ok": true,
&"category_totals": financials[&"category_totals"],
&"reconciliation_difference": financials[&"reconciliation_difference"],
&"facility_profit_loss": financials[&"facility_profit_loss"],
&"anomalies": financials[&"anomalies"],
&"settlement": settlement_report,
}
func _build_operations_daily_financials(
period_start: int,
period_end_exclusive: int,
new_wages_total: int,
payroll_rows: Array
) -> Dictionary:
if period_start < 0 or period_end_exclusive <= period_start or new_wages_total < 0:
return {&"ok": false, &"reason": &"DAILY_FINANCIAL_PERIOD_INVALID"}
if not _loan_principal_ledger_is_valid(economy_service):
return {&"ok": false, &"reason": &"INVALID_LOAN_PRINCIPAL_LEDGER"}
var category_totals := {
&"revenue": 0,
&"refunds": 0,
&"cost_of_goods": 0,
&"wages": new_wages_total,
&"maintenance": 0,
&"utilities": 0,
&"taxes_and_interest": 0,
&"fines": 0,
&"construction_cash_flow": 0,
}
var facility_rows: Dictionary = {}
var excluded_categories: Array[StringName] = [
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
]
var category_mapping := {
&"SERVICE_SALE": &"revenue",
&"REFUND": &"refunds",
&"CONSTRUCTION": &"construction_cash_flow",
&"CONSTRUCTION_REFUND": &"construction_cash_flow",
&"AUTHORITY_FINE": &"fines",
&"OPERATING_EXPENSE": &"cost_of_goods",
&"STAFF_SIGNING_COST": &"cost_of_goods",
&"EVENT_ACTION": &"cost_of_goods",
&"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods",
&"MAINTENANCE_ACCRUAL": &"maintenance",
FinancePolicy.RANK_TAX_CATEGORY: &"taxes_and_interest",
FinancePolicy.LOAN_INTEREST_CATEGORY: &"taxes_and_interest",
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
}
var operating_refund_total := 0
for entry: LedgerEntry in economy_service.get_entries():
if entry.absolute_minute < period_start \
or entry.absolute_minute >= period_end_exclusive:
continue
if entry.category in excluded_categories:
continue
if entry.category == &"OPERATING_REFUND":
operating_refund_total += entry.amount
continue
if not category_mapping.has(entry.category):
return {
&"ok": false,
&"reason": &"UNKNOWN_LEDGER_CATEGORY",
&"category": entry.category,
&"transaction_id": entry.transaction_id,
}
var category: StringName = category_mapping[entry.category]
category_totals[category] = int(category_totals.get(category, 0)) + entry.amount
if not entry.source_entity_id.is_empty() \
and (facility_service.has_instance(entry.source_entity_id) or entry.category == &"MAINTENANCE_ACCRUAL") \
and category not in [&"construction_cash_flow", &"financing_principal_inflows", &"financing_principal_outflows"]:
if not facility_rows.has(entry.source_entity_id):
facility_rows[entry.source_entity_id] = {
&"revenue": 0, &"expenses": 0, &"net_profit": 0,
}
var facility_row: Dictionary = facility_rows[entry.source_entity_id]
if category == &"revenue":
facility_row[&"revenue"] = int(facility_row[&"revenue"]) + entry.amount
else:
facility_row[&"expenses"] = int(facility_row[&"expenses"]) + entry.amount
var anomalies: Array[Dictionary] = []
var wage_allocation := _allocate_operations_wages_to_facilities(
period_start,
period_end_exclusive,
new_wages_total,
payroll_rows,
facility_rows
)
if not bool(wage_allocation.get(&"ok", false)):
return wage_allocation
anomalies.append_array(wage_allocation[&"anomalies"])
var operating_refund_offset := mini(
operating_refund_total,
int(category_totals[&"cost_of_goods"])
)
category_totals[&"cost_of_goods"] = int(category_totals[&"cost_of_goods"]) \
- operating_refund_offset
var excess_operating_refund := operating_refund_total - operating_refund_offset
if excess_operating_refund > 0:
category_totals[&"revenue"] = int(category_totals[&"revenue"]) \
+ excess_operating_refund
anomalies.append({
&"code": &"OPERATING_REFUND_EXCEEDS_PERIOD_COST",
&"refund_total": operating_refund_total,
&"cost_offset": operating_refund_offset,
&"excess_revenue": excess_operating_refund,
})
for facility_id: StringName in _sorted_name_keys(facility_rows):
var row: Dictionary = facility_rows[facility_id]
row[&"net_profit"] = int(row[&"revenue"]) - int(row[&"expenses"])
var allocation_rows: Array[Dictionary] = []
for facility_id: StringName in _sorted_name_keys(
wage_allocation[&"allocated_by_facility"]
):
allocation_rows.append({
&"facility_id": String(facility_id),
&"amount": int(wage_allocation[&"allocated_by_facility"][facility_id]),
})
var unallocated_rows: Array[Dictionary] = []
for staff_id: StringName in _sorted_name_keys(
wage_allocation[&"unallocated_by_staff"]
):
unallocated_rows.append({
&"staff_id": String(staff_id),
&"amount": int(wage_allocation[&"unallocated_by_staff"][staff_id]),
})
var profit_loss_facility_ids: Array[String] = []
for facility_id: StringName in _sorted_name_keys(facility_rows):
profit_loss_facility_ids.append(String(facility_id))
anomalies.append({
&"code": &"WAGE_ALLOCATION_EVIDENCE",
&"version": 1,
&"new_wages_total": new_wages_total,
&"allocated_total": int(wage_allocation[&"allocated_total"]),
&"unallocated_total": int(wage_allocation[&"unallocated_total"]),
&"allocations": allocation_rows,
&"unallocated_by_staff": unallocated_rows,
&"facility_ids": profit_loss_facility_ids,
})
var daily_summary := economy_service.get_daily_summary(period_start / 1_440 + 1)
if daily_summary.is_empty():
return {&"ok": false, &"reason": &"LEDGER_PERIOD_SUMMARY_MISSING"}
var difference := int(daily_summary.get(&"total_debit", 0)) \
- int(daily_summary.get(&"total_credit", 0))
return {
&"ok": true,
&"category_totals": category_totals,
&"reconciliation_difference": difference,
&"facility_profit_loss": facility_rows,
&"anomalies": anomalies,
}
func _allocate_operations_wages_to_facilities(
period_start: int,
period_end_exclusive: int,
new_wages_total: int,
payroll_rows: Array,
facility_rows: Dictionary
) -> Dictionary:
var new_wages_by_staff: Dictionary = {}
var observed_new_wages := 0
var previous_staff_text := ""
for raw_row: Variant in payroll_rows:
if not raw_row is Dictionary:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_INPUT_INVALID"}
var row: Dictionary = raw_row
var staff_id_value: Variant = row.get(&"staff_id", null)
var new_wages_value: Variant = row.get(&"new_wages", null)
if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(staff_id_value).is_empty() \
or typeof(new_wages_value) != TYPE_INT or int(new_wages_value) < 0:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_INPUT_INVALID"}
var staff_text := String(staff_id_value)
if not previous_staff_text.is_empty() and staff_text <= previous_staff_text:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_ORDER_INVALID"}
previous_staff_text = staff_text
new_wages_by_staff[StringName(staff_id_value)] = int(new_wages_value)
observed_new_wages += int(new_wages_value)
if observed_new_wages != new_wages_total:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_TOTAL_MISMATCH"}
var weights_by_staff: Dictionary = {}
for shift: ShiftInstance in staff_service.get_shifts():
if shift == null or shift.cancelled or shift.assigned_facility_id.is_empty() \
or not new_wages_by_staff.has(shift.staff_id) \
or int(new_wages_by_staff[shift.staff_id]) <= 0 \
or not facility_service.has_instance(shift.assigned_facility_id):
continue
var overlap := maxi(
mini(shift.end_at, period_end_exclusive) - maxi(shift.start_at, period_start),
0
)
if overlap <= 0:
continue
# Recorded duty is the strongest attribution fact. A future/unadvanced Shift
# still uses its scheduled overlap so daily wages never disappear merely
# because the Runtime was restored immediately before the close.
var recorded_minutes := shift.worked_minutes + shift.overtime_minutes
var weight := mini(recorded_minutes, overlap) if recorded_minutes > 0 else overlap
if weight <= 0:
continue
if not weights_by_staff.has(shift.staff_id):
weights_by_staff[shift.staff_id] = {}
var facility_weights: Dictionary = weights_by_staff[shift.staff_id]
facility_weights[shift.assigned_facility_id] = int(
facility_weights.get(shift.assigned_facility_id, 0)
) + weight
var anomalies: Array[Dictionary] = []
var allocated_total := 0
var allocated_by_facility: Dictionary = {}
var unallocated_by_staff: Dictionary = {}
for staff_id: StringName in _sorted_name_keys(new_wages_by_staff):
var wage := int(new_wages_by_staff[staff_id])
if wage <= 0:
continue
var weights: Dictionary = weights_by_staff.get(staff_id, {})
if weights.is_empty():
unallocated_by_staff[staff_id] = wage
anomalies.append({
&"code": &"UNALLOCATED_WAGES",
&"staff_id": staff_id,
&"amount": wage,
})
continue
var total_weight := 0
for facility_id: StringName in _sorted_name_keys(weights):
total_weight += int(weights[facility_id])
if total_weight <= 0:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_WEIGHT_INVALID"}
var allocations: Dictionary = {}
var floor_total := 0
var facility_ids := _sorted_name_keys(weights)
for facility_id: StringName in facility_ids:
var amount := wage * int(weights[facility_id]) / total_weight
allocations[facility_id] = amount
floor_total += amount
var remainder := wage - floor_total
for index in range(remainder):
var facility_id: StringName = facility_ids[index]
allocations[facility_id] = int(allocations[facility_id]) + 1
for facility_id: StringName in facility_ids:
var amount := int(allocations[facility_id])
if amount <= 0:
continue
if not facility_rows.has(facility_id):
facility_rows[facility_id] = {
&"revenue": 0, &"expenses": 0, &"net_profit": 0,
}
var facility_row: Dictionary = facility_rows[facility_id]
facility_row[&"expenses"] = int(facility_row[&"expenses"]) + amount
allocated_by_facility[facility_id] = int(
allocated_by_facility.get(facility_id, 0)
) + amount
allocated_total += amount
if allocated_total > new_wages_total:
return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_EXCEEDS_TOTAL"}
return {
&"ok": true,
&"allocated_total": allocated_total,
&"unallocated_total": new_wages_total - allocated_total,
&"allocated_by_facility": allocated_by_facility,
&"unallocated_by_staff": unallocated_by_staff,
&"anomalies": anomalies,
}
func _operations_arrival_boundary(
payload: Dictionary,
phase: StringName,
token: Variant
) -> Dictionary:
if phase == OperationsCoordinator.PHASE_CAPTURE:
var captured := _capture_operations_external_state()
var population_cap := (
int(operations_coordinator.get_config().get(&"population_cap", 0))
if operations_coordinator != null else 0
)
return {
&"ok": not captured.is_empty(),
&"token": captured,
&"capacity_remaining": maxi(population_cap - guest_service.guest_count(), 0) if int(_state.get_resource(&"mana")) > 0 and weather.permits_new_arrivals() else 0,
}
if phase == OperationsCoordinator.PHASE_VALIDATE:
return {
&"ok": token is Dictionary and not token.is_empty() \
and _operations_arrival_payload_is_valid(payload),
&"token": token,
}
if phase == OperationsCoordinator.PHASE_APPLY:
return _apply_operations_arrivals(payload, token)
if phase == OperationsCoordinator.PHASE_ROLLBACK:
return {&"ok": _restore_operations_external_state(token)}
return {&"ok": false}
func _operations_arrival_payload_is_valid(payload: Dictionary) -> bool:
var plans_value: Variant = payload.get(&"plans", null)
var total_value: Variant = payload.get(&"total_count", null)
var minute_value: Variant = payload.get(&"minute", null)
if not plans_value is Array or typeof(total_value) != TYPE_INT \
or int(total_value) < 0 or typeof(minute_value) != TYPE_INT \
or int(minute_value) != _clock.absolute_minute:
return false
var allowed_classes: Dictionary = {}
for class_id: StringName in mvp_content_catalog.get_guest_class_ids():
allowed_classes[class_id] = true
var species_ids := mvp_content_catalog.get_species_ids()
if allowed_classes.size() != 8 or species_ids.size() != 4:
return false
var seen_guest_ids: Dictionary = {}
var seen_plan_ids: Dictionary = {}
var counted := 0
for raw_plan: Variant in plans_value:
if not raw_plan is Dictionary:
return false
var plan: Dictionary = raw_plan
var plan_id_value: Variant = plan.get(&"plan_id", null)
var guest_class_value: Variant = plan.get(&"guest_class", null)
var facility_tag_value: Variant = plan.get(&"facility_tag", null)
var time_band_value: Variant = plan.get(&"time_band", null)
var plan_minute_value: Variant = plan.get(&"minute", null)
var count_value: Variant = plan.get(&"count", null)
var guest_ids_value: Variant = plan.get(&"guest_ids", null)
if typeof(plan_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or not _operations_sequence_id_is_valid(String(plan_id_value), "arrival-plan-") \
or seen_plan_ids.has(StringName(plan_id_value)) \
or typeof(guest_class_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or not allowed_classes.has(StringName(guest_class_value)) \
or typeof(facility_tag_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(facility_tag_value).is_empty() \
or typeof(time_band_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(time_band_value) not in OperationsCoordinator.TIME_BANDS \
or typeof(plan_minute_value) != TYPE_INT \
or int(plan_minute_value) != int(minute_value) \
or typeof(count_value) != TYPE_INT or int(count_value) <= 0 \
or not guest_ids_value is Array \
or guest_ids_value.size() != int(count_value):
return false
seen_plan_ids[StringName(plan_id_value)] = true
for raw_guest_id: Variant in guest_ids_value:
if typeof(raw_guest_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var guest_id := StringName(raw_guest_id)
var actor_id := _guest_path_actor_id(guest_id)
if not _operations_sequence_id_is_valid(String(guest_id), "guest-auto-") \
or seen_guest_ids.has(guest_id) or guest_service.has_guest(guest_id) \
or not guest_service.get_persistent_record(guest_id).is_empty() \
or economy_service.has_account(_guest_wallet_account(guest_id)) \
or path_service.has_actor_projection(actor_id) \
or _movement_credits.has(actor_id):
return false
seen_guest_ids[guest_id] = true
counted += 1
if counted != int(total_value):
return false
var population_cap := int(operations_coordinator.get_config().get(&"population_cap", 0))
return guest_service.guest_count() + counted <= population_cap \
and _operations_entrance_cell() != null
func _operations_sequence_id_is_valid(value: String, prefix: String) -> bool:
if not value.begins_with(prefix) or value.length() != prefix.length() + 8:
return false
var suffix := value.substr(prefix.length(), 8)
if not suffix.is_valid_int():
return false
var sequence := int(suffix)
return sequence > 0 and value == "%s%08d" % [prefix, sequence]
func _operations_entrance_cell() -> Variant:
var bounds := world_service.get_bounds()
for x in range(int(bounds.get(&"min_x", 0)), int(bounds.get(&"max_x", -1)) + 1):
var cell := Vector3i(x, 0, WorldService.LAYER_FRONT)
if not world_service.is_in_bounds(cell) \
or world_service.get_zone(cell) != WorldService.ZONE_BOUNDARY:
continue
var traffic := path_service.get_cell_traffic(cell)
if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)):
continue
return cell
return null
func _apply_operations_arrivals(payload: Dictionary, token: Variant) -> Dictionary:
if not token is Dictionary or token.is_empty() \
or not _operations_arrival_payload_is_valid(payload):
return {&"ok": false, &"reason": &"ARRIVAL_PRECONDITION_FAILED"}
var prepare_hook := _run_operations_test_boundary_hook(
&"ARRIVAL_PLAN", payload, OPERATIONS_TEST_PREPARE_PHASE, token
)
if not bool(prepare_hook.get(&"ok", false)):
return prepare_hook
if int(progression_service.metrics.get(&"guest_lifetime", 0)) \
> ProgressionService.MAX_SAFE_INTEGER - int(payload[&"total_count"]):
return {&"ok": false, &"reason": &"ARRIVAL_LIFETIME_LIMIT"}
var entrance_value: Variant = _operations_entrance_cell()
var spawn_rng := get_rng_stream(&"guest_spawn")
var species_ids := mvp_content_catalog.get_species_ids()
if entrance_value == null or spawn_rng == null or species_ids.size() != 4:
return {&"ok": false, &"reason": &"ARRIVAL_CONTENT_UNAVAILABLE"}
var entrance: Vector3i = entrance_value
var accepted_count := 0
var arrivals: Array[Dictionary] = []
for raw_plan: Variant in payload[&"plans"]:
var plan: Dictionary = raw_plan
var guest_class := StringName(plan[&"guest_class"])
for raw_guest_id: Variant in plan[&"guest_ids"]:
var guest_id := StringName(raw_guest_id)
var species := species_ids[spawn_rng.randi_range(0, species_ids.size() - 1)]
var definition_id := StringName("%s_%s" % [guest_class, species])
var definition := guest_service.get_definition(definition_id)
if definition == null or not definition.adult_verified \
or definition.wealth_min < 0 or definition.wealth_max < definition.wealth_min:
return {&"ok": false, &"reason": &"ARRIVAL_DEFINITION_INVALID"}
var wallet_gold := spawn_rng.randi_range(
definition.wealth_min, definition.wealth_max
)
var command := SimulationCommand.new(
1,
_clock.absolute_minute,
_clock.absolute_minute,
0,
&"spawn_guest",
&"operations_coordinator",
guest_id,
{
&"guest_id": guest_id,
&"guest_type_id": definition_id,
&"gold": wallet_gold,
&"options": {
&"current_cell": entrance,
&"needs": {
&"hunger": 0, &"sleep": 0, &"hygiene": 0,
&"medical": 0, &"leisure": 0, &"intimacy": 0,
},
&"planned_exit_at": _clock.absolute_minute + 720,
},
},
StringName("operations.arrival.%s" % guest_id)
)
var spawned := _execute_spawn_guest(command)
var guest := guest_service.get_guest(guest_id)
if not spawned.is_success() or guest == null \
or guest.guest_type_id != definition_id \
or guest.current_cell != entrance \
or guest.wallet_gold != wallet_gold \
or economy_service.get_balance(_guest_wallet_account(guest_id)) != wallet_gold:
return {
&"ok": false,
&"reason": &"ARRIVAL_SPAWN_FAILED",
&"guest_id": guest_id,
&"result": spawned.to_dictionary(),
}
var arrival_row := {
&"guest_id": guest_id,
&"plan_id": StringName(plan[&"plan_id"]),
&"guest_class": guest_class,
&"species": species,
&"definition_id": definition_id,
&"adult_verified": true,
&"wallet_gold": wallet_gold,
&"entrance_cell": entrance,
}
if not _publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"GUEST_AUTO_ARRIVED",
_clock.absolute_minute,
&"operations_coordinator",
[guest_id, definition_id],
arrival_row,
0
)):
return {&"ok": false, &"reason": &"ARRIVAL_EVENT_FAILED"}
arrivals.append(arrival_row)
accepted_count += 1
if accepted_count == 1:
var after_mutation_hook := _run_operations_test_boundary_hook(
&"ARRIVAL_PLAN",
payload,
OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE,
token
)
if not bool(after_mutation_hook.get(&"ok", false)):
return after_mutation_hook
var runtime_crosslinks_ok := _validate_runtime_crosslinks(
facility_service, guest_service, economy_service, inventory_service,
staff_service, _operations_external_committed_minute(),
_staffing_projection_enabled
)
var path_crosslinks_ok := _validate_runtime_path_crosslinks(
path_service, guest_service, staff_service, _movement_credits,
_clock.absolute_minute
)
if accepted_count != int(payload[&"total_count"]) \
or not runtime_crosslinks_ok or not path_crosslinks_ok:
return {
&"ok": false,
&"reason": &"ARRIVAL_POSTCONDITION_FAILED",
&"accepted_count": accepted_count,
&"expected_count": int(payload[&"total_count"]),
&"runtime_crosslinks_ok": runtime_crosslinks_ok,
&"path_crosslinks_ok": path_crosslinks_ok,
}
return {
&"ok": true,
&"accepted_count": accepted_count,
}
func _operations_morning_boundary(
payload: Dictionary,
phase: StringName,
token: Variant
) -> Dictionary:
if phase == OperationsCoordinator.PHASE_CAPTURE:
var captured := _capture_operations_external_state()
return {&"ok": not captured.is_empty(), &"token": captured}
if phase == OperationsCoordinator.PHASE_VALIDATE:
return {
&"ok": token is Dictionary and not token.is_empty() \
and _operations_morning_payload_is_valid(payload),
&"token": token,
}
if phase == OperationsCoordinator.PHASE_APPLY:
return _apply_operations_morning(payload, token)
if phase == OperationsCoordinator.PHASE_ROLLBACK:
return {&"ok": _restore_operations_external_state(token)}
return {&"ok": false}
func _operations_morning_payload_is_valid(payload: Dictionary) -> bool:
var day_value: Variant = payload.get(&"day", null)
var minute_value: Variant = payload.get(&"boundary_minute", null)
if typeof(day_value) != TYPE_INT or int(day_value) <= 0 \
or typeof(minute_value) != TYPE_INT:
return false
var minute := int(minute_value)
return minute == _clock.absolute_minute \
and posmod(minute, SimulationKernel.MINUTES_PER_DAY) == OPERATIONS_START_MINUTE \
and int(day_value) == minute / SimulationKernel.MINUTES_PER_DAY + 1
func _apply_operations_morning(payload: Dictionary, token: Variant) -> Dictionary:
if not token is Dictionary or token.is_empty() \
or not _operations_morning_payload_is_valid(payload):
return {&"ok": false, &"reason": &"MORNING_PRECONDITION_FAILED"}
var prepare_hook := _run_operations_test_boundary_hook(
&"MORNING_BOUNDARY", payload, OPERATIONS_TEST_PREPARE_PHASE, token
)
if not bool(prepare_hook.get(&"ok", false)):
return prepare_hook
var minute := int(payload[&"boundary_minute"])
var before_ap := _personal_action_points
_personal_action_points = mini(
_personal_action_points + 2,
MAX_PERSONAL_ACTION_POINTS
)
var authority_advance := authority_service.advance_time(minute)
if not authority_advance.is_success():
return {
&"ok": false,
&"reason": &"MORNING_AUTHORITY_ADVANCE_FAILED",
&"result": authority_advance.to_dictionary(),
}
var authority_evaluation := authority_service.evaluate_inspection(minute)
if not authority_evaluation.is_success():
return {
&"ok": false,
&"reason": &"MORNING_AUTHORITY_EVALUATION_FAILED",
&"result": authority_evaluation.to_dictionary(),
}
if not authority_evaluation.is_replay:
_publish_authority_evaluation(authority_evaluation.details)
_enqueue_pending_relationship_candidates()
# start_next_event owns exactly one expiry pass and one actor audit pass.
# Calling those helpers separately here would double-apply morning effects.
var event_start := event_service.start_next_event(
minute,
_build_event_context(minute)
)
var no_event_ready := (
not event_start.is_success()
and event_start.message == "No eligible Event candidate is ready."
)
var foreground_already_active := (
not event_start.is_success()
and event_start.message == "A foreground Event is already active."
)
if not event_start.is_success() and not no_event_ready and not foreground_already_active:
return {
&"ok": false,
&"reason": &"MORNING_EVENT_START_FAILED",
&"result": event_start.to_dictionary(),
}
if _personal_action_points < 0 \
or _personal_action_points > MAX_PERSONAL_ACTION_POINTS \
or authority_service.current_minute != minute \
or not bool(authority_service.validate_invariants().get(&"ok", false)) \
or not bool(event_service.validate_invariants().get(&"ok", false)) \
or not bool(relationship_service.validate_invariants().get(&"ok", false)):
return {&"ok": false, &"reason": &"MORNING_POSTCONDITION_FAILED"}
return {
&"ok": true,
&"event_payload": {
&"personal_action_points_before": before_ap,
&"personal_action_points_after": _personal_action_points,
&"authority_advance": authority_advance.to_dictionary(),
&"authority_evaluation": authority_evaluation.to_dictionary(),
&"event_start": event_start.to_dictionary(),
&"no_event_ready": no_event_ready,
&"foreground_already_active": foreground_already_active,
},
}
func _create_operations_runtime_snapshot() -> Dictionary:
return {
&"wrapper_version": OPERATIONS_RUNTIME_WRAPPER_VERSION,
&"enabled": _operations_enabled,
&"personal_action_points": _personal_action_points,
&"runtime_failure_latched": _operations_runtime_failure_latched,
&"runtime_failure_report": _operations_runtime_failure_report.duplicate(true),
&"deferred_runtime_minute": _operations_deferred_runtime_minute,
&"coordinator": (
operations_coordinator.create_snapshot()
if _operations_enabled and operations_coordinator != null else {}
),
}
func _parse_operations_runtime_snapshot(
raw_wrapper: Variant,
runtime_schema_version: int,
restored_minute: int,
restored_guests: GuestService,
restored_staff: StaffService,
restored_economy: EconomyService,
restored_world_seed: int,
restored_clock_paused: Variant,
restored_upkeep_settlement: FacilityUpkeepSettlement
) -> Dictionary:
var disabled := {
&"ok": true,
&"enabled": false,
&"personal_action_points": 0,
&"runtime_failure_latched": false,
&"runtime_failure_report": {},
&"deferred_runtime_minute": -1,
&"coordinator": null,
}
if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION:
return disabled
if typeof(restored_clock_paused) != TYPE_BOOL \
or not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT):
return {&"ok": false}
if not raw_wrapper is Dictionary:
return {&"ok": false}
var wrapper: Dictionary = raw_wrapper
var expected_keys: Array[StringName] = [
&"wrapper_version", &"enabled", &"personal_action_points",
&"runtime_failure_latched", &"runtime_failure_report",
&"deferred_runtime_minute", &"coordinator",
]
if not _dictionary_has_exact_canonical_keys(wrapper, expected_keys):
return {&"ok": false}
if typeof(wrapper[&"wrapper_version"]) != TYPE_INT \
or int(wrapper[&"wrapper_version"]) != OPERATIONS_RUNTIME_WRAPPER_VERSION \
or typeof(wrapper[&"enabled"]) != TYPE_BOOL \
or typeof(wrapper[&"personal_action_points"]) != TYPE_INT \
or typeof(wrapper[&"runtime_failure_latched"]) != TYPE_BOOL \
or not wrapper[&"runtime_failure_report"] is Dictionary \
or typeof(wrapper[&"deferred_runtime_minute"]) != TYPE_INT \
or not wrapper[&"coordinator"] is Dictionary:
return {&"ok": false}
var enabled := bool(wrapper[&"enabled"])
var action_points := int(wrapper[&"personal_action_points"])
var failure_latched := bool(wrapper[&"runtime_failure_latched"])
var failure_report: Dictionary = wrapper[&"runtime_failure_report"]
var deferred_runtime_minute := int(wrapper[&"deferred_runtime_minute"])
var coordinator_snapshot: Dictionary = wrapper[&"coordinator"]
if action_points < 0 or action_points > MAX_PERSONAL_ACTION_POINTS \
or failure_latched != not failure_report.is_empty() \
or (failure_latched \
and not _operations_failure_report_is_valid(failure_report, restored_minute)):
return {&"ok": false}
if not enabled:
if action_points != 0 or failure_latched or deferred_runtime_minute != -1 \
or not coordinator_snapshot.is_empty():
return {&"ok": false}
return disabled
var restored_coordinator := OperationsCoordinator.new()
if not restored_coordinator.restore_snapshot(coordinator_snapshot):
return {&"ok": false}
var coordinator_current_matches := restored_coordinator.current_minute == restored_minute
var pending_boundary_minute := int(
coordinator_snapshot.get(&"pending_boundary_minute", -1)
)
var deferred_boundary_minute := int(
coordinator_snapshot.get(&"deferred_boundary_minute", -1)
)
var normal_shape := deferred_runtime_minute == -1 \
and not failure_latched and not restored_coordinator.pause_requested \
and not restored_coordinator.is_terminal() \
and coordinator_current_matches
var failure_shape := deferred_runtime_minute == restored_minute \
and failure_latched and bool(restored_clock_paused) \
and restored_coordinator.current_minute == restored_minute - 1 \
and pending_boundary_minute == restored_minute
var pause_shape := deferred_runtime_minute == restored_minute \
and not failure_latched and bool(restored_clock_paused) \
and restored_coordinator.pause_requested and coordinator_current_matches \
and deferred_boundary_minute == restored_minute
var terminal_shape := deferred_runtime_minute == -1 \
and not failure_latched and restored_coordinator.is_terminal() \
and coordinator_current_matches and bool(restored_clock_paused)
if not bool(restored_coordinator.validate_invariants().get(&"ok", false)) \
or restored_coordinator.get_origin_minute() != OPERATIONS_COORDINATOR_ORIGIN_MINUTE \
or not (normal_shape or failure_shape or pause_shape or terminal_shape) \
or restored_coordinator.get_population_count() != restored_guests.guest_count() \
or restored_coordinator.get_arrears_total() != (_staff_payroll_arrears_total(restored_staff) + restored_upkeep_settlement.get_arrears_through(restored_coordinator.get_daily_close_minute(restored_coordinator.get_open_financial_day() - 1))) \
or not _operations_payroll_crosslinks_are_valid(
restored_coordinator, restored_staff, restored_economy, restored_upkeep_settlement
) \
or not _operations_coordinator_configuration_is_canonical(
restored_coordinator, restored_world_seed
) \
or not _operations_arrival_guest_crosslinks_are_valid(
restored_coordinator, restored_guests
) \
or (restored_coordinator.is_external_quarantined() \
and not (failure_shape or pause_shape)):
return {&"ok": false}
return {
&"ok": true,
&"enabled": true,
&"personal_action_points": action_points,
&"runtime_failure_latched": failure_latched,
&"runtime_failure_report": failure_report.duplicate(true),
&"deferred_runtime_minute": deferred_runtime_minute,
&"coordinator": restored_coordinator,
}
func _progression_guest_lifetime_matches(
progression: ProgressionService,
guests: GuestService
) -> bool:
if progression == null or guests == null:
return false
var guest_snapshot := guests.create_snapshot()
var persistent_value: Variant = guest_snapshot.get(&"persistent_records", null)
var metrics_value: Variant = progression.create_snapshot().get(&"metrics", null)
if not persistent_value is Array or not metrics_value is Dictionary:
return false
var lifetime_value: Variant = (metrics_value as Dictionary).get(&"guest_lifetime", null)
return typeof(lifetime_value) == TYPE_INT \
and int(lifetime_value) == guests.guest_count() + persistent_value.size()
func _operations_arrival_guest_crosslinks_are_valid(
coordinator: OperationsCoordinator,
guests: GuestService
) -> bool:
if coordinator == null or guests == null:
return false
var definitions_by_guest: Dictionary = {}
for guest: GuestRuntime in guests.get_guests():
definitions_by_guest[guest.guest_id] = guest.guest_type_id
var guest_snapshot := guests.create_snapshot()
var persistent_value: Variant = guest_snapshot.get(&"persistent_records", null)
if not persistent_value is Array:
return false
for raw_record: Variant in persistent_value:
if not raw_record is Dictionary:
return false
var guest_id := StringName((raw_record as Dictionary).get(&"guest_id", &""))
var definition_id := StringName(
(raw_record as Dictionary).get(&"guest_type_id", &"")
)
if guest_id.is_empty() or definition_id.is_empty() \
or definitions_by_guest.has(guest_id):
return false
definitions_by_guest[guest_id] = definition_id
var history_ids: Dictionary = {}
var expected_sequence := 1
for report: Dictionary in coordinator.get_arrival_reports():
var plans_value: Variant = report.get(&"plans", null)
if not plans_value is Array:
return false
for raw_plan: Variant in plans_value:
if not raw_plan is Dictionary:
return false
var plan: Dictionary = raw_plan
var guest_class := StringName(plan.get(&"guest_class", &""))
var ids_value: Variant = plan.get(&"guest_ids", null)
if guest_class.is_empty() or not ids_value is Array:
return false
for raw_guest_id: Variant in ids_value:
if typeof(raw_guest_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var guest_id := StringName(raw_guest_id)
if guest_id != StringName("guest-auto-%08d" % expected_sequence) \
or history_ids.has(guest_id) \
or not definitions_by_guest.has(guest_id):
return false
var definition_id := String(definitions_by_guest[guest_id])
if not definition_id.begins_with("%s_" % String(guest_class)):
return false
history_ids[guest_id] = true
expected_sequence += 1
for raw_guest_id: Variant in definitions_by_guest:
var guest_id := StringName(raw_guest_id)
if String(guest_id).begins_with("guest-auto-") \
and not history_ids.has(guest_id):
return false
return true
func _guest_aggregate_has_reserved_operations_id(guests: GuestService) -> bool:
if guests == null:
return true
for guest: GuestRuntime in guests.get_guests():
if String(guest.guest_id).begins_with("guest-auto-"):
return true
var records: Variant = guests.create_snapshot().get(&"persistent_records", null)
if not records is Array:
return true
for raw_record: Variant in records:
if not raw_record is Dictionary \
or String((raw_record as Dictionary).get(&"guest_id", "")).begins_with(
"guest-auto-"
):
return true
return false
func _operations_payroll_crosslinks_are_valid(
coordinator: OperationsCoordinator,
staff: StaffService,
economy: EconomyService,
upkeep: FacilityUpkeepSettlement
) -> bool:
if coordinator == null or staff == null or economy == null:
return false
var staff_snapshot := staff.create_snapshot()
var settlement_values: Variant = staff_snapshot.get(&"payroll_settlements", null)
if not settlement_values is Array:
return false
var summaries := coordinator.get_day_summaries()
if summaries.size() != settlement_values.size():
return false
var expected_wage_transactions: Dictionary = {}
var settlements_by_day: Dictionary = {}
for raw_settlement: Variant in settlement_values:
if not raw_settlement is Dictionary:
return false
var serialized: Dictionary = raw_settlement
var day_value: Variant = serialized.get(&"day", null)
var settlement_id_value: Variant = serialized.get(&"settlement_id", null)
if typeof(day_value) != TYPE_INT \
or typeof(settlement_id_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var day := int(day_value)
var settlement_id := StringName(settlement_id_value)
if day <= 0 or settlement_id != StringName("payroll-day-%08d" % day) \
or settlements_by_day.has(day):
return false
var staff_record := staff.get_payroll_settlement(settlement_id)
if staff_record.is_empty():
return false
settlements_by_day[day] = staff_record
var paid_total := int(staff_record.get(&"paid_total", -1))
if paid_total < 0:
return false
var idempotency_key := StringName("operations.wage.day.%08d" % day)
var transaction_id := economy.get_transaction_id_for_idempotency_key(idempotency_key)
if paid_total == 0:
if not transaction_id.is_empty():
return false
continue
if transaction_id.is_empty() or expected_wage_transactions.has(transaction_id):
return false
var entries := economy.get_transaction_entries(transaction_id)
if entries.size() != 1:
return false
var entry: LedgerEntry = entries[0]
if entry.absolute_minute != coordinator.get_daily_close_minute(day) \
or entry.debit_account != WAGE_EXPENSE_ACCOUNT \
or entry.credit_account != PLAYER_CASH_ACCOUNT \
or entry.amount != paid_total or entry.category != &"WAGE" \
or entry.source_entity_id != settlement_id \
or entry.memo_key != &"operations.payroll.settlement" \
or not entry.related_transaction_id.is_empty():
return false
expected_wage_transactions[transaction_id] = true
for summary: Dictionary in summaries:
var day := int(summary.get(&"day", -1))
if not settlements_by_day.has(day):
return false
var summary_settlement_value: Variant = summary.get(&"settlement", null)
if not summary_settlement_value is Dictionary:
return false
var summary_settlement: Dictionary = summary_settlement_value.duplicate(true)
var staff_record: Dictionary = settlements_by_day[day]
var expected_categories: Variant = _operations_expected_category_totals(
economy,
coordinator.get_financial_period_start(day),
coordinator.get_daily_close_minute(day),
int(staff_record.get(&"new_wages_total", -1))
)
if expected_categories == null \
or not _strict_variants_equal(
_operations_snapshot_comparison_value(
summary.get(&"category_totals", {})
),
_operations_snapshot_comparison_value(expected_categories)
) \
or not _operations_facility_wage_summary_is_valid(
summary, staff_record, staff, economy
):
return false
var upkeep_debt := 0
for receipt: Dictionary in upkeep.get_report()[&"receipts"]:
if int(receipt[&"period"][&"period_end"]) <= coordinator.get_daily_close_minute(day):
upkeep_debt = 0
for row: Dictionary in receipt[&"rows"]:
upkeep_debt += int(row[&"arrears"])
if summary_settlement.has(&"upkeep_arrears_total"):
if typeof(summary_settlement[&"upkeep_arrears_total"]) != TYPE_INT or int(summary_settlement[&"upkeep_arrears_total"]) != upkeep_debt:
return false
summary_settlement.erase(&"upkeep_arrears_total")
elif upkeep_debt != 0:
return false
var finance_arrears := 0
# A saved report is checked by the definition it was written with: without
# the mark it is version 1; any other mark is not a report this game wrote.
var fund_version := FUND_REPORT_VERSION
if summary_settlement.get(&"funds", null) is Dictionary:
var saved_funds: Dictionary = summary_settlement[&"funds"]
var mark: Variant = saved_funds.get(&"fund_report_version", saved_funds.get("fund_report_version", null))
if mark == null:
fund_version = 1
elif typeof(mark) != TYPE_INT or int(mark) != FUND_REPORT_VERSION:
return false
var expected_funds := _daily_fund_report(economy, day, coordinator.get_financial_period_start(day),
coordinator.get_daily_close_minute(day), fund_version)
if expected_funds.is_empty():
return false
if summary_settlement.has(&"funds"):
if not _strict_variants_equal(_operations_snapshot_comparison_value(summary_settlement[&"funds"]),
_operations_snapshot_comparison_value(expected_funds)):
return false
finance_arrears = int(expected_funds[&"rank_tax_arrears"]) + int(expected_funds[&"loan_interest_arrears"])
summary_settlement.erase(&"funds")
elif bool(expected_funds[&"has_finance_postings"]):
# Days closed before the funds were split carry no fund report and no postings.
return false
if int(summary_settlement.get(&"arrears_total", -1)) \
!= int(staff_record.get(&"unpaid_after_total", -2)) + upkeep_debt + finance_arrears \
or summary_settlement.get(&"replayed", null) != false:
return false
summary_settlement.erase(&"arrears_total")
summary_settlement.erase(&"replayed")
# OperationsCoordinator persists history in a transport-safe form, so
# StringName identifiers become String values after a round-trip. Compare
# the redundant Staff/Coordinator evidence in that same canonical form;
# numeric and structural types remain strict.
if not _strict_variants_equal(
_operations_snapshot_comparison_value(summary_settlement),
_operations_snapshot_comparison_value(staff_record)
):
return false
for entry: LedgerEntry in economy.get_entries():
if entry.category == &"WAGE" \
and not expected_wage_transactions.has(entry.transaction_id):
return false
return true
## Existing liabilities only. This projection does not issue financing or infer terms.
func get_loan_principal_report() -> Dictionary:
if not _initialized or economy_service == null or _state == null:
return {&"available": false, &"loans": []}
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var pending: Dictionary = {}
for command: SimulationCommand in command_queue.get_pending_commands():
if command.command_type != &"repay_loan_principal":
continue
var id: Variant = command.payload.get(&"loan_id", null)
if typeof(id) not in [TYPE_STRING, TYPE_STRING_NAME]:
continue
var key := StringName(id)
if not pending.has(key): pending[key] = []
pending[key].append(command.command_id)
var loans: Array[Dictionary] = []
for account: Dictionary in economy_service.get_accounts():
var id := String(account[&"account_id"])
if not id.begins_with("LOAN_LIABILITY:"):
continue
var loan := StringName(id.trim_prefix("LOAN_LIABILITY:"))
loans.append({&"loan_id": loan, &"outstanding_principal": -economy_service.get_balance(StringName(id)), &"pending_command_ids": (pending.get(loan, []) as Array).duplicate()})
loans.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"loan_id"]) < String(right[&"loan_id"]))
var valid: bool = _state.get_resource(&"gold") == cash and (loans.is_empty() or _loan_principal_ledger_is_valid(economy_service))
var ended: bool = _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal()
return {&"available": true, &"ledger_valid": valid, &"terminal": ended, &"cash": cash, &"next_command_id": _next_command_id, &"loans": loans}
## Atomic principal payment against an already attested loan liability.
## Loan issuance, rates, due dates and bank eligibility belong to the bank
## producer; this API does not create or approve any financing contract.
func repay_loan_principal(
loan_id: StringName,
amount: Variant,
idempotency_key: StringName
) -> CommandResult:
if not _initialized or economy_service == null or _state == null:
return CommandResult.failed(CommandResult.Code.EXECUTOR_FAILED, 0, "Simulation runtime is not initialized.")
if _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Repayment is unavailable after the game ends.")
if loan_id.is_empty() or idempotency_key.is_empty() or typeof(amount) != TYPE_INT \
or amount <= 0 or amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, 0, "A loan id, positive integer principal and idempotency key are required.")
if not economy_service.get_transaction_id_for_idempotency_key(idempotency_key).is_empty():
return CommandResult.failed(CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, 0, "The principal payment key has already been posted.")
if not _loan_principal_ledger_is_valid(economy_service):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "The principal ledger is inconsistent.")
var liability := StringName("LOAN_LIABILITY:" + String(loan_id))
if not economy_service.has_account(liability):
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.")
var remaining := -economy_service.get_balance(liability)
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
if amount > remaining or amount > FinancePolicy.available_funds(economy_service) or _state.get_resource(&"gold") != cash:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Principal payment exceeds the outstanding principal or available Gold, or its mirror is inconsistent.")
# A09: what the front fund cannot pay comes from the hidden fund in the same
# balanced posting, so the payment and its top-up are atomic. Defer receipts
# until the Gold mirror is synchronized, also respecting any enclosing effect scope.
var minute := _runtime_event_minute()
var entries: Array = []
if amount > cash:
entries.append(LedgerEntry.new().configure(&"", minute, PLAYER_CASH_ACCOUNT, FinancePolicy.BACK_CASH,
amount - cash, FinancePolicy.TOP_UP_CATEGORY, loan_id, &"", &"finance.back_fund.top_up"))
entries.append(LedgerEntry.new().configure(&"", minute, liability, PLAYER_CASH_ACCOUNT,
amount, &"LOAN_PRINCIPAL_REPAID", loan_id))
economy_service.begin_notification_scope()
var result := economy_service.post(entries, idempotency_key)
if result.is_success():
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
result.details[&"loan_id"] = loan_id
result.details[&"paid_principal"] = amount
result.details[&"outstanding_principal"] = remaining - amount
economy_service.end_notification_scope()
return result
## A09: what the front fund cannot pay is covered from the hidden fund first. The move
## is posted on its own (BACK_FUND_TOP_UP) and is neither income nor expense.
func top_up_front_fund(due: int, minute: int, idempotency_key: StringName) -> CommandResult:
var front := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var amount := mini(maxi(due - front, 0), economy_service.get_balance(FinancePolicy.BACK_CASH))
if amount <= 0:
return CommandResult.succeeded(0, {&"amount": 0})
var result := economy_service.transfer(FinancePolicy.BACK_CASH, PLAYER_CASH_ACCOUNT, amount, minute,
FinancePolicy.TOP_UP_CATEGORY, &"", idempotency_key, &"finance.back_fund.top_up", &"", false)
# Callers publish the notification, which also refreshes the Gold projection.
if result.is_success():
result.details[&"amount"] = amount
return result
## Whole Gold the closed upkeep periods through the minute would owe, arrears
## included; -1 if a period cannot be planned. Nothing is changed.
func _upkeep_debt_through(minute: int) -> int:
var periods: Array = []
for period: Dictionary in facility_upkeep.get_closed_periods_from(upkeep_settlement.get_settled_period_count()):
if int(period[&"period_end"]) <= minute:
periods.append(period)
return upkeep_settlement.preview_debt(periods)
## A10/A11: the closed day's rank tax and loan interest, with their older arrears.
func _daily_finance_due(economy: EconomyService, period_start: int, boundary_minute: int) -> Dictionary:
var totals: Variant = _operations_expected_category_totals(economy, period_start, boundary_minute, 0)
if totals == null:
return {&"ok": false, &"reason": &"DAILY_SALES_UNAVAILABLE"}
var tax := FinancePolicy.rank_tax(int(totals[&"revenue"]))
var interest := FinancePolicy.loan_interest(FinancePolicy.outstanding_principal(economy))
return {&"ok": true, &"rank_tax": tax, &"loan_interest": interest,
&"due_total": tax + interest - economy.get_balance(FinancePolicy.RANK_TAX_PAYABLE)
- economy.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE)}
## Accrues a daily charge at the end of the closed day and pays its older arrears
## first, then the new amount, from the front fund; the rest stays payable.
func _post_daily_finance_charge(day: int, boundary_minute: int, new_amount: int, expense: StringName,
payable: StringName, category: StringName, payment_category: StringName, kind: String) -> Dictionary:
var due := new_amount - economy_service.get_balance(payable)
var paid := mini(due, economy_service.get_balance(PLAYER_CASH_ACCOUNT))
var source := StringName("finance-day-%08d" % day)
var entries: Array = []
if new_amount > 0:
entries.append(LedgerEntry.new().configure(&"", boundary_minute - 1, expense, payable, new_amount,
category, source, &"", StringName("finance.%s.accrued" % kind)))
if paid > 0:
entries.append(LedgerEntry.new().configure(&"", boundary_minute, payable, PLAYER_CASH_ACCOUNT, paid,
payment_category, source, &"", StringName("finance.%s.paid" % kind)))
if not entries.is_empty():
var posted := economy_service.post(entries, StringName("finance.%s.day.%08d" % [kind, day]), &"", false)
if not posted.is_success():
return {&"ok": false, &"reason": &"FINANCE_LEDGER_POST_FAILED", &"result": posted.to_dictionary()}
if not _stage_operations_economy_notification(posted):
return {&"ok": false, &"reason": &"FINANCE_NOTIFICATION_STAGE_FAILED"}
return {&"ok": true, &"new": new_amount, &"paid": paid, &"arrears": due - paid}
## The fund side of a closed day, derived only from the ledger, so the live close
## and a restored save produce the same report. Version 2 (marked in the report):
## back_top_up is every top-up of the business day, the operation-time purchases
## and repayments and the legacy 00:00 upkeep as well as the close's own. A report
## without the mark is version 1, written while back_top_up counted only the
## close's own top-up; a restore checks it by that definition, not a looser one.
func _daily_fund_report(economy: EconomyService, day: int, period_start: int, boundary_minute: int,
version := FUND_REPORT_VERSION) -> Dictionary:
if economy == null or day <= 0 or period_start < 0 or boundary_minute <= period_start \
or version not in [1, FUND_REPORT_VERSION]:
return {}
var report := {
&"revenue_front": 0, &"revenue_back": 0, &"back_top_up": 0,
&"rank_tax": 0, &"rank_tax_paid": 0, &"loan_interest": 0, &"loan_interest_paid": 0,
}
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute >= period_start and entry.absolute_minute < boundary_minute \
and entry.category == &"SERVICE_CASH_SWEEP":
if entry.debit_account == FinancePolicy.BACK_CASH:
report[&"revenue_back"] = int(report[&"revenue_back"]) + entry.amount
elif entry.debit_account == PLAYER_CASH_ACCOUNT:
report[&"revenue_front"] = int(report[&"revenue_front"]) + entry.amount
var keyed := {
&"back_top_up": StringName("finance.back_top_up.day.%08d" % day),
&"rank_tax": StringName("finance.rank_tax.day.%08d" % day),
&"loan_interest": StringName("finance.loan_interest.day.%08d" % day),
}
var postings := false
for field: StringName in keyed:
var transaction := economy.get_transaction_id_for_idempotency_key(keyed[field])
if transaction.is_empty():
continue
postings = true
for entry: LedgerEntry in economy.get_transaction_entries(transaction):
if field == &"back_top_up" and entry.category == FinancePolicy.TOP_UP_CATEGORY:
report[field] = int(report[field]) + entry.amount
elif field != &"back_top_up" and entry.category in [FinancePolicy.RANK_TAX_CATEGORY, FinancePolicy.LOAN_INTEREST_CATEGORY]:
report[field] = int(report[field]) + entry.amount
elif field != &"back_top_up" and entry.category in [FinancePolicy.RANK_TAX_PAYMENT_CATEGORY, FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY]:
report[StringName(String(field) + "_paid")] = int(report[StringName(String(field) + "_paid")]) + entry.amount
else:
return {}
if version == FUND_REPORT_VERSION:
# The closes are keyed; the previous day's close is posted at period_start.
var closes := {}
for close_day: int in [day - 1, day]:
var close := economy.get_transaction_id_for_idempotency_key(StringName("finance.back_top_up.day.%08d" % close_day))
if not close.is_empty():
closes[close] = true
for entry: LedgerEntry in economy.get_entries():
if entry.category == FinancePolicy.TOP_UP_CATEGORY and entry.absolute_minute >= period_start \
and entry.absolute_minute < boundary_minute and not closes.has(entry.transaction_id):
report[&"back_top_up"] = int(report[&"back_top_up"]) + entry.amount
postings = true
report[&"fund_report_version"] = FUND_REPORT_VERSION
report[&"rank_tax_arrears"] = _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, boundary_minute)
report[&"loan_interest_arrears"] = _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, boundary_minute)
report[&"has_finance_postings"] = postings
return report
func _payable_arrears_through(economy: EconomyService, payable: StringName, minute: int) -> int:
var arrears := 0
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute > minute:
continue
if entry.credit_account == payable:
arrears += entry.amount
elif entry.debit_account == payable:
arrears -= entry.amount
return arrears
func _finance_arrears_through(economy: EconomyService, minute: int) -> int:
return _payable_arrears_through(economy, FinancePolicy.RANK_TAX_PAYABLE, minute) \
+ _payable_arrears_through(economy, FinancePolicy.LOAN_INTEREST_PAYABLE, minute)
func _loan_principal_ledger_is_valid(economy: EconomyService) -> bool:
if economy == null:
return false
var outstanding: Dictionary = {}
for entry: LedgerEntry in economy.get_entries():
if entry.category not in [&"LOAN_PRINCIPAL_BORROWED", &"LOAN_PRINCIPAL_REPAID"]:
continue
if entry.source_entity_id.is_empty():
return false
var liability := StringName("LOAN_LIABILITY:" + String(entry.source_entity_id))
if not economy.has_account(liability) or not economy.account_allows_negative(liability):
return false
var remaining := int(outstanding.get(liability, 0))
if entry.category == &"LOAN_PRINCIPAL_BORROWED":
if entry.debit_account != PLAYER_CASH_ACCOUNT or entry.credit_account != liability \
or entry.amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT - remaining:
return false
remaining += entry.amount
else:
if entry.debit_account != liability or entry.credit_account != PLAYER_CASH_ACCOUNT \
or entry.amount > remaining:
return false
remaining -= entry.amount
outstanding[liability] = remaining
for account: Dictionary in economy.get_accounts():
var id: StringName = account[&"account_id"]
if String(id).begins_with("LOAN_LIABILITY:"):
if not economy.account_allows_negative(id) or economy.get_balance(id) != -int(outstanding.get(id, 0)):
return false
return true
func _operations_expected_category_totals(
economy: EconomyService,
period_start: int,
period_end_exclusive: int,
new_wages_total: int
) -> Variant:
if economy == null or period_start < 0 or period_end_exclusive <= period_start \
or new_wages_total < 0:
return null
if not _loan_principal_ledger_is_valid(economy):
return null
var totals := {
&"revenue": 0, &"refunds": 0, &"cost_of_goods": 0,
&"wages": new_wages_total, &"maintenance": 0, &"utilities": 0,
&"taxes_and_interest": 0, &"fines": 0,
&"construction_cash_flow": 0,
}
var excluded: Array[StringName] = [
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT",
FinancePolicy.TOP_UP_CATEGORY, FinancePolicy.RANK_TAX_PAYMENT_CATEGORY,
FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY,
]
var mapping := {
&"SERVICE_SALE": &"revenue", &"REFUND": &"refunds",
&"CONSTRUCTION": &"construction_cash_flow",
&"CONSTRUCTION_REFUND": &"construction_cash_flow",
&"AUTHORITY_FINE": &"fines", &"OPERATING_EXPENSE": &"cost_of_goods",
&"STAFF_SIGNING_COST": &"cost_of_goods", &"EVENT_ACTION": &"cost_of_goods",
&"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods",
&"MAINTENANCE_ACCRUAL": &"maintenance",
FinancePolicy.RANK_TAX_CATEGORY: &"taxes_and_interest",
FinancePolicy.LOAN_INTEREST_CATEGORY: &"taxes_and_interest",
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
}
var operating_refunds := 0
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute < period_start \
or entry.absolute_minute >= period_end_exclusive:
continue
if entry.category in excluded:
continue
if entry.category == &"OPERATING_REFUND":
operating_refunds += entry.amount
continue
if not mapping.has(entry.category):
return null
var category: StringName = mapping[entry.category]
totals[category] = int(totals.get(category, 0)) + entry.amount
var offset := mini(operating_refunds, int(totals[&"cost_of_goods"]))
totals[&"cost_of_goods"] = int(totals[&"cost_of_goods"]) - offset
totals[&"revenue"] = int(totals[&"revenue"]) + operating_refunds - offset
return totals
func _operations_facility_wage_summary_is_valid(
summary: Dictionary,
staff_record: Dictionary,
_staff: StaffService,
economy: EconomyService
) -> bool:
var facilities_value: Variant = summary.get(&"facility_profit_loss", null)
var anomalies_value: Variant = summary.get(&"anomalies", null)
if not facilities_value is Dictionary or not anomalies_value is Array:
return false
var evidence: Dictionary = {}
var observed_unallocated: Dictionary = {}
for raw_anomaly: Variant in anomalies_value:
if not raw_anomaly is Dictionary:
return false
var anomaly: Dictionary = raw_anomaly
match StringName(anomaly.get(&"code", &"")):
&"WAGE_ALLOCATION_EVIDENCE":
if not evidence.is_empty() or not _dictionary_has_exact_canonical_keys(
anomaly,
[&"code", &"version", &"new_wages_total", &"allocated_total",
&"unallocated_total", &"allocations", &"unallocated_by_staff",
&"facility_ids"]
):
return false
evidence = anomaly
&"UNALLOCATED_WAGES":
if not _dictionary_has_exact_canonical_keys(
anomaly, [&"code", &"staff_id", &"amount"]
):
return false
var staff_id := StringName(anomaly.get(&"staff_id", &""))
var amount_value: Variant = anomaly.get(&"amount", null)
if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \
or int(amount_value) <= 0 or observed_unallocated.has(staff_id):
return false
observed_unallocated[staff_id] = int(amount_value)
&"OPERATING_REFUND_EXCEEDS_PERIOD_COST":
if not _dictionary_has_exact_canonical_keys(
anomaly, [&"code", &"refund_total", &"cost_offset", &"excess_revenue"]
):
return false
_:
return false
if evidence.is_empty() or int(evidence.get(&"version", -1)) != 1 \
or typeof(evidence.get(&"new_wages_total", null)) != TYPE_INT \
or int(evidence[&"new_wages_total"]) \
!= int(staff_record.get(&"new_wages_total", -1)) \
or typeof(evidence.get(&"allocated_total", null)) != TYPE_INT \
or typeof(evidence.get(&"unallocated_total", null)) != TYPE_INT \
or not evidence.get(&"allocations", null) is Array \
or not evidence.get(&"unallocated_by_staff", null) is Array \
or not evidence.get(&"facility_ids", null) is Array:
return false
var allocations: Dictionary = {}
var previous_facility := ""
var allocated_total := 0
for raw_row: Variant in evidence[&"allocations"]:
if not raw_row is Dictionary or not _dictionary_has_exact_canonical_keys(
raw_row, [&"facility_id", &"amount"]
):
return false
var facility_id := StringName((raw_row as Dictionary).get(&"facility_id", &""))
var amount_value: Variant = (raw_row as Dictionary).get(&"amount", null)
if facility_id.is_empty() or typeof(amount_value) != TYPE_INT \
or int(amount_value) <= 0 or allocations.has(facility_id) \
or (not previous_facility.is_empty() \
and String(facility_id) <= previous_facility):
return false
allocations[facility_id] = int(amount_value)
allocated_total += int(amount_value)
previous_facility = String(facility_id)
var unallocated: Dictionary = {}
var previous_staff := ""
var unallocated_total := 0
for raw_row: Variant in evidence[&"unallocated_by_staff"]:
if not raw_row is Dictionary or not _dictionary_has_exact_canonical_keys(
raw_row, [&"staff_id", &"amount"]
):
return false
var staff_id := StringName((raw_row as Dictionary).get(&"staff_id", &""))
var amount_value: Variant = (raw_row as Dictionary).get(&"amount", null)
if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \
or int(amount_value) <= 0 or unallocated.has(staff_id) \
or (not previous_staff.is_empty() and String(staff_id) <= previous_staff):
return false
unallocated[staff_id] = int(amount_value)
unallocated_total += int(amount_value)
previous_staff = String(staff_id)
var expected_facility_ids: Dictionary = {}
var previous_id := ""
for raw_id: Variant in evidence[&"facility_ids"]:
if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var facility_id := StringName(raw_id)
if facility_id.is_empty() or expected_facility_ids.has(facility_id) \
or (not previous_id.is_empty() and String(facility_id) <= previous_id):
return false
expected_facility_ids[facility_id] = true
previous_id = String(facility_id)
var new_wages_total := int(evidence[&"new_wages_total"])
if allocated_total != int(evidence[&"allocated_total"]) \
or unallocated_total != int(evidence[&"unallocated_total"]) \
or allocated_total + unallocated_total != new_wages_total \
or not _strict_variants_equal(observed_unallocated, unallocated):
return false
for facility_id: StringName in allocations:
if not expected_facility_ids.has(facility_id):
return false
var facility_rows: Dictionary = facilities_value
if facility_rows.size() != expected_facility_ids.size():
return false
for raw_id: Variant in facility_rows:
if not expected_facility_ids.has(StringName(raw_id)):
return false
var period_start := int(summary.get(&"period_start_minute", -1))
var period_end := int(summary.get(&"period_end_minute_exclusive", -1))
if period_start < 0 or period_end <= period_start:
return false
var base_rows: Dictionary = {}
for facility_id: StringName in expected_facility_ids:
base_rows[facility_id] = {&"revenue": 0, &"expenses": 0}
var excluded: Array[StringName] = [
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"OPERATING_REFUND",
]
var mapping := {
&"SERVICE_SALE": &"revenue", &"REFUND": &"expenses",
&"AUTHORITY_FINE": &"expenses", &"OPERATING_EXPENSE": &"expenses",
&"STAFF_SIGNING_COST": &"expenses", &"EVENT_ACTION": &"expenses",
&"EVENT_COST_COMMITTED": &"expenses", &"DEFENSE_SKILL_COST": &"expenses",
&"MAINTENANCE_ACCRUAL": &"expenses",
}
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute < period_start or entry.absolute_minute >= period_end \
or entry.category in excluded \
or entry.category in [&"CONSTRUCTION", &"CONSTRUCTION_REFUND"] \
or not expected_facility_ids.has(entry.source_entity_id):
continue
if not mapping.has(entry.category):
return false
var base: Dictionary = base_rows[entry.source_entity_id]
var key: StringName = mapping[entry.category]
base[key] = int(base[key]) + entry.amount
for facility_id: StringName in expected_facility_ids:
var row_value: Variant = facility_rows.get(facility_id, null)
if not row_value is Dictionary:
return false
var row: Dictionary = row_value
var base: Dictionary = base_rows[facility_id]
var expected_revenue := int(base[&"revenue"])
var expected_expenses := int(base[&"expenses"]) \
+ int(allocations.get(facility_id, 0))
if int(row.get(&"revenue", -1)) != expected_revenue \
or int(row.get(&"expenses", -1)) != expected_expenses \
or int(row.get(&"net_profit", 0)) != expected_revenue - expected_expenses:
return false
return true
func _operations_facility_wage_summary_from_live_shifts_is_valid(
summary: Dictionary,
staff_record: Dictionary,
staff: StaffService,
economy: EconomyService
) -> bool:
var facilities_value: Variant = summary.get(&"facility_profit_loss", null)
var rows_value: Variant = staff_record.get(&"rows", null)
var anomalies_value: Variant = summary.get(&"anomalies", null)
if not facilities_value is Dictionary or not rows_value is Array \
or not anomalies_value is Array:
return false
var facility_rows: Dictionary = facilities_value
var period_start := int(summary.get(&"period_start_minute", -1))
var period_end := int(summary.get(&"period_end_minute_exclusive", -1))
if period_start < 0 or period_end <= period_start:
return false
var wages_by_staff: Dictionary = {}
for raw_row: Variant in rows_value:
if not raw_row is Dictionary:
return false
var row: Dictionary = raw_row
var staff_id := StringName(row.get(&"staff_id", &""))
var wage_value: Variant = row.get(&"new_wages", null)
if staff_id.is_empty() or typeof(wage_value) != TYPE_INT \
or int(wage_value) < 0 or wages_by_staff.has(staff_id):
return false
wages_by_staff[staff_id] = int(wage_value)
var weights_by_staff: Dictionary = {}
for shift: ShiftInstance in staff.get_shifts():
if shift == null or shift.cancelled or shift.assigned_facility_id.is_empty() \
or not wages_by_staff.has(shift.staff_id) \
or int(wages_by_staff[shift.staff_id]) <= 0:
continue
var overlap := maxi(
mini(shift.end_at, period_end) - maxi(shift.start_at, period_start), 0
)
if overlap <= 0:
continue
var recorded := shift.worked_minutes + shift.overtime_minutes
var weight := mini(recorded, overlap) if recorded > 0 else overlap
if weight <= 0:
continue
if not weights_by_staff.has(shift.staff_id):
weights_by_staff[shift.staff_id] = {}
var weights: Dictionary = weights_by_staff[shift.staff_id]
weights[shift.assigned_facility_id] = int(
weights.get(shift.assigned_facility_id, 0)
) + weight
var expected_allocations: Dictionary = {}
var expected_unallocated: Dictionary = {}
for staff_id: StringName in _sorted_name_keys(wages_by_staff):
var wage := int(wages_by_staff[staff_id])
if wage <= 0:
continue
var weights: Dictionary = weights_by_staff.get(staff_id, {})
if weights.is_empty():
expected_unallocated[staff_id] = wage
continue
var facility_ids := _sorted_name_keys(weights)
var total_weight := 0
for facility_id: StringName in facility_ids:
total_weight += int(weights[facility_id])
if total_weight <= 0:
return false
var allocated := 0
for facility_id: StringName in facility_ids:
var amount := wage * int(weights[facility_id]) / total_weight
expected_allocations[facility_id] = int(
expected_allocations.get(facility_id, 0)
) + amount
allocated += amount
var remainder := wage - allocated
for index in range(remainder):
var facility_id: StringName = facility_ids[index]
expected_allocations[facility_id] = int(expected_allocations[facility_id]) + 1
var base_rows: Dictionary = {}
var excluded: Array[StringName] = [
&"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION",
&"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION",
&"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"OPERATING_REFUND",
]
var mapping := {
&"SERVICE_SALE": &"revenue", &"REFUND": &"expense",
&"AUTHORITY_FINE": &"expense", &"OPERATING_EXPENSE": &"expense",
&"STAFF_SIGNING_COST": &"expense", &"EVENT_ACTION": &"expense",
&"EVENT_COST_COMMITTED": &"expense", &"DEFENSE_SKILL_COST": &"expense",
}
for entry: LedgerEntry in economy.get_entries():
if entry.absolute_minute < period_start or entry.absolute_minute >= period_end \
or entry.category in excluded \
or entry.category in [&"CONSTRUCTION", &"CONSTRUCTION_REFUND"] \
or entry.source_entity_id.is_empty() \
or not facility_rows.has(entry.source_entity_id):
continue
if not mapping.has(entry.category):
return false
if not base_rows.has(entry.source_entity_id):
base_rows[entry.source_entity_id] = {&"revenue": 0, &"expenses": 0}
var base: Dictionary = base_rows[entry.source_entity_id]
var key: StringName = mapping[entry.category]
if key == &"revenue":
base[&"revenue"] = int(base[&"revenue"]) + entry.amount
else:
base[&"expenses"] = int(base[&"expenses"]) + entry.amount
for raw_facility_id: Variant in facility_rows:
var facility_id := StringName(raw_facility_id)
var row_value: Variant = facility_rows[raw_facility_id]
if facility_id.is_empty() or not row_value is Dictionary:
return false
var row: Dictionary = row_value
var base: Dictionary = base_rows.get(
facility_id, {&"revenue": 0, &"expenses": 0}
)
if int(row.get(&"revenue", -1)) != int(base[&"revenue"]) \
or int(row.get(&"expenses", -1)) \
!= int(base[&"expenses"]) + int(expected_allocations.get(facility_id, 0)):
return false
for facility_id: StringName in expected_allocations:
if not facility_rows.has(facility_id):
return false
var observed_unallocated: Dictionary = {}
for raw_anomaly: Variant in anomalies_value:
if not raw_anomaly is Dictionary:
return false
var anomaly: Dictionary = raw_anomaly
if StringName(anomaly.get(&"code", &"")) != &"UNALLOCATED_WAGES":
continue
var staff_id := StringName(anomaly.get(&"staff_id", &""))
var amount_value: Variant = anomaly.get(&"amount", null)
if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \
or int(amount_value) <= 0 or observed_unallocated.has(staff_id):
return false
observed_unallocated[staff_id] = int(amount_value)
return _strict_variants_equal(observed_unallocated, expected_unallocated)
func _operations_coordinator_configuration_is_canonical(
coordinator: OperationsCoordinator,
expected_world_seed: int
) -> bool:
if coordinator == null:
return false
var expected := OperationsCoordinator.new({
&"start_minute": OPERATIONS_COORDINATOR_ORIGIN_MINUTE,
&"world_seed": expected_world_seed,
})
if not _strict_variants_equal(coordinator.get_config(), expected.get_config()):
return false
var profiles := coordinator.get_demand_profiles()
if profiles.size() != DEFAULT_OPERATIONS_DEMAND_PROFILES.size():
return false
var expected_identities: Dictionary = {}
for raw_profile: Variant in DEFAULT_OPERATIONS_DEMAND_PROFILES:
var profile: Dictionary = raw_profile
var key := "%s|%s|%s" % [
profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"],
]
expected_identities[key] = true
var seen: Dictionary = {}
var expected_components := {
&"base": 100, &"reputation": 0, &"unmet_need": 0,
&"event": 0, &"recent_price_penalty": 0, &"wait_penalty": 0,
}
for profile: Dictionary in profiles:
var key := "%s|%s|%s" % [
profile.get(&"facility_tag", &""),
profile.get(&"guest_class", &""),
profile.get(&"time_band", &""),
]
if not expected_identities.has(key) or seen.has(key) \
or not _strict_variants_equal(
profile.get(&"components", {}), expected_components
):
return false
seen[key] = true
return seen.size() == expected_identities.size()
func _staff_payroll_arrears_total(service: StaffService) -> int:
var total := 0
for row: Dictionary in service.get_payroll_obligations():
var unpaid: Variant = row.get(&"unpaid_wages", null)
if typeof(unpaid) != TYPE_INT or int(unpaid) < 0:
return -1
total += int(unpaid)
return total
func _operations_failure_report_is_valid(report: Dictionary, minute: int) -> bool:
if report.size() == 2 \
and _dictionary_has_exact_canonical_keys(report, [&"code", &"minute"]):
var simple_code: Variant = report.get(&"code", null)
var simple_minute: Variant = report.get(&"minute", null)
return typeof(simple_code) in [TYPE_STRING, TYPE_STRING_NAME] \
and not String(simple_code).is_empty() \
and typeof(simple_minute) == TYPE_INT and int(simple_minute) == minute
var result_keys: Array[StringName] = [
&"code", &"command_id", &"target_id", &"message",
&"resolution_hints", &"details", &"is_replay",
]
if not _dictionary_has_exact_canonical_keys(report, result_keys):
return false
var code_value: Variant = report.get(&"code", null)
var command_id_value: Variant = report.get(&"command_id", null)
var target_value: Variant = report.get(&"target_id", null)
var message_value: Variant = report.get(&"message", null)
var hints_value: Variant = report.get(&"resolution_hints", null)
var details_value: Variant = report.get(&"details", null)
if typeof(code_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or String(code_value).is_empty() or String(code_value) == "OK" \
or typeof(command_id_value) != TYPE_INT or int(command_id_value) < 0 \
or typeof(target_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or typeof(message_value) != TYPE_STRING \
or not hints_value is Array or hints_value.size() > 32 \
or not details_value is Dictionary \
or typeof(report.get(&"is_replay", null)) != TYPE_BOOL:
return false
for raw_hint: Variant in hints_value:
if typeof(raw_hint) != TYPE_STRING or String(raw_hint).length() > 4_096:
return false
return JSON.stringify(details_value).length() <= 262_144
func _dictionary_has_exact_canonical_keys(
source: Dictionary,
expected_keys: Array[StringName]
) -> bool:
if source.size() != expected_keys.size():
return false
var seen: Dictionary = {}
for raw_key: Variant in source:
if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var key := StringName(raw_key)
if not expected_keys.has(key) or seen.has(key):
return false
seen[key] = true
return seen.size() == expected_keys.size()
func _operations_api_failure(
code: StringName,
message: String,
details: Dictionary = {}
) -> Dictionary:
return {
&"ok": false,
&"code": code,
&"message": message,
&"details": details.duplicate(true),
}
func register_save_migration_step(
from_version: int,
migration_id: StringName,
migrator: Callable
) -> Dictionary:
return save_migration_registry.register_step(from_version, migration_id, migrator)
func queue_checkpoint(
save_path := "",
metadata := {},
use_autosave := false,
reason: StringName = &"manual"
) -> Dictionary:
if not _initialized:
return {
&"ok": false,
&"code": &"RUNTIME_NOT_INITIALIZED",
&"message": "Simulation runtime is not initialized.",
}
var resolved_path := str(save_path)
if resolved_path.is_empty():
resolved_path = str(_save_policy[&"autosave_path"])
var request_id := _next_checkpoint_request_id
_next_checkpoint_request_id += 1
_pending_checkpoints.append({
&"request_id": request_id,
&"save_path": resolved_path,
&"metadata": metadata.duplicate(true) if metadata is Dictionary else {},
&"use_autosave": bool(use_autosave),
&"reason": reason if not reason.is_empty() else &"manual",
})
return {
&"ok": true,
&"code": &"CHECKPOINT_QUEUED",
&"request_id": request_id,
&"save_path": resolved_path,
&"queued_for_tick_boundary": true,
&"queued_at_absolute_minute": _clock.absolute_minute,
}
func queue_manual_checkpoint(save_path: String, metadata := {}) -> Dictionary:
return queue_checkpoint(save_path, metadata, false, &"manual")
func save_to_path(save_path: String, metadata := {}) -> Dictionary:
return _perform_checkpoint_save(save_path, metadata, false, &"manual", 0)
func inspect_recovery_candidates(
save_path := "",
generation_count := -1
) -> Dictionary:
var resolved_path := str(save_path)
if resolved_path.is_empty():
resolved_path = str(_save_policy[&"autosave_path"])
var resolved_generations := generation_count
if resolved_generations < 0:
resolved_generations = int(_save_policy[&"autosave_generations"])
var started_at_us := Time.get_ticks_usec()
var result := SaveService.inspect_recovery_candidates(
resolved_path,
resolved_generations
)
var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0)
_record_runtime_performance(&"load", runtime_duration_us)
return _with_operation_context(result, {
&"operation": &"inspect_recovery",
&"save_path": resolved_path,
&"runtime_duration_us": runtime_duration_us,
})
func load_from_path(save_path: String) -> Dictionary:
var started_at_us := Time.get_ticks_usec()
var previous_paused := _clock.paused
_clock.set_paused(true)
var result := SaveService.load_snapshot(save_path)
if not result[&"ok"] and result[&"code"] == &"UNSUPPORTED_FORMAT" \
and bool(_save_policy.get(&"auto_migrate", true)):
var migration := SaveService.migrate_save(
save_path,
save_migration_registry,
SaveService.FORMAT_VERSION
)
_record_runtime_performance(&"save", int(migration.get(&"duration_us", 0)))
if migration[&"ok"]:
result = SaveService.load_snapshot(save_path)
else:
result = _with_operation_context(migration, {
&"operation": &"load",
&"migration_attempted": true,
})
return _finish_load_operation(
result,
save_path,
previous_paused,
started_at_us,
&"load",
&"SAVE_LOADED",
&"SAVE_LOAD_FAILED"
)
func load_with_recovery(
save_path := "",
generation_count := -1
) -> Dictionary:
var resolved_path := str(save_path)
if resolved_path.is_empty():
resolved_path = str(_save_policy[&"autosave_path"])
var resolved_generations := generation_count
if resolved_generations < 0:
resolved_generations = int(_save_policy[&"autosave_generations"])
var started_at_us := Time.get_ticks_usec()
var previous_paused := _clock.paused
# Recovery is a two-stage decision. SaveService establishes package,
# checksum, format, and generic snapshot validity; Runtime then tries every
# package-loadable candidate in the same stable order against authoritative
# domain and cross-link rules. Failed domain attempts never reach the commit
# boundary in _restore_snapshot_internal.
var live_snapshot_before := create_snapshot()
_clock.set_paused(true)
var package_inventory := SaveService.inspect_recovery_candidates(
resolved_path,
resolved_generations
)
var result := _attempt_domain_valid_recovery(
resolved_path,
package_inventory
)
return _finish_recovery_operation(
result,
resolved_path,
previous_paused,
started_at_us,
live_snapshot_before
)
func _attempt_domain_valid_recovery(
save_path: String,
package_inventory: Dictionary
) -> Dictionary:
var package_report := package_inventory.duplicate(true)
# The selected package snapshot is an implementation detail of the package
# inspection pass. Every actual attempt is loaded again through the explicit,
# read-only candidate API so the final selection cannot accidentally reuse a
# different candidate's payload.
package_report.erase(&"snapshot")
var package_recovery_report: Dictionary = {}
var package_recovery_value: Variant = package_inventory.get(&"recovery_report", {})
if package_recovery_value is Dictionary:
package_recovery_report = package_recovery_value.duplicate(true)
var raw_candidates_value: Variant = package_inventory.get(&"candidates", [])
var candidates: Array[Dictionary] = []
if raw_candidates_value is Array:
for raw_candidate: Variant in raw_candidates_value:
if not raw_candidate is Dictionary:
continue
var candidate: Dictionary = raw_candidate.duplicate(true)
var package_loadable := bool(candidate.get(&"valid", false))
candidate[&"package_loadable"] = package_loadable
candidate[&"package_status"] = &"VALID" if package_loadable else &"REJECTED"
candidate[&"package_rejected"] = not package_loadable
candidate[&"package_rejection_code"] = (
&"" if package_loadable else StringName(candidate.get(&"code", &"PACKAGE_REJECTED"))
)
candidate[&"domain_status"] = &"NOT_ATTEMPTED"
candidate[&"domain_attempted"] = false
candidate[&"domain_valid"] = false
candidate[&"domain_rejected"] = false
candidate[&"domain_rejection_code"] = &""
candidate[&"failed_attempt_live_state_preserved"] = true
candidate[&"selected"] = false
candidates.append(candidate)
if not bool(package_inventory.get(&"ok", false)) \
and not _has_package_loadable_recovery_candidate(candidates):
return _recovery_failure_result(
StringName(package_inventory.get(&"code", &"NO_VALID_RECOVERY_CANDIDATE")),
str(package_inventory.get(&"message", save_path)),
candidates,
package_report,
package_recovery_report,
&"NO_PACKAGE_VALID_RECOVERY_CANDIDATE"
)
for candidate_index in range(candidates.size()):
var candidate: Dictionary = candidates[candidate_index]
if not bool(candidate[&"package_loadable"]):
continue
var candidate_id := StringName(candidate.get(&"id", &""))
var generation := int(candidate.get(&"generation", -1))
var loaded := SaveService.load_recovery_candidate(
save_path,
candidate_id,
generation
)
if not bool(loaded.get(&"ok", false)):
# A candidate may disappear or become corrupt between inventory and
# its explicit load. Treat that race as a package rejection and keep
# walking the already-established priority order.
candidate[&"package_status"] = &"REJECTED"
candidate[&"package_rejected"] = true
candidate[&"package_load_succeeded"] = false
candidate[&"package_rejection_code"] = StringName(
loaded.get(&"code", &"PACKAGE_LOAD_FAILED")
)
candidate[&"package_rejection_message"] = str(loaded.get(&"message", ""))
candidates[candidate_index] = candidate
continue
candidate[&"package_load_succeeded"] = true
candidate[&"domain_attempted"] = true
var package_attempt_report: Dictionary = {}
var loaded_report_value: Variant = loaded.get(&"recovery_report", {})
if loaded_report_value is Dictionary:
package_attempt_report = loaded_report_value.duplicate(true)
var attempt_live_guard := create_snapshot()
var attempt_pause_guard := _clock.paused
if not _restore_snapshot_internal(loaded[&"snapshot"], true):
candidate[&"domain_status"] = &"REJECTED"
candidate[&"domain_rejected"] = true
candidate[&"domain_rejection_code"] = &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED"
candidate[&"failed_attempt_live_state_preserved"] = (
create_snapshot() == attempt_live_guard
and _clock.paused == attempt_pause_guard
)
candidates[candidate_index] = candidate
continue
candidate[&"domain_status"] = &"VALID"
candidate[&"domain_valid"] = true
candidate[&"selected"] = true
candidates[candidate_index] = candidate
var result := loaded.duplicate(true)
result[&"ok"] = true
result[&"code"] = &"OK"
result[&"selection_rule"] = package_inventory.get(
&"selection_rule",
&"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST"
)
result[&"selected"] = candidate.duplicate(true)
result[&"candidates"] = candidates.duplicate(true)
result[&"candidate_attempts"] = candidates.duplicate(true)
result[&"package_candidate_report"] = package_report
result[&"package_recovery_report"] = package_attempt_report
result[&"restore_safety_report"] = latest_restore_safety_report.duplicate(true)
result[&"recovery_report"] = latest_restore_safety_report.duplicate(true)
result[&"recovery_report"][&"restore_committed"] = true
result[&"recovery_report"][&"selected_candidate_id"] = candidate_id
result[&"recovery_report"][&"package_rejections"] = _recovery_package_rejections(candidates)
result[&"recovery_report"][&"domain_rejections"] = _recovery_domain_rejections(candidates)
return result
return _recovery_failure_result(
&"RESTORE_FAILED",
save_path,
candidates,
package_report,
package_recovery_report,
&"NO_DOMAIN_VALID_RECOVERY_CANDIDATE"
)
func _finish_recovery_operation(
loaded_result: Dictionary,
save_path: String,
previous_paused: bool,
started_at_us: int,
live_snapshot_before: Dictionary
) -> Dictionary:
var result := loaded_result.duplicate(true)
if not bool(result.get(&"ok", false)):
_clock.set_paused(previous_paused)
var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0)
_record_runtime_performance(&"load", runtime_duration_us)
var live_state_preserved := true
if not bool(result.get(&"ok", false)):
live_state_preserved = create_snapshot() == live_snapshot_before
result = _with_operation_context(result, {
&"operation": &"recovery",
&"save_path": save_path,
&"runtime_duration_us": runtime_duration_us,
&"loaded_clock_paused": _clock.paused if result.get(&"ok", false) else previous_paused,
&"preload_pause_restored": true if result.get(&"ok", false) else _clock.paused == previous_paused,
&"live_state_preserved": live_state_preserved,
})
if bool(result.get(&"ok", false)):
_complete_save_operation(result, &"SAVE_RECOVERY_LOADED")
else:
# A failed recovery is an observational operation. Publishing a persisted
# DomainEvent here would itself violate the promised all-live-state atomicity.
latest_save_operation_result = _save_event_payload(result)
save_operation_completed.emit(result.duplicate(true))
return result
func _has_package_loadable_recovery_candidate(candidates: Array[Dictionary]) -> bool:
for candidate: Dictionary in candidates:
if bool(candidate.get(&"package_loadable", false)):
return true
return false
func _recovery_package_rejections(candidates: Array[Dictionary]) -> Array[Dictionary]:
var rejected: Array[Dictionary] = []
for candidate: Dictionary in candidates:
if not bool(candidate.get(&"package_rejected", false)):
continue
rejected.append({
&"id": StringName(candidate.get(&"id", &"")),
&"code": StringName(candidate.get(&"package_rejection_code", &"PACKAGE_REJECTED")),
})
return rejected
func _recovery_domain_rejections(candidates: Array[Dictionary]) -> Array[Dictionary]:
var rejected: Array[Dictionary] = []
for candidate: Dictionary in candidates:
if not bool(candidate.get(&"domain_rejected", false)):
continue
rejected.append({
&"id": StringName(candidate.get(&"id", &"")),
&"code": StringName(candidate.get(
&"domain_rejection_code",
&"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED"
)),
})
return rejected
func _recovery_failure_result(
code: StringName,
message: String,
candidates: Array[Dictionary],
package_report: Dictionary,
package_recovery_report: Dictionary,
rejection_reason: StringName
) -> Dictionary:
var restore_safety_report := {
&"report_version": 2,
&"recovery_mode": true,
&"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY",
&"automatic_repairs_applied": false,
&"restore_committed": false,
&"rejection_reason": rejection_reason,
&"package_rejections": _recovery_package_rejections(candidates),
&"domain_rejections": _recovery_domain_rejections(candidates),
&"forbidden_repairs_applied": false,
}
return {
&"ok": false,
&"code": code,
&"message": message,
&"selection_rule": package_report.get(
&"selection_rule",
&"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST"
),
&"selected": {},
&"candidates": candidates.duplicate(true),
&"candidate_attempts": candidates.duplicate(true),
&"package_candidate_report": package_report.duplicate(true),
&"package_recovery_report": package_recovery_report.duplicate(true),
&"restore_safety_report": restore_safety_report.duplicate(true),
&"recovery_report": restore_safety_report,
}
func load_recovery_candidate(
save_path: String,
candidate_id: StringName,
generation := -1
) -> Dictionary:
var started_at_us := Time.get_ticks_usec()
var previous_paused := _clock.paused
_clock.set_paused(true)
var result := SaveService.load_recovery_candidate(save_path, candidate_id, generation)
return _finish_load_operation(
result,
save_path,
previous_paused,
started_at_us,
&"recovery_candidate",
&"SAVE_RECOVERY_CANDIDATE_LOADED",
&"SAVE_RECOVERY_FAILED"
)
func migrate_save_path(
save_path: String,
target_version := SaveService.FORMAT_VERSION
) -> Dictionary:
var started_at_us := Time.get_ticks_usec()
var previous_paused := _clock.paused
_clock.set_paused(true)
var result := SaveService.migrate_save(save_path, save_migration_registry, target_version)
_clock.set_paused(previous_paused)
var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0)
_record_runtime_performance(&"save", runtime_duration_us)
result = _with_operation_context(result, {
&"operation": &"migration",
&"save_path": save_path,
&"runtime_duration_us": runtime_duration_us,
&"clock_pause_restored": _clock.paused == previous_paused,
})
_complete_save_operation(
result,
&"SAVE_MIGRATION_SUCCEEDED" if result[&"ok"] else &"SAVE_MIGRATION_FAILED"
)
return result
func _process_save_checkpoints(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
if bool(_save_policy.get(&"enabled", false)) \
and bool(_save_policy.get(&"daily_checkpoint_enabled", true)) \
and posmod(absolute_minute, SimulationKernel.MINUTES_PER_DAY) == SimulationKernel.START_ABSOLUTE_MINUTE:
var checkpoint_day := absolute_minute / SimulationKernel.MINUTES_PER_DAY + 1
if checkpoint_day != _last_daily_checkpoint_day:
var previous_checkpoint_day := _last_daily_checkpoint_day
_last_daily_checkpoint_day = checkpoint_day
var daily_result := _perform_checkpoint_save(
str(_save_policy[&"autosave_path"]),
{
&"display_name": "Daily Autosave",
&"checkpoint_day": checkpoint_day,
&"checkpoint_absolute_minute": absolute_minute,
},
true,
&"daily_0600",
0
)
if not daily_result[&"ok"]:
_last_daily_checkpoint_day = previous_checkpoint_day
if _pending_checkpoints.is_empty():
return
var requests := _pending_checkpoints.duplicate(true)
_pending_checkpoints.clear()
for request_value: Variant in requests:
if not request_value is Dictionary:
continue
var request: Dictionary = request_value
_perform_checkpoint_save(
str(request[&"save_path"]),
request[&"metadata"],
bool(request[&"use_autosave"]),
StringName(request[&"reason"]),
int(request[&"request_id"])
)
func _perform_checkpoint_save(
save_path: String,
metadata: Variant,
use_autosave: bool,
reason: StringName,
request_id: int
) -> Dictionary:
if not _initialized or _clock == null:
return {
&"ok": false,
&"code": &"RUNTIME_NOT_INITIALIZED",
&"message": "Simulation runtime is not initialized.",
}
var started_at_us := Time.get_ticks_usec()
# A rejected Path projection is not a complete Runtime save. Refuse before
# pausing or touching formal/tmp/backup files, and keep the refusal observable
# without publishing a persisted event into the live snapshot.
if path_service.create_snapshot().is_empty():
var refused: Dictionary = {
&"ok": false, &"code": &"INVALID_SNAPSHOT",
&"message": "Incomplete Path snapshot; original save preserved.",
&"operation": &"autosave" if use_autosave else &"save",
&"save_path": save_path, &"reason": reason, &"request_id": request_id,
&"checkpoint_absolute_minute": _clock.absolute_minute,
&"runtime_duration_us": maxi(int(Time.get_ticks_usec() - started_at_us), 0),
&"clock_pause_restored": true, &"formal_save_preserved": true,
&"live_state_preserved": true,
}
latest_save_operation_result = _save_event_payload(refused)
save_operation_completed.emit(refused.duplicate(true))
return refused
var previous_paused := _clock.paused
_clock.set_paused(true)
var snapshot := create_snapshot()
# Persistence pauses I/O, not game semantics. The package must retain the
# state that should resume after load rather than the temporary I/O pause.
snapshot[&"clock"][&"paused"] = previous_paused
var effective_metadata: Dictionary = metadata.duplicate(true) if metadata is Dictionary else {}
var presentation: Dictionary = {}
checkpoint_presentation_requested.emit(presentation)
if not effective_metadata.has(&"interface_selection") and presentation.get(&"interface_selection") is Dictionary:
effective_metadata[&"interface_selection"] = presentation[&"interface_selection"].duplicate(true)
effective_metadata[&"checkpoint_reason"] = reason
var result := (
SaveService.save_autosave(
save_path,
snapshot,
effective_metadata,
int(_save_policy[&"autosave_generations"])
)
if use_autosave
else SaveService.save_snapshot(save_path, snapshot, effective_metadata)
)
_clock.set_paused(previous_paused)
var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0)
_record_runtime_performance(&"save", runtime_duration_us)
result = _with_operation_context(result, {
&"operation": &"autosave" if use_autosave else &"save",
&"save_path": save_path,
&"reason": reason,
&"request_id": request_id,
&"checkpoint_absolute_minute": int(snapshot[&"clock"][&"absolute_minute"]),
&"runtime_duration_us": runtime_duration_us,
&"clock_pause_restored": _clock.paused == previous_paused,
})
var event_type := (
&"AUTOSAVE_CHECKPOINT_SUCCEEDED"
if use_autosave and result[&"ok"]
else &"AUTOSAVE_CHECKPOINT_FAILED"
if use_autosave
else &"SAVE_CHECKPOINT_SUCCEEDED"
if result[&"ok"]
else &"SAVE_CHECKPOINT_FAILED"
)
_complete_save_operation(result, event_type)
return result
func _finish_load_operation(
loaded_result: Dictionary,
save_path: String,
previous_paused: bool,
started_at_us: int,
operation: StringName,
success_event_type: StringName,
failure_event_type: StringName
) -> Dictionary:
var result := loaded_result.duplicate(true)
var recovery_mode := operation in [&"recovery", &"recovery_candidate"]
var package_recovery_report: Variant = result.get(&"recovery_report", null)
if result[&"ok"] and not _restore_snapshot_internal(result[&"snapshot"], recovery_mode):
result[&"ok"] = false
result[&"source_code"] = result.get(&"code", &"OK")
result[&"code"] = &"RESTORE_FAILED"
result[&"message"] = save_path
result[&"recovery_report"] = {
&"report_version": 1,
&"recovery_mode": recovery_mode,
&"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY",
&"automatic_repairs_applied": false,
&"restore_committed": false,
&"rejection_reason": &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED",
&"forbidden_repairs_applied": false,
}
elif result[&"ok"]:
result[&"restore_safety_report"] = latest_restore_safety_report.duplicate(true)
if recovery_mode:
result[&"recovery_report"] = latest_restore_safety_report.duplicate(true)
result[&"recovery_report"][&"restore_committed"] = true
if package_recovery_report is Dictionary:
result[&"package_recovery_report"] = package_recovery_report.duplicate(true)
if not result[&"ok"]:
_clock.set_paused(previous_paused)
var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0)
_record_runtime_performance(&"load", runtime_duration_us)
result = _with_operation_context(result, {
&"operation": operation,
&"save_path": save_path,
&"runtime_duration_us": runtime_duration_us,
&"loaded_clock_paused": _clock.paused if result[&"ok"] else previous_paused,
&"preload_pause_restored": true if result[&"ok"] else _clock.paused == previous_paused,
})
_complete_save_operation(
result,
success_event_type if result[&"ok"] else failure_event_type
)
return result
func _complete_save_operation(result: Dictionary, event_type: StringName) -> void:
latest_save_operation_result = _save_event_payload(result)
_publish_actor_event(
event_type,
&"save_service",
[],
latest_save_operation_result
)
save_operation_completed.emit(result.duplicate(true))
func _save_event_payload(result: Dictionary) -> Dictionary:
var payload := {
&"ok": bool(result.get(&"ok", false)),
&"code": StringName(result.get(&"code", &"UNKNOWN")),
&"message": str(result.get(&"message", "")),
&"operation": StringName(result.get(&"operation", &"unknown")),
&"save_path": str(result.get(&"save_path", result.get(&"path", ""))),
&"reason": StringName(result.get(&"reason", &"")),
&"request_id": int(result.get(&"request_id", 0)),
&"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", _clock.absolute_minute)),
&"duration_us": int(result.get(&"duration_us", 0)),
&"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)),
}
var selected_value: Variant = result.get(&"selected", null)
if selected_value is Dictionary:
payload[&"selected_candidate_id"] = StringName(selected_value.get(&"id", &""))
payload[&"selected_candidate_kind"] = StringName(selected_value.get(&"kind", &""))
payload[&"selected_generation"] = int(selected_value.get(&"generation", -1))
var manifest_value: Variant = result.get(&"manifest", null)
if manifest_value is Dictionary:
payload[&"save_id"] = str(manifest_value.get("save_id", ""))
payload[&"saved_absolute_minute"] = int(manifest_value.get("absolute_minute", -1))
return payload
func _with_operation_context(result: Dictionary, context: Dictionary) -> Dictionary:
var enriched := result.duplicate(true)
for key: Variant in context:
enriched[key] = context[key]
return enriched
func _record_runtime_performance(metric_id: StringName, duration_us: int) -> void:
if _clock != null and _clock.performance_budget != null:
_clock.performance_budget.record_sample(metric_id, maxi(duration_us, 0))
func _release_recovery_task_items(
inventory: InventoryService,
reservation_ids: Array[StringName],
task: StaffTask,
absolute_minute: int
) -> Dictionary:
var before := inventory.create_snapshot()
for reservation_id: StringName in reservation_ids:
var result := inventory.release_reservation(
reservation_id,
StringName("recovery.staff.task.release.%s.%s" % [task.task_id, reservation_id]),
absolute_minute
)
if not bool(result.get(&"ok", false)):
inventory.restore_snapshot(before)
return result
return {&"ok": true}
func _merge_content_filter_profiles(current: Dictionary, saved: Dictionary) -> Dictionary:
var disabled_by_name: Dictionary = {}
for raw_tag: Variant in current.get(&"disabled_content_tags", []):
var tag := StringName(raw_tag)
if not tag.is_empty():
disabled_by_name[tag] = true
for raw_tag: Variant in saved.get(&"disabled_content_tags", []):
var tag := StringName(raw_tag)
if not tag.is_empty():
disabled_by_name[tag] = true
var disabled_tags: Array[StringName] = []
for raw_tag: Variant in disabled_by_name:
disabled_tags.append(StringName(raw_tag))
disabled_tags.sort_custom(
func(left: StringName, right: StringName) -> bool:
return String(left) < String(right)
)
return {
&"filter_mode": maxi(
int(current.get(&"filter_mode", EventDefinition.FilterBehavior.NONE)),
int(saved.get(&"filter_mode", EventDefinition.FilterBehavior.NONE))
) as EventDefinition.FilterBehavior,
&"maximum_content_rating": mini(
int(current.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL)),
int(saved.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL))
) as EventDefinition.ContentRating,
&"disabled_content_tags": disabled_tags,
}
func _content_filter_restore_report(
current: Dictionary,
saved: Dictionary,
effective: Dictionary
) -> Dictionary:
var current_tags: Array = current.get(&"disabled_content_tags", [])
var saved_tags: Array = saved.get(&"disabled_content_tags", [])
var saved_was_weaker := (
int(saved.get(&"filter_mode", 0)) < int(current.get(&"filter_mode", 0))
or int(saved.get(&"maximum_content_rating", 0)) > int(current.get(&"maximum_content_rating", 0))
)
for raw_tag: Variant in current_tags:
if not saved_tags.has(StringName(raw_tag)) and not saved_tags.has(String(raw_tag)):
saved_was_weaker = true
break
return {
&"saved_profile_was_weaker": saved_was_weaker,
&"strict_profile_preserved": true,
&"current_before_load": _content_filter_profile_payload(current),
&"saved_profile": _content_filter_profile_payload(saved),
&"effective_profile": _content_filter_profile_payload(effective),
}
func _content_filter_profile_payload(profile: Dictionary) -> Dictionary:
var mode := clampi(
int(profile.get(&"filter_mode", EventDefinition.FilterBehavior.NONE)),
EventDefinition.FilterBehavior.NONE,
EventDefinition.FilterBehavior.DISABLE
)
var rating := clampi(
int(profile.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL)),
EventDefinition.ContentRating.GENERAL,
EventDefinition.ContentRating.RESTRICTED
)
var tags: Array[String] = []
for raw_tag: Variant in profile.get(&"disabled_content_tags", []):
tags.append(String(raw_tag))
tags.sort()
return {
&"filter_mode": EventDefinition.FilterBehavior.keys()[mode],
&"maximum_content_rating": EventDefinition.ContentRating.keys()[rating],
&"disabled_content_tags": tags,
}
func _save_policy_path_is_safe(save_path: String) -> bool:
if not save_path.begins_with("user://") or save_path == "user://":
return false
var relative := save_path.trim_prefix("user://").replace("\\", "/")
if relative.is_empty() or relative.is_absolute_path():
return false
for segment: String in relative.split("/", false):
if segment.is_empty() or segment == "." or segment == "..":
return false
return true
func _runtime_command_history_crosslinks_are_valid(
queue_snapshot: Dictionary,
next_command_id: int,
events: DomainEventStream
) -> bool:
if next_command_id <= 0 or events == null \
or typeof(queue_snapshot.get("version", null)) != TYPE_INT \
or int(queue_snapshot["version"]) != CommandQueue.SNAPSHOT_VERSION \
or not queue_snapshot.get("accepted", null) is Array:
return false
var seen_ids: Dictionary = {}
var processed_by_id: Dictionary = {}
var command_idempotency: Dictionary = {}
for collection_key: String in ["pending", "in_flight", "processed"]:
var rows_value: Variant = queue_snapshot.get(collection_key, null)
if not rows_value is Array:
return false
if collection_key == "in_flight" and not rows_value.is_empty():
# Runtime commands are synchronously completed in APPLY_COMMANDS and
# have no resumable in-flight execution path.
return false
for row_value: Variant in rows_value:
if not row_value is Dictionary:
return false
var row: Dictionary = row_value
var command_id_value: Variant = row.get("command_id", null)
if typeof(command_id_value) != TYPE_INT or int(command_id_value) <= 0 \
or seen_ids.has(int(command_id_value)):
return false
var command_id := int(command_id_value)
seen_ids[command_id] = true
if collection_key == "processed":
processed_by_id[command_id] = row
else:
var raw_key: Variant = row.get("idempotency_key", "")
if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var key := StringName(raw_key)
if not key.is_empty():
command_idempotency[command_id] = key
if seen_ids.size() != next_command_id - 1:
return false
for command_id in range(1, next_command_id):
if not seen_ids.has(command_id):
return false
var accepted_by_id: Dictionary = {}
var previous_accepted_id := 0
for raw_metadata: Variant in queue_snapshot["accepted"]:
if not raw_metadata is Dictionary:
return false
var metadata: Dictionary = raw_metadata
var command_id_value: Variant = metadata.get("command_id", null)
var key_value: Variant = metadata.get("idempotency_key", null)
if metadata.size() != 2 or typeof(command_id_value) != TYPE_INT \
or int(command_id_value) <= previous_accepted_id \
or typeof(key_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or not seen_ids.has(int(command_id_value)):
return false
var command_id := int(command_id_value)
if command_idempotency.has(command_id) \
and StringName(command_idempotency[command_id]) != StringName(key_value):
return false
accepted_by_id[command_id] = StringName(key_value)
command_idempotency[command_id] = StringName(key_value)
previous_accepted_id = command_id
if accepted_by_id.size() != seen_ids.size():
return false
var idempotency_value: Variant = queue_snapshot.get("idempotency", null)
if not idempotency_value is Dictionary:
return false
var mapped_ids: Dictionary = {}
for raw_key: Variant in idempotency_value:
if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(raw_key).is_empty() \
or typeof(idempotency_value[raw_key]) != TYPE_INT:
return false
var command_id := int(idempotency_value[raw_key])
if not seen_ids.has(command_id) or mapped_ids.has(command_id):
return false
mapped_ids[command_id] = StringName(raw_key)
for command_id: int in command_idempotency:
var accepted_key := StringName(command_idempotency[command_id])
if accepted_key.is_empty():
if mapped_ids.has(command_id):
return false
elif mapped_ids.get(command_id, &"") != accepted_key:
return false
var completion_by_id: Dictionary = {}
for event: SimulationDomainEvent in events.recent_events():
if event.event_type not in [&"COMMAND_COMPLETED", &"COMMAND_FAILED"]:
continue
var command_id := event.causation_command_id
if command_id <= 0 or not processed_by_id.has(command_id) \
or completion_by_id.has(command_id) \
or event.source_service != &"simulation_runtime" \
or not _strict_variants_equal(event.payload, processed_by_id[command_id]):
return false
var result_code := String((processed_by_id[command_id] as Dictionary).get("code", ""))
if (result_code == "OK") != (event.event_type == &"COMMAND_COMPLETED"):
return false
completion_by_id[command_id] = true
# Completion events share a bounded stream with all other DomainEvents. Any
# retained completion must match exactly; absence alone is not corruption
# because unrelated events can legitimately evict it.
return true
func _legacy_operations_history_is_absent(runtime_data: Dictionary) -> bool:
# v1-v4 had no Operations wrapper or payroll settlement semantics. Reject a
# relabelled v5 payload instead of silently discarding production history.
if runtime_data.has(&"operations") or runtime_data.has("operations"):
return false
var staff_value: Variant = runtime_data.get(&"staff", null)
if staff_value is Dictionary:
var settlements: Variant = (staff_value as Dictionary).get(&"payroll_settlements", [])
if settlements is Array and not settlements.is_empty():
return false
var economy_value: Variant = runtime_data.get(&"economy", null)
if economy_value is Dictionary:
var entries: Variant = (economy_value as Dictionary).get(&"entries", [])
if entries is Array:
for raw_entry: Variant in entries:
if raw_entry is Dictionary \
and StringName((raw_entry as Dictionary).get(&"category", "")) == &"WAGE":
return false
return true
func _legacy_infrastructure_history_is_absent(runtime_data: Dictionary) -> bool:
# Infrastructure aggregates first exist in Runtime v6. A lower version with
# any look-alike field or retained v6-only history is a relabelled payload,
# not a migratable legacy save.
for key: StringName in [
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure",
]:
if runtime_data.has(key) or runtime_data.has(String(key)):
return false
var stream_value: Variant = runtime_data.get(&"domain_events", null)
if stream_value is Dictionary:
var recent_events: Variant = (stream_value as Dictionary).get("recent_events", [])
if recent_events is Array:
for raw_event: Variant in recent_events:
if raw_event is Dictionary and StringName(
(raw_event as Dictionary).get("event_type", "")
) in [&"FACILITY_INFRASTRUCTURE_PROJECTED", &"EXCAVATION_COMPLETED"]:
return false
var queue_value: Variant = runtime_data.get(&"command_queue", null)
if queue_value is Dictionary:
for queue_key: String in ["pending", "in_flight"]:
var commands: Variant = (queue_value as Dictionary).get(queue_key, [])
if not commands is Array:
continue
for raw_command: Variant in commands:
if raw_command is Dictionary and StringName(
(raw_command as Dictionary).get("command_type", "")
) in [&"start_excavation", &"pause_excavation", &"resume_excavation"]:
return false
return true
func _runtime_v5_outer_snapshot_shape_is_valid(snapshot: Dictionary) -> bool:
if not _dictionary_has_exact_canonical_keys(
snapshot, [&"schema_version", &"clock", &"game_state", &"systems"]
) or typeof(snapshot.get(&"schema_version", null)) != TYPE_INT \
or int(snapshot[&"schema_version"]) != SimulationSnapshot.SCHEMA_VERSION:
return false
var systems_value: Variant = snapshot.get(&"systems", null)
var clock_value: Variant = snapshot.get(&"clock", null)
var state_value: Variant = snapshot.get(&"game_state", null)
if not systems_value is Dictionary or not clock_value is Dictionary \
or not state_value is Dictionary:
return false
var systems: Dictionary = systems_value
var clock_data: Dictionary = clock_value
var state_data: Dictionary = state_value
if not _dictionary_has_exact_canonical_keys(systems, [&"runtime"]) \
or not systems[&"runtime"] is Dictionary \
or not _dictionary_has_exact_canonical_keys(
clock_data,
[&"absolute_minute", &"speed", &"paused", &"snapshot_version"]
) \
or typeof(clock_data.get(&"absolute_minute", null)) != TYPE_INT \
or int(clock_data[&"absolute_minute"]) < 0 \
or typeof(clock_data.get(&"speed", null)) != TYPE_INT \
or int(clock_data[&"speed"]) not in [1, 2, 3, 4] \
or typeof(clock_data.get(&"paused", null)) != TYPE_BOOL \
or typeof(clock_data.get(&"snapshot_version", null)) != TYPE_INT \
or int(clock_data[&"snapshot_version"]) < 0:
return false
if not _dictionary_has_exact_canonical_keys(
state_data,
[&"resources", &"world_tree_rank", &"active_layer", &"revision"]
) or not state_data[&"resources"] is Dictionary \
or typeof(state_data.get(&"world_tree_rank", null)) != TYPE_INT \
or int(state_data[&"world_tree_rank"]) < 1 \
or int(state_data[&"world_tree_rank"]) > 7 \
or typeof(state_data.get(&"active_layer", null)) != TYPE_INT \
or int(state_data[&"active_layer"]) not in [0, 1] \
or typeof(state_data.get(&"revision", null)) != TYPE_INT \
or int(state_data[&"revision"]) < 0:
return false
var resources: Dictionary = state_data[&"resources"]
var seen_resources: Dictionary = {}
for raw_resource_id: Variant in resources:
if typeof(raw_resource_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var resource_id := StringName(raw_resource_id)
if resource_id.is_empty() or seen_resources.has(resource_id) \
or typeof(resources[raw_resource_id]) != TYPE_INT \
or int(resources[raw_resource_id]) < 0:
return false
seen_resources[resource_id] = true
for required_resource: StringName in [&"gold", &"mana", &"materials", &"faith"]:
if not seen_resources.has(required_resource):
return false
return true
func _connect_path_performance_signal() -> void:
if path_service == null or not path_service.has_signal(&"path_query_measured"):
return
var callback := Callable(self, "_on_path_query_measured")
if not path_service.is_connected(&"path_query_measured", callback):
path_service.connect(&"path_query_measured", callback)
func _on_path_query_measured(duration_us: int) -> void:
_record_runtime_performance(&"path_query", duration_us)
func _initialize_rng_streams() -> void:
_rng_streams.clear()
for stream_id in RNG_STREAM_IDS:
_rng_streams[stream_id] = DeterministicRng.from_root_seed(world_seed, stream_id)
func _connect_construction_signals() -> void:
if construction_service == null:
return
var completed_callback := Callable(self, "_on_facility_completed")
var demolished_callback := Callable(self, "_on_facility_demolished")
if not construction_service.facility_completed.is_connected(completed_callback):
construction_service.facility_completed.connect(completed_callback)
if not construction_service.facility_demolished.is_connected(demolished_callback):
construction_service.facility_demolished.connect(demolished_callback)
func _initialize_infrastructure_services() -> bool:
spatial_subslot_service = SpatialSubslotService.new()
route_subslot_path_adapter = RouteSubslotPathAdapter.new()
if not route_subslot_path_adapter.configure(
spatial_subslot_service,
path_service,
world_service.get_walkable_cells()
):
return false
facility_infrastructure_projector = FacilityInfrastructureProjector.new()
if not facility_infrastructure_projector.configure(
route_subslot_path_adapter,
spatial_subslot_service,
path_service
):
return false
excavation_service = ExcavationService.new(world_service, construction_resource_state)
excavation_service.current_minute = _clock.absolute_minute
var excavation_callback := Callable(self, "_on_excavation_completed")
if not excavation_service.excavation_completed.is_connected(excavation_callback):
excavation_service.excavation_completed.connect(excavation_callback)
return true
func rebuild_path_graph_from_world() -> bool:
if path_service == null or world_service == null:
return false
var walkable_cells: Array[Vector3i] = world_service.get_walkable_cells()
var projected_walkable_cells: Array[Vector3i] = walkable_cells.duplicate()
if route_subslot_path_adapter != null \
and route_subslot_path_adapter.is_configured() \
and spatial_subslot_service != null:
var seen_projected: Dictionary = {}
projected_walkable_cells.clear()
for cell: Vector3i in walkable_cells:
if not seen_projected.has(cell):
seen_projected[cell] = true
projected_walkable_cells.append(cell)
for cell: Vector3i in spatial_subslot_service.get_route_cells():
if not seen_projected.has(cell):
seen_projected[cell] = true
projected_walkable_cells.append(cell)
var current_path_snapshot: Dictionary = path_service.create_snapshot()
var raw_actor_cells: Variant = current_path_snapshot.get(&"actor_cells", null)
var has_runtime_actor_state: bool = (
not _movement_credits.is_empty()
or (raw_actor_cells is Array and not raw_actor_cells.is_empty())
)
# PathService correctly owns its own graph, but Runtime owns the cross-system
# Actor/Path projection contract. Reject a topology change that would evict
# a live Guest/Staff projection instead of allowing PathService to prune it.
if has_runtime_actor_state:
if _clock == null or not _validate_runtime_path_crosslinks(
path_service,
guest_service,
staff_service,
_movement_credits,
_clock.absolute_minute
):
return false
var walkable_set: Dictionary = {}
for cell: Vector3i in projected_walkable_cells:
walkable_set[cell] = true
var expected_cells: Dictionary = _expected_runtime_actor_cells(
guest_service,
staff_service
)
for actor_id: StringName in _sorted_name_keys(expected_cells):
if not walkable_set.has(expected_cells[actor_id]):
return false
if route_subslot_path_adapter != null \
and route_subslot_path_adapter.is_configured():
var synchronized := route_subslot_path_adapter.synchronize_projection(
StringName(
"runtime.base_sync.%08d" % (route_subslot_path_adapter.projection_revision + 1)
),
walkable_cells
)
return synchronized.is_success() and _prune_legacy_stairs_and_movement()
return path_service.rebuild_graph(walkable_cells) and _prune_legacy_stairs_and_movement()
func _initialize_world(rank: int) -> void:
var bounds := _bounds_for_rank(rank)
world_service.reset(bounds)
_seed_boundary_cells(bounds)
rebuild_path_graph_from_world()
path_service.add_connector(
&"starter.staff_door",
Vector3i(0, 0, WorldService.LAYER_FRONT),
Vector3i(0, 0, WorldService.LAYER_BACK),
PathService.DEFAULT_CONNECTOR_COST,
true,
&"staff"
)
func _initialize_mana_network() -> void:
mana_network_service.register_source(STARTER_MANA_SOURCE, STARTER_MANA_OUTPUT)
mana_network_service.register_connector(STARTER_MANA_BUS)
mana_network_service.connect_nodes(STARTER_MANA_SOURCE, STARTER_MANA_BUS)
func _initialize_economy() -> void:
economy_service = EconomyService.new({
PLAYER_CASH_ACCOUNT: {&"allow_negative": false},
CONSTRUCTION_EXPENSE_ACCOUNT: {&"allow_negative": false},
OPERATING_EXPENSE_ACCOUNT: {&"allow_negative": false},
AUTHORITY_FINE_ACCOUNT: {&"allow_negative": false},
WAGE_EXPENSE_ACCOUNT: {&"allow_negative": false},
})
economy_service.initialize_account(
PLAYER_CASH_ACCOUNT,
int(_state.get_resource(&"gold")),
_clock.absolute_minute,
&"runtime.initial_player_cash"
)
facility_cleanliness = FacilityCleanliness.new()
cleaning_policy = CleaningPolicy.new()
cleaning_policy.load_file()
weather = WeatherTimeline.new()
weather.initialize(&"clear", _clock.absolute_minute)
facility_upkeep = FacilityUpkeepAccrual.new()
upkeep_settlement = FacilityUpkeepSettlement.new()
FacilityUpkeepSettlement.prepare_accounts(economy_service)
FinancePolicy.prepare_accounts(economy_service)
facility_upkeep.start_at(_clock.absolute_minute)
mana_reserve = ManaReserveCycle.new()
mana_reserve.start_at(_clock.absolute_minute)
construction_resource_state = _create_resource_adapter(
economy_service,
CONSTRUCTION_EXPENSE_ACCOUNT,
&"CONSTRUCTION",
&"CONSTRUCTION_REFUND",
&"construction_service"
)
operating_resource_state = _create_resource_adapter(
economy_service,
OPERATING_EXPENSE_ACCOUNT,
&"OPERATING_EXPENSE",
&"OPERATING_REFUND",
&"simulation_runtime"
)
_connect_economy_signals()
func _create_resource_adapter(
economy: EconomyService,
counter_account: StringName,
expense_category: StringName,
refund_category: StringName,
source_entity_id: StringName
) -> LedgerBackedResourceState:
return LedgerBackedResourceState.new(
_state,
economy,
counter_account,
expense_category,
refund_category,
source_entity_id,
func() -> int: return _runtime_event_minute()
)
func _connect_economy_signals() -> void:
if economy_service == null:
return
economy_service.transaction_posted.connect(Callable(self, "_on_ledger_transaction_posted"))
func _initialize_s2_operations() -> void:
inventory_service = InventoryService.new()
inventory_service.current_minute = _clock.absolute_minute
logistics_dispatcher = LogisticsDispatcher.new()
logistics_requests = {}
facility_service = FacilityService.new()
facility_service.current_minute = _clock.absolute_minute
_configure_facility_boundaries()
_connect_facility_signals()
auto_purchase_plans = AutoPurchasePlansType.new()
kitchen_production = KitchenProductionService.new()
kitchen_production.configure(inventory_service, facility_service, definition_registry, _clock.absolute_minute)
func _initialize_s3_actors() -> void:
guest_service = GuestService.new()
guest_service.current_minute = _clock.absolute_minute
guest_service.create_definition(&"traveler", {
&"tags": [&"general", &"traveler"],
&"wealth_min": 120,
&"wealth_max": 450,
&"base_patience": 45,
&"risk_tolerance": 50,
&"preference_weights": {&"dining": 8, &"lodging": 7, &"social": 3},
&"group_size_weights": {1: 70, 2: 25, 3: 5},
&"legal_sensitivity": 65,
&"quality_sensitivity": 50,
&"need_rates": {&"hunger": 1, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0},
&"credit_limit_gold": 50,
&"minimum_quality": 25,
})
guest_service.create_definition(&"merchant", {
&"tags": [&"general", &"merchant", &"frugal"],
&"wealth_min": 300,
&"wealth_max": 1_200,
&"base_patience": 35,
&"risk_tolerance": 35,
&"preference_weights": {&"lodging": 9, &"quiet": 7, &"service": 4},
&"group_size_weights": {1: 60, 2: 40},
&"legal_sensitivity": 80,
&"quality_sensitivity": 70,
&"need_rates": {&"hunger": 1, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0},
&"credit_limit_gold": 200,
&"minimum_quality": 40,
})
guest_service.create_definition(&"adventurer", {
&"tags": [&"adventurer", &"general", &"spendthrift"],
&"wealth_min": 180,
&"wealth_max": 800,
&"base_patience": 30,
&"risk_tolerance": 85,
&"preference_weights": {&"dining": 6, &"social": 8, &"service": 2},
&"group_size_weights": {1: 45, 2: 35, 3: 15, 4: 5},
&"legal_sensitivity": 35,
&"quality_sensitivity": 35,
&"adult_verified": true,
&"need_rates": {&"hunger": 2, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0},
&"credit_limit_gold": 100,
&"minimum_quality": 20,
})
if bool(mvp_content_report.get(&"valid", false)):
for definition: GuestDefinition in mvp_content_catalog.build_all_guest_definitions():
if not guest_service.register_definition(definition):
mvp_content_report = mvp_content_report.duplicate(true)
mvp_content_report[&"valid"] = false
mvp_content_report[&"runtime_registration_error"] = guest_service.last_error
break
staff_service = StaffService.new()
staff_service.current_minute = _clock.absolute_minute
_configure_staff_boundaries()
_connect_guest_signals()
_connect_staff_signals()
func _configure_staff_boundaries() -> void:
if staff_service == null:
return
staff_service.configure_arrival_driver(Callable(self, "_handle_rescue_arrival"))
staff_service.configure_work_progress_driver(Callable(self, "_automatic_staff_work_units"))
staff_service.configure_callbacks(
Callable(self, "_reserve_staff_task_items"),
Callable(self, "_release_staff_task_items"),
Callable(self, "_estimate_staff_task_travel")
)
func _connect_guest_signals() -> void:
if guest_service == null:
return
guest_service.guest_spawned.connect(Callable(self, "_on_guest_spawned"))
guest_service.guest_state_changed.connect(Callable(self, "_on_guest_state_changed"))
guest_service.guest_queue_abandoned.connect(Callable(self, "_on_guest_queue_abandoned"))
guest_service.guest_recovery_started.connect(Callable(self, "_on_guest_recovery_started"))
guest_service.guest_despawned.connect(Callable(self, "_on_guest_despawned"))
func _connect_staff_signals() -> void:
if staff_service == null:
return
staff_service.staff_registered.connect(Callable(self, "_on_staff_registered"))
staff_service.staff_removed.connect(Callable(self, "_on_staff_removed"))
staff_service.shift_registered.connect(Callable(self, "_on_shift_registered"))
staff_service.shift_state_changed.connect(Callable(self, "_on_shift_state_changed"))
staff_service.task_registered.connect(Callable(self, "_on_staff_task_registered"))
staff_service.task_assigned.connect(Callable(self, "_on_staff_task_assigned"))
staff_service.task_reopened.connect(Callable(self, "_on_staff_task_reopened"))
staff_service.task_completed.connect(Callable(self, "_on_staff_task_completed"))
func _initialize_s4_relationship_events() -> void:
relationship_service = RelationshipService.new()
event_service = EventService.new()
event_service.create_definition(&"relationship.threshold", {
&"category": &"RELATIONSHIP",
&"priority": 80,
&"foreground": true,
&"expires_after": 1_440,
&"transaction_policy": &"ROLLBACK_ALL",
&"missing_actor_policy": &"CANCEL",
&"content_rating": &"GENERAL",
&"choices": [
{
&"choice_id": &"acknowledge",
&"text_key": &"event.relationship.threshold.acknowledge",
&"actions": [{
&"action_id": &"record_acknowledgement",
&"type": &"SET_FLAG",
&"payload": {&"flag_id": &"relationship_threshold_acknowledged"},
}],
},
{
&"choice_id": &"defer",
&"text_key": &"event.relationship.threshold.defer",
&"actions": [],
},
],
})
_configure_relationship_event_boundaries()
_connect_relationship_signals()
_connect_event_signals()
func _configure_relationship_event_boundaries() -> void:
if relationship_service == null or event_service == null:
return
relationship_service.configure_original_identity_validator(
func(payload: Dictionary) -> bool: return _original_relationship_identities_valid(payload, staff_service, mvp_content_catalog)
)
relationship_service.configure_candidate_callback(
Callable(self, "_on_relationship_threshold_candidate")
)
event_service.configure_relationship_service(relationship_service)
event_service.configure_candidate_eligibility_guard(Callable(self, "_event_candidate_placement_permitted"))
event_service.configure_callbacks(
Callable(self, "_event_actor_exists"),
Callable(self, "_execute_event_action_boundary"),
Callable(self, "_reserve_event_cost"),
Callable(self, "_release_event_cost"),
Callable(self, "_commit_event_cost"),
Callable(self, "_event_entity_exists"),
Callable(self, "_event_path_exists")
)
## Original 6.2/6.4: Tomari's placement prevents floor trouble. This does not
## choose a joining date, probability, defense score, or narrative content.
func is_floor_trouble_protected(floor_y: int) -> bool:
var tomari := staff_service.get_staff(&"heroine_tomari")
if tomari == null or tomari.employment_kind != StaffRuntime.EmploymentKind.UNIQUE:
return false
var assignment := staff_service.get_work_assignment(tomari.staff_id)
var facility := facility_service.get_instance(StringName(assignment.get(&"facility_id", &"")))
return facility != null and facility.origin.y == floor_y
func _event_candidate_placement_permitted(definition: EventDefinition, context: Dictionary) -> bool:
if definition.category != &"TROUBLE":
return true
# A business trouble identifies its existing target facility. Missing or
# conflicting location data cannot silently bypass placement protection.
var target: Variant = context.get(&"facility_id", definition.location_rule.get(&"facility_id", null))
if typeof(target) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var declared: Variant = definition.location_rule.get(&"facility_id", target)
if typeof(declared) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(declared) != StringName(target):
return false
var facility := facility_service.get_instance(StringName(target))
return facility != null and not is_floor_trouble_protected(facility.origin.y)
func _connect_relationship_signals() -> void:
if relationship_service == null:
return
relationship_service.relationship_registered.connect(Callable(self, "_on_relationship_registered"))
relationship_service.relationship_value_changed.connect(Callable(self, "_on_relationship_value_changed"))
relationship_service.shield_changed.connect(Callable(self, "_on_relationship_shield_changed"))
relationship_service.route_lock_changed.connect(Callable(self, "_on_relationship_route_lock_changed"))
func _connect_event_signals() -> void:
if event_service == null:
return
event_service.event_enqueued.connect(Callable(self, "_on_event_enqueued"))
event_service.event_started.connect(Callable(self, "_on_event_started"))
event_service.event_finished.connect(Callable(self, "_on_event_finished"))
event_service.event_cancelled.connect(Callable(self, "_on_event_cancelled"))
event_service.choice_resolved.connect(Callable(self, "_on_event_choice_resolved"))
event_service.effect_transaction_started.connect(Callable(self, "_on_event_effect_transaction_started"))
event_service.effect_transaction_finished.connect(Callable(self, "_on_event_effect_transaction_finished"))
func _initialize_s5_authority() -> void:
authority_service = AuthorityService.new()
authority_service.current_minute = _clock.absolute_minute
_configure_authority_boundaries()
_connect_authority_signals()
func _configure_authority_boundaries() -> void:
if authority_service == null:
return
authority_service.configure_callbacks(
Callable(self, "_authority_line_of_sight"),
Callable(self, "_authority_path_query"),
Callable(self, "_apply_authority_resolution_boundary"),
Callable(self, "_roll_authority_inspection")
)
func _connect_authority_signals() -> void:
if authority_service == null:
return
authority_service.heat_changed.connect(Callable(self, "_on_authority_heat_changed"))
authority_service.incident_created.connect(Callable(self, "_on_authority_incident_created"))
authority_service.incident_detector_added.connect(Callable(self, "_on_authority_incident_detector_added"))
authority_service.evidence_registered.connect(Callable(self, "_on_authority_evidence_registered"))
authority_service.evidence_discovered.connect(Callable(self, "_on_authority_evidence_discovered"))
authority_service.inspection_scheduled.connect(Callable(self, "_on_authority_inspection_scheduled"))
authority_service.inspection_state_changed.connect(Callable(self, "_on_authority_inspection_state_changed"))
authority_service.resolution_applied.connect(Callable(self, "_on_authority_resolution_applied"))
func _initialize_s5_defense() -> void:
_reset_defense_round_attempts()
defense_service = DefenseService.new()
defense_service.current_minute = _clock.absolute_minute
_configure_defense_boundaries()
_connect_defense_signals()
func _defense_operation_minute() -> int:
return _active_command_minute if _active_command_minute >= 0 else _clock.absolute_minute
func _runtime_event_minute() -> int:
return _active_command_minute if _active_command_minute >= 0 else _clock.absolute_minute
func _configure_defense_boundaries() -> void:
if defense_service == null:
return
defense_service.configure_callbacks(
Callable(self, "_roll_defense_round"),
Callable(self, "_apply_defense_skill_boundary"),
Callable(self, "_apply_defense_outcome_boundary"),
Callable(self, "_apply_defense_rng_boundary")
)
func _connect_defense_signals() -> void:
if defense_service == null:
return
var bindings := [
[&"raid_created", Callable(self, "_on_defense_raid_created")],
[&"raid_state_changed", Callable(self, "_on_defense_raid_state_changed")],
[&"active_skill_applied", Callable(self, "_on_defense_active_skill_applied")],
[&"round_resolved", Callable(self, "_on_defense_round_resolved")],
[&"raid_resolved", Callable(self, "_on_defense_raid_resolved")],
]
for binding: Array in bindings:
var signal_name: StringName = binding[0]
var callback: Callable = binding[1]
if not defense_service.is_connected(signal_name, callback):
defense_service.connect(signal_name, callback)
func _detach_defense_service(service: DefenseService) -> void:
if service == null:
return
# A caller may retain a RefCounted aggregate across Runtime initialize/load.
# Retiring it must sever both transaction callbacks and event projection so
# stale objects cannot consume the new Runtime's RNG/resources or emit ghost
# DomainEvents after an otherwise atomic restore.
service.configure_callbacks()
var bindings := [
[&"raid_created", Callable(self, "_on_defense_raid_created")],
[&"raid_state_changed", Callable(self, "_on_defense_raid_state_changed")],
[&"active_skill_applied", Callable(self, "_on_defense_active_skill_applied")],
[&"round_resolved", Callable(self, "_on_defense_round_resolved")],
[&"raid_resolved", Callable(self, "_on_defense_raid_resolved")],
]
for binding: Array in bindings:
var signal_name: StringName = binding[0]
var callback: Callable = binding[1]
if service.is_connected(signal_name, callback):
service.disconnect(signal_name, callback)
func _configure_facility_boundaries() -> void:
if facility_service == null:
return
facility_service.configure_callbacks(
Callable(self, "_validate_service_start"),
Callable(self, "_complete_service_transaction")
)
func _connect_facility_signals() -> void:
if facility_service == null:
return
facility_service.instance_registered.connect(Callable(self, "_on_cleanliness_facility_registered"))
facility_service.instance_unregistered.connect(Callable(self, "_on_cleanliness_facility_unregistered"))
facility_service.instance_unregistered.connect(Callable(self, "_on_peeping_target_removed"))
facility_service.facility_state_changed.connect(Callable(self, "_on_facility_state_changed"))
facility_service.service_started.connect(Callable(self, "_on_service_started"))
facility_service.service_interrupted.connect(Callable(self, "_on_service_interrupted"))
facility_service.service_completed.connect(Callable(self, "_on_service_completed"))
facility_service.service_failed.connect(Callable(self, "_on_service_failed"))
func _seed_boundary_cells(bounds: Dictionary) -> void:
for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1):
for layer in [WorldService.LAYER_FRONT, WorldService.LAYER_BACK]:
var cell := Vector3i(x, 0, layer)
world_service.set_walkable(cell, true)
world_service.set_support(cell, 0, true)
world_service.consume_dirty_cells()
func _expand_world_to_rank(rank: int) -> bool:
var bounds := _bounds_for_rank(rank)
if not world_service.expand_bounds(bounds):
return false
_seed_boundary_cells(bounds)
return rebuild_path_graph_from_world()
func _bounds_for_rank(rank: int) -> Dictionary:
var source: Variant = ProgressionService.RANK_BOUNDS.get(clampi(rank, 1, 7), {})
return source.duplicate(true) if source is Dictionary else {}
func _sync_progression_live_metrics() -> bool:
return progression_service.update_metrics({
&"active_above_ground_facilities": _count_active_facilities(facility_service, false),
&"active_underground_facilities": _count_active_facilities(facility_service, true),
&"asset_gold": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
})
func _count_active_facilities(service: FacilityService, underground: bool) -> int:
var count := 0
for instance: FacilityInstance in service.get_instances():
if instance.state not in [FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED]:
continue
if (instance.origin.y < 0) == underground:
count += 1
return count
func _capture_rank_external_state() -> Dictionary:
return {
&"world": world_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"excavation": excavation_service.create_snapshot(),
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
&"route_projection": route_subslot_path_adapter.create_snapshot(),
&"facility_infrastructure": facility_infrastructure_projector.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
}
func _apply_rank_external_state(target_rank: int, expected_bounds: Dictionary) -> bool:
if target_rank != int(_state.world_tree_rank) + 1 \
or not _strict_variants_equal(_bounds_for_rank(target_rank), expected_bounds):
return false
if not _expand_world_to_rank(target_rank):
return false
if not _state.set_world_tree_rank(target_rank):
return false
return (
int(_state.world_tree_rank) == target_rank
and _strict_variants_equal(world_service.get_bounds(), expected_bounds)
and _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
)
and _validate_runtime_path_crosslinks(
path_service,
guest_service,
staff_service,
_movement_credits,
_clock.absolute_minute
)
)
func _rollback_rank_external_state(snapshot: Dictionary) -> bool:
if not snapshot.get(&"world", null) is Dictionary \
or not snapshot.get(&"path", null) is Dictionary \
or not snapshot.get(&"excavation", null) is Dictionary \
or not snapshot.get(&"spatial_subslots", null) is Dictionary \
or not snapshot.get(&"route_projection", null) is Dictionary \
or not snapshot.get(&"facility_infrastructure", null) is Dictionary \
or not snapshot.get(&"game_state", null) is Dictionary \
or not snapshot.get(&"movement_credits", null) is Dictionary \
or not snapshot.get(&"domain_events", null) is Dictionary \
or not snapshot.get(&"id_generator", null) is Dictionary:
return false
var restored := world_service.restore_snapshot(snapshot[&"world"])
restored = spatial_subslot_service.restore_snapshot(snapshot[&"spatial_subslots"]) and restored
var route_snapshot: Dictionary = snapshot[&"route_projection"]
var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null)
if not projected_walkable is Array:
return false
restored = path_service.rebuild_graph(projected_walkable) and restored
restored = path_service.restore_snapshot(snapshot[&"path"]) and restored
restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored
restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored
restored = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored
restored = facility_infrastructure_projector.restore_snapshot(
snapshot[&"facility_infrastructure"]
) and restored
restored = excavation_service.restore_snapshot(snapshot[&"excavation"]) and restored
restored = _state.restore_snapshot(snapshot[&"game_state"]) and restored
_movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true)
restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored
restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored
if not restored:
return false
return (
_strict_variants_equal(world_service.create_snapshot(), snapshot[&"world"])
and _strict_variants_equal(path_service.create_snapshot(), snapshot[&"path"])
and _strict_variants_equal(elevators.create_snapshot(), snapshot[&"elevators"])
and _strict_variants_equal(legacy_stairs.create_snapshot(), snapshot[&"legacy_stairs"])
and _strict_variants_equal(
excavation_service.create_snapshot(), snapshot[&"excavation"]
)
and _strict_variants_equal(
spatial_subslot_service.create_snapshot(), snapshot[&"spatial_subslots"]
)
and _strict_variants_equal(
route_subslot_path_adapter.create_snapshot(), snapshot[&"route_projection"]
)
and _strict_variants_equal(
facility_infrastructure_projector.create_snapshot(),
snapshot[&"facility_infrastructure"]
)
and _strict_variants_equal(_state.create_snapshot(), snapshot[&"game_state"])
and _strict_variants_equal(_movement_credits, snapshot[&"movement_credits"])
and _strict_variants_equal(domain_events.create_snapshot(), snapshot[&"domain_events"])
and _strict_variants_equal(id_generator.create_snapshot(), snapshot[&"id_generator"])
and _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
)
and _validate_runtime_path_crosslinks(
path_service,
guest_service,
staff_service,
_movement_credits,
_clock.absolute_minute
)
)
func _process_commands(absolute_minute: int) -> bool:
var previous_command_minute := _active_command_minute
_active_command_minute = absolute_minute
var results := command_queue.execute_ready(absolute_minute, Callable(self, "_execute_command"))
_active_command_minute = previous_command_minute
_synchronize_operations_population()
for result in results:
var entity_ids: Array[StringName] = []
if not result.target_id.is_empty():
entity_ids.append(result.target_id)
var event := SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"COMMAND_COMPLETED" if result.is_success() else &"COMMAND_FAILED",
absolute_minute,
&"simulation_runtime",
entity_ids,
result.to_dictionary(),
result.command_id
)
_publish_domain_event(event)
command_completed.emit(result)
if _operations_resume_publish_pending:
_operations_resume_publish_pending = false
_publish_snapshot(absolute_minute)
_process_save_checkpoints(absolute_minute)
return true
return false
func _process_tick_commands(current_absolute_minute: int) -> void:
if _operations_enabled:
_operations_tick_blocked = _operations_should_block_tick()
if _operations_tick_blocked:
_clock.set_paused(true)
_clock.request_current_tick_abort()
return
_process_commands(current_absolute_minute + 1)
_synchronize_operations_population()
func _synchronize_operations_population() -> bool:
if not _operations_enabled or operations_coordinator == null \
or _operations_transaction_active:
return true
var active_count := guest_service.guest_count()
if operations_coordinator.get_population_count() == active_count:
return true
var synchronized := operations_coordinator.set_population_count(active_count)
if synchronized.is_success():
return true
_operations_runtime_failure_latched = true
_operations_runtime_failure_report = synchronized.to_dictionary()
_operations_tick_blocked = true
_clock.set_paused(true)
return false
func _execute_command(command: SimulationCommand) -> CommandResult:
if not _executors.has(command.command_type):
return CommandResult.failed(
CommandResult.Code.INVALID_COMMAND,
command.command_id,
"No executor is registered for command type '%s'." % command.command_type
)
if command.command_type in DEFENSE_COMMAND_TYPES:
var advanced := defense_service.advance_time(_defense_operation_minute())
if not advanced.is_success():
advanced.command_id = command.command_id
return advanced
var executor: Callable = _executors[command.command_type]
var returned: Variant = executor.call(command)
if returned is CommandResult:
return returned
return CommandResult.failed(
CommandResult.Code.EXECUTOR_FAILED,
command.command_id,
"Command executor returned an invalid result."
)
func _execute_change_resources(command: SimulationCommand) -> CommandResult:
var deltas: Variant = command.payload.get(&"deltas", command.payload.get("deltas", null))
if not deltas is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "deltas must be a Dictionary.")
var state_before: Dictionary = _state.create_snapshot()
var economy_before := economy_service.create_snapshot()
var progression_before := progression_service.create_snapshot()
var events_before := domain_events.create_snapshot()
var ids_before := id_generator.create_snapshot()
if not _apply_command_resource_deltas(command, deltas):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Resource balance would become negative.")
var faith_delta := 0
for raw_resource_id: Variant in deltas:
if StringName(raw_resource_id) == &"faith":
faith_delta += int(deltas[raw_resource_id])
if faith_delta > 0:
var next_faith_lifetime := int(progression_service.metrics[&"faith_lifetime"]) + faith_delta
if not progression_service.update_metrics({&"faith_lifetime": next_faith_lifetime}):
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
progression_service.restore_snapshot(progression_before)
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(ids_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Progression lifetime Faith could not be updated atomically."
)
return CommandResult.succeeded(command.command_id, {
&"revision": _state.revision,
&"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
&"faith_lifetime": int(progression_service.metrics[&"faith_lifetime"]),
})
# Repay an existing attested liability through the ordinary command queue.
# No rate, issuance, eligibility or due-date values are supplied here.
func _execute_repay_loan_principal(command: SimulationCommand) -> CommandResult:
var loan_id: Variant = command.payload.get(&"loan_id", null)
if typeof(loan_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(loan_id).is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "loan_id must be a nonempty String or StringName.")
var result := repay_loan_principal(
StringName(loan_id), command.payload.get(&"amount", null), command.idempotency_key
)
result.command_id = command.command_id
result.target_id = StringName(loan_id)
return result
func _execute_spend_resources(command: SimulationCommand) -> CommandResult:
var cost: Variant = command.payload.get(&"cost", command.payload.get("cost", null))
if not cost is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "cost must be a Dictionary.")
if not operating_resource_state.spend(cost):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Resources are insufficient.")
return CommandResult.succeeded(command.command_id, {
&"revision": _state.revision,
&"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
})
func _apply_command_resource_deltas(command: SimulationCommand, deltas: Dictionary) -> bool:
var normalized: Dictionary = {}
for raw_name: Variant in deltas:
var resource_name := StringName(raw_name)
normalized[resource_name] = int(normalized.get(resource_name, 0)) + int(deltas[raw_name])
for resource_name: StringName in normalized:
var current := economy_service.get_balance(PLAYER_CASH_ACCOUNT) \
if resource_name == &"gold" else int(_state.get_resource(resource_name))
if current + int(normalized[resource_name]) < 0:
return false
var before_state: Dictionary = _state.create_snapshot()
var before_economy: Dictionary = economy_service.create_snapshot()
var non_gold := normalized.duplicate(true)
var gold_delta := int(non_gold.get(&"gold", 0))
non_gold.erase(&"gold")
if not non_gold.is_empty() and not _state.apply_resource_deltas(non_gold):
return false
if gold_delta != 0:
var ledger_result: CommandResult
var ledger_idempotency := StringName("runtime.resource.%08d" % command.command_id)
if gold_delta > 0:
ledger_result = economy_service.transfer(
EconomyService.SYSTEM_SOURCE,
PLAYER_CASH_ACCOUNT,
gold_delta,
_runtime_event_minute(),
&"GENERATION",
command.actor_id,
ledger_idempotency
)
else:
ledger_result = economy_service.transfer(
PLAYER_CASH_ACCOUNT,
EconomyService.SYSTEM_SINK,
-gold_delta,
_runtime_event_minute(),
&"DESTRUCTION",
command.actor_id,
ledger_idempotency
)
if not ledger_result.is_success():
_state.restore_snapshot(before_state)
economy_service.restore_snapshot(before_economy)
return false
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
return true
func _execute_add_inventory_stack(command: SimulationCommand) -> CommandResult:
var owner_id := StringName(command.payload.get(&"owner_inventory_id", command.payload.get("owner_inventory_id", "")))
var item_id := StringName(command.payload.get(&"item_id", command.payload.get("item_id", "")))
var quantity := int(command.payload.get(&"quantity", command.payload.get("quantity", 0)))
var result := inventory_service.add_stack(
owner_id,
item_id,
quantity,
int(command.payload.get(&"quality", command.payload.get("quality", InventoryService.DEFAULT_QUALITY))),
int(command.payload.get(&"expires_at", command.payload.get("expires_at", -1))),
_runtime_event_minute(),
int(command.payload.get(&"unit_weight", command.payload.get("unit_weight", InventoryService.DEFAULT_UNIT_WEIGHT))),
StringName(command.payload.get(&"stack_id", command.payload.get("stack_id", "")))
)
return _command_result_from_dictionary(command.command_id, result, "Inventory stack could not be added.")
func _execute_create_logistics_request(command: SimulationCommand) -> CommandResult:
var source_id := StringName(command.payload.get(&"source_inventory_id", command.payload.get("source_inventory_id", "")))
var destination_id := StringName(command.payload.get(&"destination_inventory_id", command.payload.get("destination_inventory_id", "")))
var raw_requirements: Variant = command.payload.get(&"item_requirements", command.payload.get("item_requirements", null))
if source_id.is_empty() or destination_id.is_empty() or source_id == destination_id \
or not raw_requirements is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Source, destination, and item requirements are required.")
var requirements: Dictionary = {}
for raw_item_id: Variant in raw_requirements:
var item_id := StringName(raw_item_id)
var quantity := int(raw_requirements[raw_item_id])
if item_id.is_empty() or quantity <= 0:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Logistics requirements must be positive.")
requirements[item_id] = quantity
if requirements.is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "At least one item is required.")
var inventory_before := inventory_service.create_snapshot()
var id_before := id_generator.create_snapshot()
var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", "")))
if request_id.is_empty():
request_id = id_generator.next_id(&"logistics_request")
if logistics_requests.has(request_id):
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Logistics request id already exists.")
var reservation_ids: Array[StringName] = []
var stack_ids: Array[StringName] = []
var total_weight := 0
var deadline := int(command.payload.get(&"deadline", command.payload.get("deadline", -1)))
for item_id: StringName in _sorted_name_keys(requirements):
var reserve_result := inventory_service.reserve_item(
source_id,
item_id,
int(requirements[item_id]),
request_id,
&"logistics",
_runtime_event_minute(),
deadline,
StringName("logistics.reserve.%s.%s" % [request_id, item_id])
)
if not bool(reserve_result.get(&"ok", false)):
inventory_service.restore_snapshot(inventory_before)
id_generator.restore_snapshot(id_before)
return _command_result_from_dictionary(command.command_id, reserve_result, "Logistics stock could not be reserved.")
for reservation_id: StringName in reserve_result.get(&"reservation_ids", []):
reservation_ids.append(reservation_id)
var reservation := inventory_service.get_reservation(reservation_id)
var stack := inventory_service.get_stack(reservation.stack_id)
stack_ids.append(reservation.stack_id)
total_weight += reservation.quantity * int(stack.get(&"unit_weight", 1))
var request := LogisticsRequest.new().configure(
request_id,
source_id,
destination_id,
requirements,
int(command.payload.get(&"priority", command.payload.get("priority", 50))),
_runtime_event_minute(),
deadline,
total_weight
)
if not request.mark_reserved(reservation_ids, stack_ids, _runtime_event_minute()):
inventory_service.restore_snapshot(inventory_before)
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Logistics request could not enter RESERVED state.")
logistics_requests[request_id] = request
return CommandResult.succeeded(command.command_id, {
&"request_id": request_id,
&"reservation_ids": reservation_ids,
&"total_weight": total_weight,
})
func _execute_dispatch_logistics(command: SimulationCommand) -> CommandResult:
var carriers: Variant = command.payload.get(&"carriers", command.payload.get("carriers", null))
if not carriers is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "carriers must be a Dictionary.")
var manual_requests: Array[LogisticsRequest] = []
for request: LogisticsRequest in _sorted_logistics_requests():
if not String(request.request_id).begins_with(SupplyLogisticsBridge.REQUEST_PREFIX):
manual_requests.append(request)
var assignments := logistics_dispatcher.dispatch(manual_requests, carriers, _runtime_event_minute())
return CommandResult.succeeded(command.command_id, {
&"assignments": assignments,
&"report": logistics_dispatcher.last_report.duplicate(true),
})
func _execute_advance_logistics_request(command: SimulationCommand) -> CommandResult:
var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", "")))
if command.command_id > 0 and String(request_id).begins_with(SupplyLogisticsBridge.REQUEST_PREFIX):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Supply deliveries advance only after the porter arrives.", {&"reason": &"SUPPLY_DRIVER_REQUIRED"})
if not logistics_requests.has(request_id):
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Logistics request was not found.")
var request: LogisticsRequest = logistics_requests[request_id]
var request_before := request.create_snapshot()
var inventory_before_command := inventory_service.create_snapshot()
var dispatcher_before := logistics_dispatcher.create_snapshot()
match request.state:
LogisticsRequest.State.ASSIGNED:
if not request.transition_to(LogisticsRequest.State.PICKING_UP, "", _runtime_event_minute()):
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Logistics pickup could not begin.")
LogisticsRequest.State.PICKING_UP:
var carrier_id := request.assigned_carrier_id
var pickup := inventory_service.transfer_reserved(
request.reservation_ids,
StringName("cargo:%s" % carrier_id),
StringName("logistics.pickup.%s" % request_id),
_runtime_event_minute()
)
if not bool(pickup.get(&"ok", false)):
request.mark_blocked(String(pickup.get(&"code", &"PICKUP_FAILED")), _runtime_event_minute())
var released := logistics_dispatcher.release_assignment_for_request(
request_id, carrier_id, request.total_weight
)
if not released:
inventory_service.restore_snapshot(inventory_before_command)
logistics_dispatcher.restore_snapshot(dispatcher_before)
logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Logistics pickup failure could not release request ownership."
)
return _command_result_from_dictionary(command.command_id, pickup, "Logistics pickup failed.")
request.transition_to(LogisticsRequest.State.DELIVERING, "", _runtime_event_minute())
LogisticsRequest.State.DELIVERING:
var carrier_id := request.assigned_carrier_id
var cargo_id := StringName("cargo:%s" % carrier_id)
var inventory_before := inventory_service.create_snapshot()
for item_id: StringName in _sorted_name_keys(request.item_requirements):
var delivery := inventory_service.transfer_available(
cargo_id,
request.destination_inventory_id,
item_id,
int(request.item_requirements[item_id]),
StringName("logistics.delivery.%s.%s" % [request_id, item_id]),
_runtime_event_minute()
)
if not bool(delivery.get(&"ok", false)):
inventory_service.restore_snapshot(inventory_before)
request.mark_blocked(String(delivery.get(&"code", &"DELIVERY_FAILED")), _runtime_event_minute())
var released := logistics_dispatcher.release_assignment_for_request(
request_id, carrier_id, request.total_weight
)
if not released:
inventory_service.restore_snapshot(inventory_before_command)
logistics_dispatcher.restore_snapshot(dispatcher_before)
logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Logistics delivery failure could not release request ownership."
)
return _command_result_from_dictionary(command.command_id, delivery, "Logistics delivery failed.")
request.transition_to(LogisticsRequest.State.COMPLETED, "", _runtime_event_minute())
var released := logistics_dispatcher.release_assignment_for_request(
request_id, carrier_id, request.total_weight
)
if not released:
inventory_service.restore_snapshot(inventory_before_command)
logistics_dispatcher.restore_snapshot(dispatcher_before)
logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Completed Logistics delivery could not release request ownership."
)
_:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Logistics request cannot advance from its current state.")
return CommandResult.succeeded(command.command_id, {
&"request_id": request_id,
&"state": LogisticsRequest.State.keys()[request.state],
})
func _execute_register_guest_wallet(command: SimulationCommand) -> CommandResult:
var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", "")))
var amount := int(command.payload.get(&"gold", command.payload.get("gold", 0)))
if guest_id.is_empty() or amount < 0:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "A guest id and non-negative Gold are required.")
var account_id := _guest_wallet_account(guest_id)
if economy_service.has_account(account_id):
return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Guest wallet already exists.")
var before := economy_service.create_snapshot()
if not economy_service.register_account(account_id):
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Guest wallet account could not be registered.")
if amount > 0:
var initialized := economy_service.initialize_account(
account_id,
amount,
_runtime_event_minute(),
StringName("guest.wallet.%s" % guest_id)
)
if not initialized.is_success():
economy_service.restore_snapshot(before)
return initialized
return CommandResult.succeeded(command.command_id, {&"account_id": account_id, &"balance": amount})
func _execute_spawn_guest(command: SimulationCommand) -> CommandResult:
if _operations_enabled and operations_coordinator != null:
if operations_coordinator.is_terminal() \
or operations_coordinator.is_external_quarantined():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Operations currently block new Guest arrivals."
)
if guest_service.guest_count() >= int(
operations_coordinator.get_config().get(&"population_cap", 0)
):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Operations Guest population capacity is full."
)
var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", "")))
var id_before := id_generator.create_snapshot()
if guest_id.is_empty():
guest_id = id_generator.next_id(&"guest")
if String(guest_id).begins_with("guest-auto-") \
and not (_operations_transaction_active \
and operations_coordinator != null \
and command.actor_id == &"operations_coordinator"):
id_generator.restore_snapshot(id_before)
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"guest-auto IDs are reserved for the OperationsCoordinator."
)
var guest_type_id := StringName(command.payload.get(&"guest_type_id", command.payload.get("guest_type_id", "traveler")))
var definition := guest_service.get_definition(guest_type_id)
if definition == null:
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest definition was not found.")
if guest_service.has_guest(guest_id):
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Guest already exists.")
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {}
var initial_cell_value: Variant = _parse_world_cell(
options.get(&"current_cell", options.get("current_cell", Vector3i.ZERO))
)
if initial_cell_value == null:
id_generator.restore_snapshot(id_before)
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"Guest current_cell must be Vector3i or [x, y, layer]."
)
var initial_cell: Vector3i = initial_cell_value
var initial_traffic := path_service.get_cell_traffic(initial_cell)
var path_actor_id := _guest_path_actor_id(guest_id)
if initial_traffic.is_empty() or bool(initial_traffic.get(&"temporary_block", false)) \
or path_service.has_actor_projection(path_actor_id):
id_generator.restore_snapshot(id_before)
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Guest current_cell must be an unblocked walkable cell with a free Path actor id."
)
options[&"current_cell"] = initial_cell
var requested_gold := int(command.payload.get(
&"gold",
command.payload.get("gold", options.get(&"wallet_gold", options.get("wallet_gold", definition.wealth_min)))
))
if requested_gold < 0:
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Guest Gold cannot be negative.")
var defer_spawn_notification := _operations_transaction_active \
and operations_coordinator != null \
and command.actor_id == &"operations_coordinator"
var account_id := _guest_wallet_account(guest_id)
var economy_before := economy_service.create_snapshot()
var guest_before := guest_service.create_snapshot()
var path_before := path_service.create_snapshot()
var movement_credits_before := _movement_credits.duplicate(true)
var events_before := domain_events.create_snapshot()
var progression_before := progression_service.create_snapshot()
if economy_service.has_account(account_id):
requested_gold = economy_service.get_balance(account_id)
else:
if not economy_service.register_account(account_id):
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Guest wallet account could not be registered.")
if requested_gold > 0:
var initialized := economy_service.initialize_account(
account_id,
requested_gold,
_runtime_event_minute(),
StringName("guest.wallet.%s" % guest_id),
not defer_spawn_notification
)
if not initialized.is_success():
economy_service.restore_snapshot(economy_before)
id_generator.restore_snapshot(id_before)
initialized.command_id = command.command_id
return initialized
if defer_spawn_notification \
and not _stage_operations_economy_notification(initialized):
economy_service.restore_snapshot(economy_before)
id_generator.restore_snapshot(id_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Guest wallet notification could not be staged."
)
options[&"wallet_gold"] = requested_gold
var guest := guest_service.spawn_guest(
guest_id,
guest_type_id,
_runtime_event_minute(),
options,
not defer_spawn_notification
)
if guest != null and defer_spawn_notification:
# Runtime's Path projection and DomainEvent are part of the external
# transaction, while the observable GuestService signal is held until the
# Coordinator minute and DomainEvent batch have both committed.
_on_guest_spawned(guest)
if guest == null or not path_service.has_actor_projection(path_actor_id) \
or path_service.get_actor_projected_cell(path_actor_id) != guest.current_cell \
or not _movement_credits.has(path_actor_id):
economy_service.restore_snapshot(economy_before)
guest_service.restore_snapshot(guest_before)
path_service.restore_snapshot(path_before)
_movement_credits = movement_credits_before
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(id_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
guest_service.last_error if guest == null else "Guest Path projection failed."
)
var next_guest_lifetime := int(progression_service.metrics[&"guest_lifetime"]) + 1
if not progression_service.update_metrics({&"guest_lifetime": next_guest_lifetime}):
economy_service.restore_snapshot(economy_before)
guest_service.restore_snapshot(guest_before)
path_service.restore_snapshot(path_before)
_movement_credits = movement_credits_before
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(id_before)
progression_service.restore_snapshot(progression_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Progression lifetime Guest count could not be updated atomically."
)
if defer_spawn_notification:
_operations_queued_guest_spawn_notifications.append(guest_id)
return CommandResult.succeeded(command.command_id, {
&"guest_id": guest_id,
&"guest_type_id": guest_type_id,
&"wallet_account": account_id,
&"wallet_gold": requested_gold,
&"guest_lifetime": next_guest_lifetime,
})
func _execute_transition_guest(command: SimulationCommand) -> CommandResult:
var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", "")))
var state_index := _guest_state_index(command.payload.get(&"state", command.payload.get("state", -1)))
if guest_id.is_empty() or state_index < 0:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Guest id and valid state are required.")
var guest := guest_service.get_guest(guest_id)
if guest == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest was not found.")
if not guest.queue_request_id.is_empty() or not guest.current_service_id.is_empty():
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"A queued or serviced Guest must transition through the owning Facility transaction."
)
var result := guest_service.transition_guest(
guest_id,
state_index,
_runtime_event_minute(),
StringName(command.payload.get(&"reason", command.payload.get("reason", "PLAYER_COMMAND")))
)
return _command_result_from_dictionary(command.command_id, result, "Guest transition failed.")
func _execute_decide_guest(command: SimulationCommand) -> CommandResult:
var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", "")))
var guest := guest_service.get_guest(guest_id)
if guest == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest was not found.")
var wallet_account := _guest_wallet_account(guest_id)
if economy_service.has_account(wallet_account):
guest.wallet_gold = economy_service.get_balance(wallet_account)
var raw_candidates: Variant = command.payload.get(&"candidates", command.payload.get("candidates", []))
var candidates: Array = raw_candidates.duplicate(true) if raw_candidates is Array else []
if candidates.is_empty():
candidates = _project_guest_candidates(guest_id)
var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {}))
var context: Dictionary = raw_context.duplicate(true) if raw_context is Dictionary else {}
context[&"absolute_minute"] = _runtime_event_minute()
var result := _decide_guest_with_log(guest_id, candidates, _runtime_event_minute(), context)
return _command_result_from_dictionary(command.command_id, result, "Guest decision failed.")
func _execute_register_staff(command: SimulationCommand) -> CommandResult:
var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", "")))
var role_id := StringName(command.payload.get(&"role_id", command.payload.get("role_id", "")))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if staff_id.is_empty() or role_id.is_empty() or not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Staff id, role id and options are required.")
if _is_mvp_staff_candidate_id(staff_id) or mvp_content_catalog.get_unique_character_ids().has(staff_id):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Catalog identities must be created through their validated recruitment flow."
)
var options: Dictionary = raw_options.duplicate(true)
var initial_cell_value: Variant = _parse_world_cell(
options.get(&"current_cell", options.get("current_cell", Vector3i.ZERO))
)
if initial_cell_value == null:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"Staff current_cell must be Vector3i or [x, y, layer]."
)
var initial_cell: Vector3i = initial_cell_value
var initial_traffic := path_service.get_cell_traffic(initial_cell)
var path_actor_id := _staff_path_actor_id(staff_id)
if initial_traffic.is_empty() or bool(initial_traffic.get(&"temporary_block", false)) \
or path_service.has_actor_projection(path_actor_id):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Staff current_cell must be an unblocked walkable cell with a free Path actor id."
)
options[&"current_cell"] = initial_cell
var staff_before := staff_service.create_snapshot()
var path_before := path_service.create_snapshot()
var credits_before := _movement_credits.duplicate(true)
var events_before := domain_events.create_snapshot()
var ids_before := id_generator.create_snapshot()
options[&"employment_effective_minute"] = _runtime_event_minute()
var result := staff_service.create_staff(staff_id, role_id, options)
if result.is_success() and (
not path_service.has_actor_projection(path_actor_id)
or path_service.get_actor_projected_cell(path_actor_id) != initial_cell
or not _movement_credits.has(path_actor_id)
):
staff_service.restore_snapshot(staff_before)
path_service.restore_snapshot(path_before)
_movement_credits = credits_before
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(ids_before)
result = CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Staff Path projection failed."
)
result.command_id = command.command_id
return result
# Original 6.1 plus the approved Anri staff-screen choice. Other unique
# characters still have no approved joining timing. Zero signing cost and both
# canonical wage fields must be actual integers; payload aliases cannot qualify.
func _is_approved_free_unique_hire(candidate_id: StringName, canonical: Dictionary) -> bool:
if candidate_id != &"heroine_anri" or not mvp_content_catalog.is_valid():
return false
var staff_id_value: Variant = canonical.get(&"staff_id", null)
var role_value: Variant = canonical.get(&"role_id", null)
var options_value: Variant = canonical.get(&"options", null)
if typeof(role_value) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(role_value) != &"innkeeper":
return false
if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(staff_id_value) != candidate_id or not options_value is Dictionary:
return false
var options: Dictionary = options_value
var employment_value: Variant = options.get(&"employment_kind", null)
if typeof(employment_value) not in [TYPE_STRING, TYPE_STRING_NAME] \
or StringName(employment_value) != &"UNIQUE" \
or typeof(canonical.get(&"adult_verified", null)) != TYPE_BOOL \
or not bool(canonical[&"adult_verified"]):
return false
for value: Variant in [canonical.get(&"signing_cost", null), canonical.get(&"daily_wage", null), options.get(&"daily_wage", null)]:
if typeof(value) != TYPE_INT or int(value) != 0:
return false
return true
func _execute_hire_staff_candidate(command: SimulationCommand) -> CommandResult:
# Ordinary contracts retain the investment gate and its existing error
# priority. Only Anri has approved free recruitment; classify that exception
# from validated canonical data, never caller-supplied costs or employment.
var operations_gate := _operations_construction_investment_gate(command.command_id)
var raw_candidate_id: Variant = command.payload.get(
&"candidate_id", command.payload.get("candidate_id", null)
)
if operations_gate != null and (
typeof(raw_candidate_id) not in [TYPE_STRING, TYPE_STRING_NAME]
or StringName(raw_candidate_id) != &"heroine_anri"
):
return operations_gate
if typeof(raw_candidate_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"A catalog Staff candidate id is required."
)
var candidate_id := StringName(raw_candidate_id)
var canonical := mvp_content_catalog.staff_hire_payload(candidate_id)
if canonical.is_empty():
return CommandResult.failed(
CommandResult.Code.NOT_FOUND,
command.command_id,
"Staff candidate was not found in the validated MVP catalog."
)
if not bool(canonical.get(&"adult_verified", false)):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Staff candidate does not have verified adult eligibility."
)
if operations_gate != null:
var reason := StringName(operations_gate.details.get(&"reason", &""))
var can_join_without_investment := _is_approved_free_unique_hire(candidate_id, canonical) \
and reason in [&"ARREARS_REMAIN", &"UNPAID_EXPENSES"] \
and not _operations_should_block_tick()
if not can_join_without_investment:
return operations_gate
if _hired_staff_candidate_ids.has(candidate_id) or staff_service.has_staff(candidate_id):
return CommandResult.failed(
CommandResult.Code.DUPLICATE_COMMAND_ID,
command.command_id,
"Staff candidate has already been hired."
)
var initial_cell_value: Variant = _parse_world_cell(
command.payload.get(&"current_cell", command.payload.get("current_cell", Vector3i.ZERO))
)
if initial_cell_value == null:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"Staff current_cell must be Vector3i or [x, y, layer]."
)
var initial_cell: Vector3i = initial_cell_value
var path_actor_id := _staff_path_actor_id(candidate_id)
var traffic := path_service.get_cell_traffic(initial_cell)
if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)) \
or path_service.has_actor_projection(path_actor_id):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Staff current_cell must be an unblocked walkable cell with a free Path actor id."
)
var assigned_facility_id := StringName(command.payload.get(
&"facility_id", command.payload.get("facility_id", "")
))
if not assigned_facility_id.is_empty() \
and not facility_service.has_instance(assigned_facility_id):
return CommandResult.failed(
CommandResult.Code.NOT_FOUND,
command.command_id,
"Assigned Staff facility was not found."
)
var before := {
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"staff": staff_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"hired_ids": _hired_staff_candidate_ids.duplicate(true),
&"staffing_projection_enabled": _staffing_projection_enabled,
}
var signing_cost := int(canonical[&"signing_cost"])
if signing_cost > 0:
var payment := economy_service.transfer(
PLAYER_CASH_ACCOUNT,
OPERATING_EXPENSE_ACCOUNT,
signing_cost,
_runtime_event_minute(),
&"STAFF_SIGNING_COST",
candidate_id,
StringName("staff_signing_%s" % candidate_id),
&"staff.signing_cost"
)
if not payment.is_success():
payment.command_id = command.command_id
return payment
var options: Dictionary = (canonical[&"options"] as Dictionary).duplicate(true)
options[&"current_cell"] = initial_cell
options[&"assigned_facility_id"] = assigned_facility_id
options[&"employment_effective_minute"] = _runtime_event_minute()
var staff_result := staff_service.create_staff(
candidate_id,
StringName(canonical[&"role_id"]),
options
)
if not staff_result.is_success():
_rollback_staff_candidate_hire(before)
staff_result.command_id = command.command_id
return staff_result
var preferred_shift := StringName(canonical[&"preferred_shift"])
var shift_day := _candidate_shift_start_day(preferred_shift, _runtime_event_minute())
var shift_result: CommandResult
if preferred_shift in [&"day", &"night"]:
shift_result = staff_service.create_standard_shift(
candidate_id,
preferred_shift,
shift_day,
assigned_facility_id
)
else:
shift_result = staff_service.create_flex_shift(
candidate_id,
shift_day,
ShiftInstance.DAY_START,
ShiftInstance.DAY_END,
assigned_facility_id
)
var shift_id := StringName(shift_result.details.get(&"shift_id", &""))
var hired_staff := staff_service.get_staff(candidate_id)
var hired_shift := staff_service.get_shift(shift_id)
if not shift_result.is_success() or hired_staff == null or hired_shift == null \
or hired_staff.daily_wage != int(canonical[&"daily_wage"]) \
or hired_staff.get_employment_kind_name() != StringName(canonical[&"options"][&"employment_kind"]) \
or not path_service.has_actor_projection(path_actor_id) \
or path_service.get_actor_projected_cell(path_actor_id) != initial_cell \
or not _movement_credits.has(path_actor_id):
_rollback_staff_candidate_hire(before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Staff hire could not commit its employee, Shift, finance, and Path state atomically."
)
_hired_staff_candidate_ids[candidate_id] = true
_publish_actor_event(
&"STAFF_CANDIDATE_HIRED",
&"simulation_runtime",
[candidate_id, shift_id],
{
&"candidate_id": candidate_id,
&"role_id": StringName(canonical[&"role_id"]),
&"daily_wage": int(canonical[&"daily_wage"]),
&"employment_kind": hired_staff.get_employment_kind_name(),
&"signing_cost": signing_cost,
&"shift_id": shift_id,
&"preferred_shift": preferred_shift,
}
)
return CommandResult.succeeded(command.command_id, {
&"candidate_id": candidate_id,
&"staff_id": candidate_id,
&"role_id": StringName(canonical[&"role_id"]),
&"daily_wage": int(canonical[&"daily_wage"]),
&"employment_kind": hired_staff.get_employment_kind_name(),
&"signing_cost": signing_cost,
&"shift_id": shift_id,
&"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
})
func _execute_create_staff_shift(command: SimulationCommand) -> CommandResult:
var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", "")))
var shift_id := StringName(command.payload.get(&"shift_id", command.payload.get("shift_id", "")))
var start_day := int(command.payload.get(&"start_day", command.payload.get("start_day", _runtime_event_minute() / 1_440)))
var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", "")))
var template_id := StringName(command.payload.get(&"template_id", command.payload.get("template_id", "FLEX")))
var result: CommandResult
if String(template_id).to_upper() in ["DAY", "NIGHT"]:
result = staff_service.create_standard_shift(staff_id, template_id, start_day, facility_id, shift_id)
else:
var start_minute := int(command.payload.get(&"start_minute_of_day", command.payload.get("start_minute_of_day", posmod(_runtime_event_minute(), 1_440))))
var end_minute := int(command.payload.get(&"end_minute_of_day", command.payload.get("end_minute_of_day", posmod(start_minute + 480, 1_440))))
result = staff_service.create_flex_shift(staff_id, start_day, start_minute, end_minute, facility_id, shift_id)
result.command_id = command.command_id
return result
func _candidate_shift_start_day(preferred_shift: StringName, absolute_minute: int) -> int:
var day := absolute_minute / ShiftInstance.MINUTES_PER_DAY
var minute_of_day := posmod(absolute_minute, ShiftInstance.MINUTES_PER_DAY)
# A NIGHT record starts at 18:00 of its named day and remains meaningful even
# when hired after that start. DAY/FLEX records move to tomorrow once today's
# daytime window has ended.
if preferred_shift != &"night" and minute_of_day >= ShiftInstance.DAY_END:
day += 1
return day
func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
var restored := economy_service.restore_snapshot(snapshot[&"economy"])
restored = _state.restore_snapshot(snapshot[&"game_state"]) and restored
restored = staff_service.restore_snapshot(snapshot[&"staff"]) and restored
restored = path_service.restore_snapshot(snapshot[&"path"]) and restored
restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored
restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored
_movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true)
restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored
restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored
_hired_staff_candidate_ids = (snapshot[&"hired_ids"] as Dictionary).duplicate(true)
_staffing_projection_enabled = bool(snapshot[&"staffing_projection_enabled"])
return restored and _validate_runtime_path_crosslinks(
path_service,
guest_service,
staff_service,
_movement_credits,
_runtime_event_minute()
)
func _staff_role_matches_facility(role_id: StringName, definition: FacilityDefinition) -> bool:
if definition.required_roles.is_empty() or role_id in definition.required_roles:
return true
# Existing public-bath data uses a legacy job ID; the paid roster exposes
# the canonical attendant. The other aliases likewise retain saved job IDs.
if definition.id == &"public_bath" and role_id == &"attendant" and &"bath_attendant" in definition.required_roles:
return true
if definition.id == &"massage_room" and role_id == &"masseur" and &"therapist" in definition.required_roles:
return true
return definition.id == &"security_station" and role_id == &"guard" and &"security" in definition.required_roles
func _execute_set_staff_work_assignment(command: SimulationCommand) -> CommandResult:
for key: StringName in [&"staff_id", &"facility_id", &"template_id"]:
if typeof(command.payload.get(key, null)) not in [TYPE_STRING, TYPE_STRING_NAME]:
return _staff_assignment_failure(command.command_id, &"INVALID_PAYLOAD", "Staff, facility, and shift IDs must be strings.")
var staff_id := StringName(command.payload[&"staff_id"])
var facility_id := StringName(command.payload[&"facility_id"])
var template_id := StringName(command.payload[&"template_id"])
var staff := staff_service.get_staff(staff_id)
if staff == null:
return _staff_assignment_failure(command.command_id, &"STAFF_NOT_FOUND", "Staff was not found.")
if template_id not in [&"DAY", &"NIGHT"]:
return _staff_assignment_failure(command.command_id, &"INVALID_TEMPLATE", "Choose DAY or NIGHT.")
var block_reason := _staff_assignment_block_reason(staff)
if not block_reason.is_empty():
var messages := {
&"DEFENSE_LOCKED": "Staff is participating in a defense operation.",
&"EVENT_LOCKED": "Staff is participating in an event.",
&"STAFF_BUSY": "Finish the current task before changing assignment.",
&"STAFF_UNAVAILABLE": "Staff cannot change assignment in the current state.",
}
return _staff_assignment_failure(command.command_id, block_reason, messages[block_reason])
if not facility_id.is_empty():
var facility := facility_service.get_instance(facility_id)
if facility == null:
return _staff_assignment_failure(command.command_id, &"FACILITY_NOT_FOUND", "Facility was not found.")
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null or definition.staff_max <= 0:
return _staff_assignment_failure(command.command_id, &"FACILITY_UNSTAFFED", "This facility does not accept Staff assignments.")
if not _staff_role_matches_facility(staff.role_id, definition) and not _is_anri_manager(staff):
return _staff_assignment_failure(command.command_id, &"ROLE_MISMATCH", "Staff role does not match this facility.")
if _staff_assignment_count(facility_id, template_id, staff_id) >= definition.staff_max:
return _staff_assignment_failure(command.command_id, &"FACILITY_FULL", "This shift has reached the facility Staff limit.")
if not _staff_can_reach_facility(staff, facility):
return _staff_assignment_failure(command.command_id, &"NO_ROUTE", "Staff cannot reach this facility through an accessible route.")
var result := staff_service.set_work_assignment(staff_id, facility_id, template_id, _runtime_event_minute())
result.command_id = command.command_id
if result.is_success():
var actor_id := _staff_path_actor_id(staff_id)
var trip := elevators.get_actor_trip(actor_id)
if result.details.get(&"reason", &"") == &"ASSIGNMENT_UPDATED" and trip.get(&"state", &"") == PathService.ELEVATOR_REQUEST_QUEUED:
elevators.cancel_actor(actor_id)
_staffing_projection_enabled = true
_sync_facility_staffing(_runtime_event_minute())
if result.details.get(&"reason", &"") == &"ASSIGNMENT_UPDATED":
_publish_actor_event(&"STAFF_WORK_ASSIGNMENT_CHANGED", &"staff_service", [staff_id], result.details)
return result
## Shared by the command and presentation; always derived from live state.
func _staff_assignment_block_reason(staff: StaffRuntime) -> StringName:
if not _defense_lock_owner(staff.staff_id, &"DEFENDER").is_empty():
return &"DEFENSE_LOCKED"
if event_service.get_actor_locks().has(staff.staff_id):
return &"EVENT_LOCKED"
if not staff.current_task_id.is_empty():
return &"STAFF_BUSY"
if not staff.can_take_shift() or staff.state not in [StaffRuntime.State.OFF_DUTY, StaffRuntime.State.COMMUTING, StaffRuntime.State.AVAILABLE]:
return &"STAFF_UNAVAILABLE"
return &""
func _staff_assignment_failure(command_id: int, reason: StringName, message: String) -> CommandResult:
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command_id, message, {&"reason": reason})
func _staff_assignment_count(facility_id: StringName, template_id: StringName, excluded_staff: StringName) -> int:
var counted: Dictionary = {}
for row: Dictionary in staff_service.get_work_assignments():
var staff_id := StringName(row[&"staff_id"])
if staff_id != excluded_staff and StringName(row[&"facility_id"]) == facility_id \
and StringName(row[&"template_id"]) == template_id:
counted[staff_id] = true
var interval := StaffService._work_interval(template_id, _runtime_event_minute())
for shift: ShiftInstance in staff_service.get_shifts():
if shift.staff_id != excluded_staff and not shift.cancelled \
and shift.assigned_facility_id == facility_id \
and shift.end_at > maxi(interval.x, _runtime_event_minute()) and shift.start_at < interval.y:
counted[shift.staff_id] = true
return counted.size()
func _staff_can_reach_facility(staff: StaffRuntime, facility: FacilityInstance) -> bool:
var occupied: Array[Vector3i] = facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
var candidates: Dictionary = {}
for cell: Vector3i in occupied:
for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
var candidate := cell + offset
if path_service.has_cell(candidate):
candidates[candidate] = true
for candidate: Vector3i in candidates:
if not path_service.find_path(staff.current_cell, candidate, [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]).is_empty():
return true
return false
func _execute_set_staff_schedule_override(command: SimulationCommand) -> CommandResult:
var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", "")))
var start_day := int(command.payload.get(&"start_day", command.payload.get("start_day", -1)))
var patch_value: Variant = command.payload.get(&"patch", command.payload.get("patch", null))
if not patch_value is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Schedule override patch must be a Dictionary.")
var result := staff_service.set_schedule_override(staff_id, start_day, patch_value)
result.command_id = command.command_id
return result
func _execute_create_staff_task(command: SimulationCommand) -> CommandResult:
var task_type := StringName(command.payload.get(&"task_type", command.payload.get("task_type", "")))
var priority := int(command.payload.get(&"priority", command.payload.get("priority", 50)))
var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", "")))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if task_type == CleaningBridge.TASK_TYPE:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Cleaning jobs are scheduled from actual facility dirt.")
if task_type.is_empty() or not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Task type and options are required.")
var result := staff_service.create_task(task_type, priority, _runtime_event_minute(), raw_options, task_id)
result.command_id = command.command_id
return result
func _execute_begin_staff_task(command: SimulationCommand) -> CommandResult:
var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", "")))
var result := staff_service.begin_task(task_id, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_progress_staff_task(command: SimulationCommand) -> CommandResult:
var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", "")))
var task := staff_service.get_task(task_id)
if task != null and task.task_type == CleaningBridge.TASK_TYPE:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Cleaning work advances only at its physical destination with game time.")
var minutes := int(command.payload.get(&"minutes", command.payload.get("minutes", 1)))
var result := staff_service.progress_task(task_id, minutes, _runtime_event_minute())
result.command_id = command.command_id
return result
func _original_relationship_identities_valid(payload: Dictionary, staff_registry: StaffService, catalog: MvpContentCatalog) -> bool:
if payload.get("protagonist_id") != "player" or staff_registry == null:
return false
var subject := StringName(payload.get("subject_id", ""))
var rival := StringName(payload.get("rival_id", ""))
if subject == &"player" or rival == &"player" or subject == rival or not staff_registry.has_staff(subject):
return false
var heroine_definition := catalog.get_entry(&"unique_characters", subject)
var rival_definition := catalog.get_entry(&"unique_characters", rival)
var subject_staff := staff_registry.get_staff(subject)
return subject_staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE \
and heroine_definition.get(&"role_id") == String(subject_staff.role_id) \
and heroine_definition.get(&"character_kind") == "heroine" \
and rival_definition.get(&"character_kind") == "rival" \
and heroine_definition.get(&"counterpart_id") == String(rival) \
and rival_definition.get(&"counterpart_id") == String(subject)
func _execute_register_original_relationship(command: SimulationCommand) -> CommandResult:
var result := relationship_service.register_original_relationship(command.payload)
result.command_id = command.command_id
return result
func _execute_create_relationship(command: SimulationCommand) -> CommandResult:
var actor_a_id := StringName(command.payload.get(&"actor_a_id", command.payload.get("actor_a_id", "")))
var actor_b_id := StringName(command.payload.get(&"actor_b_id", command.payload.get("actor_b_id", "")))
if not _event_actor_exists(actor_a_id) or not _event_actor_exists(actor_b_id):
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Both relationship actors must exist.")
var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", "")))
var id_before := id_generator.create_snapshot()
if relationship_id.is_empty():
relationship_id = id_generator.next_id(&"relationship")
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {}
var result := relationship_service.create_relationship(relationship_id, actor_a_id, actor_b_id, options)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
return result
func _execute_change_relationship(command: SimulationCommand) -> CommandResult:
var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", "")))
var metric_id := StringName(command.payload.get(&"metric_id", command.payload.get("metric_id", "")))
var delta := int(command.payload.get(&"delta", command.payload.get("delta", 0)))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {}
var result := relationship_service.apply_value_change(
relationship_id, metric_id, delta, _runtime_event_minute(), options
)
result.command_id = command.command_id
return result
func _execute_attack_relationship_shield(command: SimulationCommand) -> CommandResult:
var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", "")))
var action_id := StringName(command.payload.get(&"action_id", command.payload.get("action_id", "")))
var raw_terms: Variant = command.payload.get(&"terms", command.payload.get("terms", {}))
if not raw_terms is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Shield attack terms must be a Dictionary.")
var result := relationship_service.apply_shield_attack(
relationship_id, action_id, _runtime_event_minute(), raw_terms
)
result.command_id = command.command_id
return result
func _execute_request_route_lock(command: SimulationCommand) -> CommandResult:
var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", "")))
var route_id := StringName(command.payload.get(&"route_id", command.payload.get("route_id", "")))
var lock_index := _route_lock_index(command.payload.get(&"route_lock", command.payload.get("route_lock", -1)))
if lock_index < 0:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "A valid Route Lock state is required.")
var result := relationship_service.request_route_lock(
relationship_id,
lock_index,
route_id,
bool(command.payload.get(&"irreversible_event", command.payload.get("irreversible_event", false)))
)
result.command_id = command.command_id
return result
func _execute_confirm_route_lock(command: SimulationCommand) -> CommandResult:
var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", "")))
var route_id := StringName(command.payload.get(&"route_id", command.payload.get("route_id", "")))
var result := relationship_service.confirm_route_lock(relationship_id, route_id)
result.command_id = command.command_id
return result
func _execute_register_event_definition(command: SimulationCommand) -> CommandResult:
var raw_definition: Variant = command.payload.get(&"definition", command.payload.get("definition", null))
var definition := EventDefinition.from_variant(raw_definition)
if definition == null:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Event definition is malformed.")
var result := event_service.register_definition(definition)
result.command_id = command.command_id
return result
func _execute_enqueue_event(command: SimulationCommand) -> CommandResult:
var event_id := StringName(command.payload.get(&"event_id", command.payload.get("event_id", "")))
var actor_ids := _name_array(command.payload.get(&"actor_ids", command.payload.get("actor_ids", [])))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {}
var result := event_service.enqueue_candidate(event_id, actor_ids, _runtime_event_minute(), options)
result.command_id = command.command_id
return result
func _execute_reserve_event_choice_cost(command: SimulationCommand) -> CommandResult:
var instance_id := StringName(command.payload.get(&"event_instance_id", command.payload.get("event_instance_id", "")))
var choice_id := StringName(command.payload.get(&"choice_id", command.payload.get("choice_id", "")))
var result := event_service.reserve_choice_cost(instance_id, choice_id, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_submit_event_choice(command: SimulationCommand) -> CommandResult:
var instance_id := StringName(command.payload.get(&"event_instance_id", command.payload.get("event_instance_id", "")))
var choice_instance_id := StringName(command.payload.get(&"choice_instance_id", command.payload.get("choice_instance_id", "")))
var choice_id := StringName(command.payload.get(&"choice_id", command.payload.get("choice_id", "")))
var result := event_service.submit_choice(
instance_id, choice_instance_id, choice_id, _runtime_event_minute()
)
if result.is_success() and event_service.get_foreground_instance() == null:
_clock.set_paused(false)
result.command_id = command.command_id
return result
func _execute_report_authority_incident(command: SimulationCommand) -> CommandResult:
var incident_id := StringName(command.payload.get(&"incident_id", command.payload.get("incident_id", "")))
var incident_type := StringName(command.payload.get(&"incident_type", command.payload.get("incident_type", "")))
var detector_id := StringName(command.payload.get(&"detector_id", command.payload.get("detector_id", command.actor_id)))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Incident options must be a Dictionary.")
var id_before := id_generator.create_snapshot()
if incident_id.is_empty():
incident_id = id_generator.next_id(&"incident")
var result := authority_service.create_incident(
incident_id,
incident_type,
_runtime_event_minute(),
detector_id,
(raw_options as Dictionary).duplicate(true)
)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
return result
func _execute_register_authority_evidence(command: SimulationCommand) -> CommandResult:
var evidence_id := StringName(command.payload.get(&"evidence_id", command.payload.get("evidence_id", "")))
var source := StringName(command.payload.get(&"source", command.payload.get("source", "")))
var severity := int(command.payload.get(&"severity", command.payload.get("severity", -1)))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Evidence options must be a Dictionary.")
var options: Dictionary = (raw_options as Dictionary).duplicate(true)
if not options.has(&"created_at") and not options.has("created_at"):
options[&"created_at"] = _runtime_event_minute()
var id_before := id_generator.create_snapshot()
if evidence_id.is_empty():
evidence_id = id_generator.next_id(&"evidence")
var result := authority_service.create_evidence(evidence_id, source, severity, options)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
return result
func _execute_discover_authority_evidence(command: SimulationCommand) -> CommandResult:
var evidence_id := StringName(command.payload.get(&"evidence_id", command.payload.get("evidence_id", "")))
var inspector_id := StringName(command.payload.get(&"inspector_id", command.payload.get("inspector_id", command.actor_id)))
var parsed_cell: Variant = _parse_world_cell(command.payload.get(&"inspector_cell", command.payload.get("inspector_cell", null)))
var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {}))
if parsed_cell == null or not raw_context is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Evidence discovery requires inspector_cell and Dictionary context.")
var result := authority_service.attempt_discover_evidence(
evidence_id,
inspector_id,
parsed_cell,
_runtime_event_minute(),
(raw_context as Dictionary).duplicate(true)
)
result.command_id = command.command_id
return result
func _execute_evaluate_authority_inspection(command: SimulationCommand) -> CommandResult:
var raw_terms: Variant = command.payload.get(&"terms", command.payload.get("terms", {}))
if not raw_terms is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection evaluation terms must be a Dictionary.")
var result := authority_service.evaluate_inspection(
_runtime_event_minute(),
(raw_terms as Dictionary).duplicate(true)
)
if result.is_success() and not result.is_replay:
_publish_authority_evaluation(result.details)
result.command_id = command.command_id
return result
func _execute_schedule_authority_inspection(command: SimulationCommand) -> CommandResult:
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection options must be a Dictionary.")
var scheduled_at := int(command.payload.get(&"scheduled_at", command.payload.get("scheduled_at", _runtime_event_minute())))
var result := authority_service.schedule_inspection(
scheduled_at,
(raw_options as Dictionary).duplicate(true)
)
result.command_id = command.command_id
return result
func _execute_start_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var result := authority_service.start_inspection(inspection_id, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_arrive_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var result := authority_service.mark_inspector_arrived(inspection_id, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_plan_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var raw_candidates: Variant = command.payload.get(&"candidates", command.payload.get("candidates", null))
if not raw_candidates is Array:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection patrol candidates must be an Array.")
var time_budget := int(command.payload.get(&"time_budget_minutes", command.payload.get("time_budget_minutes", 120)))
var result := authority_service.plan_patrol(
inspection_id,
raw_candidates,
_runtime_event_minute(),
time_budget
)
result.command_id = command.command_id
return result
func _execute_advance_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {}))
if not raw_context is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection path context must be a Dictionary.")
var result := authority_service.advance_patrol(
inspection_id,
_runtime_event_minute(),
(raw_context as Dictionary).duplicate(true)
)
if result.is_success() and result.details.has(&"reachable"):
var event_type := &"INSPECTION_WAYPOINT_REACHED" if bool(result.details[&"reachable"]) else &"INSPECTION_PATH_BLOCKED"
_publish_actor_event(
event_type,
&"authority_service",
[inspection_id, StringName(result.details.get(&"waypoint_id", ""))],
result.details.duplicate(true)
)
result.command_id = command.command_id
return result
func _execute_inspect_authority_waypoint(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var evidence_ids := _name_array(command.payload.get(&"evidence_ids", command.payload.get("evidence_ids", [])))
var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {}))
if not raw_context is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection evidence context must be a Dictionary.")
var result := authority_service.inspect_current_waypoint(
inspection_id,
evidence_ids,
_runtime_event_minute(),
(raw_context as Dictionary).duplicate(true)
)
if result.is_success():
_publish_actor_event(
&"INSPECTION_WAYPOINT_INSPECTED",
&"authority_service",
[inspection_id],
result.details.duplicate(true)
)
result.command_id = command.command_id
return result
func _execute_resolve_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", "")))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection resolution options must be a Dictionary.")
var id_before := id_generator.create_snapshot()
if resolution_id.is_empty():
resolution_id = id_generator.next_id(&"resolution")
var result := authority_service.resolve_inspection(
inspection_id,
resolution_id,
_runtime_event_minute(),
(raw_options as Dictionary).duplicate(true)
)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
return result
func _execute_depart_authority_inspection(command: SimulationCommand) -> CommandResult:
var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", "")))
var result := authority_service.depart_inspection(inspection_id, _runtime_event_minute())
result.command_id = command.command_id
return result
## Existing Defense commands remain authoritative. This report only explains
## why a live employee or target can be registered; no balance data is invented.
func get_defense_management_report(selected_raid_id: StringName = &"") -> Dictionary:
var raids: Array[Dictionary] = defense_service.get_management_rows()
var selected: Dictionary = {}
for row: Dictionary in raids:
if row[&"raid_id"] == selected_raid_id:
selected = row
break
if selected.is_empty() and not raids.is_empty():
selected = raids[0]
for row: Dictionary in raids:
if int(row[&"state"]) not in DefenseService.TERMINAL_STATES:
selected = row
break
var raid_id := StringName(selected.get(&"raid_id", &""))
var detail := defense_service.get_management_detail(raid_id)
var defenders: Array = detail.get(&"defenders", [])
var assets: Array = detail.get(&"assets", [])
var defender_owners: Dictionary = {}
var asset_owners: Dictionary = {}
for row: Dictionary in raids:
if int(row[&"state"]) in DefenseService.TERMINAL_STATES: continue
for actor_id: StringName in row[&"defender_ids"]: defender_owners[actor_id] = row[&"raid_id"]
for asset_id: StringName in row[&"asset_ids"]: asset_owners[asset_id] = row[&"raid_id"]
var preparing := int(selected.get(&"state", -1)) == DefenseService.RaidState.PREPARING
var staff_rows: Array[Dictionary] = []
for staff: StaffRuntime in staff_service.get_staff_members():
var entry := mvp_content_catalog.get_recruitment_entry(staff.staff_id)
var registered := false
for actor: Dictionary in defenders:
registered = registered or actor[&"actor_id"] == staff.staff_id
var reason := ""
if not preparing: reason = "準備中だけ参加者を追加できます。"
elif registered: reason = "この防衛戦に参加済みです。"
elif defender_owners.has(staff.staff_id) and defender_owners[staff.staff_id] != raid_id: reason = "別の防衛戦に参加しています。"
elif not staff.can_take_shift(): reason = "現在の体調や状態では参加できません。"
elif not staff.current_task_id.is_empty(): reason = "担当中の業務が完了するまで参加できません。"
staff_rows.append({
&"staff_id": staff.staff_id, &"display_name": String(entry.get(&"display_name", staff.staff_id)),
&"combat": int(staff.skills.get(&"combat", 0)), &"health": staff.health,
&"fatigue": staff.fatigue, &"registered": registered,
&"can_register": reason.is_empty(), &"reason": reason,
})
var objective_rows: Array[Dictionary] = []
for objective_id: StringName in selected.get(&"objective_ids", []):
var facility := facility_service.get_instance(objective_id)
var definition: Variant = definition_registry.get_facility(facility.definition_id) if facility != null else null
var registered := false
for asset: Dictionary in assets:
registered = registered or asset[&"asset_id"] == objective_id
var reason := ""
if not preparing: reason = "準備中だけ防衛対象を登録できます。"
elif registered: reason = "登録済みです。"
elif facility == null: reason = "指定された施設が見つかりません。"
elif facility.durability <= 0: reason = "破壊された施設は登録できません。"
elif not facility.queue_entries.is_empty() or not facility.active_services.is_empty(): reason = "利用中または待機中の顧客がいます。"
elif asset_owners.has(objective_id) and asset_owners[objective_id] != raid_id: reason = "別の防衛戦の対象です。"
objective_rows.append({
&"facility_id": objective_id, &"display_name": String(definition.display_name) if definition != null else String(objective_id),
&"durability": facility.durability if facility != null else 0,
&"registered": registered, &"can_register": reason.is_empty(), &"reason": reason,
})
return {&"raids": raids, &"selected": selected, &"staff": staff_rows,
&"objectives": objective_rows, &"defenders": defenders, &"assets": assets,
&"can_control": not (_operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal())}
func _execute_create_defense_raid(command: SimulationCommand) -> CommandResult:
var raw_definition: Variant = command.payload.get(&"definition", command.payload.get("definition", null))
if not raw_definition is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense Raid definition must be a Dictionary.")
var event_source_value: Variant = raw_definition.get(&"event_source", raw_definition.get("event_source", {}))
if not event_source_value is Dictionary or not event_source_value.is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Event threat provenance is reserved to the configured Event boundary.")
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var id_before := id_generator.create_snapshot()
if raid_id.is_empty():
raid_id = id_generator.next_id(&"defense_raid")
var definition: Dictionary = (raw_definition as Dictionary).duplicate(true)
if not definition.has(&"source_id") and not definition.has("source_id"):
definition[&"source_id"] = command.actor_id
var result := defense_service.create_raid(raid_id, _defense_operation_minute(), definition)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_register_defense_combatant(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var actor_id := StringName(command.payload.get(&"actor_id", command.payload.get("actor_id", command.payload.get(&"staff_id", command.payload.get("staff_id", "")))))
var side := StringName(String(command.payload.get(&"side", command.payload.get("side", ""))).to_upper())
var combat := int(command.payload.get(&"combat", command.payload.get("combat", 0)))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if raid_id.is_empty() or actor_id.is_empty() or not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense combatant requires raid_id, actor_id and Dictionary options.")
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before combatant registration.")
var options: Dictionary = (raw_options as Dictionary).duplicate(true)
var max_health := int(command.payload.get(&"max_health", command.payload.get("max_health", 100)))
if side == &"DEFENDER":
var staff := staff_service.get_staff(actor_id)
if staff == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Defense defender must reference existing Staff.")
if not staff.can_take_shift() or not staff.current_task_id.is_empty():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense defender must be healthy enough to participate and free of Staff tasks.")
var lock_owner: StringName = _defense_lock_owner(actor_id, &"DEFENDER", raid_id)
if not lock_owner.is_empty():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Staff is already assigned to another non-terminal Raid.", {&"raid_id": lock_owner})
if max_health != StaffRuntime.MAX_PSYCHOLOGY_VALUE \
or ((options.has(&"health") or options.has("health")) and int(options.get(&"health", options.get("health", -1))) != staff.health):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defender health must use the current Staff health on a 100-point baseline.")
max_health = StaffRuntime.MAX_PSYCHOLOGY_VALUE
options[&"health"] = staff.health
options[&"fatigue"] = staff.fatigue
var staff_before: Dictionary = staff_service.create_snapshot()
staff.transition_to(StaffRuntime.State.UNAVAILABLE, &"DEFENSE_ASSIGNED", _defense_operation_minute())
var defender_result := defense_service.register_combatant(
raid_id, actor_id, side, combat, max_health, options
)
if not defender_result.is_success():
staff_service.restore_snapshot(staff_before)
defender_result.command_id = command.command_id
defender_result.target_id = raid_id
return defender_result
elif side != &"ATTACKER":
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense combatant side must be DEFENDER or ATTACKER.")
var result := defense_service.register_combatant(
raid_id, actor_id, side, combat, max_health, options
)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_register_defense_asset(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var asset_id := StringName(command.payload.get(&"asset_id", command.payload.get("asset_id", command.payload.get(&"facility_id", command.payload.get("facility_id", "")))))
var kind := StringName(String(command.payload.get(&"kind", command.payload.get("kind", ""))).to_upper())
var defense := int(command.payload.get(&"defense", command.payload.get("defense", 0)))
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if raid_id.is_empty() or asset_id.is_empty() or not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense asset requires raid_id, facility asset_id and Dictionary options.")
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before asset registration.")
var facility := facility_service.get_instance(asset_id)
if facility == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Defense asset must reference an existing Facility.")
if not facility.queue_entries.is_empty() or not facility.active_services.is_empty():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "A busy Facility cannot become a Defense asset.")
var lock_owner: StringName = _defense_lock_owner(asset_id, &"ASSET", raid_id)
if not lock_owner.is_empty():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Facility is already assigned to another non-terminal Raid.", {&"raid_id": lock_owner})
var supplied_maximum := int(command.payload.get(&"max_integrity", command.payload.get("max_integrity", FacilityInstance.DEFAULT_DURABILITY)))
var options: Dictionary = (raw_options as Dictionary).duplicate(true)
if supplied_maximum != FacilityInstance.DEFAULT_DURABILITY \
or ((options.has(&"integrity") or options.has("integrity")) and int(options.get(&"integrity", options.get("integrity", -1))) != facility.durability):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense asset integrity must use the current Facility durability on a 10000-point baseline.")
options[&"integrity"] = facility.durability
var result := defense_service.register_asset(
raid_id,
asset_id,
kind,
FacilityInstance.DEFAULT_DURABILITY,
defense,
options
)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_start_defense_raid(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before Raid start.")
var result := defense_service.start_raid(raid_id, _defense_operation_minute())
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_set_defense_retreat_policy(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var policy := StringName(String(command.payload.get(&"policy", command.payload.get("policy", ""))).to_upper())
var threshold := int(command.payload.get(&"threshold", command.payload.get("threshold", -1)))
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before retreat-policy update.")
var result := defense_service.set_retreat_policy(raid_id, policy, threshold)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_queue_defense_skill(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var action_id := StringName(command.payload.get(&"action_id", command.payload.get("action_id", "")))
var actor_id := StringName(command.payload.get(&"actor_id", command.payload.get("actor_id", command.actor_id)))
var kind := StringName(String(command.payload.get(&"kind", command.payload.get("kind", ""))).to_upper())
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense skill options must be a Dictionary.")
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before skill queueing.")
var id_before := id_generator.create_snapshot()
if action_id.is_empty():
action_id = id_generator.next_id(&"defense_skill_action")
var result := defense_service.queue_active_skill(
raid_id, action_id, actor_id, kind, (raw_options as Dictionary).duplicate(true)
)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_resolve_defense_round(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", "")))
var result := _resolve_defense_round_internal(raid_id, resolution_id, _defense_operation_minute())
result.command_id = command.command_id
result.target_id = raid_id
return result
## Both the explicit command and ordinary clock progression use the same
## transaction, generated-ID namespace and cross-domain callback boundaries.
func _resolve_defense_round_internal(
raid_id: StringName,
resolution_id: StringName,
absolute_minute: int
) -> CommandResult:
_prepare_defense_round_attempts(absolute_minute)
var already_attempted := _defense_round_attempted_raids.has(raid_id)
_defense_round_attempted_raids[raid_id] = true
var previous_command_minute := _active_command_minute
_active_command_minute = absolute_minute
if not _validate_live_defense_crosslinks():
_active_command_minute = previous_command_minute
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Defense cross-domain links are invalid before round resolution.")
var id_before := id_generator.create_snapshot()
if resolution_id.is_empty():
# Existing explicit IDs can occupy this namespace without advancing the
# allocator. Skip those IDs instead of replaying history as a new attack.
while id_generator.get_counter(&"defense_round_resolution") < StableIdGenerator.MAX_COUNTER:
resolution_id = id_generator.next_id(&"defense_round_resolution")
if not defense_service.has_resolution(raid_id, resolution_id):
break
if resolution_id.is_empty() or defense_service.has_resolution(raid_id, resolution_id):
id_generator.restore_snapshot(id_before)
_active_command_minute = previous_command_minute
var exhausted := CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Defense round ID namespace is exhausted.")
exhausted.target_id = raid_id
return exhausted
var result := defense_service.resolve_next_round(raid_id, resolution_id, absolute_minute)
# Reading an old canonical result is not a new round attempt. Restore only
# the guard introduced by this replay; an earlier real failure in this same
# minute must still prevent automatic progression from hiding that failure.
if result.is_replay and not already_attempted:
_defense_round_attempted_raids.erase(raid_id)
if not result.is_success():
id_generator.restore_snapshot(id_before)
_active_command_minute = previous_command_minute
result.target_id = raid_id
return result
func _prepare_defense_round_attempts(absolute_minute: int) -> void:
if _defense_round_attempt_minute == absolute_minute:
return
_defense_round_attempt_minute = absolute_minute
_defense_round_attempted_raids.clear()
_defense_automatic_failed_minute = -1
func _reset_defense_round_attempts() -> void:
_defense_round_attempt_minute = -1
_defense_round_attempted_raids.clear()
_defense_automatic_failed_minute = -1
func _execute_begin_next_defense_wave(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before the next Wave.")
var result := defense_service.begin_next_wave(raid_id, _defense_operation_minute())
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_retreat_defenders(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", "")))
var reason := StringName(command.payload.get(&"reason", command.payload.get("reason", &"PLAYER_RETREAT")))
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before defender retreat.")
var id_before := id_generator.create_snapshot()
if resolution_id.is_empty():
resolution_id = id_generator.next_id(&"defense_retreat_resolution")
var result := defense_service.retreat_defenders(raid_id, resolution_id, _defense_operation_minute(), reason)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_cancel_defense_raid(command: SimulationCommand) -> CommandResult:
var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id)))
var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", "")))
var reason := StringName(command.payload.get(&"reason", command.payload.get("reason", &"CANCELLED")))
if not _validate_live_defense_crosslinks():
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before Raid cancellation.")
var id_before := id_generator.create_snapshot()
if resolution_id.is_empty():
resolution_id = id_generator.next_id(&"defense_cancel_resolution")
var result := defense_service.cancel_raid(raid_id, _defense_operation_minute(), reason, resolution_id)
if not result.is_success():
id_generator.restore_snapshot(id_before)
result.command_id = command.command_id
result.target_id = raid_id
return result
func _execute_create_service_request(command: SimulationCommand) -> CommandResult:
var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", "")))
var instance := facility_service.get_instance(facility_id)
if instance == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility was not found.")
var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET")
if not defense_owner.is_empty():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"A Facility assigned to a non-terminal Defense Raid cannot accept service requests.",
{&"raid_id": defense_owner}
)
var guest_ids := _name_array(command.payload.get(&"guest_ids", command.payload.get("guest_ids", [])))
if guest_ids.is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "At least one guest id is required.")
for guest_id: StringName in guest_ids:
if not economy_service.has_account(_guest_wallet_account(guest_id)):
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Every guest needs a registered wallet.")
var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {}))
if not raw_options is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Service request options must be a Dictionary.")
var options: Dictionary = raw_options.duplicate(true)
var quoted_price := maxi(
int(options.get(&"quoted_price", options.get("quoted_price", get_facility_business_price(instance)))),
0
)
# Freeze the same price used by candidate selection and the business panel.
options[&"quoted_price"] = quoted_price
if not instance.operator_open or not instance.is_within_opening_hours(_runtime_event_minute()):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This facility is closed to new service requests.", {&"reason": &"FACILITY_CLOSED"})
if instance.requires_operating_kitchen() and not bool(facility_service.get_required_kitchen_report(facility_id).get(&"operating", false)):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This dining facility requires an operating BACK kitchen.", {&"reason": &"FACILITY_CLOSED", &"dependency_reason": facility_service.get_required_kitchen_report(facility_id).get(&"reason", &"")})
var payer_account := _guest_wallet_account(guest_ids[0])
var committed_price := _outstanding_service_commitment(guest_ids[0])
if not economy_service.account_allows_negative(payer_account) \
and economy_service.get_balance(payer_account) < committed_price + quoted_price:
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"The guest cannot fund this service together with existing commitments.",
{
&"available_gold": economy_service.get_balance(payer_account),
&"committed_gold": committed_price,
&"quoted_price": quoted_price,
}
)
var inventory_before := inventory_service.create_snapshot()
var facility_before := facility_service.create_snapshot()
var guest_before := guest_service.create_snapshot()
var events_before := domain_events.create_snapshot()
var id_before := id_generator.create_snapshot()
var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", "")))
if request_id.is_empty():
request_id = id_generator.next_id(&"service_request")
var definition := definition_registry.get_facility(instance.definition_id)
if definition == null:
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility definition was not found.")
var reservation_ids: Array[StringName] = []
for item_id: StringName in _sorted_name_keys(definition.inventory_inputs):
var reserve_result := inventory_service.reserve_item(
instance.local_inventory_id,
item_id,
int(definition.inventory_inputs[item_id]),
request_id,
&"facility_service",
_runtime_event_minute(),
_runtime_event_minute() + 240,
StringName("service.reserve.%s.%s" % [request_id, item_id])
)
if not bool(reserve_result.get(&"ok", false)):
inventory_service.restore_snapshot(inventory_before)
id_generator.restore_snapshot(id_before)
return _command_result_from_dictionary(command.command_id, reserve_result, "Facility input could not be reserved.")
for reservation_id: StringName in reserve_result.get(&"reservation_ids", []):
reservation_ids.append(reservation_id)
options[&"reserved_inventory_ids"] = reservation_ids
var request := facility_service.create_request(
request_id,
facility_id,
guest_ids,
StringName(command.payload.get(&"service_type", command.payload.get("service_type", "default"))),
_runtime_event_minute(),
options
)
var enqueue_result: CommandResult
var runtime_guests: Array[StringName] = []
for guest_id: StringName in guest_ids:
if guest_service.has_guest(guest_id):
runtime_guests.append(guest_id)
if runtime_guests.is_empty():
enqueue_result = facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1))
else:
var first_guest := runtime_guests[0]
var queue_result := guest_service.enter_queue(
first_guest,
facility_id,
request_id,
_runtime_event_minute(),
int(options.get(&"initial_patience", options.get("initial_patience", -1))),
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> CommandResult:
return facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1))
)
if bool(queue_result.get(&"ok", false)):
for guest_index in range(1, runtime_guests.size()):
var joined := guest_service.enter_queue(
runtime_guests[guest_index], facility_id, request_id, _runtime_event_minute(),
int(options.get(&"initial_patience", options.get("initial_patience", -1)))
)
if not bool(joined.get(&"ok", false)):
queue_result = joined
break
enqueue_result = CommandResult.succeeded(0, {
&"expected_wait": request.expected_wait,
}) if bool(queue_result.get(&"ok", false)) else CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED, 0,
String(queue_result.get(&"message", "Guest could not enter the facility queue.")),
queue_result
)
if not enqueue_result.is_success():
inventory_service.restore_snapshot(inventory_before)
facility_service.restore_snapshot(facility_before)
guest_service.restore_snapshot(guest_before)
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(id_before)
enqueue_result.command_id = command.command_id
return enqueue_result
return CommandResult.succeeded(command.command_id, {
&"request_id": request_id,
&"reservation_ids": reservation_ids,
&"expected_wait": enqueue_result.details.get(&"expected_wait", 0),
})
func _execute_set_facility_conditions(command: SimulationCommand) -> CommandResult:
var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", "")))
var conditions: Variant = command.payload.get(&"conditions", command.payload.get("conditions", null))
if not conditions is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "conditions must be a Dictionary.")
var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET")
if not defense_owner.is_empty() and (conditions.has(&"durability") or conditions.has("durability")):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Defense-owned Facility durability can only change through its Raid.",
{&"raid_id": defense_owner}
)
var result := facility_service.set_operational_status(facility_id, conditions, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_set_facility_policy(command: SimulationCommand) -> CommandResult:
var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", "")))
var patch: Variant = command.payload.get(&"patch", command.payload.get("patch", null))
if not patch is Dictionary:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "patch must be a Dictionary.")
var effective_at := int(command.payload.get(&"effective_at", command.payload.get("effective_at", -1)))
var result := facility_service.set_policy(facility_id, patch, effective_at)
result.command_id = command.command_id
return result
func _execute_leave_facility_queue(command: SimulationCommand) -> CommandResult:
var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", "")))
var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", "")))
var request := facility_service.get_request(request_id)
if request == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Queued service request was not found.")
var inventory_before := inventory_service.create_snapshot()
var facility_before := facility_service.create_snapshot()
var guest_before := guest_service.create_snapshot()
var events_before := domain_events.create_snapshot()
var id_before := id_generator.create_snapshot()
var external_leave := func(_guest_id: StringName = &"", _facility_id: StringName = &"", _request_id: StringName = &"") -> Dictionary:
for reservation_id: StringName in request.reserved_inventory_ids:
var released := inventory_service.release_reservation(
reservation_id,
StringName("service.release.%s.%s" % [request_id, reservation_id]),
_runtime_event_minute()
)
if not bool(released.get(&"ok", false)):
return released
var leave_result := facility_service.leave_queue(facility_id, request_id)
return {&"ok": leave_result.is_success(), &"message": leave_result.message}
var runtime_guests: Array[StringName] = []
for guest_id: StringName in request.guest_ids:
if guest_service.has_guest(guest_id):
runtime_guests.append(guest_id)
var successful := true
var failure_message := "Facility queue removal failed."
if runtime_guests.is_empty():
var external_result: Dictionary = external_leave.call()
successful = bool(external_result.get(&"ok", false))
failure_message = String(external_result.get(&"message", failure_message))
else:
for guest_index in range(runtime_guests.size()):
var queued_guest := guest_service.get_guest(runtime_guests[guest_index])
var abandon := guest_service.tick_queue_patience(
queued_guest.guest_id,
_runtime_event_minute(),
-queued_guest.patience_remaining,
external_leave if guest_index == 0 else Callable()
)
if not bool(abandon.get(&"ok", false)):
successful = false
failure_message = String(abandon.get(&"message", failure_message))
break
if not successful:
inventory_service.restore_snapshot(inventory_before)
facility_service.restore_snapshot(facility_before)
guest_service.restore_snapshot(guest_before)
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(id_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, failure_message)
return CommandResult.succeeded(command.command_id, {&"request_id": request_id, &"reason": &"GUEST_LEFT"})
func preview_build_facility(definition_id: StringName, origin: Vector3i, rotation: int) -> Dictionary:
var definition := definition_registry.get_facility(definition_id)
var failure := _build_facility_precheck(0, {&"definition_id": definition_id, &"origin": origin, &"rotation": rotation})
if failure != null and definition == null:
var code := String(failure.details.get(&"reason", CommandResult.Code.keys()[failure.code]))
return {&"can_build": false, &"error_codes": [code],
&"reasons": [{&"code": code, &"message": failure.message, &"cells": [], &"details": failure.details.duplicate(true)}],
&"cells": []}
var report: Dictionary = construction_service.validate_placement(definition, origin, rotation).to_dictionary()
if failure != null:
report[&"can_build"] = false
var code := String(failure.details.get(&"reason", CommandResult.Code.keys()[failure.code]))
var codes: Array = Array(report.get(&"error_codes", []))
var reasons: Array = Array(report.get(&"reasons", []))
codes.push_front(code)
reasons.push_front({&"code": code, &"message": failure.message, &"cells": [], &"details": failure.details.duplicate(true)})
report[&"error_codes"] = codes
report[&"reasons"] = reasons
return report
func _build_facility_precheck(command_id: int, payload: Dictionary) -> CommandResult:
var operations_gate := _operations_construction_investment_gate(command_id)
if operations_gate != null:
return operations_gate
var definition_id := StringName(payload.get(&"definition_id", payload.get("definition_id", "")))
var definition := definition_registry.get_facility(definition_id)
if definition == null:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command_id, "Facility definition was not found.")
if _facility_definition_is_unsupported_vertical_proxy(definition):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command_id,
"Vertical infrastructure topology is not supported.",
{
&"reason": FacilityInfrastructureProjector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY,
&"definition_id": definition_id,
&"unchanged": true,
}
)
var origin_value: Variant = _parse_world_cell(payload.get(&"origin", payload.get("origin", null)))
if origin_value == null:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command_id, "origin must be Vector3i or [x, y, layer].")
var origin: Vector3i = origin_value
if definition.id in [&"guest_stairs", &"service_stairs"] and not path_service.has_cell(origin + Vector3i(0, -1, 0)):
return _staff_assignment_failure(command_id, &"STAIR_LANDING_MISSING", "先に階段の一つ下の階に通路を作ってください。")
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
if _elevator_touches_module(shaft, definition, origin) and _elevator_shaft_busy(shaft):
return _staff_assignment_failure(command_id, &"ELEVATOR_BUSY", "Wait until the shaft is idle before extending it.")
var rotation := int(payload.get(&"rotation", payload.get("rotation", 0)))
if rotation not in definition.allowed_rotations:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command_id, "The selected rotation is not allowed.")
return null
func _execute_build_facility(command: SimulationCommand) -> CommandResult:
var failure := _build_facility_precheck(command.command_id, command.payload)
if failure != null:
return failure
var definition_id := StringName(command.payload.get(&"definition_id", command.payload.get("definition_id", "")))
var definition := definition_registry.get_facility(definition_id)
var origin: Vector3i = _parse_world_cell(command.payload.get(&"origin", command.payload.get("origin", null)))
var rotation := int(command.payload.get(&"rotation", command.payload.get("rotation", 0)))
var result := construction_service.start_construction(
definition,
origin,
rotation,
definition.build_minutes,
_runtime_event_minute()
)
result.command_id = command.command_id
return result
func _execute_cancel_construction(command: SimulationCommand) -> CommandResult:
var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", "")))
if job_id.is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "job_id is required.")
var result := construction_service.cancel_job(job_id, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_start_excavation(command: SimulationCommand) -> CommandResult:
var operations_gate := _operations_construction_investment_gate(command.command_id)
if operations_gate != null:
return operations_gate
if progression_service.current_rank < 2:
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"Excavation requires World Tree Rank 2.",
{&"required_rank": 2, &"current_rank": progression_service.current_rank}
)
var parsed_cell: Variant = _parse_world_cell(
command.payload.get(&"cell", command.payload.get("cell", null))
)
if parsed_cell == null:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"cell must be Vector3i or [x, y, layer]."
)
var result := excavation_service.start_excavation(parsed_cell, _runtime_event_minute())
result.command_id = command.command_id
return result
func _execute_pause_excavation(command: SimulationCommand) -> CommandResult:
var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", "")))
if job_id.is_empty():
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"job_id is required."
)
var result := excavation_service.pause_job(job_id)
result.command_id = command.command_id
return result
func _execute_resume_excavation(command: SimulationCommand) -> CommandResult:
var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", "")))
if job_id.is_empty():
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"job_id is required."
)
var result := excavation_service.resume_job(job_id)
result.command_id = command.command_id
return result
func _execute_demolish_facility(command: SimulationCommand) -> CommandResult:
var instance_id := StringName(command.payload.get(&"instance_id", command.payload.get("instance_id", "")))
if instance_id.is_empty():
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "instance_id is required.")
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
if instance_id in shaft[&"module_ids"] and _elevator_shaft_busy(shaft):
return _staff_assignment_failure(command.command_id, &"ELEVATOR_BUSY", "Wait until passengers have left before demolition.")
var lost_landing_cells := _demolition_lost_route_cells(instance_id)
for actor: Dictionary in path_service.create_snapshot()[&"actor_cells"]:
if actor[&"cell"] in lost_landing_cells:
return _staff_assignment_failure(command.command_id, &"FACILITY_OCCUPIED", "この通路・階段に人がいます。移動し終わってから撤去してください。")
var stair_dependents := _stair_demolition_dependents(instance_id)
if not stair_dependents.is_empty():
return _staff_assignment_failure(command.command_id, &"STAIR_LANDING_IN_USE", "この通路につながる上の階段を先に撤去するか、建設を中止してください。")
var defense_owner: StringName = _defense_lock_owner(instance_id, &"ASSET")
if not defense_owner.is_empty():
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"A Facility assigned to a non-terminal Defense Raid cannot be demolished.",
{&"raid_id": defense_owner}
)
var supply_guard: Dictionary = supply_management.validate_demolition(self, instance_id)
if not bool(supply_guard.get(&"ok", false)):
return _staff_assignment_failure(command.command_id, StringName(supply_guard.get(&"reason", &"SUPPLY_IN_USE")), "Facility still owns supply stock, a production plan, or an active delivery.")
for assignment: Dictionary in staff_service.get_work_assignments():
if StringName(assignment[&"facility_id"]) == instance_id:
return _staff_assignment_failure(command.command_id, &"STAFF_ASSIGNED", "Unassign scheduled Staff before demolishing this facility.")
var operational_instance := facility_service.get_instance(instance_id)
if operational_instance != null \
and (not operational_instance.queue_entries.is_empty() or not operational_instance.active_services.is_empty()):
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "A busy facility cannot be demolished.")
if facility_infrastructure_projector.has_projection(instance_id):
var boundary_before := _capture_infrastructure_demolition_state()
if boundary_before.is_empty():
return CommandResult.failed(
CommandResult.Code.TRANSACTION_CLOSED,
command.command_id,
"Infrastructure demolition state could not be captured."
)
var unprojected := facility_infrastructure_projector.unproject_facility(
StringName("runtime.demolish:%s" % instance_id),
instance_id
)
if not unprojected.is_success():
if not _infrastructure_demolition_state_matches(boundary_before):
_restore_infrastructure_demolition_state(boundary_before)
var rejection_details := unprojected.details.duplicate(true)
rejection_details[&"reason"] = &"INFRASTRUCTURE_UNPROJECT_FAILED"
rejection_details[&"instance_id"] = instance_id
return CommandResult.failed(
unprojected.code,
command.command_id,
unprojected.message,
rejection_details
)
var demolished := construction_service.demolish_facility(instance_id)
demolished.command_id = command.command_id
var transport_ok := false
if demolished.is_success():
transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).get(&"ok", false)) \
and bool(_synchronize_warps().get(&"ok", false)) and _prune_legacy_stairs_and_movement()
if not transport_ok or not demolished.is_success() \
or not _validate_runtime_path_crosslinks(path_service, guest_service, staff_service, _movement_credits, _runtime_event_minute()) \
or facility_infrastructure_projector.has_projection(instance_id) \
or not construction_service.get_facility(instance_id).is_empty() \
or not world_service.get_entity_cells(instance_id).is_empty() \
or not _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
):
var rolled_back := _restore_infrastructure_demolition_state(boundary_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Infrastructure demolition failed and was rolled back.",
{
&"instance_id": instance_id,
&"rolled_back": rolled_back,
&"cause": demolished.to_dictionary(),
}
)
return demolished
var result := construction_service.demolish_facility(instance_id)
result.command_id = command.command_id
if result.is_success():
kitchen_production.remove_plan(instance_id)
auto_purchase_plans.remove_warehouse(instance_id)
return result
func _capture_infrastructure_demolition_state() -> Dictionary:
return {
&"game_state": _state.create_snapshot(),
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"world": world_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"construction": construction_service.create_snapshot(),
&"excavation": excavation_service.create_snapshot(),
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
&"route_projection": route_subslot_path_adapter.create_snapshot(),
&"facility_infrastructure": facility_infrastructure_projector.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"mana": mana_network_service.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
}
func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
for key: StringName in [
&"game_state", &"economy", &"facility_upkeep", &"upkeep_settlement",
&"world", &"path", &"elevators", &"legacy_stairs", &"construction",
&"excavation", &"spatial_subslots", &"route_projection",
&"facility_infrastructure", &"facilities", &"facility_cleanliness", &"mana", &"movement_credits",
&"domain_events", &"id_generator",
]:
if not snapshot.get(key, null) is Dictionary:
return false
var restored: bool = bool(_state.restore_snapshot(snapshot[&"game_state"]))
restored = economy_service.restore_snapshot(snapshot[&"economy"]) and restored
restored = facility_upkeep.restore_snapshot(snapshot[&"facility_upkeep"]) and restored
restored = upkeep_settlement.restore_snapshot(snapshot[&"upkeep_settlement"], economy_service) and restored
restored = world_service.restore_snapshot(snapshot[&"world"]) and restored
restored = spatial_subslot_service.restore_snapshot(snapshot[&"spatial_subslots"]) and restored
var route_snapshot: Dictionary = snapshot[&"route_projection"]
var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null)
if not projected_walkable is Array:
return false
restored = path_service.rebuild_graph(projected_walkable) and restored
restored = path_service.restore_snapshot(snapshot[&"path"]) and restored
restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored
restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored
restored = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored
restored = facility_infrastructure_projector.restore_snapshot(
snapshot[&"facility_infrastructure"]
) and restored
restored = construction_service.restore_snapshot(snapshot[&"construction"]) and restored
restored = excavation_service.restore_snapshot(snapshot[&"excavation"]) and restored
restored = facility_service.restore_snapshot(snapshot[&"facilities"]) and restored
restored = facility_cleanliness.restore_snapshot(snapshot.get(&"facility_cleanliness", {})) and restored
restored = mana_network_service.restore_snapshot(snapshot[&"mana"]) and restored
_movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true)
restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored
restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored
return restored and _infrastructure_demolition_state_matches(snapshot) \
and _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
)
func _infrastructure_demolition_state_matches(snapshot: Dictionary) -> bool:
return _strict_variants_equal(_state.create_snapshot(), snapshot.get(&"game_state", {})) \
and _strict_variants_equal(economy_service.create_snapshot(), snapshot.get(&"economy", {})) \
and _strict_variants_equal(facility_upkeep.create_snapshot(), snapshot.get(&"facility_upkeep", {})) \
and _strict_variants_equal(upkeep_settlement.create_snapshot(), snapshot.get(&"upkeep_settlement", {})) \
and _strict_variants_equal(world_service.create_snapshot(), snapshot.get(&"world", {})) \
and _strict_variants_equal(path_service.create_snapshot(), snapshot.get(&"path", {})) \
and _strict_variants_equal(elevators.create_snapshot(), snapshot.get(&"elevators", {})) \
and _strict_variants_equal(legacy_stairs.create_snapshot(), snapshot.get(&"legacy_stairs", {})) \
and _strict_variants_equal(construction_service.create_snapshot(), snapshot.get(&"construction", {})) \
and _strict_variants_equal(excavation_service.create_snapshot(), snapshot.get(&"excavation", {})) \
and _strict_variants_equal(spatial_subslot_service.create_snapshot(), snapshot.get(&"spatial_subslots", {})) \
and _strict_variants_equal(route_subslot_path_adapter.create_snapshot(), snapshot.get(&"route_projection", {})) \
and _strict_variants_equal(facility_infrastructure_projector.create_snapshot(), snapshot.get(&"facility_infrastructure", {})) \
and _strict_variants_equal(facility_service.create_snapshot(), snapshot.get(&"facilities", {})) \
and _strict_variants_equal(facility_cleanliness.create_snapshot(), snapshot.get(&"facility_cleanliness", {})) \
and _strict_variants_equal(mana_network_service.create_snapshot(), snapshot.get(&"mana", {})) \
and _strict_variants_equal(_movement_credits, snapshot.get(&"movement_credits", {})) \
and _strict_variants_equal(domain_events.create_snapshot(), snapshot.get(&"domain_events", {})) \
and _strict_variants_equal(id_generator.create_snapshot(), snapshot.get(&"id_generator", {}))
func _execute_set_world_tree_rank(command: SimulationCommand) -> CommandResult:
var operations_gate := _operations_construction_investment_gate(command.command_id)
if operations_gate != null:
return operations_gate
var rank := int(command.payload.get(&"rank", command.payload.get("rank", 0)))
var request_id := StringName(command.payload.get(
&"request_id",
command.payload.get("request_id", "rank_up_%d" % rank)
))
var progression_before := progression_service.create_snapshot()
if not _sync_progression_live_metrics():
progression_service.restore_snapshot(progression_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"Live progression facts could not be synchronized."
)
var external_before := _capture_rank_external_state()
var result := progression_service.request_rank_up(
request_id,
rank,
func(_current_rank: int, _target_rank: int) -> Dictionary:
return {&"ok": true, &"snapshot": external_before.duplicate(true)},
func(target_rank: int, expected_bounds: Dictionary) -> Dictionary:
return {&"ok": _apply_rank_external_state(target_rank, expected_bounds)},
func(snapshot: Dictionary) -> Dictionary:
return {&"ok": _rollback_rank_external_state(snapshot)},
)
result.command_id = command.command_id
if not result.is_success():
progression_service.restore_snapshot(progression_before)
return result
if result.is_replay:
# Progression replays are read-only. The live-fact synchronization above is
# deliberately rolled back so a resent command reproduces the first result
# without silently changing any metric.
progression_service.restore_snapshot(progression_before)
return result
var event := SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"WORLD_TREE_RANK_ADVANCED",
_runtime_event_minute(),
&"progression_service",
[StringName("world_tree_rank_%d" % rank)],
result.details.duplicate(true),
command.command_id
)
if not _publish_domain_event(event):
_rollback_rank_external_state(external_before)
progression_service.restore_snapshot(progression_before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED,
command.command_id,
"World Tree promotion event could not be published; the promotion was rolled back."
)
return result
func _execute_update_progression_metrics(command: SimulationCommand) -> CommandResult:
var patch: Variant = command.payload.get(&"metrics", command.payload.get("metrics", null))
if not patch is Dictionary:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"Progression metrics must be a Dictionary."
)
if patch.has(&"guest_lifetime") or patch.has("guest_lifetime"):
var requested_value: Variant = patch.get(
&"guest_lifetime", patch.get("guest_lifetime", null)
)
var guest_snapshot := guest_service.create_snapshot()
var records_value: Variant = guest_snapshot.get(&"persistent_records", null)
var derived: int = guest_service.guest_count() \
+ (records_value.size() if records_value is Array else -1)
if typeof(requested_value) != TYPE_INT or derived < 0 \
or int(requested_value) != derived:
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
"guest_lifetime is an authoritative Guest aggregate fact."
)
if not progression_service.update_metrics(patch):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
progression_service.last_error
)
return CommandResult.succeeded(command.command_id, {
&"metrics": progression_service.metrics.duplicate(true),
})
func _execute_complete_progression_quest(command: SimulationCommand) -> CommandResult:
var raw_quest_id: Variant = command.payload.get(&"quest_id", command.payload.get("quest_id", null))
if typeof(raw_quest_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD,
command.command_id,
"A progression quest id is required."
)
var quest_id := StringName(raw_quest_id)
if not progression_service.complete_quest(quest_id):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
progression_service.last_error
)
return CommandResult.succeeded(command.command_id, {&"quest_id": quest_id})
func _execute_acknowledge_operations_pause(command: SimulationCommand) -> CommandResult:
var acknowledged := acknowledge_operations_pause()
if not bool(acknowledged.get(&"ok", false)):
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command.command_id,
String(acknowledged.get(&"message", "Operations acknowledgement failed.")),
acknowledged.get(&"details", {})
)
return CommandResult.succeeded(command.command_id, acknowledged)
func _operations_construction_investment_gate(command_id: int) -> CommandResult:
if not _operations_enabled or operations_coordinator == null:
return null
var gate := operations_coordinator.can_start_construction_or_investment()
if bool(gate.get(&"allowed", false)) and _operations_total_arrears() > 0:
gate = {&"allowed": false, &"reason": &"UNPAID_EXPENSES", &"arrears_total": _operations_total_arrears()}
if bool(gate.get(&"allowed", false)):
return null
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command_id,
"Operations currently block construction and investment.",
gate
)
func _advance_construction(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
var preflight := _preflight_proxy_completions(absolute_minute)
if not bool(preflight.get(&"ok", false)):
_latch_infrastructure_failure(
StringName(preflight.get(&"code", &"INFRASTRUCTURE_PREFLIGHT_FAILED")),
absolute_minute,
String(preflight.get(&"reason", "Infrastructure completion preflight failed.")),
preflight.get(&"details", {})
)
return
# The expensive composed rollback token is only needed when Construction can
# cross the completion boundary this minute. Idle ticks and ordinary progress
# retain ConstructionService's direct job_changed notification semantics.
if not _construction_completion_is_due_this_tick():
var fast_results: Array[CommandResult] = construction_service.advance_one_minute(
absolute_minute
)
for fast_result: CommandResult in fast_results:
if fast_result == null or not fast_result.is_success():
_latch_infrastructure_failure(
&"CONSTRUCTION_ADVANCE_FAILED",
absolute_minute,
(
fast_result.message
if fast_result != null else "Construction returned an invalid result."
),
(
fast_result.to_dictionary()
if fast_result != null else {}
)
)
return
excavation_service.advance_one_minute(absolute_minute)
if _infrastructure_failure_latched:
return
if not _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
):
_latch_infrastructure_failure(
&"INFRASTRUCTURE_POSTCONDITION_FAILED",
absolute_minute,
"Construction and Excavation ended the minute with invalid infrastructure cross-links."
)
return
var boundary_before := _capture_infrastructure_demolition_state()
if boundary_before.is_empty():
_latch_infrastructure_failure(
&"INFRASTRUCTURE_CAPTURE_FAILED",
absolute_minute,
"The Construction completion boundary could not be captured."
)
return
var previous_facility_ids: Dictionary = {}
for instance: Dictionary in construction_service.get_facilities():
previous_facility_ids[StringName(instance.get(&"instance_id", &""))] = true
_construction_completion_transaction_active = true
_construction_queued_domain_events.clear()
var progress_results: Array[CommandResult] = construction_service.advance_one_minute(
absolute_minute, {}, true
)
var apply_failure: Dictionary = {}
for progress_result: CommandResult in progress_results:
if progress_result == null or not progress_result.is_success():
apply_failure = {
&"code": &"CONSTRUCTION_ADVANCE_FAILED",
&"reason": (
progress_result.message
if progress_result != null else "Construction returned an invalid result."
),
&"details": (
progress_result.to_dictionary()
if progress_result != null else {}
),
}
break
if apply_failure.is_empty():
for instance: Dictionary in construction_service.get_facilities():
var instance_id := StringName(instance.get(&"instance_id", &""))
if previous_facility_ids.has(instance_id):
continue
var applied := _apply_completed_facility_internal(instance, absolute_minute)
if not bool(applied.get(&"ok", false)):
apply_failure = applied
break
if apply_failure.is_empty() and not _construction_completion_crosslinks_are_valid(
absolute_minute
):
apply_failure = {
&"code": &"CONSTRUCTION_COMPLETION_CROSSLINK_FAILED",
&"reason": "Construction completion produced invalid Runtime cross-links.",
&"details": {},
}
if not apply_failure.is_empty():
_fail_construction_completion_boundary(
boundary_before,
StringName(apply_failure.get(&"code", &"CONSTRUCTION_COMPLETION_FAILED")),
absolute_minute,
String(apply_failure.get(&"reason", "Construction completion failed.")),
apply_failure.get(&"details", {})
)
return
# DomainEventStream validates and commits the whole batch before notifying
# subscribers. Construction's own raw signals are released only after the
# authoritative Runtime state and event batch have both committed.
var queued_events: Array[SimulationDomainEvent] = []
for event: SimulationDomainEvent in _construction_queued_domain_events:
queued_events.append(event.copy())
_construction_completion_transaction_active = false
_construction_queued_domain_events.clear()
if not domain_events.publish_batch(queued_events):
_fail_construction_completion_boundary(
boundary_before,
&"CONSTRUCTION_EVENT_COMMIT_FAILED",
absolute_minute,
"Construction completion DomainEvents could not be committed."
)
return
_construction_committing_notifications = true
var notifications_committed := construction_service.commit_deferred_notifications()
_construction_committing_notifications = false
if not notifications_committed:
# Service-created deferred rows are prevalidated by ConstructionService,
# so this branch represents an unrecoverable notification boundary fault.
_latch_infrastructure_failure(
&"CONSTRUCTION_NOTIFICATION_COMMIT_FAILED",
absolute_minute,
"Committed Construction state could not release its deferred notifications.",
{&"rolled_back": false}
)
return
# Construction is authoritative first. Excavation then advances to the same
# absolute minute, preventing an excavation completion signal from escaping
# when the Construction boundary must be rolled back.
excavation_service.advance_one_minute(absolute_minute)
if _infrastructure_failure_latched:
return
if not _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector
):
_latch_infrastructure_failure(
&"INFRASTRUCTURE_POSTCONDITION_FAILED",
absolute_minute,
"Construction and Excavation ended the minute with invalid infrastructure cross-links."
)
func _construction_completion_is_due_this_tick() -> bool:
for job: ConstructionJob in construction_service.get_jobs():
if job == null or job.is_terminal():
continue
if job.work_units_done + ConstructionJob.NORMAL_WORK_RATE \
>= job.build_minutes * ConstructionJob.MAX_PROGRESS:
return true
return false
func _apply_completed_facility_internal(instance: Dictionary, absolute_minute: int) -> Dictionary:
var instance_id := StringName(instance.get(&"instance_id", &""))
var definition_id := StringName(instance.get(&"definition_id", &""))
var definition := definition_registry.get_facility(definition_id)
if instance_id.is_empty() or definition == null:
return {
&"ok": false,
&"code": &"INVALID_COMPLETED_FACILITY",
&"reason": "Construction produced an invalid completed Facility.",
&"details": {&"instance_id": instance_id, &"definition_id": definition_id},
}
if _facility_definition_is_schema_proxy(definition):
if not _facility_definition_is_supported_infrastructure_proxy(definition):
return {
&"ok": false,
&"code": &"UNSUPPORTED_VERTICAL_TOPOLOGY",
&"reason": "An unsupported vertical proxy reached Construction completion.",
&"details": {&"instance_id": instance_id, &"definition_id": definition.id},
}
if not facility_infrastructure_projector.has_projection(instance_id):
var projection_request_id := StringName(
"runtime.complete:%s" % String(instance.get(&"source_job_id", instance_id))
)
var projected := facility_infrastructure_projector.project_completed_facility(
projection_request_id, definition, instance
)
if not projected.is_success():
return {
&"ok": false,
&"code": &"FACILITY_INFRASTRUCTURE_PROJECTION_FAILED",
&"reason": projected.message,
&"details": projected.details.duplicate(true),
}
if not _publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"FACILITY_INFRASTRUCTURE_PROJECTED",
absolute_minute,
&"facility_infrastructure_projector",
[instance_id],
projected.details.duplicate(true),
0
)):
return {
&"ok": false,
&"code": &"FACILITY_INFRASTRUCTURE_EVENT_FAILED",
&"reason": "The infrastructure projection event could not be staged.",
&"details": {&"instance_id": instance_id},
}
else:
if not facility_service.has_instance(instance_id):
_register_facility_mana_consumer(instance)
_register_operational_facility(instance)
if not facility_service.has_instance(instance_id) \
or not economy_service.has_account(_facility_revenue_account(instance_id)):
return {
&"ok": false,
&"code": &"OPERATIONAL_FACILITY_REGISTRATION_FAILED",
&"reason": "The completed Facility could not be registered operationally.",
&"details": {&"instance_id": instance_id},
}
if definition.maintenance_mana > 0 \
and mana_network_service.get_node(instance_id).is_empty():
return {
&"ok": false,
&"code": &"FACILITY_MANA_REGISTRATION_FAILED",
&"reason": "The completed Facility Mana consumer could not be registered.",
&"details": {&"instance_id": instance_id},
}
if definition.id in [&"guest_elevator", &"service_elevator"]:
_register_facility_mana_consumer(instance)
var transport := _synchronize_elevators(absolute_minute)
if not bool(transport.get(&"ok", false)):
return {&"ok": false, &"code": &"ELEVATOR_SYNC_FAILED", &"reason": elevators.last_error, &"details": transport}
if definition.id in WarpService.DEFINITION_IDS:
_register_facility_mana_consumer(instance)
var warp_sync := _synchronize_warps()
if not bool(warp_sync.get(&"ok", false)):
return {&"ok": false, &"code": &"WARP_SYNC_FAILED", &"reason": String(warp_sync.get(&"reason", "")), &"details": warp_sync}
if not _prune_legacy_stairs_and_movement():
return {&"ok": false, &"code": &"LEGACY_STAIR_PRUNE_FAILED", &"reason": "Stair topology could not be updated."}
if not _publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"FACILITY_COMPLETED",
absolute_minute,
&"construction_service",
[instance_id],
instance.duplicate(true),
0
)):
return {
&"ok": false,
&"code": &"FACILITY_COMPLETION_EVENT_FAILED",
&"reason": "The Facility completion event could not be staged.",
&"details": {&"instance_id": instance_id},
}
return {&"ok": true}
func _construction_completion_crosslinks_are_valid(absolute_minute: int) -> bool:
return construction_service.current_minute == absolute_minute \
and bool(mana_network_service.validate_invariants().get(&"valid", false)) \
and _validate_construction_facility_crosslinks(
world_service,
construction_service,
facility_service,
facility_infrastructure_projector
) \
and _validate_infrastructure_crosslinks(
world_service,
path_service,
construction_service,
construction_resource_state,
excavation_service,
spatial_subslot_service,
route_subslot_path_adapter,
facility_infrastructure_projector,
false
)
func _fail_construction_completion_boundary(
boundary_before: Dictionary,
code: StringName,
absolute_minute: int,
reason: String,
details: Dictionary = {}
) -> void:
_construction_completion_transaction_active = false
_construction_committing_notifications = false
_construction_queued_domain_events.clear()
var notifications_discarded := construction_service.discard_deferred_notifications()
var rolled_back := notifications_discarded \
and _restore_infrastructure_demolition_state(boundary_before)
var failure_details := details.duplicate(true)
failure_details[&"notifications_discarded"] = notifications_discarded
failure_details[&"rolled_back"] = rolled_back
_latch_infrastructure_failure(
code if rolled_back else &"INFRASTRUCTURE_COMPLETION_ROLLBACK_FAILED",
absolute_minute,
reason,
failure_details
)
func _preflight_proxy_completions(absolute_minute: int) -> Dictionary:
if construction_service == null or facility_infrastructure_projector == null \
or route_subslot_path_adapter == null or spatial_subslot_service == null:
return {
&"ok": false,
&"code": &"INFRASTRUCTURE_UNAVAILABLE",
&"reason": "Infrastructure services are unavailable.",
}
var due_proxy_ids: Array[StringName] = []
for job: ConstructionJob in construction_service.get_jobs():
if job == null or job.is_terminal():
continue
var definition := definition_registry.get_facility(job.definition_id)
if definition == null:
return {
&"ok": false,
&"code": &"MISSING_FACILITY_DEFINITION",
&"reason": "A Construction job references a missing Facility definition.",
&"details": {&"job_id": job.job_id, &"definition_id": job.definition_id},
}
if not _facility_definition_is_supported_infrastructure_proxy(definition):
continue
if job.work_units_done + ConstructionJob.NORMAL_WORK_RATE \
>= job.build_minutes * ConstructionJob.MAX_PROGRESS:
due_proxy_ids.append(job.job_id)
if due_proxy_ids.is_empty():
return {&"ok": true}
if route_subslot_path_adapter.is_quarantined() \
or route_subslot_path_adapter.is_projection_stale() \
or facility_infrastructure_projector.is_quarantined() \
or facility_infrastructure_projector.is_projection_stale() \
or not bool(spatial_subslot_service.validate_invariants().get(&"ok", false)) \
or not bool(route_subslot_path_adapter.validate_invariants().get(&"ok", false)) \
or not bool(facility_infrastructure_projector.validate_invariants().get(&"ok", false)):
return {
&"ok": false,
&"code": &"INFRASTRUCTURE_DEPENDENCY_STALE",
&"reason": "Infrastructure dependencies are stale or quarantined.",
&"details": {&"job_ids": due_proxy_ids.duplicate()},
}
var projector_snapshot := facility_infrastructure_projector.create_snapshot()
if int((projector_snapshot.get(&"projections", []) as Array).size()) \
> FacilityInfrastructureProjector.MAX_PROJECTIONS - due_proxy_ids.size() \
or int((projector_snapshot.get(&"processed_requests", []) as Array).size()) \
> FacilityInfrastructureProjector.MAX_PROCESSED_REQUESTS - due_proxy_ids.size():
return {
&"ok": false,
&"code": &"INFRASTRUCTURE_CAPACITY_EXHAUSTED",
&"reason": "Infrastructure projection capacity is exhausted.",
&"details": {&"job_ids": due_proxy_ids.duplicate()},
}
return {&"ok": true}
func _latch_infrastructure_failure(
code: StringName,
absolute_minute: int,
reason: String,
details: Dictionary = {}
) -> void:
_infrastructure_failure_latched = true
_infrastructure_failure_report = {
&"code": code,
&"minute": absolute_minute,
&"reason": reason,
&"details": details.duplicate(true),
}
_operations_tick_blocked = true
if _clock != null:
_clock.set_paused(true)
func get_infrastructure_report() -> Dictionary:
return {
&"failure_latched": _infrastructure_failure_latched,
&"failure_report": _infrastructure_failure_report.duplicate(true),
&"excavation": (
excavation_service.validate_invariants().duplicate(true)
if excavation_service != null else {&"ok": false}
),
&"spatial_subslots": (
spatial_subslot_service.validate_invariants().duplicate(true)
if spatial_subslot_service != null else {&"ok": false}
),
&"route_projection": (
route_subslot_path_adapter.get_projection_status().duplicate(true)
if route_subslot_path_adapter != null else {&"configured": false}
),
&"facility_infrastructure": (
facility_infrastructure_projector.get_status().duplicate(true)
if facility_infrastructure_projector != null else {&"configured": false}
),
}.duplicate(true)
func _advance_inventory_and_logistics(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
inventory_service.current_minute = absolute_minute
inventory_service.process_expired(absolute_minute, &"inventory.waste")
if int(_state.get_resource(&"mana")) > 0:
supply_management.advance_auto_purchases(self, absolute_minute)
supply_management.ensure_deliveries(self, absolute_minute)
supply_logistics.tick(absolute_minute)
func _advance_facilities(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
facility_service.advance_one_minute(absolute_minute)
kitchen_production.advance_one_minute(absolute_minute)
func _advance_guest_vitals(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
var minute_of_day := posmod(absolute_minute, 1_440)
guest_service.advance_one_minute(absolute_minute, {
&"default": {
&"unstimulated": true,
&"solo_guest": true,
&"is_night": minute_of_day < 360 or minute_of_day >= 1_080,
&"intimacy_day": 3,
&"intimacy_night": 7,
}
})
func _advance_guest_queues(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
_release_closed_business_queues(absolute_minute)
var guests_by_request: Dictionary = {}
for guest: GuestRuntime in guest_service.get_guests():
if guest.state != GuestRuntime.State.QUEUING:
continue
if not guests_by_request.has(guest.queue_request_id):
guests_by_request[guest.queue_request_id] = []
(guests_by_request[guest.queue_request_id] as Array).append(guest)
for request_id: StringName in _sorted_name_keys(guests_by_request):
var grouped_guests: Array = guests_by_request[request_id]
var queued_facility := facility_service.get_instance(grouped_guests[0].target_facility_id)
if queued_facility != null and (not queued_facility.operator_open or not queued_facility.is_within_opening_hours(absolute_minute)):
continue # A deferred closure must not spend patience while retrying.
var abandon_group := false
for guest: GuestRuntime in grouped_guests:
if guest.patience_remaining <= 1:
abandon_group = true
break
if not abandon_group:
for guest: GuestRuntime in grouped_guests:
guest_service.tick_queue_patience(guest.guest_id, absolute_minute)
continue
var inventory_before := inventory_service.create_snapshot()
var facility_before := facility_service.create_snapshot()
var guest_before := guest_service.create_snapshot()
var events_before := domain_events.create_snapshot()
var id_before := id_generator.create_snapshot()
var group_succeeded := true
for guest_index in range(grouped_guests.size()):
var guest: GuestRuntime = grouped_guests[guest_index]
var abandoned := guest_service.tick_queue_patience(
guest.guest_id,
absolute_minute,
-guest.patience_remaining,
Callable(self, "_leave_guest_queue_boundary") if guest_index == 0 else Callable()
)
if not bool(abandoned.get(&"ok", false)):
group_succeeded = false
break
if not group_succeeded:
inventory_service.restore_snapshot(inventory_before)
facility_service.restore_snapshot(facility_before)
guest_service.restore_snapshot(guest_before)
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(id_before)
func _advance_authority_incidents(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
if _operations_morning_already_processed(absolute_minute):
return
var advanced := authority_service.advance_time(absolute_minute)
if not advanced.is_success():
_clock.set_paused(true)
_publish_actor_event(&"AUTHORITY_ADVANCE_FAILED", &"authority_service", [], {
&"message": advanced.message,
&"details": advanced.details.duplicate(true),
})
return
if posmod(absolute_minute, 1_440) != AuthorityService.INSPECTION_EVALUATION_MINUTE:
return
var evaluated := authority_service.evaluate_inspection(absolute_minute)
if not evaluated.is_success():
_clock.set_paused(true)
_publish_actor_event(&"AUTHORITY_INSPECTION_EVALUATION_FAILED", &"authority_service", [], {
&"message": evaluated.message,
&"details": evaluated.details.duplicate(true),
})
return
if not evaluated.is_replay:
_publish_authority_evaluation(evaluated.details)
func _advance_defense(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
_prepare_defense_round_attempts(absolute_minute)
# A deferred operations suffix must not reattempt the same failed automatic
# batch. Resume on a later game minute, after the player resumes the clock.
if _defense_automatic_failed_minute == absolute_minute:
return
var advanced := defense_service.advance_time(absolute_minute)
if not advanced.is_success():
_defense_automatic_failed_minute = absolute_minute
_clock.set_paused(true)
_publish_actor_event(&"DEFENSE_ADVANCE_FAILED", &"defense_service", [], {
&"message": advanced.message,
&"details": advanced.details.duplicate(true),
})
return
# One attempt per due raid, in stable raid-ID order. No catch-up while-loop,
# invented deadline, offline replay, PREPARING start or BETWEEN_WAVES advance.
for due: Dictionary in defense_service.get_due_rounds(absolute_minute):
if _defense_automatic_failed_minute == absolute_minute:
return
var raid_id := StringName(due[&"raid_id"])
if _defense_round_attempted_raids.has(raid_id):
continue
var resolved := _resolve_defense_round_internal(raid_id, &"", absolute_minute)
if not resolved.is_success():
_defense_automatic_failed_minute = absolute_minute
_clock.set_paused(true)
_publish_actor_event(&"DEFENSE_AUTOMATIC_ROUND_FAILED", &"defense_service", [raid_id], {
&"round_number": due[&"round_number"],
&"earliest_minute": due[&"earliest_minute"],
&"message": resolved.message,
&"details": resolved.details.duplicate(true),
})
return
func _advance_relationship_events(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
if _operations_morning_already_processed(absolute_minute):
return
event_service.process_expired(absolute_minute)
event_service.audit_actor_availability(absolute_minute)
_enqueue_pending_relationship_candidates()
if event_service.get_foreground_instance() == null:
var started := event_service.start_next_event(
absolute_minute,
_build_event_context(absolute_minute)
)
if started.is_success() and bool(started.details.get(&"waiting_for_choice", false)):
_clock.set_paused(true)
if event_service.pause_requested:
_clock.set_paused(true)
func _operations_morning_already_processed(absolute_minute: int) -> bool:
if not _operations_enabled or operations_coordinator == null \
or posmod(absolute_minute, SimulationKernel.MINUTES_PER_DAY) \
!= OPERATIONS_START_MINUTE:
return false
var day := absolute_minute / SimulationKernel.MINUTES_PER_DAY + 1
var report := operations_coordinator.get_morning_report(day)
return not report.is_empty() and int(report.get(&"minute", -1)) == absolute_minute
func _enqueue_pending_relationship_candidates() -> void:
for candidate: Dictionary in relationship_service.get_threshold_candidates():
_on_relationship_threshold_candidate(candidate)
func _build_event_context(absolute_minute: int) -> Dictionary:
return {
&"absolute_minute": absolute_minute,
&"day": absolute_minute / 1_440 + 1,
&"minute_of_day": posmod(absolute_minute, 1_440),
&"player": {
&"gold": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
&"world_tree_rank": int(_state.world_tree_rank),
},
}
func _advance_staff_ai(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
_prune_expired_staff_elevator_waits(absolute_minute)
_advance_elevators(absolute_minute)
_advance_staff_rescue(absolute_minute)
staff_service.advance_one_minute(absolute_minute)
_prune_expired_staff_elevator_waits(absolute_minute)
staff_commute.tick(absolute_minute)
_sync_facility_staffing(absolute_minute)
cleaning_bridge.tick(absolute_minute)
for task: StaffTask in staff_service.get_tasks():
if task.state == StaffTask.State.RESERVED:
if task.state_changed_at < absolute_minute:
task.begin_moving(task.state_changed_at)
elif task.state == StaffTask.State.WORKING and task.task_type == CleaningBridge.TASK_TYPE:
# Only cleaning is deferred from StaffService to these attendance and
# dependency checks. Other tasks already progressed once this minute.
if task.state_changed_at >= absolute_minute or not cleaning_bridge.task_can_work(task, staff_service.get_staff(task.assigned_staff_id)):
continue
var work_units := CleaningBridge.work_units_per_minute(staff_service.get_staff(task.assigned_staff_id))
staff_service.progress_task(task.task_id, work_units, absolute_minute)
for staff: StaffRuntime in staff_service.get_staff_members():
if absolute_minute % 30 != 0:
continue
if staff.state == StaffRuntime.State.WORKING or (staff.state == StaffRuntime.State.AVAILABLE and staff_commute.is_present(staff, facility_service.get_instance(staff.assigned_facility_id))):
staff.add_fatigue(1)
if _operations_enabled:
staff.health = maxi(staff.health - 1, 0)
elif staff.state == StaffRuntime.State.OFF_DUTY:
var resting := get_staff_rest_report(staff.staff_id)
if not _operations_enabled or resting.get(&"resting", false):
staff.add_fatigue(-1)
if _operations_enabled and staff.health > 0:
staff.health = mini(staff.health + 1, 100)
_advance_staff_rescue(absolute_minute)
_sync_facility_staffing(absolute_minute)
_advance_guest_ai_decisions(absolute_minute)
## Record the actual completed choice, not a fresh prediction made by the UI.
func _decide_guest_with_log(guest_id: StringName, candidates: Array, minute: int, context: Dictionary) -> Dictionary:
var observation := guest_service.get_observation(guest_id)
var result := guest_service.decide(guest_id, candidates, minute, context)
if not bool(result.get(&"ok", false)):
return result
var candidate: Dictionary = result.get(&"candidate", {})
var rejections := guest_service.get_last_candidate_rejections(guest_id)
var reason_counts: Dictionary = {}
for rejection: Dictionary in rejections:
var reason := String(rejection.get(&"reason", ""))
reason_counts[reason] = int(reason_counts.get(reason, 0)) + 1
var payload := {
&"rejection_counts": reason_counts,
&"rejection_examples": rejections.slice(0, 8),
&"rejection_count": rejections.size(),
&"action": result[&"action"],
&"needs": observation.get(&"needs", {}).duplicate(true),
&"primary_needs": observation.get(&"primary_needs", []).duplicate(),
&"facility_id": candidate.get(&"facility_id", &""),
&"need_priority": candidate.get(&"need_priority", 0),
&"utility": candidate.get(&"utility", 0),
&"travel_minutes": candidate.get(&"travel_minutes", 0),
&"wait_minutes": candidate.get(&"utility_terms", {}).get(&"wait_minutes", 0),
&"estimated_total_gold": maxi(int(candidate.get(&"estimated_total_gold", candidate.get(&"price_gold", candidate.get(&"quoted_price", 0)))), 0),
}
_publish_actor_event(&"GUEST_DECISION_RECORDED", &"guest_service", [guest_id], payload)
return result
func get_guest_decision_record(guest_id: StringName) -> Dictionary:
var events := domain_events.recent_events()
for index in range(events.size() - 1, -1, -1):
var event: SimulationDomainEvent = events[index]
if event.event_type == &"GUEST_DECISION_RECORDED" and guest_id in event.entity_ids:
var report := event.payload.duplicate(true)
report[&"absolute_minute"] = event.absolute_minute
return report
return {}
func _advance_guest_ai_decisions(absolute_minute: int) -> void:
for guest: GuestRuntime in guest_service.get_guests():
# A rider inside a cabin cannot decide, leave or despawn until the
# elevator sets it down at a real landing (P14-03: passengers never vanish).
if elevators.get_actor_trip(_guest_path_actor_id(guest.guest_id)).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED:
continue
match guest.state:
GuestRuntime.State.SPAWNING:
guest_service.transition_guest(guest.guest_id, GuestRuntime.State.ENTERING, absolute_minute, &"ENTRY_STARTED")
GuestRuntime.State.ENTERING:
guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"ENTRY_COMPLETED")
GuestRuntime.State.DECIDING:
if guest.planned_exit_at <= absolute_minute:
guest_service.begin_leaving(guest.guest_id, absolute_minute, &"PLANNED_EXIT")
continue
guest.wallet_gold = economy_service.get_balance(_guest_wallet_account(guest.guest_id))
_decide_guest_with_log(
guest.guest_id,
_project_guest_candidates(guest.guest_id),
absolute_minute,
{&"absolute_minute": absolute_minute}
)
GuestRuntime.State.MOVING:
pass
GuestRuntime.State.REPLANNING, GuestRuntime.State.ABANDONING, GuestRuntime.State.INTERRUPTED:
guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"REPLAN_AFTER_INTERRUPTION")
GuestRuntime.State.LEAVING:
if guest.state_entered_at < absolute_minute:
guest_service.despawn_guest(guest.guest_id, absolute_minute)
_advance_runtime_path_traffic(absolute_minute)
func _advance_runtime_path_traffic(absolute_minute: int) -> void:
var actors: Array[Dictionary] = []
for guest: GuestRuntime in guest_service.get_guests():
actors.append({
&"actor_id": _guest_path_actor_id(guest.guest_id),
&"kind": &"guest",
&"entity_id": guest.guest_id,
})
for staff: StaffRuntime in staff_service.get_staff_members():
actors.append({
&"actor_id": _staff_path_actor_id(staff.staff_id),
&"kind": &"staff",
&"entity_id": staff.staff_id,
})
actors.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
return String(left[&"actor_id"]) < String(right[&"actor_id"])
)
for entry: Dictionary in actors:
var actor_id: StringName = entry[&"actor_id"]
if not elevators.get_actor_trip(actor_id).is_empty():
_record_expected_actor_wait(actor_id, path_service.get_actor_projected_cell(actor_id), absolute_minute)
continue
var kind: StringName = entry[&"kind"]
var entity_id: StringName = entry[&"entity_id"]
if kind == &"guest":
var guest := guest_service.get_guest(entity_id)
if guest == null:
continue
if not _path_projection_matches(actor_id, guest.current_cell):
_report_path_integration_failure(actor_id, entity_id, kind, guest.current_cell)
continue
if guest.state == GuestRuntime.State.MOVING \
and guest.state_entered_at < absolute_minute:
_advance_guest_path_travel(guest, actor_id, absolute_minute)
else:
_record_expected_actor_wait(actor_id, guest.current_cell, absolute_minute)
continue
var staff := staff_service.get_staff(entity_id)
if staff == null:
continue
if not _path_projection_matches(actor_id, staff.current_cell):
_report_path_integration_failure(actor_id, entity_id, kind, staff.current_cell)
continue
if staff.state == StaffRuntime.State.COMMUTING and staff_service.requires_attendance(staff.staff_id):
# The commute driver already consumed this tick, including fractional
# walking credit under congestion. Do not reset it as an idle task.
continue
if _operations_enabled and staff.state == StaffRuntime.State.OFF_DUTY and staff.current_task_id.is_empty():
var rest := get_staff_rest_report(staff.staff_id)
if rest.get(&"reachable", false):
advance_staff_commute(staff.staff_id, rest[&"cell"], absolute_minute)
else:
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
continue
var task := staff_service.get_task(staff.current_task_id)
if task != null and task.state == StaffTask.State.MOVING \
and task.assigned_staff_id == staff.staff_id \
and supply_logistics.can_move_task(task.task_id):
_advance_staff_task_travel(task, actor_id, absolute_minute)
else:
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
func _path_projection_matches(actor_id: StringName, cell: Vector3i) -> bool:
return path_service.has_actor_projection(actor_id) \
and path_service.get_actor_projected_cell(actor_id) == cell \
and _movement_credits.has(actor_id)
func _report_path_integration_failure(
actor_id: StringName,
entity_id: StringName,
kind: StringName,
cell: Vector3i
) -> void:
_clock.set_paused(true)
_publish_actor_event(&"PATH_ACTOR_CROSSLINK_FAILED", &"simulation_runtime", [actor_id, entity_id], {
&"actor_kind": kind,
&"actor_cell": cell,
&"projected_cell": path_service.get_actor_projected_cell(actor_id),
&"has_projection": path_service.has_actor_projection(actor_id),
&"has_movement_credit": _movement_credits.has(actor_id),
})
func _record_expected_actor_wait(
actor_id: StringName,
cell: Vector3i,
absolute_minute: int
) -> void:
# Intentional queue, service, decision, and idle waits are valid progress. Reset
# the stuck baseline so a later movement does not inherit intentional waiting.
path_service.clear_actor_progress(actor_id)
var recorded := path_service.record_actor_progress(actor_id, cell, absolute_minute)
if not bool(recorded.get(&"ok", false)):
_report_path_integration_failure(actor_id, actor_id, &"unknown", cell)
return
_movement_credits[actor_id] = {&"credit": 0, &"updated_at": absolute_minute}
func _record_blocked_actor_wait(
actor_id: StringName,
entity_id: StringName,
kind: StringName,
cell: Vector3i,
absolute_minute: int,
credit: int
) -> void:
var recorded := path_service.record_actor_progress(actor_id, cell, absolute_minute)
if not bool(recorded.get(&"ok", false)):
_report_path_integration_failure(actor_id, entity_id, kind, cell)
return
_movement_credits[actor_id] = {
&"credit": clampi(credit, 0, _cell_movement_credit_limit(path_service, cell) - 1),
&"updated_at": absolute_minute,
}
if int(recorded.get(&"minutes_without_progress", 0)) >= PathService.STUCK_THRESHOLD_MINUTES:
_recover_stuck_actor(actor_id, entity_id, kind, cell, absolute_minute)
func _movement_credit_after_one_minute(
actor_id: StringName,
cell: Vector3i,
absolute_minute: int
) -> int:
var current: Dictionary = _movement_credits.get(actor_id, {
&"credit": 0,
&"updated_at": absolute_minute - 1,
})
var carried := int(current.get(&"credit", 0))
if int(current.get(&"updated_at", -1)) >= absolute_minute:
return carried
var speed_credit := clampi(
roundi(path_service.get_speed_multiplier(cell) * MOVEMENT_CREDIT_SCALE),
MIN_MOVEMENT_CREDIT_PER_MINUTE,
MOVEMENT_CREDIT_SCALE
)
return carried + speed_credit
func _advance_guest_path_travel(
guest: GuestRuntime,
actor_id: StringName,
absolute_minute: int
) -> void:
var target_cell := _path_handle_target(guest.path_handle, guest.current_cell)
if guest.current_cell == target_cell:
_record_expected_actor_wait(actor_id, guest.current_cell, absolute_minute)
_queue_guest_at_target(guest, absolute_minute)
return
var path := path_service.find_path(guest.current_cell, target_cell)
if path.size() <= 1:
_record_blocked_actor_wait(
actor_id, guest.guest_id, &"guest", guest.current_cell, absolute_minute, 0
)
return
if _request_elevator_step(actor_id, path[0], path[1], absolute_minute, []):
return
var required_credit := _step_movement_credit(path_service, path[0], path[1], [])
var accumulated := _movement_credit_after_one_minute(
actor_id, guest.current_cell, absolute_minute
)
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
var guest_warp := _warp_step_connector(path[0], path[1], [])
if accumulated < required_credit or not _warp_fare_affordable(guest_warp, 1):
_record_blocked_actor_wait(
actor_id,
guest.guest_id,
&"guest",
guest.current_cell,
absolute_minute,
accumulated
)
return
var previous_cell := guest.current_cell
var previous_progress_cell := guest.last_progress_cell
var previous_progress_at := guest.last_progress_at
var previous_failure_count := guest.path_failure_count
var previous_recovery_kind := guest.recovery_kind
if not guest_service.record_path_success(guest.guest_id, path[1], absolute_minute) \
or not path_service.project_actor_position(actor_id, path[1], absolute_minute):
guest.current_cell = previous_cell
guest.last_progress_cell = previous_progress_cell
guest.last_progress_at = previous_progress_at
guest.path_failure_count = previous_failure_count
guest.recovery_kind = previous_recovery_kind
_report_path_integration_failure(actor_id, guest.guest_id, &"guest", previous_cell)
return
_charge_warp_fare(guest_warp, [actor_id], previous_cell, guest.current_cell)
var remaining_credit := accumulated - required_credit
_movement_credits[actor_id] = {
&"credit": remaining_credit,
&"updated_at": absolute_minute,
}
_publish_actor_event(&"GUEST_MOVED", &"path_service", [guest.guest_id, actor_id], {
&"from_cell": previous_cell,
&"cell": guest.current_cell,
&"target_cell": target_cell,
&"movement_credit": remaining_credit,
})
if guest.current_cell == target_cell:
_queue_guest_at_target(guest, absolute_minute)
func _queue_guest_at_target(guest: GuestRuntime, absolute_minute: int) -> void:
var operation_key := StringName(
"guest.auto_queue.%s.%d" % [guest.guest_id, guest.state_entered_at]
)
var result := submit_command(
&"create_service_request",
{
&"facility_id": guest.target_facility_id,
&"guest_ids": [guest.guest_id],
&"service_type": &"default",
},
guest.guest_id,
guest.target_facility_id,
0,
absolute_minute,
operation_key
)
if not result.is_success() and guest.state == GuestRuntime.State.MOVING:
guest_service.transition_guest(
guest.guest_id,
GuestRuntime.State.DECIDING,
absolute_minute,
&"QUEUE_REQUEST_FAILED"
)
func _guest_recovery_targets(current_cell: Vector3i) -> Array:
return [{
&"kind": &"emergency_exit",
&"cell": current_cell,
&"path_handle": {&"recovery": true, &"target_cell": current_cell},
}]
func _guest_recovery_path_exists(
from_cell: Vector3i,
to_cell: Vector3i,
_guest_id: StringName,
_target: Dictionary
) -> bool:
return not path_service.find_path(from_cell, to_cell).is_empty()
func _advance_staff_task_travel(
task: StaffTask,
actor_id: StringName,
absolute_minute: int
) -> void:
var staff := staff_service.get_staff(task.assigned_staff_id)
if staff == null or staff.current_task_id != task.task_id:
return
if task.task_type == CleaningBridge.TASK_TYPE and not cleaning_bridge.prepare_route(task, staff):
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
return
if staff.current_cell == task.target_cell:
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
staff_service.begin_task(task.task_id, absolute_minute)
return
var path := path_service.find_path(
staff.current_cell,
task.target_cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
if task.task_type in [&"rescue_pickup", &"rescue_transport"]:
path = path_service.find_backyard_path(staff.current_cell, task.target_cell, true)
if path.size() <= 1:
if task.task_type in [&"rescue_pickup", &"rescue_transport"]:
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
return
_record_blocked_actor_wait(
actor_id, staff.staff_id, &"staff", staff.current_cell, absolute_minute, 0
)
return
if task.task_type == &"rescue_transport" and elevators.owns_connector(path_service.get_step_connector(path[0], path[1], [&"staff"])):
var patient_actor := _staff_path_actor_id(task.target_entity_id)
var connector := path_service.get_step_connector(path[0], path[1], [&"staff"])
var pair := {}
if not _elevator_extension_pending(elevators.get_shaft_for_connector(connector)):
pair = elevators.request_pair(actor_id, patient_actor, path[0], path[1], absolute_minute, [&"staff"])
if pair.get(&"ok", false) and not pair.get(&"replay", false):
_publish_actor_event(&"ELEVATOR_RESCUE_REQUESTED", &"elevators", [actor_id, patient_actor], pair[&"trip"])
_record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute)
return
if _request_elevator_step(actor_id, path[0], path[1], absolute_minute, [&"staff"]):
return
var required_credit := _step_movement_credit(path_service, path[0], path[1], [&"staff"])
var accumulated := _movement_credit_after_one_minute(
actor_id, staff.current_cell, absolute_minute
)
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
var staff_warp := _warp_step_connector(path[0], path[1], [&"staff"])
var warp_riders := 2 if task.task_type == &"rescue_transport" else 1
if accumulated < required_credit or not _warp_fare_affordable(staff_warp, warp_riders):
_record_blocked_actor_wait(
actor_id,
staff.staff_id,
&"staff",
staff.current_cell,
absolute_minute,
accumulated
)
return
var previous_cell := staff.current_cell
staff.current_cell = path[1]
if not path_service.project_actor_position(actor_id, staff.current_cell, absolute_minute):
staff.current_cell = previous_cell
_report_path_integration_failure(actor_id, staff.staff_id, &"staff", previous_cell)
return
if task.task_type == &"rescue_transport":
var patient := staff_service.get_staff(task.target_entity_id)
if patient == null or not path_service.project_actor_position(_staff_path_actor_id(patient.staff_id), staff.current_cell, absolute_minute):
staff.current_cell = previous_cell
path_service.project_actor_position(actor_id, previous_cell, absolute_minute)
return
patient.current_cell = staff.current_cell
_record_expected_actor_wait(_staff_path_actor_id(patient.staff_id), patient.current_cell, absolute_minute)
_charge_warp_fare(staff_warp, [actor_id, _staff_path_actor_id(patient.staff_id)], previous_cell, staff.current_cell)
else:
_charge_warp_fare(staff_warp, [actor_id], previous_cell, staff.current_cell)
var remaining_credit := accumulated - required_credit
_movement_credits[actor_id] = {
&"credit": remaining_credit,
&"updated_at": absolute_minute,
}
_publish_actor_event(&"STAFF_MOVED", &"staff_service", [staff.staff_id, task.task_id], {
&"path_actor_id": actor_id,
&"from_cell": previous_cell,
&"cell": staff.current_cell,
&"target_cell": task.target_cell,
&"movement_credit": remaining_credit,
})
if staff.current_cell == task.target_cell:
staff_service.begin_task(task.task_id, absolute_minute)
func _recover_stuck_actor(
actor_id: StringName,
entity_id: StringName,
kind: StringName,
cell: Vector3i,
absolute_minute: int
) -> void:
var path_before := path_service.create_snapshot()
var guests_before := guest_service.create_snapshot()
var staff_before := staff_service.create_snapshot()
var inventory_before := inventory_service.create_snapshot()
var events_before := domain_events.create_snapshot()
var ids_before := id_generator.create_snapshot()
var credits_before := _movement_credits.duplicate(true)
var report := path_service.get_actor_stuck_report(
actor_id,
absolute_minute,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK]
)
if not bool(report.get(&"ok", false)) or not bool(report.get(&"should_recover", false)):
return
var action := &"NONE"
var action_ok := false
if kind == &"guest":
var guest := guest_service.get_guest(entity_id)
if guest != null:
var recovery_targets: Array = []
var recovery_value: Variant = report.get(&"recovery", {})
if recovery_value is Dictionary and bool(recovery_value.get(&"found", false)):
var recovery_cell: Vector3i = recovery_value[&"cell"]
recovery_targets.append({
&"kind": &"guidance",
&"cell": recovery_cell,
&"path_handle": {
&"recovery": true,
&"target_cell": recovery_cell,
},
})
var recovery_result: Dictionary = {}
for _attempt in range(GuestService.PATH_FAILURE_LIMIT):
recovery_result = guest_service.record_path_failure(
entity_id,
absolute_minute,
recovery_targets,
Callable(self, "_guest_recovery_path_exists")
)
if not bool(recovery_result.get(&"ok", false)) \
or bool(recovery_result.get(&"recovery_started", false)):
break
action_ok = bool(recovery_result.get(&"ok", false)) \
and bool(recovery_result.get(&"recovery_started", false))
action = &"GUEST_SAFE_RECOVERY" if action_ok else &"GUEST_RECOVERY_FAILED"
elif kind == &"staff":
var staff := staff_service.get_staff(entity_id)
if staff != null and not staff.current_task_id.is_empty():
if supply_logistics.owns_task(staff.current_task_id):
var recovery: Dictionary = supply_logistics.handle_path_stuck(staff.current_task_id, absolute_minute)
action_ok = bool(recovery.get(&"ok", false))
action = StringName(recovery.get(&"action", &"SUPPLY_RECOVERY_FAILED"))
else:
var cancelled := staff_service.cancel_task(staff.current_task_id, &"PATH_STUCK", absolute_minute)
action_ok = cancelled.is_success()
action = &"STAFF_TASK_CANCELLED" if action_ok else &"STAFF_TASK_CANCEL_FAILED"
if not action_ok:
path_service.restore_snapshot(path_before)
guest_service.restore_snapshot(guests_before)
staff_service.restore_snapshot(staff_before)
inventory_service.restore_snapshot(inventory_before)
domain_events.restore_snapshot(events_before)
id_generator.restore_snapshot(ids_before)
_movement_credits = credits_before
return
_movement_credits[actor_id] = {&"credit": 0, &"updated_at": absolute_minute}
_publish_actor_event(&"PATH_ACTOR_STUCK", &"path_service", [actor_id, entity_id], {
&"actor_kind": kind,
&"cell": cell,
&"minutes_without_progress": int(report[&"minutes_without_progress"]),
&"threshold_minutes": int(report[&"threshold_minutes"]),
&"recovery": (report[&"recovery"] as Dictionary).duplicate(true),
&"action": action,
&"implicit_move_applied": false,
})
func _leave_guest_queue_boundary(
_guest_id: StringName,
facility_id: StringName,
request_id: StringName
) -> Dictionary:
var request := facility_service.get_request(request_id)
if request == null:
return {&"ok": true, &"orphan_repaired": true}
var inventory_before := inventory_service.create_snapshot()
for reservation_id: StringName in request.reserved_inventory_ids:
var released := inventory_service.release_reservation(
reservation_id,
StringName("guest.abandon.%s.%s" % [request_id, reservation_id]),
_clock.absolute_minute
)
if not bool(released.get(&"ok", false)):
inventory_service.restore_snapshot(inventory_before)
return released
var leave_result := facility_service.leave_queue(facility_id, request_id, &"PATIENCE_EXHAUSTED")
if not leave_result.is_success():
inventory_service.restore_snapshot(inventory_before)
return {&"ok": false, &"message": leave_result.message}
return {&"ok": true}
func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Dictionary:
var inventory_id := &"staff.supplies"
var facility := facility_service.get_instance(task.target_entity_id)
if facility != null:
inventory_id = facility.local_inventory_id
var before := inventory_service.create_snapshot()
var reservation_ids: Array[StringName] = []
for item_id: StringName in _sorted_name_keys(task.required_items):
var result := inventory_service.reserve_item(
inventory_id,
item_id,
int(task.required_items[item_id]),
task.task_id,
&"staff_task",
absolute_minute,
absolute_minute + 480,
StringName("staff.task.reserve.%s.%s" % [task.task_id, item_id])
)
if not bool(result.get(&"ok", false)):
inventory_service.restore_snapshot(before)
return {
&"ok": false,
&"reason": StringName(result.get(&"code", &"ITEM_RESERVATION_FAILED")),
}
for reservation_id: StringName in result.get(&"reservation_ids", []):
reservation_ids.append(reservation_id)
return {&"ok": true, &"reservation_ids": reservation_ids}
func _release_staff_task_items(
reservation_ids: Array[StringName],
task: StaffTask,
absolute_minute: int
) -> Dictionary:
var before := inventory_service.create_snapshot()
for reservation_id: StringName in reservation_ids:
var result := inventory_service.release_reservation(
reservation_id,
StringName("staff.task.release.%s.%s" % [task.task_id, reservation_id]),
absolute_minute
)
if not bool(result.get(&"ok", false)):
inventory_service.restore_snapshot(before)
return result
return {&"ok": true}
func _estimate_staff_task_travel(staff: StaffRuntime, task: StaffTask, _absolute_minute: int) -> int:
if task.task_type == CleaningBridge.TASK_TYPE and not cleaning_bridge.can_dispatch(staff): return -1
if task.task_type in [&"rescue_pickup", &"rescue_transport"]:
if not elevators.get_actor_trip(_staff_path_actor_id(staff.staff_id)).is_empty():
return -1
var route := path_service.find_backyard_path(staff.current_cell, task.target_cell, true)
return _estimated_actor_path_minutes(route, [&"staff"]) if not route.is_empty() else -1
if staff.current_cell == task.target_cell:
return 0
var path := path_service.find_path(
staff.current_cell,
task.target_cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
if not path.is_empty():
return _estimated_actor_path_minutes(path, [&"staff"])
return -1
func _sync_facility_staffing(absolute_minute: int) -> void:
if not _staffing_projection_enabled:
return
var staff_members := staff_service.get_staff_members()
for facility: FacilityInstance in facility_service.get_instances():
var active_ids: Array[StringName] = []
for staff: StaffRuntime in staff_members:
if (staff_commute.is_present(staff, facility) or CleaningBridge.is_deployed(staff, facility, staff_service)) and not _is_anri_manager(staff):
active_ids.append(staff.staff_id)
facility_service.set_operational_status(
facility.instance_id,
{&"active_staff_ids": active_ids},
absolute_minute
)
func _project_guest_candidates(guest_id: StringName) -> Array:
var guest := guest_service.get_guest(guest_id)
var candidates: Array = []
if guest == null:
return candidates
var query_goals: Array = []
for instance: FacilityInstance in facility_service.get_instances():
query_goals.append_array(_facility_access_candidates(instance))
path_service.begin_guest_path_query_batch(guest.current_cell, query_goals)
for facility: FacilityInstance in facility_service.get_instances():
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null or &"staff_only" in definition.tags:
continue
if &"entrance" in definition.tags and _guest_has_checked_in(guest):
continue
var has_inputs := true
for item_id: StringName in _sorted_name_keys(definition.inventory_inputs):
if inventory_service.available_quantity(facility.local_inventory_id, item_id) < int(definition.inventory_inputs[item_id]):
has_inputs = false
break
if not has_inputs:
# Preserve the existing exclusion without doing unnecessary path queries,
# but let the decision evidence explain why this facility was unavailable.
candidates.append({&"facility_id": facility.instance_id, &"inputs_available": false})
continue
var access := _find_facility_access_path(guest.current_cell, facility)
var path: Array = access.get(&"path", [])
var need_relief := _need_relief_for_facility_tags(definition.tags)
candidates.append({
&"facility_id": facility.instance_id,
&"facility_tags": definition.tags.duplicate(),
&"is_open": facility.state in [FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED],
&"reachable": bool(access.get(&"reachable", false)),
&"travel_minutes": _estimated_actor_path_minutes(path, []),
&"expected_wait": facility.queue_entries.size() * facility.base_service_minutes / maxi(facility.capacity, 1),
&"estimated_total_gold": get_facility_business_price(facility),
&"expected_quality": clampi(facility.base_quality, 0, 100),
&"need_relief": need_relief,
&"risk_level": 0,
&"illegal_level": 0,
&"group_fit": mini(facility.capacity, 4) * 5,
&"service_category": _service_category_for_tags(definition.tags),
&"path_handle": {
&"target_cell": access.get(&"target_cell", guest.current_cell),
&"graph_revision": path_service.graph_revision,
},
})
path_service.end_guest_path_query_batch()
return candidates
func _facility_access_candidates(facility: FacilityInstance) -> Array:
var candidate_cells: Dictionary = {}
var occupied: Array[Vector3i] = facility.occupied_cells.duplicate()
if occupied.is_empty():
occupied.append(facility.origin)
for cell: Vector3i in occupied:
for direction: Vector3i in [Vector3i.LEFT, Vector3i.RIGHT]:
var candidate := Vector3i(cell.x + direction.x, cell.y + direction.y, cell.z)
if path_service.has_cell(candidate):
candidate_cells[candidate] = true
if path_service.has_cell(facility.origin):
candidate_cells[facility.origin] = true
var ordered: Array = candidate_cells.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x
)
return ordered
func _find_facility_access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary:
var ordered := _facility_access_candidates(facility)
var best_path: Array[Vector3i] = []
var best_cell := facility.origin
for candidate: Vector3i in ordered:
var path := path_service.find_path(start, candidate)
if path.is_empty():
continue
if best_path.is_empty() or path_service.calculate_guest_batch_path_cost(start, candidate, path) < path_service.calculate_guest_batch_path_cost(start, best_cell, best_path):
best_path.assign(path)
best_cell = candidate
return {&"reachable": not best_path.is_empty(), &"path": best_path, &"target_cell": best_cell}
func _need_relief_for_facility_tags(tags: Array[StringName]) -> Dictionary:
var relief := {
&"hunger": 0, &"sleep": 0, &"hygiene": 0,
&"medical": 0, &"leisure": 0, &"intimacy": 0,
}
if &"dining" in tags:
relief[&"hunger"] = 80
relief[&"leisure"] = 10
if &"lodging" in tags:
relief[&"sleep"] = 90
relief[&"hygiene"] = maxi(int(relief[&"hygiene"]), 10)
if &"social" in tags or &"leisure" in tags:
relief[&"leisure"] = maxi(int(relief[&"leisure"]), 40)
if &"hygiene" in tags or &"bath" in tags:
relief[&"hygiene"] = maxi(int(relief[&"hygiene"]), 80)
if &"medical" in tags:
relief[&"medical"] = 90
elif &"wellness" in tags:
relief[&"medical"] = maxi(int(relief[&"medical"]), 20)
if &"adult_only" in tags or &"restricted_service" in tags:
relief[&"intimacy"] = 80
return relief
func _service_category_for_tags(tags: Array[StringName]) -> StringName:
if &"dining" in tags:
return &"food"
if &"lodging" in tags:
return &"lodging"
if &"social" in tags:
return &"leisure"
return &"service"
func _path_handle_target(path_handle: Variant, fallback: Vector3i) -> Vector3i:
if path_handle is Dictionary:
var value: Variant = path_handle.get(&"target_cell", path_handle.get("target_cell", fallback))
var parsed: Variant = _parse_world_cell(value)
if parsed != null:
return parsed
return fallback
func _event_actor_exists(actor_id: StringName) -> bool:
if actor_id == &"player":
return true
return guest_service.has_guest(actor_id) or staff_service.has_staff(actor_id)
func _event_entity_exists(entity_id: Variant) -> bool:
var normalized := StringName(entity_id)
return _event_actor_exists(normalized) \
or facility_service.has_instance(normalized) \
or relationship_service.has_relationship(normalized) \
or event_service.get_instance(normalized) != null
func _event_path_exists(value: Variant) -> bool:
if not value is Dictionary:
return false
var from_value: Variant = _parse_world_cell(value.get(&"from", value.get("from", null)))
var to_value: Variant = _parse_world_cell(value.get(&"to", value.get("to", null)))
if from_value == null or to_value == null:
return false
return not path_service.find_path(from_value, to_value).is_empty()
func _roll_authority_inspection(_day_index: int, _chance: int, _terms: Dictionary) -> int:
var stream := get_rng_stream(&"authority")
return stream.randi_range(0, 99) if stream != null else 99
func _authority_line_of_sight(
from_cell: Vector3i,
to_cell: Vector3i,
_context: Dictionary
) -> bool:
if from_cell.z != to_cell.z \
or not world_service.is_in_bounds(from_cell) \
or not world_service.is_in_bounds(to_cell):
return false
if from_cell == to_cell:
return true
var x := from_cell.x
var y := from_cell.y
var delta_x := absi(to_cell.x - from_cell.x)
var delta_y := absi(to_cell.y - from_cell.y)
var step_x := 1 if from_cell.x < to_cell.x else -1
var step_y := 1 if from_cell.y < to_cell.y else -1
var error := delta_x - delta_y
while x != to_cell.x or y != to_cell.y:
var doubled_error := error * 2
if doubled_error > -delta_y:
error -= delta_y
x += step_x
if doubled_error < delta_x:
error += delta_x
y += step_y
var current := Vector3i(x, y, from_cell.z)
if current == to_cell:
return true
if not world_service.is_walkable(current) \
or not world_service.get_occupant(current).is_empty():
return false
return true
func _authority_path_query(
from_cell: Vector3i,
to_cell: Vector3i,
context: Dictionary
) -> Dictionary:
var connector_access := _name_array(
context.get(&"connector_access", context.get("connector_access", []))
)
var path := path_service.find_path(
from_cell,
to_cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
connector_access
)
if path.is_empty():
return {
&"reachable": false,
&"reason": StringName(path_service.last_error if not path_service.last_error.is_empty() else "PATH_UNREACHABLE"),
&"travel_minutes": 0,
}
return {
&"reachable": true,
&"reason": &"OK",
&"travel_minutes": maxi(path.size() - 1, 0),
&"path": path,
}
func _apply_authority_resolution_boundary(
payload: Dictionary,
phase: StringName,
rollback_token: Variant
) -> Dictionary:
if phase == &"rollback":
if not rollback_token is Dictionary \
or not rollback_token.get(&"economy", null) is Dictionary \
or not rollback_token.get(&"game_state", null) is Dictionary \
or not rollback_token.get(&"facilities", null) is Dictionary \
or not rollback_token.get(&"domain_events", null) is Dictionary \
or not rollback_token.get(&"id_generator", null) is Dictionary:
return {&"ok": false, &"error": "Authority rollback token is malformed."}
var economy_ok: bool = economy_service.restore_snapshot(rollback_token[&"economy"])
var facilities_ok: bool = facility_service.restore_snapshot(rollback_token[&"facilities"])
facilities_ok = facility_cleanliness.restore_snapshot(rollback_token.get(&"facility_cleanliness", {})) and facilities_ok
var events_ok: bool = domain_events.restore_snapshot(rollback_token[&"domain_events"])
var ids_ok: bool = id_generator.restore_snapshot(rollback_token[&"id_generator"])
var state_ok: bool = _state.restore_snapshot(rollback_token[&"game_state"])
return {
&"ok": economy_ok and state_ok and facilities_ok and events_ok and ids_ok,
&"error": "Authority fine rollback failed." if not economy_ok or not state_ok or not facilities_ok or not events_ok or not ids_ok else "",
}
var resolution_id := StringName(payload.get(&"resolution_id", payload.get("resolution_id", "")))
var inspection_id := StringName(payload.get(&"inspection_id", payload.get("inspection_id", "")))
var fine_amount := int(payload.get(&"fine_amount", payload.get("fine_amount", -1)))
var absolute_minute := int(payload.get(&"absolute_minute", payload.get("absolute_minute", -1)))
var idempotency_key := _authority_resolution_idempotency_key(resolution_id)
if resolution_id.is_empty() or inspection_id.is_empty() or fine_amount < 0 \
or absolute_minute != _runtime_event_minute() \
or not economy_service.has_account(PLAYER_CASH_ACCOUNT) \
or not economy_service.has_account(AUTHORITY_FINE_ACCOUNT):
return {&"ok": false, &"error": "Authority resolution payload is invalid."}
if phase == &"validate":
var existing_transaction := economy_service.get_transaction_id_for_idempotency_key(idempotency_key)
if not existing_transaction.is_empty():
if not _authority_fine_transaction_matches(
economy_service, existing_transaction, inspection_id, fine_amount
):
return {&"ok": false, &"error": "Authority fine idempotency key conflicts with its ledger transaction."}
return {&"ok": true, &"token": {
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"already_applied": true,
&"transaction_id": existing_transaction,
}}
if fine_amount > 0 \
and not economy_service.account_allows_negative(PLAYER_CASH_ACCOUNT) \
and economy_service.get_balance(PLAYER_CASH_ACCOUNT) < fine_amount:
return {&"ok": false, &"error": "PLAYER_CASH cannot fund the Authority fine."}
return {&"ok": true, &"token": {
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"already_applied": false,
}}
if phase != &"apply" or not rollback_token is Dictionary:
return {&"ok": false, &"token": rollback_token, &"error": "Unknown Authority resolution phase or token."}
if StringName(payload.get(&"result", &"")) == &"TEMPORARY_CLOSURE":
var targets: Dictionary = {}
for raw_id: Variant in payload.get(&"evidence_ids", []):
var evidence := authority_service.get_evidence(StringName(raw_id))
if evidence != null and evidence.is_discovered() and not evidence.is_expired(absolute_minute) and facility_service.has_instance(evidence.facility_id):
targets[evidence.facility_id] = true
for target_id: StringName in _sorted_name_keys(targets):
var stopped := facility_service.set_operational_status(target_id, {&"suspension_reason": &"RAIDED"}, absolute_minute)
if not stopped.is_success():
return {&"ok": false, &"token": rollback_token, &"error": stopped.message}
if bool(rollback_token.get(&"already_applied", false)) or fine_amount == 0:
return {
&"ok": true,
&"transaction_id": StringName(rollback_token.get(&"transaction_id", &"")),
}
var posted := economy_service.transfer(
PLAYER_CASH_ACCOUNT,
AUTHORITY_FINE_ACCOUNT,
fine_amount,
absolute_minute,
&"AUTHORITY_FINE",
inspection_id,
idempotency_key,
&"authority.inspection.fine"
)
if not posted.is_success():
return {
&"ok": false,
&"token": rollback_token,
&"error": posted.message,
}
return {
&"ok": true,
&"transaction_id": posted.details.get(&"transaction_id", &""),
}
func _authority_resolution_idempotency_key(resolution_id: StringName) -> StringName:
return StringName("authority.resolution.%s" % resolution_id)
func _authority_fine_transaction_matches(
economy: EconomyService,
transaction_id: StringName,
inspection_id: StringName,
fine_amount: int
) -> bool:
if fine_amount == 0:
return transaction_id.is_empty()
var entries := economy.get_transaction_entries(transaction_id)
if entries.size() != 1:
return false
var entry: LedgerEntry = entries[0]
return entry.debit_account == AUTHORITY_FINE_ACCOUNT \
and entry.credit_account == PLAYER_CASH_ACCOUNT \
and entry.amount == fine_amount \
and entry.category == &"AUTHORITY_FINE" \
and entry.source_entity_id == inspection_id
func _roll_defense_round(_key: StringName, _context: Dictionary) -> int:
var stream := get_rng_stream(&"defense")
return stream.randi_range(0, 100) if stream != null else 50
func _apply_defense_rng_boundary(
_context: Dictionary,
phase: StringName,
token: Variant
) -> Dictionary:
var stream := get_rng_stream(&"defense")
if stream == null:
return {&"ok": false, &"error": "Defense RNG stream is unavailable."}
if phase == &"snapshot":
return {&"ok": true, &"token": stream.snapshot()}
if phase == &"commit":
return {&"ok": true, &"token": token}
if phase == &"rollback":
if not token is Dictionary or not stream.restore(token):
return {&"ok": false, &"error": "Defense RNG rollback token is invalid."}
return {&"ok": true}
return {&"ok": false, &"error": "Unknown Defense RNG boundary phase."}
func _apply_defense_skill_boundary(
payload: Dictionary,
phase: StringName,
rollback_token: Variant
) -> Dictionary:
if phase == &"rollback":
return _restore_defense_skill_token(rollback_token)
var cost_report := _parse_defense_skill_cost(payload)
if not bool(cost_report[&"ok"]):
return cost_report
var cost: Dictionary = cost_report[&"cost"]
var raid_id := StringName(payload.get(&"raid_id", payload.get("raid_id", "")))
var action_id := StringName(payload.get(&"action_id", payload.get("action_id", "")))
if raid_id.is_empty() or action_id.is_empty():
return {&"ok": false, &"error": "Defense skill boundary IDs are invalid."}
if phase == &"validate":
var token := {
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"inventory": inventory_service.create_snapshot(),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
&"cost": cost.duplicate(true),
&"reservation_ids": [],
}
var gold := int(cost[&"gold"])
var mana := int(cost[&"mana"])
if economy_service.get_balance(PLAYER_CASH_ACCOUNT) < gold \
or int(_state.get_resource(&"mana")) < mana:
return {&"ok": false, &"error": "Defense skill resources are insufficient."}
var ledger_key := _defense_skill_ledger_key(raid_id, action_id)
if gold > 0 and not economy_service.get_transaction_id_for_idempotency_key(ledger_key).is_empty():
return {&"ok": false, &"error": "Defense skill ledger key already exists without an applied skill."}
var reservation_ids: Array[StringName] = []
var items: Array = cost[&"items"]
for item_index: int in range(items.size()):
var item: Dictionary = items[item_index]
var reserved := inventory_service.reserve_item(
StringName(item[&"inventory_id"]),
StringName(item[&"item_id"]),
int(item[&"quantity"]),
action_id,
&"defense_skill",
_defense_operation_minute(),
-1,
StringName("defense.skill.reserve.%s.%s.%d" % [raid_id, action_id, item_index])
)
if not bool(reserved.get(&"ok", false)):
token[&"reservation_ids"] = reservation_ids
return {
&"ok": false,
&"token": token,
&"error": "Defense skill item reservation failed.",
&"details": reserved,
}
for reservation_value: Variant in reserved.get(&"reservation_ids", []):
reservation_ids.append(StringName(reservation_value))
token[&"reservation_ids"] = reservation_ids
return {&"ok": true, &"token": token}
if phase != &"apply" or not _defense_skill_token_is_valid(rollback_token):
return {&"ok": false, &"token": rollback_token, &"error": "Defense skill apply token is invalid."}
var token: Dictionary = rollback_token
var mana := int(cost[&"mana"])
if mana > 0 and not bool(_state.apply_resource_deltas({&"mana": -mana})):
return {&"ok": false, &"token": token, &"error": "Defense skill Mana application failed."}
var gold := int(cost[&"gold"])
if gold > 0:
var posted := economy_service.transfer(
PLAYER_CASH_ACCOUNT,
OPERATING_EXPENSE_ACCOUNT,
gold,
_defense_operation_minute(),
&"DEFENSE_SKILL_COST",
action_id,
_defense_skill_ledger_key(raid_id, action_id),
&"defense.skill.cost"
)
if not posted.is_success():
return {&"ok": false, &"token": token, &"error": posted.message}
var reservation_ids: Array[StringName] = []
for reservation_value: Variant in token[&"reservation_ids"]:
reservation_ids.append(StringName(reservation_value))
if not reservation_ids.is_empty():
var consumed := inventory_service.consume_reservations(
reservation_ids,
StringName("defense.skill.consume.%s.%s" % [raid_id, action_id]),
_defense_operation_minute()
)
if not bool(consumed.get(&"ok", false)):
return {&"ok": false, &"token": token, &"error": "Defense skill item consumption failed.", &"details": consumed}
return {&"ok": true, &"token": token}
func _parse_defense_skill_cost(payload: Dictionary) -> Dictionary:
var metadata_value: Variant = payload.get(&"metadata", payload.get("metadata", {}))
if not metadata_value is Dictionary:
return {&"ok": false, &"error": "Defense skill metadata must be a Dictionary."}
var metadata: Dictionary = metadata_value
var cost_value: Variant = metadata.get(&"cost", metadata.get("cost", {}))
if not cost_value is Dictionary:
return {&"ok": false, &"error": "Defense skill metadata.cost must be a Dictionary."}
var cost_data: Dictionary = cost_value
var gold_value: Variant = cost_data.get(&"gold", cost_data.get("gold", 0))
var mana_value: Variant = cost_data.get(&"mana", cost_data.get("mana", 0))
if not gold_value is int or not mana_value is int:
return {&"ok": false, &"error": "Defense skill Gold and Mana costs must be integers."}
var gold := int(gold_value)
var mana := int(mana_value)
if gold < 0 or mana < 0:
return {&"ok": false, &"error": "Defense skill costs cannot be negative."}
var items_value: Variant = cost_data.get(&"items", cost_data.get("items", []))
if not items_value is Array:
return {&"ok": false, &"error": "Defense skill item costs must be an Array."}
var items: Array[Dictionary] = []
for raw_item: Variant in items_value:
if not raw_item is Dictionary:
return {&"ok": false, &"error": "Defense skill item cost is malformed."}
var inventory_value: Variant = raw_item.get(&"inventory_id", raw_item.get("inventory_id", ""))
var item_value: Variant = raw_item.get(&"item_id", raw_item.get("item_id", ""))
var quantity_value: Variant = raw_item.get(&"quantity", raw_item.get("quantity", 0))
if not (inventory_value is String or inventory_value is StringName) \
or not (item_value is String or item_value is StringName) \
or not quantity_value is int:
return {&"ok": false, &"error": "Defense skill item cost types are invalid."}
var inventory_id := StringName(inventory_value)
var item_id := StringName(item_value)
var quantity := int(quantity_value)
if inventory_id.is_empty() or item_id.is_empty() or quantity <= 0:
return {&"ok": false, &"error": "Defense skill item cost requires inventory_id, item_id and positive quantity."}
items.append({&"inventory_id": inventory_id, &"item_id": item_id, &"quantity": quantity})
items.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
var left_key := "%s:%s" % [left[&"inventory_id"], left[&"item_id"]]
var right_key := "%s:%s" % [right[&"inventory_id"], right[&"item_id"]]
return left_key < right_key
)
return {&"ok": true, &"cost": {&"gold": gold, &"mana": mana, &"items": items}}
func _defense_skill_ledger_key(raid_id: StringName, action_id: StringName) -> StringName:
return StringName("defense.skill.%s.%s" % [raid_id, action_id])
func _defense_skill_token_is_valid(token: Variant) -> bool:
return token is Dictionary \
and token.get(&"economy", null) is Dictionary \
and token.get(&"game_state", null) is Dictionary \
and token.get(&"inventory", null) is Dictionary \
and token.get(&"domain_events", null) is Dictionary \
and token.get(&"id_generator", null) is Dictionary \
and token.get(&"reservation_ids", null) is Array
func _restore_defense_skill_token(token: Variant) -> Dictionary:
if not _defense_skill_token_is_valid(token):
return {&"ok": false, &"error": "Defense skill rollback token is malformed."}
var economy_ok: bool = economy_service.restore_snapshot(token[&"economy"])
var state_ok: bool = _state.restore_snapshot(token[&"game_state"])
var inventory_ok: bool = inventory_service.restore_snapshot(token[&"inventory"])
var events_ok: bool = domain_events.restore_snapshot(token[&"domain_events"])
var ids_ok: bool = id_generator.restore_snapshot(token[&"id_generator"])
return {
&"ok": economy_ok and state_ok and inventory_ok and events_ok and ids_ok,
&"error": "Defense skill rollback failed." if (
not economy_ok or not state_ok or not inventory_ok or not events_ok or not ids_ok
) else "",
}
func _apply_defense_outcome_boundary(
payload: Dictionary,
phase: StringName,
rollback_token: Variant
) -> Dictionary:
if phase == &"rollback":
return _restore_defense_outcome_token(rollback_token)
var validation := _validate_defense_outcome_payload(payload)
if not bool(validation[&"ok"]):
return validation
if phase == &"validate":
return {&"ok": true, &"token": {
&"staff": staff_service.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
}}
if phase != &"apply" or not _defense_outcome_token_is_valid(rollback_token):
return {&"ok": false, &"token": rollback_token, &"error": "Defense outcome apply token is invalid."}
var token: Dictionary = rollback_token
var combatant_results: Array = payload[&"combatant_results"]
for result_value: Variant in combatant_results:
var result: Dictionary = result_value
if StringName(result[&"side"]) != &"DEFENDER":
continue
var staff := staff_service.get_staff(StringName(result[&"actor_id"]))
staff.health = int(result[&"health"])
staff.fatigue = int(result[&"fatigue"])
var defense_state := StringName(result[&"state"])
if defense_state == &"ACTIVE":
staff.transition_to(StaffRuntime.State.AVAILABLE, &"DEFENSE_RELEASED", _defense_operation_minute())
elif defense_state == &"INCAPACITATED":
staff.transition_to(StaffRuntime.State.INCAPACITATED, &"DEFENSE_INCAPACITATED", _defense_operation_minute())
elif defense_state == &"RETREATED":
staff.transition_to(StaffRuntime.State.FLEEING, &"DEFENSE_RETREATED", _defense_operation_minute())
var asset_results: Array = payload[&"asset_results"]
for result_value: Variant in asset_results:
var result: Dictionary = result_value
var updated := facility_service.set_operational_status(
StringName(result[&"asset_id"]),
{&"durability": int(result[&"integrity"])},
_defense_operation_minute()
)
if not updated.is_success():
return {&"ok": false, &"token": token, &"error": updated.message}
var staff_report := staff_service.validate_invariants()
if not bool(staff_report.get(&"ok", false)):
return {&"ok": false, &"token": token, &"error": "Defense outcome violates Staff invariants.", &"details": staff_report}
return {&"ok": true, &"token": token}
func _validate_defense_outcome_payload(payload: Dictionary) -> Dictionary:
var raid_id := StringName(payload.get(&"raid_id", payload.get("raid_id", "")))
var resolution_id := StringName(payload.get(&"resolution_id", payload.get("resolution_id", "")))
var operation_kind := StringName(payload.get(&"operation_kind", payload.get("operation_kind", "")))
var absolute_minute := int(payload.get(&"absolute_minute", payload.get("absolute_minute", -1)))
var combatants_value: Variant = payload.get(&"combatant_results", payload.get("combatant_results", null))
var assets_value: Variant = payload.get(&"asset_results", payload.get("asset_results", null))
var raid := defense_service.get_raid(raid_id)
if raid_id.is_empty() or resolution_id.is_empty() \
or operation_kind not in DefenseService.VALID_OPERATION_KINDS \
or absolute_minute != _defense_operation_minute() \
or not combatants_value is Array or not assets_value is Array \
or raid.is_empty():
return {&"ok": false, &"error": "Defense outcome payload root is invalid."}
var expected_combatants: Dictionary = raid[&"combatants"]
var expected_assets: Dictionary = raid[&"assets"]
if combatants_value.size() != expected_combatants.size() \
or assets_value.size() != expected_assets.size():
return {&"ok": false, &"error": "Defense outcome does not cover the complete Raid roster."}
var seen_combatants: Dictionary = {}
for value: Variant in combatants_value:
if not value is Dictionary:
return {&"ok": false, &"error": "Defense combatant outcome is malformed."}
var actor_id := StringName(value.get(&"actor_id", value.get("actor_id", "")))
var side := StringName(value.get(&"side", value.get("side", "")))
var state := StringName(value.get(&"state", value.get("state", "")))
var health := int(value.get(&"health", value.get("health", -1)))
var maximum := int(value.get(&"max_health", value.get("max_health", -1)))
var fatigue := int(value.get(&"fatigue", value.get("fatigue", -1)))
if actor_id.is_empty() or seen_combatants.has(actor_id) or not expected_combatants.has(actor_id) \
or side not in [&"DEFENDER", &"ATTACKER"] \
or state not in [&"ACTIVE", &"INCAPACITATED", &"RETREATED"] \
or health < 0 or health > maximum or fatigue < 0 or fatigue > 100 \
or (health == 0 and state == &"ACTIVE"):
return {&"ok": false, &"error": "Defense combatant outcome values are invalid."}
var expected_combatant: Dictionary = expected_combatants[actor_id]
if side != StringName(expected_combatant[&"side"]) \
or maximum != int(expected_combatant[&"max_health"]):
return {&"ok": false, &"error": "Defense combatant outcome differs from the Raid roster."}
seen_combatants[actor_id] = true
if side == &"DEFENDER" and (maximum != StaffRuntime.MAX_PSYCHOLOGY_VALUE or not staff_service.has_staff(actor_id)):
return {&"ok": false, &"error": "Defense outcome references invalid Staff."}
var seen_assets: Dictionary = {}
for value: Variant in assets_value:
if not value is Dictionary:
return {&"ok": false, &"error": "Defense asset outcome is malformed."}
var asset_id := StringName(value.get(&"asset_id", value.get("asset_id", "")))
var integrity := int(value.get(&"integrity", value.get("integrity", -1)))
var maximum := int(value.get(&"max_integrity", value.get("max_integrity", -1)))
if asset_id.is_empty() or seen_assets.has(asset_id) or not expected_assets.has(asset_id) \
or maximum != FacilityInstance.DEFAULT_DURABILITY \
or integrity < 0 or integrity > maximum \
or not facility_service.has_instance(asset_id):
return {&"ok": false, &"error": "Defense outcome references invalid Facility data."}
var expected_asset: Dictionary = expected_assets[asset_id]
if maximum != int(expected_asset[&"max_integrity"]):
return {&"ok": false, &"error": "Defense asset outcome differs from the Raid roster."}
seen_assets[asset_id] = true
return {&"ok": true}
func _defense_outcome_token_is_valid(token: Variant) -> bool:
return token is Dictionary \
and token.get(&"staff", null) is Dictionary \
and token.get(&"facilities", null) is Dictionary \
and token.get(&"domain_events", null) is Dictionary \
and token.get(&"id_generator", null) is Dictionary
func _restore_defense_outcome_token(token: Variant) -> Dictionary:
if not _defense_outcome_token_is_valid(token):
return {&"ok": false, &"error": "Defense outcome rollback token is malformed."}
var staff_ok: bool = staff_service.restore_snapshot(token[&"staff"])
var facilities_ok: bool = facility_service.restore_snapshot(token[&"facilities"])
facilities_ok = facility_cleanliness.restore_snapshot(token.get(&"facility_cleanliness", {})) and facilities_ok
var events_ok: bool = domain_events.restore_snapshot(token[&"domain_events"])
var ids_ok: bool = id_generator.restore_snapshot(token[&"id_generator"])
return {
&"ok": staff_ok and facilities_ok and events_ok and ids_ok,
&"error": "Defense outcome rollback failed." if not staff_ok or not facilities_ok or not events_ok or not ids_ok else "",
}
func _execute_event_action_boundary(
action: EventAction,
phase: StringName,
context: Dictionary,
rollback_token: Variant
) -> Dictionary:
if action.type not in [
EventAction.Type.ADD_LEDGER_ENTRY,
EventAction.Type.ADD_OR_REMOVE_ITEM,
EventAction.Type.CREATE_TASK,
EventAction.Type.CREATE_DEFENSE_RAID,
EventAction.Type.SET_FACILITY_STATE,
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY,
]:
return {&"not_handled": true}
if phase == &"rollback":
return _restore_event_action_token(rollback_token)
var validation := _build_event_defense_request(action, context) if action.type == EventAction.Type.CREATE_DEFENSE_RAID else _validate_event_action(action)
if not bool(validation.get(&"ok", false)) or phase == &"validate":
return validation
var token := _capture_event_action_token()
if action.type == EventAction.Type.CREATE_DEFENSE_RAID:
token[&"defense"] = defense_service.create_snapshot()
var result := _apply_event_action(action, context)
if not bool(result.get(&"ok", false)):
var rollback := _restore_event_action_token(token)
if not bool(rollback.get(&"ok", false)):
result[&"fatal"] = true
result[&"rollback"] = rollback
result[&"error"] = "Event Action failed and rollback failed: %s; %s" % [result.get(&"error", ""), rollback.get(&"error", "")]
return result
result[&"token"] = token
return result
func _validate_event_action(action: EventAction) -> Dictionary:
match action.type:
EventAction.Type.ADD_LEDGER_ENTRY:
var from_account := StringName(action.payload.get(&"from_account", action.payload.get("from_account", PLAYER_CASH_ACCOUNT)))
var to_account := StringName(action.payload.get(&"to_account", action.payload.get("to_account", OPERATING_EXPENSE_ACCOUNT)))
var amount := int(action.payload.get(&"amount", action.payload.get("amount", 0)))
if amount <= 0 or not economy_service.has_account(from_account) or not economy_service.has_account(to_account):
return {&"ok": false, &"error": "Ledger Action has invalid accounts or amount."}
if not economy_service.account_allows_negative(from_account) and economy_service.get_balance(from_account) < amount:
return {&"ok": false, &"error": "Ledger Action source cannot fund the amount."}
EventAction.Type.ADD_OR_REMOVE_ITEM:
var inventory_id := StringName(action.payload.get(&"inventory_id", action.payload.get("inventory_id", "")))
var item_id := StringName(action.payload.get(&"item_id", action.payload.get("item_id", "")))
var delta := int(action.payload.get(&"quantity_delta", action.payload.get("quantity_delta", 0)))
if inventory_id.is_empty() or item_id.is_empty() or delta == 0:
return {&"ok": false, &"error": "Inventory Action requires inventory, item and non-zero delta."}
if delta < 0 and inventory_service.available_quantity(inventory_id, item_id) < -delta:
return {&"ok": false, &"error": "Inventory Action stock is unavailable."}
EventAction.Type.CREATE_TASK:
if StringName(action.payload.get(&"task_type", action.payload.get("task_type", ""))).is_empty():
return {&"ok": false, &"error": "CREATE_TASK requires task_type."}
EventAction.Type.SET_FACILITY_STATE:
var facility_id := StringName(action.payload.get(&"facility_id", action.payload.get("facility_id", "")))
var conditions_value: Variant = action.payload.get(&"conditions", action.payload.get("conditions", null))
if not facility_service.has_instance(facility_id) or not conditions_value is Dictionary:
return {&"ok": false, &"error": "Facility Action target or conditions are invalid."}
var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET")
if not defense_owner.is_empty() \
and (conditions_value.has(&"durability") or conditions_value.has("durability")):
return {&"ok": false, &"error": "Event Action cannot mutate Defense-owned Facility durability."}
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY:
var entity_id := StringName(action.payload.get(&"entity_id", action.payload.get("entity_id", "")))
var cell_value: Variant = _parse_world_cell(
action.payload.get(&"cell", action.payload.get("cell", null))
)
if not _event_actor_exists(entity_id) or cell_value == null:
return {&"ok": false, &"error": "Recovery teleport target is invalid."}
var target_cell: Vector3i = cell_value
var traffic := path_service.get_cell_traffic(target_cell)
var actor_id := (
_guest_path_actor_id(entity_id)
if guest_service.has_guest(entity_id)
else _staff_path_actor_id(entity_id)
)
if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)) \
or not path_service.has_actor_projection(actor_id):
return {&"ok": false, &"error": "Recovery teleport requires a projected actor and an unblocked walkable target."}
return {&"ok": true}
func _apply_event_action(action: EventAction, context: Dictionary) -> Dictionary:
var instance_id := StringName(context.get(&"event_instance_id", ""))
match action.type:
EventAction.Type.CREATE_DEFENSE_RAID:
return _apply_event_defense_raid(action, context)
EventAction.Type.ADD_LEDGER_ENTRY:
var from_account := StringName(action.payload.get(&"from_account", action.payload.get("from_account", PLAYER_CASH_ACCOUNT)))
var to_account := StringName(action.payload.get(&"to_account", action.payload.get("to_account", OPERATING_EXPENSE_ACCOUNT)))
var amount := int(action.payload.get(&"amount", action.payload.get("amount", 0)))
var result := economy_service.transfer(
from_account, to_account, amount, _runtime_event_minute(),
&"EVENT_ACTION", instance_id,
StringName("event.action.%s.%s" % [instance_id, action.action_id])
)
return {&"ok": result.is_success(), &"error": result.message}
EventAction.Type.ADD_OR_REMOVE_ITEM:
var inventory_id := StringName(action.payload.get(&"inventory_id", action.payload.get("inventory_id", "")))
var item_id := StringName(action.payload.get(&"item_id", action.payload.get("item_id", "")))
var delta := int(action.payload.get(&"quantity_delta", action.payload.get("quantity_delta", 0)))
if delta > 0:
var added := inventory_service.add_stack(inventory_id, item_id, delta, 100, -1, _runtime_event_minute())
return {&"ok": bool(added.get(&"ok", false)), &"error": added.get(&"message", "")}
var reserved := inventory_service.reserve_item(
inventory_id, item_id, -delta, instance_id, &"event_action",
_runtime_event_minute(), _runtime_event_minute() + 1,
StringName("event.item.reserve.%s.%s" % [instance_id, action.action_id])
)
if not bool(reserved.get(&"ok", false)):
return {&"ok": false, &"error": reserved.get(&"message", "Item reservation failed.")}
var consumed := inventory_service.consume_reservations(
_name_array(reserved.get(&"reservation_ids", [])),
StringName("event.item.consume.%s.%s" % [instance_id, action.action_id]),
_runtime_event_minute()
)
return {&"ok": bool(consumed.get(&"ok", false)), &"error": consumed.get(&"message", "")}
EventAction.Type.CREATE_TASK:
var raw_options: Variant = action.payload.get(&"options", action.payload.get("options", {}))
var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {}
var created := staff_service.create_task(
StringName(action.payload.get(&"task_type", action.payload.get("task_type", ""))),
int(action.payload.get(&"priority", action.payload.get("priority", 50))),
_runtime_event_minute(),
options,
StringName(action.payload.get(&"task_id", action.payload.get("task_id", "")))
)
return {&"ok": created.is_success(), &"error": created.message}
EventAction.Type.SET_FACILITY_STATE:
var updated := facility_service.set_operational_status(
StringName(action.payload.get(&"facility_id", action.payload.get("facility_id", ""))),
action.payload.get(&"conditions", action.payload.get("conditions", {})),
_runtime_event_minute()
)
return {&"ok": updated.is_success(), &"error": updated.message}
EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY:
var entity_id := StringName(action.payload.get(&"entity_id", action.payload.get("entity_id", "")))
var cell: Vector3i = _parse_world_cell(action.payload.get(&"cell", action.payload.get("cell", null)))
var guest := guest_service.get_guest(entity_id)
var actor_id: StringName
var moved := false
if guest != null:
actor_id = _guest_path_actor_id(entity_id)
moved = guest_service.record_path_success(
entity_id, cell, _runtime_event_minute()
)
else:
var staff := staff_service.get_staff(entity_id)
actor_id = _staff_path_actor_id(entity_id)
staff.current_cell = cell
moved = true
if not moved or not path_service.project_actor_position(
actor_id, cell, _runtime_event_minute()
):
return {&"ok": false, &"error": "Recovery teleport could not commit its Path projection."}
_movement_credits[actor_id] = {
&"credit": 0,
&"updated_at": _runtime_event_minute(),
}
_publish_actor_event(&"ENTITY_RECOVERY_TELEPORTED", &"event_service", [entity_id, actor_id], {
&"cell": cell,
&"path_actor_id": actor_id,
})
return {&"ok": true}
return {&"ok": false, &"error": "Unsupported Event Action."}
func _capture_event_action_token() -> Dictionary:
return {
&"economy": economy_service.create_snapshot(),
&"facility_upkeep": facility_upkeep.create_snapshot(),
&"upkeep_settlement": upkeep_settlement.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"inventory": inventory_service.create_snapshot(),
&"staff": staff_service.create_snapshot(),
&"facilities": facility_service.create_snapshot(),
&"facility_cleanliness": facility_cleanliness.create_snapshot(),
&"guests": guest_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"legacy_stairs": legacy_stairs.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
}
func _restore_event_action_token(token: Variant) -> Dictionary:
if not token is Dictionary:
return {&"ok": false, &"error": "Event rollback token is malformed."}
# Every participant must be asked to restore even if an earlier participant
# reports an error; short-circuiting here would leave a half-rolled-back world.
var economy_ok: bool = economy_service.restore_snapshot(token.get(&"economy", {}))
var state_ok: bool = _state.restore_snapshot(token.get(&"game_state", {}))
var inventory_ok: bool = inventory_service.restore_snapshot(token.get(&"inventory", {}))
var staff_ok: bool = staff_service.restore_snapshot(token.get(&"staff", {}))
var facilities_ok: bool = facility_service.restore_snapshot(token.get(&"facilities", {}))
facilities_ok = facility_cleanliness.restore_snapshot(token.get(&"facility_cleanliness", {})) and facilities_ok
var guests_ok: bool = guest_service.restore_snapshot(token.get(&"guests", {}))
var path_ok: bool = path_service.restore_snapshot(token.get(&"path", {}))
path_ok = elevators.restore_snapshot(token.get(&"elevators", {})) and path_ok
path_ok = legacy_stairs.restore_snapshot(token.get(&"legacy_stairs", {})) and path_ok
var credits_value: Variant = token.get(&"movement_credits", null)
var credits_ok := credits_value is Dictionary
if credits_ok:
_movement_credits = (credits_value as Dictionary).duplicate(true)
var defense_ok := true
if token.has(&"defense"):
# This creates PREPARING incidents only; rollback must not reset another
# raid's pre-existing same-tick automatic/manual attempt guard.
defense_ok = token[&"defense"] is Dictionary and defense_service.restore_snapshot(token[&"defense"])
var events_ok: bool = domain_events.restore_snapshot(token.get(&"domain_events", {}))
var ids_ok: bool = id_generator.restore_snapshot(token.get(&"id_generator", {}))
var ok: bool = economy_ok and state_ok and inventory_ok and staff_ok \
and facilities_ok and guests_ok and path_ok and credits_ok and defense_ok and events_ok and ids_ok
return {&"ok": ok, &"error": "Event Action rollback failed." if not ok else ""}
func _reserve_event_cost(cost: Dictionary, context: Dictionary) -> Dictionary:
var normalized: Dictionary = {}
for raw_resource: Variant in cost:
var resource_id := StringName(raw_resource)
var amount := int(cost[raw_resource])
if resource_id.is_empty() or amount < 0:
return {&"ok": false, &"error": "Event Choice cost is invalid."}
normalized[resource_id] = amount
var economy_before := economy_service.create_snapshot()
var state_before: Dictionary = _state.create_snapshot()
var ids_before := id_generator.create_snapshot()
var instance_id := StringName(context.get(&"event_instance_id", ""))
var reservation_id: StringName = &""
for amount: int in normalized.values():
if amount > 0:
if id_generator.get_counter(&"event_cost") >= StableIdGenerator.MAX_COUNTER:
return {&"ok": false, &"error": "Event cost reservation IDs are exhausted."}
reservation_id = id_generator.next_id(&"event_cost")
break
var escrow_account := StringName("EVENT_ESCROW:%s" % instance_id)
var reservation := {&"event_instance_id": instance_id, &"cost": normalized, &"escrow_account": escrow_account}
if not reservation_id.is_empty():
reservation[&"reservation_id"] = reservation_id
var gold := int(normalized.get(&"gold", 0))
if gold > 0:
if not economy_service.has_account(escrow_account):
economy_service.register_account(escrow_account)
var transfer := economy_service.transfer(
PLAYER_CASH_ACCOUNT, escrow_account, gold, _runtime_event_minute(),
&"EVENT_COST_RESERVED", instance_id,
_event_cost_ledger_key(reservation, &"reserve")
)
if not transfer.is_success():
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
id_generator.restore_snapshot(ids_before)
return {&"ok": false, &"error": transfer.message}
var other_costs := normalized.duplicate(true)
other_costs.erase(&"gold")
if not other_costs.is_empty() and not _state.spend(other_costs):
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
id_generator.restore_snapshot(ids_before)
return {&"ok": false, &"error": "Event Choice resources are unavailable."}
return {&"ok": true, &"token": reservation}
func _release_event_cost(token: Variant, _context: Dictionary) -> Dictionary:
if token is Dictionary and token.is_empty():
return {&"ok": true}
if not token is Dictionary or not token.get(&"cost", null) is Dictionary:
return {&"ok": false, &"error": "Event Cost release token is malformed."}
var cost: Dictionary = token[&"cost"]
var instance_id := StringName(token.get(&"event_instance_id", ""))
var escrow_account := StringName(token.get(&"escrow_account", ""))
var gold := int(cost.get(&"gold", 0))
if gold > 0:
var released := economy_service.transfer(
escrow_account, PLAYER_CASH_ACCOUNT, gold, _runtime_event_minute(),
&"EVENT_COST_RELEASED", instance_id,
_event_cost_ledger_key(token, &"release")
)
if not released.is_success():
return {&"ok": false, &"error": released.message}
for raw_resource: Variant in cost:
var resource_id := StringName(raw_resource)
if resource_id != &"gold":
_state.add_resource(resource_id, int(cost[raw_resource]))
return {&"ok": true}
func _commit_event_cost(token: Variant, _context: Dictionary) -> Dictionary:
if token is Dictionary and token.is_empty():
return {&"ok": true}
if not token is Dictionary or not token.get(&"cost", null) is Dictionary:
return {&"ok": false, &"error": "Event Cost commit token is malformed."}
var cost: Dictionary = token[&"cost"]
var gold := int(cost.get(&"gold", 0))
if gold <= 0:
return {&"ok": true}
var instance_id := StringName(token.get(&"event_instance_id", ""))
var committed := economy_service.transfer(
StringName(token.get(&"escrow_account", "")), OPERATING_EXPENSE_ACCOUNT,
gold, _runtime_event_minute(), &"EVENT_COST_COMMITTED", instance_id,
_event_cost_ledger_key(token, &"commit")
)
return {&"ok": committed.is_success(), &"error": committed.message}
func _event_cost_ledger_key(token: Dictionary, operation: StringName) -> StringName:
var reservation_id := StringName(token.get(&"reservation_id", ""))
if not reservation_id.is_empty():
return StringName("event.reserved_cost.%s.%s" % [operation, reservation_id])
# Existing pending save tokens keep the original ledger operation identity.
return StringName("event.cost.%s.%s" % [operation, token.get(&"event_instance_id", "")])
func _on_relationship_threshold_candidate(candidate: Dictionary) -> void:
var candidate_id := StringName(candidate.get(&"candidate_id", ""))
if candidate_id.is_empty() or event_service.get_instance(candidate_id) != null:
return
event_service.enqueue_candidate(
&"relationship.threshold",
candidate.get(&"actor_ids", []),
int(candidate.get(&"created_at", _runtime_event_minute())),
{
&"event_instance_id": candidate_id,
&"deduplication_key": StringName(candidate.get(&"deduplication_key", candidate_id)),
&"context": {&"relationship_candidate": candidate.duplicate(true)},
}
)
func _route_lock_index(value: Variant) -> int:
if value is int:
return value if value >= 0 and value < RelationshipState.RouteLock.keys().size() else -1
return RelationshipState.RouteLock.keys().find(String(value).to_upper())
func _validate_construction_facility_crosslinks(
world: WorldService,
construction: ConstructionService,
facilities: FacilityService,
projector: FacilityInfrastructureProjector = null
) -> bool:
for construction_facility: Dictionary in construction.get_facilities():
var instance_id := StringName(construction_facility.get(&"instance_id", &""))
var definition_id := StringName(construction_facility.get(&"definition_id", &""))
var definition := definition_registry.get_facility(definition_id)
if instance_id.is_empty() or definition == null:
return false
if _facility_definition_is_schema_proxy(definition):
if facilities.has_instance(instance_id):
return false
if _facility_definition_is_supported_infrastructure_proxy(definition):
if projector == null or not projector.has_projection(instance_id):
return false
elif projector != null and projector.has_projection(instance_id):
return false
for cell: Vector3i in construction_facility.get(&"occupied_cells", []):
if world.get_occupant(cell) != instance_id \
or world.get_occupant_kind(cell) != &"facility":
return false
continue
var operational := facilities.get_instance(instance_id)
if operational == null or (projector != null and projector.has_projection(instance_id)):
return false
if operational.definition_id != StringName(
construction_facility.get(&"definition_id", &"")
) or operational.origin != construction_facility.get(&"origin", Vector3i.ZERO):
return false
var expected_cells: Dictionary = {}
for cell: Vector3i in construction_facility.get(&"occupied_cells", []):
expected_cells[cell] = true
var actual_cells: Dictionary = {}
for cell: Vector3i in operational.occupied_cells:
actual_cells[cell] = true
if expected_cells != actual_cells:
return false
for cell: Vector3i in expected_cells:
if world.get_occupant(cell) != instance_id \
or world.get_occupant_kind(cell) != &"facility":
return false
return true
func _facility_definition_is_schema_proxy(definition: Variant) -> bool:
return definition is FacilityDefinition and &"schema_facility_proxy" in definition.tags
func _facility_definition_is_supported_infrastructure_proxy(definition: Variant) -> bool:
return _facility_definition_is_schema_proxy(definition) \
and definition.id in FacilityInfrastructureProjector.SUPPORTED_KINDS
func _facility_definition_is_unsupported_vertical_proxy(definition: Variant) -> bool:
return _facility_definition_is_schema_proxy(definition) \
and definition.id in FacilityInfrastructureProjector.UNSUPPORTED_VERTICAL_KINDS
func _migrate_legacy_infrastructure_proxies(
construction: ConstructionService,
facilities: FacilityService,
projector: FacilityInfrastructureProjector
) -> bool:
if construction == null or facilities == null or projector == null:
return false
for instance: Dictionary in construction.get_facilities():
var instance_id := StringName(instance.get(&"instance_id", &""))
var definition := definition_registry.get_facility(
StringName(instance.get(&"definition_id", &""))
)
if instance_id.is_empty() or definition == null:
return false
if not _facility_definition_is_schema_proxy(definition):
continue
if facilities.has_instance(instance_id) \
and not facilities.unregister_instance(instance_id).is_success():
return false
if _facility_definition_is_unsupported_vertical_proxy(definition):
continue
if not _facility_definition_is_supported_infrastructure_proxy(definition):
return false
var projected := projector.project_completed_facility(
StringName("runtime.migrate.v6:%s" % instance_id),
definition,
instance
)
if not projected.is_success():
return false
return true
func _validate_infrastructure_crosslinks(
world: WorldService,
path: PathService,
construction: ConstructionService,
_resource_state: Variant,
excavation: ExcavationService,
spatial: SpatialSubslotService,
adapter: RouteSubslotPathAdapter,
projector: FacilityInfrastructureProjector,
require_minute_alignment := true
) -> bool:
if world == null or path == null or construction == null or excavation == null \
or spatial == null or adapter == null or projector == null:
return false
# An active projector validates its bound Spatial and RouteAdapter services
# itself. Reuse that composed check only for these exact dependencies.
# Quarantine intentionally suppresses its live dependency check, so retain
# the independent validations on that path (and for a different binding).
var composed_dependencies := projector.is_bound_to(adapter, spatial, path) and not projector.is_quarantined()
if not adapter.is_bound_to(spatial, path) \
or (not composed_dependencies and (
not bool(spatial.validate_invariants().get(&"ok", false))
or not bool(adapter.validate_invariants().get(&"ok", false)))) \
or not bool(projector.validate_invariants().get(&"ok", false)) \
or not bool(excavation.validate_invariants().get(&"ok", false)) \
or (require_minute_alignment \
and excavation.current_minute != construction.current_minute) \
or not _same_vector3i_set(
adapter.get_base_walkable_cells(false), world.get_walkable_cells(false)
) \
or not _same_vector3i_set(
adapter.get_projected_route_cells(), spatial.get_route_cells()
) \
or not path.matches_passable_cells(adapter.get_projected_walkable_cells()):
return false
var expected_projection_ids: Dictionary = {}
for instance: Dictionary in construction.get_facilities():
var instance_id := StringName(instance.get(&"instance_id", &""))
var definition_id := StringName(instance.get(&"definition_id", &""))
var definition := definition_registry.get_facility(definition_id)
if instance_id.is_empty() or definition == null:
return false
if not _facility_definition_is_schema_proxy(definition):
if projector.has_projection(instance_id):
return false
continue
if _facility_definition_is_unsupported_vertical_proxy(definition):
if projector.has_projection(instance_id):
return false
continue
if not _facility_definition_is_supported_infrastructure_proxy(definition):
return false
expected_projection_ids[instance_id] = true
var projection := projector.get_projection(instance_id)
if projection.is_empty() \
or StringName(projection.get(&"definition_id", &"")) != definition_id \
or StringName(projection.get(&"source_job_id", &"")) \
!= StringName(instance.get(&"source_job_id", &"")) \
or projection.get(&"origin", null) != instance.get(&"origin", null) \
or int(projection.get(&"rotation", -1)) != int(instance.get(&"rotation", -2)) \
or int(projection.get(&"layer", -1)) != int(instance.get(&"layer", -2)) \
or not _same_vector3i_set(
projection.get(&"cells", []), instance.get(&"occupied_cells", [])
):
return false
var actual_projection_ids: Dictionary = {}
for instance_id: StringName in projector.get_projection_ids():
if actual_projection_ids.has(instance_id):
return false
actual_projection_ids[instance_id] = true
return actual_projection_ids == expected_projection_ids
func _same_vector3i_set(first: Variant, second: Variant) -> bool:
if not first is Array or not second is Array or first.size() != second.size():
return false
var expected: Dictionary = {}
for value: Variant in first:
if not value is Vector3i or expected.has(value):
return false
expected[value] = true
for value: Variant in second:
if not value is Vector3i or not expected.has(value):
return false
expected.erase(value)
return expected.is_empty()
func _validate_runtime_crosslinks(
facilities: FacilityService,
guests: GuestService,
economy: EconomyService,
inventory: InventoryService,
staff: StaffService,
absolute_minute: int,
staffing_projection_enabled: bool
) -> bool:
if absolute_minute < 0 \
or facilities.current_minute != absolute_minute \
or guests.current_minute != absolute_minute \
or inventory.current_minute != absolute_minute \
or staff.current_minute != absolute_minute:
return false
for assignment: Dictionary in staff.get_work_assignments():
var facility_id := StringName(assignment[&"facility_id"])
if facility_id.is_empty():
continue
var assigned_facility := facilities.get_instance(facility_id)
if assigned_facility == null:
return false
var assigned_staff := staff.get_staff(StringName(assignment[&"staff_id"]))
var definition := definition_registry.get_facility(assigned_facility.definition_id)
if assigned_staff == null or definition == null or definition.staff_max <= 0 \
or (not _staff_role_matches_facility(assigned_staff.role_id, definition) and not _is_anri_manager(assigned_staff)):
return false
var queued_links: Dictionary = {}
var service_links: Dictionary = {}
for facility: FacilityInstance in facilities.get_instances():
if staffing_projection_enabled:
for staff_id: StringName in facility.active_staff_ids:
var active_staff := staff.get_staff(staff_id)
if active_staff == null \
or active_staff.assigned_facility_id != facility.instance_id \
or (active_staff.state != StaffRuntime.State.AVAILABLE and not CleaningBridge.is_deployed(active_staff, facility, staff)) \
or _is_anri_manager(active_staff):
return false
for request: ServiceRequest in facilities.get_queue(facility.instance_id):
if not _reservation_references_are_valid(
request.reserved_inventory_ids, request.request_id, inventory
):
return false
for guest_id: StringName in request.guest_ids:
if not guests.has_guest(guest_id):
continue
var guest := guests.get_guest(guest_id)
if guest.state != GuestRuntime.State.QUEUING \
or guest.queue_request_id != request.request_id \
or guest.target_facility_id != facility.instance_id:
return false
queued_links[guest_id] = request.request_id
for service: ServiceInstance in facilities.get_active_services(facility.instance_id):
if not _reservation_references_are_valid(
service.inventory_reservations, service.request_id, inventory
):
return false
for guest_id: StringName in service.guest_ids:
if not guests.has_guest(guest_id):
continue
var guest := guests.get_guest(guest_id)
if guest.state != GuestRuntime.State.RECEIVING_SERVICE \
or guest.current_service_id != service.service_id \
or guest.target_facility_id != facility.instance_id:
return false
service_links[guest_id] = service.service_id
for guest: GuestRuntime in guests.get_guests():
if not guest.queue_request_id.is_empty() \
and queued_links.get(guest.guest_id, &"") != guest.queue_request_id:
return false
if not guest.current_service_id.is_empty() \
and service_links.get(guest.guest_id, &"") != guest.current_service_id:
return false
var account_id := _guest_wallet_account(guest.guest_id)
if not economy.has_account(account_id) \
or economy.get_balance(account_id) != guest.wallet_gold:
return false
for task: StaffTask in staff.get_tasks():
if not _reservation_references_are_valid(
task.reservation_ids, task.task_id, inventory
):
return false
return true
func _validate_authority_economy_crosslinks(
authority: AuthorityService,
economy: EconomyService,
absolute_minute: int
) -> bool:
if authority == null or authority.current_minute != absolute_minute \
or not bool(authority.validate_invariants().get(&"ok", false)) \
or not economy.has_account(AUTHORITY_FINE_ACCOUNT):
return false
var authority_snapshot := authority.create_snapshot()
var raw_resolutions: Variant = authority_snapshot.get(&"applied_resolutions", null)
if not raw_resolutions is Array:
return false
var expected_fine_balance := 0
for raw_resolution: Variant in raw_resolutions:
if not raw_resolution is Dictionary \
or not raw_resolution.get(&"payload", null) is Dictionary:
return false
var resolution_id := StringName(raw_resolution.get(&"resolution_id", ""))
var payload: Dictionary = raw_resolution[&"payload"]
var inspection_id := StringName(payload.get(&"inspection_id", ""))
var fine_amount := int(payload.get(&"fine_amount", -1))
if resolution_id.is_empty() or inspection_id.is_empty() or fine_amount < 0:
return false
expected_fine_balance += fine_amount
if fine_amount == 0:
continue
var transaction_id := economy.get_transaction_id_for_idempotency_key(
_authority_resolution_idempotency_key(resolution_id)
)
if transaction_id.is_empty() or not _authority_fine_transaction_matches(
economy,
transaction_id,
inspection_id,
fine_amount
):
return false
return economy.get_balance(AUTHORITY_FINE_ACCOUNT) == expected_fine_balance
func _validate_live_defense_crosslinks() -> bool:
return _validate_defense_crosslinks(
defense_service,
staff_service,
facility_service,
_defense_operation_minute()
)
func _validate_defense_crosslinks(
defense: DefenseService,
staff: StaffService,
facilities: FacilityService,
absolute_minute: int
) -> bool:
if defense == null or staff == null or facilities == null \
or absolute_minute < 0 or defense.current_minute != absolute_minute \
or not bool(defense.validate_invariants().get(&"ok", false)):
return false
var defender_owners: Dictionary = {}
var asset_owners: Dictionary = {}
for raid_id: StringName in defense.get_raid_ids():
var raid := defense.get_raid(raid_id)
if raid.is_empty():
return false
var state := int(raid.get(&"state", -1))
var terminal := state in DefenseService.TERMINAL_STATES
var preparing := state == DefenseService.RaidState.PREPARING
var combatants_value: Variant = raid.get(&"combatants", null)
var assets_value: Variant = raid.get(&"assets", null)
if not combatants_value is Dictionary or not assets_value is Dictionary:
return false
var combatants: Dictionary = combatants_value
for actor_id: StringName in _sorted_name_keys(combatants):
var combatant_value: Variant = combatants[actor_id]
if not combatant_value is Dictionary:
return false
var combatant: Dictionary = combatant_value
if StringName(combatant.get(&"side", &"")) != &"DEFENDER":
continue
if not terminal:
if defender_owners.has(actor_id):
return false
defender_owners[actor_id] = raid_id
if int(combatant.get(&"max_health", -1)) != StaffRuntime.MAX_PSYCHOLOGY_VALUE \
or int(combatant.get(&"initial_health", -1)) != StaffRuntime.MAX_PSYCHOLOGY_VALUE:
return false
# A terminal Raid is historical. Its reconciled Staff may later heal,
# work elsewhere, leave the roster, or enter another Raid; requiring the
# old outcome to remain a permanent live mirror would make future saves
# unrestorable. Capacity baselines remain self-contained above.
if terminal:
continue
var linked_staff := staff.get_staff(actor_id)
if linked_staff == null or linked_staff.state != StaffRuntime.State.UNAVAILABLE \
or not linked_staff.current_task_id.is_empty():
return false
if preparing and (
int(combatant.get(&"health", -1)) != linked_staff.health
or int(combatant.get(&"fatigue", -1)) != linked_staff.fatigue
):
return false
var assets: Dictionary = assets_value
for asset_id: StringName in _sorted_name_keys(assets):
var asset_value: Variant = assets[asset_id]
if not asset_value is Dictionary:
return false
var asset: Dictionary = asset_value
if not terminal:
if asset_owners.has(asset_id):
return false
asset_owners[asset_id] = raid_id
if int(asset.get(&"max_integrity", -1)) != FacilityInstance.DEFAULT_DURABILITY \
or int(asset.get(&"initial_integrity", -1)) != FacilityInstance.DEFAULT_DURABILITY:
return false
# Terminal assets are historical for the same reason as terminal Staff:
# the live Facility can subsequently be repaired, reused, or demolished.
if terminal:
continue
var facility := facilities.get_instance(asset_id)
if facility == null:
return false
if preparing and int(asset.get(&"integrity", -1)) != facility.durability:
return false
return true
func _defense_lock_owner(
external_id: StringName,
lock_kind: StringName,
excluded_raid_id: StringName = &""
) -> StringName:
if defense_service == null or external_id.is_empty() \
or lock_kind not in [&"DEFENDER", &"ASSET"]:
return &""
for raid_id: StringName in defense_service.get_raid_ids():
if raid_id == excluded_raid_id:
continue
var raid := defense_service.get_raid(raid_id)
if raid.is_empty() or int(raid.get(&"state", -1)) in DefenseService.TERMINAL_STATES:
continue
if lock_kind == &"DEFENDER":
var combatants_value: Variant = raid.get(&"combatants", {})
if not combatants_value is Dictionary or not combatants_value.has(external_id):
continue
var combatant_value: Variant = combatants_value[external_id]
if combatant_value is Dictionary \
and StringName(combatant_value.get(&"side", &"")) == &"DEFENDER":
return raid_id
else:
var assets_value: Variant = raid.get(&"assets", {})
if assets_value is Dictionary and assets_value.has(external_id):
return raid_id
return &""
func _reservation_references_are_valid(
reservation_ids: Array[StringName],
requester_id: StringName,
inventory: InventoryService
) -> bool:
for reservation_id: StringName in reservation_ids:
var reservation := inventory.get_reservation(reservation_id)
if reservation == null or reservation.requester_id != requester_id:
return false
return true
## HUD view of the A06 reserve: supply, consumption and storage per interval.
func get_mana_reserve_report() -> Dictionary:
var demand := _mana_reserve_demand()
return {
&"amount": int(_state.get_resource(&"mana")),
&"capacity": ManaReserveCycle.CAPACITY,
&"generation": ManaReserveCycle.WORLD_TREE_GENERATION,
&"interval_minutes": ManaReserveCycle.INTERVAL_MINUTES,
&"demand": demand,
&"net": ManaReserveCycle.WORLD_TREE_GENERATION - demand,
&"last_flow": mana_reserve.get_last_flow(),
}
## A06 upkeep demand: every owned consumer, open or not, except destroyed
## facilities. Removed facilities are already unregistered from the network.
func _mana_reserve_demand() -> int:
var destroyed: Array[StringName] = []
for facility: FacilityInstance in facility_service.get_instances():
if facility.suspension_reason == &"DESTROYED":
destroyed.append(facility.instance_id)
return mana_network_service.get_total_consumer_demand(destroyed)
func _advance_mana(absolute_minute: int) -> void:
if _operations_tick_blocked:
return
var infrastructure_preflight := _preflight_proxy_completions(absolute_minute)
if not bool(infrastructure_preflight.get(&"ok", false)):
_latch_infrastructure_failure(
StringName(infrastructure_preflight.get(
&"code", &"INFRASTRUCTURE_PREFLIGHT_FAILED"
)),
absolute_minute,
String(infrastructure_preflight.get(
&"reason", "Infrastructure completion preflight failed."
)),
infrastructure_preflight.get(&"details", {})
)
return
# A06: generation and upkeep demand settle before local distribution and the
# whole-tower gate, so the network and projection see the committed reserve.
var reserve_flow := mana_reserve.advance_to(
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand()
)
if not bool(reserve_flow.get(&"ok", false)):
_latch_infrastructure_failure(
StringName(reserve_flow.get(&"code", &"MANA_RESERVE_FAILED")),
absolute_minute,
"The finite Mana reserve could not settle this boundary.",
{&"mana": int(_state.get_resource(&"mana"))}
)
return
if bool(reserve_flow.get(&"processed", false)):
_state.set_resource(&"mana", int(reserve_flow[&"after"]))
var report := mana_network_service.advance_one_minute(absolute_minute)
# Finite reserve is an additional gate, even between network evaluations and
# for zero-demand facilities. The network remains the local supply authority.
preload("res://scripts/mana/mana_power_projection.gd").apply(
facility_service, mana_network_service, int(_state.get_resource(&"mana")), absolute_minute
)
var warp_sync := _synchronize_warps()
if not bool(warp_sync.get(&"ok", false)):
_latch_infrastructure_failure(&"WARP_SYNC_FAILED", absolute_minute,
"Warp connectors could not follow the current power state.", warp_sync)
return
for change: Dictionary in report.get(&"state_changes", []):
var consumer_id := StringName(change.get(&"consumer_id", ""))
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"MANA_STATE_CHANGED",
absolute_minute,
&"mana_network_service",
[consumer_id],
change,
0
))
func _on_facility_completed(instance: Dictionary) -> void:
# The staged Runtime boundary has already projected/registered this Facility
# and committed its DomainEvents before Construction releases raw signals.
if _construction_committing_notifications:
return
var instance_id := StringName(instance.get(&"instance_id", &""))
var definition := definition_registry.get_facility(
StringName(instance.get(&"definition_id", &""))
)
if instance_id.is_empty() or definition == null:
_latch_infrastructure_failure(
&"INVALID_COMPLETED_FACILITY",
_runtime_event_minute(),
"Construction emitted an invalid completed Facility.",
{&"instance_id": instance_id}
)
return
if _facility_definition_is_schema_proxy(definition):
if not _facility_definition_is_supported_infrastructure_proxy(definition):
_latch_infrastructure_failure(
&"UNSUPPORTED_VERTICAL_TOPOLOGY",
_runtime_event_minute(),
"An unsupported vertical proxy reached Construction completion.",
{&"instance_id": instance_id, &"definition_id": definition.id}
)
return
var projection_request_id := StringName(
"runtime.complete:%s" % String(instance.get(&"source_job_id", instance_id))
)
var projected := facility_infrastructure_projector.project_completed_facility(
projection_request_id,
definition,
instance
)
if not projected.is_success():
_latch_infrastructure_failure(
&"FACILITY_INFRASTRUCTURE_PROJECTION_FAILED",
_runtime_event_minute(),
projected.message,
projected.details
)
return
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"FACILITY_INFRASTRUCTURE_PROJECTED",
_runtime_event_minute(),
&"facility_infrastructure_projector",
[instance_id],
projected.details.duplicate(true),
0
))
else:
_register_facility_mana_consumer(instance)
_register_operational_facility(instance)
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"FACILITY_COMPLETED",
_runtime_event_minute(),
&"construction_service",
[instance_id],
instance,
0
))
func _on_excavation_completed(report: Dictionary) -> void:
var job_id := StringName(report.get(&"job_id", &""))
if job_id.is_empty():
return
var synchronized := route_subslot_path_adapter.synchronize_projection(
StringName("runtime.excavation:%s" % job_id),
world_service.get_walkable_cells()
)
if not synchronized.is_success():
_latch_infrastructure_failure(
&"EXCAVATION_ROUTE_SYNCHRONIZATION_FAILED",
int(report.get(&"completed_at", _runtime_event_minute())),
synchronized.message,
synchronized.details
)
return
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"EXCAVATION_COMPLETED",
int(report.get(&"completed_at", _runtime_event_minute())),
&"excavation_service",
[job_id],
report.duplicate(true),
0
))
func _on_facility_demolished(instance_id: StringName, refund: Dictionary) -> void:
peeping_room_service.target_removed(instance_id)
if not mana_network_service.get_node(instance_id).is_empty():
mana_network_service.unregister_node(instance_id)
if facility_service.has_instance(instance_id):
facility_service.unregister_instance(instance_id)
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
&"FACILITY_DEMOLISHED",
_runtime_event_minute(),
&"construction_service",
[instance_id],
{&"refund": refund},
0
))
func _on_peeping_target_removed(instance_id: StringName) -> void:
peeping_room_service.target_removed(instance_id)
func _execute_peeping_room(command: SimulationCommand) -> CommandResult:
var payload := command.payload
var report: Dictionary
if command.command_type == &"peeping_room_place":
report = PeepingRooms.result(&"BUILD_DEFINITION_PENDING_D04")
elif command.command_type == &"peeping_room_validate":
if typeof(payload.get(&"origin")) != TYPE_VECTOR3I or typeof(payload.get(&"footprint", Vector2i(2, 1))) != TYPE_VECTOR2I or typeof(payload.get(&"target_instance_id", &"")) not in [TYPE_STRING, TYPE_STRING_NAME]:
report = PeepingRooms.result(&"INVALID_PAYLOAD")
else:
report = peeping_room_service.validate_placement(payload[&"origin"], payload.get(&"footprint", Vector2i(2, 1)), StringName(payload.get(&"target_instance_id", &"")))
else:
var key: StringName = &"target_instance_id" if command.command_type == &"peeping_room_relink" else &"source_event_key"
if typeof(payload.get(&"room_id")) not in [TYPE_STRING, TYPE_STRING_NAME] or typeof(payload.get(key, &"")) not in [TYPE_STRING, TYPE_STRING_NAME]:
report = PeepingRooms.result(&"INVALID_PAYLOAD")
elif command.command_type == &"peeping_room_relink":
report = peeping_room_service.relink(StringName(payload[&"room_id"]), StringName(payload.get(key, &"")))
else:
report = peeping_room_service.record_evidence(StringName(payload[&"room_id"]), StringName(payload.get(key, &"")), _runtime_event_minute())
if report[&"ok"]:
return CommandResult.succeeded(command.command_id, report)
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, String(report[&"reason"]), report)
func _on_ledger_transaction_posted(transaction_id: StringName, _entries: Array) -> void:
if _operations_flushing_economy_notifications.has(transaction_id):
return
if _state != null:
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
func get_mvp_content_report() -> Dictionary:
return mvp_content_report.duplicate(true)
func get_mvp_guest_definition_ids() -> Array[StringName]:
var result: Array[StringName] = []
for definition: GuestDefinition in mvp_content_catalog.build_all_guest_definitions():
result.append(definition.guest_type_id)
return result
func get_mvp_staff_candidate_ids() -> Array[StringName]:
return mvp_content_catalog.get_staff_candidate_ids()
func get_mvp_staff_candidate_payload(candidate_id: StringName) -> Dictionary:
return mvp_content_catalog.staff_hire_payload(candidate_id).duplicate(true)
## Read-only presentation data for the hiring screen. The validated catalog
## remains authoritative and the UI never receives a mutable reference to it.
func get_mvp_staff_candidate_summaries() -> Array[Dictionary]:
var summaries: Array[Dictionary] = []
for candidate_id: StringName in mvp_content_catalog.get_recruitable_staff_ids():
var entry := mvp_content_catalog.get_recruitment_entry(candidate_id)
var payload := mvp_content_catalog.staff_hire_payload(candidate_id)
if entry.is_empty() or payload.is_empty():
continue
var options_value: Variant = payload.get(&"options", {})
var options: Dictionary = options_value if options_value is Dictionary else {}
var skills_value: Variant = options.get(&"skills", {})
var skills: Dictionary = skills_value if skills_value is Dictionary else {}
summaries.append({
&"candidate_id": candidate_id,
&"display_name": String(entry.get(&"display_name", candidate_id)),
&"age": int(payload.get(&"age", 0)),
&"role_id": StringName(payload.get(&"role_id", &"")),
&"daily_wage": int(payload.get(&"daily_wage", 0)),
&"signing_cost": int(payload.get(&"signing_cost", 0)),
&"preferred_shift": StringName(payload.get(&"preferred_shift", &"any")),
&"skills": skills.duplicate(true),
&"hired": _hired_staff_candidate_ids.has(candidate_id),
&"unique": candidate_id == &"heroine_anri",
})
return summaries
## Defensive presentation snapshot for the Staff management panel. Planned
## assignment remains visible at night/off-duty even when the active link clears.
func get_staff_management_report() -> Dictionary:
var rows: Array[Dictionary] = []
for staff: StaffRuntime in staff_service.get_staff_members():
var entry := mvp_content_catalog.get_recruitment_entry(staff.staff_id)
var plan := staff_service.get_work_assignment(staff.staff_id)
var shift := staff_service.get_shift(staff.shift_id)
var template_id: StringName = &"DAY"
var effective_from := _runtime_event_minute()
var next_start_at := -1
var assigned_facility_id := staff.assigned_facility_id
if not plan.is_empty():
template_id = StringName(plan[&"template_id"])
assigned_facility_id = StringName(plan[&"facility_id"])
effective_from = int(plan[&"effective_from"])
next_start_at = int(plan[&"next_start_at"])
elif shift != null:
template_id = shift.template_id
effective_from = shift.start_at
else:
template_id = &"NIGHT" if StringName(entry.get(&"preferred_shift", &"day")) == &"night" else &"DAY"
var block_reason := _staff_assignment_block_reason(staff)
rows.append({
&"staff_id": staff.staff_id, &"display_name": String(entry.get(&"display_name", staff.staff_id)),
&"role_id": staff.role_id, &"state": staff.state, &"state_name": StaffRuntime.State.keys()[staff.state],
&"assigned_facility_id": assigned_facility_id, &"active_facility_id": staff.assigned_facility_id,
&"template_id": template_id, &"recurring": not plan.is_empty() and not assigned_facility_id.is_empty(),
&"effective_from": effective_from, &"next_start_at": next_start_at,
&"daily_wage": staff.get_effective_daily_wage(), &"health": staff.health, &"fatigue": staff.fatigue, &"unpaid_wages": staff.unpaid_wages,
&"current_task_id": staff.current_task_id, &"rescue_status": _staff_rescue_status(staff.staff_id),
&"rest_status": get_staff_rest_report(staff.staff_id).get(&"label", ""),
&"can_edit": block_reason.is_empty(),
&"assignment_block_reason": block_reason,
&"unique_contract": _staff_unique_contract_report(staff),
&"original_relationship": relationship_service.get_original_relationship(staff.staff_id),
})
var facilities: Array[Dictionary] = []
for facility: FacilityInstance in facility_service.get_instances():
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null or definition.staff_max <= 0:
continue
facilities.append({
&"facility_id": facility.instance_id, &"display_name": definition.display_name,
&"definition_id": facility.definition_id, &"layer": facility.origin.z, &"origin": facility.origin,
&"required_roles": definition.required_roles.duplicate(), &"staff_min": definition.staff_min, &"staff_max": definition.staff_max,
})
return {&"staff": rows, &"facilities": facilities}
## Original 6.1 / 6.2 presentation, derived from validated canonical content
## and the same planned placement as pricing. This report owns all containers;
## it does not add a hire, consent, growth policy, save field or tick-side effect.
func _staff_unique_contract_report(staff: StaffRuntime) -> Dictionary:
if staff == null or staff.employment_kind != StaffRuntime.EmploymentKind.UNIQUE or not mvp_content_catalog.is_valid():
return {}
var definition := mvp_content_catalog.get_entry(&"unique_characters", staff.staff_id)
if definition.is_empty() or StringName(definition.get(&"role_id", &"")) != staff.role_id:
return {}
var passive_effects: Array[Dictionary] = []
var contract := {
&"canonical_id": staff.staff_id,
&"free_employment": true,
&"free_upkeep": staff.get_effective_daily_wage() == 0,
&"permanent_removal_forbidden": true,
&"passive_effects": passive_effects,
}
# Only Anri's explicitly specified effect is presented here. Neither kind
# heroine nor a zero wage grants this effect to another employee.
if not _is_anri_manager(staff):
return contract
var effects: Dictionary = definition.get(&"mechanical_effects", {})
if not effects.has(&"assigned_floor_revenue_multiplier_bp"):
return contract
var plan := staff_service.get_work_assignment(staff.staff_id)
var facility_id := StringName(plan.get(&"facility_id", &""))
var facility := facility_service.get_instance(facility_id)
var effect := {
&"effect_id": &"assigned_floor_revenue_multiplier_bp",
&"display_name": "看板娘の微笑",
&"multiplier_bp": int(effects[&"assigned_floor_revenue_multiplier_bp"]),
&"scope": &"ASSIGNED_FLOOR_ALL_LAYERS",
&"applied": facility != null,
&"assigned_facility_id": facility_id,
}
if facility != null:
effect[&"floor_y"] = facility.origin.y
passive_effects.append(effect)
return contract
func get_hired_staff_candidate_ids() -> Array[StringName]:
return _sorted_name_keys(_hired_staff_candidate_ids)
func get_live_progression_report() -> Dictionary:
# Project current cash/facility facts without changing saved progression.
var projected := ProgressionService.new()
var source := progression_service.create_snapshot()
if not projected.restore_snapshot(source):
return {&"snapshot":source, &"evaluation":{&"eligible":false,&"requirements":[],&"reason":&"UNAVAILABLE"}}
var synchronized := projected.update_metrics({
&"asset_gold":economy_service.get_balance(PLAYER_CASH_ACCOUNT),
&"active_above_ground_facilities":_count_active_facilities(facility_service,false),
&"active_underground_facilities":_count_active_facilities(facility_service,true),
})
var evaluation := projected.evaluate_rank()
if not synchronized:
evaluation[&"eligible"] = false
evaluation[&"reason"] = &"UNAVAILABLE"
var gate := _operations_construction_investment_gate(0)
if gate != null:
evaluation[&"eligible"] = false
evaluation[&"reason"] = &"OPERATIONS_BLOCKED"
evaluation[&"block_reason"] = gate.details.get(&"reason", &"")
return {&"snapshot":projected.create_snapshot(), &"evaluation":evaluation}
func get_progression_report(target_rank := -1) -> Dictionary:
return {
&"snapshot": progression_service.create_snapshot(),
&"evaluation": progression_service.evaluate_rank(target_rank).duplicate(true),
}
func _saved_content_manifest_matches(catalog: MvpContentCatalog, data: Dictionary) -> bool:
var expected := _create_mvp_content_manifest(catalog)
var saved: Variant = data.get(&"catalog_manifest", null)
if _strict_variants_equal(expected, saved):
return true
# The sole v1-to-v2 catalog addition is the chapel cleric. Require the
# complete old manifest, not an arbitrary subset of current content.
expected[&"manifest_version"] = 1
expected[&"staff_candidate_ids"].erase("staff_uma")
if not _strict_variants_equal(expected, saved):
return false
var hired: Variant = data.get(&"hired_staff_candidate_ids", null)
var staff_section: Variant = data.get(&"staff", null)
if not hired is Array or "staff_uma" in hired or not staff_section is Dictionary:
return false
var staff_rows: Variant = staff_section.get(&"staff", null)
if not staff_rows is Array:
return false
for staff: Variant in staff_rows:
if not staff is Dictionary or String(staff.get(&"staff_id", "")) == "staff_uma":
return false
return true
func _clear_mvp_content_manifest_cache() -> void:
_mvp_manifest_cache_catalog = null
_mvp_manifest_cache_generation = -1
_mvp_manifest_cache = {}
func _create_mvp_content_manifest(catalog: MvpContentCatalog) -> Dictionary:
# Restore validates a temporary catalog through the full original builder.
# A rejected restore must not replace or invalidate the live Runtime cache.
if catalog != mvp_content_catalog:
return _build_mvp_content_manifest(catalog)
var generation := catalog.get_manifest_generation()
if _mvp_manifest_cache_catalog == catalog \
and _mvp_manifest_cache_generation == generation:
return _mvp_manifest_cache.duplicate(true)
var manifest := _build_mvp_content_manifest(catalog)
_mvp_manifest_cache_catalog = catalog
_mvp_manifest_cache_generation = generation
_mvp_manifest_cache = manifest
# Legacy v1 matching edits its returned manifest, including a nested array.
# The private template must stay owned even on the first cache fill.
return _mvp_manifest_cache.duplicate(true)
func _build_mvp_content_manifest(catalog: MvpContentCatalog) -> Dictionary:
var guest_definition_ids: Array[String] = []
for definition: GuestDefinition in catalog.build_all_guest_definitions():
guest_definition_ids.append(String(definition.guest_type_id))
return {
&"manifest_version": MVP_CONTENT_MANIFEST_VERSION,
&"schema_version": MvpContentCatalog.SCHEMA_VERSION,
&"guest_class_ids": _names_to_strings_for_manifest(catalog.get_guest_class_ids()),
&"species_ids": _names_to_strings_for_manifest(catalog.get_species_ids()),
&"staff_candidate_ids": _names_to_strings_for_manifest(catalog.get_staff_candidate_ids()),
&"unique_character_ids": _names_to_strings_for_manifest(catalog.get_unique_character_ids()),
&"guest_definition_ids": guest_definition_ids,
}
func _names_to_strings_for_manifest(values: Array[StringName]) -> Array[String]:
var result: Array[String] = []
for value: StringName in values:
result.append(String(value))
return result
func _catalog_definitions_are_registered(service: GuestService) -> bool:
if not bool(mvp_content_report.get(&"valid", false)):
return false
return _saved_catalog_guest_definitions_match(
service.create_snapshot(),
mvp_content_catalog
)
func _saved_catalog_guest_definitions_match(
guest_snapshot: Dictionary,
catalog: MvpContentCatalog
) -> bool:
var raw_definitions: Variant = guest_snapshot.get(&"definitions", null)
if not raw_definitions is Array:
return false
var saved_by_id: Dictionary = {}
for raw_definition: Variant in raw_definitions:
if not raw_definition is Dictionary:
return false
var raw_id: Variant = raw_definition.get(&"guest_type_id", null)
if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return false
var definition_id := StringName(raw_id)
if definition_id.is_empty() or saved_by_id.has(definition_id):
return false
saved_by_id[definition_id] = raw_definition
var expected_definitions := catalog.build_all_guest_definitions()
if expected_definitions.size() != 32:
return false
for definition: GuestDefinition in expected_definitions:
if not saved_by_id.has(definition.guest_type_id) \
or not _strict_variants_equal(
definition.create_snapshot(),
saved_by_id[definition.guest_type_id]
):
return false
return true
func _register_or_validate_legacy_catalog_definitions(
service: GuestService,
catalog: MvpContentCatalog
) -> bool:
var definitions := catalog.build_all_guest_definitions()
if definitions.size() != 32:
return false
for definition: GuestDefinition in definitions:
var existing := service.get_definition(definition.guest_type_id)
if existing == null:
if not service.register_definition(definition):
return false
elif not _strict_variants_equal(
existing.create_snapshot(),
definition.create_snapshot()
):
return false
return true
func _serialize_hired_staff_candidate_ids() -> Array[String]:
var result: Array[String] = []
for candidate_id: StringName in _sorted_name_keys(_hired_staff_candidate_ids):
result.append(String(candidate_id))
return result
func _is_mvp_staff_candidate_id(staff_id: StringName) -> bool:
return mvp_content_catalog.get_staff_candidate_ids().has(staff_id)
func _deserialize_hired_staff_candidate_ids(
raw_ids: Array,
catalog: MvpContentCatalog
) -> Variant:
var allowed: Dictionary = {}
for candidate_id: StringName in catalog.get_recruitable_staff_ids():
allowed[candidate_id] = true
var result: Dictionary = {}
var previous := ""
for raw_id: Variant in raw_ids:
if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]:
return null
var candidate_id := StringName(raw_id)
var candidate_text := String(candidate_id)
if not allowed.has(candidate_id) or result.has(candidate_id) \
or (not previous.is_empty() and candidate_text <= previous):
return null
result[candidate_id] = true
previous = candidate_text
return result
func _validate_catalog_staff_crosslinks(
staff: StaffService,
hired_ids: Dictionary,
catalog: MvpContentCatalog
) -> bool:
if hired_ids.has(&"heroine_anri") and not staff.has_staff(&"heroine_anri"):
return false
var candidate_ids: Dictionary = {}
for candidate_id: StringName in catalog.get_recruitable_staff_ids():
candidate_ids[candidate_id] = true
for member: StaffRuntime in staff.get_staff_members():
if not candidate_ids.has(member.staff_id):
continue
if not hired_ids.has(member.staff_id):
return false
var canonical := catalog.staff_hire_payload(member.staff_id)
if canonical.is_empty() \
or member.role_id != StringName(canonical[&"role_id"]) \
or member.daily_wage != int(canonical[&"daily_wage"]) \
or member.get_employment_kind_name() != StringName(canonical[&"options"][&"employment_kind"]) \
or not _strict_variants_equal(
member.permitted_task_tags,
(canonical[&"options"] as Dictionary)[&"permitted_task_tags"]
) \
or not _strict_variants_equal(
member.skills,
(canonical[&"options"] as Dictionary)[&"skills"]
):
return false
return true
func _derive_legacy_hired_staff_candidate_ids(
staff: StaffService,
staff_snapshot: Dictionary,
catalog: MvpContentCatalog
) -> Dictionary:
var allowed: Dictionary = {}
for candidate_id: StringName in catalog.get_recruitable_staff_ids():
allowed[candidate_id] = true
var result: Dictionary = {}
for member: StaffRuntime in staff.get_staff_members():
if allowed.has(member.staff_id):
result[member.staff_id] = true
var raw_departed: Variant = staff_snapshot.get(&"departed_payroll", [])
if raw_departed is Array:
for raw_record: Variant in raw_departed:
if raw_record is Dictionary:
var candidate_id := StringName(raw_record.get(&"staff_id", ""))
if allowed.has(candidate_id):
result[candidate_id] = true
return result
func _strict_variants_equal(expected: Variant, actual: Variant) -> bool:
if typeof(expected) != typeof(actual):
return false
if expected is Dictionary:
var expected_dictionary := expected as Dictionary
var actual_dictionary := actual as Dictionary
if expected_dictionary.size() != actual_dictionary.size():
return false
var unmatched_actual_keys: Array = actual_dictionary.keys()
for expected_key: Variant in expected_dictionary:
var matched_index := -1
for index in range(unmatched_actual_keys.size()):
var actual_key: Variant = unmatched_actual_keys[index]
if typeof(actual_key) == typeof(expected_key) and actual_key == expected_key:
matched_index = index
break
if matched_index < 0:
return false
var matched_key: Variant = unmatched_actual_keys[matched_index]
if not _strict_variants_equal(
expected_dictionary[expected_key],
actual_dictionary[matched_key]
):
return false
unmatched_actual_keys.remove_at(matched_index)
return unmatched_actual_keys.is_empty()
if expected is Array:
var expected_array := expected as Array
var actual_array := actual as Array
if expected_array.size() != actual_array.size():
return false
for index in range(expected_array.size()):
if not _strict_variants_equal(expected_array[index], actual_array[index]):
return false
return true
return expected == actual
func _operations_snapshot_comparison_value(value: Variant) -> Variant:
if typeof(value) == TYPE_STRING_NAME:
return String(value)
if value is Array:
var normalized_array: Array = []
for member: Variant in value:
normalized_array.append(_operations_snapshot_comparison_value(member))
return normalized_array
if value is Dictionary:
var normalized_dictionary: Dictionary = {}
for raw_key: Variant in value:
var normalized_key: Variant = raw_key
if typeof(raw_key) == TYPE_STRING_NAME:
normalized_key = String(raw_key)
if normalized_dictionary.has(normalized_key):
# A String/StringName alias collision is not a canonical snapshot.
return null
normalized_dictionary[normalized_key] = _operations_snapshot_comparison_value(
value[raw_key]
)
return normalized_dictionary
return value
func get_actor_movement_credit(actor_id: StringName) -> Dictionary:
if not _movement_credits.has(actor_id):
return {}
return (_movement_credits[actor_id] as Dictionary).duplicate(true)
func _guest_path_actor_id(guest_id: StringName) -> StringName:
return StringName("guest:%s" % guest_id)
func _staff_path_actor_id(staff_id: StringName) -> StringName:
return StringName("staff:%s" % staff_id)
func _expected_runtime_actor_cells(
guests: GuestService,
staff: StaffService
) -> Dictionary:
var expected: Dictionary = {}
for guest: GuestRuntime in guests.get_guests():
var guest_actor_id := _guest_path_actor_id(guest.guest_id)
if guest_actor_id.is_empty() or expected.has(guest_actor_id):
return {}
expected[guest_actor_id] = guest.current_cell
for member: StaffRuntime in staff.get_staff_members():
var staff_actor_id := _staff_path_actor_id(member.staff_id)
if staff_actor_id.is_empty() or expected.has(staff_actor_id):
return {}
expected[staff_actor_id] = member.current_cell
return expected
func _serialize_movement_credits() -> Array[Dictionary]:
var saved: Array[Dictionary] = []
for actor_id: StringName in _sorted_name_keys(_movement_credits):
var entry: Dictionary = _movement_credits[actor_id]
saved.append({
&"actor_id": String(actor_id),
&"credit": int(entry[&"credit"]),
&"updated_at": int(entry[&"updated_at"]),
})
return saved
func _deserialize_movement_credits(raw_entries: Array, absolute_minute: int) -> Variant:
var restored: Dictionary = {}
for raw_entry: Variant in raw_entries:
if not raw_entry is Dictionary:
return null
var actor_id_value: Variant = raw_entry.get(&"actor_id", null)
var credit_value: Variant = raw_entry.get(&"credit", null)
var updated_at_value: Variant = raw_entry.get(&"updated_at", null)
if not (actor_id_value is String or actor_id_value is StringName) \
or not credit_value is int or not updated_at_value is int:
return null
var actor_id := StringName(actor_id_value)
var credit := int(credit_value)
var updated_at := int(updated_at_value)
if actor_id.is_empty() or restored.has(actor_id) \
or credit < 0 or credit >= STAIR_MOVEMENT_CREDIT \
or updated_at < 0 or updated_at > absolute_minute:
return null
restored[actor_id] = {
&"credit": credit,
&"updated_at": updated_at,
}
return restored
func _rebuild_legacy_path_actor_projection(
path: PathService,
guests: GuestService,
staff: StaffService,
absolute_minute: int
) -> Variant:
var expected := _expected_runtime_actor_cells(guests, staff)
var entries: Array[Dictionary] = []
var progress_entries: Array[Dictionary] = []
var expected_density: Dictionary = {}
var rebuilt: Dictionary = {}
for actor_id: StringName in _sorted_name_keys(expected):
var cell: Vector3i = expected[actor_id]
if not path.has_cell(cell):
return null
entries.append({&"actor_id": actor_id, &"cell": cell})
progress_entries.append({
&"actor_id": actor_id,
&"last_cell": cell,
&"last_progress_at": absolute_minute,
&"last_observed_at": absolute_minute,
&"stuck_reported_at": -1,
})
expected_density[cell] = int(expected_density.get(cell, 0)) + 1
rebuilt[actor_id] = {&"credit": 0, &"updated_at": absolute_minute}
var migrated_path := path.create_snapshot()
migrated_path[&"actor_cells"] = entries
migrated_path[&"actor_progress"] = progress_entries
migrated_path[&"traffic_revision"] = int(migrated_path[&"traffic_revision"]) + 1
for traffic: Dictionary in migrated_path[&"cell_traffic"]:
traffic[&"current_density"] = int(expected_density.get(traffic[&"cell"], 0))
if not path.restore_snapshot(migrated_path):
return null
return rebuilt
func _validate_runtime_path_crosslinks(
path: PathService,
guests: GuestService,
staff: StaffService,
credits: Dictionary,
absolute_minute: int
) -> bool:
if path == null or guests == null or staff == null or absolute_minute < 0:
return false
var expected := _expected_runtime_actor_cells(guests, staff)
if expected.size() != guests.guest_count() + staff.get_staff_members().size() \
or credits.size() != expected.size():
return false
var path_snapshot := path.create_snapshot()
var raw_actor_cells: Variant = path_snapshot.get(&"actor_cells", null)
var raw_progress: Variant = path_snapshot.get(&"actor_progress", null)
var raw_traffic: Variant = path_snapshot.get(&"cell_traffic", null)
if not raw_actor_cells is Array or not raw_progress is Array or not raw_traffic is Array:
return false
var projected: Dictionary = {}
for raw_actor: Variant in raw_actor_cells:
if not raw_actor is Dictionary:
return false
var actor_id := StringName(raw_actor.get(&"actor_id", &""))
var cell: Variant = raw_actor.get(&"cell", null)
if actor_id.is_empty() or not cell is Vector3i or projected.has(actor_id):
return false
projected[actor_id] = cell
if projected.size() != expected.size():
return false
var progress_by_actor: Dictionary = {}
for raw_entry: Variant in raw_progress:
if not raw_entry is Dictionary:
return false
var actor_id := StringName(raw_entry.get(&"actor_id", &""))
if actor_id.is_empty() or progress_by_actor.has(actor_id):
return false
progress_by_actor[actor_id] = raw_entry
if progress_by_actor.size() != expected.size():
return false
var expected_density: Dictionary = {}
for actor_id: StringName in _sorted_name_keys(expected):
var cell: Vector3i = expected[actor_id]
if projected.get(actor_id, null) != cell or not credits.has(actor_id):
return false
var credit_entry: Variant = credits[actor_id]
var progress_entry: Variant = progress_by_actor.get(actor_id, null)
if not credit_entry is Dictionary or not progress_entry is Dictionary:
return false
var credit_value: Variant = credit_entry.get(&"credit", null)
var credit_updated_value: Variant = credit_entry.get(&"updated_at", null)
var last_progress_value: Variant = progress_entry.get(&"last_progress_at", null)
var last_observed_value: Variant = progress_entry.get(&"last_observed_at", null)
var stuck_reported_value: Variant = progress_entry.get(&"stuck_reported_at", null)
if not credit_value is int or not credit_updated_value is int \
or not last_progress_value is int or not last_observed_value is int \
or not stuck_reported_value is int:
return false
var credit := int(credit_value)
var updated_at := int(credit_updated_value)
var last_progress_at := int(last_progress_value)
var last_observed_at := int(last_observed_value)
var stuck_reported_at := int(stuck_reported_value)
if credit < 0 or credit >= _cell_movement_credit_limit(path, cell) \
or updated_at < 0 or updated_at > absolute_minute \
or progress_entry.get(&"last_cell", null) != cell \
or last_progress_at < 0 or last_progress_at > last_observed_at \
or last_observed_at != updated_at \
or last_observed_at > absolute_minute \
or (stuck_reported_at != -1 and (
stuck_reported_at < last_progress_at or stuck_reported_at > last_observed_at
)):
return false
if not path.is_actor_in_transit(actor_id):
expected_density[cell] = int(expected_density.get(cell, 0)) + 1
for raw_cell: Variant in raw_traffic:
if not raw_cell is Dictionary:
return false
var cell: Variant = raw_cell.get(&"cell", null)
var density_value: Variant = raw_cell.get(&"current_density", null)
if not cell is Vector3i or not density_value is int \
or int(density_value) != int(expected_density.get(cell, 0)):
return false
return true
func _path_snapshot_cells_match_world(
path_snapshot: Dictionary,
walkable_cells: Array[Vector3i]
) -> bool:
var schema_value: Variant = path_snapshot.get(
&"schema_version",
path_snapshot.get("schema_version", null)
)
if not schema_value is int or int(schema_value) not in [PathService.ELEVATOR_INDEX_SNAPSHOT_VERSION, PathService.SHAFT_SNAPSHOT_VERSION, PathService.SNAPSHOT_VERSION]:
return false
var raw_traffic: Variant = path_snapshot.get(
&"cell_traffic",
path_snapshot.get("cell_traffic", null)
)
if not raw_traffic is Array or raw_traffic.size() != walkable_cells.size():
return false
var expected_cells: Dictionary = {}
for cell: Vector3i in walkable_cells:
if expected_cells.has(cell):
return false
expected_cells[cell] = true
var actual_cells: Dictionary = {}
for raw_entry: Variant in raw_traffic:
if not raw_entry is Dictionary:
return false
var cell_value: Variant = raw_entry.get(&"cell", raw_entry.get("cell", null))
if not cell_value is Vector3i \
or actual_cells.has(cell_value) \
or not expected_cells.has(cell_value):
return false
actual_cells[cell_value] = true
return actual_cells.size() == expected_cells.size()
func _serialize_logistics_requests() -> Array[Dictionary]:
var saved: Array[Dictionary] = []
for request_id: StringName in _sorted_name_keys(logistics_requests):
saved.append((logistics_requests[request_id] as LogisticsRequest).create_snapshot())
return saved
func _derive_legacy_logistics_ownership(
dispatcher: LogisticsDispatcher,
requests: Dictionary,
absolute_minute: int
) -> Dictionary:
var latest_history: Dictionary = {}
for assignment: Dictionary in dispatcher.get_assignment_history():
var request_id := StringName(assignment.get(&"request_id", &""))
var assigned_at := int(assignment.get(&"assigned_at", -1))
if request_id.is_empty() or assigned_at < 0 or assigned_at > absolute_minute:
return {}
latest_history[request_id] = assignment
var expected: Dictionary = {}
for request_id: StringName in _sorted_name_keys(requests):
var request: LogisticsRequest = requests[request_id]
if request.state not in [
LogisticsRequest.State.ASSIGNED,
LogisticsRequest.State.PICKING_UP,
LogisticsRequest.State.DELIVERING,
]:
continue
if request.assigned_carrier_id.is_empty() or request.total_weight <= 0 \
or not latest_history.has(request_id):
return {}
var latest: Dictionary = latest_history[request_id]
if StringName(latest.get(&"carrier_id", &"")) != request.assigned_carrier_id \
or int(latest.get(&"weight", -1)) != request.total_weight:
return {}
expected[request_id] = {
&"carrier_id": request.assigned_carrier_id,
&"weight": request.total_weight,
&"assigned_at": int(latest[&"assigned_at"]),
}
return expected
func _validate_logistics_dispatcher_crosslinks(
dispatcher: LogisticsDispatcher,
requests: Dictionary,
absolute_minute: int
) -> bool:
if dispatcher == null or absolute_minute < 0:
return false
var ownership := dispatcher.get_assignment_ownership()
var expected_ids: Dictionary = {}
for request_id: StringName in _sorted_name_keys(requests):
var request: LogisticsRequest = requests[request_id]
if request.created_at < 0 or request.state_changed_at < request.created_at \
or request.state_changed_at > absolute_minute:
return false
var active := request.state in [
LogisticsRequest.State.ASSIGNED,
LogisticsRequest.State.PICKING_UP,
LogisticsRequest.State.DELIVERING,
]
if not active:
if not request.assigned_carrier_id.is_empty():
return false
continue
if request.assigned_carrier_id.is_empty() or request.total_weight <= 0:
return false
expected_ids[request_id] = true
var entry: Variant = ownership.get(request_id, null)
if not entry is Dictionary \
or StringName(entry.get(&"carrier_id", &"")) != request.assigned_carrier_id \
or int(entry.get(&"weight", -1)) != request.total_weight:
return false
var assigned_at := int(entry.get(&"assigned_at", -1))
if assigned_at < request.created_at or assigned_at > request.state_changed_at:
return false
if ownership.size() != expected_ids.size():
return false
for request_id: StringName in _sorted_name_keys(ownership):
if not expected_ids.has(request_id):
return false
var latest_history: Dictionary = {}
var history_fingerprints: Dictionary = {}
for assignment: Dictionary in dispatcher.get_assignment_history():
var request_id := StringName(assignment.get(&"request_id", &""))
if request_id.is_empty() or not requests.has(request_id):
return false
var request: LogisticsRequest = requests[request_id]
var carrier_id := StringName(assignment.get(&"carrier_id", &""))
var weight := int(assignment.get(&"weight", -1))
var assigned_at := int(assignment.get(&"assigned_at", -1))
var fingerprint := "%s|%s|%d|%d" % [request_id, carrier_id, weight, assigned_at]
if carrier_id.is_empty() or weight != request.total_weight \
or assigned_at < request.created_at or assigned_at > absolute_minute \
or history_fingerprints.has(fingerprint):
return false
history_fingerprints[fingerprint] = true
latest_history[request_id] = assignment
for request_id: StringName in _sorted_name_keys(ownership):
if not latest_history.has(request_id):
return false
var entry: Dictionary = ownership[request_id]
var latest: Dictionary = latest_history[request_id]
if StringName(latest[&"carrier_id"]) != StringName(entry[&"carrier_id"]) \
or int(latest[&"weight"]) != int(entry[&"weight"]) \
or int(latest[&"assigned_at"]) != int(entry[&"assigned_at"]):
return false
return true
func _serialize_event_definitions() -> Array[Dictionary]:
var saved: Array[Dictionary] = []
for definition: EventDefinition in event_service.get_definitions():
saved.append(definition.create_snapshot())
return saved
func _deserialize_logistics_requests(raw_requests: Array) -> Variant:
var restored: Dictionary = {}
for raw_request: Variant in raw_requests:
if not raw_request is Dictionary:
return null
var request := LogisticsRequest.from_snapshot(raw_request)
if request == null or restored.has(request.request_id):
return null
restored[request.request_id] = request
return restored
func _sorted_logistics_requests() -> Array:
var requests: Array = []
for request_id: StringName in _sorted_name_keys(logistics_requests):
requests.append(logistics_requests[request_id])
return requests
func _sorted_name_keys(source: Dictionary) -> Array[StringName]:
var keys: Array[StringName] = []
for raw_key: Variant in source:
keys.append(StringName(raw_key))
keys.sort_custom(func(first: StringName, second: StringName) -> bool:
return String(first) < String(second)
)
return keys
func _name_array(source: Variant) -> Array[StringName]:
var result: Array[StringName] = []
if not source is Array:
return result
for value: Variant in source:
result.append(StringName(value))
return result
func _guest_wallet_account(guest_id: StringName) -> StringName:
return StringName("GUEST_WALLET:%s" % guest_id)
func _outstanding_service_commitment(guest_id: StringName) -> int:
var total := 0
var counted_requests: Dictionary = {}
for facility: FacilityInstance in facility_service.get_instances():
for request: ServiceRequest in facility_service.get_queue(facility.instance_id):
if not request.guest_ids.is_empty() and request.guest_ids[0] == guest_id:
total += request.quoted_price
counted_requests[request.request_id] = true
for service: ServiceInstance in facility_service.get_active_services(facility.instance_id):
if counted_requests.has(service.request_id):
continue
var request := facility_service.get_request(service.request_id)
if request != null and not request.guest_ids.is_empty() \
and request.guest_ids[0] == guest_id:
total += request.quoted_price
counted_requests[request.request_id] = true
return total
func _facility_revenue_account(facility_id: StringName) -> StringName:
return StringName("FACILITY_REVENUE:%s" % facility_id)
func _command_result_from_dictionary(
command_id: int,
result: Dictionary,
fallback_message: String
) -> CommandResult:
if bool(result.get(&"ok", result.get("ok", false))):
return CommandResult.succeeded(command_id, result.duplicate(true))
return CommandResult.failed(
CommandResult.Code.VALIDATION_FAILED,
command_id,
String(result.get(&"message", result.get("message", fallback_message))),
result.duplicate(true)
)
func _register_facility_mana_consumer(instance: Dictionary) -> void:
var instance_id := StringName(instance.get(&"instance_id", &""))
var definition_id := StringName(instance.get(&"definition_id", &""))
var definition := definition_registry.get_facility(definition_id)
if instance_id.is_empty() or definition == null or definition.maintenance_mana <= 0:
return
var priority := ManaNetworkService.ConsumerPriority.NORMAL
if &"entrance" in definition.tags or &"medical" in definition.tags:
priority = ManaNetworkService.ConsumerPriority.ESSENTIAL
if mana_network_service.register_consumer(instance_id, definition.maintenance_mana, priority):
mana_network_service.connect_nodes(instance_id, STARTER_MANA_BUS)
func _register_operational_facility(construction_instance: Dictionary) -> void:
var instance_id := StringName(construction_instance.get(&"instance_id", &""))
var definition_id := StringName(construction_instance.get(&"definition_id", &""))
var definition := definition_registry.get_facility(definition_id)
if instance_id.is_empty() or definition == null or facility_service.has_instance(instance_id):
return
economy_service.register_account(_facility_revenue_account(instance_id))
facility_service.create_instance({
&"instance_id": instance_id,
&"definition_id": definition_id,
&"origin": construction_instance.get(&"origin", Vector3i.ZERO),
&"rotation": construction_instance.get(&"rotation", 0),
&"occupied_cells": construction_instance.get(&"occupied_cells", []),
&"local_inventory_id": StringName("facility.inventory:%s" % instance_id),
&"power_available": int(_state.get_resource(&"mana")) > 0,
&"customer_capacity": definition.customer_capacity,
&"service_attribute": definition.service_attribute,
&"capacity": maxi(definition.capacity, 1),
&"queue_capacity": definition.queue_capacity,
&"base_service_minutes": definition.base_service_minutes,
&"base_quality": definition.base_quality,
&"base_price": definition.base_price,
&"staff_min": definition.staff_min,
&"staff_max": definition.staff_max,
&"created_at": int(construction_instance.get(&"completed_at", _clock.absolute_minute)),
})
func _validate_service_start(request: ServiceRequest, _facility: FacilityInstance, absolute_minute: int) -> Dictionary:
if int(_state.get_resource(&"mana")) == 0:
return {&"ok": false, &"message": "The tower has no stored Mana."}
for guest_id: StringName in request.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest == null:
continue
if guest.state != GuestRuntime.State.QUEUING \
or guest.queue_request_id != request.request_id \
or guest.target_facility_id != request.facility_id:
return {&"ok": false, &"message": "Guest and Facility queue ownership do not match."}
if guest.transition_tick == absolute_minute \
and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK:
return {&"ok": false, &"message": "Guest transition budget is exhausted for this minute."}
for reservation_id: StringName in request.reserved_inventory_ids:
var reservation := inventory_service.get_reservation(reservation_id)
if reservation == null or not reservation.is_active() or reservation.is_expired(absolute_minute) \
or inventory_service.is_stack_expired(reservation.stack_id, absolute_minute):
return {&"ok": false, &"message": "A required inventory reservation is unavailable or expired."}
var payer_account := _guest_wallet_account(request.guest_ids[0])
if not economy_service.has_account(payer_account) \
or economy_service.get_balance(payer_account) < request.quoted_price:
return {&"ok": false, &"message": "The guest cannot pay the quoted service price."}
return {&"ok": true}
func _complete_service_transaction(
service: ServiceInstance,
request: ServiceRequest,
facility: FacilityInstance,
absolute_minute: int
) -> Variant:
# A sale is only observable after its inventory, rewards and cleanliness
# effects commit. Rollback removes provisional receipts before scope exit.
economy_service.begin_notification_scope()
var result: Variant = _apply_service_transaction(service, request, facility, absolute_minute)
if result is bool and result:
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
economy_service.end_notification_scope()
return result
func _apply_service_transaction(
service: ServiceInstance,
request: ServiceRequest,
facility: FacilityInstance,
absolute_minute: int
) -> Variant:
for guest_id: StringName in service.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest == null:
continue
if guest.state != GuestRuntime.State.RECEIVING_SERVICE \
or guest.current_service_id != service.service_id \
or guest.target_facility_id != facility.instance_id:
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED, 0,
"Guest and Facility service ownership do not match."
)
if guest.transition_tick == absolute_minute \
and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK:
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED, 0,
"Guest transition budget is exhausted for this minute."
)
var inventory_before := inventory_service.create_snapshot()
var economy_before := economy_service.create_snapshot()
var state_before: Dictionary = _state.create_snapshot()
if not service.inventory_reservations.is_empty():
var consumed := inventory_service.consume_reservations(
service.inventory_reservations,
StringName("service.consume.%s" % service.service_id),
absolute_minute
)
if not bool(consumed.get(&"ok", false)):
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Reserved service inventory could not be consumed.", consumed)
if service.price_snapshot > 0:
var payer_account := _guest_wallet_account(request.guest_ids[0])
var revenue_account := _facility_revenue_account(facility.instance_id)
# Only a registered definition with a hidden-service tag routes the sale to the
# hidden fund. A facility whose definition cannot be read keeps the existing
# front-fund behaviour (disclosed in execution/p05-02/REPAIR_02.md).
var sale_definition := definition_registry.get_facility(facility.definition_id)
var sale_fund := FinancePolicy.BACK_CASH \
if sale_definition != null and FinancePolicy.is_back_fund_service(sale_definition.tags) else PLAYER_CASH_ACCOUNT
if not economy_service.has_account(payer_account) or not economy_service.has_account(revenue_account):
inventory_service.restore_snapshot(inventory_before)
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "A service settlement account is missing.")
var sale_entries: Array = [
LedgerEntry.new().configure(
&"", absolute_minute, revenue_account, payer_account,
service.price_snapshot, &"SERVICE_SALE", facility.instance_id
),
LedgerEntry.new().configure(
&"", absolute_minute, sale_fund, revenue_account,
service.price_snapshot, &"SERVICE_CASH_SWEEP", facility.instance_id
),
]
var payment := economy_service.post(
sale_entries,
StringName("service.payment.%s" % service.service_id)
)
if not payment.is_success():
inventory_service.restore_snapshot(inventory_before)
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
return payment
var definition := definition_registry.get_facility(facility.definition_id)
if definition != null:
for item_id: StringName in _sorted_name_keys(definition.inventory_outputs):
var output := inventory_service.add_stack(
facility.local_inventory_id,
item_id,
int(definition.inventory_outputs[item_id]),
service.quality_snapshot,
-1,
absolute_minute
)
if not bool(output.get(&"ok", false)):
inventory_service.restore_snapshot(inventory_before)
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Facility outputs could not be committed.", output)
var progression_before: Dictionary = {}
if definition != null and definition.faith_per_service > 0:
progression_before = progression_service.create_snapshot()
var next_faith := int(progression_service.metrics[&"faith_lifetime"]) + definition.faith_per_service
if not progression_service.update_metrics({&"faith_lifetime":next_faith}) or not _state.apply_resource_deltas({&"faith":definition.faith_per_service}):
inventory_service.restore_snapshot(inventory_before)
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
progression_service.restore_snapshot(progression_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Service Faith reward could not be committed.")
if definition != null and definition.cleanliness_decay_per_service > 0:
if not facility_cleanliness.record_use(facility.instance_id, service.service_id, definition.cleanliness_decay_per_service):
inventory_service.restore_snapshot(inventory_before)
economy_service.restore_snapshot(economy_before)
_state.restore_snapshot(state_before)
if not progression_before.is_empty(): progression_service.restore_snapshot(progression_before)
return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Facility cleanliness could not be committed.")
return true
func _on_facility_state_changed(instance_id: StringName, old_state: int, new_state: int, reasons: Array[StringName]) -> void:
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"), &"FACILITY_STATE_CHANGED", _runtime_event_minute(),
&"facility_service", [instance_id],
{&"old_state": old_state, &"state": new_state, &"reasons": reasons}, 0
))
func _on_service_started(service: ServiceInstance) -> void:
var effective_minute := _runtime_event_minute()
for guest_id: StringName in service.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest == null:
continue
var wallet_account := _guest_wallet_account(guest_id)
if economy_service.has_account(wallet_account):
guest.wallet_gold = economy_service.get_balance(wallet_account)
var synchronized := guest_service.begin_service(
guest_id, service.facility_id, service.service_id, effective_minute
)
if not bool(synchronized.get(&"ok", false)):
_publish_actor_event(
&"GUEST_SERVICE_SYNC_FAILED", &"simulation_runtime",
[guest_id, service.service_id], synchronized
)
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"), &"SERVICE_STARTED", effective_minute,
&"facility_service", [service.facility_id, service.service_id], service.create_snapshot(), 0
))
func _on_service_interrupted(service: ServiceInstance, reason: StringName) -> void:
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"), &"SERVICE_INTERRUPTED", _runtime_event_minute(),
&"facility_service", [service.facility_id, service.service_id],
{&"reason": reason, &"service": service.create_snapshot()}, 0
))
func _on_service_completed(service: ServiceInstance) -> void:
var effective_minute := _runtime_event_minute()
for guest_id: StringName in service.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest == null:
continue
var wallet_account := _guest_wallet_account(guest_id)
if economy_service.has_account(wallet_account):
guest.wallet_gold = economy_service.get_balance(wallet_account)
var synchronized := guest_service.complete_service(
guest_id,
service.service_id,
effective_minute,
_service_satisfaction_sample(service, guest),
_service_need_effects(service.facility_id)
)
if not bool(synchronized.get(&"ok", false)):
_publish_actor_event(
&"GUEST_SERVICE_SYNC_FAILED", &"simulation_runtime",
[guest_id, service.service_id], synchronized
)
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"), &"SERVICE_COMPLETED", effective_minute,
&"facility_service", [service.facility_id, service.service_id], service.create_snapshot(), 0
))
func _on_service_failed(service: ServiceInstance, reason: StringName) -> void:
var effective_minute := _runtime_event_minute()
for reservation_id: StringName in service.inventory_reservations:
inventory_service.release_reservation(
reservation_id,
StringName("service.failed.%s.%s" % [service.service_id, reservation_id]),
effective_minute
)
for guest_id: StringName in service.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest != null and guest.current_service_id == service.service_id:
guest_service.interrupt_service(
guest_id, service.service_id, effective_minute
)
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"), &"SERVICE_FAILED", effective_minute,
&"facility_service", [service.facility_id, service.service_id],
{&"reason": reason, &"service": service.create_snapshot()}, 0
))
func _on_guest_spawned(guest: GuestRuntime) -> void:
if _operations_flushing_guest_spawn_notifications.has(guest.guest_id):
return
var actor_id := _guest_path_actor_id(guest.guest_id)
var projected := path_service.actor_enter_cell(
actor_id,
guest.current_cell,
_runtime_event_minute()
)
if projected:
_movement_credits[actor_id] = {
&"credit": 0,
&"updated_at": _runtime_event_minute(),
}
var payload := guest.create_snapshot()
payload[&"path_actor_id"] = actor_id
payload[&"path_projected"] = projected
payload[&"path_error"] = path_service.last_error if not projected else ""
_publish_actor_event(&"GUEST_SPAWNED", &"guest_service", [guest.guest_id], payload)
func _on_guest_state_changed(
guest_id: StringName,
old_state: int,
new_state: int,
reason: StringName
) -> void:
_publish_actor_event(&"GUEST_STATE_CHANGED", &"guest_service", [guest_id], {
&"old_state": old_state,
&"state": new_state,
&"reason": reason,
})
func _on_guest_queue_abandoned(
guest_id: StringName,
facility_id: StringName,
request_id: StringName
) -> void:
_publish_actor_event(
&"GUEST_QUEUE_ABANDONED", &"guest_service",
[guest_id, facility_id, request_id], {}
)
func _on_guest_recovery_started(
guest_id: StringName,
recovery_kind: StringName,
target_cell: Vector3i
) -> void:
_publish_actor_event(&"GUEST_RECOVERY_STARTED", &"guest_service", [guest_id], {
&"recovery_kind": recovery_kind,
&"target_cell": target_cell,
})
func _on_guest_despawned(guest_id: StringName, persistent_record: Dictionary) -> void:
var actor_id := _guest_path_actor_id(guest_id)
elevators.cancel_actor(actor_id)
var path_removed := path_service.despawn_actor(actor_id)
_movement_credits.erase(actor_id)
_synchronize_operations_population()
var payload := persistent_record.duplicate(true)
payload[&"path_actor_id"] = actor_id
payload[&"path_projection_removed"] = path_removed
_publish_actor_event(&"GUEST_DESPAWNED", &"guest_service", [guest_id], payload)
func _on_staff_registered(staff: StaffRuntime) -> void:
_staffing_projection_enabled = true
var actor_id := _staff_path_actor_id(staff.staff_id)
var projected := path_service.actor_enter_cell(
actor_id,
staff.current_cell,
_runtime_event_minute()
)
if projected:
_movement_credits[actor_id] = {
&"credit": 0,
&"updated_at": _runtime_event_minute(),
}
var payload := staff.create_snapshot()
payload[&"path_actor_id"] = actor_id
payload[&"path_projected"] = projected
payload[&"path_error"] = path_service.last_error if not projected else ""
_publish_actor_event(&"STAFF_REGISTERED", &"staff_service", [staff.staff_id], payload)
func _on_staff_removed(staff_id: StringName, reason: StringName) -> void:
var actor_id := _staff_path_actor_id(staff_id)
elevators.cancel_actor(actor_id)
var path_removed := path_service.despawn_actor(actor_id)
_movement_credits.erase(actor_id)
_publish_actor_event(&"STAFF_REMOVED", &"staff_service", [staff_id], {
&"reason": reason,
&"path_actor_id": actor_id,
&"path_projection_removed": path_removed,
})
func _on_shift_registered(shift: ShiftInstance) -> void:
_publish_actor_event(
&"STAFF_SHIFT_REGISTERED", &"staff_service",
[shift.staff_id, shift.shift_instance_id], shift.create_snapshot()
)
func _on_shift_state_changed(staff_id: StringName, shift_id: StringName, state: int) -> void:
_publish_actor_event(
&"STAFF_SHIFT_STATE_CHANGED", &"staff_service",
[staff_id, shift_id], {&"state": state}
)
func _on_staff_task_registered(task: StaffTask) -> void:
_publish_actor_event(&"STAFF_TASK_REGISTERED", &"staff_service", [task.task_id], task.create_snapshot())
func _on_staff_task_assigned(task_id: StringName, staff_id: StringName) -> void:
var task := staff_service.get_task(task_id)
if task != null and task.state == StaffTask.State.RESERVED:
task.begin_moving(_runtime_event_minute())
_publish_actor_event(&"STAFF_TASK_ASSIGNED", &"staff_service", [task_id, staff_id], {})
func _on_staff_task_reopened(task_id: StringName, reason: StringName) -> void:
_publish_actor_event(&"STAFF_TASK_REOPENED", &"staff_service", [task_id], {&"reason": reason})
func _on_staff_task_completed(task_id: StringName, staff_id: StringName) -> void:
var task := staff_service.get_task(task_id)
if task != null and task.task_type == CleaningBridge.TASK_TYPE:
if not cleaning_bridge.completed(task, staff_service.get_staff(staff_id)):
_latch_infrastructure_failure(&"CLEANING_COMPLETION_FAILED", _runtime_event_minute(), "Cleaning task effect could not be committed.")
if task != null and task.task_type == &"rescue_transport":
var patient := staff_service.get_staff(task.target_entity_id)
var clinic := facility_service.get_instance(task.rescue_facility_id)
if patient != null and clinic != null and patient.current_cell == task.target_cell and clinic.operation_state == FacilityInstance.OperationState.OPERATING:
patient.health = 100
patient.fatigue = 0
patient.transition_to(StaffRuntime.State.OFF_DUTY, &"RESCUE_TREATED", _runtime_event_minute())
_publish_actor_event(&"STAFF_RESCUED", &"staff_service", [patient.staff_id, staff_id, clinic.instance_id], {})
_publish_actor_event(&"STAFF_TASK_COMPLETED", &"staff_service", [task_id, staff_id], {})
func _on_authority_heat_changed(
old_value: int,
new_value: int,
source_id: StringName
) -> void:
var entity_ids: Array[StringName] = []
if not source_id.is_empty():
entity_ids.append(source_id)
_publish_actor_event(&"AUTHORITY_HEAT_CHANGED", &"authority_service", entity_ids, {
&"old_value": old_value,
&"new_value": new_value,
&"source_id": source_id,
})
func _on_authority_incident_created(incident: AuthorityIncident) -> void:
_publish_actor_event(
&"INCIDENT_CREATED",
&"authority_service",
[incident.incident_id],
incident.create_snapshot()
)
func _on_authority_incident_detector_added(
incident_id: StringName,
detector_id: StringName
) -> void:
var incident := authority_service.get_incident(incident_id)
_publish_actor_event(
&"INCIDENT_DETECTOR_ADDED",
&"authority_service",
[incident_id, detector_id],
{&"incident": incident.create_snapshot() if incident != null else {}}
)
func _on_authority_evidence_registered(evidence: AuthorityEvidence) -> void:
_publish_actor_event(
&"AUTHORITY_EVIDENCE_REGISTERED",
&"authority_service",
[evidence.evidence_id, evidence.incident_id, evidence.facility_id],
evidence.create_snapshot()
)
func _on_authority_evidence_discovered(
evidence: AuthorityEvidence,
method: StringName
) -> void:
_publish_actor_event(
&"AUTHORITY_EVIDENCE_DISCOVERED",
&"authority_service",
[evidence.evidence_id, evidence.incident_id, evidence.facility_id],
{
&"method": method,
&"evidence": evidence.create_snapshot(),
&"authority_evidence_points": authority_service.get_authority_state().evidence_points,
}
)
func _on_authority_inspection_scheduled(inspection: AuthorityInspection) -> void:
_publish_actor_event(
&"INSPECTION_SCHEDULED",
&"authority_service",
[inspection.inspection_id, inspection.inspector_id],
inspection.create_snapshot()
)
func _on_authority_inspection_state_changed(
inspection_id: StringName,
old_state: int,
new_state: int,
reason: StringName
) -> void:
var inspection := authority_service.get_inspection(inspection_id)
var entity_ids: Array[StringName] = [inspection_id]
if inspection != null:
entity_ids.append(inspection.inspector_id)
var payload := {
&"old_state": StringName(AuthorityInspection.State.keys()[old_state]),
&"new_state": StringName(AuthorityInspection.State.keys()[new_state]),
&"reason": reason,
}
_publish_actor_event(
&"INSPECTION_STATE_CHANGED",
&"authority_service",
entity_ids,
payload
)
if new_state == AuthorityInspection.State.ARRIVING:
_publish_actor_event(
&"INSPECTION_STARTED",
&"authority_service",
entity_ids,
payload
)
func _on_authority_resolution_applied(
inspection_id: StringName,
resolution: Dictionary
) -> void:
_publish_actor_event(
&"INSPECTION_RESOLVED",
&"authority_service",
[inspection_id, StringName(resolution.get(&"resolution_id", ""))],
resolution.duplicate(true)
)
func _on_defense_raid_created(raid_id: StringName) -> void:
_publish_actor_event(
&"DEFENSE_RAID_CREATED",
&"defense_service",
[raid_id],
defense_service.get_raid(raid_id)
)
func _on_defense_raid_state_changed(
raid_id: StringName,
old_state: int,
new_state: int,
reason: StringName
) -> void:
_publish_actor_event(
&"DEFENSE_RAID_STATE_CHANGED",
&"defense_service",
[raid_id],
{
&"old_state": StringName(DefenseService.RaidState.keys()[old_state]),
&"new_state": StringName(DefenseService.RaidState.keys()[new_state]),
&"reason": reason,
}
)
func _on_defense_active_skill_applied(
raid_id: StringName,
action_id: StringName,
round_number: int
) -> void:
_publish_actor_event(
&"DEFENSE_SKILL_APPLIED",
&"defense_service",
[raid_id, action_id],
{&"round_number": round_number}
)
func _on_defense_round_resolved(raid_id: StringName, resolution: Dictionary) -> void:
var entity_ids: Array[StringName] = [raid_id]
var resolution_id := StringName(resolution.get(&"resolution_id", ""))
var wave_id := StringName(resolution.get(&"wave_id", ""))
if not resolution_id.is_empty():
entity_ids.append(resolution_id)
if not wave_id.is_empty():
entity_ids.append(wave_id)
_publish_actor_event(
&"DEFENSE_ROUND_RESOLVED",
&"defense_service",
entity_ids,
resolution.duplicate(true)
)
func _on_defense_raid_resolved(raid_id: StringName, outcome: Dictionary) -> void:
var entity_ids: Array[StringName] = [raid_id]
var resolution_id := StringName(outcome.get(&"resolution_id", ""))
if not resolution_id.is_empty():
entity_ids.append(resolution_id)
_publish_actor_event(
&"DEFENSE_RAID_RESOLVED",
&"defense_service",
entity_ids,
outcome.duplicate(true)
)
func _publish_authority_evaluation(details: Dictionary) -> void:
var entity_ids: Array[StringName] = []
var inspection_id := StringName(details.get(&"inspection_id", ""))
if not inspection_id.is_empty():
entity_ids.append(inspection_id)
_publish_actor_event(
&"AUTHORITY_INSPECTION_EVALUATED",
&"authority_service",
entity_ids,
details.duplicate(true)
)
func _on_relationship_registered(state: RelationshipState) -> void:
_publish_actor_event(
&"RELATIONSHIP_REGISTERED",
&"relationship_service",
[state.actor_a_id, state.actor_b_id, state.relationship_id],
state.create_snapshot()
)
func _on_relationship_value_changed(
relationship_id: StringName,
metric_id: StringName,
old_value: int,
new_value: int
) -> void:
var state := relationship_service.get_relationship(relationship_id)
var actor_ids: Array[StringName] = [relationship_id]
if state != null:
actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id]
_publish_actor_event(&"RELATIONSHIP_VALUE_CHANGED", &"relationship_service", actor_ids, {
&"metric_id": metric_id,
&"old_value": old_value,
&"new_value": new_value,
})
func _on_relationship_shield_changed(
relationship_id: StringName,
old_value: int,
new_value: int
) -> void:
var state := relationship_service.get_relationship(relationship_id)
var actor_ids: Array[StringName] = [relationship_id]
if state != null:
actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id]
_publish_actor_event(&"RELATIONSHIP_SHIELD_CHANGED", &"relationship_service", actor_ids, {
&"old_value": old_value,
&"new_value": new_value,
})
func _on_relationship_route_lock_changed(
relationship_id: StringName,
old_lock: int,
new_lock: int,
route_id: StringName
) -> void:
var state := relationship_service.get_relationship(relationship_id)
var actor_ids: Array[StringName] = [relationship_id]
if state != null:
actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id]
_publish_actor_event(&"RELATIONSHIP_ROUTE_LOCK_CHANGED", &"relationship_service", actor_ids, {
&"old_lock": old_lock,
&"new_lock": new_lock,
&"route_id": route_id,
})
func _on_event_effect_transaction_started() -> void:
economy_service.begin_notification_scope()
func _on_event_effect_transaction_finished() -> void:
# A nested action batch ends before the enclosing choice cost commits.
# Keep the authoritative mirror current without releasing observer receipts.
if economy_service.notification_scope_depth() == 1:
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
economy_service.end_notification_scope()
func _on_event_enqueued(instance: EventInstance) -> void:
_publish_actor_event(
&"EVENT_ENQUEUED", &"event_service", instance.actor_ids,
instance.create_snapshot()
)
func _on_event_started(instance: EventInstance) -> void:
_publish_actor_event(
&"EVENT_STARTED", &"event_service", instance.actor_ids,
instance.create_snapshot()
)
func _on_event_finished(instance: EventInstance) -> void:
_resolve_relationship_threshold_event(instance)
_publish_actor_event(
&"EVENT_FINISHED", &"event_service", instance.actor_ids,
instance.create_snapshot()
)
func _on_event_cancelled(instance: EventInstance, reason: StringName) -> void:
_consume_relationship_candidate_from_event(instance)
# Placement rejection occurs before an Event starts and never owns a pause.
if reason != &"PLACEMENT_PROTECTION" and event_service.get_foreground_instance() == null and not event_service.pause_requested:
_clock.set_paused(false)
_publish_actor_event(&"EVENT_CANCELLED", &"event_service", instance.actor_ids, {
&"reason": reason,
&"event": instance.create_snapshot(),
})
func _on_event_choice_resolved(
instance_id: StringName,
choice_id: StringName,
result: Dictionary
) -> void:
var instance := event_service.get_instance(instance_id)
var actor_ids: Array[StringName] = []
if instance != null:
actor_ids = instance.actor_ids.duplicate()
_publish_actor_event(&"EVENT_CHOICE_RESOLVED", &"event_service", actor_ids, {
&"event_instance_id": instance_id,
&"choice_id": choice_id,
&"result": result.duplicate(true),
})
func _resolve_relationship_threshold_event(instance: EventInstance) -> void:
if instance.source_event_id != &"relationship.threshold":
return
var raw_candidate: Variant = instance.context.get(&"relationship_candidate", null)
if not raw_candidate is Dictionary:
return
var candidate: Dictionary = raw_candidate
if instance.selected_choice_id == &"acknowledge":
var relationship_id := StringName(candidate.get(&"relationship_id", ""))
var metric_id := StringName(candidate.get(&"metric_id", ""))
var state := relationship_service.get_relationship(relationship_id)
if state != null and RelationshipState.METRIC_IDS.has(metric_id):
var threshold := int(candidate.get(&"threshold", state.get_metric(metric_id)))
var delta := maxi(threshold - state.get_metric(metric_id), 0)
if delta > 0:
relationship_service.apply_value_change(
relationship_id,
metric_id,
delta,
_runtime_event_minute(),
{&"important": true, &"allow_threshold_crossing": true}
)
_consume_relationship_candidate_from_event(instance)
func _consume_relationship_candidate_from_event(instance: EventInstance) -> void:
var raw_candidate: Variant = instance.context.get(&"relationship_candidate", null)
if not raw_candidate is Dictionary:
return
var candidate_id := StringName(raw_candidate.get(&"candidate_id", ""))
if not candidate_id.is_empty():
relationship_service.consume_threshold_candidate(candidate_id)
func _publish_actor_event(
event_type: StringName,
source_system: StringName,
entity_ids: Array[StringName],
payload: Dictionary
) -> void:
_publish_domain_event(SimulationDomainEvent.new(
id_generator.next_id(&"domain_event"),
event_type,
_runtime_event_minute(),
source_system,
entity_ids,
payload,
0
))
func _service_need_effects(facility_id: StringName) -> Dictionary:
var effects := {
&"hunger": 0, &"sleep": 0, &"hygiene": 0,
&"medical": 0, &"leisure": 0, &"intimacy": 0,
}
var facility := facility_service.get_instance(facility_id)
if facility == null:
return effects
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null:
return effects
if &"dining" in definition.tags:
effects[&"hunger"] = -70
effects[&"leisure"] = -10
if &"lodging" in definition.tags:
effects[&"sleep"] = -80
effects[&"hygiene"] = mini(int(effects[&"hygiene"]), -10)
if &"social" in definition.tags or &"leisure" in definition.tags:
effects[&"leisure"] = mini(int(effects[&"leisure"]), -40)
if &"hygiene" in definition.tags or &"bath" in definition.tags:
effects[&"hygiene"] = -75
if &"medical" in definition.tags:
effects[&"medical"] = -85
elif &"wellness" in definition.tags:
effects[&"medical"] = -20
if &"adult_only" in definition.tags or &"restricted_service" in definition.tags:
effects[&"intimacy"] = -75
return effects
func _parse_world_cell(value: Variant) -> Variant:
if value is Vector3i:
return value
if value is Array and value.size() == 3:
return Vector3i(int(value[0]), int(value[1]), int(value[2]))
return null
func _guest_state_index(value: Variant) -> int:
if value is int:
return value if value >= 0 and value < GuestRuntime.State.size() else -1
var normalized := String(value).to_upper()
return GuestRuntime.State.keys().find(normalized)
func _publish_snapshot(_absolute_minute: int) -> void:
latest_snapshot = create_snapshot()
_latest_digest_dirty = true
if not snapshot_published.get_connections().is_empty():
snapshot_published.emit(latest_snapshot, latest_digest)
state_updated.emit(latest_snapshot)
func _execute_purchase_supplies(command: SimulationCommand) -> CommandResult:
return supply_management.purchase(self, command)
func _execute_set_supply_auto_purchase(command: SimulationCommand) -> CommandResult:
return supply_management.set_auto_purchase(self, command)
func _execute_set_kitchen_production(command: SimulationCommand) -> CommandResult:
return supply_management.set_production(self, command)
func get_supply_management_report() -> Dictionary:
return supply_management.get_report(self)
func preview_supply_purchase(warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary:
return supply_management.preview_purchase(self, warehouse_id, item_id, quantity)
# Existing saves retain each facility's stored base rate; reception never quotes
# a new direct sale. Already queued/in-service requests retain their old quote.
func _is_anri_manager(staff: StaffRuntime) -> bool:
return staff.staff_id == &"heroine_anri" and staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE and staff.role_id == &"innkeeper"
func get_floor_price_multiplier(floor_y: int) -> int:
var anri := staff_service.get_staff(&"heroine_anri")
if anri == null or not _is_anri_manager(anri): return 10000
var assignment := staff_service.get_work_assignment(anri.staff_id)
var facility := facility_service.get_instance(StringName(assignment.get(&"facility_id", &"")))
if facility == null or facility.origin.y != floor_y: return 10000
var definition := mvp_content_catalog.get_entry(&"unique_characters", anri.staff_id)
return int(definition.get(&"mechanical_effects", {}).get(&"assigned_floor_revenue_multiplier_bp", 10000))
## Original 6.2 / 6.4: Amelia's assigned chapel receives triple donations.
## This is a placement passive; attendance does not remove it. Existing service
## quotes remain frozen by ServiceRequest/ServiceInstance as for Anri.
func get_donation_multiplier(facility: FacilityInstance) -> int:
if facility.definition_id != &"chapel": return 1
var amelia := staff_service.get_staff(&"heroine_amelia")
if amelia == null or amelia.employment_kind != StaffRuntime.EmploymentKind.UNIQUE or amelia.role_id != &"cleric": return 1
var assignment := staff_service.get_work_assignment(amelia.staff_id)
return 3 if StringName(assignment.get(&"facility_id", &"")) == facility.instance_id else 1
func get_facility_business_price(facility: FacilityInstance) -> int:
var donation := FacilityPricing.quote(_business_base_price(facility), facility.policy_current) * get_donation_multiplier(facility)
return FacilityPricing.apply_multiplier(donation, get_floor_price_multiplier(facility.origin.y))
## Owned presentation data; accepted request/service quotations stay frozen.
## Matching-surcharge charging remains pending until its inputs are specified.
func get_facility_service_pricing_readmodel(facility: FacilityInstance) -> Dictionary:
return FacilityPricing.pending_service_readmodel(
get_facility_business_price(facility), facility.service_attribute
)
func _business_base_price(facility: FacilityInstance) -> int:
return 0 if facility.definition_id == &"reception" else facility.base_price
func get_business_management_report() -> Dictionary:
var minute: int = _clock.absolute_minute
var start := (minute / SimulationKernel.MINUTES_PER_DAY) * SimulationKernel.MINUTES_PER_DAY
var totals: Dictionary = {}
for entry: LedgerEntry in economy_service.get_entries():
if entry.absolute_minute < start or entry.absolute_minute > minute:
continue
if not totals.has(entry.source_entity_id):
totals[entry.source_entity_id] = {&"revenue": 0, &"expenses": 0}
if entry.category == &"SERVICE_SALE":
totals[entry.source_entity_id][&"revenue"] += entry.amount
elif entry.category in [&"OPERATING_EXPENSE", &"STAFF_SIGNING_COST", &"EVENT_ACTION", &"EVENT_COST_COMMITTED", &"DEFENSE_SKILL_COST", &"AUTHORITY_FINE", &"REFUND"]:
totals[entry.source_entity_id][&"expenses"] += entry.amount
var rows: Array[Dictionary] = []
var kitchens: Array[Dictionary] = []
for facility: FacilityInstance in facility_service.get_instances():
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null:
continue
if facility.definition_id == &"kitchen" and facility.origin.z == WorldService.LAYER_BACK:
kitchens.append({&"facility_id": facility.instance_id, &"display_name": definition.display_name, &"origin": Vector2i(facility.origin.x, facility.origin.y), &"layer": facility.origin.z, &"operation_state": StringName(FacilityInstance.OperationState.keys()[facility.operation_state])})
var amounts: Dictionary = totals.get(facility.instance_id, {})
var base := _business_base_price(facility)
var service_pricing := get_facility_service_pricing_readmodel(facility)
var queued_customers: Dictionary = {}
for request: ServiceRequest in facility_service.get_queue(facility.instance_id):
for guest_id: StringName in request.guest_ids:
queued_customers[guest_id] = true
rows.append({
&"facility_id": facility.instance_id,
&"definition_id": facility.definition_id,
&"category": definition.category,
&"display_name": definition.display_name,
&"origin": Vector2i(facility.origin.x, facility.origin.y),
&"layer": facility.origin.z,
&"state": StringName(FacilityInstance.State.keys()[facility.state]),
&"kitchen_dependency": facility_service.get_required_kitchen_report(facility.instance_id),
&"business_open": facility.operator_open,
&"price_mode": StringName(facility.policy_current.get(&"price_mode", &"standard")),
&"base_price": base,
&"price": service_pricing[&"normal_price"],
&"service_pricing": service_pricing,
&"floor_price_multiplier_bp": get_floor_price_multiplier(facility.origin.y),
&"donation_multiplier": get_donation_multiplier(facility),
&"faith_per_service": definition.faith_per_service,
&"can_price": base > 0,
&"queued_count": facility.queue_entries.size(),
&"active_count": facility.active_services.size(),
&"queued_customer_count": queued_customers.size(),
&"active_customer_count": facility.customer_count(),
&"customer_capacity": facility.customer_capacity,
&"cleanliness": facility_cleanliness.get_facility(facility.instance_id).get("current", -1),
&"cleaning_status": cleaning_bridge.status(facility.instance_id),
&"today_revenue": int(amounts.get(&"revenue", 0)),
&"today_expenses": int(amounts.get(&"expenses", 0)),
})
return {&"minute": minute, &"modes": FacilityPricing.modes(), &"facilities": rows, &"kitchens": kitchens}
func _guest_has_checked_in(guest: GuestRuntime) -> bool:
for visited_id: StringName in guest.visited_facility_ids:
var visited := facility_service.get_instance(visited_id)
if visited != null and visited.definition_id == &"reception":
return true
return false
func _service_satisfaction_sample(service: ServiceInstance, _guest: GuestRuntime) -> int:
# P1 uses a common expectation of 50 until archetypes have an explicit
# expected-quality field. Quality sensitivity is a separate concept.
var expectation := 50
return clampi(service.quality_snapshot + FacilityPricing.satisfaction_modifier(
service.policy_snapshot, service.quality_snapshot, expectation
), 0, 100)
func _release_closed_business_queues(absolute_minute: int) -> void:
for facility: FacilityInstance in facility_service.get_instances():
if facility.operator_open and facility.is_within_opening_hours(absolute_minute):
continue
for request: ServiceRequest in facility_service.get_queue(facility.instance_id):
_release_closed_business_request(facility, request, absolute_minute)
for guest: GuestRuntime in guest_service.get_guests():
if guest.state != GuestRuntime.State.MOVING:
continue
var facility := facility_service.get_instance(guest.target_facility_id)
if facility == null or (facility.operator_open and facility.is_within_opening_hours(absolute_minute)):
continue
var transition := guest_service.transition_guest(guest.guest_id, GuestRuntime.State.REPLANNING, absolute_minute, &"FACILITY_CLOSED")
if bool(transition.get(&"ok", false)):
guest.target_facility_id = &""
guest.path_handle = null
func _release_closed_business_request(facility: FacilityInstance, request: ServiceRequest, minute: int) -> void:
# Validate the entire group before any queue/state signals become observable.
for guest_id: StringName in request.guest_ids:
var guest := guest_service.get_guest(guest_id)
if guest == null:
continue
if guest.state != GuestRuntime.State.QUEUING or guest.queue_request_id != request.request_id or guest.target_facility_id != facility.instance_id:
return
if guest.transition_tick == minute and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK:
return
# A group releases its one reservation and queue atomically. A transition-budget
# failure retries next minute, without taking patience or dropping any stock.
var before_inventory := inventory_service.create_snapshot()
var before_facility := facility_service.create_snapshot()
var before_guest := guest_service.create_snapshot()
var before_events := domain_events.create_snapshot()
var before_ids := id_generator.create_snapshot()
var released := false
var release := func(_guest_id: StringName = &"", _facility_id: StringName = &"", _request_id: StringName = &"") -> Dictionary:
for reservation_id: StringName in request.reserved_inventory_ids:
var result := inventory_service.release_reservation(reservation_id, StringName("business.close.%s.%s" % [request.request_id, reservation_id]), minute)
if not bool(result.get(&"ok", false)):
return result
var result := facility_service.leave_queue(facility.instance_id, request.request_id, &"FACILITY_CLOSED")
return {&"ok": result.is_success()}
var ok := true
for guest_id: StringName in request.guest_ids:
if not guest_service.has_guest(guest_id):
continue
var result: Dictionary = guest_service.release_queue_for_replanning(guest_id, minute, release if not released else Callable())
if not bool(result.get(&"ok", false)):
ok = false
break
released = true
if ok and not released:
ok = bool(release.call().get(&"ok", false))
if not ok:
inventory_service.restore_snapshot(before_inventory)
facility_service.restore_snapshot(before_facility)
guest_service.restore_snapshot(before_guest)
domain_events.restore_snapshot(before_events)
id_generator.restore_snapshot(before_ids)
## Commute and elevator movement share the authoritative Path projection and
## fixed-point walking credit used by errands and guests.
func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, minute: int, reason: StringName = &"") -> Dictionary:
var staff := staff_service.get_staff(staff_id)
if staff == null:
return {&"ok": false, &"reason": &"STAFF_NOT_FOUND"}
var actor_id := _staff_path_actor_id(staff_id)
if not _path_projection_matches(actor_id, staff.current_cell):
_report_path_integration_failure(actor_id, staff_id, &"staff", staff.current_cell)
return {&"ok": false}
if not reason.is_empty() or staff.current_cell == target_cell or not elevators.get_actor_trip(actor_id).is_empty():
_record_expected_actor_wait(actor_id, staff.current_cell, minute)
return {&"ok": true, &"moved": false, &"reason": reason}
var path := path_service.find_path(staff.current_cell, target_cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
if path.size() < 2:
_record_expected_actor_wait(actor_id, staff.current_cell, minute)
return {&"ok": true, &"moved": false, &"reason": &"COMMUTE_ROUTE_BLOCKED"}
if _request_elevator_step(actor_id, path[0], path[1], minute, [&"staff"]):
return {&"ok": true, &"moved": false, &"reason": &"ELEVATOR_WAIT"}
var required_credit := _step_movement_credit(path_service, path[0], path[1], [&"staff"])
var credit := _movement_credit_after_one_minute(actor_id, staff.current_cell, minute)
credit = mini(credit, required_credit + MOVEMENT_CREDIT_SCALE - 1)
var commute_warp := _warp_step_connector(path[0], path[1], [&"staff"])
if credit < required_credit or not _warp_fare_affordable(commute_warp, 1):
_record_expected_actor_wait(actor_id, staff.current_cell, minute)
_movement_credits[actor_id] = {&"credit": credit, &"updated_at": minute}
return {&"ok": true, &"moved": false}
var previous := staff.current_cell
if not path_service.project_actor_position(actor_id, path[1], minute):
_report_path_integration_failure(actor_id, staff_id, &"staff", previous)
return {&"ok": false}
staff.current_cell = path[1]
_charge_warp_fare(commute_warp, [actor_id], previous, staff.current_cell)
_movement_credits[actor_id] = {&"credit": credit - required_credit, &"updated_at": minute}
_publish_actor_event(&"STAFF_MOVED", &"staff_commute", [staff_id, actor_id], {
&"path_actor_id": actor_id, &"from_cell": previous, &"cell": staff.current_cell,
&"target_cell": target_cell, &"reason": &"COMMUTE"})
return {&"ok": true, &"moved": true, &"cell": staff.current_cell}
func _request_elevator_step(actor_id: StringName, from: Vector3i, to: Vector3i,
minute: int, access: Array[StringName]) -> bool:
var connector_id := path_service.get_step_connector(from, to, access)
if not elevators.owns_connector(connector_id):
return false
var shaft_id := elevators.get_shaft_for_connector(connector_id)
if not _elevator_extension_pending(shaft_id):
var result := elevators.request_trip(actor_id, from, to, minute, access)
if bool(result.get(&"ok", false)) and not bool(result.get(&"replay", false)):
_publish_actor_event(&"ELEVATOR_TRIP_REQUESTED", &"elevators", [actor_id, shaft_id], result[&"trip"])
# Waiting for a cabin or a construction extension is intentional, never a
# walk across floors and never a stuck-actor timeout.
_record_expected_actor_wait(actor_id, from, minute)
return true
func _advance_elevators(minute: int) -> void:
var queued_before: Dictionary = {}
for trip: Dictionary in elevators.get_report()[&"trips"]:
if trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED:
queued_before[trip[&"request_id"]] = true
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
var powered := int(_state.get_resource(&"mana")) > 0
for module_id: StringName in shaft[&"module_ids"]:
var state := mana_network_service.get_consumer_state(module_id)
if state not in [ManaNetworkService.ConsumerState.ACTIVE, ManaNetworkService.ConsumerState.DEGRADED]:
powered = false
_apply_elevator_arrivals(elevators.set_powered(shaft[&"shaft_id"], powered, minute), minute)
_apply_elevator_arrivals(elevators.advance_one_minute(minute), minute)
for trip: Dictionary in elevators.get_report()[&"trips"]:
if trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED and queued_before.has(trip[&"request_id"]):
var payload := trip.duplicate(true)
payload[&"boarded_at"] = minute
payload[&"wait_minutes"] = minute - int(trip[&"enqueued_at"])
_publish_actor_event(&"ELEVATOR_BOARDED", &"elevators", [trip[&"actor_id"], trip[&"shaft_id"]], payload)
func _apply_elevator_arrivals(arrivals: Array[Dictionary], minute: int) -> void:
for arrival: Dictionary in arrivals:
var actor_id := StringName(arrival[&"actor_id"])
if not path_service.has_actor_projection(actor_id):
continue
var previous: Vector3i = path_service.get_actor_projected_cell(actor_id)
var cell: Vector3i = arrival[&"cell"]
var entity_id := StringName(String(actor_id).trim_prefix("guest:").trim_prefix("staff:"))
var guest := guest_service.get_guest(entity_id) if String(actor_id).begins_with("guest:") else null
var staff := staff_service.get_staff(entity_id) if String(actor_id).begins_with("staff:") else null
if guest == null and staff == null:
_report_path_integration_failure(actor_id, entity_id, &"elevator", previous)
continue
if not path_service.project_actor_position(actor_id, cell, minute):
_report_path_integration_failure(actor_id, entity_id, &"elevator", previous)
continue
if guest != null:
guest_service.record_path_success(guest.guest_id, cell, minute)
else:
staff.current_cell = cell
_movement_credits[actor_id] = {&"credit": 0, &"updated_at": minute}
_publish_actor_event(&"ELEVATOR_ARRIVED", &"elevators", [actor_id, entity_id], arrival)
func _execute_set_elevator_operation(command: SimulationCommand) -> CommandResult:
var shaft_id := StringName(command.payload.get(&"shaft_id", &""))
var open_value: Variant = command.payload.get(&"operator_open", null)
if typeof(open_value) != TYPE_BOOL:
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "operator_open must be a bool.")
var found := false
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
if shaft[&"shaft_id"] == shaft_id: found = true
if not found:
return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Elevator shaft was not found.")
_apply_elevator_arrivals(elevators.set_operator_open(shaft_id, open_value, _runtime_event_minute()), _runtime_event_minute())
_publish_actor_event(&"ELEVATOR_OPERATION_CHANGED", &"elevators", [shaft_id], {&"operator_open": open_value})
return CommandResult.succeeded(command.command_id, {&"shaft_id": shaft_id, &"operator_open": open_value})
## A07: warp connectors exist for every completed warp pair and are usable only
## while both warps are powered and the reserve can pay one fare.
func _synchronize_warps() -> Dictionary:
var available := {}
var reserve_ok := int(_state.get_resource(&"mana")) > 0
for warp: Dictionary in WarpService.warps_from_facilities(construction_service.get_facilities()):
var state := mana_network_service.get_consumer_state(warp[&"instance_id"])
available[warp[&"instance_id"]] = reserve_ok and state in [ManaNetworkService.ConsumerState.ACTIVE, ManaNetworkService.ConsumerState.DEGRADED]
return WarpService.synchronize(path_service, construction_service.get_facilities(), available,
int(_state.get_resource(&"mana")) >= WarpService.MANA_PER_TRIP)
func _warp_step_connector(from: Vector3i, to: Vector3i, access: Array[StringName]) -> StringName:
var connector_id := path_service.get_step_connector(from, to, access)
return connector_id if WarpService.is_warp_connector(connector_id) else &""
func _warp_fare_affordable(connector_id: StringName, riders: int) -> bool:
return connector_id.is_empty() or int(_state.get_resource(&"mana")) >= WarpService.MANA_PER_TRIP * riders
## Charged once, in the same tick as the committed step; affordability was
## checked before the step, so the deduction cannot fail here.
func _charge_warp_fare(connector_id: StringName, actor_ids: Array, from: Vector3i, to: Vector3i) -> void:
if connector_id.is_empty():
return
var fare := WarpService.MANA_PER_TRIP * actor_ids.size()
_state.apply_resource_deltas({&"mana": -fare})
var subjects: Array[StringName] = [connector_id]
for actor_id: Variant in actor_ids:
subjects.append(StringName(actor_id))
_publish_actor_event(&"WARP_TRAVELED", &"warp", subjects, {
&"connector_id": connector_id, &"from_cell": from, &"cell": to,
&"riders": actor_ids.size(), &"mana": fare})
func _synchronize_elevators(minute: int) -> Dictionary:
return elevators.synchronize(ElevatorService.configurations_from_facilities(
construction_service.get_facilities(), definition_registry), minute)
func _elevator_shaft_busy(shaft: Dictionary) -> bool:
return shaft.get(&"phase", &"IDLE") != &"IDLE" or int(shaft.get(&"waiting_count", 0)) > 0 \
or int(shaft.get(&"passenger_count", 0)) > 0
func _elevator_touches_module(shaft: Dictionary, definition: FacilityDefinition, origin: Vector3i) -> bool:
if definition == null or definition.id not in [&"guest_elevator", &"service_elevator"]:
return false
var stops: Array = shaft[&"stops"]
var bottom: Vector3i = stops.front()
var top: Vector3i = stops.back()
return origin.x == bottom.x and origin.z == bottom.z and definition.footprint.x == int(shaft[&"width"]) \
and origin.y <= top.y + 1 and origin.y + definition.footprint.y >= bottom.y
func _elevator_extension_pending(shaft_id: StringName) -> bool:
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
if shaft[&"shaft_id"] != shaft_id: continue
for job: ConstructionJob in construction_service.get_jobs():
if job.state in [ConstructionJob.State.COMPLETED, ConstructionJob.State.CANCELLED]: continue
if _elevator_touches_module(shaft, definition_registry.get_facility(job.definition_id), job.origin):
return true
return false
func _validate_elevator_crosslinks(service: ElevatorService, paths: PathService,
construction: ConstructionService, guests: GuestService, staff: StaffService, minute: int) -> bool:
if service.current_minute != minute: return false
var expected := ElevatorService.configurations_from_facilities(construction.get_facilities(), definition_registry)
var saved_configs: Array = []
for car: Dictionary in service.create_snapshot()[&"shafts"]:
saved_configs.append(car[&"config"])
# Construction uses numeric spatial order while snapshots use lexical IDs.
# Compare canonical shaft identity order (x=-2/10, floors=2/10 otherwise differ).
var by_id := func(a: Dictionary, b: Dictionary) -> bool:
return String(a[&"shaft_id"]) < String(b[&"shaft_id"])
expected.sort_custom(by_id)
saved_configs.sort_custom(by_id)
if expected != saved_configs: return false
for trip: Dictionary in service.get_report()[&"trips"]:
var actor_id := StringName(trip[&"actor_id"])
var entity_id := StringName(String(actor_id).trim_prefix("guest:").trim_prefix("staff:"))
var guest := guests.get_guest(entity_id) if String(actor_id).begins_with("guest:") else null
var worker := staff.get_staff(entity_id) if String(actor_id).begins_with("staff:") else null
if guest == null and worker == null: return false
if guest != null and (guest.current_cell != trip[&"from"] or trip[&"from"].z != WorldService.LAYER_FRONT): return false
if worker != null and worker.current_cell != trip[&"from"]: return false
if not paths.has_actor_projection(actor_id) or paths.get_actor_projected_cell(actor_id) != trip[&"from"]: return false
return true
func is_staff_in_transit(staff_id: StringName) -> bool:
return not elevators.get_actor_trip(_staff_path_actor_id(staff_id)).is_empty()
func _estimated_actor_path_minutes(path: Array, access: Array[StringName]) -> int:
for index: int in range(1, path.size()):
var connector_id := path_service.get_step_connector(path[index - 1], path[index], access)
if elevators.owns_connector(connector_id) \
or _step_movement_credit_for_connector(path_service, path[index - 1], path[index], connector_id) > MOVEMENT_CREDIT_SCALE:
var cost: int
if access.is_empty() and path_service.get_script().resource_path == "res://scripts/world/path_service.gd" \
and path[0] is Vector3i and path[path.size() - 1] is Vector3i:
cost = path_service.calculate_guest_batch_path_cost(path[0], path[path.size() - 1], path)
else:
cost = path_service.calculate_path_cost(path, access)
return ceili(float(cost) / PathService.ORTHOGONAL_MOVE_COST)
return maxi(path.size() - 1, 0)
func _prune_expired_staff_elevator_waits(minute: int) -> void:
for trip: Dictionary in elevators.get_report()[&"trips"]:
if trip.has(&"companion_id"): continue
var actor_id := StringName(trip[&"actor_id"])
if trip[&"state"] != PathService.ELEVATOR_REQUEST_QUEUED or not String(actor_id).begins_with("staff:"):
continue
var staff := staff_service.get_staff(StringName(String(actor_id).trim_prefix("staff:")))
if staff == null:
elevators.cancel_actor(actor_id)
continue
if staff.state == StaffRuntime.State.OFF_DUTY and get_staff_rest_report(staff.staff_id).get(&"reachable", false):
continue
if not staff_service.requires_attendance(staff.staff_id):
continue
var task := staff_service.get_task(staff.current_task_id)
if task != null and task.state == StaffTask.State.MOVING and task.assigned_staff_id == staff.staff_id:
continue
var shift := staff_service.get_shift(staff.shift_id)
if not staff.can_take_shift() or shift == null or (not shift.contains_minute(minute) and not shift.is_commute_window(minute)):
elevators.cancel_actor(actor_id)
_publish_actor_event(&"ELEVATOR_COMMUTE_CANCELLED", &"staff_commute", [staff.staff_id, actor_id], {&"reason": &"SHIFT_ENDED_OR_UNAVAILABLE"})
## Stairs carry half-minute progress between ticks. Doors keep their established
## one-step walking behavior, and elevator time is owned by the cabin service.
static func _step_movement_credit(path: PathService, from: Vector3i, to: Vector3i,
access: Array[StringName]) -> int:
return _step_movement_credit_for_connector(path,from,to,path.get_step_connector(from,to,access))
static func _step_movement_credit_for_connector(path: PathService, from: Vector3i, to: Vector3i, connector_id: StringName) -> int:
if String(connector_id).begins_with("infra.connector:") and from.x == to.x \
and from.z == to.z and absi(from.y - to.y) == 1:
var connector := path.get_connector(connector_id)
if connector.get(&"movement_type", &"") == PathService.MOVEMENT_CONNECTOR \
and int(connector.get(&"cost", 0)) == 150:
return STAIR_MOVEMENT_CREDIT
return MOVEMENT_CREDIT_SCALE
static func _cell_movement_credit_limit(path: PathService, cell: Vector3i) -> int:
# Temporary blockades must not invalidate progress saved on a staircase.
# Inspect structural edges here instead of querying currently usable routes.
for edge: Dictionary in path.get_neighbors(cell):
var next: Vector3i = edge[&"to"]
if String(edge[&"connector_id"]).begins_with("infra.connector:") \
and edge[&"movement_type"] == PathService.MOVEMENT_CONNECTOR \
and int(edge[&"base_cost"]) == 150 and cell.x == next.x \
and cell.z == next.z and absi(cell.y - next.y) == 1:
return STAIR_MOVEMENT_CREDIT
return MOVEMENT_CREDIT_SCALE
func _prune_legacy_stairs_and_movement() -> bool:
if not legacy_stairs.prune_invalid():
return false
# A removed staircase discards only progress that can no longer be used.
for actor_id: StringName in _movement_credits:
if path_service.has_actor_projection(actor_id):
var cell: Vector3i = path_service.get_actor_projected_cell(actor_id)
_movement_credits[actor_id][&"credit"] = mini(int(_movement_credits[actor_id][&"credit"]),
_cell_movement_credit_limit(path_service, cell) - 1)
return true
func get_elevator_wait_statistics(shaft_id: StringName) -> Dictionary:
var samples: Array[int] = []
var oldest_wait := 0
var minute := _runtime_event_minute()
for event: SimulationDomainEvent in domain_events.recent_events():
if event.event_type != &"ELEVATOR_BOARDED" or event.absolute_minute < minute - 60 \
or event.absolute_minute > minute or event.payload.get(&"shaft_id", &"") != shaft_id:
continue
var wait: int = int(event.payload.get(&"wait_minutes", -1))
if wait >= 0:
samples.append(wait)
for trip: Dictionary in elevators.get_report()[&"trips"]:
if trip[&"shaft_id"] == shaft_id and trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED:
oldest_wait = maxi(oldest_wait, minute - int(trip[&"enqueued_at"]))
samples.sort()
var total := 0
for wait: int in samples:
total += wait
return {&"sample_count": samples.size(), &"window_minutes": 60,
&"event_history_limit": DomainEventStream.RECENT_EVENT_LIMIT,
&"mean_wait_minutes": float(total) / samples.size() if not samples.is_empty() else 0.0,
&"p95_wait_minutes": samples[ceili(samples.size() * 0.95) - 1] if not samples.is_empty() else 0,
&"oldest_wait_minutes": oldest_wait}
func _demolition_lost_route_cells(instance_id: StringName) -> Array[Vector3i]:
var lost_cells: Array[Vector3i] = []
var projection := facility_infrastructure_projector.get_projection(instance_id)
var base := route_subslot_path_adapter.get_base_walkable_cells()
for cell: Vector3i in projection.get(&"cells", []):
if cell not in base:
lost_cells.append(cell)
return lost_cells
func _stair_demolition_dependents(instance_id: StringName) -> Array[StringName]:
var lost_cells := _demolition_lost_route_cells(instance_id)
var dependents := facility_infrastructure_projector.get_stair_dependents(lost_cells)
dependents.erase(instance_id)
# A staircase under construction already relies on its lower landing too.
for job: ConstructionJob in construction_service.get_jobs():
if not job.is_terminal() and job.definition_id in [&"guest_stairs", &"service_stairs"] \
and job.origin + Vector3i(0, -1, 0) in lost_cells:
dependents.append(job.job_id)
return dependents
## Use the same entrance and horizontal access cells as ordinary Guest AI.
## Facility access_available is an operating override, not a topology cache.
func is_facility_reachable(facility_id: StringName) -> bool:
var facility := facility_service.get_instance(facility_id)
var entrance: Variant = _operations_entrance_cell()
if facility == null or entrance == null or not facility.access_available:
return false
# Read-only UI queries need existence, not the cheapest destination.
# Share Guest AI's candidates and path rules without caching topology state.
for candidate: Vector3i in _facility_access_candidates(facility):
if not path_service.find_path(entrance, candidate).is_empty():
return true
return false
func get_facility_access_report(facility_id: StringName) -> Dictionary:
var facility := facility_service.get_instance(facility_id)
var entrance: Variant = _operations_entrance_cell()
if facility == null or entrance == null or not facility.access_available:
return {&"reachable": false, &"path": []}
return _find_facility_access_path(entrance, facility)
func _staff_is_being_carried(patient_id: StringName) -> bool:
for task: StaffTask in staff_service.get_tasks():
if task.task_type == &"rescue_transport" and not task.is_terminal() and not task.assigned_staff_id.is_empty() and task.target_entity_id == patient_id:
return true
return false
func _rescue_destination(patient_cell: Vector3i) -> Dictionary:
var best := {}
var distance := 2147483647
for clinic: FacilityInstance in facility_service.get_instances():
if clinic.definition_id != &"infirmary" or clinic.operation_state != FacilityInstance.OperationState.OPERATING:
continue
var cells := {clinic.origin: true}
for cell: Vector3i in clinic.occupied_cells:
for delta in [Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
cells[cell + delta] = true
var ordered := cells.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i): return a.y < b.y if a.y != b.y else a.x < b.x)
for cell: Vector3i in ordered:
if cell.z != WorldService.LAYER_BACK:
continue
var route := path_service.find_backyard_path(patient_cell, cell, true)
if not route.is_empty() and route.size() < distance:
distance = route.size()
best = {&"facility_id": clinic.instance_id, &"cell": cell}
return best
func _handle_rescue_arrival(task: StaffTask, carrier: StaffRuntime, minute: int) -> bool:
if task.task_type == CleaningBridge.TASK_TYPE:
return carrier.current_cell == task.target_cell and cleaning_bridge.task_can_work(task, carrier)
if task.task_type not in [&"rescue_pickup", &"rescue_transport"]:
return true
var patient := staff_service.get_staff(task.target_entity_id)
if patient == null or patient.state != StaffRuntime.State.INCAPACITATED or carrier.current_cell != task.target_cell:
return false
if task.task_type == &"rescue_transport":
var clinic := facility_service.get_instance(task.rescue_facility_id)
return clinic != null and clinic.operation_state == FacilityInstance.OperationState.OPERATING and patient.current_cell == carrier.current_cell
if patient.current_cell != carrier.current_cell:
return false
var destination := _rescue_destination(patient.current_cell)
if destination.is_empty():
return false
task.task_type = &"rescue_transport"
task.rescue_facility_id = destination[&"facility_id"]
task.target_cell = destination[&"cell"]
task.state = StaffTask.State.MOVING
task.state_changed_at = minute
_publish_actor_event(&"STAFF_RESCUE_PICKED_UP", &"staff_service", [patient.staff_id, carrier.staff_id], {&"facility_id": task.rescue_facility_id})
return false
func _advance_staff_rescue(minute: int) -> void:
for task: StaffTask in staff_service.get_tasks():
if task.task_type not in [&"rescue_pickup", &"rescue_transport"] or task.is_terminal():
continue
var patient := staff_service.get_staff(task.target_entity_id)
var carrier := staff_service.get_staff(task.assigned_staff_id)
var invalid := patient == null or patient.state != StaffRuntime.State.INCAPACITATED
if not task.assigned_staff_id.is_empty():
invalid = invalid or carrier == null or not carrier.can_take_shift() or carrier.current_task_id != task.task_id
if task.task_type == &"rescue_transport":
var clinic := facility_service.get_instance(task.rescue_facility_id)
invalid = invalid or carrier == null or clinic == null or clinic.operation_state != FacilityInstance.OperationState.OPERATING
if patient != null and carrier != null:
invalid = invalid or patient.current_cell != carrier.current_cell
if invalid:
if carrier != null and elevators.get_actor_trip(_staff_path_actor_id(carrier.staff_id)).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED:
continue # Keep the pair aboard until a real or emergency landing.
staff_service.cancel_task(task.task_id, &"RESCUE_INTERRUPTED", minute)
if patient != null: elevators.cancel_actor(_staff_path_actor_id(patient.staff_id))
if carrier != null: elevators.cancel_actor(_staff_path_actor_id(carrier.staff_id))
for patient: StaffRuntime in staff_service.get_staff_members():
if not _defense_lock_owner(patient.staff_id, &"DEFENDER").is_empty():
continue
if patient.health > 0 and patient.state != StaffRuntime.State.INCAPACITATED:
continue
if not patient.current_task_id.is_empty():
staff_service.cancel_task(patient.current_task_id, &"STAFF_COLLAPSED", minute)
if patient.state != StaffRuntime.State.INCAPACITATED or patient.state_reason.is_empty():
patient.transition_to(StaffRuntime.State.INCAPACITATED, &"STAFF_COLLAPSED", minute)
_publish_actor_event(&"STAFF_COLLAPSED", &"staff_service", [patient.staff_id], {&"cell": patient.current_cell})
if patient.current_cell.z == WorldService.LAYER_FRONT and patient.state_reason != &"COLLAPSE_WITNESSED":
for guest: GuestRuntime in guest_service.get_guests():
if _authority_line_of_sight(guest.current_cell, patient.current_cell, {}):
if progression_service.update_metrics({&"reputation": maxi(int(progression_service.metrics[&"reputation"]) - 1, 0)}):
patient.state_reason = &"COLLAPSE_WITNESSED"
_publish_actor_event(&"STAFF_COLLAPSE_WITNESSED", &"staff_service", [patient.staff_id, guest.guest_id], {})
break
var pending := false
for task: StaffTask in staff_service.get_tasks():
if task.task_type in [&"rescue_pickup", &"rescue_transport"] and task.target_entity_id == patient.staff_id and not task.is_terminal():
pending = true
if not elevators.get_actor_trip(_staff_path_actor_id(patient.staff_id)).is_empty():
continue
if pending or _rescue_destination(patient.current_cell).is_empty():
continue
staff_service.create_task(&"rescue_pickup", 95, minute, {
&"target_entity_id": patient.staff_id, &"target_cell": patient.current_cell,
&"required_role_tags": [&"medic"], &"task_tags": [&"rescue"],
&"estimated_minutes": 30, &"interrupt_level": 0, &"emergency": true,
})
func get_staff_rescue_report() -> Dictionary:
var report := {&"waiting": 0, &"transporting": 0, &"treating": 0}
for patient: StaffRuntime in staff_service.get_staff_members():
if patient.state != StaffRuntime.State.INCAPACITATED:
continue
var category: StringName = &"waiting"
for task: StaffTask in staff_service.get_tasks():
if task.target_entity_id == patient.staff_id and task.task_type == &"rescue_transport" and not task.is_terminal():
category = &"treating" if task.state == StaffTask.State.WORKING else &"transporting"
report[category] += 1
return report
func _staff_rescue_status(staff_id: StringName) -> String:
var staff := staff_service.get_staff(staff_id)
if staff == null or staff.state != StaffRuntime.State.INCAPACITATED:
return ""
for task: StaffTask in staff_service.get_tasks():
if task.target_entity_id != staff_id or task.is_terminal(): continue
if task.task_type == &"rescue_transport":
return "治療中" if task.state == StaffTask.State.WORKING else "搬送中"
if task.task_type == &"rescue_pickup" and not task.assigned_staff_id.is_empty():
return "救護班が向かっています"
return "救護待ち(医療職・医務室・裏経路を確認)"
## Rest destinations derive from saved facilities and position, with no hidden timer.
func get_staff_rest_report(staff_id: StringName) -> Dictionary:
var staff := staff_service.get_staff(staff_id)
if staff == null or staff.state != StaffRuntime.State.OFF_DUTY or staff.health <= 0 or not staff.current_task_id.is_empty(): return {}
if not _defense_lock_owner(staff_id, &"DEFENDER").is_empty() or event_service.get_actor_locks().has(staff_id): return {}
var best := {&"reachable": false, &"resting": false, &"label": "休憩先なし(稼働中の待機室・宿舎と経路を確認)"}
var best_cost := 2147483647
for room: FacilityInstance in facility_service.get_instances():
if room.definition_id not in [&"staff_waiting_room", &"staff_dormitory"] or room.operation_state != FacilityInstance.OperationState.OPERATING: continue
var route := staff_commute.find_facility_access_path(staff.current_cell, room)
if not route.get(&"reachable", false): continue
var cost := path_service.calculate_path_cost(route[&"path"], [&"staff"])
if cost >= best_cost: continue
best_cost = cost
var resting := StaffCommuteService.is_at_facility(staff, room) and not is_staff_in_transit(staff_id)
best = {&"reachable": true, &"resting": resting, &"cell": route[&"cell"], &"facility_id": room.instance_id,
&"label": ("宿舎で休憩中" if room.definition_id == &"staff_dormitory" else "待機室で休憩中") if resting else "休憩先へ移動中"}
return best
func _advance_weather(absolute_minute: int) -> void:
weather.advance_to(absolute_minute)
func get_weather_report() -> Dictionary:
return {&"kind": weather.get_kind(), &"minute": weather.get_minute()}
## Read-only migration preview; applying dimensions remains a separate transaction.
func preview_facility_dimension_migration(instance_id: StringName) -> Dictionary:
var instance := construction_service.get_facility(instance_id)
if instance.is_empty():
return {&"geometry_ok": false, &"ready_to_apply": false, &"errors": [&"COMPLETED_FACILITY_REQUIRED"]}
var weights := {}
for built: Dictionary in construction_service.get_facilities():
var definition := definition_registry.get_facility(built[&"definition_id"])
if definition == null:
return {&"geometry_ok": false, &"ready_to_apply": false, &"errors": [&"MISSING_FACILITY_DEFINITION"]}
weights[built[&"instance_id"]] = definition.weight_per_cell
var preview: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").inspect(world_service, instance, weights[instance_id], weights)
if preview.has(&"remaining_gates"):
preview[&"remaining_gates"].erase(&"DEFINITION_WEIGHT_VALIDATION")
preview[&"definition_weights"] = weights
if preview.get(&"geometry_ok", false):
preview[&"construction_candidate"] = preload("res://scripts/construction/facility_resize_preview.gd").prepare_construction_candidate(world_service, construction_service, instance_id, weights[instance_id], weights)
preview[&"actors_on_removed_cells"] = []
for guest: GuestRuntime in guest_service.get_guests():
if guest.current_cell in preview.get(&"removed", []):
preview[&"actors_on_removed_cells"].append({&"kind": &"guest", &"id": guest.guest_id, &"cell": guest.current_cell})
for worker: StaffRuntime in staff_service.get_staff_members():
if worker.current_cell in preview.get(&"removed", []):
preview[&"actors_on_removed_cells"].append({&"kind": &"staff", &"id": worker.staff_id, &"cell": worker.current_cell})
preview[&"construction_jobs"] = []
for job: ConstructionJob in construction_service.get_jobs():
preview[&"construction_jobs"].append({&"job_id": job.job_id, &"definition_id": job.definition_id, &"state": job.state})
return preview
## Assemble and validate a snapshot in an isolated Runtime, including access
## preservation. Disk backup, actor relocation and atomic commit remain separate.
func prepare_facility_dimension_snapshot(instance_id: StringName) -> Dictionary:
if construction_service.get_facility(instance_id).is_empty() and construction_service.get_job(instance_id) != null:
return _prepare_construction_dimension_snapshot(instance_id)
var preview := preview_facility_dimension_migration(instance_id)
var result := {&"ok": false, &"ready_to_apply": false, &"errors": [], &"preview": preview}
if not preview.get(&"construction_candidate", {}).get(&"ok", false):
result[&"errors"].append(&"CONSTRUCTION_CANDIDATE_REQUIRED")
return result
if not preview[&"actors_on_removed_cells"].is_empty():
result[&"errors"].append(&"ACTOR_RELOCATION_REQUIRED")
return result
var candidate := create_snapshot()
var data: Dictionary = candidate[&"systems"][&"runtime"]
data[&"world"] = preview[&"construction_candidate"][&"world"].duplicate(true)
data[&"construction"] = preview[&"construction_candidate"][&"construction"].duplicate(true)
var found := false
for instance: Dictionary in data[&"facilities"][&"instances"]:
if StringName(instance[&"instance_id"]) != instance_id: continue
found = true
var cells: Array = []
for cell: Vector3i in preview[&"target_cells"]: cells.append([cell.x, cell.y, cell.z])
instance[&"occupied_cells"] = cells
if not found:
result[&"errors"].append(&"OPERATIONAL_FACILITY_REQUIRED")
return result
var isolated: Variant = get_script().new()
var candidate_clock: Variant = load("res://autoload/game_clock.gd").new()
var candidate_state: Variant = load("res://autoload/game_state.gd").new()
candidate_clock.reset(1, 6, 0)
candidate_state.reset()
isolated.initialize(candidate_clock, candidate_state, world_seed)
# The immutable definition resources must match the source registry,
# including any explicitly registered expansion definitions.
isolated.definition_registry = definition_registry
var restored: bool = isolated.restore_snapshot(candidate)
if restored:
result[&"routes"] = preload("res://scripts/construction/facility_resize_routes.gd").inspect(self, isolated, instance_id)
if result[&"routes"][&"errors"].is_empty() and not result[&"routes"][&"guest_replans"].is_empty():
result[&"retargeting"] = preload("res://scripts/construction/facility_resize_routes.gd").retarget_guests(isolated, result[&"routes"])
if result[&"retargeting"][&"ok"]:
candidate = isolated.create_snapshot()
restored = isolated.restore_snapshot(candidate)
if restored:
result[&"routes"] = preload("res://scripts/construction/facility_resize_routes.gd").inspect(self, isolated, instance_id)
isolated.free()
candidate_clock.free()
candidate_state.free()
if not restored:
result[&"errors"].append(&"RUNTIME_CANDIDATE_REJECTED")
return result
if not result[&"routes"][&"ok"]:
result[&"errors"].append(&"ROUTE_MIGRATION_REQUIRED")
return result
result[&"ok"] = true
result[&"snapshot"] = candidate
return result
## Explicit disk migration. Source Runtime stays untouched; the formal package
## is replaced only after isolated domain/route validation and verified backup.
func migrate_facility_dimensions_at_path(save_path: String, instance_id: StringName = &"", expected_source_digest: String = "") -> Dictionary:
var loaded := SaveService.load_snapshot(save_path)
if not loaded[&"ok"]: return loaded
if not expected_source_digest.is_empty() and SimulationDigest.compute(loaded[&"snapshot"]) != expected_source_digest:
return {&"ok": false, &"code": &"MIGRATION_SOURCE_CHANGED"}
var isolated: Variant = get_script().new()
var migration_clock: Variant = load("res://autoload/game_clock.gd").new()
var migration_state: Variant = load("res://autoload/game_state.gd").new()
migration_clock.reset(1, 6, 0); migration_state.reset()
isolated.initialize(migration_clock, migration_state, world_seed)
isolated.definition_registry = definition_registry
var prepared := {&"ok": false, &"errors": [&"SOURCE_RUNTIME_REJECTED"]}
if isolated.restore_snapshot(loaded[&"snapshot"]):
if instance_id.is_empty():
prepared = isolated.prepare_all_facility_dimension_snapshots()
else:
prepared = isolated.prepare_facility_dimension_snapshot(instance_id)
isolated.free(); migration_clock.free(); migration_state.free()
if not prepared[&"ok"]:
return {&"ok": false, &"code": &"DIMENSION_MIGRATION_REJECTED", &"details": prepared}
return SaveService.replace_snapshot_with_backup(save_path, loaded[&"snapshot"], prepared[&"snapshot"])
## Runs on an isolated migration Runtime; caller commits only the final snapshot.
func prepare_all_facility_dimension_snapshots() -> Dictionary:
var original := create_snapshot()
var targets: Array[StringName] = []
var shapes: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").DIMENSIONS
for job: ConstructionJob in construction_service.get_jobs():
if job.is_terminal() or not shapes.has(job.definition_id): continue
var size: Vector2i = shapes[job.definition_id][&"new"]
if job.rotation in [90,270]: size = Vector2i(size.y,size.x)
if not preload("res://scripts/construction/facility_resize_preview.gd")._same_cells(job.reserved_cells, world_service.cells_for_footprint(job.origin,size)):
targets.append(job.job_id)
for facility: Dictionary in construction_service.get_facilities():
if not shapes.has(facility[&"definition_id"]): continue
var size: Vector2i = shapes[facility[&"definition_id"]][&"new"]
if facility[&"rotation"] in [90,270]: size = Vector2i(size.y,size.x)
if not preload("res://scripts/construction/facility_resize_preview.gd")._same_cells(facility[&"occupied_cells"], world_service.cells_for_footprint(facility[&"origin"],size)):
targets.append(facility[&"instance_id"])
for id: StringName in targets:
var prepared := prepare_facility_dimension_snapshot(id)
if not prepared[&"ok"] or not restore_snapshot(prepared[&"snapshot"]):
var rolled_back := restore_snapshot(original)
return {&"ok": false, &"errors": [&"BATCH_DIMENSION_REJECTED"], &"facility_id": id, &"details": prepared, &"rolled_back": rolled_back}
return {&"ok": true, &"snapshot": create_snapshot(), &"facility_ids": targets}
func _prepare_construction_dimension_snapshot(job_id: StringName) -> Dictionary:
var weights := {}
for facility: Dictionary in construction_service.get_facilities():
var definition := definition_registry.get_facility(facility[&"definition_id"])
if definition == null: return {&"ok": false, &"errors": [&"MISSING_FACILITY_DEFINITION"]}
weights[facility[&"instance_id"]] = definition.weight_per_cell
var job := construction_service.get_job(job_id)
var definition := definition_registry.get_facility(job.definition_id)
if job.is_terminal() or definition == null:
return {&"ok": false, &"errors": [&"ACTIVE_JOB_REQUIRED"]}
weights[job_id] = definition.weight_per_cell
var prepared: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").prepare_construction_candidate(world_service, construction_service, job_id, definition.weight_per_cell, weights)
if not prepared[&"ok"]: return prepared
var candidate := create_snapshot()
candidate[&"systems"][&"runtime"][&"world"] = prepared[&"world"]
candidate[&"systems"][&"runtime"][&"construction"] = prepared[&"construction"]
var isolated: Variant = get_script().new()
var candidate_clock: Variant = load("res://autoload/game_clock.gd").new()
var candidate_state: Variant = load("res://autoload/game_state.gd").new()
candidate_clock.reset(1,6,0); candidate_state.reset()
isolated.initialize(candidate_clock, candidate_state, world_seed)
isolated.definition_registry = definition_registry
var valid: bool = isolated.restore_snapshot(candidate)
isolated.free(); candidate_clock.free(); candidate_state.free()
if not valid: return {&"ok": false, &"errors": [&"RUNTIME_CANDIDATE_REJECTED"]}
return {&"ok": true, &"ready_to_apply": false, &"snapshot": candidate}
## Read-only saved-game preflight used by the load menu before confirmation.
func preview_facility_dimensions_at_path(save_path: String) -> Dictionary:
var loaded := SaveService.inspect_snapshot(save_path)
if not loaded[&"ok"]: return loaded
var isolated: Variant = get_script().new()
var preview_clock: Variant = load("res://autoload/game_clock.gd").new()
var preview_state: Variant = load("res://autoload/game_state.gd").new()
preview_clock.reset(1,6,0); preview_state.reset()
isolated.initialize(preview_clock,preview_state,world_seed)
isolated.definition_registry=definition_registry
var result := {&"ok": false, &"code": &"SOURCE_RUNTIME_REJECTED"}
if isolated.restore_snapshot(loaded[&"snapshot"]):
result=isolated.prepare_all_facility_dimension_snapshots()
result[&"source_digest"]=SimulationDigest.compute(loaded[&"snapshot"])
result[&"changes"]=[]
if result[&"ok"]:
for id: StringName in result[&"facility_ids"]:
var facility: Dictionary=isolated.construction_service.get_facility(id)
var job: ConstructionJob=isolated.construction_service.get_job(id)
var definition_id: StringName=facility.get(&"definition_id",&"") if job==null else job.definition_id
var definition: FacilityDefinition=definition_registry.get_facility(definition_id)
result[&"changes"].append({&"id":id,&"name":definition.display_name,&"size":definition.footprint,&"building":job!=null})
result.erase(&"snapshot")
isolated.free();preview_clock.free();preview_state.free()
return result
func _on_cleanliness_facility_registered(facility: FacilityInstance) -> void:
var values := cleaning_policy.create_snapshot()
if facility_cleanliness.get_facility(facility.instance_id).is_empty():
facility_cleanliness.register_facility(facility.instance_id, values["initial_cleanliness"], values["maximum_cleanliness"])
func _on_cleanliness_facility_unregistered(facility_id: StringName) -> void:
for task: StaffTask in staff_service.get_tasks():
if task.task_type == CleaningBridge.TASK_TYPE and task.target_entity_id == facility_id and not task.is_terminal():
staff_service.cancel_task(task.task_id, &"CLEANING_TARGET_REMOVED", _runtime_event_minute())
if not facility_cleanliness.get_facility(facility_id).is_empty():
facility_cleanliness.unregister_facility(facility_id)
_sync_facility_staffing(_runtime_event_minute())
func _validate_cleanliness_crosslinks(cleanliness: RefCounted, facilities: FacilityService, staff: StaffService, path: PathService) -> bool:
var snapshot: Dictionary = cleanliness.create_snapshot()
var instances := facilities.get_instances()
var completed_cleanings: Dictionary = {}
if snapshot["facilities"].size() != instances.size(): return false
for facility: FacilityInstance in instances:
var row: Dictionary = cleanliness.get_facility(facility.instance_id)
if row.is_empty() or row["maximum"] != 100 or row["initial"] != 100: return false
for receipt: Dictionary in snapshot["receipts"]:
if receipt["kind"] == "clean":
var task := staff.get_task(StringName(receipt["source_id"]))
if task == null or task.task_type != CleaningBridge.TASK_TYPE or task.state != StaffTask.State.COMPLETED or task.target_entity_id != StringName(receipt["facility_id"]) or receipt["amount"] != cleaning_policy.create_snapshot()["recovery_per_completed_task"]: return false
if task.progress_minutes != task.estimated_minutes: return false
completed_cleanings[task.task_id] = true
continue
if receipt["kind"] != "use": return false
var service := facilities.get_service(StringName(receipt["source_id"]))
if service == null or service.state != ServiceInstance.State.COMPLETED or service.facility_id != StringName(receipt["facility_id"]): return false
var facility := facilities.get_instance(service.facility_id)
var definition := definition_registry.get_facility(facility.definition_id)
if definition == null or receipt["amount"] != definition.cleanliness_decay_per_service: return false
var pending: Dictionary = {}
for task: StaffTask in staff.get_tasks():
if task.task_type != CleaningBridge.TASK_TYPE: continue
if task.estimated_minutes != cleaning_policy.create_snapshot()["work_minutes"] or task.required_role_tags != [&"cleaner"]: return false
if task.state == StaffTask.State.COMPLETED and facilities.has_instance(task.target_entity_id) and not completed_cleanings.has(task.task_id): return false
if task.is_terminal(): continue
var facility := facilities.get_instance(task.target_entity_id)
if facility == null or task.target_cell not in CleaningBridge.access_cells(facility, path, false) or pending.has(task.target_entity_id): return false
pending[task.target_entity_id] = true
if not task.assigned_staff_id.is_empty():
var worker := staff.get_staff(task.assigned_staff_id)
if not CleaningBridge.is_deployed(worker, facilities.get_instance(worker.assigned_facility_id) if worker != null else null, staff): return false
return true
func _automatic_staff_work_units(task_id: StringName) -> int:
var task := staff_service.get_task(task_id)
# Cleaning is progressed after attendance, dependency and cancellation checks.
return 0 if task != null and task.task_type == CleaningBridge.TASK_TYPE else 1
## This is a configured event sink, not a thief/riot/loan predicate. Producers
## must supply a cause identity and a complete, approved threat template.
func _build_event_defense_request(action: EventAction, context: Dictionary) -> Dictionary:
var instance_value: Variant = context.get(&"event_instance_id", context.get("event_instance_id", null))
var source_value: Variant = context.get(&"threat_source_id", context.get("threat_source_id", null))
var template_value: Variant = action.payload.get(&"template_id", action.payload.get("template_id", null))
var kind_value: Variant = action.payload.get(&"source_kind", action.payload.get("source_kind", null))
for raw: Variant in [instance_value, source_value, template_value, kind_value]:
if typeof(raw) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw).is_empty():
return {&"ok": false, &"error": "Configured threat requires event, source, template and source-kind identities."}
var source_kind := String(kind_value).to_upper()
if source_kind not in ["THIEF", "BANKRUPTCY_RIOT", "BANK_DELINQUENCY", "AUTHORITY_ESCALATION"]:
return {&"ok": false, &"error": "Configured threat source kind is unknown."}
var identity := JSON.stringify([String(instance_value), String(action.action_id)], "", true, true)
var raid_id := StringName("event_defense_raid." + identity.sha256_text())
var prepared := _prepare_event_threat_template(action, StringName(source_value), raid_id)
if not bool(prepared.get(&"ok", false)):
return prepared
var definition: Dictionary = prepared[&"definition"]
var digest := String(prepared[&"digest"])
var event_source := {
"event_instance_id": String(instance_value), "event_action_id": String(action.action_id),
"template_id": String(template_value), "template_digest": digest,
"source_kind": source_kind,
}
definition[&"event_source"] = event_source
var existing := defense_service.get_raid(raid_id)
if not existing.is_empty():
if existing.get(&"event_source", {}) != event_source or not _event_threat_template_matches_raid(existing, prepared[&"canonical_raid"]):
return {&"ok": false, &"error": "Event threat identity conflicts with an already-created incident."}
return {&"ok": true, &"raid_id": raid_id, &"definition": definition, &"is_replay": true}
for objective_id: StringName in prepared[&"objective_ids"]:
var objective := facility_service.get_instance(objective_id)
if objective == null or objective.durability <= 0:
return {&"ok": false, &"error": "Configured threat objective is unavailable."}
if not _defense_lock_owner(objective_id, &"ASSET", raid_id).is_empty():
return {&"ok": false, &"error": "Configured threat objective already belongs to another active incident."}
return {&"ok": true, &"raid_id": raid_id, &"definition": definition, &"is_replay": false}
func _prepare_event_threat_template(action: EventAction, source_id: StringName, raid_id: StringName) -> Dictionary:
var raw_definition: Variant = action.payload.get(&"definition", action.payload.get("definition", null))
if not raw_definition is Dictionary:
return {&"ok": false, &"error": "Configured threat requires its complete definition."}
var definition: Dictionary = raw_definition.duplicate(true)
# Do not silently turn service defaults into newly approved enemy values.
for field: StringName in [&"round_minutes", &"max_duration_minutes", &"defender_retreat_threshold", &"terrain_bonus", &"command_bonus", &"surprise_penalty", &"multiple_entry_penalty"]:
if typeof(definition.get(field, definition.get(String(field), null))) != TYPE_INT:
return {&"ok": false, &"error": "Configured threat requires an explicit integer: %s." % field}
var retreat_value: Variant = definition.get(&"retreat_policy", definition.get("retreat_policy", null))
if typeof(retreat_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
return {&"ok": false, &"error": "Configured threat requires an explicit retreat policy."}
for field: StringName in [&"entry_points", &"objective_ids"]:
var raw_ids: Variant = definition.get(field, definition.get(String(field), null))
if not raw_ids is Array or raw_ids.is_empty():
return {&"ok": false, &"error": "Configured threat requires explicit %s." % field}
for raw_id: Variant in raw_ids:
if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw_id).is_empty():
return {&"ok": false, &"error": "Configured threat entry/objective IDs must be nonempty names."}
var raw_waves: Variant = definition.get(&"waves", definition.get("waves", null))
if not raw_waves is Array or raw_waves.is_empty():
return {&"ok": false, &"error": "Configured threat requires explicit waves."}
for raw_wave: Variant in raw_waves:
if not raw_wave is Dictionary:
return {&"ok": false, &"error": "Configured threat wave must be a dictionary."}
for field: StringName in [&"threat_strength", &"max_rounds", &"retreat_threshold"]:
if typeof(raw_wave.get(field, raw_wave.get(String(field), null))) != TYPE_INT:
return {&"ok": false, &"error": "Configured threat wave requires an explicit integer: %s." % field}
definition[&"source_id"] = source_id
# An isolated aggregate validates and canonicalizes without touching live
# clocks, RNG, IDs, facilities, events or the queue. Time zero is only the
# canonical probe's timestamp, never the actual incident creation time.
definition.erase(&"event_source")
definition.erase("event_source")
var probe := DefenseService.new()
var valid := probe.create_raid(raid_id, 0, definition)
if not valid.is_success():
return {&"ok": false, &"error": valid.message, &"details": valid.details}
var canonical: Dictionary = (probe.create_snapshot()[&"raids"] as Array)[0]
var digest := JSON.stringify(canonical, "", true, true).sha256_text()
var canonical_raid := probe.get_raid(raid_id)
return {&"ok": true, &"definition": definition, &"digest": digest, &"objective_ids": canonical_raid[&"objective_ids"], &"canonical_raid": canonical_raid}
func _apply_event_defense_raid(action: EventAction, context: Dictionary) -> Dictionary:
var request := _build_event_defense_request(action, context)
if not bool(request.get(&"ok", false)):
return request
if bool(request.get(&"is_replay", false)):
return {&"ok": true, &"raid_id": request[&"raid_id"], &"is_replay": true}
var advanced := defense_service.advance_time(_defense_operation_minute())
if not advanced.is_success():
return {&"ok": false, &"error": advanced.message}
var created := defense_service.create_raid(request[&"raid_id"], _defense_operation_minute(), request[&"definition"])
if not created.is_success():
return {&"ok": false, &"error": created.message}
if not _validate_live_defense_crosslinks():
return {&"ok": false, &"error": "Configured event threat violates live Defense crosslinks."}
return {&"ok": true, &"raid_id": request[&"raid_id"], &"is_replay": false}
## Historical provenance is checked against the saved event definition and
## cause context, without requiring a departed guest or repaid loan to exist.
func _validate_event_defense_crosslinks(events: EventService, defense: DefenseService) -> bool:
for raid_id: StringName in defense.get_raid_ids():
var raid := defense.get_raid(raid_id)
var source: Dictionary = raid.get(&"event_source", {})
if source.is_empty():
continue
var instance_id := StringName(source["event_instance_id"])
var action_id := StringName(source["event_action_id"])
var identity := JSON.stringify([String(instance_id), String(action_id)], "", true, true)
if raid_id != StringName("event_defense_raid." + identity.sha256_text()):
return false
var instance := events.get_instance(instance_id)
if instance == null:
return false
var source_value: Variant = instance.context.get(&"threat_source_id", instance.context.get("threat_source_id", null))
if typeof(source_value) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(source_value) != raid[&"source_id"]:
return false
var definition := events.get_definition(instance.resolved_event_id)
if definition == null:
return false
var actions: Array[EventAction] = []
for collection: Array in [definition.automatic_actions, definition.fallback_actions, definition.failure_actions]:
actions.append_array(collection)
for choice: EventChoice in definition.choices:
actions.append_array(choice.actions)
var matches := false
for action: EventAction in actions:
if action.action_id != action_id or action.type != EventAction.Type.CREATE_DEFENSE_RAID:
continue
if String(action.payload.get(&"template_id", action.payload.get("template_id", ""))) != source["template_id"] or String(action.payload.get(&"source_kind", action.payload.get("source_kind", ""))).to_upper() != source["source_kind"]:
continue
var prepared := _prepare_event_threat_template(action, StringName(source_value), raid_id)
if bool(prepared.get(&"ok", false)) and String(prepared[&"digest"]) == source["template_digest"] and _event_threat_template_matches_raid(raid, prepared[&"canonical_raid"]):
matches = true
break
if not matches:
return false
return true
## Compare the immutable creation configuration to its original template.
## Combat progress, damage, roster, timestamps and player retreat-policy edits
## remain mutable and must not be compared to a newly-created probe.
func _event_threat_template_matches_raid(raid: Dictionary, canonical: Dictionary) -> bool:
for field: StringName in [&"raid_id", &"source_id", &"max_duration_minutes", &"round_minutes", &"terrain_bonus", &"command_bonus", &"surprise_penalty", &"multiple_entry_penalty", &"entry_points", &"objective_ids"]:
if not _strict_variants_equal(raid.get(field, null), canonical.get(field, null)):
return false
var waves: Array = raid.get(&"waves", [])
var template_waves: Array = canonical.get(&"waves", [])
if waves.size() != template_waves.size():
return false
for index: int in range(waves.size()):
var wave: Dictionary = waves[index]
var template_wave: Dictionary = template_waves[index]
for field: StringName in [&"wave_id", &"threat_strength", &"retreat_threshold", &"max_rounds", &"entry_points", &"objective_ids"]:
if not _strict_variants_equal(wave.get(field, null), template_wave.get(field, null)):
return false
return true