#nullable enable using Godot; using System; using System.Collections.Generic; using static GodotInk.MarshalUtils; using PropertyList = Godot.Collections.Array; namespace GodotInk; [Tool] #if GODOT4_1_OR_GREATER [GlobalClass, Icon("../GodotInk.svg")] #endif public partial class InkStory : Resource { [Signal] public delegate void ContinuedEventHandler(); [Signal] public delegate void MadeChoiceEventHandler(InkChoice choice); protected virtual string RawStory { get => rawStory; set { rawStory = value; #if TOOLS // There's really no need to instantiate Ink.Runtime in the editor itself. // if (Engine.IsEditorHint()) return; <- Commenting for now, because it prevents // the InkDock from running stories. #endif InitializeRuntimeStory(); } } private string rawStory = string.Empty; private Ink.Runtime.Story runtimeStory = null!; private readonly Dictionary> observers = new(); private readonly Dictionary internalObservers = new(); public static InkStory Create(string rawStory) { return new InkStory() { rawStory = rawStory }; } private void InitializeRuntimeStory() { if (runtimeStory != null) { runtimeStory.onDidContinue -= OnContinued; runtimeStory.onMakeChoice -= OnMadeChoice; } runtimeStory = new Ink.Runtime.Story(rawStory); runtimeStory.onDidContinue += OnContinued; runtimeStory.onMakeChoice += OnMadeChoice; } public string CurrentText => runtimeStory.currentText; public IReadOnlyList CurrentChoices => ToVariants(runtimeStory.currentChoices); public IReadOnlyList CurrentTags => runtimeStory.currentTags; public bool HasWarning => runtimeStory.hasWarning; public IReadOnlyList CurrentWarnings => runtimeStory.currentWarnings; public bool HasError => runtimeStory.hasError; public IReadOnlyList CurrentErrors => runtimeStory.currentErrors; /// /// Check whether more content is available if you were to call Continue() - i.e. /// are we mid story rather than at a choice point or at the end. /// public bool CanContinue => runtimeStory.canContinue; /// /// Continue the story for one line of content, if possible. /// If you're not sure if there's more content available, for example if you /// want to check whether you're at a choice point or at the end of the story, /// you should call canContinue before calling this function. /// /// The line of text content. public string Continue() { return runtimeStory.Continue(); } /// /// Continue the story until the next choice point or until it runs out of content. /// This is as opposed to the Continue() method which only evaluates one line of /// output at a time. /// /// The resulting text evaluated by the ink engine, concatenated together. public string ContinueMaximally() { return runtimeStory.ContinueMaximally(); } /// /// Chooses the Choice from the currentChoices list with the given /// index. Internally, this sets the current content path to that /// pointed to by the Choice, ready to continue story evaluation. /// /// The index of the choice to choose. public void ChooseChoiceIndex(int choiceIdx) { runtimeStory.ChooseChoiceIndex(choiceIdx); } public void ChoosePathString(string path, bool resetCallstack = true, params Variant[] arguments) { runtimeStory.ChoosePathString(path, resetCallstack, FromVariants(arguments)); } /// /// Unwinds the callstack. Useful to reset the Story's evaluation /// without actually changing any meaningful state, for example if /// you want to exit a section of story prematurely and tell it to /// go elsewhere with a call to ChoosePathString(...). /// Doing so without calling ResetCallstack() could cause unexpected /// issues if, for example, the Story was in a tunnel already. /// public void ResetCallstack() { runtimeStory.ResetCallstack(); } /// /// Reset the Story back to its initial state as it was when it was /// first constructed. /// public void ResetState() { runtimeStory.ResetState(); } /// /// Get any global tags associated with the story. These are defined as /// hash tags defined at the very top of the story. /// public IReadOnlyList GlobalTags => runtimeStory.globalTags; /// /// Gets any tags associated with a particular knot or knot.stitch. /// These are defined as hash tags defined at the very top of a knot or stitch. /// /// The path of the knot or stitch, in the form "knot" or "knot.stitch". /// The list of tags. public IReadOnlyList TagsForContentAtPath(string path) { return runtimeStory.TagsForContentAtPath(path); } public string CurrentFlowName => runtimeStory.currentFlowName; public bool CurrentFlowIsDefaultFlow => runtimeStory.currentFlowIsDefaultFlow; public IReadOnlyList AliveFlowNames => runtimeStory.aliveFlowNames; /// /// /// /// public void RemoveFlow(string flowName) { runtimeStory.RemoveFlow(flowName); } /// /// /// /// public void SwitchFlow(string flowName) { runtimeStory.SwitchFlow(flowName); } /// /// /// public void SwitchToDefaultFlow() { runtimeStory.SwitchToDefaultFlow(); } public int VisitCountAtPathString(string pathString) { return runtimeStory.state.VisitCountAtPathString(pathString); } public Variant FetchVariable(string variableName) { return ToVariant(runtimeStory.variablesState[variableName]); } public T FetchVariable<[MustBeVariant] T>(string variableName) { return FetchVariable(variableName).As(); } public void StoreVariable(string variableName, Variant value) { runtimeStory.variablesState[variableName] = FromVariant(value); } public void StoreVariable<[MustBeVariant] T>(string variableName, T value) { StoreVariable(variableName, Variant.From(value)); } /// /// /// /// /// public void ObserveVariable(string variableName, Callable observer) { if (!internalObservers.ContainsKey(variableName)) { Ink.Runtime.Story.VariableObserver internalObserver = BuildObserver(); runtimeStory.ObserveVariable(variableName, internalObserver); internalObservers[variableName] = internalObserver; } if (observers.ContainsKey(variableName)) _ = observers[variableName].Add(observer); else observers[variableName] = new() { observer }; } /// /// /// /// /// public void ObserveVariable(string[] variableNames, Callable observer) { foreach (string variableName in variableNames) ObserveVariable(variableName, observer); } private Ink.Runtime.Story.VariableObserver BuildObserver() { return delegate (string name, object? value) { if (!observers.TryGetValue(name, out var callables)) return; Variant variant = ToVariant(value); foreach (Callable callable in callables) _ = callable.Call(name, variant); }; } /// /// /// /// public void RemoveVariableObserver(Callable callable) { foreach (string variableName in observers.Keys) RemoveVariableObserver(callable, variableName); } /// /// /// /// public void RemoveVariableObserver(string specificVariableName) { runtimeStory.RemoveVariableObserver(null, specificVariableName); _ = internalObservers.Remove(specificVariableName); _ = observers.Remove(specificVariableName); } /// /// /// /// /// public void RemoveVariableObserver(Callable callable, string specificVariableName) { var callables = observers[specificVariableName]; if (!callables.Contains(callable)) return; _ = callables.Remove(callable); if (callables.Count > 0) return; runtimeStory.RemoveVariableObserver(null, specificVariableName); _ = internalObservers.Remove(specificVariableName); } /// /// An ink file can provide a fallback functions for when when an EXTERNAL has been left /// unbound by the client, and the fallback function will be called instead. Useful when /// testing a story in play mode, when it's not possible to write a client-side C# external /// function, but you don't want it to fail to run. /// public bool AllowExternalFunctionFallbacks => runtimeStory.allowExternalFunctionFallbacks; /// /// Checks if a function exists. /// /// The name of the function as declared in ink. /// True if the function exists, else false. public bool HasFunction(string functionName) { return runtimeStory.HasFunction(functionName); } /// /// Evaluates a function defined in ink. /// /// The name of the function as declared in ink. /// /// The arguments that the ink function takes, if any. Note that we don't (can't) do any /// validation on the number of arguments right now, so make sure you get it right! /// /// /// The return value as returned from the ink function with `~ return myValue`, or a nil /// variant if nothing is returned. /// public Variant EvaluateFunction(string functionName, params Variant[] arguments) { object? result = runtimeStory.EvaluateFunction(functionName, FromVariants(arguments)); return ToVariant(result); } /// /// Evaluates a function defined in ink. /// /// The name of the function as declared in ink. /// /// The arguments that the ink function takes, if any. Note that we don't (can't) do any /// validation on the number of arguments right now, so make sure you get it right! /// /// /// The return value as returned from the ink function with `~ return myValue`, or a nil /// variant if nothing is returned. /// public Variant EvaluateFunction(string functionName, Godot.Collections.Array arguments) { object? result = runtimeStory.EvaluateFunction(functionName, FromVariants(arguments)); return ToVariant(result); } /// /// Evaluates a function defined in ink, and gathers the possibly multi-line text as generated /// by the function. This text output is any text written as normal content within the function, /// as opposed to the return value, as returned with `~ return`. /// /// The name of the function as declared in ink. /// The text content produced by the function via normal ink, if any. /// /// The arguments that the ink function takes, if any. Note that we don't (can't) do any /// validation on the number of arguments right now, so make sure you get it right! /// /// /// The return value as returned from the ink function with `~ return myValue`, or a nil /// variant if nothing is returned. /// public Variant EvaluateFunction(string functionName, out string textOutput, params Variant[] arguments) { object? result = runtimeStory.EvaluateFunction(functionName, out textOutput, FromVariants(arguments)); return ToVariant(result); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The Godot Callable to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Callable callable, bool lookaheadSafe = false) { runtimeStory.BindExternalFunctionGeneral(funcName, trampoline, lookaheadSafe); object? trampoline(object?[] arguments) => FromVariant(callable.Call(ToVariants(arguments))); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# function to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Func func, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, trampoline, lookaheadSafe); object? trampoline() => FromVariant(func.Invoke()); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# function to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Func func, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, (Func)trampoline, lookaheadSafe); object? trampoline(T a) => FromVariant(func.Invoke(a)); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# function to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Func func, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, (Func)trampoline, lookaheadSafe); object? trampoline(T1 a, T2 b) => FromVariant(func.Invoke(a, b)); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# function to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Func func, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, (Func)trampoline, lookaheadSafe); object? trampoline(T1 a, T2 b, T3 c) => FromVariant(func.Invoke(a, b, c)); } /// /// Bind a C# function to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# function to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Func func, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, (Func)trampoline, lookaheadSafe); object? trampoline(T1 a, T2 b, T3 c, T4 d) => FromVariant(func.Invoke(a, b, c, d)); } /// /// Bind a C# Action to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# action to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Action action, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, action, lookaheadSafe); } /// /// Bind a C# Action to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# action to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Action action, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, action, lookaheadSafe); } /// /// Bind a C# Action to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# action to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Action action, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, action, lookaheadSafe); } /// /// Bind a C# Action to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# action to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Action action, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, action, lookaheadSafe); } /// /// Bind a C# Action to an ink EXTERNAL function declaration. /// /// EXTERNAL ink function name to bind to. /// The C# action to bind. /// The ink engine often evaluates further /// than you might expect beyond the current line just in case it sees /// glue that will cause the two lines to become one. In this case it's /// possible that a function can appear to be called twice instead of /// just once, and earlier than you expect. If it's safe for your /// function to be called in this way (since the result and side effect /// of the function will not change), then you can pass 'true'. /// Usually, you want to pass 'false', especially if you want some action /// to be performed in game code when this function is called. public void BindExternalFunction(string funcName, Action action, bool lookaheadSafe = false) { runtimeStory.BindExternalFunction(funcName, action, lookaheadSafe); } /// /// Remove a binding for a named EXTERNAL ink function. /// /// The name of the EXTERNAL ink function to unbind. public void UnbindExternalFunction(string funcName) { runtimeStory.UnbindExternalFunction(funcName); } /// /// /// /// public void Error(string message) { runtimeStory.Error(message); } /// /// /// /// /// public void Error(string message, bool useEndLineNumber) { runtimeStory.Error(message, useEndLineNumber); } /// /// /// /// public void Warning(string message) { runtimeStory.Warning(message); } /// /// Save the current story state a JSON string. /// /// The current state serialized into a JSON string. public string SaveState() { return runtimeStory.state.ToJson(); } /// /// Save the current story state to a JSON file. /// /// The path to the file we will be writing to. public void SaveStateFile(string filePath) { using FileAccess file = FileAccess.Open(filePath, FileAccess.ModeFlags.Write); file.StoreString(SaveState()); } /// /// Load a JSON string as the current story state. /// /// The JSON string to load. public void LoadState(string jsonState) { runtimeStory.state.LoadJson(jsonState); } /// /// Load the content of a JSON file as the current story state. /// /// The path to the file we will be reading from. public void LoadStateFile(string filePath) { using FileAccess file = FileAccess.Open(filePath, FileAccess.ModeFlags.Read); LoadState(file.GetAsText()); } private void OnContinued() { _ = EmitSignal(SignalName.Continued); } private void OnMadeChoice(Ink.Runtime.Choice choice) { _ = EmitSignal(SignalName.MadeChoice, new InkChoice(choice)); } public override PropertyList _GetPropertyList() { PropertyList properties = base._GetPropertyList() ?? new PropertyList(); properties.Add(new Godot.Collections.Dictionary() { { "name", PropertyName.RawStory }, { "type", Variant.From(Variant.Type.Object) }, { "usage", Variant.From(PropertyUsageFlags.NoEditor) }, }); return properties; } }