feat: index loaded resource discovery

This commit is contained in:
Rijad Zuzo
2026-07-17 00:34:30 +02:00
parent 2c88ed6ea8
commit 5356c34fd7
21 changed files with 1392 additions and 69 deletions
+125 -4
View File
@@ -1,6 +1,8 @@
class_name ActionTargetResolver
extends RefCounted
var last_resource_query_stats: Dictionary = {}
func resolve(
npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node
@@ -28,10 +30,33 @@ func _resolve_resource(
definition: ActionDefinition,
simulation_manager: Node,
active_world_adapter: Node
) -> Dictionary:
last_resource_query_stats = {}
if (
active_world_adapter.has_method("get_resource_candidates_in_radius")
and active_world_adapter.has_method("get_resource_query_profile")
):
return _resolve_resource_spatial(
npc, origin, definition, simulation_manager, active_world_adapter
)
return _resolve_resource_linear(
npc, origin, definition, simulation_manager, active_world_adapter
)
func _resolve_resource_linear(
npc: SimNPC,
origin: Vector3,
definition: ActionDefinition,
simulation_manager: Node,
active_world_adapter: Node
) -> Dictionary:
var best: Dictionary = {}
var best_score := INF
for candidate in active_world_adapter.get_resource_candidates(definition.resource_action_id):
var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates(
definition.resource_action_id
)
for candidate in candidates:
var node_id := StringName(candidate["target_id"])
var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id)
if state == null or not state.can_npc_use() or not state.is_available_for(npc.id):
@@ -41,12 +66,108 @@ func _resolve_resource(
if score < best_score:
best_score = score
best = candidate
if best.is_empty():
last_resource_query_stats = {
"mode": "linear",
"loaded_candidate_count": candidates.size(),
"candidates_inspected": candidates.size(),
"range_pass_count": 1,
}
return _reserve_resource_candidate(best, npc, simulation_manager)
func _resolve_resource_spatial(
npc: SimNPC,
origin: Vector3,
definition: ActionDefinition,
simulation_manager: Node,
active_world_adapter: Node
) -> Dictionary:
var profile: Dictionary = active_world_adapter.get_resource_query_profile(
definition.resource_action_id, origin
)
var loaded_count := int(profile.get("candidate_count", 0))
if loaded_count == 0:
last_resource_query_stats = {
"mode": "spatial",
"loaded_candidate_count": 0,
"candidates_inspected": 0,
"range_pass_count": 0,
}
return {}
var target_id := StringName(best["target_id"])
var best: Dictionary = {}
var best_score := INF
var best_registration_order := 9223372036854775807
var maximum_distance := maxf(float(profile["max_distance"]), 0.0)
var radius := minf(maxf(float(profile["initial_radius"]), 0.1), maximum_distance)
var previous_radius := -1.0
var candidates_inspected := 0
var range_pass_count := 0
while true:
var candidates: Array[Dictionary] = active_world_adapter.get_resource_candidates_in_radius(
definition.resource_action_id, origin, radius, previous_radius
)
range_pass_count += 1
candidates_inspected += candidates.size()
for candidate in candidates:
var node_id := StringName(candidate["target_id"])
var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id)
if state == null or not state.can_npc_use() or not state.is_available_for(npc.id):
continue
var position: Vector3 = candidate["position"]
var score := score_resource_candidate(npc, origin, position, state)
var registration_order := int(candidate["registration_order"])
if (
score < best_score
or (score == best_score and registration_order < best_registration_order)
):
best_score = score
best_registration_order = registration_order
best = candidate
if radius >= maximum_distance:
break
if (
not best.is_empty()
and _minimum_resource_score_beyond(npc, radius, profile) > best_score
):
break
previous_radius = radius
var next_radius := minf(maximum_distance, radius * 2.0)
if next_radius <= radius:
break
radius = next_radius
last_resource_query_stats = {
"mode": "spatial",
"loaded_candidate_count": loaded_count,
"candidates_inspected": candidates_inspected,
"range_pass_count": range_pass_count,
"final_radius": radius,
}
return _reserve_resource_candidate(best, npc, simulation_manager)
func _minimum_resource_score_beyond(npc: SimNPC, radius: float, profile: Dictionary) -> float:
var comfort_overage := maxf(radius - float(profile["max_comfort_distance"]), 0.0)
var risk_weight := 20.0 + maxf(100.0 - npc.energy, 0.0) * 0.2
return (
radius
+ comfort_overage * 2.5
+ float(profile["min_safety_risk"]) * risk_weight
- float(profile["max_discovery_priority"])
)
func _reserve_resource_candidate(
candidate: Dictionary, npc: SimNPC, simulation_manager: Node
) -> Dictionary:
if candidate.is_empty():
return {}
var target_id := StringName(candidate["target_id"])
if not simulation_manager.reserve_resource(target_id, npc.id):
return {}
return best
return candidate
func _resolve_activity(
@@ -0,0 +1,239 @@
class_name LoadedResourceDiscoveryBenchmark
extends RefCounted
const SCHEMA_VERSION := 1
const WORKLOAD_ID := &"loaded_resource_target_resolution"
const RESOURCE_SPACING := 20.0
const DEFAULT_QUERY_COUNT := 400
const DEFAULT_SAMPLE_COUNT := 7
const WARMUP_QUERY_COUNT := 40
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
class LinearResourceAdapter:
extends Node
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
var candidates: Array[Dictionary] = []
for node in ResourceNode.get_all():
if node.action_id != action_id or node.interaction_point == null:
continue
(
candidates
. append(
{
"target_id": String(node.node_id),
"position": node.interaction_point.global_position,
"resource_id": String(node.resource_id),
"safety_risk": node.safety_risk,
"comfort_distance": node.comfort_distance,
"discovery_priority": node.discovery_priority,
}
)
)
return candidates
func create_fixture(parent: Node, resource_count: int, seed_value: int) -> Dictionary:
if parent == null or resource_count <= 0:
return {}
var fixture_root := Node.new()
fixture_root.name = "LoadedResourceDiscoveryFixture"
parent.add_child(fixture_root)
var resource_root := Node3D.new()
resource_root.name = "ResourceNodes"
fixture_root.add_child(resource_root)
for resource_index in resource_count:
resource_root.add_child(_create_resource(resource_index, resource_count))
var adapter := ActiveWorldAdapter.new()
adapter.name = "ActiveWorldAdapter"
fixture_root.add_child(adapter)
var manager: Node = SimulationManagerScript.new()
manager.name = "SimulationManager"
manager.debug_logs = false
manager.simulation_seed = seed_value
manager.active_world_adapter = adapter
fixture_root.add_child(manager)
manager.set_process(false)
manager.register_loaded_resource_nodes()
var npc: SimNPC = manager.npcs[0]
npc.energy = 62.0
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
return {
"root": fixture_root,
"resource_root": resource_root,
"adapter": adapter,
"manager": manager,
"npc": npc,
"resource_count": resource_count,
}
func create_linear_adapter() -> Node:
return LinearResourceAdapter.new()
func measure_fixture(
fixture: Dictionary,
query_count: int = DEFAULT_QUERY_COUNT,
sample_count: int = DEFAULT_SAMPLE_COUNT,
include_spatial: bool = true
) -> Dictionary:
if fixture.is_empty() or query_count <= 0 or sample_count <= 0:
return {}
var adapter: ActiveWorldAdapter = fixture["adapter"]
var origins := _build_origins(int(fixture["resource_count"]), query_count)
var linear_adapter := create_linear_adapter()
(fixture["root"] as Node).add_child(linear_adapter)
_run_queries(fixture, linear_adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
var linear := _measure_mode(fixture, linear_adapter, origins, sample_count, false)
if linear.is_empty():
return {}
var result := {
"schema_version": SCHEMA_VERSION,
"workload_id": String(WORKLOAD_ID),
"resource_count": int(fixture["resource_count"]),
"query_count": query_count,
"sample_count": sample_count,
"linear": linear,
}
if (
include_spatial
and adapter.has_method("get_resource_candidates_in_radius")
and adapter.has_method("get_resource_query_profile")
):
_run_queries(fixture, adapter, origins, mini(WARMUP_QUERY_COUNT, query_count), false)
var spatial := _measure_mode(fixture, adapter, origins, sample_count, true)
if spatial.is_empty() or spatial["selected_checksum"] != linear["selected_checksum"]:
return {}
result["spatial"] = spatial
result["speedup"] = (
float(linear["usec_per_query_median"])
/ maxf(float(spatial["usec_per_query_median"]), 0.0001)
)
result["inspected_reduction_percent"] = (
(
1.0
- float(spatial["candidates_inspected_average"]) / float(fixture["resource_count"])
)
* 100.0
)
return result
func free_fixture(fixture: Dictionary) -> void:
if fixture.has("root") and is_instance_valid(fixture["root"]):
(fixture["root"] as Node).free()
func _measure_mode(
fixture: Dictionary,
query_adapter: Object,
origins: Array[Vector3],
sample_count: int,
spatial: bool
) -> Dictionary:
var elapsed_samples: Array[int] = []
var inspected_samples: Array[float] = []
var checksum := ""
for _sample_index in sample_count:
var sample := _run_queries(fixture, query_adapter, origins, origins.size(), spatial)
if sample.is_empty():
return {}
if checksum.is_empty():
checksum = sample["selected_checksum"]
elif checksum != sample["selected_checksum"]:
return {}
elapsed_samples.append(int(sample["elapsed_usec"]))
inspected_samples.append(float(sample["candidates_inspected_average"]))
elapsed_samples.sort()
inspected_samples.sort()
var elapsed_median := elapsed_samples[elapsed_samples.size() / 2]
return {
"elapsed_usec_samples": elapsed_samples,
"elapsed_usec_median": elapsed_median,
"usec_per_query_median": float(elapsed_median) / float(origins.size()),
"queries_per_second_median": float(origins.size()) * 1000000.0 / float(elapsed_median),
"candidates_inspected_average": inspected_samples[inspected_samples.size() / 2],
"selected_checksum": checksum,
}
func _run_queries(
fixture: Dictionary,
query_adapter: Object,
origins: Array[Vector3],
query_count: int,
spatial: bool
) -> Dictionary:
var manager: Node = fixture["manager"]
var npc: SimNPC = fixture["npc"]
var selected_ids := PackedStringArray()
var inspected_total := 0
var started_usec := Time.get_ticks_usec()
for query_index in query_count:
var result: Dictionary = manager.target_resolver.resolve(
npc, origins[query_index], manager, query_adapter
)
if result.is_empty():
return {}
var target_id := StringName(result["target_id"])
selected_ids.append(String(target_id))
manager.release_resource(target_id, npc.id)
if spatial and not manager.target_resolver.last_resource_query_stats.is_empty():
inspected_total += int(
manager.target_resolver.last_resource_query_stats.get("candidates_inspected", 0)
)
else:
inspected_total += int(fixture["resource_count"])
var elapsed_usec := maxi(Time.get_ticks_usec() - started_usec, 1)
return {
"elapsed_usec": elapsed_usec,
"candidates_inspected_average": float(inspected_total) / float(query_count),
"selected_checksum": "|".join(selected_ids).sha256_text(),
}
func _create_resource(resource_index: int, resource_count: int) -> ResourceNode:
var node := ResourceNode.new()
node.name = "Resource_%04d" % resource_index
node.node_id = StringName("benchmark_resource_%04d" % resource_index)
node.action_id = SimulationIds.ACTION_GATHER_FOOD
node.resource_id = SimulationIds.RESOURCE_FOOD
node.initial_amount = 100000.0
node.initial_enabled = resource_index % 29 != 0
node.safety_risk = float(resource_index % 6) * 0.06
node.comfort_distance = 16.0 + float(resource_index % 5) * 5.0
node.discovery_priority = float(resource_index % 9) * 0.35
node.debug_label_enabled = false
node.position = _resource_position(resource_index, resource_count)
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
node.add_child(interaction_point)
return node
func _build_origins(resource_count: int, query_count: int) -> Array[Vector3]:
var origins: Array[Vector3] = []
for query_index in query_count:
var resource_index := (query_index * 37 + 11) % resource_count
var offset := Vector3(
float((query_index * 7) % 13) - 6.0, 0.0, float((query_index * 11) % 17) - 8.0
)
origins.append(_resource_position(resource_index, resource_count) + offset)
return origins
func _resource_position(resource_index: int, resource_count: int) -> Vector3:
var columns := ceili(sqrt(float(resource_count)))
var column := resource_index % columns
var row := resource_index / columns
var centered_column := float(column) - float(columns - 1) * 0.5
var row_count := ceili(float(resource_count) / float(columns))
var centered_row := float(row) - float(row_count - 1) * 0.5
return Vector3(
centered_column * RESOURCE_SPACING,
sin(float(resource_index) * 0.37) * 2.5,
centered_row * RESOURCE_SPACING
)
@@ -0,0 +1 @@
uid://ccstdqk3xgxnu