Add explicit stairs and measure elevator congestion with real actors
This commit is contained in:
+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