Files

1164 lines
43 KiB
C#

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);
/// <summary>
/// Emitted when some dialogue has reached the end.
/// </summary>
/// <param name="dialogueResource">The calling dialogue resource.</param>
public delegate void DialogueEndedEventHandler(Resource dialogueResource);
/// <summary>
/// Emitted when a dialogue balloon is created and dialogue starts.
/// </summary>
public static event DialogueStartedEventHandler? DialogueStarted;
/// <summary>
/// Emitted when a cue is encountered while traversing dialogue, usually when jumping from a goto line.
/// </summary>
public static event PassedCueEventHandler? PassedCue;
/// <summary>
/// Emitted when a line of dialogue is encountered.
/// </summary>
public static event GotDialogueEventHandler? GotDialogue;
/// <summary>
/// Emitted when a mutation is encountered.
/// </summary>
public static event MutatedEventHandler? Mutated;
/// <summary>
/// Emitted when waiting for input.
/// </summary>
public static event WaitingForInputEventHandler? WaitingForInput;
/// <summary>
/// Emitted when waiting for input received input.
/// </summary>
public static event WaitedForInputEventHandler? WaitedForInput;
/// <summary>
/// Emitted when some dialogue has reached the end.
/// </summary>
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<int, TaskCompletionSource<RefCounted?>> getNextLineRequests = new();
private static readonly System.Collections.Generic.Dictionary<int, TaskCompletionSource<RefCounted?>> getLineRequests = new();
private static readonly System.Collections.Generic.Dictionary<int, TaskCompletionSource<bool>> mutateRequests = new();
private static Type[]? cachedAssemblyTypes;
private static Type[] AssemblyTypes => cachedAssemblyTypes ??= Assembly.GetExecutingAssembly().GetTypes();
private static readonly System.Collections.Generic.Dictionary<Type, MethodInfo[]> 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;
}
/// <summary>
/// The list of globals that dialogue can query.
/// </summary>
public static Godot.Collections.Array GameStates
{
get => (Godot.Collections.Array)Instance.Get("game_states");
set => Instance.Set("game_states", value);
}
/// <summary>
/// Allow dialogue to call singletons.
/// </summary>
public static bool IncludeSingletons
{
get => (bool)Instance.Get("include_singletons");
set => Instance.Set("include_singletons", value);
}
/// <summary>
/// Allow dialogue to call static methods/properties on classes
/// </summary>
public static bool IncludeClasses
{
get => (bool)Instance.Get("include_classes");
set => Instance.Set("include_classes", value);
}
/// <summary>
/// Allow dialogue to call methods on the dialogue resource.
/// </summary>
public static bool IncludeDialogueResourceAsSelf
{
get => (bool)Instance.Get("include_dialogue_resource_as_self");
set => Instance.Set("include_dialogue_resource_as_self", value);
}
/// <summary>
/// A runtime override for the project setting to ignore missing state values.
/// </summary>
public static bool IgnoreMissingStateValues
{
get => (bool)Instance.Get("ignore_missing_state_values");
set => Instance.Set("ignore_missing_state_values", value);
}
/// <summary>
/// Used to resolve the current scene. Override if your game manages the current scene itself.
/// </summary>
public static Func<Node> GetCurrentScene
{
set => Instance.Set("get_current_scene", Callable.From(value));
}
/// <summary>
/// Used to resolve the load function used in <c>*.dialogue</c> files. Override this if you need safer loading or custom loading logic.
/// </summary>
public static Func<string, GodotObject> LoadFromWithinDialogue = GD.Load;
/// <summary>
/// Set a filter hook for member access on GodotObject things. The delegate is
/// stored on the GDScript-side <c>validate_member_access</c> Callable property
/// and is invoked whenever a thing's property, method, or string-keyed index is
/// about to be accessed at runtime. Return <c>""</c> to allow, or a non-empty
/// error string to deny.
/// <paramref name="kind"/> is one of: "property", "property_set", "method", "index".
/// </summary>
public static Func<Variant, StringName, StringName, string> ValidateMemberAccess
{
set => Instance.Set("validate_member_access", Callable.From(value));
}
/// <summary>
/// Set a random seed.
/// </summary>
/// <param name="nextSeed"></param>
public static void ReseedRandomizer(int nextSeed)
{
Instance.Call("reseed_randomizer", nextSeed);
}
/// <summary>
/// Step through lines and run any mutations until we either hit some dialogue or the end of the conversation.
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates">Array of extra nodes to expose as state to the dialogue</param>
/// <param name="mutation_behaviour"></param>
/// <returns>The next dialogue line</returns>
public static async Task<DialogueLine?> GetNextDialogueLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null, MutationBehaviour mutation_behaviour = MutationBehaviour.Wait)
{
int id = random.Next();
var tcs = new TaskCompletionSource<RefCounted?>();
getNextLineRequests[id] = tcs;
Instance.Call("_bridge_get_next_dialogue_line", id, dialogueResource, key, extraGameStates ?? new Array<Variant>(), (int)mutation_behaviour);
var line = await tcs.Task;
return line == null ? null : new DialogueLine(line);
}
/// <summary>
/// Get a line by its ID
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates">Array of extra nodes to expose as state to the dialogue</param>
/// <returns></returns>
public static async Task<DialogueLine?> GetLine(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
int id = random.Next();
var tcs = new TaskCompletionSource<RefCounted?>();
getLineRequests[id] = tcs;
Instance.Call("_bridge_get_line", id, dialogueResource, key, extraGameStates ?? new Array<Variant>());
var line = await tcs.Task;
return line == null ? null : new DialogueLine(line);
}
/// <summary>
/// Generate a dialogue resource on the fly from some text
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static Resource CreateResourceFromText(string text)
{
return (Resource)Instance.Call("create_resource_from_text", text);
}
/// <summary>
/// Set the default balloon to use at runtime.
/// </summary>
/// <param name="balloonScene"></param>
/// <returns></returns>
public static Error SetDefaultBalloon(Variant balloonScene)
{
return (Error)(int)Instance.Call("set_default_balloon", balloonScene);
}
/// <summary>
/// Show the example balloon
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates"></param>
/// <returns></returns>
public static CanvasLayer ShowExampleDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (CanvasLayer)Instance.Call("show_example_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
/// <summary>
/// Show the configured dialogue balloon
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates"></param>
/// <returns></returns>
public static Node ShowDialogueBalloon(Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon", dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
/// <summary>
/// Show a given balloon scene
/// </summary>
/// <param name="balloonScene"></param>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates"></param>
/// <returns></returns>
public static Node ShowDialogueBalloonScene(string balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
/// <summary>
/// Show a given balloon scene
/// </summary>
/// <param name="balloonScene"></param>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates"></param>
/// <returns></returns>
public static Node ShowDialogueBalloonScene(PackedScene balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
/// <summary>
/// Show a given balloon scene
/// </summary>
/// <param name="balloonScene"></param>
/// <param name="dialogueResource"></param>
/// <param name="key"></param>
/// <param name="extraGameStates"></param>
/// <returns></returns>
public static Node ShowDialogueBalloonScene(Node balloonScene, Resource dialogueResource, string key = "", Array<Variant>? extraGameStates = null)
{
return (Node)Instance.Call("show_dialogue_balloon_scene", balloonScene, dialogueResource, key, extraGameStates ?? new Array<Variant>());
}
/// <summary>
/// Resolve a static line ID to an actual line ID
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="staticId"></param>
/// <returns></returns>
public static string StaticIdToLineId(Resource dialogueResource, string staticId)
{
return (string)Instance.Call("static_id_to_line_id", dialogueResource, staticId);
}
/// <summary>
/// Resolve a static line ID to any actual line IDs that match
/// </summary>
/// <param name="dialogueResource"></param>
/// <param name="staticId"></param>
/// <returns></returns>
public static Array<string> StaticIdToLineIds(Resource dialogueResource, string staticId)
{
return (Array<string>)Instance.Call("static_id_to_line_ids", dialogueResource, staticId);
}
public static async Task Mutate(Dictionary mutation, Array<Variant>? extraGameStates = null, bool isInlineMutation = false)
{
int id = random.Next();
var tcs = new TaskCompletionSource<bool>();
mutateRequests[id] = tcs;
Instance.Call("_bridge_mutate", id, mutation, extraGameStates ?? new Array<Variant>(), isInlineMutation);
await tcs.Task;
}
public static Array<Dictionary> GetMembersForScript(Script script)
{
string typeName = script.ResourcePath.GetFile().GetBaseName();
var matchingType = AssemblyTypes.FirstOrDefault(t => t.Name == typeName);
if (matchingType == null) return new Array<Dictionary>();
return GetMembersForType(matchingType);
}
public static Array<Dictionary> GetMembersForPropertyChain(Script script, Array<string> chain)
{
string typeName = script.ResourcePath.GetFile().GetBaseName();
var currentType = AssemblyTypes.FirstOrDefault(t => t.Name == typeName);
if (currentType == null) return new Array<Dictionary>();
foreach (var segment in chain)
{
currentType = ResolvePropertyType(currentType, segment);
if (currentType == null) return new Array<Dictionary>();
}
return GetMembersForType(currentType);
}
public static Dictionary GetMethodInfoForPropertyChain(Script script, Array<string> 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<Dictionary> GetMembersForType(Type type)
{
Array<Dictionary> members = new Array<Dictionary>();
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<Dictionary>();
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<T> and Dictionary<TKey, TValue>, 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<Dictionary> GetMethodList(GodotObject thing)
{
var methodList = new Array<Dictionary>();
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<Dictionary>();
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<Variant> args)
{
return GetMethodInfoFor(thing, method, args) != null;
}
public async void ResolveThingMethod(float id, GodotObject thing, string method, Array<Variant> 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<Variant> 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
{
/// <summary>
/// The ID of this line
/// </summary>
public string Id { get; set; } = "";
/// <summary>
/// The internal type of this dialogue object.
/// </summary>
public string Type { get; set; } = "dialogue";
/// <summary>
/// The next line ID after this line.
/// </summary>
public string NextId { get; set; } = "";
/// <summary>
/// The character name that is saying this line.
/// </summary>
public string Character { get; set; } = "";
/// <summary>
/// The dialogue being spoken.
/// </summary>
public string Text { get; set; } = "";
/// <summary>
/// The key to use for translating this line.
/// </summary>
public string StaticId { get; set; } = "";
/// <summary>
/// A list of responses attached to this line of dialogue.
/// </summary>
public Array<DialogueResponse> Responses { get; } = new Array<DialogueResponse>();
/// <summary>
/// How long to show this line before advancing to the next. Either a float of seconds (as a string), <c>"auto"</c>, or empty string.
/// </summary>
public string? Time { get; private set; }
/// <summary>
/// A map for speed changes when typing out the dialogue text.
/// </summary>
public Dictionary Speeds { get; private set; } = new Dictionary();
/// <summary>
/// A map of any mutations to run while typing out the dialogue text.
/// </summary>
public Array<Godot.Collections.Array> InlineMutations { get; private set; } = new Array<Godot.Collections.Array>();
/// <summary>
/// A list of lines that are spoken simultaneously with this one.
/// </summary>
public Array<DialogueLine> ConcurrentLines { get; } = new Array<DialogueLine>();
/// <summary>
/// A list of any extra game states to check when resolving variables and mutations.
/// </summary>
public Array<Variant> ExtraGameStates { get; } = new Array<Variant>();
/// <summary>
/// Any #tags that were included in the line
/// </summary>
public Array<string> Tags { get; private set; } = new Array<string>();
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<Godot.Collections.Array>)data.Get("inline_mutations");
Time = (string)data.Get("time");
Tags = (Array<string>)data.Get("tags");
foreach (var concurrent_line_data in (Array<RefCounted>)data.Get("concurrent_lines"))
{
ConcurrentLines.Add(new DialogueLine(concurrent_line_data));
}
foreach (var response in (Array<RefCounted>)data.Get("responses"))
{
Responses.Add(new DialogueResponse(response));
}
}
/// <summary>
/// Check if a dialogue line has a given tag.
/// </summary>
/// <param name="tagName"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// Get the value of a tag if the tag is in the form of <c>tag=value</c>
/// </summary>
/// <param name="tagName"></param>
/// <returns></returns>
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 $"<DialogueLine character=\"{Character}\" text=\"{Text}\">";
case "mutation":
return "<DialogueLine mutation>";
default:
return "";
}
}
}
public partial class DialogueResponse : RefCounted
{
/// <summary>
/// The ID of this response
/// </summary>
public string NextId { get; set; } = "";
/// <summary>
/// <c>true</c> if the condition of this line was met.
/// </summary>
public bool IsAllowed { get; set; } = true;
/// <summary>
/// The original condition text.
/// </summary>
public string ConditionAsText { get; set; } = "";
/// <summary>
/// The prompt for this response.
/// </summary>
public string Text { get; set; } = "";
/// <summary>
/// The key to use for translating the text.
/// </summary>
public string StaticId { get; set; } = "";
/// <summary>
/// Any #tags
/// </summary>
public Array<string> Tags { get; private set; } = new Array<string>();
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<string>)data.Get("tags");
}
/// <summary>
/// Check if a dialogue line has a given tag.
/// </summary>
/// <param name="tagName"></param>
/// <returns></returns>
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;
}
}
/// <summary>
/// Get the value of a tag if the tag is in the form of <c>tag=value</c>
/// </summary>
/// <param name="tagName"></param>
/// <returns></returns>
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 $"<DialogueResponse text=\"{Text}\"";
}
}
}