Files
gamedev-the-steward/world/dialogue/DialogueManagerConversationPresenter.gd
T
2026-08-12 20:32:53 +02:00

204 lines
6.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name DialogueManagerConversationPresenter
extends ConversationPresenter
const DEFAULT_TEMPLATE_CATALOG_PATH := "res://simulation/dialogue/resources/jajce_conversation_templates.tres"
var last_error := ""
var _dialogue_manager: Object
var _templates: ConversationTemplateCatalog
func _init(
dialogue_manager: Object = null, template_catalog: ConversationTemplateCatalog = null
) -> void:
_dialogue_manager = dialogue_manager
_templates = template_catalog
if _templates == null:
_templates = load(DEFAULT_TEMPLATE_CATALOG_PATH) as ConversationTemplateCatalog
func present_turn(turn: ConversationTurn, presentation_context: Dictionary = {}) -> Variant:
last_error = ""
if turn == null or not turn.is_valid():
return _fail("Conversation turn is invalid")
if not _presentation_context_is_valid(presentation_context):
return _fail("Presentation context is invalid")
var manager := _resolve_dialogue_manager()
if manager == null or not manager.has_method("create_resource_from_text"):
return _fail("Dialogue Manager v3 runtime is unavailable")
if _templates == null or not _templates.validate().is_empty():
return _fail("Conversation template catalog is invalid")
var source := build_source(turn, presentation_context)
if source.is_empty():
return null
var resource: Variant = manager.call("create_resource_from_text", source)
if resource is Resource:
resource.set_meta("conversation_id", String(turn.get_conversation_id()))
resource.set_meta("conversation_revision", turn.get_revision())
resource.set_meta("semantic_act_id", String(turn.get_act().get_act_id()))
resource.set_meta("semantic_intent_id", String(turn.get_act().get_intent_id()))
return resource
func build_source(turn: ConversationTurn, presentation_context: Dictionary = {}) -> String:
last_error = ""
if turn == null or not turn.is_valid():
return _fail_string("Conversation turn is invalid")
if not _presentation_context_is_valid(presentation_context):
return _fail_string("Presentation context is invalid")
if _templates == null or not _templates.validate().is_empty():
return _fail_string("Conversation template catalog is invalid")
var act := turn.get_act()
var speaker_label := _entity_name(act.get_speaker(), presentation_context)
var listener_label := _entity_name(act.get_listener(), presentation_context)
var topic_label := _topic_name(act.get_causal_topic_ids(), presentation_context)
var speaker_name := sanitize_dialogue_text(speaker_label)
var line_text := _render_template(
_select_template(
_templates.get_line_templates(act.get_intent_id()),
_stable_key(turn, act.get_act_id(), act.get_intent_id(), &"line")
),
speaker_label,
listener_label,
topic_label
)
var line_tags := PackedStringArray(
[
"conversation_id=%s" % turn.get_conversation_id(),
"revision=%d" % turn.get_revision(),
"act_id=%s" % act.get_act_id(),
"intent_id=%s" % act.get_intent_id(),
]
)
for topic_id in act.get_causal_topic_ids():
line_tags.append("topic_id=%s" % topic_id)
var lines := PackedStringArray(
[
"~ start",
"%s: %s [#%s]" % [speaker_name, line_text, ",".join(line_tags)],
]
)
for option in turn.get_options():
if not option.is_enabled():
continue
var option_topic := _topic_name(option.get_causal_topic_ids(), presentation_context)
var option_text := _render_template(
_select_template(
_templates.get_option_templates(option.get_intent_id()),
_stable_key(turn, option.get_option_id(), option.get_intent_id(), &"option")
),
_entity_name(option.get_speaker(), presentation_context),
_entity_name(option.get_listener(), presentation_context),
option_topic
)
var option_tags := PackedStringArray(
[
"option_id=%s" % option.get_option_id(),
"intent_id=%s" % option.get_intent_id(),
"enabled=true",
]
)
for topic_id in option.get_causal_topic_ids():
option_tags.append("topic_id=%s" % topic_id)
lines.append("- %s [#%s] => END" % [option_text, ",".join(option_tags)])
lines.append("=> END")
return "\n".join(lines)
static func sanitize_dialogue_text(value: String) -> String:
var sanitized := value.replace("\\", "\")
sanitized = sanitized.replace("\r", " ").replace("\n", " ").replace("\t", " ")
sanitized = sanitized.replace("[", "[").replace("]", "]")
sanitized = sanitized.replace("{", "{").replace("}", "}")
sanitized = sanitized.replace("=>", "=>")
sanitized = sanitized.replace(":", "\\:")
return " ".join(sanitized.split(" ", false)).strip_edges()
func _stable_key(
turn: ConversationTurn, semantic_id: StringName, intent_id: StringName, kind: StringName
) -> String:
return (
"%s|%d|%s|%s|%s"
% [turn.get_conversation_id(), turn.get_revision(), semantic_id, intent_id, kind]
)
func _select_template(templates: Array[String], stable_key: String) -> String:
if templates.is_empty():
return "..."
var digest := stable_key.sha256_text()
var stable_index := 0
for index in range(16):
stable_index = (stable_index * 33 + digest.unicode_at(index)) % templates.size()
return templates[stable_index]
func _render_template(
template: String, speaker_name: String, listener_name: String, topic_name: String
) -> String:
return sanitize_dialogue_text(
template.replace("{speaker}", speaker_name).replace("{listener}", listener_name).replace(
"{topic}", topic_name
)
)
func _entity_name(entity: WorldEntityRef, context: Dictionary) -> String:
var names: Dictionary = context.get("entity_names", {})
var value := String(
names.get(entity.index_key(), names.get(String(entity.get_entity_id()), ""))
)
if value.is_empty():
value = String(entity.get_entity_id()).replace("_", " ").capitalize()
return value
func _topic_name(topic_ids: Array[StringName], context: Dictionary) -> String:
if topic_ids.is_empty():
return "the matter at hand"
var labels: Dictionary = context.get("topic_labels", {})
var topic_id := topic_ids[0]
var value := String(labels.get(String(topic_id), ""))
if value.is_empty():
value = String(topic_id).replace("_", " ")
return value
func _presentation_context_is_valid(context: Dictionary) -> bool:
for key in context:
if (
(key is not String and key is not StringName)
or String(key) not in ["entity_names", "topic_labels"]
or context[key] is not Dictionary
):
return false
for entry_key in context[key] as Dictionary:
if entry_key is not String and entry_key is not StringName:
return false
var value: Variant = context[key][entry_key]
if value is not String:
return false
return true
func _resolve_dialogue_manager() -> Object:
if _dialogue_manager != null and is_instance_valid(_dialogue_manager):
return _dialogue_manager
if Engine.has_singleton("DialogueManager"):
return Engine.get_singleton("DialogueManager")
return null
func _fail(message: String) -> Variant:
last_error = message
return null
func _fail_string(message: String) -> String:
last_error = message
return ""