Files
TheTree_mock/autoload/simulation_runtime.gd
T
TakeshiandClaude Opus 5.5 0cfaec1626 P14-02: Add buildable Mana warp (A07)
2x2 FRONT warp at 3000G/30 materials/360 min, 15G/day and 1 Mana upkeep.
Every completed warp pair is linked by one path connector, so crossing is a
single step; a 3-Mana fare per rider is charged once in the step's tick.
Warps close while either end is unpowered, the reserve is empty or below one
fare, and blocked arrivals offer no step. Saves validate the derived links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:45:28 +09:00

13749 lines
608 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"
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)
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
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)
func _operations_total_arrears() -> int:
return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_total()
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 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 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"}
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 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 \
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 settlement_report := settlement.details.duplicate(true)
settlement_report[&"upkeep_arrears_total"] = upkeep_settlement.get_arrears_total()
settlement_report[&"arrears_total"] = arrears_total + int(settlement_report[&"upkeep_arrears_total"])
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",
]
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",
&"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
if int(summary_settlement.get(&"arrears_total", -1)) \
!= int(staff_record.get(&"unpaid_after_total", -2)) + upkeep_debt \
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 > cash 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.")
# The single balanced posting is atomic. Defer receipts until the Gold
# mirror is synchronized, also respecting any enclosing effect scope.
economy_service.begin_notification_scope()
var result := economy_service.transfer(
PLAYER_CASH_ACCOUNT, liability, amount, _runtime_event_minute(),
&"LOAN_PRINCIPAL_REPAID", loan_id, 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
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",
]
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",
&"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)
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 == WarpService.DEFINITION_ID:
_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():
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)
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, PLAYER_CASH_ACCOUNT, 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