Connect physical elevators and staff commutes to construction, saves, and cutaway UI
This commit is contained in:
+303
-17
@@ -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 := 7
|
||||
const 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
|
||||
@@ -90,6 +91,8 @@ var logistics_requests: Dictionary = {}
|
||||
var kitchen_production := KitchenProductionService.new()
|
||||
var supply_management := SupplyManagementService.new()
|
||||
var supply_logistics := SupplyLogisticsBridge.new()
|
||||
var elevators := ElevatorService.new()
|
||||
var staff_commute := StaffCommuteService.new()
|
||||
var facility_service := FacilityService.new()
|
||||
var guest_service := GuestService.new()
|
||||
var staff_service := StaffService.new()
|
||||
@@ -233,12 +236,18 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
|
||||
_initialize_rng_streams()
|
||||
supply_logistics = SupplyLogisticsBridge.new()
|
||||
supply_logistics.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(&"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"))
|
||||
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"))
|
||||
@@ -461,6 +470,7 @@ func create_snapshot() -> Dictionary:
|
||||
&"economy": economy_service.create_snapshot(),
|
||||
&"inventory": inventory_service.create_snapshot(),
|
||||
&"kitchen_production": kitchen_production.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"logistics_dispatcher": logistics_dispatcher.create_snapshot(),
|
||||
&"logistics_requests": _serialize_logistics_requests(),
|
||||
&"facilities": facility_service.create_snapshot(),
|
||||
@@ -525,6 +535,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
CONTENT_RUNTIME_SNAPSHOT_VERSION,
|
||||
OPERATIONS_RUNTIME_SNAPSHOT_VERSION,
|
||||
INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION,
|
||||
SUPPLY_RUNTIME_SNAPSHOT_VERSION,
|
||||
RUNTIME_SNAPSHOT_VERSION,
|
||||
]:
|
||||
return false
|
||||
@@ -552,8 +563,10 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure",
|
||||
])
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION:
|
||||
runtime_current_keys.append(&"kitchen_production")
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
runtime_current_keys.append(&"elevators")
|
||||
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 \
|
||||
@@ -581,9 +594,13 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
]:
|
||||
if not runtime_data.get(infrastructure_key, null) is Dictionary:
|
||||
return false
|
||||
if runtime_schema_version < RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"kitchen_production"):
|
||||
if runtime_schema_version < SUPPLY_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"kitchen_production"):
|
||||
return false
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"kitchen_production", null) is Dictionary:
|
||||
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"):
|
||||
return false
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"elevators", null) is Dictionary:
|
||||
return false
|
||||
var restored_catalog := MvpContentCatalog.new()
|
||||
var restored_catalog_report := restored_catalog.rebuild()
|
||||
@@ -1018,9 +1035,40 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
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):
|
||||
if runtime_schema_version >= 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 >= 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
|
||||
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():
|
||||
return false
|
||||
var restored_kitchen := KitchenProductionService.new()
|
||||
restored_kitchen.configure(restored_inventory, restored_facilities, definition_registry, restored_runtime_domain_minute)
|
||||
if runtime_schema_version >= RUNTIME_SNAPSHOT_VERSION:
|
||||
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():
|
||||
@@ -1075,6 +1123,8 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
facility_service = restored_facilities
|
||||
guest_service = restored_guests
|
||||
staff_service = restored_staff
|
||||
elevators = restored_elevators
|
||||
staff_commute.configure(self)
|
||||
supply_logistics.configure(self)
|
||||
mvp_content_catalog = restored_catalog
|
||||
mvp_content_report = restored_catalog_report.duplicate(true)
|
||||
@@ -1688,6 +1738,7 @@ func _build_operations_external_state() -> Dictionary:
|
||||
&"authority": authority_service.create_snapshot(),
|
||||
&"world": world_service.create_snapshot(),
|
||||
&"path": path_service.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"excavation": excavation_service.create_snapshot(),
|
||||
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
|
||||
&"route_projection": route_subslot_path_adapter.create_snapshot(),
|
||||
@@ -1721,7 +1772,8 @@ func _runtime_aggregate_committed_minute() -> int:
|
||||
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 kitchen_production.current_minute != minute \
|
||||
or elevators.current_minute != minute:
|
||||
return -1
|
||||
return minute
|
||||
|
||||
@@ -1733,7 +1785,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",
|
||||
&"staff", &"relationships", &"events", &"authority", &"world", &"path", &"elevators",
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure",
|
||||
&"movement_credits", &"domain_events", &"id_generator", &"rng_streams",
|
||||
@@ -1785,6 +1837,7 @@ func _restore_operations_external_state(token: Variant) -> bool:
|
||||
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
|
||||
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"]
|
||||
@@ -1884,6 +1937,7 @@ func _operations_external_state_matches(saved: Dictionary) -> bool:
|
||||
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(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"]) \
|
||||
@@ -5014,6 +5068,7 @@ func _capture_rank_external_state() -> Dictionary:
|
||||
return {
|
||||
&"world": world_service.create_snapshot(),
|
||||
&"path": path_service.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"excavation": excavation_service.create_snapshot(),
|
||||
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
|
||||
&"route_projection": route_subslot_path_adapter.create_snapshot(),
|
||||
@@ -5076,6 +5131,7 @@ func _rollback_rank_external_state(snapshot: Dictionary) -> bool:
|
||||
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 = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored
|
||||
restored = facility_infrastructure_projector.restore_snapshot(
|
||||
snapshot[&"facility_infrastructure"]
|
||||
@@ -5836,6 +5892,7 @@ func _execute_hire_staff_candidate(command: SimulationCommand) -> CommandResult:
|
||||
&"game_state": _state.create_snapshot(),
|
||||
&"staff": staff_service.create_snapshot(),
|
||||
&"path": path_service.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"movement_credits": _movement_credits.duplicate(true),
|
||||
&"domain_events": domain_events.create_snapshot(),
|
||||
&"id_generator": id_generator.create_snapshot(),
|
||||
@@ -5963,6 +6020,7 @@ func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
|
||||
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
|
||||
_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
|
||||
@@ -6012,6 +6070,10 @@ func _execute_set_staff_work_assignment(command: SimulationCommand) -> CommandRe
|
||||
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":
|
||||
@@ -6045,7 +6107,7 @@ func _staff_can_reach_facility(staff: StaffRuntime, facility: FacilityInstance)
|
||||
occupied.append(facility.origin)
|
||||
var candidates: Dictionary = {}
|
||||
for cell: Vector3i in occupied:
|
||||
for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
|
||||
for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
|
||||
var candidate := cell + offset
|
||||
if path_service.has_cell(candidate):
|
||||
candidates[candidate] = true
|
||||
@@ -6803,6 +6865,9 @@ 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
|
||||
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.")
|
||||
var rotation := int(command.payload.get(&"rotation", command.payload.get("rotation", 0)))
|
||||
if rotation not in definition.allowed_rotations:
|
||||
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "The selected rotation is not allowed.")
|
||||
@@ -6879,6 +6944,9 @@ 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 defense_owner: StringName = _defense_lock_owner(instance_id, &"ASSET")
|
||||
if not defense_owner.is_empty():
|
||||
return CommandResult.failed(
|
||||
@@ -6923,7 +6991,10 @@ func _execute_demolish_facility(command: SimulationCommand) -> CommandResult:
|
||||
)
|
||||
var demolished := construction_service.demolish_facility(instance_id)
|
||||
demolished.command_id = command.command_id
|
||||
if not demolished.is_success() \
|
||||
var transport_ok := false
|
||||
if demolished.is_success():
|
||||
transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).get(&"ok", false))
|
||||
if not transport_ok or not demolished.is_success() \
|
||||
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() \
|
||||
@@ -6962,6 +7033,7 @@ func _capture_infrastructure_demolition_state() -> Dictionary:
|
||||
&"economy": economy_service.create_snapshot(),
|
||||
&"world": world_service.create_snapshot(),
|
||||
&"path": path_service.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"construction": construction_service.create_snapshot(),
|
||||
&"excavation": excavation_service.create_snapshot(),
|
||||
&"spatial_subslots": spatial_subslot_service.create_snapshot(),
|
||||
@@ -6977,7 +7049,7 @@ func _capture_infrastructure_demolition_state() -> Dictionary:
|
||||
|
||||
func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"game_state", &"economy", &"world", &"path", &"construction",
|
||||
&"game_state", &"economy", &"world", &"path", &"elevators", &"construction",
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure", &"facilities", &"mana", &"movement_credits",
|
||||
&"domain_events", &"id_generator",
|
||||
@@ -6994,6 +7066,7 @@ func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
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 = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored
|
||||
restored = facility_infrastructure_projector.restore_snapshot(
|
||||
snapshot[&"facility_infrastructure"]
|
||||
@@ -7023,6 +7096,7 @@ func _infrastructure_demolition_state_matches(snapshot: Dictionary) -> bool:
|
||||
and _strict_variants_equal(economy_service.create_snapshot(), snapshot.get(&"economy", {})) \
|
||||
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(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", {})) \
|
||||
@@ -7412,6 +7486,11 @@ func _apply_completed_facility_internal(instance: Dictionary, absolute_minute: i
|
||||
&"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 not _publish_domain_event(SimulationDomainEvent.new(
|
||||
id_generator.next_id(&"domain_event"),
|
||||
&"FACILITY_COMPLETED",
|
||||
@@ -7731,7 +7810,11 @@ func _build_event_context(absolute_minute: int) -> Dictionary:
|
||||
func _advance_staff_ai(absolute_minute: int) -> void:
|
||||
if _operations_tick_blocked:
|
||||
return
|
||||
_prune_expired_staff_elevator_waits(absolute_minute)
|
||||
_advance_elevators(absolute_minute)
|
||||
staff_service.advance_one_minute(absolute_minute)
|
||||
_prune_expired_staff_elevator_waits(absolute_minute)
|
||||
staff_commute.tick(absolute_minute)
|
||||
for task: StaffTask in staff_service.get_tasks():
|
||||
if task.state == StaffTask.State.RESERVED:
|
||||
if task.state_changed_at < absolute_minute:
|
||||
@@ -7795,6 +7878,9 @@ func _advance_runtime_path_traffic(absolute_minute: int) -> void:
|
||||
)
|
||||
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":
|
||||
@@ -7816,6 +7902,10 @@ func _advance_runtime_path_traffic(absolute_minute: int) -> void:
|
||||
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
|
||||
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 \
|
||||
@@ -7918,6 +8008,8 @@ func _advance_guest_path_travel(
|
||||
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 accumulated := _movement_credit_after_one_minute(
|
||||
actor_id, guest.current_cell, absolute_minute
|
||||
)
|
||||
@@ -8022,6 +8114,8 @@ func _advance_staff_task_travel(
|
||||
actor_id, staff.staff_id, &"staff", staff.current_cell, absolute_minute, 0
|
||||
)
|
||||
return
|
||||
if _request_elevator_step(actor_id, path[0], path[1], absolute_minute, [&"staff"]):
|
||||
return
|
||||
var accumulated := _movement_credit_after_one_minute(
|
||||
actor_id, staff.current_cell, absolute_minute
|
||||
)
|
||||
@@ -8219,7 +8313,7 @@ func _estimate_staff_task_travel(staff: StaffRuntime, task: StaffTask, _absolute
|
||||
[&"staff"]
|
||||
)
|
||||
if not path.is_empty():
|
||||
return maxi(path.size() - 1, 0)
|
||||
return _estimated_actor_path_minutes(path, [&"staff"])
|
||||
return -1
|
||||
|
||||
func _sync_facility_staffing(absolute_minute: int) -> void:
|
||||
@@ -8229,8 +8323,7 @@ func _sync_facility_staffing(absolute_minute: int) -> void:
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
var active_ids: Array[StringName] = []
|
||||
for staff: StaffRuntime in staff_members:
|
||||
if staff.assigned_facility_id == facility.instance_id \
|
||||
and staff.state == StaffRuntime.State.AVAILABLE:
|
||||
if staff_commute.is_present(staff, facility):
|
||||
active_ids.append(staff.staff_id)
|
||||
facility_service.set_operational_status(
|
||||
facility.instance_id,
|
||||
@@ -8264,7 +8357,7 @@ func _project_guest_candidates(guest_id: StringName) -> Array:
|
||||
&"facility_tags": definition.tags.duplicate(),
|
||||
&"is_open": facility.state in [FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED],
|
||||
&"reachable": bool(access.get(&"reachable", false)),
|
||||
&"travel_minutes": maxi(path.size() - 1, 0),
|
||||
&"travel_minutes": _estimated_actor_path_minutes(path, []),
|
||||
&"expected_wait": facility.queue_entries.size() * facility.base_service_minutes / maxi(facility.capacity, 1),
|
||||
&"estimated_total_gold": FacilityPricing.quote(_business_base_price(facility), facility.policy_current),
|
||||
&"expected_quality": clampi(facility.base_quality, 0, 100),
|
||||
@@ -8286,7 +8379,7 @@ func _find_facility_access_path(start: Vector3i, facility: FacilityInstance) ->
|
||||
if occupied.is_empty():
|
||||
occupied.append(facility.origin)
|
||||
for cell: Vector3i in occupied:
|
||||
for direction: Vector3i in [Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
|
||||
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
|
||||
@@ -8304,7 +8397,7 @@ func _find_facility_access_path(start: Vector3i, facility: FacilityInstance) ->
|
||||
var path := path_service.find_path(start, candidate)
|
||||
if path.is_empty():
|
||||
continue
|
||||
if best_path.is_empty() or path.size() < best_path.size():
|
||||
if best_path.is_empty() or path_service.calculate_path_cost(path) < path_service.calculate_path_cost(best_path):
|
||||
best_path.assign(path)
|
||||
best_cell = candidate
|
||||
return {&"reachable": not best_path.is_empty(), &"path": best_path, &"target_cell": best_cell}
|
||||
@@ -9013,6 +9106,7 @@ func _capture_event_action_token() -> Dictionary:
|
||||
&"facilities": facility_service.create_snapshot(),
|
||||
&"guests": guest_service.create_snapshot(),
|
||||
&"path": path_service.create_snapshot(),
|
||||
&"elevators": elevators.create_snapshot(),
|
||||
&"movement_credits": _movement_credits.duplicate(true),
|
||||
&"domain_events": domain_events.create_snapshot(),
|
||||
&"id_generator": id_generator.create_snapshot(),
|
||||
@@ -10247,7 +10341,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) != PathService.SNAPSHOT_VERSION:
|
||||
if not schema_value is int or int(schema_value) not in [PathService.ELEVATOR_INDEX_SNAPSHOT_VERSION, PathService.SNAPSHOT_VERSION]:
|
||||
return false
|
||||
var raw_traffic: Variant = path_snapshot.get(
|
||||
&"cell_traffic",
|
||||
@@ -10735,6 +10829,7 @@ func _on_guest_recovery_started(
|
||||
|
||||
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()
|
||||
@@ -10764,6 +10859,7 @@ func _on_staff_registered(staff: StaffRuntime) -> void:
|
||||
|
||||
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], {
|
||||
@@ -11312,3 +11408,193 @@ func _release_closed_business_request(facility: FacilityInstance, request: Servi
|
||||
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 credit := _movement_credit_after_one_minute(actor_id, staff.current_cell, minute)
|
||||
if credit < MOVEMENT_CREDIT_SCALE:
|
||||
_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]
|
||||
_movement_credits[actor_id] = {&"credit": credit - MOVEMENT_CREDIT_SCALE, &"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:
|
||||
for shaft: Dictionary in elevators.get_report()[&"shafts"]:
|
||||
var powered := true
|
||||
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)
|
||||
|
||||
|
||||
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})
|
||||
|
||||
|
||||
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()):
|
||||
if elevators.owns_connector(path_service.get_step_connector(path[index - 1], path[index], access)):
|
||||
return ceili(float(path_service.calculate_path_cost(path, access)) / 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"]:
|
||||
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 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"})
|
||||
|
||||
Reference in New Issue
Block a user