feat: route resources through typed content

This commit is contained in:
Rijad Zuzo
2026-08-12 20:51:01 +02:00
parent 4e4ae1a969
commit dc0f59fcf0
31 changed files with 1071 additions and 10 deletions
@@ -0,0 +1,237 @@
extends GutTest
func test_core_catalog_enumerates_new_authoring_domains_deterministically() -> void:
var catalog := ContentCatalog.create_core()
assert_true(catalog.is_valid(), "%s" % [catalog.get_errors()])
assert_eq(
_tag_ids(catalog.get_capability_tags()),
[&"item.food", &"item.medicine", &"item.wood", &"resource.harvestable", &"storage.village"]
)
assert_eq(
_resource_ids(catalog.get_resources()),
[&"resource_berry_patch", &"resource_herb_patch", &"resource_tree"]
)
assert_eq(
_storage_ids(catalog.get_storages()),
[&"village_apothecary", &"village_pantry", &"village_woodpile"]
)
var report := catalog.get_authoring_report()
assert_true(report["valid"])
assert_eq(report["resources"], _resource_ids(catalog.get_resources()))
assert_eq(report["storages"], _storage_ids(catalog.get_storages()))
assert_false(report.has("presentation_assets"))
var returned_resources := catalog.get_resources()
returned_resources.clear()
assert_eq(catalog.get_resources().size(), 3)
func test_herb_route_is_data_only_and_legacy_routes_remain_stable() -> void:
var catalog := ContentCatalog.create_core()
var routing := StorageRoutingPolicy.new(catalog)
var herb_source := catalog.get_resource(&"resource_herb_patch")
assert_not_null(herb_source)
assert_eq(herb_source.item_id, &"herb")
assert_eq(herb_source.extraction_action_id, SimulationIds.ACTION_GATHER_FOOD)
assert_eq(routing.resolve_definition(herb_source.item_id).storage_id, &"village_apothecary")
assert_eq(
routing.resolve_definition(SimulationIds.RESOURCE_FOOD).storage_id,
SimulationIds.STORAGE_VILLAGE_PANTRY
)
assert_eq(
routing.resolve_definition(SimulationIds.RESOURCE_WOOD).storage_id,
SimulationIds.STORAGE_VILLAGE_WOODPILE
)
assert_null(routing.resolve_definition(&"unknown_item"))
var apothecary := catalog.get_storage(&"village_apothecary")
var nearly_full := apothecary.create_state({"herb": 39.0})
var states: Array[StorageStateRecord] = [nearly_full]
assert_same(routing.resolve_state(&"herb", 1.0, states), nearly_full)
assert_null(routing.resolve_state(&"herb", 2.0, states))
func test_routes_rank_priority_then_stable_storage_id() -> void:
var core := load(ContentCatalog.CORE_PACK_PATH) as SimulationContentPack
var addon := _pack(&"routing_addon")
addon.required_pack_ids = [&"core"]
var early := _storage(&"a_medicine_store", &"item.medicine", 100)
var low_priority := _storage(&"field_medicine_store", &"item.medicine", 10)
addon.storages = [low_priority, early]
var packs: Array[SimulationContentPack] = [addon, core]
var catalog := ContentCatalog.new()
assert_true(catalog.rebuild(packs).is_empty(), "%s" % [catalog.get_errors()])
var candidates := StorageRoutingPolicy.new(catalog).get_candidates(&"herb")
assert_eq(
_storage_ids(candidates),
[&"a_medicine_store", &"village_apothecary", &"field_medicine_store"]
)
func test_catalog_rejects_duplicate_content_ids_without_publishing_overrides() -> void:
var first := _complete_minimal_pack(&"first")
var second := _complete_minimal_pack(&"second")
var packs: Array[SimulationContentPack] = [second, first]
var catalog := ContentCatalog.new()
var duplicated_cues: Array[StringName] = [&"known.cue", &"known.cue"]
var errors := catalog.rebuild(packs, [], duplicated_cues)
for expected in [
"Duplicate action_id 'shared_action'",
"Duplicate item_id 'shared_item'",
"Duplicate tag_id 'shared_tag'",
"Duplicate resource_id 'shared_resource'",
"Duplicate storage_id 'shared_storage'",
"Duplicate supported presentation cue 'known.cue'",
]:
assert_true(_has_error(errors, expected), "%s missing from %s" % [expected, errors])
assert_false(catalog.is_valid())
assert_true(catalog.get_items().is_empty())
assert_null(catalog.get_storage(&"shared_storage"))
func test_catalog_rejects_unknown_action_item_tag_handler_and_cue_references() -> void:
var pack := _pack(&"broken")
var item := _item(&"broken_item", &"unknown_item_tag")
item.presentation_cue_id = &"unknown.item.cue"
var action := _action(&"broken_action")
action.actor_capability_tags = [&"unknown_actor_tag"]
action.target_capability_tags = [&"unknown_target_tag"]
action.completion_handler_id = &"unknown_handler"
action.presentation_cue_id = &"unknown.action.cue"
var effect := ActionEffect.new()
effect.effect_id = &"broken_effect"
effect.effect_kind = ActionEffect.KIND_SCHEDULE_ACTION
effect.scheduled_action_id = &"unknown_scheduled_action"
effect.item_id = &"unknown_effect_item"
action.effects = [effect]
var resource := _resource(&"broken_resource", &"unknown_resource_item", &"unknown_action")
resource.capability_tag_ids = [&"unknown_resource_tag"]
resource.presentation_cue_id = &"unknown.resource.cue"
var storage := _storage(&"broken_storage", &"unknown_storage_item_tag", 1)
storage.accepted_item_ids = [&"unknown_storage_item"]
storage.capability_tag_ids = [&"unknown_storage_tag"]
storage.deposit_action_id = &"unknown_deposit_action"
storage.presentation_cue_id = &"unknown.storage.cue"
pack.items = [item]
pack.actions = [action]
pack.resources = [resource]
pack.storages = [storage]
var packs: Array[SimulationContentPack] = [pack]
var handlers: Array[StringName] = [&"known_handler"]
var cues: Array[StringName] = [&"known.cue"]
var errors := ContentCatalog.new().rebuild(packs, handlers, cues)
for expected in [
"unknown tag 'unknown_item_tag'",
"unknown presentation cue 'unknown.item.cue'",
"unknown tag 'unknown_actor_tag'",
"unknown tag 'unknown_target_tag'",
"unknown completion handler 'unknown_handler'",
"unknown action 'unknown_scheduled_action'",
"unknown item 'unknown_effect_item'",
"unknown item 'unknown_resource_item'",
"unknown action 'unknown_action'",
"unknown tag 'unknown_resource_tag'",
"unknown item 'unknown_storage_item'",
"unknown tag 'unknown_storage_item_tag'",
"unknown tag 'unknown_storage_tag'",
"unknown action 'unknown_deposit_action'",
"unknown presentation cue 'unknown.storage.cue'",
]:
assert_true(_has_error(errors, expected), "%s missing from %s" % [expected, errors])
func _complete_minimal_pack(pack_id: StringName) -> SimulationContentPack:
var pack := _pack(pack_id)
var tag := CapabilityTagDefinition.new()
tag.tag_id = &"shared_tag"
tag.display_name = "Shared tag"
tag.domain = CapabilityTagDefinition.DOMAIN_ITEM
var item := _item(&"shared_item", tag.tag_id)
var action := _action(&"shared_action")
action.target_type = SimulationIds.TARGET_RESOURCE
action.resource_action_id = action.action_id
var resource := _resource(&"shared_resource", item.item_id, action.action_id)
var storage := _storage(&"shared_storage", tag.tag_id, 1)
pack.capability_tags = [tag]
pack.items = [item]
pack.actions = [action]
pack.resources = [resource]
pack.storages = [storage]
return pack
func _pack(pack_id: StringName) -> SimulationContentPack:
var pack := SimulationContentPack.new()
pack.pack_id = pack_id
pack.display_name = String(pack_id).capitalize()
return pack
func _item(item_id: StringName, tag_id: StringName) -> ItemDefinition:
var item := ItemDefinition.new()
item.item_id = item_id
item.display_name = String(item_id).capitalize()
item.category = ItemDefinition.CATEGORY_RESOURCE
item.capability_tag_ids = [tag_id]
return item
func _action(action_id: StringName) -> ActionDefinition:
var action := ActionDefinition.new()
action.action_id = action_id
action.display_name = String(action_id).capitalize()
action.target_type = SimulationIds.TARGET_ACTIVITY
return action
func _resource(
resource_id: StringName, item_id: StringName, action_id: StringName
) -> ResourceDefinition:
var resource := ResourceDefinition.new()
resource.resource_id = resource_id
resource.display_name = String(resource_id).capitalize()
resource.item_id = item_id
resource.extraction_action_id = action_id
return resource
func _storage(
storage_id: StringName, accepted_item_tag_id: StringName, priority: int
) -> StorageDefinition:
var storage := StorageDefinition.new()
storage.storage_id = storage_id
storage.display_name = String(storage_id).capitalize()
storage.accepted_item_tag_ids = [accepted_item_tag_id]
storage.routing_priority = priority
return storage
func _tag_ids(definitions: Array[CapabilityTagDefinition]) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.tag_id)
return ids
func _resource_ids(definitions: Array[ResourceDefinition]) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.resource_id)
return ids
func _storage_ids(definitions: Array[StorageDefinition]) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.storage_id)
return ids
func _has_error(errors: Array[String], fragment: String) -> bool:
for error in errors:
if fragment in error:
return true
return false
@@ -0,0 +1 @@
uid://deq7osx44nnn3