├── icon.png ├── addons └── GodotTIE │ ├── GodotTIE_icon.png │ ├── plugin.cfg │ ├── GodotTIE_plugin.gd │ ├── GodotTIE_icon.png.import │ └── text_interface_engine.gd ├── ink-engine-runtime ├── Void.cs ├── INamedContent.cs ├── PushPop.cs ├── Tag.cs ├── DebugMetadata.cs ├── StringJoinExtension.cs ├── StoryException.cs ├── VariableAssignment.cs ├── Glue.cs ├── VariableReference.cs ├── Choice.cs ├── ListDefinitionsOrigin.cs ├── ChoicePoint.cs ├── ListDefinition.cs ├── Divert.cs ├── ControlCommand.cs ├── Path.cs ├── Object.cs ├── SimpleJson.cs ├── Container.cs ├── Value.cs ├── VariablesState.cs ├── CallStack.cs ├── NativeFunctionCall.cs ├── InkList.cs └── JsonSerialisation.cs ├── .gitignore ├── icon.png.import ├── ink-scripts ├── Monsieur.ink └── Monsieur.ink.json ├── project.godot ├── README.md ├── Properties └── AssemblyInfo.cs ├── ink-godot-example.sln ├── Main.gd ├── StoryNode.cs ├── default_env.tres ├── Main.tscn └── ink-godot-example.csproj /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StStep/ink-godot-example/HEAD/icon.png -------------------------------------------------------------------------------- /addons/GodotTIE/GodotTIE_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StStep/ink-godot-example/HEAD/addons/GodotTIE/GodotTIE_icon.png -------------------------------------------------------------------------------- /ink-engine-runtime/Void.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | internal class Void : Runtime.Object 4 | { 5 | public Void () 6 | { 7 | } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /ink-engine-runtime/INamedContent.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Runtime 3 | { 4 | internal interface INamedContent 5 | { 6 | string name { get; } 7 | bool hasValidName { get; } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /addons/GodotTIE/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GodotTIE" 4 | description="Adds a Text Interface Engine node to Godot." 5 | author="Henrique Alves" 6 | version="1.1" 7 | script="GodotTIE_plugin.gd" 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | *.swo 4 | .fscache 5 | .import 6 | .DS_Store 7 | .DS_Store? 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | ehthumbs.db 12 | Thumbs.db 13 | *.lnk 14 | .mono 15 | *.vs 16 | *.user 17 | -------------------------------------------------------------------------------- /ink-engine-runtime/PushPop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Runtime 5 | { 6 | internal enum PushPopType 7 | { 8 | Tunnel, 9 | Function 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /ink-engine-runtime/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink.Runtime 4 | { 5 | internal class Tag : Runtime.Object 6 | { 7 | public string text { get; private set; } 8 | 9 | public Tag (string tagText) 10 | { 11 | this.text = tagText; 12 | } 13 | 14 | public override string ToString () 15 | { 16 | return "# " + text; 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /addons/GodotTIE/GodotTIE_plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | # When this plugin node enters tree, add the custom type 6 | 7 | add_custom_type("TextInterfaceEngine","ReferenceRect",preload("res://addons/GodotTIE/text_interface_engine.gd"),preload("res://addons/GodotTIE/GodotTIE_icon.png")) 8 | 9 | func _exit_tree(): 10 | # When the plugin node exits the tree, remove the custom type 11 | 12 | remove_custom_type("TextInterfaceEngine") 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | 7 | [params] 8 | 9 | compress/mode=0 10 | compress/lossy_quality=0.7 11 | compress/hdr_mode=0 12 | compress/normal_map=0 13 | flags/repeat=0 14 | flags/filter=true 15 | flags/mipmaps=false 16 | flags/anisotropic=false 17 | flags/srgb=2 18 | process/fix_alpha_border=true 19 | process/premult_alpha=false 20 | process/HDR_as_SRGB=false 21 | stream=false 22 | size_limit=0 23 | detect_3d=true 24 | svg/scale=1.0 25 | -------------------------------------------------------------------------------- /addons/GodotTIE/GodotTIE_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/GodotTIE_icon.png-06c5755e573090fa68219dcd4c5b75cc.stex" 6 | 7 | [params] 8 | 9 | compress/mode=0 10 | compress/lossy_quality=0.7 11 | compress/hdr_mode=0 12 | compress/normal_map=0 13 | flags/repeat=0 14 | flags/filter=true 15 | flags/mipmaps=false 16 | flags/anisotropic=false 17 | flags/srgb=2 18 | process/fix_alpha_border=true 19 | process/premult_alpha=false 20 | process/HDR_as_SRGB=false 21 | stream=false 22 | size_limit=0 23 | detect_3d=true 24 | svg/scale=1.0 25 | -------------------------------------------------------------------------------- /ink-engine-runtime/DebugMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | internal class DebugMetadata 4 | { 5 | internal int startLineNumber = 0; 6 | internal int endLineNumber = 0; 7 | internal string fileName = null; 8 | internal string sourceName = null; 9 | 10 | public DebugMetadata () 11 | { 12 | } 13 | 14 | public override string ToString () 15 | { 16 | if (fileName != null) { 17 | return string.Format ("line {0} of {1}", startLineNumber, fileName); 18 | } else { 19 | return "line " + startLineNumber; 20 | } 21 | 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /ink-engine-runtime/StringJoinExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Ink.Runtime 6 | { 7 | internal static class StringExt 8 | { 9 | public static string Join(string separator, List objects) 10 | { 11 | var sb = new StringBuilder (); 12 | 13 | var isFirst = true; 14 | foreach (var o in objects) { 15 | 16 | if (!isFirst) 17 | sb.Append (separator); 18 | 19 | sb.Append (o.ToString ()); 20 | 21 | isFirst = false; 22 | } 23 | 24 | return sb.ToString (); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /ink-scripts/Monsieur.ink: -------------------------------------------------------------------------------- 1 | - I looked at Monsieur Fogg 2 | * ... and I could contain myself no longer. 3 | 'What is the purpose of our journey, Monsieur?' 4 | 'A wager,' he replied. 5 | * * 'A wager!'[] I returned. 6 | He nodded. 7 | * * * 'But surely that is foolishness!' 8 | * * * 'A most serious matter then!' 9 | - - - He nodded again. 10 | * * * 'But can we win?' 11 | 'That is what we will endeavour to find out,' he answered. 12 | * * * 'A modest wager, I trust?' 13 | 'Twenty thousand pounds,' he replied, quite flatly. 14 | * * * I asked nothing further of him then[.], and after a final, polite cough, he offered nothing more to me. <> 15 | * * 'Ah[.'],' I replied, uncertain what I thought. 16 | - - After that, <> 17 | * ... but I said nothing[] and <> 18 | - we passed the day in silence. 19 | - -> END -------------------------------------------------------------------------------- /ink-engine-runtime/StoryException.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | /// 4 | /// Exception that represents an error when running a Story at runtime. 5 | /// An exception being thrown of this type is typically when there's 6 | /// a bug in your ink, rather than in the ink engine itself! 7 | /// 8 | public class StoryException : System.Exception 9 | { 10 | internal bool useEndLineNumber; 11 | 12 | /// 13 | /// Constructs a default instance of a StoryException without a message. 14 | /// 15 | public StoryException () { } 16 | 17 | /// 18 | /// Constructs an instance of a StoryException with a message. 19 | /// 20 | /// The error message. 21 | public StoryException(string message) : base(message) {} 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=3 10 | 11 | [application] 12 | 13 | config/name="ink-godot-example" 14 | run/main_scene="res://Main.tscn" 15 | config/icon="res://icon.png" 16 | 17 | [editor_plugins] 18 | 19 | enabled=PoolStringArray( "GodotTIE" ) 20 | 21 | [gdnative] 22 | 23 | singletons=[ ] 24 | 25 | [input] 26 | 27 | ui_back=[ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777220,"unicode":0,"echo":false,"script":null) 28 | ] 29 | 30 | [rendering] 31 | 32 | environment/default_environment="res://default_env.tres" 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ink Godot 3.0 Example Integration 2 | 3 | This project is an example on how Godot 3.0 Alpha 2, with mono runtime integration, can be used with the Inkle/Ink storytelling scripts. 4 | 5 | # Requirements 6 | 7 | * [Godot 3.0 Alpha 2](https://godotengine.org/article/dev-snapshot-godot-3-0-alpha-2) 8 | 9 | ## Integration Thoughts 10 | 11 | While I originally intended to simply use the ink-runtime-engine.dll, I ran into some issues importing it within Godot. 12 | I was able to access it with Reflection, but the visual studio 'Add-Reference' option was leading to missing 'System.Runtime' assembly references. 13 | After failing to solve this issue, I resorted to including the Ink runtime code in the project as source. 14 | 15 | # References 16 | 17 | * [Ink](https://www.inklestudios.com/ink/) - Provides and parses the JSON 18 | * [GodotTIE](https://github.com/henriquelalves/GodotTIE/tree/master/addons/GodotTIE) - Fun text display addon, just for visuals 19 | -------------------------------------------------------------------------------- /ink-engine-runtime/VariableAssignment.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Ink.Runtime 4 | { 5 | // The value to be assigned is popped off the evaluation stack, so no need to keep it here 6 | internal class VariableAssignment : Runtime.Object 7 | { 8 | public string variableName { get; protected set; } 9 | public bool isNewDeclaration { get; protected set; } 10 | public bool isGlobal { get; set; } 11 | 12 | public VariableAssignment (string variableName, bool isNewDeclaration) 13 | { 14 | this.variableName = variableName; 15 | this.isNewDeclaration = isNewDeclaration; 16 | } 17 | 18 | // Require default constructor for serialisation 19 | public VariableAssignment() : this(null, false) {} 20 | 21 | public override string ToString () 22 | { 23 | return "VarAssign to " + variableName; 24 | } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | // Information about this assembly is defined by the following attributes. 4 | // Change them to the values specific to your project. 5 | 6 | [assembly: AssemblyTitle("ink-godot-example")] 7 | [assembly: AssemblyDescription("")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("")] 11 | [assembly: AssemblyCopyright("")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 16 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 17 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 18 | 19 | [assembly: AssemblyVersion("1.0.*")] 20 | 21 | // The following attributes are used to specify the signing key for the assembly, 22 | // if desired. See the Mono documentation for more information about signing. 23 | 24 | //[assembly: AssemblyDelaySign(false)] 25 | //[assembly: AssemblyKeyFile("")] 26 | -------------------------------------------------------------------------------- /ink-godot-example.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2012 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ink-godot-example", "ink-godot-example.csproj", "{DB17E893-FB58-4A96-A764-EEFE4CBE92DF}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | Tools|Any CPU = Tools|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Release|Any CPU.Build.0 = Release|Any CPU 16 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Tools|Any CPU.ActiveCfg = Tools|Any CPU 17 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF}.Tools|Any CPU.Build.0 = Tools|Any CPU 18 | EndGlobalSection 19 | EndGlobal 20 | -------------------------------------------------------------------------------- /Main.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | onready var tie = get_node("VBoxContainer/MarginContainer/Panel/TextInterfaceEngine") 4 | onready var story = get_node('StoryNode') 5 | 6 | func _ready(): 7 | pass 8 | 9 | func _on_Button_pressed(): 10 | print("Starting text...") 11 | story.Reset() 12 | tie.reset() 13 | tie.set_color(Color(1,1,1)) 14 | tie.set_state(tie.STATE_OUTPUT) 15 | 16 | func _on_input_enter(s): 17 | print("Input Enter ",s) 18 | 19 | if story.Choose(s.to_int() - 1): 20 | tie.add_newline() 21 | tie.add_newline() 22 | else: 23 | tie.add_newline() 24 | 25 | func _on_buff_end(): 26 | if story.CanContinue(): 27 | tie.buff_text(story.Continue(), 0.01) 28 | tie.set_state(tie.STATE_OUTPUT) 29 | elif story.CanChoose(): 30 | var ch = story.GetChoices() 31 | var i = 1 32 | for c in ch: 33 | tie.buff_text("Choice %d: %s\n" % [i, c], 0.01) 34 | i+=1 35 | tie.set_state(tie.STATE_OUTPUT) 36 | tie.buff_input() 37 | else: 38 | pass 39 | 40 | func _on_enter_break(): 41 | print("Enter Break") 42 | 43 | func _on_resume_break(): 44 | print("Resume Break") 45 | 46 | func _on_state_change(i): 47 | print("New state: ", i) 48 | 49 | func _on_tag_buff(s): 50 | print("Tag Buff ",s) 51 | -------------------------------------------------------------------------------- /ink-engine-runtime/Glue.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | internal enum GlueType 4 | { 5 | Bidirectional, 6 | Left, 7 | Right 8 | } 9 | 10 | internal class Glue : Runtime.Object 11 | { 12 | public GlueType glueType { get; set; } 13 | 14 | public bool isLeft { 15 | get { 16 | return glueType == GlueType.Left; 17 | } 18 | } 19 | 20 | public bool isBi { 21 | get { 22 | return glueType == GlueType.Bidirectional; 23 | } 24 | } 25 | 26 | public bool isRight { 27 | get { 28 | return glueType == GlueType.Right; 29 | } 30 | } 31 | 32 | public Glue(GlueType type) { 33 | glueType = type; 34 | } 35 | 36 | public override string ToString () 37 | { 38 | switch (glueType) { 39 | case GlueType.Bidirectional: return "BidirGlue"; 40 | case GlueType.Left: return "LeftGlue"; 41 | case GlueType.Right: return "RightGlue"; 42 | } 43 | 44 | return "UnexpectedGlueType"; 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /StoryNode.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | using System.Collections.Generic; 4 | using Ink.Runtime; 5 | 6 | public class StoryNode : Node 7 | { 8 | private String input_path = "ink-scripts/Monsieur.ink.json"; 9 | 10 | private Story _inkStory = null; 11 | 12 | public override void _Ready() 13 | { 14 | String text = System.IO.File.ReadAllText(input_path); 15 | _inkStory = new Story(text); 16 | } 17 | 18 | public void Reset() 19 | { 20 | _inkStory.ResetState(); 21 | } 22 | 23 | public bool CanContinue() 24 | { 25 | return _inkStory.canContinue; 26 | } 27 | 28 | public String Continue() 29 | { 30 | return _inkStory.Continue(); 31 | } 32 | 33 | public bool CanChoose() 34 | { 35 | return (_inkStory.currentChoices.Count > 0); 36 | } 37 | 38 | public bool Choose(int i) 39 | { 40 | if ( i >= _inkStory.currentChoices.Count || i < 0) 41 | return false; 42 | 43 | _inkStory.ChooseChoiceIndex(i); 44 | return true; 45 | } 46 | 47 | public String[] GetChoices() 48 | { 49 | var ret = new String[_inkStory.currentChoices.Count]; 50 | for (int i = 0; i < _inkStory.currentChoices.Count; ++i) { 51 | Choice choice = _inkStory.currentChoices [i]; 52 | ret[i] = choice.text; 53 | } 54 | 55 | return ret; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ink-engine-runtime/VariableReference.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | internal class VariableReference : Runtime.Object 4 | { 5 | // Normal named variable 6 | public string name { get; set; } 7 | 8 | // Variable reference is actually a path for a visit (read) count 9 | public Path pathForCount { get; set; } 10 | 11 | internal Container containerForCount { 12 | get { 13 | return this.ResolvePath (pathForCount) as Container; 14 | } 15 | } 16 | 17 | public string pathStringForCount { 18 | get { 19 | if( pathForCount == null ) 20 | return null; 21 | 22 | return CompactPathString(pathForCount); 23 | } 24 | set { 25 | if (value == null) 26 | pathForCount = null; 27 | else 28 | pathForCount = new Path (value); 29 | } 30 | } 31 | 32 | public VariableReference (string name) 33 | { 34 | this.name = name; 35 | } 36 | 37 | // Require default constructor for serialisation 38 | public VariableReference() {} 39 | 40 | public override string ToString () 41 | { 42 | if (name != null) { 43 | return string.Format ("var({0})", name); 44 | } else { 45 | var pathStr = pathStringForCount; 46 | return string.Format("read_count({0})", pathStr); 47 | } 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ink-engine-runtime/Choice.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Runtime 3 | { 4 | /// 5 | /// A generated Choice from the story. 6 | /// A single ChoicePoint in the Story could potentially generate 7 | /// different Choices dynamically dependent on state, so they're 8 | /// separated. 9 | /// 10 | public class Choice : Runtime.Object 11 | { 12 | /// 13 | /// The main text to presented to the player for this Choice. 14 | /// 15 | public string text { get; set; } 16 | 17 | /// 18 | /// The target path that the Story should be diverted to if 19 | /// this Choice is chosen. 20 | /// 21 | public string pathStringOnChoice { get { return choicePoint.pathStringOnChoice; } } 22 | 23 | /// 24 | /// Get the path to the original choice point - where was this choice defined in the story? 25 | /// 26 | /// A dot separated path into the story data. 27 | public string sourcePath { 28 | get { 29 | return choicePoint.path.componentsString; 30 | } 31 | } 32 | 33 | /// 34 | /// The original index into currentChoices list on the Story when 35 | /// this Choice was generated, for convenience. 36 | /// 37 | public int index { get; set; } 38 | 39 | internal ChoicePoint choicePoint { get; set; } 40 | internal CallStack.Thread threadAtGeneration { get; set; } 41 | internal int originalThreadIndex; 42 | 43 | // Only used temporarily for loading/saving from JSON 44 | internal string originalChoicePath; 45 | 46 | 47 | internal Choice() 48 | { 49 | } 50 | 51 | internal Choice (ChoicePoint choice) 52 | { 53 | this.choicePoint = choice; 54 | } 55 | 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /ink-engine-runtime/ListDefinitionsOrigin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | internal class ListDefinitionsOrigin 6 | { 7 | public List lists { 8 | get { 9 | var listOfLists = new List (); 10 | foreach (var namedList in _lists) { 11 | listOfLists.Add (namedList.Value); 12 | } 13 | return listOfLists; 14 | } 15 | } 16 | 17 | public ListDefinitionsOrigin (List lists) 18 | { 19 | _lists = new Dictionary (); 20 | foreach (var list in lists) { 21 | _lists [list.name] = list; 22 | } 23 | } 24 | 25 | public bool TryGetDefinition (string name, out ListDefinition def) 26 | { 27 | return _lists.TryGetValue (name, out def); 28 | } 29 | 30 | public ListValue FindSingleItemListWithName (string name) 31 | { 32 | InkListItem item = InkListItem.Null; 33 | ListDefinition list = null; 34 | 35 | // Name could be in the form itemName or listName.itemName 36 | var nameParts = name.Split ('.'); 37 | if (nameParts.Length == 2) { 38 | item = new InkListItem (nameParts [0], nameParts [1]); 39 | TryGetDefinition (item.originName, out list); 40 | } else { 41 | foreach (var namedList in _lists) { 42 | var listWithItem = namedList.Value; 43 | item = new InkListItem (namedList.Key, name); 44 | if (listWithItem.ContainsItem (item)) { 45 | list = listWithItem; 46 | break; 47 | } 48 | } 49 | } 50 | 51 | // Manager to get the list that contains the given item? 52 | if (list != null) { 53 | int itemValue = list.ValueForItem (item); 54 | return new ListValue (item, itemValue); 55 | } 56 | 57 | return null; 58 | } 59 | 60 | Dictionary _lists; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | radiance_size = 4 6 | sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) 7 | sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) 8 | sky_curve = 0.25 9 | sky_energy = 1.0 10 | ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) 11 | ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) 12 | ground_curve = 0.01 13 | ground_energy = 1.0 14 | sun_color = Color( 1, 1, 1, 1 ) 15 | sun_latitude = 35.0 16 | sun_longitude = 0.0 17 | sun_angle_min = 1.0 18 | sun_angle_max = 100.0 19 | sun_curve = 0.05 20 | sun_energy = 16.0 21 | texture_size = 2 22 | 23 | [resource] 24 | 25 | background_mode = 2 26 | background_sky = SubResource( 1 ) 27 | background_sky_custom_fov = 0.0 28 | background_color = Color( 0, 0, 0, 1 ) 29 | background_energy = 1.0 30 | background_canvas_max_layer = 0 31 | ambient_light_color = Color( 0, 0, 0, 1 ) 32 | ambient_light_energy = 1.0 33 | ambient_light_sky_contribution = 1.0 34 | fog_enabled = false 35 | fog_color = Color( 0.5, 0.6, 0.7, 1 ) 36 | fog_sun_color = Color( 1, 0.9, 0.7, 1 ) 37 | fog_sun_amount = 0.0 38 | fog_depth_enabled = true 39 | fog_depth_begin = 10.0 40 | fog_depth_curve = 1.0 41 | fog_transmit_enabled = false 42 | fog_transmit_curve = 1.0 43 | fog_height_enabled = false 44 | fog_height_min = 0.0 45 | fog_height_max = 100.0 46 | fog_height_curve = 1.0 47 | tonemap_mode = 0 48 | tonemap_exposure = 1.0 49 | tonemap_white = 1.0 50 | auto_exposure_enabled = false 51 | auto_exposure_scale = 0.4 52 | auto_exposure_min_luma = 0.05 53 | auto_exposure_max_luma = 8.0 54 | auto_exposure_speed = 0.5 55 | ss_reflections_enabled = false 56 | ss_reflections_max_steps = 64 57 | ss_reflections_fade_in = 0.15 58 | ss_reflections_fade_out = 2.0 59 | ss_reflections_depth_tolerance = 0.2 60 | ss_reflections_roughness = true 61 | ssao_enabled = false 62 | ssao_radius = 1.0 63 | ssao_intensity = 1.0 64 | ssao_radius2 = 0.0 65 | ssao_intensity2 = 1.0 66 | ssao_bias = 0.01 67 | ssao_light_affect = 0.0 68 | ssao_color = Color( 0, 0, 0, 1 ) 69 | ssao_quality = 0 70 | ssao_blur = 3 71 | ssao_edge_sharpness = 4.0 72 | dof_blur_far_enabled = false 73 | dof_blur_far_distance = 10.0 74 | dof_blur_far_transition = 5.0 75 | dof_blur_far_amount = 0.1 76 | dof_blur_far_quality = 1 77 | dof_blur_near_enabled = false 78 | dof_blur_near_distance = 2.0 79 | dof_blur_near_transition = 1.0 80 | dof_blur_near_amount = 0.1 81 | dof_blur_near_quality = 1 82 | glow_enabled = false 83 | glow_levels/1 = false 84 | glow_levels/2 = false 85 | glow_levels/3 = true 86 | glow_levels/4 = false 87 | glow_levels/5 = true 88 | glow_levels/6 = false 89 | glow_levels/7 = false 90 | glow_intensity = 0.8 91 | glow_strength = 1.0 92 | glow_bloom = 0.0 93 | glow_blend_mode = 2 94 | glow_hdr_threshold = 1.0 95 | glow_hdr_scale = 2.0 96 | glow_bicubic_upscale = false 97 | adjustment_enabled = false 98 | adjustment_brightness = 1.0 99 | adjustment_contrast = 1.0 100 | adjustment_saturation = 1.0 101 | 102 | -------------------------------------------------------------------------------- /ink-engine-runtime/ChoicePoint.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Ink.Runtime 4 | { 5 | /// 6 | /// The ChoicePoint represents the point within the Story where 7 | /// a Choice instance gets generated. The distinction is made 8 | /// because the text of the Choice can be dynamically generated. 9 | /// 10 | internal class ChoicePoint : Runtime.Object 11 | { 12 | internal Path pathOnChoice { 13 | get { 14 | // Resolve any relative paths to global ones as we come across them 15 | if (_pathOnChoice != null && _pathOnChoice.isRelative) { 16 | var choiceTargetObj = choiceTarget; 17 | if (choiceTargetObj) { 18 | _pathOnChoice = choiceTargetObj.path; 19 | } 20 | } 21 | return _pathOnChoice; 22 | } 23 | set { 24 | _pathOnChoice = value; 25 | } 26 | } 27 | Path _pathOnChoice; 28 | 29 | internal Container choiceTarget { 30 | get { 31 | return this.ResolvePath (_pathOnChoice) as Container; 32 | } 33 | } 34 | 35 | internal string pathStringOnChoice { 36 | get { 37 | return CompactPathString (pathOnChoice); 38 | } 39 | set { 40 | pathOnChoice = new Path (value); 41 | } 42 | } 43 | 44 | internal bool hasCondition { get; set; } 45 | internal bool hasStartContent { get; set; } 46 | internal bool hasChoiceOnlyContent { get; set; } 47 | internal bool onceOnly { get; set; } 48 | internal bool isInvisibleDefault { get; set; } 49 | 50 | internal int flags { 51 | get { 52 | int flags = 0; 53 | if (hasCondition) flags |= 1; 54 | if (hasStartContent) flags |= 2; 55 | if (hasChoiceOnlyContent) flags |= 4; 56 | if (isInvisibleDefault) flags |= 8; 57 | if (onceOnly) flags |= 16; 58 | return flags; 59 | } 60 | set { 61 | hasCondition = (value & 1) > 0; 62 | hasStartContent = (value & 2) > 0; 63 | hasChoiceOnlyContent = (value & 4) > 0; 64 | isInvisibleDefault = (value & 8) > 0; 65 | onceOnly = (value & 16) > 0; 66 | } 67 | } 68 | 69 | internal ChoicePoint (bool onceOnly) 70 | { 71 | this.onceOnly = onceOnly; 72 | } 73 | 74 | public ChoicePoint() : this(true) {} 75 | 76 | public override string ToString () 77 | { 78 | int? targetLineNum = DebugLineNumberOfPath (pathOnChoice); 79 | string targetString = pathOnChoice.ToString (); 80 | 81 | if (targetLineNum != null) { 82 | targetString = " line " + targetLineNum; 83 | } 84 | 85 | return "Choice: -> " + targetString; 86 | } 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /ink-engine-runtime/ListDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | internal class ListDefinition 6 | { 7 | public string name { get { return _name; } } 8 | 9 | public Dictionary items { 10 | get { 11 | if (_items == null) { 12 | _items = new Dictionary (); 13 | foreach (var itemNameAndValue in _itemNameToValues) { 14 | var item = new InkListItem (name, itemNameAndValue.Key); 15 | _items [item] = itemNameAndValue.Value; 16 | } 17 | } 18 | return _items; 19 | } 20 | } 21 | Dictionary _items; 22 | 23 | public int ValueForItem (InkListItem item) 24 | { 25 | int intVal; 26 | if (_itemNameToValues.TryGetValue (item.itemName, out intVal)) 27 | return intVal; 28 | else 29 | return 0; 30 | } 31 | 32 | public bool ContainsItem (InkListItem item) 33 | { 34 | if (item.originName != name) return false; 35 | 36 | return _itemNameToValues.ContainsKey (item.itemName); 37 | } 38 | 39 | public bool ContainsItemWithName (string itemName) 40 | { 41 | return _itemNameToValues.ContainsKey (itemName); 42 | } 43 | 44 | public bool TryGetItemWithValue (int val, out InkListItem item) 45 | { 46 | foreach (var namedItem in _itemNameToValues) { 47 | if (namedItem.Value == val) { 48 | item = new InkListItem (name, namedItem.Key); 49 | return true; 50 | } 51 | } 52 | 53 | item = InkListItem.Null; 54 | return false; 55 | } 56 | 57 | public bool TryGetValueForItem (InkListItem item, out int intVal) 58 | { 59 | return _itemNameToValues.TryGetValue (item.itemName, out intVal); 60 | } 61 | 62 | public ListValue ListRange (int min, int max) 63 | { 64 | var rawList = new InkList (); 65 | foreach (var nameAndValue in _itemNameToValues) { 66 | if (nameAndValue.Value >= min && nameAndValue.Value <= max) { 67 | var item = new InkListItem (name, nameAndValue.Key); 68 | rawList [item] = nameAndValue.Value; 69 | } 70 | } 71 | return new ListValue(rawList); 72 | } 73 | 74 | public ListDefinition (string name, Dictionary items) 75 | { 76 | _name = name; 77 | _itemNameToValues = items; 78 | } 79 | 80 | string _name; 81 | 82 | // The main representation should be simple item names rather than a RawListItem, 83 | // since we mainly want to access items based on their simple name, since that's 84 | // how they'll be most commonly requested from ink. 85 | Dictionary _itemNameToValues; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ink-scripts/Monsieur.ink.json: -------------------------------------------------------------------------------- 1 | {"inkVersion":17,"root":[[["^I looked at Monsieur Fogg","\n",["ev",{"^->":"0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^... and I could contain myself no longer.",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"\n","\n","^'What is the purpose of our journey, Monsieur?'","\n","^'A wager,' he replied.","\n",[["ev",{"^->":"0.g-0.2.c.12.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^'A wager!'",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"^ I returned.","\n","\n","^He nodded.","\n",[["ev",{"^->":"0.g-0.2.c.12.0.c.11.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^'But surely that is foolishness!'",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.11.0.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"\n","\n",{"->":".^.^.^.g-0"},{"#f":7}]}],["ev",{"^->":"0.g-0.2.c.12.0.c.11.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^'A most serious matter then!'",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.11.1.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"\n","\n",{"->":".^.^.^.g-0"},{"#f":7}]}],{"g-0":["^He nodded again.","\n",["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^'But can we win?'",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.2.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"\n","\n","^'That is what we will endeavour to find out,' he answered.","\n",{"->":"0.g-0.2.c.12.g-0"},{"#f":7}]}],["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^'A modest wager, I trust?'",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.3.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"\n","\n","^'Twenty thousand pounds,' he replied, quite flatly.","\n",{"->":"0.g-0.2.c.12.g-0"},{"#f":7}]}],["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.","/str","/ev",{"*":".^.c","flg":22},{"s":["^I asked nothing further of him then",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.0.c.11.g-0.4.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"^, and after a final, polite cough, he offered nothing more to me. ","<>","\n","\n",{"->":"0.g-0.2.c.12.g-0"},{"#f":7}]}],{"#f":7}]}],{"#f":7}]}],["ev",{"^->":"0.g-0.2.c.12.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.'","/str","/ev",{"*":".^.c","flg":22},{"s":["^'Ah",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.2.c.12.1.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"^,' I replied, uncertain what I thought.","\n","\n",{"->":".^.^.^.g-0"},{"#f":7}]}],{"g-0":["^After that, ","<>","\n",{"->":"0.g-1"},{"#f":7}]}],{"#f":7}]}],["ev",{"^->":"0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.c","flg":18},{"s":["^... but I said nothing",{"->":"$r","var":true},null],"c":["ev",{"^->":"0.g-0.3.c.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.s"},[{"#n":"$r2"}],"^ and ","<>","\n","\n",{"->":"0.g-1"},{"#f":7}]}],{"#f":7,"#n":"g-0"}],{"g-1":["^we passed the day in silence.","\n",["end",{"#f":7,"#n":"g-2"}],{"#f":7}]}],"done",{"#f":3}],"listDefs":{}} -------------------------------------------------------------------------------- /Main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=2] 2 | 3 | [ext_resource path="res://Main.gd" type="Script" id=1] 4 | [ext_resource path="res://addons/GodotTIE/text_interface_engine.gd" type="Script" id=2] 5 | [ext_resource path="res://addons/GodotTIE/GodotTIE_icon.png" type="Texture" id=3] 6 | [ext_resource path="res://StoryNode.cs" type="Script" id=4] 7 | 8 | [node name="Main" type="Node"] 9 | 10 | script = ExtResource( 1 ) 11 | 12 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 13 | 14 | anchor_left = 0.0 15 | anchor_top = 0.0 16 | anchor_right = 1.0 17 | anchor_bottom = 1.0 18 | rect_pivot_offset = Vector2( 0, 0 ) 19 | rect_clip_content = false 20 | mouse_filter = 1 21 | size_flags_horizontal = 1 22 | size_flags_vertical = 1 23 | alignment = 0 24 | 25 | [node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"] 26 | 27 | anchor_left = 0.0 28 | anchor_top = 0.0 29 | anchor_right = 0.0 30 | anchor_bottom = 0.0 31 | margin_right = 1024.0 32 | margin_bottom = 576.0 33 | rect_pivot_offset = Vector2( 0, 0 ) 34 | rect_clip_content = false 35 | mouse_filter = 0 36 | size_flags_horizontal = 3 37 | size_flags_vertical = 3 38 | custom_constants/margin_right = 10 39 | custom_constants/margin_top = 10 40 | custom_constants/margin_left = 10 41 | _sections_unfolded = [ "Size Flags", "custom_constants" ] 42 | 43 | [node name="Panel" type="Panel" parent="VBoxContainer/MarginContainer"] 44 | 45 | anchor_left = 0.0 46 | anchor_top = 0.0 47 | anchor_right = 0.0 48 | anchor_bottom = 0.0 49 | margin_left = 10.0 50 | margin_top = 10.0 51 | margin_right = 1014.0 52 | margin_bottom = 576.0 53 | rect_min_size = Vector2( 200, 200 ) 54 | rect_pivot_offset = Vector2( 0, 0 ) 55 | rect_clip_content = false 56 | mouse_filter = 0 57 | size_flags_horizontal = 3 58 | size_flags_vertical = 3 59 | _sections_unfolded = [ "Grow Direction", "Rect", "Size Flags" ] 60 | 61 | [node name="TextInterfaceEngine" type="ReferenceRect" parent="VBoxContainer/MarginContainer/Panel"] 62 | 63 | anchor_left = 0.0 64 | anchor_top = 0.0 65 | anchor_right = 1.0 66 | anchor_bottom = 1.0 67 | rect_pivot_offset = Vector2( 0, 0 ) 68 | rect_clip_content = false 69 | mouse_filter = 0 70 | size_flags_horizontal = 1 71 | size_flags_vertical = 1 72 | script = ExtResource( 2 ) 73 | __meta__ = { 74 | "_edit_lock_": true, 75 | "_editor_icon": ExtResource( 3 ) 76 | } 77 | SCROLL_ON_MAX_LINES = true 78 | BREAK_ON_MAX_LINES = false 79 | AUTO_SKIP_WORDS = true 80 | LOG_SKIPPED_LINES = true 81 | SCROLL_SKIPPED_LINES = false 82 | FONT = null 83 | PRINT_INPUT = true 84 | BLINKING_INPUT = true 85 | INPUT_CHARACTERS_LIMIT = -1 86 | 87 | [node name="Button" type="Button" parent="VBoxContainer"] 88 | 89 | anchor_left = 0.0 90 | anchor_top = 0.0 91 | anchor_right = 0.0 92 | anchor_bottom = 0.0 93 | margin_left = 491.0 94 | margin_top = 580.0 95 | margin_right = 532.0 96 | margin_bottom = 600.0 97 | rect_pivot_offset = Vector2( 0, 0 ) 98 | rect_clip_content = false 99 | mouse_filter = 0 100 | size_flags_horizontal = 4 101 | size_flags_vertical = 1 102 | toggle_mode = false 103 | enabled_focus_mode = 0 104 | shortcut = null 105 | group = null 106 | text = "Start" 107 | flat = false 108 | _sections_unfolded = [ "Size Flags" ] 109 | 110 | [node name="StoryNode" type="Node" parent="."] 111 | 112 | script = ExtResource( 4 ) 113 | 114 | [connection signal="buff_end" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_buff_end"] 115 | 116 | [connection signal="enter_break" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_enter_break"] 117 | 118 | [connection signal="input_enter" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_input_enter"] 119 | 120 | [connection signal="resume_break" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_resume_break"] 121 | 122 | [connection signal="state_change" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_state_change"] 123 | 124 | [connection signal="tag_buff" from="VBoxContainer/MarginContainer/Panel/TextInterfaceEngine" to="." method="_on_tag_buff"] 125 | 126 | [connection signal="pressed" from="VBoxContainer/Button" to="." method="_on_Button_pressed"] 127 | 128 | 129 | -------------------------------------------------------------------------------- /ink-godot-example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {DB17E893-FB58-4A96-A764-EEFE4CBE92DF} 7 | Library 8 | .mono\temp\bin\$(Configuration) 9 | ink-godot-example 10 | ink-godot-example 11 | v4.5 12 | .mono\temp\obj 13 | $(BaseIntermediateOutputPath)\$(Configuration) 14 | 15 | 16 | true 17 | full 18 | false 19 | DEBUG; 20 | prompt 21 | 4 22 | false 23 | 24 | 25 | full 26 | true 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | true 33 | full 34 | false 35 | DEBUG;TOOLS; 36 | prompt 37 | 4 38 | false 39 | 40 | 41 | 42 | $(ProjectDir)\.mono\assemblies\GodotSharp.dll 43 | False 44 | 45 | 46 | $(ProjectDir)\.mono\assemblies\GodotSharpEditor.dll 47 | False 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /ink-engine-runtime/Divert.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Ink.Runtime 4 | { 5 | internal class Divert : Runtime.Object 6 | { 7 | public Path targetPath { 8 | get { 9 | // Resolve any relative paths to global ones as we come across them 10 | if (_targetPath != null && _targetPath.isRelative) { 11 | var targetObj = targetContent; 12 | if (targetObj) { 13 | _targetPath = targetObj.path; 14 | } 15 | } 16 | return _targetPath; 17 | } 18 | set { 19 | _targetPath = value; 20 | _targetContent = null; 21 | } 22 | } 23 | Path _targetPath; 24 | 25 | public Runtime.Object targetContent { 26 | get { 27 | if (_targetContent == null) { 28 | _targetContent = ResolvePath (_targetPath); 29 | } 30 | 31 | return _targetContent; 32 | } 33 | } 34 | Runtime.Object _targetContent; 35 | 36 | public string targetPathString { 37 | get { 38 | if (targetPath == null) 39 | return null; 40 | 41 | return CompactPathString (targetPath); 42 | } 43 | set { 44 | if (value == null) { 45 | targetPath = null; 46 | } else { 47 | targetPath = new Path (value); 48 | } 49 | } 50 | } 51 | 52 | public string variableDivertName { get; set; } 53 | public bool hasVariableTarget { get { return variableDivertName != null; } } 54 | 55 | public bool pushesToStack { get; set; } 56 | public PushPopType stackPushType; 57 | 58 | public bool isExternal { get; set; } 59 | public int externalArgs { get; set; } 60 | 61 | public bool isConditional { get; set; } 62 | 63 | public Divert () 64 | { 65 | pushesToStack = false; 66 | } 67 | 68 | public Divert(PushPopType stackPushType) 69 | { 70 | pushesToStack = true; 71 | this.stackPushType = stackPushType; 72 | } 73 | 74 | public override bool Equals (object obj) 75 | { 76 | var otherDivert = obj as Divert; 77 | if (otherDivert) { 78 | if (this.hasVariableTarget == otherDivert.hasVariableTarget) { 79 | if (this.hasVariableTarget) { 80 | return this.variableDivertName == otherDivert.variableDivertName; 81 | } else { 82 | return this.targetPath.Equals(otherDivert.targetPath); 83 | } 84 | } 85 | } 86 | return false; 87 | } 88 | 89 | public override int GetHashCode () 90 | { 91 | if (hasVariableTarget) { 92 | const int variableTargetSalt = 12345; 93 | return variableDivertName.GetHashCode() + variableTargetSalt; 94 | } else { 95 | const int pathTargetSalt = 54321; 96 | return targetPath.GetHashCode() + pathTargetSalt; 97 | } 98 | } 99 | 100 | public override string ToString () 101 | { 102 | if (hasVariableTarget) { 103 | return "Divert(variable: " + variableDivertName + ")"; 104 | } 105 | else if (targetPath == null) { 106 | return "Divert(null)"; 107 | } else { 108 | 109 | var sb = new StringBuilder (); 110 | 111 | string targetStr = targetPath.ToString (); 112 | int? targetLineNum = DebugLineNumberOfPath (targetPath); 113 | if (targetLineNum != null) { 114 | targetStr = "line " + targetLineNum; 115 | } 116 | 117 | sb.Append ("Divert"); 118 | 119 | if (isConditional) 120 | sb.Append ("?"); 121 | 122 | if (pushesToStack) { 123 | if (stackPushType == PushPopType.Function) { 124 | sb.Append (" function"); 125 | } else { 126 | sb.Append (" tunnel"); 127 | } 128 | } 129 | 130 | sb.Append (" -> "); 131 | sb.Append (targetPathString); 132 | 133 | sb.Append (" ("); 134 | sb.Append (targetStr); 135 | sb.Append (")"); 136 | 137 | return sb.ToString (); 138 | } 139 | } 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /ink-engine-runtime/ControlCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink.Runtime 4 | { 5 | internal class ControlCommand : Runtime.Object 6 | { 7 | public enum CommandType 8 | { 9 | NotSet = -1, 10 | EvalStart, 11 | EvalOutput, 12 | EvalEnd, 13 | Duplicate, 14 | PopEvaluatedValue, 15 | PopFunction, 16 | PopTunnel, 17 | BeginString, 18 | EndString, 19 | NoOp, 20 | ChoiceCount, 21 | TurnsSince, 22 | ReadCount, 23 | Random, 24 | SeedRandom, 25 | VisitIndex, 26 | SequenceShuffleIndex, 27 | StartThread, 28 | Done, 29 | End, 30 | ListFromInt, 31 | ListRange, 32 | //---- 33 | TOTAL_VALUES 34 | } 35 | 36 | public CommandType commandType { get; protected set; } 37 | 38 | public ControlCommand (CommandType commandType) 39 | { 40 | this.commandType = commandType; 41 | } 42 | 43 | // Require default constructor for serialisation 44 | public ControlCommand() : this(CommandType.NotSet) {} 45 | 46 | internal override Object Copy() 47 | { 48 | return new ControlCommand (commandType); 49 | } 50 | 51 | // The following static factory methods are to make generating these objects 52 | // slightly more succinct. Without these, the code gets pretty massive! e.g. 53 | // 54 | // var c = new Runtime.ControlCommand(Runtime.ControlCommand.CommandType.EvalStart) 55 | // 56 | // as opposed to 57 | // 58 | // var c = Runtime.ControlCommand.EvalStart() 59 | 60 | public static ControlCommand EvalStart() { 61 | return new ControlCommand(CommandType.EvalStart); 62 | } 63 | 64 | public static ControlCommand EvalOutput() { 65 | return new ControlCommand(CommandType.EvalOutput); 66 | } 67 | 68 | public static ControlCommand EvalEnd() { 69 | return new ControlCommand(CommandType.EvalEnd); 70 | } 71 | 72 | public static ControlCommand Duplicate() { 73 | return new ControlCommand(CommandType.Duplicate); 74 | } 75 | 76 | public static ControlCommand PopEvaluatedValue() { 77 | return new ControlCommand (CommandType.PopEvaluatedValue); 78 | } 79 | 80 | public static ControlCommand PopFunction() { 81 | return new ControlCommand (CommandType.PopFunction); 82 | } 83 | 84 | public static ControlCommand PopTunnel() { 85 | return new ControlCommand (CommandType.PopTunnel); 86 | } 87 | 88 | public static ControlCommand BeginString() { 89 | return new ControlCommand (CommandType.BeginString); 90 | } 91 | 92 | public static ControlCommand EndString() { 93 | return new ControlCommand (CommandType.EndString); 94 | } 95 | 96 | public static ControlCommand NoOp() { 97 | return new ControlCommand(CommandType.NoOp); 98 | } 99 | 100 | public static ControlCommand ChoiceCount() { 101 | return new ControlCommand(CommandType.ChoiceCount); 102 | } 103 | 104 | public static ControlCommand TurnsSince() { 105 | return new ControlCommand(CommandType.TurnsSince); 106 | } 107 | 108 | public static ControlCommand ReadCount () 109 | { 110 | return new ControlCommand (CommandType.ReadCount); 111 | } 112 | 113 | public static ControlCommand Random () 114 | { 115 | return new ControlCommand (CommandType.Random); 116 | } 117 | 118 | public static ControlCommand SeedRandom () 119 | { 120 | return new ControlCommand (CommandType.SeedRandom); 121 | } 122 | 123 | public static ControlCommand VisitIndex() { 124 | return new ControlCommand(CommandType.VisitIndex); 125 | } 126 | 127 | public static ControlCommand SequenceShuffleIndex() { 128 | return new ControlCommand(CommandType.SequenceShuffleIndex); 129 | } 130 | 131 | public static ControlCommand StartThread() { 132 | return new ControlCommand (CommandType.StartThread); 133 | } 134 | 135 | public static ControlCommand Done() { 136 | return new ControlCommand (CommandType.Done); 137 | } 138 | 139 | public static ControlCommand End() { 140 | return new ControlCommand (CommandType.End); 141 | } 142 | 143 | public static ControlCommand ListFromInt () { 144 | return new ControlCommand (CommandType.ListFromInt); 145 | } 146 | 147 | public static ControlCommand ListRange () 148 | { 149 | return new ControlCommand (CommandType.ListRange); 150 | } 151 | 152 | public override string ToString () 153 | { 154 | return commandType.ToString(); 155 | } 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /ink-engine-runtime/Path.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Diagnostics; 5 | using Ink.Runtime; 6 | 7 | namespace Ink.Runtime 8 | { 9 | internal class Path : IEquatable 10 | { 11 | static string parentId = "^"; 12 | 13 | // Immutable Component 14 | internal class Component : IEquatable 15 | { 16 | public int index { get; private set; } 17 | public string name { get; private set; } 18 | public bool isIndex { get { return index >= 0; } } 19 | public bool isParent { 20 | get { 21 | return name == Path.parentId; 22 | } 23 | } 24 | 25 | public Component(int index) 26 | { 27 | Debug.Assert(index >= 0); 28 | this.index = index; 29 | this.name = null; 30 | } 31 | 32 | public Component(string name) 33 | { 34 | Debug.Assert(name != null && name.Length > 0); 35 | this.name = name; 36 | this.index = -1; 37 | } 38 | 39 | public static Component ToParent() 40 | { 41 | return new Component (parentId); 42 | } 43 | 44 | public override string ToString () 45 | { 46 | if (isIndex) { 47 | return index.ToString (); 48 | } else { 49 | return name; 50 | } 51 | } 52 | 53 | public override bool Equals (object obj) 54 | { 55 | return Equals (obj as Component); 56 | } 57 | 58 | public bool Equals(Component otherComp) 59 | { 60 | if (otherComp != null && otherComp.isIndex == this.isIndex) { 61 | if (isIndex) { 62 | return index == otherComp.index; 63 | } else { 64 | return name == otherComp.name; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | 71 | public override int GetHashCode () 72 | { 73 | if (isIndex) 74 | return this.index; 75 | else 76 | return this.name.GetHashCode (); 77 | } 78 | } 79 | 80 | public List components { get; private set; } 81 | 82 | public bool isRelative { get; private set; } 83 | 84 | public Component head 85 | { 86 | get 87 | { 88 | if (components.Count > 0) { 89 | return components.First (); 90 | } else { 91 | return null; 92 | } 93 | } 94 | } 95 | 96 | public Path tail 97 | { 98 | get 99 | { 100 | if (components.Count >= 2) { 101 | List tailComps = components.GetRange (1, components.Count - 1); 102 | return new Path(tailComps); 103 | } 104 | 105 | else { 106 | return Path.self; 107 | } 108 | 109 | } 110 | } 111 | 112 | public int length { get { return components.Count; } } 113 | 114 | public Component lastComponent 115 | { 116 | get 117 | { 118 | if (components.Count > 0) { 119 | return components.Last (); 120 | } else { 121 | return null; 122 | } 123 | } 124 | } 125 | 126 | public bool containsNamedComponent { 127 | get { 128 | foreach(var comp in components) { 129 | if( !comp.isIndex ) { 130 | return true; 131 | } 132 | } 133 | return false; 134 | } 135 | } 136 | 137 | public Path() 138 | { 139 | components = new List (); 140 | } 141 | 142 | public Path(Component head, Path tail) : this() 143 | { 144 | components.Add (head); 145 | components.AddRange (tail.components); 146 | } 147 | 148 | public Path(IEnumerable components, bool relative = false) : this() 149 | { 150 | this.components.AddRange (components); 151 | this.isRelative = relative; 152 | } 153 | 154 | public Path(string componentsString) : this() 155 | { 156 | this.componentsString = componentsString; 157 | } 158 | 159 | public static Path self { 160 | get { 161 | var path = new Path (); 162 | path.isRelative = true; 163 | return path; 164 | } 165 | } 166 | 167 | public Path PathByAppendingPath(Path pathToAppend) 168 | { 169 | Path p = new Path (); 170 | 171 | int upwardMoves = 0; 172 | for (int i = 0; i < pathToAppend.components.Count; ++i) { 173 | if (pathToAppend.components [i].isParent) { 174 | upwardMoves++; 175 | } else { 176 | break; 177 | } 178 | } 179 | 180 | for (int i = 0; i < this.components.Count - upwardMoves; ++i) { 181 | p.components.Add (this.components [i]); 182 | } 183 | 184 | for(int i=upwardMoves; i 8 | /// Base class for all ink runtime content. 9 | /// 10 | public /* TODO: abstract */ class Object 11 | { 12 | /// 13 | /// Runtime.Objects can be included in the main Story as a hierarchy. 14 | /// Usually parents are Container objects. (TODO: Always?) 15 | /// 16 | /// The parent. 17 | public Runtime.Object parent { get; set; } 18 | 19 | internal Runtime.DebugMetadata debugMetadata { 20 | get { 21 | if (_debugMetadata == null) { 22 | if (parent) { 23 | return parent.debugMetadata; 24 | } 25 | } 26 | 27 | return _debugMetadata; 28 | } 29 | 30 | set { 31 | _debugMetadata = value; 32 | } 33 | } 34 | 35 | // TODO: Come up with some clever solution for not having 36 | // to have debug metadata on the object itself, perhaps 37 | // for serialisation purposes at least. 38 | DebugMetadata _debugMetadata; 39 | 40 | internal int? DebugLineNumberOfPath(Path path) 41 | { 42 | if (path == null) 43 | return null; 44 | 45 | // Try to get a line number from debug metadata 46 | var root = this.rootContentContainer; 47 | if (root) { 48 | Runtime.Object targetContent = null; 49 | 50 | // Sometimes paths can be "invalid" if they're externally defined 51 | // in the game. TODO: Change ContentAtPath to return null, and 52 | // only throw an exception in places that actually care! 53 | try { 54 | targetContent = root.ContentAtPath (path); 55 | } catch { } 56 | 57 | if (targetContent) { 58 | var dm = targetContent.debugMetadata; 59 | if (dm != null) { 60 | return dm.startLineNumber; 61 | } 62 | } 63 | } 64 | 65 | return null; 66 | } 67 | 68 | internal Path path 69 | { 70 | get 71 | { 72 | if (_path == null) { 73 | 74 | if (parent == null) { 75 | _path = new Path (); 76 | } else { 77 | // Maintain a Stack so that the order of the components 78 | // is reversed when they're added to the Path. 79 | // We're iterating up the hierarchy from the leaves/children to the root. 80 | var comps = new Stack (); 81 | 82 | var child = this; 83 | Container container = child.parent as Container; 84 | 85 | while (container) { 86 | 87 | var namedChild = child as INamedContent; 88 | if (namedChild != null && namedChild.hasValidName) { 89 | comps.Push (new Path.Component (namedChild.name)); 90 | } else { 91 | comps.Push (new Path.Component (container.content.IndexOf(child))); 92 | } 93 | 94 | child = container; 95 | container = container.parent as Container; 96 | } 97 | 98 | _path = new Path (comps); 99 | } 100 | 101 | } 102 | 103 | return _path; 104 | } 105 | } 106 | Path _path; 107 | 108 | internal Runtime.Object ResolvePath(Path path) 109 | { 110 | if (path.isRelative) { 111 | 112 | Container nearestContainer = this as Container; 113 | if (!nearestContainer) { 114 | Debug.Assert (this.parent != null, "Can't resolve relative path because we don't have a parent"); 115 | nearestContainer = this.parent as Container; 116 | Debug.Assert (nearestContainer != null, "Expected parent to be a container"); 117 | Debug.Assert (path.components [0].isParent); 118 | path = path.tail; 119 | } 120 | 121 | return nearestContainer.ContentAtPath (path); 122 | } else { 123 | return this.rootContentContainer.ContentAtPath (path); 124 | } 125 | } 126 | 127 | internal Path ConvertPathToRelative(Path globalPath) 128 | { 129 | // 1. Find last shared ancestor 130 | // 2. Drill up using ".." style (actually represented as "^") 131 | // 3. Re-build downward chain from common ancestor 132 | 133 | var ownPath = this.path; 134 | 135 | int minPathLength = Math.Min (globalPath.components.Count, ownPath.components.Count); 136 | int lastSharedPathCompIndex = -1; 137 | 138 | for (int i = 0; i < minPathLength; ++i) { 139 | var ownComp = ownPath.components [i]; 140 | var otherComp = globalPath.components [i]; 141 | 142 | if (ownComp.Equals (otherComp)) { 143 | lastSharedPathCompIndex = i; 144 | } else { 145 | break; 146 | } 147 | } 148 | 149 | // No shared path components, so just use global path 150 | if (lastSharedPathCompIndex == -1) 151 | return globalPath; 152 | 153 | int numUpwardsMoves = (ownPath.components.Count-1) - lastSharedPathCompIndex; 154 | 155 | var newPathComps = new List (); 156 | 157 | for(int up=0; up(ref T obj, T value) where T : Runtime.Object 209 | { 210 | if (obj) 211 | obj.parent = null; 212 | 213 | obj = value; 214 | 215 | if( obj ) 216 | obj.parent = this; 217 | } 218 | 219 | /// Allow implicit conversion to bool so you don't have to do: 220 | /// if( myObj != null ) ... 221 | public static implicit operator bool (Object obj) 222 | { 223 | var isNull = object.ReferenceEquals (obj, null); 224 | return !isNull; 225 | } 226 | 227 | /// Required for implicit bool comparison 228 | public static bool operator ==(Object a, Object b) 229 | { 230 | return object.ReferenceEquals (a, b); 231 | } 232 | 233 | /// Required for implicit bool comparison 234 | public static bool operator !=(Object a, Object b) 235 | { 236 | return !(a == b); 237 | } 238 | 239 | /// Required for implicit bool comparison 240 | public override bool Equals (object obj) 241 | { 242 | return object.ReferenceEquals (obj, this); 243 | } 244 | 245 | /// Required for implicit bool comparison 246 | public override int GetHashCode () 247 | { 248 | return base.GetHashCode (); 249 | } 250 | } 251 | } 252 | 253 | -------------------------------------------------------------------------------- /ink-engine-runtime/SimpleJson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Ink.Runtime 6 | { 7 | /// 8 | /// Simple custom JSON serialisation implementation that takes JSON-able System.Collections that 9 | /// are produced by the ink engine and converts to and from JSON text. 10 | /// 11 | internal static class SimpleJson 12 | { 13 | public static string DictionaryToText (Dictionary rootObject) 14 | { 15 | return new Writer (rootObject).ToString (); 16 | } 17 | 18 | public static Dictionary TextToDictionary (string text) 19 | { 20 | return new Reader (text).ToDictionary (); 21 | } 22 | 23 | class Reader 24 | { 25 | public Reader (string text) 26 | { 27 | _text = text; 28 | _offset = 0; 29 | 30 | SkipWhitespace (); 31 | 32 | _rootObject = ReadObject (); 33 | } 34 | 35 | public Dictionary ToDictionary () 36 | { 37 | return (Dictionary)_rootObject; 38 | } 39 | 40 | bool IsNumberChar (char c) 41 | { 42 | return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+'; 43 | } 44 | 45 | object ReadObject () 46 | { 47 | var currentChar = _text [_offset]; 48 | 49 | if( currentChar == '{' ) 50 | return ReadDictionary (); 51 | 52 | else if (currentChar == '[') 53 | return ReadArray (); 54 | 55 | else if (currentChar == '"') 56 | return ReadString (); 57 | 58 | else if (IsNumberChar(currentChar)) 59 | return ReadNumber (); 60 | 61 | else if (TryRead ("true")) 62 | return true; 63 | 64 | else if (TryRead ("false")) 65 | return false; 66 | 67 | else if (TryRead ("null")) 68 | return null; 69 | 70 | throw new System.Exception ("Unhandled object type in JSON: "+_text.Substring (_offset, 30)); 71 | } 72 | 73 | Dictionary ReadDictionary () 74 | { 75 | var dict = new Dictionary (); 76 | 77 | Expect ("{"); 78 | 79 | SkipWhitespace (); 80 | 81 | // Empty dictionary? 82 | if (TryRead ("}")) 83 | return dict; 84 | 85 | do { 86 | 87 | SkipWhitespace (); 88 | 89 | // Key 90 | var key = ReadString (); 91 | Expect (key != null, "dictionary key"); 92 | 93 | SkipWhitespace (); 94 | 95 | // : 96 | Expect (":"); 97 | 98 | SkipWhitespace (); 99 | 100 | // Value 101 | var val = ReadObject (); 102 | Expect (val != null, "dictionary value"); 103 | 104 | // Add to dictionary 105 | dict [key] = val; 106 | 107 | SkipWhitespace (); 108 | 109 | } while ( TryRead (",") ); 110 | 111 | Expect ("}"); 112 | 113 | return dict; 114 | } 115 | 116 | List ReadArray () 117 | { 118 | var list = new List (); 119 | 120 | Expect ("["); 121 | 122 | SkipWhitespace (); 123 | 124 | // Empty list? 125 | if (TryRead ("]")) 126 | return list; 127 | 128 | do { 129 | 130 | SkipWhitespace (); 131 | 132 | // Value 133 | var val = ReadObject (); 134 | 135 | // Add to array 136 | list.Add (val); 137 | 138 | SkipWhitespace (); 139 | 140 | } while (TryRead (",")); 141 | 142 | Expect ("]"); 143 | 144 | return list; 145 | } 146 | 147 | string ReadString () 148 | { 149 | Expect ("\""); 150 | 151 | var startOffset = _offset; 152 | 153 | for (; _offset < _text.Length; _offset++) { 154 | var c = _text [_offset]; 155 | 156 | // Escaping. Escaped character will be skipped over in next loop. 157 | if (c == '\\') { 158 | _offset++; 159 | } else if( c == '"' ) { 160 | break; 161 | } 162 | } 163 | 164 | Expect ("\""); 165 | 166 | var str = _text.Substring (startOffset, _offset - startOffset - 1); 167 | str = str.Replace ("\\\\", "\\"); 168 | str = str.Replace ("\\\"", "\""); 169 | str = str.Replace ("\\r", ""); 170 | str = str.Replace ("\\n", "\n"); 171 | return str; 172 | } 173 | 174 | object ReadNumber () 175 | { 176 | var startOffset = _offset; 177 | 178 | bool isFloat = false; 179 | for (; _offset < _text.Length; _offset++) { 180 | var c = _text [_offset]; 181 | if (c == '.') isFloat = true; 182 | if (IsNumberChar (c)) 183 | continue; 184 | else 185 | break; 186 | } 187 | 188 | string numStr = _text.Substring (startOffset, _offset - startOffset); 189 | 190 | if (isFloat) { 191 | float f; 192 | if (float.TryParse (numStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) { 193 | return f; 194 | } 195 | } else { 196 | int i; 197 | if (int.TryParse (numStr, out i)) { 198 | return i; 199 | } 200 | } 201 | 202 | throw new System.Exception ("Failed to parse number value"); 203 | } 204 | 205 | bool TryRead (string textToRead) 206 | { 207 | if (_offset + textToRead.Length > _text.Length) 208 | return false; 209 | 210 | for (int i = 0; i < textToRead.Length; i++) { 211 | if (textToRead [i] != _text [_offset + i]) 212 | return false; 213 | } 214 | 215 | _offset += textToRead.Length; 216 | 217 | return true; 218 | } 219 | 220 | void Expect (string expectedStr) 221 | { 222 | if (!TryRead (expectedStr)) 223 | Expect (false, expectedStr); 224 | } 225 | 226 | void Expect (bool condition, string message = null) 227 | { 228 | if (!condition) { 229 | if (message == null) { 230 | message = "Unexpected token"; 231 | } else { 232 | message = "Expected " + message; 233 | } 234 | message += " at offset " + _offset; 235 | 236 | throw new System.Exception (message); 237 | } 238 | } 239 | 240 | void SkipWhitespace () 241 | { 242 | while (_offset < _text.Length) { 243 | var c = _text [_offset]; 244 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r') 245 | _offset++; 246 | else 247 | break; 248 | } 249 | } 250 | 251 | string _text; 252 | int _offset; 253 | 254 | object _rootObject; 255 | } 256 | 257 | class Writer 258 | { 259 | public Writer (object rootObject) 260 | { 261 | _sb = new StringBuilder (); 262 | 263 | WriteObject (rootObject); 264 | } 265 | 266 | void WriteObject (object obj) 267 | { 268 | if (obj is int) { 269 | _sb.Append ((int)obj); 270 | } else if (obj is float) { 271 | string floatStr = ((float)obj).ToString(System.Globalization.CultureInfo.InvariantCulture); 272 | _sb.Append (floatStr); 273 | if (!floatStr.Contains (".")) _sb.Append (".0"); 274 | } else if( obj is bool) { 275 | _sb.Append ((bool)obj == true ? "true" : "false"); 276 | } else if (obj == null) { 277 | _sb.Append ("null"); 278 | } else if (obj is string) { 279 | string str = (string)obj; 280 | 281 | // Escape backslashes, quotes and newlines 282 | str = str.Replace ("\\", "\\\\"); 283 | str = str.Replace ("\"", "\\\""); 284 | str = str.Replace ("\n", "\\n"); 285 | str = str.Replace ("\r", ""); 286 | 287 | _sb.AppendFormat ("\"{0}\"", str); 288 | } else if (obj is Dictionary) { 289 | WriteDictionary ((Dictionary)obj); 290 | } else if (obj is List) { 291 | WriteList ((List)obj); 292 | }else { 293 | throw new System.Exception ("ink's SimpleJson writer doesn't currently support this object: " + obj); 294 | } 295 | } 296 | 297 | void WriteDictionary (Dictionary dict) 298 | { 299 | _sb.Append ("{"); 300 | 301 | bool isFirst = true; 302 | foreach (var keyValue in dict) { 303 | 304 | if (!isFirst) _sb.Append (","); 305 | 306 | _sb.Append ("\""); 307 | _sb.Append (keyValue.Key); 308 | _sb.Append ("\":"); 309 | 310 | WriteObject (keyValue.Value); 311 | 312 | isFirst = false; 313 | } 314 | 315 | _sb.Append ("}"); 316 | } 317 | 318 | void WriteList (List list) 319 | { 320 | _sb.Append ("["); 321 | 322 | bool isFirst = true; 323 | foreach (var obj in list) { 324 | if (!isFirst) _sb.Append (","); 325 | 326 | WriteObject (obj); 327 | 328 | isFirst = false; 329 | } 330 | 331 | _sb.Append ("]"); 332 | } 333 | 334 | public override string ToString () 335 | { 336 | return _sb.ToString (); 337 | } 338 | 339 | 340 | StringBuilder _sb; 341 | } 342 | } 343 | } 344 | 345 | -------------------------------------------------------------------------------- /ink-engine-runtime/Container.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.ComponentModel; 7 | 8 | namespace Ink.Runtime 9 | { 10 | internal class Container : Runtime.Object, INamedContent 11 | { 12 | public string name { get; set; } 13 | 14 | public List content { 15 | get { 16 | return _content; 17 | } 18 | set { 19 | AddContent (value); 20 | } 21 | } 22 | List _content; 23 | 24 | public Dictionary namedContent { get; set; } 25 | 26 | public Dictionary namedOnlyContent { 27 | get { 28 | var namedOnlyContentDict = new Dictionary(); 29 | foreach (var kvPair in namedContent) { 30 | namedOnlyContentDict [kvPair.Key] = (Runtime.Object)kvPair.Value; 31 | } 32 | 33 | foreach (var c in content) { 34 | var named = c as INamedContent; 35 | if (named != null && named.hasValidName) { 36 | namedOnlyContentDict.Remove (named.name); 37 | } 38 | } 39 | 40 | if (namedOnlyContentDict.Count == 0) 41 | namedOnlyContentDict = null; 42 | 43 | return namedOnlyContentDict; 44 | } 45 | set { 46 | var existingNamedOnly = namedOnlyContent; 47 | if (existingNamedOnly != null) { 48 | foreach (var kvPair in existingNamedOnly) { 49 | namedContent.Remove (kvPair.Key); 50 | } 51 | } 52 | 53 | if (value == null) 54 | return; 55 | 56 | foreach (var kvPair in value) { 57 | var named = kvPair.Value as INamedContent; 58 | if( named != null ) 59 | AddToNamedContentOnly (named); 60 | } 61 | } 62 | } 63 | 64 | public bool visitsShouldBeCounted { get; set; } 65 | public bool turnIndexShouldBeCounted { get; set; } 66 | public bool countingAtStartOnly { get; set; } 67 | 68 | [Flags] 69 | public enum CountFlags 70 | { 71 | Visits = 1, 72 | Turns = 2, 73 | CountStartOnly = 4 74 | } 75 | 76 | public int countFlags 77 | { 78 | get { 79 | CountFlags flags = 0; 80 | if (visitsShouldBeCounted) flags |= CountFlags.Visits; 81 | if (turnIndexShouldBeCounted) flags |= CountFlags.Turns; 82 | if (countingAtStartOnly) flags |= CountFlags.CountStartOnly; 83 | 84 | // If we're only storing CountStartOnly, it serves no purpose, 85 | // since it's dependent on the other two to be used at all. 86 | // (e.g. for setting the fact that *if* a gather or choice's 87 | // content is counted, then is should only be counter at the start) 88 | // So this is just an optimisation for storage. 89 | if (flags == CountFlags.CountStartOnly) { 90 | flags = 0; 91 | } 92 | 93 | return (int)flags; 94 | } 95 | set { 96 | var flag = (CountFlags)value; 97 | if ((flag & CountFlags.Visits) > 0) visitsShouldBeCounted = true; 98 | if ((flag & CountFlags.Turns) > 0) turnIndexShouldBeCounted = true; 99 | if ((flag & CountFlags.CountStartOnly) > 0) countingAtStartOnly = true; 100 | } 101 | } 102 | 103 | public bool hasValidName 104 | { 105 | get { return name != null && name.Length > 0; } 106 | } 107 | 108 | public Path pathToFirstLeafContent 109 | { 110 | get { 111 | if( _pathToFirstLeafContent == null ) 112 | _pathToFirstLeafContent = path.PathByAppendingPath (internalPathToFirstLeafContent); 113 | 114 | return _pathToFirstLeafContent; 115 | } 116 | } 117 | Path _pathToFirstLeafContent; 118 | 119 | Path internalPathToFirstLeafContent 120 | { 121 | get { 122 | var path = new Path (); 123 | var container = this; 124 | while (container != null) { 125 | if (container.content.Count > 0) { 126 | path.components.Add (new Path.Component (0)); 127 | container = container.content [0] as Container; 128 | } 129 | } 130 | return path; 131 | } 132 | } 133 | 134 | public Container () 135 | { 136 | _content = new List (); 137 | namedContent = new Dictionary (); 138 | } 139 | 140 | public void AddContent(Runtime.Object contentObj) 141 | { 142 | content.Add (contentObj); 143 | 144 | if (contentObj.parent) { 145 | throw new System.Exception ("content is already in " + contentObj.parent); 146 | } 147 | 148 | contentObj.parent = this; 149 | 150 | TryAddNamedContent (contentObj); 151 | } 152 | 153 | public void AddContent(IList contentList) 154 | { 155 | foreach (var c in contentList) { 156 | AddContent (c); 157 | } 158 | } 159 | 160 | public void InsertContent(Runtime.Object contentObj, int index) 161 | { 162 | content.Insert (index, contentObj); 163 | 164 | if (contentObj.parent) { 165 | throw new System.Exception ("content is already in " + contentObj.parent); 166 | } 167 | 168 | contentObj.parent = this; 169 | 170 | TryAddNamedContent (contentObj); 171 | } 172 | 173 | public void TryAddNamedContent(Runtime.Object contentObj) 174 | { 175 | var namedContentObj = contentObj as INamedContent; 176 | if (namedContentObj != null && namedContentObj.hasValidName) { 177 | AddToNamedContentOnly (namedContentObj); 178 | } 179 | } 180 | 181 | public void AddToNamedContentOnly(INamedContent namedContentObj) 182 | { 183 | Debug.Assert (namedContentObj is Runtime.Object, "Can only add Runtime.Objects to a Runtime.Container"); 184 | var runtimeObj = (Runtime.Object)namedContentObj; 185 | runtimeObj.parent = this; 186 | 187 | namedContent [namedContentObj.name] = namedContentObj; 188 | } 189 | 190 | public void AddContentsOfContainer(Container otherContainer) 191 | { 192 | content.AddRange (otherContainer.content); 193 | foreach (var obj in otherContainer.content) { 194 | obj.parent = this; 195 | TryAddNamedContent (obj); 196 | } 197 | } 198 | 199 | protected Runtime.Object ContentWithPathComponent(Path.Component component) 200 | { 201 | if (component.isIndex) { 202 | 203 | if (component.index >= 0 && component.index < content.Count) { 204 | return content [component.index]; 205 | } 206 | 207 | // When path is out of range, quietly return nil 208 | // (useful as we step/increment forwards through content) 209 | else { 210 | return null; 211 | } 212 | 213 | } 214 | 215 | else if (component.isParent) { 216 | return this.parent; 217 | } 218 | 219 | else { 220 | INamedContent foundContent = null; 221 | if (namedContent.TryGetValue (component.name, out foundContent)) { 222 | return (Runtime.Object)foundContent; 223 | } else { 224 | throw new StoryException ("Content '"+component.name+"' not found at path: '"+this.path+"'"); 225 | } 226 | } 227 | } 228 | 229 | public Runtime.Object ContentAtPath(Path path, int partialPathLength = -1) 230 | { 231 | if (partialPathLength == -1) 232 | partialPathLength = path.components.Count; 233 | 234 | Container currentContainer = this; 235 | Runtime.Object currentObj = this; 236 | 237 | for (int i = 0; i < partialPathLength; ++i) { 238 | var comp = path.components [i]; 239 | if (currentContainer == null) 240 | throw new System.Exception ("Path continued, but previous object wasn't a container: " + currentObj); 241 | currentObj = currentContainer.ContentWithPathComponent(comp); 242 | currentContainer = currentObj as Container; 243 | } 244 | 245 | return currentObj; 246 | } 247 | 248 | public void BuildStringOfHierarchy(StringBuilder sb, int indentation, Runtime.Object pointedObj) 249 | { 250 | Action appendIndentation = () => { 251 | const int spacesPerIndent = 4; 252 | for(int i=0; i (); 306 | 307 | foreach (var objKV in namedContent) { 308 | if (content.Contains ((Runtime.Object)objKV.Value)) { 309 | continue; 310 | } else { 311 | onlyNamed.Add (objKV.Key, objKV.Value); 312 | } 313 | } 314 | 315 | if (onlyNamed.Count > 0) { 316 | appendIndentation (); 317 | sb.AppendLine ("-- named: --"); 318 | 319 | foreach (var objKV in onlyNamed) { 320 | 321 | Debug.Assert (objKV.Value is Container, "Can only print out named Containers"); 322 | var container = (Container)objKV.Value; 323 | container.BuildStringOfHierarchy (sb, indentation, pointedObj); 324 | 325 | sb.AppendLine (); 326 | 327 | } 328 | } 329 | 330 | 331 | indentation--; 332 | 333 | appendIndentation (); 334 | sb.Append ("]"); 335 | } 336 | 337 | public virtual string BuildStringOfHierarchy() 338 | { 339 | var sb = new StringBuilder (); 340 | 341 | BuildStringOfHierarchy (sb, 0, null); 342 | 343 | return sb.ToString (); 344 | } 345 | 346 | } 347 | } 348 | 349 | -------------------------------------------------------------------------------- /ink-engine-runtime/Value.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Runtime 5 | { 6 | // Order is significant for type coersion. 7 | // If types aren't directly compatible for an operation, 8 | // they're coerced to the same type, downward. 9 | // Higher value types "infect" an operation. 10 | // (This may not be the most sensible thing to do, but it's worked so far!) 11 | internal enum ValueType 12 | { 13 | // Used in coersion 14 | Int, 15 | Float, 16 | List, 17 | String, 18 | 19 | // Not used for coersion described above 20 | DivertTarget, 21 | VariablePointer 22 | } 23 | 24 | internal abstract class Value : Runtime.Object 25 | { 26 | public abstract ValueType valueType { get; } 27 | public abstract bool isTruthy { get; } 28 | 29 | public abstract Value Cast(ValueType newType); 30 | 31 | public abstract object valueObject { get; } 32 | 33 | public static Value Create(object val) 34 | { 35 | // Implicitly lose precision from any doubles we get passed in 36 | if (val is double) { 37 | double doub = (double)val; 38 | val = (float)doub; 39 | } 40 | 41 | // Implicitly convert bools into ints 42 | if (val is bool) { 43 | bool b = (bool)val; 44 | val = (int)(b ? 1 : 0); 45 | } 46 | 47 | if (val is int) { 48 | return new IntValue ((int)val); 49 | } else if (val is long) { 50 | return new IntValue ((int)(long)val); 51 | } else if (val is float) { 52 | return new FloatValue ((float)val); 53 | } else if (val is double) { 54 | return new FloatValue ((float)(double)val); 55 | } else if (val is string) { 56 | return new StringValue ((string)val); 57 | } else if (val is Path) { 58 | return new DivertTargetValue ((Path)val); 59 | } else if (val is InkList) { 60 | return new ListValue ((InkList)val); 61 | } 62 | 63 | return null; 64 | } 65 | 66 | internal override Object Copy() 67 | { 68 | return Create (valueObject); 69 | } 70 | } 71 | 72 | internal abstract class Value : Value 73 | { 74 | public T value { get; set; } 75 | 76 | public override object valueObject { 77 | get { 78 | return (object)value; 79 | } 80 | } 81 | 82 | public Value (T val) 83 | { 84 | value = val; 85 | } 86 | 87 | public override string ToString () 88 | { 89 | return value.ToString(); 90 | } 91 | } 92 | 93 | internal class IntValue : Value 94 | { 95 | public override ValueType valueType { get { return ValueType.Int; } } 96 | public override bool isTruthy { get { return value != 0; } } 97 | 98 | public IntValue(int intVal) : base(intVal) 99 | { 100 | } 101 | 102 | public IntValue() : this(0) {} 103 | 104 | public override Value Cast(ValueType newType) 105 | { 106 | if (newType == valueType) { 107 | return this; 108 | } 109 | 110 | if (newType == ValueType.Float) { 111 | return new FloatValue ((float)this.value); 112 | } 113 | 114 | if (newType == ValueType.String) { 115 | return new StringValue("" + this.value); 116 | } 117 | 118 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 119 | } 120 | } 121 | 122 | internal class FloatValue : Value 123 | { 124 | public override ValueType valueType { get { return ValueType.Float; } } 125 | public override bool isTruthy { get { return value != 0.0f; } } 126 | 127 | public FloatValue(float val) : base(val) 128 | { 129 | } 130 | 131 | public FloatValue() : this(0.0f) {} 132 | 133 | public override Value Cast(ValueType newType) 134 | { 135 | if (newType == valueType) { 136 | return this; 137 | } 138 | 139 | if (newType == ValueType.Int) { 140 | return new IntValue ((int)this.value); 141 | } 142 | 143 | if (newType == ValueType.String) { 144 | return new StringValue("" + this.value.ToString(System.Globalization.CultureInfo.InvariantCulture)); 145 | } 146 | 147 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 148 | } 149 | } 150 | 151 | internal class StringValue : Value 152 | { 153 | public override ValueType valueType { get { return ValueType.String; } } 154 | public override bool isTruthy { get { return value.Length > 0; } } 155 | 156 | public bool isNewline { get; private set; } 157 | public bool isInlineWhitespace { get; private set; } 158 | public bool isNonWhitespace { 159 | get { 160 | return !isNewline && !isInlineWhitespace; 161 | } 162 | } 163 | 164 | public StringValue(string str) : base(str) 165 | { 166 | // Classify whitespace status 167 | isNewline = value == "\n"; 168 | isInlineWhitespace = true; 169 | foreach (var c in value) { 170 | if (c != ' ' && c != '\t') { 171 | isInlineWhitespace = false; 172 | break; 173 | } 174 | } 175 | } 176 | 177 | public StringValue() : this("") {} 178 | 179 | public override Value Cast(ValueType newType) 180 | { 181 | if (newType == valueType) { 182 | return this; 183 | } 184 | 185 | if (newType == ValueType.Int) { 186 | 187 | int parsedInt; 188 | if (int.TryParse (value, out parsedInt)) { 189 | return new IntValue (parsedInt); 190 | } else { 191 | return null; 192 | } 193 | } 194 | 195 | if (newType == ValueType.Float) { 196 | float parsedFloat; 197 | if (float.TryParse (value, System.Globalization.NumberStyles.Float ,System.Globalization.CultureInfo.InvariantCulture, out parsedFloat)) { 198 | return new FloatValue (parsedFloat); 199 | } else { 200 | return null; 201 | } 202 | } 203 | 204 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 205 | } 206 | } 207 | 208 | internal class DivertTargetValue : Value 209 | { 210 | public Path targetPath { get { return this.value; } set { this.value = value; } } 211 | public override ValueType valueType { get { return ValueType.DivertTarget; } } 212 | public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a divert target"); } } 213 | 214 | public DivertTargetValue(Path targetPath) : base(targetPath) 215 | { 216 | } 217 | 218 | public DivertTargetValue() : base(null) 219 | {} 220 | 221 | public override Value Cast(ValueType newType) 222 | { 223 | if (newType == valueType) 224 | return this; 225 | 226 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 227 | } 228 | 229 | public override string ToString () 230 | { 231 | return "DivertTargetValue(" + targetPath + ")"; 232 | } 233 | } 234 | 235 | // TODO: Think: Erm, I get that this contains a string, but should 236 | // we really derive from Value? That seems a bit misleading to me. 237 | internal class VariablePointerValue : Value 238 | { 239 | public string variableName { get { return this.value; } set { this.value = value; } } 240 | public override ValueType valueType { get { return ValueType.VariablePointer; } } 241 | public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a variable pointer"); } } 242 | 243 | // Where the variable is located 244 | // -1 = default, unknown, yet to be determined 245 | // 0 = in global scope 246 | // 1+ = callstack element index + 1 (so that the first doesn't conflict with special global scope) 247 | public int contextIndex { get; set; } 248 | 249 | public VariablePointerValue(string variableName, int contextIndex = -1) : base(variableName) 250 | { 251 | this.contextIndex = contextIndex; 252 | } 253 | 254 | public VariablePointerValue() : this(null) 255 | { 256 | } 257 | 258 | public override Value Cast(ValueType newType) 259 | { 260 | if (newType == valueType) 261 | return this; 262 | 263 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 264 | } 265 | 266 | public override string ToString () 267 | { 268 | return "VariablePointerValue(" + variableName + ")"; 269 | } 270 | 271 | internal override Object Copy() 272 | { 273 | return new VariablePointerValue (variableName, contextIndex); 274 | } 275 | } 276 | 277 | internal class ListValue : Value 278 | { 279 | public override ValueType valueType { 280 | get { 281 | return ValueType.List; 282 | } 283 | } 284 | 285 | // Truthy if it contains any non-zero items 286 | public override bool isTruthy { 287 | get { 288 | foreach (var kv in value) { 289 | int listItemIntValue = kv.Value; 290 | if (listItemIntValue != 0) 291 | return true; 292 | } 293 | return false; 294 | } 295 | } 296 | 297 | public override Value Cast (ValueType newType) 298 | { 299 | if (newType == ValueType.Int) { 300 | var max = value.maxItem; 301 | if( max.Key.isNull ) 302 | return new IntValue (0); 303 | else 304 | return new IntValue (max.Value); 305 | } 306 | 307 | else if (newType == ValueType.Float) { 308 | var max = value.maxItem; 309 | if (max.Key.isNull) 310 | return new FloatValue (0.0f); 311 | else 312 | return new FloatValue ((float)max.Value); 313 | } 314 | 315 | else if (newType == ValueType.String) { 316 | var max = value.maxItem; 317 | if (max.Key.isNull) 318 | return new StringValue (""); 319 | else { 320 | return new StringValue (max.Key.ToString()); 321 | } 322 | } 323 | 324 | if (newType == valueType) 325 | return this; 326 | 327 | throw new System.Exception ("Unexpected type cast of Value to new ValueType"); 328 | } 329 | 330 | public ListValue () : base(null) { 331 | value = new InkList (); 332 | } 333 | 334 | public ListValue (InkList list) : base (null) 335 | { 336 | value = new InkList (list); 337 | } 338 | 339 | public ListValue (InkListItem singleItem, int singleValue) : base (null) 340 | { 341 | value = new InkList { 342 | {singleItem, singleValue} 343 | }; 344 | } 345 | 346 | public static void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue) 347 | { 348 | var oldList = oldValue as ListValue; 349 | var newList = newValue as ListValue; 350 | 351 | // When assigning the emtpy list, try to retain any initial origin names 352 | if (oldList && newList && newList.value.Count == 0) 353 | newList.value.SetInitialOriginNames (oldList.value.originNames); 354 | } 355 | } 356 | 357 | } 358 | 359 | -------------------------------------------------------------------------------- /ink-engine-runtime/VariablesState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | /// 6 | /// Encompasses all the global variables in an ink Story, and 7 | /// allows binding of a VariableChanged event so that that game 8 | /// code can be notified whenever the global variables change. 9 | /// 10 | public class VariablesState : IEnumerable 11 | { 12 | internal delegate void VariableChanged(string variableName, Runtime.Object newValue); 13 | internal event VariableChanged variableChangedEvent; 14 | 15 | internal bool batchObservingVariableChanges 16 | { 17 | get { 18 | return _batchObservingVariableChanges; 19 | } 20 | set { 21 | _batchObservingVariableChanges = value; 22 | if (value) { 23 | _changedVariables = new HashSet (); 24 | } 25 | 26 | // Finished observing variables in a batch - now send 27 | // notifications for changed variables all in one go. 28 | else { 29 | if (_changedVariables != null) { 30 | foreach (var variableName in _changedVariables) { 31 | var currentValue = _globalVariables [variableName]; 32 | variableChangedEvent (variableName, currentValue); 33 | } 34 | } 35 | 36 | _changedVariables = null; 37 | } 38 | } 39 | } 40 | bool _batchObservingVariableChanges; 41 | 42 | // Allow StoryState to change the current callstack, e.g. for 43 | // temporary function evaluation. 44 | internal CallStack callStack { 45 | get { 46 | return _callStack; 47 | } 48 | set { 49 | _callStack = value; 50 | } 51 | } 52 | 53 | /// 54 | /// Get or set the value of a named global ink variable. 55 | /// The types available are the standard ink types. Certain 56 | /// types will be implicitly casted when setting. 57 | /// For example, doubles to floats, longs to ints, and bools 58 | /// to ints. 59 | /// 60 | public object this[string variableName] 61 | { 62 | get { 63 | Runtime.Object varContents; 64 | if ( _globalVariables.TryGetValue (variableName, out varContents) ) 65 | return (varContents as Runtime.Value).valueObject; 66 | else 67 | return null; 68 | } 69 | set { 70 | 71 | // This is the main 72 | if (!_globalVariables.ContainsKey (variableName)) { 73 | throw new StoryException ("Variable '" + variableName + "' doesn't exist, so can't be set."); 74 | } 75 | 76 | var val = Runtime.Value.Create(value); 77 | if (val == null) { 78 | if (value == null) { 79 | throw new StoryException ("Cannot pass null to VariableState"); 80 | } else { 81 | throw new StoryException ("Invalid value passed to VariableState: "+value.ToString()); 82 | } 83 | } 84 | 85 | SetGlobal (variableName, val); 86 | } 87 | } 88 | 89 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 90 | { 91 | return GetEnumerator(); 92 | } 93 | 94 | /// 95 | /// Enumerator to allow iteration over all global variables by name. 96 | /// 97 | public IEnumerator GetEnumerator() 98 | { 99 | return _globalVariables.Keys.GetEnumerator(); 100 | } 101 | 102 | internal VariablesState (CallStack callStack, ListDefinitionsOrigin listDefsOrigin) 103 | { 104 | _globalVariables = new Dictionary (); 105 | _callStack = callStack; 106 | _listDefsOrigin = listDefsOrigin; 107 | } 108 | 109 | internal void CopyFrom (VariablesState toCopy) 110 | { 111 | _globalVariables = new Dictionary (toCopy._globalVariables); 112 | 113 | variableChangedEvent = toCopy.variableChangedEvent; 114 | 115 | if (toCopy.batchObservingVariableChanges != batchObservingVariableChanges) { 116 | 117 | if (toCopy.batchObservingVariableChanges) { 118 | _batchObservingVariableChanges = true; 119 | _changedVariables = new HashSet (toCopy._changedVariables); 120 | } else { 121 | _batchObservingVariableChanges = false; 122 | _changedVariables = null; 123 | } 124 | } 125 | } 126 | 127 | internal Dictionary jsonToken 128 | { 129 | get { 130 | return Json.DictionaryRuntimeObjsToJObject(_globalVariables); 131 | } 132 | set { 133 | _globalVariables = Json.JObjectToDictionaryRuntimeObjs (value); 134 | } 135 | } 136 | 137 | internal Runtime.Object GetVariableWithName(string name) 138 | { 139 | return GetVariableWithName (name, -1); 140 | } 141 | 142 | Runtime.Object GetVariableWithName(string name, int contextIndex) 143 | { 144 | Runtime.Object varValue = GetRawVariableWithName (name, contextIndex); 145 | 146 | // Get value from pointer? 147 | var varPointer = varValue as VariablePointerValue; 148 | if (varPointer) { 149 | varValue = ValueAtVariablePointer (varPointer); 150 | } 151 | 152 | return varValue; 153 | } 154 | 155 | Runtime.Object GetRawVariableWithName(string name, int contextIndex) 156 | { 157 | Runtime.Object varValue = null; 158 | 159 | // 0 context = global 160 | if (contextIndex == 0 || contextIndex == -1) { 161 | if ( _globalVariables.TryGetValue (name, out varValue) ) 162 | return varValue; 163 | 164 | var listItemValue = _listDefsOrigin.FindSingleItemListWithName (name); 165 | if (listItemValue) 166 | return listItemValue; 167 | } 168 | 169 | // Temporary 170 | varValue = _callStack.GetTemporaryVariableWithName (name, contextIndex); 171 | 172 | if (varValue == null) 173 | throw new System.Exception ("RUNTIME ERROR: Variable '"+name+"' could not be found in context '"+contextIndex+"'. This shouldn't be possible so is a bug in the ink engine. Please try to construct a minimal story that reproduces the problem and report to inkle, thank you!"); 174 | 175 | return varValue; 176 | } 177 | 178 | internal Runtime.Object ValueAtVariablePointer(VariablePointerValue pointer) 179 | { 180 | return GetVariableWithName (pointer.variableName, pointer.contextIndex); 181 | } 182 | 183 | internal void Assign(VariableAssignment varAss, Runtime.Object value) 184 | { 185 | var name = varAss.variableName; 186 | int contextIndex = -1; 187 | 188 | // Are we assigning to a global variable? 189 | bool setGlobal = false; 190 | if (varAss.isNewDeclaration) { 191 | setGlobal = varAss.isGlobal; 192 | } else { 193 | setGlobal = _globalVariables.ContainsKey (name); 194 | } 195 | 196 | // Constructing new variable pointer reference 197 | if (varAss.isNewDeclaration) { 198 | var varPointer = value as VariablePointerValue; 199 | if (varPointer) { 200 | var fullyResolvedVariablePointer = ResolveVariablePointer (varPointer); 201 | value = fullyResolvedVariablePointer; 202 | } 203 | 204 | } 205 | 206 | // Assign to existing variable pointer? 207 | // Then assign to the variable that the pointer is pointing to by name. 208 | else { 209 | 210 | // De-reference variable reference to point to 211 | VariablePointerValue existingPointer = null; 212 | do { 213 | existingPointer = GetRawVariableWithName (name, contextIndex) as VariablePointerValue; 214 | if (existingPointer) { 215 | name = existingPointer.variableName; 216 | contextIndex = existingPointer.contextIndex; 217 | setGlobal = (contextIndex == 0); 218 | } 219 | } while(existingPointer); 220 | } 221 | 222 | 223 | if (setGlobal) { 224 | SetGlobal (name, value); 225 | } else { 226 | _callStack.SetTemporaryVariable (name, value, varAss.isNewDeclaration, contextIndex); 227 | } 228 | } 229 | 230 | void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue) 231 | { 232 | var oldList = oldValue as ListValue; 233 | var newList = newValue as ListValue; 234 | if (oldList && newList && newList.value.Count == 0) 235 | newList.value.SetInitialOriginNames (oldList.value.originNames); 236 | } 237 | 238 | void SetGlobal(string variableName, Runtime.Object value) 239 | { 240 | Runtime.Object oldValue = null; 241 | _globalVariables.TryGetValue (variableName, out oldValue); 242 | 243 | ListValue.RetainListOriginsForAssignment (oldValue, value); 244 | 245 | _globalVariables [variableName] = value; 246 | 247 | if (variableChangedEvent != null && !value.Equals (oldValue)) { 248 | 249 | if (batchObservingVariableChanges) { 250 | _changedVariables.Add (variableName); 251 | } else { 252 | variableChangedEvent (variableName, value); 253 | } 254 | } 255 | } 256 | 257 | // Given a variable pointer with just the name of the target known, resolve to a variable 258 | // pointer that more specifically points to the exact instance: whether it's global, 259 | // or the exact position of a temporary on the callstack. 260 | VariablePointerValue ResolveVariablePointer(VariablePointerValue varPointer) 261 | { 262 | int contextIndex = varPointer.contextIndex; 263 | 264 | if( contextIndex == -1 ) 265 | contextIndex = GetContextIndexOfVariableNamed (varPointer.variableName); 266 | 267 | var valueOfVariablePointedTo = GetRawVariableWithName (varPointer.variableName, contextIndex); 268 | 269 | // Extra layer of indirection: 270 | // When accessing a pointer to a pointer (e.g. when calling nested or 271 | // recursive functions that take a variable references, ensure we don't create 272 | // a chain of indirection by just returning the final target. 273 | var doubleRedirectionPointer = valueOfVariablePointedTo as VariablePointerValue; 274 | if (doubleRedirectionPointer) { 275 | return doubleRedirectionPointer; 276 | } 277 | 278 | // Make copy of the variable pointer so we're not using the value direct from 279 | // the runtime. Temporary must be local to the current scope. 280 | else { 281 | return new VariablePointerValue (varPointer.variableName, contextIndex); 282 | } 283 | } 284 | 285 | // 0 if named variable is global 286 | // 1+ if named variable is a temporary in a particular call stack element 287 | int GetContextIndexOfVariableNamed(string varName) 288 | { 289 | if (_globalVariables.ContainsKey (varName)) 290 | return 0; 291 | 292 | return _callStack.currentElementIndex; 293 | } 294 | 295 | Dictionary _globalVariables; 296 | 297 | // Used for accessing temporary variables 298 | CallStack _callStack; 299 | HashSet _changedVariables; 300 | ListDefinitionsOrigin _listDefsOrigin; 301 | } 302 | } 303 | 304 | -------------------------------------------------------------------------------- /ink-engine-runtime/CallStack.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Diagnostics; 4 | 5 | namespace Ink.Runtime 6 | { 7 | internal class CallStack 8 | { 9 | internal class Element 10 | { 11 | public Container currentContainer; 12 | public int currentContentIndex; 13 | 14 | public bool inExpressionEvaluation; 15 | public Dictionary temporaryVariables; 16 | public PushPopType type; 17 | 18 | public Runtime.Object currentObject { 19 | get { 20 | if (currentContainer && currentContentIndex < currentContainer.content.Count) { 21 | return currentContainer.content [currentContentIndex]; 22 | } 23 | 24 | return null; 25 | } 26 | set { 27 | var currentObj = value; 28 | if (currentObj == null) { 29 | currentContainer = null; 30 | currentContentIndex = 0; 31 | return; 32 | } 33 | 34 | currentContainer = currentObj.parent as Container; 35 | if (currentContainer != null) 36 | currentContentIndex = currentContainer.content.IndexOf (currentObj); 37 | 38 | // Two reasons why the above operation might not work: 39 | // - currentObj is already the root container 40 | // - currentObj is a named container rather than being an object at an index 41 | if (currentContainer == null || currentContentIndex == -1) { 42 | currentContainer = currentObj as Container; 43 | currentContentIndex = 0; 44 | } 45 | } 46 | } 47 | 48 | public Element(PushPopType type, Container container, int contentIndex, bool inExpressionEvaluation = false) { 49 | this.currentContainer = container; 50 | this.currentContentIndex = contentIndex; 51 | this.inExpressionEvaluation = inExpressionEvaluation; 52 | this.temporaryVariables = new Dictionary(); 53 | this.type = type; 54 | } 55 | 56 | public Element Copy() 57 | { 58 | var copy = new Element (this.type, this.currentContainer, this.currentContentIndex, this.inExpressionEvaluation); 59 | copy.temporaryVariables = new Dictionary(this.temporaryVariables); 60 | return copy; 61 | } 62 | } 63 | 64 | internal class Thread 65 | { 66 | public List callstack; 67 | public int threadIndex; 68 | public Runtime.Object previousContentObject; 69 | 70 | public Thread() { 71 | callstack = new List(); 72 | } 73 | 74 | public Thread(Dictionary jThreadObj, Story storyContext) : this() { 75 | threadIndex = (int) jThreadObj ["threadIndex"]; 76 | 77 | List jThreadCallstack = (List) jThreadObj ["callstack"]; 78 | foreach (object jElTok in jThreadCallstack) { 79 | 80 | var jElementObj = (Dictionary)jElTok; 81 | 82 | PushPopType pushPopType = (PushPopType)(int)jElementObj ["type"]; 83 | 84 | Container currentContainer = null; 85 | int contentIndex = 0; 86 | 87 | string currentContainerPathStr = null; 88 | object currentContainerPathStrToken; 89 | if (jElementObj.TryGetValue ("cPath", out currentContainerPathStrToken)) { 90 | currentContainerPathStr = currentContainerPathStrToken.ToString (); 91 | currentContainer = storyContext.ContentAtPath (new Path(currentContainerPathStr)) as Container; 92 | contentIndex = (int) jElementObj ["idx"]; 93 | } 94 | 95 | bool inExpressionEvaluation = (bool)jElementObj ["exp"]; 96 | 97 | var el = new Element (pushPopType, currentContainer, contentIndex, inExpressionEvaluation); 98 | 99 | var jObjTemps = (Dictionary) jElementObj ["temp"]; 100 | el.temporaryVariables = Json.JObjectToDictionaryRuntimeObjs (jObjTemps); 101 | 102 | callstack.Add (el); 103 | } 104 | 105 | object prevContentObjPath; 106 | if( jThreadObj.TryGetValue("previousContentObject", out prevContentObjPath) ) { 107 | var prevPath = new Path((string)prevContentObjPath); 108 | previousContentObject = storyContext.ContentAtPath(prevPath); 109 | } 110 | } 111 | 112 | public Thread Copy() { 113 | var copy = new Thread (); 114 | copy.threadIndex = threadIndex; 115 | foreach(var e in callstack) { 116 | copy.callstack.Add(e.Copy()); 117 | } 118 | copy.previousContentObject = previousContentObject; 119 | return copy; 120 | } 121 | 122 | public Dictionary jsonToken { 123 | get { 124 | var threadJObj = new Dictionary (); 125 | 126 | var jThreadCallstack = new List (); 127 | foreach (CallStack.Element el in callstack) { 128 | var jObj = new Dictionary (); 129 | if (el.currentContainer) { 130 | jObj ["cPath"] = el.currentContainer.path.componentsString; 131 | jObj ["idx"] = el.currentContentIndex; 132 | } 133 | jObj ["exp"] = el.inExpressionEvaluation; 134 | jObj ["type"] = (int) el.type; 135 | jObj ["temp"] = Json.DictionaryRuntimeObjsToJObject (el.temporaryVariables); 136 | jThreadCallstack.Add (jObj); 137 | } 138 | 139 | threadJObj ["callstack"] = jThreadCallstack; 140 | threadJObj ["threadIndex"] = threadIndex; 141 | 142 | if (previousContentObject != null) 143 | threadJObj ["previousContentObject"] = previousContentObject.path.ToString(); 144 | 145 | return threadJObj; 146 | } 147 | } 148 | } 149 | 150 | public List elements { 151 | get { 152 | return callStack; 153 | } 154 | } 155 | 156 | public int depth { 157 | get { 158 | return elements.Count; 159 | } 160 | } 161 | 162 | public Element currentElement { 163 | get { 164 | return callStack [callStack.Count - 1]; 165 | } 166 | } 167 | 168 | public int currentElementIndex { 169 | get { 170 | return callStack.Count - 1; 171 | } 172 | } 173 | 174 | public Thread currentThread 175 | { 176 | get { 177 | return _threads [_threads.Count - 1]; 178 | } 179 | set { 180 | Debug.Assert (_threads.Count == 1, "Shouldn't be directly setting the current thread when we have a stack of them"); 181 | _threads.Clear (); 182 | _threads.Add (value); 183 | } 184 | } 185 | 186 | public bool canPop { 187 | get { 188 | return callStack.Count > 1; 189 | } 190 | } 191 | 192 | public CallStack (Container rootContentContainer) 193 | { 194 | _threads = new List (); 195 | _threads.Add (new Thread ()); 196 | 197 | _threads [0].callstack.Add (new Element (PushPopType.Tunnel, rootContentContainer, 0)); 198 | } 199 | 200 | public CallStack(CallStack toCopy) 201 | { 202 | _threads = new List (); 203 | foreach (var otherThread in toCopy._threads) { 204 | _threads.Add (otherThread.Copy ()); 205 | } 206 | } 207 | 208 | // Unfortunately it's not possible to implement jsonToken since 209 | // the setter needs to take a Story as a context in order to 210 | // look up objects from paths for currentContainer within elements. 211 | public void SetJsonToken(Dictionary jObject, Story storyContext) 212 | { 213 | _threads.Clear (); 214 | 215 | var jThreads = (List) jObject ["threads"]; 216 | 217 | foreach (object jThreadTok in jThreads) { 218 | var jThreadObj = (Dictionary)jThreadTok; 219 | var thread = new Thread (jThreadObj, storyContext); 220 | _threads.Add (thread); 221 | } 222 | 223 | _threadCounter = (int)jObject ["threadCounter"]; 224 | } 225 | 226 | // See above for why we can't implement jsonToken 227 | public Dictionary GetJsonToken() { 228 | 229 | var jObject = new Dictionary (); 230 | 231 | var jThreads = new List (); 232 | foreach (CallStack.Thread thread in _threads) { 233 | jThreads.Add (thread.jsonToken); 234 | } 235 | 236 | jObject ["threads"] = jThreads; 237 | jObject ["threadCounter"] = _threadCounter; 238 | 239 | return jObject; 240 | } 241 | 242 | public void PushThread() 243 | { 244 | var newThread = currentThread.Copy (); 245 | _threadCounter++; 246 | newThread.threadIndex = _threadCounter; 247 | _threads.Add (newThread); 248 | } 249 | 250 | public void PopThread() 251 | { 252 | if (canPopThread) { 253 | _threads.Remove (currentThread); 254 | } else { 255 | throw new System.Exception("Can't pop thread"); 256 | } 257 | } 258 | 259 | public bool canPopThread 260 | { 261 | get { 262 | return _threads.Count > 1; 263 | } 264 | } 265 | 266 | public void Push(PushPopType type) 267 | { 268 | // When pushing to callstack, maintain the current content path, but jump out of expressions by default 269 | callStack.Add (new Element(type, currentElement.currentContainer, currentElement.currentContentIndex, inExpressionEvaluation: false)); 270 | } 271 | 272 | public bool CanPop(PushPopType? type = null) { 273 | 274 | if (!canPop) 275 | return false; 276 | 277 | if (type == null) 278 | return true; 279 | 280 | return currentElement.type == type; 281 | } 282 | 283 | public void Pop(PushPopType? type = null) 284 | { 285 | if (CanPop (type)) { 286 | callStack.RemoveAt (callStack.Count - 1); 287 | return; 288 | } else { 289 | throw new System.Exception("Mismatched push/pop in Callstack"); 290 | } 291 | } 292 | 293 | // Get variable value, dereferencing a variable pointer if necessary 294 | public Runtime.Object GetTemporaryVariableWithName(string name, int contextIndex = -1) 295 | { 296 | if (contextIndex == -1) 297 | contextIndex = currentElementIndex+1; 298 | 299 | Runtime.Object varValue = null; 300 | 301 | var contextElement = callStack [contextIndex-1]; 302 | 303 | if (contextElement.temporaryVariables.TryGetValue (name, out varValue)) { 304 | return varValue; 305 | } else { 306 | return null; 307 | } 308 | } 309 | 310 | public void SetTemporaryVariable(string name, Runtime.Object value, bool declareNew, int contextIndex = -1) 311 | { 312 | if (contextIndex == -1) 313 | contextIndex = currentElementIndex+1; 314 | 315 | var contextElement = callStack [contextIndex-1]; 316 | 317 | if (!declareNew && !contextElement.temporaryVariables.ContainsKey(name)) { 318 | throw new StoryException ("Could not find temporary variable to set: " + name); 319 | } 320 | 321 | Runtime.Object oldValue; 322 | if( contextElement.temporaryVariables.TryGetValue(name, out oldValue) ) 323 | ListValue.RetainListOriginsForAssignment (oldValue, value); 324 | 325 | contextElement.temporaryVariables [name] = value; 326 | } 327 | 328 | // Find the most appropriate context for this variable. 329 | // Are we referencing a temporary or global variable? 330 | // Note that the compiler will have warned us about possible conflicts, 331 | // so anything that happens here should be safe! 332 | public int ContextForVariableNamed(string name) 333 | { 334 | // Current temporary context? 335 | // (Shouldn't attempt to access contexts higher in the callstack.) 336 | if (currentElement.temporaryVariables.ContainsKey (name)) { 337 | return currentElementIndex+1; 338 | } 339 | 340 | // Global 341 | else { 342 | return 0; 343 | } 344 | } 345 | 346 | public Thread ThreadWithIndex(int index) 347 | { 348 | return _threads.Find (t => t.threadIndex == index); 349 | } 350 | 351 | private List callStack 352 | { 353 | get { 354 | return currentThread.callstack; 355 | } 356 | } 357 | 358 | List _threads; 359 | int _threadCounter; 360 | } 361 | } 362 | 363 | -------------------------------------------------------------------------------- /addons/GodotTIE/text_interface_engine.gd: -------------------------------------------------------------------------------- 1 | #MADE BY HENRIQUE ALVES 2 | #LICENSE STUFF BLABLABLA 3 | #(MIT License) 4 | 5 | # Intern initializations 6 | extends ReferenceRect # Extends from ReferenceRect 7 | 8 | const _ARRAY_CHARS = [" ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/","0","1","2","3","4","5","6","7","8","9",":",";","<","=",">","?","@","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","[","\\","]","^","_","`","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","{","|","}","~"] 9 | 10 | const STATE_WAITING = 0 11 | const STATE_OUTPUT = 1 12 | const STATE_INPUT = 2 13 | 14 | const BUFF_DEBUG = 0 15 | const BUFF_TEXT = 1 16 | const BUFF_SILENCE = 2 17 | const BUFF_BREAK = 3 18 | const BUFF_INPUT = 4 19 | const BUFF_CLEAR = 5 20 | 21 | onready var _buffer = [] # 0 = Debug; 1 = Text; 2 = Silence; 3 = Break; 4 = Input 22 | onready var _label = Label.new() # The Label in which the text is going to be displayed 23 | onready var _state = 0 # 0 = Waiting; 1 = Output; 2 = Input 24 | 25 | onready var _output_delay = 0 26 | onready var _output_delay_limit = 0 27 | onready var _on_break = false 28 | onready var _max_lines_reached = false 29 | onready var _buff_beginning = true 30 | onready var _turbo = false 31 | onready var _max_lines = 0 32 | 33 | onready var _blink_input_visible = false 34 | onready var _blink_input_timer = 0 35 | onready var _input_timer_limit = 1 36 | onready var _input_index = 0 37 | 38 | # =============================================== 39 | # Text display properties! 40 | export(bool) var SCROLL_ON_MAX_LINES = true # If this is true, the text buffer update will stop after reaching the maximum number of lines; else, it will stop to wait for user input, and than clear the text. 41 | export(bool) var BREAK_ON_MAX_LINES = false # If the text output pauses waiting for the user when reaching the maximum number of lines 42 | export(bool) var AUTO_SKIP_WORDS = true # If words that dont fit the line only start to be printed on next line 43 | export(bool) var LOG_SKIPPED_LINES = true # false = delete every line that is not showing on screen 44 | export(bool) var SCROLL_SKIPPED_LINES = false # if the user will be able to scroll through the skipped lines; weird stuff can happen if this and BREAK_ON_MAX_LINE/LOG_SKIPPED_LINES 45 | export(Font) var FONT 46 | # Text input properties! 47 | export(bool) var PRINT_INPUT = true # If the input is going to be printed 48 | export(bool) var BLINKING_INPUT = true # If there is a _ blinking when input is appropriate 49 | export(int) var INPUT_CHARACTERS_LIMIT = -1 # If -1, there'll be no limits in the number of characters 50 | # Signals! 51 | signal input_enter(input) # When user finished an input 52 | signal buff_end() # When there is no more outputs in _buffer 53 | signal state_change(state) # When the state of the engine changes 54 | signal enter_break() # When the engine stops on a break 55 | signal resume_break() # When the engine resumes from a break 56 | signal tag_buff(tag) # When the _buffer reaches a buff which is tagged 57 | signal buff_cleared() # When the buffer's been cleared of text 58 | # =============================================== 59 | 60 | func buff_debug(f, lab = false, arg0 = null, push_front = false): # For simple debug purposes; use with care 61 | var b = {"buff_type":BUFF_DEBUG,"debug_function":f,"debug_label":lab,"debug_arg":arg0} 62 | if(! push_front): 63 | _buffer.append(b) 64 | else: 65 | _buffer.push_front(b) 66 | 67 | func buff_text(text, vel = 0, tag = "", push_front = false): # The text for the output, and its printing velocity (per character) 68 | var b = {"buff_type":BUFF_TEXT, "buff_text":text, "buff_vel":vel, "buff_tag":tag} 69 | if !push_front: 70 | _buffer.append(b) 71 | else: 72 | _buffer.push_front(b) 73 | 74 | func buff_silence(length, tag = "", push_front = false): # A duration without output 75 | var b = {"buff_type":BUFF_SILENCE, "buff_length":length, "buff_tag":tag} 76 | if !push_front: 77 | _buffer.append(b) 78 | else: 79 | _buffer.push_front(b) 80 | 81 | func buff_break(tag = "", push_front = false): # Stop output until the player hits enter 82 | var b = {"buff_type":BUFF_BREAK, "buff_tag":tag} 83 | if !push_front: 84 | _buffer.append(b) 85 | else: 86 | _buffer.push_front(b) 87 | 88 | func buff_input(tag = "", push_front = false): # 'Schedule' a change state to Input in the buffer 89 | var b = {"buff_type":BUFF_INPUT, "buff_tag":tag} 90 | if !push_front: 91 | _buffer.append(b) 92 | else: 93 | _buffer.push_front(b) 94 | 95 | func buff_clear(tag = "", push_front = false): # Clear the text buffer when this buffer command is run. 96 | var b = {"buff_type":BUFF_CLEAR, "buff_tag":tag} 97 | if !push_front: 98 | _buffer.append(b) 99 | else: 100 | _buffer.push_front(b) 101 | 102 | func clear_text(): # Deletes ALL the text on the label 103 | _label.set_lines_skipped(0) 104 | _label.set_text("") 105 | 106 | func clear_buffer(): # Clears all buffs in _buffer 107 | _on_break = false 108 | set_state(STATE_WAITING) 109 | _buffer.clear() 110 | 111 | _output_delay = 0 112 | _output_delay_limit = 0 113 | _buff_beginning = true 114 | _turbo = false 115 | _max_lines_reached = false 116 | 117 | func reset(): # Reset TIE to its initial 100% cleared state 118 | clear_text() 119 | clear_buffer() 120 | 121 | func clear_skipped_lines(): # Deletes only the 'hidden' lines, if LOG_SKIPPED_LINES is false 122 | if(LOG_SKIPPED_LINES == true): 123 | _clear_skipped_lines() 124 | 125 | func add_newline(): # Add a new line to the label text 126 | _label_print("\n") 127 | 128 | func get_text(): # Get current text on Label 129 | return _label.get_text() 130 | 131 | func set_turbomode(s): # Print stuff in the maximum velocity and ignore breaks 132 | _turbo = s; 133 | 134 | # Careful when changing fonts on-the-fly! It might break the text if there is something 135 | # already printed! 136 | func set_font_bypath(str_path): # Changes the font of the text; weird stuff will happen if you use this function after text has been printed 137 | _label.add_font_override("font",load(str_path)) 138 | _max_lines = floor(get_size().y/(_label.get_line_height()+_label.get_constant("line_spacing"))) 139 | 140 | func set_font_byresource(font): # Changes font of the text (uses the resource) 141 | _label.add_font_override("font", font) 142 | _max_lines = floor(get_size().y/(_label.get_line_height()+_label.get_constant("line_spacing"))) 143 | 144 | func set_color(c): # Changes the color of the text 145 | _label.add_color_override("font_color", c) 146 | 147 | func set_state(i): # Changes the state of the Text Interface Engine 148 | emit_signal("state_change", int(i)) 149 | if _state == STATE_INPUT: 150 | _blink_input(true) 151 | _state = i 152 | if(i == 2): # Set input index to last character on the label 153 | _input_index = _label.get_text().length() 154 | 155 | func set_buff_speed(v): # Changes the velocity of the text being printed 156 | if (_buffer[0]["buff_type"] == BUFF_TEXT): 157 | _buffer[0]["buff_vel"] = v 158 | 159 | # ============================================== 160 | # Reserved methods 161 | 162 | # Override 163 | func _ready(): 164 | set_physics_process(true) 165 | set_process_input(true) 166 | 167 | add_child(_label) 168 | 169 | # Setting font of the text 170 | if(FONT != null): 171 | _label.add_font_override("font", FONT) 172 | 173 | # Setting size of the frame 174 | _max_lines = floor(get_size().y/(_label.get_line_height()+_label.get_constant("line_spacing"))) 175 | _label.set_size(Vector2(get_size().x,get_size().y)) 176 | _label.set_autowrap(true) 177 | 178 | func _physics_process(delta): 179 | if(_state == STATE_OUTPUT): # Output 180 | if(_buffer.size() == 0): 181 | set_state(STATE_WAITING) 182 | emit_signal("buff_end") 183 | return 184 | 185 | var o = _buffer[0] # Calling this var 'o' was one of my biggest mistakes during the development of this code. I'm sorry about this. 186 | 187 | if (o["buff_type"] == BUFF_DEBUG): # ---- It's a debug! ---- 188 | if(o["debug_label"] == false): 189 | if(o["debug_arg"] == null): 190 | print(self.call(o["debug_function"])) 191 | else: 192 | print(self.call(o["debug_function"],o["debug_arg"])) 193 | else: 194 | if(o["debug_arg"] == null): 195 | print(_label.call(o["debug_function"])) 196 | else: 197 | print(_label.call(o["debug_function"],o["debug_arg"])) 198 | _buffer.pop_front() 199 | elif (o["buff_type"] == BUFF_TEXT): # ---- It's a text! ---- 200 | # -- Print Text -- 201 | 202 | if(o["buff_tag"] != "" and _buff_beginning == true): 203 | emit_signal("tag_buff", o["buff_tag"]) 204 | 205 | if (_turbo): # In case of turbo, print everything on this buff 206 | o["buff_vel"] = 0 207 | 208 | if(o["buff_vel"] == 0): # If the velocity is 0, than just print everything 209 | while(o["buff_text"] != ""): # Not optimal (not really printing everything at the same time); but is the only way to work with line break 210 | if(AUTO_SKIP_WORDS and (o["buff_text"][0] == " " or _buff_beginning)): 211 | _skip_word() 212 | _label_print(o["buff_text"][0]) 213 | _buff_beginning = false 214 | o["buff_text"] = o["buff_text"].right(1) 215 | if(_max_lines_reached == true): 216 | break 217 | 218 | else: # Else, print each character according to velocity 219 | _output_delay_limit = o["buff_vel"] 220 | if(_buff_beginning): 221 | _output_delay = _output_delay_limit + delta 222 | else: 223 | _output_delay += delta 224 | if(_output_delay > _output_delay_limit): 225 | if(AUTO_SKIP_WORDS and (o["buff_text"][0] == " " or _buff_beginning)): 226 | _skip_word() 227 | _label_print(o["buff_text"][0]) 228 | _buff_beginning = false 229 | _output_delay -= _output_delay_limit 230 | o["buff_text"] = o["buff_text"].right(1) 231 | # -- Popout Buff -- 232 | if (o["buff_text"] == ""): # This buff finished, so pop it out of the array 233 | _buffer.pop_front() 234 | _buff_beginning = true 235 | _output_delay = 0 236 | elif (o["buff_type"] == BUFF_SILENCE): # ---- It's a silence! ---- 237 | if(o["buff_tag"] != "" and _buff_beginning == true): 238 | emit_signal("tag_buff", o["buff_tag"]) 239 | _buff_beginning = false 240 | _output_delay_limit = o["buff_length"] # Length of the silence 241 | _output_delay += delta 242 | if(_output_delay > _output_delay_limit): 243 | _output_delay = 0 244 | _buff_beginning = true 245 | _buffer.pop_front() 246 | elif (o["buff_type"] == BUFF_BREAK): # ---- It's a break! ---- 247 | if(o["buff_tag"] != "" and _buff_beginning == true): 248 | emit_signal("tag_buff", o["buff_tag"]) 249 | _buff_beginning = false 250 | if(_turbo): # Ignore this break 251 | _buffer.pop_front() 252 | elif(!_on_break): 253 | emit_signal("enter_break") 254 | _on_break = true 255 | elif (o["buff_type"] == BUFF_INPUT): # ---- It's an Input! ---- 256 | if(o["buff_tag"] != ""and _buff_beginning == true): 257 | emit_signal("tag_buff", o["buff_tag"]) 258 | _buff_beginning = false 259 | set_state(STATE_INPUT) 260 | _buffer.pop_front() 261 | elif (o["buff_type"] == BUFF_CLEAR): # ---- It's a clear command! ---- 262 | if(o["buff_tag"] != ""and _buff_beginning == true): 263 | emit_signal("tag_buff", o["buff_tag"]) 264 | _buff_beginning = false 265 | _label.set_text("") 266 | _buffer.pop_front() 267 | emit_signal("buff_cleared") 268 | elif(_state == STATE_INPUT): 269 | if BLINKING_INPUT: 270 | _blink_input_timer += delta 271 | if(_blink_input_timer > _input_timer_limit): 272 | _blink_input_timer -= _input_timer_limit 273 | _blink_input() 274 | 275 | pass 276 | 277 | func _input(event): 278 | # User is just scrolling the text 279 | if SCROLL_SKIPPED_LINES and (event.is_action_pressed("ui_up") or event.is_action_pressed("ui_down")): 280 | if event.is_action_pressed("ui_up"): 281 | if(_label.get_lines_skipped() > 0): 282 | _label.set_lines_skipped(_label.get_lines_skipped()-1) 283 | else: 284 | if(_label.get_lines_skipped() < _label.get_line_count()-_max_lines): 285 | _label.set_lines_skipped(_label.get_lines_skipped()+1) 286 | elif(_state == 1 and _on_break): # If its on a break 287 | if event.is_action_pressed("ui_accept"): 288 | emit_signal("resume_break") 289 | _buffer.pop_front() # Pop out break buff 290 | _on_break = false 291 | elif(_state == 2): # If its on the input state 292 | if(BLINKING_INPUT): # Stop blinking line while inputing 293 | _blink_input(true) 294 | 295 | var input = _label.get_text().right(_input_index) # Get Input 296 | input = input.replace("\n","") 297 | 298 | if event.is_action_pressed("ui_back"): # Delete last character 299 | _delete_last_character(true) 300 | elif event.is_action_pressed("ui_accept"): # Finish input 301 | emit_signal("input_enter", input) 302 | if(!PRINT_INPUT): # Delete input 303 | var i = _label.get_text().length() - _input_index 304 | while(i > 0): 305 | _delete_last_character() 306 | i-=1 307 | set_state(STATE_OUTPUT) 308 | 309 | elif(event is InputEventKey and event.unicode >= 32 and event.unicode <= 126): # Add character 310 | if(INPUT_CHARACTERS_LIMIT < 0 or input.length() < INPUT_CHARACTERS_LIMIT): 311 | _label_print(_ARRAY_CHARS[event.unicode-32]) 312 | else: 313 | pass 314 | else: 315 | pass 316 | 317 | # Private 318 | func _clear_skipped_lines(): 319 | var i = 0 320 | var n = 0 321 | while i < _label.get_lines_skipped(): 322 | n = _label.get_text().findn("\n", n)+1 323 | i+=1 324 | _label.set_text(_label.get_text().right(n)) 325 | _label.set_lines_skipped(0) 326 | 327 | func _blink_input(reset = false): 328 | if(reset == true): 329 | if(_blink_input_visible): 330 | _delete_last_character() 331 | _blink_input_visible = false 332 | _blink_input_timer = 0 333 | return 334 | if(_blink_input_visible): 335 | _delete_last_character() 336 | _blink_input_visible = false 337 | else: 338 | _blink_input_visible = true 339 | _label_print("_") 340 | 341 | func _delete_last_character(scrollup = false): 342 | var n = _label.get_line_count() 343 | _label.set_text(_label.get_text().left(_label.get_text().length()-1)) 344 | if( scrollup and n > _label.get_line_count() and _label.get_lines_skipped() > 0 and _blink_input_visible == false): 345 | _label.set_lines_skipped(_label.get_lines_skipped()-1) 346 | 347 | func _get_last_line(): 348 | var i = _label.get_text().rfind("\n") 349 | if (i == -1): 350 | return _label.get_text() 351 | return _label.get_text().substr(i,_label.get_text().length()-i) 352 | 353 | func _has_to_skip_word(word): # what an awful name 354 | var ret = false 355 | var n = _label.get_line_count() 356 | _label.set_text(_label.get_text() + word) 357 | if(_label.get_line_count() > n): 358 | ret = true 359 | _label.set_text(_label.get_text().left(_label.get_text().length()-word.length())) #omg 360 | return ret 361 | 362 | func _skip_word(): 363 | var ot = _buffer[0]["buff_text"] 364 | 365 | # which comes first, a space or a new line (else, till the end) 366 | var f_space = ot.findn(" ",1) 367 | if f_space == -1: 368 | f_space = ot.length() 369 | var f_newline = ot.findn("\n",1) 370 | if f_newline == -1: 371 | f_newline = ot.length() 372 | var length = min(f_space, f_newline) 373 | 374 | if(_has_to_skip_word(ot.substr(0,length))): 375 | 376 | if(_buffer[0]["buff_text"][0] == " "): 377 | 378 | _buffer[0]["buff_text"][0] = "\n" 379 | else: 380 | _buffer[0]["buff_text"] = _buffer[0]["buff_text"].insert(0,"\n") 381 | 382 | func _label_print(t): # Add text to the label 383 | var n = _label.get_line_count() 384 | _label.set_text(_label.get_text() + t) 385 | if(_label.get_line_count() > n): # If number of lines increased 386 | if(_label.get_line_count()-_label.get_lines_skipped() > _max_lines): # If it exceeds _max_lines 387 | # Check if it is a rogue blinking input 388 | if(_blink_input_visible == true): 389 | _blink_input(true) 390 | return 391 | 392 | if(_state == 1 and BREAK_ON_MAX_LINES and _max_lines_reached == false): # Add a break when maximum lines are reached 393 | _delete_last_character() 394 | _max_lines_reached = true 395 | _buffer[0]["buff_text"] = t + _buffer[0]["buff_text"] 396 | buff_break("", true) 397 | return t 398 | 399 | if(_max_lines_reached): # Reset maximum lines break 400 | _max_lines_reached = false 401 | 402 | if(SCROLL_ON_MAX_LINES): # Scroll text, or clear everything 403 | _label.set_lines_skipped(_label.get_lines_skipped()+1) 404 | else: 405 | _label.set_lines_skipped(_label.get_lines_skipped()+_max_lines) 406 | 407 | if (t != "\n" and n > 0): # Add a line breaker, so the engine will be able to get each line 408 | _label.set_text(_label.get_text().insert( _label.get_text().length()-1,"\n")) 409 | 410 | if(LOG_SKIPPED_LINES == false): # Delete skipped lines 411 | _clear_skipped_lines() 412 | return t 413 | -------------------------------------------------------------------------------- /ink-engine-runtime/NativeFunctionCall.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Runtime 5 | { 6 | internal class NativeFunctionCall : Runtime.Object 7 | { 8 | public const string Add = "+"; 9 | public const string Subtract = "-"; 10 | public const string Divide = "/"; 11 | public const string Multiply = "*"; 12 | public const string Mod = "%"; 13 | public const string Negate = "_"; // distinguish from "-" for subtraction 14 | 15 | public const string Equal = "=="; 16 | public const string Greater = ">"; 17 | public const string Less = "<"; 18 | public const string GreaterThanOrEquals = ">="; 19 | public const string LessThanOrEquals = "<="; 20 | public const string NotEquals = "!="; 21 | public const string Not = "!"; 22 | 23 | 24 | 25 | public const string And = "&&"; 26 | public const string Or = "||"; 27 | 28 | public const string Min = "MIN"; 29 | public const string Max = "MAX"; 30 | 31 | public const string Has = "?"; 32 | public const string Hasnt = "!?"; 33 | public const string Intersect = "^"; 34 | 35 | public const string ListMin = "LIST_MIN"; 36 | public const string ListMax = "LIST_MAX"; 37 | public const string All = "LIST_ALL"; 38 | public const string Count = "LIST_COUNT"; 39 | public const string ValueOfList = "LIST_VALUE"; 40 | public const string Invert = "LIST_INVERT"; 41 | 42 | public static NativeFunctionCall CallWithName(string functionName) 43 | { 44 | return new NativeFunctionCall (functionName); 45 | } 46 | 47 | public static bool CallExistsWithName(string functionName) 48 | { 49 | GenerateNativeFunctionsIfNecessary (); 50 | return _nativeFunctions.ContainsKey (functionName); 51 | } 52 | 53 | public string name { 54 | get { 55 | return _name; 56 | } 57 | protected set { 58 | _name = value; 59 | if( !_isPrototype ) 60 | _prototype = _nativeFunctions [_name]; 61 | } 62 | } 63 | string _name; 64 | 65 | public int numberOfParameters { 66 | get { 67 | if (_prototype) { 68 | return _prototype.numberOfParameters; 69 | } else { 70 | return _numberOfParameters; 71 | } 72 | } 73 | protected set { 74 | _numberOfParameters = value; 75 | } 76 | } 77 | 78 | int _numberOfParameters; 79 | 80 | public Runtime.Object Call(List parameters) 81 | { 82 | if (_prototype) { 83 | return _prototype.Call(parameters); 84 | } 85 | 86 | if (numberOfParameters != parameters.Count) { 87 | throw new System.Exception ("Unexpected number of parameters"); 88 | } 89 | 90 | bool hasList = false; 91 | foreach (var p in parameters) { 92 | if (p is Void) 93 | throw new StoryException ("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?"); 94 | if (p is ListValue) 95 | hasList = true; 96 | } 97 | 98 | // Binary operations on lists are treated outside of the standard coerscion rules 99 | if( parameters.Count == 2 && hasList ) 100 | return CallBinaryListOperation (parameters); 101 | 102 | var coercedParams = CoerceValuesToSingleType (parameters); 103 | ValueType coercedType = coercedParams[0].valueType; 104 | 105 | if (coercedType == ValueType.Int) { 106 | return Call (coercedParams); 107 | } else if (coercedType == ValueType.Float) { 108 | return Call (coercedParams); 109 | } else if (coercedType == ValueType.String) { 110 | return Call (coercedParams); 111 | } else if (coercedType == ValueType.DivertTarget) { 112 | return Call (coercedParams); 113 | } else if (coercedType == ValueType.List) { 114 | return Call (coercedParams); 115 | } 116 | 117 | return null; 118 | } 119 | 120 | Value Call(List parametersOfSingleType) 121 | { 122 | Value param1 = (Value) parametersOfSingleType [0]; 123 | ValueType valType = param1.valueType; 124 | 125 | var val1 = (Value)param1; 126 | 127 | int paramCount = parametersOfSingleType.Count; 128 | 129 | if (paramCount == 2 || paramCount == 1) { 130 | 131 | object opForTypeObj = null; 132 | if (!_operationFuncs.TryGetValue (valType, out opForTypeObj)) { 133 | throw new StoryException ("Cannot perform operation '"+this.name+"' on "+valType); 134 | } 135 | 136 | // Binary 137 | if (paramCount == 2) { 138 | Value param2 = (Value) parametersOfSingleType [1]; 139 | 140 | var val2 = (Value)param2; 141 | 142 | var opForType = (BinaryOp)opForTypeObj; 143 | 144 | // Return value unknown until it's evaluated 145 | object resultVal = opForType (val1.value, val2.value); 146 | 147 | return Value.Create (resultVal); 148 | } 149 | 150 | // Unary 151 | else { 152 | 153 | var opForType = (UnaryOp)opForTypeObj; 154 | 155 | var resultVal = opForType (val1.value); 156 | 157 | return Value.Create (resultVal); 158 | } 159 | } 160 | 161 | else { 162 | throw new System.Exception ("Unexpected number of parameters to NativeFunctionCall: " + parametersOfSingleType.Count); 163 | } 164 | } 165 | 166 | Value CallBinaryListOperation (List parameters) 167 | { 168 | // List-Int addition/subtraction returns a List (e.g. "alpha" + 1 = "beta") 169 | if ((name == "+" || name == "-") && parameters [0] is ListValue && parameters [1] is IntValue) 170 | return CallListIncrementOperation (parameters); 171 | 172 | var v1 = parameters [0] as Value; 173 | var v2 = parameters [1] as Value; 174 | 175 | // And/or with any other type requires coerscion to bool (int) 176 | if ((name == "&&" || name == "||") && (v1.valueType != ValueType.List || v2.valueType != ValueType.List)) { 177 | var op = _operationFuncs [ValueType.Int] as BinaryOp; 178 | var result = (int)op (v1.isTruthy ? 1 : 0, v2.isTruthy ? 1 : 0); 179 | return new IntValue (result); 180 | } 181 | 182 | // Normal (list • list) operation 183 | if (v1.valueType == ValueType.List && v2.valueType == ValueType.List) 184 | return Call (new List { v1, v2 }); 185 | 186 | throw new StoryException ("Can not call use '" + name + "' operation on " + v1.valueType + " and " + v2.valueType); 187 | } 188 | 189 | Value CallListIncrementOperation (List listIntParams) 190 | { 191 | var listVal = (ListValue)listIntParams [0]; 192 | var intVal = (IntValue)listIntParams [1]; 193 | 194 | 195 | var resultRawList = new InkList (); 196 | 197 | foreach (var listItemWithValue in listVal.value) { 198 | var listItem = listItemWithValue.Key; 199 | var listItemValue = listItemWithValue.Value; 200 | 201 | // Find + or - operation 202 | var intOp = (BinaryOp)_operationFuncs [ValueType.Int]; 203 | 204 | // Return value unknown until it's evaluated 205 | int targetInt = (int) intOp (listItemValue, intVal.value); 206 | 207 | // Find this item's origin (linear search should be ok, should be short haha) 208 | ListDefinition itemOrigin = null; 209 | foreach (var origin in listVal.value.origins) { 210 | if (origin.name == listItem.originName) { 211 | itemOrigin = origin; 212 | break; 213 | } 214 | } 215 | if (itemOrigin != null) { 216 | InkListItem incrementedItem; 217 | if (itemOrigin.TryGetItemWithValue (targetInt, out incrementedItem)) 218 | resultRawList.Add (incrementedItem, targetInt); 219 | } 220 | } 221 | 222 | return new ListValue (resultRawList); 223 | } 224 | 225 | List CoerceValuesToSingleType(List parametersIn) 226 | { 227 | ValueType valType = ValueType.Int; 228 | 229 | ListValue specialCaseList = null; 230 | 231 | // Find out what the output type is 232 | // "higher level" types infect both so that binary operations 233 | // use the same type on both sides. e.g. binary operation of 234 | // int and float causes the int to be casted to a float. 235 | foreach (var obj in parametersIn) { 236 | var val = (Value)obj; 237 | if (val.valueType > valType) { 238 | valType = val.valueType; 239 | } 240 | 241 | if (val.valueType == ValueType.List) { 242 | specialCaseList = val as ListValue; 243 | } 244 | } 245 | 246 | // Coerce to this chosen type 247 | var parametersOut = new List (); 248 | 249 | // Special case: Coercing to Ints to Lists 250 | // We have to do it early when we have both parameters 251 | // to hand - so that we can make use of the List's origin 252 | if (valType == ValueType.List) { 253 | 254 | foreach (Value val in parametersIn) { 255 | if (val.valueType == ValueType.List) { 256 | parametersOut.Add (val); 257 | } else if (val.valueType == ValueType.Int) { 258 | int intVal = (int)val.valueObject; 259 | var list = specialCaseList.value.originOfMaxItem; 260 | 261 | InkListItem item; 262 | if (list.TryGetItemWithValue (intVal, out item)) { 263 | var castedValue = new ListValue (item, intVal); 264 | parametersOut.Add (castedValue); 265 | } else 266 | throw new StoryException ("Could not find List item with the value " + intVal + " in " + list.name); 267 | } else 268 | throw new StoryException ("Cannot mix Lists and " + val.valueType + " values in this operation"); 269 | } 270 | 271 | } 272 | 273 | // Normal Coercing (with standard casting) 274 | else { 275 | foreach (Value val in parametersIn) { 276 | var castedValue = val.Cast (valType); 277 | parametersOut.Add (castedValue); 278 | } 279 | } 280 | 281 | return parametersOut; 282 | } 283 | 284 | public NativeFunctionCall(string name) 285 | { 286 | GenerateNativeFunctionsIfNecessary (); 287 | 288 | this.name = name; 289 | } 290 | 291 | // Require default constructor for serialisation 292 | public NativeFunctionCall() { 293 | GenerateNativeFunctionsIfNecessary (); 294 | } 295 | 296 | // Only called internally to generate prototypes 297 | NativeFunctionCall (string name, int numberOfParamters) 298 | { 299 | _isPrototype = true; 300 | this.name = name; 301 | this.numberOfParameters = numberOfParamters; 302 | } 303 | 304 | static void GenerateNativeFunctionsIfNecessary() 305 | { 306 | if (_nativeFunctions == null) { 307 | _nativeFunctions = new Dictionary (); 308 | 309 | // Int operations 310 | AddIntBinaryOp(Add, (x, y) => x + y); 311 | AddIntBinaryOp(Subtract, (x, y) => x - y); 312 | AddIntBinaryOp(Multiply, (x, y) => x * y); 313 | AddIntBinaryOp(Divide, (x, y) => x / y); 314 | AddIntBinaryOp(Mod, (x, y) => x % y); 315 | AddIntUnaryOp (Negate, x => -x); 316 | 317 | AddIntBinaryOp(Equal, (x, y) => x == y ? 1 : 0); 318 | AddIntBinaryOp(Greater, (x, y) => x > y ? 1 : 0); 319 | AddIntBinaryOp(Less, (x, y) => x < y ? 1 : 0); 320 | AddIntBinaryOp(GreaterThanOrEquals, (x, y) => x >= y ? 1 : 0); 321 | AddIntBinaryOp(LessThanOrEquals, (x, y) => x <= y ? 1 : 0); 322 | AddIntBinaryOp(NotEquals, (x, y) => x != y ? 1 : 0); 323 | AddIntUnaryOp (Not, x => (x == 0) ? 1 : 0); 324 | 325 | AddIntBinaryOp(And, (x, y) => x != 0 && y != 0 ? 1 : 0); 326 | AddIntBinaryOp(Or, (x, y) => x != 0 || y != 0 ? 1 : 0); 327 | 328 | AddIntBinaryOp(Max, (x, y) => Math.Max(x, y)); 329 | AddIntBinaryOp(Min, (x, y) => Math.Min(x, y)); 330 | 331 | // Float operations 332 | AddFloatBinaryOp(Add, (x, y) => x + y); 333 | AddFloatBinaryOp(Subtract, (x, y) => x - y); 334 | AddFloatBinaryOp(Multiply, (x, y) => x * y); 335 | AddFloatBinaryOp(Divide, (x, y) => x / y); 336 | AddFloatBinaryOp(Mod, (x, y) => x % y); // TODO: Is this the operation we want for floats? 337 | AddFloatUnaryOp (Negate, x => -x); 338 | 339 | AddFloatBinaryOp(Equal, (x, y) => x == y ? (int)1 : (int)0); 340 | AddFloatBinaryOp(Greater, (x, y) => x > y ? (int)1 : (int)0); 341 | AddFloatBinaryOp(Less, (x, y) => x < y ? (int)1 : (int)0); 342 | AddFloatBinaryOp(GreaterThanOrEquals, (x, y) => x >= y ? (int)1 : (int)0); 343 | AddFloatBinaryOp(LessThanOrEquals, (x, y) => x <= y ? (int)1 : (int)0); 344 | AddFloatBinaryOp(NotEquals, (x, y) => x != y ? (int)1 : (int)0); 345 | AddFloatUnaryOp (Not, x => (x == 0.0f) ? (int)1 : (int)0); 346 | 347 | AddFloatBinaryOp(And, (x, y) => x != 0.0f && y != 0.0f ? (int)1 : (int)0); 348 | AddFloatBinaryOp(Or, (x, y) => x != 0.0f || y != 0.0f ? (int)1 : (int)0); 349 | 350 | AddFloatBinaryOp(Max, (x, y) => Math.Max(x, y)); 351 | AddFloatBinaryOp(Min, (x, y) => Math.Min(x, y)); 352 | 353 | // String operations 354 | AddStringBinaryOp(Add, (x, y) => x + y); // concat 355 | AddStringBinaryOp(Equal, (x, y) => x.Equals(y) ? (int)1 : (int)0); 356 | AddStringBinaryOp (NotEquals, (x, y) => !x.Equals (y) ? (int)1 : (int)0); 357 | AddStringBinaryOp (Has, (x, y) => x.Contains(y) ? (int)1 : (int)0); 358 | 359 | // List operations 360 | AddListBinaryOp (Add, (x, y) => x.Union (y)); 361 | AddListBinaryOp (Subtract, (x, y) => x.Without(y)); 362 | AddListBinaryOp (Has, (x, y) => x.Contains (y) ? (int)1 : (int)0); 363 | AddListBinaryOp (Hasnt, (x, y) => x.Contains (y) ? (int)0 : (int)1); 364 | AddListBinaryOp (Intersect, (x, y) => x.Intersect (y)); 365 | 366 | AddListBinaryOp (Equal, (x, y) => x.Equals(y) ? (int)1 : (int)0); 367 | AddListBinaryOp (Greater, (x, y) => x.GreaterThan(y) ? (int)1 : (int)0); 368 | AddListBinaryOp (Less, (x, y) => x.LessThan(y) ? (int)1 : (int)0); 369 | AddListBinaryOp (GreaterThanOrEquals, (x, y) => x.GreaterThanOrEquals(y) ? (int)1 : (int)0); 370 | AddListBinaryOp (LessThanOrEquals, (x, y) => x.LessThanOrEquals(y) ? (int)1 : (int)0); 371 | AddListBinaryOp (NotEquals, (x, y) => !x.Equals(y) ? (int)1 : (int)0); 372 | 373 | AddListBinaryOp (And, (x, y) => x.Count > 0 && y.Count > 0 ? (int)1 : (int)0); 374 | AddListBinaryOp (Or, (x, y) => x.Count > 0 || y.Count > 0 ? (int)1 : (int)0); 375 | 376 | AddListUnaryOp (Not, x => x.Count == 0 ? (int)1 : (int)0); 377 | 378 | // Placeholders to ensure that these special case functions can exist, 379 | // since these function is never actually run, and is special cased in Call 380 | AddListUnaryOp (Invert, x => x.inverse); 381 | AddListUnaryOp (All, x => x.all); 382 | AddListUnaryOp (ListMin, (x) => x.MinAsList()); 383 | AddListUnaryOp (ListMax, (x) => x.MaxAsList()); 384 | AddListUnaryOp (Count, (x) => x.Count); 385 | AddListUnaryOp (ValueOfList, (x) => x.maxItem.Value); 386 | 387 | // Special case: The only operation you can do on divert target values 388 | BinaryOp divertTargetsEqual = (Path d1, Path d2) => { 389 | return d1.Equals (d2) ? 1 : 0; 390 | }; 391 | AddOpToNativeFunc (Equal, 2, ValueType.DivertTarget, divertTargetsEqual); 392 | 393 | } 394 | } 395 | 396 | void AddOpFuncForType(ValueType valType, object op) 397 | { 398 | if (_operationFuncs == null) { 399 | _operationFuncs = new Dictionary (); 400 | } 401 | 402 | _operationFuncs [valType] = op; 403 | } 404 | 405 | static void AddOpToNativeFunc(string name, int args, ValueType valType, object op) 406 | { 407 | NativeFunctionCall nativeFunc = null; 408 | if (!_nativeFunctions.TryGetValue (name, out nativeFunc)) { 409 | nativeFunc = new NativeFunctionCall (name, args); 410 | _nativeFunctions [name] = nativeFunc; 411 | } 412 | 413 | nativeFunc.AddOpFuncForType (valType, op); 414 | } 415 | 416 | static void AddIntBinaryOp(string name, BinaryOp op) 417 | { 418 | AddOpToNativeFunc (name, 2, ValueType.Int, op); 419 | } 420 | 421 | static void AddIntUnaryOp(string name, UnaryOp op) 422 | { 423 | AddOpToNativeFunc (name, 1, ValueType.Int, op); 424 | } 425 | 426 | static void AddFloatBinaryOp(string name, BinaryOp op) 427 | { 428 | AddOpToNativeFunc (name, 2, ValueType.Float, op); 429 | } 430 | 431 | static void AddStringBinaryOp(string name, BinaryOp op) 432 | { 433 | AddOpToNativeFunc (name, 2, ValueType.String, op); 434 | } 435 | 436 | static void AddListBinaryOp (string name, BinaryOp op) 437 | { 438 | AddOpToNativeFunc (name, 2, ValueType.List, op); 439 | } 440 | 441 | static void AddListUnaryOp (string name, UnaryOp op) 442 | { 443 | AddOpToNativeFunc (name, 1, ValueType.List, op); 444 | } 445 | 446 | static void AddFloatUnaryOp(string name, UnaryOp op) 447 | { 448 | AddOpToNativeFunc (name, 1, ValueType.Float, op); 449 | } 450 | 451 | public override string ToString () 452 | { 453 | return "Native '" + name + "'"; 454 | } 455 | 456 | delegate object BinaryOp(T left, T right); 457 | delegate object UnaryOp(T val); 458 | 459 | NativeFunctionCall _prototype; 460 | bool _isPrototype; 461 | 462 | // Operations for each data type, for a single operation (e.g. "+") 463 | Dictionary _operationFuncs; 464 | 465 | static Dictionary _nativeFunctions; 466 | 467 | } 468 | } 469 | 470 | -------------------------------------------------------------------------------- /ink-engine-runtime/InkList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace Ink.Runtime 5 | { 6 | /// 7 | /// The underlying type for a list item in ink. It stores the original list definition 8 | /// name as well as the item name, but without the value of the item. When the value is 9 | /// stored, it's stored in a KeyValuePair of InkListItem and int. 10 | /// 11 | public struct InkListItem 12 | { 13 | /// 14 | /// The name of the list where the item was originally defined. 15 | /// 16 | public readonly string originName; 17 | 18 | /// 19 | /// The main name of the item as defined in ink. 20 | /// 21 | public readonly string itemName; 22 | 23 | /// 24 | /// Create an item with the given original list definition name, and the name of this 25 | /// item. 26 | /// 27 | public InkListItem (string originName, string itemName) 28 | { 29 | this.originName = originName; 30 | this.itemName = itemName; 31 | } 32 | 33 | /// 34 | /// Create an item from a dot-separted string of the form "listDefinitionName.listItemName". 35 | /// 36 | public InkListItem (string fullName) 37 | { 38 | var nameParts = fullName.Split ('.'); 39 | this.originName = nameParts [0]; 40 | this.itemName = nameParts [1]; 41 | } 42 | 43 | internal static InkListItem Null { 44 | get { 45 | return new InkListItem (null, null); 46 | } 47 | } 48 | 49 | internal bool isNull { 50 | get { 51 | return originName == null && itemName == null; 52 | } 53 | } 54 | 55 | /// 56 | /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName". 57 | /// 58 | public string fullName { 59 | get { 60 | return (originName ?? "?") + "." + itemName; 61 | } 62 | } 63 | 64 | /// 65 | /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName". 66 | /// Calls fullName internally. 67 | /// 68 | public override string ToString () 69 | { 70 | return fullName; 71 | } 72 | 73 | /// 74 | /// Is this item the same as another item? 75 | /// 76 | public override bool Equals (object obj) 77 | { 78 | if (obj is InkListItem) { 79 | var otherItem = (InkListItem)obj; 80 | return otherItem.itemName == itemName 81 | && otherItem.originName == originName; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | /// 88 | /// Get the hashcode for an item. 89 | /// 90 | public override int GetHashCode () 91 | { 92 | int originCode = 0; 93 | int itemCode = itemName.GetHashCode (); 94 | if (originName != null) 95 | originCode = originName.GetHashCode (); 96 | 97 | return originCode + itemCode; 98 | } 99 | } 100 | 101 | /// 102 | /// The InkList is the underlying type that's used to store an instance of a 103 | /// list in ink. It's not used for the *definition* of the list, but for a list 104 | /// value that's stored in a variable. 105 | /// Somewhat confusingly, it's backed by a C# Dictionary, and has nothing to 106 | /// do with a C# List! 107 | /// 108 | public class InkList : Dictionary 109 | { 110 | /// 111 | /// Create a new empty ink list. 112 | /// 113 | public InkList () { } 114 | 115 | /// 116 | /// Create a new ink list that contains the same contents as another list. 117 | /// 118 | public InkList (InkList otherList) : base (otherList) { _originNames = otherList.originNames; } 119 | 120 | /// 121 | /// Create a new empty ink list that's intended to hold items from a particular origin 122 | /// list definition. The origin Story is needed in order to be able to look up that definition. 123 | /// 124 | public InkList (string singleOriginListName, Story originStory) 125 | { 126 | SetInitialOriginName (singleOriginListName); 127 | 128 | ListDefinition def; 129 | if (originStory.listDefinitions.TryGetDefinition (singleOriginListName, out def)) 130 | origins = new List { def }; 131 | else 132 | throw new System.Exception ("InkList origin could not be found in story when constructing new list: " + singleOriginListName); 133 | } 134 | 135 | internal InkList (KeyValuePair singleElement) 136 | { 137 | Add (singleElement.Key, singleElement.Value); 138 | } 139 | 140 | /// 141 | /// Adds the given item to the ink list. Note that the item must come from a list definition that 142 | /// is already "known" to this list, so that the item's value can be looked up. By "known", we mean 143 | /// that it already has items in it from that source, or it did at one point - it can't be a 144 | /// completely fresh empty list, or a list that only contains items from a different list definition. 145 | /// 146 | public void AddItem (InkListItem item) 147 | { 148 | if (item.originName == null) { 149 | AddItem (item.itemName); 150 | return; 151 | } 152 | 153 | foreach (var origin in origins) { 154 | if (origin.name == item.originName) { 155 | int intVal; 156 | if (origin.TryGetValueForItem (item, out intVal)) { 157 | this [item] = intVal; 158 | return; 159 | } else { 160 | throw new System.Exception ("Could not add the item " + item + " to this list because it doesn't exist in the original list definition in ink."); 161 | } 162 | } 163 | } 164 | 165 | throw new System.Exception ("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found."); 166 | } 167 | 168 | /// 169 | /// Adds the given item to the ink list, attempting to find the origin list definition that it belongs to. 170 | /// The item must therefore come from a list definition that is already "known" to this list, so that the 171 | /// item's value can be looked up. By "known", we mean that it already has items in it from that source, or 172 | /// it did at one point - it can't be a completely fresh empty list, or a list that only contains items from 173 | /// a different list definition. 174 | /// 175 | public void AddItem (string itemName) 176 | { 177 | ListDefinition foundListDef = null; 178 | 179 | foreach (var origin in origins) { 180 | if (origin.ContainsItemWithName (itemName)) { 181 | if (foundListDef != null) { 182 | throw new System.Exception ("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name); 183 | } else { 184 | foundListDef = origin; 185 | } 186 | } 187 | } 188 | 189 | if (foundListDef == null) 190 | throw new System.Exception ("Could not add the item " + itemName + " to this list because it isn't known to any list definitions previously associated with this list."); 191 | 192 | var item = new InkListItem (foundListDef.name, itemName); 193 | var itemVal = foundListDef.ValueForItem(item); 194 | this [item] = itemVal; 195 | } 196 | 197 | /// 198 | /// Returns true if this ink list contains an item with the given short name 199 | /// (ignoring the original list where it was defined). 200 | /// 201 | public bool ContainsItemNamed (string itemName) 202 | { 203 | foreach (var itemWithValue in this) { 204 | if (itemWithValue.Key.itemName == itemName) return true; 205 | } 206 | return false; 207 | } 208 | 209 | // Story has to set this so that the value knows its origin, 210 | // necessary for certain operations (e.g. interacting with ints). 211 | // Only the story has access to the full set of lists, so that 212 | // the origin can be resolved from the originListName. 213 | internal List origins; 214 | internal ListDefinition originOfMaxItem { 215 | get { 216 | if (origins == null) return null; 217 | 218 | var maxOriginName = maxItem.Key.originName; 219 | foreach (var origin in origins) { 220 | if (origin.name == maxOriginName) 221 | return origin; 222 | } 223 | 224 | return null; 225 | } 226 | } 227 | 228 | // Origin name needs to be serialised when content is empty, 229 | // assuming a name is availble, for list definitions with variable 230 | // that is currently empty. 231 | internal List originNames { 232 | get { 233 | if (this.Count > 0) { 234 | if (_originNames == null && this.Count > 0) 235 | _originNames = new List (); 236 | else 237 | _originNames.Clear (); 238 | 239 | foreach (var itemAndValue in this) 240 | _originNames.Add (itemAndValue.Key.originName); 241 | } 242 | 243 | return _originNames; 244 | } 245 | } 246 | List _originNames; 247 | 248 | internal void SetInitialOriginName (string initialOriginName) 249 | { 250 | _originNames = new List { initialOriginName }; 251 | } 252 | 253 | internal void SetInitialOriginNames (List initialOriginNames) 254 | { 255 | if (initialOriginNames == null) 256 | _originNames = null; 257 | else 258 | _originNames = new List(initialOriginNames); 259 | } 260 | 261 | /// 262 | /// Get the maximum item in the list, equivalent to calling LIST_MAX(list) in ink. 263 | /// 264 | public KeyValuePair maxItem { 265 | get { 266 | KeyValuePair max = new KeyValuePair(); 267 | foreach (var kv in this) { 268 | if (max.Key.isNull || kv.Value > max.Value) 269 | max = kv; 270 | } 271 | return max; 272 | } 273 | } 274 | 275 | /// 276 | /// Get the minimum item in the list, equivalent to calling LIST_MIN(list) in ink. 277 | /// 278 | public KeyValuePair minItem { 279 | get { 280 | var min = new KeyValuePair (); 281 | foreach (var kv in this) { 282 | if (min.Key.isNull || kv.Value < min.Value) 283 | min = kv; 284 | } 285 | return min; 286 | } 287 | } 288 | 289 | /// 290 | /// The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink 291 | /// 292 | public InkList inverse { 293 | get { 294 | var list = new InkList (); 295 | if (origins != null) { 296 | foreach (var origin in origins) { 297 | foreach (var itemAndValue in origin.items) { 298 | if (!this.ContainsKey (itemAndValue.Key)) 299 | list.Add (itemAndValue.Key, itemAndValue.Value); 300 | } 301 | } 302 | 303 | } 304 | return list; 305 | } 306 | } 307 | 308 | /// 309 | /// The list of all items from the original list definition, equivalent to calling 310 | /// LIST_ALL(list) in ink. 311 | /// 312 | public InkList all { 313 | get { 314 | var list = new InkList (); 315 | if (origins != null) { 316 | foreach (var origin in origins) { 317 | foreach (var itemAndValue in origin.items) 318 | list[itemAndValue.Key] = itemAndValue.Value; 319 | } 320 | } 321 | return list; 322 | } 323 | } 324 | 325 | /// 326 | /// Returns a new list that is the combination of the current list and one that's 327 | /// passed in. Equivalent to calling (list1 + list2) in ink. 328 | /// 329 | public InkList Union (InkList otherList) 330 | { 331 | var union = new InkList (this); 332 | foreach (var kv in otherList) { 333 | union [kv.Key] = kv.Value; 334 | } 335 | return union; 336 | } 337 | 338 | /// 339 | /// Returns a new list that is the intersection of the current list with another 340 | /// list that's passed in - i.e. a list of the items that are shared between the 341 | /// two other lists. Equivalent to calling (list1 ^ list2) in ink. 342 | /// 343 | public InkList Intersect (InkList otherList) 344 | { 345 | var intersection = new InkList (); 346 | foreach (var kv in this) { 347 | if (otherList.ContainsKey (kv.Key)) 348 | intersection.Add (kv.Key, kv.Value); 349 | } 350 | return intersection; 351 | } 352 | 353 | /// 354 | /// Returns a new list that's the same as the current one, except with the given items 355 | /// removed that are in the passed in list. Equivalent to calling (list1 - list2) in ink. 356 | /// 357 | /// List to remove. 358 | public InkList Without (InkList listToRemove) 359 | { 360 | var result = new InkList (this); 361 | foreach (var kv in listToRemove) 362 | result.Remove (kv.Key); 363 | return result; 364 | } 365 | 366 | /// 367 | /// Returns true if the current list contains all the items that are in the list that 368 | /// is passed in. Equivalent to calling (list1 ? list2) in ink. 369 | /// 370 | /// Other list. 371 | public bool Contains (InkList otherList) 372 | { 373 | foreach (var kv in otherList) { 374 | if (!this.ContainsKey (kv.Key)) return false; 375 | } 376 | return true; 377 | } 378 | 379 | /// 380 | /// Returns true if all the item values in the current list are greater than all the 381 | /// item values in the passed in list. Equivalent to calling (list1 > list2) in ink. 382 | /// 383 | public bool GreaterThan (InkList otherList) 384 | { 385 | if (Count == 0) return false; 386 | if (otherList.Count == 0) return true; 387 | 388 | // All greater 389 | return minItem.Value > otherList.maxItem.Value; 390 | } 391 | 392 | /// 393 | /// Returns true if the item values in the current list overlap or are all greater than 394 | /// the item values in the passed in list. None of the item values in the current list must 395 | /// fall below the item values in the passed in list. Equivalent to (list1 >= list2) in ink, 396 | /// or LIST_MIN(list1) >= LIST_MIN(list2) && LIST_MAX(list1) >= LIST_MAX(list2). 397 | /// 398 | public bool GreaterThanOrEquals (InkList otherList) 399 | { 400 | if (Count == 0) return false; 401 | if (otherList.Count == 0) return true; 402 | 403 | return minItem.Value >= otherList.minItem.Value 404 | && maxItem.Value >= otherList.maxItem.Value; 405 | } 406 | 407 | /// 408 | /// Returns true if all the item values in the current list are less than all the 409 | /// item values in the passed in list. Equivalent to calling (list1 < list2) in ink. 410 | /// 411 | public bool LessThan (InkList otherList) 412 | { 413 | if (otherList.Count == 0) return false; 414 | if (Count == 0) return true; 415 | 416 | return maxItem.Value < otherList.minItem.Value; 417 | } 418 | 419 | /// 420 | /// Returns true if the item values in the current list overlap or are all less than 421 | /// the item values in the passed in list. None of the item values in the current list must 422 | /// go above the item values in the passed in list. Equivalent to (list1 <= list2) in ink, 423 | /// or LIST_MAX(list1) <= LIST_MAX(list2) && LIST_MIN(list1) <= LIST_MIN(list2). 424 | /// 425 | public bool LessThanOrEquals (InkList otherList) 426 | { 427 | if (otherList.Count == 0) return false; 428 | if (Count == 0) return true; 429 | 430 | return maxItem.Value <= otherList.maxItem.Value 431 | && minItem.Value <= otherList.minItem.Value; 432 | } 433 | 434 | internal InkList MaxAsList () 435 | { 436 | if (Count > 0) 437 | return new InkList (maxItem); 438 | else 439 | return new InkList (); 440 | } 441 | 442 | internal InkList MinAsList () 443 | { 444 | if (Count > 0) 445 | return new InkList (minItem); 446 | else 447 | return new InkList (); 448 | } 449 | 450 | /// 451 | /// Returns true if the passed object is also an ink list that contains 452 | /// the same items as the current list, false otherwise. 453 | /// 454 | public override bool Equals (object other) 455 | { 456 | var otherRawList = other as InkList; 457 | if (otherRawList == null) return false; 458 | if (otherRawList.Count != Count) return false; 459 | 460 | foreach (var kv in this) { 461 | if (!otherRawList.ContainsKey (kv.Key)) 462 | return false; 463 | } 464 | 465 | return true; 466 | } 467 | 468 | /// 469 | /// Return the hashcode for this object, used for comparisons and inserting into dictionaries. 470 | /// 471 | public override int GetHashCode () 472 | { 473 | int ownHash = 0; 474 | foreach (var kv in this) 475 | ownHash += kv.Key.GetHashCode (); 476 | return ownHash; 477 | } 478 | 479 | /// 480 | /// Returns a string in the form "a, b, c" with the names of the items in the list, without 481 | /// the origin list definition names. Equivalent to writing {list} in ink. 482 | /// 483 | public override string ToString () 484 | { 485 | var ordered = new List> (); 486 | ordered.AddRange (this); 487 | ordered.Sort ((x, y) => x.Value.CompareTo (y.Value)); 488 | 489 | var sb = new StringBuilder (); 490 | for (int i = 0; i < ordered.Count; i++) { 491 | if (i > 0) 492 | sb.Append (", "); 493 | 494 | var item = ordered [i].Key; 495 | sb.Append (item.itemName); 496 | } 497 | 498 | return sb.ToString (); 499 | } 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /ink-engine-runtime/JsonSerialisation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Ink.Runtime 6 | { 7 | internal static class Json 8 | { 9 | public static List ListToJArray(List serialisables) where T : Runtime.Object 10 | { 11 | var jArray = new List (); 12 | foreach (var s in serialisables) { 13 | jArray.Add (RuntimeObjectToJToken(s)); 14 | } 15 | return jArray; 16 | } 17 | 18 | public static List JArrayToRuntimeObjList(List jArray, bool skipLast=false) where T : Runtime.Object 19 | { 20 | int count = jArray.Count; 21 | if (skipLast) 22 | count--; 23 | 24 | var list = new List (jArray.Count); 25 | 26 | for (int i = 0; i < count; i++) { 27 | var jTok = jArray [i]; 28 | var runtimeObj = JTokenToRuntimeObject (jTok) as T; 29 | list.Add (runtimeObj); 30 | } 31 | 32 | return list; 33 | } 34 | 35 | public static List JArrayToRuntimeObjList(List jArray, bool skipLast=false) 36 | { 37 | return JArrayToRuntimeObjList (jArray, skipLast); 38 | } 39 | 40 | public static Dictionary DictionaryRuntimeObjsToJObject(Dictionary dictionary) 41 | { 42 | var jsonObj = new Dictionary (); 43 | 44 | foreach (var keyVal in dictionary) { 45 | var runtimeObj = keyVal.Value as Runtime.Object; 46 | if (runtimeObj != null) 47 | jsonObj [keyVal.Key] = RuntimeObjectToJToken(runtimeObj); 48 | } 49 | 50 | return jsonObj; 51 | } 52 | 53 | public static Dictionary JObjectToDictionaryRuntimeObjs(Dictionary jObject) 54 | { 55 | var dict = new Dictionary (jObject.Count); 56 | 57 | foreach (var keyVal in jObject) { 58 | dict [keyVal.Key] = JTokenToRuntimeObject(keyVal.Value); 59 | } 60 | 61 | return dict; 62 | } 63 | 64 | public static Dictionary JObjectToIntDictionary(Dictionary jObject) 65 | { 66 | var dict = new Dictionary (jObject.Count); 67 | foreach (var keyVal in jObject) { 68 | dict [keyVal.Key] = (int)keyVal.Value; 69 | } 70 | return dict; 71 | } 72 | 73 | public static Dictionary IntDictionaryToJObject(Dictionary dict) 74 | { 75 | var jObj = new Dictionary (); 76 | foreach (var keyVal in dict) { 77 | jObj [keyVal.Key] = keyVal.Value; 78 | } 79 | return jObj; 80 | } 81 | 82 | // ---------------------- 83 | // JSON ENCODING SCHEME 84 | // ---------------------- 85 | // 86 | // Glue: "<>", "G<", "G>" 87 | // 88 | // ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop", 89 | // "choiceCnt", "turns", "visit", "seq", "thread", "done", "end" 90 | // 91 | // NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc 92 | // 93 | // Void: "void" 94 | // 95 | // Value: "^string value", "^^string value beginning with ^" 96 | // 5, 5.2 97 | // {"^->": "path.target"} 98 | // {"^var": "varname", "ci": 0} 99 | // 100 | // Container: [...] 101 | // [..., 102 | // { 103 | // "subContainerName": ..., 104 | // "#f": 5, // flags 105 | // "#n": "containerOwnName" // only if not redundant 106 | // } 107 | // ] 108 | // 109 | // Divert: {"->": "path.target", "c": true } 110 | // {"->": "path.target", "var": true} 111 | // {"f()": "path.func"} 112 | // {"->t->": "path.tunnel"} 113 | // {"x()": "externalFuncName", "exArgs": 5} 114 | // 115 | // Var Assign: {"VAR=": "varName", "re": true} // reassignment 116 | // {"temp=": "varName"} 117 | // 118 | // Var ref: {"VAR?": "varName"} 119 | // {"CNT?": "stitch name"} 120 | // 121 | // ChoicePoint: {"*": pathString, 122 | // "flg": 18 } 123 | // 124 | // Choice: Nothing too clever, it's only used in the save state, 125 | // there's not likely to be many of them. 126 | // 127 | // Tag: {"#": "the tag text"} 128 | public static Runtime.Object JTokenToRuntimeObject(object token) 129 | { 130 | if (token is int || token is float) { 131 | return Value.Create (token); 132 | } 133 | 134 | if (token is string) { 135 | string str = (string)token; 136 | 137 | // String value 138 | char firstChar = str[0]; 139 | if (firstChar == '^') 140 | return new StringValue (str.Substring (1)); 141 | else if( firstChar == '\n' && str.Length == 1) 142 | return new StringValue ("\n"); 143 | 144 | // Glue 145 | if (str == "<>") 146 | return new Runtime.Glue (GlueType.Bidirectional); 147 | else if(str == "G<") 148 | return new Runtime.Glue (GlueType.Left); 149 | else if(str == "G>") 150 | return new Runtime.Glue (GlueType.Right); 151 | 152 | // Control commands (would looking up in a hash set be faster?) 153 | for (int i = 0; i < _controlCommandNames.Length; ++i) { 154 | string cmdName = _controlCommandNames [i]; 155 | if (str == cmdName) { 156 | return new Runtime.ControlCommand ((ControlCommand.CommandType)i); 157 | } 158 | } 159 | 160 | // Native functions 161 | // "^" conflicts with the way to identify strings, so now 162 | // we know it's not a string, we can convert back to the proper 163 | // symbol for the operator. 164 | if (str == "L^") str = "^"; 165 | if( NativeFunctionCall.CallExistsWithName(str) ) 166 | return NativeFunctionCall.CallWithName (str); 167 | 168 | // Pop 169 | if (str == "->->") 170 | return Runtime.ControlCommand.PopTunnel (); 171 | else if (str == "~ret") 172 | return Runtime.ControlCommand.PopFunction (); 173 | 174 | // Void 175 | if (str == "void") 176 | return new Runtime.Void (); 177 | } 178 | 179 | if (token is Dictionary) { 180 | 181 | var obj = (Dictionary < string, object> )token; 182 | object propValue; 183 | 184 | // Divert target value to path 185 | if (obj.TryGetValue ("^->", out propValue)) 186 | return new DivertTargetValue (new Path ((string)propValue)); 187 | 188 | // VariablePointerValue 189 | if (obj.TryGetValue ("^var", out propValue)) { 190 | var varPtr = new VariablePointerValue ((string)propValue); 191 | if (obj.TryGetValue ("ci", out propValue)) 192 | varPtr.contextIndex = (int)propValue; 193 | return varPtr; 194 | } 195 | 196 | // Divert 197 | bool isDivert = false; 198 | bool pushesToStack = false; 199 | PushPopType divPushType = PushPopType.Function; 200 | bool external = false; 201 | if (obj.TryGetValue ("->", out propValue)) { 202 | isDivert = true; 203 | } 204 | else if (obj.TryGetValue ("f()", out propValue)) { 205 | isDivert = true; 206 | pushesToStack = true; 207 | divPushType = PushPopType.Function; 208 | } 209 | else if (obj.TryGetValue ("->t->", out propValue)) { 210 | isDivert = true; 211 | pushesToStack = true; 212 | divPushType = PushPopType.Tunnel; 213 | } 214 | else if (obj.TryGetValue ("x()", out propValue)) { 215 | isDivert = true; 216 | external = true; 217 | pushesToStack = false; 218 | divPushType = PushPopType.Function; 219 | } 220 | if (isDivert) { 221 | var divert = new Divert (); 222 | divert.pushesToStack = pushesToStack; 223 | divert.stackPushType = divPushType; 224 | divert.isExternal = external; 225 | 226 | string target = propValue.ToString (); 227 | 228 | if (obj.TryGetValue ("var", out propValue)) 229 | divert.variableDivertName = target; 230 | else 231 | divert.targetPathString = target; 232 | 233 | divert.isConditional = obj.TryGetValue("c", out propValue); 234 | 235 | if (external) { 236 | if (obj.TryGetValue ("exArgs", out propValue)) 237 | divert.externalArgs = (int)propValue; 238 | } 239 | 240 | return divert; 241 | } 242 | 243 | // Choice 244 | if (obj.TryGetValue ("*", out propValue)) { 245 | var choice = new ChoicePoint (); 246 | choice.pathStringOnChoice = propValue.ToString(); 247 | 248 | if (obj.TryGetValue ("flg", out propValue)) 249 | choice.flags = (int)propValue; 250 | 251 | return choice; 252 | } 253 | 254 | // Variable reference 255 | if (obj.TryGetValue ("VAR?", out propValue)) { 256 | return new VariableReference (propValue.ToString ()); 257 | } else if (obj.TryGetValue ("CNT?", out propValue)) { 258 | var readCountVarRef = new VariableReference (); 259 | readCountVarRef.pathStringForCount = propValue.ToString (); 260 | return readCountVarRef; 261 | } 262 | 263 | // Variable assignment 264 | bool isVarAss = false; 265 | bool isGlobalVar = false; 266 | if (obj.TryGetValue ("VAR=", out propValue)) { 267 | isVarAss = true; 268 | isGlobalVar = true; 269 | } else if (obj.TryGetValue ("temp=", out propValue)) { 270 | isVarAss = true; 271 | isGlobalVar = false; 272 | } 273 | if (isVarAss) { 274 | var varName = propValue.ToString (); 275 | var isNewDecl = !obj.TryGetValue("re", out propValue); 276 | var varAss = new VariableAssignment (varName, isNewDecl); 277 | varAss.isGlobal = isGlobalVar; 278 | return varAss; 279 | } 280 | 281 | // Tag 282 | if (obj.TryGetValue ("#", out propValue)) { 283 | return new Runtime.Tag ((string)propValue); 284 | } 285 | 286 | // List value 287 | if (obj.TryGetValue ("list", out propValue)) { 288 | var listContent = (Dictionary)propValue; 289 | var rawList = new InkList (); 290 | if (obj.TryGetValue ("origins", out propValue)) { 291 | var namesAsObjs = (List)propValue; 292 | rawList.SetInitialOriginNames (namesAsObjs.Cast().ToList()); 293 | } 294 | foreach (var nameToVal in listContent) { 295 | var item = new InkListItem (nameToVal.Key); 296 | var val = (int)nameToVal.Value; 297 | rawList.Add (item, val); 298 | } 299 | return new ListValue (rawList); 300 | } 301 | 302 | // Used when serialising save state only 303 | if (obj ["originalChoicePath"] != null) 304 | return JObjectToChoice (obj); 305 | } 306 | 307 | // Array is always a Runtime.Container 308 | if (token is List) { 309 | return JArrayToContainer((List)token); 310 | } 311 | 312 | if (token == null) 313 | return null; 314 | 315 | throw new System.Exception ("Failed to convert token to runtime object: " + token); 316 | } 317 | 318 | public static object RuntimeObjectToJToken(Runtime.Object obj) 319 | { 320 | var container = obj as Container; 321 | if (container) { 322 | return ContainerToJArray (container); 323 | } 324 | 325 | var divert = obj as Divert; 326 | if (divert) { 327 | string divTypeKey = "->"; 328 | if (divert.isExternal) 329 | divTypeKey = "x()"; 330 | else if (divert.pushesToStack) { 331 | if (divert.stackPushType == PushPopType.Function) 332 | divTypeKey = "f()"; 333 | else if (divert.stackPushType == PushPopType.Tunnel) 334 | divTypeKey = "->t->"; 335 | } 336 | 337 | string targetStr; 338 | if (divert.hasVariableTarget) 339 | targetStr = divert.variableDivertName; 340 | else 341 | targetStr = divert.targetPathString; 342 | 343 | var jObj = new Dictionary (); 344 | jObj[divTypeKey] = targetStr; 345 | 346 | if (divert.hasVariableTarget) 347 | jObj ["var"] = true; 348 | 349 | if (divert.isConditional) 350 | jObj ["c"] = true; 351 | 352 | if (divert.externalArgs > 0) 353 | jObj ["exArgs"] = divert.externalArgs; 354 | 355 | return jObj; 356 | } 357 | 358 | var choicePoint = obj as ChoicePoint; 359 | if (choicePoint) { 360 | var jObj = new Dictionary (); 361 | jObj ["*"] = choicePoint.pathStringOnChoice; 362 | jObj ["flg"] = choicePoint.flags; 363 | return jObj; 364 | } 365 | 366 | var intVal = obj as IntValue; 367 | if (intVal) 368 | return intVal.value; 369 | 370 | var floatVal = obj as FloatValue; 371 | if (floatVal) 372 | return floatVal.value; 373 | 374 | var strVal = obj as StringValue; 375 | if (strVal) { 376 | if (strVal.isNewline) 377 | return "\n"; 378 | else 379 | return "^" + strVal.value; 380 | } 381 | 382 | var listVal = obj as ListValue; 383 | if (listVal) { 384 | return InkListToJObject (listVal); 385 | } 386 | 387 | var divTargetVal = obj as DivertTargetValue; 388 | if (divTargetVal) { 389 | var divTargetJsonObj = new Dictionary (); 390 | divTargetJsonObj ["^->"] = divTargetVal.value.componentsString; 391 | return divTargetJsonObj; 392 | } 393 | 394 | var varPtrVal = obj as VariablePointerValue; 395 | if (varPtrVal) { 396 | var varPtrJsonObj = new Dictionary (); 397 | varPtrJsonObj ["^var"] = varPtrVal.value; 398 | varPtrJsonObj ["ci"] = varPtrVal.contextIndex; 399 | return varPtrJsonObj; 400 | } 401 | 402 | var glue = obj as Runtime.Glue; 403 | if (glue) { 404 | if (glue.isBi) 405 | return "<>"; 406 | else if (glue.isLeft) 407 | return "G<"; 408 | else 409 | return "G>"; 410 | } 411 | 412 | var controlCmd = obj as ControlCommand; 413 | if (controlCmd) { 414 | return _controlCommandNames [(int)controlCmd.commandType]; 415 | } 416 | 417 | var nativeFunc = obj as Runtime.NativeFunctionCall; 418 | if (nativeFunc) { 419 | var name = nativeFunc.name; 420 | 421 | // Avoid collision with ^ used to indicate a string 422 | if (name == "^") name = "L^"; 423 | return name; 424 | } 425 | 426 | 427 | // Variable reference 428 | var varRef = obj as VariableReference; 429 | if (varRef) { 430 | var jObj = new Dictionary (); 431 | string readCountPath = varRef.pathStringForCount; 432 | if (readCountPath != null) { 433 | jObj ["CNT?"] = readCountPath; 434 | } else { 435 | jObj ["VAR?"] = varRef.name; 436 | } 437 | 438 | return jObj; 439 | } 440 | 441 | // Variable assignment 442 | var varAss = obj as VariableAssignment; 443 | if (varAss) { 444 | string key = varAss.isGlobal ? "VAR=" : "temp="; 445 | var jObj = new Dictionary (); 446 | jObj [key] = varAss.variableName; 447 | 448 | // Reassignment? 449 | if (!varAss.isNewDeclaration) 450 | jObj ["re"] = true; 451 | 452 | return jObj; 453 | } 454 | 455 | // Void 456 | var voidObj = obj as Void; 457 | if (voidObj) 458 | return "void"; 459 | 460 | // Tag 461 | var tag = obj as Tag; 462 | if (tag) { 463 | var jObj = new Dictionary (); 464 | jObj ["#"] = tag.text; 465 | return jObj; 466 | } 467 | 468 | // Used when serialising save state only 469 | var choice = obj as Choice; 470 | if (choice) 471 | return ChoiceToJObject (choice); 472 | 473 | throw new System.Exception ("Failed to convert runtime object to Json token: " + obj); 474 | } 475 | 476 | static List ContainerToJArray(Container container) 477 | { 478 | var jArray = ListToJArray (container.content); 479 | 480 | // Container is always an array [...] 481 | // But the final element is always either: 482 | // - a dictionary containing the named content, as well as possibly 483 | // the key "#" with the count flags 484 | // - null, if neither of the above 485 | var namedOnlyContent = container.namedOnlyContent; 486 | var countFlags = container.countFlags; 487 | if (namedOnlyContent != null && namedOnlyContent.Count > 0 || countFlags > 0 || container.name != null) { 488 | 489 | Dictionary terminatingObj; 490 | if (namedOnlyContent != null) { 491 | terminatingObj = DictionaryRuntimeObjsToJObject (namedOnlyContent); 492 | 493 | // Strip redundant names from containers if necessary 494 | foreach (var namedContentObj in terminatingObj) { 495 | var subContainerJArray = namedContentObj.Value as List; 496 | if (subContainerJArray != null) { 497 | var attrJObj = subContainerJArray [subContainerJArray.Count - 1] as Dictionary; 498 | if (attrJObj != null) { 499 | attrJObj.Remove ("#n"); 500 | if (attrJObj.Count == 0) 501 | subContainerJArray [subContainerJArray.Count - 1] = null; 502 | } 503 | } 504 | } 505 | 506 | } else 507 | terminatingObj = new Dictionary (); 508 | 509 | if( countFlags > 0 ) 510 | terminatingObj ["#f"] = countFlags; 511 | 512 | if( container.name != null ) 513 | terminatingObj ["#n"] = container.name; 514 | 515 | jArray.Add (terminatingObj); 516 | } 517 | 518 | // Add null terminator to indicate that there's no dictionary 519 | else { 520 | jArray.Add (null); 521 | } 522 | 523 | return jArray; 524 | } 525 | 526 | static Container JArrayToContainer(List jArray) 527 | { 528 | var container = new Container (); 529 | container.content = JArrayToRuntimeObjList (jArray, skipLast:true); 530 | 531 | // Final object in the array is always a combination of 532 | // - named content 533 | // - a "#f" key with the countFlags 534 | // (if either exists at all, otherwise null) 535 | var terminatingObj = jArray [jArray.Count - 1] as Dictionary; 536 | if (terminatingObj != null) { 537 | 538 | var namedOnlyContent = new Dictionary (terminatingObj.Count); 539 | 540 | foreach (var keyVal in terminatingObj) { 541 | if (keyVal.Key == "#f") { 542 | container.countFlags = (int)keyVal.Value; 543 | } else if (keyVal.Key == "#n") { 544 | container.name = keyVal.Value.ToString (); 545 | } else { 546 | var namedContentItem = JTokenToRuntimeObject(keyVal.Value); 547 | var namedSubContainer = namedContentItem as Container; 548 | if (namedSubContainer) 549 | namedSubContainer.name = keyVal.Key; 550 | namedOnlyContent [keyVal.Key] = namedContentItem; 551 | } 552 | } 553 | 554 | container.namedOnlyContent = namedOnlyContent; 555 | } 556 | 557 | return container; 558 | } 559 | 560 | static Choice JObjectToChoice(Dictionary jObj) 561 | { 562 | var choice = new Choice(); 563 | choice.text = jObj ["text"].ToString(); 564 | choice.index = (int)jObj ["index"]; 565 | choice.originalChoicePath = jObj ["originalChoicePath"].ToString(); 566 | choice.originalThreadIndex = (int)jObj ["originalThreadIndex"]; 567 | return choice; 568 | } 569 | 570 | static Dictionary ChoiceToJObject(Choice choice) 571 | { 572 | var jObj = new Dictionary (); 573 | jObj ["text"] = choice.text; 574 | jObj ["index"] = choice.index; 575 | jObj ["originalChoicePath"] = choice.originalChoicePath; 576 | jObj ["originalThreadIndex"] = choice.originalThreadIndex; 577 | return jObj; 578 | } 579 | 580 | static Dictionary InkListToJObject (ListValue listVal) 581 | { 582 | var rawList = listVal.value; 583 | 584 | var dict = new Dictionary (); 585 | 586 | var content = new Dictionary (); 587 | 588 | foreach (var itemAndValue in rawList) { 589 | var item = itemAndValue.Key; 590 | int val = itemAndValue.Value; 591 | content [item.ToString ()] = val; 592 | } 593 | 594 | dict ["list"] = content; 595 | 596 | if (rawList.Count == 0 && rawList.originNames != null && rawList.originNames.Count > 0) { 597 | dict ["origins"] = rawList.originNames.Cast ().ToList (); 598 | } 599 | 600 | return dict; 601 | } 602 | 603 | public static Dictionary ListDefinitionsToJToken (ListDefinitionsOrigin origin) 604 | { 605 | var result = new Dictionary (); 606 | foreach (ListDefinition def in origin.lists) { 607 | var listDefJson = new Dictionary (); 608 | foreach (var itemToVal in def.items) { 609 | InkListItem item = itemToVal.Key; 610 | int val = itemToVal.Value; 611 | listDefJson [item.itemName] = (object)val; 612 | } 613 | result [def.name] = listDefJson; 614 | } 615 | return result; 616 | } 617 | 618 | public static ListDefinitionsOrigin JTokenToListDefinitions (object obj) 619 | { 620 | var defsObj = (Dictionary)obj; 621 | 622 | var allDefs = new List (); 623 | 624 | foreach (var kv in defsObj) { 625 | var name = (string) kv.Key; 626 | var listDefJson = (Dictionary)kv.Value; 627 | 628 | // Cast (string, object) to (string, int) for items 629 | var items = new Dictionary (); 630 | foreach (var nameValue in listDefJson) 631 | items.Add(nameValue.Key, (int)nameValue.Value); 632 | 633 | var def = new ListDefinition (name, items); 634 | allDefs.Add (def); 635 | } 636 | 637 | return new ListDefinitionsOrigin (allDefs); 638 | } 639 | 640 | static Json() 641 | { 642 | _controlCommandNames = new string[(int)ControlCommand.CommandType.TOTAL_VALUES]; 643 | 644 | _controlCommandNames [(int)ControlCommand.CommandType.EvalStart] = "ev"; 645 | _controlCommandNames [(int)ControlCommand.CommandType.EvalOutput] = "out"; 646 | _controlCommandNames [(int)ControlCommand.CommandType.EvalEnd] = "/ev"; 647 | _controlCommandNames [(int)ControlCommand.CommandType.Duplicate] = "du"; 648 | _controlCommandNames [(int)ControlCommand.CommandType.PopEvaluatedValue] = "pop"; 649 | _controlCommandNames [(int)ControlCommand.CommandType.PopFunction] = "~ret"; 650 | _controlCommandNames [(int)ControlCommand.CommandType.PopTunnel] = "->->"; 651 | _controlCommandNames [(int)ControlCommand.CommandType.BeginString] = "str"; 652 | _controlCommandNames [(int)ControlCommand.CommandType.EndString] = "/str"; 653 | _controlCommandNames [(int)ControlCommand.CommandType.NoOp] = "nop"; 654 | _controlCommandNames [(int)ControlCommand.CommandType.ChoiceCount] = "choiceCnt"; 655 | _controlCommandNames [(int)ControlCommand.CommandType.TurnsSince] = "turns"; 656 | _controlCommandNames [(int)ControlCommand.CommandType.ReadCount] = "readc"; 657 | _controlCommandNames [(int)ControlCommand.CommandType.Random] = "rnd"; 658 | _controlCommandNames [(int)ControlCommand.CommandType.SeedRandom] = "srnd"; 659 | _controlCommandNames [(int)ControlCommand.CommandType.VisitIndex] = "visit"; 660 | _controlCommandNames [(int)ControlCommand.CommandType.SequenceShuffleIndex] = "seq"; 661 | _controlCommandNames [(int)ControlCommand.CommandType.StartThread] = "thread"; 662 | _controlCommandNames [(int)ControlCommand.CommandType.Done] = "done"; 663 | _controlCommandNames [(int)ControlCommand.CommandType.End] = "end"; 664 | _controlCommandNames [(int)ControlCommand.CommandType.ListFromInt] = "listInt"; 665 | _controlCommandNames [(int)ControlCommand.CommandType.ListRange] = "range"; 666 | 667 | for (int i = 0; i < (int)ControlCommand.CommandType.TOTAL_VALUES; ++i) { 668 | if (_controlCommandNames [i] == null) 669 | throw new System.Exception ("Control command not accounted for in serialisation"); 670 | } 671 | } 672 | 673 | static string[] _controlCommandNames; 674 | } 675 | } 676 | 677 | 678 | --------------------------------------------------------------------------------