class_name SituationDefinition extends Resource @export var situation_definition_id: StringName @export var display_name: String @export_multiline var description: String @export_range(0, 100, 1) var severity := 1 @export_range(-100, 100, 1) var priority := 0 @export var trigger_predicates: Array[SituationPredicate] = [] @export var resolution_predicates: Array[SituationPredicate] = [] @export var expiry_predicate_ids: Array[StringName] = [] @export var dedupe_key_fields: Array[StringName] = [] @export_range(0, 1000000, 1) var expiry_ticks := 0 @export_range(0, 1000000, 1) var cooldown_ticks := 0 @export var dialogue_topic_ids: Array[StringName] = [] @export var dialogue_intent_ids: Array[StringName] = [] @export var alternatives: Array[SituationAlternativeDefinition] = [] func validate( supported_event_predicate_ids: Array[StringName] = [], supported_state_predicate_ids: Array[StringName] = [], supported_expiry_predicate_ids: Array[StringName] = [] ) -> Array[String]: var errors: Array[String] = [] if situation_definition_id.is_empty(): errors.append("situation_definition_id is empty") if display_name.is_empty(): errors.append("display_name is empty for '%s'" % situation_definition_id) if trigger_predicates.is_empty(): errors.append("'%s' has no trigger predicates" % situation_definition_id) if severity < 0 or severity > 100: errors.append("severity is outside 0..100 for '%s'" % situation_definition_id) if priority < -100 or priority > 100: errors.append("priority is outside -100..100 for '%s'" % situation_definition_id) if expiry_ticks < 0 or cooldown_ticks < 0: errors.append("expiry/cooldown ticks cannot be negative for '%s'" % situation_definition_id) _validate_predicates( trigger_predicates, "trigger", supported_event_predicate_ids, supported_state_predicate_ids, errors ) _validate_predicates( resolution_predicates, "resolution", supported_event_predicate_ids, supported_state_predicate_ids, errors ) for predicate_id in expiry_predicate_ids: if predicate_id.is_empty(): errors.append("'%s' has an empty expiry predicate ID" % situation_definition_id) elif ( not supported_expiry_predicate_ids.is_empty() and predicate_id not in supported_expiry_predicate_ids ): errors.append( ( "'%s' references unsupported expiry predicate '%s'" % [situation_definition_id, predicate_id] ) ) _validate_unique_names(dedupe_key_fields, "dedupe key", 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 = {} for alternative in alternatives: if alternative == null: errors.append("'%s' contains a null alternative" % situation_definition_id) continue for error in alternative.validate(): errors.append("Alternative '%s': %s" % [alternative.alternative_id, error]) if alternatives_by_id.has(alternative.alternative_id): errors.append( ( "'%s' has duplicate alternative '%s'" % [situation_definition_id, alternative.alternative_id] ) ) alternatives_by_id[alternative.alternative_id] = true for predicate_id in alternative.resolution_predicate_ids: if not _has_predicate(predicate_id): errors.append( ( "Alternative '%s' references unknown resolution predicate '%s'" % [alternative.alternative_id, predicate_id] ) ) 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: for predicate in trigger_predicates: if predicate != null and predicate.predicate_id == predicate_id: return predicate for predicate in resolution_predicates: if predicate != null and predicate.predicate_id == predicate_id: return predicate return null func get_alternative(alternative_id: StringName) -> SituationAlternativeDefinition: for alternative in alternatives: if alternative != null and alternative.alternative_id == alternative_id: return alternative return null func _has_predicate(predicate_id: StringName) -> bool: return get_predicate(predicate_id) != null func _validate_predicates( predicates: Array[SituationPredicate], label: String, supported_event_ids: Array[StringName], supported_state_ids: Array[StringName], errors: Array[String] ) -> void: var seen: Dictionary = {} for predicate in predicates: if predicate == null: errors.append("'%s' contains a null %s predicate" % [situation_definition_id, label]) continue for error in predicate.validate(): errors.append( "%s predicate '%s': %s" % [label.capitalize(), predicate.predicate_id, error] ) if seen.has(predicate.predicate_id): errors.append( ( "'%s' has duplicate %s predicate '%s'" % [situation_definition_id, label, predicate.predicate_id] ) ) seen[predicate.predicate_id] = true var supported_ids := ( supported_event_ids if StringName(predicate.source) == SituationPredicate.SOURCE_EVENT else supported_state_ids ) if not supported_ids.is_empty() and predicate.evaluator_id not in supported_ids: errors.append( ( "'%s' references unsupported %s predicate '%s'" % [situation_definition_id, predicate.source, predicate.evaluator_id] ) ) func _validate_unique_names(names: Array[StringName], label: String, errors: Array[String]) -> void: var seen: Dictionary = {} for name in names: if name.is_empty(): errors.append("'%s' has an empty %s" % [situation_definition_id, label]) elif seen.has(name): errors.append("'%s' has duplicate %s '%s'" % [situation_definition_id, label, name]) seen[name] = true