Add explicit stairs and measure elevator congestion with real actors
This commit is contained in:
@@ -44,6 +44,18 @@ const FACILITY_PATHS: Array[String] = [
|
||||
"res://data/facilities/adult_private_room.tres",
|
||||
]
|
||||
|
||||
## Required movement infrastructure added after the original sixteen-facility
|
||||
## catalog. Separate paths preserve the baseline design table and legacy scans.
|
||||
const INFRASTRUCTURE_FACILITY_IDS: Array[StringName] = [
|
||||
&"guest_stairs",
|
||||
&"service_stairs",
|
||||
]
|
||||
|
||||
const INFRASTRUCTURE_FACILITY_PATHS: Array[String] = [
|
||||
"res://data/facilities/infrastructure/guest_stairs.tres",
|
||||
"res://data/facilities/infrastructure/service_stairs.tres",
|
||||
]
|
||||
|
||||
## Optional post-MVP expansion definitions. They live below a subdirectory so
|
||||
## DefinitionRegistry's baseline directory scan remains exactly compatible with
|
||||
## existing saves and the sixteen-facility acceptance fixture.
|
||||
@@ -101,6 +113,14 @@ static func load_expansion() -> Array[FacilityDefinition]:
|
||||
result.append(definition)
|
||||
return result
|
||||
|
||||
static func load_infrastructure() -> Array[FacilityDefinition]:
|
||||
var result: Array[FacilityDefinition] = []
|
||||
for path in INFRASTRUCTURE_FACILITY_PATHS:
|
||||
var definition := load(path) as FacilityDefinition
|
||||
if definition != null:
|
||||
result.append(definition)
|
||||
return result
|
||||
|
||||
static func load_available(include_expansion := false) -> Array[FacilityDefinition]:
|
||||
var result := load_all()
|
||||
if include_expansion:
|
||||
@@ -108,7 +128,9 @@ static func load_available(include_expansion := false) -> Array[FacilityDefiniti
|
||||
return result
|
||||
|
||||
static func by_id(facility_id: StringName) -> FacilityDefinition:
|
||||
for definition in load_available(true):
|
||||
var definitions := load_available(true)
|
||||
definitions.append_array(load_infrastructure())
|
||||
for definition in definitions:
|
||||
if definition.id == facility_id:
|
||||
return definition
|
||||
return null
|
||||
|
||||
@@ -82,12 +82,15 @@ func _read_progress() -> Dictionary:
|
||||
if reception == null:
|
||||
return _build_step(&"reception", &"reception", "1/6 受付をつくる", "表の地表階に受付を配置します。完成した受付の上に客室を増築できます。", &"build_reception")
|
||||
if rooms.is_empty():
|
||||
return _build_step(&"room", &"standard_room", "2/6 客室をつくる", "受付が支えになる位置に一般客室を配置します。", &"build_room")
|
||||
return _build_step(&"room", &"standard_room", "2/6 客室をつくる", "受付が支えになる位置に一般客室を配置します。上階へは客用階段で道をつなぎます。", &"build_room")
|
||||
var accessible_room := false
|
||||
for room: FacilityInstance in rooms:
|
||||
accessible_room = accessible_room or room.access_available
|
||||
if not reception.access_available or not accessible_room:
|
||||
return _step(&"access", "3/6 道をつなぐ", "客室の出入口まで客用通路をつなぎます。施設を選択すると、通行不可など営業できない理由を確認できます。", &"build_corridor", "客用通路を選ぶ")
|
||||
accessible_room = accessible_room or _can_reach_facility(room)
|
||||
if not _can_reach_facility(reception) or not accessible_room:
|
||||
var upper_room := false
|
||||
for room: FacilityInstance in rooms:
|
||||
upper_room = upper_room or room.origin.y != 0
|
||||
return _step(&"access", "3/6 道をつなぐ", "上下階は客用階段でつなぎます。上階のマスに置くと、直下の通路へ接続します。同じ階は客用通路を延ばし、客室の左右の出入口まで道をつなぎましょう。", &"build_stairs" if upper_room else &"build_corridor", "客用階段を選ぶ" if upper_room else "客用通路を選ぶ")
|
||||
if not _has_reception_assignment(reception):
|
||||
return _step(&"staff", "4/6 受付係を配属", "従業員から受付係を雇い、雇用済み一覧でこの受付と勤務帯を選び「配属・勤務帯を変更」を押します。", &"staff", "従業員を開く")
|
||||
if first_sale < 0:
|
||||
@@ -95,8 +98,19 @@ func _read_progress() -> Dictionary:
|
||||
return _step(&"close", "6/6 翌朝の決算へ", "最初の宿泊売上が入りました。06:00に営業日が締まり、給与・利益・未払いを帳簿で確認できます。", &"resume" if _clock.paused else &"ledger", "時間を進める" if _clock.paused else "帳簿を見る")
|
||||
|
||||
|
||||
func _can_reach_facility(facility: FacilityInstance) -> bool:
|
||||
if not facility.access_available:
|
||||
return false
|
||||
# Runtime uses the same public entrance and access cells as real guests.
|
||||
# Component-only guide fixtures retain their explicit availability flags.
|
||||
if _runtime.has_method(&"get_facility_access_report"):
|
||||
var report: Dictionary = _runtime.call(&"get_facility_access_report", facility.instance_id)
|
||||
return bool(report.get(&"reachable", false))
|
||||
return true
|
||||
|
||||
|
||||
func _reception_score(facility: FacilityInstance) -> int:
|
||||
return (2 if facility.access_available else 0) + (1 if _has_reception_assignment(facility) else 0)
|
||||
return (2 if _can_reach_facility(facility) else 0) + (1 if _has_reception_assignment(facility) else 0)
|
||||
|
||||
|
||||
func _has_reception_assignment(reception: FacilityInstance) -> bool:
|
||||
|
||||
@@ -17,6 +17,9 @@ static func tile(canvas: CanvasItem, texture: Texture2D, index: int, target: Rec
|
||||
|
||||
static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float, dungeon: Texture2D, town: Texture2D, back: bool, decoration_seed := 0) -> void:
|
||||
var p := pixel
|
||||
if id in [&"guest_stairs", &"service_stairs"]:
|
||||
stairs(canvas, rect, p, id == &"service_stairs")
|
||||
return
|
||||
if id in [&"guest_corridor", &"service_corridor"]:
|
||||
canvas.draw_rect(rect, Color("c6b999") if not back else Color("a6b6a0"))
|
||||
canvas.draw_rect(Rect2(rect.position.x,rect.end.y-4*p,rect.size.x,3*p),WOOD_LIGHT)
|
||||
@@ -101,6 +104,30 @@ static func room(canvas: CanvasItem, rect: Rect2, id: StringName, pixel: float,
|
||||
table(canvas, Vector2(mid-6*p, floor_y-7*p), p)
|
||||
|
||||
|
||||
## The upper landing owns a short flight down to the floor below. Six square
|
||||
## treads and a stepped handrail keep the shape legible at the 16 px room scale.
|
||||
static func stairs(canvas: CanvasItem, rect: Rect2, p: float, staff_only: bool, compact := false) -> void:
|
||||
if not compact:
|
||||
canvas.draw_rect(rect, INK)
|
||||
canvas.draw_rect(rect.grow(-p), Color("c7c6a6") if not staff_only else Color("a8b6aa"))
|
||||
canvas.draw_rect(Rect2(rect.position.x, rect.end.y-4*p, rect.size.x, 3*p), WOOD)
|
||||
canvas.draw_rect(Rect2(rect.position.x, rect.end.y-p, rect.size.x, p), INK)
|
||||
var at := Vector2(rect.get_center().x-6*p, rect.end.y-5*p)
|
||||
var tread := Color("d8bf89") if not staff_only else Color("aec0bf")
|
||||
var rise := Color("8a7155") if not staff_only else Color("617f82")
|
||||
for step: int in range(6):
|
||||
var top := at + Vector2(step*2, -step*2)*p
|
||||
canvas.draw_rect(Rect2(top, Vector2(2, 2+step*2)*p), rise)
|
||||
canvas.draw_rect(Rect2(top, Vector2(3, 1)*p), tread)
|
||||
canvas.draw_rect(Rect2(top+Vector2(0,-5)*p, Vector2(2,1)*p), INK)
|
||||
if step % 2 == 0:
|
||||
canvas.draw_rect(Rect2(top+Vector2(0,-4)*p, Vector2(1,4)*p), WOOD if not staff_only else TEAL)
|
||||
canvas.draw_rect(Rect2(at+Vector2(10,-11)*p, Vector2(3,2)*p), tread)
|
||||
if staff_only and not compact:
|
||||
canvas.draw_rect(Rect2(rect.position+Vector2(2,2)*p, Vector2(3,3)*p), TEAL)
|
||||
canvas.draw_rect(Rect2(rect.position+Vector2(3,3)*p, Vector2(1,1)*p), CREAM)
|
||||
|
||||
|
||||
## Static shaft rails remain when a car is stopped or the transport service is
|
||||
## unavailable. The car itself is drawn once from its authoritative trip state.
|
||||
static func elevator_shaft(canvas: CanvasItem, rect: Rect2, p: float, back: bool) -> void:
|
||||
|
||||
@@ -31,26 +31,26 @@ func prepare(runtime: Variant, clock: Variant, tree: SceneTree) -> bool:
|
||||
return false
|
||||
for facility: Dictionary in _runtime.construction_service.get_facilities():
|
||||
facility_ids[facility[&"definition_id"]] = facility[&"instance_id"]
|
||||
# Four rooms sit on the completed reception and dining structure. Corridor
|
||||
# cells are ordinary supported construction; unfinished elevator topology is
|
||||
# deliberately excluded from this scenario.
|
||||
# Four rooms sit on the completed reception and dining structure. One
|
||||
# continuous, paid staircase in the center joins all three upper landings;
|
||||
# same-floor corridor construction links the rooms on either side.
|
||||
for x: int in [-8, -6, 2, 4]:
|
||||
if not _build(&"standard_room", Vector3i(x, 2, 0)):
|
||||
return false
|
||||
for x: int in range(-4, 2):
|
||||
if not _build(&"guest_corridor", Vector3i(x, 1, 0)):
|
||||
if not _build(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 1, 0)):
|
||||
return false
|
||||
if not await advance(60):
|
||||
return false
|
||||
for x: int in range(-4, 2):
|
||||
if not _build(&"guest_corridor", Vector3i(x, 2, 0)):
|
||||
if not _build(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 2, 0)):
|
||||
return false
|
||||
if not await advance(10):
|
||||
return false
|
||||
# This walkway follows the upper side of the rooms. Its authoritative Path
|
||||
# projection allows real guests and workers to move between the three levels.
|
||||
# This walkway follows the upper side of the rooms. The final stair segment
|
||||
# gives real guests and the cleaner a legal route to this floor.
|
||||
for x: int in range(-8, 6):
|
||||
if not _build(&"guest_corridor", Vector3i(x, 3, 0)):
|
||||
if not _build(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 3, 0)):
|
||||
return false
|
||||
if not await advance(10):
|
||||
return false
|
||||
|
||||
@@ -33,12 +33,16 @@ const KIND_SERVICE_CORRIDOR: StringName = &"service_corridor"
|
||||
const KIND_STAFF_DOOR: StringName = &"staff_door"
|
||||
const KIND_GUEST_ELEVATOR: StringName = &"guest_elevator"
|
||||
const KIND_SERVICE_ELEVATOR: StringName = &"service_elevator"
|
||||
const KIND_GUEST_STAIRS: StringName = &"guest_stairs"
|
||||
const KIND_SERVICE_STAIRS: StringName = &"service_stairs"
|
||||
const SUPPORTED_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_CORRIDOR,
|
||||
KIND_SERVICE_CORRIDOR,
|
||||
KIND_STAFF_DOOR,
|
||||
KIND_GUEST_ELEVATOR,
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
KIND_GUEST_STAIRS,
|
||||
KIND_SERVICE_STAIRS,
|
||||
]
|
||||
const KNOWN_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_CORRIDOR,
|
||||
@@ -46,10 +50,14 @@ const KNOWN_KINDS: Array[StringName] = [
|
||||
KIND_STAFF_DOOR,
|
||||
KIND_GUEST_ELEVATOR,
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
KIND_GUEST_STAIRS,
|
||||
KIND_SERVICE_STAIRS,
|
||||
]
|
||||
|
||||
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = []
|
||||
const VERTICAL_KINDS: Array[StringName] = [KIND_GUEST_ELEVATOR, KIND_SERVICE_ELEVATOR]
|
||||
const STAIR_KINDS: Array[StringName] = [KIND_GUEST_STAIRS, KIND_SERVICE_STAIRS]
|
||||
const CONNECTOR_KINDS: Array[StringName] = [KIND_STAFF_DOOR, KIND_GUEST_STAIRS, KIND_SERVICE_STAIRS]
|
||||
const REASON_UNSUPPORTED_VERTICAL_TOPOLOGY: StringName = &"UNSUPPORTED_VERTICAL_TOPOLOGY"
|
||||
const QUARANTINE_TRANSACTION_ROLLBACK: StringName = &"TRANSACTION_ROLLBACK_UNCERTAIN"
|
||||
const QUARANTINE_RESTORE_ROLLBACK: StringName = &"RESTORE_ROLLBACK_UNCERTAIN"
|
||||
@@ -65,6 +73,7 @@ const ADAPTER_PROJECT_REQUEST_PREFIX := "infra.project:"
|
||||
const ADAPTER_UNPROJECT_REQUEST_PREFIX := "infra.unproject:"
|
||||
const STAFF_ACCESS_ID: StringName = &"staff"
|
||||
const STAFF_DOOR_CONNECTOR_COST := 120
|
||||
const STAIR_CONNECTOR_COST := 150
|
||||
|
||||
const _INSTANCE_KEYS: Array[StringName] = [
|
||||
&"instance_id", &"definition_id", &"origin", &"rotation", &"layer",
|
||||
@@ -193,6 +202,8 @@ func project_facility(
|
||||
return _failure(CommandResultType.Code.DUPLICATE_COMMAND_ID, "Stable infrastructure ID collides with existing authoritative state.")
|
||||
if not _dependencies_match_current_state():
|
||||
return _failure(CommandResultType.Code.TRANSACTION_CLOSED, "Facility infrastructure dependencies are stale or invalid.")
|
||||
if kind in STAIR_KINDS and not _path.has_cell(_stair_lower_landing(record)):
|
||||
return _failure(CommandResultType.Code.VALIDATION_FAILED, "Stairs require an existing passable landing directly below their origin.")
|
||||
|
||||
var owned_before := _capture_owned_state()
|
||||
_transaction_active = true
|
||||
@@ -218,23 +229,22 @@ func project_facility(
|
||||
dependency_capture,
|
||||
"ROUTE projection failed or reentrant callback tampering was detected."
|
||||
)
|
||||
if kind == KIND_STAFF_DOOR:
|
||||
var back_cell: Vector3i = record[&"cells"][0]
|
||||
var front_cell := Vector3i(back_cell.x, back_cell.y, PathType.FRONT)
|
||||
if kind in CONNECTOR_KINDS:
|
||||
var connector := _expected_stair_connector(record) if kind in STAIR_KINDS else _expected_staff_connector(record)
|
||||
var connector_ok := _path.configure_connector(
|
||||
StringName(record[&"connector_id"]),
|
||||
front_cell,
|
||||
back_cell,
|
||||
STAFF_DOOR_CONNECTOR_COST,
|
||||
connector[&"from"],
|
||||
connector[&"to"],
|
||||
connector[&"cost"],
|
||||
true,
|
||||
STAFF_ACCESS_ID,
|
||||
connector[&"access_id"],
|
||||
true
|
||||
)
|
||||
if not connector_ok or not _owned_state_matches(guard_state):
|
||||
return _rollback_failure(
|
||||
owned_before,
|
||||
dependency_capture,
|
||||
"Staff-door connector projection failed or callback tampering was detected."
|
||||
"Infrastructure connector projection failed or callback tampering was detected."
|
||||
)
|
||||
if kind in VERTICAL_KINDS:
|
||||
var shaft_cells: Array[Vector3i] = []
|
||||
@@ -327,7 +337,7 @@ func unproject_facility(request_id_value: Variant, instance_id_value: Variant) -
|
||||
return _rollback_failure(
|
||||
owned_before,
|
||||
dependency_capture,
|
||||
"Staff-door connector removal failed or callback tampering was detected."
|
||||
"Infrastructure connector removal failed or callback tampering was detected."
|
||||
)
|
||||
if record[&"kind"] in VERTICAL_KINDS:
|
||||
if not _path.remove_vertical_walk_blockers(StringName(SHAFT_OWNER_PREFIX + String(instance_id))) or not _owned_state_matches(guard_state):
|
||||
@@ -396,6 +406,21 @@ func get_projection_ids() -> Array[StringName]:
|
||||
return _sorted_names(_projections)
|
||||
|
||||
|
||||
## Before removing a ROUTE/base cell, the owner can reject demolition that
|
||||
## would leave a staircase without a lower landing. Overlapping remaining
|
||||
## passable sources may still keep that landing valid.
|
||||
func get_stair_dependents(cells: Array[Vector3i]) -> Array[StringName]:
|
||||
var selected: Dictionary = {}
|
||||
for cell: Vector3i in cells:
|
||||
selected[cell] = true
|
||||
var result: Array[StringName] = []
|
||||
for instance_id: StringName in _sorted_names(_projections):
|
||||
var record: Dictionary = _projections[instance_id]
|
||||
if record[&"kind"] in STAIR_KINDS and selected.has(_stair_lower_landing(record)):
|
||||
result.append(instance_id)
|
||||
return result
|
||||
|
||||
|
||||
func get_processed_request_ids() -> Array[StringName]:
|
||||
return _sorted_names(_processed_requests)
|
||||
|
||||
@@ -511,6 +536,7 @@ func _canonicalize_live_project_input(definition_value: Variant, instance_value:
|
||||
or definition.footprint != schema[&"footprint"]
|
||||
or definition.pass_through != bool(schema[&"pass_through"])
|
||||
or canonical_tags != schema[&"tags"]
|
||||
or (definition.id in STAIR_KINDS and definition.allowed_rotations != [0])
|
||||
):
|
||||
return {&"ok": false, &"message": "Facility definition layer, footprint, pass-through flag, or tags diverge from its infrastructure schema."}
|
||||
var canonical_definition := {
|
||||
@@ -565,6 +591,7 @@ func _canonicalize_live_instance(
|
||||
if not _cell_is_valid(origin) \
|
||||
or rotation not in [0, 90, 180, 270] \
|
||||
or rotation not in allowed_rotations \
|
||||
or (definition[&"id"] in STAIR_KINDS and (rotation != 0 or not _cell_is_valid(origin + Vector3i(0, -1, 0)))) \
|
||||
or layer != int(definition[&"layer"]) \
|
||||
or origin.z != layer \
|
||||
or completed_at < 0 \
|
||||
@@ -601,6 +628,17 @@ func _canonicalize_live_instance(
|
||||
|
||||
static func _kind_schema(kind: StringName) -> Dictionary:
|
||||
match kind:
|
||||
KIND_GUEST_STAIRS, KIND_SERVICE_STAIRS:
|
||||
var staff_only := kind == KIND_SERVICE_STAIRS
|
||||
var tags: Array[StringName] = [&"movement", &"route_subslot", &"schema_facility_proxy", &"stairs", &"vertical_connector"]
|
||||
tags.append(&"staff_only" if staff_only else &"guest_access")
|
||||
return {
|
||||
&"layer": PathType.BACK if staff_only else PathType.FRONT,
|
||||
&"footprint": Vector2i.ONE,
|
||||
&"pass_through": true,
|
||||
&"staff_only": staff_only,
|
||||
&"tags": _sorted_name_array(tags),
|
||||
}
|
||||
KIND_GUEST_CORRIDOR:
|
||||
return {
|
||||
&"layer": PathType.FRONT,
|
||||
@@ -664,7 +702,7 @@ static func _projection_from_project_input(input: Dictionary) -> Dictionary:
|
||||
&"definition_id": kind,
|
||||
&"kind": kind,
|
||||
&"route_entity_id": StringName(ROUTE_ENTITY_PREFIX + String(instance_id)),
|
||||
&"connector_id": StringName(CONNECTOR_PREFIX + String(instance_id)) if kind == KIND_STAFF_DOOR else &"",
|
||||
&"connector_id": StringName(CONNECTOR_PREFIX + String(instance_id)) if kind in CONNECTOR_KINDS else &"",
|
||||
&"layer": int(definition[&"layer"]),
|
||||
&"cells": (instance[&"occupied_cells"] as Array).duplicate(),
|
||||
&"staff_only": bool(_kind_schema(kind)[&"staff_only"]),
|
||||
@@ -973,6 +1011,11 @@ func _projection_live_matches(record: Dictionary) -> bool:
|
||||
or _source.get_entity_cells(route_entity_id) != record[&"cells"]:
|
||||
return false
|
||||
var connector_id: StringName = record[&"connector_id"]
|
||||
if record[&"kind"] in STAIR_KINDS:
|
||||
return _path.has_cell(_stair_lower_landing(record)) \
|
||||
and not connector_id.is_empty() \
|
||||
and _path.has_connector(connector_id) \
|
||||
and _path.get_connector(connector_id) == _expected_stair_connector(record)
|
||||
if record[&"kind"] == KIND_STAFF_DOOR:
|
||||
if connector_id.is_empty() or not _path.has_connector(connector_id):
|
||||
return false
|
||||
@@ -1010,6 +1053,21 @@ static func _expected_staff_connector(record: Dictionary) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
static func _stair_lower_landing(record: Dictionary) -> Vector3i:
|
||||
return (record[&"origin"] as Vector3i) + Vector3i(0, -1, 0)
|
||||
|
||||
|
||||
static func _expected_stair_connector(record: Dictionary) -> Dictionary:
|
||||
var connector := _expected_staff_connector(record)
|
||||
var upper_cell: Vector3i = record[&"origin"]
|
||||
connector[&"from"] = upper_cell
|
||||
connector[&"to"] = _stair_lower_landing(record)
|
||||
connector[&"cost"] = STAIR_CONNECTOR_COST
|
||||
connector[&"access_id"] = STAFF_ACCESS_ID if record[&"kind"] == KIND_SERVICE_STAIRS else &""
|
||||
connector[&"current_floor"] = upper_cell.y
|
||||
return connector
|
||||
|
||||
|
||||
static func _has_managed_namespace_collision(
|
||||
source_service: SpatialSubslotService,
|
||||
path_service: PathService,
|
||||
@@ -1257,7 +1315,7 @@ static func _parse_projection(raw: Variant) -> Dictionary:
|
||||
or kind not in SUPPORTED_KINDS \
|
||||
or definition_id != kind \
|
||||
or route_entity_id != StringName(ROUTE_ENTITY_PREFIX + String(instance_id)) \
|
||||
or connector_id != (StringName(CONNECTOR_PREFIX + String(instance_id)) if kind == KIND_STAFF_DOOR else &"") \
|
||||
or connector_id != (StringName(CONNECTOR_PREFIX + String(instance_id)) if kind in CONNECTOR_KINDS else &"") \
|
||||
or not _name_is_valid(source_job_id, MAX_INSTANCE_ID_LENGTH):
|
||||
return {&"ok": false, &"message": "Facility infrastructure projection stable-ID crosslink is invalid."}
|
||||
var schema := _kind_schema(kind)
|
||||
@@ -1297,6 +1355,7 @@ static func _parse_projection(raw: Variant) -> Dictionary:
|
||||
or not _cell_is_valid(origin)
|
||||
or origin.z != int(record[&"layer"])
|
||||
or rotation not in [0, 90, 180, 270]
|
||||
or (kind in STAIR_KINDS and (rotation != 0 or not _cell_is_valid(origin + Vector3i(0, -1, 0))))
|
||||
or completed_at < 0
|
||||
or completed_at > MAX_SAFE_INTEGER
|
||||
or record[&"cells"] != _footprint_cells(origin, record[&"footprint"], rotation)
|
||||
@@ -1461,6 +1520,7 @@ static func _parse_snapshot_project_input(raw: Variant) -> Dictionary:
|
||||
or origin.z != layer \
|
||||
or layer != int(definition[&"layer"]) \
|
||||
or rotation not in [0, 90, 180, 270] \
|
||||
or (definition_id in STAIR_KINDS and (rotation != 0 or not _cell_is_valid(origin + Vector3i(0, -1, 0)))) \
|
||||
or completed_at < 0 \
|
||||
or completed_at > MAX_SAFE_INTEGER:
|
||||
return {&"ok": false, &"message": "Snapshot completed-facility semantic fields are invalid."}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
class_name LegacyStairService
|
||||
extends RefCounted
|
||||
|
||||
## Owns only the stairs materialized once from pre-policy vertical WALK edges.
|
||||
## Their original route score and access remain unchanged. Demolished landings
|
||||
## permanently remove their compatibility connector; new floors need stairs.
|
||||
|
||||
const PathType = preload("res://scripts/world/path_service.gd")
|
||||
const SNAPSHOT_VERSION := 1
|
||||
const CONNECTOR_PREFIX := "legacy.stairs:"
|
||||
const CONNECTOR_COST := 100
|
||||
const MAX_STAIRS := 131_072
|
||||
const MAX_COORDINATE_ABS := 100_000
|
||||
|
||||
var last_error := ""
|
||||
var _path: PathService
|
||||
var _stairs: Dictionary = {}
|
||||
var _migration_closed := false
|
||||
var _transaction_active := false
|
||||
var _quarantined := false
|
||||
|
||||
|
||||
## Bind before or after Path restoration. restore_snapshot performs the exact
|
||||
## namespace and topology crosslink checks after all dependencies are loaded.
|
||||
func configure(path_value: Variant) -> bool:
|
||||
if _transaction_active or _path != null:
|
||||
return _fail("Legacy stairs are already configured or transacting.")
|
||||
if not path_value is PathService:
|
||||
return _fail("Legacy stairs require PathService.")
|
||||
_path = path_value
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func migrate_existing_vertical_walks() -> bool:
|
||||
if not _available():
|
||||
return false
|
||||
if _path.get_vertical_walk_requires_connector():
|
||||
if not _live_matches(_stairs, true):
|
||||
return _fail("Current legacy stairs do not match Path ownership or landings.")
|
||||
_migration_closed = true
|
||||
last_error = ""
|
||||
return true
|
||||
if _migration_closed or not _stairs.is_empty():
|
||||
return _fail("Legacy vertical WALK migration cannot be run again.")
|
||||
if not _live_matches({}, false):
|
||||
return _fail("Legacy stair connector namespace contains unowned state.")
|
||||
var capture := _capture_path()
|
||||
if capture.is_empty():
|
||||
return false
|
||||
var blocked := _shaft_cells()
|
||||
var next_stairs: Dictionary = {}
|
||||
for from_cell: Vector3i in capture[&"cells"]:
|
||||
for raw_edge: Variant in _path.get_neighbors(from_cell):
|
||||
var edge: Dictionary = raw_edge
|
||||
var to_cell: Vector3i = edge[&"to"]
|
||||
if edge[&"movement_type"] != PathType.MOVEMENT_WALK \
|
||||
or not StringName(edge[&"connector_id"]).is_empty() \
|
||||
or to_cell != from_cell + Vector3i(0, 1, 0) \
|
||||
or blocked.has(from_cell) or blocked.has(to_cell):
|
||||
continue
|
||||
if not _valid_cell(from_cell) or not _valid_cell(to_cell):
|
||||
return _fail("Legacy vertical WALK endpoints exceed the supported coordinate range.")
|
||||
var stair_id := _stable_id(from_cell)
|
||||
next_stairs[stair_id] = {&"id": stair_id, &"from": from_cell, &"to": to_cell}
|
||||
if next_stairs.size() > MAX_STAIRS:
|
||||
return _fail("Legacy stair migration exceeds the supported capacity.")
|
||||
return _apply_candidate(next_stairs, capture)
|
||||
|
||||
|
||||
## Called after authoritative cells/shafts change. Missing endpoints and new
|
||||
## shafts retire records; no adjacency scan is ever performed here.
|
||||
func prune_invalid() -> bool:
|
||||
if not _available():
|
||||
return false
|
||||
if not _live_matches(_stairs, true, false):
|
||||
return _fail("Legacy stair ownership or vertical policy was altered.")
|
||||
var blocked := _shaft_cells()
|
||||
var next_stairs: Dictionary = {}
|
||||
for stair_id: StringName in _sorted_ids(_stairs):
|
||||
var record: Dictionary = _stairs[stair_id]
|
||||
if _landing_is_valid(record, blocked):
|
||||
next_stairs[stair_id] = record.duplicate(true)
|
||||
if next_stairs == _stairs:
|
||||
last_error = ""
|
||||
return true
|
||||
var capture := _capture_path()
|
||||
if capture.is_empty():
|
||||
return false
|
||||
return _apply_candidate(next_stairs, capture)
|
||||
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
if _path == null:
|
||||
return {}
|
||||
var rows: Array[Dictionary] = []
|
||||
for stair_id: StringName in _sorted_ids(_stairs):
|
||||
var record: Dictionary = _stairs[stair_id]
|
||||
rows.append({&"id": String(stair_id), &"from": record[&"from"], &"to": record[&"to"]})
|
||||
return {&"schema_version": SNAPSHOT_VERSION, &"stairs": rows}
|
||||
|
||||
|
||||
func restore_snapshot(data: Dictionary) -> bool:
|
||||
if _path == null or _transaction_active:
|
||||
return _fail("Legacy stair restore requires an idle configured service.")
|
||||
var parsed := _parse_snapshot(data)
|
||||
if not bool(parsed[&"ok"]):
|
||||
return _fail(String(parsed[&"error"]))
|
||||
var next_stairs: Dictionary = parsed[&"stairs"]
|
||||
if not _live_matches(next_stairs, true):
|
||||
return _fail("Legacy stair snapshot does not exactly match Path connectors and landings.")
|
||||
_stairs = next_stairs.duplicate(true)
|
||||
_migration_closed = true
|
||||
_quarantined = false
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func validate_invariants() -> Dictionary:
|
||||
var ok := _path != null and not _quarantined and _live_matches(_stairs, true)
|
||||
return {&"ok": ok, &"stair_count": _stairs.size(), &"quarantined": _quarantined}
|
||||
|
||||
|
||||
## Assemble one Path snapshot so migration remains linear in graph size and
|
||||
## preserves every unrelated connector, actor, traffic row, and shaft owner.
|
||||
func _apply_candidate(next_stairs: Dictionary, capture: Dictionary) -> bool:
|
||||
var before: Dictionary = capture[&"snapshot"]
|
||||
var next_snapshot := before.duplicate(true)
|
||||
var connectors: Array = []
|
||||
for raw_connector: Dictionary in before[&"connectors"]:
|
||||
if not String(raw_connector[&"id"]).begins_with(CONNECTOR_PREFIX):
|
||||
connectors.append(raw_connector.duplicate(true))
|
||||
for stair_id: StringName in _sorted_ids(next_stairs):
|
||||
connectors.append(_expected_connector(next_stairs[stair_id]))
|
||||
connectors.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"id"]) < String(right[&"id"]))
|
||||
next_snapshot[&"connectors"] = connectors
|
||||
next_snapshot[&"vertical_walk_requires_connector"] = true
|
||||
next_snapshot[&"graph_revision"] = int(before[&"graph_revision"]) + 1
|
||||
var verifier := PathType.new()
|
||||
if not verifier.restore_snapshot(next_snapshot) or not verifier.rebuild_graph(capture[&"cells"]):
|
||||
return _fail("Legacy stair candidate Path snapshot is invalid: %s" % verifier.last_error)
|
||||
var expected := verifier.create_snapshot()
|
||||
var stairs_before := _stairs.duplicate(true)
|
||||
var closed_before := _migration_closed
|
||||
var path_before := _path
|
||||
_transaction_active = true
|
||||
var applied := _path.restore_snapshot(expected.duplicate(true))
|
||||
if not applied or _path != path_before or not _transaction_active or _stairs != stairs_before \
|
||||
or _migration_closed != closed_before \
|
||||
or _path.create_snapshot() != expected \
|
||||
or not _live_matches(next_stairs, true):
|
||||
_path = path_before
|
||||
var restored := false
|
||||
for _attempt: int in range(2):
|
||||
_path.rebuild_graph(capture[&"cells"])
|
||||
_path.restore_snapshot(before.duplicate(true))
|
||||
_path.rebuild_graph(capture[&"cells"])
|
||||
if _path.create_snapshot() == before:
|
||||
restored = true
|
||||
break
|
||||
_stairs = stairs_before
|
||||
_migration_closed = closed_before
|
||||
_transaction_active = false
|
||||
_quarantined = not restored
|
||||
return _fail("Legacy stair transaction failed; exact rollback %s." % ("restored" if restored else "is uncertain"))
|
||||
_stairs = next_stairs.duplicate(true)
|
||||
_migration_closed = true
|
||||
_transaction_active = false
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func _live_matches(stairs: Dictionary, require_policy: bool, require_landings := true) -> bool:
|
||||
if _path == null or (require_policy and not _path.get_vertical_walk_requires_connector()):
|
||||
return false
|
||||
var seen: Dictionary = {}
|
||||
for raw_connector: Dictionary in _path.create_snapshot().get(&"connectors", []):
|
||||
var stair_id := StringName(raw_connector[&"id"])
|
||||
if not String(stair_id).begins_with(CONNECTOR_PREFIX):
|
||||
continue
|
||||
if not stairs.has(stair_id) or _path.get_connector(stair_id) != _expected_connector(stairs[stair_id]):
|
||||
return false
|
||||
seen[stair_id] = true
|
||||
if seen.size() != stairs.size():
|
||||
return false
|
||||
if require_landings:
|
||||
var blocked := _shaft_cells()
|
||||
for record: Dictionary in stairs.values():
|
||||
if not _landing_is_valid(record, blocked):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _landing_is_valid(record: Dictionary, blocked: Dictionary) -> bool:
|
||||
return _path.has_cell(record[&"from"]) and _path.has_cell(record[&"to"]) \
|
||||
and not blocked.has(record[&"from"]) and not blocked.has(record[&"to"])
|
||||
|
||||
|
||||
func _shaft_cells() -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
for cells: Array in _path.get_vertical_walk_blockers().values():
|
||||
for cell: Vector3i in cells:
|
||||
result[cell] = true
|
||||
return result
|
||||
|
||||
|
||||
func _capture_path() -> Dictionary:
|
||||
var snapshot := _path.create_snapshot()
|
||||
var cells: Array[Vector3i] = []
|
||||
for row: Dictionary in snapshot.get(&"cell_traffic", []):
|
||||
var cell: Vector3i = row[&"cell"]
|
||||
if not _path.has_cell(cell):
|
||||
_fail("Legacy stair migration requires the materialized Path graph.")
|
||||
return {}
|
||||
cells.append(cell)
|
||||
return {&"snapshot": snapshot, &"cells": cells}
|
||||
|
||||
|
||||
static func _parse_snapshot(data: Dictionary) -> Dictionary:
|
||||
if not _keys_match(data, [&"schema_version", &"stairs"]) \
|
||||
or typeof(data.get(&"schema_version")) != TYPE_INT \
|
||||
or data[&"schema_version"] != SNAPSHOT_VERSION \
|
||||
or not data.get(&"stairs") is Array \
|
||||
or data[&"stairs"].size() > MAX_STAIRS:
|
||||
return {&"ok": false, &"error": "Legacy stair snapshot fields or version are invalid."}
|
||||
var stairs: Dictionary = {}
|
||||
var previous_id := ""
|
||||
for raw: Variant in data[&"stairs"]:
|
||||
if not raw is Dictionary or not _keys_match(raw, [&"id", &"from", &"to"]) \
|
||||
or typeof(raw.get(&"id")) != TYPE_STRING \
|
||||
or not _valid_cell(raw.get(&"from")) or not _valid_cell(raw.get(&"to")):
|
||||
return {&"ok": false, &"error": "Legacy stair record types are invalid."}
|
||||
var lower: Vector3i = raw[&"from"]
|
||||
var upper: Vector3i = raw[&"to"]
|
||||
var stair_id := StringName(raw[&"id"])
|
||||
if upper != lower + Vector3i(0, 1, 0) or stair_id != _stable_id(lower) \
|
||||
or (not previous_id.is_empty() and String(stair_id) <= previous_id):
|
||||
return {&"ok": false, &"error": "Legacy stair geometry, stable ID, or order is invalid."}
|
||||
stairs[stair_id] = {&"id": stair_id, &"from": lower, &"to": upper}
|
||||
previous_id = String(stair_id)
|
||||
return {&"ok": true, &"stairs": stairs}
|
||||
|
||||
|
||||
static func _expected_connector(record: Dictionary) -> Dictionary:
|
||||
var lower: Vector3i = record[&"from"]
|
||||
return {
|
||||
&"id": record[&"id"], &"from": lower, &"to": record[&"to"],
|
||||
&"cost": CONNECTOR_COST, &"bidirectional": true, &"access_id": &"", &"enabled": true,
|
||||
&"movement_type": PathType.MOVEMENT_CONNECTOR, &"dynamic_cost": 0,
|
||||
&"operational_state": PathType.CONNECTOR_OPERATIONAL,
|
||||
&"cabin_capacity": 8, &"queue_capacity": 256, &"cabin_state": PathType.CABIN_IDLE,
|
||||
&"current_floor": lower.y, &"travel_minutes_per_floor": 1, &"request_queue": [],
|
||||
}
|
||||
|
||||
|
||||
static func _stable_id(lower: Vector3i) -> StringName:
|
||||
return StringName("%s%d:%d:%d" % [CONNECTOR_PREFIX, lower.z, lower.x, lower.y])
|
||||
|
||||
|
||||
static func _valid_cell(value: Variant) -> bool:
|
||||
return value is Vector3i and value.z in [PathType.FRONT, PathType.BACK] \
|
||||
and absi(value.x) <= MAX_COORDINATE_ABS and absi(value.y) <= MAX_COORDINATE_ABS
|
||||
|
||||
|
||||
static func _keys_match(value: Dictionary, expected: Array[StringName]) -> bool:
|
||||
if value.size() != expected.size():
|
||||
return false
|
||||
for key: Variant in value:
|
||||
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(key) not in expected:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _sorted_ids(value: Dictionary) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for stair_id: StringName in value:
|
||||
result.append(stair_id)
|
||||
result.sort_custom(func(left: StringName, right: StringName) -> bool: return String(left) < String(right))
|
||||
return result
|
||||
|
||||
|
||||
func _available() -> bool:
|
||||
if _path == null or _transaction_active or _quarantined:
|
||||
return _fail("Legacy stairs require an idle, configured, available service.")
|
||||
return true
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
last_error = message
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0ti0lvvkubaj
|
||||
+146
-17
@@ -17,7 +17,8 @@ const CONGESTION_COST_PER_EXCESS := 75
|
||||
const MINIMUM_SPEED_MULTIPLIER := 0.25
|
||||
const STUCK_THRESHOLD_MINUTES := 30
|
||||
const ELEVATOR_STARVATION_MINUTES := 60
|
||||
const SNAPSHOT_VERSION := 4
|
||||
const SNAPSHOT_VERSION := 5
|
||||
const SHAFT_SNAPSHOT_VERSION := 4
|
||||
const ELEVATOR_INDEX_SNAPSHOT_VERSION := 3
|
||||
const MAX_VERTICAL_BLOCKER_OWNERS := 4_096
|
||||
const MAX_VERTICAL_BLOCKER_CELLS := 262_144
|
||||
@@ -83,6 +84,8 @@ var _path_cache: Dictionary = {}
|
||||
var _cell_traffic: Dictionary = {}
|
||||
var _actor_cells: Dictionary = {}
|
||||
var _actor_progress: Dictionary = {}
|
||||
var _vertical_walk_requires_connector := false
|
||||
var _transit_actor_ids: Dictionary = {}
|
||||
var _known_elevator_request_ids: Dictionary = {}
|
||||
var _active_elevator_units: Dictionary = {}
|
||||
var _pending_graph_revision := -1
|
||||
@@ -183,6 +186,28 @@ func get_vertical_walk_blockers() -> Dictionary:
|
||||
return _vertical_walk_blockers.duplicate(true)
|
||||
|
||||
|
||||
func set_vertical_walk_requires_connector(required: bool) -> bool:
|
||||
if _vertical_walk_requires_connector == required:
|
||||
last_error = ""
|
||||
return true
|
||||
_vertical_walk_requires_connector = required
|
||||
_mark_graph_changed()
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func get_vertical_walk_requires_connector() -> bool:
|
||||
return _vertical_walk_requires_connector
|
||||
|
||||
|
||||
func get_transit_actor_ids() -> Array[StringName]:
|
||||
return _sorted_name_keys(_transit_actor_ids)
|
||||
|
||||
|
||||
func is_actor_in_transit(actor_id: StringName) -> bool:
|
||||
return _transit_actor_ids.has(actor_id)
|
||||
|
||||
|
||||
func _rebuild_vertical_walk_blocked_cells() -> void:
|
||||
_vertical_walk_blocked_cells.clear()
|
||||
for cells: Array in _vertical_walk_blockers.values():
|
||||
@@ -254,6 +279,7 @@ func configure_connector(
|
||||
if active[&"state"] == ELEVATOR_REQUEST_BOARDED:
|
||||
next_active[unit_id] = active.duplicate(true)
|
||||
_active_elevator_units = next_active
|
||||
_refresh_transit_actor_ids()
|
||||
for request_id: Variant in index_result[&"known_request_ids"]:
|
||||
_known_elevator_request_ids[request_id] = true
|
||||
_rebuild_derived_graph()
|
||||
@@ -326,6 +352,7 @@ func set_connectors(configurations: Array) -> bool:
|
||||
return false
|
||||
_connectors = next_connectors
|
||||
_active_elevator_units = normalized_collection[&"active_units"]
|
||||
_refresh_transit_actor_ids()
|
||||
for request_id: Variant in normalized_collection[&"known_request_ids"]:
|
||||
_known_elevator_request_ids[request_id] = true
|
||||
_rebuild_derived_graph()
|
||||
@@ -577,17 +604,23 @@ func project_actor_position(
|
||||
var changed_cells: Array[Vector3i] = []
|
||||
if _actor_cells.has(actor_id):
|
||||
var previous: Vector3i = _actor_cells[actor_id]
|
||||
if not _decrement_density(previous):
|
||||
if not is_actor_in_transit(actor_id) and not _decrement_density(previous):
|
||||
last_error = "Actor density projection is inconsistent for: %s" % actor_id
|
||||
return false
|
||||
changed_cells.append(previous)
|
||||
if not is_actor_in_transit(actor_id):
|
||||
changed_cells.append(previous)
|
||||
_actor_cells[actor_id] = cell
|
||||
_increment_density(cell)
|
||||
changed_cells.append(cell)
|
||||
if _active_elevator_units.get(actor_id, {}).get(&"state", &"") == ELEVATOR_REQUEST_BOARDED:
|
||||
_transit_actor_ids[actor_id] = true
|
||||
else:
|
||||
_transit_actor_ids.erase(actor_id)
|
||||
_increment_density(cell)
|
||||
changed_cells.append(cell)
|
||||
# Leaving the previous cell can make an unselected alternative cheaper, so a
|
||||
# move requires broad cache invalidation even though congestion itself does
|
||||
# not advance graph_revision.
|
||||
_mark_traffic_changed(changed_cells, changed_cells.size() > 1)
|
||||
if not changed_cells.is_empty():
|
||||
_mark_traffic_changed(changed_cells, changed_cells.size() > 1)
|
||||
if absolute_minute >= 0:
|
||||
record_actor_progress(actor_id, cell, absolute_minute)
|
||||
last_error = ""
|
||||
@@ -603,13 +636,16 @@ func actor_leave_cell(actor_id: StringName, clear_progress: bool = true) -> bool
|
||||
last_error = "Actor has no projected cell: %s" % actor_id
|
||||
return false
|
||||
var previous: Vector3i = _actor_cells[actor_id]
|
||||
if not _decrement_density(previous):
|
||||
var was_in_transit := is_actor_in_transit(actor_id)
|
||||
if not was_in_transit and not _decrement_density(previous):
|
||||
last_error = "Actor density projection is inconsistent for: %s" % actor_id
|
||||
return false
|
||||
_actor_cells.erase(actor_id)
|
||||
_transit_actor_ids.erase(actor_id)
|
||||
if clear_progress:
|
||||
_actor_progress.erase(actor_id)
|
||||
_mark_traffic_changed([previous], true)
|
||||
if not was_in_transit:
|
||||
_mark_traffic_changed([previous], true)
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
@@ -651,7 +687,8 @@ func project_occupancy(entries: Array, clear_unprojected_progress: bool = true)
|
||||
last_error = "Occupancy projection contains a duplicate actor or blocked cell."
|
||||
return false
|
||||
next_actor_cells[actor_id] = cell
|
||||
next_densities[cell] = int(next_densities.get(cell, 0)) + 1
|
||||
if _active_elevator_units.get(actor_id, {}).get(&"state", &"") != ELEVATOR_REQUEST_BOARDED:
|
||||
next_densities[cell] = int(next_densities.get(cell, 0)) + 1
|
||||
|
||||
var changed: Dictionary = {}
|
||||
var may_make_routes_cheaper := false
|
||||
@@ -667,6 +704,7 @@ func project_occupancy(entries: Array, clear_unprojected_progress: bool = true)
|
||||
traffic[&"current_density"] = next_density
|
||||
_cell_traffic[cell] = traffic
|
||||
_actor_cells = next_actor_cells
|
||||
_transit_actor_ids = _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
|
||||
if clear_unprojected_progress:
|
||||
var stale_progress_ids: Array = []
|
||||
for actor_id: Variant in _actor_progress:
|
||||
@@ -762,6 +800,7 @@ func cancel_elevator_request(connector_id: StringName, request_id: StringName) -
|
||||
connector[&"request_queue"] = queue
|
||||
_connectors[connector_id] = connector
|
||||
_active_elevator_units.erase(request[&"unit_id"])
|
||||
_refresh_transit_actor_ids()
|
||||
# Request ids are tombstoned permanently; cancellation permits a new
|
||||
# id for the same unit but never reuse of the cancelled id.
|
||||
_mark_connector_traffic_changed(connector, true)
|
||||
@@ -838,6 +877,7 @@ func dequeue_elevator_boarding(
|
||||
if not active.is_empty() and active[&"request_id"] == request[&"request_id"]:
|
||||
active[&"state"] = ELEVATOR_REQUEST_BOARDED
|
||||
_active_elevator_units[unit_id] = active
|
||||
_refresh_transit_actor_ids()
|
||||
_mark_connector_traffic_changed(connector, true)
|
||||
last_error = ""
|
||||
return boarded
|
||||
@@ -858,6 +898,7 @@ func complete_elevator_request(connector_id: StringName, request_id: StringName)
|
||||
last_error = "Only a boarded elevator request can complete."
|
||||
return false
|
||||
_active_elevator_units.erase(unit_id)
|
||||
_refresh_transit_actor_ids()
|
||||
last_error = ""
|
||||
return true
|
||||
last_error = "Unknown active elevator request id: %s" % request_id
|
||||
@@ -1220,6 +1261,15 @@ func find_nearest_walkable_recovery(
|
||||
return _coordinate_less(left, right)
|
||||
)
|
||||
var target: Vector3i = candidates[0]
|
||||
if _vertical_walk_requires_connector:
|
||||
var reachable := false
|
||||
for candidate: Vector3i in candidates:
|
||||
if _route_exists_with_overrides(origin, candidate, layers, [], true, false):
|
||||
target = candidate
|
||||
reachable = true
|
||||
break
|
||||
if not reachable:
|
||||
return {&"found": false, &"reason": &"NO_SAFE_WALKABLE_CELL"}
|
||||
return {
|
||||
&"found": true,
|
||||
&"reason": &"SAFE_WALKABLE_CELL",
|
||||
@@ -1293,17 +1343,30 @@ func create_snapshot() -> Dictionary:
|
||||
&"known_elevator_request_ids": saved_known_requests,
|
||||
&"active_elevator_units": saved_active_units,
|
||||
&"vertical_walk_blockers": saved_vertical_blockers,
|
||||
&"vertical_walk_requires_connector": _vertical_walk_requires_connector,
|
||||
&"transit_actor_ids": get_transit_actor_ids(),
|
||||
}
|
||||
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
var version := int(_dictionary_value(snapshot, &"schema_version", -1))
|
||||
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, ELEVATOR_INDEX_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
|
||||
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, ELEVATOR_INDEX_SNAPSHOT_VERSION, SHAFT_SNAPSHOT_VERSION, SNAPSHOT_VERSION]:
|
||||
last_error = "Unsupported PathService snapshot version: %d" % version
|
||||
return false
|
||||
if version < SNAPSHOT_VERSION and (snapshot.has(&"vertical_walk_blockers") or snapshot.has("vertical_walk_blockers")):
|
||||
if version < SHAFT_SNAPSHOT_VERSION and (snapshot.has(&"vertical_walk_blockers") or snapshot.has("vertical_walk_blockers")):
|
||||
last_error = "Legacy PathService snapshots cannot contain shaft ownership fields."
|
||||
return false
|
||||
if version < SNAPSHOT_VERSION and (snapshot.has(&"vertical_walk_requires_connector") or snapshot.has("vertical_walk_requires_connector") \
|
||||
or snapshot.has(&"transit_actor_ids") or snapshot.has("transit_actor_ids")):
|
||||
last_error = "Legacy PathService snapshots cannot contain vertical policy or transit fields."
|
||||
return false
|
||||
var next_vertical_policy := false
|
||||
if version >= SNAPSHOT_VERSION:
|
||||
var policy_value: Variant = _dictionary_value(snapshot, &"vertical_walk_requires_connector", null)
|
||||
if typeof(policy_value) != TYPE_BOOL:
|
||||
last_error = "PathService vertical movement policy must be a bool."
|
||||
return false
|
||||
next_vertical_policy = bool(policy_value)
|
||||
var raw_connectors: Variant = _dictionary_value(snapshot, &"connectors", null)
|
||||
if not raw_connectors is Array:
|
||||
last_error = "PathService snapshot connectors must be an Array."
|
||||
@@ -1317,8 +1380,10 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
var next_active_units: Dictionary = connector_result[&"active_units"]
|
||||
|
||||
if version == LEGACY_SNAPSHOT_VERSION:
|
||||
var changed := next_connectors != _connectors or not _vertical_walk_blockers.is_empty()
|
||||
var changed := next_connectors != _connectors or not _vertical_walk_blockers.is_empty() or _vertical_walk_requires_connector
|
||||
_vertical_walk_blockers.clear()
|
||||
_vertical_walk_requires_connector = false
|
||||
_transit_actor_ids.clear()
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_connectors = next_connectors
|
||||
_cell_traffic.clear()
|
||||
@@ -1387,13 +1452,19 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
next_known_requests = index_result[&"known_request_ids"]
|
||||
next_active_units = index_result[&"active_units"]
|
||||
var next_vertical_blockers: Dictionary = {}
|
||||
if version >= SNAPSHOT_VERSION:
|
||||
if version >= SHAFT_SNAPSHOT_VERSION:
|
||||
var blockers_result := _normalize_snapshot_vertical_blockers(_dictionary_value(snapshot, &"vertical_walk_blockers", null), next_traffic)
|
||||
if not bool(blockers_result[&"ok"]):
|
||||
last_error = String(blockers_result[&"error"])
|
||||
return false
|
||||
next_vertical_blockers = blockers_result[&"blockers"]
|
||||
if not _snapshot_densities_match(next_traffic, next_actor_cells):
|
||||
var next_transit := _derive_transit_actor_ids(next_actor_cells, next_active_units)
|
||||
if version >= SNAPSHOT_VERSION:
|
||||
var transit_value: Variant = _dictionary_value(snapshot, &"transit_actor_ids", null)
|
||||
if not _transit_snapshot_matches(transit_value, next_transit):
|
||||
last_error = "PathService transit actors must match projected boarded elevator units."
|
||||
return false
|
||||
if not _snapshot_densities_match(next_traffic, next_actor_cells, next_transit if version >= SNAPSHOT_VERSION else {}):
|
||||
last_error = "PathService snapshot density does not match actor projections."
|
||||
return false
|
||||
if not _passable_cells.is_empty():
|
||||
@@ -1407,6 +1478,8 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
|
||||
_connectors = next_connectors
|
||||
_vertical_walk_blockers = next_vertical_blockers
|
||||
_vertical_walk_requires_connector = next_vertical_policy
|
||||
_transit_actor_ids = next_transit
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_cell_traffic = next_traffic
|
||||
_actor_cells = next_actor_cells
|
||||
@@ -1414,6 +1487,11 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
_known_elevator_request_ids = next_known_requests
|
||||
_active_elevator_units = next_active_units
|
||||
traffic_revision = saved_traffic_revision
|
||||
# Older snapshots counted passengers at their boarding anchor. Validate that
|
||||
# old rule above before a one-time migration to physical floor occupancy.
|
||||
if version < SNAPSHOT_VERSION and not _transit_actor_ids.is_empty():
|
||||
_recompute_all_densities()
|
||||
traffic_revision += 1
|
||||
_pending_graph_revision = saved_graph_revision if _passable_cells.is_empty() else -1
|
||||
_traffic_loaded_without_graph = _passable_cells.is_empty() and not next_traffic.is_empty()
|
||||
if not _passable_cells.is_empty():
|
||||
@@ -1519,6 +1597,7 @@ func _run_a_star(
|
||||
var neighbor: Vector3i = edge[&"to"]
|
||||
if neighbor.z not in allowed_layers \
|
||||
or _cell_is_temporarily_blocked(neighbor) \
|
||||
or not _edge_meets_vertical_policy(current, edge) \
|
||||
or not _edge_is_accessible(edge, allowed_access_ids):
|
||||
continue
|
||||
var tentative_cost: int = int(g_score[current]) + _edge_total_cost(edge)
|
||||
@@ -1573,6 +1652,11 @@ func _reconstruct_path(came_from: Dictionary, goal: Vector3i) -> Array[Vector3i]
|
||||
return reversed_path
|
||||
|
||||
|
||||
func _edge_meets_vertical_policy(from_cell: Vector3i, edge: Dictionary) -> bool:
|
||||
return not _vertical_walk_requires_connector or from_cell.y == (edge[&"to"] as Vector3i).y \
|
||||
or not StringName(edge[&"connector_id"]).is_empty() or edge[&"movement_type"] != MOVEMENT_WALK
|
||||
|
||||
|
||||
func _edge_is_accessible(edge: Dictionary, allowed_access_ids: Array[StringName]) -> bool:
|
||||
var required_access: StringName = edge[&"access_id"]
|
||||
if not required_access.is_empty() and required_access not in allowed_access_ids:
|
||||
@@ -1612,6 +1696,8 @@ func _find_edge(
|
||||
var edge: Dictionary = edge_value
|
||||
if edge[&"to"] != to_cell:
|
||||
continue
|
||||
if not _edge_meets_vertical_policy(from_cell, edge):
|
||||
continue
|
||||
if allowed_access_ids is Array and not _edge_is_accessible(edge, allowed_access_ids):
|
||||
continue
|
||||
var cost := _edge_total_cost(edge)
|
||||
@@ -1705,7 +1791,8 @@ func _route_exists_with_overrides(
|
||||
var neighbor_set: Dictionary = {}
|
||||
for direction: Vector3i in _CARDINAL_DIRECTIONS:
|
||||
var neighbor := current + direction
|
||||
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(current, neighbor):
|
||||
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(current, neighbor) \
|
||||
and (not _vertical_walk_requires_connector or current.y == neighbor.y):
|
||||
neighbor_set[neighbor] = true
|
||||
for connector_id: StringName in connector_ids:
|
||||
var connector: Dictionary = _connectors[connector_id]
|
||||
@@ -1759,6 +1846,7 @@ func _ensure_cell_traffic_for_graph(cells: Dictionary, reset_missing: bool) -> v
|
||||
invalid_actor_ids.append(actor_id)
|
||||
for actor_id: Variant in invalid_actor_ids:
|
||||
_actor_cells.erase(actor_id)
|
||||
_transit_actor_ids = _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
|
||||
_recompute_all_densities()
|
||||
|
||||
|
||||
@@ -1796,10 +1884,49 @@ func _recompute_all_densities() -> void:
|
||||
_cell_traffic[cell_value] = traffic
|
||||
for actor_id: Variant in _actor_cells:
|
||||
var cell: Vector3i = _actor_cells[actor_id]
|
||||
if _cell_traffic.has(cell):
|
||||
if _cell_traffic.has(cell) and not is_actor_in_transit(actor_id):
|
||||
_increment_density(cell)
|
||||
|
||||
|
||||
static func _derive_transit_actor_ids(actor_cells: Dictionary, active_units: Dictionary) -> Dictionary:
|
||||
var transit: Dictionary = {}
|
||||
for actor_id: Variant in active_units:
|
||||
if actor_cells.has(actor_id) and active_units[actor_id][&"state"] == ELEVATOR_REQUEST_BOARDED:
|
||||
transit[actor_id] = true
|
||||
return transit
|
||||
|
||||
|
||||
func _refresh_transit_actor_ids() -> void:
|
||||
var next := _derive_transit_actor_ids(_actor_cells, _active_elevator_units)
|
||||
if next == _transit_actor_ids:
|
||||
return
|
||||
var changed_cells: Dictionary = {}
|
||||
var may_make_routes_cheaper := false
|
||||
for actor_id: Variant in next:
|
||||
if not _transit_actor_ids.has(actor_id):
|
||||
changed_cells[_actor_cells[actor_id]] = true
|
||||
may_make_routes_cheaper = true
|
||||
for actor_id: Variant in _transit_actor_ids:
|
||||
if not next.has(actor_id) and _actor_cells.has(actor_id):
|
||||
changed_cells[_actor_cells[actor_id]] = true
|
||||
_transit_actor_ids = next
|
||||
_recompute_all_densities()
|
||||
if not changed_cells.is_empty():
|
||||
_mark_traffic_changed(changed_cells.keys(), may_make_routes_cheaper)
|
||||
|
||||
|
||||
static func _transit_snapshot_matches(raw_ids: Variant, expected: Dictionary) -> bool:
|
||||
if not raw_ids is Array or raw_ids.size() != expected.size():
|
||||
return false
|
||||
var previous := ""
|
||||
for value: Variant in raw_ids:
|
||||
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME] or String(value).is_empty() \
|
||||
or not expected.has(StringName(value)) or (not previous.is_empty() and String(value) <= previous):
|
||||
return false
|
||||
previous = String(value)
|
||||
return true
|
||||
|
||||
|
||||
func _mark_traffic_changed(cells: Variant, may_make_routes_cheaper: bool = false) -> void:
|
||||
traffic_revision += 1
|
||||
if may_make_routes_cheaper:
|
||||
@@ -2306,9 +2433,11 @@ func _normalize_snapshot_actor_progress(raw_entries: Array) -> Dictionary:
|
||||
return {&"ok": true, &"progress": progress}
|
||||
|
||||
|
||||
func _snapshot_densities_match(traffic: Dictionary, actor_cells: Dictionary) -> bool:
|
||||
func _snapshot_densities_match(traffic: Dictionary, actor_cells: Dictionary, transit: Dictionary = {}) -> bool:
|
||||
var expected: Dictionary = {}
|
||||
for actor_id: Variant in actor_cells:
|
||||
if transit.has(actor_id):
|
||||
continue
|
||||
var cell: Vector3i = actor_cells[actor_id]
|
||||
expected[cell] = int(expected.get(cell, 0)) + 1
|
||||
for cell_value: Variant in traffic:
|
||||
|
||||
Reference in New Issue
Block a user