using Godot; using Godot.Collections; using System; using System.Linq; using System.Reflection; using System.Threading.Tasks; #nullable enable namespace DialogueManagerRuntime { public enum MutationBehaviour { Wait, DoNotWait, Skip } public enum TranslationSource { None, Guess, CSV, PO } public partial class DialogueManager : RefCounted { public delegate void DialogueStartedEventHandler(Resource dialogueResource); public delegate void PassedCueEventHandler(string cue); public delegate void GotDialogueEventHandler(DialogueLine dialogueLine); public delegate void WaitingForInputEventHandler(); public delegate void WaitedForInputEventHandler(); public delegate void MutatedEventHandler(Dictionary mutation); /// /// Emitted when some dialogue has reached the end. /// /// The calling dialogue resource. public delegate void DialogueEndedEventHandler(Resource dialogueResource); /// /// Emitted when a dialogue balloon is created and dialogue starts. /// public static event DialogueStartedEventHandler? DialogueStarted; /// /// Emitted when a cue is encountered while traversing dialogue, usually when jumping from a goto line. /// public static event PassedCueEventHandler? PassedCue; /// /// Emitted when a line of dialogue is encountered. /// public static event GotDialogueEventHandler? GotDialogue; /// /// Emitted when a mutation is encountered. /// public static event MutatedEventHandler? Mutated; /// /// Emitted when waiting for input. /// public static event WaitingForInputEventHandler? WaitingForInput; /// /// Emitted when waiting for input received input. /// public static event WaitedForInputEventHandler? WaitedForInput; /// /// Emitted when some dialogue has reached the end. /// public static event DialogueEndedEventHandler? DialogueEnded; [Signal] public delegate void ResolvedEventHandler(double id, Variant value); private static Random random = new Random(); private static readonly System.Collections.Generic.Dictionary> getNextLineRequests = new(); private static readonly System.Collections.Generic.Dictionary> getLineRequests = new(); private static readonly System.Collections.Generic.Dictionary> mutateRequests = new(); private static Type[]? cachedAssemblyTypes; private static Type[] AssemblyTypes => cachedAssemblyTypes ??= Assembly.GetExecutingAssembly().GetTypes(); private static readonly System.Collections.Generic.Dictionary MethodCache = new(); private static MethodInfo[] GetMethodsForType(Type type) { if (!MethodCache.TryGetValue(type, out var methods)) { methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); MethodCache[type] = methods; } return methods; } private static void OnBridgeGetNextDialogueLineCompleted(int callId, RefCounted? line) { if (getNextLineRequests.Remove(callId, out var tcs)) { tcs.SetResult(line); } } private static void OnBridgeGetLineCompleted(int callId, RefCounted? line) { if (getLineRequests.Remove(callId, out var tcs)) { tcs.SetResult(line); } } private static void OnBridgeMutated(int callId) { if (mutateRequests.Remove(callId, out var tcs)) { tcs.SetResult(true); } } private static GodotObject? instance; public static GodotObject Instance => instance ??= InitializeInstance(); private static GodotObject InitializeInstance() { var dm = Engine.GetSingleton("DialogueManager"); dm.Connect("dialogue_started", Callable.From((Resource dialogueResource) => DialogueStarted?.Invoke(dialogueResource))); dm.Connect("passed_cue", Callable.From((string cue) => PassedCue?.Invoke(cue))); dm.Connect("got_dialogue", Callable.From((RefCounted line) => GotDialogue?.Invoke(new DialogueLine(line)))); dm.Connect("mutated", Callable.From((Dictionary mutation) => Mutated?.Invoke(mutation))); dm.Connect("waiting_for_input", Callable.From(() => WaitingForInput?.Invoke())); dm.Connect("waited_for_input", Callable.From(() => WaitedForInput?.Invoke())); dm.Connect("dialogue_ended", Callable.From((Resource dialogueResource) => DialogueEnded?.Invoke(dialogueResource))); dm.Connect("bridge_get_next_dialogue_line_completed", Callable.From((int callId, RefCounted? line) => OnBridgeGetNextDialogueLineCompleted(callId, line))); dm.Connect("bridge_get_line_completed", Callable.From((int callId, RefCounted? line) => OnBridgeGetLineCompleted(callId, line))); dm.Connect("bridge_mutated", Callable.From((int callId) => OnBridgeMutated(callId))); return dm; } /// /// The list of globals that dialogue can query. /// public static Godot.Collections.Array GameStates { get => (Godot.Collections.Array)Instance.Get("game_states"); set => Instance.Set("game_states", value); } /// /// Allow dialogue to call singletons. /// public static bool IncludeSingletons { get => (bool)Instance.Get("include_singletons"); set => Instance.Set("include_singletons", value); } /// /// Allow dialogue to call static methods/properties on classes /// public static bool IncludeClasses { get => (bool)Instance.Get("include_classes"); set => Instance.Set("include_classes", value); } /// /// Allow dialogue to call methods on the dialogue resource. /// public static bool IncludeDialogueResourceAsSelf { get => (bool)Instance.Get("include_dialogue_resource_as_self"); set => Instance.Set("include_dialogue_resource_as_self", value); } /// /// A runtime override for the project setting to ignore missing state values. /// public static bool IgnoreMissingStateValues { get => (bool)Instance.Get("ignore_missing_state_values"); set => Instance.Set("ignore_missing_state_values", value); } /// /// Used to resolve the current scene. Override if your game manages the current scene itself. /// public static Func GetCurrentScene { set => Instance.Set("get_current_scene", Callable.From(value)); } /// /// Used to resolve the load function used in *.dialogue files. Override this if you need safer loading or custom loading logic. /// public static Func LoadFromWithinDialogue = GD.Load; /// /// Set a filter hook for member access on GodotObject things. The delegate is /// stored on the GDScript-side validate_member_access Callable property /// and is invoked whenever a thing's property, method, or string-keyed index is /// about to be accessed at runtime. Return "" to allow, or a non-empty /// error string to deny. /// is one of: "property", "property_set", "method", "index". /// public static Func ValidateMemberAccess { set => Instance.Set("validate_member_access", Callable.From(value)); } /// /// Set a random seed. /// /// public static void ReseedRandomizer(int nextSeed) { Instance.Call("reseed_randomizer", nextSeed); } /// /// Step through lines and run any mutations until we either hit some dialogue or the end of the conversation. /// /// /// /// Array of extra nodes to expose as state to the dialogue /// /// The next dialogue line public static async Task GetNextDialogueLine(Resource dialogueResource, string key = "", Array? extraGameStates = null, MutationBehaviour mutation_behaviour = MutationBehaviour.Wait) { int id = random.Next(); var tcs = new TaskCompletionSource(); getNextLineRequests[id] = tcs; Instance.Call("_bridge_get_next_dialogue_line", id, dialogueResource, key, extraGameStates ?? new Array(), (int)mutation_behaviour); var line = await tcs.Task; return line == null ? null : new DialogueLine(line); } /// /// Get a line by its ID /// /// /// /// Array of extra nodes to expose as state to the dialogue /// public static async Task GetLine(Resource dialogueResource, string key = "", Array? extraGameStates = null) { int id = random.Next(); var tcs = new TaskCompletionSource(); getLineRequests[id] = tcs; Instance.Call("_bridge_get_line", id, dialogueResource, key, extraGameStates ?? new Array()); var line = await tcs.Task; return line == null ? null : new DialogueLine(line); } /// /// Generate a dialogue resource on the fly from some text /// /// /// public static Resource CreateResourceFromText(string text) { return (Resource)Instance.Call("create_resource_from_text", text); } /// /// Set the default balloon to use at runtime. /// /// /// public static Error SetDefaultBalloon(Variant balloonScene) { return (Error)(int)Instance.Call("set_default_balloon", balloonScene); } /// /// Show the example balloon /// /// /// /// /// public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) { return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); } /// /// Show the configured dialogue balloon /// /// /// /// /// public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array? extraGameStates = null) { return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array()); } /// /// Show a given balloon scene /// /// /// /// /// /// public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) { return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); } /// /// Show a given balloon scene /// /// /// /// /// /// public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) { return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); } /// /// Show a given balloon scene /// /// /// /// /// /// public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array? extraGameStates = null) { return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array()); } /// /// Resolve a static line ID to an actual line ID /// /// /// /// public static string StaticIdToLineId(Resource dialogueResource, string staticId) { return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId); } /// /// Resolve a static line ID to any actual line IDs that match /// /// /// /// public static Array StaticIdToLineIds(Resource dialogueResource, string staticId) { return (Array)Instance.Call("static_id_to_line_ids", dialogueResource, staticId); } public static async Task Mutate(Dictionary mutation, Array? extraGameStates = null, bool isInlineMutation = false) { int id = random.Next(); var tcs = new TaskCompletionSource(); mutateRequests[id] = tcs; Instance.Call("_bridge_mutate", id, mutation, extraGameStates ?? new Array(), isInlineMutation); await tcs.Task; } public static Array GetMembersForScript(Script script) { string typeName = script.ResourcePath.GetFile().GetBaseName(); var matchingType = AssemblyTypes.FirstOrDefault(t => t.Name == typeName); if (matchingType == null) return new Array(); return GetMembersForType(matchingType); } public static Array GetMembersForPropertyChain(Script script, Array chain) { string typeName = script.ResourcePath.GetFile().GetBaseName(); var currentType = AssemblyTypes.FirstOrDefault(t => t.Name == typeName); if (currentType == null) return new Array(); foreach (var segment in chain) { currentType = ResolvePropertyType(currentType, segment); if (currentType == null) return new Array(); } return GetMembersForType(currentType); } public static Dictionary GetMethodInfoForPropertyChain(Script script, Array chain, string methodName) { string typeName = script.ResourcePath.GetFile().GetBaseName(); var currentType = AssemblyTypes.FirstOrDefault(t => t.Name == typeName); if (currentType == null) return new Dictionary(); foreach (var segment in chain) { currentType = ResolvePropertyType(currentType, segment); if (currentType == null) return new Dictionary(); } var methodInfo = currentType .GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public) .FirstOrDefault(m => m.Name == methodName && !m.IsSpecialName); if (methodInfo == null) return new Dictionary(); return BuildMethodDictionary(methodInfo); } private static Array GetMembersForType(Type type) { Array members = new Array(); if (type.IsEnum) { foreach (var name in type.GetEnumNames()) { members.Add(new Dictionary() { { "name", name }, { "type", "enum" } }); } return members; } var memberInfos = type.GetMembers(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); foreach (var memberInfo in memberInfos) { switch (memberInfo.MemberType) { case MemberTypes.Field: FieldInfo fieldInfo = (FieldInfo)memberInfo; string fieldType; if (fieldInfo.FieldType.ToString().Contains("EventHandler")) { fieldType = "signal"; } else if (fieldInfo.IsLiteral) { fieldType = "constant"; } else { fieldType = "property"; } members.Add(new Dictionary() { { "name", memberInfo.Name }, { "type", fieldType }, { "class_name", GetFriendlyTypeName(fieldInfo.FieldType) } }); break; case MemberTypes.Property: PropertyInfo propInfo = (PropertyInfo)memberInfo; members.Add(new Dictionary() { { "name", memberInfo.Name }, { "type", "property" }, { "class_name", GetFriendlyTypeName(propInfo.PropertyType) } }); break; case MemberTypes.Method: MethodInfo methodInfo = (MethodInfo)memberInfo; if (methodInfo.IsSpecialName) continue; members.Add(BuildMethodDictionary(methodInfo)); break; case MemberTypes.NestedType: members.Add(new Dictionary() { { "name", memberInfo.Name }, { "type", "constant" } }); break; default: continue; } } return members; } private static Dictionary BuildMethodDictionary(MethodInfo methodInfo) { var args = new Array(); foreach (var param in methodInfo.GetParameters()) { args.Add(new Dictionary() { { "name", param.Name ?? "" }, { "type", (int)Variant.Type.Nil }, { "class_name", GetFriendlyTypeName(param.ParameterType) } }); } return new Dictionary() { { "name", methodInfo.Name }, { "type", "method" }, { "args", args } }; } private static Type? ResolvePropertyType(Type type, string memberName) { var field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); if (field != null) return field.FieldType; var prop = type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); if (prop != null) return prop.PropertyType; var nested = type.GetNestedType(memberName, BindingFlags.Public); if (nested != null) return nested; return null; } private static string GetFriendlyTypeName(Type type) { if (type == typeof(int)) return "int"; if (type == typeof(long)) return "long"; if (type == typeof(float)) return "float"; if (type == typeof(double)) return "double"; if (type == typeof(bool)) return "bool"; if (type == typeof(string)) return "string"; if (type == typeof(void)) return "void"; if (type == typeof(byte)) return "byte"; if (type == typeof(short)) return "short"; if (type == typeof(char)) return "char"; if (type == typeof(decimal)) return "decimal"; return type.Name; } public bool ThingHasConstant(GodotObject thing, string property) { var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); return memberInfos.Length > 0; } public Variant ResolveThingConstant(GodotObject thing, string property) { var memberInfos = thing.GetType().GetMember(property, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); foreach (var memberInfo in memberInfos) { if (memberInfo != null) { try { switch (memberInfo) { case FieldInfo fieldInfo: return ConvertValueToVariant(fieldInfo.GetValue(thing)); case PropertyInfo propInfo: return ConvertValueToVariant(propInfo.GetValue(thing)); case Type nestedType when nestedType.IsEnum: return GetEnumAsDictionary(nestedType); default: break; } } catch (Exception e) { throw new Exception($"{property} is not supported by Variant.", e); } } } throw new Exception($"{property} is not a public constant on {thing}"); } Dictionary GetEnumAsDictionary(Type enumType) { Dictionary dictionary = new Dictionary(); foreach (var value in enumType.GetEnumValuesAsUnderlyingType()) { var key = enumType.GetEnumName(value); if (key != null) { dictionary.Add(key, ConvertValueToVariant(value)); } } return dictionary; } Variant ConvertValueToVariant(object? value) { if (value == null) return default; Type rawType = value.GetType(); if (rawType.IsEnum) { value = Convert.ChangeType(value, Enum.GetUnderlyingType(rawType)); } return value switch { Variant v => v, bool v => Variant.From(v), byte v => Variant.From((long)v), sbyte v => Variant.From((long)v), short v => Variant.From((long)v), ushort v => Variant.From((long)v), int v => Variant.From((long)v), uint v => Variant.From((long)v), long v => Variant.From(v), ulong v => Variant.From((long)v), float v => Variant.From((double)v), double v => Variant.From(v), string v => Variant.From(v), Godot.Collections.Array v => v, Godot.Collections.Dictionary v => v, GodotObject godotObj => Variant.From(godotObj), _ => ConvertOtherValueToVariant(value, rawType) }; } // Anything Variant supports that isn't covered by the fast paths above (typed collections like Array and Dictionary, packed arrays, // Vector2/3/4, Color, etc) is boxed through the generic marshaller so it survives the trip into GDScript instead of silently becoming null. Variant ConvertOtherValueToVariant(object value, Type rawType) { try { MethodInfo from = typeof(Variant) .GetMethods(BindingFlags.Public | BindingFlags.Static) .First(m => m.Name == nameof(Variant.From) && m.IsGenericMethodDefinition) .MakeGenericMethod(rawType); return (Variant)from.Invoke(null, new[] { value })!; } catch (Exception) { return default; } } public Array GetMethodList(GodotObject thing) { var methodList = new Array(); if (thing == null) return methodList; Type type = thing.GetType(); MethodInfo[] methodInfos = GetMethodsForType(type); foreach (MethodInfo method in methodInfos) { if (method.IsSpecialName) continue; var methodInfo = new Dictionary(); methodInfo["name"] = method.Name; methodInfo["flags"] = 0; methodInfo["dotnet"] = true; var argsList = new Array(); ParameterInfo[] parameters = method.GetParameters(); foreach (ParameterInfo parameter in parameters) { var paramInfo = new Dictionary() { }; Variant.Type godotType = ConvertToVariantType(parameter.ParameterType); paramInfo["name"] = parameter.Name ?? ""; paramInfo["type"] = (int)godotType; if (godotType == Variant.Type.Object) { paramInfo["class_name"] = parameter.ParameterType.Name; } else { paramInfo["class_name"] = string.Empty; } argsList.Add(paramInfo); } methodInfo["args"] = argsList; methodList.Add(methodInfo); } return methodList; } private Variant.Type ConvertToVariantType(Type type) { if (type == typeof(void)) return Variant.Type.Nil; if (type == typeof(bool)) return Variant.Type.Bool; if (type == typeof(long) || type == typeof(int) || type == typeof(short) || type == typeof(byte)) return Variant.Type.Int; if (type == typeof(double) || type == typeof(float)) return Variant.Type.Float; if (type == typeof(string)) return Variant.Type.String; if (type == typeof(Vector2)) return Variant.Type.Vector2; if (type == typeof(Vector2I)) return Variant.Type.Vector2I; if (type == typeof(Rect2)) return Variant.Type.Rect2; if (type == typeof(Rect2I)) return Variant.Type.Rect2I; if (type == typeof(Vector3)) return Variant.Type.Vector3; if (type == typeof(Vector3I)) return Variant.Type.Vector3I; if (type == typeof(Transform2D)) return Variant.Type.Transform2D; if (type == typeof(Vector4)) return Variant.Type.Vector4; if (type == typeof(Vector4I)) return Variant.Type.Vector4I; if (type == typeof(Plane)) return Variant.Type.Plane; if (type == typeof(Quaternion)) return Variant.Type.Quaternion; if (type == typeof(Aabb)) return Variant.Type.Aabb; if (type == typeof(Basis)) return Variant.Type.Basis; if (type == typeof(Transform3D)) return Variant.Type.Transform3D; if (type == typeof(Projection)) return Variant.Type.Projection; if (type == typeof(Color)) return Variant.Type.Color; if (type == typeof(StringName)) return Variant.Type.StringName; if (type == typeof(NodePath)) return Variant.Type.NodePath; if (type == typeof(Rid)) return Variant.Type.Rid; if (typeof(GodotObject).IsAssignableFrom(type)) return Variant.Type.Object; if (typeof(Dictionary).IsAssignableFrom(type)) return Variant.Type.Dictionary; if (typeof(Godot.Collections.Array).IsAssignableFrom(type)) return Variant.Type.Array; if (type == typeof(byte[])) return Variant.Type.PackedByteArray; if (type == typeof(int[])) return Variant.Type.PackedInt32Array; if (type == typeof(long[])) return Variant.Type.PackedInt64Array; if (type == typeof(float[])) return Variant.Type.PackedFloat32Array; if (type == typeof(double[])) return Variant.Type.PackedFloat64Array; if (type == typeof(string[])) return Variant.Type.PackedStringArray; if (type == typeof(Vector2[])) return Variant.Type.PackedVector2Array; if (type == typeof(Vector3[])) return Variant.Type.PackedVector3Array; if (type == typeof(Color[])) return Variant.Type.PackedColorArray; if (type == typeof(Vector4[])) return Variant.Type.PackedVector4Array; return Variant.Type.Nil; } private bool IsCompatible(Type expectedType, Type providedType) { if (expectedType.IsAssignableFrom(providedType)) return true; Variant.Type expectedVariant = ConvertToVariantType(expectedType); Variant.Type actualVariant = ConvertToVariantType(providedType); return expectedVariant == actualVariant && expectedVariant != Variant.Type.Nil; } public bool ThingHasMethod(GodotObject thing, string method, Array args) { return GetMethodInfoFor(thing, method, args) != null; } public async void ResolveThingMethod(float id, GodotObject thing, string method, Array args) { // Add a single frame wait in case the method returns before signals can listen await ToSignal(Engine.GetMainLoop(), SceneTree.SignalName.ProcessFrame); var methodInfo = GetMethodInfoFor(thing, method, args); if (methodInfo == null) { EmitSignal(SignalName.Resolved, id, default); return; } #nullable disable // Convert the method args to something reflection can handle ParameterInfo[] argTypes = methodInfo.GetParameters(); object[] _args = new object[argTypes.Length]; for (int i = 0; i < argTypes.Length; i++) { // check if args is assignable from derived type if (i < args.Count && args[i].Obj != null) { if (argTypes[i].ParameterType.IsAssignableFrom(args[i].Obj.GetType())) { _args[i] = args[i].Obj; } // fallback to assigning primitive types else { _args[i] = Convert.ChangeType(args[i].Obj, argTypes[i].ParameterType); } } else if (argTypes[i].DefaultValue != null) { _args[i] = argTypes[i].DefaultValue; } } // invoke method and handle the result based on return type object result = methodInfo.Invoke(thing, _args); if (result is Task taskResult) { await taskResult; try { object value = taskResult.GetType().GetProperty("Result").GetValue(taskResult); EmitSignal(SignalName.Resolved, id, ConvertValueToVariant(value)); } catch (Exception) { EmitSignal(SignalName.Resolved, id, default); } } else { EmitSignal(SignalName.Resolved, id, ConvertValueToVariant(result)); } } #nullable enable private MethodInfo? GetMethodInfoFor(GodotObject thing, string method, Array args) { return GetMethodsForType(thing.GetType()).Where(m => m.Name == method).FirstOrDefault(m => { ParameterInfo[] parameters = m.GetParameters(); if (args.Count > parameters.Length) return false; for (int i = args.Count; i < parameters.Length; i++) { if (!parameters[i].IsOptional) return false; } Type[] argTypes = args.Select(arg => { // If the item is boxed inside a Godot Variant, extract its actual type. if (arg is Variant godotVariant) { return godotVariant.VariantType switch { Variant.Type.Nil => typeof(object), Variant.Type.Bool => typeof(bool), Variant.Type.Int => typeof(long), // Godot ints map to C# longs Variant.Type.Float => typeof(double), // Godot floats map to C# doubles Variant.Type.String => typeof(string), Variant.Type.Object => godotVariant.AsGodotObject()?.GetType() ?? typeof(object), _ => godotVariant.Obj?.GetType() ?? typeof(object) }; } return arg.GetType(); }).ToArray(); // Check each given parameter type against what the method wants. for (int i = 0; i < argTypes.Length; i++) { Type expectedType = parameters[i].ParameterType; Type actualType = argTypes[i]; // If a parameter isn't compatible skip this method overload. if (!IsCompatible(expectedType, actualType)) { return false; } } return true; }); } public static string GetErrorMessage(int error) { return (string)Instance.Call("_bridge_get_error_message", error); } public static string Translate(string s) { return (string)Instance.Call("_bridge_translate", s); } } public partial class DialogueLine : RefCounted { /// /// The ID of this line /// public string Id { get; set; } = ""; /// /// The internal type of this dialogue object. /// public string Type { get; set; } = "dialogue"; /// /// The next line ID after this line. /// public string NextId { get; set; } = ""; /// /// The character name that is saying this line. /// public string Character { get; set; } = ""; /// /// The dialogue being spoken. /// public string Text { get; set; } = ""; /// /// The key to use for translating this line. /// public string StaticId { get; set; } = ""; /// /// A list of responses attached to this line of dialogue. /// public Array Responses { get; } = new Array(); /// /// How long to show this line before advancing to the next. Either a float of seconds (as a string), "auto", or empty string. /// public string? Time { get; private set; } /// /// A map for speed changes when typing out the dialogue text. /// public Dictionary Speeds { get; private set; } = new Dictionary(); /// /// A map of any mutations to run while typing out the dialogue text. /// public Array InlineMutations { get; private set; } = new Array(); /// /// A list of lines that are spoken simultaneously with this one. /// public Array ConcurrentLines { get; } = new Array(); /// /// A list of any extra game states to check when resolving variables and mutations. /// public Array ExtraGameStates { get; } = new Array(); /// /// Any #tags that were included in the line /// public Array Tags { get; private set; } = new Array(); public DialogueLine(RefCounted data) { Id = (string)data.Get("id"); Type = (string)data.Get("type"); NextId = (string)data.Get("next_id"); Character = (string)data.Get("character"); Text = (string)data.Get("text"); StaticId = (string)data.Get("static_id"); Speeds = (Dictionary)data.Get("speeds"); InlineMutations = (Array)data.Get("inline_mutations"); Time = (string)data.Get("time"); Tags = (Array)data.Get("tags"); foreach (var concurrent_line_data in (Array)data.Get("concurrent_lines")) { ConcurrentLines.Add(new DialogueLine(concurrent_line_data)); } foreach (var response in (Array)data.Get("responses")) { Responses.Add(new DialogueResponse(response)); } } /// /// Check if a dialogue line has a given tag. /// /// /// public bool HasTag(string tagName) { if (Tags.Contains(tagName)) { return true; } else { string wrapped = $"{tagName}="; foreach (var tag in Tags) { if (tag.StartsWith(wrapped)) { return true; } } return false; } } /// /// Get the value of a tag if the tag is in the form of tag=value /// /// /// public string GetTagValue(string tagName) { string wrapped = $"{tagName}="; foreach (var tag in Tags) { if (tag.StartsWith(wrapped)) { return tag.Substring(wrapped.Length); } } return ""; } public override string ToString() { switch (Type) { case "dialogue": return $""; case "mutation": return ""; default: return ""; } } } public partial class DialogueResponse : RefCounted { /// /// The ID of this response /// public string NextId { get; set; } = ""; /// /// true if the condition of this line was met. /// public bool IsAllowed { get; set; } = true; /// /// The original condition text. /// public string ConditionAsText { get; set; } = ""; /// /// The prompt for this response. /// public string Text { get; set; } = ""; /// /// The key to use for translating the text. /// public string StaticId { get; set; } = ""; /// /// Any #tags /// public Array Tags { get; private set; } = new Array(); public DialogueResponse(RefCounted data) { NextId = (string)data.Get("next_id"); IsAllowed = (bool)data.Get("is_allowed"); Text = (string)data.Get("text"); StaticId = (string)data.Get("static_id"); Tags = (Array)data.Get("tags"); } /// /// Check if a dialogue line has a given tag. /// /// /// public bool HasTag(string tagName) { if (Tags.Contains(tagName)) { return true; } else { string wrapped = $"{tagName}="; foreach (var tag in Tags) { if (tag.StartsWith(wrapped)) { return true; } } return false; } } /// /// Get the value of a tag if the tag is in the form of tag=value /// /// /// public string GetTagValue(string tagName) { string wrapped = $"{tagName}="; foreach (var tag in Tags) { if (tag.StartsWith(wrapped)) { return tag.Substring(wrapped.Length); } } return ""; } public override string ToString() { return $"