diff --git a/addons/GodotInk/Src/InkStory.cs b/addons/GodotInk/Src/InkStory.cs index eba1240..8bf2246 100644 --- a/addons/GodotInk/Src/InkStory.cs +++ b/addons/GodotInk/Src/InkStory.cs @@ -16,683 +16,683 @@ namespace GodotInk; #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; + [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. + // 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; - } + 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; + } } diff --git a/quickStartGuide.cs b/quickStartGuide.cs new file mode 100644 index 0000000..afe7a7e --- /dev/null +++ b/quickStartGuide.cs @@ -0,0 +1,10 @@ +using Godot; +using System; +using GodotInk; + + +public partial class quickStartGuide : Godot.VBoxContainer +{ + [Export] + private InkStory story; +} diff --git a/quickStartGuide.cs.uid b/quickStartGuide.cs.uid new file mode 100644 index 0000000..01f9c84 --- /dev/null +++ b/quickStartGuide.cs.uid @@ -0,0 +1 @@ +uid://he25bgumbumx diff --git a/quick_start_guide.tscn b/quick_start_guide.tscn new file mode 100644 index 0000000..14e2a30 --- /dev/null +++ b/quick_start_guide.tscn @@ -0,0 +1,15 @@ +[gd_scene format=3 uid="uid://dcnqfwqs74v6f"] + +[ext_resource type="Script" uid="uid://he25bgumbumx" path="res://quickStartGuide.cs" id="1_k86so"] +[ext_resource type="Script" uid="uid://mj1x4xcobhpa" path="res://addons/GodotInk/Src/InkStory.cs" id="2_8d0dk"] + +[sub_resource type="Resource" id="Resource_3qd5q"] +script = ExtResource("2_8d0dk") +metadata/_custom_type_script = "uid://mj1x4xcobhpa" + +[node name="quickStartGuide" type="VBoxContainer" unique_id=281304102] +offset_right = 1152.0 +offset_bottom = 648.0 +script = ExtResource("1_k86so") +story = SubResource("Resource_3qd5q") +metadata/_edit_use_anchors_ = true