Files
gamedev-the-steward/world/targets/WorldTargetRegistry.gd
T
2026-08-12 21:58:52 +02:00

301 lines
9.4 KiB
GDScript

class_name WorldTargetRegistry
extends RefCounted
var last_error := ""
var _context_id: StringName
var _world_id: StringName
var _location_id: StringName
var _entries_by_id: Dictionary = {}
var _generation_by_id: Dictionary = {}
var _sorted_target_ids: Array[StringName] = []
var _target_ids_by_kind: Dictionary = {}
var _target_ids_by_capability: Dictionary = {}
func _init(
owned_context_id: StringName = &"",
owned_world_id: StringName = &"",
owned_location_id: StringName = &""
) -> void:
_context_id = owned_context_id
_world_id = owned_world_id
_location_id = owned_location_id
func register_target(
descriptor: WorldTargetDescriptor, provider: Object = null
) -> WorldTargetHandle:
last_error = ""
if _context_id.is_empty():
return _fail_handle("Registry context_id must not be empty")
if descriptor == null or not descriptor.is_valid():
return _fail_handle("Target descriptor is invalid")
if provider != null and not is_instance_valid(provider):
return _fail_handle("Target provider is invalid")
var target_id := descriptor.get_target_id()
if _entries_by_id.has(target_id):
var existing := _entries_by_id[target_id]["descriptor"] as WorldTargetDescriptor
return _fail_handle(
(
"Target ID '%s' is already registered as kind '%s'"
% [target_id, existing.get_target_kind()]
)
)
var isolated_descriptor := descriptor.copy()
var generation := int(_generation_by_id.get(target_id, 0)) + 1
_generation_by_id[target_id] = generation
_entries_by_id[target_id] = {
"descriptor": isolated_descriptor,
"generation": generation,
"has_provider": provider != null,
"provider_ref": weakref(provider) if provider != null else null,
}
_insert_sorted_unique(_sorted_target_ids, target_id)
_add_to_index(_target_ids_by_kind, isolated_descriptor.get_target_kind(), target_id)
for capability_id in isolated_descriptor.get_capability_ids():
_add_to_index(_target_ids_by_capability, capability_id, target_id)
return _make_handle(isolated_descriptor, generation)
func update_target(handle: WorldTargetHandle, descriptor: WorldTargetDescriptor) -> bool:
last_error = ""
var entry := _get_handle_entry(handle)
if entry.is_empty():
return _fail_bool("Target handle is stale or belongs to another registry")
if descriptor == null or not descriptor.is_valid():
return _fail_bool("Target descriptor is invalid")
var previous := entry["descriptor"] as WorldTargetDescriptor
if (
descriptor.get_target_id() != previous.get_target_id()
or descriptor.get_target_kind() != previous.get_target_kind()
):
return _fail_bool("Target identity and kind cannot change during an update")
for capability_id in previous.get_capability_ids():
_remove_from_index(_target_ids_by_capability, capability_id, previous.get_target_id())
var isolated_descriptor := descriptor.copy()
entry["descriptor"] = isolated_descriptor
_entries_by_id[isolated_descriptor.get_target_id()] = entry
for capability_id in isolated_descriptor.get_capability_ids():
_add_to_index(_target_ids_by_capability, capability_id, isolated_descriptor.get_target_id())
return true
func update_target_position(handle: WorldTargetHandle, local_position: Vector3) -> bool:
last_error = ""
var entry := _get_handle_entry(handle)
if entry.is_empty():
return _fail_bool("Target handle is stale or belongs to another registry")
if not local_position.is_finite():
return _fail_bool("Target position must be finite")
var previous := entry["descriptor"] as WorldTargetDescriptor
entry["descriptor"] = previous.with_local_position(local_position)
_entries_by_id[previous.get_target_id()] = entry
return true
func unregister_target(handle: WorldTargetHandle) -> bool:
last_error = ""
var entry := _get_handle_entry(handle)
if entry.is_empty():
return _fail_bool("Target handle is stale or belongs to another registry")
_erase_entry(handle.get_target_id(), entry)
return true
func clear() -> void:
for target_id in _sorted_target_ids.duplicate():
_erase_entry(target_id, _entries_by_id[target_id])
last_error = ""
func prune_invalid_providers() -> Array[StringName]:
var removed_ids: Array[StringName] = []
for target_id in _sorted_target_ids.duplicate():
var entry: Dictionary = _entries_by_id[target_id]
if not bool(entry["has_provider"]):
continue
var provider_ref := entry["provider_ref"] as WeakRef
if provider_ref != null and provider_ref.get_ref() != null:
continue
_erase_entry(target_id, entry)
removed_ids.append(target_id)
return removed_ids
func has_target(target_id: StringName) -> bool:
return _entries_by_id.has(target_id)
func get_context_id() -> StringName:
return _context_id
func get_world_id() -> StringName:
return _world_id
func get_location_id() -> StringName:
return _location_id
func get_identity() -> Dictionary:
return {
"context_id": _context_id,
"world_id": _world_id,
"location_id": _location_id,
}
func size() -> int:
return _entries_by_id.size()
func get_descriptor(target_id: StringName) -> WorldTargetDescriptor:
var entry: Dictionary = _entries_by_id.get(target_id, {})
return entry.get("descriptor") as WorldTargetDescriptor
func get_handle(target_id: StringName) -> WorldTargetHandle:
var entry: Dictionary = _entries_by_id.get(target_id, {})
if entry.is_empty():
return null
return _make_handle(entry["descriptor"] as WorldTargetDescriptor, int(entry["generation"]))
func resolve_handle(handle: WorldTargetHandle) -> WorldTargetDescriptor:
var entry := _get_handle_entry(handle)
return entry.get("descriptor") as WorldTargetDescriptor
func is_handle_valid(handle: WorldTargetHandle, require_provider: bool = false) -> bool:
var entry := _get_handle_entry(handle)
if entry.is_empty():
return false
if not require_provider:
return true
if not bool(entry["has_provider"]):
return false
var provider_ref := entry["provider_ref"] as WeakRef
return provider_ref != null and provider_ref.get_ref() != null
func get_provider(handle: WorldTargetHandle) -> Object:
var entry := _get_handle_entry(handle)
if entry.is_empty() or not bool(entry["has_provider"]):
return null
var provider_ref := entry["provider_ref"] as WeakRef
return provider_ref.get_ref() if provider_ref != null else null
func get_target_ids(
target_kind: StringName = &"", capability_id: StringName = &""
) -> Array[StringName]:
var source_ids: Array = _sorted_target_ids
if not capability_id.is_empty():
source_ids = _target_ids_by_capability.get(capability_id, [])
elif not target_kind.is_empty():
source_ids = _target_ids_by_kind.get(target_kind, [])
var result: Array[StringName] = []
for target_id in source_ids:
var descriptor := get_descriptor(target_id)
if descriptor == null:
continue
if not target_kind.is_empty() and descriptor.get_target_kind() != target_kind:
continue
if not capability_id.is_empty() and not descriptor.has_capability(capability_id):
continue
result.append(target_id)
return result
func get_descriptors(
target_kind: StringName = &"", capability_id: StringName = &""
) -> Array[WorldTargetDescriptor]:
var descriptors: Array[WorldTargetDescriptor] = []
for target_id in get_target_ids(target_kind, capability_id):
descriptors.append(get_descriptor(target_id))
return descriptors
func _get_handle_entry(handle: WorldTargetHandle) -> Dictionary:
if (
handle == null
or not handle.is_well_formed()
or handle.get_registry_instance_id() != get_instance_id()
or handle.get_context_id() != _context_id
):
return {}
var entry: Dictionary = _entries_by_id.get(handle.get_target_id(), {})
if entry.is_empty():
return {}
var descriptor := entry["descriptor"] as WorldTargetDescriptor
if (
int(entry["generation"]) != handle.get_generation()
or descriptor.get_target_kind() != handle.get_target_kind()
):
return {}
return entry
func _make_handle(descriptor: WorldTargetDescriptor, generation: int) -> WorldTargetHandle:
return WorldTargetHandle.new(
get_instance_id(),
_context_id,
descriptor.get_target_id(),
descriptor.get_target_kind(),
generation
)
func _erase_entry(target_id: StringName, entry: Dictionary) -> void:
var descriptor := entry["descriptor"] as WorldTargetDescriptor
_sorted_target_ids.erase(target_id)
_remove_from_index(_target_ids_by_kind, descriptor.get_target_kind(), target_id)
for capability_id in descriptor.get_capability_ids():
_remove_from_index(_target_ids_by_capability, capability_id, target_id)
_entries_by_id.erase(target_id)
func _add_to_index(index: Dictionary, index_id: StringName, target_id: StringName) -> void:
var target_ids: Array = index.get(index_id, [])
_insert_sorted_unique(target_ids, target_id)
index[index_id] = target_ids
func _remove_from_index(index: Dictionary, index_id: StringName, target_id: StringName) -> void:
var target_ids: Array = index.get(index_id, [])
target_ids.erase(target_id)
if target_ids.is_empty():
index.erase(index_id)
else:
index[index_id] = target_ids
func _insert_sorted_unique(target_ids: Array, target_id: StringName) -> void:
var low := 0
var high := target_ids.size()
var target_text := String(target_id)
while low < high:
var middle := (low + high) / 2
var middle_text := String(target_ids[middle])
if middle_text < target_text:
low = middle + 1
else:
high = middle
if low < target_ids.size() and String(target_ids[low]) == target_text:
return
target_ids.insert(low, target_id)
func _fail_handle(message: String) -> WorldTargetHandle:
last_error = message
return null
func _fail_bool(message: String) -> bool:
last_error = message
return false