feat: author emergent situations and dialogue

This commit is contained in:
Rijad Zuzo
2026-08-12 21:58:12 +02:00
parent 14120115d4
commit d12e9515fb
29 changed files with 856 additions and 143 deletions
+300 -3
View File
@@ -2,6 +2,15 @@ class_name ContentCatalog
extends RefCounted extends RefCounted
const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres" const CORE_PACK_PATH := "res://simulation/definitions/packs/core.tres"
const DEFAULT_SITUATION_EVENT_PREDICATE_IDS: Array[StringName] = [
&"event_type", &"event_payload_equals", &"event_participant_id", &"event_pantry_supplied"
]
const DEFAULT_SITUATION_STATE_PREDICATE_IDS: Array[StringName] = [
&"state_number_lte", &"state_number_gte"
]
const DEFAULT_SITUATION_EXPIRY_PREDICATE_IDS: Array[StringName] = [
&"state_number_gt", &"age_reached"
]
var _valid := false var _valid := false
var _errors: Array[String] = [] var _errors: Array[String] = []
@@ -14,6 +23,9 @@ var _resources: Array[ResourceDefinition] = []
var _storages: Array[StorageDefinition] = [] var _storages: Array[StorageDefinition] = []
var _enemies: Array[EnemyDefinition] = [] var _enemies: Array[EnemyDefinition] = []
var _animals: Array[AnimalDefinition] = [] var _animals: Array[AnimalDefinition] = []
var _situations: Array[SituationDefinition] = []
var _dialogue_intents: Array[ConversationIntentDefinition] = []
var _dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
var _packs_by_id: Dictionary = {} var _packs_by_id: Dictionary = {}
var _actions_by_id: Dictionary = {} var _actions_by_id: Dictionary = {}
var _professions_by_id: Dictionary = {} var _professions_by_id: Dictionary = {}
@@ -23,11 +35,17 @@ var _resources_by_id: Dictionary = {}
var _storages_by_id: Dictionary = {} var _storages_by_id: Dictionary = {}
var _enemies_by_id: Dictionary = {} var _enemies_by_id: Dictionary = {}
var _animals_by_id: Dictionary = {} var _animals_by_id: Dictionary = {}
var _situations_by_id: Dictionary = {}
var _dialogue_intents_by_id: Dictionary = {}
var _dialogue_template_catalogs_by_id: Dictionary = {}
static func create_core( static func create_core(
supported_handler_ids: Array[StringName] = [], supported_handler_ids: Array[StringName] = [],
supported_presentation_cue_ids: Array[StringName] = [] supported_presentation_cue_ids: Array[StringName] = [],
supported_situation_event_predicate_ids: Array[StringName] = [],
supported_situation_state_predicate_ids: Array[StringName] = [],
supported_situation_expiry_predicate_ids: Array[StringName] = []
) -> ContentCatalog: ) -> ContentCatalog:
var catalog := ContentCatalog.new() var catalog := ContentCatalog.new()
var pack := load(CORE_PACK_PATH) as SimulationContentPack var pack := load(CORE_PACK_PATH) as SimulationContentPack
@@ -35,14 +53,24 @@ static func create_core(
catalog._errors = ["Core content pack failed to load from '%s'" % CORE_PACK_PATH] catalog._errors = ["Core content pack failed to load from '%s'" % CORE_PACK_PATH]
return catalog return catalog
var packs: Array[SimulationContentPack] = [pack] var packs: Array[SimulationContentPack] = [pack]
catalog.rebuild(packs, supported_handler_ids, supported_presentation_cue_ids) catalog.rebuild(
packs,
supported_handler_ids,
supported_presentation_cue_ids,
supported_situation_event_predicate_ids,
supported_situation_state_predicate_ids,
supported_situation_expiry_predicate_ids
)
return catalog return catalog
func rebuild( func rebuild(
content_packs: Array[SimulationContentPack], content_packs: Array[SimulationContentPack],
supported_handler_ids: Array[StringName] = [], supported_handler_ids: Array[StringName] = [],
supported_presentation_cue_ids: Array[StringName] = [] supported_presentation_cue_ids: Array[StringName] = [],
supported_situation_event_predicate_ids: Array[StringName] = [],
supported_situation_state_predicate_ids: Array[StringName] = [],
supported_situation_expiry_predicate_ids: Array[StringName] = []
) -> Array[String]: ) -> Array[String]:
_clear_published_content() _clear_published_content()
var errors: Array[String] = [] var errors: Array[String] = []
@@ -69,6 +97,24 @@ func rebuild(
errors.append("Duplicate supported presentation cue '%s'" % cue_id) errors.append("Duplicate supported presentation cue '%s'" % cue_id)
else: else:
supported_cues[cue_id] = true supported_cues[cue_id] = true
var supported_event_predicates := _validated_id_set(
supported_situation_event_predicate_ids,
DEFAULT_SITUATION_EVENT_PREDICATE_IDS,
"situation event predicate",
errors
)
var supported_state_predicates := _validated_id_set(
supported_situation_state_predicate_ids,
DEFAULT_SITUATION_STATE_PREDICATE_IDS,
"situation state predicate",
errors
)
var supported_expiry_predicates := _validated_id_set(
supported_situation_expiry_predicate_ids,
DEFAULT_SITUATION_EXPIRY_PREDICATE_IDS,
"situation expiry predicate",
errors
)
var packs_by_id := {} var packs_by_id := {}
var actions_by_id := {} var actions_by_id := {}
@@ -79,6 +125,9 @@ func rebuild(
var storages_by_id := {} var storages_by_id := {}
var enemies_by_id := {} var enemies_by_id := {}
var animals_by_id := {} var animals_by_id := {}
var situations_by_id := {}
var dialogue_intents_by_id := {}
var dialogue_template_catalogs_by_id := {}
var actions: Array[ActionDefinition] = [] var actions: Array[ActionDefinition] = []
var professions: Array[ProfessionDefinition] = [] var professions: Array[ProfessionDefinition] = []
var capability_tags: Array[CapabilityTagDefinition] = [] var capability_tags: Array[CapabilityTagDefinition] = []
@@ -87,6 +136,9 @@ func rebuild(
var storages: Array[StorageDefinition] = [] var storages: Array[StorageDefinition] = []
var enemies: Array[EnemyDefinition] = [] var enemies: Array[EnemyDefinition] = []
var animals: Array[AnimalDefinition] = [] var animals: Array[AnimalDefinition] = []
var situations: Array[SituationDefinition] = []
var dialogue_intents: Array[ConversationIntentDefinition] = []
var dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
for pack in pack_list: for pack in pack_list:
if pack == null: if pack == null:
@@ -106,6 +158,11 @@ func rebuild(
_collect_storages(pack, storages, storages_by_id, errors) _collect_storages(pack, storages, storages_by_id, errors)
_collect_enemies(pack, enemies, enemies_by_id, errors) _collect_enemies(pack, enemies, enemies_by_id, errors)
_collect_animals(pack, animals, animals_by_id, errors) _collect_animals(pack, animals, animals_by_id, errors)
_collect_situations(pack, situations, situations_by_id, errors)
_collect_dialogue_intents(pack, dialogue_intents, dialogue_intents_by_id, errors)
_collect_dialogue_template_catalogs(
pack, dialogue_template_catalogs, dialogue_template_catalogs_by_id, errors
)
for pack in pack_list: for pack in pack_list:
if pack == null: if pack == null:
@@ -149,6 +206,19 @@ func rebuild(
) )
_validate_enemy_references(enemies, items_by_id, errors) _validate_enemy_references(enemies, items_by_id, errors)
_validate_animal_references(animals, actions_by_id, supported_cues, errors) _validate_animal_references(animals, actions_by_id, supported_cues, errors)
_validate_situation_references(
situations,
actions_by_id,
items_by_id,
dialogue_intents_by_id,
supported_event_predicates,
supported_state_predicates,
supported_expiry_predicates,
errors
)
_validate_dialogue_references(
dialogue_intents, dialogue_intents_by_id, dialogue_template_catalogs_by_id, errors
)
errors.sort() errors.sort()
_errors = errors _errors = errors
if not errors.is_empty(): if not errors.is_empty():
@@ -162,6 +232,9 @@ func rebuild(
storages.sort_custom(_sort_storages) storages.sort_custom(_sort_storages)
enemies.sort_custom(_sort_enemies) enemies.sort_custom(_sort_enemies)
animals.sort_custom(_sort_animals) animals.sort_custom(_sort_animals)
situations.sort_custom(_sort_situations)
dialogue_intents.sort_custom(_sort_dialogue_intents)
dialogue_template_catalogs.sort_custom(_sort_dialogue_template_catalogs)
_packs = pack_list _packs = pack_list
_actions = actions _actions = actions
_professions = professions _professions = professions
@@ -171,6 +244,9 @@ func rebuild(
_storages = storages _storages = storages
_enemies = enemies _enemies = enemies
_animals = animals _animals = animals
_situations = situations
_dialogue_intents = dialogue_intents
_dialogue_template_catalogs = dialogue_template_catalogs
_packs_by_id = packs_by_id _packs_by_id = packs_by_id
_actions_by_id = actions_by_id _actions_by_id = actions_by_id
_professions_by_id = professions_by_id _professions_by_id = professions_by_id
@@ -180,6 +256,9 @@ func rebuild(
_storages_by_id = storages_by_id _storages_by_id = storages_by_id
_enemies_by_id = enemies_by_id _enemies_by_id = enemies_by_id
_animals_by_id = animals_by_id _animals_by_id = animals_by_id
_situations_by_id = situations_by_id
_dialogue_intents_by_id = dialogue_intents_by_id
_dialogue_template_catalogs_by_id = dialogue_template_catalogs_by_id
_valid = true _valid = true
return [] return []
@@ -228,6 +307,18 @@ func get_animals() -> Array[AnimalDefinition]:
return _animals.duplicate() return _animals.duplicate()
func get_situations() -> Array[SituationDefinition]:
return _situations.duplicate()
func get_dialogue_intents() -> Array[ConversationIntentDefinition]:
return _dialogue_intents.duplicate()
func get_dialogue_template_catalogs() -> Array[ConversationTemplateCatalog]:
return _dialogue_template_catalogs.duplicate()
func get_pack(pack_id: StringName) -> SimulationContentPack: func get_pack(pack_id: StringName) -> SimulationContentPack:
return _packs_by_id.get(pack_id) as SimulationContentPack return _packs_by_id.get(pack_id) as SimulationContentPack
@@ -264,6 +355,18 @@ func get_animal(animal_definition_id: StringName) -> AnimalDefinition:
return _animals_by_id.get(animal_definition_id) as AnimalDefinition return _animals_by_id.get(animal_definition_id) as AnimalDefinition
func get_situation(situation_definition_id: StringName) -> SituationDefinition:
return _situations_by_id.get(situation_definition_id) as SituationDefinition
func get_dialogue_intent(intent_id: StringName) -> ConversationIntentDefinition:
return _dialogue_intents_by_id.get(intent_id) as ConversationIntentDefinition
func get_dialogue_template_catalog(catalog_id: StringName) -> ConversationTemplateCatalog:
return _dialogue_template_catalogs_by_id.get(catalog_id) as ConversationTemplateCatalog
func get_authoring_report() -> Dictionary: func get_authoring_report() -> Dictionary:
return { return {
"valid": _valid, "valid": _valid,
@@ -277,6 +380,9 @@ func get_authoring_report() -> Dictionary:
"storages": _ids_for(_storages, &"storage_id"), "storages": _ids_for(_storages, &"storage_id"),
"enemies": _ids_for(_enemies, &"enemy_id"), "enemies": _ids_for(_enemies, &"enemy_id"),
"animals": _ids_for(_animals, &"animal_definition_id"), "animals": _ids_for(_animals, &"animal_definition_id"),
"situations": _ids_for(_situations, &"situation_definition_id"),
"dialogue_intents": _ids_for(_dialogue_intents, &"intent_id"),
"dialogue_template_catalogs": _ids_for(_dialogue_template_catalogs, &"catalog_id"),
} }
@@ -292,6 +398,9 @@ func _clear_published_content() -> void:
_storages.clear() _storages.clear()
_enemies.clear() _enemies.clear()
_animals.clear() _animals.clear()
_situations.clear()
_dialogue_intents.clear()
_dialogue_template_catalogs.clear()
_packs_by_id.clear() _packs_by_id.clear()
_actions_by_id.clear() _actions_by_id.clear()
_professions_by_id.clear() _professions_by_id.clear()
@@ -301,6 +410,9 @@ func _clear_published_content() -> void:
_storages_by_id.clear() _storages_by_id.clear()
_enemies_by_id.clear() _enemies_by_id.clear()
_animals_by_id.clear() _animals_by_id.clear()
_situations_by_id.clear()
_dialogue_intents_by_id.clear()
_dialogue_template_catalogs_by_id.clear()
static func _collect_actions( static func _collect_actions(
@@ -431,6 +543,56 @@ static func _collect_animals(
definitions.append(definition) definitions.append(definition)
static func _collect_situations(
pack: SimulationContentPack,
definitions: Array[SituationDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.situations:
if definition == null:
continue
if definitions_by_id.has(definition.situation_definition_id):
errors.append(
"Duplicate situation_definition_id '%s'" % definition.situation_definition_id
)
continue
definitions_by_id[definition.situation_definition_id] = definition
definitions.append(definition)
static func _collect_dialogue_intents(
pack: SimulationContentPack,
definitions: Array[ConversationIntentDefinition],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.dialogue_intents:
if definition == null:
continue
if definitions_by_id.has(definition.intent_id):
errors.append("Duplicate dialogue intent_id '%s'" % definition.intent_id)
continue
definitions_by_id[definition.intent_id] = definition
definitions.append(definition)
static func _collect_dialogue_template_catalogs(
pack: SimulationContentPack,
definitions: Array[ConversationTemplateCatalog],
definitions_by_id: Dictionary,
errors: Array[String]
) -> void:
for definition in pack.dialogue_template_catalogs:
if definition == null:
continue
if definitions_by_id.has(definition.catalog_id):
errors.append("Duplicate dialogue template catalog_id '%s'" % definition.catalog_id)
continue
definitions_by_id[definition.catalog_id] = definition
definitions.append(definition)
static func _validate_action_references( static func _validate_action_references(
actions: Array[ActionDefinition], actions: Array[ActionDefinition],
actions_by_id: Dictionary, actions_by_id: Dictionary,
@@ -730,6 +892,125 @@ static func _validate_animal_references(
) )
static func _validate_situation_references(
situations: Array[SituationDefinition],
actions_by_id: Dictionary,
items_by_id: Dictionary,
dialogue_intents_by_id: Dictionary,
supported_event_predicates: Dictionary,
supported_state_predicates: Dictionary,
supported_expiry_predicates: Dictionary,
errors: Array[String]
) -> void:
var event_predicate_ids := _sorted_string_name_keys(supported_event_predicates)
var state_predicate_ids := _sorted_string_name_keys(supported_state_predicates)
var expiry_predicate_ids := _sorted_string_name_keys(supported_expiry_predicates)
for definition in situations:
for error in definition.validate(
event_predicate_ids, state_predicate_ids, expiry_predicate_ids
):
errors.append("Situation '%s': %s" % [definition.situation_definition_id, error])
for intent_id in definition.dialogue_intent_ids:
if not intent_id.is_empty() and not dialogue_intents_by_id.has(intent_id):
errors.append(
(
"Situation '%s' references unknown dialogue intent '%s'"
% [definition.situation_definition_id, intent_id]
)
)
for alternative in definition.alternatives:
if alternative == null:
continue
var action_id := StringName(alternative.commitment_terms.get("action_id", ""))
if not action_id.is_empty() and not actions_by_id.has(action_id):
errors.append(
(
"Situation alternative '%s' references unknown action '%s'"
% [alternative.alternative_id, action_id]
)
)
var item_id := StringName(alternative.commitment_terms.get("item_id", ""))
if not item_id.is_empty() and not items_by_id.has(item_id):
errors.append(
(
"Situation alternative '%s' references unknown item '%s'"
% [alternative.alternative_id, item_id]
)
)
static func _validate_dialogue_references(
intents: Array[ConversationIntentDefinition],
intents_by_id: Dictionary,
template_catalogs_by_id: Dictionary,
errors: Array[String]
) -> void:
var intent_ids_by_template_catalog := {}
for definition in intents:
for response_intent_id in definition.response_intent_ids:
if not intents_by_id.has(response_intent_id):
errors.append(
(
"Dialogue intent '%s' references unknown response intent '%s'"
% [definition.intent_id, response_intent_id]
)
)
if not template_catalogs_by_id.has(definition.template_catalog_id):
errors.append(
(
"Dialogue intent '%s' references unknown template catalog '%s'"
% [definition.intent_id, definition.template_catalog_id]
)
)
continue
var catalog_intent_ids: Array[StringName] = []
for existing_intent_id in intent_ids_by_template_catalog.get(
definition.template_catalog_id, []
):
catalog_intent_ids.append(StringName(existing_intent_id))
catalog_intent_ids.append(definition.intent_id)
intent_ids_by_template_catalog[definition.template_catalog_id] = catalog_intent_ids
for raw_catalog_id in template_catalogs_by_id:
var catalog_id := StringName(raw_catalog_id)
var catalog := template_catalogs_by_id.get(catalog_id) as ConversationTemplateCatalog
var expected_intent_ids: Array[StringName] = []
for expected_intent_id in intent_ids_by_template_catalog.get(catalog_id, []):
expected_intent_ids.append(StringName(expected_intent_id))
expected_intent_ids.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
for error in catalog.validate(expected_intent_ids):
errors.append("Dialogue template catalog '%s': %s" % [catalog_id, error])
static func _validated_id_set(
provided_ids: Array[StringName],
default_ids: Array[StringName],
label: String,
errors: Array[String]
) -> Dictionary:
var result := {}
var ids := provided_ids if not provided_ids.is_empty() else default_ids
for id in ids:
if id.is_empty():
errors.append("Supported %s ID is empty" % label)
elif result.has(id):
errors.append("Duplicate supported %s '%s'" % [label, id])
else:
result[id] = true
return result
static func _sorted_string_name_keys(source: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for key in source:
result.append(StringName(key))
result.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
return result
static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool: static func _sort_packs(first: SimulationContentPack, second: SimulationContentPack) -> bool:
if first == null: if first == null:
return second != null return second != null
@@ -772,6 +1053,22 @@ static func _sort_animals(first: AnimalDefinition, second: AnimalDefinition) ->
return String(first.animal_definition_id) < String(second.animal_definition_id) return String(first.animal_definition_id) < String(second.animal_definition_id)
static func _sort_situations(first: SituationDefinition, second: SituationDefinition) -> bool:
return String(first.situation_definition_id) < String(second.situation_definition_id)
static func _sort_dialogue_intents(
first: ConversationIntentDefinition, second: ConversationIntentDefinition
) -> bool:
return String(first.intent_id) < String(second.intent_id)
static func _sort_dialogue_template_catalogs(
first: ConversationTemplateCatalog, second: ConversationTemplateCatalog
) -> bool:
return String(first.catalog_id) < String(second.catalog_id)
static func _ids_for(definitions: Array, id_property: StringName) -> Array[StringName]: static func _ids_for(definitions: Array, id_property: StringName) -> Array[StringName]:
var ids: Array[StringName] = [] var ids: Array[StringName] = []
for definition in definitions: for definition in definitions:
@@ -13,6 +13,9 @@ extends Resource
@export var storages: Array[StorageDefinition] = [] @export var storages: Array[StorageDefinition] = []
@export var enemies: Array[EnemyDefinition] = [] @export var enemies: Array[EnemyDefinition] = []
@export var animals: Array[AnimalDefinition] = [] @export var animals: Array[AnimalDefinition] = []
@export var situations: Array[SituationDefinition] = []
@export var dialogue_intents: Array[ConversationIntentDefinition] = []
@export var dialogue_template_catalogs: Array[ConversationTemplateCatalog] = []
func validate() -> Array[String]: func validate() -> Array[String]:
@@ -33,6 +36,9 @@ func validate() -> Array[String]:
_validate_storages(errors) _validate_storages(errors)
_validate_enemies(errors) _validate_enemies(errors)
_validate_animals(errors) _validate_animals(errors)
_validate_situations(errors)
_validate_dialogue_intents(errors)
_validate_dialogue_template_catalogs(errors)
return errors return errors
@@ -108,6 +114,32 @@ func _validate_animals(errors: Array[String]) -> void:
errors.append("AnimalDefinition: " + error) errors.append("AnimalDefinition: " + error)
func _validate_situations(errors: Array[String]) -> void:
for definition in situations:
if definition == null:
errors.append("situations contains a null definition")
continue
for error in definition.validate():
errors.append("SituationDefinition: " + error)
func _validate_dialogue_intents(errors: Array[String]) -> void:
for definition in dialogue_intents:
if definition == null:
errors.append("dialogue_intents contains a null definition")
continue
for error in definition.validate():
errors.append("ConversationIntentDefinition: " + error)
func _validate_dialogue_template_catalogs(errors: Array[String]) -> void:
for catalog in dialogue_template_catalogs:
if catalog == null:
errors.append("dialogue_template_catalogs contains a null catalog")
elif catalog.catalog_id.is_empty():
errors.append("ConversationTemplateCatalog: catalog_id is empty")
static func _validate_dependency_ids( static func _validate_dependency_ids(
ids: Array[StringName], label: String, errors: Array[String] ids: Array[StringName], label: String, errors: Array[String]
) -> void: ) -> void:
+22 -1
View File
@@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=42 format=3] [gd_resource type="Resource" script_class="SimulationContentPack" load_steps=60 format=3]
[ext_resource type="Script" path="res://simulation/definitions/SimulationContentPack.gd" id="1_pack"] [ext_resource type="Script" path="res://simulation/definitions/SimulationContentPack.gd" id="1_pack"]
[ext_resource type="Resource" path="res://simulation/definitions/actions/defend.tres" id="2_defend"] [ext_resource type="Resource" path="res://simulation/definitions/actions/defend.tres" id="2_defend"]
@@ -40,6 +40,24 @@
[ext_resource type="Resource" path="res://simulation/definitions/storages/woodpile.tres" id="38_woodpile"] [ext_resource type="Resource" path="res://simulation/definitions/storages/woodpile.tres" id="38_woodpile"]
[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_goat.tres" id="39_goat"] [ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_goat.tres" id="39_goat"]
[ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_sheep.tres" id="40_sheep"] [ext_resource type="Resource" path="res://simulation/animals/definitions/domestic_sheep.tres" id="40_sheep"]
[ext_resource type="Resource" path="res://simulation/situations/resources/pantry_shortage.tres" id="41_pantry_situation"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/accept.tres" id="42_accept"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/acknowledge_supersession.tres" id="43_acknowledge"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_wellbeing.tres" id="44_wellbeing"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_what_happened.tres" id="45_happened"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_who_else.tres" id="46_who"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_work.tres" id="47_work"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/decline.tres" id="48_decline"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/describe_need.tres" id="49_need"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/goodbye.tres" id="50_goodbye"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/greet.tres" id="51_greet"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/offer_help.tres" id="52_offer"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/renegotiate.tres" id="53_renegotiate"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/report_progress.tres" id="54_progress"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/reproach.tres" id="55_reproach"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/share_fact.tres" id="56_share"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/thank.tres" id="57_thank"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/jajce_conversation_templates.tres" id="58_templates"]
[resource] [resource]
script = ExtResource("1_pack") script = ExtResource("1_pack")
@@ -53,3 +71,6 @@ resources = [ExtResource("33_berry_patch"), ExtResource("34_herb_patch"), ExtRes
storages = [ExtResource("36_apothecary"), ExtResource("37_pantry"), ExtResource("38_woodpile")] storages = [ExtResource("36_apothecary"), ExtResource("37_pantry"), ExtResource("38_woodpile")]
enemies = [ExtResource("24_raider"), ExtResource("25_wolf"), ExtResource("26_boar")] enemies = [ExtResource("24_raider"), ExtResource("25_wolf"), ExtResource("26_boar")]
animals = [ExtResource("39_goat"), ExtResource("40_sheep")] animals = [ExtResource("39_goat"), ExtResource("40_sheep")]
situations = [ExtResource("41_pantry_situation")]
dialogue_intents = [ExtResource("42_accept"), ExtResource("43_acknowledge"), ExtResource("44_wellbeing"), ExtResource("45_happened"), ExtResource("46_who"), ExtResource("47_work"), ExtResource("48_decline"), ExtResource("49_need"), ExtResource("50_goodbye"), ExtResource("51_greet"), ExtResource("52_offer"), ExtResource("53_renegotiate"), ExtResource("54_progress"), ExtResource("55_reproach"), ExtResource("56_share"), ExtResource("57_thank")]
dialogue_template_catalogs = [ExtResource("58_templates")]
@@ -3,12 +3,15 @@ extends Resource
@export var intent_id: StringName @export var intent_id: StringName
@export var response_intent_ids: Array[StringName] = [] @export var response_intent_ids: Array[StringName] = []
@export var template_catalog_id: StringName
func validate() -> Array[String]: func validate() -> Array[String]:
var errors: Array[String] = [] var errors: Array[String] = []
if not ConversationSemantic.is_valid_id(intent_id): if not ConversationSemantic.is_valid_id(intent_id):
errors.append("intent_id is invalid") errors.append("intent_id is invalid")
if template_catalog_id.is_empty():
errors.append("template_catalog_id is empty for '%s'" % intent_id)
var seen: Dictionary = {} var seen: Dictionary = {}
for response_intent_id in response_intent_ids: for response_intent_id in response_intent_ids:
if not ConversationSemantic.is_valid_id(response_intent_id): if not ConversationSemantic.is_valid_id(response_intent_id):
@@ -20,3 +23,11 @@ func validate() -> Array[String]:
else: else:
seen[response_intent_id] = true seen[response_intent_id] = true
return errors return errors
func duplicate_definition() -> ConversationIntentDefinition:
var copy := ConversationIntentDefinition.new()
copy.intent_id = intent_id
copy.response_intent_ids = response_intent_ids.duplicate()
copy.template_catalog_id = template_catalog_id
return copy
@@ -1,12 +1,15 @@
class_name ConversationTemplateCatalog class_name ConversationTemplateCatalog
extends Resource extends Resource
@export var catalog_id: StringName
@export var line_templates: Dictionary = {} @export var line_templates: Dictionary = {}
@export var option_templates: Dictionary = {} @export var option_templates: Dictionary = {}
func validate(intent_ids: Array[StringName] = ConversationIntentIds.ALL) -> Array[String]: func validate(intent_ids: Array[StringName] = ConversationIntentIds.ALL) -> Array[String]:
var errors: Array[String] = [] var errors: Array[String] = []
if catalog_id.is_empty():
errors.append("catalog_id is empty")
_validate_templates(line_templates, "line", intent_ids, errors) _validate_templates(line_templates, "line", intent_ids, errors)
_validate_templates(option_templates, "option", intent_ids, errors) _validate_templates(option_templates, "option", intent_ids, errors)
errors.sort() errors.sort()
@@ -21,6 +24,14 @@ func get_option_templates(intent_id: StringName) -> Array[String]:
return _get_templates(option_templates, intent_id) return _get_templates(option_templates, intent_id)
func get_line_template_ids() -> Array[StringName]:
return _template_ids(line_templates)
func get_option_template_ids() -> Array[StringName]:
return _template_ids(option_templates)
func _get_templates(source: Dictionary, intent_id: StringName) -> Array[String]: func _get_templates(source: Dictionary, intent_id: StringName) -> Array[String]:
var result: Array[String] = [] var result: Array[String] = []
var values: Variant = source.get(String(intent_id), []) var values: Variant = source.get(String(intent_id), [])
@@ -34,6 +45,23 @@ func _get_templates(source: Dictionary, intent_id: StringName) -> Array[String]:
func _validate_templates( func _validate_templates(
source: Dictionary, kind: String, intent_ids: Array[StringName], errors: Array[String] source: Dictionary, kind: String, intent_ids: Array[StringName], errors: Array[String]
) -> void: ) -> void:
var allowed_ids := {}
for intent_id in intent_ids:
allowed_ids[intent_id] = true
for raw_intent_id in source:
if not raw_intent_id is String and not raw_intent_id is StringName:
errors.append("Non-string %s template intent ID" % kind)
continue
var source_intent_id := StringName(raw_intent_id)
if not allowed_ids.has(source_intent_id):
errors.append("Unknown %s template intent '%s'" % [kind, source_intent_id])
var raw_templates: Variant = source[raw_intent_id]
if not raw_templates is Array and not raw_templates is PackedStringArray:
errors.append("Invalid %s templates for '%s'" % [kind, source_intent_id])
continue
for raw_template in raw_templates:
if not raw_template is String:
errors.append("Non-string %s template for '%s'" % [kind, source_intent_id])
for intent_id in intent_ids: for intent_id in intent_ids:
var templates := _get_templates(source, intent_id) var templates := _get_templates(source, intent_id)
if templates.is_empty(): if templates.is_empty():
@@ -42,3 +70,14 @@ func _validate_templates(
for template in templates: for template in templates:
if template.strip_edges().is_empty(): if template.strip_edges().is_empty():
errors.append("Empty %s template for '%s'" % [kind, intent_id]) errors.append("Empty %s template for '%s'" % [kind, intent_id])
func _template_ids(source: Dictionary) -> Array[StringName]:
var result: Array[StringName] = []
for raw_id in source:
if raw_id is String or raw_id is StringName:
result.append(StringName(raw_id))
result.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
return result
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"accept"
response_intent_ids = Array[StringName]([&"thank", &"report_progress", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"acknowledge_supersession"
response_intent_ids = Array[StringName]([&"thank", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"ask_wellbeing"
response_intent_ids = Array[StringName]([&"describe_need", &"share_fact", &"offer_help", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"ask_what_happened"
response_intent_ids = Array[StringName]([&"share_fact", &"ask_who_else", &"offer_help", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"ask_who_else"
response_intent_ids = Array[StringName]([&"share_fact", &"offer_help", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"ask_work"
response_intent_ids = Array[StringName]([&"describe_need", &"report_progress", &"share_fact", &"ask_who_else", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"decline"
response_intent_ids = Array[StringName]([&"reproach", &"renegotiate", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"describe_need"
response_intent_ids = Array[StringName]([&"offer_help", &"decline", &"ask_who_else", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,8 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"goodbye"
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"greet"
response_intent_ids = Array[StringName]([&"ask_work", &"ask_wellbeing", &"ask_what_happened", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"offer_help"
response_intent_ids = Array[StringName]([&"accept", &"decline", &"renegotiate"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"renegotiate"
response_intent_ids = Array[StringName]([&"accept", &"decline", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"report_progress"
response_intent_ids = Array[StringName]([&"thank", &"renegotiate", &"acknowledge_supersession", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"reproach"
response_intent_ids = Array[StringName]([&"acknowledge_supersession", &"renegotiate", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"share_fact"
response_intent_ids = Array[StringName]([&"ask_what_happened", &"ask_who_else", &"acknowledge_supersession", &"thank", &"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ConversationIntentDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="1"]
[resource]
script = ExtResource("1")
intent_id = &"thank"
response_intent_ids = Array[StringName]([&"goodbye"])
template_catalog_id = &"jajce_conversation_templates"
@@ -1,87 +1,23 @@
[gd_resource type="Resource" script_class="ConversationIntentCatalog" load_steps=18 format=3] [gd_resource type="Resource" script_class="ConversationIntentCatalog" load_steps=18 format=3]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentCatalog.gd" id="1_catalog"] [ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentCatalog.gd" id="1_catalog"]
[ext_resource type="Script" path="res://simulation/dialogue/ConversationIntentDefinition.gd" id="2_intent"] [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/accept.tres" id="2_accept"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/acknowledge_supersession.tres" id="3_acknowledge"]
[sub_resource type="Resource" id="Intent_greet"] [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_wellbeing.tres" id="4_wellbeing"]
script = ExtResource("2_intent") [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_what_happened.tres" id="5_happened"]
intent_id = &"greet" [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_who_else.tres" id="6_who"]
response_intent_ids = Array[StringName]([&"ask_work", &"ask_wellbeing", &"ask_what_happened", &"goodbye"]) [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/ask_work.tres" id="7_work"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/decline.tres" id="8_decline"]
[sub_resource type="Resource" id="Intent_ask_work"] [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/describe_need.tres" id="9_need"]
script = ExtResource("2_intent") [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/goodbye.tres" id="10_goodbye"]
intent_id = &"ask_work" [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/greet.tres" id="11_greet"]
response_intent_ids = Array[StringName]([&"describe_need", &"report_progress", &"share_fact", &"ask_who_else", &"goodbye"]) [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/offer_help.tres" id="12_offer"]
[ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/renegotiate.tres" id="13_renegotiate"]
[sub_resource type="Resource" id="Intent_ask_wellbeing"] [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/report_progress.tres" id="14_progress"]
script = ExtResource("2_intent") [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/reproach.tres" id="15_reproach"]
intent_id = &"ask_wellbeing" [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/share_fact.tres" id="16_share"]
response_intent_ids = Array[StringName]([&"describe_need", &"share_fact", &"offer_help", &"goodbye"]) [ext_resource type="Resource" path="res://simulation/dialogue/resources/intents/thank.tres" id="17_thank"]
[sub_resource type="Resource" id="Intent_ask_what_happened"]
script = ExtResource("2_intent")
intent_id = &"ask_what_happened"
response_intent_ids = Array[StringName]([&"share_fact", &"ask_who_else", &"offer_help", &"goodbye"])
[sub_resource type="Resource" id="Intent_share_fact"]
script = ExtResource("2_intent")
intent_id = &"share_fact"
response_intent_ids = Array[StringName]([&"ask_what_happened", &"ask_who_else", &"acknowledge_supersession", &"thank", &"goodbye"])
[sub_resource type="Resource" id="Intent_describe_need"]
script = ExtResource("2_intent")
intent_id = &"describe_need"
response_intent_ids = Array[StringName]([&"offer_help", &"decline", &"ask_who_else", &"goodbye"])
[sub_resource type="Resource" id="Intent_ask_who_else"]
script = ExtResource("2_intent")
intent_id = &"ask_who_else"
response_intent_ids = Array[StringName]([&"share_fact", &"offer_help", &"goodbye"])
[sub_resource type="Resource" id="Intent_offer_help"]
script = ExtResource("2_intent")
intent_id = &"offer_help"
response_intent_ids = Array[StringName]([&"accept", &"decline", &"renegotiate"])
[sub_resource type="Resource" id="Intent_accept"]
script = ExtResource("2_intent")
intent_id = &"accept"
response_intent_ids = Array[StringName]([&"thank", &"report_progress", &"goodbye"])
[sub_resource type="Resource" id="Intent_decline"]
script = ExtResource("2_intent")
intent_id = &"decline"
response_intent_ids = Array[StringName]([&"reproach", &"renegotiate", &"goodbye"])
[sub_resource type="Resource" id="Intent_report_progress"]
script = ExtResource("2_intent")
intent_id = &"report_progress"
response_intent_ids = Array[StringName]([&"thank", &"renegotiate", &"acknowledge_supersession", &"goodbye"])
[sub_resource type="Resource" id="Intent_renegotiate"]
script = ExtResource("2_intent")
intent_id = &"renegotiate"
response_intent_ids = Array[StringName]([&"accept", &"decline", &"goodbye"])
[sub_resource type="Resource" id="Intent_thank"]
script = ExtResource("2_intent")
intent_id = &"thank"
response_intent_ids = Array[StringName]([&"goodbye"])
[sub_resource type="Resource" id="Intent_reproach"]
script = ExtResource("2_intent")
intent_id = &"reproach"
response_intent_ids = Array[StringName]([&"acknowledge_supersession", &"renegotiate", &"goodbye"])
[sub_resource type="Resource" id="Intent_acknowledge_supersession"]
script = ExtResource("2_intent")
intent_id = &"acknowledge_supersession"
response_intent_ids = Array[StringName]([&"thank", &"goodbye"])
[sub_resource type="Resource" id="Intent_goodbye"]
script = ExtResource("2_intent")
intent_id = &"goodbye"
[resource] [resource]
script = ExtResource("1_catalog") script = ExtResource("1_catalog")
intents = Array[ExtResource("2_intent")]([SubResource("Intent_greet"), SubResource("Intent_ask_work"), SubResource("Intent_ask_wellbeing"), SubResource("Intent_ask_what_happened"), SubResource("Intent_share_fact"), SubResource("Intent_describe_need"), SubResource("Intent_ask_who_else"), SubResource("Intent_offer_help"), SubResource("Intent_accept"), SubResource("Intent_decline"), SubResource("Intent_report_progress"), SubResource("Intent_renegotiate"), SubResource("Intent_thank"), SubResource("Intent_reproach"), SubResource("Intent_acknowledge_supersession"), SubResource("Intent_goodbye")]) intents = [ExtResource("11_greet"), ExtResource("7_work"), ExtResource("4_wellbeing"), ExtResource("5_happened"), ExtResource("16_share"), ExtResource("9_need"), ExtResource("6_who"), ExtResource("12_offer"), ExtResource("2_accept"), ExtResource("8_decline"), ExtResource("14_progress"), ExtResource("13_renegotiate"), ExtResource("17_thank"), ExtResource("15_reproach"), ExtResource("3_acknowledge"), ExtResource("10_goodbye")]
@@ -4,6 +4,7 @@
[resource] [resource]
script = ExtResource("1_catalog") script = ExtResource("1_catalog")
catalog_id = &"jajce_conversation_templates"
line_templates = { line_templates = {
"accept": ["That works for me.", "Yes, let us do that."], "accept": ["That works for me.", "Yes, let us do that."],
"acknowledge_supersession": ["Things have changed since then; I understand.", "That news has overtaken our earlier plan."], "acknowledge_supersession": ["Things have changed since then; I understand.", "That news has overtaken our earlier plan."],
@@ -17,12 +17,27 @@ func validate() -> Array[String]:
errors.append("display_name is empty for alternative '%s'" % alternative_id) errors.append("display_name is empty for alternative '%s'" % alternative_id)
if resolution_predicate_ids.is_empty(): if resolution_predicate_ids.is_empty():
errors.append("alternative '%s' has no resolution predicates" % alternative_id) errors.append("alternative '%s' has no resolution predicates" % alternative_id)
var seen_predicate_ids := {}
for predicate_id in resolution_predicate_ids: for predicate_id in resolution_predicate_ids:
if predicate_id.is_empty(): if predicate_id.is_empty():
errors.append("alternative '%s' has an empty resolution predicate ID" % alternative_id) errors.append("alternative '%s' has an empty resolution predicate ID" % alternative_id)
elif seen_predicate_ids.has(predicate_id):
errors.append(
(
"alternative '%s' has duplicate resolution predicate '%s'"
% [alternative_id, predicate_id]
)
)
seen_predicate_ids[predicate_id] = true
var seen_topic_ids := {}
for topic_id in dialogue_topic_ids: for topic_id in dialogue_topic_ids:
if topic_id.is_empty(): if topic_id.is_empty():
errors.append("alternative '%s' has an empty dialogue topic ID" % alternative_id) errors.append("alternative '%s' has an empty dialogue topic ID" % alternative_id)
elif seen_topic_ids.has(topic_id):
errors.append(
"alternative '%s' has duplicate dialogue topic '%s'" % [alternative_id, topic_id]
)
seen_topic_ids[topic_id] = true
for term_key in commitment_terms: for term_key in commitment_terms:
if not term_key is String and not term_key is StringName: if not term_key is String and not term_key is StringName:
errors.append("alternative '%s' has a non-string commitment term key" % alternative_id) errors.append("alternative '%s' has a non-string commitment term key" % alternative_id)
+43 -1
View File
@@ -13,6 +13,7 @@ extends Resource
@export_range(0, 1000000, 1) var expiry_ticks := 0 @export_range(0, 1000000, 1) var expiry_ticks := 0
@export_range(0, 1000000, 1) var cooldown_ticks := 0 @export_range(0, 1000000, 1) var cooldown_ticks := 0
@export var dialogue_topic_ids: Array[StringName] = [] @export var dialogue_topic_ids: Array[StringName] = []
@export var dialogue_intent_ids: Array[StringName] = []
@export var alternatives: Array[SituationAlternativeDefinition] = [] @export var alternatives: Array[SituationAlternativeDefinition] = []
@@ -63,11 +64,24 @@ func validate(
) )
_validate_unique_names(dedupe_key_fields, "dedupe key", errors) _validate_unique_names(dedupe_key_fields, "dedupe key", errors)
_validate_unique_names(dialogue_topic_ids, "dialogue topic", errors) _validate_unique_names(dialogue_topic_ids, "dialogue topic", errors)
_validate_unique_names(dialogue_intent_ids, "dialogue intent", errors)
var predicate_ids := {}
for predicate in trigger_predicates + resolution_predicates:
if predicate == null:
continue
if predicate_ids.has(predicate.predicate_id):
errors.append(
(
"'%s' has duplicate predicate '%s' across trigger/resolution predicates"
% [situation_definition_id, predicate.predicate_id]
)
)
predicate_ids[predicate.predicate_id] = true
var alternatives_by_id: Dictionary = {} var alternatives_by_id: Dictionary = {}
for alternative in alternatives: for alternative in alternatives:
if alternative == null: if alternative == null:
errors.append("'%s' contains a null alternative" % situation_definition_id) errors.append("'%s' contains a null alternative" % situation_definition_id)
continue continue
for error in alternative.validate(): for error in alternative.validate():
errors.append("Alternative '%s': %s" % [alternative.alternative_id, error]) errors.append("Alternative '%s': %s" % [alternative.alternative_id, error])
if alternatives_by_id.has(alternative.alternative_id): if alternatives_by_id.has(alternative.alternative_id):
@@ -89,6 +103,34 @@ func validate(
return errors return errors
func duplicate_definition() -> SituationDefinition:
var copy := SituationDefinition.new()
copy.situation_definition_id = situation_definition_id
copy.display_name = display_name
copy.description = description
copy.severity = severity
copy.priority = priority
for predicate in trigger_predicates:
copy.trigger_predicates.append(
predicate.duplicate_predicate() if predicate != null else null
)
for predicate in resolution_predicates:
copy.resolution_predicates.append(
predicate.duplicate_predicate() if predicate != null else null
)
copy.expiry_predicate_ids = expiry_predicate_ids.duplicate()
copy.dedupe_key_fields = dedupe_key_fields.duplicate()
copy.expiry_ticks = expiry_ticks
copy.cooldown_ticks = cooldown_ticks
copy.dialogue_topic_ids = dialogue_topic_ids.duplicate()
copy.dialogue_intent_ids = dialogue_intent_ids.duplicate()
for alternative in alternatives:
copy.alternatives.append(
alternative.duplicate_alternative() if alternative != null else null
)
return copy
func get_predicate(predicate_id: StringName) -> SituationPredicate: func get_predicate(predicate_id: StringName) -> SituationPredicate:
for predicate in trigger_predicates: for predicate in trigger_predicates:
if predicate != null and predicate.predicate_id == predicate_id: if predicate != null and predicate.predicate_id == predicate_id:
+8 -57
View File
@@ -2,6 +2,7 @@ class_name SituationSystem
extends RefCounted extends RefCounted
const MAX_CONCURRENT_SITUATIONS := 3 const MAX_CONCURRENT_SITUATIONS := 3
const PANTRY_SHORTAGE_DEFINITION_PATH := "res://simulation/situations/resources/pantry_shortage.tres"
const DEFINITION_PANTRY_SHORTAGE := &"situation_pantry_shortage" const DEFINITION_PANTRY_SHORTAGE := &"situation_pantry_shortage"
const PREDICATE_EVENT_TYPE := &"event_type" const PREDICATE_EVENT_TYPE := &"event_type"
const PREDICATE_EVENT_PAYLOAD_EQUALS := &"event_payload_equals" const PREDICATE_EVENT_PAYLOAD_EQUALS := &"event_payload_equals"
@@ -26,68 +27,18 @@ var _next_situation_id := 0
static func create_default() -> SituationSystem: static func create_default() -> SituationSystem:
var system := SituationSystem.new() var system := SituationSystem.new()
var definitions: Array[SituationDefinition] = [create_pantry_shortage_definition()] var definitions: Array[SituationDefinition] = []
var catalog := ContentCatalog.create_core()
if catalog.is_valid():
for definition in catalog.get_situations():
definitions.append(definition.duplicate_definition())
system.configure(definitions) system.configure(definitions)
return system return system
static func create_pantry_shortage_definition() -> SituationDefinition: static func create_pantry_shortage_definition() -> SituationDefinition:
var definition := SituationDefinition.new() var authored := load(PANTRY_SHORTAGE_DEFINITION_PATH) as SituationDefinition
definition.situation_definition_id = DEFINITION_PANTRY_SHORTAGE return authored.duplicate_definition() if authored != null else null
definition.display_name = "The Pantry Is Empty"
definition.description = "The village pantry has run out of food."
definition.severity = 70
definition.priority = 80
definition.trigger_predicates = [
_event_predicate(
&"pantry_withdrawal", PREDICATE_EVENT_TYPE, {"event_type": "storage_withdrawn"}
),
_event_predicate(
&"withdrawal_from_pantry",
PREDICATE_EVENT_PARTICIPANT_ID,
{"role": "source", "entity_id": "village_pantry"}
),
_event_predicate(
&"withdrawal_of_food",
PREDICATE_EVENT_PAYLOAD_EQUALS,
{"key": "item_id", "value": "food"}
),
_state_predicate(
&"pantry_empty", PREDICATE_STATE_NUMBER_LTE, {"fact_key": "pantry.food", "value": 0.0}
),
]
definition.resolution_predicates = [
_event_predicate(
&"pantry_restocked",
PREDICATE_EVENT_PANTRY_SUPPLIED,
{"target_id": "village_pantry", "item_id": "food", "amount": 1.0}
)
]
definition.expiry_predicate_ids = [EXPIRY_AGE_REACHED]
definition.dedupe_key_fields = [&"world_id", &"source_id"]
definition.expiry_ticks = 200
definition.cooldown_ticks = 50
definition.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE]
var restock := SituationAlternativeDefinition.new()
restock.alternative_id = ALTERNATIVE_RESTOCK
restock.display_name = "Restock the pantry"
restock.description = "Bring at least one food item to the village pantry."
restock.resolution_predicate_ids = [&"pantry_restocked"]
restock.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE]
restock.commitment_terms = {
"action_id": "deposit_food",
"target_id": "village_pantry",
"item_id": "food",
"amount": 1.0,
}
var endure := SituationAlternativeDefinition.new()
endure.alternative_id = ALTERNATIVE_ENDURE
endure.display_name = "Endure the shortage"
endure.description = "Make no promise and let the immediate concern pass."
endure.resolution_predicate_ids = [&"pantry_restocked"]
endure.dialogue_topic_ids = [DIALOGUE_TOPIC_PANTRY_SHORTAGE]
definition.alternatives = [restock, endure]
return definition
func configure(definitions: Array[SituationDefinition]) -> Array[String]: func configure(definitions: Array[SituationDefinition]) -> Array[String]:
@@ -0,0 +1,70 @@
[gd_resource type="Resource" script_class="SituationDefinition" load_steps=10 format=3]
[ext_resource type="Script" path="res://simulation/situations/SituationDefinition.gd" id="1_definition"]
[ext_resource type="Script" path="res://simulation/situations/SituationPredicate.gd" id="2_predicate"]
[ext_resource type="Script" path="res://simulation/situations/SituationAlternativeDefinition.gd" id="3_alternative"]
[sub_resource type="Resource" id="Predicate_pantry_withdrawal"]
script = ExtResource("2_predicate")
predicate_id = &"pantry_withdrawal"
evaluator_id = &"event_type"
parameters = {"event_type": "storage_withdrawn"}
[sub_resource type="Resource" id="Predicate_withdrawal_from_pantry"]
script = ExtResource("2_predicate")
predicate_id = &"withdrawal_from_pantry"
evaluator_id = &"event_participant_id"
parameters = {"entity_id": "village_pantry", "role": "source"}
[sub_resource type="Resource" id="Predicate_withdrawal_of_food"]
script = ExtResource("2_predicate")
predicate_id = &"withdrawal_of_food"
evaluator_id = &"event_payload_equals"
parameters = {"key": "item_id", "value": "food"}
[sub_resource type="Resource" id="Predicate_pantry_empty"]
script = ExtResource("2_predicate")
predicate_id = &"pantry_empty"
evaluator_id = &"state_number_lte"
source = "state"
parameters = {"fact_key": "pantry.food", "value": 0.0}
[sub_resource type="Resource" id="Predicate_pantry_restocked"]
script = ExtResource("2_predicate")
predicate_id = &"pantry_restocked"
evaluator_id = &"event_pantry_supplied"
parameters = {"amount": 1.0, "item_id": "food", "target_id": "village_pantry"}
[sub_resource type="Resource" id="Alternative_restock"]
script = ExtResource("3_alternative")
alternative_id = &"restock_pantry"
display_name = "Restock the pantry"
description = "Bring at least one food item to the village pantry."
resolution_predicate_ids = Array[StringName]([&"pantry_restocked"])
dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"])
commitment_terms = {"action_id": "deposit_food", "amount": 1.0, "item_id": "food", "target_id": "village_pantry"}
[sub_resource type="Resource" id="Alternative_endure"]
script = ExtResource("3_alternative")
alternative_id = &"endure_shortage"
display_name = "Endure the shortage"
description = "Make no promise and let the immediate concern pass."
resolution_predicate_ids = Array[StringName]([&"pantry_restocked"])
dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"])
[resource]
script = ExtResource("1_definition")
situation_definition_id = &"situation_pantry_shortage"
display_name = "The Pantry Is Empty"
description = "The village pantry has run out of food."
severity = 70
priority = 80
trigger_predicates = [SubResource("Predicate_pantry_withdrawal"), SubResource("Predicate_withdrawal_from_pantry"), SubResource("Predicate_withdrawal_of_food"), SubResource("Predicate_pantry_empty")]
resolution_predicates = [SubResource("Predicate_pantry_restocked")]
expiry_predicate_ids = Array[StringName]([&"age_reached"])
dedupe_key_fields = Array[StringName]([&"world_id", &"source_id"])
expiry_ticks = 200
cooldown_ticks = 50
dialogue_topic_ids = Array[StringName]([&"topic_pantry_shortage"])
dialogue_intent_ids = Array[StringName]([&"accept", &"decline", &"describe_need", &"offer_help", &"report_progress"])
alternatives = [SubResource("Alternative_restock"), SubResource("Alternative_endure")]
@@ -0,0 +1,154 @@
extends GutTest
func test_core_pack_owns_sorted_situation_intent_and_template_content() -> void:
var catalog := ContentCatalog.create_core()
assert_true(catalog.is_valid(), "%s" % [catalog.get_errors()])
assert_eq(_situation_ids(catalog.get_situations()), [&"situation_pantry_shortage"])
assert_eq(_intent_ids(catalog.get_dialogue_intents()), _sorted_ids(ConversationIntentIds.ALL))
assert_eq(
_template_catalog_ids(catalog.get_dialogue_template_catalogs()),
[&"jajce_conversation_templates"]
)
var authored := catalog.get_situation(SituationSystem.DEFINITION_PANTRY_SHORTAGE)
var compatibility_copy := SituationSystem.create_pantry_shortage_definition()
assert_not_null(authored)
assert_not_same(authored, compatibility_copy)
assert_eq(authored.display_name, compatibility_copy.display_name)
assert_eq(authored.priority, compatibility_copy.priority)
assert_eq(authored.dialogue_intent_ids, compatibility_copy.dialogue_intent_ids)
compatibility_copy.priority = -100
assert_eq(authored.priority, 80, "Compatibility callers cannot mutate authored content")
var legacy_catalog := (
load("res://simulation/dialogue/resources/jajce_conversation_intents.tres")
as ConversationIntentCatalog
)
assert_not_null(legacy_catalog)
assert_true(legacy_catalog.rebuild().is_empty(), "%s" % [legacy_catalog.get_errors()])
assert_eq(legacy_catalog.get_intent_ids(), _intent_ids(catalog.get_dialogue_intents()))
var templates := catalog.get_dialogue_template_catalog(&"jajce_conversation_templates")
assert_not_null(templates)
assert_true(templates.validate(legacy_catalog.get_intent_ids()).is_empty())
func test_invalid_cross_references_fail_without_partial_publication() -> void:
var core := load(ContentCatalog.CORE_PACK_PATH) as SimulationContentPack
var addon := SimulationContentPack.new()
addon.pack_id = &"broken_authored_content"
addon.display_name = "Broken authored content"
addon.required_pack_ids = [&"core"]
var broken_situation := _broken_situation()
assert_eq(broken_situation.alternatives.size(), 2)
assert_eq(
(
broken_situation
. validate()
. filter(func(error: String) -> bool: return "alternative" in error.to_lower())
. size()
),
3
)
addon.situations = [core.situations[0], broken_situation]
var broken_intent := ConversationIntentDefinition.new()
broken_intent.intent_id = &"broken_intent"
broken_intent.response_intent_ids = [&"missing_response"]
broken_intent.template_catalog_id = &"missing_template_catalog"
addon.dialogue_intents = [core.dialogue_intents[0], broken_intent]
var rogue_templates := ConversationTemplateCatalog.new()
rogue_templates.catalog_id = &"rogue_templates"
rogue_templates.line_templates = {"ghost_intent": ["Ghost line"]}
rogue_templates.option_templates = {"ghost_intent": ["Ghost option"]}
addon.dialogue_template_catalogs = [core.dialogue_template_catalogs[0], rogue_templates]
var catalog := ContentCatalog.new()
var packs: Array[SimulationContentPack] = [addon, core]
var errors := catalog.rebuild(packs)
for expected in [
"Duplicate situation_definition_id 'situation_pantry_shortage'",
"duplicate predicate 'shared_predicate' across trigger/resolution predicates",
"unsupported event predicate 'unknown_predicate_handler'",
"duplicate alternative 'duplicate_alternative'",
"references unknown resolution predicate 'missing_predicate'",
"references unknown dialogue intent 'missing_intent'",
"Duplicate dialogue intent_id 'accept'",
"references unknown response intent 'missing_response'",
"references unknown template catalog 'missing_template_catalog'",
"Duplicate dialogue template catalog_id 'jajce_conversation_templates'",
"Unknown line template intent 'ghost_intent'",
"Unknown option template intent 'ghost_intent'",
]:
assert_true(_has_error(errors, expected), "%s missing from %s" % [expected, errors])
assert_false(catalog.is_valid())
assert_true(catalog.get_situations().is_empty())
assert_true(catalog.get_dialogue_intents().is_empty())
assert_true(catalog.get_dialogue_template_catalogs().is_empty())
assert_null(catalog.get_situation(&"broken_situation"))
assert_null(catalog.get_dialogue_intent(&"broken_intent"))
func _broken_situation() -> SituationDefinition:
var trigger := SituationPredicate.new()
trigger.predicate_id = &"shared_predicate"
trigger.evaluator_id = &"unknown_predicate_handler"
trigger.source = String(SituationPredicate.SOURCE_EVENT)
var resolution := SituationPredicate.new()
resolution.predicate_id = &"shared_predicate"
resolution.evaluator_id = &"event_type"
resolution.source = String(SituationPredicate.SOURCE_EVENT)
var first_alternative := SituationAlternativeDefinition.new()
first_alternative.alternative_id = &"duplicate_alternative"
first_alternative.display_name = "First alternative"
first_alternative.resolution_predicate_ids = [&"missing_predicate"]
var second_alternative := first_alternative.duplicate_alternative()
var definition := SituationDefinition.new()
definition.situation_definition_id = &"broken_situation"
definition.display_name = "Broken situation"
definition.trigger_predicates = [trigger]
definition.resolution_predicates = [resolution]
definition.dialogue_intent_ids = [&"missing_intent"]
definition.alternatives = [first_alternative, second_alternative]
return definition
func _situation_ids(definitions: Array[SituationDefinition]) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.situation_definition_id)
return ids
func _intent_ids(definitions: Array[ConversationIntentDefinition]) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.intent_id)
return ids
func _template_catalog_ids(
definitions: Array[ConversationTemplateCatalog],
) -> Array[StringName]:
var ids: Array[StringName] = []
for definition in definitions:
ids.append(definition.catalog_id)
return ids
func _sorted_ids(source: Array[StringName]) -> Array[StringName]:
var ids := source.duplicate()
ids.sort_custom(
func(left: StringName, right: StringName) -> bool: return String(left) < String(right)
)
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://bupkjb7j38smr