Save The Tree implementation and acceptance work in progress

Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
Takeshi
2026-10-11 15:58:00 +09:00
parent 39f6068461
commit b84bade045
235 changed files with 495831 additions and 312 deletions
+402 -9
View File
@@ -76,11 +76,14 @@ var last_no_route_reason: StringName = ROUTE_OK
var last_path_cost := 0
var _passable_cells: Dictionary = {}
var _sorted_passable_cell_keys: Array = []
var _connectors: Dictionary = {}
var _adjacency: Dictionary = {}
var _vertical_walk_blockers: Dictionary = {}
var _vertical_walk_blocked_cells: Dictionary = {}
var _path_cache: Dictionary = {}
# Derived state limited to one synchronous candidate projection, never serialized.
var _query_batch: Dictionary = {}
var _cell_traffic: Dictionary = {}
var _actor_cells: Dictionary = {}
var _actor_progress: Dictionary = {}
@@ -272,6 +275,7 @@ func configure_connector(
and not _active_request_id_exists(StringName(request_id)):
last_error = "Connector configuration reuses a retired elevator request id."
return false
var previous_configuration: Dictionary = _connectors.get(connector_id, {})
_connectors[connector_id] = configuration
var next_active: Dictionary = index_result[&"active_units"]
for unit_id: Variant in _active_elevator_units:
@@ -282,7 +286,7 @@ func configure_connector(
_refresh_transit_actor_ids()
for request_id: Variant in index_result[&"known_request_ids"]:
_known_elevator_request_ids[request_id] = true
_rebuild_derived_graph()
_rebuild_connector_endpoint_edges(previous_configuration, configuration)
_mark_graph_changed()
last_error = ""
return true
@@ -1021,7 +1025,14 @@ func _find_path_internal(
_path_cache.erase(cache_key)
cache_misses += 1
var path := _run_a_star(start, goal, allowed_layers, allowed_access_ids)
if start.z != goal.z and not _guest_batch_has_permitted_layer_crossing(start, goal, allowed_layers, allowed_access_ids):
return _route_failure(ROUTE_GOAL_UNREACHABLE)
var shared: Variant = _query_batch_path(start, goal, allowed_layers, allowed_access_ids)
var path: Array[Vector3i] = []
if shared == null:
path = _run_a_star(start, goal, allowed_layers, allowed_access_ids)
else:
path.assign(shared)
if path.is_empty():
return _route_failure(_diagnose_no_route_reason(
start,
@@ -1031,11 +1042,282 @@ func _find_path_internal(
))
_path_cache[cache_key] = _copy_path(path)
last_no_route_reason = ROUTE_OK
last_path_cost = _calculate_path_cost_for_access(path, allowed_access_ids)
var batch_cost := -1
if allowed_access_ids.is_empty() and allowed_layers == [FRONT,BACK]:
batch_cost = _guest_batch_path_cost(start,goal,path)
last_path_cost = batch_cost if batch_cost >= 0 else _calculate_path_cost_for_access(path, allowed_access_ids)
last_error = ""
return path
# Only the default guest access profile is shared. A mutation invalidates the
# whole batch through the same public cache invalidation paths as ordinary routes.
func begin_guest_path_query_batch(start: Vector3i, goals: Array) -> void:
_query_batch.clear()
# Without connectors the original Manhattan solver stops on goal pop.
# Equal-cost canonical parents can then differ: retain that solver exactly.
if _connectors.is_empty(): return
var layers: Array[int] = [FRONT, BACK]
var access: Array[StringName] = []
var targets: Dictionary = {}
var crossings: Dictionary = {}
for value: Variant in goals:
if not value is Vector3i: continue
var goal: Vector3i = value
if not _passable_cells.has(goal) or _cell_is_temporarily_blocked(goal): continue
if start.z != goal.z:
if not crossings.has(goal.z): crossings[goal.z] = _has_permitted_layer_crossing(start.z, goal.z, access)
if not bool(crossings[goal.z]): continue
targets[goal] = true
if not targets.is_empty() and _passable_cells.has(start) and not _cell_is_temporarily_blocked(start):
_query_batch = {&"start":start, &"targets":targets, &"ready":false, &"tree":{}, &"crossings":crossings}
# Directed layer permission depends only on connectors/access in this synchronous
# default guest batch. All existing invalidation and restore paths clear it.
func _guest_batch_has_permitted_layer_crossing(start: Vector3i, goal: Vector3i, layers: Array[int], access: Array[StringName]) -> bool:
if not _query_batch.is_empty() and start == _query_batch[&"start"] and layers == [FRONT,BACK] and access.is_empty():
var crossings: Dictionary = _query_batch[&"crossings"]
if crossings.has(goal.z): return bool(crossings[goal.z])
return _has_permitted_layer_crossing(start.z,goal.z,access)
func end_guest_path_query_batch() -> void:
_query_batch.clear()
func _query_batch_path(start: Vector3i, goal: Vector3i, layers: Array[int], access: Array[StringName]) -> Variant:
if _query_batch.is_empty() or start != _query_batch[&"start"] \
or layers != [FRONT, BACK] or not access.is_empty() or not _query_batch[&"targets"].has(goal):
return null
if start == goal: return [start]
if not bool(_query_batch[&"ready"]):
_query_batch[&"tree"] = _build_guest_query_tree(start, _query_batch[&"targets"], layers, access)
_query_batch[&"arithmetic_wrapped"] = _guest_tree_arithmetic_wrapped
_query_batch[&"ready"] = true
var tree: Dictionary = _query_batch[&"tree"]
if not bool(tree.get(&"usable", false)): return null
var settled: Dictionary = tree[&"settled"]
if not settled.has(goal): return []
return _reconstruct_path(tree[&"parents"], goal)
# Positive edge costs make every shortest predecessor settle before its child.
# Resolving equal costs by the original coordinate parent rule gives the same
# canonical paths as the admissible goal search. Stop after requested goals, not
# after visiting the entire graph. Any nonpositive edge retains the old solver.
var _guest_tree_workspace: Dictionary = {}
# Derived search state only. Wrapped accumulated costs cannot price a cell path.
var _guest_tree_arithmetic_wrapped: bool = false
func _build_guest_query_tree(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
_guest_tree_arithmetic_wrapped = false
if get_script().resource_path != "res://scripts/world/path_service.gd" or layers != [FRONT,BACK] or not access.is_empty():
return _build_guest_query_tree_uncached(start,goals,layers,access)
return _build_guest_query_tree_shared(start,goals,layers,access)
func _build_guest_query_tree_uncached(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
# The sorted graph cells preserve the original x/y/layer heap and parent order.
# All numeric workspace is local to this search, including the coordinate map.
var cells: Array = _sorted_passable_cell_keys
var indices: Dictionary = {}
for index: int in range(cells.size()): indices[cells[index]] = index
if not indices.has(start): return {&"usable":false}
var costs := PackedInt64Array()
var parents := PackedInt64Array()
var finalized := PackedByteArray()
if costs.resize(cells.size()) != OK or parents.resize(cells.size()) != OK or finalized.resize(cells.size()) != OK:
return {&"usable":false}
costs.fill(_INFINITE_COST)
parents.fill(-1)
var start_index: int = indices[start]
costs[start_index] = 0
var discovered: Array[int] = [start_index]
var parent_discovered: Array[int] = []
var heap_nodes: Array[int] = []
var heap_costs: Array[int] = []
_indexed_tree_heap_push(heap_nodes, heap_costs, start_index, 0)
var pending: Dictionary = goals.duplicate()
var settled: Dictionary = {}
while not heap_nodes.is_empty():
var entry_cost: int = heap_costs[0]
var current_index := _indexed_tree_heap_pop(heap_nodes, heap_costs)
if finalized[current_index] != 0 or entry_cost != costs[current_index]: continue
var current: Vector3i = cells[current_index]
finalized[current_index] = 1
settled[current] = true
pending.erase(current)
if pending.is_empty(): break
var edges: Array = _adjacency.get(current, [])
for value: Variant in edges:
var edge: Dictionary = value
var neighbor: Vector3i = edge[&"to"]
if neighbor.z not in layers or _cell_is_temporarily_blocked(neighbor) \
or not _edge_meets_vertical_policy(current,edge) or not _edge_is_accessible(edge,access): continue
var edge_cost := _edge_total_cost(edge)
if edge_cost <= 0: return {&"usable":false}
if not indices.has(neighbor): return {&"usable":false}
var neighbor_index: int = indices[neighbor]
var cost := costs[current_index] + edge_cost
var known := costs[neighbor_index]
var better_parent := cost == known and parents[neighbor_index] >= 0 and current_index < parents[neighbor_index]
if cost > known or (cost == known and not better_parent): continue
if parents[neighbor_index] < 0:
parent_discovered.append(neighbor_index)
if neighbor_index != start_index: discovered.append(neighbor_index)
parents[neighbor_index] = current_index
costs[neighbor_index] = cost
finalized[neighbor_index] = 0
settled.erase(neighbor)
_indexed_tree_heap_push(heap_nodes, heap_costs, neighbor_index, cost)
# Keep the existing result dictionaries and their first-discovery order.
var result_parents: Dictionary = {}
var result_costs: Dictionary = {}
for index: int in discovered:
var cell: Vector3i = cells[index]
result_costs[cell] = costs[index]
for index: int in parent_discovered:
result_parents[cells[index]] = cells[parents[index]]
return {&"usable":true,&"settled":settled,&"parents":result_parents,&"costs":result_costs}
func _build_guest_query_tree_shared(start: Vector3i, goals: Dictionary, layers: Array[int], access: Array[StringName]) -> Dictionary:
# The sorted graph cells preserve the original x/y/layer heap and parent order.
# All numeric workspace is local to this search, including the coordinate map.
var cells: Array = _sorted_passable_cell_keys
if _guest_tree_workspace.is_empty():
var fresh_indices: Dictionary = {}
for index: int in range(cells.size()): fresh_indices[cells[index]] = index
_guest_tree_workspace = {&"indices":fresh_indices, &"edges":{}}
var indices: Dictionary = _guest_tree_workspace[&"indices"]
if not indices.has(start): return {&"usable":false}
var costs := PackedInt64Array()
var parents := PackedInt64Array()
var finalized := PackedByteArray()
if costs.resize(cells.size()) != OK or parents.resize(cells.size()) != OK or finalized.resize(cells.size()) != OK:
return {&"usable":false}
costs.fill(_INFINITE_COST)
parents.fill(-1)
var start_index: int = indices[start]
costs[start_index] = 0
var discovered: Array[int] = [start_index]
var parent_discovered: Array[int] = []
var heap_nodes: Array[int] = []
var heap_costs: Array[int] = []
_indexed_tree_heap_push(heap_nodes, heap_costs, start_index, 0)
var pending: Dictionary = goals.duplicate()
var settled: Dictionary = {}
while not heap_nodes.is_empty():
var entry_cost: int = heap_costs[0]
var current_index := _indexed_tree_heap_pop(heap_nodes, heap_costs)
if finalized[current_index] != 0 or entry_cost != costs[current_index]: continue
var current: Vector3i = cells[current_index]
finalized[current_index] = 1
settled[current] = true
pending.erase(current)
if pending.is_empty(): break
var edges: Array = _guest_tree_shared_edges(current, layers, access)
for value: Variant in edges:
var row: Array = value
var neighbor: Vector3i = row[0]
var edge_cost: int = row[1]
if edge_cost <= 0: return {&"usable":false}
if not indices.has(neighbor): return {&"usable":false}
var neighbor_index: int = indices[neighbor]
var cost := costs[current_index] + edge_cost
if cost < costs[current_index]: _guest_tree_arithmetic_wrapped = true
var known := costs[neighbor_index]
var better_parent := cost == known and parents[neighbor_index] >= 0 and current_index < parents[neighbor_index]
if cost > known or (cost == known and not better_parent): continue
if parents[neighbor_index] < 0:
parent_discovered.append(neighbor_index)
if neighbor_index != start_index: discovered.append(neighbor_index)
parents[neighbor_index] = current_index
costs[neighbor_index] = cost
finalized[neighbor_index] = 0
settled.erase(neighbor)
_indexed_tree_heap_push(heap_nodes, heap_costs, neighbor_index, cost)
# Keep the existing result dictionaries and their first-discovery order.
var result_parents: Dictionary = {}
var result_costs: Dictionary = {}
for index: int in discovered:
var cell: Vector3i = cells[index]
result_costs[cell] = costs[index]
for index: int in parent_discovered:
result_parents[cells[index]] = cells[parents[index]]
return {&"usable":true,&"settled":settled,&"parents":result_parents,&"costs":result_costs}
func _guest_tree_shared_edges(current: Vector3i, layers: Array[int], access: Array[StringName]) -> Array:
var shared: Dictionary = _guest_tree_workspace[&"edges"]
if shared.has(current): return shared[current]
var result: Array = []
for value: Variant in _adjacency.get(current,[]):
var edge: Dictionary = value
var neighbor: Vector3i = edge[&"to"]
if neighbor.z not in layers or _cell_is_temporarily_blocked(neighbor) \
or not _edge_meets_vertical_policy(current,edge) or not _edge_is_accessible(edge,access): continue
var cost := _edge_total_cost(edge)
result.append([neighbor,cost])
if cost <= 0 or not (_guest_tree_workspace[&"indices"] as Dictionary).has(neighbor): break
shared[current]=result
return result
func _indexed_tree_heap_push(nodes: Array[int], costs: Array[int], node: int, cost: int) -> void:
nodes.append(node)
costs.append(cost)
var index := nodes.size() - 1
while index > 0:
var parent := (index - 1) >> 1
if cost > costs[parent] or (cost == costs[parent] and node >= nodes[parent]): break
nodes[index] = nodes[parent]
costs[index] = costs[parent]
index = parent
nodes[index] = node
costs[index] = cost
func _indexed_tree_heap_pop(nodes: Array[int], costs: Array[int]) -> int:
var first := nodes[0]
var last_node: int = nodes.pop_back()
var last_cost: int = costs.pop_back()
if nodes.is_empty(): return first
var index := 0
while index * 2 + 1 < nodes.size():
var child := index * 2 + 1
if child + 1 < nodes.size() and (costs[child + 1] < costs[child] or (costs[child + 1] == costs[child] and nodes[child + 1] < nodes[child])):
child += 1
if costs[child] > last_cost or (costs[child] == last_cost and nodes[child] >= last_node): break
nodes[index] = nodes[child]
costs[index] = costs[child]
index = child
nodes[index] = last_node
costs[index] = last_cost
return first
# A matching current batch route without arithmetic wrap has an exact cost. Compare
# the whole path, not only its endpoints: a valid alternate may cost differently.
# Mutation/restore invalidation clears the batch even for equal revision numbers.
func _guest_batch_path_cost(start: Vector3i, goal: Vector3i, path: Array) -> int:
if _query_batch.is_empty() or start != _query_batch[&"start"] or not _query_batch[&"targets"].has(goal): return -1
if start == goal and path == [start]: return 0
if not bool(_query_batch[&"ready"]): return -1
if bool(_query_batch.get(&"arithmetic_wrapped", false)): return -1
var tree: Dictionary = _query_batch[&"tree"]
if not bool(tree.get(&"usable",false)) or not tree[&"settled"].has(goal): return -1
if path != _reconstruct_path(tree[&"parents"],goal): return -1
return int(tree[&"costs"][goal])
# Same public last_error behavior as default-access calculate_path_cost.
func calculate_guest_batch_path_cost(start: Vector3i, goal: Vector3i, path: Array) -> int:
var cost := _guest_batch_path_cost(start,goal,path)
if cost < 0: return calculate_path_cost(path)
last_error = ""
return cost
func find_path_report(
start: Vector3i,
goal: Vector3i,
@@ -1087,6 +1369,8 @@ func invalidate_cache_for_cells(cells: Variant) -> int:
if not normalization[&"ok"]:
last_error = normalization[&"error"]
return 0
_query_batch.clear()
_guest_tree_workspace.clear()
var affected: Dictionary = normalization[&"cells"]
var keys_to_remove: Array = []
for cache_key: Variant in _path_cache:
@@ -1104,6 +1388,8 @@ func invalidate_local_cache(cells: Variant) -> int:
func invalidate_all_cache() -> int:
_query_batch.clear()
_guest_tree_workspace.clear()
var removed := _path_cache.size()
_path_cache.clear()
return removed
@@ -1158,8 +1444,20 @@ func get_edge_report(from_cell: Vector3i, to_cell: Vector3i, connector_access: V
return report
# The caller only needs the selected connector id. Avoid constructing the
# detailed report and its extra cost calculation. Without a connector edge to
# this destination every possible selected edge has the empty connector id.
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", &""))
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 &""
for edge: Dictionary in _adjacency.get(from_cell, []):
if edge[&"to"] == to_cell and not StringName(edge[&"connector_id"]).is_empty():
return StringName(_find_edge(from_cell,to_cell,normalized[&"values"]).get(&"connector_id", &""))
return &""
func get_neighbors(cell: Vector3i) -> Array:
@@ -1322,8 +1620,7 @@ func create_snapshot() -> Dictionary:
connector[&"request_queue"] = queue
saved_connectors.append(connector)
var cells: Array = _passable_cells.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
var cells: Array = _sorted_passable_cell_keys
var saved_cell_traffic: Array = []
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
@@ -1536,10 +1833,56 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
return true
## A single connector changes only the outgoing edges of its old/new endpoints.
## Keep the same walk-edge rules, sorted connector iteration and edge ordering
## as the full rebuild. Topology changes still use the full graph rebuild.
func _rebuild_connector_endpoint_edges(previous: Dictionary, current: Dictionary) -> void:
var affected: Dictionary = {}
for configuration: Dictionary in [previous, current]:
if configuration.is_empty():
continue
for key: StringName in [&"from", &"to"]:
var cell: Vector3i = configuration[key]
if _passable_cells.has(cell):
affected[cell] = true
var cells: Array = affected.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
_adjacency[cell] = []
for direction in _CARDINAL_DIRECTIONS:
var neighbor := cell + direction
if _passable_cells.has(neighbor) and _ordinary_walk_edge_allowed(cell, neighbor):
_append_edge(cell, neighbor, ORTHOGONAL_MOVE_COST, &"", &"", MOVEMENT_WALK)
var connector_ids: Array = _connectors.keys()
connector_ids.sort_custom(Callable(self, "_string_name_less"))
for connector_id_value: Variant in connector_ids:
var connector_id: StringName = connector_id_value
var connector: Dictionary = _connectors[connector_id]
if not _connector_is_operational(connector):
continue
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
if not _passable_cells.has(from_cell) or not _passable_cells.has(to_cell):
continue
var cost: int = connector[&"cost"]
var access_id: StringName = connector[&"access_id"]
var movement_type: StringName = connector[&"movement_type"]
if affected.has(from_cell):
_append_edge(from_cell, to_cell, cost, connector_id, access_id, movement_type)
if bool(connector[&"bidirectional"]) and affected.has(to_cell):
_append_edge(to_cell, from_cell, cost, connector_id, access_id, movement_type)
for cell_value: Variant in cells:
var edges: Array = _adjacency[cell_value]
edges.sort_custom(Callable(self, "_edge_less"))
_adjacency[cell_value] = edges
func _rebuild_derived_graph() -> void:
_adjacency.clear()
var cells: Array = _passable_cells.keys()
cells.sort_custom(Callable(self, "_coordinate_less"))
_sorted_passable_cell_keys = cells
for cell_value: Variant in cells:
var cell: Vector3i = cell_value
_adjacency[cell] = []
@@ -1611,19 +1954,26 @@ func _run_a_star(
if start == goal:
return [start]
var connector_scale := _positive_connector_heuristic_scale()
var settled_goal_cost := _INFINITE_COST
var frontier: Array[Array] = []
_heap_push(frontier, [start, 0, _heuristic(start, goal)])
_heap_push(frontier, [start, 0, _query_distance_lower_bound(start, goal, connector_scale)])
var closed_set: Dictionary = {}
var came_from: Dictionary = {}
var g_score: Dictionary = {start: 0}
while not frontier.is_empty():
var entry := _heap_pop(frontier)
if connector_scale > 0 and int(entry[2]) > settled_goal_cost:
break
var current: Vector3i = entry[0]
if closed_set.has(current) or int(entry[1]) != int(g_score[current]):
continue
if current == goal:
return _reconstruct_path(came_from, current)
if connector_scale <= 0:
return _reconstruct_path(came_from, current)
settled_goal_cost = int(entry[1])
continue
closed_set[current] = true
var edges: Array = _adjacency.get(current, [])
@@ -1647,11 +1997,37 @@ func _run_a_star(
g_score[neighbor] = tentative_cost
if closed_set.has(neighbor):
closed_set.erase(neighbor)
_heap_push(frontier, [neighbor, tentative_cost, tentative_cost + _heuristic(neighbor, goal)])
_heap_push(frontier, [neighbor, tentative_cost, tentative_cost + _query_distance_lower_bound(neighbor, goal, connector_scale)])
if settled_goal_cost < _INFINITE_COST:
return _reconstruct_path(came_from, goal)
return []
## Every permitted edge costs at least scale * its Manhattan displacement.
## A zero-cost connector keeps the existing conservative search unchanged.
func _positive_connector_heuristic_scale() -> int:
if _connectors.is_empty():
return 0
var scale := ORTHOGONAL_MOVE_COST
for connector_value: Variant in _connectors.values():
var connector: Dictionary = connector_value
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
var displacement := absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y) + absi(from_cell.z - to_cell.z)
if displacement > 0:
scale = mini(scale, int(connector[&"cost"]) / displacement)
if scale <= 0:
return 0
return scale
func _query_distance_lower_bound(from_cell: Vector3i, to_cell: Vector3i, connector_scale: int) -> int:
if connector_scale <= 0:
return _heuristic(from_cell, to_cell)
return (absi(from_cell.x - to_cell.x) + absi(from_cell.y - to_cell.y) + absi(from_cell.z - to_cell.z)) * connector_scale
# Query-local heap. Stale improved-cost entries are skipped on removal.
# Keep the original f-score / x / y / layer order, including reopened nodes.
func _heap_less(left: Array, right: Array) -> bool:
@@ -1796,6 +2172,23 @@ func _cell_is_temporarily_blocked(cell: Vector3i) -> bool:
return not traffic.is_empty() and bool(traffic[&"temporary_block"])
## Cardinal walking never changes layers. Even an unavailable or blocked
## connector must be retained here so the ordinary solver preserves its reason.
## Only the absence of any permitted directed crossing proves unreachable.
func _has_permitted_layer_crossing(from_layer: int, to_layer: int, allowed_access_ids: Array[StringName]) -> bool:
for connector: Dictionary in _connectors.values():
var access_id: StringName = connector[&"access_id"]
if not access_id.is_empty() and access_id not in allowed_access_ids:
continue
var from_cell: Vector3i = connector[&"from"]
var to_cell: Vector3i = connector[&"to"]
if from_cell.z == from_layer and to_cell.z == to_layer:
return true
if bool(connector[&"bidirectional"]) and to_cell.z == from_layer and from_cell.z == to_layer:
return true
return false
func _diagnose_no_route_reason(
start: Vector3i,
goal: Vector3i,
+13 -12
View File
@@ -191,8 +191,8 @@ func get_quarantine_reason() -> StringName:
return _quarantine_reason
func get_base_walkable_cells() -> Array[Vector3i]:
return _sorted_cells(_base_walkable_cells.keys())
func get_base_walkable_cells(sort_result := true) -> Array[Vector3i]:
return _sorted_cells(_base_walkable_cells.keys(), sort_result)
func get_projected_route_cells() -> Array[Vector3i]:
@@ -563,7 +563,7 @@ func _projection_status_internal() -> Dictionary:
}
var current_source_revision := _source.get_revision_for_category(_source.CATEGORY_ROUTE)
var current_route_cells := _source.get_route_cells()
var path_capture := _capture_path_state(_path)
var path_capture := _capture_path_state(_path, false)
var source_changed: bool = (
current_source_revision != source_revision
or current_route_cells != _projected_route_cells
@@ -573,7 +573,7 @@ func _projection_status_internal() -> Dictionary:
or path_capture[&"cells"] != _projected_walkable_cells
)
var projection_crosslink_ok: bool = (
_union_cells(get_base_walkable_cells(), _projected_route_cells)
_union_cells(get_base_walkable_cells(false), _projected_route_cells)
== _projected_walkable_cells
)
return {
@@ -690,7 +690,7 @@ func _quarantine(reason: StringName) -> void:
_transaction_active = false
static func _capture_path_state(path_service: PathService) -> Dictionary:
static func _capture_path_state(path_service: PathService, capture_snapshot := true) -> Dictionary:
var snapshot := path_service.create_snapshot()
var parsed_cells := _path_cells_from_snapshot(snapshot)
if not bool(parsed_cells[&"ok"]):
@@ -701,11 +701,11 @@ static func _capture_path_state(path_service: PathService) -> Dictionary:
&"ok": false,
&"message": "Path snapshot topology has not been materialized into the live graph.",
}
return {
&"ok": true,
&"snapshot": snapshot.duplicate(true),
&"cells": (parsed_cells[&"cells"] as Array).duplicate(),
}
var result := {&"ok": true}
if capture_snapshot:
result[&"snapshot"] = snapshot.duplicate(true)
result[&"cells"] = (parsed_cells[&"cells"] as Array).duplicate()
return result
static func _path_cells_from_snapshot(snapshot: Dictionary) -> Dictionary:
@@ -1072,14 +1072,15 @@ static func _union_cells(first: Array, second: Array) -> Array[Vector3i]:
return _sorted_cells(union.keys())
static func _sorted_cells(source: Variant) -> Array[Vector3i]:
static func _sorted_cells(source: Variant, sort_result := true) -> Array[Vector3i]:
var result: Array[Vector3i] = []
if not source is Array:
return result
for raw_cell: Variant in source:
if raw_cell is Vector3i:
result.append(raw_cell)
result.sort_custom(_cell_less)
if sort_result:
result.sort_custom(_cell_less)
return result
+5 -4
View File
@@ -229,8 +229,8 @@ func is_walkable(cell: Vector3i) -> bool:
return is_in_bounds(cell) and bool(_walkable.get(cell, false))
func get_walkable_cells() -> Array[Vector3i]:
return _sorted_dictionary_cells(_walkable)
func get_walkable_cells(sort_result := true) -> Array[Vector3i]:
return _sorted_dictionary_cells(_walkable, sort_result)
func set_excavated(cell: Vector3i, value: bool) -> bool:
@@ -728,12 +728,13 @@ func _sorted_entity_ids() -> Array[StringName]:
return result
func _sorted_dictionary_cells(source: Dictionary) -> Array[Vector3i]:
func _sorted_dictionary_cells(source: Dictionary, sort_result := true) -> Array[Vector3i]:
var result: Array[Vector3i] = []
for cell: Variant in source:
if cell is Vector3i:
result.append(cell)
result.sort_custom(_cell_less)
if sort_result:
result.sort_custom(_cell_less)
return result