Connect physical elevators and staff commutes to construction, saves, and cutaway UI
This commit is contained in:
@@ -37,8 +37,6 @@ const SUPPORTED_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_CORRIDOR,
|
||||
KIND_SERVICE_CORRIDOR,
|
||||
KIND_STAFF_DOOR,
|
||||
]
|
||||
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = [
|
||||
KIND_GUEST_ELEVATOR,
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
]
|
||||
@@ -50,6 +48,8 @@ const KNOWN_KINDS: Array[StringName] = [
|
||||
KIND_SERVICE_ELEVATOR,
|
||||
]
|
||||
|
||||
const UNSUPPORTED_VERTICAL_KINDS: Array[StringName] = []
|
||||
const VERTICAL_KINDS: Array[StringName] = [KIND_GUEST_ELEVATOR, KIND_SERVICE_ELEVATOR]
|
||||
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"
|
||||
@@ -60,6 +60,7 @@ const QUARANTINE_REASONS: Array[StringName] = [
|
||||
|
||||
const ROUTE_ENTITY_PREFIX := "infra.route:"
|
||||
const CONNECTOR_PREFIX := "infra.connector:"
|
||||
const SHAFT_OWNER_PREFIX := "infra.shaft:"
|
||||
const ADAPTER_PROJECT_REQUEST_PREFIX := "infra.project:"
|
||||
const ADAPTER_UNPROJECT_REQUEST_PREFIX := "infra.unproject:"
|
||||
const STAFF_ACCESS_ID: StringName = &"staff"
|
||||
@@ -168,16 +169,6 @@ func project_facility(
|
||||
var canonical_input: Dictionary = input_result[&"input"]
|
||||
var definition_input: Dictionary = canonical_input[&"definition"]
|
||||
var kind: StringName = definition_input[&"id"]
|
||||
if kind in UNSUPPORTED_VERTICAL_KINDS:
|
||||
return _failure(
|
||||
CommandResultType.Code.VALIDATION_FAILED,
|
||||
"Vertical facility projection is unsupported until ordinary adjacent walk edges can be suppressed safely.",
|
||||
{
|
||||
&"reason": REASON_UNSUPPORTED_VERTICAL_TOPOLOGY,
|
||||
&"definition_id": kind,
|
||||
&"unchanged": true,
|
||||
}
|
||||
)
|
||||
if kind not in SUPPORTED_KINDS:
|
||||
return _failure(CommandResultType.Code.INVALID_PAYLOAD, "Facility definition is not an infrastructure proxy.")
|
||||
var request_id := StringName(request_id_value)
|
||||
@@ -197,6 +188,7 @@ func project_facility(
|
||||
return _failure(CommandResultType.Code.DUPLICATE_COMMAND_ID, "Facility instance is already projected.")
|
||||
var record := _projection_from_project_input(canonical_input)
|
||||
if _source.has_entity(record[&"route_entity_id"]) \
|
||||
or _path.get_vertical_walk_blockers().has(StringName(SHAFT_OWNER_PREFIX + String(instance_id))) \
|
||||
or (not StringName(record[&"connector_id"]).is_empty() and _path.has_connector(record[&"connector_id"])):
|
||||
return _failure(CommandResultType.Code.DUPLICATE_COMMAND_ID, "Stable infrastructure ID collides with existing authoritative state.")
|
||||
if not _dependencies_match_current_state():
|
||||
@@ -244,6 +236,11 @@ func project_facility(
|
||||
dependency_capture,
|
||||
"Staff-door connector projection failed or callback tampering was detected."
|
||||
)
|
||||
if kind in VERTICAL_KINDS:
|
||||
var shaft_cells: Array[Vector3i] = []
|
||||
shaft_cells.assign(record[&"cells"])
|
||||
if not _path.set_vertical_walk_blockers(StringName(SHAFT_OWNER_PREFIX + String(instance_id)), shaft_cells) or not _owned_state_matches(guard_state):
|
||||
return _rollback_failure(owned_before, dependency_capture, "Elevator shaft projection failed or callback tampering was detected.")
|
||||
var candidate_projections := _projections.duplicate(true)
|
||||
candidate_projections[instance_id] = record.duplicate(true)
|
||||
if not _dependencies_match_projection_map(candidate_projections) \
|
||||
@@ -332,6 +329,9 @@ func unproject_facility(request_id_value: Variant, instance_id_value: Variant) -
|
||||
dependency_capture,
|
||||
"Staff-door 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):
|
||||
return _rollback_failure(owned_before, dependency_capture, "Elevator shaft unprojection failed or callback tampering was detected.")
|
||||
var route_result := _adapter.remove_route(
|
||||
StringName(ADAPTER_UNPROJECT_REQUEST_PREFIX + String(request_id)),
|
||||
StringName(record[&"route_entity_id"])
|
||||
@@ -977,6 +977,14 @@ func _projection_live_matches(record: Dictionary) -> bool:
|
||||
if connector_id.is_empty() or not _path.has_connector(connector_id):
|
||||
return false
|
||||
return _path.get_connector(connector_id) == _expected_staff_connector(record)
|
||||
if record[&"kind"] in VERTICAL_KINDS:
|
||||
var blockers := _path.get_vertical_walk_blockers()
|
||||
var owner_id := StringName(SHAFT_OWNER_PREFIX + String(record[&"instance_id"]))
|
||||
if not blockers.has(owner_id):
|
||||
return false
|
||||
var cells: Array = (blockers[owner_id] as Array).duplicate()
|
||||
cells.sort_custom(_cell_less)
|
||||
return connector_id.is_empty() and cells == record[&"cells"]
|
||||
return connector_id.is_empty()
|
||||
|
||||
|
||||
@@ -1009,16 +1017,22 @@ static func _has_managed_namespace_collision(
|
||||
) -> bool:
|
||||
var expected_routes: Dictionary = {}
|
||||
var expected_connectors: Dictionary = {}
|
||||
var expected_shafts: Dictionary = {}
|
||||
for record_value: Variant in projections.values():
|
||||
if not record_value is Dictionary:
|
||||
return true
|
||||
var record: Dictionary = record_value
|
||||
expected_routes[record[&"route_entity_id"]] = true
|
||||
if record[&"kind"] in VERTICAL_KINDS:
|
||||
expected_shafts[StringName(SHAFT_OWNER_PREFIX + String(record[&"instance_id"]))] = true
|
||||
if not StringName(record[&"connector_id"]).is_empty():
|
||||
expected_connectors[record[&"connector_id"]] = true
|
||||
for entity_id: StringName in source_service.get_entity_ids(source_service.CATEGORY_ROUTE):
|
||||
if String(entity_id).begins_with(ROUTE_ENTITY_PREFIX) and not expected_routes.has(entity_id):
|
||||
return true
|
||||
for owner_id: Variant in path_service.get_vertical_walk_blockers():
|
||||
if String(owner_id).begins_with(SHAFT_OWNER_PREFIX) and not expected_shafts.has(owner_id):
|
||||
return true
|
||||
var path_snapshot := path_service.create_snapshot()
|
||||
var connector_rows: Variant = path_snapshot.get(&"connectors", null)
|
||||
if not connector_rows is Array:
|
||||
|
||||
@@ -17,7 +17,10 @@ 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 := 3
|
||||
const SNAPSHOT_VERSION := 4
|
||||
const ELEVATOR_INDEX_SNAPSHOT_VERSION := 3
|
||||
const MAX_VERTICAL_BLOCKER_OWNERS := 4_096
|
||||
const MAX_VERTICAL_BLOCKER_CELLS := 262_144
|
||||
const LEGACY_SNAPSHOT_VERSION := 1
|
||||
const TRAFFIC_SNAPSHOT_VERSION := 2
|
||||
|
||||
@@ -74,6 +77,8 @@ var last_path_cost := 0
|
||||
var _passable_cells: Dictionary = {}
|
||||
var _connectors: Dictionary = {}
|
||||
var _adjacency: Dictionary = {}
|
||||
var _vertical_walk_blockers: Dictionary = {}
|
||||
var _vertical_walk_blocked_cells: Dictionary = {}
|
||||
var _path_cache: Dictionary = {}
|
||||
var _cell_traffic: Dictionary = {}
|
||||
var _actor_cells: Dictionary = {}
|
||||
@@ -124,6 +129,79 @@ func rebuild_graph(passable_cells: Variant) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
## A shaft is always physical topology, independent of cabin availability.
|
||||
## Owners may overlap; removing one owner does not clear another owner's cells.
|
||||
func set_vertical_walk_blockers(owner_id: StringName, cells: Array[Vector3i]) -> bool:
|
||||
if not _vertical_blocker_owner_is_valid(owner_id):
|
||||
last_error = "Vertical walk blockers require a bounded, nonempty owner id."
|
||||
return false
|
||||
if cells.is_empty() or cells.size() > MAX_VERTICAL_BLOCKER_CELLS:
|
||||
last_error = "Vertical walk blockers require a bounded, nonempty cell set."
|
||||
return false
|
||||
var normalization := _normalize_cell_set(cells)
|
||||
if not bool(normalization[&"ok"]) or (normalization[&"cells"] as Dictionary).size() != cells.size():
|
||||
last_error = "Vertical walk blocker cells must be valid and unique."
|
||||
return false
|
||||
for cell: Vector3i in cells:
|
||||
if not _passable_cells.has(cell):
|
||||
last_error = "Vertical walk blocker cells must belong to the current graph."
|
||||
return false
|
||||
var canonical: Array[Vector3i] = cells.duplicate()
|
||||
canonical.sort_custom(Callable(self, "_coordinate_less"))
|
||||
if _vertical_walk_blockers.get(owner_id, []) == canonical:
|
||||
last_error = ""
|
||||
return true
|
||||
var candidate := _vertical_walk_blockers.duplicate(true)
|
||||
candidate[owner_id] = canonical
|
||||
var count := 0
|
||||
for owned_cells: Array in candidate.values():
|
||||
count += owned_cells.size()
|
||||
if candidate.size() > MAX_VERTICAL_BLOCKER_OWNERS or count > MAX_VERTICAL_BLOCKER_CELLS:
|
||||
last_error = "Vertical walk blocker ownership capacity exceeded."
|
||||
return false
|
||||
_vertical_walk_blockers = candidate
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_rebuild_derived_graph()
|
||||
_mark_graph_changed()
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func remove_vertical_walk_blockers(owner_id: StringName) -> bool:
|
||||
if not _vertical_walk_blockers.has(owner_id):
|
||||
last_error = "Unknown vertical walk blocker owner."
|
||||
return false
|
||||
_vertical_walk_blockers.erase(owner_id)
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_rebuild_derived_graph()
|
||||
_mark_graph_changed()
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
|
||||
func get_vertical_walk_blockers() -> Dictionary:
|
||||
return _vertical_walk_blockers.duplicate(true)
|
||||
|
||||
|
||||
func _rebuild_vertical_walk_blocked_cells() -> void:
|
||||
_vertical_walk_blocked_cells.clear()
|
||||
for cells: Array in _vertical_walk_blockers.values():
|
||||
for cell: Vector3i in cells:
|
||||
_vertical_walk_blocked_cells[cell] = true
|
||||
|
||||
|
||||
func _ordinary_walk_edge_allowed(from_cell: Vector3i, to_cell: Vector3i) -> bool:
|
||||
return from_cell.y == to_cell.y or (
|
||||
not _vertical_walk_blocked_cells.has(from_cell)
|
||||
and not _vertical_walk_blocked_cells.has(to_cell)
|
||||
)
|
||||
|
||||
|
||||
func _vertical_blocker_owner_is_valid(owner_id: StringName) -> bool:
|
||||
var value := String(owner_id)
|
||||
return not value.is_empty() and value.length() <= 128 and value == value.strip_edges()
|
||||
|
||||
|
||||
func configure_connector(
|
||||
connector_id: StringName,
|
||||
from_cell: Vector3i,
|
||||
@@ -995,6 +1073,28 @@ func get_connector(connector_id: StringName) -> Dictionary:
|
||||
return (_connectors[connector_id] as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
## Resolves the same cheapest accessible edge that find_path/calculate_path_cost
|
||||
## use, so a movement driver can distinguish riding from an ordinary step.
|
||||
func get_edge_report(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> Dictionary:
|
||||
var normalized := _normalize_access_ids(connector_access)
|
||||
if not bool(normalized[&"ok"]):
|
||||
last_error = String(normalized[&"error"])
|
||||
return {}
|
||||
if _cell_is_temporarily_blocked(from_cell) or _cell_is_temporarily_blocked(to_cell):
|
||||
return {}
|
||||
var edge := _find_edge(from_cell, to_cell, normalized[&"values"])
|
||||
if edge.is_empty():
|
||||
return {}
|
||||
var report := edge.duplicate(true)
|
||||
report[&"from"] = from_cell
|
||||
report[&"total_cost"] = _edge_total_cost(edge)
|
||||
return report
|
||||
|
||||
|
||||
func get_step_connector(from_cell: Vector3i, to_cell: Vector3i, connector_access: Variant = null) -> StringName:
|
||||
return StringName(get_edge_report(from_cell, to_cell, connector_access).get(&"connector_id", &""))
|
||||
|
||||
|
||||
func get_neighbors(cell: Vector3i) -> Array:
|
||||
if not _adjacency.has(cell):
|
||||
return []
|
||||
@@ -1179,6 +1279,9 @@ func create_snapshot() -> Dictionary:
|
||||
var saved_active_units: Array = []
|
||||
for unit_id: Variant in active_unit_ids:
|
||||
saved_active_units.append((_active_elevator_units[unit_id] as Dictionary).duplicate(true))
|
||||
var saved_vertical_blockers: Array[Dictionary] = []
|
||||
for owner_id: StringName in _sorted_name_keys(_vertical_walk_blockers):
|
||||
saved_vertical_blockers.append({&"owner_id": owner_id, &"cells": (_vertical_walk_blockers[owner_id] as Array).duplicate()})
|
||||
return {
|
||||
&"schema_version": SNAPSHOT_VERSION,
|
||||
&"graph_revision": graph_revision,
|
||||
@@ -1189,14 +1292,18 @@ func create_snapshot() -> Dictionary:
|
||||
&"actor_progress": saved_progress,
|
||||
&"known_elevator_request_ids": saved_known_requests,
|
||||
&"active_elevator_units": saved_active_units,
|
||||
&"vertical_walk_blockers": saved_vertical_blockers,
|
||||
}
|
||||
|
||||
|
||||
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, SNAPSHOT_VERSION]:
|
||||
if version not in [LEGACY_SNAPSHOT_VERSION, TRAFFIC_SNAPSHOT_VERSION, ELEVATOR_INDEX_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")):
|
||||
last_error = "Legacy PathService snapshots cannot contain shaft ownership fields."
|
||||
return false
|
||||
var raw_connectors: Variant = _dictionary_value(snapshot, &"connectors", null)
|
||||
if not raw_connectors is Array:
|
||||
last_error = "PathService snapshot connectors must be an Array."
|
||||
@@ -1210,7 +1317,9 @@ 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
|
||||
var changed := next_connectors != _connectors or not _vertical_walk_blockers.is_empty()
|
||||
_vertical_walk_blockers.clear()
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_connectors = next_connectors
|
||||
_cell_traffic.clear()
|
||||
_actor_cells.clear()
|
||||
@@ -1256,7 +1365,7 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
last_error = progress_result[&"error"]
|
||||
return false
|
||||
var next_progress: Dictionary = progress_result[&"progress"]
|
||||
if version == SNAPSHOT_VERSION:
|
||||
if version >= ELEVATOR_INDEX_SNAPSHOT_VERSION:
|
||||
var raw_known_requests: Variant = _dictionary_value(
|
||||
snapshot,
|
||||
&"known_elevator_request_ids",
|
||||
@@ -1277,6 +1386,13 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
return false
|
||||
next_known_requests = index_result[&"known_request_ids"]
|
||||
next_active_units = index_result[&"active_units"]
|
||||
var next_vertical_blockers: Dictionary = {}
|
||||
if version >= 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):
|
||||
last_error = "PathService snapshot density does not match actor projections."
|
||||
return false
|
||||
@@ -1290,6 +1406,8 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
_connectors = next_connectors
|
||||
_vertical_walk_blockers = next_vertical_blockers
|
||||
_rebuild_vertical_walk_blocked_cells()
|
||||
_cell_traffic = next_traffic
|
||||
_actor_cells = next_actor_cells
|
||||
_actor_progress = next_progress
|
||||
@@ -1320,7 +1438,7 @@ func _rebuild_derived_graph() -> void:
|
||||
var cell: Vector3i = cell_value
|
||||
for direction in _CARDINAL_DIRECTIONS:
|
||||
var neighbor := cell + direction
|
||||
if _passable_cells.has(neighbor):
|
||||
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(cell, neighbor):
|
||||
_append_edge(
|
||||
cell,
|
||||
neighbor,
|
||||
@@ -1587,7 +1705,7 @@ func _route_exists_with_overrides(
|
||||
var neighbor_set: Dictionary = {}
|
||||
for direction: Vector3i in _CARDINAL_DIRECTIONS:
|
||||
var neighbor := current + direction
|
||||
if _passable_cells.has(neighbor):
|
||||
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(current, neighbor):
|
||||
neighbor_set[neighbor] = true
|
||||
for connector_id: StringName in connector_ids:
|
||||
var connector: Dictionary = _connectors[connector_id]
|
||||
@@ -2080,6 +2198,41 @@ func _normalize_snapshot_elevator_indexes(
|
||||
}
|
||||
|
||||
|
||||
func _normalize_snapshot_vertical_blockers(raw: Variant, traffic: Dictionary) -> Dictionary:
|
||||
var failure := {&"ok": false, &"error": "Path shaft ownership rows are invalid, unordered, or crosslink unknown cells."}
|
||||
if not raw is Array or raw.size() > MAX_VERTICAL_BLOCKER_OWNERS:
|
||||
return failure
|
||||
var blockers: Dictionary = {}
|
||||
var previous_owner := ""
|
||||
var total_cells := 0
|
||||
for row: Variant in raw:
|
||||
if not row is Dictionary or row.size() != 2:
|
||||
return failure
|
||||
var owner_value: Variant = _dictionary_value(row, &"owner_id", null)
|
||||
var cells_value: Variant = _dictionary_value(row, &"cells", null)
|
||||
if typeof(owner_value) not in [TYPE_STRING, TYPE_STRING_NAME] or not cells_value is Array:
|
||||
return failure
|
||||
var owner_id := StringName(owner_value)
|
||||
if not _vertical_blocker_owner_is_valid(owner_id) or String(owner_id) <= previous_owner or cells_value.is_empty():
|
||||
return failure
|
||||
previous_owner = String(owner_id)
|
||||
total_cells += cells_value.size()
|
||||
if total_cells > MAX_VERTICAL_BLOCKER_CELLS:
|
||||
return failure
|
||||
var cells: Array[Vector3i] = []
|
||||
var seen: Dictionary = {}
|
||||
for cell: Variant in cells_value:
|
||||
if not cell is Vector3i or not traffic.has(cell) or seen.has(cell):
|
||||
return failure
|
||||
seen[cell] = true
|
||||
cells.append(cell)
|
||||
cells.sort_custom(Callable(self, "_coordinate_less"))
|
||||
if cells != cells_value:
|
||||
return failure
|
||||
blockers[owner_id] = cells
|
||||
return {&"ok": true, &"blockers": blockers}
|
||||
|
||||
|
||||
func _normalize_snapshot_cell_traffic(raw_entries: Array) -> Dictionary:
|
||||
var traffic: Dictionary = {}
|
||||
for raw_entry: Variant in raw_entries:
|
||||
|
||||
Reference in New Issue
Block a user