Add explicit stairs and measure elevator congestion with real actors
This commit is contained in:
+195
-24
@@ -15,7 +15,8 @@ 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 := 8
|
||||
const 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
|
||||
@@ -41,6 +42,7 @@ const DEFAULT_OPERATIONS_DEMAND_PROFILES := [
|
||||
{&"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
|
||||
@@ -92,6 +94,7 @@ var kitchen_production := KitchenProductionService.new()
|
||||
var supply_management := SupplyManagementService.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()
|
||||
@@ -190,6 +193,9 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
|
||||
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()
|
||||
@@ -376,7 +382,8 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
|
||||
Callable(self, "_process_save_checkpoints")
|
||||
)
|
||||
_initialized = true
|
||||
var report := definition_registry.rebuild()
|
||||
var report := definition_registry.rebuild_from_paths(
|
||||
FacilityCatalog.FACILITY_PATHS + FacilityCatalog.INFRASTRUCTURE_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
|
||||
@@ -471,6 +478,7 @@ func create_snapshot() -> Dictionary:
|
||||
&"inventory": inventory_service.create_snapshot(),
|
||||
&"kitchen_production": kitchen_production.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(),
|
||||
@@ -536,6 +544,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
OPERATIONS_RUNTIME_SNAPSHOT_VERSION,
|
||||
INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION,
|
||||
SUPPLY_RUNTIME_SNAPSHOT_VERSION,
|
||||
TRANSPORT_RUNTIME_SNAPSHOT_VERSION,
|
||||
RUNTIME_SNAPSHOT_VERSION,
|
||||
]:
|
||||
return false
|
||||
@@ -565,8 +574,10 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
])
|
||||
if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION:
|
||||
runtime_current_keys.append(&"kitchen_production")
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION:
|
||||
runtime_current_keys.append(&"elevators")
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
runtime_current_keys.append(&"legacy_stairs")
|
||||
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 \
|
||||
@@ -598,9 +609,13 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
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 < RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"elevators"):
|
||||
if runtime_schema_version < TRANSPORT_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"elevators"):
|
||||
return false
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"elevators", null) is Dictionary:
|
||||
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"elevators", null) is Dictionary:
|
||||
return false
|
||||
if runtime_schema_version < RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"legacy_stairs"):
|
||||
return false
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"legacy_stairs", null) is Dictionary:
|
||||
return false
|
||||
var restored_catalog := MvpContentCatalog.new()
|
||||
var restored_catalog_report := restored_catalog.rebuild()
|
||||
@@ -719,6 +734,20 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
restored_route_adapter, restored_spatial, restored_path
|
||||
):
|
||||
return false
|
||||
if runtime_schema_version < 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 >= 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
|
||||
@@ -1042,7 +1071,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
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):
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
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
|
||||
@@ -1053,7 +1082,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
{&"active_staff_ids": present_ids}, restored_runtime_domain_minute)
|
||||
var restored_elevators := ElevatorService.new()
|
||||
restored_elevators.configure(restored_path)
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION:
|
||||
if not restored_elevators.restore_snapshot(runtime_data[&"elevators"]):
|
||||
return false
|
||||
else:
|
||||
@@ -1124,6 +1153,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
guest_service = restored_guests
|
||||
staff_service = restored_staff
|
||||
elevators = restored_elevators
|
||||
legacy_stairs = restored_legacy_stairs
|
||||
staff_commute.configure(self)
|
||||
supply_logistics.configure(self)
|
||||
mvp_content_catalog = restored_catalog
|
||||
@@ -1739,6 +1769,7 @@ func _build_operations_external_state() -> Dictionary:
|
||||
&"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(),
|
||||
@@ -1785,7 +1816,7 @@ func _restore_operations_external_state(token: Variant) -> bool:
|
||||
var saved: Dictionary = token
|
||||
var required_dictionary_keys: Array[StringName] = [
|
||||
&"game_state", &"command_queue", &"economy", &"inventory", &"kitchen_production", &"facilities", &"guests",
|
||||
&"staff", &"relationships", &"events", &"authority", &"world", &"path", &"elevators",
|
||||
&"staff", &"relationships", &"events", &"authority", &"world", &"path", &"elevators", &"legacy_stairs",
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure",
|
||||
&"movement_credits", &"domain_events", &"id_generator", &"rng_streams",
|
||||
@@ -1838,6 +1869,7 @@ func _restore_operations_external_state(token: Variant) -> bool:
|
||||
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"]
|
||||
@@ -1938,6 +1970,7 @@ func _operations_external_state_matches(saved: Dictionary) -> bool:
|
||||
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"]) \
|
||||
@@ -4681,8 +4714,8 @@ func rebuild_path_graph_from_world() -> bool:
|
||||
),
|
||||
walkable_cells
|
||||
)
|
||||
return synchronized.is_success()
|
||||
return path_service.rebuild_graph(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)
|
||||
@@ -5069,6 +5102,7 @@ func _capture_rank_external_state() -> Dictionary:
|
||||
&"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(),
|
||||
@@ -5132,6 +5166,7 @@ func _rollback_rank_external_state(snapshot: Dictionary) -> bool:
|
||||
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"]
|
||||
@@ -5146,6 +5181,8 @@ func _rollback_rank_external_state(snapshot: Dictionary) -> bool:
|
||||
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"]
|
||||
)
|
||||
@@ -5893,6 +5930,7 @@ func _execute_hire_staff_candidate(command: SimulationCommand) -> CommandResult:
|
||||
&"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(),
|
||||
@@ -6021,6 +6059,7 @@ func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
|
||||
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
|
||||
@@ -6865,6 +6904,8 @@ func _execute_build_facility(command: SimulationCommand) -> CommandResult:
|
||||
if origin_value == null:
|
||||
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.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.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.command_id, &"ELEVATOR_BUSY", "Wait until the shaft is idle before extending it.")
|
||||
@@ -6947,6 +6988,13 @@ func _execute_demolish_facility(command: SimulationCommand) -> CommandResult:
|
||||
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(
|
||||
@@ -6993,8 +7041,9 @@ func _execute_demolish_facility(command: SimulationCommand) -> CommandResult:
|
||||
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))
|
||||
transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).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() \
|
||||
@@ -7034,6 +7083,7 @@ func _capture_infrastructure_demolition_state() -> Dictionary:
|
||||
&"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(),
|
||||
@@ -7049,7 +7099,7 @@ func _capture_infrastructure_demolition_state() -> Dictionary:
|
||||
|
||||
func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"game_state", &"economy", &"world", &"path", &"elevators", &"construction",
|
||||
&"game_state", &"economy", &"world", &"path", &"elevators", &"legacy_stairs", &"construction",
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure", &"facilities", &"mana", &"movement_credits",
|
||||
&"domain_events", &"id_generator",
|
||||
@@ -7067,6 +7117,7 @@ func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
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"]
|
||||
@@ -7097,6 +7148,7 @@ func _infrastructure_demolition_state_matches(snapshot: Dictionary) -> bool:
|
||||
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", {})) \
|
||||
@@ -7491,6 +7543,8 @@ func _apply_completed_facility_internal(instance: Dictionary, absolute_minute: i
|
||||
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 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",
|
||||
@@ -7965,7 +8019,7 @@ func _record_blocked_actor_wait(
|
||||
_report_path_integration_failure(actor_id, entity_id, kind, cell)
|
||||
return
|
||||
_movement_credits[actor_id] = {
|
||||
&"credit": clampi(credit, 0, MOVEMENT_CREDIT_SCALE - 1),
|
||||
&"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:
|
||||
@@ -8010,10 +8064,12 @@ func _advance_guest_path_travel(
|
||||
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
|
||||
)
|
||||
if accumulated < MOVEMENT_CREDIT_SCALE:
|
||||
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if accumulated < required_credit:
|
||||
_record_blocked_actor_wait(
|
||||
actor_id,
|
||||
guest.guest_id,
|
||||
@@ -8037,7 +8093,7 @@ func _advance_guest_path_travel(
|
||||
guest.recovery_kind = previous_recovery_kind
|
||||
_report_path_integration_failure(actor_id, guest.guest_id, &"guest", previous_cell)
|
||||
return
|
||||
var remaining_credit := accumulated - MOVEMENT_CREDIT_SCALE
|
||||
var remaining_credit := accumulated - required_credit
|
||||
_movement_credits[actor_id] = {
|
||||
&"credit": remaining_credit,
|
||||
&"updated_at": absolute_minute,
|
||||
@@ -8116,10 +8172,12 @@ func _advance_staff_task_travel(
|
||||
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
|
||||
)
|
||||
if accumulated < MOVEMENT_CREDIT_SCALE:
|
||||
accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if accumulated < required_credit:
|
||||
_record_blocked_actor_wait(
|
||||
actor_id,
|
||||
staff.staff_id,
|
||||
@@ -8135,7 +8193,7 @@ func _advance_staff_task_travel(
|
||||
staff.current_cell = previous_cell
|
||||
_report_path_integration_failure(actor_id, staff.staff_id, &"staff", previous_cell)
|
||||
return
|
||||
var remaining_credit := accumulated - MOVEMENT_CREDIT_SCALE
|
||||
var remaining_credit := accumulated - required_credit
|
||||
_movement_credits[actor_id] = {
|
||||
&"credit": remaining_credit,
|
||||
&"updated_at": absolute_minute,
|
||||
@@ -9107,6 +9165,7 @@ func _capture_event_action_token() -> Dictionary:
|
||||
&"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(),
|
||||
@@ -9124,6 +9183,8 @@ func _restore_event_action_token(token: Variant) -> Dictionary:
|
||||
var facilities_ok: bool = facility_service.restore_snapshot(token.get(&"facilities", {}))
|
||||
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:
|
||||
@@ -10202,7 +10263,7 @@ func _deserialize_movement_credits(raw_entries: Array, absolute_minute: int) ->
|
||||
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 >= MOVEMENT_CREDIT_SCALE \
|
||||
or credit < 0 or credit >= STAIR_MOVEMENT_CREDIT \
|
||||
or updated_at < 0 or updated_at > absolute_minute:
|
||||
return null
|
||||
restored[actor_id] = {
|
||||
@@ -10311,7 +10372,7 @@ func _validate_runtime_path_crosslinks(
|
||||
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 >= MOVEMENT_CREDIT_SCALE \
|
||||
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 \
|
||||
@@ -10321,7 +10382,8 @@ func _validate_runtime_path_crosslinks(
|
||||
stuck_reported_at < last_progress_at or stuck_reported_at > last_observed_at
|
||||
)):
|
||||
return false
|
||||
expected_density[cell] = int(expected_density.get(cell, 0)) + 1
|
||||
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
|
||||
@@ -10341,7 +10403,7 @@ func _path_snapshot_cells_match_world(
|
||||
&"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.SNAPSHOT_VERSION]:
|
||||
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",
|
||||
@@ -11430,8 +11492,10 @@ func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, 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)
|
||||
if credit < MOVEMENT_CREDIT_SCALE:
|
||||
credit = mini(credit, required_credit + MOVEMENT_CREDIT_SCALE - 1)
|
||||
if credit < required_credit:
|
||||
_record_expected_actor_wait(actor_id, staff.current_cell, minute)
|
||||
_movement_credits[actor_id] = {&"credit": credit, &"updated_at": minute}
|
||||
return {&"ok": true, &"moved": false}
|
||||
@@ -11440,7 +11504,7 @@ func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, minute:
|
||||
_report_path_integration_failure(actor_id, staff_id, &"staff", previous)
|
||||
return {&"ok": false}
|
||||
staff.current_cell = path[1]
|
||||
_movement_credits[actor_id] = {&"credit": credit - MOVEMENT_CREDIT_SCALE, &"updated_at": minute}
|
||||
_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"})
|
||||
@@ -11464,6 +11528,10 @@ func _request_elevator_step(actor_id: StringName, from: Vector3i, to: Vector3i,
|
||||
|
||||
|
||||
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 := true
|
||||
for module_id: StringName in shaft[&"module_ids"]:
|
||||
@@ -11472,6 +11540,12 @@ func _advance_elevators(minute: int) -> void:
|
||||
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:
|
||||
@@ -11575,7 +11649,8 @@ func is_staff_in_transit(staff_id: StringName) -> bool:
|
||||
|
||||
func _estimated_actor_path_minutes(path: Array, access: Array[StringName]) -> int:
|
||||
for index: int in range(1, path.size()):
|
||||
if elevators.owns_connector(path_service.get_step_connector(path[index - 1], path[index], access)):
|
||||
if elevators.owns_connector(path_service.get_step_connector(path[index - 1], path[index], access)) \
|
||||
or _step_movement_credit(path_service, path[index - 1], path[index], access) > MOVEMENT_CREDIT_SCALE:
|
||||
return ceili(float(path_service.calculate_path_cost(path, access)) / PathService.ORTHOGONAL_MOVE_COST)
|
||||
return maxi(path.size() - 1, 0)
|
||||
|
||||
@@ -11598,3 +11673,99 @@ func _prune_expired_staff_elevator_waits(minute: int) -> void:
|
||||
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:
|
||||
var connector_id := path.get_step_connector(from, to, access)
|
||||
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 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)
|
||||
|
||||
Reference in New Issue
Block a user