From 8b57dd5b89d67c3134f32d1f7c0eb5f25857e5ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20=C4=8Cern=C3=BD?= <cernym65@fit.cvut.cz>
Date: Sat, 14 Jan 2023 20:08:49 +0100
Subject: [PATCH] add interaction triggers, ink

---
 Assets/Ink.meta                               |    8 +
 Assets/Ink/Editor.meta                        |    8 +
 Assets/Ink/Editor/Core.meta                   |    9 +
 Assets/Ink/Editor/Core/Compiler.meta          |    9 +
 .../Editor/Core/Compiler/Auto Compiler.meta   |    9 +
 .../Auto Compiler/InkPostProcessor.cs         |  178 +
 .../Auto Compiler/InkPostProcessor.cs.meta    |   12 +
 .../Ink/Editor/Core/Compiler/InkCompiler.cs   |  684 ++++
 .../Editor/Core/Compiler/InkCompiler.cs.meta  |   12 +
 .../Core/Compiler/UnityInkFileHandler.cs      |   23 +
 .../Core/Compiler/UnityInkFileHandler.cs.meta |   11 +
 Assets/Ink/Editor/Core/Ink Library.meta       |    9 +
 .../Editor/Core/Ink Library/InkCompilerLog.cs |   58 +
 .../Core/Ink Library/InkCompilerLog.cs.meta   |   11 +
 Assets/Ink/Editor/Core/Ink Library/InkFile.cs |  371 ++
 .../Editor/Core/Ink Library/InkFile.cs.meta   |   12 +
 .../Ink/Editor/Core/Ink Library/InkLibrary.cs |  475 +++
 .../Core/Ink Library/InkLibrary.cs.meta       |   12 +
 Assets/Ink/Editor/Core/Ink Settings.meta      |    9 +
 .../Editor/Core/Ink Settings/InkSettings.cs   |  114 +
 .../Core/Ink Settings/InkSettings.cs.meta     |   12 +
 .../Core/Ink Settings/InkSettingsEditor.cs    |  103 +
 .../Ink Settings/InkSettingsEditor.cs.meta    |   12 +
 Assets/Ink/Editor/Core/InkEditorUtils.cs      |  294 ++
 Assets/Ink/Editor/Core/InkEditorUtils.cs.meta |   12 +
 Assets/Ink/Editor/InkEditor.asmdef            |   16 +
 Assets/Ink/Editor/InkEditor.asmdef.meta       |    7 +
 Assets/Ink/Editor/Tools.meta                  |    8 +
 Assets/Ink/Editor/Tools/Build Validation.meta |    8 +
 .../InkPreBuildValidationCheck.cs             |   66 +
 .../InkPreBuildValidationCheck.cs.meta        |   11 +
 Assets/Ink/Editor/Tools/File Icons.meta       |    8 +
 .../Tools/File Icons/InkBrowserIcons.cs       |  184 +
 .../Tools/File Icons/InkBrowserIcons.cs.meta  |   12 +
 .../Editor/Tools/File Icons/Resources.meta    |   10 +
 .../Resources/InkChildIcon-Large.psd          |    3 +
 .../Resources/InkChildIcon-Large.psd.meta     |   57 +
 .../File Icons/Resources/InkChildIcon.psd     |    3 +
 .../Resources/InkChildIcon.psd.meta           |   57 +
 .../Resources/InkCompileManualIcon.psd        |    3 +
 .../Resources/InkCompileManualIcon.psd.meta   |   84 +
 .../Resources/InkDefaultTemplate.txt          |    3 +
 .../Resources/InkDefaultTemplate.txt.meta     |    8 +
 .../File Icons/Resources/InkErrorIcon.psd     |    3 +
 .../Resources/InkErrorIcon.psd.meta           |   57 +
 .../Resources/InkFileIcon-Large.psd           |    3 +
 .../Resources/InkFileIcon-Large.psd.meta      |   57 +
 .../Resources/InkFileIcon-retina.psd          |    3 +
 .../Resources/InkFileIcon-retina.psd.meta     |   57 +
 .../File Icons/Resources/InkFileIcon.psd      |    3 +
 .../File Icons/Resources/InkFileIcon.psd.meta |   57 +
 .../File Icons/Resources/InkTodoIcon.psd      |    3 +
 .../File Icons/Resources/InkTodoIcon.psd.meta |   84 +
 .../Resources/InkUnknownFileIcon.psd          |    3 +
 .../Resources/InkUnknownFileIcon.psd.meta     |   57 +
 .../File Icons/Resources/InkWarningIcon.psd   |    3 +
 .../Resources/InkWarningIcon.psd.meta         |   57 +
 Assets/Ink/Editor/Tools/Ink Inspector.meta    |    9 +
 .../Tools/Ink Inspector/DefaultAssetEditor.cs |   60 +
 .../Ink Inspector/DefaultAssetEditor.cs.meta  |   12 +
 .../Ink Inspector/DefaultAssetInspector.cs    |   19 +
 .../DefaultAssetInspector.cs.meta             |   12 +
 .../Tools/Ink Inspector/InkInspector.cs       |  418 +++
 .../Tools/Ink Inspector/InkInspector.cs.meta  |   12 +
 Assets/Ink/Editor/Tools/Player Window.meta    |    8 +
 .../Player Window/InkHistoryContentItem.cs    |   86 +
 .../InkHistoryContentItem.cs.meta             |   11 +
 .../Tools/Player Window/InkPlayerWindow.cs    | 2537 +++++++++++++
 .../Player Window/InkPlayerWindow.cs.meta     |   12 +
 Assets/Ink/Editor/Tools/Startup Window.meta   |    8 +
 .../InkUnityIntegrationStartupWindow.cs       |  123 +
 .../InkUnityIntegrationStartupWindow.cs.meta  |   11 +
 .../Tools/Startup Window/Resources.meta       |    8 +
 .../Startup Window/Resources/InkLogoIcon.png  |    3 +
 .../Resources/InkLogoIcon.png.meta            |  121 +
 Assets/Ink/Extras.meta                        |    8 +
 Assets/Ink/Extras/Sublime3Syntax.zip          |    3 +
 Assets/Ink/Extras/Sublime3Syntax.zip.meta     |    8 +
 Assets/Ink/InkLibs.meta                       |    8 +
 Assets/Ink/InkLibs/Ink-Libraries.asmdef       |    6 +
 Assets/Ink/InkLibs/Ink-Libraries.asmdef.meta  |    7 +
 Assets/Ink/InkLibs/InkCompiler.meta           |    8 +
 .../Ink/InkLibs/InkCompiler/CharacterRange.cs |   54 +
 .../InkCompiler/CharacterRange.cs.meta        |   11 +
 .../Ink/InkLibs/InkCompiler/CharacterSet.cs   |   53 +
 .../InkLibs/InkCompiler/CharacterSet.cs.meta  |   11 +
 .../InkLibs/InkCompiler/CommandLineInput.cs   |   12 +
 .../InkCompiler/CommandLineInput.cs.meta      |   11 +
 Assets/Ink/InkLibs/InkCompiler/Compiler.cs    |  209 ++
 .../Ink/InkLibs/InkCompiler/Compiler.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkCompiler/FileHandler.cs |   24 +
 .../InkLibs/InkCompiler/FileHandler.cs.meta   |   11 +
 Assets/Ink/InkLibs/InkCompiler/InkParser.meta |    8 +
 .../InkParser/CommentEliminator.cs            |   93 +
 .../InkParser/CommentEliminator.cs.meta       |   11 +
 .../InkCompiler/InkParser/InkParser.cs        |  171 +
 .../InkCompiler/InkParser/InkParser.cs.meta   |   11 +
 .../InkParser/InkParser_AuthorWarning.cs      |   28 +
 .../InkParser/InkParser_AuthorWarning.cs.meta |   11 +
 .../InkParser/InkParser_CharacterRanges.cs    |   64 +
 .../InkParser_CharacterRanges.cs.meta         |   11 +
 .../InkParser/InkParser_Choices.cs            |  225 ++
 .../InkParser/InkParser_Choices.cs.meta       |   11 +
 .../InkParser/InkParser_CommandLineInput.cs   |  129 +
 .../InkParser_CommandLineInput.cs.meta        |   11 +
 .../InkParser/InkParser_Conditional.cs        |  288 ++
 .../InkParser/InkParser_Conditional.cs.meta   |   11 +
 .../InkParser/InkParser_Content.cs            |  217 ++
 .../InkParser/InkParser_Content.cs.meta       |   11 +
 .../InkCompiler/InkParser/InkParser_Divert.cs |  206 ++
 .../InkParser/InkParser_Divert.cs.meta        |   11 +
 .../InkParser/InkParser_Expressions.cs        |  512 +++
 .../InkParser/InkParser_Expressions.cs.meta   |   11 +
 .../InkParser/InkParser_Include.cs            |   76 +
 .../InkParser/InkParser_Include.cs.meta       |   11 +
 .../InkCompiler/InkParser/InkParser_Knot.cs   |  239 ++
 .../InkParser/InkParser_Knot.cs.meta          |   11 +
 .../InkCompiler/InkParser/InkParser_Logic.cs  |  421 +++
 .../InkParser/InkParser_Logic.cs.meta         |   11 +
 .../InkParser/InkParser_Sequences.cs          |  231 ++
 .../InkParser/InkParser_Sequences.cs.meta     |   11 +
 .../InkParser/InkParser_Statements.cs         |  163 +
 .../InkParser/InkParser_Statements.cs.meta    |   11 +
 .../InkCompiler/InkParser/InkParser_Tags.cs   |   50 +
 .../InkParser/InkParser_Tags.cs.meta          |   11 +
 .../InkParser/InkParser_Whitespace.cs         |  112 +
 .../InkParser/InkParser_Whitespace.cs.meta    |   11 +
 .../InkStringConversionExtensions.cs          |   18 +
 .../InkStringConversionExtensions.cs.meta     |   11 +
 .../InkLibs/InkCompiler/ParsedHierarchy.meta  |    8 +
 .../ParsedHierarchy/AuthorWarning.cs          |   20 +
 .../ParsedHierarchy/AuthorWarning.cs.meta     |   11 +
 .../InkCompiler/ParsedHierarchy/Choice.cs     |  290 ++
 .../ParsedHierarchy/Choice.cs.meta            |   11 +
 .../ParsedHierarchy/Conditional.cs            |   71 +
 .../ParsedHierarchy/Conditional.cs.meta       |   11 +
 .../ConditionalSingleBranch.cs                |  158 +
 .../ConditionalSingleBranch.cs.meta           |   11 +
 .../ParsedHierarchy/ConstantDeclaration.cs    |   46 +
 .../ConstantDeclaration.cs.meta               |   11 +
 .../ParsedHierarchy/ContentList.cs            |   78 +
 .../ParsedHierarchy/ContentList.cs.meta       |   11 +
 .../InkCompiler/ParsedHierarchy/Divert.cs     |  403 +++
 .../ParsedHierarchy/Divert.cs.meta            |   11 +
 .../ParsedHierarchy/DivertTarget.cs           |  172 +
 .../ParsedHierarchy/DivertTarget.cs.meta      |   11 +
 .../InkCompiler/ParsedHierarchy/Expression.cs |  307 ++
 .../ParsedHierarchy/Expression.cs.meta        |   11 +
 .../ParsedHierarchy/ExternalDeclaration.cs    |   30 +
 .../ExternalDeclaration.cs.meta               |   11 +
 .../InkCompiler/ParsedHierarchy/FlowBase.cs   |  439 +++
 .../ParsedHierarchy/FlowBase.cs.meta          |   11 +
 .../InkCompiler/ParsedHierarchy/FlowLevel.cs  |   11 +
 .../ParsedHierarchy/FlowLevel.cs.meta         |   11 +
 .../ParsedHierarchy/FunctionCall.cs           |  242 ++
 .../ParsedHierarchy/FunctionCall.cs.meta      |   11 +
 .../InkCompiler/ParsedHierarchy/Gather.cs     |   51 +
 .../ParsedHierarchy/Gather.cs.meta            |   11 +
 .../ParsedHierarchy/INamedContent.cs          |    9 +
 .../ParsedHierarchy/INamedContent.cs.meta     |   11 +
 .../ParsedHierarchy/IWeavePoint.cs            |   15 +
 .../ParsedHierarchy/IWeavePoint.cs.meta       |   11 +
 .../InkCompiler/ParsedHierarchy/Identifier.cs |   13 +
 .../ParsedHierarchy/Identifier.cs.meta        |   11 +
 .../ParsedHierarchy/IncludedFile.cs           |   20 +
 .../ParsedHierarchy/IncludedFile.cs.meta      |   11 +
 .../InkCompiler/ParsedHierarchy/Knot.cs       |   35 +
 .../InkCompiler/ParsedHierarchy/Knot.cs.meta  |   11 +
 .../InkCompiler/ParsedHierarchy/List.cs       |   53 +
 .../InkCompiler/ParsedHierarchy/List.cs.meta  |   11 +
 .../ParsedHierarchy/ListDefinition.cs         |  139 +
 .../ParsedHierarchy/ListDefinition.cs.meta    |   11 +
 .../InkCompiler/ParsedHierarchy/Number.cs     |   53 +
 .../ParsedHierarchy/Number.cs.meta            |   11 +
 .../InkCompiler/ParsedHierarchy/Object.cs     |  363 ++
 .../ParsedHierarchy/Object.cs.meta            |   11 +
 .../InkCompiler/ParsedHierarchy/Path.cs       |  192 +
 .../InkCompiler/ParsedHierarchy/Path.cs.meta  |   11 +
 .../InkCompiler/ParsedHierarchy/Return.cs     |   39 +
 .../ParsedHierarchy/Return.cs.meta            |   11 +
 .../InkCompiler/ParsedHierarchy/Sequence.cs   |  206 ++
 .../ParsedHierarchy/Sequence.cs.meta          |   11 +
 .../InkCompiler/ParsedHierarchy/Stitch.cs     |   14 +
 .../ParsedHierarchy/Stitch.cs.meta            |   11 +
 .../InkCompiler/ParsedHierarchy/Story.cs      |  508 +++
 .../InkCompiler/ParsedHierarchy/Story.cs.meta |   11 +
 .../ParsedHierarchy/StringExpression.cs       |   70 +
 .../ParsedHierarchy/StringExpression.cs.meta  |   11 +
 .../InkCompiler/ParsedHierarchy/Text.cs       |   24 +
 .../InkCompiler/ParsedHierarchy/Text.cs.meta  |   11 +
 .../ParsedHierarchy/TunnelOnwards.cs          |   85 +
 .../ParsedHierarchy/TunnelOnwards.cs.meta     |   11 +
 .../ParsedHierarchy/VariableAssignment.cs     |  123 +
 .../VariableAssignment.cs.meta                |   11 +
 .../ParsedHierarchy/VariableReference.cs      |  152 +
 .../ParsedHierarchy/VariableReference.cs.meta |   11 +
 .../InkCompiler/ParsedHierarchy/Weave.cs      |  730 ++++
 .../InkCompiler/ParsedHierarchy/Weave.cs.meta |   11 +
 .../InkCompiler/ParsedHierarchy/Wrap.cs       |   27 +
 .../InkCompiler/ParsedHierarchy/Wrap.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkCompiler/Plugins.meta   |    8 +
 .../Ink/InkLibs/InkCompiler/Plugins/Plugin.cs |   19 +
 .../InkCompiler/Plugins/Plugin.cs.meta        |   11 +
 .../InkCompiler/Plugins/PluginManager.cs      |   40 +
 .../InkCompiler/Plugins/PluginManager.cs.meta |   11 +
 Assets/Ink/InkLibs/InkCompiler/Stats.cs       |   68 +
 Assets/Ink/InkLibs/InkCompiler/Stats.cs.meta  |   11 +
 .../Ink/InkLibs/InkCompiler/StringParser.meta |    8 +
 .../InkCompiler/StringParser/StringParser.cs  |  685 ++++
 .../StringParser/StringParser.cs.meta         |   11 +
 .../StringParser/StringParserState.cs         |  170 +
 .../StringParser/StringParserState.cs.meta    |   11 +
 Assets/Ink/InkLibs/InkRuntime.meta            |    9 +
 Assets/Ink/InkLibs/InkRuntime/CallStack.cs    |  443 +++
 .../Ink/InkLibs/InkRuntime/CallStack.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/Choice.cs       |   54 +
 Assets/Ink/InkLibs/InkRuntime/Choice.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs  |   89 +
 .../InkLibs/InkRuntime/ChoicePoint.cs.meta    |   11 +
 Assets/Ink/InkLibs/InkRuntime/Container.cs    |  366 ++
 .../Ink/InkLibs/InkRuntime/Container.cs.meta  |   11 +
 .../Ink/InkLibs/InkRuntime/ControlCommand.cs  |  170 +
 .../InkLibs/InkRuntime/ControlCommand.cs.meta |   11 +
 .../Ink/InkLibs/InkRuntime/DebugMetadata.cs   |   76 +
 .../InkLibs/InkRuntime/DebugMetadata.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/Divert.cs       |  149 +
 Assets/Ink/InkLibs/InkRuntime/Divert.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/Error.cs        |   24 +
 Assets/Ink/InkLibs/InkRuntime/Error.cs.meta   |   11 +
 Assets/Ink/InkLibs/InkRuntime/Flow.cs         |   93 +
 Assets/Ink/InkLibs/InkRuntime/Flow.cs.meta    |   11 +
 Assets/Ink/InkLibs/InkRuntime/Glue.cs         |   13 +
 Assets/Ink/InkLibs/InkRuntime/Glue.cs.meta    |   11 +
 .../Ink/InkLibs/InkRuntime/INamedContent.cs   |   10 +
 .../InkLibs/InkRuntime/INamedContent.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/InkList.cs      |  584 +++
 Assets/Ink/InkLibs/InkRuntime/InkList.cs.meta |   11 +
 .../InkLibs/InkRuntime/JsonSerialisation.cs   |  720 ++++
 .../InkRuntime/JsonSerialisation.cs.meta      |   11 +
 .../Ink/InkLibs/InkRuntime/ListDefinition.cs  |   75 +
 .../InkLibs/InkRuntime/ListDefinition.cs.meta |   11 +
 .../InkRuntime/ListDefinitionsOrigin.cs       |   53 +
 .../InkRuntime/ListDefinitionsOrigin.cs.meta  |   11 +
 .../InkLibs/InkRuntime/NativeFunctionCall.cs  |  508 +++
 .../InkRuntime/NativeFunctionCall.cs.meta     |   11 +
 Assets/Ink/InkLibs/InkRuntime/Object.cs       |  250 ++
 Assets/Ink/InkLibs/InkRuntime/Object.cs.meta  |   11 +
 Assets/Ink/InkLibs/InkRuntime/Path.cs         |  277 ++
 Assets/Ink/InkLibs/InkRuntime/Path.cs.meta    |   11 +
 Assets/Ink/InkLibs/InkRuntime/Pointer.cs      |   70 +
 Assets/Ink/InkLibs/InkRuntime/Pointer.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/Profiler.cs     |  394 +++
 .../Ink/InkLibs/InkRuntime/Profiler.cs.meta   |   11 +
 Assets/Ink/InkLibs/InkRuntime/PushPop.cs      |   13 +
 Assets/Ink/InkLibs/InkRuntime/PushPop.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/SearchResult.cs |   18 +
 .../InkLibs/InkRuntime/SearchResult.cs.meta   |   11 +
 Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs   |  647 ++++
 .../Ink/InkLibs/InkRuntime/SimpleJson.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/StatePatch.cs   |   66 +
 .../Ink/InkLibs/InkRuntime/StatePatch.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/Story.cs        | 2763 +++++++++++++++
 Assets/Ink/InkLibs/InkRuntime/Story.cs.meta   |   11 +
 .../Ink/InkLibs/InkRuntime/StoryException.cs  |   24 +
 .../InkLibs/InkRuntime/StoryException.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/StoryState.cs   | 1250 +++++++
 .../Ink/InkLibs/InkRuntime/StoryState.cs.meta |   11 +
 .../InkLibs/InkRuntime/StringJoinExtension.cs |   28 +
 .../InkRuntime/StringJoinExtension.cs.meta    |   11 +
 Assets/Ink/InkLibs/InkRuntime/Tag.cs          |   20 +
 Assets/Ink/InkLibs/InkRuntime/Tag.cs.meta     |   11 +
 Assets/Ink/InkLibs/InkRuntime/Value.cs        |  405 +++
 Assets/Ink/InkLibs/InkRuntime/Value.cs.meta   |   11 +
 .../InkLibs/InkRuntime/VariableAssignment.cs  |   27 +
 .../InkRuntime/VariableAssignment.cs.meta     |   11 +
 .../InkLibs/InkRuntime/VariableReference.cs   |   51 +
 .../InkRuntime/VariableReference.cs.meta      |   11 +
 .../Ink/InkLibs/InkRuntime/VariablesState.cs  |  417 +++
 .../InkLibs/InkRuntime/VariablesState.cs.meta |   11 +
 Assets/Ink/InkLibs/InkRuntime/Void.cs         |   10 +
 Assets/Ink/InkLibs/InkRuntime/Void.cs.meta    |   11 +
 Assets/Ink/README.md                          |  158 +
 Assets/Ink/README.md.meta                     |    7 +
 Assets/Ink/package.json                       |   20 +
 Assets/Ink/package.json.meta                  |    7 +
 .../Code/Scripts/NPCScripts/DialogueHolder.cs |   11 +
 .../Scripts/NPCScripts/DialogueHolder.cs.meta |   11 +
 .../Scripts/Player/PlayerActionsController.cs |   34 +-
 .../Code/Scripts/Player/PlayerInteractUI.cs   |   59 +-
 Assets/SZZ/Dialogue.meta                      |    8 +
 Assets/SZZ/Dialogue/test.ink                  |    3 +
 Assets/SZZ/Dialogue/test.ink.meta             |    7 +
 Assets/SZZ/Dialogue/test.json                 |    1 +
 Assets/SZZ/Dialogue/test.json.meta            |    7 +
 Assets/SZZ/Level/Prefabs/test room.prefab     |  158 +-
 Assets/SZZ/Level/Scenes/building a.unity      |  293 +-
 Assets/SZZ/Level/Scenes/sandbox michal.unity  |  300 +-
 .../Scenes/sandbox michal/LightingData.asset  |    4 +-
 .../sandbox michal/Lightmap-0_comp_dir.png    |    4 +-
 .../sandbox michal/Lightmap-0_comp_light.exr  |    4 +-
 Assets/SZZ/Prefabs/Adela.prefab               | 3129 +++++++++++++++++
 Assets/SZZ/Prefabs/Adela.prefab.meta          |    7 +
 Assets/SZZ/Prefabs/DialogueTrigger.prefab     |   61 +
 .../SZZ/Prefabs/DialogueTrigger.prefab.meta   |    7 +
 Assets/SZZ/Prefabs/PlayerCapsule.prefab       |    6 +-
 Assets/SZZ/Prefabs/View.prefab                |  137 +-
 Assets/SZZ/Settings/InputMap.inputactions     |   20 +
 Assets/SZZ/UI/continue-icon.png               |    3 +
 Assets/SZZ/UI/continue-icon.png.meta          |  135 +
 ProjectSettings/DynamicsManager.asset         |   10 +-
 ProjectSettings/InkSettings.asset             |   21 +
 ProjectSettings/QualitySettings.asset         |    1 +
 ProjectSettings/TagManager.asset              |    2 +-
 313 files changed, 32286 insertions(+), 359 deletions(-)
 create mode 100644 Assets/Ink.meta
 create mode 100644 Assets/Ink/Editor.meta
 create mode 100644 Assets/Ink/Editor/Core.meta
 create mode 100644 Assets/Ink/Editor/Core/Compiler.meta
 create mode 100644 Assets/Ink/Editor/Core/Compiler/Auto Compiler.meta
 create mode 100644 Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs
 create mode 100644 Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Compiler/InkCompiler.cs
 create mode 100644 Assets/Ink/Editor/Core/Compiler/InkCompiler.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs
 create mode 100644 Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Library.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkFile.cs
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkFile.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs
 create mode 100644 Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Settings.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs
 create mode 100644 Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs
 create mode 100644 Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs.meta
 create mode 100644 Assets/Ink/Editor/Core/InkEditorUtils.cs
 create mode 100644 Assets/Ink/Editor/Core/InkEditorUtils.cs.meta
 create mode 100644 Assets/Ink/Editor/InkEditor.asmdef
 create mode 100644 Assets/Ink/Editor/InkEditor.asmdef.meta
 create mode 100644 Assets/Ink/Editor/Tools.meta
 create mode 100644 Assets/Ink/Editor/Tools/Build Validation.meta
 create mode 100644 Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs
 create mode 100644 Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd
 create mode 100644 Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd.meta
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector.meta
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs
 create mode 100644 Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Player Window.meta
 create mode 100644 Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs
 create mode 100644 Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs
 create mode 100644 Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window.meta
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs.meta
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window/Resources.meta
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png
 create mode 100644 Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png.meta
 create mode 100644 Assets/Ink/Extras.meta
 create mode 100644 Assets/Ink/Extras/Sublime3Syntax.zip
 create mode 100644 Assets/Ink/Extras/Sublime3Syntax.zip.meta
 create mode 100644 Assets/Ink/InkLibs.meta
 create mode 100644 Assets/Ink/InkLibs/Ink-Libraries.asmdef
 create mode 100644 Assets/Ink/InkLibs/Ink-Libraries.asmdef.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Compiler.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Compiler.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/FileHandler.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/FileHandler.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Plugins.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Stats.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/Stats.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/StringParser.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs
 create mode 100644 Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/CallStack.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/CallStack.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Choice.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Choice.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Container.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Container.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Divert.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Divert.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Error.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Error.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Flow.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Flow.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Glue.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Glue.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/INamedContent.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/INamedContent.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/InkList.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/InkList.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Object.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Object.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Path.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Path.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Pointer.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Pointer.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Profiler.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Profiler.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/PushPop.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/PushPop.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/SearchResult.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/SearchResult.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StatePatch.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StatePatch.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Story.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Story.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StoryException.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StoryException.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StoryState.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StoryState.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Tag.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Tag.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Value.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Value.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariableReference.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariableReference.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariablesState.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/VariablesState.cs.meta
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Void.cs
 create mode 100644 Assets/Ink/InkLibs/InkRuntime/Void.cs.meta
 create mode 100644 Assets/Ink/README.md
 create mode 100644 Assets/Ink/README.md.meta
 create mode 100644 Assets/Ink/package.json
 create mode 100644 Assets/Ink/package.json.meta
 create mode 100644 Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs
 create mode 100644 Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs.meta
 create mode 100644 Assets/SZZ/Dialogue.meta
 create mode 100644 Assets/SZZ/Dialogue/test.ink
 create mode 100644 Assets/SZZ/Dialogue/test.ink.meta
 create mode 100644 Assets/SZZ/Dialogue/test.json
 create mode 100644 Assets/SZZ/Dialogue/test.json.meta
 create mode 100644 Assets/SZZ/Prefabs/Adela.prefab
 create mode 100644 Assets/SZZ/Prefabs/Adela.prefab.meta
 create mode 100644 Assets/SZZ/Prefabs/DialogueTrigger.prefab
 create mode 100644 Assets/SZZ/Prefabs/DialogueTrigger.prefab.meta
 create mode 100644 Assets/SZZ/UI/continue-icon.png
 create mode 100644 Assets/SZZ/UI/continue-icon.png.meta
 create mode 100644 ProjectSettings/InkSettings.asset

diff --git a/Assets/Ink.meta b/Assets/Ink.meta
new file mode 100644
index 0000000..c54e62f
--- /dev/null
+++ b/Assets/Ink.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 592c86d078915044bb2ad24e7bc06581
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor.meta b/Assets/Ink/Editor.meta
new file mode 100644
index 0000000..31aef10
--- /dev/null
+++ b/Assets/Ink/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e829cba164e3db943916a82d6c825672
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core.meta b/Assets/Ink/Editor/Core.meta
new file mode 100644
index 0000000..efcf4df
--- /dev/null
+++ b/Assets/Ink/Editor/Core.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: df28596461d414a1b9f56cb406a23a3f
+folderAsset: yes
+timeCreated: 1459882215
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Compiler.meta b/Assets/Ink/Editor/Core/Compiler.meta
new file mode 100644
index 0000000..1fc519d
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: ddef5103d5ce1401da019f7df6c472af
+folderAsset: yes
+timeCreated: 1459882122
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Compiler/Auto Compiler.meta b/Assets/Ink/Editor/Core/Compiler/Auto Compiler.meta
new file mode 100644
index 0000000..806a074
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/Auto Compiler.meta	
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: e277a78690c74451084fbfbedc0507e8
+folderAsset: yes
+timeCreated: 1459941651
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs b/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs
new file mode 100644
index 0000000..3b8a8ef
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs	
@@ -0,0 +1,178 @@
+// Automatically creates JSON files from an ink placed within the Assets/Ink folder.
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+using Debug = UnityEngine.Debug;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.UnityIntegration {
+	class InkPostProcessor : AssetPostprocessor {
+		// Several assets moved at the same time can cause unity to call OnPostprocessAllAssets several times as a result of moving additional files, or simply due to minor time differences.
+		// This queue tells the compiler which files to recompile after moves have completed.
+		// Not a perfect solution - If Unity doesn't move all the files in the same attempt you can expect some error messages to appear on compile.
+		private static List<string> queuedMovedAssets = new List<string>();
+		public static bool disabled = false;
+		// Recompiles any ink files as a result of an ink file (re)import
+		private static void OnPostprocessAllAssets (string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) {
+			if(disabled) return;
+			if(deletedAssets.Length > 0) {
+				OnDeleteAssets(deletedAssets);
+			}
+			if(movedAssets.Length > 0) {
+				OnMoveAssets(movedAssets.Except(importedAssets).ToArray());
+			}
+			if(importedAssets.Length > 0) {
+				OnImportAssets(importedAssets);
+			}
+            #if !UNITY_2020_1_OR_NEWER
+			if(InkLibrary.created)
+            #endif
+            InkLibrary.Clean();
+		}
+
+		private static void OnDeleteAssets (string[] deletedAssets) {
+			bool deletedInk = false;
+			foreach (var deletedAssetPath in deletedAssets) {
+				if(InkEditorUtils.IsInkFile(deletedAssetPath)) {
+					deletedInk = true;
+					break;
+				}
+			}
+			if(!deletedInk)
+				return;
+
+//			bool alsoDeleteJSON = false;
+//			alsoDeleteJSON = EditorUtility.DisplayDialog("Deleting .ink file", "Also delete the JSON file associated with the deleted .ink file?", "Yes", "No"));
+			List<InkFile> masterFilesAffected = new List<InkFile>();
+			for (int i = InkLibrary.instance.inkLibrary.Count - 1; i >= 0; i--) {
+				if(InkLibrary.instance.inkLibrary [i].inkAsset == null) {
+					if(!InkLibrary.instance.inkLibrary[i].isMaster) {
+						foreach(var masterInkFile in InkLibrary.instance.inkLibrary[i].masterInkFiles) {
+							if(!masterFilesAffected.Contains(masterInkFile))
+								masterFilesAffected.Add(masterInkFile);
+						}
+					}
+					if(InkSettings.instance.handleJSONFilesAutomatically) {
+                        var assetPath = AssetDatabase.GetAssetPath(InkLibrary.instance.inkLibrary[i].jsonAsset);
+						if(assetPath != null && assetPath != string.Empty) {
+                            AssetDatabase.DeleteAsset(assetPath);
+                        }
+                    }
+					InkLibrary.RemoveAt(i);
+				}
+			}
+			// After deleting files, we might have broken some include references, so we rebuild them. There's probably a faster way to do this, or we could probably just remove any null references, but this is a bit more robust.
+			foreach(InkFile inkFile in InkLibrary.instance.inkLibrary) {
+				inkFile.FindIncludedFiles();
+			}
+			foreach(InkFile masterFile in masterFilesAffected) {
+				if(InkSettings.instance.compileAutomatically || masterFile.compileAutomatically) {
+					InkCompiler.CompileInk(masterFile);
+				}
+			}
+		}
+
+		private static void OnMoveAssets (string[] movedAssets) {
+			if (!InkSettings.instance.handleJSONFilesAutomatically) 
+				return;
+			
+			List<string> validMovedAssets = new List<string>();
+			for (var i = 0; i < movedAssets.Length; i++) {
+				if(!InkEditorUtils.IsInkFile(movedAssets[i]))
+					continue;
+				validMovedAssets.Add(movedAssets[i]);
+				queuedMovedAssets.Add(movedAssets[i]);
+
+			}
+			// Move compiled JSON files.
+			// This can cause Unity to postprocess assets again.
+			bool assetMoved = false;
+			foreach(var inkFilePath in validMovedAssets) {
+				InkFile inkFile = InkLibrary.GetInkFileWithPath(inkFilePath);
+				if(inkFile == null) continue;
+				if(inkFile.jsonAsset == null) continue;
+
+				string jsonAssetPath = AssetDatabase.GetAssetPath(inkFile.jsonAsset);
+				
+				string movedAssetDir = Path.GetDirectoryName(inkFilePath);
+				string movedAssetFile = Path.GetFileName(inkFilePath);
+				string newPath = InkEditorUtils.CombinePaths(movedAssetDir, Path.GetFileNameWithoutExtension(movedAssetFile)) + ".json";
+				AssetDatabase.MoveAsset(jsonAssetPath, newPath);
+				assetMoved = true;
+			}
+
+			// Check if no JSON assets were moved (as a result of none needing to move, or this function being called as a result of JSON files being moved)
+			if(!assetMoved && queuedMovedAssets.Count > 0) {
+				List<InkFile> filesToCompile = new List<InkFile>();
+
+				// Add the old master file to the files to be recompiled
+				foreach(var inkFilePath in queuedMovedAssets) {
+					InkFile inkFile = InkLibrary.GetInkFileWithPath(inkFilePath);
+					if(inkFile == null) continue;
+					foreach(var masterInkFile in inkFile.masterInkFilesIncludingSelf) {
+						if(!filesToCompile.Contains(inkFile))
+							filesToCompile.Add(inkFile);
+					}
+				}
+
+				InkLibrary.RebuildInkFileConnections();
+
+				// Add the new file to be recompiled
+				foreach(var inkFilePath in queuedMovedAssets) {
+					InkFile inkFile = InkLibrary.GetInkFileWithPath(inkFilePath);
+					if(inkFile == null) continue;
+
+					foreach(var masterInkFile in inkFile.masterInkFilesIncludingSelf) {
+						if(!filesToCompile.Contains(inkFile))
+							filesToCompile.Add(inkFile);
+					}
+				}
+
+				queuedMovedAssets.Clear();
+
+				// Compile any ink files that are deemed master files a rebuild
+				foreach(var inkFile in filesToCompile) {
+					if(inkFile.isMaster) {
+						if(InkSettings.instance.compileAutomatically || inkFile.compileAutomatically) {
+							InkCompiler.CompileInk(inkFile);
+						}
+					}
+				}
+			}
+		}
+
+		private static void OnImportAssets (string[] importedAssets) {
+			List<string> importedInkAssets = new List<string>();
+			string inklecateFileLocation = null;
+			foreach (var importedAssetPath in importedAssets) {
+				if(InkEditorUtils.IsInkFile(importedAssetPath))
+					importedInkAssets.Add(importedAssetPath);
+				else if (Path.GetFileName(importedAssetPath) == "inklecate" && Path.GetExtension(importedAssetPath) == "")
+					inklecateFileLocation = importedAssetPath;
+			}
+			if(importedInkAssets.Count > 0)
+				PostprocessInkFiles(importedInkAssets);
+			if(inklecateFileLocation != null)
+				PostprocessInklecate(inklecateFileLocation);
+		}
+
+		private static void PostprocessInklecate (string inklecateFileLocation) {
+			// This should probably only recompile files marked to compile automatically, but it's such a rare case, and one where you probably do want to compile.
+			// To fix, one day!
+			Debug.Log("Inklecate updated. Recompiling all Ink files...");
+			InkEditorUtils.RecompileAll();
+		}
+
+		private static void PostprocessInkFiles (List<string> importedInkAssets) {
+			if(EditorApplication.isPlaying && InkSettings.instance.delayInPlayMode) {
+				foreach(var fileToImport in importedInkAssets) {
+					InkCompiler.AddToPendingCompilationStack(fileToImport);
+				}
+			} else {
+				InkLibrary.CreateOrReadUpdatedInkFiles (importedInkAssets);
+				InkCompiler.CompileInk(InkCompiler.GetUniqueMasterInkFilesToCompile (importedInkAssets).ToArray());
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs.meta b/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs.meta
new file mode 100644
index 0000000..541dddb
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/Auto Compiler/InkPostProcessor.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 45a9c84618e20498993d11d2bb89946e
+timeCreated: 1459667420
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs b/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs
new file mode 100644
index 0000000..c16aefb
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs
@@ -0,0 +1,684 @@
+using UnityEngine;
+using UnityEditor;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using UnityEditorInternal;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using System.Threading;
+using Debug = UnityEngine.Debug;
+
+namespace Ink.UnityIntegration {
+	#if UNITY_2020_1_OR_NEWER
+    [FilePath("Library/InkCompiler.asset", FilePathAttribute.Location.ProjectFolder)]
+	public class InkCompiler : ScriptableSingleton<InkCompiler> {
+    #else
+	public class InkCompiler : ScriptableObject {
+    #endif
+    
+        #if !UNITY_2020_1_OR_NEWER
+		public static bool created {
+			get {
+				return (_instance != (UnityEngine.Object) null);
+			}
+		}
+		private static InkCompiler _instance;
+		public static InkCompiler instance {
+			get {
+				if(!created)
+                	LoadOrCreateInstance();
+				return _instance;
+			} private set {
+				if(_instance == value) return;
+				_instance = value;
+            }
+		}
+        
+		static string absoluteSavePath {
+			get {
+				return System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),"Library","InkCompiler.asset"));
+			}
+		}
+		public static void LoadOrCreateInstance () {
+			InternalEditorUtility.LoadSerializedFileAndForget(absoluteSavePath);
+			if(created) {
+				if(InkEditorUtils.isFirstCompile) {
+					ClearCompilationStacks();
+				}
+			} else {
+				instance = ScriptableObject.CreateInstance<InkCompiler>();
+				instance.hideFlags = HideFlags.HideAndDontSave;
+			}
+		}
+		public void Save (bool saveAsText) {
+			InternalEditorUtility.SaveToSerializedFileAndForget((UnityEngine.Object[]) new InkCompiler[1] {this}, absoluteSavePath, saveAsText);
+		}
+
+		protected InkCompiler () {
+			if (created)
+				Debug.LogError((object) "ScriptableSingleton already exists. Did you query the singleton in a constructor?");
+			else {
+				instance = this;
+			}
+		}
+        #endif
+		
+        
+        public class AssetSaver : UnityEditor.AssetModificationProcessor {
+            static string[] OnWillSaveAssets(string[] paths) {
+                InkCompiler.instance.Save(true);
+                return paths;
+            }
+        }
+		public static void SaveStatic (bool saveAsText) {
+			#if !UNITY_2020_1_OR_NEWER
+            if(!created) return;
+			#endif
+			instance.Save(saveAsText);
+		}
+
+		public static bool compiling {
+			get {
+				return instance.compilationStack.Count > 0;
+			}
+		}
+		public static bool buildBlocked = false;
+		static bool playModeBlocked = false;
+
+		public delegate void OnCompileInkEvent (InkFile inkFile);
+		public static event OnCompileInkEvent OnCompileInk;
+
+		// Track if we've currently locked compilation of Unity C# Scripts
+		public static bool hasLockedUnityCompilation = false;
+        
+        private static List<Action> onCompleteActions = new List<Action>();
+
+		
+		// If InkSettings' delayInPlayMode option is true, dirty files are added here when they're changed in play mode
+		// This ensures they're remembered when you exit play mode and can be compiled
+		public List<string> pendingCompilationStack = new List<string>();
+		// The state of files currently being compiled.
+		public List<InkCompiler.CompilationStackItem> compilationStack = new List<InkCompiler.CompilationStackItem>();
+
+
+		[Serializable]
+		public class CompilationStackItem {
+			public enum State {
+				// Default state, item is about to be queued for compilation
+				Queued,
+				
+				// Item is now owned by the thread pool and being compiled
+				Compiling,
+				
+				// Compilation has finished, item to be processed for errors and result handled
+				Complete,
+			}
+
+			public State state = State.Queued;
+			public bool immediate;
+			public InkFile inkFile;
+			public string compiledJson;
+			public string inkAbsoluteFilePath;
+			public string jsonAbsoluteFilePath;
+			public List<InkCompilerLog> logOutput = new List<InkCompilerLog>();
+			public List<string> unhandledErrorOutput = new List<string>();
+			public DateTime startTime;
+			public DateTime endTime;
+
+			public float timeTaken {
+				get {
+					if(state == State.Complete) return (float)(endTime - startTime).TotalSeconds;
+					else return (float)(DateTime.Now - startTime).TotalSeconds;
+				}
+			}
+
+			public CompilationStackItem () {}
+		}
+
+		// This always runs after the InkEditorUtils constructor
+		[InitializeOnLoadMethod]
+		static void OnProjectLoadedInEditor() {
+			#if UNITY_2017_1_OR_NEWER
+			EditorApplication.playModeStateChanged += OnPlayModeChange;
+			#else
+			EditorApplication.playmodeStateChanged += LegacyOnPlayModeChange;
+			#endif
+			EditorApplication.update += Update;
+            // I really don't know if this can fire, since it assumes that it compiled so can't have been locked. But safety first!
+            EditorApplication.UnlockReloadAssemblies();
+			#if UNITY_2019_4_OR_NEWER
+			// This one, on the other hand, seems to actually occur sometimes - presumably because c# compiles at the same time as the ink.
+			if(InkEditorUtils.disallowedAutoRefresh) {
+				InkEditorUtils.disallowedAutoRefresh = false;
+				try {
+					AssetDatabase.AllowAutoRefresh();
+				} catch (Exception e) {
+					Debug.LogWarning("Failed AllowAutoRefresh "+e);
+				}
+			}
+			#endif
+		}
+		
+		private static void Update () {
+            #if UNITY_2020_1_OR_NEWER
+            // If we're not compiling but have locked C# compilation then now is the time to reset
+			if (!compiling && hasLockedUnityCompilation) {
+				hasLockedUnityCompilation = false;
+				EditorApplication.UnlockReloadAssemblies();
+			}
+			#else
+            // If we're not compiling but have locked C# compilation then now is the time to reset
+			if ((!InkLibrary.created || !compiling) && hasLockedUnityCompilation) {
+				hasLockedUnityCompilation = false;
+				EditorApplication.UnlockReloadAssemblies();
+			}
+			if(!InkLibrary.created) 
+				return;
+            #endif
+
+			if(compiling) {
+				// Check for timeouts, in case of an unhandled bug with this system/the ink compiler!
+				for (int i = instance.compilationStack.Count - 1; i >= 0; i--) {
+					var compilingFile = instance.compilationStack [i];
+					if (compilingFile.state == CompilationStackItem.State.Compiling) {
+						if (compilingFile.timeTaken > InkSettings.instance.compileTimeout) {
+							// TODO - Cancel the thread if it's still going. Not critical, since its kinda fine if it compiles a bit later, but it's not clear.
+							RemoveCompilingFile(i);
+							Debug.LogError("Ink Compiler timed out for "+compilingFile.inkAbsoluteFilePath+".\nCompilation should never take more than a few seconds, but for large projects or slow computers you may want to increase the timeout time in the InkSettings file.\nIf this persists there may be another issue; or else check an ink file exists at this path and try Assets/Recompile Ink, else please report as a bug with the following error log at this address: https://github.com/inkle/ink/issues\nError log:\n"+string.Join("\n",compilingFile.unhandledErrorOutput.ToArray()));
+							TryCompileNextFileInStack();
+						}
+					}
+				}
+
+				// When all files have compiled, run the complete function.
+				if(NumFilesInCompilingStackInState(CompilationStackItem.State.Compiling) == 0) {
+					if(NumFilesInCompilingStackInState(CompilationStackItem.State.Queued) == 0) {
+						DelayedComplete();
+					} else {
+						// We used to avoid calling this here in favour of calling it CompileInkThreaded but it seems that it doesn't run when called there, for some reason.
+						// If someone can make this work please let me know!
+						TryCompileNextFileInStack();
+					}
+				}
+			}
+
+
+			// If we're not showing a progress bar in Linux this whole step is superfluous
+			#if !UNITY_EDITOR_LINUX
+			UpdateProgressBar();
+			#endif
+		}
+
+        static void RemoveCompilingFile (int index) {
+            instance.compilationStack.RemoveAt(index);
+            instance.Save(true);
+            // Progress bar prevents delayCall callback from firing in Linux Editor, locking the
+            // compilation until it times out. Let's just not show progress bars in Linux Editor    
+            #if !UNITY_EDITOR_LINUX
+            if (instance.compilationStack.Count == 0) EditorUtility.ClearProgressBar();
+            #endif
+        }
+
+		static void UpdateProgressBar () {
+			if(instance.compilationStack.Count == 0) return;
+			int numCompiling = NumFilesInCompilingStackInState(CompilationStackItem.State.Compiling);
+			string message = "Compiling .Ink File "+(instance.compilationStack.Count-numCompiling)+" of "+instance.compilationStack.Count+".";
+			if(playModeBlocked) message += " Will enter play mode when complete.";
+			if(buildBlocked || playModeBlocked || EditorApplication.isPlaying) EditorUtility.DisplayProgressBar("Compiling Ink...", message, GetEstimatedCompilationProgress());
+			else EditorUtility.ClearProgressBar();
+		}
+
+		public static float GetEstimatedCompilationProgress () {
+			if(!compiling) return 1;
+			float progress = 0;
+			foreach (var compilingFile in instance.compilationStack) {
+				if (compilingFile.state == CompilationStackItem.State.Compiling)
+					progress += compilingFile.timeTaken / InkSettings.instance.compileTimeout;
+				if (compilingFile.state == CompilationStackItem.State.Complete)
+					progress += 1;
+			}
+			progress /= instance.compilationStack.Count;
+			return progress;
+		}
+
+		#if UNITY_2017_1_OR_NEWER
+		static void OnPlayModeChange (PlayModeStateChange mode) {
+			if(mode == PlayModeStateChange.EnteredEditMode && instance.pendingCompilationStack.Count > 0)
+				CompilePendingFiles();
+			if(mode == PlayModeStateChange.ExitingEditMode && compiling)
+				BlockPlayMode();
+			if(mode == PlayModeStateChange.EnteredPlayMode && compiling)
+				EnteredPlayModeWhenCompiling();
+		}
+		#else
+		static void LegacyOnPlayModeChange () {
+			if(!EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPlaying && instance.pendingCompilationStack.Count > 0) 
+				CompilePendingFiles();
+			if(EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPlaying && compiling)
+				BlockPlayMode();
+			if(EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPlaying && compiling)
+				EnteredPlayModeWhenCompiling();
+		}
+		#endif
+
+		static void CompilePendingFiles () {
+			InkLibrary.CreateOrReadUpdatedInkFiles (instance.pendingCompilationStack);
+			foreach (var pendingMasterFile in GetUniqueMasterInkFilesToCompile(instance.pendingCompilationStack))
+				InkCompiler.CompileInk(pendingMasterFile);
+		}
+
+		static void BlockPlayMode () {
+			EditorApplication.isPlaying = false;
+			var percentage = String.Format("{0:P0}.", GetEstimatedCompilationProgress());
+			Debug.LogWarning("Delayed entering play mode because Ink is still compiling ("+percentage+"). Will enter play mode on completion.");
+			playModeBlocked = true;
+		}
+
+		static void EnteredPlayModeWhenCompiling () {
+			Debug.LogError("Entered Play Mode while Ink was still compiling! Your story will not be up to date. Recommend exiting and re-entering play mode.\nWe normally delay entering play mode when compiling, so you've found an edge case!");
+		}
+
+		public static void CompileInk (params InkFile[] inkFiles) {
+            CompileInk(inkFiles, false, null);
+        }
+		public static void CompileInk (InkFile[] inkFiles, bool immediate, Action onComplete) {
+			#if UNITY_2019_4_OR_NEWER
+			if(!InkEditorUtils.disallowedAutoRefresh) {
+				InkEditorUtils.disallowedAutoRefresh = true;
+				try {
+					AssetDatabase.DisallowAutoRefresh();
+				} catch (Exception e) {
+					Debug.LogWarning("Failed DisallowAutoRefresh "+e);
+				}
+			}
+			#endif
+            
+			InkLibrary.Validate();
+            if(onComplete != null) onCompleteActions.Add(onComplete);
+			StringBuilder filesCompiledLog = new StringBuilder("Files compiled:");
+			foreach (var inkFile in inkFiles) filesCompiledLog.AppendLine().Append(inkFile.filePath);
+			
+			StringBuilder outputLog = new StringBuilder ();
+			outputLog.Append ("Ink compilation started at ");
+			outputLog.AppendLine (DateTime.Now.ToLongTimeString ());
+			outputLog.Append (filesCompiledLog.ToString());
+			Debug.Log(outputLog);
+			
+			foreach(var inkFile in inkFiles) {
+				CompileInkInternal (inkFile, immediate);
+			}
+		}
+
+		/// <summary>
+		/// Starts a System.Process that compiles a master ink file, creating a playable JSON file that can be parsed by the Ink.Story class
+		/// </summary>
+		/// <param name="inkFile">Ink file.</param>
+		private static void CompileInkInternal (InkFile inkFile, bool immediate) {
+			if(inkFile == null) {
+				Debug.LogError("Tried to compile ink file but input was null.");
+				return;
+			}
+			if(!inkFile.isMaster)
+				Debug.LogWarning("Compiling InkFile which is an include. Any file created is likely to be invalid. Did you mean to call CompileInk on inkFile.master?");
+
+			// If we've not yet locked C# compilation do so now
+			if (!hasLockedUnityCompilation)
+			{
+				hasLockedUnityCompilation = true;
+				EditorApplication.LockReloadAssemblies();
+			}
+			
+            RemoveFromPendingCompilationStack(inkFile);
+			if(GetCompilationStackItem(inkFile) != null) {
+				UnityEngine.Debug.LogWarning("Tried compiling ink file, but file is already compiling. "+inkFile.filePath);
+				return;
+			}
+
+			string inputPath = InkEditorUtils.CombinePaths(inkFile.absoluteFolderPath, Path.GetFileName(inkFile.filePath));
+			Debug.Assert(inkFile.absoluteFilePath == inputPath);
+
+			CompilationStackItem pendingFile = new CompilationStackItem
+			{
+				inkFile = InkLibrary.GetInkFileWithAbsolutePath(inputPath),
+				inkAbsoluteFilePath = inputPath,
+				jsonAbsoluteFilePath = inkFile.absoluteJSONPath,
+				state = CompilationStackItem.State.Queued,
+				immediate = immediate
+			};
+
+			AddToCompilationStack(pendingFile);
+
+			TryCompileNextFileInStack();
+		}
+
+
+
+
+		private static void TryCompileNextFileInStack () {
+			if(!compiling) return;
+			InkCompiler.CompilationStackItem fileToCompile = null;
+			foreach(var x in instance.compilationStack) {
+				if(x.state == CompilationStackItem.State.Compiling) return;
+				if(x.state == CompilationStackItem.State.Queued) {
+					fileToCompile = x;
+					break;
+				}
+			}
+			if(fileToCompile != null) {
+				if(fileToCompile.immediate) {
+					CompileInkThreaded(fileToCompile);
+				} else {
+					if(EditorApplication.isCompiling) Debug.LogWarning("Was compiling scripts when ink compilation started! This seems to cause the thread to cancel and complete, but the work isn't done. It may cause a timeout.");
+					ThreadPool.QueueUserWorkItem(CompileInkThreaded, fileToCompile);
+				}
+			} else {
+			}
+		}
+
+		private static void BeginCompilingFile(CompilationStackItem item) {
+			if(item.state != CompilationStackItem.State.Queued) return;
+			item.state = CompilationStackItem.State.Compiling;
+			item.startTime = DateTime.Now;
+		}
+		private static void CompleteCompilingFile(CompilationStackItem item) {
+			if(item.state != CompilationStackItem.State.Compiling) return;
+			item.state = CompilationStackItem.State.Complete;
+			item.endTime = DateTime.Now;
+			if (item.timeTaken > InkSettings.instance.compileTimeout * 0.6f)
+				Debug.LogWarning ("Compilation for "+Path.GetFileName(item.inkFile.filePath)+" took over 60% of the time required to timeout the compiler. Consider increasing the compile timeout on the InkSettings file.");
+		}
+
+		private static void CompileInkThreaded(object itemObj) {
+			CompilationStackItem item = (CompilationStackItem) itemObj;
+			if(item.state == CompilationStackItem.State.Compiling) {
+				Debug.LogWarning("CompileInkThreaded was called on a file that is already compiling! This is most likely a threading bug. Please report this!");
+				return;
+			}
+			BeginCompilingFile(item);
+
+			var inputString = File.ReadAllText(item.inkAbsoluteFilePath);
+			var compiler = new Compiler(inputString, new Compiler.Options
+			{
+				countAllVisits = true,
+				fileHandler = new UnityInkFileHandler(Path.GetDirectoryName(item.inkAbsoluteFilePath)),
+				errorHandler = (string message, ErrorType type) => {
+					InkCompilerLog log;
+					if(InkCompilerLog.TryParse(message, out log)) {
+						if(string.IsNullOrEmpty(log.fileName)) log.fileName = Path.GetFileName(item.inkAbsoluteFilePath);
+						item.logOutput.Add(log);
+					} else {
+						Debug.LogWarning("Couldn't parse log "+message);
+					}
+				}
+			});
+
+			try
+			{
+				var compiledStory = compiler.Compile();
+				if (compiledStory != null)
+					item.compiledJson = compiledStory.ToJson();
+			}
+			catch (SystemException e)
+			{
+				item.unhandledErrorOutput.Add(string.Format(
+					"Ink Compiler threw exception \nError: {0}\n---- Trace ----\n{1}\n--------\n", e.Message,
+					e.StackTrace));
+			}
+
+			CompleteCompilingFile(item);
+
+			// This doesn't seem to execute when called in a thread, and I apparently don't have a bloody clue how threads work.
+			// If someone can make this work, I'd rather that TryCompileNextFileInStack ran directly after CompileInkThreaded finishes.
+			// I couldn't make it work, so I've put TryCompileNextFileInStack in Update instead. Bleh!
+			// TryCompileNextFileInStack();
+		}
+
+		// When all files in stack have been compiled. This is called via update because Process events run in another thread.
+		private static void DelayedComplete () {
+			if(NumFilesInCompilingStackInState(CompilationStackItem.State.Compiling) > 0) {
+				Debug.LogWarning("Delayed, but a file is now compiling! You can ignore this warning.");
+				return;
+			}
+			bool errorsFound = false;
+			StringBuilder filesCompiledLog = new StringBuilder("Files compiled:");
+
+			// Create and import compiled files
+			AssetDatabase.StartAssetEditing();
+			foreach (var compilingFile in instance.compilationStack) {
+				// Complete status is also set when an error occured, in these cases 'compiledJson' will be null so there's no import to process
+				if (compilingFile.compiledJson == null) continue;
+				
+				// Write new compiled data to the file system
+				File.WriteAllText(compilingFile.jsonAbsoluteFilePath, compilingFile.compiledJson, Encoding.UTF8);
+				AssetDatabase.ImportAsset(compilingFile.inkFile.jsonPath);
+			}
+			AssetDatabase.StopAssetEditing();
+
+			foreach (var compilingFile in instance.compilationStack) {
+				// Load and store a reference to the compiled file
+				compilingFile.inkFile.FindCompiledJSONAsset();
+				
+				filesCompiledLog.AppendLine().Append(compilingFile.inkFile.filePath);
+				filesCompiledLog.Append(string.Format(" ({0}s)", compilingFile.timeTaken));
+				if(compilingFile.unhandledErrorOutput.Count > 0) {
+					filesCompiledLog.Append(" (With unhandled error)");
+					StringBuilder errorLog = new StringBuilder ();
+					errorLog.Append ("Unhandled error(s) occurred compiling Ink file ");
+					errorLog.Append ("'");
+					errorLog.Append (compilingFile.inkFile.filePath);
+					errorLog.Append ("'");
+					errorLog.AppendLine ("! Please report following error(s) as a bug:");
+					foreach (var error in compilingFile.unhandledErrorOutput)
+						errorLog.AppendLine (error);
+					Debug.LogError(errorLog);
+					compilingFile.inkFile.unhandledCompileErrors = compilingFile.unhandledErrorOutput;
+					errorsFound = true;
+				} else {
+					SetOutputLog(compilingFile);
+					bool errorsInEntireStory = false;
+					bool warningsInEntireStory = false;
+					foreach(var inkFile in compilingFile.inkFile.inkFilesInIncludeHierarchy) {
+						if(inkFile.hasErrors) {
+							errorsInEntireStory = true;
+						}
+						if(inkFile.hasWarnings) {
+							warningsInEntireStory = true;
+						}
+					}
+					if(errorsInEntireStory) {
+						filesCompiledLog.Append(" (With error)");
+						errorsFound = true;
+					}
+					if(warningsInEntireStory) {
+						filesCompiledLog.Append(" (With warning)");
+					}
+				}
+			}
+			
+
+			foreach (var compilingFile in instance.compilationStack) {
+				if (OnCompileInk != null) {
+					OnCompileInk (compilingFile.inkFile);
+				}
+			}
+
+			StringBuilder outputLog = new StringBuilder ();
+			if(errorsFound) {
+				outputLog.Append ("Ink compilation completed with errors at ");
+				outputLog.AppendLine (DateTime.Now.ToLongTimeString ());
+				outputLog.Append (filesCompiledLog.ToString());
+				Debug.LogError(outputLog);
+			} else {
+				outputLog.Append ("Ink compilation completed at ");
+				outputLog.AppendLine (DateTime.Now.ToLongTimeString ());
+				outputLog.Append (filesCompiledLog.ToString());
+				Debug.Log(outputLog);
+			}
+
+			ClearCompilationStack();
+			
+			#if !UNITY_EDITOR_LINUX
+			EditorUtility.ClearProgressBar();
+			#endif
+			
+			#if UNITY_2019_4_OR_NEWER
+			if(InkEditorUtils.disallowedAutoRefresh) {
+				InkEditorUtils.disallowedAutoRefresh = false;
+				try {
+					AssetDatabase.AllowAutoRefresh();
+				} catch (Exception e) {
+					Debug.LogWarning("Failed AllowAutoRefresh "+e);
+				}
+			}
+			#endif
+
+            // This is now allowed, if compiled manually. I've left this code commented out because at some point we might want to track what caused a file to compile. 
+            // if(EditorApplication.isPlayingOrWillChangePlaymode && InkSettings.instance.delayInPlayMode) {
+			// 	Debug.LogError("Ink just finished recompiling while in play mode. This should never happen when InkSettings.instance.delayInPlayMode is true!");
+			// }
+            
+            buildBlocked = false;
+
+			if(playModeBlocked) {
+                playModeBlocked = false;
+				if(!errorsFound) {
+					// Delaying gives the editor a frame to clear the progress bar.
+					EditorApplication.delayCall += () => {
+						Debug.Log("Compilation completed, entering play mode.");
+						EditorApplication.isPlaying = true;
+					};
+				} else {
+					Debug.LogWarning("Play mode not entered after ink compilation because ink had errors.");
+				}
+			}
+
+            foreach(var onCompleteAction in onCompleteActions) {
+                if(onCompleteAction != null) onCompleteAction();
+            }
+            onCompleteActions.Clear();
+		}
+		
+		
+		private static void SetOutputLog (CompilationStackItem pendingFile) {
+			pendingFile.inkFile.errors.Clear();
+			pendingFile.inkFile.warnings.Clear();
+			pendingFile.inkFile.todos.Clear();
+
+			foreach(var childInkFile in pendingFile.inkFile.inkFilesInIncludeHierarchy) {
+				childInkFile.unhandledCompileErrors.Clear();
+				childInkFile.errors.Clear();
+				childInkFile.warnings.Clear();
+				childInkFile.todos.Clear();
+			}
+
+			foreach(var output in pendingFile.logOutput) {
+				if(output.type == ErrorType.Error) {
+					pendingFile.inkFile.errors.Add(output);
+					Debug.LogError("Ink "+output.type+": "+output.content + " (at "+output.fileName+":"+output.lineNumber+")", pendingFile.inkFile.inkAsset);
+				} else if (output.type == ErrorType.Warning) {
+					pendingFile.inkFile.warnings.Add(output);
+					Debug.LogWarning("Ink "+output.type+": "+output.content + " (at "+output.fileName+" "+output.lineNumber+")", pendingFile.inkFile.inkAsset);
+				} else if (output.type == ErrorType.Author) {
+					pendingFile.inkFile.todos.Add(output);
+					if(InkSettings.instance.printInkLogsInConsoleOnCompile)
+						Debug.Log("Ink Log: "+output.content + " (at "+output.fileName+" "+output.lineNumber+")", pendingFile.inkFile.inkAsset);
+				}
+			}
+		}
+
+
+
+		public static List<InkFile> GetUniqueMasterInkFilesToCompile (List<string> importedInkAssets) {
+			List<InkFile> masterInkFiles = new List<InkFile>();
+			foreach (var importedAssetPath in importedInkAssets) {
+                foreach(var masterInkFile in GetMasterFilesIncludingInkAssetPath(importedAssetPath)) {
+					if (!masterInkFiles.Contains(masterInkFile) && (InkSettings.instance.compileAutomatically || masterInkFile.compileAutomatically)) {
+						masterInkFiles.Add(masterInkFile);
+					}
+				}
+            }
+			return masterInkFiles;
+		}
+
+		// An ink file might actually have several owners! This should be reflected here.
+        public static IEnumerable<InkFile> GetMasterFilesIncludingInkAssetPath (string importedAssetPath) {
+            InkFile inkFile = InkLibrary.GetInkFileWithPath(importedAssetPath);
+            // Trying to catch a rare (and not especially important) bug that seems to happen occasionally when opening a project
+            // It's probably this - I've noticed it before in another context.
+            Debug.Assert(InkSettings.instance != null, "No ink settings file. This is a bug. For now you should be able to fix this via Assets > Rebuild Ink Library");
+            // I've caught it here before
+            Debug.Assert(inkFile != null, "No internal InkFile reference at path "+importedAssetPath+". This is a bug. For now you can fix this via Assets > Rebuild Ink Library");
+            Debug.Assert(inkFile != null);
+            return inkFile.masterInkFilesIncludingSelf;
+        }
+
+
+
+
+
+
+		public static void AddToCompilationStack (InkCompiler.CompilationStackItem compilationStackItem) {
+			if(!instance.compilationStack.Contains(compilationStackItem)) {
+				instance.compilationStack.Add(compilationStackItem);
+				instance.Save(true);
+			}
+		}
+
+        public static void ClearCompilationStack () {
+			if(instance.compilationStack.Count != 0) {
+				instance.compilationStack.Clear();
+				instance.Save(true);
+			}
+        }
+
+
+        public static void AddToPendingCompilationStack (string filePath) {
+			if(!instance.pendingCompilationStack.Contains(filePath)) {
+				instance.pendingCompilationStack.Add(filePath);
+				instance.Save(true);
+			}
+		}
+
+        public static void RemoveFromPendingCompilationStack (InkFile inkFile) {
+            bool anyChange = false;
+			anyChange = instance.pendingCompilationStack.Remove(inkFile.filePath) || anyChange;
+            foreach(var includeFile in inkFile.inkFilesInIncludeHierarchy) {
+                anyChange = instance.pendingCompilationStack.Remove(includeFile.filePath) || anyChange;
+            }
+			if(anyChange)
+				instance.Save(true);
+        }
+        public static void ClearCompilationStacks () {
+            instance.compilationStack.Clear();
+            instance.pendingCompilationStack.Clear();
+			instance.Save(true);
+        }
+
+		public static int NumFilesInCompilingStackInState (InkCompiler.CompilationStackItem.State state) {
+			int count = 0;
+			foreach(var x in instance.compilationStack) {
+				if(x.state == state) 
+					count++;
+			}
+			return count;
+		}
+		public static List<InkCompiler.CompilationStackItem> FilesInCompilingStackInState (InkCompiler.CompilationStackItem.State state) {
+			List<InkCompiler.CompilationStackItem> items = new List<InkCompiler.CompilationStackItem>();
+			foreach(var x in instance.compilationStack) {
+				if(x.state == state) 
+					items.Add(x);
+			}
+			return items;
+		}
+
+		public static InkCompiler.CompilationStackItem GetCompilationStackItem (InkFile inkFile) {
+			foreach(var x in instance.compilationStack) {
+				if(x.inkFile == inkFile) 
+					return x;
+			}
+			return null;
+		}
+	}
+}
diff --git a/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs.meta b/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs.meta
new file mode 100644
index 0000000..55c5640
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/InkCompiler.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 430cb8f71c23c438cb8b0ce85cc80fa6
+timeCreated: 1459463931
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs b/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs
new file mode 100644
index 0000000..96e07fb
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs
@@ -0,0 +1,23 @@
+using Ink;
+using System.IO;
+
+// Utility class for the ink compiler, used to work out how to find include files and their contents
+public class UnityInkFileHandler : IFileHandler {
+    private readonly string rootDirectory;
+
+    public UnityInkFileHandler(string rootDirectory)
+    {
+        this.rootDirectory = rootDirectory;
+    }
+    
+    public string ResolveInkFilename(string includeName)
+    {
+        // Convert to Unix style, and then use FileInfo.FullName to parse any ..\
+        return new FileInfo(Path.Combine(rootDirectory, includeName).Replace('\\', '/')).FullName;
+    }
+
+    public string LoadInkFileContents(string fullFilename)
+    {
+        return File.ReadAllText(fullFilename);
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs.meta b/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs.meta
new file mode 100644
index 0000000..3313f41
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Compiler/UnityInkFileHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2de904d59497c1c47b0792de1a85faf6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Library.meta b/Assets/Ink/Editor/Core/Ink Library.meta
new file mode 100644
index 0000000..f15d8c0
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library.meta	
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: c547bc1dd015a497cb998538d42d2340
+folderAsset: yes
+timeCreated: 1459878666
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs b/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs
new file mode 100644
index 0000000..a4b9cc0
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs	
@@ -0,0 +1,58 @@
+using System.Text.RegularExpressions;
+using Debug = UnityEngine.Debug;
+
+namespace Ink.UnityIntegration
+{
+    [System.Serializable]
+	public class InkCompilerLog {
+		public Ink.ErrorType type;
+		public string content;
+		public string fileName;
+		public int lineNumber;
+
+		public InkCompilerLog (Ink.ErrorType type, string content, string fileName, int lineNumber = -1) {
+			this.type = type;
+			this.content = content;
+			this.fileName = fileName;
+			this.lineNumber = lineNumber;
+		}
+
+		public static bool TryParse (string rawLog, out InkCompilerLog log) {
+			var match = _errorRegex.Match(rawLog);
+			if (match.Success) {
+				Ink.ErrorType errorType = Ink.ErrorType.Author;
+				string filename = null;
+				int lineNo = -1;
+				string message = null;
+				
+				var errorTypeCapture = match.Groups["errorType"];
+				if( errorTypeCapture != null ) {
+					var errorTypeStr = errorTypeCapture.Value;
+					if(errorTypeStr == "AUTHOR" || errorTypeStr == "TODO") errorType = Ink.ErrorType.Author;
+					else if(errorTypeStr == "WARNING") errorType = Ink.ErrorType.Warning;
+					else if(errorTypeStr == "ERROR") errorType = Ink.ErrorType.Error;
+					else Debug.LogWarning("Could not parse error type from "+errorTypeStr);
+				}
+				
+				var filenameCapture = match.Groups["filename"];
+				if (filenameCapture != null)
+					filename = filenameCapture.Value;
+				
+				var lineNoCapture = match.Groups["lineNo"];
+				if (lineNoCapture != null)
+					lineNo = int.Parse (lineNoCapture.Value);
+				
+				var messageCapture = match.Groups["message"];
+				if (messageCapture != null)
+					message = messageCapture.Value.Trim();
+				log = new InkCompilerLog(errorType, message, filename, lineNo);
+				return true;
+			} else {
+				Debug.LogWarning("Could not parse InkFileLog from log: "+rawLog);
+				log = null;
+				return false;
+			}
+		}
+		private static Regex _errorRegex = new Regex(@"(?<errorType>ERROR|WARNING|TODO):(?:\s(?:'(?<filename>[^']*)'\s)?line (?<lineNo>\d+):)?(?<message>.*)");
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs.meta b/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs.meta
new file mode 100644
index 0000000..57dd96d
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkCompilerLog.cs.meta	
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2af758247ca160446b0727c6e91632cc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkFile.cs b/Assets/Ink/Editor/Core/Ink Library/InkFile.cs
new file mode 100644
index 0000000..e51d757
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkFile.cs	
@@ -0,0 +1,371 @@
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+using Debug = UnityEngine.Debug;
+using System.Collections.Generic;
+using System;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Ink.UnityIntegration {
+	// Helper class for ink files that maintains INCLUDE connections between ink files
+	[System.Serializable]
+	public class InkFile {
+		
+		public bool compileAutomatically = false;
+		// A reference to the ink file
+		public DefaultAsset inkAsset;
+
+        //specify json destination folder (if None, default to same folder as ink file)
+        public DefaultAsset jsonAssetDirectory;
+
+		// The compiled json file. Use this to start a story.
+		public TextAsset jsonAsset;
+
+		// The file path relative to the Assets folder (Assets/Ink/Story.ink)
+		public string filePath {
+			get {
+				if(inkAsset == null) 
+					return null;
+
+				return InkEditorUtils.SanitizePathString(AssetDatabase.GetAssetPath(inkAsset));
+			}
+		}
+
+		// The full file path (C:/Users/Inkle/HeavensVault/Assets/Ink/Story.ink)
+		public string absoluteFilePath {
+			get {
+				if(inkAsset == null) 
+					return null;
+				return InkEditorUtils.UnityRelativeToAbsolutePath(filePath);
+			}
+		}
+
+		public string absoluteFolderPath {
+			get {
+				return InkEditorUtils.SanitizePathString(Path.GetDirectoryName(absoluteFilePath));
+			}
+		}
+
+		// The path of any compiled json file. Relative to assets folder.
+        public string jsonPath {
+			get {
+                var _filePath = filePath;
+                Debug.Assert(_filePath != null, "File path for ink file is null! The ink library requires rebuilding.");
+
+                DefaultAsset jsonFolder = jsonAssetDirectory;
+                if (jsonFolder == null) // no path specified for this specific file
+                {
+                    if(InkSettings.instance.defaultJsonAssetPath != null) 
+                    {
+                        // use default path in InkSettings
+                        jsonFolder = InkSettings.instance.defaultJsonAssetPath;
+                    }
+
+                    if (jsonFolder == null)
+                    {
+                        //fallback to same folder as .ink file
+                        jsonFolder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(Path.GetDirectoryName(_filePath));
+                    }
+                }
+
+                Debug.Assert(jsonFolder != null, "JSON folder not found for ink file at path "+_filePath);
+
+                string jsonPath = AssetDatabase.GetAssetPath(jsonFolder);
+                string strJsonAssetPath = InkEditorUtils.CombinePaths(jsonPath, Path.GetFileNameWithoutExtension(_filePath)) + ".json";
+                return strJsonAssetPath;
+			}
+		}
+
+		public string absoluteJSONPath {
+			get {
+				if(inkAsset == null) 
+					return null;
+				return InkEditorUtils.UnityRelativeToAbsolutePath(jsonPath);
+			}
+		}
+
+
+
+
+
+
+		// Fatal unhandled errors that should be reported as compiler bugs.
+		public List<string> unhandledCompileErrors = new List<string>();
+		public bool hasUnhandledCompileErrors {
+			get {
+				return unhandledCompileErrors.Count > 0;
+			}
+		}
+
+		// Fatal errors caused by errors in the user's ink script.
+		public List<InkCompilerLog> errors = new List<InkCompilerLog>();
+		public bool hasErrors {
+			get {
+				return errors.Count > 0;
+			}
+		}
+
+		public List<InkCompilerLog> warnings = new List<InkCompilerLog>();
+		public bool hasWarnings {
+			get {
+				return warnings.Count > 0;
+			}
+		}
+
+		public List<InkCompilerLog> todos = new List<InkCompilerLog>();
+		public bool hasTodos {
+			get {
+				return todos.Count > 0;
+			}
+		}
+
+		public bool requiresCompile {
+			get {
+				if(!isMaster) return false;
+				return jsonAsset == null || lastEditDate > lastCompileDate || hasUnhandledCompileErrors;
+			}
+		}
+
+		/// <summary>
+		/// Gets the last compile date of the story.
+		/// </summary>
+		/// <value>The last compile date of the story.</value>
+		public DateTime lastCompileDate {
+			get {
+				if(isMaster) {
+					string fullJSONFilePath = InkEditorUtils.UnityRelativeToAbsolutePath(AssetDatabase.GetAssetPath(jsonAsset));
+					return File.GetLastWriteTime(fullJSONFilePath);
+				} else {
+					return default(DateTime);
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets the last edit date of the file.
+		/// </summary>
+		/// <value>The last edit date of the file.</value>
+		public DateTime lastEditDate {
+			get {
+				return File.GetLastWriteTime(absoluteFilePath);
+			}
+		}
+
+		// File that contains this file as an include, if one exists.
+		public List<DefaultAsset> parents;
+		public IEnumerable<InkFile> parentInkFiles {
+			get {
+				if(parents != null && parents.Count != 0) {
+					foreach(var parentInkAsset in parents) {
+						yield return InkLibrary.GetInkFileWithFile(parentInkAsset);
+					}
+				}
+			}
+		}
+		// Is this ink file a parent file?
+		public bool isParent {
+			get {
+				return includes.Count > 0;
+			}
+		}
+
+		public List<DefaultAsset> masterInkAssets;
+		public IEnumerable<InkFile> masterInkFiles {
+			get {
+				if(masterInkAssets != null && masterInkAssets.Count != 0) {
+					foreach(var masterInkAsset in masterInkAssets) {
+						yield return InkLibrary.GetInkFileWithFile(masterInkAsset);
+					}
+				}
+			}
+		}
+		public IEnumerable<InkFile> masterInkFilesIncludingSelf {
+			get {
+				if(isMaster) yield return this;
+				else {
+					foreach(var masterInkFile in masterInkFiles) {
+						yield return masterInkFile;
+					}
+				}
+			}
+		}
+		public DefaultAsset masterInkAsset;
+
+		// Is this ink file a master file?
+		public bool isMaster {
+			get {
+				return masterInkAssets == null || masterInkAssets.Count == 0;
+			}
+		}
+		
+
+		// The files included by this file
+		// We cache the paths of the files to be included for performance, giving us more freedom to refresh the actual includes list without needing to parse all the text.
+		public List<string> includePaths = new List<string>();
+		public List<DefaultAsset> includes = new List<DefaultAsset>();
+		// The InkFiles of the includes of this file
+		public List<InkFile> includesInkFiles {
+			get {
+				List<InkFile> _includesInkFiles = new List<InkFile>();
+				foreach(var child in includes) {
+					if(child == null) {
+						Debug.LogError("Error compiling ink: Ink file include in "+filePath+" is null.", inkAsset);
+						continue;
+					}
+					_includesInkFiles.Add(InkLibrary.GetInkFileWithFile(child));
+				}
+				return _includesInkFiles;
+			}
+		}
+		// The InkFiles in the include hierarchy of this file.
+		public List<InkFile> inkFilesInIncludeHierarchy {
+			get {
+				List<InkFile> _inkFilesInIncludeHierarchy = new List<InkFile>();
+				_inkFilesInIncludeHierarchy.Add(this);
+				foreach(var child in includesInkFiles) {
+					if (child == null)
+						return null;
+					_inkFilesInIncludeHierarchy.AddRange(child.inkFilesInIncludeHierarchy);
+				}
+				return _inkFilesInIncludeHierarchy;
+			}
+		}
+	    
+
+
+
+
+		public InkFile (DefaultAsset inkAsset) {
+			Debug.Assert(inkAsset != null);
+			this.inkAsset = inkAsset;
+			
+			ParseContent();
+		}
+
+		public void FindCompiledJSONAsset () {
+            Debug.Assert(inkAsset != null);
+            jsonAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(jsonPath);
+		}
+
+
+
+		
+
+
+		
+
+
+//		public string content;
+		// The contents of the .ink file
+		public string GetFileContents () {
+			if(inkAsset == null) {
+				Debug.LogWarning("Ink file asset is null! Rebuild library using Assets > Rebuild Ink Library");
+				return "";
+			}
+			return File.ReadAllText(absoluteFilePath);
+		}
+
+		public void ParseContent () {
+			includePaths.Clear();
+			includePaths.AddRange(InkIncludeParser.ParseIncludes(GetFileContents()));
+		}
+
+		public void FindIncludedFiles (bool addMissing = false) {
+			includes.Clear();
+			foreach(string includePath in includePaths) {
+				string localIncludePath = InkEditorUtils.CombinePaths(Path.GetDirectoryName(filePath), includePath);
+				// This enables parsing ..\ and the like. Can we use Path.GetFullPath instead?
+				var fullIncludePath = new FileInfo(localIncludePath).FullName;
+				localIncludePath = InkEditorUtils.AbsoluteToUnityRelativePath(fullIncludePath);
+				DefaultAsset includedInkFileAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(localIncludePath);
+				if(includedInkFileAsset == null) {
+					Debug.LogError(filePath+ " expected child .ink asset at '"+localIncludePath+"' but file was not found.", inkAsset);
+				} else {
+					InkFile includedInkFile = InkLibrary.GetInkFileWithFile(includedInkFileAsset, addMissing);
+					if(includedInkFile == null) {
+						Debug.LogError(filePath+ " expected child InkFile from .ink asset at '"+localIncludePath+"' but file was not found.", inkAsset);
+					} else if (includedInkFile.includes.Contains(inkAsset)) {
+						Debug.LogError("Circular INCLUDE reference between '"+filePath+"' and '"+includedInkFile.filePath+"'.", inkAsset);
+					} else
+						includes.Add(includedInkFileAsset);
+				}
+			}
+		}
+
+		public static class InkIncludeParser {
+			static Regex _includeRegex;
+			static Regex includeRegex {
+				get {
+					if(_includeRegex == null) {
+						_includeRegex = new Regex (@"^\s*INCLUDE\s+([^\r\n]+)\r*$", RegexOptions.Multiline);
+					}
+					return _includeRegex;
+				}
+			}
+	        public static IEnumerable<string> ParseIncludes (string inkContents) {
+	            return FindIncludes (EliminateComments(inkContents));
+	        }
+
+	        static string EliminateComments(string inkStr) {
+	            var sb = new StringBuilder ();
+	            int idx = 0;
+	            while(idx < inkStr.Length) {
+	                var commentStarterIdx = inkStr.IndexOf ('/', idx);
+	                // Final string?
+	                if (commentStarterIdx == -1 || commentStarterIdx >= inkStr.Length-2 ) {
+	                    sb.Append (inkStr.Substring (idx, inkStr.Length - idx));
+	                    break;
+	                }
+	                sb.Append (inkStr.Substring (idx, commentStarterIdx - idx));
+	                var commentStarter = inkStr.Substring (commentStarterIdx, 2);
+	                if (commentStarter == "//" || commentStarter == "/*") {
+	                    int endOfCommentIdx = -1;
+	                    // Single line comments
+	                    if (commentStarter == "//") {
+	                        endOfCommentIdx = inkStr.IndexOf ('\n', commentStarterIdx);
+	                        if (endOfCommentIdx == -1)
+	                            endOfCommentIdx = inkStr.Length;
+	                        else if (inkStr [endOfCommentIdx - 1] == '\r')
+	                            endOfCommentIdx = endOfCommentIdx - 1;
+	                    } 
+	                    // Block comments
+	                    else if (commentStarter == "/*") {
+	                        endOfCommentIdx = inkStr.IndexOf ("*/", idx);
+	                        if (endOfCommentIdx == -1)
+	                            endOfCommentIdx = inkStr.Length;
+	                        else
+	                            endOfCommentIdx += 2;
+	                        // If there are *any* newlines, we should add one in here,
+	                        // so that lines are spit up correctly
+	                        if (inkStr.IndexOf ('\n', commentStarterIdx, endOfCommentIdx - commentStarterIdx) != -1)
+	                            sb.Append ("\n");
+	                    }
+	                    // Skip over comment
+	                    if (endOfCommentIdx > -1)
+	                        idx = endOfCommentIdx;
+	                } 
+	                // Normal slash we need, not a comment
+	                else {
+	                    sb.Append ("/");
+	                    idx = commentStarterIdx + 1;
+	                }
+	            }
+	            return sb.ToString ();
+	        }
+	        static IEnumerable<string> FindIncludes(string str) {
+	            MatchCollection matches = includeRegex.Matches(str);
+	            foreach (Match match in matches)
+	            {
+	                var capture = match.Groups [1].Captures [0];
+	                yield return capture.Value;
+	            }
+	        }
+		}
+
+		
+		public override string ToString () {
+			return string.Format ("[InkFile: filePath={0}]", filePath);
+		} 
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkFile.cs.meta b/Assets/Ink/Editor/Core/Ink Library/InkFile.cs.meta
new file mode 100644
index 0000000..6e74f72
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkFile.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: b497ccd4b61cb4fee8b8f0ce86302e83
+timeCreated: 1459464092
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs b/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs
new file mode 100644
index 0000000..806dbc3
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs	
@@ -0,0 +1,475 @@
+using UnityEngine;
+using UnityEditor;
+using UnityEditorInternal;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Diagnostics;
+using Debug = UnityEngine.Debug;
+
+/// <summary>
+/// Holds a reference to an InkFile object for every .ink file detected in the Assets folder.
+/// Provides helper functions to easily obtain these files.
+/// </summary>
+namespace Ink.UnityIntegration {
+    #if UNITY_2020_1_OR_NEWER
+    [FilePath("Library/InkLibrary.asset", FilePathAttribute.Location.ProjectFolder)]
+	public class InkLibrary : ScriptableSingleton<InkLibrary>, IEnumerable<InkFile> {
+    #else
+	public class InkLibrary : ScriptableObject, IEnumerable<InkFile> {
+    #endif
+        //
+		public static System.Version inkVersionCurrent = new System.Version(1,0,0);
+		public static System.Version unityIntegrationVersionCurrent = new System.Version(1,0,0);
+
+		static string absoluteSavePath {
+			get {
+				return System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),"Library","InkLibrary.asset"));
+			}
+		}
+		
+		#if !UNITY_2020_1_OR_NEWER
+		public static bool created {
+			get {
+				// If it's null, there's no InkLibrary loaded
+				return (_instance != (Object) null);
+			}
+		}
+		private static InkLibrary _instance;
+		public static InkLibrary instance {
+			get {
+				if(!created)
+                	LoadOrCreateInstance();
+				return _instance;
+			} private set {
+				if(_instance == value) return;
+				_instance = value;
+            }
+		}
+        
+		
+		// This occurs on recompile, creation and load (note that data has not necessarily been loaded at this point!)
+		protected InkLibrary () {
+			if (created)
+				Debug.LogError((object) "ScriptableSingleton already exists. Did you query the singleton in a constructor?");
+			else {
+				instance = this;
+			}
+		}
+
+		public static void LoadOrCreateInstance () {
+			InternalEditorUtility.LoadSerializedFileAndForget(absoluteSavePath);
+			if(created) {
+				if(InkEditorUtils.isFirstCompile) {
+					Validate();
+				}
+			} else {
+				instance = ScriptableObject.CreateInstance<InkLibrary>();
+				instance.hideFlags = HideFlags.HideAndDontSave;
+				Rebuild();
+				instance.Save(true);
+			}
+		}
+		
+		public void Save (bool saveAsText) {
+			if(!created) return;			
+			InternalEditorUtility.SaveToSerializedFileAndForget((Object[]) new InkLibrary[1] {instance}, absoluteSavePath, saveAsText);
+		}
+
+		static void EnsureCreated () {
+			if(!created) LoadOrCreateInstance();
+		}
+        #endif
+        
+        public class AssetSaver : UnityEditor.AssetModificationProcessor {
+            static string[] OnWillSaveAssets(string[] paths) {
+                instance.Save(true);
+                return paths;
+            }
+        }
+
+		public List<InkFile> inkLibrary = new List<InkFile>();
+		Dictionary<DefaultAsset, InkFile> inkLibraryDictionary;
+		
+        public int Count {
+            get {
+                return inkLibrary.Count;
+            }
+        }
+        public InkFile this[int key] {
+            get {
+                return inkLibrary[key];
+            } set {
+                inkLibrary[key] = value;
+            }
+        }
+        IEnumerator<InkFile> IEnumerable<InkFile>.GetEnumerator() {
+            return inkLibrary.GetEnumerator();
+        }
+
+        IEnumerator IEnumerable.GetEnumerator() {
+            return inkLibrary.GetEnumerator();
+        }
+
+		void OnValidate () {
+            BuildLookupDictionary();
+            Validate();
+        }
+		// After recompile, the data associated with the object is fetched (or whatever happens to it) by this point. 
+		void OnEnable () {
+			// Deletes the persistent version of this asset that we used to use prior to 0.9.71
+			if(!Application.isPlaying && EditorUtility.IsPersistent(this)) {
+				var path = AssetDatabase.GetAssetPath(this);
+				if(!string.IsNullOrEmpty(path)) {
+					#if !UNITY_2020_1_OR_NEWER
+                    if(_instance == this) _instance = null;
+					#endif
+					AssetDatabase.DeleteAsset(path);
+					AssetDatabase.Refresh();
+					return;
+				}
+			}
+		}
+
+        static void BuildLookupDictionary () {
+            if(instance.inkLibraryDictionary == null) instance.inkLibraryDictionary = new Dictionary<DefaultAsset, InkFile>();
+            else instance.inkLibraryDictionary.Clear();
+			foreach(var inkFile in instance.inkLibrary) {
+                instance.inkLibraryDictionary.Add(inkFile.inkAsset, inkFile);
+            }
+        }
+        
+		/// <summary>
+		/// Checks if the library is corrupt and rebuilds if necessary. Returns true if the library was valid
+		/// </summary>
+        public static bool Validate () {
+            if(RequiresRebuild()) {
+                Rebuild();
+                Debug.LogWarning("InkLibrary was invalid and has been rebuilt. This can occur if files are moved/deleted while the editor is closed. You can ignore this warning.");
+				return false;
+            } else {
+				return true;
+			}
+        }
+        
+		/// <summary>
+		/// Checks if the library is corrupt and requires a Rebuild. 
+        /// This can happen when asset IDs change, causing the wrong file to be referenced.
+        /// This occassionally occurs from source control.
+        /// This is a fairly performant check.
+		/// </summary>
+        static bool RequiresRebuild () {
+            #if !UNITY_2020_1_OR_NEWER
+			EnsureCreated();
+            #endif
+			foreach(var inkFile in instance.inkLibrary) {
+                if(inkFile == null) {
+                    return true;
+                }
+                if(inkFile.inkAsset == null) {
+                    return true;
+                }
+                if(!instance.inkLibraryDictionary.ContainsKey(inkFile.inkAsset)) {
+                    return true;
+                }
+                if(inkFile.inkAsset == null) {
+                    return true;
+                }
+                foreach(var include in inkFile.includes) {
+                    if(include == null) {
+                        return true;
+                    }
+                    if(!instance.inkLibraryDictionary.ContainsKey(include)) {
+                        return true;
+                    }
+                } 
+            }
+            return false;
+        }
+
+		/// <summary>
+		/// Removes and null references in the library
+		/// </summary>
+		public static bool Clean () {
+            bool wasDirty = false;
+			for (int i = instance.Count - 1; i >= 0; i--) {
+				InkFile inkFile = InkLibrary.instance[i];
+				if (inkFile.inkAsset == null) {
+					InkLibrary.RemoveAt(i);
+                    wasDirty = true;
+                }
+			}
+            return wasDirty;
+		}
+
+        public static void Add (InkFile inkFile) {
+            instance.inkLibrary.Add(inkFile);
+			SortInkLibrary();
+			instance.inkLibraryDictionary.Add(inkFile.inkAsset, inkFile);
+        }
+        public static void RemoveAt (int index) {
+            var inkFile = instance.inkLibrary[index];
+            instance.inkLibrary.RemoveAt(index);
+            instance.inkLibraryDictionary.Remove(inkFile.inkAsset);
+        }
+		static void SortInkLibrary () {
+            instance.inkLibrary = instance.inkLibrary.OrderBy(x => x.filePath).ToList();
+		}
+
+		/// <summary>
+		/// Updates the ink library. Executed whenever an ink file is changed by InkToJSONPostProcessor
+		/// Can be called manually, but incurs a performance cost.
+		/// </summary>
+		public static void Rebuild () {
+			// Disable the asset post processor in case any assetdatabase functions called as a result of this would cause further operations.
+			InkPostProcessor.disabled = true;
+			
+            // Remove any old file connections
+            Clean();
+
+			// Reset the asset name
+			instance.name = "Ink Library "+unityIntegrationVersionCurrent.ToString();
+            
+			// Add any new file connections (if any are found it replaces the old library entirely)
+			string[] inkFilePaths = GetAllInkFilePaths();
+			bool inkLibraryChanged = false;
+			List<InkFile> newInkLibrary = new List<InkFile>(inkFilePaths.Length);
+			for (int i = 0; i < inkFilePaths.Length; i++) {
+				InkFile inkFile = GetInkFileWithAbsolutePath(inkFilePaths [i]);
+				// If the ink library doesn't have a representation for this file, then make one 
+                if(inkFile == null) {
+					inkLibraryChanged = true;
+					string localAssetPath = InkEditorUtils.AbsoluteToUnityRelativePath(inkFilePaths [i]);
+					DefaultAsset inkFileAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(localAssetPath);
+					// If the ink file can't be found, it might not yet have been imported. We try to manually import it to fix this.
+					if(inkFileAsset == null) {
+						AssetDatabase.ImportAsset(localAssetPath);
+						inkFileAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(localAssetPath);
+						if(inkFileAsset == null) {
+							Debug.LogWarning("Ink File Asset not found at "+localAssetPath+". This can occur if the .meta file has not yet been created. This issue should resolve itself, but if unexpected errors occur, rebuild Ink Library using Assets > Recompile Ink");
+							continue;
+						}
+					}
+					inkFile = new InkFile(inkFileAsset);
+				}
+				newInkLibrary.Add(inkFile);
+			}
+			if(inkLibraryChanged) {
+				instance.inkLibrary = newInkLibrary;
+				SortInkLibrary();
+			}
+            BuildLookupDictionary();
+
+			RebuildInkFileConnections();
+
+			foreach (InkFile inkFile in instance.inkLibrary) inkFile.FindCompiledJSONAsset();
+			instance.Save(true);
+			
+			// Re-enable the ink asset post processor
+			InkPostProcessor.disabled = false;
+			Debug.Log("Ink Library was rebuilt.");
+		}
+
+		public static void CreateOrReadUpdatedInkFiles (List<string> importedInkAssets) {
+			foreach (var importedAssetPath in importedInkAssets) {
+				InkFile inkFile = InkLibrary.GetInkFileWithPath(importedAssetPath);
+				if(inkFile == null) {
+					DefaultAsset asset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(importedAssetPath);
+					inkFile = new InkFile(asset);
+					Add(inkFile);
+				} else {
+					inkFile.ParseContent();
+				}
+			}
+			// Now we've updated all the include paths for the ink library we can create master/child references between them.
+			RebuildInkFileConnections();
+		}
+
+        // Finds absolute file paths of all the ink files in Application.dataPath
+		private static string[] GetAllInkFilePaths () {
+			string[] inkFilePaths = Directory.GetFiles(Application.dataPath, "*.ink", SearchOption.AllDirectories);
+			for (int i = 0; i < inkFilePaths.Length; i++) {
+				inkFilePaths [i] = InkEditorUtils.SanitizePathString(inkFilePaths [i]);
+			}
+			return inkFilePaths;
+		}
+
+		// All the master files
+		public static IEnumerable<InkFile> GetMasterInkFiles () {
+			if(instance.inkLibrary == null) yield break;
+			foreach (InkFile inkFile in instance.inkLibrary) {
+				if(inkFile.isMaster) 
+					yield return inkFile;
+			}
+		}
+
+		// All the master files which are dirty and are set to compile
+		public static IEnumerable<InkFile> GetFilesRequiringRecompile () {
+			foreach(InkFile inkFile in InkLibrary.GetMasterInkFiles ()) {
+				if(inkFile.requiresCompile && (InkSettings.instance.compileAutomatically || inkFile.compileAutomatically)) 
+					yield return inkFile;
+			}
+		}
+
+		// All the master files which are set to compile
+		public static IEnumerable<InkFile> FilesCompiledByRecompileAll () {
+			foreach(InkFile inkFile in InkLibrary.GetMasterInkFiles ()) {
+				if(InkSettings.instance.compileAutomatically || inkFile.compileAutomatically) 
+					yield return inkFile;
+			}
+		}
+
+		/// <summary>
+		/// Gets the ink file from the .ink file reference.
+		/// </summary>
+		/// <returns>The ink file with path.</returns>
+		/// <param name="file">File asset.</param>
+		/// <param name="addIfMissing">Adds the file if missing from inkLibrary.</param>
+		public static InkFile GetInkFileWithFile (DefaultAsset file, bool addIfMissing = false) {
+			if(instance.inkLibrary == null) return null;
+			
+			if (!file) {
+				Debug.LogError("Can't add null file.");
+				return null;
+			}
+
+            if(instance.inkLibraryDictionary == null) {
+				Debug.LogWarning("GetInkFileWithFile: inkLibraryDictionary was null! This should never occur, but is handled following a user reported bug. If this has never been seen long after 12/08/2020, it can be safely removed");
+				BuildLookupDictionary();
+			}
+			foreach(InkFile inkFile in instance.inkLibrary) {
+				if(inkFile.inkAsset == file) {
+					return inkFile;
+				}
+			}
+
+			if (addIfMissing) {
+				InkFile newFile = new InkFile(file);
+				instance.inkLibrary.Add(newFile);
+				Debug.Log(file + " missing from ink library. Adding it now.");
+				return newFile;
+			}
+
+			System.Text.StringBuilder listOfFiles = new System.Text.StringBuilder();
+			foreach(InkFile inkFile in instance.inkLibrary) {
+				listOfFiles.AppendLine(inkFile.ToString());
+			}
+			Debug.LogWarning (file + " missing from ink library. Please rebuild.\n"+listOfFiles);
+			return null;
+		}
+
+		/// <summary>
+		/// Gets the ink file with path relative to Assets folder, for example: "Assets/Ink/myStory.ink".
+		/// </summary>
+		/// <returns>The ink file with path.</returns>
+		/// <param name="path">Path.</param>
+		public static InkFile GetInkFileWithPath (string path) {
+			if(instance.inkLibrary == null) return null;
+			foreach(InkFile inkFile in instance.inkLibrary) {
+				if(inkFile.filePath == path) {
+					return inkFile;
+				}
+			}
+			return null;
+		}
+
+		/// <summary>
+		/// Gets the ink file with absolute path.
+		/// </summary>
+		/// <returns>The ink file with path.</returns>
+		/// <param name="path">Path.</param>
+		public static InkFile GetInkFileWithAbsolutePath (string absolutePath) {
+			if(instance.inkLibrary == null) return null;
+			foreach(InkFile inkFile in instance.inkLibrary) {
+				if(inkFile.absoluteFilePath == absolutePath) {
+					return inkFile;
+				}
+			}
+			return null;
+		}
+
+
+		/// <summary>
+		/// Rebuilds which files are master files and the connections between the files.
+		/// </summary>
+		public static void RebuildInkFileConnections () {
+			Queue<InkFile> inkFileQueue = new Queue<InkFile>(instance.inkLibrary);
+			while (inkFileQueue.Count > 0) {
+				InkFile inkFile = inkFileQueue.Dequeue();
+				inkFile.parents = new List<DefaultAsset>();
+				inkFile.masterInkAssets = new List<DefaultAsset>();
+				inkFile.ParseContent();
+				inkFile.FindIncludedFiles(true);
+
+				foreach (InkFile includedInkFile in inkFile.includesInkFiles) {
+					if (!inkFileQueue.Contains(includedInkFile)) {
+						inkFileQueue.Enqueue(includedInkFile);
+					}
+				}
+			}
+
+			// We now set the master file for ink files. As a file can be in an include hierarchy, we need to do this in two passes.
+			// First, we set the master file to the file that includes an ink file.
+			foreach (InkFile inkFile in instance.inkLibrary) {
+				if(inkFile.includes.Count == 0) 
+					continue;
+				foreach (InkFile otherInkFile in instance.inkLibrary) {
+					if(inkFile == otherInkFile) 
+						continue;
+					if(inkFile.includes.Contains(otherInkFile.inkAsset)) {
+						if(!otherInkFile.parents.Contains(inkFile.inkAsset)) {
+							otherInkFile.parents.Add(inkFile.inkAsset);
+						}
+					}
+				}
+			}
+			// Next, we create a list of all the files owned by the actual master file, which we obtain by travelling up the parent tree from each file.
+			Dictionary<InkFile, List<InkFile>> masterChildRelationships = new Dictionary<InkFile, List<InkFile>>();
+			foreach (InkFile inkFile in instance.inkLibrary) {
+				foreach(var parentInkFile in inkFile.parentInkFiles) {
+					InkFile lastMasterInkFile = parentInkFile;
+					InkFile masterInkFile = parentInkFile;
+					while (masterInkFile.parents.Count != 0) {
+						// This shouldn't just pick first, but iterate the whole lot! 
+						// I didn't feel like writing a recursive algorithm until it's actually needed though - a file included by several parents is already a rare enough case!
+						masterInkFile = masterInkFile.parentInkFiles.First();
+						lastMasterInkFile = masterInkFile;
+					}
+					if(lastMasterInkFile.parents.Count > 1) {
+						Debug.LogError("The ink ownership tree has another master file that is not discovered! This is an oversight of the current implementation. If you requres this feature, please take a look at the comment in the code above - if you solve it let us know and we'll merge it in!");
+					}
+					if(!masterChildRelationships.ContainsKey(masterInkFile)) {
+						masterChildRelationships.Add(masterInkFile, new List<InkFile>());
+					}
+					masterChildRelationships[masterInkFile].Add(inkFile);
+				}
+
+				// if(inkFile.parent == null) 
+				// 	continue;
+				// InkFile parent = inkFile.parentInkFile;
+				// while (parent.metaInfo.parent != null) {
+				// 	parent = parent.metaInfo.parentInkFile;
+				// }
+				// if(!masterChildRelationships.ContainsKey(parent)) {
+				// 	masterChildRelationships.Add(parent, new List<InkFile>());
+				// }
+				// masterChildRelationships[parent].Add(inkFile);
+			}
+			// Finally, we set the master file of the children
+			foreach (var inkFileRelationship in masterChildRelationships) {
+				foreach(InkFile childInkFile in inkFileRelationship.Value) {
+					if(!childInkFile.masterInkAssets.Contains(inkFileRelationship.Key.inkAsset)) {
+						childInkFile.masterInkAssets.Add(inkFileRelationship.Key.inkAsset);
+					} else {
+						Debug.LogWarning("Child file already contained master file reference! This is weird!");
+					}
+					if(InkSettings.instance.handleJSONFilesAutomatically && childInkFile.jsonAsset != null) {
+						AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(childInkFile.jsonAsset));
+						childInkFile.jsonAsset = null;
+					}
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs.meta b/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs.meta
new file mode 100644
index 0000000..8733c04
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Library/InkLibrary.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: dc234ab28a32840f5adfb84a075bc023
+timeCreated: 1485516445
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Settings.meta b/Assets/Ink/Editor/Core/Ink Settings.meta
new file mode 100644
index 0000000..4b3f692
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Settings.meta	
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 6188b37f5f5824313b73fb964d1b269e
+folderAsset: yes
+timeCreated: 1459878666
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs b/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs
new file mode 100644
index 0000000..0bde848
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs	
@@ -0,0 +1,114 @@
+using UnityEngine;
+using UnityEditor;
+using Debug = UnityEngine.Debug;
+
+/// <summary>
+/// Holds a reference to an InkFile object for every .ink file detected in the Assets folder.
+/// Provides helper functions to easily obtain these files.
+/// </summary>
+namespace Ink.UnityIntegration {
+    #if UNITY_2020_1_OR_NEWER
+    [FilePath("ProjectSettings/InkSettings.asset", FilePathAttribute.Location.ProjectFolder)]
+	public class InkSettings : ScriptableSingleton<InkSettings> {
+    #else
+	public class InkSettings : ScriptableObject {
+    #endif
+        #if !UNITY_2020_1_OR_NEWER
+		public static bool created {
+			get {
+                // If it's null, there's just no InkSettings asset in the project
+                return _instance != null;
+            }
+		}
+		static string absoluteSavePath {
+			get {
+				return System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath,"..","ProjectSettings","InkSettings.asset"));
+
+			}
+		}
+		public static void SaveStatic (bool saveAsText) {
+			UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget(new[] { instance }, absoluteSavePath, saveAsText);
+		}
+        public void Save (bool saveAsText) {
+			UnityEditorInternal.InternalEditorUtility.SaveToSerializedFileAndForget((UnityEngine.Object[]) new InkSettings[1] {this}, absoluteSavePath, saveAsText);
+		}
+
+		private static InkSettings _instance;
+		public static InkSettings instance {
+			get {
+				if(_instance == null) {
+					Object[] objects = UnityEditorInternal.InternalEditorUtility.LoadSerializedFileAndForget(absoluteSavePath);
+					if (objects != null && objects.Length > 0) {
+						instance = objects[0] as InkSettings;
+					} else {
+						instance = ScriptableObject.CreateInstance<InkSettings>();
+						instance.Save(true);
+					}
+				}
+				return _instance;
+			} private set {
+                if(_instance == value) return;
+				_instance = value;
+			}
+		}
+        #else
+		public static void SaveStatic (bool saveAsText) {
+			instance.Save(saveAsText);
+		}
+        #endif
+
+        public class AssetSaver : UnityEditor.AssetModificationProcessor {
+            static string[] OnWillSaveAssets(string[] paths) {
+                InkSettings.instance.Save(true);
+                return paths;
+            }
+        }
+
+		
+		
+		public TextAsset templateFile;
+		public string templateFilePath {
+			get {
+				if(templateFile == null) return "";
+				else return AssetDatabase.GetAssetPath(templateFile);
+			}
+		}
+
+
+        public DefaultAsset defaultJsonAssetPath;
+
+        public bool compileAutomatically = true;
+		public bool delayInPlayMode = true;
+		public bool handleJSONFilesAutomatically = true;
+
+		public int compileTimeout = 30;
+		
+		public bool printInkLogsInConsoleOnCompile;
+
+		#if UNITY_EDITOR && !UNITY_2018_1_OR_NEWER
+		[MenuItem("Edit/Project Settings/Ink", false, 500)]
+		public static void SelectFromProjectSettings() {
+			Selection.activeObject = instance;
+		}
+		#elif UNITY_EDITOR && UNITY_2018_1_OR_NEWER
+		public static SerializedObject GetSerializedSettings() {
+			return new SerializedObject(instance);
+		}
+		#endif
+        
+		// Deletes the persistent version of this asset that we used to use prior to 0.9.71
+		void OnEnable () {
+			if(!Application.isPlaying && EditorUtility.IsPersistent(this)) {
+				var path = AssetDatabase.GetAssetPath(this);
+				if(!string.IsNullOrEmpty(path)) {
+					#if !UNITY_2020_1_OR_NEWER
+                    if(_instance == this) _instance = null;
+					#endif
+                    AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(this));
+					AssetDatabase.Refresh();
+					return;
+				}
+			}
+		}
+	}	
+}
diff --git a/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs.meta b/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs.meta
new file mode 100644
index 0000000..2042be3
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Settings/InkSettings.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 983af1b6602bf416b9928d40b37a38bd
+timeCreated: 1485516445
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs b/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs
new file mode 100644
index 0000000..14a3a59
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs	
@@ -0,0 +1,103 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+namespace Ink.UnityIntegration {
+
+	[CustomEditor(typeof(InkSettings))]
+	public class InkSettingsEditor : Editor {
+
+		#pragma warning disable
+		protected InkSettings data;
+		
+		public void OnEnable() {
+			data = (InkSettings) target;
+		}
+		
+		public override void OnInspectorGUI() {
+			serializedObject.Update();
+
+			DrawSettings(serializedObject);
+
+			if(GUI.changed && target != null)
+				EditorUtility.SetDirty(target);
+			serializedObject.ApplyModifiedProperties();
+	    }
+
+		#if UNITY_2018_1_OR_NEWER
+		[SettingsProvider]
+		public static SettingsProvider CreateInkSettingsProvider() {
+			// First parameter is the path in the Settings window.
+			// Second parameter is the scope of this setting: it only appears in the Project Settings window.
+			var provider = new SettingsProvider("Project/Ink", SettingsScope.Project) {
+				// By default the last token of the path is used as display name if no label is provided.
+				label = "Ink",
+				// Create the SettingsProvider and initialize its drawing (IMGUI) function in place:
+				guiHandler = (searchContext) => {
+                    // Drawing the SO makes them disabled, and I have no idea why. Drawing manually until fixed.
+					// var settings = InkSettings.GetSerializedSettings();
+					DrawSettings(InkSettings.instance);
+				},
+
+				// Populate the search keywords to enable smart search filtering and label highlighting:
+				// keywords = new HashSet<string>(new[] { "Number", "Some String" })
+			};
+			return provider;
+		}
+		#endif
+
+        static string versionLabel => string.Format("Ink Unity Integration version "+InkLibrary.unityIntegrationVersionCurrent+"\nInk version "+InkLibrary.inkVersionCurrent+"\nInk story format version "+Ink.Runtime.Story.inkVersionCurrent+"\nInk save format version "+Ink.Runtime.StoryState.kInkSaveStateVersion);
+		static void DrawSettings (InkSettings settings) {
+			var cachedLabelWidth = EditorGUIUtility.labelWidth;
+			EditorGUIUtility.labelWidth = 260;
+
+			EditorGUILayout.HelpBox(versionLabel, MessageType.Info);
+
+			if(settings.templateFile == null) {
+				EditorGUILayout.HelpBox("Template not found. Ink files created via Assets > Create > Ink will be blank.", MessageType.Info);
+			}
+			settings.templateFile = (TextAsset)EditorGUILayout.ObjectField(new GUIContent("Ink Template", "Optional. The default content of files created via Assets > Create > Ink."), settings.templateFile, typeof(TextAsset));
+			settings.defaultJsonAssetPath = (DefaultAsset)EditorGUILayout.ObjectField(new GUIContent("New JSON Path", "By default, story JSON files are placed next to the ink. Drag a folder here to place new JSON files there instead."), settings.defaultJsonAssetPath, typeof(DefaultAsset));
+            settings.compileAutomatically = EditorGUILayout.Toggle(new GUIContent("Compile All Ink Automatically", "When disabled, automatic compilation can be enabled on a per-story basis via the inspector for a master story file. This can be helpful when you have several stories in a single project."), settings.compileAutomatically);
+            settings.delayInPlayMode = EditorGUILayout.Toggle(new GUIContent("Delay compilation if in Play Mode", "When enabled, ink compilation is delayed if in play mode. Files will be compiled on re-entering edit mode."), settings.delayInPlayMode);
+            settings.printInkLogsInConsoleOnCompile = EditorGUILayout.Toggle(new GUIContent("Print ink TODOs in console on compile", "When enabled, ink lines starting with TODO are printed in the console."), settings.printInkLogsInConsoleOnCompile);
+            settings.handleJSONFilesAutomatically = EditorGUILayout.Toggle(new GUIContent("Handle JSON Automatically", "Whether JSON files are moved, renamed and deleted along with their ink files."), settings.handleJSONFilesAutomatically);
+			settings.compileTimeout = EditorGUILayout.IntField(new GUIContent("Compile Timeout", "The max time the compiler will attempt to compile for in case of unhanded errors. You may need to increase this for very large ink projects."), settings.compileTimeout);
+
+			EditorGUIUtility.labelWidth = cachedLabelWidth;
+
+            EditorGUILayout.Separator();
+            if(GUILayout.Button("Show changelog")) {
+                InkUnityIntegrationStartupWindow.ShowWindow();
+            }
+		}
+		static void DrawSettings (SerializedObject settings) {
+			var cachedLabelWidth = EditorGUIUtility.labelWidth;
+			EditorGUIUtility.labelWidth = 260;
+			EditorGUI.BeginChangeCheck();
+
+			EditorGUILayout.HelpBox(versionLabel, MessageType.Info);
+
+			if(settings.FindProperty("templateFile").objectReferenceValue == null) {
+				EditorGUILayout.HelpBox("Template not found. Ink files created via Assets > Create > Ink will be blank.", MessageType.Info);
+			}
+			EditorGUILayout.PropertyField(settings.FindProperty("templateFile"), new GUIContent("Ink Template", "Optional. The default content of files created via Assets > Create > Ink."));
+			EditorGUILayout.PropertyField(settings.FindProperty("defaultJsonAssetPath"), new GUIContent("New JSON Path", "By default, story JSON files are placed next to the ink. Drag a folder here to place new JSON files there instead."));
+            EditorGUILayout.PropertyField(settings.FindProperty("compileAutomatically"), new GUIContent("Compile All Ink Automatically", "When disabled, automatic compilation can be enabled on a per-story basis via the inspector for a master story file. This can be helpful when you have several stories in a single project."));
+            EditorGUILayout.PropertyField(settings.FindProperty("delayInPlayMode"), new GUIContent("Delay compilation if in Play Mode", "When enabled, ink compilation is delayed if in play mode. Files will be compiled on re-entering edit mode."));
+            EditorGUILayout.PropertyField(settings.FindProperty("printInkLogsInConsoleOnCompile"), new GUIContent("Print ink TODOs in console on compile", "When enabled, ink lines starting with TODO are printed in the console."));
+            EditorGUILayout.PropertyField(settings.FindProperty("handleJSONFilesAutomatically"), new GUIContent("Handle JSON Automatically", "Whether JSON files are moved, renamed and deleted along with their ink files."));
+			EditorGUILayout.PropertyField(settings.FindProperty("compileTimeout"), new GUIContent("Compile Timeout", "The max time the compiler will attempt to compile for in case of unhanded errors. You may need to increase this for very large ink projects."));
+            
+            if(EditorGUI.EndChangeCheck()) {
+				settings.ApplyModifiedProperties();
+			}
+			EditorGUIUtility.labelWidth = cachedLabelWidth;
+            
+            EditorGUILayout.Separator();
+            if(GUILayout.Button("Show changelog")) {
+                InkUnityIntegrationStartupWindow.ShowWindow();
+            }
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs.meta b/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs.meta
new file mode 100644
index 0000000..67dc76a
--- /dev/null
+++ b/Assets/Ink/Editor/Core/Ink Settings/InkSettingsEditor.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4842c69e8d1f0443d8f4626c65ea356a
+timeCreated: 1485516445
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Core/InkEditorUtils.cs b/Assets/Ink/Editor/Core/InkEditorUtils.cs
new file mode 100644
index 0000000..0bab9cc
--- /dev/null
+++ b/Assets/Ink/Editor/Core/InkEditorUtils.cs
@@ -0,0 +1,294 @@
+using UnityEngine;
+using UnityEditor;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Linq;
+using Ink.Runtime;
+using UnityEditor.ProjectWindowCallback;
+using UnityEditor.Callbacks;
+using Path = System.IO.Path;
+
+namespace Ink.UnityIntegration {
+	class CreateInkAssetAction : EndNameEditAction {
+		public override void Action(int instanceId, string pathName, string resourceFile) {
+			var text = "";
+			if(File.Exists(resourceFile)) {
+				StreamReader streamReader = new StreamReader(resourceFile);
+				text = streamReader.ReadToEnd();
+				streamReader.Close();
+			}
+			UnityEngine.Object asset = CreateScriptAsset(pathName, text);
+			ProjectWindowUtil.ShowCreatedAsset(asset);
+		}
+		
+		internal static UnityEngine.Object CreateScriptAsset(string pathName, string text) {
+			string fullPath = Path.GetFullPath(pathName);
+			UTF8Encoding encoding = new UTF8Encoding(true, false);
+			bool append = false;
+			StreamWriter streamWriter = new StreamWriter(fullPath, append, encoding);
+			streamWriter.Write(text);
+			streamWriter.Close();
+			AssetDatabase.ImportAsset(pathName);
+			return AssetDatabase.LoadAssetAtPath(pathName, typeof(DefaultAsset));
+		}
+	}
+    
+	[InitializeOnLoad]
+	public static class InkEditorUtils {
+		public const string inkFileExtension = ".ink";
+		const string lastCompileTimeKey = "InkIntegrationLastCompileTime";
+
+		
+		// When compiling we call AssetDatabase.DisallowAutoRefresh. 
+		// We NEED to remember to re-allow it or unity stops registering file changes!
+		// The issue is that you need to pair calls perfectly, and you can't even use a try-catch to get around it.
+		// So - we cache if we've disabled auto refresh here, since this persists across plays.
+		// This does have one issue - this setting is saved even when unity re-opens, but the internal asset refresh state isn't.
+		// We need this to reset on launching the editor.
+		// We currently fix this by setting it false on InkEditorUtils.OnOpenUnityEditor
+		// A potentially better approach is to use playerprefs for this, since it's really nothing to do with the library.
+		public static bool disallowedAutoRefresh {
+			get {
+				if(EditorPrefs.HasKey("InkLibraryDisallowedAutoRefresh")) 
+					return EditorPrefs.GetBool("InkLibraryDisallowedAutoRefresh");
+				return false;
+			} set {
+				EditorPrefs.SetBool("InkLibraryDisallowedAutoRefresh", value);
+			}
+		}
+
+		// This should run before any of the other ink integration scripts.
+		static InkEditorUtils () {
+			EnsureFirstLaunchHandled();
+			EditorApplication.wantsToQuit += WantsToQuit;
+		}
+
+		// Save the current EditorApplication.timeSinceStartup so OnOpenUnityEditor is sure to run next time the editor opens. 
+		static bool WantsToQuit () {
+			LoadAndSaveLastCompileTime();
+			return true;
+		}
+		public static bool isFirstCompile;
+		static void EnsureFirstLaunchHandled () {
+			float lastCompileTime = LoadAndSaveLastCompileTime();
+			isFirstCompile = EditorApplication.timeSinceStartup < lastCompileTime;
+			if(isFirstCompile)
+				OnOpenUnityEditor();
+		}
+
+		static float LoadAndSaveLastCompileTime () {
+			float lastCompileTime = 0;
+			if(EditorPrefs.HasKey(lastCompileTimeKey))
+				lastCompileTime = EditorPrefs.GetFloat(lastCompileTimeKey);
+			EditorPrefs.SetFloat(lastCompileTimeKey, (float)EditorApplication.timeSinceStartup);
+			return lastCompileTime;
+		}
+
+		static void OnOpenUnityEditor () {
+			disallowedAutoRefresh = false;
+		}
+
+		[MenuItem("Assets/Rebuild Ink Library", false, 200)]
+		public static void RebuildLibrary() {
+			InkLibrary.Rebuild();
+		}
+
+		[MenuItem("Assets/Recompile Ink", false, 201)]
+		public static void RecompileAll() {
+			var filesToRecompile = InkLibrary.FilesCompiledByRecompileAll().ToArray();
+			string logString = filesToRecompile.Any() ? 
+				"Recompile All will compile "+string.Join(", ", filesToRecompile.Select(x => Path.GetFileName(x.filePath)).ToArray()) :
+				"No valid ink found. Note that only files with 'Compile Automatic' checked are compiled if not set to compile all files automatically in InkSettings file.";
+			Debug.Log(logString);
+			InkCompiler.CompileInk(filesToRecompile);
+		}
+
+        public static void RecompileAllImmediately() {
+            var filesToRecompile = InkLibrary.FilesCompiledByRecompileAll().ToArray();
+            string logString = filesToRecompile.Any() ? 
+                                   "Recompile All Immediately will compile "+string.Join(", ", filesToRecompile.Select(x => Path.GetFileName(x.filePath)).ToArray()) :
+                                   "No valid ink found. Note that only files with 'Compile Automatic' checked are compiled if not set to compile all files automatically in InkSettings file.";
+            Debug.Log(logString);
+            InkCompiler.CompileInk(filesToRecompile, true, null);
+        }
+
+
+		[MenuItem("Assets/Create/Ink", false, 120)]
+		public static void CreateNewInkFile () {
+			string fileName = "New Ink.ink";
+			string filePath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(GetSelectedPathOrFallback(), fileName));
+			CreateNewInkFile(filePath, InkSettings.instance.templateFilePath);
+		}
+
+		public static void CreateNewInkFile (string filePath, string templateFileLocation) {
+			ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, ScriptableObject.CreateInstance<CreateInkAssetAction>(), filePath, InkBrowserIcons.inkFileIcon, templateFileLocation);
+		}
+
+		private static string GetSelectedPathOrFallback() {
+			string path = "Assets";
+			foreach (UnityEngine.Object obj in Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets)) {
+				path = AssetDatabase.GetAssetPath(obj);
+				if (!string.IsNullOrEmpty(path) && File.Exists(path)) {
+					path = Path.GetDirectoryName(path);
+					break;
+				}
+			}
+			return path;
+		}
+
+		
+
+		[MenuItem("Help/Ink/About")]
+		public static void OpenAbout() {
+			Application.OpenURL("https://github.com/inkle/ink#ink");
+		}
+
+		[MenuItem("Help/Ink/Writing Tutorial...")]
+		public static void OpenWritingDocumentation() {
+			Application.OpenURL("https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md");
+		}
+		
+        [MenuItem("Help/Ink/API Documentation...")]
+		public static void OpenAPIDocumentation() {
+			Application.OpenURL("https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md");
+		}
+
+		[MenuItem("Help/Ink/Donate...")]
+		public static void Donate() {
+			Application.OpenURL("https://www.patreon.com/inkle");
+		}
+
+		[PostProcessBuildAttribute(-1)]
+		public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) {
+			if(!Debug.isDebugBuild) {
+				var color = EditorGUIUtility.isProSkin ? "#3498db" : "blue";
+				Debug.Log("<color="+color+">Thanks for using ink, and best of luck with your release!\nIf you're doing well, please help fund the project via Patreon https://www.patreon.com/inkle</color>");
+			}
+		}
+
+		public static TextAsset CreateStoryStateTextFile (string jsonStoryState, string defaultPath = "Assets/Ink", string defaultName = "storyState") {
+			string name = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(defaultPath, defaultName+".json")).Substring(defaultPath.Length+1);
+			string fullPathName = EditorUtility.SaveFilePanel("Save Story State", defaultPath, name, "json");
+			if(fullPathName == "") 
+				return null;
+			using (StreamWriter outfile = new StreamWriter(fullPathName)) {
+				outfile.Write(jsonStoryState);
+			}
+			string relativePath = AbsoluteToUnityRelativePath(fullPathName);
+			AssetDatabase.ImportAsset(relativePath);
+			TextAsset textAsset = AssetDatabase.LoadAssetAtPath<TextAsset>(relativePath);
+			return textAsset;
+		}
+
+		public static bool StoryContainsVariables (Story story) {
+			return story.variablesState.GetEnumerator().MoveNext();
+		}
+
+		public static bool CheckStoryIsValid (string storyJSON, out Exception exception) {
+			try {
+				new Story(storyJSON);
+			} catch (Exception ex) {
+				exception = ex;
+				return false;
+			}
+			exception = null;
+			return true;
+		}
+
+		public static bool CheckStoryIsValid (string storyJSON, out Story story) {
+			try {
+				story = new Story(storyJSON);
+			} catch {
+				story = null;
+				return false;
+			}
+			return true;
+		}
+
+		public static bool CheckStoryIsValid (string storyJSON, out Exception exception, out Story story) {
+			try {
+				story = new Story(storyJSON);
+			} catch (Exception ex) {
+				exception = ex;
+				story = null;
+				return false;
+			}
+			exception = null;
+			return true;
+		}
+
+		public static bool CheckStoryStateIsValid (string storyJSON, string storyStateJSON) {
+			Story story;
+			if(CheckStoryIsValid(storyJSON, out story)) {
+				try {
+					story.state.LoadJson(storyStateJSON);
+				} catch {
+					return false;
+				}
+			}
+			return true;
+		}
+		
+		// Returns a sanitized version of the supplied string by:
+		//    - swapping MS Windows-style file separators with Unix/Mac style file separators.
+		// If null is provided, null is returned.
+		public static string SanitizePathString(string path) {
+			if (path == null) {
+				return null;
+			}
+			return path.Replace('\\', '/');
+		}
+		
+		// Combines two file paths and returns that path.  Unlike C#'s native Paths.Combine, regardless of operating 
+		// system this method will always return a path which uses forward slashes ('/' characters) exclusively to ensure
+		// equality checks on path strings return equalities as expected.
+		public static string CombinePaths(string firstPath, string secondPath) {
+            Debug.Assert(firstPath != null);
+            Debug.Assert(secondPath != null);
+			return SanitizePathString(Path.Combine(firstPath, secondPath));
+		}
+
+		public static string AbsoluteToUnityRelativePath(string fullPath) {
+			return SanitizePathString(fullPath.Substring(Application.dataPath.Length-6));
+		}
+
+		public static string UnityRelativeToAbsolutePath(string filePath) {
+			return InkEditorUtils.CombinePaths(Application.dataPath, filePath.Substring(7));
+		}
+
+		/// <summary>
+		/// Draws a property field for a story using GUILayout, allowing you to attach stories to the player window for debugging.
+		/// </summary>
+		/// <param name="story">Story.</param>
+		/// <param name="label">Label.</param>
+		public static void DrawStoryPropertyField (Story story, GUIContent label) {
+			Debug.LogWarning("DrawStoryPropertyField has been moved from InkEditorUtils to InkPlayerWindow");
+		}
+
+		/// <summary>
+		/// Draws a property field for a story using GUI, allowing you to attach stories to the player window for debugging.
+		/// </summary>
+		/// <param name="position">Position.</param>
+		/// <param name="story">Story.</param>
+		/// <param name="label">Label.</param>
+		public static void DrawStoryPropertyField (Rect position, Story story, GUIContent label) {
+			Debug.LogWarning("DrawStoryPropertyField has been moved from InkEditorUtils to InkPlayerWindow");
+		}
+		
+		/// <summary>
+		/// Checks to see if the given path is an ink file or not, regardless of extension.
+		/// </summary>
+		/// <param name="path">The path to check.</param>
+		/// <returns>True if it's an ink file, otherwise false.</returns>
+		public static bool IsInkFile(string path) {
+			string extension = Path.GetExtension(path);
+			if (extension == InkEditorUtils.inkFileExtension) {
+				return true;
+			}
+
+			return String.IsNullOrEmpty(extension) && InkLibrary.instance.inkLibrary.Exists(f => f.filePath == path);
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Core/InkEditorUtils.cs.meta b/Assets/Ink/Editor/Core/InkEditorUtils.cs.meta
new file mode 100644
index 0000000..a5f10a7
--- /dev/null
+++ b/Assets/Ink/Editor/Core/InkEditorUtils.cs.meta
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: eeaeb991395be4e9e9ab83197c649ad8
+timeCreated: 1459944754
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/InkEditor.asmdef b/Assets/Ink/Editor/InkEditor.asmdef
new file mode 100644
index 0000000..c4d7d6d
--- /dev/null
+++ b/Assets/Ink/Editor/InkEditor.asmdef
@@ -0,0 +1,16 @@
+{
+    "name": "InkEditor",
+    "references": [
+        "Ink-Libraries"
+    ],
+    "optionalUnityReferences": [],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": []
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/InkEditor.asmdef.meta b/Assets/Ink/Editor/InkEditor.asmdef.meta
new file mode 100644
index 0000000..f61b141
--- /dev/null
+++ b/Assets/Ink/Editor/InkEditor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2047ab7874eb449d78c2b459017aa506
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools.meta b/Assets/Ink/Editor/Tools.meta
new file mode 100644
index 0000000..12eec72
--- /dev/null
+++ b/Assets/Ink/Editor/Tools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a26a7e3f7a600f249af24114621ba554
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Build Validation.meta b/Assets/Ink/Editor/Tools/Build Validation.meta
new file mode 100644
index 0000000..4bb5faf
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Build Validation.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 15e2b2aa92f3dff4b9aa6924a01bfd96
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs b/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs
new file mode 100644
index 0000000..a5e562b
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs	
@@ -0,0 +1,66 @@
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Build;
+using System.Text;
+using Ink.UnityIntegration;
+using System.Linq;
+#if UNITY_2018_1_OR_NEWER
+using UnityEditor.Build.Reporting;
+#endif
+
+class InkPreBuildValidationCheck : 
+#if UNITY_2018_1_OR_NEWER
+IPreprocessBuildWithReport
+#else
+IPreprocessBuild
+#endif
+{
+	public int callbackOrder { get { return 0; } }
+	
+    #if UNITY_2018_1_OR_NEWER
+    public void OnPreprocessBuild(BuildReport report) {
+        PreprocessValidationStep();
+    }
+    #else
+    public void OnPreprocessBuild(BuildTarget target, string path) {
+		PreprocessValidationStep();
+	}
+    #endif
+
+    static void PreprocessValidationStep () {
+        AssertNotCompiling();
+        CheckForInvalidFiles();
+    }
+
+    static void AssertNotCompiling () {
+        if(InkCompiler.compiling) {
+            StringBuilder sb = new StringBuilder("Ink is currently compiling!");
+            var errorString = sb.ToString();
+            InkCompiler.buildBlocked = true;
+            if(UnityEditor.EditorUtility.DisplayDialog("Ink Build Error!", errorString, "Ok")) {
+                Debug.LogError(errorString);
+            }
+        }
+    }
+    // When syncronous compilation is allowed we should try to replace this error with a compile.
+    static void CheckForInvalidFiles () {
+        var filesToRecompile = InkLibrary.GetFilesRequiringRecompile();
+        if(filesToRecompile.Any()) {
+            StringBuilder sb = new StringBuilder();
+            sb.AppendLine("There are Ink files which should be compiled, but appear not to be. You can resolve this by either:");
+            sb.AppendLine(" - Compiling your files via 'Assets/Recompile Ink'");
+            var resolveStep = " - Disabling 'Compile Automatically' "+(InkSettings.instance.compileAutomatically ? "in your Ink Settings file" : "for each of the files listed below");
+            sb.AppendLine(resolveStep);
+            sb.AppendLine();
+            sb.AppendLine("Files:");
+            var filesAsString = string.Join(", ", filesToRecompile.Select(x => x.filePath).ToArray());
+            sb.AppendLine(filesAsString);
+            var errorString = sb.ToString();
+            if(!UnityEditor.EditorUtility.DisplayDialog("Ink Build Error!", errorString, "Build anyway", "Cancel build")) {
+                Debug.LogError(errorString);
+            } else {
+                Debug.LogWarning(errorString);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs.meta b/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs.meta
new file mode 100644
index 0000000..8be8a80
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Build Validation/InkPreBuildValidationCheck.cs.meta	
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 260430d96c5f0aa46a31363984dc9088
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons.meta b/Assets/Ink/Editor/Tools/File Icons.meta
new file mode 100644
index 0000000..6b67090
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 51bebdd29a127594b90edb5938e78b79
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs b/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs
new file mode 100644
index 0000000..722642b
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs	
@@ -0,0 +1,184 @@
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+/* 
+* This script allows you to set custom icons for folders in project browser.
+* Recommended icon sizes - small: 16x16 px, large: 64x64 px;
+*/
+
+namespace Ink.UnityIntegration {
+	[InitializeOnLoad]
+	public class InkBrowserIcons {
+		private static bool isRetina {
+			get {
+				float unityVersion = float.Parse(Application.unityVersion.Substring (0, 3));
+				return Application.platform == RuntimePlatform.OSXEditor && unityVersion >= 5.4f;
+			}
+		}
+	    private const float largeIconSize = 64f;
+
+		private static Texture2D _inkFileIcon;
+		public static Texture2D inkFileIcon {
+			get {
+				if(_inkFileIcon == null) {
+					if(isRetina) {
+						_inkFileIcon = Resources.Load<Texture2D>("InkFileIcon-retina");
+					} else {
+						_inkFileIcon = Resources.Load<Texture2D>("InkFileIcon");
+					}
+				}
+				return _inkFileIcon;
+			}
+		}
+		private static Texture2D _inkFileIconLarge;
+		public static Texture2D inkFileIconLarge {
+			get {
+				if(_inkFileIconLarge == null) {
+					_inkFileIconLarge = Resources.Load<Texture2D>("InkFileIcon-large");
+				}
+				return _inkFileIconLarge;
+			}
+		}
+		private static Texture2D _errorIcon;
+		public static Texture2D errorIcon {
+			get {
+				if(_errorIcon == null) {
+					_errorIcon = Resources.Load<Texture2D>("InkErrorIcon");
+				}
+				return _errorIcon;
+			}
+		}
+		private static Texture2D _warningIcon;
+		public static Texture2D warningIcon {
+			get {
+				if(_warningIcon == null) {
+					_warningIcon = Resources.Load<Texture2D>("InkWarningIcon");
+				}
+				return _warningIcon;
+			}
+		}
+		private static Texture2D _todoIcon;
+		public static Texture2D todoIcon {
+			get {
+				if(_todoIcon == null) {
+					_todoIcon = Resources.Load<Texture2D>("InkTodoIcon");
+				}
+				return _todoIcon;
+			}
+		}
+		private static Texture2D _manualIcon;
+		public static Texture2D manualIcon {
+			get {
+				if(_manualIcon == null) {
+					_manualIcon = Resources.Load<Texture2D>("InkCompileManualIcon");
+				}
+				return _manualIcon;
+			}
+		}
+		private static Texture2D _childIcon;
+		public static Texture2D childIcon {
+			get {
+				if(_childIcon == null) {
+					_childIcon = Resources.Load<Texture2D>("InkChildIcon");
+				}
+				return _childIcon;
+			}
+		}
+		private static Texture2D _childIconLarge;
+		public static Texture2D childIconLarge {
+			get {
+				if(_childIconLarge == null) {
+					_childIconLarge = Resources.Load<Texture2D>("InkChildIcon-Large");
+				}
+				return _childIconLarge;
+			}
+		}
+		private static Texture2D _unknownFileIcon;
+		public static Texture2D unknownFileIcon {
+			get {
+				if(_unknownFileIcon == null) {
+					_unknownFileIcon = Resources.Load<Texture2D>("InkUnknownFileIcon");
+				}
+				return _unknownFileIcon;
+			}
+		}
+
+	    static InkBrowserIcons() {
+			EditorApplication.projectWindowItemOnGUI += OnDrawProjectWindowItem;
+	    }
+
+	    static void OnDrawProjectWindowItem(string guid, Rect rect) {
+	        string path = AssetDatabase.GUIDToAssetPath(guid);
+			if (InkEditorUtils.IsInkFile(path)) {
+				DefaultAsset asset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(path);
+				DrawInkFile(InkLibrary.GetInkFileWithFile(asset), rect);
+			}
+	    }
+
+		static void DrawInkFile (InkFile inkFile, Rect rect) {
+			bool isSmall = rect.width > rect.height;
+			if (isSmall) {
+				rect.width = rect.height;
+			} else {
+				rect.height = rect.width;
+			}
+			if (rect.width >= largeIconSize) {
+				DrawLarge(inkFile, rect);
+			} else {
+				DrawSmall(inkFile, rect);
+			}
+	    }
+
+		static void DrawLarge (InkFile inkFile, Rect rect) {
+			var offset = (rect.width - largeIconSize) * 0.5f;
+			rect = new Rect(rect.x + offset, rect.y + offset, largeIconSize, largeIconSize);
+			if(inkFileIconLarge != null)
+				GUI.DrawTexture(rect, inkFileIconLarge);
+
+			Rect miniRect = new Rect(rect.center, rect.size * 0.5f);
+			if(inkFile == null) {
+				if(unknownFileIcon != null) {
+					GUI.DrawTexture(miniRect, unknownFileIcon);
+				}
+			} else {
+				if(inkFile.hasErrors && errorIcon != null) {
+					GUI.DrawTexture(miniRect, errorIcon);
+				} else if(inkFile.hasWarnings && warningIcon != null) {
+					GUI.DrawTexture(miniRect, warningIcon);
+				} else if(inkFile.hasTodos && todoIcon != null) {
+					GUI.DrawTexture(miniRect, todoIcon);
+				}
+				if(!inkFile.isMaster && childIcon != null) {
+					GUI.DrawTexture(new Rect(rect.x, rect.y, rect.width * 0.5f, rect.height * 0.5f), childIconLarge);
+				}
+			}
+		}
+
+		static void DrawSmall (InkFile inkFile, Rect rect) {
+			if(inkFileIcon != null)
+				GUI.DrawTexture(rect, inkFileIcon);
+
+			if(inkFile == null) {
+				if(unknownFileIcon != null) {
+					GUI.DrawTexture(new Rect(rect.x, rect.y, unknownFileIcon.width, unknownFileIcon.height), unknownFileIcon);
+				}
+			} else {
+				if(!InkSettings.instance.compileAutomatically && !inkFile.compileAutomatically && inkFile.isMaster)
+					GUI.DrawTexture(new Rect(rect.x, rect.y + rect.size.y * 0.5f, rect.size.x * 0.5f, rect.size.y * 0.5f), manualIcon);
+
+				Rect miniRect = new Rect(rect.center, rect.size * 0.5f);
+				if(inkFile.hasErrors && errorIcon != null) {
+					GUI.DrawTexture(miniRect, errorIcon);
+				} else if(inkFile.hasWarnings && warningIcon != null) {
+					GUI.DrawTexture(miniRect, warningIcon);
+				} else if(inkFile.hasTodos && todoIcon != null) {
+					GUI.DrawTexture(miniRect, todoIcon);
+				}
+				if(!inkFile.isMaster && childIcon != null) {
+					GUI.DrawTexture(new Rect(rect.x, rect.y, childIcon.width, childIcon.height), childIcon);
+				}
+			}
+	    }
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs.meta b/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs.meta
new file mode 100644
index 0000000..6a18f00
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/InkBrowserIcons.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 38c650d4ee11f47559699f833d29d4b9
+timeCreated: 1459341699
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources.meta b/Assets/Ink/Editor/Tools/File Icons/Resources.meta
new file mode 100644
index 0000000..1af2805
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources.meta	
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 7db532cf76e3b4072a4e7ea2ff88d03e
+folderAsset: yes
+timeCreated: 1475576594
+licenseType: Store
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd
new file mode 100644
index 0000000..186a1dc
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8fd610044df3d917e89f7996a6704fe111848a3f339dd03f1a89aaabf0a1f14b
+size 29759
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd.meta
new file mode 100644
index 0000000..c3508fc
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon-Large.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: f4e541342c07c476094e04dc118fed7b
+timeCreated: 1463742861
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd
new file mode 100644
index 0000000..5ab5b4c
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:362701572e3181b5dcfd21aae40cf6d9f9f58cf4e2e096bbaec6b11fb0a614f7
+size 24016
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd.meta
new file mode 100644
index 0000000..dda4895
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkChildIcon.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: 53256257fd17e4dfda5f08016551019e
+timeCreated: 1461958426
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd
new file mode 100644
index 0000000..7bf4c79
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dc50553a3f939c71460acefe7ba720b8561a055a39723c69a8e8f0654922f21c
+size 95600
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd.meta
new file mode 100644
index 0000000..e9d3729
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkCompileManualIcon.psd.meta	
@@ -0,0 +1,84 @@
+fileFormatVersion: 2
+guid: 1e91fdbfb2b154a18b675257bc4d43d9
+timeCreated: 1485360612
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 4
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 0
+    linearTexture: 1
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 2
+  textureShape: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Standalone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: iPhone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt b/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt
new file mode 100644
index 0000000..4e1a12e
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt	
@@ -0,0 +1,3 @@
+Hello world!
+	*	Hello back!
+	Nice to hear from you!
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt.meta
new file mode 100644
index 0000000..25f7607
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkDefaultTemplate.txt.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0487be7f554cd45a6916f94d2b6baf50
+timeCreated: 1462291397
+licenseType: Store
+TextScriptImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd
new file mode 100644
index 0000000..c5c27f9
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:24c4f1278f1be311100dd66b96d00238e012e0227b25d81e60247a5ea9453b9e
+size 81094
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd.meta
new file mode 100644
index 0000000..f996900
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkErrorIcon.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: ab12885a47a21456596113fc9233946c
+timeCreated: 1461099630
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd
new file mode 100644
index 0000000..de964b9
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf2e39498a1fb78017993f8d8569c8f43acc6e09407df34c47aac68ec84ad44e
+size 884643
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd.meta
new file mode 100644
index 0000000..afaa89b
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-Large.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: 957c8ea47888d42349ae208cffe69c4b
+timeCreated: 1463734461
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 16
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd
new file mode 100644
index 0000000..63d699d
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bac32c548ebe40ee117b6312b99e2a6b8bab399713140ce18250dd0e3454677
+size 29421
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd.meta
new file mode 100644
index 0000000..ec20d5d
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon-retina.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: bd3c497264be941e3a935055a433bbb9
+timeCreated: 1459953147
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd
new file mode 100644
index 0000000..8a344fc
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:86cc450345c028f4d500e0bf0d76319cb1829bb046099735fea66ae3bcb20267
+size 83098
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd.meta
new file mode 100644
index 0000000..ea72bd3
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkFileIcon.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: 8e6b218461abe4192baeeabfbe7a9ced
+timeCreated: 1459341708
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 16
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd
new file mode 100644
index 0000000..5800588
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:185f4d3d11ad50f253684f2df2450f3f956ef06d6e3d5c54a95bc67e8a9d63d7
+size 76048
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd.meta
new file mode 100644
index 0000000..fadbe0d
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkTodoIcon.psd.meta	
@@ -0,0 +1,84 @@
+fileFormatVersion: 2
+guid: 73b0b2e787c654227845ff231c5eab5d
+timeCreated: 1485360612
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 4
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 0
+    linearTexture: 1
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 2
+  textureShape: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Standalone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: iPhone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd
new file mode 100644
index 0000000..1d0a61c
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5a6771a1c283db1f508083853c487eb4431b2c9cfa60530fd3e2f1436b34d95a
+size 24714
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd.meta
new file mode 100644
index 0000000..3d802ed
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkUnknownFileIcon.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: b26518ba7d72a4fd3bc42599f621c839
+timeCreated: 1462526037
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd b/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd
new file mode 100644
index 0000000..982b540
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6dae2af1da1b1353d4d320162beb62885050e3e10f307f826d05d366de892d8
+size 66606
diff --git a/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd.meta b/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd.meta
new file mode 100644
index 0000000..d355c19
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/File Icons/Resources/InkWarningIcon.psd.meta	
@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: efc1324f6160840a9a4c26231cc2d304
+timeCreated: 1461099630
+licenseType: Store
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 2
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    linearTexture: 1
+    correctGamma: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 0
+  cubemapConvolution: 0
+  cubemapConvolutionSteps: 7
+  cubemapConvolutionExponent: 1.5
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 0
+    aniso: 1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  rGBM: 0
+  compressionQuality: 50
+  allowsAlphaSplitting: 0
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaIsTransparency: 1
+  textureType: 2
+  buildTargetSettings: []
+  spriteSheet:
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector.meta b/Assets/Ink/Editor/Tools/Ink Inspector.meta
new file mode 100644
index 0000000..7f872c8
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector.meta	
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: c7ff1896f35a148d8a4c1850f1e8e13d
+folderAsset: yes
+timeCreated: 1459934446
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs
new file mode 100644
index 0000000..5289271
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs	
@@ -0,0 +1,60 @@
+using UnityEngine;
+using UnityEditor;
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Ink.UnityIntegration {
+	[CustomEditor(typeof(DefaultAsset), true)]
+	public class DefaultAssetEditor : Editor {
+
+		private DefaultAssetInspector inspector;
+
+		private void OnEnable () {
+			inspector = FindObjectInspector ();
+			if(inspector != null) {
+				inspector.editor = this;
+				inspector.serializedObject = serializedObject;
+				inspector.target = target;
+				inspector.OnEnable();
+			}
+		}
+
+		private void OnDisable () {
+			if(inspector != null)
+				inspector.OnDisable();
+		}
+
+		protected override void OnHeaderGUI () {
+			if(inspector != null) {
+				inspector.OnHeaderGUI();
+			}
+			else
+				base.OnHeaderGUI();
+		}
+
+		public override void OnInspectorGUI () {
+			if(inspector != null) {
+				GUI.enabled = true;
+				inspector.OnInspectorGUI();
+			}
+			else
+				base.OnInspectorGUI();
+		}
+
+		private DefaultAssetInspector FindObjectInspector () {
+			var assembly = Assembly.GetExecutingAssembly();
+			var assetPath = AssetDatabase.GetAssetPath(target);
+			foreach(var type in assembly.GetTypes()) {
+				if(type.IsSubclassOf(typeof(DefaultAssetInspector))) {
+					DefaultAssetInspector objectInspector = (DefaultAssetInspector)Activator.CreateInstance(type);
+					if(objectInspector.IsValid(assetPath)) {
+						objectInspector.target = target;
+						return objectInspector;
+					}
+				}
+			}
+			return null;
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs.meta b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs.meta
new file mode 100644
index 0000000..c3c6061
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetEditor.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: fe3507ae5d970447197e131c39ac31b6
+timeCreated: 1464440328
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs
new file mode 100644
index 0000000..70816a0
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs	
@@ -0,0 +1,19 @@
+using UnityEngine;
+using UnityEditor;
+
+namespace Ink.UnityIntegration {
+	public abstract class DefaultAssetInspector {
+		// Reference to the actual editor we draw to
+		public Editor editor;
+		// Shortcut to the target object
+		public Object target;
+		// Shortcut to the serializedObject
+		public SerializedObject serializedObject;
+
+		public abstract bool IsValid(string assetPath);
+		public virtual void OnEnable () {}
+		public virtual void OnDisable () {}
+		public virtual void OnHeaderGUI () {}
+		public virtual void OnInspectorGUI() {}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs.meta b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs.meta
new file mode 100644
index 0000000..3002c4f
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/DefaultAssetInspector.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 7fa6f007190b6408c8aebb32050a3bbe
+timeCreated: 1470299519
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs b/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs
new file mode 100644
index 0000000..1583ad6
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs	
@@ -0,0 +1,418 @@
+using UnityEngine;
+using UnityEditor;
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEditorInternal;
+using Object = UnityEngine.Object;
+
+namespace Ink.UnityIntegration {
+	public class InkInspector : DefaultAssetInspector {
+
+		private InkFile inkFile;
+		private ReorderableList includesFileList;
+		private ReorderableList mastersFileList;
+		private ReorderableList errorList;
+		private ReorderableList warningList;
+		private ReorderableList todosList;
+		private string cachedTrimmedFileContents;
+		private const int maxCharacters = 16000;
+
+		public override bool IsValid(string assetPath) {
+			return Path.GetExtension(assetPath) == InkEditorUtils.inkFileExtension;
+		}
+
+		public override void OnHeaderGUI () {
+			GUILayout.BeginHorizontal();
+			GUILayout.Space(38f);
+			GUILayout.BeginVertical();
+			GUILayout.Space(19f);
+			GUILayout.BeginHorizontal();
+
+			GUILayoutUtility.GetRect(10f, 10f, 16f, 16f, EditorStyles.layerMaskField);
+			GUILayout.FlexibleSpace();
+
+			EditorGUI.BeginDisabledGroup(inkFile == null);
+			if (GUILayout.Button("Open", EditorStyles.miniButton)) {
+				AssetDatabase.OpenAsset(inkFile.inkAsset, 3);
+				GUIUtility.ExitGUI();
+			}
+			EditorGUI.EndDisabledGroup();
+
+			GUILayout.EndHorizontal();
+			GUILayout.EndVertical();
+			GUILayout.EndHorizontal();
+
+			Rect lastRect = GUILayoutUtility.GetLastRect();
+			Rect rect = new Rect(lastRect.x, lastRect.y, lastRect.width, lastRect.height);
+			Rect iconRect = new Rect(rect.x + 6f, rect.y + 6f, 32f, 32f);
+			GUI.DrawTexture(iconRect, InkBrowserIcons.inkFileIconLarge);
+			Rect childIconRect = new Rect(iconRect.x, iconRect.y, 16f, 16f);
+			if(inkFile == null) {
+				GUI.DrawTexture(childIconRect, InkBrowserIcons.unknownFileIcon, ScaleMode.ScaleToFit);
+			} else if(!inkFile.isMaster) {
+				GUI.DrawTexture(childIconRect, InkBrowserIcons.childIconLarge, ScaleMode.ScaleToFit);
+			}
+
+			Rect titleRect = new Rect(rect.x + 44f, rect.y + 6f, rect.width - 44f - 38f - 4f, 16f);
+			titleRect.yMin -= 2f;
+			titleRect.yMax += 2f;
+			GUI.Label(titleRect, editor.target.name, EditorStyles.largeLabel);
+		}
+
+		public override void OnEnable () {
+			Rebuild();
+			InkCompiler.OnCompileInk += OnCompileInk;
+		}
+
+		public override void OnDisable () {
+			InkCompiler.OnCompileInk -= OnCompileInk;
+		}
+
+		void OnCompileInk (InkFile inkFile) {
+			Rebuild();
+		}
+
+		void Rebuild () {
+			cachedTrimmedFileContents = "";
+			string assetPath = AssetDatabase.GetAssetPath(target);
+			inkFile = InkLibrary.GetInkFileWithPath(assetPath);
+			if(inkFile == null) 
+				return;
+
+			if (inkFile.includes.Count > 0) CreateIncludeList ();
+			else includesFileList = null;
+
+			if (inkFile.masterInkAssets.Count > 0) CreateMastersList ();
+			else mastersFileList = null;
+
+			CreateErrorList();
+			CreateWarningList();
+			CreateTodoList();
+			cachedTrimmedFileContents = inkFile.GetFileContents();
+			cachedTrimmedFileContents = cachedTrimmedFileContents.Substring(0, Mathf.Min(cachedTrimmedFileContents.Length, maxCharacters));
+			if(cachedTrimmedFileContents.Length >= maxCharacters)
+				cachedTrimmedFileContents += "...\n\n<...etc...>";
+		}
+
+		void CreateIncludeList () {
+			List<DefaultAsset> includeTextAssets = inkFile.includes;
+			includesFileList = new ReorderableList(includeTextAssets, typeof(DefaultAsset), false, false, false, false);
+			includesFileList.drawHeaderCallback = (Rect rect) => {  
+				EditorGUI.LabelField(rect, "Included Files");
+			};
+			includesFileList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				DefaultAsset childAssetFile = ((List<DefaultAsset>)includesFileList.list)[index];
+				if(childAssetFile == null) {
+					Debug.LogError("Ink file in include list is null. This should never occur. Use Assets > Recompile Ink to fix this issue.");
+					EditorGUI.LabelField(rect, new GUIContent("Warning: Ink File in include list is null. Use Assets > Recompile Ink to fix this issue."));
+					return;
+				}
+				InkFile childInkFile = InkLibrary.GetInkFileWithFile(childAssetFile);
+				if(childInkFile == null) {
+					Debug.LogError("Ink File for included file "+childAssetFile+" not found. This should never occur. Use Assets > Recompile Ink to fix this issue.");
+					EditorGUI.LabelField(rect, new GUIContent("Warning: Ink File for included file "+childAssetFile+" not found. Use Assets > Recompile Ink to fix this issue."));
+					return;
+				}
+				Rect iconRect = new Rect(rect.x, rect.y, 0, 16);
+				if(childInkFile.hasErrors || childInkFile.hasWarnings) {
+					iconRect.width = 20;
+				}
+				Rect objectFieldRect = new Rect(iconRect.xMax, rect.y, rect.width - iconRect.width - 80, 16);
+				Rect selectRect = new Rect(objectFieldRect.xMax, rect.y, 80, 16);
+				if(childInkFile.hasErrors) {
+					EditorGUI.LabelField(iconRect, new GUIContent(InkBrowserIcons.errorIcon));
+				} else if(childInkFile.hasWarnings) {
+					EditorGUI.LabelField(iconRect, new GUIContent(InkBrowserIcons.warningIcon));
+				}
+				EditorGUI.BeginDisabledGroup(true);
+				EditorGUI.ObjectField(objectFieldRect, childAssetFile, typeof(Object), false);
+				EditorGUI.EndDisabledGroup();
+				if(GUI.Button(selectRect, "Select")) {
+					Selection.activeObject = childAssetFile;
+				}
+			};
+		}
+
+		void CreateMastersList () {
+			List<DefaultAsset> mastersTextAssets = inkFile.masterInkAssets;
+			mastersFileList = new ReorderableList(mastersTextAssets, typeof(DefaultAsset), false, false, false, false);
+			mastersFileList.drawHeaderCallback = (Rect rect) => {  
+				EditorGUI.LabelField(rect, "Master Files");
+			};
+			mastersFileList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				DefaultAsset masterAssetFile = ((List<DefaultAsset>)mastersFileList.list)[index];
+				if(masterAssetFile == null) {
+					Debug.LogError("Ink file in masters list is null. This should never occur. Use Assets > Recompile Ink to fix this issue.");
+					EditorGUI.LabelField(rect, new GUIContent("Warning: Ink File in masters list is null. Use Assets > Recompile Ink to fix this issue."));
+					return;
+				}
+				InkFile masterInkFile = InkLibrary.GetInkFileWithFile(masterAssetFile);
+				if(masterInkFile == null) {
+					Debug.LogError("Ink File for master file "+masterAssetFile+" not found. This should never occur. Use Assets > Recompile Ink to fix this issue.");
+					EditorGUI.LabelField(rect, new GUIContent("Warning: Ink File for master file "+masterAssetFile+" not found. Use Assets > Recompile Ink to fix this issue."));
+					return;
+				}
+				Rect iconRect = new Rect(rect.x, rect.y, 0, 16);
+				if(masterInkFile.hasErrors || masterInkFile.hasWarnings) {
+					iconRect.width = 20;
+				}
+				Rect objectFieldRect = new Rect(iconRect.xMax, rect.y, rect.width - iconRect.width - 80, 16);
+				Rect selectRect = new Rect(objectFieldRect.xMax, rect.y, 80, 16);
+				if(masterInkFile.hasErrors) {
+					EditorGUI.LabelField(iconRect, new GUIContent(InkBrowserIcons.errorIcon));
+				} else if(masterInkFile.hasWarnings) {
+					EditorGUI.LabelField(iconRect, new GUIContent(InkBrowserIcons.warningIcon));
+				}
+				EditorGUI.BeginDisabledGroup(true);
+				EditorGUI.ObjectField(objectFieldRect, masterAssetFile, typeof(Object), false);
+				EditorGUI.EndDisabledGroup();
+				if(GUI.Button(selectRect, "Select")) {
+					Selection.activeObject = masterAssetFile;
+				}
+
+				
+			// foreach(var masterInkFile in inkFile.masterInkFiles) {
+			// 	EditorGUILayout.BeginHorizontal();
+			// 	if(masterInkFile.hasErrors) {
+			// 		GUILayout.Label(new GUIContent(InkBrowserIcons.errorIcon), GUILayout.Width(20));
+			// 	} else if(masterInkFile.hasWarnings) {
+			// 		GUILayout.Label(new GUIContent(InkBrowserIcons.warningIcon), GUILayout.Width(20));
+			// 	}
+			// 	EditorGUI.BeginDisabledGroup(true);
+			// 	EditorGUILayout.ObjectField("Master Ink File", masterInkFile.inkAsset, typeof(Object), false);
+			// 	EditorGUI.EndDisabledGroup();
+			// 	if(GUILayout.Button("Select", GUILayout.Width(80))) {
+			// 		Selection.activeObject = masterInkFile.inkAsset;
+			// 	}
+			// 	EditorGUILayout.EndHorizontal();
+			// }
+			};
+		}
+
+		void CreateErrorList () {
+			errorList = new ReorderableList(inkFile.errors, typeof(string), false, false, false, false);
+			errorList.elementHeight = 18;
+			errorList.drawHeaderCallback = (Rect rect) => {  
+				EditorGUI.LabelField(rect, new GUIContent(InkBrowserIcons.errorIcon), new GUIContent("Errors"));
+			};
+			errorList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				Rect labelRect = new Rect(rect.x, rect.y, rect.width - 80, rect.height);
+				Rect buttonRect = new Rect(labelRect.xMax, rect.y, 80, rect.height-2);
+				InkCompilerLog log = ((List<InkCompilerLog>)errorList.list)[index];
+				string label = log.content;
+				GUI.Label(labelRect, label);
+				string openLabel = "Open"+ (log.lineNumber == -1 ? "" : " ("+log.lineNumber+")");
+				if(GUI.Button(buttonRect, openLabel)) {
+					OpenInEditor(inkFile.filePath, log.lineNumber);
+				}
+			};
+		}
+
+		void CreateWarningList () {
+			warningList = new ReorderableList(inkFile.warnings, typeof(string), false, false, false, false);
+			warningList.elementHeight = 18;
+			warningList.drawHeaderCallback = (Rect rect) => {  
+				EditorGUI.LabelField(rect, new GUIContent(InkBrowserIcons.warningIcon), new GUIContent("Warnings"));
+			};
+			warningList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				Rect labelRect = new Rect(rect.x, rect.y, rect.width - 80, rect.height);
+				Rect buttonRect = new Rect(labelRect.xMax, rect.y, 80, rect.height-2);
+				InkCompilerLog log = ((List<InkCompilerLog>)warningList.list)[index];
+				string label = log.content;
+				GUI.Label(labelRect, label);
+				string openLabel = "Open"+ (log.lineNumber == -1 ? "" : " ("+log.lineNumber+")");
+				if(GUI.Button(buttonRect, openLabel)) {
+					OpenInEditor(inkFile.filePath, log.lineNumber);
+				}
+			};
+		}
+
+		void CreateTodoList () {
+			todosList = new ReorderableList(inkFile.todos, typeof(string), false, false, false, false);
+			todosList.elementHeight = 18;
+			todosList.drawHeaderCallback = (Rect rect) => {  
+				EditorGUI.LabelField(rect, "To do");
+			};
+			todosList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				Rect labelRect = new Rect(rect.x, rect.y, rect.width - 80, rect.height);
+				Rect buttonRect = new Rect(labelRect.xMax, rect.y, 80, rect.height-2);
+				InkCompilerLog log = ((List<InkCompilerLog>)todosList.list)[index];
+				string label = log.content;
+				GUI.Label(labelRect, label);
+				string openLabel = "Open"+ (log.lineNumber == -1 ? "" : " ("+log.lineNumber+")");
+				if(GUI.Button(buttonRect, openLabel)) {
+					OpenInEditor(inkFile.filePath, log.lineNumber);
+				}
+			};
+		}
+
+		static void OpenInEditor (string filePath, int lineNumber) {
+			#if UNITY_2019_1_OR_NEWER
+			// This function replaces OpenFileAtLineExternal, but I guess it's totally internal and can't be accessed.
+			// CodeEditorUtility.Editor.Current.OpenProject(filePath, lineNumber);
+			#pragma warning disable
+			UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(filePath, lineNumber);
+			#pragma warning restore
+			#else
+			UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(filePath, lineNumber);
+			#endif
+		}
+
+		public override void OnInspectorGUI () {
+			editor.Repaint();
+			serializedObject.Update();
+			if(inkFile == null) {
+				EditorGUILayout.HelpBox("Ink File is not in library.", MessageType.Warning);
+				if(GUILayout.Button("Rebuild Library")) {
+					InkLibrary.Rebuild();
+					Rebuild();
+				}
+				return;
+			}
+
+			if(InkCompiler.GetCompilationStackItem(inkFile) != null) {
+				EditorGUILayout.HelpBox("File is compiling...", MessageType.Info);
+				return;
+			}
+			
+			if(inkFile.isMaster) {
+				DrawMasterFileHeader();
+				DrawEditAndCompileDates(inkFile);
+				if(inkFile.hasUnhandledCompileErrors) {
+					EditorGUILayout.HelpBox("Last compiled failed", MessageType.Error);
+				} if(inkFile.hasErrors) {
+					EditorGUILayout.HelpBox("Last compiled had errors", MessageType.Error);
+				} else if(inkFile.hasWarnings) {
+					EditorGUILayout.HelpBox("Last compile had warnings", MessageType.Warning);
+				} else if(inkFile.jsonAsset == null) {
+					EditorGUILayout.HelpBox("Ink file has not been compiled", MessageType.Warning);
+				}
+				if(inkFile.requiresCompile && GUILayout.Button("Compile")) {
+					InkCompiler.CompileInk(inkFile);
+				}
+				DrawIncludedFiles();
+
+				DrawCompileErrors();
+				DrawErrors();
+				DrawWarnings();
+				DrawTODOList();
+			} else {
+				DrawSubFileHeader();
+			}
+
+			DrawFileContents ();
+			
+
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		void DrawMasterFileHeader () {
+			EditorGUILayout.LabelField("Master File", EditorStyles.boldLabel);
+			if(!InkSettings.instance.compileAutomatically) {
+				inkFile.compileAutomatically = EditorGUILayout.Toggle("Compile Automatially", inkFile.compileAutomatically);
+				EditorApplication.RepaintProjectWindow();
+			}
+			EditorGUI.BeginDisabledGroup(true);
+			EditorGUILayout.ObjectField("JSON Asset", inkFile.jsonAsset, typeof(TextAsset), false);
+			EditorGUI.EndDisabledGroup();
+
+			if(inkFile.jsonAsset != null && inkFile.errors.Count == 0 && GUILayout.Button("Play")) {
+				InkPlayerWindow.LoadAndPlay(inkFile.jsonAsset);
+			}
+
+//				if(!checkedStoryForErrors) {
+//					if(GUILayout.Button("Check for errors")) {
+//						GetStoryErrors();
+//					}
+//				} else {
+//					if(exception != null) {
+//						EditorGUILayout.HelpBox("Story is invalid\n"+exception.ToString(), MessageType.Error);
+//					} else {
+//						EditorGUILayout.HelpBox("Story is valid", MessageType.Info);
+//					}
+//				}
+		}
+
+		void DrawSubFileHeader() {
+			EditorGUILayout.LabelField("Include File", EditorStyles.boldLabel);
+			if(mastersFileList != null && mastersFileList.count > 0) {
+				mastersFileList.DoLayoutList();
+			}
+		}
+
+		void DrawEditAndCompileDates (InkFile masterInkFile) {
+			string editAndCompileDateString = "";
+			DateTime lastEditDate = inkFile.lastEditDate;
+			editAndCompileDateString += "Last edit date "+lastEditDate.ToString();
+			if(masterInkFile.jsonAsset != null) {
+				DateTime lastCompileDate = masterInkFile.lastCompileDate;
+				editAndCompileDateString += "\nLast compile date "+lastCompileDate.ToString();
+				if(lastEditDate > lastCompileDate) {
+                    if(EditorApplication.isPlaying && InkSettings.instance.delayInPlayMode) {
+					    editAndCompileDateString += "\nWill compile on exiting play mode";
+                        EditorGUILayout.HelpBox(editAndCompileDateString, MessageType.Info);
+                    } else {
+					    EditorGUILayout.HelpBox(editAndCompileDateString, MessageType.Warning);
+                    }
+				} else {
+					EditorGUILayout.HelpBox(editAndCompileDateString, MessageType.None);
+				}
+			} else {
+				EditorGUILayout.HelpBox(editAndCompileDateString, MessageType.None);
+			}
+		}
+
+		void DrawIncludedFiles () {
+			if(includesFileList != null && includesFileList.count > 0) {
+				includesFileList.DoLayoutList();
+			}
+		}
+
+		void DrawCompileErrors () {
+			if(inkFile.unhandledCompileErrors.Count == 0) 
+				return;
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			EditorGUILayout.HelpBox("Compiler bug prevented compilation of JSON file. Please help us fix it by reporting this as a bug.", MessageType.Error);
+			EditorGUILayout.BeginHorizontal();
+			if(GUILayout.Button("Report via Github")) {
+				Application.OpenURL("https://github.com/inkle/ink-unity-integration/issues/new");
+			}
+			if(GUILayout.Button("Report via Email")) {
+				Application.OpenURL("mailto:info@inklestudios.com");
+			}
+			EditorGUILayout.EndHorizontal();
+			foreach(string compileError in inkFile.unhandledCompileErrors) {
+				GUILayout.TextArea(compileError);
+			}
+			EditorGUILayout.EndVertical();
+		}
+
+		void DrawErrors () {
+			if(errorList != null && errorList.count > 0) {
+				errorList.DoLayoutList();
+			}
+		}
+
+		void DrawWarnings () {
+			if(warningList != null && warningList.count > 0) {
+				warningList.DoLayoutList();
+			}
+		}
+
+		void DrawTODOList () {
+			if(todosList != null && todosList.count > 0) {
+				todosList.DoLayoutList();
+			}
+		}
+
+		void DrawFileContents () {
+			float width = EditorGUIUtility.currentViewWidth-50;
+			float height = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(cachedTrimmedFileContents), width);
+			EditorGUILayout.BeginVertical(EditorStyles.textArea);
+			EditorGUILayout.SelectableLabel(cachedTrimmedFileContents, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true), GUILayout.Width(width), GUILayout.Height(height));
+			EditorGUILayout.EndVertical();
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs.meta b/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs.meta
new file mode 100644
index 0000000..681542c
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Ink Inspector/InkInspector.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 6905e53703af64a70aa6eb42f98b047c
+timeCreated: 1460011343
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Player Window.meta b/Assets/Ink/Editor/Tools/Player Window.meta
new file mode 100644
index 0000000..bb61b1d
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Player Window.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 09e392e872101fd4885d68e71c7ab38b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs b/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs
new file mode 100644
index 0000000..5698031
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs	
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using Ink.Runtime;
+using UnityEngine;
+
+namespace Ink.UnityIntegration.Debugging {
+    [System.Serializable]
+    public class InkHistoryContentItem {
+        public enum ContentType {
+            PresentedContent,
+            ChooseChoice,
+            PresentedChoice,
+            EvaluateFunction,
+            CompleteEvaluateFunction,
+            ChoosePathString,
+            Warning,
+            Error,
+            DebugNote
+        }
+
+        public string content;
+        public List<string> tags;
+        public ContentType contentType;
+        [SerializeField]
+        JsonDateTime _time;
+        public DateTime time {
+            get {
+                return _time;
+            } private set {
+                _time = value;
+            }
+        }
+
+        InkHistoryContentItem (string text, ContentType contentType) {
+            this.content = text;
+            this.contentType = contentType;
+            this.time = DateTime.Now;
+        }
+        InkHistoryContentItem (string text, List<string> tags, ContentType contentType) {
+            this.content = text;
+            this.tags = tags;
+            this.contentType = contentType;
+            this.time = DateTime.Now;
+        }
+
+        public static InkHistoryContentItem CreateForContent (string choiceText, List<string> tags) {
+            return new InkHistoryContentItem(choiceText, tags, InkHistoryContentItem.ContentType.PresentedContent);
+        }
+        public static InkHistoryContentItem CreateForPresentChoice (Choice choice) {
+            return new InkHistoryContentItem(choice.text.Trim(), InkHistoryContentItem.ContentType.PresentedChoice);
+        }
+        public static InkHistoryContentItem CreateForMakeChoice (Choice choice) {
+            return new InkHistoryContentItem(choice.text.Trim(), InkHistoryContentItem.ContentType.ChooseChoice);
+        }
+        public static InkHistoryContentItem CreateForEvaluateFunction (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.EvaluateFunction);
+        }
+        public static InkHistoryContentItem CreateForCompleteEvaluateFunction (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.CompleteEvaluateFunction);
+        }
+        public static InkHistoryContentItem CreateForChoosePathString (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.ChoosePathString);
+        }
+        public static InkHistoryContentItem CreateForWarning (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.Warning);
+        }
+        public static InkHistoryContentItem CreateForError (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.Error);
+        }
+        public static InkHistoryContentItem CreateForDebugNote (string choiceText) {
+            return new InkHistoryContentItem(choiceText, InkHistoryContentItem.ContentType.DebugNote);
+        }
+
+        struct JsonDateTime {
+            public long value;
+            public static implicit operator DateTime(JsonDateTime jdt) {
+                return DateTime.FromFileTime(jdt.value);
+            }
+            public static implicit operator JsonDateTime(DateTime dt) {
+                JsonDateTime jdt = new JsonDateTime();
+                jdt.value = dt.ToFileTime();
+                return jdt;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs.meta b/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs.meta
new file mode 100644
index 0000000..e65102b
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Player Window/InkHistoryContentItem.cs.meta	
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9eeba5396eb679a4fbf1e275b4da1b2c
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs b/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs
new file mode 100644
index 0000000..cb7c46d
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs	
@@ -0,0 +1,2537 @@
+using UnityEngine;
+using UnityEditorInternal;
+using UnityEditor;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Linq;
+using Ink.Runtime;
+using Ink.UnityIntegration.Debugging;
+
+namespace Ink.UnityIntegration {
+
+	/// <summary>
+	/// Ink player window. Tests stories in an editor window.
+	/// Stories may be attached at runtime. InkPlayerWindow.DrawStoryPropertyField may be used for this.
+	/// </summary>
+	public class InkPlayerWindow : EditorWindow {
+        
+        
+        #region User Facing
+		
+        /// <summary>
+		/// Draws a property field for a story using GUILayout, allowing you to attach stories to the player window for debugging.
+		/// </summary>
+		/// <param name="story">Story.</param>
+		/// <param name="label">Label.</param>
+		public static void DrawStoryPropertyField (Story story, GUIContent label) {
+			DrawStoryPropertyField(story, InkPlayerParams.ForAttachedStories, label);
+		}
+		public static void DrawStoryPropertyField (Story story, InkPlayerParams playerParams, GUIContent label) {
+			EditorGUILayout.BeginHorizontal();
+			EditorGUILayout.PrefixLabel(label);
+			if(EditorApplication.isPlaying) {
+				if(story != null) {
+					if(InkPlayerWindow.isOpen) {
+						// InkPlayerWindow window = InkPlayerWindow.GetWindow(false);
+						if(InkPlayerWindow.attached && InkPlayerWindow.story == story) {
+							if(GUILayout.Button("Detach")) {
+								InkPlayerWindow.Detach();
+							}
+						} else {
+							if(GUILayout.Button("Attach")) {
+								InkPlayerWindow.Attach(story, playerParams);
+							}
+						}
+					} else {
+						if(GUILayout.Button("Open Player Window")) {
+							InkPlayerWindow.GetWindow();
+						}
+					}
+				} else {
+					EditorGUI.BeginDisabledGroup(true);
+					GUILayout.Button("Story cannot be null to attach to editor");
+					EditorGUI.EndDisabledGroup();
+				}
+			} else {
+				EditorGUI.BeginDisabledGroup(true);
+				GUILayout.Button("Enter play mode to attach to editor");
+				EditorGUI.EndDisabledGroup();
+			}
+			EditorGUILayout.EndHorizontal();
+
+			if(story != null) {
+				EditorGUI.indentLevel++;
+				EditorGUILayout.BeginHorizontal();
+				EditorGUILayout.LabelField("Can Continue", story.canContinue.ToString());
+				EditorGUI.BeginDisabledGroup(true);
+				if(GUILayout.Button("Continue")) {
+					story.Continue();
+				}
+				EditorGUI.EndDisabledGroup();
+				EditorGUILayout.EndHorizontal();
+
+				EditorGUILayout.LabelField("Current Text", story.currentText);
+				foreach(var choice in story.currentChoices) {
+					EditorGUILayout.BeginHorizontal();
+					EditorGUILayout.LabelField("Choice "+choice.index, choice.text);
+					EditorGUI.BeginDisabledGroup(true);
+					if(GUILayout.Button("Choose")) {
+						story.ChooseChoiceIndex(choice.index);
+					}
+					EditorGUI.EndDisabledGroup();
+					EditorGUILayout.EndHorizontal();
+				}
+				EditorGUI.indentLevel--;
+			}
+		}
+
+		
+
+		/// <summary>
+		/// Draws a property field for a story using GUI, allowing you to attach stories to the player window for debugging.
+		/// </summary>
+		/// <param name="position">Position.</param>
+		/// <param name="story">Story.</param>
+		/// <param name="label">Label.</param>
+		public static void DrawStoryPropertyField (Rect position, Story story, GUIContent label) {
+			position = EditorGUI.PrefixLabel(position, label);
+			InkPlayerWindow.GetWindow(false);
+			if(EditorApplication.isPlaying && story != null/* && story.state != null*/) {
+				if(InkPlayerWindow.attached && InkPlayerWindow.story == story) {
+					if(GUI.Button(position, "Detach")) {
+						InkPlayerWindow.Detach();
+					}
+				} else {
+					if(GUI.Button(position, "Attach")) {
+						InkPlayerWindow.Attach(story);
+					}
+				}
+			} else {
+				EditorGUI.BeginDisabledGroup(true);
+				GUI.Button(position, "Enter play mode to attach to editor");
+				EditorGUI.EndDisabledGroup();
+			}
+		}
+
+        
+
+
+
+		// Allows you to change what the ink player does/is allowed to do when a story is loaded.
+		// Especially handy for attached stories, where performance is critical or play controls might interfere with the game
+		// TODO: Show these params somewhere slightly out of the way (debug mode for the window?) so you can fiddle with them mid-game if you <i>really</i> need to
+		public struct InkPlayerParams {
+			public bool disablePlayControls;
+			public bool disableUndoHistory;
+			public bool disableChoices;
+			public bool disableStateLoading;
+			public bool disableSettingVariables;
+
+			public static InkPlayerParams Standard {
+				get {
+					return new InkPlayerParams();
+				}
+			} 
+			public static InkPlayerParams ForAttachedStories {
+				get {
+					var inkPlayerParams = new InkPlayerParams();
+					inkPlayerParams.disablePlayControls = true;
+					inkPlayerParams.disableUndoHistory = true;
+					inkPlayerParams.disableChoices = true;
+					inkPlayerParams.disableStateLoading = true;
+					inkPlayerParams.disableSettingVariables = true;
+					return inkPlayerParams;
+				}
+			} 
+		}
+		public static InkPlayerParams playerParams;
+
+		
+        // Allows telling the story to play automatically.
+		[System.Serializable]
+		public class PlayerOptions {
+			public bool continueAutomatically = true;
+			public bool chooseAutomatically = false;
+			public float continueAutomaticallyTimeInterval = 0.1f;
+			public float chooseAutomaticallyTimeInterval = 0.1f;
+		}
+		public static PlayerOptions playerOptions = new PlayerOptions();
+
+
+        // Allows injecting right click context options into the story content view.
+        public delegate void ContextMenuDelegate(GenericMenu contextMenu, InkHistoryContentItem content);
+        public static List<ContextMenuDelegate> contextMenuDelegates = new List<ContextMenuDelegate>();
+
+        #endregion
+
+
+
+
+
+
+
+
+
+
+		[System.Serializable]
+		public class InkPlayerWindowState {
+			static string settingsEditorPrefsKey = typeof(InkPlayerWindowState).Name +" Settings";
+			public static event Action OnCreateOrLoad;
+			static InkPlayerWindowState _Instance;
+			public static InkPlayerWindowState Instance {
+				get {
+					if(_Instance == null) LoadOrCreateAndSave();
+					return _Instance;
+				}
+			}
+
+			static InkPlayerWindowState LoadOrCreateAndSave () {
+				Load();
+				if(_Instance == null) CreateAndSave();
+				return _Instance;
+			}
+
+			public static void CreateAndSave () {
+				_Instance = new InkPlayerWindowState();
+				Save(_Instance);
+				if(OnCreateOrLoad != null) OnCreateOrLoad();
+			}
+			
+			public static void Save () {
+				Save(_Instance);
+			}
+
+			public static void Save (InkPlayerWindowState settings) {
+				string data = JsonUtility.ToJson(settings);
+				EditorPrefs.SetString(settingsEditorPrefsKey, data);
+			}
+
+			static void Load () {
+				if(!EditorPrefs.HasKey(settingsEditorPrefsKey)) return;
+				string data = EditorPrefs.GetString(settingsEditorPrefsKey);
+				try {
+					_Instance = JsonUtility.FromJson<InkPlayerWindowState>(data);
+					if(_Instance != null) if(OnCreateOrLoad != null) OnCreateOrLoad();
+				} catch {
+					Debug.LogError("Save Data was corrupt and could not be parsed. New data created. Old data was:\n"+data);
+					CreateAndSave();
+				}
+			}
+
+			public string lastStoryJSONAssetPath;
+			public bool lastStoryWasPlaying;
+			public TextAsset TryGetLastStoryJSONAsset () {
+				if(lastStoryJSONAssetPath == null) return null;
+				var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(lastStoryJSONAssetPath);
+				if(asset == null) {
+					lastStoryJSONAssetPath = null;
+					Save();
+				}
+				return asset;
+			}
+
+			public StoryPanelState storyPanelState = new StoryPanelState() {showing=true};
+			public BaseStoryPanelState choicePanelState = new BaseStoryPanelState() {showing=true};
+			public DivertPanelState divertPanelState = new DivertPanelState();
+			public NamedContentPanelState namedContentPanelState = new NamedContentPanelState();
+			public FunctionPanelState functionPanelState = new FunctionPanelState();
+			// public FunctionPanelState.FunctionParams functionParams = new FunctionPanelState.FunctionParams();
+			public VariablesPanelState variablesPanelState = new VariablesPanelState();
+			public ObservedVariablesPanelState observedVariablesPanelState = new ObservedVariablesPanelState();
+			public BaseStoryPanelState saveLoadPanelState = new BaseStoryPanelState();
+			public BaseStoryPanelState profilerPanelState = new BaseStoryPanelState();
+		}
+
+		
+		private const string windowTitle = "Ink Player";
+
+		public static bool isOpen {get; private set;}
+		public static bool attached {get; private set;}
+		static bool attachedWhileInPlayMode {get; set;}
+
+		static TextAsset _storyJSONTextAsset;
+		static TextAsset storyJSONTextAsset {
+			get {
+				return _storyJSONTextAsset;
+			} set {
+				if(_storyJSONTextAsset == value) return;
+				_storyJSONTextAsset = value;
+				if (_storyJSONTextAsset != null) {
+					InkPlayerWindowState.Instance.lastStoryJSONAssetPath = AssetDatabase.GetAssetPath(storyJSONTextAsset);
+					string fullJSONFilePath = InkEditorUtils.UnityRelativeToAbsolutePath(InkPlayerWindowState.Instance.lastStoryJSONAssetPath);
+					currentStoryJSONLastEditDateTime = File.GetLastWriteTime(fullJSONFilePath);
+				} else {
+					InkPlayerWindowState.Instance.lastStoryJSONAssetPath = null;
+				}
+				InkPlayerWindowState.Save();
+			}
+		}
+		static string storyJSON;
+		static DateTime currentStoryJSONLastEditDateTime;
+
+		private static Story _story;
+		public static Story story {
+			get {
+				return _story;
+			} private set {
+				if(_story != null) {
+					OnUnsetStory();
+				}
+				_story = value;
+				if(_story != null) {
+					OnSetStory();
+				}
+			}
+		}
+
+		private static TextAsset _storyStateTextAsset;
+		public static TextAsset storyStateTextAsset {
+			get {
+				return _storyStateTextAsset;
+			} set {
+				if(_storyStateTextAsset == value) 
+					return;
+				_storyStateTextAsset = value;
+				if(_storyStateTextAsset != null)
+					storyStateValid = InkEditorUtils.CheckStoryStateIsValid(storyJSONTextAsset.text, storyStateTextAsset.text);
+			}
+		}
+		
+		private static UndoHistory<InkPlayerHistoryItem> storyStateHistory = new UndoHistory<InkPlayerHistoryItem>();
+		private static List<InkHistoryContentItem> storyHistory = new List<InkHistoryContentItem>();
+
+		
+		private static Exception playStoryException;
+		private static bool storyStateValid = false;
+
+
+//		private WindowState windowState = new WindowState();
+//		public class WindowState {
+		static Vector2 _scrollPosition;
+		static Vector2 scrollPosition {
+			get {
+				return _scrollPosition;
+			} set {
+				if(_scrollPosition == value) return;
+				_scrollPosition = value;
+			}
+		}
+//		}
+
+		
+		
+		[System.Serializable]
+		public class BaseStoryPanelState {
+			public bool showing;
+			[SerializeField]
+			Vector2 _scrollPosition;
+			public Vector2 scrollPosition {
+				get {
+					return _scrollPosition;
+				} set {
+					if(_scrollPosition == value) return;
+					_scrollPosition = value;
+					// Deselect lines on scrolling because otherwise unity does weird things with the selection
+					// Disabled because it was deselecting the search field as you typed
+					// GUI.FocusControl(null);
+				}
+			}
+            
+			public float y;
+			public float height = 200;
+		}
+
+		[System.Serializable]
+		public class StoryPanelState : BaseStoryPanelState {
+			public DisplayOptions displayOptions = new DisplayOptions();
+			public string searchString = string.Empty;
+			
+			public const float minScrollRectHeight = 30;
+			public const float maxScrollRectHeight = 480;
+		}
+
+
+		[System.Serializable]
+		public class NamedContentPanelState : BaseStoryPanelState {
+			public string searchString = string.Empty;
+		}
+
+		[System.Serializable]
+		public class DivertPanelState : BaseStoryPanelState {
+			public string divertCommand = String.Empty;
+		}
+
+		static ReorderableList functionInputList;
+		[System.Serializable]
+		public class FunctionPanelState : BaseStoryPanelState {
+			[System.Serializable]
+			public class FunctionParams {
+				[System.Serializable]
+				public class FunctionInput {
+					public enum FunctionInputType {
+						Int,
+						String,
+						Bool,
+						InkVariable,
+						InkListVariable
+					}
+					public FunctionInputType type;
+					public int intValue;
+					public string stringValue;
+					public bool boolValue;
+					public string inkVariablePath;
+					public object inkVariableValue;
+					public InkList inkListVariableValue;
+					public string inkListVariablePath;
+
+					public void RefreshInkVariableValue (Story story) {
+						if(!string.IsNullOrWhiteSpace(inkVariablePath)) inkVariableValue = story.variablesState[inkVariablePath];
+						else inkVariableValue = null;
+					}
+					public void RefreshInkListVariableValue (Story story) {
+						inkListVariableValue = null;
+						try {
+							if(!string.IsNullOrWhiteSpace(inkListVariablePath)) 
+								inkListVariableValue = Ink.Runtime.InkList.FromString(inkListVariablePath, story);
+						} catch {}
+					}
+				}
+				public string functionName = String.Empty;
+				public List<FunctionInput> inputs = new List<FunctionInput>();
+			}
+			public FunctionParams functionParams = new FunctionParams();
+			public string testedFunctionName = null;
+			public object functionReturnValue = null;
+		}
+
+		[System.Serializable]
+		public class VariablesPanelState : BaseStoryPanelState {
+			public string searchString = string.Empty;
+			public List<string> expandedVariables = new List<string>();
+		}
+		public class ObservedVariablesPanelState : BaseStoryPanelState {
+			public List<string> observedVariableNames = new List<string>();
+			public Dictionary<string, ObservedVariable> observedVariables = new Dictionary<string, ObservedVariable>();
+		}
+
+		[System.Serializable]
+		public class DisplayOptions {
+			[Flags]
+			public enum VisibilityOptions {
+				Warnings = 1 << 0,
+				Errors = 1 << 1,
+				Content = 1 << 2,
+				PresentedChoices = 1 << 3,
+				SelectedChoice = 1 << 4,
+				Function = 1 << 5,
+				ChoosePathString = 1 << 6,
+				DebugNotes = 1 << 7,
+				TimeStamp = 1 << 8,
+				EmptyEntries = 1 << 9,
+				Tags = 1 << 10,
+			}
+			public VisibilityOptions visibilityOptions = VisibilityOptions.Warnings | VisibilityOptions.Errors | VisibilityOptions.Content;
+
+			public bool displayWarningsInConsole = true;
+			public bool displayErrorsInConsole = true;
+		}
+
+		static GUIStyle searchTextFieldStyle;
+		static GUIStyle searchCancelButtonStyle;
+		
+		public static DateTime dateTimeNow;
+
+
+		static float lastOnGUITime = -1f;
+		static float lastUpdateTime = -1f;
+
+		public enum AutoScrollMode {
+			NONE,
+			Snap,
+			Smooth
+		}
+		static AutoScrollMode markedForScrollToBottom;
+		
+		static AutoScrollMode markedForScrollToSelectedLine;
+		static InkHistoryContentItem selectedLine;
+
+        static bool mainScrollViewActive;
+		
+		static bool doingAutoscroll;
+		static float autoscrollTarget;
+		static float autoscrollVelocity;
+		static float autoscrollSmoothTime = 0.25f;
+
+		static float timeUntilNextAutomaticChoice = 0;
+		static float timeUntilNextAutomaticContinue = 0;
+
+
+		[MenuItem("Window/Ink Player %#i", false, 2300)]
+		public static InkPlayerWindow GetWindow () {
+			System.Type windowType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow");
+			return GetWindow<InkPlayerWindow>(windowTitle, true, windowType);
+		}
+
+		public static InkPlayerWindow GetWindow (bool focus) {
+			System.Type windowType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow");
+			return GetWindow<InkPlayerWindow>(windowTitle, focus, windowType);
+		}
+		
+        // TODO - find a way to restore tethered stories after recompile. This is tricky because we don't have a reference to the json, and stories aren't serialized.
+        // We should probably save the story to this path - but watch out for giant stories.
+        // var jsonStr = story.ToJson ();
+        // https://docs.microsoft.com/en-us/dotnet/api/system.io.path.gettemppath
+        // Directory.temporaryFolder
+		void OnEnable () {
+			if(isOpen) return;
+			isOpen = true;
+
+			InkPlayerWindowState.OnCreateOrLoad += () => {
+				// InkPlayerWindowState.Instance.functionPanelState.functionParams = InkPlayerWindowState.Instance.functionParams;
+				BuildFunctionInputList();
+			};
+			
+			BuildFunctionInputList();
+			
+			EditorApplication.update += Update;
+
+			if(story == null && !EditorApplication.isPlayingOrWillChangePlaymode) {
+				var lastLoadedStory = InkPlayerWindowState.Instance.TryGetLastStoryJSONAsset();
+				if(lastLoadedStory != null) {
+					if(InkPlayerWindowState.Instance.lastStoryWasPlaying) {
+						LoadAndPlay(lastLoadedStory);
+					} else {
+						TryPrepareInternal(lastLoadedStory);
+					}
+				}
+			}
+		}
+
+		void OnDisable () {
+			EditorApplication.update -= Update;
+		}
+
+		private void OnBecameVisible() {
+			if(doingAutoscroll) {
+				InkPlayerWindowState.Instance.storyPanelState.scrollPosition = new Vector2(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.x, autoscrollTarget);
+				doingAutoscroll = false;
+			}
+		}
+	
+		private void OnBecameInvisible() {}
+
+		void OnDestroy () {
+			isOpen = false;
+		}
+
+		private static void Update () {
+			var time = Time.realtimeSinceStartup;
+			var deltaTime = 0f;
+			if(lastUpdateTime != -1)
+				deltaTime = time - lastUpdateTime;
+			lastUpdateTime = time;
+			
+            if(story != null) {
+				timeUntilNextAutomaticChoice -= deltaTime;
+				if(story.currentChoices.Count > 0 && playerOptions.chooseAutomatically) {
+					if(timeUntilNextAutomaticChoice <= 0) {
+						MakeRandomChoice();
+						timeUntilNextAutomaticChoice = playerOptions.chooseAutomaticallyTimeInterval;
+					}
+				}
+				if(story.canContinue && playerOptions.continueAutomatically) {
+					timeUntilNextAutomaticContinue -= deltaTime;
+					if(timeUntilNextAutomaticContinue <= 0) {
+						if(playerOptions.continueAutomaticallyTimeInterval == 0) {
+							story.ContinueMaximally();
+						} else {
+							ContinueStory();
+						}
+						timeUntilNextAutomaticContinue = playerOptions.continueAutomaticallyTimeInterval;
+					}
+				}
+			}
+		}
+
+
+
+
+		
+
+		static void OnDidContinue () {
+            AddStoryContent(story.currentText.Trim(), story.currentTags);
+			if(story.currentChoices != null) {
+				foreach(var choice in story.currentChoices) {
+					AddToHistory(InkHistoryContentItem.CreateForPresentChoice(choice));
+				}
+			}
+			AddWarningsAndErrorsToHistory();
+		}
+		static void OnMakeChoice (Choice choice) {
+            AddToHistory(InkHistoryContentItem.CreateForMakeChoice(choice));		
+			AddWarningsAndErrorsToHistory();
+		}
+		static void OnEvaluateFunction (string functionName, object[] arguments) {
+            StringBuilder sb = new StringBuilder(functionName);
+			if(arguments != null && arguments.Length > 0) {
+				sb.Append(" with args: ");
+				for (int i = 0; i < arguments.Length; i++) {
+					if(arguments[i] == null) sb.Append("NULL");
+					else {
+						sb.Append(arguments[i]);
+						sb.Append(" (");
+						sb.Append(arguments[i].GetType().Name);
+						sb.Append(")");
+					}
+					if(i < arguments.Length-1) sb.Append(", ");
+				}
+			}
+			AddToHistory(InkHistoryContentItem.CreateForEvaluateFunction(sb.ToString().Trim()));		
+			AddWarningsAndErrorsToHistory();
+		}
+		static void OnCompleteEvaluateFunction (string functionName, object[] arguments, string textOutput, object result) {
+			StringBuilder sb = new StringBuilder(functionName);
+			if(arguments != null && arguments.Length > 0) {
+				sb.Append(" with args: ");
+				for (int i = 0; i < arguments.Length; i++) {
+					if(arguments[i] == null) sb.Append("NULL");
+					else {
+						sb.Append(arguments[i]);
+						sb.Append(" (");
+						sb.Append(arguments[i].GetType().Name);
+						sb.Append(")");
+					}
+					if(i < arguments.Length-1) sb.Append(", ");
+				}
+				bool hasTextOutput = textOutput != null && textOutput != string.Empty;
+				if(hasTextOutput) sb.Append(" text output is: "+textOutput);
+				if(result != null) sb.Append(" result is: "+result);
+				if(!hasTextOutput && result == null) sb.Append("has no output");
+			}
+			AddToHistory(InkHistoryContentItem.CreateForCompleteEvaluateFunction(sb.ToString().Trim()));		
+			AddWarningsAndErrorsToHistory();
+		}
+		static void OnChoosePathString (string pathString, object[] arguments) {
+            StringBuilder sb = new StringBuilder("ChoosePathString: ");
+			sb.Append(pathString);
+			if(arguments != null) {
+				sb.Append(" with args: ");
+				for (int i = 0; i < arguments.Length; i++) {
+					if(arguments[i] == null) sb.Append("NULL");
+					else {
+						sb.Append(arguments[i]);
+						sb.Append(" (");
+						sb.Append(arguments[i].GetType().Name);
+						sb.Append(")");
+					}
+					if(i < arguments.Length-1) sb.Append(", ");
+				}
+			}
+			AddToHistory(InkHistoryContentItem.CreateForChoosePathString(sb.ToString().Trim()));
+			AddWarningsAndErrorsToHistory();
+		}
+        static void OnLoadState () {
+            OnDidContinue();
+        }
+
+		static void AddWarningsAndErrorsToHistory () {
+			if(story.hasWarning) {
+				foreach(var warning in story.currentWarnings) {
+					AddToHistory(InkHistoryContentItem.CreateForWarning(warning.Trim()));
+					if(InkPlayerWindowState.Instance.storyPanelState.displayOptions.displayWarningsInConsole) {
+						Debug.LogWarning("Ink Warning: "+warning.Trim());
+					}
+				}
+			}
+			if(story.hasError) {
+				foreach(var error in story.currentErrors) {
+					AddToHistory(InkHistoryContentItem.CreateForError(error.Trim()));
+					if(InkPlayerWindowState.Instance.storyPanelState.displayOptions.displayErrorsInConsole) {
+						Debug.LogError("Ink Error: "+error.Trim());
+					}
+				}
+			}
+		}
+
+
+
+
+
+		// Loads an existing story to the player window. Handy for debugging stories running in games in editor.
+		public static void Attach (Story story) {
+			Attach(story, InkPlayerWindow.InkPlayerParams.ForAttachedStories);
+		}
+		public static void Attach (Story story, InkPlayerParams inkPlayerParams) {
+			Clear();
+			playerOptions.continueAutomatically = false;
+			playerOptions.chooseAutomatically = false;
+			playerParams = inkPlayerParams;
+			attached = true;
+			attachedWhileInPlayMode = EditorApplication.isPlaying;
+			InkPlayerWindow.story = story;
+            
+            // This allows reconstructing the story so it can be used after recompile. However, it can be expensive to run so there's a good argument for not running it on play!
+            // var lastTime = Time.realtimeSinceStartup;
+            // storyJSON = InkPlayerWindow.story.ToJson();
+            // File.WriteAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "AttachedStory.json"), storyJSON);
+            // Debug.Log("Wrote to "+System.IO.Path.Combine(System.IO.Path.GetTempPath(), "AttachedStory.json")+" in "+(Time.realtimeSinceStartup-lastTime));
+		}
+		
+		public static void Detach () {
+			GetWindow();
+			DetachInstance();
+		}
+
+		static void DetachInstance () {
+			attached = false;
+			story = null;
+		}
+
+		public static void LoadAndPlay (TextAsset storyJSONTextAsset) {
+			GetWindow();
+			if(InkPlayerWindow.story != null) {
+				if(EditorUtility.DisplayDialog("Story in progress", "The Ink Player Window is already playing a story. Would you like to stop it and load the new story?", "Stop and load", "Cancel")) {
+					InkPlayerWindow.Stop();
+					InkPlayerWindow.Play(storyJSONTextAsset);
+				}
+			} else {
+				InkPlayerWindow.Play(storyJSONTextAsset);
+			}
+		}
+
+		public static void LoadAndPlay (string storyJSON) {
+			GetWindow();
+			if(InkPlayerWindow.story != null) {
+				if(EditorUtility.DisplayDialog("Story in progress", "The Ink Player Window is already playing a story. Would you like to stop it and load the new story?", "Stop and load", "Cancel")) {
+					InkPlayerWindow.Stop();
+					InkPlayerWindow.Play(storyJSON);
+				}
+			} else {
+				InkPlayerWindow.Play(storyJSON);
+			}
+		}
+
+		static void Play (TextAsset storyJSONTextAsset) {
+			Play(storyJSONTextAsset, InkPlayerParams.Standard);
+		}
+		static void Play (TextAsset storyJSONTextAsset, InkPlayerParams inkPlayerParams) {
+			if(TryPrepareInternal(storyJSONTextAsset)) {
+				InkPlayerWindow.playerParams = inkPlayerParams;
+				PlayInternal();
+			}
+		}
+		static void Play (string storyJSON) {
+			Play(storyJSON, InkPlayerParams.Standard);
+		}
+		static void Play (string storyJSON, InkPlayerParams inkPlayerParams) {
+			if(TryPrepareInternal(storyJSON)) {
+				InkPlayerWindow.playerParams = inkPlayerParams;
+				PlayInternal();
+			}
+		}
+
+		static void PlayInternal () {
+			story = new Story(storyJSON);
+			story.allowExternalFunctionFallbacks = true;
+		}
+
+		// Loads the story, ready to be played
+		static bool TryPrepareInternal (TextAsset newStoryJSONTextAsset) {
+			// This forces a refresh
+			storyJSONTextAsset = null;
+			storyJSONTextAsset = newStoryJSONTextAsset;
+			if(storyJSONTextAsset == null || !InkEditorUtils.CheckStoryIsValid(storyJSONTextAsset.text, out playStoryException))
+				return false;
+			storyJSON = storyJSONTextAsset.text;
+			return true;
+		}
+		static bool TryPrepareInternal (string newStoryJSON) {
+			if(!InkEditorUtils.CheckStoryIsValid(storyJSON, out playStoryException))
+				return false;
+			InkPlayerWindow.storyJSONTextAsset = null;
+			InkPlayerWindow.storyJSON = newStoryJSON;
+			return true;
+		}
+
+		static void OnUnsetStory () {
+			_story.onDidContinue -= OnDidContinue;
+			_story.onMakeChoice -= OnMakeChoice;
+			_story.onEvaluateFunction -= OnEvaluateFunction;
+			_story.onCompleteEvaluateFunction -= OnCompleteEvaluateFunction;
+			_story.onChoosePathString -= OnChoosePathString;
+			_story.state.onDidLoadState -= OnLoadState;
+			foreach(var observedVariableName in InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames) {
+				UnobserveVariable(observedVariableName, false);
+			}
+			InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Clear();
+
+			InkPlayerWindowState.Instance.lastStoryWasPlaying = false;
+			InkPlayerWindowState.Save();
+		}
+
+		static void OnSetStory () {
+			_story.onDidContinue += OnDidContinue;
+			_story.onMakeChoice += OnMakeChoice;
+			_story.onEvaluateFunction += OnEvaluateFunction;
+			_story.onCompleteEvaluateFunction += OnCompleteEvaluateFunction;
+			_story.onChoosePathString += OnChoosePathString;
+			_story.state.onDidLoadState += OnLoadState;
+			
+			// Recalculate function ink variables
+			foreach(var input in InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs) {
+				if(input.type == FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkVariable) {
+					input.RefreshInkVariableValue(story);
+				} else if(input.type == FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkListVariable) {
+					input.RefreshInkListVariableValue(story);
+				}
+			}
+			
+			// Reobserve variables
+			var variablesToObserve = new List<string>(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames);
+			InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Clear();
+			InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Clear();
+			foreach(var observedVariableName in variablesToObserve) {
+				if(_story.variablesState.Contains(observedVariableName)) {
+					var observedVariable = ObserveVariable(observedVariableName, true);
+					observedVariable.AddValueState(_story.variablesState[observedVariableName]);
+				}
+			}
+
+			InkPlayerWindowState.Instance.lastStoryWasPlaying = true;
+			InkPlayerWindowState.Save();
+			
+			PingAutomator();
+		}
+
+		static void PingAutomator () {
+			if(playerParams.disablePlayControls) return;
+            if(story.canContinue && playerOptions.continueAutomatically) {
+				TryContinue();
+			} else if(story.currentChoices.Count > 0 && playerOptions.chooseAutomatically) { 
+				MakeRandomChoice();
+			}
+		}
+		
+		
+		static void Stop () {
+			Clear ();
+		}
+
+		static void Clear () {
+			if(storyStateHistory != null) storyStateHistory.Clear();
+			if(storyHistory != null) storyHistory.Clear();
+            RefreshVisibleHistory();
+			story = null;
+		}
+		
+		static void Restart () {
+			Stop();
+			if(storyJSONTextAsset != null)
+				Play(storyJSONTextAsset);
+			else
+				Play(storyJSON);
+		}
+		
+		static void ContinueStory () {
+			story.Continue();
+		}
+
+		static void AddStoryContent (string content, List<string> tags) {
+			AddToHistory(InkHistoryContentItem.CreateForContent(content, tags));
+			if(!playerParams.disableUndoHistory) AddToStateHistory();
+		}
+		
+		static void AddToHistory (InkHistoryContentItem content) {
+			storyHistory.Add(content);
+            RefreshVisibleHistory();
+			ScrollToBottom();
+		}
+
+		static void AddToStateHistory () {
+			InkPlayerHistoryItem historyItem = new InkPlayerHistoryItem(story.state.ToJson(), new List<InkHistoryContentItem>(storyHistory));
+			storyStateHistory.AddToUndoHistory(historyItem);
+		}
+		
+		static void Undo () {
+			InkPlayerHistoryItem item = storyStateHistory.Undo();
+			story.state.LoadJson(item.inkStateJSON);
+			storyHistory = new List<InkHistoryContentItem>(item.storyHistory);
+            RefreshVisibleHistory();
+            ScrollToBottom();
+		}
+		
+		static void Redo () {
+			InkPlayerHistoryItem item = storyStateHistory.Redo();
+			story.state.LoadJson(item.inkStateJSON);
+			storyHistory = new List<InkHistoryContentItem>(item.storyHistory);
+			RefreshVisibleHistory();
+            ScrollToBottom();
+		}
+
+		static void SaveStoryState (string storyStateJSON) {
+			AddToHistory(InkHistoryContentItem.CreateForDebugNote("Saved state"));
+
+			// Text asset can be null if we attached to an existing story rather than loading our own
+			string dirPath = string.Empty;
+			string storyName = "story";
+			if( storyJSONTextAsset != null ) {
+				dirPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(storyJSONTextAsset));
+				storyName = storyJSONTextAsset.name;
+			}
+
+			storyStateTextAsset = InkEditorUtils.CreateStoryStateTextFile(storyStateJSON, dirPath, storyName+"_SaveState");
+		}
+
+		static void LoadStoryState (string storyStateJSON) {
+			storyHistory.Clear();
+			storyStateHistory.Clear();
+			AddToHistory(InkHistoryContentItem.CreateForDebugNote("Loaded state"));
+			story.state.LoadJson(storyStateJSON);
+		}
+
+		static void ScrollToBottom (bool instant = false) {
+			markedForScrollToBottom = instant ? AutoScrollMode.Snap : AutoScrollMode.Smooth;
+		}
+
+		static void ScrollToSelectedLine (bool instant = false) {
+			markedForScrollToSelectedLine = instant ? AutoScrollMode.Snap : AutoScrollMode.Smooth;
+		}
+
+		static void TryContinue () {
+			if(!story.canContinue) 
+				return;
+			// if(playerOptions.continueAutomatically) {
+			// 	while (story.canContinue) {
+			// 		ContinueStory();
+			// 	}
+			// } else {
+				ContinueStory();
+			// }
+		}
+		
+		void OnGUI () {
+			HandleDragAndDrop();
+			if(searchTextFieldStyle == null) searchTextFieldStyle = GUI.skin.FindStyle("ToolbarSeachTextField");
+			if(searchCancelButtonStyle == null) searchCancelButtonStyle = GUI.skin.FindStyle("ToolbarSeachCancelButton");
+
+			dateTimeNow = System.DateTime.Now;
+			var time = Time.realtimeSinceStartup;
+			var deltaTime = 0f;
+			if(lastOnGUITime != -1)
+				deltaTime = time - lastOnGUITime;
+			lastOnGUITime = time;
+			
+			if(doingAutoscroll) {
+				var newY = Mathf.SmoothDamp(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y, autoscrollTarget, ref autoscrollVelocity, autoscrollSmoothTime, Mathf.Infinity, deltaTime);
+				InkPlayerWindowState.Instance.storyPanelState.scrollPosition = new Vector2(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.x, newY);
+				if(Mathf.Abs(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y - autoscrollTarget) < 0.1f) doingAutoscroll = false;
+			}
+
+			this.Repaint();
+			scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+
+			if(story == null && attached) DetachInstance();
+
+			DisplayHeader();
+
+			if(playStoryException == null) {
+				DisplayStoryControlGroup();
+			} else {
+				DisplayErrors();
+			}
+
+			if(story != null && story.state != null) {
+				DrawStoryHistory();
+				DrawChoices();
+				DrawProfilerData();
+				DrawSaveLoad();
+				DrawNamedContent();
+				// DrawDiverts();
+				DrawFunctions();
+				DrawVariables();
+				InkPlayerWindowState.Save();
+			} else {
+				// EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.6f));
+				GUILayout.Space(40);
+				EditorGUILayout.LabelField("Use this window to play and debug ink stories", EditorStyles.boldLabel);
+				GUILayout.Space(10);
+				EditorGUILayout.LabelField("You can tether your story as it runs in Play Mode to this window, allowing you to view and edit variables and divert in real time!", EditorStyles.wordWrappedLabel);
+				GUILayout.Space(20);
+				if(GUILayout.Button("View Documentation")) {
+					Application.OpenURL("https://github.com/inkle/ink-unity-integration/blob/master/Documentation/InkPlayerWindow.md");
+				}
+				// EditorGUILayout.EndVertical();
+			}
+
+            var lastRectMaxY = GUILayoutUtility.GetLastRect().yMax;
+            EditorGUILayout.EndScrollView();
+            if (Event.current.type == EventType.Repaint) {
+                var scrollRectMaxY = GUILayoutUtility.GetLastRect().yMax;
+                mainScrollViewActive = lastRectMaxY >= (scrollRectMaxY-3);
+            }
+		}
+		
+		void DisplayHeader () {
+			if(attached) {
+				EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+				var headerTitle = new System.Text.StringBuilder("Attached");
+				if(attachedWhileInPlayMode != EditorApplication.isPlaying) {
+					if(attachedWhileInPlayMode) headerTitle.Append(" (Ex-play-mode story)");
+				}
+				GUILayout.Label(new GUIContent(headerTitle.ToString(), "This story reference has been attached from elsewhere"));
+				if (GUILayout.Button(new GUIContent("Detach", "Detach from the loaded external story"), EditorStyles.toolbarButton)) {
+					Detach();
+				}
+				EditorGUILayout.EndHorizontal();
+			} else {
+				EditorGUILayout.BeginVertical();
+				EditorGUI.BeginChangeCheck();
+				storyJSONTextAsset = EditorGUILayout.ObjectField("Story JSON", storyJSONTextAsset, typeof(TextAsset), false) as TextAsset;
+				if(EditorGUI.EndChangeCheck()) {
+					if(storyJSONTextAsset == null) {
+						story = null;
+        
+						playStoryException = null;
+					} else {
+						Stop();
+						Play(storyJSONTextAsset);
+					}
+				}
+				if(storyJSONTextAsset != null && storyJSON != null) {
+					string fullJSONFilePath = InkEditorUtils.UnityRelativeToAbsolutePath(AssetDatabase.GetAssetPath(storyJSONTextAsset));
+					var updatedStoryJSONLastEditDateTime = File.GetLastWriteTime(fullJSONFilePath);
+					if (currentStoryJSONLastEditDateTime != updatedStoryJSONLastEditDateTime ) {
+						EditorGUILayout.BeginHorizontal();
+						EditorGUILayout.HelpBox ("Story JSON file has changed. Reload or restart to play updated story.", MessageType.Warning);
+						EditorGUILayout.BeginVertical();
+						if(GUILayout.Button("Reload")) {
+							var storyStateJSON = story.state.ToJson();
+							Play(storyJSONTextAsset, InkPlayerWindow.playerParams);
+							story.state.LoadJson(storyStateJSON);
+						}
+						if(GUILayout.Button("Restart")) {
+							Restart();
+						}
+						EditorGUILayout.EndVertical();
+						EditorGUILayout.EndHorizontal();
+					}
+				}
+				EditorGUILayout.EndVertical();
+			}
+		}
+		
+		void DisplayErrors () {
+			EditorGUILayout.HelpBox(playStoryException.Message, MessageType.Error);
+		}
+
+		void DisplayStoryControlGroup () {
+			EditorGUILayout.BeginHorizontal (EditorStyles.toolbar);
+
+			if(story == null) {
+				EditorGUI.BeginDisabledGroup(storyJSONTextAsset == null);
+				if(GUILayout.Button(new GUIContent("Start", "Run the story"), EditorStyles.toolbarButton)) {
+					Play(storyJSONTextAsset, InkPlayerParams.Standard);
+				}
+				EditorGUI.EndDisabledGroup();
+			} else {
+				EditorGUI.BeginDisabledGroup(playerParams.disablePlayControls);
+				if(GUILayout.Button(new GUIContent("Stop", "Stop the story"), EditorStyles.toolbarButton)) {
+					Stop();
+				}
+				if(GUILayout.Button(new GUIContent("Restart", "Restarts the story"), EditorStyles.toolbarButton)) {
+					Restart();
+				}
+				EditorGUI.EndDisabledGroup();
+			}
+
+			GUILayout.FlexibleSpace();
+
+			// Profiler button
+			if( story != null ) {
+				var profileButtonTitle = new GUIContent(
+					isProfiling ? "Stop Profiling" : "Profile", 
+					(isProfiling ? "Stop" : "Start") + " Profiling");
+				isProfiling = GUILayout.Toggle(isProfiling, profileButtonTitle, EditorStyles.toolbarButton);
+
+				GUILayout.FlexibleSpace();
+			}
+				
+			// Undo/Redo
+			if(story != null) {
+				EditorGUI.BeginDisabledGroup(playerParams.disableUndoHistory || !storyStateHistory.canUndo);
+				if(GUILayout.Button(new GUIContent("Undo", "Undo the last continue or choice"), EditorStyles.toolbarButton, GUILayout.Width(36))) {
+					Undo();
+				}
+				EditorGUI.EndDisabledGroup();
+				EditorGUI.BeginDisabledGroup(playerParams.disableUndoHistory || !storyStateHistory.canRedo);
+				if(GUILayout.Button(new GUIContent("Redo", "Redo the last continue or choice"), EditorStyles.toolbarButton, GUILayout.Width(36))) {
+					Redo();
+				}
+				EditorGUI.EndDisabledGroup();
+			}
+
+			GUILayout.FlexibleSpace();
+
+			EditorGUI.BeginDisabledGroup(playerParams.disablePlayControls);
+			EditorGUI.BeginChangeCheck();
+			var newContinueAutomatically = GUILayout.Toggle(playerOptions.continueAutomatically && !playerParams.disablePlayControls, new GUIContent("Auto-Continue", "Continues content automatically"), EditorStyles.toolbarButton);
+			if(EditorGUI.EndChangeCheck()) {
+				if(!playerParams.disablePlayControls) playerOptions.continueAutomatically = newContinueAutomatically;
+				PingAutomator();
+			}
+
+			EditorGUI.BeginChangeCheck();
+			var newChooseAutomatically = GUILayout.Toggle(playerOptions.chooseAutomatically && !playerParams.disablePlayControls, new GUIContent("Auto-Choice", "Makes choices automatically"), EditorStyles.toolbarButton);
+			if(EditorGUI.EndChangeCheck()) {
+				if(!playerParams.disablePlayControls) playerOptions.chooseAutomatically = newChooseAutomatically;
+			}
+			EditorGUI.EndDisabledGroup();
+			playerOptions.continueAutomaticallyTimeInterval = playerOptions.chooseAutomaticallyTimeInterval = GUILayout.HorizontalSlider(playerOptions.continueAutomaticallyTimeInterval, 1f, 0f, GUILayout.Width(80));
+			GUILayout.EndHorizontal();
+		}
+			
+
+
+
+		
+		#region Story
+		void DrawStoryHistory () {
+			DisplayStoryHeader();
+			if(InkPlayerWindowState.Instance.storyPanelState.showing)
+				DisplayStoryBody ();
+		}
+
+		void DisplayStoryHeader () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			
+			InkPlayerWindowState.Instance.storyPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.storyPanelState.showing, "Content", true);
+			
+			if(GUILayout.Button(new GUIContent("Clear", "Clears the output"), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) {
+				storyHistory.Clear();
+				ScrollToBottom();
+			}
+			GUILayout.Space(6);
+			if(GUILayout.Button(new GUIContent("Copy", "Copy the output to clipboard"), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) {
+				CopyStoryHistoryToClipboard();
+			}
+			
+			EditorGUI.BeginChangeCheck();
+            DrawVisibilityOptions();
+
+            void DrawVisibilityOptions () {
+                Enum newVisibilityOptions = EditorGUILayout.EnumFlagsField(GUIContent.none, InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions, EditorStyles.toolbarDropDown, GUILayout.Width(80));
+                InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions = (DisplayOptions.VisibilityOptions)(int)Convert.ChangeType(newVisibilityOptions, typeof(DisplayOptions.VisibilityOptions));
+
+                // TODO: tooltips for options. I'd REALLY like for it not to show "Mixed ..." in the box mais c'est la vie
+                // TODO: Add a "default" option in the dropdown
+                // See:
+                // https://github.com/Unity-Technologies/UnityCsReference/blob/master/Editor/Mono/EditorGUI.cs#L3561
+                // But a lot of the code is internal.
+
+                // var enumValue = InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions;
+                // var style = EditorStyles.toolbarDropDown;
+                // var position = EditorGUILayout.GetControlRect(true, 18, style, GUILayout.Width(80));
+                
+                // var enumType = typeof(DisplayOptions.VisibilityOptions);
+                // var includeObsolete = false;
+                // var label = GUIContent.none;
+                // var displayNames = Enum.GetNames(typeof(DisplayOptions.VisibilityOptions));
+                // // var flagValues = new int[];
+
+                // var id = GUIUtility.GetControlID(0, FocusType.Keyboard, position);
+                // position = EditorGUI.PrefixLabel(position, id, label);
+
+                // InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions = (DisplayOptions.VisibilityOptions)EditorGUI.MaskField(position, (int)enumValue, displayNames, style);
+            }
+			if(EditorGUI.EndChangeCheck()) {
+                RefreshVisibleHistory();
+				ScrollToBottom();
+			}
+
+			bool changed = DrawSearchBar(ref InkPlayerWindowState.Instance.storyPanelState.searchString);
+			if(changed) {
+			    RefreshVisibleHistory();
+				if(selectedLine != null) ScrollToSelectedLine(true);
+				else ScrollToBottom();
+			}
+
+			EditorGUILayout.EndHorizontal();
+		}
+
+		void CopyStoryHistoryToClipboard () {
+			StringBuilder sb = new StringBuilder("Story Log\n");
+			foreach(InkHistoryContentItem content in storyHistory) {
+				sb.AppendLine();
+				sb.Append(content.time.ToShortDateString());
+				sb.Append(" ");
+				sb.Append(content.time.ToLongTimeString());
+				sb.Append(" (");
+				sb.Append(content.contentType.ToString());
+				sb.Append(") ");
+				sb.Append(content.content);
+			}
+			GUIUtility.systemCopyBuffer = sb.ToString();
+		}
+
+		static bool ShouldShowContentWithSearchString (string contentString, string searchString) {
+			if(StringContains(contentString, InkPlayerWindowState.Instance.storyPanelState.searchString, StringComparison.OrdinalIgnoreCase)) return true;
+			return false;
+		}
+
+		static bool ShouldShowContent (InkHistoryContentItem content, DisplayOptions.VisibilityOptions visibilityOpts) {
+			switch(content.contentType) {
+				case InkHistoryContentItem.ContentType.PresentedContent: {
+					if((visibilityOpts & DisplayOptions.VisibilityOptions.Content) != 0) {
+						if(content.content.Length == 0 && (visibilityOpts & DisplayOptions.VisibilityOptions.EmptyEntries) == 0) return false;
+						else return true;
+					} else return false;
+				}
+				case InkHistoryContentItem.ContentType.ChooseChoice:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.SelectedChoice) != 0;
+				case InkHistoryContentItem.ContentType.PresentedChoice:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.PresentedChoices) != 0;
+				case InkHistoryContentItem.ContentType.DebugNote:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.DebugNotes) != 0;
+				case InkHistoryContentItem.ContentType.Warning:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.Warnings) != 0;
+				case InkHistoryContentItem.ContentType.Error:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.Errors) != 0;
+				case InkHistoryContentItem.ContentType.ChoosePathString:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.ChoosePathString) != 0;
+				case InkHistoryContentItem.ContentType.EvaluateFunction:
+					return (visibilityOpts & DisplayOptions.VisibilityOptions.Function) != 0;
+				default: break;
+			}
+			return false;
+		}
+		
+		static List<InkHistoryContentItem> validHistory = new List<InkHistoryContentItem>();
+		static void RefreshVisibleHistory () {
+			validHistory.Clear();
+			bool doingSearch = !string.IsNullOrWhiteSpace(InkPlayerWindowState.Instance.storyPanelState.searchString);
+			var visibilityOpts = InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions;
+			var count = storyHistory.Count;
+			for(int i = 0; i < count; i++) {
+				var content = storyHistory[i];
+				if(doingSearch && !ShouldShowContentWithSearchString(content.content, InkPlayerWindowState.Instance.storyPanelState.searchString)) continue;
+				if(!ShouldShowContent(content, visibilityOpts)) continue;
+				validHistory.Add(content);
+			}
+		}
+		void DisplayStoryBody () {	
+			float contentMarginXY = 4;
+			float contentSpacing = 8;
+			
+			var timestampWidth = 58;
+			var contentTypeWidth = 26;
+			var tagsWidth = 160;
+
+			var visibilityOptions = InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions;
+			bool showTimestamp = (visibilityOptions & DisplayOptions.VisibilityOptions.TimeStamp) != 0; 
+			bool showTags = (visibilityOptions & DisplayOptions.VisibilityOptions.Tags) != 0; 
+
+			var lastRect = GUILayoutUtility.GetLastRect();
+			var containerWidth = position.width - GUI.skin.verticalScrollbar.fixedWidth;
+            if(mainScrollViewActive) containerWidth -= GUI.skin.verticalScrollbar.fixedWidth;
+			
+			var lineWidth = containerWidth - contentMarginXY * 2;
+			
+			var contentWidth = lineWidth;
+			if(showTimestamp) {
+				contentWidth -= timestampWidth;
+				contentWidth -= contentSpacing;
+			}
+			contentWidth -= contentTypeWidth;
+			contentWidth -= contentSpacing;
+			if(showTags) {
+				contentWidth -= tagsWidth;
+				contentWidth -= contentSpacing;
+			}
+			float totalHeight = 0;
+			float[] heights = new float[storyHistory.Count];
+			int selectedLineIndex = -1;
+			float selectedLineY = -1;
+
+			for(int i = 0; i < validHistory.Count; i++) {
+				var content = validHistory[i];
+				heights[i] = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(content.content), contentWidth);
+				if(showTags) {
+					var tagsHeight = EditorStyles.wordWrappedLabel.CalcHeight(new GUIContent(GetTagsString(content.tags)), tagsWidth);
+					heights[i] = Mathf.Max(heights[i], tagsHeight);
+				}
+				heights[i] += contentMarginXY * 2;
+				if(content == selectedLine) {
+					selectedLineIndex = i;
+					selectedLineY = totalHeight;
+				}
+				totalHeight += heights[i];
+			}
+
+            void OnRefreshSelectedLine () {
+                float _totalHeight = 0;
+                for(int i = 0; i < validHistory.Count; i++) {
+                    var content = validHistory[i];
+                    if(content == selectedLine) {
+                        selectedLineIndex = i;
+                        selectedLineY = _totalHeight;
+                    }
+                    _totalHeight += heights[i];
+                }
+            }
+
+			if(Event.current.type == EventType.Repaint) {
+				InkPlayerWindowState.Instance.storyPanelState.y = lastRect.yMax;
+			}
+			var viewportRect = new Rect(0, lastRect.yMax, position.width, InkPlayerWindowState.Instance.storyPanelState.height);
+            if(mainScrollViewActive) viewportRect.width -= GUI.skin.verticalScrollbar.fixedWidth;
+			var containerRect = new Rect(0,0,containerWidth, totalHeight);
+			
+			var newScrollPos = GUI.BeginScrollView(viewportRect, InkPlayerWindowState.Instance.storyPanelState.scrollPosition, containerRect, false, true);
+			if(newScrollPos != InkPlayerWindowState.Instance.storyPanelState.scrollPosition) {
+				doingAutoscroll = false;
+				InkPlayerWindowState.Instance.storyPanelState.scrollPosition = newScrollPos;
+			}
+
+			var y = 0f;
+			var panelTop = InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y;
+			var panelBottom = InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y + viewportRect.height;
+			// int numShown = 0;
+			// var log = "";
+
+			// This appears to be necessary, else the selected text moves around when scrolling!
+			if(doingAutoscroll) {
+				GUI.FocusControl(null);
+			}
+
+			for(int i = 0; i < validHistory.Count; i++) {
+				var endY = y + heights[i];
+				if(panelTop <= endY && panelBottom >= y) {
+					// if(numShown == 0) {
+					//     log += "Total space "+totalHeight+" Scroll "+InkPlayerWindowState.Instance.storyPanelState.storyScrollPosition.y+" Space "+y+", showing: ";
+					// }
+					var content = validHistory[i];
+					var lineContainerRect = new Rect(0, y, containerWidth, heights[i]);
+					var lineRect = new Rect(lineContainerRect.x + contentMarginXY, lineContainerRect.y + contentMarginXY, lineContainerRect.width - contentMarginXY * 2, lineContainerRect.height - contentMarginXY * 2);
+					
+					GUIStyle lineStyle = null;
+					if(selectedLine == content) lineStyle = historyItemBGStyleSelected.guiStyle;
+					else lineStyle = i % 2 == 0 ? historyItemBGStyleDark.guiStyle : historyItemBGStyleLight.guiStyle;
+					
+					GUI.Box(lineContainerRect, GUIContent.none, lineStyle);
+					if(Event.current.type == EventType.MouseDown && lineContainerRect.Contains(Event.current.mousePosition)) {
+						if(Event.current.button == 0) {
+							selectedLine = content;
+                            OnRefreshSelectedLine();
+                            // To avoid disruption, only scroll when the line is close to the edge of the panel
+                            var targetY = GetTargetScrollPositionToCenterStoryLine(i, false);
+                            Debug.Log(viewportRect+" "+Mathf.Abs(targetY-InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y));
+                            if(Mathf.Abs(targetY-InkPlayerWindowState.Instance.storyPanelState.scrollPosition.y) > viewportRect.height * 0.225f) {
+                                ScrollToSelectedLine();
+                            }
+                            Event.current.Use();
+						}
+					}
+					string textToDisplay = String.Empty;
+					
+					var x = lineRect.x;
+					if(showTimestamp) {
+						var timeRect = new Rect(x, lineRect.y, timestampWidth, lineRect.height);
+						DisplayLineTime(timeRect, content);
+						x += timestampWidth;
+						x += contentSpacing;
+					}
+					var iconRect = new Rect(x, lineRect.y, contentTypeWidth, lineRect.height);
+					DisplayLineIcon(iconRect, content);
+					x += contentTypeWidth;
+					x += contentSpacing;
+					
+					var contentRect = new Rect(x, lineRect.y, contentWidth, lineRect.height);
+					DisplayLine(contentRect, content);
+					x += contentWidth;
+					x += contentSpacing;
+					if(showTags) {
+						var tagsRect = new Rect(x, lineRect.y, tagsWidth, lineRect.height);
+						DisplayTags(tagsRect, content);
+						x += tagsWidth;
+						x += contentSpacing;
+					}
+
+					if(Event.current.type == EventType.MouseDown && lineContainerRect.Contains(Event.current.mousePosition)) {
+						if(Event.current.button == 1) {
+							if(GUI.GetNameOfFocusedControl() != content.GetHashCode().ToString()) {
+								GUI.FocusControl(null);
+								var contextMenu = new GenericMenu();
+								contextMenu.AddItem(new GUIContent("Copy"), false, () => {
+									GUIUtility.systemCopyBuffer = content.content;
+								});
+                                foreach(var contextMenuDelegate in contextMenuDelegates) {
+                                    contextMenuDelegate(contextMenu, content);
+                                }
+								contextMenu.ShowAsContext();
+								Event.current.Use();
+							}
+						}
+					}
+
+					// log += i+", ";
+				}
+				y = endY;
+			}
+
+			var lineX = contentMarginXY;
+			if(showTimestamp) {
+				lineX += timestampWidth;
+				lineX += contentSpacing * 0.5f;
+				GUI.Box(new Rect(lineX, 0, 1, containerRect.height), "", dividerLineStyle.guiStyle);
+				lineX += contentSpacing * 0.5f;
+			}
+
+			lineX += contentTypeWidth;
+			lineX += contentSpacing * 0.5f;
+			GUI.Box(new Rect(lineX, 0, 1, containerRect.height), "", dividerLineStyle.guiStyle);
+			lineX += contentSpacing * 0.5f;
+			
+			if(showTags) {
+				lineX += contentWidth;
+				lineX += contentSpacing * 0.5f;
+				GUI.Box(new Rect(lineX, 0, 1, containerRect.height), "", dividerLineStyle.guiStyle);
+				lineX += contentSpacing * 0.5f;
+			}
+
+			GUI.EndScrollView();
+			GUILayout.Space(viewportRect.height);
+
+			if(Event.current.type == EventType.Layout) {
+				if(markedForScrollToBottom != AutoScrollMode.NONE) {
+					var targetPosition = totalHeight - viewportRect.height;
+					if(markedForScrollToBottom == AutoScrollMode.Smooth) {
+						doingAutoscroll = true;
+						autoscrollTarget = targetPosition;
+					} else if(markedForScrollToBottom == AutoScrollMode.Snap) {
+						doingAutoscroll = false;
+						InkPlayerWindowState.Instance.storyPanelState.scrollPosition = new Vector2(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.x, targetPosition);
+					}
+					autoscrollVelocity = 0;
+					markedForScrollToBottom = AutoScrollMode.NONE;
+				}
+				if(markedForScrollToSelectedLine != AutoScrollMode.NONE && selectedLineIndex != -1) {
+					var targetPosition = GetTargetScrollPositionToCenterStoryLine(selectedLineIndex);
+					if(markedForScrollToSelectedLine == AutoScrollMode.Smooth) {
+						doingAutoscroll = true;
+						autoscrollTarget = targetPosition;
+					} else if(markedForScrollToSelectedLine == AutoScrollMode.Snap) {
+						doingAutoscroll = false;
+						InkPlayerWindowState.Instance.storyPanelState.scrollPosition = new Vector2(InkPlayerWindowState.Instance.storyPanelState.scrollPosition.x, targetPosition);
+					}
+					autoscrollVelocity = 0;
+					markedForScrollToSelectedLine = AutoScrollMode.NONE;
+				}
+			}
+
+			float GetTargetScrollPositionToCenterStoryLine (int lineIndex, bool clamped = true) {
+				var targetY = (selectedLineY + heights[lineIndex] * 0.5f) - viewportRect.height * 0.5f;
+				if(clamped) targetY = Mathf.Clamp(targetY, 0, totalHeight - viewportRect.height);
+				return targetY;
+			}
+		}
+
+		static ColoredBackgroundGUIStyle _historyItemBGStyleDark;
+		static ColoredBackgroundGUIStyle historyItemBGStyleDark {
+			get {
+				if(_historyItemBGStyleDark == null) _historyItemBGStyleDark = new ColoredBackgroundGUIStyle(new Color(0.8470589f,0.8470589f,0.8470589f,1), new Color(0.21f,0.21f,0.21f,1f), new Color(0.92f,0.92f,0.92f,1), new Color(0.3f,0.3f,0.3f,1f));
+				return _historyItemBGStyleDark;
+			}
+		}
+
+		static ColoredBackgroundGUIStyle _historyItemBGStyleLight;
+		static ColoredBackgroundGUIStyle historyItemBGStyleLight {
+			get {
+				if(_historyItemBGStyleLight == null) _historyItemBGStyleLight = new ColoredBackgroundGUIStyle(new Color(0.8745099f,0.8745099f,0.8745099f,1f), new Color(0.23f,0.23f,0.23f,1f), new Color(0.92f,0.92f,0.92f,1), new Color(0.3f,0.3f,0.3f,1f));
+				return _historyItemBGStyleLight;
+			}
+		}
+
+		static ColoredBackgroundGUIStyle _historyItemBGStyleSelected;
+		static ColoredBackgroundGUIStyle historyItemBGStyleSelected {
+			get {
+				if(_historyItemBGStyleSelected == null) _historyItemBGStyleSelected = new ColoredBackgroundGUIStyle(new Color(0.3920879f,0.6161963f,0.9339623f,1f), new Color(0.243137255f,0.37254902f,0.588235294f,1f));
+				return _historyItemBGStyleSelected;
+			}
+		}
+
+		static ColoredBackgroundGUIStyle _dividerLineStyle;
+		static ColoredBackgroundGUIStyle dividerLineStyle {
+			get {
+				if(_dividerLineStyle == null) _dividerLineStyle = new ColoredBackgroundGUIStyle(new Color(0.85f,0.85f,0.85f,1f), new Color(0.25f,0.25f,0.25f,1f));
+				return _dividerLineStyle;
+			}
+		}
+
+		void DisplayLineTime (Rect rect, InkHistoryContentItem content) {
+			EditorGUI.LabelField(rect, new GUIContent(content.time.ToLongTimeString()));
+		}
+
+		void DisplayLineIcon (Rect rect, InkHistoryContentItem content) {
+			var visibilityOptions = InkPlayerWindowState.Instance.storyPanelState.displayOptions.visibilityOptions;
+			if(content.contentType == InkHistoryContentItem.ContentType.ChooseChoice && (visibilityOptions & DisplayOptions.VisibilityOptions.SelectedChoice) != 0) {
+				var icon = new GUIContent("*", "Selected Choice");
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.PresentedChoice && (visibilityOptions & DisplayOptions.VisibilityOptions.PresentedChoices) != 0) {
+				var icon = new GUIContent("*?", "Presented Choice");
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.EvaluateFunction && (visibilityOptions & DisplayOptions.VisibilityOptions.Function) != 0) {
+				var icon = new GUIContent("f(x)", "Took Function");
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.ChoosePathString && (visibilityOptions & DisplayOptions.VisibilityOptions.ChoosePathString) != 0) {
+				var icon = new GUIContent("->", "Took Path");
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.DebugNote && (visibilityOptions & DisplayOptions.VisibilityOptions.DebugNotes) != 0) {
+				var icon = new GUIContent("//", "Debug Note");
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.Warning && (visibilityOptions & DisplayOptions.VisibilityOptions.Warnings) != 0) {
+				var icon = EditorGUIUtility.IconContent("console.warnicon.sml");
+				icon.tooltip = "Warning";
+				EditorGUI.LabelField(rect, icon);
+			} else if(content.contentType == InkHistoryContentItem.ContentType.Error && (visibilityOptions & DisplayOptions.VisibilityOptions.Errors) != 0) {
+				var icon = EditorGUIUtility.IconContent("console.erroricon.sml");
+				icon.tooltip = "Error";
+				EditorGUI.LabelField(rect, icon);
+			} else {
+				// var icon = EditorGUIUtility.IconContent("console.infoicon.sml");
+				// EditorGUI.LabelField(rect, icon);
+			}
+		}
+
+		void DisplayLine (Rect rect, InkHistoryContentItem content) {
+			float timeSinceLastWrite = (float)(dateTimeNow - content.time).TotalSeconds;
+			var revealTime = 0.8f;
+			var l = Mathf.InverseLerp(revealTime, 0, timeSinceLastWrite);
+			var newColor = new Color(GUI.color.r, GUI.color.g, GUI.color.b, 0);
+			var color = Color.Lerp(GUI.color, newColor, l);
+			var oldGUIColor = GUI.color; 
+			GUI.color = color;
+			GUI.SetNextControlName(content.GetHashCode().ToString());
+			EditorGUI.SelectableLabel(rect, content.content, EditorStyles.wordWrappedLabel);
+			GUI.color = oldGUIColor;
+		}
+
+		void DisplayTags (Rect rect, InkHistoryContentItem content) {
+			float timeSinceLastWrite = (float)(dateTimeNow - content.time).TotalSeconds;
+			var revealTime = 0.8f;
+			var l = Mathf.InverseLerp(revealTime, 0, timeSinceLastWrite);
+			var newColor = new Color(GUI.color.r, GUI.color.g, GUI.color.b, 0);
+			var color = Color.Lerp(GUI.color, newColor, l);
+			var oldGUIColor = GUI.color; 
+			GUI.color = color;
+			GUI.SetNextControlName(content.GetHashCode().ToString());
+			EditorGUI.SelectableLabel(rect, GetTagsString(content.tags), EditorStyles.wordWrappedLabel);
+			GUI.color = oldGUIColor;
+		}
+
+		string GetTagsString (List<string> tags) {
+			return (tags == null || tags.Count == 0) ? string.Empty : string.Join("\n", tags);
+		}
+		#endregion
+
+
+
+
+
+
+		#region Choices
+		void DrawChoices () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.choicePanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.choicePanelState.showing, "Choices", true);
+			EditorGUILayout.EndHorizontal();
+			if(InkPlayerWindowState.Instance.choicePanelState.showing)
+				DisplayChoices ();
+		}
+
+		void DisplayChoices () {
+			EditorGUI.BeginDisabledGroup(playerParams.disableChoices);
+			GUILayout.BeginVertical();
+			if(story.canContinue) {
+				EditorGUI.BeginDisabledGroup(playerOptions.continueAutomatically);
+				if(GUILayout.Button(new GUIContent("Continue", "Continues once"))) {
+					ContinueStory();
+				}
+				if(GUILayout.Button(new GUIContent("Continue Maximally", "Continues until the next choice"))) {
+					while(story.canContinue) {
+						ContinueStory();
+					}
+				}
+				EditorGUI.EndDisabledGroup();
+			} else if(story.currentChoices.Count > 0) {
+				EditorGUI.BeginDisabledGroup(playerOptions.chooseAutomatically);
+				foreach(Choice choice in story.currentChoices) {
+					GUILayout.BeginHorizontal();
+					if(GUILayout.Button(new GUIContent(choice.text.Trim(), "Index: "+choice.index.ToString()+"\nSourcePath: "+choice.sourcePath.Trim()))) {
+						MakeChoice(choice);
+					}
+					GUILayout.EndHorizontal();
+				}
+				EditorGUI.EndDisabledGroup();
+			} else {
+				GUILayout.Label("Reached end of story");
+			}
+
+			GUILayout.EndVertical();
+			EditorGUI.EndDisabledGroup();
+		}
+
+		static void MakeRandomChoice () {
+			MakeChoice(story.currentChoices[UnityEngine.Random.Range(0, story.currentChoices.Count)]);
+		}
+
+		static void MakeChoice (Choice choice) {
+			story.ChooseChoiceIndex(choice.index);
+			if(!playerParams.disableUndoHistory) AddToStateHistory();
+			TryContinue();
+		}
+		#endregion
+		
+
+
+
+
+		#region SaveLoad
+		static void DrawSaveLoad () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.saveLoadPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.saveLoadPanelState.showing, "Story State", true);
+			EditorGUILayout.EndHorizontal();
+			if(InkPlayerWindowState.Instance.saveLoadPanelState.showing)
+				DrawSaveLoadPanel ();
+		}
+
+		static void DrawSaveLoadPanel () {
+			GUILayout.BeginVertical();
+
+			EditorGUILayout.BeginHorizontal();
+			string currentStateJSON = story.state.ToJson();
+			if(currentStateJSON.Length < 20000) {
+				EditorGUILayout.TextField("Current State JSON", currentStateJSON);
+			} else {
+				EditorGUILayout.TextField("Current State JSON", "Too long to display!");
+			}
+			EditorGUI.BeginDisabledGroup(GUIUtility.systemCopyBuffer == currentStateJSON);
+			if (GUILayout.Button("Copy To Clipboard")) {
+				GUIUtility.systemCopyBuffer = currentStateJSON;
+			}
+			EditorGUI.EndDisabledGroup();
+			if (GUILayout.Button("Save As...")) {
+				SaveStoryState(currentStateJSON);
+			}
+			EditorGUILayout.EndHorizontal();
+
+			EditorGUI.BeginDisabledGroup(playerParams.disableStateLoading);
+			EditorGUILayout.BeginHorizontal();
+			EditorGUI.BeginChangeCheck();
+			storyStateTextAsset = EditorGUILayout.ObjectField("Load Story State JSON File", storyStateTextAsset, typeof(TextAsset), false) as TextAsset;
+			EditorGUI.BeginDisabledGroup(storyStateTextAsset == null);
+			if (GUILayout.Button("Load")) {
+				LoadStoryState(storyStateTextAsset.text);
+			}
+			EditorGUI.EndDisabledGroup();
+			EditorGUILayout.EndHorizontal();
+			if(storyStateTextAsset != null && !storyStateValid) {
+				EditorGUILayout.HelpBox("Loaded story state file is not valid.", MessageType.Error);
+			}
+			GUILayout.EndVertical();
+			EditorGUI.EndDisabledGroup();
+		}
+		#endregion
+
+
+
+
+		
+
+		#region Diverts
+		void DrawNamedContent () {
+			DrawNamedContentHeader();
+			if(InkPlayerWindowState.Instance.namedContentPanelState.showing)
+				DrawNamedContentPanel ();
+		}
+
+		void DrawNamedContentHeader () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.namedContentPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.namedContentPanelState.showing, "Named Content", true);
+			
+			EditorGUI.BeginDisabledGroup(!InkPlayerWindowState.Instance.namedContentPanelState.showing);
+			bool changed = DrawSearchBar(ref InkPlayerWindowState.Instance.namedContentPanelState.searchString);
+			if(changed) InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition = Vector2.zero;
+			EditorGUI.EndDisabledGroup();
+
+			EditorGUILayout.EndHorizontal();
+		}
+
+        void DrawNamedContentPanel () {	
+			float contentMarginX = 4;
+			float contentMarginY = 0;
+			float indentChangeVerticalSpacing = 6;
+			
+			var lastRect = GUILayoutUtility.GetLastRect();
+			var containerWidth = position.width - GUI.skin.verticalScrollbar.fixedWidth;
+            if(mainScrollViewActive) containerWidth -= GUI.skin.verticalScrollbar.fixedWidth;
+			
+			float totalHeight = 0;
+
+			List<Rect> rects = new List<Rect>();
+			List<string> paths = new List<string>();
+            {
+                AddContainer(string.Empty, story.mainContentContainer);
+                void AddContainer (string currentPath, Container container, int indent = 0) {
+                    if(container == null || container.namedOnlyContent == null) return;
+                    
+                    var lastTotalHeight = totalHeight;
+                    indent++;
+                    foreach(var contentKVP in container.namedOnlyContent) {
+                        var newPath = currentPath.Length == 0 ? contentKVP.Key : currentPath+"."+contentKVP.Key;
+                        AddContent(newPath, contentKVP, indent);
+                    }
+                    indent--;
+                    if(lastTotalHeight != totalHeight) totalHeight += indentChangeVerticalSpacing;
+                }
+                void AddContent (string currentPath, KeyValuePair<string, Runtime.Object> contentKVP, int indent = 0) {
+                    if(SearchStringMatch(currentPath, InkPlayerWindowState.Instance.namedContentPanelState.searchString)) {
+                        var itemHeight = EditorGUIUtility.singleLineHeight;
+                        itemHeight += contentMarginY * 2;
+
+                        rects.Add(new Rect(indent*8, totalHeight, containerWidth-indent*8, itemHeight));
+                        totalHeight += itemHeight;
+                        paths.Add(currentPath);
+                    }
+                    
+                    var namedContainer = contentKVP.Value as Container;
+                    AddContainer(currentPath, namedContainer, indent);
+                }
+            }
+            totalHeight -= indentChangeVerticalSpacing;
+
+			if(Event.current.type == EventType.Repaint) {
+				InkPlayerWindowState.Instance.namedContentPanelState.y = lastRect.yMax;
+			}
+			var viewportRect = new Rect(0, lastRect.yMax, position.width, InkPlayerWindowState.Instance.namedContentPanelState.height);
+            if(mainScrollViewActive) viewportRect.width -= GUI.skin.verticalScrollbar.fixedWidth;
+			var containerRect = new Rect(0,0,containerWidth, totalHeight);
+			
+			var newScrollPos = GUI.BeginScrollView(viewportRect, InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition, containerRect, false, true);
+			if(newScrollPos != InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition) {
+				doingAutoscroll = false;
+				InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition = newScrollPos;
+			}
+
+			var panelTop = InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition.y;
+			var panelBottom = InkPlayerWindowState.Instance.namedContentPanelState.scrollPosition.y + viewportRect.height;
+            
+			for(int i = 0; i < paths.Count; i++) {
+				if(panelTop <= rects[i].yMax && panelBottom >= rects[i].yMin) {
+					var content = paths[i];
+					var lineContainerRect = rects[i];
+					var lineRect = new Rect(lineContainerRect.x + contentMarginX, lineContainerRect.y + contentMarginY, lineContainerRect.width - contentMarginX * 2, lineContainerRect.height - contentMarginY * 2);		
+					DrawNamedContentItem(lineRect, content);
+				}
+			}
+
+			GUI.EndScrollView();
+			GUILayout.Space(viewportRect.height);
+		}
+
+		void DrawNamedContentItem (Rect rect, string currentPath) {
+			EditorGUI.LabelField(rect, new GUIContent(currentPath, "Path"));
+			EditorGUI.LabelField(new Rect(rect.xMax-200, rect.y, 32, rect.height), new GUIContent(story.state.VisitCountAtPathString(currentPath).ToString(), "Read count"));
+			if(GUI.Button(new Rect(rect.xMax-168, rect.y, 80, rect.height), new GUIContent("Divert"))) {
+                // This is a bit horrible tbh. Not all the paths we show are valid for diverting, but we don't really have a way of testing it.
+                // Additionally, doing this can brick the story, so it's important we prevent it.
+                // We test by creating an entirely new story and running it, and checking for errors in the flow.
+                // The result is that this function is really slow :(
+				var hadError = false;
+                try {
+                    // We might optimise this by caching story.ToJson() - we could use this in other places too.
+                    var tmpStory = new Story(story.ToJson());
+                    var state = story.state.ToJson();
+                    tmpStory.state.LoadJson(state);
+				    tmpStory.ChoosePathString(currentPath);
+				    tmpStory.ContinueMaximally();
+                } catch (Exception e) {
+                    Debug.LogWarning("Could not divert to "+currentPath+"! Only Knots and Stitches can be diverted to. Is this a function? Alternatively, the path might lead to an error, which we prevent from occuring in this tool to safeguard the state.\n"+e.ToString());
+                    hadError = true;
+                } finally {
+                    if(!hadError) {
+				        story.ChoosePathString(currentPath);
+                        AddToHistory(InkHistoryContentItem.CreateForDebugNote("Diverted to '"+currentPath+"'"));
+                    }
+                }
+            }
+
+            if(Event.current.type == EventType.MouseDown && Event.current.button == 1 && rect.Contains(Event.current.mousePosition)) {
+                var contextMenu = new GenericMenu();
+                contextMenu.AddItem(new GUIContent("Copy Path"), false, () => {
+                    GUIUtility.systemCopyBuffer = currentPath;
+                });
+                contextMenu.ShowAsContext();
+                Event.current.Use();
+            }
+		}
+		#endregion
+
+
+		#region Functions
+		void DrawFunctions () {
+			DrawFunctionsHeader();
+			if(InkPlayerWindowState.Instance.functionPanelState.showing)
+				DrawFunctionsPanel ();
+		}
+
+		void DrawFunctionsHeader () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.functionPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.functionPanelState.showing, "Functions", true);
+			EditorGUILayout.EndHorizontal();
+		}
+
+		void DrawFunctionsPanel () {
+			GUILayout.BeginVertical();
+
+			DrawFunctionInput();
+			DrawFunctionOutput();
+
+			GUILayout.EndVertical();
+		}
+
+		void DrawFunctionInput () {
+			// TODO - Autocomplete function names using this, which I should add to Story.cs ( ask joe first! )	
+			// public IEnumerable<string> allFunctionNames {
+			//     get {
+			//         return mainContentContainer.namedContent.Keys;
+			//     }
+			// }
+
+			GUILayout.BeginVertical(GUI.skin.box);
+			EditorGUI.BeginChangeCheck();
+			InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName = EditorGUILayout.TextField("Function Name", InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName);
+			if(EditorGUI.EndChangeCheck()) {
+				InkPlayerWindowState.Instance.functionPanelState.testedFunctionName = null;
+				InkPlayerWindowState.Instance.functionPanelState.functionReturnValue = null;
+			}
+			functionInputList.DoLayoutList();
+			bool functionIsValid = InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName != String.Empty && story.HasFunction(InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName);
+			EditorGUI.BeginDisabledGroup(!functionIsValid);
+			if (GUILayout.Button(new GUIContent("Execute", "Runs the function"))) {
+				AddToHistory(InkHistoryContentItem.CreateForDebugNote("Execute function '"+InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName+"'"));
+				string outputContent = null;
+				object[] allInput = new object[InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs.Count];
+				for (int i = 0; i < InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs.Count; i++) {
+					var input = InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs[i];
+					object obj = null;
+					switch(input.type) {
+					case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.Int:
+						obj = input.intValue;
+						break;
+					case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.String:
+						obj = input.stringValue;
+						break;
+					case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.Bool:
+						obj = input.boolValue;
+						break;
+					case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkVariable:
+						obj = input.inkVariableValue;
+						break;
+					case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkListVariable:
+						obj = input.inkListVariableValue;
+						break;
+					}
+					allInput[i] = obj;
+				}
+
+				InkPlayerWindowState.Instance.functionPanelState.functionReturnValue = story.EvaluateFunction(InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName, out outputContent, allInput);
+				if(outputContent != null)
+					AddStoryContent(outputContent, null);
+				InkPlayerWindowState.Instance.functionPanelState.testedFunctionName = InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName;
+			}
+			EditorGUI.EndDisabledGroup();
+			GUILayout.EndVertical();
+		}
+
+		void DrawFunctionOutput () {
+			bool functionIsValid = InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName != String.Empty && story.HasFunction(InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName);
+			if(functionIsValid && InkPlayerWindowState.Instance.functionPanelState.functionParams.functionName == InkPlayerWindowState.Instance.functionPanelState.testedFunctionName) {
+				GUILayout.BeginVertical(GUI.skin.box);
+				if(InkPlayerWindowState.Instance.functionPanelState.functionReturnValue == null) {
+					EditorGUILayout.LabelField("Output (Null)");
+				} else if(InkPlayerWindowState.Instance.functionPanelState.functionReturnValue is string) {
+					EditorGUILayout.TextField("Output (String)", (string)InkPlayerWindowState.Instance.functionPanelState.functionReturnValue);
+				} else if(InkPlayerWindowState.Instance.functionPanelState.functionReturnValue is float) {
+					EditorGUILayout.FloatField("Output (Float)", (float)InkPlayerWindowState.Instance.functionPanelState.functionReturnValue);
+				} else if(InkPlayerWindowState.Instance.functionPanelState.functionReturnValue is int) {
+					EditorGUILayout.IntField("Output (Int)", (int)InkPlayerWindowState.Instance.functionPanelState.functionReturnValue);
+				} else if(InkPlayerWindowState.Instance.functionPanelState.functionReturnValue is InkList) {
+					EditorGUILayoutInkListField(new GUIContent("Output (InkList)"), (InkList)InkPlayerWindowState.Instance.functionPanelState.functionReturnValue);
+				} else {
+					EditorGUILayout.LabelField("Function returned unexpected type "+InkPlayerWindowState.Instance.functionPanelState.functionReturnValue.GetType().Name+".");
+				}
+				GUILayout.EndVertical();
+			}
+		}
+
+		void BuildFunctionInputList () {
+			functionInputList = new ReorderableList(InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs, typeof(FunctionPanelState.FunctionParams.FunctionInput), true, true, true, true);
+			functionInputList.drawHeaderCallback = (Rect rect) => {
+				EditorGUI.LabelField(rect, "Inputs");
+			};
+			functionInputList.elementHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing * 2;
+			functionInputList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
+				var input = InkPlayerWindowState.Instance.functionPanelState.functionParams.inputs[index];
+				Rect typeRect = new Rect(rect.x, rect.y, 80, EditorGUIUtility.singleLineHeight);
+				input.type = (FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType)EditorGUI.EnumPopup(typeRect, input.type);
+				Rect inputRect = new Rect(rect.x + 90, rect.y, rect.width - 90, EditorGUIUtility.singleLineHeight);
+				switch(input.type) {
+				case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.Int:
+					input.intValue = EditorGUI.IntField(inputRect, input.intValue);
+					break;
+				case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.String:
+					input.stringValue = EditorGUI.TextField(inputRect, input.stringValue);
+					break;
+				case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.Bool:
+					input.boolValue = EditorGUI.Toggle(inputRect, input.boolValue);
+					break;
+				case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkVariable:
+					{
+						var halfInput = new Rect(inputRect.x, inputRect.y, Mathf.RoundToInt(inputRect.width * 0.5f) - 5, inputRect.height);
+						var halfInput2 = new Rect(inputRect.x + Mathf.RoundToInt(inputRect.width * 0.5f) + 5, inputRect.y, Mathf.RoundToInt(inputRect.width * 0.5f) - 10, inputRect.height);
+						EditorGUI.BeginChangeCheck();
+						input.inkVariablePath = EditorGUI.TextField(halfInput, input.inkVariablePath);
+						if(EditorGUI.EndChangeCheck()) input.RefreshInkVariableValue(story);
+						
+						EditorGUI.BeginDisabledGroup(true);
+						DrawVariable(halfInput2, GUIContent.none, input.inkVariableValue);
+						EditorGUI.EndDisabledGroup();
+					}
+					break;
+				case FunctionPanelState.FunctionParams.FunctionInput.FunctionInputType.InkListVariable:
+					{
+						var halfInput = new Rect(inputRect.x, inputRect.y, Mathf.RoundToInt(inputRect.width * 0.5f) - 5, inputRect.height);
+						var halfInput2 = new Rect(inputRect.x + Mathf.RoundToInt(inputRect.width * 0.5f) + 5, inputRect.y, Mathf.RoundToInt(inputRect.width * 0.5f) - 10, inputRect.height);
+						EditorGUI.BeginChangeCheck();
+						input.inkListVariablePath = EditorGUI.TextField(halfInput, input.inkListVariablePath);
+						if(EditorGUI.EndChangeCheck()) input.RefreshInkListVariableValue(story);
+						EditorGUI.BeginDisabledGroup(true);
+						DrawVariable(halfInput2, GUIContent.none, input.inkListVariableValue);
+						EditorGUI.EndDisabledGroup();
+					}
+					break;
+				}
+			};
+		}
+		#endregion
+
+
+
+		
+		#region Variables
+		void DrawVariables () {
+            EditorGUILayout.BeginVertical();
+			if(InkEditorUtils.StoryContainsVariables(story)) {
+				DrawVariablesHeader();
+				if(InkPlayerWindowState.Instance.variablesPanelState.showing)
+					DrawVariablesPanel ();
+
+				if(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Count > 0) {
+					DrawObservedVariablesHeader();
+					if(InkPlayerWindowState.Instance.observedVariablesPanelState.showing)
+						DrawObservedVariablesPanel ();
+				}
+			}
+            EditorGUILayout.EndVertical();
+		}
+
+		void DrawVariablesHeader () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.variablesPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.variablesPanelState.showing, "Variables", true);
+
+			EditorGUI.BeginDisabledGroup(!InkPlayerWindowState.Instance.variablesPanelState.showing);
+			bool changed = DrawSearchBar(ref InkPlayerWindowState.Instance.variablesPanelState.searchString);
+			if(changed) InkPlayerWindowState.Instance.variablesPanelState.scrollPosition = Vector2.zero;
+			EditorGUI.EndDisabledGroup();
+
+			EditorGUILayout.EndHorizontal();
+		}
+
+		void DrawVariablesPanel () {
+			GUILayout.BeginVertical(GUI.skin.box, GUILayout.ExpandHeight(true));
+			InkPlayerWindowState.Instance.variablesPanelState.scrollPosition = EditorGUILayout.BeginScrollView(InkPlayerWindowState.Instance.variablesPanelState.scrollPosition);
+			string variableToChange = null;
+			object newVariableValue = null;
+			foreach(string variable in story.variablesState) {
+				DrawObservableVariable(variable, ref variableToChange, ref newVariableValue);
+			}
+			if(variableToChange != null) {
+				AddToHistory(InkHistoryContentItem.CreateForDebugNote("Change '"+variableToChange+"' from '"+story.variablesState[variableToChange]+"' to '"+newVariableValue+"'"));
+				story.variablesState[variableToChange] = newVariableValue;
+				variableToChange = null;
+				newVariableValue = null;
+			}
+			
+			EditorGUILayout.EndScrollView();
+			GUILayout.EndVertical();
+		}
+
+        // TODO - only draw those that are visible in the scroll rect, as we do for content. Important for performance on larger projects.
+        void DrawObservableVariable (string variable, ref string variableToChange, ref object newVariableValue) {
+            if(!SearchStringMatch(variable, InkPlayerWindowState.Instance.variablesPanelState.searchString)) 
+                return;
+            EditorGUILayout.BeginHorizontal();
+            object variableValue = story.variablesState[variable];
+            EditorGUI.BeginChangeCheck();
+            variableValue = DrawVariable(new GUIContent(variable), variable, variableValue);
+            if(EditorGUI.EndChangeCheck() && story.variablesState[variable] != variableValue) {
+                variableToChange = variable;
+                newVariableValue = variableValue;
+            }
+
+            if(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.ContainsKey(variable)) {
+                if(GUILayout.Button(new GUIContent("<-", "Un-observe this variable"), GUILayout.Width(24))) {
+                    UnobserveVariable(variable, true);
+                }
+            } else {
+                if(GUILayout.Button(new GUIContent("->", "Click to observe this variable, tracking changes"), GUILayout.Width(24))) {
+                    var observedVariable = ObserveVariable(variable, true);
+                    observedVariable.AddValueState(variableValue);
+                }
+            }
+            EditorGUILayout.EndHorizontal();
+        }
+
+		static ObservedVariable ObserveVariable (string variableName, bool alsoAddToCache) {
+			if(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.ContainsKey(variableName)) return InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables[variableName];
+			var observedVariable = new ObservedVariable(variableName);
+			observedVariable.variableObserver = (_variableName, newValue) => {
+				observedVariable.AddValueState(newValue);
+			};
+			story.ObserveVariable(variableName, observedVariable.variableObserver);
+			InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Add(variableName, observedVariable);
+			if(alsoAddToCache) {
+				InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Add(variableName);
+				if(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Count != InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Count) {
+					Debug.LogError(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Count +" "+ InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Count);
+					InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Clear();
+					InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Clear();
+				}
+			}
+			return observedVariable;
+		}
+
+		static void UnobserveVariable (string variableName, bool alsoRemoveFromCache) {
+			if(!InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.ContainsKey(variableName)) return;
+			
+			var observedVariable = InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables[variableName];
+			story.RemoveVariableObserver(observedVariable.variableObserver, variableName);
+			InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Remove(variableName);
+			if(alsoRemoveFromCache) {
+				InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Remove(variableName);
+				if(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Count != InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Count) {
+					Debug.LogError(InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Count +" "+ InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Count);
+					InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariableNames.Clear();
+					InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables.Clear();
+				}
+			}
+		}
+
+		object DrawVariable (GUIContent guiContent, string variableName, object variableValue) {
+			EditorGUILayout.BeginHorizontal();
+			if(variableValue is string) {
+				EditorGUI.BeginDisabledGroup(playerParams.disableSettingVariables);
+				variableValue = EditorGUILayout.TextField(guiContent, (string)variableValue);
+				EditorGUI.EndDisabledGroup();
+			} else if(variableValue is float) {
+				EditorGUI.BeginDisabledGroup(playerParams.disableSettingVariables);
+				variableValue = EditorGUILayout.FloatField(guiContent, (float)variableValue);
+				EditorGUI.EndDisabledGroup();
+			} else if(variableValue is int) {
+				EditorGUI.BeginDisabledGroup(playerParams.disableSettingVariables);
+				variableValue = EditorGUILayout.IntField(guiContent, (int)variableValue);
+				EditorGUI.EndDisabledGroup();
+			} else if(variableValue is bool) {
+				EditorGUI.BeginDisabledGroup(playerParams.disableSettingVariables);
+				variableValue = EditorGUILayout.Toggle(guiContent, (bool)variableValue);
+				EditorGUI.EndDisabledGroup();
+			} else if(variableValue is InkList) {
+				EditorGUILayoutInkListField(guiContent, (InkList)variableValue, variableName);
+			} else if(variableValue is Ink.Runtime.Path) {
+				var c = new GUIContent(((Ink.Runtime.Path)variableValue).ToString()+" (Ink.Runtime.Path)");
+				EditorGUILayout.LabelField(guiContent, c);
+			} else if(variableValue == null) {
+				EditorGUILayout.LabelField(guiContent, new GUIContent("InkPlayerError: Variable is null"));
+			} else {
+				EditorGUILayout.LabelField(guiContent, new GUIContent("InkPlayerError: Variable is of unexpected type "+variableValue.GetType().Name+"."));
+			}
+			EditorGUILayout.EndHorizontal();
+			return variableValue;
+		}
+
+		object DrawVariable (Rect rect, GUIContent variable, object variableValue) {
+			if(variableValue is string) {
+				variableValue = EditorGUI.TextField(rect, variable, (string)variableValue);
+			} else if(variableValue is float) {
+				variableValue = EditorGUI.FloatField(rect, variable, (float)variableValue);
+			} else if(variableValue is int) {
+				variableValue = EditorGUI.IntField(rect, variable, (int)variableValue);
+			} else if(variableValue is InkList) {
+				var c = new GUIContent(variable);
+				var inkList = (InkList)variableValue;
+				c.text += " (InkList)";
+				if(inkList.Any()) {
+					bool first = true;
+					foreach(var item in inkList) {
+						if(!first) c.text += ", ";
+						c.text += item.ToString();
+						first = false;
+					}
+				} else {
+					c.text += " Empty";
+				}
+				EditorGUI.LabelField(rect, c);
+			} else if(variableValue is Ink.Runtime.Path) {
+				var c = new GUIContent(((Ink.Runtime.Path)variableValue).ToString()+" (Ink.Runtime.Path)");
+				EditorGUI.LabelField(rect, c);
+			} else if(variableValue == null) {
+				EditorGUI.LabelField(rect, variable, new GUIContent("InkPlayerError: Variable is null"));
+			} else {
+				EditorGUI.LabelField(rect, variable, new GUIContent("InkPlayerError: Variable is of unexpected type "+variableValue.GetType().Name+"."));
+			}
+			return variableValue;
+		}
+
+		void DrawObservedVariablesHeader () {
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.observedVariablesPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.observedVariablesPanelState.showing, "Observed Variables", true);
+			EditorGUILayout.EndHorizontal();
+		}
+
+		void DrawObservedVariablesPanel () {
+			List<string> allToRemove = new List<string>();
+			foreach(var observedVariable in InkPlayerWindowState.Instance.observedVariablesPanelState.observedVariables) {
+				bool removeVariable = DrawObservedVariable(observedVariable.Value);
+				if(removeVariable)
+					allToRemove.Add(observedVariable.Key);
+			}
+			foreach(var toRemove in allToRemove) {
+				UnobserveVariable(toRemove, true);
+			}
+		}
+
+		bool DrawObservedVariable (ObservedVariable observedVariable) {
+			GUILayout.BeginHorizontal();
+			observedVariable.expanded = EditorGUILayout.Foldout(observedVariable.expanded, observedVariable.variable, true);
+			if(GUILayout.Button("<-", GUILayout.Width(24))) {
+				return true;
+			}
+			GUILayout.EndHorizontal();
+
+			if(observedVariable.expanded) {
+				GUILayout.BeginVertical(GUILayout.ExpandHeight(false));
+				observedVariable.scrollPosition = EditorGUILayout.BeginScrollView(observedVariable.scrollPosition, GUI.skin.box);
+				
+				foreach(var value in observedVariable.values) {
+					DrawVariable(new GUIContent(value.dateTime.ToLongTimeString()), observedVariable.variable, value.state);
+				}
+				
+				EditorGUILayout.EndScrollView();
+				GUILayout.EndVertical();
+			}
+
+			return false;
+		}
+		#endregion
+
+
+
+
+
+		#region Profiler
+		bool isProfiling {
+			get {
+				return _currentStoryProfiler != null;
+			}
+			set {
+				var shouldBeProfiling = value;
+				if( shouldBeProfiling != isProfiling ) {
+					if( _currentStoryProfiler == null ) {
+						_currentStoryProfiler = story.StartProfiling();
+					} else {
+						story.EndProfiling();
+						_profilerResultRootNode = _currentStoryProfiler.rootNode;
+
+						Debug.Log(_currentStoryProfiler.StepLengthReport());
+
+						_previousStoryProfiler = _currentStoryProfiler;
+						_currentStoryProfiler = null;
+					}
+				}
+			}
+		}
+		ProfileNode _profilerResultRootNode;
+		Ink.Runtime.Profiler _currentStoryProfiler;
+		Ink.Runtime.Profiler _previousStoryProfiler;
+
+
+		void DrawProfilerData() {
+
+			// Don't show profiler data at all if you've never clicked Profile button
+			if( _profilerResultRootNode == null && !isProfiling ) return;
+
+			EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+			InkPlayerWindowState.Instance.profilerPanelState.showing = EditorGUILayout.Foldout(InkPlayerWindowState.Instance.profilerPanelState.showing, "Profiler data", true);
+			GUILayout.FlexibleSpace();
+			if( _previousStoryProfiler != null && GUILayout.Button("Save mega log", EditorStyles.toolbarButton) ) {
+
+				var path = EditorUtility.SaveFilePanel(
+					"Save mega log",
+					"",
+					"megalog.txt",
+					"txt");
+				if( path != null && path.Length > 0 ) 
+					File.WriteAllText(path, _previousStoryProfiler.Megalog());
+
+			}
+			EditorGUILayout.EndHorizontal();
+
+			if(InkPlayerWindowState.Instance.profilerPanelState.showing) {
+				if(isProfiling) {
+					EditorGUILayout.LabelField("Click 'Stop Profiling' to show profiling results.", EditorStyles.centeredGreyMiniLabel);
+				} else {
+					DisplayProfileDataNode(_profilerResultRootNode);
+				}
+			}
+		}
+
+		void DisplayProfileDataNode(ProfileNode node) {
+			var key = node.key;
+			if( key == null ) {
+				if( node == _profilerResultRootNode )
+					key = "TOTAL";
+				else
+					key = "?";
+			}
+
+			var nodeText = key + ": " + node.ownReport;
+
+			if( node.hasChildren ) {
+				node.openInUI = EditorGUILayout.Foldout(node.openInUI, nodeText, true);
+
+				if( node.openInUI ) {
+					EditorGUI.indentLevel++;
+
+					foreach(var childNode in node.descendingOrderedNodes)
+						DisplayProfileDataNode(childNode.Value);
+
+					EditorGUI.indentLevel--;
+				}
+			} else {
+				EditorGUILayout.LabelField(nodeText);
+			}
+		}
+		#endregion
+		
+
+
+
+
+		BaseStoryPanelState resizingPanel;
+		// float height = 50;
+		Rect GetResizeArea (float x, float width, float centerY) {
+			float height = 10;
+			return new Rect(x, centerY-Mathf.RoundToInt(height * 0.5f), width, height);
+		}
+		void HandleDragAndDrop () {
+			// Rect area1 = GUILayoutUtility.GetRect (0.0f, height, GUILayout.ExpandWidth (true));
+			// Rect area2 = GUILayoutUtility.GetRect (0.0f, 50.0f, GUILayout.ExpandWidth (true));
+        	// GUI.Box (area1, "Add Trigger");
+        	// GUI.Box (area2, "Add Trigger");
+
+			// if (Event.current.type == EventType.DragUpdated) {
+			// 	DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
+			// 	Event.current.Use();
+			// } else if (Event.current.type == EventType.DragPerform) {
+			// 	// To consume drag data.
+				
+			// 	DragAndDrop.AcceptDrag();
+			// 	foreach (var obj in DragAndDrop.objectReferences) {
+			// 		if(obj is TextAsset && System.IO.Path.GetExtension(AssetDatabase.GetAssetPath(obj)) == ".json") {
+			// 			Play(obj as TextAsset);
+			// 		}
+			// 	}
+			// }
+            
+            if (resizingPanel != null && Event.current.type == EventType.MouseUp) {
+                resizingPanel = null;
+                Event.current.Use();
+            }
+            HandlePanelResize(InkPlayerWindowState.Instance.storyPanelState, StoryPanelState.minScrollRectHeight, StoryPanelState.maxScrollRectHeight);
+            HandlePanelResize(InkPlayerWindowState.Instance.namedContentPanelState, StoryPanelState.minScrollRectHeight, StoryPanelState.maxScrollRectHeight);
+		}
+		void HandlePanelResize (BaseStoryPanelState panel, float minHeight, float maxHeight) {
+			var resizeArea = GetResizeArea(0, position.width, panel.y+panel.height);
+			EditorGUIUtility.AddCursorRect(resizeArea, MouseCursor.ResizeVertical);
+			
+			if (Event.current.type == EventType.MouseDown) {
+				if(resizeArea.Contains(Event.current.mousePosition)) {
+					resizingPanel = panel;
+					Event.current.Use();
+				}
+			}
+			if (resizingPanel == panel) {
+				if(Event.current.type == EventType.MouseDrag) {
+					var targetHeight = panel.height + Event.current.delta.y;
+					panel.height = Mathf.Clamp(targetHeight, minHeight, maxHeight);
+					Event.current.Use();
+				}
+			}
+        }
+		
+		#region Utils
+		static bool StringContains(string str, string toCheck, StringComparison comp) {
+			if(toCheck.Length == 0) return false;
+			return str.IndexOf(toCheck, comp) >= 0;
+		}
+
+		static bool DrawSearchBar (ref string searchString) {
+			var lastString = searchString;
+			searchString = GUILayout.TextField(searchString, searchTextFieldStyle);
+			if (GUILayout.Button("", searchCancelButtonStyle)) {
+				searchString = string.Empty;
+			}
+			return lastString != searchString;
+		}
+
+        static bool SearchStringMatch (string content, string searchString) {
+            return string.IsNullOrWhiteSpace(searchString) || StringContains(content, searchString, StringComparison.OrdinalIgnoreCase);
+        }
+
+
+		static void EditorGUILayoutInkListField (GUIContent guiContent, InkList inkList, string expandedVariableKey = null) {
+			if(inkList.Any()) {
+				var show = expandedVariableKey == null ? true : InkPlayerWindowState.Instance.variablesPanelState.expandedVariables.Contains(expandedVariableKey);
+				var c = new GUIContent(guiContent);
+				c.text += " (InkList with "+inkList.Count+" entries)";
+				EditorGUILayout.BeginVertical();
+				
+				EditorGUI.BeginChangeCheck();
+				show = EditorGUILayout.Foldout(show, c, true);
+				if(EditorGUI.EndChangeCheck() && expandedVariableKey != null) {
+					if(show) InkPlayerWindowState.Instance.variablesPanelState.expandedVariables.Add(expandedVariableKey);
+					else InkPlayerWindowState.Instance.variablesPanelState.expandedVariables.Remove(expandedVariableKey);
+				}
+				
+				if(show) {
+					EditorGUI.indentLevel++;
+					foreach(var item in inkList) {
+						EditorGUILayout.BeginHorizontal();
+						EditorGUILayout.LabelField(new GUIContent(item.Key.fullName));
+						// Disabled until I can be bothered to integrate this into the change detection system
+						EditorGUI.BeginDisabledGroup(true);
+						EditorGUILayout.IntField(item.Value, GUILayout.Width(100));
+						EditorGUI.EndDisabledGroup();
+						EditorGUILayout.EndHorizontal();
+					}
+					EditorGUI.indentLevel--;
+				}
+				EditorGUILayout.EndVertical();
+			} else {
+				var c = new GUIContent(guiContent);
+				c.text += " (InkList)";
+				EditorGUILayout.PrefixLabel(c);
+				EditorGUILayout.LabelField("Empty");
+			}
+		}
+
+		// static void EditorGUILayoutInkListField (string text, InkList inkList) {
+		//     EditorGUILayoutInkListField(new GUIContent(text), inkList);
+		// }
+
+		// static void EditorGUILayoutInkListField (GUIContent content, InkList inkList) {
+		//     EditorGUILayout.BeginVertical(GUI.skin.box);
+		//     EditorGUILayout.LabelField("InkList with "+inkList.Count+" values:", EditorStyles.boldLabel);
+		//     foreach(var item in inkList) {
+		//         EditorGUILayout.LabelField(item.Key.ToString()+" ("+item.Value.ToString()+")");
+		//     }
+		//     EditorGUILayout.EndVertical();
+		// }
+		static void EditorGUIInkListField (Rect rect, GUIContent content, InkList inkList, string variableName) {
+			EditorGUI.PrefixLabel(rect, content);
+			if(inkList.Any()) {
+				if(GUILayout.Button("Log Contents")) {
+					string log = "Log for InkList "+variableName+":";
+					foreach(var item in inkList)
+						log += item.ToString() + " / ";
+					Debug.Log(log);
+				}
+			} else {
+				EditorGUI.LabelField(rect, "Empty");
+			}
+		}
+		#endregion
+	}
+
+	// Keeps a history of state changes for an ink variable. Handy for debugging.
+	public class ObservedVariable {
+		public string variable;
+		public Story.VariableObserver variableObserver;
+		public List<ObservedVariableState> values = new List<ObservedVariableState>();
+		public bool expanded = true;
+		public Vector2 scrollPosition = Vector2.zero;
+
+		public class ObservedVariableState {
+			public object state;
+			public DateTime dateTime;
+			public ObservedVariableState (object state) {
+				// Make sure to clone any object ref types! (just InkList at time of writing)
+				if(state is InkList) state = new InkList((InkList)state);
+				this.state = state;
+				dateTime = DateTime.Now;
+			}
+		}
+
+		public ObservedVariable (string variable) {
+			this.variable = variable;
+		}
+		public void AddValueState (object value) {
+			values.Add(new ObservedVariableState(value));
+		}
+	}
+    
+
+	public class ColoredBackgroundGUIStyle {
+		public GUIStyle guiStyle;
+		public ColoredBackgroundGUIStyle (Color color) : this (color, color) {}
+		public ColoredBackgroundGUIStyle (Color colorFree, Color colorPro) {
+			guiStyle = new GUIStyle();
+
+			var texture = new Texture2D( 1, 1 );
+			texture.SetPixel(0, 0, EditorGUIUtility.isProSkin ? colorPro : colorFree);
+			texture.Apply();
+			guiStyle.normal.background = texture;
+		}
+		public ColoredBackgroundGUIStyle (Color colorFree, Color colorPro, Color hoverColorFree, Color hoverColorPro) {
+			guiStyle = new GUIStyle();
+
+			var texture = new Texture2D( 1, 1 );
+			texture.SetPixel(0, 0, EditorGUIUtility.isProSkin ? colorPro : colorFree);
+			texture.Apply();
+			guiStyle.normal.background = texture;
+
+			var hoverTexture = new Texture2D( 1, 1 );
+			hoverTexture.SetPixel(0, 0, EditorGUIUtility.isProSkin ? hoverColorPro : hoverColorFree);
+			hoverTexture.Apply();
+			guiStyle.hover.background = hoverTexture;
+		}
+	}
+
+
+	[System.Serializable]
+	public class UndoHistory<T> where T : class {
+		
+		private int _undoHistoryIndex;
+		public int undoHistoryIndex {
+			get {
+				return _undoHistoryIndex;
+			} set {
+				_undoHistoryIndex = Mathf.Clamp(value, 0, undoHistory.Count-1);
+				if(OnChangeHistoryIndex != null) OnChangeHistoryIndex(undoHistory[undoHistoryIndex]);
+			}
+		}
+		
+		public List<T> undoHistory;
+		public int maxHistoryItems = 100;
+		
+		public bool canUndo {
+			get {
+				return undoHistory.Count > 0 && undoHistoryIndex > 0;
+			}
+		}
+		
+		public bool canRedo {
+			get {
+				return undoHistory.Count > 0 && undoHistoryIndex < undoHistory.Count - 1;
+			}
+		}
+		
+		public delegate void OnUndoEvent(T historyItem);
+		public event OnUndoEvent OnUndo;
+		
+		public delegate void OnRedoEvent(T historyItem);
+		public event OnRedoEvent OnRedo;
+		
+		public delegate void OnChangeHistoryIndexEvent(T historyItem);
+		public event OnChangeHistoryIndexEvent OnChangeHistoryIndex;
+		
+		public delegate void OnChangeUndoHistoryEvent();
+		public event OnChangeUndoHistoryEvent OnChangeUndoHistory;
+		
+		public UndoHistory () {
+			undoHistory = new List<T>();
+			_undoHistoryIndex = -1;
+		}
+		
+		public UndoHistory (int maxHistoryItems) : this () {
+			this.maxHistoryItems = Mathf.Clamp(maxHistoryItems, 1, int.MaxValue);
+		}
+		
+		public virtual void AddToUndoHistory (T state) {
+			if(undoHistory.Count > 0 && undoHistory.Count - (undoHistoryIndex + 1) > 0) {
+				undoHistory.RemoveRange(undoHistoryIndex + 1, undoHistory.Count - (undoHistoryIndex + 1));
+			}
+			
+			if(undoHistory.Count >= maxHistoryItems) {
+				undoHistory.RemoveAt (0);
+				_undoHistoryIndex--;
+			}
+			
+			undoHistory.Add (state);
+			_undoHistoryIndex++;
+			
+			if(OnChangeUndoHistory != null) OnChangeUndoHistory();
+		}
+		
+		public virtual void Clear () {
+			undoHistory.Clear();
+			_undoHistoryIndex = -1;
+			if(OnChangeUndoHistory != null) OnChangeUndoHistory();
+		}
+		
+		public virtual T Undo () {
+			if(!canUndo) {
+				if(undoHistory.Count > 0)
+					return default(T);
+			} else {
+				undoHistoryIndex--;
+				if(OnUndo != null) OnUndo(undoHistory[undoHistoryIndex]);
+			}
+			return undoHistory[undoHistoryIndex];
+		}
+		
+		public virtual T Redo () {
+			if(!canRedo) {
+				if(undoHistory.Count > 0)
+					return default(T);
+			} else {
+				undoHistoryIndex++;
+				if(OnRedo != null) OnRedo(undoHistory[undoHistoryIndex]);
+			}
+			return undoHistory[undoHistoryIndex];
+		}
+		
+		protected virtual void ApplyHistoryItem (T historyItem) {}
+	}
+
+	public class InkPlayerHistoryItem {
+		public string inkStateJSON;
+		public List<InkHistoryContentItem> storyHistory;
+		
+		public InkPlayerHistoryItem (string inkStateJSON, List<InkHistoryContentItem> storyHistory) {
+			this.inkStateJSON = inkStateJSON;
+			this.storyHistory = storyHistory;
+		}
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs.meta b/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs.meta
new file mode 100644
index 0000000..6f4ed30
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Player Window/InkPlayerWindow.cs.meta	
@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: bf969d7f2c98e470c9797844e20b20ee
+timeCreated: 1459342679
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Startup Window.meta b/Assets/Ink/Editor/Tools/Startup Window.meta
new file mode 100644
index 0000000..6afaed2
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e6bbfb931dd0d104199fc4542fc5a85d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs b/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs
new file mode 100644
index 0000000..a009936
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs	
@@ -0,0 +1,123 @@
+using UnityEditor;
+using UnityEngine;
+
+namespace Ink.UnityIntegration {
+	[InitializeOnLoad]
+	public class InkUnityIntegrationStartupWindow : EditorWindow {
+		const string editorPrefsKeyForVersionSeen = "Ink Unity Integration Startup Window Version Confirmed";
+		const int announcementVersion = 2;
+		
+		Vector2 scrollPosition;
+		static int announcementVersionPreviouslySeen;
+
+		private static Texture2D _logoIcon;
+		public static Texture2D logoIcon {
+			get {
+				if(_logoIcon == null) {
+					_logoIcon = Resources.Load<Texture2D>("InkLogoIcon");
+				}
+				return _logoIcon;
+			}
+		}
+
+		static InkUnityIntegrationStartupWindow () {
+			UnityEditor.EditorApplication.delayCall += TryCreateWindow;      
+		}
+
+		static void TryCreateWindow() {
+			announcementVersionPreviouslySeen = EditorPrefs.GetInt(editorPrefsKeyForVersionSeen, -1);
+			if(announcementVersion != announcementVersionPreviouslySeen) {
+				ShowWindow();
+			}
+		}
+		
+        public static void ShowWindow () {
+            InkUnityIntegrationStartupWindow window = EditorWindow.GetWindow(typeof(InkUnityIntegrationStartupWindow), true, "Ink Update "+InkLibrary.unityIntegrationVersionCurrent.ToString(), true) as InkUnityIntegrationStartupWindow;
+            window.minSize = new Vector2(200, 200);
+            var size = new Vector2(520, 320);
+            window.position = new Rect((Screen.currentResolution.width-size.x) * 0.5f, (Screen.currentResolution.height-size.y) * 0.5f, size.x, size.y);
+            EditorPrefs.SetInt(editorPrefsKeyForVersionSeen, announcementVersion);
+        }
+        
+		void OnGUI ()
+		{
+			EditorGUILayout.BeginVertical();
+			var areaSize = new Vector2(90,90);
+			GUILayout.BeginArea(new Rect((position.width-areaSize.x)*0.5f, 15, areaSize.x, areaSize.y));
+			EditorGUILayout.BeginVertical();
+			EditorGUILayout.LabelField(new GUIContent(logoIcon), GUILayout.Width(areaSize.x), GUILayout.Height(areaSize.x*((float)logoIcon.height/logoIcon.width)));
+			GUILayout.Space(5);
+			EditorGUILayout.LabelField("Version "+InkLibrary.unityIntegrationVersionCurrent.ToString(), EditorStyles.centeredGreyMiniLabel);
+			EditorGUILayout.LabelField("Ink version "+InkLibrary.inkVersionCurrent.ToString(), EditorStyles.centeredGreyMiniLabel);
+			EditorGUILayout.EndVertical();
+			GUILayout.EndArea();
+
+			GUILayout.Space(20+areaSize.y);
+			
+			if(announcementVersionPreviouslySeen == -1) {
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				EditorGUILayout.LabelField("New to ink?", EditorStyles.boldLabel);
+				EditorGUILayout.EndVertical();
+			}
+
+			{
+				EditorGUILayout.BeginHorizontal();
+			
+				if (GUILayout.Button("About Ink")) {
+					Application.OpenURL("https://www.inklestudios.com/ink/");
+				}
+				if (GUILayout.Button("❤️Support Us!❤️")) {
+					Application.OpenURL("https://www.patreon.com/inkle");
+				}
+				if (GUILayout.Button("Close")) {
+					Close();
+				}
+				EditorGUILayout.EndHorizontal();
+			}
+
+			EditorGUILayout.Space();
+			
+			{
+				scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
+				{
+                    // 1.0.0
+					EditorGUILayout.BeginVertical(GUI.skin.box);
+					EditorGUILayout.LabelField("🎉Version 1.0.0🎉:", EditorStyles.boldLabel);
+					EditorGUILayout.LabelField("• Update ink to 1.0.0", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Ink Editor Window: Allow resizing (some) panels", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Ink Editor Window: Named content panel ", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Ink Editor Window: Improved performance for large stories", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Allow compiling include files that don't have the .ink file extension", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Remove ability to use a custom inklecate (legacy compiler)", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Fixes settings menu on 2020+", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Improved migration from earlier versions", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Moved persistent compilation tracking code from InkLibrary into InkCompiler", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Use Unity's new ScriptableSingleton for InkLibrary, InkSettings and InkCompiler on 2020+", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.EndVertical();
+					EditorGUILayout.BeginVertical(GUI.skin.box);
+                    // 0.9.71
+					EditorGUILayout.BeginVertical(GUI.skin.box);
+					EditorGUILayout.LabelField("Version 0.9.71:", EditorStyles.boldLabel);
+					EditorGUILayout.LabelField("• Resolves some compilation issues.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.EndVertical();
+					EditorGUILayout.BeginVertical(GUI.skin.box);
+                    // 0.9.60
+					EditorGUILayout.LabelField("Version 0.9.60:", EditorStyles.boldLabel);
+					EditorGUILayout.LabelField("• Moved InkLibrary and InkSettings from Assets into Library and ProjectSettings.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("   ‣ InkLibrary should no longer be tracked in source control.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("   ‣ Changes to InkSettings must be migrated manually.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("   ‣ The InkLibrary and InkSettings files in your project folder should be deleted.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.LabelField("• Added a divertable list of knots, stitches and other named content to the Ink Editor Window, replacing the Diverts subpanel.", EditorStyles.wordWrappedLabel);
+					EditorGUILayout.EndVertical();
+				}
+
+				EditorGUILayout.EndScrollView();
+			}
+			EditorGUILayout.Space();
+
+			EditorGUILayout.EndVertical();
+		}
+
+		
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs.meta b/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs.meta
new file mode 100644
index 0000000..0d29a5a
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window/InkUnityIntegrationStartupWindow.cs.meta	
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c436e11a2a780e14680d25152259eadd
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Startup Window/Resources.meta b/Assets/Ink/Editor/Tools/Startup Window/Resources.meta
new file mode 100644
index 0000000..dd2090f
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window/Resources.meta	
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ed82eefbb606deb49b637f3947d9cfb0
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png b/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png
new file mode 100644
index 0000000..bcbf194
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png	
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53ff661a69424f04607a33a1841e0c4157f3668ea166f41f86e677ae6ecdb673
+size 6234
diff --git a/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png.meta b/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png.meta
new file mode 100644
index 0000000..cbdc4bf
--- /dev/null
+++ b/Assets/Ink/Editor/Tools/Startup Window/Resources/InkLogoIcon.png.meta	
@@ -0,0 +1,121 @@
+fileFormatVersion: 2
+guid: 09746d3311e36764faa38d0e982f6a2d
+TextureImporter:
+  fileIDToRecycleName: {}
+  externalObjects: {}
+  serializedVersion: 9
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: -1
+    aniso: 1
+    mipBias: -100
+    wrapU: 1
+    wrapV: 1
+    wrapW: -1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 2
+  textureShape: 1
+  singleChannelComponent: 0
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - serializedVersion: 2
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+  - serializedVersion: 2
+    buildTarget: Standalone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+  - serializedVersion: 2
+    buildTarget: WebGL
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+  - serializedVersion: 2
+    buildTarget: Nintendo Switch
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Extras.meta b/Assets/Ink/Extras.meta
new file mode 100644
index 0000000..83eea07
--- /dev/null
+++ b/Assets/Ink/Extras.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: da9154df703de4360bd91ca5ce4ceb2d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/Extras/Sublime3Syntax.zip b/Assets/Ink/Extras/Sublime3Syntax.zip
new file mode 100644
index 0000000..b93377a
--- /dev/null
+++ b/Assets/Ink/Extras/Sublime3Syntax.zip
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:862974b5a1b59e076bb79b952415533f3f0b50522134509355a552152e1384c1
+size 11784
diff --git a/Assets/Ink/Extras/Sublime3Syntax.zip.meta b/Assets/Ink/Extras/Sublime3Syntax.zip.meta
new file mode 100644
index 0000000..5e66724
--- /dev/null
+++ b/Assets/Ink/Extras/Sublime3Syntax.zip.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: db1b6d593e1a4418489b76bb1e8b5ddd
+timeCreated: 1460814135
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs.meta b/Assets/Ink/InkLibs.meta
new file mode 100644
index 0000000..8e21c9b
--- /dev/null
+++ b/Assets/Ink/InkLibs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c6a6d084e4a5aea4b9b93f3f5e26e396
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/Ink-Libraries.asmdef b/Assets/Ink/InkLibs/Ink-Libraries.asmdef
new file mode 100644
index 0000000..988193b
--- /dev/null
+++ b/Assets/Ink/InkLibs/Ink-Libraries.asmdef
@@ -0,0 +1,6 @@
+{
+    "name": "Ink-Libraries",
+    "references": [],
+    "includePlatforms": [],
+    "excludePlatforms": []
+}
\ No newline at end of file
diff --git a/Assets/Ink/InkLibs/Ink-Libraries.asmdef.meta b/Assets/Ink/InkLibs/Ink-Libraries.asmdef.meta
new file mode 100644
index 0000000..b601883
--- /dev/null
+++ b/Assets/Ink/InkLibs/Ink-Libraries.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 58bed0e7c5306824586d7eda03609289
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler.meta b/Assets/Ink/InkLibs/InkCompiler.meta
new file mode 100644
index 0000000..770d159
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d9d4bbcea2b35784b919f188e0d2c800
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs b/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs
new file mode 100644
index 0000000..40ae689
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink
+{
+    /// <summary>
+    /// A class representing a character range. Allows for lazy-loading a corresponding <see cref="CharacterSet">character set</see>.
+    /// </summary>
+    public sealed class CharacterRange
+    {
+        public static CharacterRange Define(char start, char end, IEnumerable<char> excludes = null)
+        {
+            return new CharacterRange (start, end, excludes);
+        }
+
+        /// <summary>
+        /// Returns a <see cref="CharacterSet">character set</see> instance corresponding to the character range
+        /// represented by the current instance.
+        /// </summary>
+        /// <remarks>
+        /// The internal character set is created once and cached in memory.
+        /// </remarks>
+        /// <returns>The char set.</returns>
+        public CharacterSet ToCharacterSet ()
+        {
+            if (_correspondingCharSet.Count == 0) 
+            {
+                for (char c = _start; c <= _end; c++)
+                {
+                    if (!_excludes.Contains (c)) 
+                    {
+                        _correspondingCharSet.Add (c);
+                    }
+                }
+            }
+            return _correspondingCharSet;
+        }
+
+        public char start { get { return _start; } }
+        public char end { get { return _end; } }
+
+        CharacterRange (char start, char end, IEnumerable<char> excludes)
+        {
+        	_start = start;
+        	_end = end;
+            _excludes = excludes == null ? new HashSet<char>() : new HashSet<char> (excludes);
+        }
+
+        char _start;
+        char _end;
+        ICollection<char> _excludes;
+        CharacterSet _correspondingCharSet = new CharacterSet();
+    }    
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs.meta b/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs.meta
new file mode 100644
index 0000000..9478e6a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CharacterRange.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f3d8ce43e51d74eb791e0665c2ba4fc7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs b/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs
new file mode 100644
index 0000000..7e0eef2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Ink
+{
+
+	public class CharacterSet : HashSet<char>
+	{
+		public static CharacterSet FromRange(char start, char end) 
+		{
+			return new CharacterSet ().AddRange (start, end);
+		}
+
+		public CharacterSet ()
+		{
+		}
+
+		public CharacterSet(string str)
+		{
+            AddCharacters (str);
+		}
+
+        public CharacterSet(CharacterSet charSetToCopy)
+        {
+            AddCharacters (charSetToCopy);
+        }
+
+		public CharacterSet AddRange(char start, char end)
+		{
+			for(char c=start; c<=end; ++c) {
+				Add (c);
+			}
+			return this;
+		}
+
+		public CharacterSet AddCharacters(IEnumerable<char> chars)
+		{
+            foreach (char c in chars) {
+				Add (c);
+			}
+			return this;
+		}
+
+        public CharacterSet AddCharacters (string chars)
+        {
+        	foreach (char c in chars) {
+        		Add (c);
+        	}
+        	return this;
+        }
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs.meta b/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs.meta
new file mode 100644
index 0000000..f386d72
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CharacterSet.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e8d8b00ea012047a29ce7e7b4f177835
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs b/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs
new file mode 100644
index 0000000..88f1dfe
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs
@@ -0,0 +1,12 @@
+namespace Ink
+{
+    public class CommandLineInput
+    {
+        public bool isHelp;
+        public bool isExit;
+        public int? choiceInput;
+        public int? debugSource;
+        public string debugPathLookup;
+        public object userImmediateModeStatement;
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs.meta b/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs.meta
new file mode 100644
index 0000000..9382b66
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/CommandLineInput.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c05786c433ce449658bbeffe36e74ffd
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/Compiler.cs b/Assets/Ink/InkLibs/InkCompiler/Compiler.cs
new file mode 100644
index 0000000..f3e0032
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Compiler.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using Ink;
+
+namespace Ink
+{
+    public class Compiler
+    {
+        public class Options
+        {
+            public string sourceFilename;
+            public List<string> pluginNames;
+            public bool countAllVisits;
+            public Ink.ErrorHandler errorHandler;
+            public Ink.IFileHandler fileHandler;
+        }
+
+        public Parsed.Story parsedStory {
+            get {
+                return _parsedStory;
+            }
+        }
+
+        public Compiler (string inkSource, Options options = null)
+        {
+            _inputString = inkSource;
+            _options = options ?? new Options();
+            if( _options.pluginNames != null )
+                _pluginManager = new PluginManager (_options.pluginNames);
+        }
+
+        public Parsed.Story Parse()
+        {
+            _parser = new InkParser(_inputString, _options.sourceFilename, OnParseError, _options.fileHandler);
+            _parsedStory = _parser.Parse();
+            return _parsedStory;
+        }
+
+        public Runtime.Story Compile ()
+        {
+            Parse();
+
+            if( _pluginManager != null )
+                _pluginManager.PostParse(_parsedStory);
+
+            if (_parsedStory != null && !_hadParseError) {
+
+                _parsedStory.countAllVisits = _options.countAllVisits;
+
+                _runtimeStory = _parsedStory.ExportRuntime (_options.errorHandler);
+
+                if( _pluginManager != null )
+                    _pluginManager.PostExport (_parsedStory, _runtimeStory);
+            } else {
+                _runtimeStory = null;
+            }
+
+            return _runtimeStory;
+        }
+
+        public class CommandLineInputResult {
+            public bool requestsExit;
+            public int choiceIdx = -1;
+            public string divertedPath;
+            public string output;
+        }
+        public CommandLineInputResult HandleInput (CommandLineInput inputResult)
+        {
+            var result = new CommandLineInputResult ();
+
+            // Request for debug source line number
+            if (inputResult.debugSource != null) {
+                var offset = (int)inputResult.debugSource;
+                var dm = DebugMetadataForContentAtOffset (offset);
+                if (dm != null)
+                    result.output = "DebugSource: " + dm.ToString ();
+                else
+                    result.output = "DebugSource: Unknown source";
+            }
+
+            // Request for runtime path lookup (to line number)
+            else if (inputResult.debugPathLookup != null) {
+                var pathStr = inputResult.debugPathLookup;
+                var contentResult = _runtimeStory.ContentAtPath (new Runtime.Path (pathStr));
+                var dm = contentResult.obj.debugMetadata;
+                if( dm != null )
+                    result.output = "DebugSource: " + dm.ToString ();
+                else
+                    result.output = "DebugSource: Unknown source";
+            }
+
+            // User entered some ink
+            else if (inputResult.userImmediateModeStatement != null) {
+                var parsedObj = inputResult.userImmediateModeStatement as Parsed.Object;
+                return ExecuteImmediateStatement(parsedObj);
+
+            } else {
+              return null;
+            }
+
+            return result;
+        }
+
+        CommandLineInputResult ExecuteImmediateStatement(Parsed.Object parsedObj) {
+            var result = new CommandLineInputResult ();
+
+           // Variable assignment: create in Parsed.Story as well as the Runtime.Story
+           // so that we don't get an error message during reference resolution
+           if (parsedObj is Parsed.VariableAssignment) {
+               var varAssign = (Parsed.VariableAssignment)parsedObj;
+               if (varAssign.isNewTemporaryDeclaration) {
+                   _parsedStory.TryAddNewVariableDeclaration (varAssign);
+               }
+           }
+
+           parsedObj.parent = _parsedStory;
+           var runtimeObj = parsedObj.runtimeObject;
+
+           parsedObj.ResolveReferences (_parsedStory);
+
+           if (!_parsedStory.hadError) {
+
+               // Divert
+               if (parsedObj is Parsed.Divert) {
+                   var parsedDivert = parsedObj as Parsed.Divert;
+                   result.divertedPath = parsedDivert.runtimeDivert.targetPath.ToString();
+               }
+
+               // Expression or variable assignment
+               else if (parsedObj is Parsed.Expression || parsedObj is Parsed.VariableAssignment) {
+                   var evalResult = _runtimeStory.EvaluateExpression ((Runtime.Container)runtimeObj);
+                   if (evalResult != null) {
+                       result.output = evalResult.ToString ();
+                   }
+               }
+           } else {
+               _parsedStory.ResetError ();
+           }
+
+          return result;
+        }
+
+        public void RetrieveDebugSourceForLatestContent ()
+        {
+            foreach (var outputObj in _runtimeStory.state.outputStream) {
+                var textContent = outputObj as Runtime.StringValue;
+                if (textContent != null) {
+                    var range = new DebugSourceRange ();
+                    range.length = textContent.value.Length;
+                    range.debugMetadata = textContent.debugMetadata;
+                    range.text = textContent.value;
+                    _debugSourceRanges.Add (range);
+                }
+            }
+        }
+
+        Runtime.DebugMetadata DebugMetadataForContentAtOffset (int offset)
+        {
+            int currOffset = 0;
+
+            Runtime.DebugMetadata lastValidMetadata = null;
+            foreach (var range in _debugSourceRanges) {
+                if (range.debugMetadata != null)
+                    lastValidMetadata = range.debugMetadata;
+
+                if (offset >= currOffset && offset < currOffset + range.length)
+                    return lastValidMetadata;
+
+                currOffset += range.length;
+            }
+
+            return null;
+        }
+
+        public struct DebugSourceRange
+        {
+            public int length;
+            public Runtime.DebugMetadata debugMetadata;
+            public string text;
+        }
+
+        // Need to wrap the error handler so that we know
+        // when there was a critical error between parse and codegen stages
+        void OnParseError (string message, ErrorType errorType)
+        {
+            if( errorType == ErrorType.Error )
+                _hadParseError = true;
+            
+            if (_options.errorHandler != null)
+                _options.errorHandler (message, errorType);
+            else
+                throw new System.Exception(message);
+        }
+
+        string _inputString;
+        Options _options;
+
+
+        InkParser _parser;
+        Parsed.Story _parsedStory;
+        Runtime.Story _runtimeStory;
+
+        PluginManager _pluginManager;
+
+        bool _hadParseError;
+
+        List<DebugSourceRange> _debugSourceRanges = new List<DebugSourceRange> ();
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/Compiler.cs.meta b/Assets/Ink/InkLibs/InkCompiler/Compiler.cs.meta
new file mode 100644
index 0000000..5dc2dca
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Compiler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 041ad81bb22094b95bec754d7d480a82
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs b/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs
new file mode 100644
index 0000000..51a60bb
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs
@@ -0,0 +1,24 @@
+using System.IO;
+
+namespace Ink
+{
+    public interface IFileHandler
+    {
+        string ResolveInkFilename (string includeName);
+        string LoadInkFileContents (string fullFilename);
+    }
+
+    public class DefaultFileHandler : Ink.IFileHandler {
+        public string ResolveInkFilename (string includeName)
+        {
+            var workingDir = Directory.GetCurrentDirectory ();
+            var fullRootInkPath = Path.Combine (workingDir, includeName);
+            return fullRootInkPath;
+        }
+
+        public string LoadInkFileContents (string fullFilename)
+        {
+        	return File.ReadAllText (fullFilename);
+        }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs.meta b/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs.meta
new file mode 100644
index 0000000..468d20f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/FileHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: beb7e0e9816ad714290ca610ac174808
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser.meta
new file mode 100644
index 0000000..1bb30df
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9e0d4431a2a4b447e8ed4b5978110831
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs
new file mode 100644
index 0000000..9d0cfee
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs
@@ -0,0 +1,93 @@
+
+namespace Ink
+{
+    /// <summary>
+    /// Pre-pass before main ink parser runs. It actually performs two main tasks:
+    ///  - comment elimination to simplify the parse rules in the main parser
+    ///  - Conversion of Windows line endings (\r\n) to the simpler Unix style (\n), so
+    ///    we don't have to worry about them later.
+    /// </summary>
+    public class CommentEliminator : StringParser
+    {
+        public CommentEliminator (string input) : base(input)
+        {
+        }
+
+        public string Process()
+        {
+            // Make both comments and non-comments optional to handle trivial empty file case (or *only* comments)
+            var stringList = Interleave<string>(Optional (CommentsAndNewlines), Optional(MainInk));
+
+            if (stringList != null) {
+                return string.Join("", stringList.ToArray());
+            } else {
+                return null;
+            }
+        }
+
+        string MainInk()
+        {
+            return ParseUntil (CommentsAndNewlines, _commentOrNewlineStartCharacter, null);
+        }
+
+        string CommentsAndNewlines()
+        {
+            var newlines = Interleave<string> (Optional (ParseNewline), Optional (ParseSingleComment));
+
+            if (newlines != null) {
+                return string.Join ("", newlines.ToArray());
+            } else {
+                return null;
+            }
+        }
+
+        // Valid comments always return either an empty string or pure newlines,
+        // which we want to keep so that line numbers stay the same
+        string ParseSingleComment()
+        {
+            return (string) OneOf (EndOfLineComment, BlockComment);
+        }
+
+        string EndOfLineComment()
+        {
+            if (ParseString ("//") == null) {
+                return null;
+            }
+
+            ParseUntilCharactersFromCharSet (_newlineCharacters);
+
+            return "";
+        }
+
+        string BlockComment()
+        {
+            if (ParseString ("/*") == null) {
+                return null;
+            }
+
+            int startLineIndex = lineIndex;
+
+            var commentResult = ParseUntil (String("*/"), _commentBlockEndCharacter, null);
+
+            if (!endOfInput) {
+                ParseString ("*/");
+            }
+
+            // Count the number of lines that were inside the block, and replicate them as newlines
+            // so that the line indexing still works from the original source
+            if (commentResult != null) {
+                return new string ('\n', lineIndex - startLineIndex);
+            } 
+
+            // No comment at all
+            else {
+                return null;
+            }
+        }
+          
+        CharacterSet _commentOrNewlineStartCharacter = new CharacterSet ("/\r\n");
+        CharacterSet _commentBlockEndCharacter = new CharacterSet("*");
+        CharacterSet _newlineCharacters = new CharacterSet ("\n\r");
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs.meta
new file mode 100644
index 0000000..85deaa1
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/CommentEliminator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 59b3b9369a2a7481aab00ef369b7c4a1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs
new file mode 100644
index 0000000..4068edb
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Ink
+{
+    public partial class InkParser : StringParser
+    {
+        public InkParser(string str, string filenameForMetadata = null, Ink.ErrorHandler externalErrorHandler = null, IFileHandler fileHandler = null)
+            : this(str, filenameForMetadata, externalErrorHandler, null, fileHandler)
+        {  }
+
+        InkParser(string str, string inkFilename = null, Ink.ErrorHandler externalErrorHandler = null, InkParser rootParser = null, IFileHandler fileHandler = null) : base(str) {
+            _filename = inkFilename;
+            RegisterExpressionOperators ();
+            GenerateStatementLevelRules ();
+
+            // Built in handler for all standard parse errors and warnings
+            this.errorHandler = OnStringParserError;
+
+            // The above parse errors are then formatted as strings and passed
+            // to the Ink.ErrorHandler, or it throws an exception
+            _externalErrorHandler = externalErrorHandler;
+
+            _fileHandler = fileHandler ?? new DefaultFileHandler();
+
+            if (rootParser == null) {
+                _rootParser = this;
+
+                _openFilenames = new HashSet<string> ();
+
+                if (inkFilename != null) {
+                    var fullRootInkPath = _fileHandler.ResolveInkFilename (inkFilename);
+                    _openFilenames.Add (fullRootInkPath);
+                }
+
+            } else {
+                _rootParser = rootParser;
+            }
+
+        }
+
+        // Main entry point
+        public Parsed.Story Parse()
+        {
+            List<Parsed.Object> topLevelContent = StatementsAtLevel (StatementLevel.Top);
+
+            // Note we used to return null if there were any errors, but this would mean
+            // that include files would return completely empty rather than attempting to
+            // continue with errors. Returning an empty include files meant that anything
+            // that *did* compile successfully would otherwise be ignored, generating way
+            // more errors than necessary.
+            return new Parsed.Story (topLevelContent, isInclude:_rootParser != this);
+        }
+
+        protected List<T> SeparatedList<T> (SpecificParseRule<T> mainRule, ParseRule separatorRule) where T : class
+        {
+            T firstElement = Parse (mainRule);
+            if (firstElement == null) return null;
+
+            var allElements = new List<T> ();
+            allElements.Add (firstElement);
+
+            do {
+
+                int nextElementRuleId = BeginRule ();
+
+                var sep = separatorRule ();
+                if (sep == null) {
+                    FailRule (nextElementRuleId);
+                    break;
+                }
+
+                var nextElement = Parse (mainRule);
+                if (nextElement == null) {
+                    FailRule (nextElementRuleId);
+                    break;
+                }
+
+                SucceedRule (nextElementRuleId);
+
+                allElements.Add (nextElement);
+
+            } while (true);
+
+            return allElements;
+        }
+
+        protected override string PreProcessInputString(string str)
+        {
+            var inputWithCommentsRemoved = (new CommentEliminator (str)).Process();
+            return inputWithCommentsRemoved;
+        }
+
+        protected Runtime.DebugMetadata CreateDebugMetadata(StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
+        {
+            var md = new Runtime.DebugMetadata ();
+            md.startLineNumber = stateAtStart.lineIndex + 1;
+            md.endLineNumber = stateAtEnd.lineIndex + 1;
+            md.startCharacterNumber = stateAtStart.characterInLineIndex + 1;
+            md.endCharacterNumber = stateAtEnd.characterInLineIndex + 1;
+            md.fileName = _filename;
+            return md;
+        }
+
+        protected override void RuleDidSucceed(object result, StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
+        {
+            // Apply DebugMetadata based on the state at the start of the rule
+            // (i.e. use line number as it was at the start of the rule)
+            var parsedObj = result as Parsed.Object;
+            if ( parsedObj) {
+                parsedObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
+                return;
+            }
+
+            // A list of objects that doesn't already have metadata?
+            var parsedListObjs = result as List<Parsed.Object>;
+            if (parsedListObjs != null) {
+                foreach (var parsedListObj in parsedListObjs) {
+                    if (!parsedListObj.hasOwnDebugMetadata) {
+                        parsedListObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
+                    }
+                }
+            }
+
+            var id = result as Parsed.Identifier;
+            if (id != null) {
+                id.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
+            }
+        }
+
+        protected bool parsingStringExpression
+        {
+            get {
+                return GetFlag ((uint)CustomFlags.ParsingString);
+            }
+            set {
+                SetFlag ((uint)CustomFlags.ParsingString, value);
+            }
+        }
+
+        protected enum CustomFlags {
+            ParsingString = 0x1
+        }
+
+        void OnStringParserError(string message, int index, int lineIndex, bool isWarning)
+        {
+            var warningType = isWarning ? "WARNING:" : "ERROR:";
+            string fullMessage;
+
+            if (_filename != null) {
+                fullMessage = string.Format(warningType+" '{0}' line {1}: {2}",  _filename, (lineIndex+1), message);
+            } else {
+                fullMessage = string.Format(warningType+" line {0}: {1}", (lineIndex+1), message);
+            }
+
+            if (_externalErrorHandler != null) {
+                _externalErrorHandler (fullMessage, isWarning ? ErrorType.Warning : ErrorType.Error);
+            } else {
+                throw new System.Exception (fullMessage);
+            }
+        }
+
+        IFileHandler _fileHandler;
+
+        Ink.ErrorHandler _externalErrorHandler;
+
+        string _filename;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs.meta
new file mode 100644
index 0000000..a6d7fbe
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 97d6f86dbb9994db6bd0572b69f53f25
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs
new file mode 100644
index 0000000..6e73e59
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs
@@ -0,0 +1,28 @@
+using Ink.Parsed;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected AuthorWarning AuthorWarning()
+        {
+            Whitespace ();
+
+            var identifier = Parse (IdentifierWithMetadata);
+            if (identifier == null || identifier.name != "TODO")
+                return null;
+
+            Whitespace ();
+
+            ParseString (":");
+
+            Whitespace ();
+
+            var message = ParseUntilCharactersFromString ("\n\r");
+
+            return new AuthorWarning (message);
+        }
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs.meta
new file mode 100644
index 0000000..acd2d60
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_AuthorWarning.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5a2f15af905c94ac9bdfb197799eec41
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs
new file mode 100644
index 0000000..3ac3962
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs
@@ -0,0 +1,64 @@
+using Ink.Parsed;
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+		public static readonly CharacterRange LatinBasic = 
+			CharacterRange.Define ('\u0041', '\u007A', excludes: new CharacterSet().AddRange('\u005B', '\u0060'));
+		public static readonly CharacterRange LatinExtendedA = CharacterRange.Define('\u0100', '\u017F'); // no excludes here
+		public static readonly CharacterRange LatinExtendedB = CharacterRange.Define('\u0180', '\u024F'); // no excludes here
+		public static readonly CharacterRange Greek = 
+			CharacterRange.Define('\u0370', '\u03FF', excludes: new CharacterSet().AddRange('\u0378','\u0385').AddCharacters("\u0374\u0375\u0378\u0387\u038B\u038D\u03A2"));
+		public static readonly CharacterRange Cyrillic = 
+			CharacterRange.Define('\u0400', '\u04FF', excludes: new CharacterSet().AddRange('\u0482', '\u0489'));
+		public static readonly CharacterRange Armenian = 
+			CharacterRange.Define('\u0530', '\u058F', excludes: new CharacterSet().AddCharacters("\u0530").AddRange('\u0557', '\u0560').AddRange('\u0588', '\u058E'));
+		public static readonly CharacterRange Hebrew = 
+			CharacterRange.Define('\u0590', '\u05FF', excludes: new CharacterSet());
+		public static readonly CharacterRange Arabic = 
+			CharacterRange.Define('\u0600', '\u06FF', excludes: new CharacterSet());
+		public static readonly CharacterRange Korean =
+			CharacterRange.Define('\uAC00', '\uD7AF', excludes: new CharacterSet());
+	    	public static readonly CharacterRange Latin1Supplement =
+			CharacterRange.Define('\u0080', '\u00FF', excludes: new CharacterSet());
+
+        private void ExtendIdentifierCharacterRanges(CharacterSet identifierCharSet)
+        {
+            var characterRanges = ListAllCharacterRanges();
+
+            foreach (var charRange in characterRanges)
+            {
+                identifierCharSet.AddCharacters(charRange.ToCharacterSet());
+            }
+        }
+
+        /// <summary>
+        /// Gets an array of <see cref="CharacterRange" /> representing all of the currently supported
+        /// non-ASCII character ranges that can be used in identifier names.
+        /// </summary>
+        /// <returns>
+        /// An array of <see cref="CharacterRange" /> representing all of the currently supported
+        /// non-ASCII character ranges that can be used in identifier names.
+        /// </returns>
+        public static CharacterRange[] ListAllCharacterRanges() {
+            return new CharacterRange[] {
+                LatinBasic,
+                LatinExtendedA,
+                LatinExtendedB,
+                Arabic,
+                Armenian,
+                Cyrillic,
+                Greek,
+                Hebrew,
+                Korean,
+		Latin1Supplement,
+            };
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs.meta
new file mode 100644
index 0000000..654a582
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CharacterRanges.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2383f2e1b977347c2b6fb596e9af6d97
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs
new file mode 100644
index 0000000..39c148a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs
@@ -0,0 +1,225 @@
+using Ink.Parsed;
+using System.Diagnostics;
+
+namespace Ink
+{
+	public partial class InkParser
+	{
+		protected Choice Choice()
+		{
+            bool onceOnlyChoice = true;
+            var bullets = Interleave <string>(OptionalExclude(Whitespace), String("*") );
+            if (bullets == null) {
+
+                bullets = Interleave <string>(OptionalExclude(Whitespace), String("+") );
+                if (bullets == null) {
+                    return null;
+                }
+
+                onceOnlyChoice = false;
+            }
+
+            // Optional name for the choice
+            Identifier optionalName = Parse(BracketedName);
+
+            Whitespace ();
+
+            // Optional condition for whether the choice should be shown to the player
+            Expression conditionExpr = Parse(ChoiceCondition);
+
+            Whitespace ();
+
+            // Ordinarily we avoid parser state variables like these, since
+            // nesting would require us to store them in a stack. But since you should
+            // never be able to nest choices within choice content, it's fine here.
+            Debug.Assert(_parsingChoice == false, "Already parsing a choice - shouldn't have nested choices");
+            _parsingChoice = true;
+
+            ContentList startContent = null;
+            var startTextAndLogic = Parse (MixedTextAndLogic);
+            if (startTextAndLogic != null)
+                startContent = new ContentList (startTextAndLogic);
+
+
+            ContentList optionOnlyContent = null;
+            ContentList innerContent = null;
+
+            // Check for a the weave style format:
+            //   * "Hello[."]," he said.
+            bool hasWeaveStyleInlineBrackets = ParseString("[") != null;
+            if (hasWeaveStyleInlineBrackets) {
+
+                var optionOnlyTextAndLogic = Parse (MixedTextAndLogic);
+                if (optionOnlyTextAndLogic != null)
+                    optionOnlyContent = new ContentList (optionOnlyTextAndLogic);
+
+
+                Expect (String("]"), "closing ']' for weave-style option");
+
+                var innerTextAndLogic = Parse (MixedTextAndLogic);
+                if( innerTextAndLogic != null )
+                    innerContent = new ContentList (innerTextAndLogic);
+            }
+
+			Whitespace ();
+
+            // Finally, now we know we're at the end of the main choice body, parse
+            // any diverts separately.
+            var diverts =  Parse(MultiDivert);
+
+            _parsingChoice = false;
+
+            Whitespace ();
+
+            // Completely empty choice without even an empty divert?
+            bool emptyContent = !startContent && !innerContent && !optionOnlyContent;
+            if (emptyContent && diverts == null)
+                Warning ("Choice is completely empty. Interpretting as a default fallback choice. Add a divert arrow to remove this warning: * ->");
+
+            // * [] some text
+            else if (!startContent && hasWeaveStyleInlineBrackets && !optionOnlyContent)
+                Warning ("Blank choice - if you intended a default fallback choice, use the `* ->` syntax");
+
+            if (!innerContent) innerContent = new ContentList ();
+
+            var tags = Parse (Tags);
+            if (tags != null) {
+                innerContent.AddContent(tags);
+            }
+
+            // Normal diverts on the end of a choice - simply add to the normal content
+            if (diverts != null) {
+                foreach (var divObj in diverts) {
+                    // may be TunnelOnwards
+                    var div = divObj as Divert;
+
+                    // Empty divert serves no purpose other than to say
+                    // "this choice is intentionally left blank"
+                    // (as an invisible default choice)
+                    if (div && div.isEmpty) continue;
+
+                    innerContent.AddContent (divObj);
+                }
+            }
+
+            // Terminate main content with a newline since this is the end of the line
+            // Note that this will be redundant if the diverts above definitely take
+            // the flow away permanently.
+            innerContent.AddContent (new Text ("\n"));
+
+            var choice = new Choice (startContent, optionOnlyContent, innerContent);
+            choice.identifier = optionalName;
+            choice.indentationDepth = bullets.Count;
+            choice.hasWeaveStyleInlineBrackets = hasWeaveStyleInlineBrackets;
+            choice.condition = conditionExpr;
+            choice.onceOnly = onceOnlyChoice;
+            choice.isInvisibleDefault = emptyContent;
+
+            return choice;
+
+		}
+
+        protected Expression ChoiceCondition()
+        {
+            var conditions = Interleave<Expression> (ChoiceSingleCondition, ChoiceConditionsSpace);
+            if (conditions == null)
+                return null;
+            else if (conditions.Count == 1)
+                return conditions [0];
+            else {
+                return new MultipleConditionExpression (conditions);
+            }
+        }
+
+        protected object ChoiceConditionsSpace()
+        {
+            // Both optional
+            // Newline includes initial end of line whitespace
+            Newline ();
+            Whitespace ();
+            return ParseSuccess;
+        }
+
+        protected Expression ChoiceSingleCondition()
+        {
+            if (ParseString ("{") == null)
+                return null;
+
+            var condExpr = Expect(Expression, "choice condition inside { }") as Expression;
+            DisallowIncrement (condExpr);
+
+            Expect (String ("}"), "closing '}' for choice condition");
+
+            return condExpr;
+        }
+
+        protected Gather Gather()
+        {
+            object gatherDashCountObj = Parse(GatherDashes);
+            if (gatherDashCountObj == null) {
+                return null;
+            }
+
+            int gatherDashCount = (int)gatherDashCountObj;
+
+            // Optional name for the gather
+            Identifier optionalName = Parse(BracketedName);
+
+            var gather = new Gather (optionalName, gatherDashCount);
+
+            // Optional newline before gather's content begins
+            Newline ();
+
+            return gather;
+        }
+
+        protected object GatherDashes()
+        {
+            Whitespace ();
+
+            int gatherDashCount = 0;
+
+            while (ParseDashNotArrow () != null) {
+                gatherDashCount++;
+                Whitespace ();
+            }
+
+            if (gatherDashCount == 0)
+                return null;
+
+            return gatherDashCount;
+        }
+
+        protected object ParseDashNotArrow()
+        {
+            var ruleId = BeginRule ();
+
+            if (ParseString ("->") == null && ParseSingleCharacter () == '-') {
+                return SucceedRule (ruleId);
+            } else {
+                return FailRule (ruleId);
+            }
+        }
+
+        protected Identifier BracketedName()
+        {
+            if (ParseString ("(") == null)
+                return null;
+
+            Whitespace ();
+
+            Identifier name = Parse(IdentifierWithMetadata);
+            if (name == null)
+                return null;
+
+            Whitespace ();
+
+            Expect (String (")"), "closing ')' for bracketed name");
+
+            return name;
+        }
+
+        bool _parsingChoice;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs.meta
new file mode 100644
index 0000000..62f009b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Choices.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ea21bfef8977a4907b49628d9b651ebf
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs
new file mode 100644
index 0000000..a04ef17
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs
@@ -0,0 +1,129 @@
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        // Valid returned objects:
+        //  - "help"
+        //  - int: for choice number
+        //  - Parsed.Divert
+        //  - Variable declaration/assignment
+        //  - Epression
+        //  - Lookup debug source for character offset
+        //  - Lookup debug source for runtime path
+        public CommandLineInput CommandLineUserInput()
+        {
+            CommandLineInput result = new CommandLineInput ();
+
+            Whitespace ();
+
+            if (ParseString ("help") != null) {
+                result.isHelp = true;
+                return result;
+            }
+
+            if (ParseString ("exit") != null || ParseString ("quit") != null) {
+                result.isExit = true;
+                return result;
+            }
+
+            return (CommandLineInput) OneOf (
+                DebugSource,
+                DebugPathLookup,
+                UserChoiceNumber, 
+                UserImmediateModeStatement
+            );
+        }
+
+        CommandLineInput DebugSource ()
+        {
+            Whitespace ();
+
+            if (ParseString ("DebugSource") == null)
+                return null;
+
+            Whitespace ();
+
+            var expectMsg = "character offset in parentheses, e.g. DebugSource(5)";
+            if (Expect (String ("("), expectMsg) == null)
+                return null;
+
+            Whitespace ();
+
+            int? characterOffset = ParseInt ();
+            if (characterOffset == null) {
+                Error (expectMsg);
+                return null;
+            }
+
+            Whitespace ();
+
+            Expect (String (")"), "closing parenthesis");
+
+            var inputStruct = new CommandLineInput ();
+            inputStruct.debugSource = characterOffset;
+            return inputStruct;
+        }
+
+        CommandLineInput DebugPathLookup ()
+        {
+            Whitespace ();
+
+            if (ParseString ("DebugPath") == null)
+                return null;
+
+            if (Whitespace () == null)
+                return null;
+
+            var pathStr = Expect (RuntimePath, "path") as string;
+
+            var inputStruct = new CommandLineInput ();
+            inputStruct.debugPathLookup = pathStr;
+            return inputStruct;
+        }
+
+        string RuntimePath ()
+        {
+            if (_runtimePathCharacterSet == null) {
+                _runtimePathCharacterSet = new CharacterSet (identifierCharSet);
+                _runtimePathCharacterSet.Add ('-'); // for c-0, g-0 etc
+                _runtimePathCharacterSet.Add ('.');
+
+            }
+            
+            return ParseCharactersFromCharSet (_runtimePathCharacterSet);
+        }
+
+        CommandLineInput UserChoiceNumber()
+        {
+            Whitespace ();
+
+            int? number = ParseInt ();
+            if (number == null) {
+                return null;
+            }
+
+            Whitespace ();
+
+            if (Parse(EndOfLine) == null) {
+                return null;
+            }
+
+            var inputStruct = new CommandLineInput ();
+            inputStruct.choiceInput = number;
+            return inputStruct;
+        }
+
+        CommandLineInput UserImmediateModeStatement()
+        {
+            var statement = OneOf (SingleDivert, TempDeclarationOrAssignment, Expression);
+
+            var inputStruct = new CommandLineInput ();
+            inputStruct.userImmediateModeStatement = statement;
+            return inputStruct;
+        }
+
+        CharacterSet _runtimePathCharacterSet;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs.meta
new file mode 100644
index 0000000..830f889
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_CommandLineInput.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5ca27cda0c9364bfbaf37f8db278563f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs
new file mode 100644
index 0000000..c1f60ec
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs
@@ -0,0 +1,288 @@
+using System.Collections.Generic;
+using System.Linq;
+using Ink.Parsed;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected Conditional InnerConditionalContent()
+        {
+            var initialQueryExpression = Parse(ConditionExpression);
+            var conditional = Parse(() => InnerConditionalContent (initialQueryExpression));
+            if (conditional == null)
+                return null;
+
+            return conditional;
+        }
+
+        protected Conditional InnerConditionalContent(Expression initialQueryExpression)
+        {
+            List<ConditionalSingleBranch> alternatives;
+
+            bool canBeInline = initialQueryExpression != null;
+            bool isInline = Parse(Newline) == null;
+
+            if (isInline && !canBeInline) {
+                return null;
+            }
+
+            // Inline innards
+            if (isInline) {
+                alternatives = InlineConditionalBranches ();
+            } 
+
+            // Multiline innards
+            else {
+                alternatives = MultilineConditionalBranches ();
+                if (alternatives == null) {
+
+                    // Allow single piece of content within multi-line expression, e.g.:
+                    // { true: 
+                    //    Some content that isn't preceded by '-'
+                    // }
+                    if (initialQueryExpression) {
+                        List<Parsed.Object> soleContent = StatementsAtLevel (StatementLevel.InnerBlock);
+                        if (soleContent != null) {
+                            var soleBranch = new ConditionalSingleBranch (soleContent);
+                            alternatives = new List<ConditionalSingleBranch> ();
+                            alternatives.Add (soleBranch);
+
+                            // Also allow a final "- else:" clause
+                            var elseBranch = Parse (SingleMultilineCondition);
+                            if (elseBranch) {
+                                if (!elseBranch.isElse) {
+                                    ErrorWithParsedObject ("Expected an '- else:' clause here rather than an extra condition", elseBranch);
+                                    elseBranch.isElse = true;
+                                }
+                                alternatives.Add (elseBranch);
+                            }
+                        }
+                    }
+
+                    // Still null?
+                    if (alternatives == null) {
+                        return null;
+                    }
+                } 
+
+                // Empty true branch - didn't get parsed, but should insert one for semantic correctness,
+                // and to make sure that any evaluation stack values get tidied up correctly.
+                else if (alternatives.Count == 1 && alternatives [0].isElse && initialQueryExpression) {
+                    var emptyTrueBranch = new ConditionalSingleBranch (null);
+                    emptyTrueBranch.isTrueBranch = true;
+                    alternatives.Insert (0, emptyTrueBranch);
+                }
+
+                // Like a switch statement
+                // { initialQueryExpression:
+                //    ... match the expression
+                // }
+                if (initialQueryExpression) {
+
+                    bool earlierBranchesHaveOwnExpression = false;
+                    for (int i = 0; i < alternatives.Count; ++i) {
+                        var branch = alternatives [i];
+                        bool isLast = (i == alternatives.Count - 1);
+
+                        // Matching equality with initial query expression
+                        // We set this flag even for the "else" clause so that
+                        // it knows to tidy up the evaluation stack at the end
+
+                        // Match query
+                        if (branch.ownExpression) {
+                            branch.matchingEquality = true;
+                            earlierBranchesHaveOwnExpression = true;
+                        }
+
+                        // Else (final branch)
+                        else if (earlierBranchesHaveOwnExpression && isLast) {
+                            branch.matchingEquality = true;
+                            branch.isElse = true;
+                        } 
+
+                        // Binary condition:
+                        // { trueOrFalse:
+                        //    - when true
+                        //    - when false
+                        // }
+                        else {
+
+                            if (!isLast && alternatives.Count > 2) {
+                                ErrorWithParsedObject ("Only final branch can be an 'else'. Did you miss a ':'?", branch);
+                            } else {
+                                if (i == 0)
+                                    branch.isTrueBranch = true;
+                                else
+                                    branch.isElse = true;
+                            }
+                        }
+                    }
+                } 
+
+                // No initial query, so just a multi-line conditional. e.g.:
+                // {
+                //   - x > 3:  greater than three
+                //   - x == 3: equal to three
+                //   - x < 3:  less than three
+                // }
+                else {
+                    
+                    for (int i = 0; i < alternatives.Count; ++i) {
+                        var alt = alternatives [i];
+                        bool isLast = (i == alternatives.Count - 1);
+                        if (alt.ownExpression == null) {
+                            if (isLast) {
+                                alt.isElse = true;
+                            } else {
+                                if (alt.isElse) {
+                                    // Do we ALSO have a valid "else" at the end? Let's report the error there.
+                                    var finalClause = alternatives [alternatives.Count - 1];
+                                    if (finalClause.isElse) {
+                                        ErrorWithParsedObject ("Multiple 'else' cases. Can have a maximum of one, at the end.", finalClause);
+                                    } else {
+                                        ErrorWithParsedObject ("'else' case in conditional should always be the final one", alt);
+                                    }
+                                } else {
+                                    ErrorWithParsedObject ("Branch doesn't have condition. Are you missing a ':'? ", alt);
+                                }
+
+                            }
+                        }
+                    }
+                        
+                    if (alternatives.Count == 1 && alternatives [0].ownExpression == null) {
+                        ErrorWithParsedObject ("Condition block with no conditions", alternatives [0]);
+                    }
+                }
+            }
+
+            // TODO: Come up with water-tight error conditions... it's quite a flexible system!
+            // e.g.
+            //   - inline conditionals must have exactly 1 or 2 alternatives
+            //   - multiline expression shouldn't have mixed existence of branch-conditions?
+            if (alternatives == null)
+                return null;
+
+            foreach (var branch in alternatives) {
+                branch.isInline = isInline;
+            }
+
+            var cond = new Conditional (initialQueryExpression, alternatives);
+            return cond;
+        }
+
+        protected List<ConditionalSingleBranch> InlineConditionalBranches()
+        {
+            var listOfLists = Interleave<List<Parsed.Object>> (MixedTextAndLogic, Exclude (String ("|")), flatten: false);
+            if (listOfLists == null || listOfLists.Count == 0) {
+                return null;
+            }
+
+            var result = new List<ConditionalSingleBranch> ();
+
+            if (listOfLists.Count > 2) {
+                Error ("Expected one or two alternatives separated by '|' in inline conditional");
+            } else {
+                
+                var trueBranch = new ConditionalSingleBranch (listOfLists[0]);
+                trueBranch.isTrueBranch = true;
+                result.Add (trueBranch);
+
+                if (listOfLists.Count > 1) {
+                    var elseBranch = new ConditionalSingleBranch (listOfLists[1]);
+                    elseBranch.isElse = true;
+                    result.Add (elseBranch);
+                }
+            }
+
+            return result;
+        }
+
+        protected List<ConditionalSingleBranch> MultilineConditionalBranches()
+        {
+            MultilineWhitespace ();
+
+            List<object> multipleConditions = OneOrMore (SingleMultilineCondition);
+            if (multipleConditions == null)
+                return null;
+            
+            MultilineWhitespace ();
+
+            return multipleConditions.Cast<ConditionalSingleBranch>().ToList();
+        }
+
+        protected ConditionalSingleBranch SingleMultilineCondition()
+        {
+            Whitespace ();
+
+            // Make sure we're not accidentally parsing a divert
+            if (ParseString ("->") != null)
+                return null;
+
+            if (ParseString ("-") == null)
+                return null;
+
+            Whitespace ();
+
+            Expression expr = null;
+            bool isElse = Parse(ElseExpression) != null;
+
+            if( !isElse )
+                expr = Parse(ConditionExpression);
+
+            List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
+            if (expr == null && content == null) {
+                Error ("expected content for the conditional branch following '-'");
+
+                // Recover
+                content = new List<Ink.Parsed.Object> ();
+                content.Add (new Text (""));
+            }
+
+            // Allow additional multiline whitespace, if the statements were empty (valid)
+            // then their surrounding multiline whitespacce needs to be handled manually.
+            // e.g.
+            // { x:
+            //   - 1:    // intentionally left blank, but newline needs to be parsed
+            //   - 2: etc
+            // }
+            MultilineWhitespace ();
+
+            var branch = new ConditionalSingleBranch (content);
+            branch.ownExpression = expr;
+            branch.isElse = isElse;
+            return branch;
+        }
+
+        protected Expression ConditionExpression()
+        {
+            var expr = Parse(Expression);
+            if (expr == null)
+                return null;
+
+            DisallowIncrement (expr);
+
+            Whitespace ();
+
+            if (ParseString (":") == null)
+                return null;
+
+            return expr;
+        }
+
+        protected object ElseExpression()
+        {
+            if (ParseString ("else") == null)
+                return null;
+
+            Whitespace ();
+
+            if (ParseString (":") == null)
+                return null;
+
+            return ParseSuccess;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs.meta
new file mode 100644
index 0000000..1b22271
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Conditional.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 334d80c537ee2473ea6a7cbd20e09f14
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs
new file mode 100644
index 0000000..e1bc7ab
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs
@@ -0,0 +1,217 @@
+using Ink.Parsed;
+using System.Text;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        void TrimEndWhitespace(List<Parsed.Object> mixedTextAndLogicResults, bool terminateWithSpace)
+        {
+            // Trim whitespace from end
+            if (mixedTextAndLogicResults.Count > 0) {
+                var lastObjIdx = mixedTextAndLogicResults.Count - 1;
+                var lastObj = mixedTextAndLogicResults[lastObjIdx];
+                if (lastObj is Text) {
+                    var text = (Text)lastObj;
+                    text.text = text.text.TrimEnd (' ', '\t');
+
+                    if (terminateWithSpace)
+                        text.text += " ";
+
+                    // No content left at all? trim the whole object
+                    else if( text.text.Length == 0 ) {
+                        mixedTextAndLogicResults.RemoveAt(lastObjIdx);
+
+                        // Recurse in case there's more whitespace
+                        TrimEndWhitespace(mixedTextAndLogicResults, terminateWithSpace:false);
+                    }
+                }
+            }
+        }
+
+        protected List<Parsed.Object> LineOfMixedTextAndLogic()
+        {
+            // Consume any whitespace at the start of the line
+            // (Except for escaped whitespace)
+            Parse (Whitespace);
+
+            var result = Parse(MixedTextAndLogic);
+
+            // Terminating tag
+            bool onlyTags = false;
+            var tags = Parse (Tags);
+            if (tags != null) {
+                if (result == null) {
+                    result = tags.Cast<Parsed.Object> ().ToList ();
+                    onlyTags = true;
+                } else {
+                    foreach (var tag in tags) {
+                        result.Add(tag);
+                    }
+                }
+            }
+
+            if (result == null || result.Count == 0)
+                return null;
+
+            // Warn about accidentally writing "return" without "~"
+            var firstText = result[0] as Text;
+            if (firstText) {
+                if (firstText.text.StartsWith ("return")) {
+                    Warning ("Do you need a '~' before 'return'? If not, perhaps use a glue: <> (since it's lowercase) or rewrite somehow?");
+                }
+            }
+            if (result.Count == 0)
+                return null;
+
+            var lastObj = result [result.Count - 1];
+            if (!(lastObj is Divert)) {
+                TrimEndWhitespace (result, terminateWithSpace:false);
+            }
+
+            // Add newline since it's the end of the line
+            // (so long as it's a line with only tags)
+            if( !onlyTags )
+                result.Add (new Text ("\n"));
+
+            Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
+
+            return result;
+        }
+
+        protected List<Parsed.Object> MixedTextAndLogic()
+        {
+            // Check for disallowed "~" within this context
+            var disallowedTilda = ParseObject(Spaced(String("~")));
+            if (disallowedTilda != null)
+                Error ("You shouldn't use a '~' here - tildas are for logic that's on its own line. To do inline logic, use { curly braces } instead");
+
+            // Either, or both interleaved
+            var results = Interleave<Parsed.Object>(Optional (ContentText), Optional (InlineLogicOrGlue));
+
+            // Terminating divert?
+            // (When parsing content for the text of a choice, diverts aren't allowed.
+            //  The divert on the end of the body of a choice is handled specially.)
+            if (!_parsingChoice) {
+
+                var diverts = Parse (MultiDivert);
+                if (diverts != null) {
+
+                    // May not have had any results at all if there's *only* a divert!
+                    if (results == null)
+                        results = new List<Parsed.Object> ();
+
+                    TrimEndWhitespace (results, terminateWithSpace:true);
+
+                    results.AddRange (diverts);
+                }
+
+            }
+                
+            if (results == null)
+                return null;
+
+            return results;
+        }
+
+        protected Parsed.Text ContentText()
+        {
+            return ContentTextAllowingEcapeChar ();
+        }
+
+        protected Parsed.Text ContentTextAllowingEcapeChar()
+        {
+            StringBuilder sb = null;
+
+            do {
+                var str = Parse(ContentTextNoEscape);
+                bool gotEscapeChar = ParseString(@"\") != null;
+
+                if( gotEscapeChar || str != null ) {
+                    if( sb == null ) {
+                        sb = new StringBuilder();
+                    }
+
+                    if( str != null ) {
+                        sb.Append(str);
+                    }
+
+                    if( gotEscapeChar ) {
+                        char c = ParseSingleCharacter();
+                        sb.Append(c);
+                    }
+
+                } else {
+                    break;
+                }
+
+            } while(true);
+
+            if (sb != null ) {
+                return new Parsed.Text (sb.ToString ());
+
+            } else {
+                return null;
+            }
+        }
+
+        // Content text is an unusual parse rule compared with most since it's
+        // less about saying "this is is the small selection of stuff that we parse"
+        // and more "we parse ANYTHING except this small selection of stuff".
+        protected string ContentTextNoEscape()
+        {
+            // Eat through text, pausing at the following characters, and
+            // attempt to parse the nonTextRule.
+            // "-": possible start of divert or start of gather
+            // "<": possible start of glue
+            if (_nonTextPauseCharacters == null) {
+                _nonTextPauseCharacters = new CharacterSet ("-<");
+            }
+
+            // If we hit any of these characters, we stop *immediately* without bothering to even check the nonTextRule
+            // "{" for start of logic
+            // "|" for mid logic branch
+            if (_nonTextEndCharacters == null) {
+                _nonTextEndCharacters = new CharacterSet ("{}|\n\r\\#");
+                _notTextEndCharactersChoice = new CharacterSet (_nonTextEndCharacters);
+                _notTextEndCharactersChoice.AddCharacters ("[]");
+                _notTextEndCharactersString = new CharacterSet (_nonTextEndCharacters);
+                _notTextEndCharactersString.AddCharacters ("\"");
+            }
+
+            // When the ParseUntil pauses, check these rules in case they evaluate successfully
+            ParseRule nonTextRule = () => OneOf (ParseDivertArrow, ParseThreadArrow, EndOfLine, Glue);
+
+            CharacterSet endChars = null;
+            if (parsingStringExpression) {
+                endChars = _notTextEndCharactersString;
+            } 
+            else if (_parsingChoice) {
+                endChars = _notTextEndCharactersChoice;
+            } 
+            else {
+                endChars = _nonTextEndCharacters;
+            }
+
+            string pureTextContent = ParseUntil (nonTextRule, _nonTextPauseCharacters, endChars);
+            if (pureTextContent != null ) {
+                return pureTextContent;
+
+            } else {
+                return null;
+            }
+
+        }
+
+        CharacterSet _nonTextPauseCharacters;
+        CharacterSet _nonTextEndCharacters;
+        CharacterSet _notTextEndCharactersChoice;
+        CharacterSet _notTextEndCharactersString;
+
+
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs.meta
new file mode 100644
index 0000000..c0489c0
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Content.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8af10c051a6c942b1a43cb31ceb18114
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs
new file mode 100644
index 0000000..079d85a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs
@@ -0,0 +1,206 @@
+using System.Collections.Generic;
+using Ink.Parsed;
+
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected List<Parsed.Object> MultiDivert()
+        {
+            Whitespace ();
+
+            List<Parsed.Object> diverts = null;
+
+            // Try single thread first
+            var threadDivert = Parse(StartThread);
+            if (threadDivert) {
+                diverts = new List<Object> ();
+                diverts.Add (threadDivert);
+                return diverts;
+            }
+
+            // Normal diverts and tunnels
+            var arrowsAndDiverts = Interleave<object> (
+                ParseDivertArrowOrTunnelOnwards,
+                DivertIdentifierWithArguments);
+
+            if (arrowsAndDiverts == null)
+                return null;
+
+            diverts = new List<Parsed.Object> ();
+
+            // Possible patterns:
+            //  ->                   -- explicit gather
+            //  ->->                 -- tunnel onwards
+            //  -> div               -- normal divert
+            //  ->-> div             -- tunnel onwards, followed by override divert
+            //  -> div ->            -- normal tunnel
+            //  -> div ->->          -- tunnel then tunnel continue
+            //  -> div -> div        -- tunnel then divert
+            //  -> div -> div ->     -- tunnel then tunnel
+            //  -> div -> div ->->
+            //  -> div -> div ->-> div    (etc)
+
+            // Look at the arrows and diverts
+            for (int i = 0; i < arrowsAndDiverts.Count; ++i) {
+                bool isArrow = (i % 2) == 0;
+
+                // Arrow string
+                if (isArrow) {
+
+                    // Tunnel onwards
+                    if ((string)arrowsAndDiverts [i] == "->->") {
+
+                        bool tunnelOnwardsPlacementValid = (i == 0 || i == arrowsAndDiverts.Count - 1 || i == arrowsAndDiverts.Count - 2);
+                        if (!tunnelOnwardsPlacementValid)
+                            Error ("Tunnel onwards '->->' must only come at the begining or the start of a divert");
+
+                        var tunnelOnwards = new TunnelOnwards ();
+                        if (i < arrowsAndDiverts.Count - 1) {
+                            var tunnelOnwardDivert = arrowsAndDiverts [i+1] as Parsed.Divert;
+                            tunnelOnwards.divertAfter = tunnelOnwardDivert;
+                        }
+
+                        diverts.Add (tunnelOnwards);
+
+                        // Not allowed to do anything after a tunnel onwards.
+                        // If we had anything left it would be caused in the above Error for
+                        // the positioning of a ->->
+                        break;
+                    }
+                }
+
+                // Divert
+                else {
+
+                    var divert = arrowsAndDiverts [i] as Divert;
+
+                    // More to come? (further arrows) Must be tunnelling.
+                    if (i < arrowsAndDiverts.Count - 1) {
+                        divert.isTunnel = true;
+                    }
+
+                    diverts.Add (divert);
+                }
+            }
+
+            // Single -> (used for default choices)
+            if (diverts.Count == 0 && arrowsAndDiverts.Count == 1) {
+                var gatherDivert = new Divert ((Parsed.Object)null);
+                gatherDivert.isEmpty = true;
+                diverts.Add (gatherDivert);
+
+                if (!_parsingChoice)
+                    Error ("Empty diverts (->) are only valid on choices");
+            }
+
+            return diverts;
+        }
+
+        protected Divert StartThread()
+        {
+            Whitespace ();
+
+            if (ParseThreadArrow() == null)
+                return null;
+
+            Whitespace ();
+
+            var divert = Expect(DivertIdentifierWithArguments, "target for new thread", () => new Divert(null)) as Divert;
+            divert.isThread = true;
+
+            return divert;
+        }
+
+        protected Divert DivertIdentifierWithArguments()
+        {
+            Whitespace ();
+
+            List<Identifier> targetComponents = Parse (DotSeparatedDivertPathComponents);
+            if (targetComponents == null)
+                return null;
+
+            Whitespace ();
+
+            var optionalArguments = Parse(ExpressionFunctionCallArguments);
+
+            Whitespace ();
+
+            var targetPath = new Path (targetComponents);
+            return new Divert (targetPath, optionalArguments);
+        }
+
+        protected Divert SingleDivert()
+        {
+            var diverts = Parse (MultiDivert);
+            if (diverts == null)
+                return null;
+
+            // Ideally we'd report errors if we get the
+            // wrong kind of divert, but unfortunately we
+            // have to hack around the fact that sequences use
+            // a very similar syntax.
+            // i.e. if you have a multi-divert at the start
+            // of a sequence, it initially tries to parse it
+            // as a divert target (part of an expression of
+            // a conditional) and gives errors. So instead
+            // we just have to blindly reject it as a single
+            // divert, and give a slightly less nice error
+            // when you DO use a multi divert as a divert taret.
+
+            if (diverts.Count != 1) {
+                return null;
+            }
+
+            var singleDivert = diverts [0];
+            if (singleDivert is TunnelOnwards) {
+                return null;
+            }
+
+            var divert = diverts [0] as Divert;
+            if (divert.isTunnel) {
+                return null;
+            }
+
+            return divert;
+        }
+
+        List<Identifier> DotSeparatedDivertPathComponents()
+        {
+            return Interleave<Identifier> (Spaced (IdentifierWithMetadata), Exclude (String (".")));
+        }
+
+        protected string ParseDivertArrowOrTunnelOnwards()
+        {
+            int numArrows = 0;
+            while (ParseString ("->") != null)
+                numArrows++;
+
+            if (numArrows == 0)
+                return null;
+
+            else if (numArrows == 1)
+                return "->";
+
+            else if (numArrows == 2)
+                return "->->";
+
+            else {
+                Error ("Unexpected number of arrows in divert. Should only have '->' or '->->'");
+                return "->->";
+            }
+        }
+
+        protected string ParseDivertArrow()
+        {
+            return ParseString ("->");
+        }
+
+        protected string ParseThreadArrow()
+        {
+            return ParseString ("<-");
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs.meta
new file mode 100644
index 0000000..19d2dfc
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Divert.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7aec7c9fb87244b3a8c64753c8a2f9b7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs
new file mode 100644
index 0000000..106f895
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs
@@ -0,0 +1,512 @@
+using System;
+using Ink.Parsed;
+using System.Collections.Generic;
+
+namespace Ink
+{
+	public partial class InkParser
+	{
+		protected class InfixOperator
+		{
+			public string type;
+			public int precedence;
+            public bool requireWhitespace;
+
+            public InfixOperator(string type, int precedence, bool requireWhitespace) {
+				this.type = type;
+				this.precedence = precedence;
+                this.requireWhitespace = requireWhitespace;
+			}
+
+			public override string ToString ()
+			{
+				return type;
+			}
+		}
+
+        protected Parsed.Object TempDeclarationOrAssignment()
+        {
+            Whitespace ();
+
+            bool isNewDeclaration = ParseTempKeyword();
+
+            Whitespace ();
+
+            Identifier varIdentifier = null;
+            if (isNewDeclaration) {
+                varIdentifier = (Identifier)Expect (IdentifierWithMetadata, "variable name");
+            } else {
+                varIdentifier = Parse(IdentifierWithMetadata);
+            }
+
+            if (varIdentifier == null) {
+                return null;
+            }
+
+            Whitespace();
+
+            // += -=
+            bool isIncrement = ParseString ("+") != null;
+            bool isDecrement = ParseString ("-") != null;
+            if (isIncrement && isDecrement) Error ("Unexpected sequence '+-'");
+
+            if (ParseString ("=") == null) {
+                // Definitely in an assignment expression?
+                if (isNewDeclaration) Error ("Expected '='");
+                return null;
+            }
+
+            Expression assignedExpression = (Expression)Expect (Expression, "value expression to be assigned");
+
+            if (isIncrement || isDecrement) {
+                var result = new IncDecExpression (varIdentifier, assignedExpression, isIncrement);
+                return result;
+            } else {
+                var result = new VariableAssignment (varIdentifier, assignedExpression);
+                result.isNewTemporaryDeclaration = isNewDeclaration;
+                return result;
+            }
+        }
+
+        protected void DisallowIncrement (Parsed.Object expr)
+        {
+        	if (expr is Parsed.IncDecExpression)
+        		Error ("Can't use increment/decrement here. It can only be used on a ~ line");
+        }
+
+        protected bool ParseTempKeyword()
+        {
+            var ruleId = BeginRule ();
+
+            if (Parse (Identifier) == "temp") {
+                SucceedRule (ruleId);
+                return true;
+            } else {
+                FailRule (ruleId);
+                return false;
+            }
+        }
+
+        protected Parsed.Return ReturnStatement()
+        {
+            Whitespace ();
+
+            var returnOrDone = Parse(Identifier);
+            if (returnOrDone != "return") {
+                return null;
+            }
+
+            Whitespace ();
+
+            var expr = Parse(Expression);
+
+            var returnObj = new Return (expr);
+            return returnObj;
+        }
+
+		protected Expression Expression() {
+			return Expression(minimumPrecedence:0);
+		}
+
+		// Pratt Parser
+		// aka "Top down operator precedence parser"
+		// http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
+		// Algorithm overview:
+		// The two types of precedence are handled in two different ways:
+		//   ((((a . b) . c) . d) . e)			#1
+		//   (a . (b . (c . (d . e))))			#2
+		// Where #1 is automatically handled by successive loops within the main 'while' in this function,
+		// so long as continuing operators have lower (or equal) precedence (e.g. imagine some series of "*"s then "+" above.
+		// ...and #2 is handled by recursion of the right hand term in the binary expression parser.
+		// (see link for advice on how to extend for postfix and mixfix operators)
+		protected Expression Expression(int minimumPrecedence)
+		{
+			Whitespace ();
+
+			// First parse a unary expression e.g. "-a" or parethensised "(1 + 2)"
+			var expr = ExpressionUnary ();
+			if (expr == null) {
+                return null;
+			}
+
+			Whitespace ();
+
+			// Attempt to parse (possibly multiple) continuing infix expressions (e.g. 1 + 2 + 3)
+			while(true) {
+				var ruleId = BeginRule ();
+
+				// Operator
+				var infixOp = ParseInfixOperator ();
+				if (infixOp != null && infixOp.precedence > minimumPrecedence) {
+
+					// Expect right hand side of operator
+					var expectationMessage = string.Format("right side of '{0}' expression", infixOp.type);
+					var multiaryExpr = Expect (() => ExpressionInfixRight (left: expr, op: infixOp), expectationMessage);
+                    if (multiaryExpr == null) {
+
+                        // Fail for operator and right-hand side of multiary expression
+                        FailRule (ruleId);
+
+                        return null;
+                    }
+
+                    expr = SucceedRule(ruleId, multiaryExpr) as Parsed.Expression;
+
+					continue;
+				}
+
+                FailRule (ruleId);
+				break;
+			}
+
+            Whitespace ();
+
+            return expr;
+		}
+
+        protected Expression ExpressionUnary()
+		{
+            // Divert target is a special case - it can't have any other operators
+            // applied to it, and we also want to check for it first so that we don't
+            // confuse "->" for subtraction.
+            var divertTarget = Parse (ExpressionDivertTarget);
+            if (divertTarget != null) {
+                return divertTarget;
+            }
+
+            var prefixOp = (string) OneOf (String ("-"), String ("!"));
+
+            // Don't parse like the string rules above, in case its actually
+            // a variable that simply starts with "not", e.g. "notable".
+            // This rule uses the Identifier rule, which will scan as much text
+            // as possible before returning.
+            if (prefixOp == null) {
+                prefixOp = Parse(ExpressionNot);
+            }
+
+			Whitespace ();
+
+            // - Since we allow numbers at the start of variable names, variable names are checked before literals
+            // - Function calls before variable names in case we see parentheses
+            var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression;
+
+            // Only recurse immediately if we have one of the (usually optional) unary ops
+            if (expr == null && prefixOp != null) {
+                expr = ExpressionUnary ();
+            }
+
+			if (expr == null)
+                return null;
+
+            if (prefixOp != null) {
+                expr = UnaryExpression.WithInner(expr, prefixOp);
+			}
+
+            Whitespace ();
+
+            var postfixOp = (string) OneOf (String ("++"), String ("--"));
+            if (postfixOp != null) {
+                bool isInc = postfixOp == "++";
+
+                if (!(expr is VariableReference)) {
+                    Error ("can only increment and decrement variables, but saw '" + expr + "'");
+
+                    // Drop down and succeed without the increment after reporting error
+                } else {
+                    // TODO: Language Server - (Identifier combined into one vs. list of Identifiers)
+                    var varRef = (VariableReference)expr;
+                    expr = new IncDecExpression(varRef.identifier, isInc);
+                }
+
+            }
+
+            return expr;
+		}
+
+        protected string ExpressionNot()
+        {
+            var id = Identifier ();
+            if (id == "not") {
+                return id;
+            }
+
+            return null;
+        }
+
+		protected Expression ExpressionLiteral()
+		{
+            return (Expression) OneOf (ExpressionFloat, ExpressionInt, ExpressionBool, ExpressionString);
+		}
+
+        protected Expression ExpressionDivertTarget()
+        {
+            Whitespace ();
+
+            var divert = Parse(SingleDivert);
+            if (divert == null)
+                return null;
+
+            if (divert.isThread)
+                return null;
+
+            Whitespace ();
+
+            return new DivertTarget (divert);
+        }
+
+        protected Number ExpressionInt()
+        {
+            int? intOrNull = ParseInt ();
+            if (intOrNull == null) {
+                return null;
+            } else {
+                return new Number (intOrNull.Value);
+            }
+        }
+
+        protected Number ExpressionFloat()
+        {
+            float? floatOrNull = ParseFloat ();
+            if (floatOrNull == null) {
+                return null;
+            } else {
+                return new Number (floatOrNull.Value);
+            }
+        }
+
+        protected StringExpression ExpressionString()
+        {
+            var openQuote = ParseString ("\"");
+            if (openQuote == null)
+                return null;
+
+            // Set custom parser state flag so that within the text parser,
+            // it knows to treat the quote character (") as an end character
+            parsingStringExpression = true;
+
+            List<Parsed.Object> textAndLogic = Parse (MixedTextAndLogic);
+
+            Expect (String ("\""), "close quote for string expression");
+
+            parsingStringExpression = false;
+
+            if (textAndLogic == null) {
+                textAndLogic = new List<Ink.Parsed.Object> ();
+                textAndLogic.Add (new Parsed.Text (""));
+            }
+
+            else if (textAndLogic.Exists (c => c is Divert))
+                Error ("String expressions cannot contain diverts (->)");
+
+            return new StringExpression (textAndLogic);
+        }
+
+        protected Number ExpressionBool()
+        {
+            var id = Parse(Identifier);
+            if (id == "true") {
+                return new Number (true);
+            } else if (id == "false") {
+                return new Number (false);
+            }
+
+            return null;
+        }
+
+        protected Expression ExpressionFunctionCall()
+        {
+            var iden = Parse(IdentifierWithMetadata);
+            if (iden == null)
+                return null;
+
+            Whitespace ();
+
+            var arguments = Parse(ExpressionFunctionCallArguments);
+            if (arguments == null) {
+                return null;
+            }
+
+            return new FunctionCall(iden, arguments);
+        }
+
+        protected List<Expression> ExpressionFunctionCallArguments()
+        {
+            if (ParseString ("(") == null)
+                return null;
+
+            // "Exclude" requires the rule to succeed, but causes actual comma string to be excluded from the list of results
+            ParseRule commas = Exclude (String (","));
+            var arguments = Interleave<Expression>(Expression, commas);
+            if (arguments == null) {
+                arguments = new List<Expression> ();
+            }
+
+            Whitespace ();
+
+            Expect (String (")"), "closing ')' for function call");
+
+            return arguments;
+        }
+
+        protected Expression ExpressionVariableName()
+        {
+            List<Identifier> path = Interleave<Identifier> (IdentifierWithMetadata, Exclude (Spaced (String ("."))));
+
+            if (path == null || Story.IsReservedKeyword (path[0].name) )
+                return null;
+
+            return new VariableReference (path);
+        }
+
+		protected Expression ExpressionParen()
+		{
+			if (ParseString ("(") == null)
+                return null;
+
+            var innerExpr = Parse(Expression);
+			if (innerExpr == null)
+                return null;
+
+			Whitespace ();
+
+            Expect (String(")"), "closing parenthesis ')' for expression");
+
+            return innerExpr;
+		}
+
+		protected Expression ExpressionInfixRight(Parsed.Expression left, InfixOperator op)
+		{
+			Whitespace ();
+
+            var right = Parse(() => Expression (op.precedence));
+			if (right) {
+
+				// We assume that the character we use for the operator's type is the same
+				// as that used internally by e.g. Runtime.Expression.Add, Runtime.Expression.Multiply etc
+				var expr = new BinaryExpression (left, right, op.type);
+                return expr;
+			}
+
+            return null;
+
+		}
+
+		private InfixOperator ParseInfixOperator()
+		{
+            foreach (var op in _binaryOperators) {
+
+                int ruleId = BeginRule ();
+
+                if (ParseString (op.type) != null) {
+
+                    if (op.requireWhitespace) {
+                        if (Whitespace () == null) {
+                            FailRule (ruleId);
+                            continue;
+                        }
+                    }
+
+                    return (InfixOperator) SucceedRule(ruleId, op);
+                }
+
+                FailRule (ruleId);
+            }
+
+            return null;
+		}
+
+        protected Parsed.List ExpressionList ()
+        {
+            Whitespace ();
+
+            if (ParseString ("(") == null)
+                return null;
+
+            Whitespace ();
+
+            // When list has:
+            //  - 0 elements (null list) - this is okay, it's an empty list: "()"
+            //  - 1 element - it could be confused for a single non-list related
+            //    identifier expression in brackets, but this is a useless thing
+            //    to do, so we reserve that syntax for a list with one item.
+            //  - 2 or more elements - normal!
+            List<Identifier> memberNames = SeparatedList (ListMember, Spaced (String (",")));
+
+            Whitespace ();
+
+            // May have failed to parse the inner list - the parentheses may
+            // be for a normal expression
+            if (ParseString (")") == null)
+                return null;
+
+            return new List (memberNames);
+        }
+
+        protected Identifier ListMember ()
+        {
+            Whitespace ();
+
+            Identifier identifier = Parse (IdentifierWithMetadata);
+            if (identifier == null)
+                return null;
+
+            var dot = ParseString (".");
+            if (dot != null) {
+                Identifier identifier2 = Expect (IdentifierWithMetadata, "element name within the set " + identifier) as Identifier;
+                identifier.name = identifier.name + "." + identifier2?.name;
+            }
+
+            Whitespace ();
+
+            return identifier;
+        }
+
+		void RegisterExpressionOperators()
+		{
+            _maxBinaryOpLength = 0;
+			_binaryOperators = new List<InfixOperator> ();
+
+            // These will be tried in order, so we need "<=" before "<"
+            // for correctness
+
+            RegisterBinaryOperator ("&&", precedence:1);
+            RegisterBinaryOperator ("||", precedence:1);
+            RegisterBinaryOperator ("and", precedence:1, requireWhitespace: true);
+            RegisterBinaryOperator ("or", precedence:1, requireWhitespace: true);
+
+            RegisterBinaryOperator ("==", precedence:2);
+            RegisterBinaryOperator (">=", precedence:2);
+            RegisterBinaryOperator ("<=", precedence:2);
+            RegisterBinaryOperator ("<", precedence:2);
+            RegisterBinaryOperator (">", precedence:2);
+            RegisterBinaryOperator ("!=", precedence:2);
+
+            // (apples, oranges) + cabbages has (oranges, cabbages) == true
+            RegisterBinaryOperator ("?", precedence: 3);
+            RegisterBinaryOperator ("has", precedence: 3, requireWhitespace:true);
+            RegisterBinaryOperator ("!?", precedence: 3);
+            RegisterBinaryOperator ("hasnt", precedence: 3, requireWhitespace: true);
+            RegisterBinaryOperator ("^", precedence: 3);
+
+			RegisterBinaryOperator ("+", precedence:4);
+			RegisterBinaryOperator ("-", precedence:5);
+			RegisterBinaryOperator ("*", precedence:6);
+			RegisterBinaryOperator ("/", precedence:7);
+
+            RegisterBinaryOperator ("%", precedence:8);
+            RegisterBinaryOperator ("mod", precedence:8, requireWhitespace:true);
+
+
+		}
+
+        void RegisterBinaryOperator(string op, int precedence, bool requireWhitespace = false)
+		{
+            _binaryOperators.Add(new InfixOperator (op, precedence, requireWhitespace));
+            _maxBinaryOpLength = Math.Max (_maxBinaryOpLength, op.Length);
+		}
+
+        List<InfixOperator> _binaryOperators;
+        int _maxBinaryOpLength;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs.meta
new file mode 100644
index 0000000..fc5d820
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Expressions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 85c9eb24bda894235981df7a3689da51
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs
new file mode 100644
index 0000000..9490c2f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs
@@ -0,0 +1,76 @@
+using Ink.Parsed;
+using System.Collections.Generic;
+using System.IO;
+
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected object IncludeStatement()
+        {
+            Whitespace ();
+
+            if (ParseString ("INCLUDE") == null)
+                return null;
+
+            Whitespace ();
+
+            var filename = (string) Expect(() => ParseUntilCharactersFromString ("\n\r"), "filename for include statement");
+            filename = filename.TrimEnd (' ', '\t');
+
+            // Working directory should already have been set up relative to the root ink file.
+            var fullFilename = _rootParser._fileHandler.ResolveInkFilename (filename);
+
+            if (FilenameIsAlreadyOpen (fullFilename)) {
+                Error ("Recursive INCLUDE detected: '" + fullFilename + "' is already open.");
+                ParseUntilCharactersFromString("\r\n");
+                return new IncludedFile(null);
+            } else {
+                AddOpenFilename (fullFilename);
+            }
+
+            Parsed.Story includedStory = null;
+            string includedString = null;
+            try {
+                includedString = _rootParser._fileHandler.LoadInkFileContents(fullFilename);
+            }
+            catch {
+                Error ("Failed to load: '"+filename+"'");
+            }
+
+
+            if (includedString != null ) {
+                InkParser parser = new InkParser(includedString, filename, _externalErrorHandler, _rootParser);
+                includedStory = parser.Parse();
+            }
+
+            RemoveOpenFilename (fullFilename);
+
+            // Return valid IncludedFile object even if there were errors when parsing.
+            // We don't want to attempt to re-parse the include line as something else,
+            // and we want to include the bits that *are* valid, so we don't generate
+            // more errors than necessary.
+            return new IncludedFile (includedStory);
+        }
+
+        bool FilenameIsAlreadyOpen(string fullFilename)
+        {
+            return _rootParser._openFilenames.Contains (fullFilename);
+        }
+
+        void AddOpenFilename(string fullFilename)
+        {
+            _rootParser._openFilenames.Add (fullFilename);
+        }
+
+        void RemoveOpenFilename(string fullFilename)
+        {
+            _rootParser._openFilenames.Remove (fullFilename);
+        }
+                   
+        InkParser _rootParser;
+        HashSet<string> _openFilenames;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs.meta
new file mode 100644
index 0000000..7328506
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Include.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f4b92fc2dbd664298b01416321678d61
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs
new file mode 100644
index 0000000..d124243
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs
@@ -0,0 +1,239 @@
+using System.Collections.Generic;
+using Ink.Parsed;
+using System.Linq;
+
+namespace Ink
+{
+	public partial class InkParser
+	{
+        protected class NameWithMetadata {
+            public string name;
+            public Runtime.DebugMetadata metadata;
+        }
+
+        protected class FlowDecl
+        {
+            public Identifier name;
+            public List<FlowBase.Argument> arguments;
+            public bool isFunction;
+        }
+
+		protected Knot KnotDefinition()
+		{
+            var knotDecl = Parse(KnotDeclaration);
+            if (knotDecl == null)
+                return null;
+
+			Expect(EndOfLine, "end of line after knot name definition", recoveryRule: SkipToNextLine);
+
+			ParseRule innerKnotStatements = () => StatementsAtLevel (StatementLevel.Knot);
+
+            var content = Expect (innerKnotStatements, "at least one line within the knot", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
+
+            return new Knot (knotDecl.name, content, knotDecl.arguments, knotDecl.isFunction);
+		}
+
+        protected FlowDecl KnotDeclaration()
+        {
+            Whitespace ();
+
+            if (KnotTitleEquals () == null)
+                return null;
+
+            Whitespace ();
+
+
+            Identifier identifier = Parse(IdentifierWithMetadata);
+            Identifier knotName;
+
+            bool isFunc = identifier?.name == "function";
+            if (isFunc) {
+                Expect (Whitespace, "whitespace after the 'function' keyword");
+                knotName = Parse(IdentifierWithMetadata);
+            } else {
+                knotName = identifier;
+            }
+
+            if (knotName == null) {
+                Error ("Expected the name of the " + (isFunc ? "function" : "knot"));
+                knotName = new Identifier { name = "" }; // prevent later null ref
+            }
+
+            Whitespace ();
+
+            List<FlowBase.Argument> parameterNames = Parse (BracketedKnotDeclArguments);
+
+            Whitespace ();
+
+            // Optional equals after name
+            Parse(KnotTitleEquals);
+
+            return new FlowDecl () { name = knotName, arguments = parameterNames, isFunction = isFunc };
+        }
+
+        protected string KnotTitleEquals()
+        {
+            // 2+ "=" starts a knot
+            var multiEquals = ParseCharactersFromString ("=");
+            if (multiEquals == null || multiEquals.Length <= 1) {
+                return null;
+            } else {
+                return multiEquals;
+            }
+        }
+
+		protected object StitchDefinition()
+		{
+            var decl = Parse(StitchDeclaration);
+            if (decl == null)
+                return null;
+
+			Expect(EndOfLine, "end of line after stitch name", recoveryRule: SkipToNextLine);
+
+			ParseRule innerStitchStatements = () => StatementsAtLevel (StatementLevel.Stitch);
+
+            var content = Expect(innerStitchStatements, "at least one line within the stitch", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
+
+            return new Stitch (decl.name, content, decl.arguments, decl.isFunction );
+		}
+
+        protected FlowDecl StitchDeclaration()
+        {
+            Whitespace ();
+
+            // Single "=" to define a stitch
+            if (ParseString ("=") == null)
+                return null;
+
+            // If there's more than one "=", that's actually a knot definition (or divert), so this rule should fail
+            if (ParseString ("=") != null)
+                return null;
+
+            Whitespace ();
+
+            // Stitches aren't allowed to be functions, but we parse it anyway and report the error later
+            bool isFunc = ParseString ("function") != null;
+            if ( isFunc ) {
+                Whitespace ();
+            }
+
+            Identifier stitchName = Parse(IdentifierWithMetadata);
+            if (stitchName == null)
+                return null;
+
+            Whitespace ();
+
+            List<FlowBase.Argument> flowArgs = Parse(BracketedKnotDeclArguments);
+
+            Whitespace ();
+
+            return new FlowDecl () { name = stitchName, arguments = flowArgs, isFunction = isFunc };
+        }
+
+
+		protected object KnotStitchNoContentRecoveryRule()
+		{
+            // Jump ahead to the next knot or the end of the file
+            ParseUntil (KnotDeclaration, new CharacterSet ("="), null);
+
+            var recoveredFlowContent = new List<Parsed.Object>();
+			recoveredFlowContent.Add( new Parsed.Text("<ERROR IN FLOW>" ) );
+			return recoveredFlowContent;
+		}
+
+        protected List<FlowBase.Argument> BracketedKnotDeclArguments()
+        {
+            if (ParseString ("(") == null)
+                return null;
+
+            var flowArguments = Interleave<FlowBase.Argument>(Spaced(FlowDeclArgument), Exclude (String(",")));
+
+            Expect (String (")"), "closing ')' for parameter list");
+
+            // If no parameters, create an empty list so that this method is type safe and
+            // doesn't attempt to return the ParseSuccess object
+            if (flowArguments == null) {
+                flowArguments = new List<FlowBase.Argument> ();
+            }
+
+            return flowArguments;
+        }
+
+        protected FlowBase.Argument FlowDeclArgument()
+        {
+            // Possible forms:
+            //  name
+            //  -> name      (variable divert target argument
+            //  ref name
+            //  ref -> name  (variable divert target by reference)
+            var firstIden = Parse(IdentifierWithMetadata);
+            Whitespace ();
+            var divertArrow = ParseDivertArrow ();
+            Whitespace ();
+            var secondIden = Parse(IdentifierWithMetadata);
+
+            if (firstIden == null && secondIden == null)
+                return null;
+
+
+            var flowArg = new FlowBase.Argument ();
+            if (divertArrow != null) {
+                flowArg.isDivertTarget = true;
+            }
+
+            // Passing by reference
+            if (firstIden != null && firstIden.name == "ref") {
+
+                if (secondIden == null) {
+                    Error ("Expected an parameter name after 'ref'");
+                }
+
+                flowArg.identifier = secondIden;
+                flowArg.isByReference = true;
+            }
+
+            // Simple argument name
+            else {
+
+                if (flowArg.isDivertTarget) {
+                    flowArg.identifier = secondIden;
+                } else {
+                    flowArg.identifier = firstIden;
+                }
+
+                if (flowArg.identifier == null) {
+                    Error ("Expected an parameter name");
+                }
+
+                flowArg.isByReference = false;
+            }
+
+            return flowArg;
+        }
+
+        protected ExternalDeclaration ExternalDeclaration()
+        {
+            Whitespace ();
+
+            Identifier external = Parse(IdentifierWithMetadata);
+            if (external == null || external.name != "EXTERNAL")
+                return null;
+
+            Whitespace ();
+
+            var funcIdentifier = Expect(IdentifierWithMetadata, "name of external function") as Identifier ?? new Identifier();
+
+            Whitespace ();
+
+            var parameterNames = Expect (BracketedKnotDeclArguments, "declaration of arguments for EXTERNAL, even if empty, i.e. 'EXTERNAL "+funcIdentifier+"()'") as List<FlowBase.Argument>;
+            if (parameterNames == null)
+                parameterNames = new List<FlowBase.Argument> ();
+
+            var argNames = parameterNames.Select (arg => arg.identifier?.name).ToList();
+
+            return new ExternalDeclaration (funcIdentifier, argNames);
+        }
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs.meta
new file mode 100644
index 0000000..a0b8e17
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Knot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 77e8119b05a284a889e4b2978c6b1617
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs
new file mode 100644
index 0000000..44e8e03
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs
@@ -0,0 +1,421 @@
+using System.Collections.Generic;
+using System.Linq;
+using Ink.Parsed;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+
+        protected Parsed.Object LogicLine()
+        {
+            Whitespace ();
+
+            if (ParseString ("~") == null) {
+                return null;
+            }
+
+            Whitespace ();
+
+            // Some example lines we need to be able to distinguish between:
+            // ~ temp x = 5  -- var decl + assign
+            // ~ temp x      -- var decl
+            // ~ x = 5       -- var assign
+            // ~ x           -- expr (not var decl or assign)
+            // ~ f()         -- expr
+            // We don't treat variable decl/assign as an expression since we don't want an assignment
+            // to have a return value, or to be used in compound expressions.
+            ParseRule afterTilda = () => OneOf (ReturnStatement, TempDeclarationOrAssignment, Expression);
+
+            var result = Expect(afterTilda, "expression after '~'", recoveryRule: SkipToNextLine) as Parsed.Object;
+
+            // Prevent further errors, already reported expected expression and have skipped to next line.
+            if (result == null) return new ContentList();
+
+            // Parse all expressions, but tell the writer off if they did something useless like:
+            //  ~ 5 + 4
+            // And even:
+            //  ~ false && myFunction()
+            // ...since it's bad practice, and won't do what they expect if
+            // they're expecting C's lazy evaluation.
+            if (result is Expression && !(result is FunctionCall || result is IncDecExpression) ) {
+
+                // TODO: Remove this specific error message when it has expired in usefulness
+                var varRef = result as VariableReference;
+                if (varRef && varRef.name == "include") {
+                    Error ("'~ include' is no longer the correct syntax - please use 'INCLUDE your_filename.ink', without the tilda, and in block capitals.");
+                }
+
+                else {
+                    Error ("Logic following a '~' can't be that type of expression. It can only be something like:\n\t~ return\n\t~ var x = blah\n\t~ x++\n\t~ myFunction()");
+                }
+            }
+
+            // Line is pure function call? e.g.
+            //  ~ f()
+            // Add extra pop to make sure we tidy up after ourselves.
+            // We no longer need anything on the evaluation stack.
+            var funCall = result as FunctionCall;
+            if (funCall) funCall.shouldPopReturnedValue = true;
+
+            // If the expression contains a function call, then it could produce a text side effect,
+            // in which case it needs a newline on the end. e.g.
+            //  ~ printMyName()
+            //  ~ x = 1 + returnAValueAndAlsoPrintStuff()
+            // If no text gets printed, then the extra newline will have to be culled later.
+            // Multiple newlines on the output will be removed, so there will be no "leak" for
+            // long running calculations. It's disappointingly messy though :-/
+            if (result.Find<FunctionCall>() != null ) {
+                result = new ContentList (result, new Parsed.Text ("\n"));
+            }
+
+            Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
+
+            return result as Parsed.Object;
+        }
+
+        protected Parsed.Object VariableDeclaration()
+        {
+            Whitespace ();
+
+            var id = Parse (Identifier);
+            if (id != "VAR")
+                return null;
+
+            Whitespace ();
+
+            var varName = Expect (IdentifierWithMetadata, "variable name") as Identifier;
+
+            Whitespace ();
+
+            Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)");
+
+            Whitespace ();
+
+            var definition = Expect (Expression, "initial value for ");
+
+            var expr = definition as Parsed.Expression;
+
+            if (expr) {
+                if (!(expr is Number || expr is StringExpression || expr is DivertTarget || expr is VariableReference || expr is List)) {
+                    Error ("initial value for a variable must be a number, constant, list or divert target");
+                }
+
+                if (Parse (ListElementDefinitionSeparator) != null)
+                    Error ("Unexpected ','. If you're trying to declare a new list, use the LIST keyword, not VAR");
+
+                // Ensure string expressions are simple
+                else if (expr is StringExpression) {
+                    var strExpr = expr as StringExpression;
+                    if (!strExpr.isSingleString)
+                        Error ("Constant strings cannot contain any logic.");
+                }
+
+                var result = new VariableAssignment (varName, expr);
+                result.isGlobalDeclaration = true;
+                return result;
+            }
+
+            return null;
+        }
+
+        protected Parsed.VariableAssignment ListDeclaration ()
+        {
+            Whitespace ();
+
+            var id = Parse (Identifier);
+            if (id != "LIST")
+                return null;
+
+            Whitespace ();
+
+            var varName = Expect (IdentifierWithMetadata, "list name") as Identifier;
+
+            Whitespace ();
+
+            Expect (String ("="), "the '=' for an assignment of the list definition");
+
+            Whitespace ();
+
+            var definition = Expect (ListDefinition, "list item names") as ListDefinition;
+
+            if (definition) {
+
+                definition.identifier = varName;
+
+                return new VariableAssignment (varName, definition);
+            }
+
+            return null;
+        }
+
+        protected Parsed.ListDefinition ListDefinition ()
+        {
+            AnyWhitespace ();
+
+            var allElements = SeparatedList (ListElementDefinition, ListElementDefinitionSeparator);
+            if (allElements == null)
+                return null;
+
+            return new ListDefinition (allElements);
+        }
+
+        protected string ListElementDefinitionSeparator ()
+        {
+            AnyWhitespace ();
+
+            if (ParseString (",") == null) return null;
+
+            AnyWhitespace ();
+
+            return ",";
+        }
+
+        protected Parsed.ListElementDefinition ListElementDefinition ()
+        {
+            var inInitialList = ParseString ("(") != null;
+            var needsToCloseParen = inInitialList;
+
+            Whitespace ();
+
+            var name = Parse (IdentifierWithMetadata);
+            if (name == null)
+                return null;
+
+            Whitespace ();
+
+            if (inInitialList) {
+                if (ParseString (")") != null) {
+                    needsToCloseParen = false;
+                    Whitespace ();
+                }
+            }
+
+            int? elementValue = null;
+            if (ParseString ("=") != null) {
+
+                Whitespace ();
+
+                var elementValueNum = Expect (ExpressionInt, "value to be assigned to list item") as Number;
+                if (elementValueNum != null) {
+                    elementValue = (int) elementValueNum.value;
+                }
+
+                if (needsToCloseParen) {
+                    Whitespace ();
+
+                    if (ParseString (")") != null)
+                        needsToCloseParen = false;
+                }
+            }
+
+            if (needsToCloseParen)
+                Error("Expected closing ')'");
+
+            return new ListElementDefinition (name, inInitialList, elementValue);
+        }
+
+        protected Parsed.Object ConstDeclaration()
+        {
+            Whitespace ();
+
+            var id = Parse (Identifier);
+            if (id != "CONST")
+                return null;
+
+            Whitespace ();
+
+            var varName = Expect (IdentifierWithMetadata, "constant name") as Identifier;
+
+            Whitespace ();
+
+            Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)");
+
+            Whitespace ();
+
+            var expr = Expect (Expression, "initial value for ") as Parsed.Expression;
+            if (!(expr is Number || expr is DivertTarget || expr is StringExpression)) {
+                Error ("initial value for a constant must be a number or divert target");
+            }
+
+            // Ensure string expressions are simple
+            else if (expr is StringExpression) {
+                var strExpr = expr as StringExpression;
+                if (!strExpr.isSingleString)
+                    Error ("Constant strings cannot contain any logic.");
+            }
+
+
+            var result = new ConstantDeclaration (varName, expr);
+            return result;
+        }
+
+        protected Parsed.Object InlineLogicOrGlue()
+        {
+            return (Parsed.Object) OneOf (InlineLogic, Glue);
+        }
+
+        protected Parsed.Glue Glue()
+        {
+            // Don't want to parse whitespace, since it might be important
+            // surrounding the glue.
+            var glueStr = ParseString("<>");
+            if (glueStr != null) {
+                return new Parsed.Glue (new Runtime.Glue ());
+            } else {
+                return null;
+            }
+        }
+
+        protected Parsed.Object InlineLogic()
+        {
+            if ( ParseString ("{") == null) {
+                return null;
+            }
+
+            Whitespace ();
+
+            var logic = (Parsed.Object) Expect(InnerLogic, "some kind of logic, conditional or sequence within braces: { ... }");
+            if (logic == null)
+                return null;
+
+            DisallowIncrement (logic);
+
+            ContentList contentList = logic as ContentList;
+            if (!contentList) {
+                contentList = new ContentList (logic);
+            }
+
+            Whitespace ();
+
+            Expect (String("}"), "closing brace '}' for inline logic");
+
+            return contentList;
+        }
+
+        protected Parsed.Object InnerLogic()
+        {
+            Whitespace ();
+
+            // Explicitly try the combinations of inner logic
+            // that could potentially have conflicts first.
+
+            // Explicit sequence annotation?
+            SequenceType? explicitSeqType = (SequenceType?) ParseObject(SequenceTypeAnnotation);
+            if (explicitSeqType != null) {
+                var contentLists = (List<ContentList>) Expect(InnerSequenceObjects, "sequence elements (for cycle/stoping etc)");
+                if (contentLists == null)
+                    return null;
+                return new Sequence (contentLists, (SequenceType) explicitSeqType);
+            }
+
+            // Conditional with expression?
+            var initialQueryExpression = Parse(ConditionExpression);
+            if (initialQueryExpression) {
+                var conditional = (Conditional) Expect(() => InnerConditionalContent (initialQueryExpression), "conditional content following query");
+                return conditional;
+            }
+
+            // Now try to evaluate each of the "full" rules in turn
+            ParseRule[] rules = {
+
+                // Conditional still necessary, since you can have a multi-line conditional
+                // without an initial query expression:
+                // {
+                //   - true:  this is true
+                //   - false: this is false
+                // }
+                InnerConditionalContent,
+                InnerSequence,
+                InnerExpression,
+            };
+
+            // Adapted from "OneOf" structuring rule except that in
+            // order for the rule to succeed, it has to maximally
+            // cover the entire string within the { }. Used to
+            // differentiate between:
+            //  {myVar}                 -- Expression (try first)
+            //  {my content is jolly}   -- sequence with single element
+            foreach (ParseRule rule in rules) {
+                int ruleId = BeginRule ();
+
+                Parsed.Object result = ParseObject(rule) as Parsed.Object;
+                if (result) {
+
+                    // Not yet at end?
+                    if (Peek (Spaced (String ("}"))) == null)
+                        FailRule (ruleId);
+
+                    // Full parse of content within braces
+                    else
+                        return (Parsed.Object) SucceedRule (ruleId, result);
+
+                } else {
+                    FailRule (ruleId);
+                }
+            }
+
+            return null;
+        }
+
+        protected Parsed.Object InnerExpression()
+        {
+            var expr = Parse(Expression);
+            if (expr) {
+                expr.outputWhenComplete = true;
+            }
+            return expr;
+        }
+
+        protected Identifier IdentifierWithMetadata()
+        {
+            var id = Identifier();
+            if( id == null ) return null;
+
+            // InkParser.RuleDidSucceed will add DebugMetadata
+            return new Identifier { name = id, debugMetadata = null };
+        }
+
+        // Note: we allow identifiers that start with a number,
+        // but not if they *only* comprise numbers
+        protected string Identifier()
+        {
+            // Parse remaining characters (if any)
+            var name = ParseCharactersFromCharSet (identifierCharSet);
+            if (name == null)
+                return null;
+
+            // Reject if it's just a number
+            bool isNumberCharsOnly = true;
+            foreach (var c in name) {
+                if ( !(c >= '0' && c <= '9') ) {
+                    isNumberCharsOnly = false;
+                    break;
+                }
+            }
+            if (isNumberCharsOnly) {
+                return null;
+            }
+
+            return name;
+        }
+
+        CharacterSet identifierCharSet {
+            get {
+                if (_identifierCharSet == null) {
+                    (_identifierCharSet = new CharacterSet ())
+                        .AddRange ('A', 'Z')
+                        .AddRange ('a', 'z')
+                        .AddRange ('0', '9')
+                        .Add ('_');
+                    // Enable non-ASCII characters for story identifiers.
+                    ExtendIdentifierCharacterRanges (_identifierCharSet);
+                }
+                return _identifierCharSet;
+            }
+        }
+
+        private CharacterSet _identifierCharSet;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs.meta
new file mode 100644
index 0000000..01448c1
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Logic.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aa9a6c9127e3f4eb8a46f532557ccd2b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs
new file mode 100644
index 0000000..b9156e8
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs
@@ -0,0 +1,231 @@
+using System.Collections.Generic;
+using System.Linq;
+using Ink.Parsed;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected Sequence InnerSequence()
+        {
+            Whitespace ();
+
+            // Default sequence type
+            SequenceType seqType = SequenceType.Stopping;
+
+            // Optional explicit sequence type
+            SequenceType? parsedSeqType = (SequenceType?) Parse(SequenceTypeAnnotation);
+            if (parsedSeqType != null)
+                seqType = parsedSeqType.Value;
+
+            var contentLists = Parse(InnerSequenceObjects);
+            if (contentLists == null || contentLists.Count <= 1) {
+                return null;
+            }
+
+            return new Sequence (contentLists, seqType);
+        }
+
+        protected object SequenceTypeAnnotation()
+        {
+            var annotation = (SequenceType?) Parse(SequenceTypeSymbolAnnotation);
+
+            if(annotation == null)
+                annotation = (SequenceType?) Parse(SequenceTypeWordAnnotation);
+
+            if (annotation == null)
+                return null;
+
+            switch (annotation.Value)
+            {
+                case SequenceType.Once:
+                case SequenceType.Cycle:
+                case SequenceType.Stopping:
+                case SequenceType.Shuffle:
+                case (SequenceType.Shuffle | SequenceType.Stopping):
+                case (SequenceType.Shuffle | SequenceType.Once):
+                    break;
+
+                default:
+                    Error("Sequence type combination not supported: " + annotation.Value);
+                    return SequenceType.Stopping;
+            }
+
+            return annotation;
+        }
+
+        protected object SequenceTypeSymbolAnnotation()
+        {
+            if(_sequenceTypeSymbols == null )
+                _sequenceTypeSymbols = new CharacterSet("!&~$ ");
+
+            var sequenceType = (SequenceType)0;
+            var sequenceAnnotations = ParseCharactersFromCharSet(_sequenceTypeSymbols);
+            if (sequenceAnnotations == null)
+                return null;
+
+            foreach(char symbolChar in sequenceAnnotations) {
+                switch(symbolChar) {
+                    case '!': sequenceType |= SequenceType.Once; break;
+                    case '&': sequenceType |= SequenceType.Cycle; break;
+                    case '~': sequenceType |= SequenceType.Shuffle; break;
+                    case '$': sequenceType |= SequenceType.Stopping; break;
+                }
+            }
+
+            if (sequenceType == (SequenceType)0)
+                return null;
+
+            return sequenceType;
+        }
+
+        CharacterSet _sequenceTypeSymbols = new CharacterSet("!&~$");
+
+        protected object SequenceTypeWordAnnotation()
+        {
+            var sequenceTypes = Interleave<SequenceType?>(SequenceTypeSingleWord, Exclude(Whitespace));
+            if (sequenceTypes == null || sequenceTypes.Count == 0)
+                return null;
+
+            if (ParseString (":") == null)
+                return null;
+
+            var combinedSequenceType = (SequenceType)0;
+            foreach(var seqType in sequenceTypes) {
+                combinedSequenceType |= seqType.Value;
+            }
+
+            return combinedSequenceType;
+        }
+
+        protected object SequenceTypeSingleWord()
+        {
+            SequenceType? seqType = null;
+
+            var word = Parse(IdentifierWithMetadata);
+            if (word != null)
+            {
+                switch (word.name)
+                {
+                    case "once":
+                        seqType = SequenceType.Once;
+                        break;
+                    case "cycle":
+                        seqType = SequenceType.Cycle;
+                        break;
+                    case "shuffle":
+                        seqType = SequenceType.Shuffle;
+                        break;
+                    case "stopping":
+                        seqType = SequenceType.Stopping;
+                        break;
+                }
+            }
+
+            if (seqType == null)
+                return null;
+
+            return seqType;
+        }
+
+            protected List<ContentList> InnerSequenceObjects()
+        {
+            var multiline = Parse(Newline) != null;
+
+            List<ContentList> result = null;
+            if (multiline) {
+                result = Parse(InnerMultilineSequenceObjects);
+            } else {
+                result = Parse(InnerInlineSequenceObjects);
+            }
+
+            return result;
+        }
+
+        protected List<ContentList> InnerInlineSequenceObjects()
+        {
+            var interleavedContentAndPipes = Interleave<object> (Optional (MixedTextAndLogic), String ("|"), flatten:false);
+            if (interleavedContentAndPipes == null)
+                return null;
+
+            var result = new List<ContentList> ();
+
+            // The content and pipes won't necessarily be perfectly interleaved in the sense that
+            // the content can be missing, but in that case it's intended that there's blank content.
+            bool justHadContent = false;
+            foreach (object contentOrPipe in interleavedContentAndPipes) {
+
+                // Pipe/separator
+                if (contentOrPipe as string == "|") {
+
+                    // Expected content, saw pipe - need blank content now
+                    if (!justHadContent) {
+
+                        // Add blank content
+                        result.Add (new ContentList ());
+                    }
+
+                    justHadContent = false;
+                }
+
+                // Real content
+                else {
+
+                    var content = contentOrPipe as List<Parsed.Object>;
+                    if (content == null) {
+                        Error ("Expected content, but got " + contentOrPipe + " (this is an ink compiler bug!)");
+                    } else {
+                        result.Add (new ContentList (content));
+                    }
+
+                    justHadContent = true;
+                }
+            }
+
+            // Ended in a pipe? Need to insert final blank content
+            if (!justHadContent)
+                result.Add (new ContentList ());
+
+            return result;
+        }
+
+        protected List<ContentList> InnerMultilineSequenceObjects()
+        {
+            MultilineWhitespace ();
+
+            var contentLists = OneOrMore (SingleMultilineSequenceElement);
+            if (contentLists == null)
+                return null;
+
+            return contentLists.Cast<ContentList> ().ToList();
+        }
+
+        protected ContentList SingleMultilineSequenceElement()
+        {
+            Whitespace ();
+
+            // Make sure we're not accidentally parsing a divert
+            if (ParseString ("->") != null)
+                return null;
+
+            if (ParseString ("-") == null)
+                return null;
+
+            Whitespace ();
+
+
+            List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
+
+            if (content == null)
+                MultilineWhitespace ();
+
+            // Add newline at the start of each branch
+            else {
+                content.Insert (0, new Parsed.Text ("\n"));
+            }
+
+            return new ContentList (content);
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs.meta
new file mode 100644
index 0000000..4b5f9e2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Sequences.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0e33e10232e5c4587a8c460cf9d99960
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs
new file mode 100644
index 0000000..0b622f2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Ink.Parsed;
+
+namespace Ink
+{
+	public partial class InkParser
+	{
+		protected enum StatementLevel
+		{
+            InnerBlock,
+			Stitch,
+			Knot,
+			Top
+		}
+
+		protected List<Parsed.Object> StatementsAtLevel(StatementLevel level)
+		{
+            // Check for error: Should not be allowed gather dashes within an inner block
+            if (level == StatementLevel.InnerBlock) {
+                object badGatherDashCount = Parse(GatherDashes);
+                if (badGatherDashCount != null) {
+                    Error ("You can't use a gather (the dashes) within the { curly braces } context. For multi-line sequences and conditions, you should only use one dash.");
+                }
+            }
+                
+			return Interleave<Parsed.Object>(
+                Optional (MultilineWhitespace), 
+                () => StatementAtLevel (level), 
+                untilTerminator: () => StatementsBreakForLevel(level));
+		}
+            
+        protected object StatementAtLevel(StatementLevel level)
+        {            
+            ParseRule[] rulesAtLevel = _statementRulesAtLevel[(int)level];
+
+            var statement = OneOf (rulesAtLevel);
+
+            // For some statements, allow them to parse, but create errors, since
+            // writers may think they can use the statement, so it's useful to have 
+            // the error message.
+            if (level == StatementLevel.Top) {
+                if( statement is Return ) 
+                    Error ("should not have return statement outside of a knot");
+            }
+
+            return statement;
+        }
+
+        protected object StatementsBreakForLevel(StatementLevel level)
+        {
+            Whitespace ();
+
+            ParseRule[] breakRules = _statementBreakRulesAtLevel[(int)level];
+
+            var breakRuleResult = OneOf (breakRules);
+            if (breakRuleResult == null)
+                return null;
+
+            return breakRuleResult;
+        }
+
+		void GenerateStatementLevelRules()
+		{
+            var levels = Enum.GetValues (typeof(StatementLevel)).Cast<StatementLevel> ().ToList();
+
+            _statementRulesAtLevel = new ParseRule[levels.Count][];
+            _statementBreakRulesAtLevel = new ParseRule[levels.Count][];
+
+            foreach (var level in levels) {
+                List<ParseRule> rulesAtLevel = new List<ParseRule> ();
+                List<ParseRule> breakingRules = new List<ParseRule> ();
+
+                // Diverts can go anywhere
+                rulesAtLevel.Add(Line(MultiDivert));
+
+                // Knots can only be parsed at Top/Global scope
+                if (level >= StatementLevel.Top)
+                    rulesAtLevel.Add (KnotDefinition);
+
+                rulesAtLevel.Add(Line(Choice));
+
+                rulesAtLevel.Add(Line(AuthorWarning));
+
+                // Gather lines would be confused with multi-line block separators, like
+                // within a multi-line if statement
+                if (level > StatementLevel.InnerBlock) {
+                    rulesAtLevel.Add (Gather);
+                }
+
+                // Stitches (and gathers) can (currently) only go in Knots and top level
+                if (level >= StatementLevel.Knot) {
+                    rulesAtLevel.Add (StitchDefinition);
+                }
+
+                // Global variable declarations can go anywhere
+                rulesAtLevel.Add(Line(ListDeclaration));
+                rulesAtLevel.Add(Line(VariableDeclaration));
+                rulesAtLevel.Add(Line(ConstDeclaration));
+                rulesAtLevel.Add(Line(ExternalDeclaration));
+
+                // Global include can go anywhere
+                rulesAtLevel.Add(Line(IncludeStatement));
+
+                // Normal logic / text can go anywhere
+                rulesAtLevel.Add(LogicLine);
+                rulesAtLevel.Add(LineOfMixedTextAndLogic);
+
+                // --------
+                // Breaking rules
+
+                // Break current knot with a new knot
+                if (level <= StatementLevel.Knot) {
+                    breakingRules.Add (KnotDeclaration);
+                }
+
+                // Break current stitch with a new stitch
+                if (level <= StatementLevel.Stitch) {
+                    breakingRules.Add (StitchDeclaration);
+                }
+
+                // Breaking an inner block (like a multi-line condition statement)
+                if (level <= StatementLevel.InnerBlock) {
+                    breakingRules.Add (ParseDashNotArrow);
+                    breakingRules.Add (String ("}"));
+                }
+
+                _statementRulesAtLevel [(int)level] = rulesAtLevel.ToArray ();
+                _statementBreakRulesAtLevel [(int)level] = breakingRules.ToArray ();
+            }
+		}
+
+		protected object SkipToNextLine()
+		{
+			ParseUntilCharactersFromString ("\n\r");
+			ParseNewline ();
+			return ParseSuccess;
+		}
+
+		// Modifier to turn a rule into one that expects a newline on the end.
+		// e.g. anywhere you can use "MixedTextAndLogic" as a rule, you can use 
+		// "Line(MixedTextAndLogic)" to specify that it expects a newline afterwards.
+		protected ParseRule Line(ParseRule inlineRule)
+		{
+			return () => {
+				object result = ParseObject(inlineRule);
+                if (result == null) {
+                    return null;
+                }
+
+				Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
+
+				return result;
+			};
+		}
+
+
+        ParseRule[][] _statementRulesAtLevel;
+        ParseRule[][] _statementBreakRulesAtLevel;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs.meta
new file mode 100644
index 0000000..c1ae3b9
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Statements.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 42fdd6da57c584bf58ff578d093b8529
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs
new file mode 100644
index 0000000..7f8d12a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs
@@ -0,0 +1,50 @@
+using System.Text;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink
+{
+    public partial class InkParser
+    {
+        protected Parsed.Tag Tag ()
+        {
+            Whitespace ();
+
+            if (ParseString ("#") == null)
+                return null;
+
+            Whitespace ();
+
+            var sb = new StringBuilder ();
+            do {
+                // Read up to another #, end of input or newline
+                string tagText = ParseUntilCharactersFromCharSet (_endOfTagCharSet);
+                sb.Append (tagText);
+
+                // Escape character
+                if (ParseString ("\\") != null) {
+                    char c = ParseSingleCharacter ();
+                    if( c != (char)0 ) sb.Append(c);
+                    continue;
+                }
+
+                break;
+            } while ( true );
+
+            var fullTagText = sb.ToString ().Trim();
+
+            return new Parsed.Tag (new Runtime.Tag (fullTagText));
+        }
+
+        protected List<Parsed.Tag> Tags ()
+        {
+            var tags = OneOrMore (Tag);
+            if (tags == null) return null;
+
+            return tags.Cast<Parsed.Tag>().ToList();
+        }
+
+        CharacterSet _endOfTagCharSet = new CharacterSet ("#\n\r\\");
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs.meta
new file mode 100644
index 0000000..bb98f11
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Tags.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fed5929e88fa3480e8f02d3f686c0c2a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs
new file mode 100644
index 0000000..96031e7
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs
@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+
+namespace Ink
+{
+	public partial class InkParser
+	{
+		// Handles both newline and endOfFile
+		protected object EndOfLine()
+		{
+            return OneOf(Newline, EndOfFile);
+		}
+
+        // Allow whitespace before the actual newline
+        protected object Newline()
+        {
+            Whitespace();
+
+            bool gotNewline = ParseNewline () != null;
+
+            // Optional \r, definite \n to support Windows (\r\n) and Mac/Unix (\n)
+
+            if( !gotNewline ) {
+                return null;
+            } else {
+                return ParseSuccess;
+            }
+        }
+
+		protected object EndOfFile()
+		{
+			Whitespace();
+
+            if (!endOfInput)
+                return null;
+
+            return ParseSuccess;
+		}
+
+
+		// General purpose space, returns N-count newlines (fails if no newlines)
+		protected object MultilineWhitespace()
+		{
+            List<object> newlines = OneOrMore(Newline);
+            if (newlines == null)
+                return null;
+
+			// Use content field of Token to say how many newlines there were
+			// (in most circumstances it's unimportant)
+			int numNewlines = newlines.Count;
+			if (numNewlines >= 1) {
+                return ParseSuccess;
+			} else {
+                return null;
+			}
+		}
+
+		protected object Whitespace()
+		{
+			if( ParseCharactersFromCharSet(_inlineWhitespaceChars) != null ) {
+				return ParseSuccess;
+			}
+
+			return null;
+		}
+
+        protected ParseRule Spaced(ParseRule rule)
+        {
+            return () => {
+
+                Whitespace ();
+
+                var result = ParseObject(rule);
+                if (result == null) {
+                    return null;
+                }
+
+                Whitespace ();
+
+                return result;
+            };
+        }
+
+        protected object AnyWhitespace ()
+        {
+            bool anyWhitespace = false;
+            while (OneOf (Whitespace, MultilineWhitespace) != null) {
+                anyWhitespace = true;
+            }
+            return anyWhitespace ? ParseSuccess : null;
+        }
+
+        protected ParseRule MultiSpaced (ParseRule rule)
+        {
+            return () => {
+
+                AnyWhitespace ();
+
+                var result = ParseObject (rule);
+                if (result == null) {
+                    return null;
+                }
+
+                AnyWhitespace ();
+
+                return result;
+            };
+        }
+
+		private CharacterSet _inlineWhitespaceChars = new CharacterSet(" \t");
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs.meta
new file mode 100644
index 0000000..28542d4
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkParser/InkParser_Whitespace.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fbdaec262d8f64dd684a79c235405a10
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs b/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs
new file mode 100644
index 0000000..47f07b8
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Ink
+{
+    public static class InkStringConversionExtensions
+    {
+        public static string[] ToStringsArray<T>(this List<T> list) {
+            int count = list.Count;
+            var strings = new string[count];
+
+            for(int i = 0; i < count; i++) {
+                strings[i] = list[i].ToString();
+            }
+
+            return strings;
+        }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs.meta b/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs.meta
new file mode 100644
index 0000000..82021d9
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/InkStringConversionExtensions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 12f91c70015a64a2ba10b2e2fd5d25b0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy.meta
new file mode 100644
index 0000000..3fc7022
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6e3f49d19a4e4473086ad32869ce362f
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs
new file mode 100644
index 0000000..e61805b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs
@@ -0,0 +1,20 @@
+
+namespace Ink.Parsed
+{
+    public class AuthorWarning : Parsed.Object
+    {
+        public string warningMessage;
+
+        public AuthorWarning(string message)
+        {
+            warningMessage = message;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            Warning (warningMessage);
+            return null;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs.meta
new file mode 100644
index 0000000..e4c5ac4
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/AuthorWarning.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 36d3c5151e15a45b4b7a2ac7b355bff9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs
new file mode 100644
index 0000000..4b2934f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs
@@ -0,0 +1,290 @@
+using System.Text;
+
+namespace Ink.Parsed
+{
+    public class Choice : Parsed.Object, IWeavePoint, INamedContent
+	{
+        public ContentList startContent { get; protected set; }
+        public ContentList choiceOnlyContent { get; protected set; }
+        public ContentList innerContent { get; protected set; }
+
+        public string name
+        {
+            get { return identifier?.name; }
+        }
+        public Identifier identifier { get; set; }
+
+        public Expression condition {
+            get {
+                return _condition;
+            }
+            set {
+                _condition = value;
+                if( _condition )
+                    AddContent (_condition);
+            }
+        }
+
+        public bool onceOnly { get; set; }
+        public bool isInvisibleDefault { get; set; }
+
+        public int    indentationDepth { get; set; }// = 1;
+        public bool   hasWeaveStyleInlineBrackets { get; set; }
+
+        // Required for IWeavePoint interface
+        // Choice's target container. Used by weave to append any extra
+        // nested weave content into.
+        public Runtime.Container runtimeContainer { get { return _innerContentContainer; } }
+
+
+        public Runtime.Container innerContentContainer {
+            get {
+                return _innerContentContainer;
+            }
+        }
+
+        public override Runtime.Container containerForCounting {
+            get {
+                return _innerContentContainer;
+            }
+        }
+
+        // Override runtimePath to point to the Choice's target content (after it's chosen),
+        // as opposed to the default implementation which would point to the choice itself
+        // (or it's outer container), which is what runtimeObject is.
+        public override Runtime.Path runtimePath
+        {
+            get {
+                return _innerContentContainer.path;
+            }
+        }
+
+        public Choice (ContentList startContent, ContentList choiceOnlyContent, ContentList innerContent)
+		{
+            this.startContent = startContent;
+            this.choiceOnlyContent = choiceOnlyContent;
+            this.innerContent = innerContent;
+			this.indentationDepth = 1;
+
+            if (startContent)
+                AddContent (this.startContent);
+
+            if (choiceOnlyContent)
+                AddContent (this.choiceOnlyContent);
+
+            if( innerContent )
+                AddContent (this.innerContent);
+
+            this.onceOnly = true; // default
+		}
+
+
+		public override Runtime.Object GenerateRuntimeObject ()
+        {
+            _outerContainer = new Runtime.Container ();
+
+            // Content names for different types of choice:
+            //  * start content [choice only content] inner content
+            //  * start content   -> divert
+            //  * start content
+            //  * [choice only content]
+
+            // Hmm, this structure has become slightly insane!
+            //
+            // [
+            //     EvalStart
+            //     assign $r = $r1   -- return target = return label 1
+            //     BeginString
+            //     -> s
+            //     [(r1)]            -- return label 1 (after start content)
+            //     EndString
+            //     BeginString
+            //     ... choice only content
+            //     EndEval
+            //     Condition expression
+            //     choice: -> "c-0"
+            //     (s) = [
+            //         start content
+            //         -> r          -- goto return label 1 or 2
+            //     ]
+            //  ]
+            //
+            //  in parent's container: (the inner content for the choice)
+            //
+            //  (c-0) = [
+            //      EvalStart
+            //      assign $r = $r2   -- return target = return label 2
+            //      EndEval
+            //      -> s
+            //      [(r2)]            -- return label 1 (after start content)
+            //      inner content
+            //  ]
+            //
+
+            _runtimeChoice = new Runtime.ChoicePoint (onceOnly);
+            _runtimeChoice.isInvisibleDefault = this.isInvisibleDefault;
+
+            if (startContent || choiceOnlyContent || condition) {
+                _outerContainer.AddContent (Runtime.ControlCommand.EvalStart ());
+            }
+
+            // Start content is put into a named container that's referenced both
+            // when displaying the choice initially, and when generating the text
+            // when the choice is chosen.
+            if (startContent) {
+
+                // Generate start content and return
+                //  - We can't use a function since it uses a call stack element, which would
+                //    put temporary values out of scope. Instead we manually divert around.
+                //  - $r is a variable divert target contains the return point
+                _returnToR1 = new Runtime.DivertTargetValue ();
+                _outerContainer.AddContent (_returnToR1);
+                var varAssign = new Runtime.VariableAssignment ("$r", true);
+                _outerContainer.AddContent (varAssign);
+
+                // Mark the start of the choice text generation, so that the runtime
+                // knows where to rewind to to extract the content from the output stream.
+                _outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
+
+                _divertToStartContentOuter = new Runtime.Divert ();
+                _outerContainer.AddContent (_divertToStartContentOuter);
+
+                // Start content itself in a named container
+                _startContentRuntimeContainer = startContent.GenerateRuntimeObject () as Runtime.Container;
+                _startContentRuntimeContainer.name = "s";
+
+                // Effectively, the "return" statement - return to the point specified by $r
+                var varDivert = new Runtime.Divert ();
+                varDivert.variableDivertName = "$r";
+                _startContentRuntimeContainer.AddContent (varDivert);
+
+                // Add the container
+                _outerContainer.AddToNamedContentOnly (_startContentRuntimeContainer);
+
+                // This is the label to return to
+                _r1Label = new Runtime.Container ();
+                _r1Label.name = "$r1";
+                _outerContainer.AddContent (_r1Label);
+
+                _outerContainer.AddContent (Runtime.ControlCommand.EndString ());
+
+                _runtimeChoice.hasStartContent = true;
+            }
+
+            // Choice only content - mark the start, then generate it directly into the outer container
+            if (choiceOnlyContent) {
+                _outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
+
+                var choiceOnlyRuntimeContent = choiceOnlyContent.GenerateRuntimeObject () as Runtime.Container;
+                _outerContainer.AddContentsOfContainer (choiceOnlyRuntimeContent);
+
+                _outerContainer.AddContent (Runtime.ControlCommand.EndString ());
+
+                _runtimeChoice.hasChoiceOnlyContent = true;
+            }
+
+            // Generate any condition for this choice
+            if (condition) {
+                condition.GenerateIntoContainer (_outerContainer);
+                _runtimeChoice.hasCondition = true;
+            }
+
+            if (startContent || choiceOnlyContent || condition) {
+                _outerContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
+            }
+
+            // Add choice itself
+            _outerContainer.AddContent (_runtimeChoice);
+
+            // Container that choice points to for when it's chosen
+            _innerContentContainer = new Runtime.Container ();
+
+            // Repeat start content by diverting to its container
+            if (startContent) {
+
+                // Set the return point when jumping back into the start content
+                //  - In this case, it's the $r2 point, within the choice content "c".
+                _returnToR2 = new Runtime.DivertTargetValue ();
+                _innerContentContainer.AddContent (Runtime.ControlCommand.EvalStart ());
+                _innerContentContainer.AddContent (_returnToR2);
+                _innerContentContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
+                var varAssign = new Runtime.VariableAssignment ("$r", true);
+                _innerContentContainer.AddContent (varAssign);
+
+                // Main divert into start content
+                _divertToStartContentInner = new Runtime.Divert ();
+                _innerContentContainer.AddContent (_divertToStartContentInner);
+
+                // Define label to return to
+                _r2Label = new Runtime.Container ();
+                _r2Label.name = "$r2";
+                _innerContentContainer.AddContent (_r2Label);
+            }
+
+            // Choice's own inner content
+            if (innerContent) {
+				var innerChoiceOnlyContent = innerContent.GenerateRuntimeObject () as Runtime.Container;
+                _innerContentContainer.AddContentsOfContainer (innerChoiceOnlyContent);
+            }
+
+            if (this.story.countAllVisits) {
+                _innerContentContainer.visitsShouldBeCounted = true;
+            }
+
+            _innerContentContainer.countingAtStartOnly = true;
+
+            return _outerContainer;
+		}
+
+        public override void ResolveReferences(Story context)
+		{
+			// Weave style choice - target own content container
+            if (_innerContentContainer) {
+                _runtimeChoice.pathOnChoice = _innerContentContainer.path;
+
+                if (onceOnly)
+                    _innerContentContainer.visitsShouldBeCounted = true;
+            }
+
+            if (_returnToR1)
+                _returnToR1.targetPath = _r1Label.path;
+
+            if (_returnToR2)
+                _returnToR2.targetPath = _r2Label.path;
+
+            if( _divertToStartContentOuter )
+                _divertToStartContentOuter.targetPath = _startContentRuntimeContainer.path;
+
+            if( _divertToStartContentInner )
+                _divertToStartContentInner.targetPath = _startContentRuntimeContainer.path;
+
+            base.ResolveReferences (context);
+
+            if( identifier != null && identifier.name.Length > 0 )
+                context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
+		}
+
+        public override string ToString ()
+        {
+            if (choiceOnlyContent != null) {
+                return string.Format ("* {0}[{1}]...", startContent, choiceOnlyContent);
+            } else {
+                return string.Format ("* {0}...", startContent);
+            }
+        }
+
+        Runtime.ChoicePoint _runtimeChoice;
+        Runtime.Container _innerContentContainer;
+        Runtime.Container _outerContainer;
+        Runtime.Container _startContentRuntimeContainer;
+        Runtime.Divert _divertToStartContentOuter;
+        Runtime.Divert _divertToStartContentInner;
+        Runtime.Container _r1Label;
+        Runtime.Container _r2Label;
+        Runtime.DivertTargetValue _returnToR1;
+        Runtime.DivertTargetValue _returnToR2;
+        Expression _condition;
+	}
+
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs.meta
new file mode 100644
index 0000000..257a1de
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Choice.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 24c93c5d49d7f498f9b658d38f48e0b3
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs
new file mode 100644
index 0000000..2343df2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using Ink.Runtime;
+
+namespace Ink.Parsed
+{
+    public class Conditional : Parsed.Object
+    {
+		public Expression initialCondition { get; private set; }
+		public List<ConditionalSingleBranch> branches { get; private set; }
+        
+        public Conditional (Expression condition, List<ConditionalSingleBranch> branches)
+        {
+            this.initialCondition = condition;
+            if (this.initialCondition) {
+                AddContent (condition);
+            }
+
+            this.branches = branches;
+            if (this.branches != null) {
+                AddContent (this.branches.Cast<Parsed.Object> ().ToList ());
+            }
+
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+
+            // Initial condition
+            if (this.initialCondition) {
+                container.AddContent (initialCondition.runtimeObject);
+            }
+
+            // Individual branches
+            foreach (var branch in branches) {
+                var branchContainer = (Container) branch.runtimeObject;
+                container.AddContent (branchContainer);
+            }
+
+            // If it's a switch-like conditional, each branch
+            // will have a "duplicate" operation for the original
+            // switched value. If there's no final else clause
+            // and we fall all the way through, we need to clean up.
+            // (An else clause doesn't dup but it *does* pop)
+            if (this.initialCondition != null && branches [0].ownExpression != null && !branches [branches.Count - 1].isElse) {
+                container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
+            }
+
+            // Target for branches to rejoin to
+            _reJoinTarget = Runtime.ControlCommand.NoOp ();
+            container.AddContent (_reJoinTarget);
+
+            return container;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            var pathToReJoin = _reJoinTarget.path;
+
+            foreach (var branch in branches) {
+                branch.returnDivert.targetPath = pathToReJoin;
+            }
+
+            base.ResolveReferences (context);
+        }
+            
+        Runtime.ControlCommand _reJoinTarget;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs.meta
new file mode 100644
index 0000000..9f25076
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Conditional.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a6cda85f80c124e12b215fdbc95b33bb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs
new file mode 100644
index 0000000..3f66636
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs
@@ -0,0 +1,158 @@
+
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class ConditionalSingleBranch : Parsed.Object
+    {
+        // bool condition, e.g.:
+        // { 5 == 4:
+        //   - the true branch
+        //   - the false branch
+        // }
+        public bool isTrueBranch { get; set; }
+
+        // When each branch has its own expression like a switch statement,
+        // this is non-null. e.g.
+        // { x:
+        //    - 4: the value of x is four (ownExpression is the value 4)
+        //    - 3: the value of x is three
+        // }
+        public Expression ownExpression { 
+            get { 
+                return _ownExpression; 
+            } 
+            set { 
+                _ownExpression = value; 
+                if (_ownExpression) {
+                    AddContent (_ownExpression); 
+                }
+            }
+        }
+
+        // In the above example, match equality of x with 4 for the first branch.
+        // This is as opposed to simply evaluating boolean equality for each branch,
+        // example when shouldMatchEqualtity is FALSE:
+        // {
+        //    3 > 2:  This will happen
+        //    2 > 3:  This won't happen
+        // }
+        public bool matchingEquality { get; set; }
+
+        public bool isElse { get; set; }
+
+        public bool isInline { get; set; }
+
+        public Runtime.Divert returnDivert { get; protected set; }
+
+        public ConditionalSingleBranch (List<Parsed.Object> content)
+        {
+            // Branches are allowed to be empty
+            if (content != null) {
+                _innerWeave = new Weave (content);
+                AddContent (_innerWeave);
+            }
+        }
+
+        // Runtime content can be summarised as follows:
+        //  - Evaluate an expression if necessary to branch on
+        //  - Branch to a named container if true
+        //       - Divert back to main flow
+        //         (owner Conditional is in control of this target point)
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            // Check for common mistake, of putting "else:" instead of "- else:"
+            if (_innerWeave) {
+                foreach (var c in _innerWeave.content) {
+                    var text = c as Parsed.Text;
+                    if (text) {
+                        // Don't need to trim at the start since the parser handles that already
+                        if (text.text.StartsWith ("else:")) {
+                            Warning ("Saw the text 'else:' which is being treated as content. Did you mean '- else:'?", text);
+                        }
+                    }
+                }
+            }
+                                           
+            var container = new Runtime.Container ();
+
+            // Are we testing against a condition that's used for more than just this
+            // branch? If so, the first thing we need to do is replicate the value that's
+            // on the evaluation stack so that we don't fully consume it, in case other
+            // branches need to use it.
+            bool duplicatesStackValue = matchingEquality && !isElse;
+            if ( duplicatesStackValue )
+                container.AddContent (Runtime.ControlCommand.Duplicate ());
+
+            _conditionalDivert = new Runtime.Divert ();
+
+            // else clause is unconditional catch-all, otherwise the divert is conditional
+            _conditionalDivert.isConditional = !isElse;
+
+            // Need extra evaluation?
+            if( !isTrueBranch && !isElse ) {
+
+                bool needsEval = ownExpression != null;
+                if( needsEval )
+                    container.AddContent (Runtime.ControlCommand.EvalStart ());
+
+                if (ownExpression)
+                    ownExpression.GenerateIntoContainer (container);
+
+                // Uses existing duplicated value
+                if (matchingEquality)
+                    container.AddContent (Runtime.NativeFunctionCall.CallWithName ("=="));
+
+                if( needsEval ) 
+                    container.AddContent (Runtime.ControlCommand.EvalEnd ()); 
+            }
+
+            // Will pop from stack if conditional
+            container.AddContent (_conditionalDivert);
+
+            _contentContainer = GenerateRuntimeForContent ();
+            _contentContainer.name = "b";
+
+            // Multi-line conditionals get a newline at the start of each branch
+            // (as opposed to the start of the multi-line conditional since the condition
+            //  may evaluate to false.)
+            if (!isInline) {
+                _contentContainer.InsertContent (new Runtime.StringValue ("\n"), 0);
+            }
+
+            if( duplicatesStackValue || (isElse && matchingEquality) )
+                _contentContainer.InsertContent (Runtime.ControlCommand.PopEvaluatedValue (), 0);
+
+            container.AddToNamedContentOnly (_contentContainer);
+
+            returnDivert = new Runtime.Divert ();
+            _contentContainer.AddContent (returnDivert);
+
+            return container;
+        }
+
+        Runtime.Container GenerateRuntimeForContent()
+        {
+            // Empty branch - create empty container
+            if (_innerWeave == null) {
+                return new Runtime.Container ();
+            }
+
+            return _innerWeave.rootContainer;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            _conditionalDivert.targetPath = _contentContainer.path;
+
+            base.ResolveReferences (context);
+        }
+
+        Runtime.Container _contentContainer;
+        Runtime.Divert _conditionalDivert;
+        Expression _ownExpression;
+
+        Weave _innerWeave;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs.meta
new file mode 100644
index 0000000..a74605b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConditionalSingleBranch.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5a9e4e9e3b695418f92d259e9d047fb1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs
new file mode 100644
index 0000000..3852bcd
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs
@@ -0,0 +1,46 @@
+//using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class ConstantDeclaration : Parsed.Object
+    {
+        public string constantName
+        {
+            get { return constantIdentifier?.name; }
+        }
+        public Identifier constantIdentifier { get; protected set; }
+        public Expression expression { get; protected set; }
+
+        public ConstantDeclaration (Identifier name, Expression assignedExpression)
+        {
+            this.constantIdentifier = name;
+
+            // Defensive programming in case parsing of assignedExpression failed
+            if( assignedExpression )
+                this.expression = AddContent(assignedExpression);
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            // Global declarations don't generate actual procedural
+            // runtime objects, but instead add a global variable to the story itself.
+            // The story then initialises them all in one go at the start of the game.
+            return null;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            context.CheckForNamingCollisions (this, constantIdentifier, Story.SymbolType.Var);
+        }
+
+        public override string typeName {
+            get {
+                return "Constant";
+            }
+        }
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs.meta
new file mode 100644
index 0000000..284fb56
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ConstantDeclaration.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d6c599566895c444da36111b8aa6bd5e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs
new file mode 100644
index 0000000..737875e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ink.Parsed
+{
+    public class ContentList : Parsed.Object
+    {
+        public bool dontFlatten { get; set; }
+
+        public Runtime.Container runtimeContainer {
+            get {
+                return (Runtime.Container) this.runtimeObject;
+            }
+        }
+
+        public ContentList (List<Parsed.Object> objects)
+        {
+            if( objects != null )
+                AddContent (objects);
+        }
+
+        public ContentList (params Parsed.Object[] objects)
+        {
+            if (objects != null) {
+                var objList = new List<Parsed.Object> (objects);
+                AddContent (objList);
+            }
+        }
+            
+        public ContentList()
+        {
+        }
+
+        public void TrimTrailingWhitespace()
+        {
+            for (int i = this.content.Count - 1; i >= 0; --i) {
+                var text = this.content [i] as Text;
+                if (text == null)
+                    break;
+
+                text.text = text.text.TrimEnd (' ', '\t');
+                if (text.text.Length == 0)
+                    this.content.RemoveAt (i);
+                else
+                    break;
+            }
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+            if (content != null) {
+                foreach (var obj in content) {
+                    var contentObjRuntime = obj.runtimeObject;
+
+                    // Some objects (e.g. author warnings) don't generate runtime objects
+                    if( contentObjRuntime )
+                        container.AddContent (contentObjRuntime);
+                }
+            }
+
+            if( dontFlatten )
+                story.DontFlattenContainer (container);
+
+            return container;
+        }
+
+        public override string ToString ()
+        {
+            var sb = new StringBuilder ();
+            sb.Append ("ContentList(");
+            sb.Append(string.Join (", ", content.ToStringsArray()));
+            sb.Append (")");
+            return sb.ToString ();
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs.meta
new file mode 100644
index 0000000..e6b2817
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ContentList.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d310eb8e3c23e41db891c939a1313cda
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs
new file mode 100644
index 0000000..f87ce8d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs
@@ -0,0 +1,403 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Parsed
+{
+	public class Divert : Parsed.Object
+	{
+		public Parsed.Path target { get; protected set; }
+        public Parsed.Object targetContent { get; protected set; }
+        public List<Expression> arguments { get; protected set; }
+		public Runtime.Divert runtimeDivert { get; protected set; }
+        public bool isFunctionCall { get; set; }
+        public bool isEmpty { get; set; }
+        public bool isTunnel { get; set; }
+        public bool isThread { get; set; }
+        public bool isEnd {
+            get {
+                return target != null && target.dotSeparatedComponents == "END";
+            }
+        }
+        public bool isDone {
+            get {
+                return target != null && target.dotSeparatedComponents == "DONE";
+            }
+        }
+
+        public Divert (Parsed.Path target, List<Expression> arguments = null)
+		{
+			this.target = target;
+            this.arguments = arguments;
+
+            if (arguments != null) {
+                AddContent (arguments.Cast<Parsed.Object> ().ToList ());
+            }
+		}
+
+        public Divert (Parsed.Object targetContent)
+        {
+            this.targetContent = targetContent;
+        }
+
+		public override Runtime.Object GenerateRuntimeObject ()
+		{
+            // End = end flow immediately
+            // Done = return from thread or instruct the flow that it's safe to exit
+            if (isEnd) {
+                return Runtime.ControlCommand.End ();
+            }
+            if (isDone) {
+                return Runtime.ControlCommand.Done ();
+            }
+
+            runtimeDivert = new Runtime.Divert ();
+
+            // Normally we resolve the target content during the
+            // Resolve phase, since we expect all runtime objects to
+            // be available in order to find the final runtime path for
+            // the destination. However, we need to resolve the target
+            // (albeit without the runtime target) early so that
+            // we can get information about the arguments - whether
+            // they're by reference - since it affects the code we
+            // generate here.
+            ResolveTargetContent ();
+
+
+            CheckArgumentValidity ();
+
+            // Passing arguments to the knot
+            bool requiresArgCodeGen = arguments != null && arguments.Count > 0;
+            if ( requiresArgCodeGen || isFunctionCall || isTunnel || isThread ) {
+
+                var container = new Runtime.Container ();
+
+                // Generate code for argument evaluation
+                // This argument generation is coded defensively - it should
+                // attempt to generate the code for all the parameters, even if
+                // they don't match the expected arguments. This is so that the
+                // parameter objects themselves are generated correctly and don't
+                // get into a state of attempting to resolve references etc
+                // without being generated.
+                if (requiresArgCodeGen) {
+
+                    // Function calls already in an evaluation context
+                    if (!isFunctionCall) {
+                        container.AddContent (Runtime.ControlCommand.EvalStart());
+                    }
+
+                    List<FlowBase.Argument> targetArguments = null;
+                    if( targetContent )
+                        targetArguments = (targetContent as FlowBase).arguments;
+
+                    for (var i = 0; i < arguments.Count; ++i) {
+                        Expression argToPass = arguments [i];
+                        FlowBase.Argument argExpected = null;
+                        if( targetArguments != null && i < targetArguments.Count )
+                            argExpected = targetArguments [i];
+
+                        // Pass by reference: argument needs to be a variable reference
+                        if (argExpected != null && argExpected.isByReference) {
+
+                            var varRef = argToPass as VariableReference;
+                            if (varRef == null) {
+                                Error ("Expected variable name to pass by reference to 'ref " + argExpected.identifier + "' but saw " + argToPass.ToString ());
+                                break;
+                            }
+
+                            // Check that we're not attempting to pass a read count by reference
+                            var targetPath = new Path(varRef.pathIdentifiers);
+                            Parsed.Object targetForCount = targetPath.ResolveFromContext (this);
+                            if (targetForCount != null) {
+                                Error ("can't pass a read count by reference. '" + targetPath.dotSeparatedComponents+"' is a knot/stitch/label, but '"+target.dotSeparatedComponents+"' requires the name of a VAR to be passed.");
+                                break;
+                            }
+
+                            var varPointer = new Runtime.VariablePointerValue (varRef.name);
+                            container.AddContent (varPointer);
+                        }
+
+                        // Normal value being passed: evaluate it as normal
+                        else {
+                            argToPass.GenerateIntoContainer (container);
+                        }
+                    }
+
+                    // Function calls were already in an evaluation context
+                    if (!isFunctionCall) {
+                        container.AddContent (Runtime.ControlCommand.EvalEnd());
+                    }
+                }
+
+
+                // Starting a thread? A bit like a push to the call stack below... but not.
+                // It sort of puts the call stack on a thread stack (argh!) - forks the full flow.
+                if (isThread) {
+                    container.AddContent(Runtime.ControlCommand.StartThread());
+                }
+
+                // If this divert is a function call, tunnel, we push to the call stack
+                // so we can return again
+                else if (isFunctionCall || isTunnel) {
+                    runtimeDivert.pushesToStack = true;
+                    runtimeDivert.stackPushType = isFunctionCall ? Runtime.PushPopType.Function : Runtime.PushPopType.Tunnel;
+                }
+
+                // Jump into the "function" (knot/stitch)
+                container.AddContent (runtimeDivert);
+
+                return container;
+            }
+
+            // Simple divert
+            else {
+                return runtimeDivert;
+            }
+		}
+
+
+        // When the divert is to a target that's actually a variable name
+        // rather than an explicit knot/stitch name, try interpretting it
+        // as such by getting the variable name.
+        public string PathAsVariableName()
+        {
+            return target.firstComponent;
+        }
+
+
+        void ResolveTargetContent()
+        {
+            if (isEmpty || isEnd) {
+                return;
+            }
+
+            if (targetContent == null) {
+
+                // Is target of this divert a variable name that will be de-referenced
+                // at runtime? If so, there won't be any further reference resolution
+                // we can do at this point.
+                var variableTargetName = PathAsVariableName ();
+                if (variableTargetName != null) {
+                    var flowBaseScope = ClosestFlowBase ();
+                    var resolveResult = flowBaseScope.ResolveVariableWithName (variableTargetName, fromNode: this);
+                    if (resolveResult.found) {
+
+                        // Make sure that the flow was typed correctly, given that we know that this
+                        // is meant to be a divert target
+                        if (resolveResult.isArgument) {
+                            var argument = resolveResult.ownerFlow.arguments.Where (a => a.identifier.name == variableTargetName).First();
+                            if ( !argument.isDivertTarget ) {
+                                Error ("Since '" + argument.identifier + "' is used as a variable divert target (on "+this.debugMetadata+"), it should be marked as: -> " + argument.identifier, resolveResult.ownerFlow);
+                            }
+                        }
+
+                        runtimeDivert.variableDivertName = variableTargetName;
+                        return;
+
+                    }
+                }
+
+                targetContent = target.ResolveFromContext (this);
+            }
+        }
+
+        public override void ResolveReferences(Story context)
+		{
+            if (isEmpty || isEnd || isDone) {
+                return;
+            }
+
+            if (targetContent) {
+                runtimeDivert.targetPath = targetContent.runtimePath;
+            }
+
+            // Resolve children (the arguments)
+            base.ResolveReferences (context);
+
+            // May be null if it's a built in function (e.g. TURNS_SINCE)
+            // or if it's a variable target.
+            var targetFlow = targetContent as FlowBase;
+            if (targetFlow) {
+                if (!targetFlow.isFunction && this.isFunctionCall) {
+                    base.Error (targetFlow.identifier + " hasn't been marked as a function, but it's being called as one. Do you need to delcare the knot as '== function " + targetFlow.identifier + " =='?");
+                } else if (targetFlow.isFunction && !this.isFunctionCall && !(this.parent is DivertTarget)) {
+                    base.Error (targetFlow.identifier + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + targetFlow.identifier + "(...)'");
+                }
+            }
+
+            // Check validity of target content
+            bool targetWasFound = targetContent != null;
+            bool isBuiltIn = false;
+            bool isExternal = false;
+
+            if (target.numberOfComponents == 1 ) {
+
+                // BuiltIn means TURNS_SINCE, CHOICE_COUNT, RANDOM or SEED_RANDOM
+                isBuiltIn = FunctionCall.IsBuiltIn (target.firstComponent);
+
+                // Client-bound function?
+                isExternal = context.IsExternal (target.firstComponent);
+
+                if (isBuiltIn || isExternal) {
+                    if (!isFunctionCall) {
+                        base.Error (target.firstComponent + " must be called as a function: ~ " + target.firstComponent + "()");
+                    }
+                    if (isExternal) {
+                        runtimeDivert.isExternal = true;
+                        if( arguments != null )
+                            runtimeDivert.externalArgs = arguments.Count;
+                        runtimeDivert.pushesToStack = false;
+                        runtimeDivert.targetPath = new Runtime.Path (this.target.firstComponent);
+                        CheckExternalArgumentValidity (context);
+                    }
+                    return;
+                }
+            }
+
+            // Variable target?
+            if (runtimeDivert.variableDivertName != null) {
+                return;
+            }
+
+            if( !targetWasFound && !isBuiltIn && !isExternal )
+                Error ("target not found: '" + target + "'");
+		}
+
+        // Returns false if there's an error
+        void CheckArgumentValidity()
+        {
+            if (isEmpty)
+                return;
+
+            // Argument passing: Check for errors in number of arguments
+            var numArgs = 0;
+            if (arguments != null && arguments.Count > 0)
+                numArgs = arguments.Count;
+
+            // Missing content?
+            // Can't check arguments properly. It'll be due to some
+            // other error though, so although there's a problem and
+            // we report false, we don't need to report a specific error.
+            // It may also be because it's a valid call to an external
+            // function, that we check at the resolve stage.
+            if (targetContent == null) {
+                return;
+            }
+
+            FlowBase targetFlow = targetContent as FlowBase;
+
+            // No error, crikey!
+            if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters)) {
+                return;
+            }
+
+            if (targetFlow == null && numArgs > 0) {
+                Error ("target needs to be a knot or stitch in order to pass arguments");
+                return;
+            }
+
+            if (targetFlow.arguments == null && numArgs > 0) {
+                Error ("target (" + targetFlow.name + ") doesn't take parameters");
+                return;
+            }
+
+            if( this.parent is DivertTarget ) {
+                if (numArgs > 0)
+                    Error ("can't store arguments in a divert target variable");
+                return;
+            }
+
+            var paramCount = targetFlow.arguments.Count;
+            if (paramCount != numArgs) {
+
+                string butClause;
+                if (numArgs == 0) {
+                    butClause = "but there weren't any passed to it";
+                } else if (numArgs < paramCount) {
+                    butClause = "but only got " + numArgs;
+                } else {
+                    butClause = "but got " + numArgs;
+                }
+                Error ("to '" + targetFlow.identifier + "' requires " + paramCount + " arguments, "+butClause);
+                return;
+            }
+
+            // Light type-checking for divert target arguments
+            for (int i = 0; i < paramCount; ++i) {
+                FlowBase.Argument flowArg = targetFlow.arguments [i];
+                Parsed.Expression divArgExpr = arguments [i];
+
+                // Expecting a divert target as an argument, let's do some basic type checking
+                if (flowArg.isDivertTarget) {
+
+                    // Not passing a divert target or any kind of variable reference?
+                    var varRef = divArgExpr as VariableReference;
+                    if (!(divArgExpr is DivertTarget) && varRef == null ) {
+                        Error ("Target '" + targetFlow.identifier + "' expects a divert target for the parameter named -> " + flowArg.identifier + " but saw " + divArgExpr, divArgExpr);
+                    }
+
+                    // Passing 'a' instead of '-> a'?
+                    // i.e. read count instead of divert target
+                    else if (varRef != null) {
+
+                        // Unfortunately have to manually resolve here since we're still in code gen
+                        var knotCountPath = new Path(varRef.pathIdentifiers);
+                        Parsed.Object targetForCount = knotCountPath.ResolveFromContext (varRef);
+                        if (targetForCount != null) {
+                            Error ("Passing read count of '" + knotCountPath.dotSeparatedComponents + "' instead of a divert target. You probably meant '" + knotCountPath + "'");
+                        }
+                    }
+                }
+            }
+
+            if (targetFlow == null) {
+                Error ("Can't call as a function or with arguments unless it's a knot or stitch");
+                return;
+            }
+
+            return;
+        }
+
+        void CheckExternalArgumentValidity(Story context)
+        {
+            string externalName = target.firstComponent;
+            ExternalDeclaration external = null;
+            var found = context.externals.TryGetValue(externalName, out external);
+            System.Diagnostics.Debug.Assert (found, "external not found");
+
+            int externalArgCount = external.argumentNames.Count;
+            int ownArgCount = 0;
+            if (arguments != null) {
+                ownArgCount = arguments.Count;
+            }
+
+            if (ownArgCount != externalArgCount) {
+                Error ("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + externalArgCount + " but got " + ownArgCount);
+            }
+        }
+
+        public override void Error (string message, Object source = null, bool isWarning = false)
+        {
+            // Could be getting an error from a nested Divert
+            if (source != this && source) {
+                base.Error (message, source);
+                return;
+            }
+
+            if (isFunctionCall) {
+                base.Error ("Function call " + message, source, isWarning);
+            } else {
+                base.Error ("Divert " + message, source, isWarning);
+            }
+        }
+
+        public override string ToString ()
+        {
+            if (target != null)
+                return target.ToString ();
+            else
+                return "-> <empty divert>";
+        }
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs.meta
new file mode 100644
index 0000000..c94ce22
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Divert.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 26c47b19962e641869a39b85cd86f9e1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs
new file mode 100644
index 0000000..c1223a4
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs
@@ -0,0 +1,172 @@
+
+namespace Ink.Parsed
+{
+    public class DivertTarget : Expression
+    {
+        public Divert divert;
+
+        public DivertTarget (Divert divert)
+        {
+            this.divert = AddContent(divert);
+        }
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+        {
+            divert.GenerateRuntimeObject();
+
+            _runtimeDivert = (Runtime.Divert) divert.runtimeDivert;
+            _runtimeDivertTargetValue = new Runtime.DivertTargetValue ();
+
+            container.AddContent (_runtimeDivertTargetValue);
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            if( divert.isDone || divert.isEnd )
+            {
+                Error("Can't Can't use -> DONE or -> END as variable divert targets", this);
+                return;
+            }
+
+            Parsed.Object usageContext = this;
+            while (usageContext && usageContext is Expression) {
+
+                bool badUsage = false;
+                bool foundUsage = false;
+
+                var usageParent = usageContext.parent;
+                if (usageParent is BinaryExpression) {
+
+                    // Only allowed to compare for equality
+
+                    var binaryExprParent = usageParent as BinaryExpression;
+                    if (binaryExprParent.opName != "==" && binaryExprParent.opName != "!=") {
+                        badUsage = true;
+                    } else {
+                        if (!(binaryExprParent.leftExpression is DivertTarget || binaryExprParent.leftExpression is VariableReference)) {
+                            badUsage = true;
+                        }
+                        if (!(binaryExprParent.rightExpression is DivertTarget || binaryExprParent.rightExpression is VariableReference)) {
+                            badUsage = true;
+                        }
+                    }
+                    foundUsage = true;
+                }
+                else if( usageParent is FunctionCall ) {
+                    var funcCall = usageParent as FunctionCall;
+                    if( !funcCall.isTurnsSince && !funcCall.isReadCount ) {
+                        badUsage = true;
+                    }
+                    foundUsage = true;
+                }
+                else if (usageParent is Expression) {
+                    badUsage = true;
+                    foundUsage = true;
+                }
+                else if (usageParent is MultipleConditionExpression) {
+                    badUsage = true;
+                    foundUsage = true;
+                } else if (usageParent is Choice && ((Choice)usageParent).condition == usageContext) {
+                    badUsage = true;
+                    foundUsage = true;
+                } else if (usageParent is Conditional || usageParent is ConditionalSingleBranch) {
+                    badUsage = true;
+                    foundUsage = true;
+                }
+
+                if (badUsage) {
+                    Error ("Can't use a divert target like that. Did you intend to call '" + divert.target + "' as a function: likeThis(), or check the read count: likeThis, with no arrows?", this);
+                }
+
+                if (foundUsage)
+                    break;
+
+                usageContext = usageParent;
+            }
+
+            // Example ink for this case:
+            //
+            //     VAR x = -> blah
+            //
+            // ...which means that "blah" is expected to be a literal stitch  target rather
+            // than a variable name. We can't really intelligently recover from this (e.g. if blah happens to
+            // contain a divert target itself) since really we should be generating a variable reference
+            // rather than a concrete DivertTarget, so we list it as an error.
+            if (_runtimeDivert.hasVariableTarget)
+                Error ("Since '"+divert.target.dotSeparatedComponents+"' is a variable, it shouldn't be preceded by '->' here.");
+
+            // Main resolve
+            _runtimeDivertTargetValue.targetPath = _runtimeDivert.targetPath;
+
+            // Tell hard coded (yet variable) divert targets that they also need to be counted
+            // TODO: Only detect DivertTargets that are values rather than being used directly for
+            // read or turn counts. Should be able to detect this by looking for other uses of containerForCounting
+            var targetContent = this.divert.targetContent;
+            if (targetContent != null ) {
+                var target = targetContent.containerForCounting;
+                if (target != null)
+                {
+                    // Purpose is known: used directly in TURNS_SINCE(-> divTarg)
+                    var parentFunc = this.parent as FunctionCall;
+                    if( parentFunc && parentFunc.isTurnsSince ) {
+                        target.turnIndexShouldBeCounted = true;
+                    }
+
+                    // Unknown purpose, count everything
+                    else {
+                        target.visitsShouldBeCounted = true;
+                        target.turnIndexShouldBeCounted = true;
+                    }
+
+                }
+
+                // Unfortunately not possible:
+                // https://github.com/inkle/ink/issues/538
+                //
+                // VAR func = -> double
+                //
+                // === function double(ref x)
+                //    ~ x = x * 2
+                //
+                // Because when generating the parameters for a function
+                // to be called, it needs to know ahead of time when
+                // compiling whether to pass a variable reference or value.
+                //
+                var targetFlow = (targetContent as FlowBase);
+                if (targetFlow != null && targetFlow.arguments != null)
+                {
+                    foreach(var arg in targetFlow.arguments) {
+                        if(arg.isByReference)
+                        {
+                            Error("Can't store a divert target to a knot or function that has by-reference arguments ('"+targetFlow.identifier+"' has 'ref "+arg.identifier+"').");
+                        }
+                    }
+                }
+            }
+        }
+
+        // Equals override necessary in order to check for CONST multiple definition equality
+        public override bool Equals (object obj)
+        {
+            var otherDivTarget = obj as DivertTarget;
+            if (otherDivTarget == null) return false;
+
+            var targetStr = this.divert.target.dotSeparatedComponents;
+            var otherTargetStr = otherDivTarget.divert.target.dotSeparatedComponents;
+
+            return targetStr.Equals (otherTargetStr);
+        }
+
+        public override int GetHashCode ()
+        {
+            var targetStr = this.divert.target.dotSeparatedComponents;
+            return targetStr.GetHashCode ();
+        }
+
+        Runtime.DivertTargetValue _runtimeDivertTargetValue;
+        Runtime.Divert _runtimeDivert;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs.meta
new file mode 100644
index 0000000..3cd13c9
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/DivertTarget.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d8a428e7434204b02921e47651c42329
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs
new file mode 100644
index 0000000..ad75202
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs
@@ -0,0 +1,307 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Parsed
+{
+	public abstract class Expression : Parsed.Object
+	{
+        public bool outputWhenComplete { get; set; }
+
+		public override Runtime.Object GenerateRuntimeObject ()
+		{
+            var container = new Runtime.Container ();
+
+            // Tell Runtime to start evaluating the following content as an expression
+            container.AddContent (Runtime.ControlCommand.EvalStart());
+
+            GenerateIntoContainer (container);
+
+            // Tell Runtime to output the result of the expression evaluation to the output stream
+            if (outputWhenComplete) {
+                container.AddContent (Runtime.ControlCommand.EvalOutput());
+            }
+
+            // Tell Runtime to stop evaluating the content as an expression
+            container.AddContent (Runtime.ControlCommand.EvalEnd());
+
+            return container;
+		}
+
+        // When generating the value of a constant expression,
+        // we can't just keep generating the same constant expression into
+        // different places where the constant value is referenced, since then
+        // the same runtime objects would be used in multiple places, which
+        // is impossible since each runtime object should have one parent.
+        // Instead, we generate a prototype of the runtime object(s), then
+        // copy them each time they're used.
+        public void GenerateConstantIntoContainer(Runtime.Container container)
+        {
+            if( _prototypeRuntimeConstantExpression == null ) {
+                _prototypeRuntimeConstantExpression = new Runtime.Container ();
+                GenerateIntoContainer (_prototypeRuntimeConstantExpression);
+            }
+
+            foreach (var runtimeObj in _prototypeRuntimeConstantExpression.content) {
+                container.AddContent (runtimeObj.Copy());
+            }
+        }
+
+        public abstract void GenerateIntoContainer (Runtime.Container container);
+
+        Runtime.Container _prototypeRuntimeConstantExpression;
+	}
+
+	public class BinaryExpression : Expression
+	{
+		public Expression leftExpression;
+		public Expression rightExpression;
+		public string opName;
+
+		public BinaryExpression(Expression left, Expression right, string opName)
+		{
+            leftExpression = AddContent(left);
+            rightExpression = AddContent(right);
+			this.opName = opName;
+		}
+
+        public override void GenerateIntoContainer(Runtime.Container container)
+		{
+			leftExpression.GenerateIntoContainer (container);
+			rightExpression.GenerateIntoContainer (container);
+
+            opName = NativeNameForOp (opName);
+
+            container.AddContent(Runtime.NativeFunctionCall.CallWithName(opName));
+		}
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            // Check for the following case:
+            //
+            //    (not A) ? B
+            //
+            // Since this easy to accidentally do:
+            //
+            //    not A ? B
+            //
+            // when you intend:
+            //
+            //    not (A ? B)
+            if (NativeNameForOp (opName) == "?") {
+                var leftUnary = leftExpression as UnaryExpression;
+                if( leftUnary != null && (leftUnary.op == "not" || leftUnary.op == "!") ) {
+                    Error ("Using 'not' or '!' here negates '"+leftUnary.innerExpression+"' rather than the result of the '?' or 'has' operator. You need to add parentheses around the (A ? B) expression.");
+                }
+            }
+        }
+
+        string NativeNameForOp(string opName)
+        {
+            if (opName == "and")
+                return "&&";
+
+            if (opName == "or")
+                return "||";
+
+            if (opName == "mod")
+                return "%";
+
+            if (opName == "has")
+                return "?";
+
+            if (opName == "hasnt")
+                return "!?";
+
+            return opName;
+        }
+
+        public override string ToString ()
+        {
+            return string.Format ("({0} {1} {2})", leftExpression, opName, rightExpression);
+        }
+	}
+
+    public class UnaryExpression : Expression
+	{
+		public Expression innerExpression;
+        public string op;
+
+        // Attempt to flatten inner expression immediately
+        // e.g. convert (-(5)) into (-5)
+        public static Expression WithInner(Expression inner, string op) {
+
+            var innerNumber = inner as Number;
+            if( innerNumber ) {
+
+                if( op == "-" ) {
+                    if( innerNumber.value is int ) {
+                        return new Number( -((int)innerNumber.value) );
+                    } else if( innerNumber.value is float ) {
+                        return new Number( -((float)innerNumber.value) );
+                    }
+                }
+
+                else if( op == "!" || op == "not" ) {
+                    if( innerNumber.value is int ) {
+                        return new Number( (int)innerNumber.value == 0 );
+                    } else if( innerNumber.value is float ) {
+                        return new Number( (float)innerNumber.value == 0.0f );
+                    } else if( innerNumber.value is bool ) {
+                        return new Number( !(bool)innerNumber.value );
+                    }
+                }
+
+                throw new System.Exception ("Unexpected operation or number type");
+            }
+
+            // Normal fallback
+            var unary = new UnaryExpression (inner, op);
+            return unary;
+        }
+
+        public UnaryExpression(Expression inner, string op)
+		{
+            this.innerExpression = AddContent(inner);
+            this.op = op;
+		}
+
+        public override void GenerateIntoContainer(Runtime.Container container)
+		{
+			innerExpression.GenerateIntoContainer (container);
+
+            container.AddContent(Runtime.NativeFunctionCall.CallWithName(nativeNameForOp));
+		}
+
+        public override string ToString ()
+        {
+            return nativeNameForOp + innerExpression;
+        }
+
+        string nativeNameForOp
+        {
+            get {
+                // Replace "-" with "_" to make it unique (compared to subtraction)
+                if (op == "-")
+                    return "_";
+                if (op == "not")
+                    return "!";
+                return op;
+            }
+        }
+	}
+
+    public class IncDecExpression : Expression
+    {
+        public Identifier varIdentifier;
+        public bool isInc;
+        public Expression expression;
+
+        public IncDecExpression(Identifier varIdentifier, bool isInc)
+        {
+            this.varIdentifier = varIdentifier;
+            this.isInc = isInc;
+        }
+
+        public IncDecExpression (Identifier varIdentifier, Expression expression, bool isInc) : this(varIdentifier, isInc)
+        {
+            this.expression = expression;
+            AddContent (expression);
+        }
+
+        public override void GenerateIntoContainer(Runtime.Container container)
+        {
+            // x = x + y
+            // ^^^ ^ ^ ^
+            //  4  1 3 2
+            // Reverse polish notation: (x 1 +) (assign to x)
+
+            // 1.
+            container.AddContent (new Runtime.VariableReference (varIdentifier?.name));
+
+            // 2.
+            // - Expression used in the form ~ x += y
+            // - Simple version: ~ x++
+            if (expression)
+                expression.GenerateIntoContainer (container);
+            else
+                container.AddContent (new Runtime.IntValue (1));
+
+            // 3.
+            container.AddContent (Runtime.NativeFunctionCall.CallWithName (isInc ? "+" : "-"));
+
+            // 4.
+            _runtimeAssignment = new Runtime.VariableAssignment(varIdentifier?.name, false);
+            container.AddContent (_runtimeAssignment);
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            var varResolveResult = context.ResolveVariableWithName(varIdentifier?.name, fromNode: this);
+            if (!varResolveResult.found) {
+                Error ("variable for "+incrementDecrementWord+" could not be found: '"+varIdentifier+"' after searching: "+this.descriptionOfScope);
+            }
+
+            _runtimeAssignment.isGlobal = varResolveResult.isGlobal;
+
+            if (!(parent is Weave) && !(parent is FlowBase) && !(parent is ContentList)) {
+                Error ("Can't use " + incrementDecrementWord + " as sub-expression");
+            }
+        }
+
+        string incrementDecrementWord {
+            get {
+                if (isInc)
+                    return "increment";
+                else
+                    return "decrement";
+            }
+        }
+
+        public override string ToString ()
+        {
+            if (expression)
+                return varIdentifier + (isInc ? " += " : " -= ") + expression.ToString ();
+            else
+                return varIdentifier + (isInc ? "++" : "--");
+        }
+
+        Runtime.VariableAssignment _runtimeAssignment;
+    }
+
+    public class MultipleConditionExpression : Expression
+    {
+        public List<Expression> subExpressions {
+            get {
+                return this.content.Cast<Expression> ().ToList ();
+            }
+        }
+
+        public MultipleConditionExpression(List<Expression> conditionExpressions)
+        {
+            AddContent (conditionExpressions);
+        }
+
+        public override void GenerateIntoContainer(Runtime.Container container)
+        {
+            //    A && B && C && D
+            // => (((A B &&) C &&) D &&) etc
+            bool isFirst = true;
+            foreach (var conditionExpr in subExpressions) {
+
+                conditionExpr.GenerateIntoContainer (container);
+
+                if (!isFirst) {
+                    container.AddContent (Runtime.NativeFunctionCall.CallWithName ("&&"));
+                }
+
+                isFirst = false;
+            }
+        }
+    }
+
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs.meta
new file mode 100644
index 0000000..a2fa495
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Expression.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 476809ca3ec8f4743afd6fa33bd6e442
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs
new file mode 100644
index 0000000..9094500
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class ExternalDeclaration : Parsed.Object, INamedContent
+    {
+        public string name
+        {
+            get { return identifier?.name; }
+        }
+        public Identifier identifier { get; set; }
+        public List<string> argumentNames { get; set; }
+
+        public ExternalDeclaration (Identifier identifier, List<string> argumentNames)
+        {
+            this.identifier = identifier;
+            this.argumentNames = argumentNames;
+        }
+
+        public override Ink.Runtime.Object GenerateRuntimeObject ()
+        {
+            story.AddExternal (this);
+
+            // No runtime code exists for an external, only metadata
+            return null;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs.meta
new file mode 100644
index 0000000..0a5662d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ExternalDeclaration.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ca4476886042f471a9771284a027b45f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs
new file mode 100644
index 0000000..17aca42
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs
@@ -0,0 +1,439 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+	// Base class for Knots and Stitches
+    public abstract class FlowBase : Parsed.Object, INamedContent
+	{
+        public class Argument
+        {
+            public Identifier identifier;
+            public bool isByReference;
+            public bool isDivertTarget;
+        }
+
+        public string name
+        {
+            get { return identifier?.name; }
+        }
+        public Identifier identifier { get; set; }
+        public List<Argument> arguments { get; protected set; }
+        public bool hasParameters { get { return arguments != null && arguments.Count > 0; } }
+        public Dictionary<string, VariableAssignment> variableDeclarations;
+
+        public abstract FlowLevel flowLevel { get; }
+        public bool isFunction { get; protected set; }
+
+        public FlowBase (Identifier name = null, List<Parsed.Object> topLevelObjects = null, List<Argument> arguments = null, bool isFunction = false, bool isIncludedStory = false)
+		{
+			this.identifier = name;
+
+			if (topLevelObjects == null) {
+				topLevelObjects = new List<Parsed.Object> ();
+			}
+
+            // Used by story to add includes
+            PreProcessTopLevelObjects (topLevelObjects);
+
+            topLevelObjects = SplitWeaveAndSubFlowContent (topLevelObjects, isRootStory:this is Story && !isIncludedStory);
+
+            AddContent(topLevelObjects);
+
+            this.arguments = arguments;
+            this.isFunction = isFunction;
+            this.variableDeclarations = new Dictionary<string, VariableAssignment> ();
+		}
+
+        List<Parsed.Object> SplitWeaveAndSubFlowContent(List<Parsed.Object> contentObjs, bool isRootStory)
+        {
+            var weaveObjs = new List<Parsed.Object> ();
+            var subFlowObjs = new List<Parsed.Object> ();
+
+            _subFlowsByName = new Dictionary<string, FlowBase> ();
+
+            foreach (var obj in contentObjs) {
+
+                var subFlow = obj as FlowBase;
+                if (subFlow) {
+                    if (_firstChildFlow == null)
+                        _firstChildFlow = subFlow;
+
+                    subFlowObjs.Add (obj);
+                    _subFlowsByName [subFlow.identifier?.name] = subFlow;
+                } else {
+                    weaveObjs.Add (obj);
+                }
+            }
+
+            // Implicit final gather in top level story for ending without warning that you run out of content
+            if (isRootStory) {
+            	weaveObjs.Add (new Gather (null, 1));
+            	weaveObjs.Add (new Divert (new Path (Identifier.Done)));
+            }
+
+            var finalContent = new List<Parsed.Object> ();
+
+            if (weaveObjs.Count > 0) {
+                _rootWeave = new Weave (weaveObjs, 0);
+                finalContent.Add (_rootWeave);
+            }
+
+            if (subFlowObjs.Count > 0) {
+                finalContent.AddRange (subFlowObjs);
+            }
+
+            return finalContent;
+        }
+
+        protected virtual void PreProcessTopLevelObjects(List<Parsed.Object> topLevelObjects)
+        {
+            // empty by default, used by Story to process included file references
+        }
+
+        public struct VariableResolveResult
+        {
+            public bool found;
+            public bool isGlobal;
+            public bool isArgument;
+            public bool isTemporary;
+            public FlowBase ownerFlow;
+        }
+
+        public VariableResolveResult ResolveVariableWithName(string varName, Parsed.Object fromNode)
+        {
+            var result = new VariableResolveResult ();
+
+            // Search in the stitch / knot that owns the node first
+            var ownerFlow = fromNode == null ? this : fromNode.ClosestFlowBase ();
+
+            // Argument
+            if (ownerFlow.arguments != null ) {
+                foreach (var arg in ownerFlow.arguments) {
+                    if (arg.identifier.name.Equals (varName)) {
+                        result.found = true;
+                        result.isArgument = true;
+                        result.ownerFlow = ownerFlow;
+                        return result;
+                    }
+                }
+            }
+
+            // Temp
+            var story = this.story; // optimisation
+            if (ownerFlow != story && ownerFlow.variableDeclarations.ContainsKey (varName)) {
+                result.found = true;
+                result.ownerFlow = ownerFlow;
+                result.isTemporary = true;
+                return result;
+            }
+
+            // Global
+            if (story.variableDeclarations.ContainsKey (varName)) {
+                result.found = true;
+                result.ownerFlow = story;
+                result.isGlobal = true;
+                return result;
+            }
+
+            result.found = false;
+            return result;
+        }
+
+        public void TryAddNewVariableDeclaration(VariableAssignment varDecl)
+        {
+            var varName = varDecl.variableName;
+            if (variableDeclarations.ContainsKey (varName)) {
+
+                var prevDeclError = "";
+                var debugMetadata = variableDeclarations [varName].debugMetadata;
+                if (debugMetadata != null) {
+                    prevDeclError = " ("+variableDeclarations [varName].debugMetadata+")";
+                }
+                Error("found declaration variable '"+varName+"' that was already declared"+prevDeclError, varDecl, false);
+
+                return;
+            }
+
+            variableDeclarations [varDecl.variableName] = varDecl;
+        }
+
+        public void ResolveWeavePointNaming ()
+        {
+            // Find all weave points and organise them by name ready for
+            // diverting. Also detect naming collisions.
+            if( _rootWeave )
+                _rootWeave.ResolveWeavePointNaming ();
+
+            if (_subFlowsByName != null) {
+                foreach (var namedSubFlow in _subFlowsByName) {
+                    namedSubFlow.Value.ResolveWeavePointNaming ();
+                }
+            }
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            Return foundReturn = null;
+            if (isFunction) {
+                CheckForDisallowedFunctionFlowControl ();
+            }
+
+            // Non-functon: Make sure knots and stitches don't attempt to use Return statement
+            else if( flowLevel == FlowLevel.Knot || flowLevel == FlowLevel.Stitch ) {
+                foundReturn = Find<Return> ();
+                if (foundReturn != null) {
+                    Error ("Return statements can only be used in knots that are declared as functions: == function " + this.identifier + " ==", foundReturn);
+                }
+            }
+
+            var container = new Runtime.Container ();
+            container.name = identifier?.name;
+
+            if( this.story.countAllVisits ) {
+                container.visitsShouldBeCounted = true;
+            }
+
+            GenerateArgumentVariableAssignments (container);
+
+            // Run through content defined for this knot/stitch:
+            //  - First of all, any initial content before a sub-stitch
+            //    or any weave content is added to the main content container
+            //  - The first inner knot/stitch is automatically entered, while
+            //    the others are only accessible by an explicit divert
+            //       - The exception to this rule is if the knot/stitch takes
+            //         parameters, in which case it can't be auto-entered.
+            //  - Any Choices and Gathers (i.e. IWeavePoint) found are
+            //    processsed by GenerateFlowContent.
+            int contentIdx = 0;
+            while (content != null && contentIdx < content.Count) {
+
+                Parsed.Object obj = content [contentIdx];
+
+                // Inner knots and stitches
+                if (obj is FlowBase) {
+
+                    var childFlow = (FlowBase)obj;
+
+                    var childFlowRuntime = childFlow.runtimeObject;
+
+                    // First inner stitch - automatically step into it
+                    // 20/09/2016 - let's not auto step into knots
+                    if (contentIdx == 0 && !childFlow.hasParameters
+                        && this.flowLevel == FlowLevel.Knot) {
+                        _startingSubFlowDivert = new Runtime.Divert ();
+                        container.AddContent(_startingSubFlowDivert);
+                        _startingSubFlowRuntime = childFlowRuntime;
+                    }
+
+                    // Check for duplicate knots/stitches with same name
+                    var namedChild = (Runtime.INamedContent)childFlowRuntime;
+                    Runtime.INamedContent existingChild = null;
+                    if (container.namedContent.TryGetValue(namedChild.name, out existingChild) ) {
+                        var errorMsg = string.Format ("{0} already contains flow named '{1}' (at {2})",
+                            this.GetType().Name,
+                            namedChild.name,
+                            (existingChild as Runtime.Object).debugMetadata);
+
+                        Error (errorMsg, childFlow);
+                    }
+
+                    container.AddToNamedContentOnly (namedChild);
+                }
+
+                // Other content (including entire Weaves that were grouped in the constructor)
+                // At the time of writing, all FlowBases have a maximum of one piece of "other content"
+                // and it's always the root Weave
+                else {
+                    container.AddContent (obj.runtimeObject);
+                }
+
+                contentIdx++;
+            }
+
+            // CHECK FOR FINAL LOOSE ENDS!
+            // Notes:
+            //  - Functions don't need to terminate - they just implicitly return
+            //  - If return statement was found, don't continue finding warnings for missing control flow,
+            // since it's likely that a return statement has been used instead of a ->-> or something,
+            // or the writer failed to mark the knot as a function.
+            //  - _rootWeave may be null if it's a knot that only has stitches
+            if (flowLevel != FlowLevel.Story && !this.isFunction && _rootWeave != null && foundReturn == null) {
+                _rootWeave.ValidateTermination (WarningInTermination);
+            }
+
+            return container;
+        }
+
+        void GenerateArgumentVariableAssignments(Runtime.Container container)
+        {
+            if (this.arguments == null || this.arguments.Count == 0) {
+                return;
+            }
+
+            // Assign parameters in reverse since they'll be popped off the evaluation stack
+            // No need to generate EvalStart and EvalEnd since there's nothing being pushed
+            // back onto the evaluation stack.
+            for (int i = arguments.Count - 1; i >= 0; --i) {
+                var paramName = arguments [i].identifier?.name;
+
+                var assign = new Runtime.VariableAssignment (paramName, isNewDeclaration:true);
+                container.AddContent (assign);
+            }
+        }
+
+        public Parsed.Object ContentWithNameAtLevel(string name, FlowLevel? level = null, bool deepSearch = false)
+        {
+            // Referencing self?
+            if (level == this.flowLevel || level == null) {
+                if (name == this.identifier?.name) {
+                    return this;
+                }
+            }
+
+            if ( level == FlowLevel.WeavePoint || level == null ) {
+
+                Parsed.Object weavePointResult = null;
+
+                if (_rootWeave) {
+                    weavePointResult = (Parsed.Object)_rootWeave.WeavePointNamed (name);
+                    if (weavePointResult)
+                        return weavePointResult;
+                }
+
+                // Stop now if we only wanted a result if it's a weave point?
+                if (level == FlowLevel.WeavePoint)
+                    return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
+            }
+
+            // If this flow would be incapable of containing the requested level, early out
+            // (e.g. asking for a Knot from a Stitch)
+            if (level != null && level < this.flowLevel)
+                return null;
+
+            FlowBase subFlow = null;
+
+            if (_subFlowsByName.TryGetValue (name, out subFlow)) {
+                if (level == null || level == subFlow.flowLevel)
+                    return subFlow;
+            }
+
+            return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
+        }
+
+        Parsed.Object DeepSearchForAnyLevelContent(string name)
+        {
+            var weaveResultSelf = ContentWithNameAtLevel (name, level:FlowLevel.WeavePoint, deepSearch: false);
+            if (weaveResultSelf) {
+                return weaveResultSelf;
+            }
+
+            foreach (var subFlowNamePair in _subFlowsByName) {
+                var subFlow = subFlowNamePair.Value;
+                var deepResult = subFlow.ContentWithNameAtLevel (name, level:null, deepSearch: true);
+                if (deepResult)
+                    return deepResult;
+            }
+
+            return null;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            if (_startingSubFlowDivert) {
+                _startingSubFlowDivert.targetPath = _startingSubFlowRuntime.path;
+            }
+
+            base.ResolveReferences(context);
+
+            // Check validity of parameter names
+            if (arguments != null) {
+
+                foreach (var arg in arguments)
+                    context.CheckForNamingCollisions (this, arg.identifier, Story.SymbolType.Arg, "argument");
+
+                // Separately, check for duplicate arugment names, since they aren't Parsed.Objects,
+                // so have to be checked independently.
+                for (int i = 0; i < arguments.Count; i++) {
+                    for (int j = i + 1; j < arguments.Count; j++) {
+                        if (arguments [i].identifier?.name == arguments [j].identifier?.name) {
+                            Error ("Multiple arguments with the same name: '" + arguments [i].identifier + "'");
+                        }
+                    }
+                }
+            }
+
+            // Check naming collisions for knots and stitches
+            if (flowLevel != FlowLevel.Story) {
+                // Weave points aren't FlowBases, so this will only be knot or stitch
+                var symbolType = flowLevel == FlowLevel.Knot ? Story.SymbolType.Knot : Story.SymbolType.SubFlowAndWeave;
+                context.CheckForNamingCollisions (this, identifier, symbolType);
+            }
+        }
+
+        void CheckForDisallowedFunctionFlowControl()
+        {
+            if (!(this is Knot)) {
+                Error ("Functions cannot be stitches - i.e. they should be defined as '== function myFunc ==' rather than public to another knot.");
+            }
+
+            // Not allowed sub-flows
+            foreach (var subFlowAndName in _subFlowsByName) {
+                var name = subFlowAndName.Key;
+                var subFlow = subFlowAndName.Value;
+                Error ("Functions may not contain stitches, but saw '"+name+"' within the function '"+this.identifier+"'", subFlow);
+            }
+
+            var allDiverts = _rootWeave.FindAll<Divert> ();
+            foreach (var divert in allDiverts) {
+                if( !divert.isFunctionCall && !(divert.parent is DivertTarget) )
+                    Error ("Functions may not contain diverts, but saw '"+divert.ToString()+"'", divert);
+            }
+
+            var allChoices = _rootWeave.FindAll<Choice> ();
+            foreach (var choice in allChoices) {
+                Error ("Functions may not contain choices, but saw '"+choice.ToString()+"'", choice);
+            }
+        }
+
+        void WarningInTermination(Parsed.Object terminatingObject)
+        {
+            string message = "Apparent loose end exists where the flow runs out. Do you need a '-> DONE' statement, choice or divert?";
+            if (terminatingObject.parent == _rootWeave && _firstChildFlow) {
+                message = message + " Note that if you intend to enter '"+_firstChildFlow.identifier+"' next, you need to divert to it explicitly.";
+            }
+
+            var terminatingDivert = terminatingObject as Divert;
+            if (terminatingDivert && terminatingDivert.isTunnel) {
+                message = message + " When final tunnel to '"+terminatingDivert.target+" ->' returns it won't have anywhere to go.";
+            }
+
+            Warning (message, terminatingObject);
+        }
+
+        protected Dictionary<string, FlowBase> subFlowsByName {
+            get {
+                return _subFlowsByName;
+            }
+        }
+
+        public override string typeName {
+            get {
+                if (isFunction) return "Function";
+                else return flowLevel.ToString ();
+            }
+        }
+
+        public override string ToString ()
+        {
+            return typeName+" '" + identifier + "'";
+        }
+
+        Weave _rootWeave;
+        Dictionary<string, FlowBase> _subFlowsByName;
+        Runtime.Divert _startingSubFlowDivert;
+        Runtime.Object _startingSubFlowRuntime;
+        FlowBase _firstChildFlow;
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs.meta
new file mode 100644
index 0000000..b1b5fcc
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowBase.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 573f03d14273b46a29e16c5ff9ec8059
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs
new file mode 100644
index 0000000..5bbdce0
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs
@@ -0,0 +1,11 @@
+namespace Ink.Parsed
+{
+    public enum FlowLevel
+    {
+        Story,
+        Knot,
+        Stitch,
+        WeavePoint // not actually a FlowBase, but used for diverts
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs.meta
new file mode 100644
index 0000000..63c122a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FlowLevel.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b3e0618d06b104e3a82b27f11b68ad54
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs
new file mode 100644
index 0000000..d3d40f5
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs
@@ -0,0 +1,242 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class FunctionCall : Expression
+    {
+        public string name { get { return _proxyDivert.target.firstComponent; } }
+        public Divert proxyDivert { get { return _proxyDivert; } }
+        public List<Expression> arguments { get { return _proxyDivert.arguments; } }
+        public Runtime.Divert runtimeDivert { get { return _proxyDivert.runtimeDivert; } }
+        public bool isChoiceCount { get { return name == "CHOICE_COUNT"; } }
+        public bool isTurns { get { return name == "TURNS"; } }
+        public bool isTurnsSince { get { return name == "TURNS_SINCE"; } }
+        public bool isRandom { get { return name == "RANDOM"; } }
+        public bool isSeedRandom { get { return name == "SEED_RANDOM"; } }
+        public bool isListRange { get { return name == "LIST_RANGE"; } }
+        public bool isListRandom { get { return name == "LIST_RANDOM"; } }
+        public bool isReadCount { get { return name == "READ_COUNT"; } }
+
+        public bool shouldPopReturnedValue;
+
+        public FunctionCall (Identifier functionName, List<Expression> arguments)
+        {
+            _proxyDivert = new Parsed.Divert(new Path(functionName), arguments);
+            _proxyDivert.isFunctionCall = true;
+            AddContent (_proxyDivert);
+        }
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+        {
+            var foundList = story.ResolveList (name);
+
+            bool usingProxyDivert = false;
+
+            if (isChoiceCount) {
+
+                if (arguments.Count > 0)
+                    Error ("The CHOICE_COUNT() function shouldn't take any arguments");
+
+                container.AddContent (Runtime.ControlCommand.ChoiceCount ());
+
+            } else if (isTurns) {
+
+                if (arguments.Count > 0)
+                    Error ("The TURNS() function shouldn't take any arguments");
+
+                container.AddContent (Runtime.ControlCommand.Turns ());
+
+            } else if (isTurnsSince || isReadCount) {
+
+                var divertTarget = arguments [0] as DivertTarget;
+                var variableDivertTarget = arguments [0] as VariableReference;
+
+                if (arguments.Count != 1 || (divertTarget == null && variableDivertTarget == null)) {
+                    Error ("The " + name + "() function should take one argument: a divert target to the target knot, stitch, gather or choice you want to check. e.g. TURNS_SINCE(-> myKnot)");
+                    return;
+                }
+
+                if (divertTarget) {
+                    _divertTargetToCount = divertTarget;
+                    AddContent (_divertTargetToCount);
+
+                    _divertTargetToCount.GenerateIntoContainer (container);
+                } else {
+                    _variableReferenceToCount = variableDivertTarget;
+                    AddContent (_variableReferenceToCount);
+
+                    _variableReferenceToCount.GenerateIntoContainer (container);
+                }
+
+                if (isTurnsSince)
+                    container.AddContent (Runtime.ControlCommand.TurnsSince ());
+                else
+                    container.AddContent (Runtime.ControlCommand.ReadCount ());
+
+            } else if (isRandom) {
+                if (arguments.Count != 2)
+                    Error ("RANDOM should take 2 parameters: a minimum and a maximum integer");
+
+                // We can type check single values, but not complex expressions
+                for (int arg = 0; arg < arguments.Count; arg++) {
+                    if (arguments [arg] is Number) {
+                        var num = arguments [arg] as Number;
+                        if (!(num.value is int)) {
+                            string paramName = arg == 0 ? "minimum" : "maximum";
+                            Error ("RANDOM's " + paramName + " parameter should be an integer");
+                        }
+                    }
+
+                    arguments [arg].GenerateIntoContainer (container);
+                }
+
+                container.AddContent (Runtime.ControlCommand.Random ());
+
+            } else if (isSeedRandom) {
+                if (arguments.Count != 1)
+                    Error ("SEED_RANDOM should take 1 parameter - an integer seed");
+
+                var num = arguments [0] as Number;
+                if (num && !(num.value is int)) {
+                    Error ("SEED_RANDOM's parameter should be an integer seed");
+                }
+
+                arguments [0].GenerateIntoContainer (container);
+
+                container.AddContent (Runtime.ControlCommand.SeedRandom ());
+
+            } else if (isListRange) {
+                if (arguments.Count != 3)
+                    Error ("LIST_RANGE should take 3 parameters - a list, a min and a max");
+
+                for (int arg = 0; arg < arguments.Count; arg++)
+                    arguments [arg].GenerateIntoContainer (container);
+
+                container.AddContent (Runtime.ControlCommand.ListRange ());
+
+            } else if( isListRandom ) {
+                if (arguments.Count != 1)
+                    Error ("LIST_RANDOM should take 1 parameter - a list");
+
+                arguments [0].GenerateIntoContainer (container);
+
+                container.AddContent (Runtime.ControlCommand.ListRandom ());
+
+            } else if (Runtime.NativeFunctionCall.CallExistsWithName (name)) {
+
+                var nativeCall = Runtime.NativeFunctionCall.CallWithName (name);
+
+                if (nativeCall.numberOfParameters != arguments.Count) {
+                    var msg = name + " should take " + nativeCall.numberOfParameters + " parameter";
+                    if (nativeCall.numberOfParameters > 1)
+                        msg += "s";
+                    Error (msg);
+                }
+
+                for (int arg = 0; arg < arguments.Count; arg++)
+                    arguments [arg].GenerateIntoContainer (container);
+
+                container.AddContent (Runtime.NativeFunctionCall.CallWithName (name));
+            } else if (foundList != null) {
+                if (arguments.Count > 1)
+                    Error ("Can currently only construct a list from one integer (or an empty list from a given list definition)");
+
+                // List item from given int
+                if (arguments.Count == 1) {
+                    container.AddContent (new Runtime.StringValue (name));
+                    arguments [0].GenerateIntoContainer (container);
+                    container.AddContent (Runtime.ControlCommand.ListFromInt ());
+                }
+
+                // Empty list with given origin.
+                else {
+                    var list = new Runtime.InkList ();
+                    list.SetInitialOriginName (name);
+                    container.AddContent (new Runtime.ListValue (list));
+                }
+            }
+
+            // Normal function call
+            else {
+                container.AddContent (_proxyDivert.runtimeObject);
+                usingProxyDivert = true;
+            }
+
+            // Don't attempt to resolve as a divert if we're not doing a normal function call
+            if( !usingProxyDivert ) content.Remove (_proxyDivert);
+
+            // Function calls that are used alone on a tilda-based line:
+            //  ~ func()
+            // Should tidy up any returned value from the evaluation stack,
+            // since it's unused.
+            if (shouldPopReturnedValue)
+                container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            // If we aren't using the proxy divert after all (e.g. if
+            // it's a native function call), but we still have arguments,
+            // we need to make sure they get resolved since the proxy divert
+            // is no longer in the content array.
+            if (!content.Contains(_proxyDivert) && arguments != null) {
+                foreach (var arg in arguments)
+                    arg.ResolveReferences (context);
+            }
+
+            if( _divertTargetToCount ) {
+                var divert = _divertTargetToCount.divert;
+                var attemptingTurnCountOfVariableTarget = divert.runtimeDivert.variableDivertName != null;
+
+                if( attemptingTurnCountOfVariableTarget ) {
+                    Error("When getting the TURNS_SINCE() of a variable target, remove the '->' - i.e. it should just be TURNS_SINCE("+divert.runtimeDivert.variableDivertName+")");
+                    return;
+                }
+
+                var targetObject = divert.targetContent;
+                if( targetObject == null ) {
+                    if( !attemptingTurnCountOfVariableTarget ) {
+                        Error("Failed to find target for TURNS_SINCE: '"+divert.target+"'");
+                    }
+                } else {
+                    targetObject.containerForCounting.turnIndexShouldBeCounted = true;
+                }
+            }
+
+            else if( _variableReferenceToCount ) {
+                var runtimeVarRef = _variableReferenceToCount.runtimeVarRef;
+                if( runtimeVarRef.pathForCount != null ) {
+                    Error("Should be "+name+"(-> "+_variableReferenceToCount.name+"). Usage without the '->' only makes sense for variable targets.");
+                }
+            }
+        }
+
+        public static bool IsBuiltIn(string name)
+        {
+            if (Runtime.NativeFunctionCall.CallExistsWithName (name))
+                return true;
+
+            return name == "CHOICE_COUNT"
+                || name == "TURNS_SINCE"
+                || name == "TURNS"
+                || name == "RANDOM"
+                || name == "SEED_RANDOM"
+                || name == "LIST_VALUE"
+                || name == "LIST_RANDOM"
+                || name == "READ_COUNT";
+        }
+
+        public override string ToString ()
+        {
+            var strArgs = string.Join (", ", arguments.ToStringsArray());
+            return string.Format ("{0}({1})", name, strArgs);
+        }
+
+        Parsed.Divert _proxyDivert;
+        Parsed.DivertTarget _divertTargetToCount;
+        Parsed.VariableReference _variableReferenceToCount;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs.meta
new file mode 100644
index 0000000..769b057
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/FunctionCall.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1308e59f4c6e3430797ed5278369c5a5
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs
new file mode 100644
index 0000000..ca86acf
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs
@@ -0,0 +1,51 @@
+
+namespace Ink.Parsed
+{
+    public class Gather : Parsed.Object, IWeavePoint, INamedContent
+    {
+        public string name
+        {
+            get { return identifier?.name; }
+        }
+        public Identifier identifier { get; set; }
+        public int indentationDepth { get; protected set; }
+
+        public Runtime.Container runtimeContainer { get { return (Runtime.Container) runtimeObject; } }
+
+        public Gather (Identifier identifier, int indentationDepth)
+        {
+            this.identifier = identifier;
+            this.indentationDepth = indentationDepth;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+            container.name = name;
+
+            if (this.story.countAllVisits) {
+                container.visitsShouldBeCounted = true;
+            }
+
+            container.countingAtStartOnly = true;
+
+            // A gather can have null content, e.g. it's just purely a line with "-"
+            if (content != null) {
+                foreach (var c in content) {
+                    container.AddContent (c.runtimeObject);
+                }
+            }
+
+            return container;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            if( identifier != null && identifier.name.Length > 0 )
+                context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs.meta
new file mode 100644
index 0000000..a81a3f6
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Gather.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c820ea7ecd7524dfa9c467fa4e72e054
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs
new file mode 100644
index 0000000..fa14449
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs
@@ -0,0 +1,9 @@
+
+namespace Ink.Parsed
+{
+    public interface INamedContent
+    {
+        string name { get; }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs.meta
new file mode 100644
index 0000000..45b0920
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/INamedContent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 262d1f7783ec640e4b3aeef875166a20
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs
new file mode 100644
index 0000000..4155823
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public interface IWeavePoint
+    {
+        int indentationDepth { get; }
+        Runtime.Container runtimeContainer { get; }
+        List<Parsed.Object> content { get; }
+        string name { get; }
+        Identifier identifier { get; }
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs.meta
new file mode 100644
index 0000000..426c827
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IWeavePoint.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c7b0b5c8842a4c17ba25edb46903749
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs
new file mode 100644
index 0000000..5f76529
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs
@@ -0,0 +1,13 @@
+namespace Ink.Parsed {
+    public class Identifier {
+        public string name;
+        public Runtime.DebugMetadata debugMetadata;
+
+        public override string ToString()
+        {
+            return name;
+        }
+
+        public static Identifier Done = new Identifier { name = "DONE", debugMetadata = null };
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs.meta
new file mode 100644
index 0000000..44d4d62
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Identifier.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 50e035c7d009a254c8d832c3863be7b6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs
new file mode 100644
index 0000000..5eee99b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs
@@ -0,0 +1,20 @@
+
+namespace Ink.Parsed
+{
+    public class IncludedFile : Parsed.Object
+    {
+        public Parsed.Story includedStory { get; private set; }
+
+        public IncludedFile (Parsed.Story includedStory)
+        {
+            this.includedStory = includedStory;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            // Left to the main story to process
+            return null;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs.meta
new file mode 100644
index 0000000..21d3f50
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/IncludedFile.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d9cbcf6c1e16545b2be5a72f4064d93b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs
new file mode 100644
index 0000000..a39a44c
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+	public class Knot : FlowBase
+	{
+        public override FlowLevel flowLevel { get { return FlowLevel.Knot; } }
+
+        public Knot (Identifier name, List<Parsed.Object> topLevelObjects, List<Argument> arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction)
+		{
+		}
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            var parentStory = this.story;
+
+            // Enforce rule that stitches must not have the same
+            // name as any knots that exist in the story
+            foreach (var stitchNamePair in subFlowsByName) {
+                var stitchName = stitchNamePair.Key;
+
+                var knotWithStitchName = parentStory.ContentWithNameAtLevel (stitchName, FlowLevel.Knot, false);
+                if (knotWithStitchName) {
+                    var stitch = stitchNamePair.Value;
+                    var errorMsg = string.Format ("Stitch '{0}' has the same name as a knot (on {1})", stitch.identifier, knotWithStitchName.debugMetadata);
+                    Error(errorMsg, stitch);
+                }
+            }
+        }
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs.meta
new file mode 100644
index 0000000..a68d0af
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Knot.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 004df9f5aa1804ff1b359d44aa670019
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs
new file mode 100644
index 0000000..db48d85
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class List : Parsed.Expression
+    {
+        public List<Identifier> itemIdentifierList;
+
+        public List (List<Identifier> itemIdentifierList)
+        {
+            this.itemIdentifierList = itemIdentifierList;
+        }
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+        {
+            var runtimeRawList = new Runtime.InkList ();
+
+            if (itemIdentifierList != null) {
+                foreach (var itemIdentifier in itemIdentifierList) {
+                    var nameParts = itemIdentifier?.name.Split ('.');
+
+                    string listName = null;
+                    string listItemName = null;
+                    if (nameParts.Length > 1) {
+                        listName = nameParts [0];
+                        listItemName = nameParts [1];
+                    } else {
+                        listItemName = nameParts [0];
+                    }
+
+                    var listItem = story.ResolveListItem (listName, listItemName, this);
+                    if (listItem == null) {
+                        if (listName == null)
+                            Error ("Could not find list definition that contains item '" + itemIdentifier + "'");
+                        else
+                            Error ("Could not find list item " + itemIdentifier);
+                    } else {
+                        if (listName == null)
+                            listName = ((ListDefinition)listItem.parent).identifier?.name;
+                        var item = new Runtime.InkListItem (listName, listItem.name);
+
+                        if (runtimeRawList.ContainsKey (item))
+                            Warning ("Duplicate of item '"+itemIdentifier+"' in list.");
+                        else
+                            runtimeRawList [item] = listItem.seriesValue;
+                    }
+                }
+            }
+
+            container.AddContent(new Runtime.ListValue (runtimeRawList));
+        }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs.meta
new file mode 100644
index 0000000..63ddda9
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/List.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 672f1903584d34411b719aea4d59f1b7
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs
new file mode 100644
index 0000000..2010d1a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Parsed
+{
+    public class ListDefinition : Parsed.Object
+    {
+        public Identifier identifier;
+        public List<ListElementDefinition> itemDefinitions;
+
+        public VariableAssignment variableAssignment;
+
+        public Runtime.ListDefinition runtimeListDefinition {
+            get {
+                var allItems = new Dictionary<string, int> ();
+                foreach (var e in itemDefinitions) {
+                    if( !allItems.ContainsKey(e.name) )
+                        allItems.Add (e.name, e.seriesValue);
+                    else
+                        Error("List '"+identifier+"' contains dupicate items called '"+e.name+"'");
+                }
+
+                return new Runtime.ListDefinition (identifier?.name, allItems);
+            }
+        }
+
+        public ListElementDefinition ItemNamed (string itemName)
+        {
+            if (_elementsByName == null) {
+                _elementsByName = new Dictionary<string, ListElementDefinition> ();
+                foreach (var el in itemDefinitions) {
+                    _elementsByName [el.name] = el;
+                }
+            }
+
+            ListElementDefinition foundElement;
+            if (_elementsByName.TryGetValue (itemName, out foundElement))
+                return foundElement;
+
+            return null;
+        }
+
+        public ListDefinition (List<ListElementDefinition> elements)
+        {
+            this.itemDefinitions = elements;
+
+            int currentValue = 1;
+            foreach (var e in this.itemDefinitions) {
+                if (e.explicitValue != null)
+                    currentValue = e.explicitValue.Value;
+
+                e.seriesValue = currentValue;
+
+                currentValue++;
+            }
+
+            AddContent (elements);
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var initialValues = new Runtime.InkList ();
+            foreach (var itemDef in itemDefinitions) {
+                if (itemDef.inInitialList) {
+                    var item = new Runtime.InkListItem (this.identifier?.name, itemDef.name);
+                    initialValues [item] = itemDef.seriesValue;
+                }
+            }
+
+            // Set origin name, so
+            initialValues.SetInitialOriginName (identifier?.name);
+
+            return new Runtime.ListValue (initialValues);
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            context.CheckForNamingCollisions (this, identifier, Story.SymbolType.List);
+        }
+
+        public override string typeName {
+            get {
+                return "List definition";
+            }
+        }
+
+        Dictionary<string, ListElementDefinition> _elementsByName;
+    }
+
+    public class ListElementDefinition : Parsed.Object
+    {
+        public string name
+        {
+            get { return identifier?.name; }
+        }
+        public Identifier identifier;
+        public int? explicitValue;
+        public int seriesValue;
+        public bool inInitialList;
+
+        public string fullName {
+            get {
+                var parentList = parent as ListDefinition;
+                if (parentList == null)
+                    throw new System.Exception ("Can't get full name without a parent list");
+
+                return parentList.identifier + "." + name;
+            }
+        }
+
+        public ListElementDefinition (Identifier identifier, bool inInitialList, int? explicitValue = null)
+        {
+            this.identifier = identifier;
+            this.inInitialList = inInitialList;
+            this.explicitValue = explicitValue;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            throw new System.NotImplementedException ();
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            context.CheckForNamingCollisions (this, identifier, Story.SymbolType.ListItem);
+        }
+
+        public override string typeName {
+        	get {
+                return "List element";
+            }
+        }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs.meta
new file mode 100644
index 0000000..c6dd824
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/ListDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 848e06b169a60427cbf371a506af2b8d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs
new file mode 100644
index 0000000..be67bfd
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs
@@ -0,0 +1,53 @@
+
+namespace Ink.Parsed
+{
+	public class Number : Parsed.Expression
+	{
+		public object value;
+		
+		public Number(object value)
+		{
+            if (value is int || value is float || value is bool) {
+                this.value = value;
+            } else {
+                throw new System.Exception ("Unexpected object type in Number");
+            }
+		}
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+		{
+            if (value is int) {
+                container.AddContent (new Runtime.IntValue ((int)value));
+            } else if (value is float) {
+                container.AddContent (new Runtime.FloatValue ((float)value));
+            } else if(value is bool) {
+                container.AddContent (new Runtime.BoolValue ((bool)value));
+            }
+		}
+
+        public override string ToString ()
+        {
+            if (value is float) {
+                return ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
+            } else {
+                return value.ToString();
+            }
+        }
+
+        // Equals override necessary in order to check for CONST multiple definition equality
+        public override bool Equals (object obj)
+        {
+            var otherNum = obj as Number;
+            if (otherNum == null) return false;
+
+            return this.value.Equals (otherNum.value);
+        }
+
+        public override int GetHashCode ()
+        {
+            return this.value.GetHashCode ();
+        }
+         
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs.meta
new file mode 100644
index 0000000..200d311
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Number.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 193db1f0576f340febab521eb678105e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs
new file mode 100644
index 0000000..381a11e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs
@@ -0,0 +1,363 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ink.Parsed
+{
+	public abstract class Object
+	{
+        public Runtime.DebugMetadata debugMetadata {
+            get {
+                if (_debugMetadata == null) {
+                    if (parent) {
+                        return parent.debugMetadata;
+                    }
+                }
+
+                return _debugMetadata;
+            }
+
+            set {
+                _debugMetadata = value;
+            }
+        }
+        private Runtime.DebugMetadata _debugMetadata;
+
+        public bool hasOwnDebugMetadata {
+            get {
+                return _debugMetadata != null;
+            }
+        }
+
+        public virtual string typeName {
+            get {
+                return GetType().Name;
+            }
+        }
+
+		public Parsed.Object parent { get; set; }
+        public List<Parsed.Object> content { get; protected set; }
+
+        public Parsed.Story story {
+            get {
+                Parsed.Object ancestor = this;
+                while (ancestor.parent) {
+                    ancestor = ancestor.parent;
+                }
+                return ancestor as Parsed.Story;
+            }
+        }
+
+		private Runtime.Object _runtimeObject;
+		public Runtime.Object runtimeObject
+		{
+			get {
+				if (_runtimeObject == null) {
+					_runtimeObject = GenerateRuntimeObject ();
+                    if( _runtimeObject )
+                        _runtimeObject.debugMetadata = debugMetadata;
+				}
+				return _runtimeObject;
+			}
+
+			set {
+				_runtimeObject = value;
+			}
+		}
+
+        // virtual so that certian object types can return a different
+        // path than just the path to the main runtimeObject.
+        // e.g. a Choice returns a path to its content rather than
+        // its outer container.
+        public virtual Runtime.Path runtimePath
+        {
+            get {
+                return runtimeObject.path;
+            }
+        }
+
+        // When counting visits and turns since, different object
+        // types may have different containers that needs to be counted.
+        // For most it'll just be the object's main runtime object,
+        // but for e.g. choices, it'll be the target container.
+        public virtual Runtime.Container containerForCounting
+        {
+            get {
+                return this.runtimeObject as Runtime.Container;
+            }
+        }
+
+        public Parsed.Path PathRelativeTo(Parsed.Object otherObj)
+        {
+            var ownAncestry = ancestry;
+            var otherAncestry = otherObj.ancestry;
+
+            Parsed.Object highestCommonAncestor = null;
+            int minLength = System.Math.Min (ownAncestry.Count, otherAncestry.Count);
+            for (int i = 0; i < minLength; ++i) {
+                var a1 = ancestry [i];
+                var a2 = otherAncestry [i];
+                if (a1 == a2)
+                    highestCommonAncestor = a1;
+                else
+                    break;
+            }
+
+            FlowBase commonFlowAncestor = highestCommonAncestor as FlowBase;
+            if (commonFlowAncestor == null)
+                commonFlowAncestor = highestCommonAncestor.ClosestFlowBase ();
+
+
+            var pathComponents = new List<Identifier> ();
+            bool hasWeavePoint = false;
+            FlowLevel baseFlow = FlowLevel.WeavePoint;
+
+            var ancestor = this;
+            while(ancestor && (ancestor != commonFlowAncestor) && !(ancestor is Story)) {
+
+                if (ancestor == commonFlowAncestor)
+                    break;
+
+                if (!hasWeavePoint) {
+                    var weavePointAncestor = ancestor as IWeavePoint;
+                    if (weavePointAncestor != null && weavePointAncestor.identifier != null) {
+                        pathComponents.Add (weavePointAncestor.identifier);
+                        hasWeavePoint = true;
+                        continue;
+                    }
+                }
+
+                var flowAncestor = ancestor as FlowBase;
+                if (flowAncestor) {
+                    pathComponents.Add (flowAncestor.identifier);
+                    baseFlow = flowAncestor.flowLevel;
+                }
+
+                ancestor = ancestor.parent;
+            }
+
+            pathComponents.Reverse ();
+
+            if (pathComponents.Count > 0) {
+                return new Path (baseFlow, pathComponents);
+            }
+
+            return null;
+        }
+
+        public List<Parsed.Object> ancestry
+        {
+            get {
+                var result = new List<Parsed.Object> ();
+
+                var ancestor = this.parent;
+                while(ancestor) {
+                    result.Add (ancestor);
+                    ancestor = ancestor.parent;
+                }
+
+                result.Reverse ();
+
+                return result;
+            }
+        }
+
+        public string descriptionOfScope
+        {
+            get {
+                var locationNames = new List<string> ();
+
+                Parsed.Object ancestor = this;
+                while (ancestor) {
+                    var ancestorFlow = ancestor as FlowBase;
+                    if (ancestorFlow && ancestorFlow.identifier != null) {
+                        locationNames.Add ("'" + ancestorFlow.identifier + "'");
+                    }
+                    ancestor = ancestor.parent;
+                }
+
+                var scopeSB = new StringBuilder ();
+                if (locationNames.Count > 0) {
+                    var locationsListStr = string.Join (", ", locationNames.ToArray());
+                    scopeSB.Append (locationsListStr);
+                    scopeSB.Append (" and ");
+                }
+
+                scopeSB.Append ("at top scope");
+
+                return scopeSB.ToString ();
+            }
+        }
+
+        // Return the object so that method can be chained easily
+        public T AddContent<T>(T subContent) where T : Parsed.Object
+        {
+            if (content == null) {
+                content = new List<Parsed.Object> ();
+            }
+
+            // Make resilient to content not existing, which can happen
+            // in the case of parse errors where we've already reported
+            // an error but still want a valid structure so we can
+            // carry on parsing.
+            if( subContent ) {
+                subContent.parent = this;
+                content.Add(subContent);
+            }
+
+            return subContent;
+        }
+
+        public void AddContent<T>(List<T> listContent) where T : Parsed.Object
+        {
+            foreach (var obj in listContent) {
+                AddContent (obj);
+            }
+        }
+
+        public T InsertContent<T>(int index, T subContent) where T : Parsed.Object
+        {
+            if (content == null) {
+                content = new List<Parsed.Object> ();
+            }
+
+            subContent.parent = this;
+            content.Insert (index, subContent);
+
+            return subContent;
+        }
+
+        public delegate bool FindQueryFunc<T>(T obj);
+        public T Find<T>(FindQueryFunc<T> queryFunc = null) where T : class
+        {
+            var tObj = this as T;
+            if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
+                return tObj;
+            }
+
+            if (content == null)
+                return null;
+
+            foreach (var obj in content) {
+                var nestedResult = obj.Find (queryFunc);
+                if (nestedResult != null)
+                    return nestedResult;
+            }
+
+            return null;
+        }
+
+
+        public List<T> FindAll<T>(FindQueryFunc<T> queryFunc = null) where T : class
+        {
+            var found = new List<T> ();
+
+            FindAll (queryFunc, found);
+
+            return found;
+        }
+
+        void FindAll<T>(FindQueryFunc<T> queryFunc, List<T> foundSoFar) where T : class
+        {
+            var tObj = this as T;
+            if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
+                foundSoFar.Add (tObj);
+            }
+
+            if (content == null)
+                return;
+
+            foreach (var obj in content) {
+                obj.FindAll (queryFunc, foundSoFar);
+            }
+        }
+
+		public abstract Runtime.Object GenerateRuntimeObject ();
+
+        public virtual void ResolveReferences(Story context)
+		{
+            if (content != null) {
+                foreach(var obj in content) {
+                    obj.ResolveReferences (context);
+                }
+            }
+		}
+
+        public FlowBase ClosestFlowBase()
+        {
+            var ancestor = this.parent;
+            while (ancestor) {
+                if (ancestor is FlowBase) {
+                    return (FlowBase)ancestor;
+                }
+                ancestor = ancestor.parent;
+            }
+
+            return null;
+        }
+
+        public virtual void Error(string message, Parsed.Object source = null, bool isWarning = false)
+		{
+			if (source == null) {
+				source = this;
+			}
+
+            // Only allow a single parsed object to have a single error *directly* associated with it
+            if (source._alreadyHadError && !isWarning) {
+                return;
+            }
+            if (source._alreadyHadWarning && isWarning) {
+                return;
+            }
+
+            if (this.parent) {
+                this.parent.Error (message, source, isWarning);
+            } else {
+                throw new System.Exception ("No parent object to send error to: "+message);
+            }
+
+            if (isWarning) {
+                source._alreadyHadWarning = true;
+            } else {
+                source._alreadyHadError = true;
+            }
+
+		}
+
+        public void Warning(string message, Parsed.Object source = null)
+        {
+            Error (message, source, isWarning: true);
+        }
+
+        // Allow implicit conversion to bool so you don't have to do:
+        // if( myObj != null ) ...
+        public static implicit operator bool (Object obj)
+        {
+            var isNull = object.ReferenceEquals (obj, null);
+            return !isNull;
+        }
+
+        public static bool operator ==(Object a, Object b)
+        {
+            return object.ReferenceEquals (a, b);
+        }
+
+        public static bool operator !=(Object a, Object b)
+        {
+            return !(a == b);
+        }
+
+        public override bool Equals (object obj)
+        {
+            return object.ReferenceEquals (obj, this);
+        }
+
+        public override int GetHashCode ()
+        {
+            return base.GetHashCode ();
+        }
+
+        bool _alreadyHadError;
+        bool _alreadyHadWarning;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs.meta
new file mode 100644
index 0000000..0cc5d6f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Object.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bfde668c261f944e08e08a5b097043ea
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs
new file mode 100644
index 0000000..7a17bd3
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Parsed
+{
+	public class Path
+	{
+        public FlowLevel baseTargetLevel {
+            get {
+                if (baseLevelIsAmbiguous)
+                    return FlowLevel.Story;
+                else
+                    return (FlowLevel) _baseTargetLevel;
+            }
+        }
+
+        public bool baseLevelIsAmbiguous {
+            get {
+                return _baseTargetLevel == null;
+            }
+        }
+
+        public string firstComponent {
+            get {
+                if (components == null || components.Count == 0)
+                    return null;
+
+                return components [0].name;
+            }
+        }
+
+        public int numberOfComponents {
+            get {
+                return components.Count;
+            }
+        }
+
+        public string dotSeparatedComponents {
+            get {
+                if( _dotSeparatedComponents == null ) {
+                    _dotSeparatedComponents = string.Join(".", components.Select(c => c?.name));
+                }
+
+                return _dotSeparatedComponents;
+            }
+        }
+        string _dotSeparatedComponents;
+
+        public List<Identifier> components { get; }
+
+        public Path(FlowLevel baseFlowLevel, List<Identifier> components)
+        {
+            _baseTargetLevel = baseFlowLevel;
+            this.components = components;
+        }
+
+        public Path(List<Identifier> components)
+        {
+            _baseTargetLevel = null;
+            this.components = components;
+        }
+
+        public Path(Identifier ambiguousName)
+        {
+            _baseTargetLevel = null;
+            components = new List<Identifier> ();
+            components.Add (ambiguousName);
+        }
+
+		public override string ToString ()
+		{
+            if (components == null || components.Count == 0) {
+                if (baseTargetLevel == FlowLevel.WeavePoint)
+                    return "-> <next gather point>";
+                else
+                    return "<invalid Path>";
+            }
+
+            return "-> " + dotSeparatedComponents;
+		}
+
+        public Parsed.Object ResolveFromContext(Parsed.Object context)
+        {
+            if (components == null || components.Count == 0) {
+                return null;
+            }
+
+            // Find base target of path from current context. e.g.
+            //   ==> BASE.sub.sub
+            var baseTargetObject = ResolveBaseTarget (context);
+            if (baseTargetObject == null) {
+                return null;
+
+            }
+
+            // Given base of path, resolve final target by working deeper into hierarchy
+            //  e.g. ==> base.mid.FINAL
+            if (components.Count > 1) {
+                return ResolveTailComponents (baseTargetObject);
+            }
+
+            return baseTargetObject;
+        }
+
+        // Find the root object from the base, i.e. root from:
+        //    root.sub1.sub2
+        Parsed.Object ResolveBaseTarget(Parsed.Object originalContext)
+        {
+            var firstComp = firstComponent;
+
+            // Work up the ancestry to find the node that has the named object
+            Parsed.Object ancestorContext = originalContext;
+            while (ancestorContext != null) {
+
+                // Only allow deep search when searching deeper from original context.
+                // Don't allow search upward *then* downward, since that's searching *everywhere*!
+                // Allowed examples:
+                //  - From an inner gather of a stitch, you should search up to find a knot called 'x'
+                //    at the root of a story, but not a stitch called 'x' in that knot.
+                //  - However, from within a knot, you should be able to find a gather/choice
+                //    anywhere called 'x'
+                // (that latter example is quite loose, but we allow it)
+                bool deepSearch = ancestorContext == originalContext;
+
+                var foundBase = TryGetChildFromContext (ancestorContext, firstComp, null, deepSearch);
+                if (foundBase != null)
+                    return foundBase;
+
+                ancestorContext = ancestorContext.parent;
+            }
+
+            return null;
+        }
+
+        // Find the final child from path given root, i.e.:
+        //   root.sub.finalChild
+        Parsed.Object ResolveTailComponents(Parsed.Object rootTarget)
+        {
+            Parsed.Object foundComponent = rootTarget;
+            for (int i = 1; i < components.Count; ++i) {
+                var compName = components [i].name;
+
+                FlowLevel minimumExpectedLevel;
+                var foundFlow = foundComponent as FlowBase;
+                if (foundFlow != null)
+                    minimumExpectedLevel = (FlowLevel)(foundFlow.flowLevel + 1);
+                else
+                    minimumExpectedLevel = FlowLevel.WeavePoint;
+
+
+                foundComponent = TryGetChildFromContext (foundComponent, compName, minimumExpectedLevel);
+                if (foundComponent == null)
+                    break;
+            }
+
+            return foundComponent;
+        }
+
+        // See whether "context" contains a child with a given name at a given flow level
+        // Can either be a named knot/stitch (a FlowBase) or a weave point within a Weave (Choice or Gather)
+        // This function also ignores any other object types that are neither FlowBase nor Weave.
+        // Called from both ResolveBase (force deep) and ResolveTail for the individual components.
+        Parsed.Object TryGetChildFromContext(Parsed.Object context, string childName, FlowLevel? minimumLevel, bool forceDeepSearch = false)
+        {
+            // null childLevel means that we don't know where to find it
+            bool ambiguousChildLevel = minimumLevel == null;
+
+            // Search for WeavePoint within Weave
+            var weaveContext = context as Weave;
+            if ( weaveContext != null && (ambiguousChildLevel || minimumLevel == FlowLevel.WeavePoint)) {
+                return (Parsed.Object) weaveContext.WeavePointNamed (childName);
+            }
+
+            // Search for content within Flow (either a sub-Flow or a WeavePoint)
+            var flowContext = context as FlowBase;
+            if (flowContext != null) {
+
+                // When searching within a Knot, allow a deep searches so that
+                // named weave points (choices and gathers) can be found within any stitch
+                // Otherwise, we just search within the immediate object.
+                var shouldDeepSearch = forceDeepSearch || flowContext.flowLevel == FlowLevel.Knot;
+                return flowContext.ContentWithNameAtLevel (childName, minimumLevel, shouldDeepSearch);
+            }
+
+            return null;
+        }
+
+        FlowLevel? _baseTargetLevel;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs.meta
new file mode 100644
index 0000000..933f6c6
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Path.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cc7b7f981067b4ab7ac3fd451c958785
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs
new file mode 100644
index 0000000..2acb78d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs
@@ -0,0 +1,39 @@
+namespace Ink.Parsed
+{
+    public class Return : Parsed.Object
+    {
+        public Expression returnedExpression { get; protected set; }
+
+        public Return (Expression returnedExpression = null)
+        {            
+            if (returnedExpression) {
+                this.returnedExpression = AddContent(returnedExpression);
+            }
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+
+            // Evaluate expression
+            if (returnedExpression) {
+                container.AddContent (returnedExpression.runtimeObject);
+            } 
+
+            // Return Runtime.Void when there's no expression to evaluate
+            // (This evaluation will just add the Void object to the evaluation stack)
+            else {
+                container.AddContent (Runtime.ControlCommand.EvalStart ());
+                container.AddContent (new Runtime.Void());
+                container.AddContent (Runtime.ControlCommand.EvalEnd ());
+            }
+
+            // Then pop the call stack
+            // (the evaluated expression will leave the return value on the evaluation stack)
+            container.AddContent (Runtime.ControlCommand.PopFunction());
+
+            return container;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs.meta
new file mode 100644
index 0000000..669126d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Return.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4af7a89511e7e44b1b38d2bdafdeaad1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs
new file mode 100644
index 0000000..b628280
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs
@@ -0,0 +1,206 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    [System.Flags]
+    public enum SequenceType
+    {
+        Stopping = 1, // default
+        Cycle = 2,
+        Shuffle = 4,
+        Once = 8
+    }
+
+    public class Sequence : Parsed.Object
+    {
+
+        public List<Parsed.Object> sequenceElements;
+        public SequenceType sequenceType;
+
+        public Sequence (List<ContentList> elementContentLists, SequenceType sequenceType)
+        {
+            this.sequenceType = sequenceType;
+            this.sequenceElements = new List<Parsed.Object> ();
+
+            foreach (var elementContentList in elementContentLists) {
+
+                var contentObjs = elementContentList.content;
+
+                Parsed.Object seqElObject = null;
+
+                // Don't attempt to create a weave for the sequence element 
+                // if the content list is empty. Weaves don't like it!
+                if (contentObjs == null || contentObjs.Count == 0)
+                    seqElObject = elementContentList;
+                else
+                    seqElObject = new Weave (contentObjs);
+                
+                this.sequenceElements.Add (seqElObject);
+                AddContent (seqElObject);
+            }
+        }
+
+        // Generate runtime code that looks like:
+        //
+        //   chosenIndex = MIN(sequence counter, num elements) e.g. for "Stopping"
+        //   if chosenIndex == 0, divert to s0
+        //   if chosenIndex == 1, divert to s1  [etc]
+        //
+        //   - s0:
+        //      <content for sequence element>
+        //      divert to no-op
+        //   - s1:
+        //      <content for sequence element>
+        //      divert to no-op
+        //   - s2:
+        //      empty branch if using "once"
+        //      divert to no-op
+        //
+        //    no-op
+        //
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+            container.visitsShouldBeCounted = true;
+            container.countingAtStartOnly = true;
+
+            _sequenceDivertsToResove = new List<SequenceDivertToResolve> ();
+
+            // Get sequence read count
+            container.AddContent (Runtime.ControlCommand.EvalStart ());
+            container.AddContent (Runtime.ControlCommand.VisitIndex ());
+
+            bool once = (sequenceType & SequenceType.Once) > 0;
+            bool cycle = (sequenceType & SequenceType.Cycle) > 0;
+            bool stopping = (sequenceType & SequenceType.Stopping) > 0;
+            bool shuffle = (sequenceType & SequenceType.Shuffle) > 0;
+
+            var seqBranchCount = sequenceElements.Count;
+            if (once) seqBranchCount++;
+
+            // Chosen sequence index:
+            //  - Stopping: take the MIN(read count, num elements - 1)
+            //  - Once: take the MIN(read count, num elements)
+            //    (the last one being empty)
+            if (stopping || once) {
+                //var limit = stopping ? seqBranchCount-1 : seqBranchCount;
+                container.AddContent (new Runtime.IntValue (seqBranchCount-1));
+                container.AddContent (Runtime.NativeFunctionCall.CallWithName ("MIN"));
+            } 
+
+            // - Cycle: take (read count % num elements)
+            else if (cycle) {
+                container.AddContent (new Runtime.IntValue (sequenceElements.Count));
+                container.AddContent (Runtime.NativeFunctionCall.CallWithName ("%"));
+            }
+
+            // Shuffle
+            if (shuffle) {
+
+                // Create point to return to when sequence is complete
+                var postShuffleNoOp = Runtime.ControlCommand.NoOp();
+
+                // When visitIndex == lastIdx, we skip the shuffle
+                if ( once || stopping )
+                {
+                    // if( visitIndex == lastIdx ) -> skipShuffle
+                    int lastIdx = stopping ? sequenceElements.Count - 1 : sequenceElements.Count;
+                    container.AddContent(Runtime.ControlCommand.Duplicate());
+                    container.AddContent(new Runtime.IntValue(lastIdx));
+                    container.AddContent(Runtime.NativeFunctionCall.CallWithName("=="));
+
+                    var skipShuffleDivert = new Runtime.Divert();
+                    skipShuffleDivert.isConditional = true;
+                    container.AddContent(skipShuffleDivert);
+
+                    AddDivertToResolve(skipShuffleDivert, postShuffleNoOp);
+                }
+
+                // This one's a bit more complex! Choose the index at runtime.
+                var elementCountToShuffle = sequenceElements.Count;
+                if (stopping) elementCountToShuffle--;
+                container.AddContent (new Runtime.IntValue (elementCountToShuffle));
+                container.AddContent (Runtime.ControlCommand.SequenceShuffleIndex ());
+                if (once || stopping) container.AddContent(postShuffleNoOp);
+            }
+
+            container.AddContent (Runtime.ControlCommand.EvalEnd ());
+
+            // Create point to return to when sequence is complete
+            var postSequenceNoOp = Runtime.ControlCommand.NoOp();
+
+            // Each of the main sequence branches, and one extra empty branch if 
+            // we have a "once" sequence.
+            for (var elIndex=0; elIndex<seqBranchCount; elIndex++) {
+
+                // This sequence element:
+                //  if( chosenIndex == this index ) divert to this sequence element
+                // duplicate chosen sequence index, since it'll be consumed by "=="
+                container.AddContent (Runtime.ControlCommand.EvalStart ());
+                container.AddContent (Runtime.ControlCommand.Duplicate ()); 
+                container.AddContent (new Runtime.IntValue (elIndex));
+                container.AddContent (Runtime.NativeFunctionCall.CallWithName ("=="));
+                container.AddContent (Runtime.ControlCommand.EvalEnd ());
+
+                // Divert branch for this sequence element
+                var sequenceDivert = new Runtime.Divert ();
+                sequenceDivert.isConditional = true;
+                container.AddContent (sequenceDivert);
+
+                Runtime.Container contentContainerForSequenceBranch;
+
+                // Generate content for this sequence element
+                if ( elIndex < sequenceElements.Count ) {
+                    var el = sequenceElements[elIndex];
+                    contentContainerForSequenceBranch = (Runtime.Container)el.runtimeObject;
+                } 
+
+                // Final empty branch for "once" sequences
+                else {
+                    contentContainerForSequenceBranch = new Runtime.Container();
+                }
+
+                contentContainerForSequenceBranch.name = "s" + elIndex;
+                contentContainerForSequenceBranch.InsertContent(Runtime.ControlCommand.PopEvaluatedValue(), 0);
+
+                // When sequence element is complete, divert back to end of sequence
+                var seqBranchCompleteDivert = new Runtime.Divert ();
+                contentContainerForSequenceBranch.AddContent (seqBranchCompleteDivert);
+                container.AddToNamedContentOnly (contentContainerForSequenceBranch);
+
+                // Save the diverts for reference resolution later (in ResolveReferences)
+                AddDivertToResolve (sequenceDivert, contentContainerForSequenceBranch);
+                AddDivertToResolve (seqBranchCompleteDivert, postSequenceNoOp);
+            }
+
+            container.AddContent (postSequenceNoOp);
+
+            return container;
+        }
+
+        void AddDivertToResolve(Runtime.Divert divert, Runtime.Object targetContent)
+        {
+            _sequenceDivertsToResove.Add( new SequenceDivertToResolve() { 
+                divert = divert, 
+                targetContent = targetContent
+            });
+        }
+
+        public override void ResolveReferences(Story context)
+        {
+            base.ResolveReferences (context);
+
+            foreach (var toResolve in _sequenceDivertsToResove) {
+                toResolve.divert.targetPath = toResolve.targetContent.path;
+            }
+        }
+
+        class SequenceDivertToResolve
+        {
+            public Runtime.Divert divert;
+            public Runtime.Object targetContent;
+        }
+        List<SequenceDivertToResolve> _sequenceDivertsToResove;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs.meta
new file mode 100644
index 0000000..f2d5b6f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Sequence.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c69e88da4e89a46e7a7f0ae08b5b08e4
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs
new file mode 100644
index 0000000..a4ac723
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+	public class Stitch : FlowBase
+	{
+        public override FlowLevel flowLevel { get { return FlowLevel.Stitch; } }
+
+        public Stitch (Identifier name, List<Parsed.Object> topLevelObjects, List<Argument> arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction)
+		{
+		}
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs.meta
new file mode 100644
index 0000000..19f7e7f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Stitch.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc4bd341820aa4690b360e54951da250
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs
new file mode 100644
index 0000000..cca1b5e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs
@@ -0,0 +1,508 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.CompilerServices;
+using System.Diagnostics;
+
+[assembly: InternalsVisibleTo("tests")]
+
+namespace Ink.Parsed
+{
+	public class Story : FlowBase
+    {
+        public override FlowLevel flowLevel { get { return FlowLevel.Story; } }
+
+        /// <summary>
+        /// Had error during code gen, resolve references?
+        /// Most of the time it shouldn't be necessary to use this
+        /// since errors should be caught by the error handler.
+        /// </summary>
+        internal bool hadError { get { return _hadError; } }
+        internal bool hadWarning { get { return _hadWarning; } }
+
+        public Dictionary<string, Expression> constants;
+        public Dictionary<string, ExternalDeclaration> externals;
+
+        // Build setting for exporting:
+        // When true, the visit count for *all* knots, stitches, choices,
+        // and gathers is counted. When false, only those that are direclty
+        // referenced by the ink are recorded. Use this flag to allow game-side
+        // querying of  arbitrary knots/stitches etc.
+        // Storing all counts is more robust and future proof (updates to the story file
+        // that reference previously uncounted visits are possible, but generates a much
+        // larger safe file, with a lot of potentially redundant counts.
+        public bool countAllVisits = false;
+
+        public Story (List<Parsed.Object> toplevelObjects, bool isInclude = false) : base(null, toplevelObjects, isIncludedStory:isInclude)
+		{
+            // Don't do anything much on construction, leave it lightweight until
+            // the ExportRuntime method is called.
+		}
+
+        // Before this function is called, we have IncludedFile objects interspersed
+        // in our content wherever an include statement was.
+        // So that the include statement can be added in a sensible place (e.g. the
+        // top of the file) without side-effects of jumping into a knot that was
+        // defined in that include, we separate knots and stitches from anything
+        // else defined at the top scope of the included file.
+        //
+        // Algorithm: For each IncludedFile we find, split its contents into
+        // knots/stiches and any other content. Insert the normal content wherever
+        // the include statement was, and append the knots/stitches to the very
+        // end of the main story.
+        protected override void PreProcessTopLevelObjects(List<Parsed.Object> topLevelContent)
+        {
+            var flowsFromOtherFiles = new List<FlowBase> ();
+
+            // Inject included files
+            int i = 0;
+            while (i < topLevelContent.Count) {
+                var obj = topLevelContent [i];
+                if (obj is IncludedFile) {
+
+                    var file = (IncludedFile)obj;
+
+                    // Remove the IncludedFile itself
+                    topLevelContent.RemoveAt (i);
+
+                    // When an included story fails to load, the include
+                    // line itself is still valid, so we have to handle it here
+                    if (file.includedStory) {
+
+                        var nonFlowContent = new List<Parsed.Object> ();
+
+                        var subStory = file.includedStory;
+
+                        // Allow empty file
+                        if (subStory.content != null) {
+
+                            foreach (var subStoryObj in subStory.content) {
+                                if (subStoryObj is FlowBase) {
+                                    flowsFromOtherFiles.Add ((FlowBase)subStoryObj);
+                                } else {
+                                    nonFlowContent.Add (subStoryObj);
+                                }
+                            }
+
+                            // Add newline on the end of the include
+                            nonFlowContent.Add (new Parsed.Text ("\n"));
+
+                            // Add contents of the file in its place
+                            topLevelContent.InsertRange (i, nonFlowContent);
+
+                            // Skip past the content of this sub story
+                            // (since it will already have recursively included
+                            //  any lines from other files)
+                            i += nonFlowContent.Count;
+                        }
+
+                    }
+
+                    // Include object has been removed, with possible content inserted,
+                    // and position of 'i' will have been determined already.
+                    continue;
+                }
+
+                // Non-include: skip over it
+                else {
+                    i++;
+                }
+            }
+
+            // Add the flows we collected from the included files to the
+            // end of our list of our content
+            topLevelContent.AddRange (flowsFromOtherFiles.ToArray());
+
+        }
+
+        public Runtime.Story ExportRuntime(ErrorHandler errorHandler = null)
+		{
+            _errorHandler = errorHandler;
+
+            // Find all constants before main export begins, so that VariableReferences know
+            // whether to generate a runtime variable reference or the literal value
+            constants = new Dictionary<string, Expression> ();
+            foreach (var constDecl in FindAll<ConstantDeclaration> ()) {
+
+                // Check for duplicate definitions
+                Parsed.Expression existingDefinition = null;
+                if (constants.TryGetValue (constDecl.constantName, out existingDefinition)) {
+                    if (!existingDefinition.Equals (constDecl.expression)) {
+                        var errorMsg = string.Format ("CONST '{0}' has been redefined with a different value. Multiple definitions of the same CONST are valid so long as they contain the same value. Initial definition was on {1}.", constDecl.constantName, existingDefinition.debugMetadata);
+                        Error (errorMsg, constDecl, isWarning:false);
+                    }
+                }
+
+                constants [constDecl.constantName] = constDecl.expression;
+            }
+
+            // List definitions are treated like constants too - they should be usable
+            // from other variable declarations.
+            _listDefs = new Dictionary<string, ListDefinition> ();
+            foreach (var listDef in FindAll<ListDefinition> ()) {
+                _listDefs [listDef.identifier?.name] = listDef;
+            }
+
+            externals = new Dictionary<string, ExternalDeclaration> ();
+
+            // Resolution of weave point names has to come first, before any runtime code generation
+            // since names have to be ready before diverts start getting created.
+            // (It used to be done in the constructor for a weave, but didn't allow us to generate
+            // errors when name resolution failed.)
+            ResolveWeavePointNaming ();
+
+            // Get default implementation of runtimeObject, which calls ContainerBase's generation method
+            var rootContainer = runtimeObject as Runtime.Container;
+
+            // Export initialisation of global variables
+            // TODO: We *could* add this as a declarative block to the story itself...
+            var variableInitialisation = new Runtime.Container ();
+            variableInitialisation.AddContent (Runtime.ControlCommand.EvalStart ());
+
+            // Global variables are those that are local to the story and marked as global
+            var runtimeLists = new List<Runtime.ListDefinition> ();
+            foreach (var nameDeclPair in variableDeclarations) {
+                var varName = nameDeclPair.Key;
+                var varDecl = nameDeclPair.Value;
+                if (varDecl.isGlobalDeclaration) {
+
+                    if (varDecl.listDefinition != null) {
+                        _listDefs[varName] = varDecl.listDefinition;
+                        variableInitialisation.AddContent (varDecl.listDefinition.runtimeObject);
+                        runtimeLists.Add (varDecl.listDefinition.runtimeListDefinition);
+                    } else {
+                        varDecl.expression.GenerateIntoContainer (variableInitialisation);
+                    }
+
+                    var runtimeVarAss = new Runtime.VariableAssignment (varName, isNewDeclaration:true);
+                    runtimeVarAss.isGlobal = true;
+                    variableInitialisation.AddContent (runtimeVarAss);
+                }
+            }
+
+            variableInitialisation.AddContent (Runtime.ControlCommand.EvalEnd ());
+            variableInitialisation.AddContent (Runtime.ControlCommand.End ());
+
+            if (variableDeclarations.Count > 0) {
+                variableInitialisation.name = "global decl";
+                rootContainer.AddToNamedContentOnly (variableInitialisation);
+            }
+
+            // Signal that it's safe to exit without error, even if there are no choices generated
+            // (this only happens at the end of top level content that isn't in any particular knot)
+            rootContainer.AddContent (Runtime.ControlCommand.Done ());
+
+			// Replace runtimeObject with Story object instead of the Runtime.Container generated by Parsed.ContainerBase
+            var runtimeStory = new Runtime.Story (rootContainer, runtimeLists);
+
+			runtimeObject = runtimeStory;
+
+            if (_hadError)
+                return null;
+
+            // Optimisation step - inline containers that can be
+            FlattenContainersIn (rootContainer);
+
+			// Now that the story has been fulled parsed into a hierarchy,
+			// and the derived runtime hierarchy has been built, we can
+			// resolve referenced symbols such as variables and paths.
+			// e.g. for paths " -> knotName --> stitchName" into an INKPath (knotName.stitchName)
+			// We don't make any assumptions that the INKPath follows the same
+			// conventions as the script format, so we resolve to actual objects before
+			// translating into an INKPath. (This also allows us to choose whether
+			// we want the paths to be absolute)
+			ResolveReferences (this);
+
+            if (_hadError)
+                return null;
+
+            runtimeStory.ResetState ();
+
+			return runtimeStory;
+		}
+
+        public ListDefinition ResolveList (string listName)
+        {
+            ListDefinition list;
+            if (!_listDefs.TryGetValue (listName, out list))
+                return null;
+            return list;
+        }
+
+        public ListElementDefinition ResolveListItem (string listName, string itemName, Parsed.Object source = null)
+        {
+            ListDefinition listDef = null;
+
+            // Search a specific list if we know its name (i.e. the form listName.itemName)
+            if (listName != null) {
+                if (!_listDefs.TryGetValue (listName, out listDef))
+                    return null;
+
+                return listDef.ItemNamed (itemName);
+            }
+
+            // Otherwise, try to search all lists
+            else {
+
+                ListElementDefinition foundItem = null;
+                ListDefinition originalFoundList = null;
+
+                foreach (var namedList in _listDefs) {
+                    var listToSearch = namedList.Value;
+                    var itemInThisList = listToSearch.ItemNamed (itemName);
+                    if (itemInThisList) {
+                        if (foundItem != null) {
+                            Error ("Ambiguous item name '" + itemName + "' found in multiple sets, including "+originalFoundList.identifier+" and "+listToSearch.identifier, source, isWarning:false);
+                        } else {
+                            foundItem = itemInThisList;
+                            originalFoundList = listToSearch;
+                        }
+                    }
+                }
+
+                return foundItem;
+            }
+        }
+
+        void FlattenContainersIn (Runtime.Container container)
+        {
+            // Need to create a collection to hold the inner containers
+            // because otherwise we'd end up modifying during iteration
+            var innerContainers = new HashSet<Runtime.Container> ();
+
+            foreach (var c in container.content) {
+                var innerContainer = c as Runtime.Container;
+                if (innerContainer)
+                    innerContainers.Add (innerContainer);
+            }
+
+            // Can't flatten the named inner containers, but we can at least
+            // iterate through their children
+            if (container.namedContent != null) {
+                foreach (var keyValue in container.namedContent) {
+                    var namedInnerContainer = keyValue.Value as Runtime.Container;
+                    if (namedInnerContainer)
+                        innerContainers.Add (namedInnerContainer);
+                }
+            }
+
+            foreach (var innerContainer in innerContainers) {
+                TryFlattenContainer (innerContainer);
+                FlattenContainersIn (innerContainer);
+            }
+        }
+
+        void TryFlattenContainer (Runtime.Container container)
+        {
+            if (container.namedContent.Count > 0 || container.hasValidName || _dontFlattenContainers.Contains(container))
+                return;
+
+            // Inline all the content in container into the parent
+            var parentContainer = container.parent as Runtime.Container;
+            if (parentContainer) {
+
+                var contentIdx = parentContainer.content.IndexOf (container);
+                parentContainer.content.RemoveAt (contentIdx);
+
+                var dm = container.ownDebugMetadata;
+
+                foreach (var innerContent in container.content) {
+                    innerContent.parent = null;
+                    if (dm != null && innerContent.ownDebugMetadata == null)
+                        innerContent.debugMetadata = dm;
+                    parentContainer.InsertContent (innerContent, contentIdx);
+                    contentIdx++;
+                }
+            }
+        }
+
+        public override void Error(string message, Parsed.Object source, bool isWarning)
+		{
+            ErrorType errorType = isWarning ? ErrorType.Warning : ErrorType.Error;
+
+            var sb = new StringBuilder ();
+            if (source is AuthorWarning) {
+                sb.Append ("TODO: ");
+                errorType = ErrorType.Author;
+            } else if (isWarning) {
+                sb.Append ("WARNING: ");
+            } else {
+                sb.Append ("ERROR: ");
+            }
+
+            if (source && source.debugMetadata != null && source.debugMetadata.startLineNumber >= 1 ) {
+
+                if (source.debugMetadata.fileName != null) {
+                    sb.AppendFormat ("'{0}' ", source.debugMetadata.fileName);
+                }
+
+                sb.AppendFormat ("line {0}: ", source.debugMetadata.startLineNumber);
+            }
+
+            sb.Append (message);
+
+            message = sb.ToString ();
+
+            if (_errorHandler != null) {
+                _hadError = errorType == ErrorType.Error;
+                _hadWarning = errorType == ErrorType.Warning;
+                _errorHandler (message, errorType);
+            } else {
+                throw new System.Exception (message);
+            }
+		}
+
+        public void ResetError()
+        {
+            _hadError = false;
+            _hadWarning = false;
+        }
+
+        public bool IsExternal(string namedFuncTarget)
+        {
+            return externals.ContainsKey (namedFuncTarget);
+        }
+
+        public void AddExternal(ExternalDeclaration decl)
+        {
+            if (externals.ContainsKey (decl.name)) {
+                Error ("Duplicate EXTERNAL definition of '"+decl.name+"'", decl, false);
+            } else {
+                externals [decl.name] = decl;
+            }
+        }
+
+        public void DontFlattenContainer (Runtime.Container container)
+        {
+            _dontFlattenContainers.Add (container);
+        }
+
+
+
+        void NameConflictError (Parsed.Object obj, string name, Parsed.Object existingObj, string typeNameToPrint)
+        {
+            obj.Error (typeNameToPrint+" '" + name + "': name has already been used for a " + existingObj.typeName.ToLower() + " on " +existingObj.debugMetadata);
+        }
+
+        public static bool IsReservedKeyword (string name)
+        {
+            switch (name) {
+            case "true":
+            case "false":
+            case "not":
+            case "return":
+            case "else":
+            case "VAR":
+            case "CONST":
+            case "temp":
+            case "LIST":
+            case "function":
+                return true;
+            }
+
+            return false;
+        }
+
+        public enum SymbolType : uint
+        {
+        	Knot,
+        	List,
+        	ListItem,
+        	Var,
+        	SubFlowAndWeave,
+        	Arg,
+            Temp
+        }
+
+        // Check given symbol type against everything that's of a higher priority in the ordered SymbolType enum (above).
+        // When the given symbol type level is reached, we early-out / return.
+        public void CheckForNamingCollisions (Parsed.Object obj, Identifier identifier, SymbolType symbolType, string typeNameOverride = null)
+        {
+            string typeNameToPrint = typeNameOverride ?? obj.typeName;
+            if (IsReservedKeyword (identifier?.name)) {
+                obj.Error ("'"+name + "' cannot be used for the name of a " + typeNameToPrint.ToLower() + " because it's a reserved keyword");
+                return;
+            }
+
+            if (FunctionCall.IsBuiltIn (identifier?.name)) {
+                obj.Error ("'"+name + "' cannot be used for the name of a " + typeNameToPrint.ToLower() + " because it's a built in function");
+                return;
+            }
+
+            // Top level knots
+            FlowBase knotOrFunction = ContentWithNameAtLevel (identifier?.name, FlowLevel.Knot) as FlowBase;
+            if (knotOrFunction && (knotOrFunction != obj || symbolType == SymbolType.Arg)) {
+                NameConflictError (obj, identifier?.name, knotOrFunction, typeNameToPrint);
+                return;
+            }
+
+            if (symbolType < SymbolType.List) return;
+
+            // Lists
+            foreach (var namedListDef in _listDefs) {
+                var listDefName = namedListDef.Key;
+                var listDef = namedListDef.Value;
+                if (identifier?.name == listDefName && obj != listDef && listDef.variableAssignment != obj) {
+                    NameConflictError (obj, identifier?.name, listDef, typeNameToPrint);
+                }
+
+                // We don't check for conflicts between individual elements in
+                // different lists because they are namespaced.
+                if (!(obj is ListElementDefinition)) {
+                    foreach (var item in listDef.itemDefinitions) {
+                        if (identifier?.name == item.name) {
+                            NameConflictError (obj, identifier?.name, item, typeNameToPrint);
+                        }
+                    }
+                }
+            }
+
+            // Don't check for VAR->VAR conflicts because that's handled separately
+            // (necessary since checking looks up in a dictionary)
+            if (symbolType <= SymbolType.Var) return;
+
+            // Global variable collision
+            VariableAssignment varDecl = null;
+            if (variableDeclarations.TryGetValue(identifier?.name, out varDecl) ) {
+                if (varDecl != obj && varDecl.isGlobalDeclaration && varDecl.listDefinition == null) {
+                    NameConflictError (obj, identifier?.name, varDecl, typeNameToPrint);
+                }
+            }
+
+            if (symbolType < SymbolType.SubFlowAndWeave) return;
+
+            // Stitches, Choices and Gathers
+            var path = new Path (identifier);
+            var targetContent = path.ResolveFromContext (obj);
+            if (targetContent && targetContent != obj) {
+                NameConflictError (obj, identifier?.name, targetContent, typeNameToPrint);
+                return;
+            }
+
+            if (symbolType < SymbolType.Arg) return;
+
+            // Arguments to the current flow
+            if (symbolType != SymbolType.Arg) {
+				FlowBase flow = obj as FlowBase;
+				if( flow == null ) flow = obj.ClosestFlowBase ();
+				if (flow && flow.hasParameters) {
+					foreach (var arg in flow.arguments) {
+						if (arg.identifier?.name == identifier?.name) {
+							obj.Error (typeNameToPrint+" '" + name + "': Name has already been used for a argument to "+flow.identifier+" on " +flow.debugMetadata);
+							return;
+						}
+					}
+				}
+            }
+        }
+
+        ErrorHandler _errorHandler;
+        bool _hadError;
+        bool _hadWarning;
+
+        HashSet<Runtime.Container> _dontFlattenContainers = new HashSet<Runtime.Container>();
+
+        Dictionary<string, Parsed.ListDefinition> _listDefs;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs.meta
new file mode 100644
index 0000000..a3f2b5d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Story.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7ea84180e05064c52bb98a05546a7ddb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs
new file mode 100644
index 0000000..189222d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ink.Parsed
+{
+    public class StringExpression : Parsed.Expression
+    {
+        public bool isSingleString {
+            get {
+                if (content.Count != 1)
+                    return false;
+
+                var c = content [0];
+                if (!(c is Text))
+                    return false;
+
+                return true;
+            }
+        }
+
+        public StringExpression (List<Parsed.Object> content)
+        {
+            AddContent (content);
+        }
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+        {
+            container.AddContent (Runtime.ControlCommand.BeginString());
+
+            foreach (var c in content) {
+                container.AddContent (c.runtimeObject);
+            }
+                
+            container.AddContent (Runtime.ControlCommand.EndString());
+        }
+
+        public override string ToString ()
+        {
+            var sb = new StringBuilder ();
+            foreach (var c in content) {
+                sb.Append (c.ToString ());
+            }
+            return sb.ToString ();
+        }
+
+        // Equals override necessary in order to check for CONST multiple definition equality
+        public override bool Equals (object obj)
+        {
+            var otherStr = obj as StringExpression;
+            if (otherStr == null) return false;
+
+            // Can only compare direct equality on single strings rather than
+            // complex string expressions that contain dynamic logic
+            if (!this.isSingleString || !otherStr.isSingleString) {
+                return false;
+            }
+
+            var thisTxt = this.ToString ();
+            var otherTxt = otherStr.ToString ();
+            return thisTxt.Equals (otherTxt);
+        }
+
+        public override int GetHashCode ()
+        {
+            return this.ToString ().GetHashCode ();
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs.meta
new file mode 100644
index 0000000..1877a07
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/StringExpression.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f0fdc424d743642aeae05b3b736e4bfb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs
new file mode 100644
index 0000000..f1b59bc
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs
@@ -0,0 +1,24 @@
+
+namespace Ink.Parsed
+{
+	public class Text : Parsed.Object
+	{
+		public string text { get; set; }
+
+		public Text (string str)
+		{
+			text = str;
+		}
+
+		public override Runtime.Object GenerateRuntimeObject ()
+		{
+			return new Runtime.StringValue(this.text);
+		}
+
+        public override string ToString ()
+        {
+            return this.text;
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs.meta
new file mode 100644
index 0000000..d929218
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Text.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5cce79a72f1f94786a6bb0297db32376
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs
new file mode 100644
index 0000000..a47dd38
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class TunnelOnwards : Parsed.Object
+    {
+        public Divert divertAfter {
+            get {
+                return _divertAfter;
+            }
+            set {
+                _divertAfter = value;
+                if (_divertAfter) AddContent (_divertAfter);
+            }
+        }
+        Divert _divertAfter;
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            var container = new Runtime.Container ();
+
+            // Set override path for tunnel onwards (or nothing)
+            container.AddContent (Runtime.ControlCommand.EvalStart ());
+
+            if (divertAfter) {
+
+                // Generate runtime object's generated code and steal the arguments runtime code
+                var returnRuntimeObj = divertAfter.GenerateRuntimeObject ();
+                var returnRuntimeContainer = returnRuntimeObj as Runtime.Container;
+                if (returnRuntimeContainer) {
+
+                    // Steal all code for generating arguments from the divert
+                    var args = divertAfter.arguments;
+                    if (args != null && args.Count > 0) {
+
+                        // Steal everything betwen eval start and eval end
+                        int evalStart = -1;
+                        int evalEnd = -1;
+                        for (int i = 0; i < returnRuntimeContainer.content.Count; i++) {
+                            var cmd = returnRuntimeContainer.content [i] as Runtime.ControlCommand;
+                            if (cmd) {
+                                if (evalStart == -1 && cmd.commandType == Runtime.ControlCommand.CommandType.EvalStart)
+                                    evalStart = i;
+                                else if (cmd.commandType == Runtime.ControlCommand.CommandType.EvalEnd)
+                                    evalEnd = i;
+                            }
+                        }
+
+                        for (int i = evalStart + 1; i < evalEnd; i++) {
+                            var obj = returnRuntimeContainer.content [i];
+                            obj.parent = null; // prevent error of being moved between owners
+                            container.AddContent (returnRuntimeContainer.content [i]);
+                        }
+                    }
+                }
+
+                // Finally, divert to the requested target 
+                _overrideDivertTarget = new Runtime.DivertTargetValue ();
+                container.AddContent (_overrideDivertTarget);
+            } 
+
+            // No divert after tunnel onwards
+            else {
+                container.AddContent (new Runtime.Void ());
+            }
+
+            container.AddContent (Runtime.ControlCommand.EvalEnd ());
+
+            container.AddContent (Runtime.ControlCommand.PopTunnel ());
+
+            return container;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            if (divertAfter && divertAfter.targetContent)
+                _overrideDivertTarget.targetPath = divertAfter.targetContent.runtimePath;
+        }
+
+        Runtime.DivertTargetValue _overrideDivertTarget;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs.meta
new file mode 100644
index 0000000..7c5a3a3
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/TunnelOnwards.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 73a38744ac93c440794944efec87b970
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs
new file mode 100644
index 0000000..34e281e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    public class VariableAssignment : Parsed.Object
+    {
+        public string variableName
+        {
+            get { return variableIdentifier.name; }
+        }
+        public Identifier variableIdentifier { get; protected set; }
+        public Expression expression { get; protected set; }
+        public ListDefinition listDefinition { get; protected set; }
+
+        public bool isGlobalDeclaration { get; set; }
+        public bool isNewTemporaryDeclaration { get; set; }
+
+        public bool isDeclaration {
+            get {
+                return isGlobalDeclaration || isNewTemporaryDeclaration;
+            }
+        }
+
+        public VariableAssignment (Identifier identifier, Expression assignedExpression)
+        {
+            this.variableIdentifier = identifier;
+
+            // Defensive programming in case parsing of assignedExpression failed
+            if( assignedExpression )
+                this.expression = AddContent(assignedExpression);
+        }
+
+        public VariableAssignment (Identifier identifier, ListDefinition listDef)
+        {
+            this.variableIdentifier = identifier;
+
+            if (listDef) {
+                this.listDefinition = AddContent (listDef);
+                this.listDefinition.variableAssignment = this;
+            }
+
+            // List definitions are always global
+            isGlobalDeclaration = true;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            FlowBase newDeclScope = null;
+            if (isGlobalDeclaration) {
+                newDeclScope = story;
+            } else if(isNewTemporaryDeclaration) {
+                newDeclScope = ClosestFlowBase ();
+            }
+
+            if( newDeclScope )
+                newDeclScope.TryAddNewVariableDeclaration (this);
+
+            // Global declarations don't generate actual procedural
+            // runtime objects, but instead add a global variable to the story itself.
+            // The story then initialises them all in one go at the start of the game.
+            if( isGlobalDeclaration )
+                return null;
+
+            var container = new Runtime.Container ();
+
+            // The expression's runtimeObject is actually another nested container
+            if( expression != null )
+                container.AddContent (expression.runtimeObject);
+            else if( listDefinition != null )
+                container.AddContent (listDefinition.runtimeObject);
+
+            _runtimeAssignment = new Runtime.VariableAssignment(variableName, isNewTemporaryDeclaration);
+            container.AddContent (_runtimeAssignment);
+
+            return container;
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            // List definitions are checked for conflicts separately
+            if( this.isDeclaration && listDefinition == null )
+                context.CheckForNamingCollisions (this, variableIdentifier, this.isGlobalDeclaration ? Story.SymbolType.Var : Story.SymbolType.Temp);
+
+            // Initial VAR x = [intialValue] declaration, not re-assignment
+            if (this.isGlobalDeclaration) {
+                var variableReference = expression as VariableReference;
+                if (variableReference && !variableReference.isConstantReference && !variableReference.isListItemReference) {
+                    Error ("global variable assignments cannot refer to other variables, only literal values, constants and list items");
+                }
+            }
+
+            if (!this.isNewTemporaryDeclaration) {
+                var resolvedVarAssignment = context.ResolveVariableWithName(this.variableName, fromNode: this);
+                if (!resolvedVarAssignment.found) {
+                    if (story.constants.ContainsKey (variableName)) {
+                        Error ("Can't re-assign to a constant (do you need to use VAR when declaring '" + this.variableName + "'?)", this);
+                    } else {
+                        Error ("Variable could not be found to assign to: '" + this.variableName + "'", this);
+                    }
+                }
+
+                // A runtime assignment may not have been generated if it's the initial global declaration,
+                // since these are hoisted out and handled specially in Story.ExportRuntime.
+                if( _runtimeAssignment != null )
+                    _runtimeAssignment.isGlobal = resolvedVarAssignment.isGlobal;
+            }
+        }
+
+
+        public override string typeName {
+            get {
+                if (isNewTemporaryDeclaration) return "temp";
+                else if (isGlobalDeclaration) return "VAR";
+                else return "variable assignment";
+            }
+        }
+
+        Runtime.VariableAssignment _runtimeAssignment;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs.meta
new file mode 100644
index 0000000..aaf3aba
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableAssignment.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e6399748d4c08492e863106c86944abf
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs
new file mode 100644
index 0000000..f0359aa
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs
@@ -0,0 +1,152 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Parsed
+{
+    public class VariableReference : Expression
+    {
+        // - Normal variables have a single item in their "path"
+        // - Knot/stitch names for read counts are actual dot-separated paths
+        //   (though this isn't actually used at time of writing)
+        // - List names are dot separated: listName.itemName (or just itemName)
+        public string name { get; private set; }
+
+        public Identifier identifier {
+            get {
+                // Merging the list of identifiers into a single identifier.
+                // Debug metadata is also merged.
+                if (pathIdentifiers == null || pathIdentifiers.Count == 0) {
+                    return null;
+                }
+
+                if( _singleIdentifier == null ) {
+                    var name = string.Join (".", path.ToArray());
+                    var firstDebugMetadata = pathIdentifiers.First().debugMetadata;
+                    var debugMetadata = pathIdentifiers.Aggregate(firstDebugMetadata, (acc, id) => acc.Merge(id.debugMetadata));
+                    _singleIdentifier = new Identifier { name = name, debugMetadata = debugMetadata };
+                }
+                
+                return _singleIdentifier;
+            }
+        }
+        Identifier _singleIdentifier;
+
+        public List<Identifier> pathIdentifiers;
+        public List<string> path { get; private set; }
+
+        // Only known after GenerateIntoContainer has run
+        public bool isConstantReference;
+        public bool isListItemReference;
+
+        public Runtime.VariableReference runtimeVarRef { get { return _runtimeVarRef; } }
+
+        public VariableReference (List<Identifier> pathIdentifiers)
+        {
+            this.pathIdentifiers = pathIdentifiers;
+            this.path = pathIdentifiers.Select(id => id?.name).ToList();
+            this.name = string.Join (".", pathIdentifiers);
+        }
+
+        public override void GenerateIntoContainer (Runtime.Container container)
+        {
+            Expression constantValue = null;
+
+            // If it's a constant reference, just generate the literal expression value
+            // It's okay to access the constants at code generation time, since the
+            // first thing the ExportRuntime function does it search for all the constants
+            // in the story hierarchy, so they're all available.
+            if ( story.constants.TryGetValue (name, out constantValue) ) {
+                constantValue.GenerateConstantIntoContainer (container);
+                isConstantReference = true;
+                return;
+            }
+
+            _runtimeVarRef = new Runtime.VariableReference (name);
+
+            // List item reference?
+            // Path might be to a list (listName.listItemName or just listItemName)
+            if (path.Count == 1 || path.Count == 2) {
+                string listItemName = null;
+                string listName = null;
+
+                if (path.Count == 1) listItemName = path [0];
+                else {
+                    listName = path [0];
+                    listItemName = path [1];
+                }
+
+                var listItem = story.ResolveListItem (listName, listItemName, this);
+                if (listItem) {
+                    isListItemReference = true;
+                }
+            }
+
+            container.AddContent (_runtimeVarRef);
+        }
+
+        public override void ResolveReferences (Story context)
+        {
+            base.ResolveReferences (context);
+
+            // Work is already done if it's a constant or list item reference
+            if (isConstantReference || isListItemReference) {
+                return;
+            }
+
+            // Is it a read count?
+            var parsedPath = new Path (pathIdentifiers);
+            Parsed.Object targetForCount = parsedPath.ResolveFromContext (this);
+            if (targetForCount) {
+
+                targetForCount.containerForCounting.visitsShouldBeCounted = true;
+
+                // If this is an argument to a function that wants a variable to be
+                // passed by reference, then the Parsed.Divert will have generated a
+                // Runtime.VariablePointerValue instead of allowing this object
+                // to generate its RuntimeVariableReference. This only happens under
+                // error condition since we shouldn't be passing a read count by
+                // reference, but we don't want it to crash!
+                if (_runtimeVarRef == null) return;
+
+                _runtimeVarRef.pathForCount = targetForCount.runtimePath;
+                _runtimeVarRef.name = null;
+
+                // Check for very specific writer error: getting read count and
+                // printing it as content rather than as a piece of logic
+                // e.g. Writing {myFunc} instead of {myFunc()}
+                var targetFlow = targetForCount as FlowBase;
+                if (targetFlow && targetFlow.isFunction) {
+
+                    // Is parent context content rather than logic?
+                    if ( parent is Weave || parent is ContentList || parent is FlowBase) {
+                        Warning ("'" + targetFlow.identifier + "' being used as read count rather than being called as function. Perhaps you intended to write " + targetFlow.name + "()");
+                    }
+                }
+
+                return;
+            }
+
+            // Couldn't find this multi-part path at all, whether as a divert
+            // target or as a list item reference.
+            if (path.Count > 1) {
+                var errorMsg = "Could not find target for read count: " + parsedPath;
+                if (path.Count <= 2)
+                    errorMsg += ", or couldn't find list item with the name " + string.Join (",", path.ToArray());
+                Error (errorMsg);
+                return;
+            }
+
+            if (!context.ResolveVariableWithName (this.name, fromNode: this).found) {
+                Error("Unresolved variable: "+this.ToString(), this);
+            }
+        }
+
+        public override string ToString ()
+        {
+            return string.Join(".", path.ToArray());
+        }
+
+        Runtime.VariableReference _runtimeVarRef;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs.meta
new file mode 100644
index 0000000..5ab1e83
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/VariableReference.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: df944176b643540b3b7c9a1bb01d3780
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs
new file mode 100644
index 0000000..42048e1
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs
@@ -0,0 +1,730 @@
+using System.Collections.Generic;
+
+namespace Ink.Parsed
+{
+    // Used by the FlowBase when constructing the weave flow from
+    // a flat list of content objects.
+    public class Weave : Parsed.Object
+    {
+        // Containers can be chained as multiple gather points
+        // get created as the same indentation level.
+        // rootContainer is always the first in the chain, while
+        // currentContainer is the latest.
+        public Runtime.Container rootContainer { 
+            get {
+                if (_rootContainer == null) {
+                    GenerateRuntimeObject ();
+                }
+
+                return _rootContainer;
+            }
+        }
+        Runtime.Container currentContainer { get; set; }
+
+		public int baseIndentIndex { get; private set; }
+
+        // Loose ends are:
+        //  - Choices or Gathers that need to be joined up
+        //  - Explicit Divert to gather points (i.e. "->" without a target)
+        public List<IWeavePoint> looseEnds;
+
+        public List<GatherPointToResolve> gatherPointsToResolve;
+        public class GatherPointToResolve
+        {
+            public Runtime.Divert divert;
+            public Runtime.Object targetRuntimeObj;
+        }
+
+        public Parsed.Object lastParsedSignificantObject
+        {
+            get {
+                if (content.Count == 0) return null;
+
+                // Don't count extraneous newlines or VAR/CONST declarations,
+                // since they're "empty" statements outside of the main flow.
+                Parsed.Object lastObject = null;
+                for (int i = content.Count - 1; i >= 0; --i) {
+                    lastObject = content [i];
+
+                    var lastText = lastObject as Parsed.Text;
+                    if (lastText && lastText.text == "\n") {
+                        continue;
+                    }
+
+                    if (IsGlobalDeclaration (lastObject))
+                        continue;
+                    
+                    break;
+                }
+
+                var lastWeave = lastObject as Weave;
+                if (lastWeave)
+                    lastObject = lastWeave.lastParsedSignificantObject;
+                
+                return lastObject;
+            }
+        }
+                        
+        public Weave(List<Parsed.Object> cont, int indentIndex=-1) 
+        {
+            if (indentIndex == -1) {
+                baseIndentIndex = DetermineBaseIndentationFromContent (cont);
+            } else {
+                baseIndentIndex = indentIndex;
+            }
+
+            AddContent (cont);
+
+            ConstructWeaveHierarchyFromIndentation ();
+        }
+
+        public void ResolveWeavePointNaming ()
+        {
+            var namedWeavePoints = FindAll<IWeavePoint> (w => !string.IsNullOrEmpty (w.name));
+
+            _namedWeavePoints = new Dictionary<string, IWeavePoint> ();
+
+            foreach (var weavePoint in namedWeavePoints) {
+
+                // Check for weave point naming collisions
+                IWeavePoint existingWeavePoint;
+                if (_namedWeavePoints.TryGetValue (weavePoint.name, out existingWeavePoint)) {
+                    var typeName = existingWeavePoint is Gather ? "gather" : "choice";
+                    var existingObj = (Parsed.Object)existingWeavePoint;
+
+                    Error ("A " + typeName + " with the same label name '" + weavePoint.name + "' already exists in this context on line " + existingObj.debugMetadata.startLineNumber, (Parsed.Object)weavePoint);
+                }
+
+                _namedWeavePoints [weavePoint.name] = weavePoint;
+            }
+        }
+
+        void ConstructWeaveHierarchyFromIndentation()
+        {
+            // Find nested indentation and convert to a proper object hierarchy
+            // (i.e. indented content is replaced with a Weave object that contains
+            // that nested content)
+            int contentIdx = 0;
+            while (contentIdx < content.Count) {
+
+                Parsed.Object obj = content [contentIdx];
+
+                // Choice or Gather
+                if (obj is IWeavePoint) {
+                    var weavePoint = (IWeavePoint)obj;
+                    var weaveIndentIdx = weavePoint.indentationDepth - 1;
+
+                    // Inner level indentation - recurse
+                    if (weaveIndentIdx > baseIndentIndex) {
+
+                        // Step through content until indent jumps out again
+                        int innerWeaveStartIdx = contentIdx;
+                        while (contentIdx < content.Count) {
+                            var innerWeaveObj = content [contentIdx] as IWeavePoint;
+                            if (innerWeaveObj != null) {
+                                var innerIndentIdx = innerWeaveObj.indentationDepth - 1;
+                                if (innerIndentIdx <= baseIndentIndex) {
+                                    break;
+                                }
+                            }
+
+                            contentIdx++;
+                        }
+
+                        int weaveContentCount = contentIdx - innerWeaveStartIdx;
+
+                        var weaveContent = content.GetRange (innerWeaveStartIdx, weaveContentCount);
+                        content.RemoveRange (innerWeaveStartIdx, weaveContentCount);
+
+                        var weave = new Weave (weaveContent, weaveIndentIdx);
+                        InsertContent (innerWeaveStartIdx, weave);
+
+                        // Continue iteration from this point
+                        contentIdx = innerWeaveStartIdx;
+                    }
+
+                } 
+
+                contentIdx++;
+            }
+        }
+            
+        // When the indentation wasn't told to us at construction time using
+        // a choice point with a known indentation level, we may be told to
+        // determine the indentation level by incrementing from our closest ancestor.
+        public int DetermineBaseIndentationFromContent(List<Parsed.Object> contentList)
+        {
+            foreach (var obj in contentList) {
+                if (obj is IWeavePoint) {
+                    return ((IWeavePoint)obj).indentationDepth - 1;
+                }
+            }
+
+            // No weave points, so it doesn't matter
+            return 0;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            _rootContainer = currentContainer = new Runtime.Container();
+            looseEnds = new List<IWeavePoint> ();
+
+            gatherPointsToResolve = new List<GatherPointToResolve> ();
+
+            // Iterate through content for the block at this level of indentation
+            //  - Normal content is nested under Choices and Gathers
+            //  - Blocks that are further indented cause recursion
+            //  - Keep track of loose ends so that they can be diverted to Gathers
+            foreach(var obj in content) {
+
+                // Choice or Gather
+                if (obj is IWeavePoint) {
+                    AddRuntimeForWeavePoint ((IWeavePoint)obj);
+                } 
+
+                // Non-weave point
+                else {
+
+                    // Nested weave
+                    if (obj is Weave) {
+                        var weave = (Weave)obj;
+                        AddRuntimeForNestedWeave (weave);
+                        gatherPointsToResolve.AddRange (weave.gatherPointsToResolve);
+                    }
+
+                    // Other object
+                    // May be complex object that contains statements - e.g. a multi-line conditional
+                    else {
+                        AddGeneralRuntimeContent (obj.runtimeObject);
+                    }
+                }
+            }
+
+            // Pass any loose ends up the hierarhcy
+            PassLooseEndsToAncestors();
+
+            return _rootContainer;
+        }
+
+        // Found gather point:
+        //  - gather any loose ends
+        //  - set the gather as the main container to dump new content in
+        void AddRuntimeForGather(Gather gather)
+        {
+            // Determine whether this Gather should be auto-entered:
+            //  - It is auto-entered if there were no choices in the last section
+            //  - A section is "since the previous gather" - so reset now
+            bool autoEnter = !hasSeenChoiceInSection;
+            hasSeenChoiceInSection = false;
+
+            var gatherContainer = gather.runtimeContainer;
+
+            if (gather.name == null) {
+                // Use disallowed character so it's impossible to have a name collision
+                gatherContainer.name = "g-" + _unnamedGatherCount;
+                _unnamedGatherCount++;
+            }
+                
+            // Auto-enter: include in main content
+            if (autoEnter) {
+                currentContainer.AddContent (gatherContainer);
+            } 
+
+            // Don't auto-enter:
+            // Add this gather to the main content, but only accessible
+            // by name so that it isn't stepped into automatically, but only via
+            // a divert from a loose end.
+            else {
+                _rootContainer.AddToNamedContentOnly (gatherContainer);
+            }
+
+            // Consume loose ends: divert them to this gather
+            foreach (IWeavePoint looseEndWeavePoint in looseEnds) {
+
+                var looseEnd = (Parsed.Object)looseEndWeavePoint;
+
+                // Skip gather loose ends that are at the same level
+                // since they'll be handled by the auto-enter code below
+                // that only jumps into the gather if (current runtime choices == 0)
+                if (looseEnd is Gather) {
+                    var prevGather = (Gather)looseEnd;
+                    if (prevGather.indentationDepth == gather.indentationDepth) {
+                        continue;
+                    }
+                }
+
+                Runtime.Divert divert = null;
+
+                if (looseEnd is Parsed.Divert) {
+                    divert = (Runtime.Divert) looseEnd.runtimeObject;
+                } else {
+                    divert = new Runtime.Divert ();
+                    var looseWeavePoint = looseEnd as IWeavePoint;
+                    looseWeavePoint.runtimeContainer.AddContent (divert);
+                }
+                   
+                // Pass back knowledge of this loose end being diverted
+                // to the FlowBase so that it can maintain a list of them,
+                // and resolve the divert references later
+                gatherPointsToResolve.Add (new GatherPointToResolve{ divert = divert, targetRuntimeObj = gatherContainer });
+            }
+            looseEnds.Clear ();
+
+            // Replace the current container itself
+            currentContainer = gatherContainer;
+        }
+
+        void AddRuntimeForWeavePoint(IWeavePoint weavePoint)
+        {
+            // Current level Gather
+            if (weavePoint is Gather) {
+                AddRuntimeForGather ((Gather)weavePoint);
+            } 
+
+            // Current level choice
+            else if (weavePoint is Choice) {
+
+                // Gathers that contain choices are no longer loose ends
+                // (same as when weave points get nested content)
+                if (previousWeavePoint is Gather) {
+                    looseEnds.Remove (previousWeavePoint);
+                }
+
+                // Add choice point content
+                var choice = (Choice)weavePoint;
+                currentContainer.AddContent (choice.runtimeObject);
+
+                // Add choice's inner content to self
+                choice.innerContentContainer.name = "c-" + _choiceCount;
+                currentContainer.AddToNamedContentOnly (choice.innerContentContainer);
+                _choiceCount++;
+
+                hasSeenChoiceInSection = true;
+            }
+
+            // Keep track of loose ends
+            addContentToPreviousWeavePoint = false; // default
+            if (WeavePointHasLooseEnd (weavePoint)) {
+                looseEnds.Add (weavePoint);
+
+
+                var looseChoice = weavePoint as Choice;
+                if (looseChoice) {
+                    addContentToPreviousWeavePoint = true;
+                }
+            }
+            previousWeavePoint = weavePoint;
+        }
+
+        // Add nested block at a greater indentation level
+        public void AddRuntimeForNestedWeave(Weave nestedResult)
+        {
+            // Add this inner block to current container
+            // (i.e. within the main container, or within the last defined Choice/Gather)
+            AddGeneralRuntimeContent (nestedResult.rootContainer);
+
+            // Now there's a deeper indentation level, the previous weave point doesn't
+            // count as a loose end (since it will have content to go to)
+            if (previousWeavePoint != null) {
+                looseEnds.Remove (previousWeavePoint);
+                addContentToPreviousWeavePoint = false;
+            }
+        }
+
+        // Normal content gets added into the latest Choice or Gather by default,
+        // unless there hasn't been one yet.
+        void AddGeneralRuntimeContent(Runtime.Object content)
+        {
+            // Content is allowed to evaluate runtimeObject to null
+            // (e.g. AuthorWarning, which doesn't make it into the runtime)
+            if (content == null)
+                return;
+            
+            if (addContentToPreviousWeavePoint) {
+                previousWeavePoint.runtimeContainer.AddContent (content);
+            } else {
+                currentContainer.AddContent (content);
+            }
+        }
+
+        void PassLooseEndsToAncestors()
+        {
+            if (looseEnds.Count == 0) return;
+
+            // Search for Weave ancestor to pass loose ends to for gathering.
+            // There are two types depending on whether the current weave
+            // is separated by a conditional or sequence.
+            //  - An "inner" weave is one that is directly connected to the current
+            //    weave - i.e. you don't have to pass through a conditional or
+            //    sequence to get to it. We're allowed to pass all loose ends to
+            //    one of these.
+            //  - An "outer" weave is one that is outside of a conditional/sequence
+            //    that the current weave is nested within. We're only allowed to
+            //    pass gathers (i.e. 'normal flow') loose ends up there, not normal
+            //    choices. The rule is that choices have to be diverted explicitly
+            //    by the author since it's ambiguous where flow should go otherwise.
+            //
+            // e.g.:
+            //
+            //   - top                       <- e.g. outer weave
+            //   {true:
+            //       * choice                <- e.g. inner weave
+            //         * * choice 2
+            //             more content      <- e.g. current weave
+            //       * choice 2
+            //   }
+            //   - more of outer weave
+            //
+            Weave closestInnerWeaveAncestor = null;
+            Weave closestOuterWeaveAncestor = null;
+
+            // Find inner and outer ancestor weaves as defined above.
+            bool nested = false;
+            for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
+            {
+
+                // Found ancestor?
+                var weaveAncestor = ancestor as Weave;
+                if (weaveAncestor != null)
+                {
+                    if (!nested && closestInnerWeaveAncestor == null)
+                        closestInnerWeaveAncestor = weaveAncestor;
+
+                    if (nested && closestOuterWeaveAncestor == null)
+                        closestOuterWeaveAncestor = weaveAncestor;
+                }
+
+
+                // Weaves nested within Sequences or Conditionals are
+                // "sealed" - any loose ends require explicit diverts.
+                if (ancestor is Sequence || ancestor is Conditional)
+                    nested = true;
+            }
+
+            // No weave to pass loose ends to at all?
+            if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null)
+                return;
+
+            // Follow loose end passing logic as defined above
+            for (int i = looseEnds.Count - 1; i >= 0; i--) {
+                var looseEnd = looseEnds[i];
+
+                bool received = false;
+
+                // This weave is nested within a conditional or sequence:
+                //  - choices can only be passed up to direct ancestor ("inner") weaves
+                //  - gathers can be passed up to either, but favour the closer (inner) weave
+                //    if there is one
+                if(nested) {
+                    if( looseEnd is Choice && closestInnerWeaveAncestor != null) {
+                        closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
+                        received = true;
+                    }
+
+                    else if( !(looseEnd is Choice) ) {
+                        var receivingWeave = closestInnerWeaveAncestor ?? closestOuterWeaveAncestor;
+                        if(receivingWeave != null) {
+                            receivingWeave.ReceiveLooseEnd(looseEnd);
+                            received = true;
+                        }
+                    }
+                }
+
+                // No nesting, all loose ends can be safely passed up
+                else {
+                    closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
+                    received = true;
+                }
+
+                if(received) looseEnds.RemoveAt(i);
+            }
+        }
+
+        void ReceiveLooseEnd(IWeavePoint childWeaveLooseEnd)
+        {
+            looseEnds.Add(childWeaveLooseEnd);
+        }
+
+        public override void ResolveReferences(Story context)
+        {
+            base.ResolveReferences (context);
+
+            // Check that choices nested within conditionals and sequences are terminated
+            if( looseEnds != null && looseEnds.Count > 0 ) {
+                var isNestedWeave = false;
+                for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
+                {
+                    if (ancestor is Sequence || ancestor is Conditional)
+                    {
+                        isNestedWeave = true;
+                        break;
+                    }
+                }
+                if (isNestedWeave)
+                {
+                    ValidateTermination(BadNestedTerminationHandler);
+                }
+            }
+
+            foreach(var gatherPoint in gatherPointsToResolve) {
+                gatherPoint.divert.targetPath = gatherPoint.targetRuntimeObj.path;
+            }
+                
+            CheckForWeavePointNamingCollisions ();
+        }
+
+        public IWeavePoint WeavePointNamed(string name)
+        {
+            if (_namedWeavePoints == null)
+                return null;
+
+            IWeavePoint weavePointResult = null;
+            if (_namedWeavePoints.TryGetValue (name, out weavePointResult))
+                return weavePointResult;
+
+            return null;
+        }
+
+        // Global VARs and CONSTs are treated as "outside of the flow"
+        // when iterating over content that follows loose ends
+        bool IsGlobalDeclaration (Parsed.Object obj)
+        {
+
+            var varAss = obj as VariableAssignment;
+            if (varAss && varAss.isGlobalDeclaration && varAss.isDeclaration)
+                return true;
+            
+            var constDecl = obj as ConstantDeclaration;
+            if (constDecl)
+                return true;
+
+            return false;
+        }
+
+        // While analysing final loose ends, we look to see whether there
+        // are any diverts etc which choices etc divert from
+        IEnumerable<Parsed.Object> ContentThatFollowsWeavePoint (IWeavePoint weavePoint)
+        {
+            var obj = (Parsed.Object)weavePoint;
+
+            // Inner content first (e.g. for a choice)
+            if (obj.content != null) {
+                foreach (var contentObj in obj.content) {
+
+                    // Global VARs and CONSTs are treated as "outside of the flow"
+                    if (IsGlobalDeclaration (contentObj)) continue;
+
+                    yield return contentObj;
+                }
+            }
+
+
+            var parentWeave = obj.parent as Weave;
+            if (parentWeave == null) {
+                throw new System.Exception ("Expected weave point parent to be weave?");
+            }
+
+            var weavePointIdx = parentWeave.content.IndexOf (obj);
+
+            for (int i = weavePointIdx+1; i < parentWeave.content.Count; i++) {
+                var laterObj = parentWeave.content [i];
+
+                // Global VARs and CONSTs are treated as "outside of the flow"
+                if (IsGlobalDeclaration (laterObj)) continue;
+
+                // End of the current flow
+                if (laterObj is IWeavePoint) 
+                    break;
+
+                // Other weaves will be have their own loose ends
+                if (laterObj is Weave)
+                    break;
+
+                yield return laterObj;
+            }
+        }
+
+        public delegate void BadTerminationHandler (Parsed.Object terminatingObj);
+        public void ValidateTermination (BadTerminationHandler badTerminationHandler)
+        {
+            // Don't worry if the last object in the flow is a "TODO",
+            // even if there are other loose ends in other places
+            if (lastParsedSignificantObject is AuthorWarning) {
+                return;
+            }
+
+            // By now, any sub-weaves will have passed loose ends up to the root weave (this).
+            // So there are 2 possible situations:
+            //  - There are loose ends from somewhere in the flow.
+            //    These aren't necessarily "real" loose ends - they're weave points
+            //    that don't connect to any lower weave points, so we just
+            //    have to check that they terminate properly.
+            //  - This weave is just a list of content with no actual weave points,
+            //    so we just need to check that the list of content terminates.
+
+            bool hasLooseEnds = looseEnds != null && looseEnds.Count > 0;
+
+            if (hasLooseEnds) {
+                foreach (var looseEnd in looseEnds) {
+                    var looseEndFlow = ContentThatFollowsWeavePoint (looseEnd);
+                    ValidateFlowOfObjectsTerminates (looseEndFlow, (Parsed.Object)looseEnd, badTerminationHandler);
+                }
+            }
+
+            // No loose ends... is there any inner weaving at all?
+            // If not, make sure the single content stream is terminated correctly
+            else {
+
+                // If there's any actual weaving, assume that content is 
+                // terminated correctly since we would've had a loose end otherwise
+                foreach (var obj in content) {
+                    if (obj is IWeavePoint) return;
+                }
+
+                // Straight linear flow? Check it terminates
+                ValidateFlowOfObjectsTerminates (content, this, badTerminationHandler);
+            }
+        }
+
+        void BadNestedTerminationHandler(Parsed.Object terminatingObj)
+        {
+            Conditional conditional = null;
+            for (var ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) {
+                if( ancestor is Sequence || ancestor is Conditional ) {
+                    conditional = ancestor as Conditional;
+                    break;
+                }
+            }
+
+            var errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards.";
+
+            // Tutorialise proper choice syntax if this looks like a single choice within a condition, e.g.
+            // { condition:
+            //      * choice
+            // }
+            if (conditional != null) {
+                var numChoices = conditional.FindAll<Choice>().Count;
+                if( numChoices == 1 ) {
+                    errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, "+ errorMsg.ToLower();
+                }
+            }
+
+            Error(errorMsg, terminatingObj);
+        }
+
+        void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed.Object defaultObj, BadTerminationHandler badTerminationHandler)
+        {
+            bool terminated = false;
+            Parsed.Object terminatingObj = defaultObj;
+            foreach (var flowObj in objFlow) {
+
+                var divert = flowObj.Find<Divert> (d => !d.isThread && !d.isTunnel && !d.isFunctionCall && !(d.parent is DivertTarget));
+                if (divert != null) {
+                    terminated = true;
+                }
+
+                if (flowObj.Find<TunnelOnwards> () != null) {
+                    terminated = true;
+                    break;
+                }
+
+                terminatingObj = flowObj;
+            }
+
+
+            if (!terminated) {
+
+                // Author has left a note to self here - clearly we don't need
+                // to leave them with another warning since they know what they're doing.
+                if (terminatingObj is AuthorWarning) {
+                    return;
+                }
+
+                badTerminationHandler (terminatingObj);
+            }
+        }
+            
+        bool WeavePointHasLooseEnd(IWeavePoint weavePoint)
+        {
+            // No content, must be a loose end.
+            if (weavePoint.content == null) return true;
+
+            // If a weave point is diverted from, it doesn't have a loose end.
+            // Detect a divert object within a weavePoint's main content
+            // Work backwards since we're really interested in the end,
+            // although it doesn't actually make a difference!
+            // (content after a divert will simply be inaccessible)
+            for (int i = weavePoint.content.Count - 1; i >= 0; --i) {
+                var innerDivert = weavePoint.content [i] as Divert;
+                if (innerDivert) {
+                    bool willReturn = innerDivert.isThread || innerDivert.isTunnel || innerDivert.isFunctionCall;
+                    if (!willReturn) return false;
+                }
+            }
+
+            return true;
+        }
+
+        // Enforce rule that weave points must not have the same
+        // name as any stitches or knots upwards in the hierarchy
+        void CheckForWeavePointNamingCollisions()
+        {
+            if (_namedWeavePoints == null)
+                return;
+            
+            var ancestorFlows = new List<FlowBase> ();
+            foreach (var obj in this.ancestry) {
+                var flow = obj as FlowBase;
+                if (flow)
+                    ancestorFlows.Add (flow);
+                else
+                    break;
+            }
+
+
+            foreach (var namedWeavePointPair in _namedWeavePoints) {
+                var weavePointName = namedWeavePointPair.Key;
+                var weavePoint = (Parsed.Object) namedWeavePointPair.Value;
+
+                foreach(var flow in ancestorFlows) {
+
+                    // Shallow search
+                    var otherContentWithName = flow.ContentWithNameAtLevel (weavePointName);
+
+                    if (otherContentWithName && otherContentWithName != weavePoint) {
+                        var errorMsg = string.Format ("{0} '{1}' has the same label name as a {2} (on {3})", 
+                            weavePoint.GetType().Name, 
+                            weavePointName, 
+                            otherContentWithName.GetType().Name, 
+                            otherContentWithName.debugMetadata);
+
+                        Error(errorMsg, (Parsed.Object) weavePoint);
+                    }
+
+                }
+            }
+        }
+
+        // Keep track of previous weave point (Choice or Gather)
+        // at the current indentation level:
+        //  - to add ordinary content to be nested under it
+        //  - to add nested content under it when it's indented
+        //  - to remove it from the list of loose ends when
+        //     - it has indented content since it's no longer a loose end
+        //     - it's a gather and it has a choice added to it
+        IWeavePoint previousWeavePoint = null;
+        bool addContentToPreviousWeavePoint = false;
+
+        // Used for determining whether the next Gather should auto-enter
+        bool hasSeenChoiceInSection = false;
+
+        int _unnamedGatherCount;
+
+        int _choiceCount;
+
+
+        Runtime.Container _rootContainer;
+        Dictionary<string, IWeavePoint> _namedWeavePoints;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs.meta
new file mode 100644
index 0000000..81391d2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 94f6b0e0f70d94e05b4292888083b3c6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs
new file mode 100644
index 0000000..d6b435a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs
@@ -0,0 +1,27 @@
+
+namespace Ink.Parsed
+{
+    public class Wrap<T> : Parsed.Object where T : Runtime.Object
+    {
+        public Wrap (T objToWrap)
+        {
+            _objToWrap = objToWrap;
+        }
+
+        public override Runtime.Object GenerateRuntimeObject ()
+        {
+            return _objToWrap;
+        }
+
+        T _objToWrap;
+    }
+
+    // Shorthand for writing Parsed.Wrap<Runtime.Glue> and Parsed.Wrap<Runtime.Tag>
+    public class Glue : Wrap<Runtime.Glue> {
+        public Glue (Runtime.Glue glue) : base(glue) {}
+    }
+    public class Tag : Wrap<Runtime.Tag> {
+        public Tag (Runtime.Tag tag) : base (tag) { }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs.meta b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs.meta
new file mode 100644
index 0000000..5975726
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Wrap.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 01c402ee24cb74892a82fb28f502dfac
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/Plugins.meta b/Assets/Ink/InkLibs/InkCompiler/Plugins.meta
new file mode 100644
index 0000000..32cfbf2
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Plugins.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0c1077a29b8b64f06a5b5c98a067c2b9
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs b/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs
new file mode 100644
index 0000000..c56e98c
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Ink
+{
+    public interface IPlugin
+    {  
+        // Hooks: if in doubt use PostExport, since the parsedStory is in a more finalised state.
+
+        // Hook for immediately after the story has been parsed into its basic Parsed hierarchy.
+        // Could be useful for modifying the story before it's exported.
+        void PostParse(Parsed.Story parsedStory);
+
+        // Hook for after parsed story has been converted into its runtime equivalent. Note that
+        // during this process the parsed story will have changed structure too, to take into 
+        // account analysis of the structure of Weave, for example.
+        void PostExport(Parsed.Story parsedStory, Runtime.Story runtimeStory);
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs.meta b/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs.meta
new file mode 100644
index 0000000..c26c428
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Plugins/Plugin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: afc85318112c14186b267528f5930957
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs b/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs
new file mode 100644
index 0000000..60421c1
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ink
+{
+    public class PluginManager
+    {
+        public PluginManager (List<string> pluginNames)
+        {
+            _plugins = new List<IPlugin> ();
+
+            // TODO: Make these plugin names DLL filenames, and load up their assemblies
+            foreach (string pluginName in pluginNames) {
+                //if (pluginName == "ChoiceListPlugin") {
+                //    _plugins.Add (new InkPlugin.ChoiceListPlugin ());
+                //}else  
+                {
+                    throw new System.Exception ("Plugin not found");
+                }
+            }
+        }
+
+        public void PostParse(Parsed.Story parsedStory)
+        {
+            foreach (var plugin in _plugins) {
+                plugin.PostParse (parsedStory);
+            }
+        }
+
+        public void PostExport(Parsed.Story parsedStory, Runtime.Story runtimeStory)
+        {
+            foreach (var plugin in _plugins) {
+                plugin.PostExport (parsedStory, runtimeStory);
+            }
+        }
+
+        List<IPlugin> _plugins;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs.meta b/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs.meta
new file mode 100644
index 0000000..802f525
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Plugins/PluginManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e61aa8846eb6f40ed858857d1ea2225d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/Stats.cs b/Assets/Ink/InkLibs/InkCompiler/Stats.cs
new file mode 100644
index 0000000..2f5d566
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Stats.cs
@@ -0,0 +1,68 @@
+
+namespace Ink {
+    public struct Stats {
+
+        public int words;
+        public int knots;
+        public int stitches;
+        public int functions;
+        public int choices;
+        public int gathers;
+        public int diverts;
+
+        public static Stats Generate(Ink.Parsed.Story story) {
+            var stats = new Stats();
+
+            var allText = story.FindAll<Ink.Parsed.Text>();
+
+            // Count all the words across all strings
+            stats.words = 0;
+            foreach(var text in allText) {
+
+                var wordsInThisStr = 0;
+                var wasWhiteSpace = true;
+                foreach(var c in text.text) {
+                    if( c == ' ' || c == '\t' || c == '\n' || c == '\r' ) {
+                        wasWhiteSpace = true;
+                    } else if( wasWhiteSpace ) {
+                        wordsInThisStr++;
+                        wasWhiteSpace = false;
+                    }
+                }
+
+                stats.words += wordsInThisStr;
+            }
+
+            var knots = story.FindAll<Ink.Parsed.Knot>();
+            stats.knots = knots.Count;
+
+            stats.functions = 0;
+            foreach(var knot in knots)
+                if (knot.isFunction) stats.functions++;
+
+            var stitches = story.FindAll<Ink.Parsed.Stitch>();
+            stats.stitches = stitches.Count;
+
+            var choices = story.FindAll<Ink.Parsed.Choice>();
+            stats.choices = choices.Count;
+
+            // Skip implicit gather that's generated at top of story
+            // (we know which it is because it isn't assigned debug metadata)
+            var gathers = story.FindAll<Ink.Parsed.Gather>(g => g.debugMetadata != null);
+            stats.gathers = gathers.Count;
+
+            // May not be entirely what you expect.
+            // Does it nevertheless have value?
+            // Includes:
+            //  - DONE, END
+            //  - Function calls
+            //  - Some implicitly generated weave diverts
+            // But we subtract one for the implicit DONE
+            // at the end of the main flow outside of knots.
+            var diverts = story.FindAll<Ink.Parsed.Divert>();
+            stats.diverts = diverts.Count - 1;
+
+            return stats;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/InkLibs/InkCompiler/Stats.cs.meta b/Assets/Ink/InkLibs/InkCompiler/Stats.cs.meta
new file mode 100644
index 0000000..d1f427e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/Stats.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 13f65af28ab16d04a982dfcd855a2ccc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/StringParser.meta b/Assets/Ink/InkLibs/InkCompiler/StringParser.meta
new file mode 100644
index 0000000..552bdba
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/StringParser.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0b421d81498824ac79aca726eb847d1a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs
new file mode 100644
index 0000000..6feed54
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs
@@ -0,0 +1,685 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Diagnostics;
+using System.Text;
+
+namespace Ink
+{
+	public class StringParser
+	{
+		public delegate object ParseRule();
+
+        public delegate T SpecificParseRule<T>() where T : class;
+
+        public delegate void ErrorHandler(string message, int index, int lineIndex, bool isWarning);
+
+		public StringParser (string str)
+		{
+            str = PreProcessInputString (str);
+
+            state = new StringParserState();
+
+            if (str != null) {
+                _chars = str.ToCharArray ();
+            } else {
+                _chars = new char[0];
+            }
+
+			inputString = str;
+		}
+
+		public class ParseSuccessStruct {};
+		public static ParseSuccessStruct ParseSuccess = new ParseSuccessStruct();
+
+		public static CharacterSet numbersCharacterSet = new CharacterSet("0123456789");
+
+        protected ErrorHandler errorHandler { get; set; }
+
+		public char currentCharacter
+		{
+			get
+			{
+				if (index >= 0 && remainingLength > 0) {
+					return _chars [index];
+				} else {
+					return (char)0;
+				}
+			}
+		}
+
+		public StringParserState state { get; private set; }
+
+        public bool hadError { get; protected set; }
+
+        // Don't do anything by default, but provide ability for subclasses
+        // to manipulate the string before it's used as input (converted to a char array)
+        protected virtual string PreProcessInputString(string str)
+        {
+            return str;
+        }
+
+		//--------------------------------
+		// Parse state
+		//--------------------------------
+
+        protected int BeginRule()
+        {
+            return state.Push ();
+        }
+
+        protected object FailRule(int expectedRuleId)
+        {
+            state.Pop (expectedRuleId);
+            return null;
+        }
+
+        protected void CancelRule(int expectedRuleId)
+        {
+            state.Pop (expectedRuleId);
+        }
+
+        protected object SucceedRule(int expectedRuleId, object result = null)
+        {
+            // Get state at point where this rule stared evaluating
+            var stateAtSucceedRule = state.Peek(expectedRuleId);
+            var stateAtBeginRule = state.PeekPenultimate ();
+
+
+            // Allow subclass to receive callback
+            RuleDidSucceed (result, stateAtBeginRule, stateAtSucceedRule);
+
+            // Flatten state stack so that we maintain the same values,
+            // but remove one level in the stack.
+            state.Squash();
+
+            if (result == null) {
+                result = ParseSuccess;
+            }
+
+            return result;
+        }
+
+        protected virtual void RuleDidSucceed(object result, StringParserState.Element startState, StringParserState.Element endState)
+        {
+
+        }
+
+        protected object Expect(ParseRule rule, string message = null, ParseRule recoveryRule = null)
+		{
+            object result = ParseObject(rule);
+			if (result == null) {
+				if (message == null) {
+                    message = rule.Method.Name;
+				}
+
+                string butSaw;
+                string lineRemainder = LineRemainder ();
+                if (lineRemainder == null || lineRemainder.Length == 0) {
+                    butSaw = "end of line";
+                } else {
+                    butSaw = "'" + lineRemainder + "'";
+                }
+
+                Error ("Expected "+message+" but saw "+butSaw);
+
+				if (recoveryRule != null) {
+					result = recoveryRule ();
+				}
+			}
+			return result;
+		}
+
+        protected void Error(string message, bool isWarning = false)
+		{
+            ErrorOnLine (message, lineIndex + 1, isWarning);
+		}
+
+        protected void ErrorWithParsedObject(string message, Parsed.Object result, bool isWarning = false)
+        {
+            ErrorOnLine (message, result.debugMetadata.startLineNumber, isWarning);
+        }
+
+        protected void ErrorOnLine(string message, int lineNumber, bool isWarning)
+        {
+            if ( !state.errorReportedAlreadyInScope ) {
+
+                var errorType = isWarning ? "Warning" : "Error";
+
+                if (errorHandler == null) {
+                    throw new System.Exception (errorType+" on line " + lineNumber + ": " + message);
+                } else {
+                    errorHandler (message, index, lineNumber-1, isWarning);
+                }
+
+                state.NoteErrorReported ();
+            }
+
+            if( !isWarning )
+                hadError = true;
+        }
+
+        protected void Warning(string message)
+        {
+            Error(message, isWarning:true);
+        }
+
+		public bool endOfInput
+		{
+			get { return index >= _chars.Length; }
+		}
+
+		public string remainingString
+		{
+			get {
+				return new string(_chars, index, remainingLength);
+			}
+		}
+
+        public string LineRemainder()
+		{
+            return (string) Peek (() => ParseUntilCharactersFromString ("\n\r"));
+		}
+
+		public int remainingLength
+		{
+			get {
+				return _chars.Length - index;
+			}
+		}
+
+		public string inputString { get; private set; }
+
+
+        public int lineIndex
+        {
+            set {
+                state.lineIndex = value;
+            }
+            get {
+                return state.lineIndex;
+            }
+        }
+
+        public int characterInLineIndex
+        {
+            set {
+                state.characterInLineIndex = value;
+            }
+            get {
+                return state.characterInLineIndex;
+            }
+        }
+
+        public int index
+        {
+            // If we want subclass parsers to be able to set the index directly,
+            // then we would need to know what the lineIndex of the new
+            // index would be - would we have to step through manually
+            // counting the newlines to do so?
+            private set {
+                state.characterIndex = value;
+            }
+            get {
+                return state.characterIndex;
+            }
+        }
+
+        public void SetFlag(uint flag, bool trueOrFalse) {
+            if (trueOrFalse) {
+                state.customFlags |= flag;
+            } else {
+                state.customFlags &= ~flag;
+            }
+        }
+
+        public bool GetFlag(uint flag) {
+            return (state.customFlags & flag) != 0;
+        }
+
+		//--------------------------------
+		// Structuring
+		//--------------------------------
+
+        public object ParseObject(ParseRule rule)
+        {
+            int ruleId = BeginRule ();
+
+            var stackHeightBefore = state.stackHeight;
+
+            var result = rule ();
+
+            if (stackHeightBefore != state.stackHeight) {
+                throw new System.Exception ("Mismatched Begin/Fail/Succeed rules");
+            }
+
+            if (result == null)
+                return FailRule (ruleId);
+
+            SucceedRule (ruleId, result);
+            return result;
+        }
+
+        public T Parse<T>(SpecificParseRule<T> rule) where T : class
+        {
+            int ruleId = BeginRule ();
+
+            var result = rule () as T;
+            if (result == null) {
+                FailRule (ruleId);
+                return null;
+            }
+
+            SucceedRule (ruleId, result);
+            return result;
+        }
+
+		public object OneOf(params ParseRule[] array)
+		{
+			foreach (ParseRule rule in array) {
+                object result = ParseObject(rule);
+				if (result != null)
+                    return result;
+			}
+
+			return null;
+		}
+
+		public List<object> OneOrMore(ParseRule rule)
+		{
+			var results = new List<object> ();
+
+			object result = null;
+			do {
+                result = ParseObject(rule);
+				if( result != null ) {
+					results.Add(result);
+				}
+			} while(result != null);
+
+			if (results.Count > 0) {
+				return results;
+			} else {
+				return null;
+			}
+		}
+
+		public ParseRule Optional(ParseRule rule)
+		{
+			return () => {
+                object result = ParseObject(rule);
+				if( result == null ) {
+					result = ParseSuccess;
+				}
+				return result;
+			};
+		}
+
+        // Return ParseSuccess instead the real result so that it gets excluded
+        // from result arrays (e.g. Interleave)
+        public ParseRule Exclude(ParseRule rule)
+        {
+            return () => {
+                object result = ParseObject(rule);
+                if( result == null ) {
+                    return null;
+                }
+                return ParseSuccess;
+            };
+        }
+
+        // Combination of both of the above
+        public ParseRule OptionalExclude(ParseRule rule)
+        {
+            return () => {
+                ParseObject(rule);
+                return ParseSuccess;
+            };
+        }
+
+        // Convenience method for creating more readable ParseString rules that can be combined
+        // in other structuring rules (like OneOf etc)
+        // e.g. OneOf(String("one"), String("two"))
+        protected ParseRule String(string str)
+        {
+            return () => ParseString (str);
+        }
+
+		private void TryAddResultToList<T>(object result, List<T> list, bool flatten = true)
+		{
+			if (result == ParseSuccess) {
+				return;
+			}
+
+			if (flatten) {
+				var resultCollection = result as System.Collections.ICollection;
+				if (resultCollection != null) {
+					foreach (object obj in resultCollection) {
+						Debug.Assert (obj is T);
+						list.Add ((T)obj);
+					}
+					return;
+				}
+			}
+
+			Debug.Assert (result is T);
+			list.Add ((T)result);
+		}
+
+
+		public List<T> Interleave<T>(ParseRule ruleA, ParseRule ruleB, ParseRule untilTerminator = null, bool flatten = true)
+		{
+            int ruleId = BeginRule ();
+
+			var results = new List<T> ();
+
+			// First outer padding
+            var firstA = ParseObject(ruleA);
+			if (firstA == null) {
+                return (List<T>) FailRule(ruleId);
+			} else {
+				TryAddResultToList(firstA, results, flatten);
+			}
+
+			object lastMainResult = null, outerResult = null;
+			do {
+
+				// "until" condition hit?
+				if( untilTerminator != null && Peek(untilTerminator) != null ) {
+					break;
+				}
+
+				// Main inner
+                lastMainResult = ParseObject(ruleB);
+				if( lastMainResult == null ) {
+					break;
+				} else {
+					TryAddResultToList(lastMainResult, results, flatten);
+				}
+
+				// Outer result (i.e. last A in ABA)
+				outerResult = null;
+				if( lastMainResult != null ) {
+                    outerResult = ParseObject(ruleA);
+					if (outerResult == null) {
+						break;
+					} else {
+						TryAddResultToList(outerResult, results, flatten);
+					}
+				}
+
+			// Stop if there are no results, or if both are the placeholder "ParseSuccess" (i.e. Optional success rather than a true value)
+			} while((lastMainResult != null || outerResult != null)
+				 && !(lastMainResult == ParseSuccess && outerResult == ParseSuccess) && remainingLength > 0);
+
+			if (results.Count == 0) {
+                return (List<T>) FailRule(ruleId);
+			}
+
+            return (List<T>) SucceedRule(ruleId, results);
+		}
+
+		//--------------------------------
+		// Basic string parsing
+		//--------------------------------
+
+		public string ParseString(string str)
+		{
+			if (str.Length > remainingLength) {
+				return null;
+			}
+
+            int ruleId = BeginRule ();
+
+            // Optimisation from profiling:
+            // Store in temporary local variables
+            // since they're properties that would have to access
+            // the rule stack every time otherwise.
+            int i = index;
+            int cli = characterInLineIndex;
+            int li = lineIndex;
+
+			bool success = true;
+			foreach (char c in str) {
+				if ( _chars[i] != c) {
+					success = false;
+					break;
+				}
+                if (c == '\n') {
+                    li++;
+                    cli = -1;
+                }
+				i++;
+                cli++;
+			}
+
+            index = i;
+            characterInLineIndex = cli;
+            lineIndex = li;
+
+			if (success) {
+                return (string) SucceedRule(ruleId, str);
+			}
+			else {
+                return (string) FailRule (ruleId);
+			}
+		}
+
+        public char ParseSingleCharacter()
+        {
+            if (remainingLength > 0) {
+                char c = _chars [index];
+                if (c == '\n') {
+                    lineIndex++;
+                    characterInLineIndex = -1;
+                }
+                index++;
+                characterInLineIndex++;
+                return c;
+            } else {
+                return (char)0;
+            }
+        }
+
+		public string ParseUntilCharactersFromString(string str, int maxCount = -1)
+		{
+			return ParseCharactersFromString(str, false, maxCount);
+		}
+
+		public string ParseUntilCharactersFromCharSet(CharacterSet charSet, int maxCount = -1)
+		{
+			return ParseCharactersFromCharSet(charSet, false, maxCount);
+		}
+
+		public string ParseCharactersFromString(string str, int maxCount = -1)
+		{
+			return ParseCharactersFromString(str, true, maxCount);
+		}
+
+		public string ParseCharactersFromString(string str, bool shouldIncludeStrChars, int maxCount = -1)
+		{
+			return ParseCharactersFromCharSet (new CharacterSet(str), shouldIncludeStrChars);
+		}
+
+		public string ParseCharactersFromCharSet(CharacterSet charSet, bool shouldIncludeChars = true, int maxCount = -1)
+		{
+			if (maxCount == -1) {
+				maxCount = int.MaxValue;
+			}
+
+			int startIndex = index;
+
+            // Optimisation from profiling:
+            // Store in temporary local variables
+            // since they're properties that would have to access
+            // the rule stack every time otherwise.
+            int i = index;
+            int cli = characterInLineIndex;
+            int li = lineIndex;
+
+			int count = 0;
+            while ( i < _chars.Length && charSet.Contains (_chars [i]) == shouldIncludeChars && count < maxCount ) {
+                if (_chars [i] == '\n') {
+                    li++;
+                    cli = -1;
+                }
+                i++;
+                cli++;
+				count++;
+			}
+
+            index = i;
+            characterInLineIndex = cli;
+            lineIndex = li;
+
+			int lastCharIndex = index;
+			if (lastCharIndex > startIndex) {
+				return new string (_chars, startIndex, index - startIndex);
+			} else {
+				return null;
+			}
+		}
+
+		public object Peek(ParseRule rule)
+		{
+			int ruleId = BeginRule ();
+			object result = rule ();
+            CancelRule (ruleId);
+			return result;
+		}
+
+		public string ParseUntil(ParseRule stopRule, CharacterSet pauseCharacters = null, CharacterSet endCharacters = null)
+		{
+			int ruleId = BeginRule ();
+
+
+			CharacterSet pauseAndEnd = new CharacterSet ();
+			if (pauseCharacters != null) {
+				pauseAndEnd.UnionWith (pauseCharacters);
+			}
+			if (endCharacters != null) {
+				pauseAndEnd.UnionWith (endCharacters);
+			}
+
+			StringBuilder parsedString = new StringBuilder ();
+			object ruleResultAtPause = null;
+
+			// Keep attempting to parse strings up to the pause (and end) points.
+			//  - At each of the pause points, attempt to parse according to the rule
+			//  - When the end point is reached (or EOF), we're done
+			do {
+
+				// TODO: Perhaps if no pause or end characters are passed, we should check *every* character for stopRule?
+				string partialParsedString = ParseUntilCharactersFromCharSet(pauseAndEnd);
+				if( partialParsedString != null ) {
+					parsedString.Append(partialParsedString);
+				}
+
+				// Attempt to run the parse rule at this pause point
+				ruleResultAtPause = Peek(stopRule);
+
+				// Rule completed - we're done
+				if( ruleResultAtPause != null ) {
+					break;
+				} else {
+
+					if( endOfInput ) {
+						break;
+					}
+
+					// Reached a pause point, but rule failed. Step past and continue parsing string
+					char pauseCharacter = currentCharacter;
+					if( pauseCharacters != null && pauseCharacters.Contains(pauseCharacter) ) {
+						parsedString.Append(pauseCharacter);
+                        if( pauseCharacter == '\n' ) {
+                            lineIndex++;
+                            characterInLineIndex = -1;
+                        }
+						index++;
+                        characterInLineIndex++;
+						continue;
+					} else {
+						break;
+					}
+				}
+
+			} while(true);
+
+			if (parsedString.Length > 0) {
+                return (string) SucceedRule (ruleId, parsedString.ToString ());
+			} else {
+                return (string) FailRule (ruleId);
+			}
+
+		}
+
+        // No need to Begin/End rule since we never parse a newline, so keeping oldIndex is good enough
+		public int? ParseInt()
+		{
+			int oldIndex = index;
+            int oldCharacterInLineIndex = characterInLineIndex;
+
+			bool negative = ParseString ("-") != null;
+
+			// Optional whitespace
+			ParseCharactersFromString (" \t");
+
+			var parsedString = ParseCharactersFromCharSet (numbersCharacterSet);
+            if(parsedString == null) {
+                // Roll back and fail
+                index = oldIndex;
+                characterInLineIndex = oldCharacterInLineIndex;
+                return null;
+            }
+
+			int parsedInt;
+			if (int.TryParse (parsedString, out parsedInt)) {
+				return negative ? -parsedInt : parsedInt;
+			}
+
+            Error("Failed to read integer value: " + parsedString + ". Perhaps it's out of the range of acceptable numbers ink supports? (" + int.MinValue + " to " + int.MaxValue + ")");
+            return null;
+        }
+
+        // No need to Begin/End rule since we never parse a newline, so keeping oldIndex is good enough
+        public float? ParseFloat()
+        {
+            int oldIndex = index;
+            int oldCharacterInLineIndex = characterInLineIndex;
+
+            int? leadingInt = ParseInt ();
+            if (leadingInt != null) {
+
+                if (ParseString (".") != null) {
+
+                    var afterDecimalPointStr = ParseCharactersFromCharSet (numbersCharacterSet);
+                    return float.Parse (leadingInt+"." + afterDecimalPointStr, System.Globalization.CultureInfo.InvariantCulture);
+                }
+            }
+
+            // Roll back and fail
+            index = oldIndex;
+            characterInLineIndex = oldCharacterInLineIndex;
+            return null;
+        }
+
+        // You probably want "endOfLine", since it handles endOfFile too.
+        protected string ParseNewline()
+        {
+            int ruleId = BeginRule();
+
+            // Optional \r, definite \n to support Windows (\r\n) and Mac/Unix (\n)
+            // 2nd May 2016: Always collapse \r\n to just \n
+            ParseString ("\r");
+
+            if( ParseString ("\n") == null ) {
+                return (string) FailRule(ruleId);
+            } else {
+                return (string) SucceedRule(ruleId, "\n");
+            }
+        }
+
+		private char[] _chars;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs.meta b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs.meta
new file mode 100644
index 0000000..c70cc5f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 604abcb1d19ba4d28a071f968011e743
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs
new file mode 100644
index 0000000..625d905
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs
@@ -0,0 +1,170 @@
+
+namespace Ink
+{
+	public class StringParserState
+	{
+		public int lineIndex {
+			get { return currentElement.lineIndex; }
+			set { currentElement.lineIndex = value; }
+		}
+
+		public int characterIndex {
+			get { return currentElement.characterIndex; }
+			set { currentElement.characterIndex = value; }
+		}
+
+        public int characterInLineIndex {
+            get { return currentElement.characterInLineIndex; }
+            set { currentElement.characterInLineIndex = value; }
+        }
+
+        public uint customFlags {
+            get { return currentElement.customFlags; }
+            set { currentElement.customFlags = value; }
+        }
+
+        public bool errorReportedAlreadyInScope {
+            get {
+                return currentElement.reportedErrorInScope;
+            }
+        }
+
+        public int stackHeight {
+            get {
+                return _numElements;
+            }
+        }
+
+		public class Element {
+			public int characterIndex;
+            public int characterInLineIndex;
+			public int lineIndex;
+            public bool reportedErrorInScope;
+            public int uniqueId;
+            public uint customFlags;
+
+			public Element() {
+
+            }
+
+            public void CopyFrom(Element fromElement)
+            {
+                _uniqueIdCounter++;
+                this.uniqueId = _uniqueIdCounter;
+                this.characterIndex = fromElement.characterIndex;
+                this.characterInLineIndex = fromElement.characterInLineIndex;
+                this.lineIndex = fromElement.lineIndex;
+                this.customFlags = fromElement.customFlags;
+                this.reportedErrorInScope = false;
+            }
+
+            // Squash is used when succeeding from a rule,
+            // so only the state information we wanted to carry forward is
+            // retained. e.g. characterIndex and lineIndex are global,
+            // however uniqueId is specific to the individual rule,
+            // and likewise, custom flags are designed for the temporary
+            // state of the individual rule too.
+            public void SquashFrom(Element fromElement)
+            {
+                this.characterIndex = fromElement.characterIndex;
+                this.characterInLineIndex = fromElement.characterInLineIndex;
+                this.lineIndex = fromElement.lineIndex;
+                this.reportedErrorInScope = fromElement.reportedErrorInScope;
+            }
+
+            static int _uniqueIdCounter;
+		}
+
+		public StringParserState ()
+		{
+            const int kExpectedMaxStackDepth = 200;
+            _stack = new Element[kExpectedMaxStackDepth];
+
+            for (int i = 0; i < kExpectedMaxStackDepth; ++i) {
+                _stack [i] = new Element ();
+            }
+
+            _numElements = 1;
+		}
+
+		public int Push()
+		{
+            if (_numElements >= _stack.Length)
+                throw new System.Exception ("Stack overflow in parser state");
+
+            var prevElement = _stack [_numElements - 1];
+            var newElement = _stack[_numElements];
+            _numElements++;
+
+            newElement.CopyFrom (prevElement);
+
+            return newElement.uniqueId;
+		}
+
+        public void Pop(int expectedRuleId)
+		{
+            if (_numElements == 1) {
+				throw new System.Exception ("Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?");
+			}
+
+            if ( currentElement.uniqueId != expectedRuleId)
+                throw new System.Exception ("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?");
+
+			// Restore state
+            _numElements--;
+		}
+
+        public Element Peek(int expectedRuleId)
+		{
+            if (currentElement.uniqueId != expectedRuleId)
+                throw new System.Exception ("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?");
+
+            return _stack[_numElements-1];
+		}
+
+        public Element PeekPenultimate()
+        {
+            if (_numElements >= 2) {
+                return _stack [_numElements - 2];
+            } else {
+                return null;
+            }
+        }
+
+		// Reduce stack height while maintaining currentElement
+		// Remove second last element: i.e. "squash last two elements together"
+        // Used when succeeding from a rule (and ONLY when succeeding, since
+        // the state of the top element is retained).
+		public void Squash()
+		{
+            if (_numElements < 2) {
+				throw new System.Exception ("Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?");
+			}
+
+            var penultimateEl = _stack [_numElements - 2];
+            var lastEl = _stack [_numElements - 1];
+
+            penultimateEl.SquashFrom (lastEl);
+
+            _numElements--;
+		}
+
+        public void NoteErrorReported()
+        {
+            foreach (var el in _stack) {
+                el.reportedErrorInScope = true;
+            }
+        }
+
+		protected Element currentElement
+		{
+			get {
+                return _stack [_numElements - 1];
+			}
+		}
+
+        private Element[] _stack;
+        private int _numElements;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs.meta b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs.meta
new file mode 100644
index 0000000..946f13b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkCompiler/StringParser/StringParserState.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a7713c3de2b3f4a58adbeb1d2aaa69d8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime.meta b/Assets/Ink/InkLibs/InkRuntime.meta
new file mode 100644
index 0000000..35a652f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 09ba8a8c2206e48cfa59f4f51335455e
+folderAsset: yes
+timeCreated: 1460992356
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/CallStack.cs b/Assets/Ink/InkLibs/InkRuntime/CallStack.cs
new file mode 100644
index 0000000..06b5e66
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/CallStack.cs
@@ -0,0 +1,443 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Diagnostics;
+
+namespace Ink.Runtime
+{
+    public class CallStack
+    {
+        public class Element
+        {
+            public Pointer currentPointer;
+
+            public bool inExpressionEvaluation;
+            public Dictionary<string, Runtime.Object> temporaryVariables;
+            public PushPopType type;
+
+            // When this callstack element is actually a function evaluation called from the game,
+            // we need to keep track of the size of the evaluation stack when it was called
+            // so that we know whether there was any return value.
+            public int evaluationStackHeightWhenPushed;
+
+            // When functions are called, we trim whitespace from the start and end of what
+            // they generate, so we make sure know where the function's start and end are.
+            public int functionStartInOuputStream;
+
+            public Element(PushPopType type, Pointer pointer, bool inExpressionEvaluation = false) {
+                this.currentPointer = pointer;
+                this.inExpressionEvaluation = inExpressionEvaluation;
+                this.temporaryVariables = new Dictionary<string, Object>();
+                this.type = type;
+            }
+
+            public Element Copy()
+            {
+                var copy = new Element (this.type, currentPointer, this.inExpressionEvaluation);
+                copy.temporaryVariables = new Dictionary<string,Object>(this.temporaryVariables);
+                copy.evaluationStackHeightWhenPushed = evaluationStackHeightWhenPushed;
+                copy.functionStartInOuputStream = functionStartInOuputStream;
+                return copy;
+            }
+        }
+
+        public class Thread
+        {
+            public List<Element> callstack;
+            public int threadIndex;
+            public Pointer previousPointer;
+
+            public Thread() {
+                callstack = new List<Element>();
+            }
+
+			public Thread(Dictionary<string, object> jThreadObj, Story storyContext) : this() {
+                threadIndex = (int) jThreadObj ["threadIndex"];
+
+				List<object> jThreadCallstack = (List<object>) jThreadObj ["callstack"];
+				foreach (object jElTok in jThreadCallstack) {
+
+					var jElementObj = (Dictionary<string, object>)jElTok;
+
+                    PushPopType pushPopType = (PushPopType)(int)jElementObj ["type"];
+
+                    Pointer pointer = Pointer.Null;
+
+					string currentContainerPathStr = null;
+					object currentContainerPathStrToken;
+					if (jElementObj.TryGetValue ("cPath", out currentContainerPathStrToken)) {
+						currentContainerPathStr = currentContainerPathStrToken.ToString ();
+
+                        var threadPointerResult = storyContext.ContentAtPath (new Path (currentContainerPathStr));
+                        pointer.container = threadPointerResult.container;
+                        pointer.index = (int)jElementObj ["idx"];
+
+                        if (threadPointerResult.obj == null)
+                            throw new System.Exception ("When loading state, internal story location couldn't be found: " + currentContainerPathStr + ". Has the story changed since this save data was created?");
+                        else if (threadPointerResult.approximate)
+                            storyContext.Warning ("When loading state, exact internal story location couldn't be found: '" + currentContainerPathStr + "', so it was approximated to '"+pointer.container.path.ToString()+"' to recover. Has the story changed since this save data was created?");
+					}
+
+                    bool inExpressionEvaluation = (bool)jElementObj ["exp"];
+
+					var el = new Element (pushPopType, pointer, inExpressionEvaluation);
+
+                    object temps;
+                    if ( jElementObj.TryGetValue("temp", out temps) ) {
+                        el.temporaryVariables = Json.JObjectToDictionaryRuntimeObjs((Dictionary<string, object>)temps);
+                    } else {
+                        el.temporaryVariables.Clear();
+                    }					
+
+					callstack.Add (el);
+				}
+
+				object prevContentObjPath;
+				if( jThreadObj.TryGetValue("previousContentObject", out prevContentObjPath) ) {
+					var prevPath = new Path((string)prevContentObjPath);
+                    previousPointer = storyContext.PointerAtPath(prevPath);
+                }
+			}
+
+            public Thread Copy() {
+                var copy = new Thread ();
+                copy.threadIndex = threadIndex;
+                foreach(var e in callstack) {
+                    copy.callstack.Add(e.Copy());
+                }
+                copy.previousPointer = previousPointer;
+                return copy;
+            }
+
+            public void WriteJson(SimpleJson.Writer writer)
+            {
+                writer.WriteObjectStart();
+
+                // callstack
+                writer.WritePropertyStart("callstack");
+                writer.WriteArrayStart();
+                foreach (CallStack.Element el in callstack)
+                {
+                    writer.WriteObjectStart();
+                    if(!el.currentPointer.isNull) {
+                        writer.WriteProperty("cPath", el.currentPointer.container.path.componentsString);
+                        writer.WriteProperty("idx", el.currentPointer.index);
+                    }
+
+                    writer.WriteProperty("exp", el.inExpressionEvaluation);
+                    writer.WriteProperty("type", (int)el.type);
+
+                    if(el.temporaryVariables.Count > 0) {
+                        writer.WritePropertyStart("temp");
+                        Json.WriteDictionaryRuntimeObjs(writer, el.temporaryVariables);
+                        writer.WritePropertyEnd();
+                    }
+
+                    writer.WriteObjectEnd();
+                }
+                writer.WriteArrayEnd();
+                writer.WritePropertyEnd();
+
+                // threadIndex
+                writer.WriteProperty("threadIndex", threadIndex);
+
+                if (!previousPointer.isNull)
+                {
+                    writer.WriteProperty("previousContentObject", previousPointer.Resolve().path.ToString());
+                }
+
+                writer.WriteObjectEnd();
+            }
+        }
+
+        public List<Element> elements {
+            get {
+                return callStack;
+            }
+        }
+
+		public int depth {
+			get {
+				return elements.Count;
+			}
+		}
+
+        public Element currentElement { 
+            get {
+                var thread = _threads [_threads.Count - 1];
+                var cs = thread.callstack;
+                return cs [cs.Count - 1];
+            } 
+        }
+
+        public int currentElementIndex {
+            get {
+                return callStack.Count - 1;
+            }
+        }
+
+        public Thread currentThread
+        {
+            get {
+                return _threads [_threads.Count - 1];
+            }
+            set {
+                Debug.Assert (_threads.Count == 1, "Shouldn't be directly setting the current thread when we have a stack of them");
+                _threads.Clear ();
+                _threads.Add (value);
+            }
+        }
+
+        public bool canPop {
+            get {
+                return callStack.Count > 1;
+            }
+        }
+
+        public CallStack (Story storyContext)
+        {
+            _startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
+            Reset();
+        }
+
+
+        public CallStack(CallStack toCopy)
+        {
+            _threads = new List<Thread> ();
+            foreach (var otherThread in toCopy._threads) {
+                _threads.Add (otherThread.Copy ());
+            }
+            _threadCounter = toCopy._threadCounter;
+            _startOfRoot = toCopy._startOfRoot;
+        }
+
+        public void Reset() 
+        {
+            _threads = new List<Thread>();
+            _threads.Add(new Thread());
+
+            _threads[0].callstack.Add(new Element(PushPopType.Tunnel, _startOfRoot));
+        }
+
+
+        // Unfortunately it's not possible to implement jsonToken since
+        // the setter needs to take a Story as a context in order to
+        // look up objects from paths for currentContainer within elements.
+        public void SetJsonToken(Dictionary<string, object> jObject, Story storyContext)
+        {
+            _threads.Clear ();
+
+            var jThreads = (List<object>) jObject ["threads"];
+
+            foreach (object jThreadTok in jThreads) {
+                var jThreadObj = (Dictionary<string, object>)jThreadTok;
+                var thread = new Thread (jThreadObj, storyContext);
+                _threads.Add (thread);
+            }
+
+            _threadCounter = (int)jObject ["threadCounter"];
+            _startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
+        }
+
+        public void WriteJson(SimpleJson.Writer w)
+        {
+            w.WriteObject(writer =>
+            {
+                writer.WritePropertyStart("threads");
+                {
+                    writer.WriteArrayStart();
+
+                    foreach (CallStack.Thread thread in _threads)
+                    {
+                        thread.WriteJson(writer);
+                    }
+
+                    writer.WriteArrayEnd();
+                }
+                writer.WritePropertyEnd();
+
+                writer.WritePropertyStart("threadCounter");
+                {
+                    writer.Write(_threadCounter);
+                }
+                writer.WritePropertyEnd();
+            });
+        
+        }
+
+        public void PushThread()
+        {
+            var newThread = currentThread.Copy ();
+            _threadCounter++;
+            newThread.threadIndex = _threadCounter;
+            _threads.Add (newThread);
+        }
+
+        public Thread ForkThread()
+        {
+            var forkedThread = currentThread.Copy();
+            _threadCounter++;
+            forkedThread.threadIndex = _threadCounter;
+            return forkedThread;
+        }
+
+        public void PopThread()
+        {
+            if (canPopThread) {
+                _threads.Remove (currentThread);
+            } else {
+				throw new System.Exception("Can't pop thread");
+            }
+        }
+
+        public bool canPopThread
+        {
+            get {
+				return _threads.Count > 1 && !elementIsEvaluateFromGame;
+            }
+        }
+
+		public bool elementIsEvaluateFromGame
+		{
+			get {
+				return currentElement.type == PushPopType.FunctionEvaluationFromGame;
+			}
+		}
+
+        public void Push(PushPopType type, int externalEvaluationStackHeight = 0, int outputStreamLengthWithPushed = 0)
+        {
+            // When pushing to callstack, maintain the current content path, but jump out of expressions by default
+            var element = new Element (
+                type, 
+                currentElement.currentPointer,
+                inExpressionEvaluation: false
+            );
+
+            element.evaluationStackHeightWhenPushed = externalEvaluationStackHeight;
+            element.functionStartInOuputStream = outputStreamLengthWithPushed;
+
+            callStack.Add (element);
+        }
+
+        public bool CanPop(PushPopType? type = null) {
+
+            if (!canPop)
+                return false;
+            
+            if (type == null)
+                return true;
+            
+            return currentElement.type == type;
+        }
+            
+        public void Pop(PushPopType? type = null)
+        {
+            if (CanPop (type)) {
+                callStack.RemoveAt (callStack.Count - 1);
+                return;
+            } else {
+				throw new System.Exception("Mismatched push/pop in Callstack");
+            }
+        }
+
+        // Get variable value, dereferencing a variable pointer if necessary
+        public Runtime.Object GetTemporaryVariableWithName(string name, int contextIndex = -1)
+        {
+            if (contextIndex == -1)
+                contextIndex = currentElementIndex+1;
+            
+            Runtime.Object varValue = null;
+
+            var contextElement = callStack [contextIndex-1];
+
+            if (contextElement.temporaryVariables.TryGetValue (name, out varValue)) {
+                return varValue;
+            } else {
+                return null;
+            }
+        }
+            
+        public void SetTemporaryVariable(string name, Runtime.Object value, bool declareNew, int contextIndex = -1)
+        {
+            if (contextIndex == -1)
+                contextIndex = currentElementIndex+1;
+
+            var contextElement = callStack [contextIndex-1];
+            
+            if (!declareNew && !contextElement.temporaryVariables.ContainsKey(name)) {
+                throw new System.Exception ("Could not find temporary variable to set: " + name);
+            }
+
+            Runtime.Object oldValue;
+            if( contextElement.temporaryVariables.TryGetValue(name, out oldValue) )
+                ListValue.RetainListOriginsForAssignment (oldValue, value);
+
+            contextElement.temporaryVariables [name] = value;
+        }
+
+        // Find the most appropriate context for this variable.
+        // Are we referencing a temporary or global variable?
+        // Note that the compiler will have warned us about possible conflicts,
+        // so anything that happens here should be safe!
+        public int ContextForVariableNamed(string name)
+        {
+            // Current temporary context?
+            // (Shouldn't attempt to access contexts higher in the callstack.)
+            if (currentElement.temporaryVariables.ContainsKey (name)) {
+                return currentElementIndex+1;
+            } 
+
+            // Global
+            else {
+                return 0;
+            }
+        }
+            
+        public Thread ThreadWithIndex(int index)
+        {
+            return _threads.Find (t => t.threadIndex == index);
+        }
+
+        private List<Element> callStack
+        {
+            get {
+                return currentThread.callstack;
+            }
+        }
+
+		public string callStackTrace {
+			get {
+				var sb = new System.Text.StringBuilder();
+
+				for(int t=0; t<_threads.Count; t++) {
+
+					var thread = _threads[t];
+					var isCurrent = (t == _threads.Count-1);
+					sb.AppendFormat("=== THREAD {0}/{1} {2}===\n", (t+1), _threads.Count, (isCurrent ? "(current) ":""));
+
+					for(int i=0; i<thread.callstack.Count; i++) {
+
+						if( thread.callstack[i].type == PushPopType.Function )
+							sb.Append("  [FUNCTION] ");
+						else
+							sb.Append("  [TUNNEL] ");
+
+						var pointer = thread.callstack[i].currentPointer;
+						if( !pointer.isNull ) {
+							sb.Append("<SOMEWHERE IN ");
+							sb.Append(pointer.container.path.ToString());
+							sb.AppendLine(">");
+						}
+					}
+				}
+
+
+				return sb.ToString();
+			}
+		}
+
+        List<Thread> _threads;
+        int _threadCounter;
+        Pointer _startOfRoot;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/CallStack.cs.meta b/Assets/Ink/InkLibs/InkRuntime/CallStack.cs.meta
new file mode 100644
index 0000000..518122b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/CallStack.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 152213b0affe5410c9b7ef8eac085a64
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Choice.cs b/Assets/Ink/InkLibs/InkRuntime/Choice.cs
new file mode 100644
index 0000000..766a758
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Choice.cs
@@ -0,0 +1,54 @@
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// A generated Choice from the story.
+    /// A single ChoicePoint in the Story could potentially generate
+    /// different Choices dynamically dependent on state, so they're
+    /// separated.
+    /// </summary>
+	public class Choice : Runtime.Object
+	{
+        /// <summary>
+        /// The main text to presented to the player for this Choice.
+        /// </summary>
+        public string text { get; set; }
+
+        /// <summary>
+        /// The target path that the Story should be diverted to if
+        /// this Choice is chosen.
+        /// </summary>
+        public string pathStringOnChoice {
+            get {
+                return targetPath.ToString ();
+            }
+            set {
+                targetPath = new Path (value);
+            }
+        }
+
+        /// <summary>
+        /// Get the path to the original choice point - where was this choice defined in the story?
+        /// </summary>
+        /// <value>A dot separated path into the story data.</value>
+        public string sourcePath;
+
+        /// <summary>
+        /// The original index into currentChoices list on the Story when
+        /// this Choice was generated, for convenience.
+        /// </summary>
+        public int index { get; set; }
+
+        public Path targetPath;
+
+        public CallStack.Thread threadAtGeneration { get; set; }
+        public int originalThreadIndex;
+
+        public bool isInvisibleDefault;
+
+        public Choice()
+        {
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Choice.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Choice.cs.meta
new file mode 100644
index 0000000..f4d89d7
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Choice.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8db545ff917fa46d7aa5f472a9fdbad8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs b/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs
new file mode 100644
index 0000000..d212dea
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs
@@ -0,0 +1,89 @@
+using System.ComponentModel;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// The ChoicePoint represents the point within the Story where
+    /// a Choice instance gets generated. The distinction is made
+    /// because the text of the Choice can be dynamically generated.
+    /// </summary>
+	public class ChoicePoint : Runtime.Object
+	{
+        public Path pathOnChoice {
+            get {
+                // Resolve any relative paths to global ones as we come across them
+                if (_pathOnChoice != null && _pathOnChoice.isRelative) {
+                    var choiceTargetObj = choiceTarget;
+                    if (choiceTargetObj) {
+                        _pathOnChoice = choiceTargetObj.path;
+                    }
+                }
+                return _pathOnChoice;
+            }
+            set {
+                _pathOnChoice = value;
+            }
+        }
+        Path _pathOnChoice;
+
+        public Container choiceTarget {
+            get {
+                return this.ResolvePath (_pathOnChoice).container;
+            }
+        }
+
+        public string pathStringOnChoice {
+            get {
+                return CompactPathString (pathOnChoice);
+            }
+            set {
+                pathOnChoice = new Path (value);
+            }
+        }
+
+        public bool hasCondition { get; set; }
+        public bool hasStartContent { get; set; }
+        public bool hasChoiceOnlyContent { get; set; }
+        public bool onceOnly { get; set; }
+        public bool isInvisibleDefault { get; set; }
+
+        public int flags {
+            get {
+                int flags = 0;
+                if (hasCondition)         flags |= 1;
+                if (hasStartContent)      flags |= 2;
+                if (hasChoiceOnlyContent) flags |= 4;
+                if (isInvisibleDefault)   flags |= 8;
+                if (onceOnly)             flags |= 16;
+                return flags;
+            }
+            set {
+                hasCondition = (value & 1) > 0;
+                hasStartContent = (value & 2) > 0;
+                hasChoiceOnlyContent = (value & 4) > 0;
+                isInvisibleDefault = (value & 8) > 0;
+                onceOnly = (value & 16) > 0;
+            }
+        }
+
+        public ChoicePoint (bool onceOnly)
+		{
+            this.onceOnly = onceOnly;
+		}
+
+        public ChoicePoint() : this(true) {}
+
+        public override string ToString ()
+        {
+            int? targetLineNum = DebugLineNumberOfPath (pathOnChoice);
+            string targetString = pathOnChoice.ToString ();
+
+            if (targetLineNum != null) {
+                targetString = " line " + targetLineNum + "("+targetString+")";
+            } 
+
+            return "Choice: -> " + targetString;
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs.meta b/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs.meta
new file mode 100644
index 0000000..a73b14f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ChoicePoint.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b790cb942d6b84ca28e93a2b46d8c48f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Container.cs b/Assets/Ink/InkLibs/InkRuntime/Container.cs
new file mode 100644
index 0000000..a8a122b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Container.cs
@@ -0,0 +1,366 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.ComponentModel;
+
+namespace Ink.Runtime
+{
+	public class Container : Runtime.Object, INamedContent
+	{
+		public string name { get; set; }
+
+        public List<Runtime.Object> content { 
+            get {
+                return _content;
+            }
+            set {
+                AddContent (value);
+            }
+        }
+        List<Runtime.Object> _content;
+
+		public Dictionary<string, INamedContent> namedContent { get; set; }
+
+        public Dictionary<string, Runtime.Object> namedOnlyContent { 
+            get {
+                var namedOnlyContentDict = new Dictionary<string, Runtime.Object>();
+                foreach (var kvPair in namedContent) {
+                    namedOnlyContentDict [kvPair.Key] = (Runtime.Object)kvPair.Value;
+                }
+
+                foreach (var c in content) {
+                    var named = c as INamedContent;
+                    if (named != null && named.hasValidName) {
+                        namedOnlyContentDict.Remove (named.name);
+                    }
+                }
+
+                if (namedOnlyContentDict.Count == 0)
+                    namedOnlyContentDict = null;
+
+                return namedOnlyContentDict;
+            } 
+            set {
+                var existingNamedOnly = namedOnlyContent;
+                if (existingNamedOnly != null) {
+                    foreach (var kvPair in existingNamedOnly) {
+                        namedContent.Remove (kvPair.Key);
+                    }
+                }
+
+                if (value == null)
+                    return;
+                
+                foreach (var kvPair in value) {
+                    var named = kvPair.Value as INamedContent;
+                    if( named != null )
+                        AddToNamedContentOnly (named);
+                }
+            }
+        }
+            
+        public bool visitsShouldBeCounted { get; set; }
+        public bool turnIndexShouldBeCounted { get; set; }
+        public bool countingAtStartOnly { get; set; }
+
+        [Flags]
+        public enum CountFlags
+        {
+            Visits         = 1,
+            Turns          = 2,
+            CountStartOnly = 4
+        }
+                
+        public int countFlags
+        {
+            get {
+                CountFlags flags = 0;
+                if (visitsShouldBeCounted)    flags |= CountFlags.Visits;
+                if (turnIndexShouldBeCounted) flags |= CountFlags.Turns;
+                if (countingAtStartOnly)      flags |= CountFlags.CountStartOnly;
+
+                // If we're only storing CountStartOnly, it serves no purpose,
+                // since it's dependent on the other two to be used at all.
+                // (e.g. for setting the fact that *if* a gather or choice's
+                // content is counted, then is should only be counter at the start)
+                // So this is just an optimisation for storage.
+                if (flags == CountFlags.CountStartOnly) {
+                    flags = 0;
+                }
+
+                return (int)flags;
+            }
+            set {
+                var flag = (CountFlags)value;
+                if ((flag & CountFlags.Visits) > 0) visitsShouldBeCounted = true;
+                if ((flag & CountFlags.Turns) > 0)  turnIndexShouldBeCounted = true;
+                if ((flag & CountFlags.CountStartOnly) > 0) countingAtStartOnly = true;
+            }
+        }
+
+		public bool hasValidName 
+		{
+			get { return name != null && name.Length > 0; }
+		}
+
+		public Path pathToFirstLeafContent
+		{
+			get {
+                if( _pathToFirstLeafContent == null )
+                    _pathToFirstLeafContent = path.PathByAppendingPath (internalPathToFirstLeafContent);
+
+                return _pathToFirstLeafContent;
+			}
+		}
+        Path _pathToFirstLeafContent;
+
+        Path internalPathToFirstLeafContent
+        {
+            get {
+				var components = new List<Path.Component>();
+                var container = this;
+                while (container != null) {
+                    if (container.content.Count > 0) {
+                        components.Add (new Path.Component (0));
+                        container = container.content [0] as Container;
+                    }
+                }
+				return new Path(components);
+            }
+        }
+
+		public Container ()
+		{
+            _content = new List<Runtime.Object> ();
+			namedContent = new Dictionary<string, INamedContent> ();
+		}
+
+		public void AddContent(Runtime.Object contentObj)
+		{
+			content.Add (contentObj);
+
+            if (contentObj.parent) {
+                throw new System.Exception ("content is already in " + contentObj.parent);
+            }
+
+			contentObj.parent = this;
+
+			TryAddNamedContent (contentObj);
+		}
+
+        public void AddContent(IList<Runtime.Object> contentList)
+        {
+            foreach (var c in contentList) {
+                AddContent (c);
+            }
+        }
+
+        public void InsertContent(Runtime.Object contentObj, int index)
+        {
+            content.Insert (index, contentObj);
+
+            if (contentObj.parent) {
+                throw new System.Exception ("content is already in " + contentObj.parent);
+            }
+
+            contentObj.parent = this;
+
+            TryAddNamedContent (contentObj);
+        }
+            
+		public void TryAddNamedContent(Runtime.Object contentObj)
+		{
+			var namedContentObj = contentObj as INamedContent;
+			if (namedContentObj != null && namedContentObj.hasValidName) {
+				AddToNamedContentOnly (namedContentObj);
+			}
+		}
+
+		public void AddToNamedContentOnly(INamedContent namedContentObj)
+		{
+			Debug.Assert (namedContentObj is Runtime.Object, "Can only add Runtime.Objects to a Runtime.Container");
+			var runtimeObj = (Runtime.Object)namedContentObj;
+			runtimeObj.parent = this;
+
+			namedContent [namedContentObj.name] = namedContentObj;
+		}
+
+        public void AddContentsOfContainer(Container otherContainer)
+        {
+            content.AddRange (otherContainer.content);
+            foreach (var obj in otherContainer.content) {
+                obj.parent = this;
+                TryAddNamedContent (obj);
+            }
+        }
+
+		protected Runtime.Object ContentWithPathComponent(Path.Component component)
+		{
+            if (component.isIndex) {
+
+                if (component.index >= 0 && component.index < content.Count) {
+                    return content [component.index];
+                }
+
+				// When path is out of range, quietly return nil
+				// (useful as we step/increment forwards through content)
+				else {
+                    return null;
+                }
+
+            } 
+
+            else if (component.isParent) {
+                return this.parent;
+            }
+
+            else {
+                INamedContent foundContent = null;
+                if (namedContent.TryGetValue (component.name, out foundContent)) {
+                    return (Runtime.Object)foundContent;
+                } else {
+                    return null;
+                }
+			}
+		}
+
+        public SearchResult ContentAtPath(Path path, int partialPathStart = 0, int partialPathLength = -1)
+		{
+            if (partialPathLength == -1)
+                partialPathLength = path.length;
+
+            var result = new SearchResult ();
+            result.approximate = false;
+
+            Container currentContainer = this;
+            Runtime.Object currentObj = this;
+
+            for (int i = partialPathStart; i < partialPathLength; ++i) {
+				var comp = path.GetComponent(i);
+
+                // Path component was wrong type
+                if (currentContainer == null) {
+                    result.approximate = true;
+                    break;
+                }
+
+                var foundObj = currentContainer.ContentWithPathComponent(comp);
+
+                // Couldn't resolve entire path?
+                if (foundObj == null) {
+                    result.approximate = true;
+                    break;
+                } 
+
+                currentObj = foundObj;
+                currentContainer = foundObj as Container;
+            }
+
+            result.obj = currentObj;
+
+            return result;
+		}
+         
+        public void BuildStringOfHierarchy(StringBuilder sb, int indentation, Runtime.Object pointedObj)
+        {
+            Action appendIndentation = () => { 
+                const int spacesPerIndent = 4;
+                for(int i=0; i<spacesPerIndent*indentation;++i) { 
+                    sb.Append(" "); 
+                } 
+            };
+
+            appendIndentation ();
+            sb.Append("[");
+
+            if (this.hasValidName) {
+                sb.AppendFormat (" ({0})", this.name);
+            }
+
+            if (this == pointedObj) {
+                sb.Append ("  <---");
+            }
+
+            sb.AppendLine ();
+
+            indentation++;
+            
+            for (int i=0; i<content.Count; ++i) {
+
+                var obj = content [i];
+
+                if (obj is Container) {
+
+                    var container = (Container)obj;
+
+                    container.BuildStringOfHierarchy (sb, indentation, pointedObj);
+
+                } else {
+                    appendIndentation ();
+                    if (obj is StringValue) {
+                        sb.Append ("\"");
+                        sb.Append (obj.ToString ().Replace ("\n", "\\n"));
+                        sb.Append ("\"");
+                    } else {
+                        sb.Append (obj.ToString ());
+                    }
+                }
+
+                if (i != content.Count - 1) {
+                    sb.Append (",");
+                }
+
+                if ( !(obj is Container) && obj == pointedObj ) {
+                    sb.Append ("  <---");
+                }
+                    
+                sb.AppendLine ();
+            }
+                
+
+            var onlyNamed = new Dictionary<string, INamedContent> ();
+
+            foreach (var objKV in namedContent) {
+                if (content.Contains ((Runtime.Object)objKV.Value)) {
+                    continue;
+                } else {
+                    onlyNamed.Add (objKV.Key, objKV.Value);
+                }
+            }
+
+            if (onlyNamed.Count > 0) {
+                appendIndentation ();
+                sb.AppendLine ("-- named: --");
+
+                foreach (var objKV in onlyNamed) {
+
+                    Debug.Assert (objKV.Value is Container, "Can only print out named Containers");
+                    var container = (Container)objKV.Value;
+                    container.BuildStringOfHierarchy (sb, indentation, pointedObj);
+
+                    sb.AppendLine ();
+
+                }
+            }
+
+
+            indentation--;
+
+            appendIndentation ();
+            sb.Append ("]");
+        }
+
+        public virtual string BuildStringOfHierarchy()
+        {
+            var sb = new StringBuilder ();
+
+            BuildStringOfHierarchy (sb, 0, null);
+
+            return sb.ToString ();
+        }
+
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Container.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Container.cs.meta
new file mode 100644
index 0000000..8c5a93a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Container.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2739750c31df04462a8dedfa904e8760
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs b/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs
new file mode 100644
index 0000000..8d3ad09
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs
@@ -0,0 +1,170 @@
+using System;
+
+namespace Ink.Runtime
+{
+    public class ControlCommand : Runtime.Object
+    {
+        public enum CommandType
+        {
+            NotSet = -1,
+            EvalStart,
+            EvalOutput,
+            EvalEnd,
+            Duplicate,
+            PopEvaluatedValue,
+            PopFunction,
+            PopTunnel,
+            BeginString,
+            EndString,
+            NoOp,
+            ChoiceCount,
+            Turns,
+            TurnsSince,
+            ReadCount,
+            Random,
+            SeedRandom,
+            VisitIndex,
+            SequenceShuffleIndex,
+            StartThread,
+            Done,
+            End,
+            ListFromInt,
+            ListRange,
+            ListRandom,
+            //----
+            TOTAL_VALUES
+        }
+            
+        public CommandType commandType { get; protected set; }
+
+        public ControlCommand (CommandType commandType)
+        {
+            this.commandType = commandType;
+        }
+
+        // Require default constructor for serialisation
+        public ControlCommand() : this(CommandType.NotSet) {}
+
+        public override Object Copy()
+        {
+            return new ControlCommand (commandType);
+        }
+
+        // The following static factory methods are to make generating these objects
+        // slightly more succinct. Without these, the code gets pretty massive! e.g.
+        //
+        //     var c = new Runtime.ControlCommand(Runtime.ControlCommand.CommandType.EvalStart)
+        // 
+        // as opposed to
+        //
+        //     var c = Runtime.ControlCommand.EvalStart()
+
+        public static ControlCommand EvalStart() {
+            return new ControlCommand(CommandType.EvalStart);
+        }
+
+        public static ControlCommand EvalOutput() {
+            return new ControlCommand(CommandType.EvalOutput);
+        }
+
+        public static ControlCommand EvalEnd() {
+            return new ControlCommand(CommandType.EvalEnd);
+        }
+
+        public static ControlCommand Duplicate() {
+            return new ControlCommand(CommandType.Duplicate);
+        }
+
+        public static ControlCommand PopEvaluatedValue() {
+            return new ControlCommand (CommandType.PopEvaluatedValue);
+        }
+
+        public static ControlCommand PopFunction() {
+            return new ControlCommand (CommandType.PopFunction);
+        }
+
+        public static ControlCommand PopTunnel() {
+            return new ControlCommand (CommandType.PopTunnel);
+        }
+            
+        public static ControlCommand BeginString() {
+            return new ControlCommand (CommandType.BeginString);
+        }
+
+        public static ControlCommand EndString() {
+            return new ControlCommand (CommandType.EndString);
+        }
+
+        public static ControlCommand NoOp() {
+            return new ControlCommand(CommandType.NoOp);
+        }
+
+        public static ControlCommand ChoiceCount() {
+            return new ControlCommand(CommandType.ChoiceCount);
+        }
+
+        public static ControlCommand Turns ()
+        {
+            return new ControlCommand (CommandType.Turns);
+        }
+
+        public static ControlCommand TurnsSince() {
+            return new ControlCommand(CommandType.TurnsSince);
+        }
+
+        public static ControlCommand ReadCount ()
+        {
+            return new ControlCommand (CommandType.ReadCount);
+        }
+
+        public static ControlCommand Random ()
+        {
+            return new ControlCommand (CommandType.Random);
+        }
+
+        public static ControlCommand SeedRandom ()
+        {
+            return new ControlCommand (CommandType.SeedRandom);
+        }
+
+        public static ControlCommand VisitIndex() {
+            return new ControlCommand(CommandType.VisitIndex);
+        }
+            
+        public static ControlCommand SequenceShuffleIndex() {
+            return new ControlCommand(CommandType.SequenceShuffleIndex);
+        }
+
+        public static ControlCommand StartThread() {
+            return new ControlCommand (CommandType.StartThread);
+        }
+
+        public static ControlCommand Done() {
+            return new ControlCommand (CommandType.Done);
+        }
+
+        public static ControlCommand End() {
+            return new ControlCommand (CommandType.End);
+        }
+
+        public static ControlCommand ListFromInt () {
+            return new ControlCommand (CommandType.ListFromInt);
+        }
+
+        public static ControlCommand ListRange ()
+        {
+            return new ControlCommand (CommandType.ListRange);
+        }
+
+        public static ControlCommand ListRandom ()
+        {
+            return new ControlCommand (CommandType.ListRandom);
+        }
+
+        public override string ToString ()
+        {
+            return commandType.ToString();
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs.meta b/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs.meta
new file mode 100644
index 0000000..145c645
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ControlCommand.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 41100189b7af04624a6c92ee13e18232
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs b/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs
new file mode 100644
index 0000000..1d06f30
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace Ink.Runtime
+{
+    public class DebugMetadata
+    {
+        public int startLineNumber = 0;
+        public int endLineNumber = 0;
+        public int startCharacterNumber = 0;
+        public int endCharacterNumber = 0;
+        public string fileName = null;
+        public string sourceName = null;
+
+        public DebugMetadata ()
+        {
+        }
+
+        // Currently only used in VariableReference in order to
+        // merge the debug metadata of a Path.Of.Indentifiers into
+        // one single range.
+        public DebugMetadata Merge(DebugMetadata dm)
+        {
+            var newDebugMetadata = new DebugMetadata();
+
+            // These are not supposed to be differ between 'this' and 'dm'.
+            newDebugMetadata.fileName = fileName;
+            newDebugMetadata.sourceName = sourceName;
+
+            if (startLineNumber < dm.startLineNumber)
+            {
+                newDebugMetadata.startLineNumber = startLineNumber;
+                newDebugMetadata.startCharacterNumber = startCharacterNumber;
+            }
+            else if (startLineNumber > dm.startLineNumber)
+            {
+                newDebugMetadata.startLineNumber = dm.startLineNumber;
+                newDebugMetadata.startCharacterNumber = dm.startCharacterNumber;
+            }
+            else
+            {
+                newDebugMetadata.startLineNumber = startLineNumber;
+                newDebugMetadata.startCharacterNumber = Math.Min(startCharacterNumber, dm.startCharacterNumber);
+            }
+
+            if (endLineNumber > dm.endLineNumber)
+            {
+                newDebugMetadata.endLineNumber = endLineNumber;
+                newDebugMetadata.endCharacterNumber = endCharacterNumber;
+            }
+            else if (endLineNumber < dm.endLineNumber)
+            {
+                newDebugMetadata.endLineNumber = dm.endLineNumber;
+                newDebugMetadata.endCharacterNumber = dm.endCharacterNumber;
+            }
+            else
+            {
+                newDebugMetadata.endLineNumber = endLineNumber;
+                newDebugMetadata.endCharacterNumber = Math.Max(endCharacterNumber, dm.endCharacterNumber);
+            }
+
+            return newDebugMetadata;
+        }
+
+        public override string ToString ()
+        {
+            if (fileName != null) {
+                return string.Format ("line {0} of {1}", startLineNumber, fileName);
+            } else {
+                return "line " + startLineNumber;
+            }
+
+        }
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs.meta b/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs.meta
new file mode 100644
index 0000000..f038691
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/DebugMetadata.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c1ae8e933de9541df9d12af663906aaf
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Divert.cs b/Assets/Ink/InkLibs/InkRuntime/Divert.cs
new file mode 100644
index 0000000..5c15a68
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Divert.cs
@@ -0,0 +1,149 @@
+using System.Text;
+
+namespace Ink.Runtime
+{
+	public class Divert : Runtime.Object
+	{
+        public Path targetPath { 
+            get { 
+                // Resolve any relative paths to global ones as we come across them
+                if (_targetPath != null && _targetPath.isRelative) {
+                    var targetObj = targetPointer.Resolve();
+                    if (targetObj) {
+                        _targetPath = targetObj.path;
+                    }
+                }
+                return _targetPath;
+            }
+            set {
+                _targetPath = value;
+                _targetPointer = Pointer.Null;
+            } 
+        }
+        Path _targetPath;
+
+        public Pointer targetPointer {
+            get {
+                if (_targetPointer.isNull) {
+                    var targetObj = ResolvePath (_targetPath).obj;
+
+                    if (_targetPath.lastComponent.isIndex) {
+                        _targetPointer.container = targetObj.parent as Container;
+                        _targetPointer.index = _targetPath.lastComponent.index;
+                    } else {
+                        _targetPointer = Pointer.StartOf (targetObj as Container);
+                    }
+                }
+                return _targetPointer;
+            }
+        }
+        Pointer _targetPointer;
+        
+
+        public string targetPathString {
+            get {
+                if (targetPath == null)
+                    return null;
+
+                return CompactPathString (targetPath);
+            }
+            set {
+                if (value == null) {
+                    targetPath = null;
+                } else {
+                    targetPath = new Path (value);
+                }
+            }
+        }
+            
+        public string variableDivertName { get; set; }
+        public bool hasVariableTarget { get { return variableDivertName != null; } }
+
+        public bool pushesToStack { get; set; }
+        public PushPopType stackPushType;
+
+        public bool isExternal { get; set; }
+        public int externalArgs { get; set; }
+
+        public bool isConditional { get; set; }
+
+		public Divert ()
+		{
+            pushesToStack = false;
+		}
+
+        public Divert(PushPopType stackPushType)
+        {
+            pushesToStack = true;
+            this.stackPushType = stackPushType;
+        }
+
+        public override bool Equals (object obj)
+        {
+            var otherDivert = obj as Divert;
+            if (otherDivert) {
+                if (this.hasVariableTarget == otherDivert.hasVariableTarget) {
+                    if (this.hasVariableTarget) {
+                        return this.variableDivertName == otherDivert.variableDivertName;
+                    } else {
+                        return this.targetPath.Equals(otherDivert.targetPath);
+                    }
+                }
+            }
+            return false;
+        }
+
+        public override int GetHashCode ()
+        {
+            if (hasVariableTarget) {
+                const int variableTargetSalt = 12345;
+                return variableDivertName.GetHashCode() + variableTargetSalt;
+            } else {
+                const int pathTargetSalt = 54321;
+                return targetPath.GetHashCode() + pathTargetSalt;
+            }
+        }
+
+        public override string ToString ()
+        {
+            if (hasVariableTarget) {
+                return "Divert(variable: " + variableDivertName + ")";
+            }
+            else if (targetPath == null) {
+                return "Divert(null)";
+            } else {
+
+                var sb = new StringBuilder ();
+
+                string targetStr = targetPath.ToString ();
+                int? targetLineNum = DebugLineNumberOfPath (targetPath);
+                if (targetLineNum != null) {
+                    targetStr = "line " + targetLineNum;
+                }
+
+                sb.Append ("Divert");
+
+                if (isConditional)
+                    sb.Append ("?");
+
+                if (pushesToStack) {
+                    if (stackPushType == PushPopType.Function) {
+                        sb.Append (" function");
+                    } else {
+                        sb.Append (" tunnel");
+                    }
+                }
+
+                sb.Append (" -> ");
+                sb.Append (targetPathString);
+
+                sb.Append (" (");
+                sb.Append (targetStr);
+                sb.Append (")");
+
+                return sb.ToString ();
+            }
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Divert.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Divert.cs.meta
new file mode 100644
index 0000000..f6bcc96
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Divert.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e98dd0cea4d0a4ddcbc9a1f135b486d4
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Error.cs b/Assets/Ink/InkLibs/InkRuntime/Error.cs
new file mode 100644
index 0000000..38a5dc5
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Error.cs
@@ -0,0 +1,24 @@
+namespace Ink
+{
+    /// <summary>
+    /// Callback for errors throughout both the ink runtime and compiler.
+    /// </summary>
+    public delegate void ErrorHandler(string message, ErrorType type);
+
+    /// <summary>
+    /// Author errors will only ever come from the compiler so don't need to be handled
+    /// by your Story error handler. The "Error" ErrorType is by far the most common
+    /// for a runtime story error (rather than compiler error), though the Warning type
+    /// is also possible. 
+    /// </summary>
+    public enum ErrorType
+    {
+        /// Generated by a "TODO" note in the ink source
+        Author,
+        /// You should probably fix this, but it's not critical
+        Warning,
+        /// Critical error that can't be recovered from
+        Error
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Error.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Error.cs.meta
new file mode 100644
index 0000000..5ad970e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Error.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5119822ad16c24c40b1f80c639c17671
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Flow.cs b/Assets/Ink/InkLibs/InkRuntime/Flow.cs
new file mode 100644
index 0000000..37da8ec
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Flow.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public class Flow {
+        public string name;
+        public CallStack callStack;
+        public List<Runtime.Object> outputStream;
+        public List<Choice> currentChoices;
+
+        public Flow(string name, Story story) {
+            this.name = name;
+            this.callStack = new CallStack(story);
+            this.outputStream = new List<Object>();
+            this.currentChoices = new List<Choice>();
+        }
+
+        public Flow(string name, Story story, Dictionary<string, object> jObject) {
+            this.name = name;
+            this.callStack = new CallStack(story);
+            this.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstack"], story);
+            this.outputStream = Json.JArrayToRuntimeObjList ((List<object>)jObject ["outputStream"]);
+			this.currentChoices = Json.JArrayToRuntimeObjList<Choice>((List<object>)jObject ["currentChoices"]);
+
+            // choiceThreads is optional
+            object jChoiceThreadsObj;
+            jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj);
+            LoadFlowChoiceThreads((Dictionary<string, object>)jChoiceThreadsObj, story);
+        }
+
+        public void WriteJson(SimpleJson.Writer writer)
+        {
+            writer.WriteObjectStart();
+
+            writer.WriteProperty("callstack", callStack.WriteJson);
+            writer.WriteProperty("outputStream", w => Json.WriteListRuntimeObjs(w, outputStream));
+
+            // choiceThreads: optional
+            // Has to come BEFORE the choices themselves are written out
+            // since the originalThreadIndex of each choice needs to be set
+            bool hasChoiceThreads = false;
+            foreach (Choice c in currentChoices)
+            {
+                c.originalThreadIndex = c.threadAtGeneration.threadIndex;
+
+                if (callStack.ThreadWithIndex(c.originalThreadIndex) == null)
+                {
+                    if (!hasChoiceThreads)
+                    {
+                        hasChoiceThreads = true;
+                        writer.WritePropertyStart("choiceThreads");
+                        writer.WriteObjectStart();
+                    }
+
+                    writer.WritePropertyStart(c.originalThreadIndex);
+                    c.threadAtGeneration.WriteJson(writer);
+                    writer.WritePropertyEnd();
+                }
+            }
+
+            if (hasChoiceThreads)
+            {
+                writer.WriteObjectEnd();
+                writer.WritePropertyEnd();
+            }
+
+
+            writer.WriteProperty("currentChoices", w => {
+                w.WriteArrayStart();
+                foreach (var c in currentChoices)
+                    Json.WriteChoice(w, c);
+                w.WriteArrayEnd();
+            });
+
+
+            writer.WriteObjectEnd();
+        }
+
+        // Used both to load old format and current
+        public void LoadFlowChoiceThreads(Dictionary<string, object> jChoiceThreads, Story story)
+        {
+            foreach (var choice in currentChoices) {
+				var foundActiveThread = callStack.ThreadWithIndex(choice.originalThreadIndex);
+				if( foundActiveThread != null ) {
+                    choice.threadAtGeneration = foundActiveThread.Copy ();
+				} else {
+					var jSavedChoiceThread = (Dictionary <string, object>) jChoiceThreads[choice.originalThreadIndex.ToString()];
+					choice.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, story);
+				}
+			}
+        }
+    }
+}
\ No newline at end of file
diff --git a/Assets/Ink/InkLibs/InkRuntime/Flow.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Flow.cs.meta
new file mode 100644
index 0000000..9d75b48
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Flow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 625dee430631288439017a5dc0349c86
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Glue.cs b/Assets/Ink/InkLibs/InkRuntime/Glue.cs
new file mode 100644
index 0000000..cfdfb25
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Glue.cs
@@ -0,0 +1,13 @@
+namespace Ink.Runtime
+{
+    public class Glue : Runtime.Object
+    {
+        public Glue() { }
+
+        public override string ToString ()
+        {
+            return "Glue";
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Glue.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Glue.cs.meta
new file mode 100644
index 0000000..a46209f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Glue.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 60dc9de8c1e834c6eade8d924a7afe45
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs b/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs
new file mode 100644
index 0000000..3e45199
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs
@@ -0,0 +1,10 @@
+
+namespace Ink.Runtime
+{
+	public interface INamedContent
+	{
+		string name { get; }
+		bool hasValidName { get; }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs.meta b/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs.meta
new file mode 100644
index 0000000..3f8aa54
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/INamedContent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: db1f7c668efd4422d8437ac965745242
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/InkList.cs b/Assets/Ink/InkLibs/InkRuntime/InkList.cs
new file mode 100644
index 0000000..7bc6d66
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/InkList.cs
@@ -0,0 +1,584 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// The underlying type for a list item in ink. It stores the original list definition
+    /// name as well as the item name, but without the value of the item. When the value is
+    /// stored, it's stored in a KeyValuePair of InkListItem and int.
+    /// </summary>
+    public struct InkListItem
+    {
+        /// <summary>
+        /// The name of the list where the item was originally defined.
+        /// </summary>
+        public readonly string originName;
+
+        /// <summary>
+        /// The main name of the item as defined in ink.
+        /// </summary>
+        public readonly string itemName;
+
+        /// <summary>
+        /// Create an item with the given original list definition name, and the name of this
+        /// item.
+        /// </summary>
+        public InkListItem (string originName, string itemName)
+        {
+            this.originName = originName;
+            this.itemName = itemName;
+        }
+
+        /// <summary>
+        /// Create an item from a dot-separted string of the form "listDefinitionName.listItemName".
+        /// </summary>
+        public InkListItem (string fullName)
+        {
+            var nameParts = fullName.Split ('.');
+            this.originName = nameParts [0];
+            this.itemName = nameParts [1];
+        }
+
+        public static InkListItem Null {
+            get {
+                return new InkListItem (null, null);
+            }
+        }
+
+        public bool isNull {
+            get {
+                return originName == null && itemName == null;
+            }
+        }
+
+        /// <summary>
+        /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
+        /// </summary>
+        public string fullName {
+            get {
+                return (originName ?? "?") + "." + itemName;
+            }
+        }
+
+        /// <summary>
+        /// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
+        /// Calls fullName internally.
+        /// </summary>
+        public override string ToString ()
+        {
+            return fullName;
+        }
+
+        /// <summary>
+        /// Is this item the same as another item?
+        /// </summary>
+        public override bool Equals (object obj)
+        {
+            if (obj is InkListItem) {
+                var otherItem = (InkListItem)obj;
+                return otherItem.itemName   == itemName 
+                    && otherItem.originName == originName;
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Get the hashcode for an item.
+        /// </summary>
+        public override int GetHashCode ()
+        {
+            int originCode = 0;
+            int itemCode = itemName.GetHashCode ();
+            if (originName != null)
+                originCode = originName.GetHashCode ();
+            
+            return originCode + itemCode;
+        }
+    }
+
+    /// <summary>
+    /// The InkList is the underlying type that's used to store an instance of a
+    /// list in ink. It's not used for the *definition* of the list, but for a list
+    /// value that's stored in a variable.
+    /// Somewhat confusingly, it's backed by a C# Dictionary, and has nothing to
+    /// do with a C# List!
+    /// </summary>
+    public class InkList : Dictionary<InkListItem, int>
+    {
+        /// <summary>
+        /// Create a new empty ink list.
+        /// </summary>
+        public InkList () { }
+
+        /// <summary>
+        /// Create a new ink list that contains the same contents as another list.
+        /// </summary>
+        public InkList(InkList otherList) : base(otherList)
+        {
+            _originNames = otherList.originNames;
+            if (otherList.origins != null)
+            {
+                origins = new List<ListDefinition>(otherList.origins);
+            }
+        }
+
+        /// <summary>
+        /// Create a new empty ink list that's intended to hold items from a particular origin
+        /// list definition. The origin Story is needed in order to be able to look up that definition.
+        /// </summary>
+        public InkList (string singleOriginListName, Story originStory)
+        {
+            SetInitialOriginName (singleOriginListName);
+
+            ListDefinition def;
+            if (originStory.listDefinitions.TryListGetDefinition (singleOriginListName, out def))
+                origins = new List<ListDefinition> { def };
+            else
+                throw new System.Exception ("InkList origin could not be found in story when constructing new list: " + singleOriginListName);
+        }
+
+        public InkList (KeyValuePair<InkListItem, int> singleElement)
+        {
+            Add (singleElement.Key, singleElement.Value);
+		}
+
+		/// <summary>
+		/// Converts a string to an ink list and returns for use in the story.
+		/// </summary>
+		/// <returns>InkList created from string list item</returns>
+		/// <param name="itemKey">Item key.</param>
+		/// <param name="originStory">Origin story.</param>
+		public static InkList FromString(string myListItem, Story originStory) {
+			var listValue = originStory.listDefinitions.FindSingleItemListWithName (myListItem);
+			if (listValue)
+				return new InkList (listValue.value);
+			else 
+                throw new System.Exception ("Could not find the InkListItem from the string '" + myListItem + "' to create an InkList because it doesn't exist in the original list definition in ink.");
+		}
+
+
+        /// <summary>
+        /// Adds the given item to the ink list. Note that the item must come from a list definition that
+        /// is already "known" to this list, so that the item's value can be looked up. By "known", we mean
+        /// that it already has items in it from that source, or it did at one point - it can't be a 
+        /// completely fresh empty list, or a list that only contains items from a different list definition.
+        /// </summary>
+        public void AddItem (InkListItem item)
+        {
+            if (item.originName == null) {
+                AddItem (item.itemName);
+                return;
+            }
+            
+            foreach (var origin in origins) {
+                if (origin.name == item.originName) {
+                    int intVal;
+                    if (origin.TryGetValueForItem (item, out intVal)) {
+                        this [item] = intVal;
+                        return;
+                    } else {
+                        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.");
+                    }
+                }
+            }
+
+            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.");
+        }
+
+        /// <summary>
+        /// Adds the given item to the ink list, attempting to find the origin list definition that it belongs to.
+        /// The item must therefore come from a list definition that is already "known" to this list, so that the
+        /// item's value can be looked up. By "known", we mean that it already has items in it from that source, or
+        /// it did at one point - it can't be a completely fresh empty list, or a list that only contains items from
+        /// a different list definition.
+        /// </summary>
+        public void AddItem (string itemName)
+        {
+            ListDefinition foundListDef = null;
+
+            foreach (var origin in origins) {
+                if (origin.ContainsItemWithName (itemName)) {
+                    if (foundListDef != null) {
+                        throw new System.Exception ("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name);
+                    } else {
+                        foundListDef = origin;
+                    }
+                }
+            }
+
+            if (foundListDef == null)
+                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.");
+
+            var item = new InkListItem (foundListDef.name, itemName);
+            var itemVal = foundListDef.ValueForItem(item);
+            this [item] = itemVal;
+        }
+
+        /// <summary>
+        /// Returns true if this ink list contains an item with the given short name
+        /// (ignoring the original list where it was defined).
+        /// </summary>
+        public bool ContainsItemNamed (string itemName)
+        {
+            foreach (var itemWithValue in this) {
+                if (itemWithValue.Key.itemName == itemName) return true;
+            }
+            return false;
+        }
+
+        // Story has to set this so that the value knows its origin,
+        // necessary for certain operations (e.g. interacting with ints).
+        // Only the story has access to the full set of lists, so that
+        // the origin can be resolved from the originListName.
+        public List<ListDefinition> origins;
+        public ListDefinition originOfMaxItem {
+            get {
+                if (origins == null) return null;
+
+                var maxOriginName = maxItem.Key.originName;
+                foreach (var origin in origins) {
+                    if (origin.name == maxOriginName)
+                        return origin;
+                }
+
+                return null;
+            }
+        }
+
+        // Origin name needs to be serialised when content is empty,
+        // assuming a name is availble, for list definitions with variable
+        // that is currently empty.
+        public List<string> originNames {
+            get {
+                if (this.Count > 0) {
+                    if (_originNames == null && this.Count > 0)
+                        _originNames = new List<string> ();
+                    else
+                        _originNames.Clear ();
+
+                    foreach (var itemAndValue in this)
+                        _originNames.Add (itemAndValue.Key.originName);
+                }
+
+                return _originNames;
+            }
+        }
+        List<string> _originNames;
+
+        public void SetInitialOriginName (string initialOriginName)
+        {
+            _originNames = new List<string> { initialOriginName };
+        }
+
+        public void SetInitialOriginNames (List<string> initialOriginNames)
+        {
+            if (initialOriginNames == null)
+                _originNames = null;
+            else
+                _originNames = new List<string>(initialOriginNames);
+        }
+
+        /// <summary>
+        /// Get the maximum item in the list, equivalent to calling LIST_MAX(list) in ink.
+        /// </summary>
+        public KeyValuePair<InkListItem, int> maxItem {
+            get {
+                KeyValuePair<InkListItem, int> max = new KeyValuePair<InkListItem, int>();
+                foreach (var kv in this) {
+                    if (max.Key.isNull || kv.Value > max.Value)
+                        max = kv;
+                }
+                return max;
+            }
+        }
+
+        /// <summary>
+        /// Get the minimum item in the list, equivalent to calling LIST_MIN(list) in ink.
+        /// </summary>
+        public KeyValuePair<InkListItem, int> minItem {
+            get {
+                var min = new KeyValuePair<InkListItem, int> ();
+                foreach (var kv in this) {
+                    if (min.Key.isNull || kv.Value < min.Value)
+                        min = kv;
+                }
+                return min;
+            }
+        }
+
+        /// <summary>
+        /// The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink
+        /// </summary>
+        public InkList inverse {
+            get {
+                var list = new InkList ();
+                if (origins != null) {
+                    foreach (var origin in origins) {
+                        foreach (var itemAndValue in origin.items) {
+                            if (!this.ContainsKey (itemAndValue.Key))
+                                list.Add (itemAndValue.Key, itemAndValue.Value);
+                        }
+                    }
+
+                }
+                return list;
+            }
+        }
+
+        /// <summary>
+        /// The list of all items from the original list definition, equivalent to calling
+        /// LIST_ALL(list) in ink.
+        /// </summary>
+        public InkList all {
+            get {
+                var list = new InkList ();
+                if (origins != null) {
+                    foreach (var origin in origins) {
+                        foreach (var itemAndValue in origin.items)
+                            list[itemAndValue.Key] = itemAndValue.Value;
+                    }
+                }
+                return list;
+            }
+        }
+
+        /// <summary>
+        /// Returns a new list that is the combination of the current list and one that's
+        /// passed in. Equivalent to calling (list1 + list2) in ink.
+        /// </summary>
+        public InkList Union (InkList otherList)
+        {
+            var union = new InkList (this);
+            foreach (var kv in otherList) {
+                union [kv.Key] = kv.Value;
+            }
+            return union;
+        }
+
+        /// <summary>
+        /// Returns a new list that is the intersection of the current list with another
+        /// list that's passed in - i.e. a list of the items that are shared between the
+        /// two other lists. Equivalent to calling (list1 ^ list2) in ink.
+        /// </summary>
+        public InkList Intersect (InkList otherList)
+        {
+            var intersection = new InkList ();
+            foreach (var kv in this) {
+                if (otherList.ContainsKey (kv.Key))
+                    intersection.Add (kv.Key, kv.Value);
+            }
+            return intersection;
+        }
+
+        /// <summary>
+        /// Returns a new list that's the same as the current one, except with the given items
+        /// removed that are in the passed in list. Equivalent to calling (list1 - list2) in ink.
+        /// </summary>
+        /// <param name="listToRemove">List to remove.</param>
+        public InkList Without (InkList listToRemove)
+        {
+            var result = new InkList (this);
+            foreach (var kv in listToRemove)
+                result.Remove (kv.Key);
+            return result;
+        }
+
+        /// <summary>
+        /// Returns true if the current list contains all the items that are in the list that
+        /// is passed in. Equivalent to calling (list1 ? list2) in ink.
+        /// </summary>
+        /// <param name="otherList">Other list.</param>
+        public bool Contains (InkList otherList)
+        {
+            foreach (var kv in otherList) {
+                if (!this.ContainsKey (kv.Key)) return false;
+            }
+            return true;
+        }
+
+        /// <summary>
+        /// Returns true if all the item values in the current list are greater than all the
+        /// item values in the passed in list. Equivalent to calling (list1 > list2) in ink.
+        /// </summary>
+        public bool GreaterThan (InkList otherList)
+        {
+            if (Count == 0) return false;
+            if (otherList.Count == 0) return true;
+
+            // All greater
+            return minItem.Value > otherList.maxItem.Value;
+        }
+
+        /// <summary>
+        /// Returns true if the item values in the current list overlap or are all greater than
+        /// the item values in the passed in list. None of the item values in the current list must
+        /// fall below the item values in the passed in list. Equivalent to (list1 >= list2) in ink,
+        /// or LIST_MIN(list1) >= LIST_MIN(list2) &amp;&amp; LIST_MAX(list1) >= LIST_MAX(list2).
+        /// </summary>
+        public bool GreaterThanOrEquals (InkList otherList)
+        {
+            if (Count == 0) return false;
+            if (otherList.Count == 0) return true;
+
+            return minItem.Value >= otherList.minItem.Value
+                && maxItem.Value >= otherList.maxItem.Value;
+        }
+
+        /// <summary>
+        /// Returns true if all the item values in the current list are less than all the
+        /// item values in the passed in list. Equivalent to calling (list1 &lt; list2) in ink.
+        /// </summary>
+        public bool LessThan (InkList otherList)
+        {
+            if (otherList.Count == 0) return false;
+            if (Count == 0) return true;
+
+            return maxItem.Value < otherList.minItem.Value;
+        }
+
+        /// <summary>
+        /// Returns true if the item values in the current list overlap or are all less than
+        /// the item values in the passed in list. None of the item values in the current list must
+        /// go above the item values in the passed in list. Equivalent to (list1 &lt;= list2) in ink,
+        /// or LIST_MAX(list1) &lt;= LIST_MAX(list2) &amp;&amp; LIST_MIN(list1) &lt;= LIST_MIN(list2).
+        /// </summary>
+        public bool LessThanOrEquals (InkList otherList)
+        {
+            if (otherList.Count == 0) return false;
+            if (Count == 0) return true;
+
+            return maxItem.Value <= otherList.maxItem.Value
+                && minItem.Value <= otherList.minItem.Value;
+        }
+
+        public InkList MaxAsList ()
+        {
+            if (Count > 0)
+                return new InkList (maxItem);
+            else
+                return new InkList ();
+        }
+
+        public InkList MinAsList ()
+        {
+            if (Count > 0)
+                return new InkList (minItem);
+            else
+                return new InkList ();
+        }
+
+        /// <summary>
+        /// Returns a sublist with the elements given the minimum and maxmimum bounds.
+        /// The bounds can either be ints which are indices into the entire (sorted) list,
+        /// or they can be InkLists themselves. These are intended to be single-item lists so
+        /// you can specify the upper and lower bounds. If you pass in multi-item lists, it'll
+        /// use the minimum and maximum items in those lists respectively.
+        /// WARNING: Calling this method requires a full sort of all the elements in the list.
+        /// </summary>
+        public InkList ListWithSubRange(object minBound, object maxBound) 
+        {
+            if (this.Count == 0) return new InkList();
+
+            var ordered = orderedItems;
+
+            int minValue = 0;
+            int maxValue = int.MaxValue;
+
+            if (minBound is int)
+            {
+                minValue = (int)minBound;
+            }
+
+            else
+            {
+                if( minBound is InkList && ((InkList)minBound).Count > 0 )
+                    minValue = ((InkList)minBound).minItem.Value;
+            }
+
+            if (maxBound is int)
+                maxValue = (int)maxBound;
+            else 
+            {
+                if (minBound is InkList && ((InkList)minBound).Count > 0)
+                    maxValue = ((InkList)maxBound).maxItem.Value;
+            }
+
+            var subList = new InkList();
+            subList.SetInitialOriginNames(originNames);
+            foreach(var item in ordered) {
+                if( item.Value >= minValue && item.Value <= maxValue ) {
+                    subList.Add(item.Key, item.Value);
+                }
+            }
+
+            return subList;
+        }
+
+        /// <summary>
+        /// Returns true if the passed object is also an ink list that contains
+        /// the same items as the current list, false otherwise.
+        /// </summary>
+        public override bool Equals (object other)
+        {
+            var otherRawList = other as InkList;
+            if (otherRawList == null) return false;
+            if (otherRawList.Count != Count) return false;
+
+            foreach (var kv in this) {
+                if (!otherRawList.ContainsKey (kv.Key))
+                    return false;
+            }
+
+            return true;
+        }
+
+        /// <summary>
+        /// Return the hashcode for this object, used for comparisons and inserting into dictionaries.
+        /// </summary>
+        public override int GetHashCode ()
+        {
+            int ownHash = 0;
+            foreach (var kv in this)
+                ownHash += kv.Key.GetHashCode ();
+            return ownHash;
+        }
+
+        List<KeyValuePair<InkListItem, int>> orderedItems {
+            get {
+                var ordered = new List<KeyValuePair<InkListItem, int>>();
+                ordered.AddRange(this);
+                ordered.Sort((x, y) => {
+                    // Ensure consistent ordering of mixed lists.
+                    if( x.Value == y.Value ) {
+                        return x.Key.originName.CompareTo(y.Key.originName);
+                    } else {
+                        return x.Value.CompareTo(y.Value);
+                    }
+                });
+                return ordered;
+            }
+        }
+
+        /// <summary>
+        /// Returns a string in the form "a, b, c" with the names of the items in the list, without
+        /// the origin list definition names. Equivalent to writing {list} in ink.
+        /// </summary>
+        public override string ToString ()
+        {
+            var ordered = orderedItems;
+
+            var sb = new StringBuilder ();
+            for (int i = 0; i < ordered.Count; i++) {
+                if (i > 0)
+                    sb.Append (", ");
+
+                var item = ordered [i].Key;
+                sb.Append (item.itemName);
+            }
+
+            return sb.ToString ();
+        }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkRuntime/InkList.cs.meta b/Assets/Ink/InkLibs/InkRuntime/InkList.cs.meta
new file mode 100644
index 0000000..93e0e58
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/InkList.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3a56f188ddfd3452386a797f878ab7ed
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs b/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs
new file mode 100644
index 0000000..dcde266
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs
@@ -0,0 +1,720 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink.Runtime
+{
+    public static class Json
+    {
+        public static List<T> JArrayToRuntimeObjList<T>(List<object> jArray, bool skipLast=false) where T : Runtime.Object
+        {
+            int count = jArray.Count;
+            if (skipLast)
+                count--;
+
+            var list = new List<T> (jArray.Count);
+
+            for (int i = 0; i < count; i++) {
+                var jTok = jArray [i];
+                var runtimeObj = JTokenToRuntimeObject (jTok) as T;
+                list.Add (runtimeObj);
+            }
+
+            return list;
+        }
+
+        public static List<Runtime.Object> JArrayToRuntimeObjList(List<object> jArray, bool skipLast=false)
+        {
+            return JArrayToRuntimeObjList<Runtime.Object> (jArray, skipLast);
+        }
+
+        public static void WriteDictionaryRuntimeObjs(SimpleJson.Writer writer, Dictionary<string, Runtime.Object> dictionary) 
+        {
+            writer.WriteObjectStart();
+            foreach(var keyVal in dictionary) {
+                writer.WritePropertyStart(keyVal.Key);
+                WriteRuntimeObject(writer, keyVal.Value);
+                writer.WritePropertyEnd();
+            }
+            writer.WriteObjectEnd();
+        }
+
+
+        public static void WriteListRuntimeObjs(SimpleJson.Writer writer, List<Runtime.Object> list)
+        {
+            writer.WriteArrayStart();
+            foreach (var val in list)
+            {
+                WriteRuntimeObject(writer, val);
+            }
+            writer.WriteArrayEnd();
+        }
+
+        public static void WriteIntDictionary(SimpleJson.Writer writer, Dictionary<string, int> dict)
+        {
+            writer.WriteObjectStart();
+            foreach (var keyVal in dict)
+                writer.WriteProperty(keyVal.Key, keyVal.Value);
+            writer.WriteObjectEnd();
+        }
+
+        public static void WriteRuntimeObject(SimpleJson.Writer writer, Runtime.Object obj)
+        {
+            var container = obj as Container;
+            if (container) {
+                WriteRuntimeContainer(writer, container);
+                return;
+            }
+
+            var divert = obj as Divert;
+            if (divert)
+            {
+                string divTypeKey = "->";
+                if (divert.isExternal)
+                    divTypeKey = "x()";
+                else if (divert.pushesToStack)
+                {
+                    if (divert.stackPushType == PushPopType.Function)
+                        divTypeKey = "f()";
+                    else if (divert.stackPushType == PushPopType.Tunnel)
+                        divTypeKey = "->t->";
+                }
+
+                string targetStr;
+                if (divert.hasVariableTarget)
+                    targetStr = divert.variableDivertName;
+                else
+                    targetStr = divert.targetPathString;
+
+                writer.WriteObjectStart();
+
+                writer.WriteProperty(divTypeKey, targetStr);
+
+                if (divert.hasVariableTarget)
+                    writer.WriteProperty("var", true);
+
+                if (divert.isConditional)
+                    writer.WriteProperty("c", true);
+
+                if (divert.externalArgs > 0)
+                    writer.WriteProperty("exArgs", divert.externalArgs);
+
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            var choicePoint = obj as ChoicePoint;
+            if (choicePoint)
+            {
+                writer.WriteObjectStart();
+                writer.WriteProperty("*", choicePoint.pathStringOnChoice);
+                writer.WriteProperty("flg", choicePoint.flags);
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            var boolVal = obj as BoolValue;
+            if (boolVal) {
+                writer.Write(boolVal.value);
+                return;
+            }
+
+            var intVal = obj as IntValue;
+            if (intVal) {
+                writer.Write(intVal.value);
+                return;
+            }
+
+            var floatVal = obj as FloatValue;
+            if (floatVal) {
+                writer.Write(floatVal.value);
+                return;
+            }
+
+            var strVal = obj as StringValue;
+            if (strVal)
+            {
+                if (strVal.isNewline)
+                    writer.Write("\\n", escape:false);
+                else {
+                    writer.WriteStringStart();
+                    writer.WriteStringInner("^");
+                    writer.WriteStringInner(strVal.value);
+                    writer.WriteStringEnd();
+                }
+                return;
+            }
+
+            var listVal = obj as ListValue;
+            if (listVal)
+            {
+                WriteInkList(writer, listVal);
+                return;
+            }
+
+            var divTargetVal = obj as DivertTargetValue;
+            if (divTargetVal)
+            {
+                writer.WriteObjectStart();
+                writer.WriteProperty("^->", divTargetVal.value.componentsString);
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            var varPtrVal = obj as VariablePointerValue;
+            if (varPtrVal)
+            {
+                writer.WriteObjectStart();
+                writer.WriteProperty("^var", varPtrVal.value);
+                writer.WriteProperty("ci", varPtrVal.contextIndex);
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            var glue = obj as Runtime.Glue;
+            if (glue) {
+                writer.Write("<>");
+                return;
+            }
+
+            var controlCmd = obj as ControlCommand;
+            if (controlCmd)
+            {
+                writer.Write(_controlCommandNames[(int)controlCmd.commandType]);
+                return;
+            }
+
+            var nativeFunc = obj as Runtime.NativeFunctionCall;
+            if (nativeFunc)
+            {
+                var name = nativeFunc.name;
+
+                // Avoid collision with ^ used to indicate a string
+                if (name == "^") name = "L^";
+
+                writer.Write(name);
+                return;
+            }
+
+
+            // Variable reference
+            var varRef = obj as VariableReference;
+            if (varRef)
+            {
+                writer.WriteObjectStart();
+
+                string readCountPath = varRef.pathStringForCount;
+                if (readCountPath != null)
+                {
+                    writer.WriteProperty("CNT?", readCountPath);
+                }
+                else
+                {
+                    writer.WriteProperty("VAR?", varRef.name);
+                }
+
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            // Variable assignment
+            var varAss = obj as VariableAssignment;
+            if (varAss)
+            {
+                writer.WriteObjectStart();
+
+                string key = varAss.isGlobal ? "VAR=" : "temp=";
+                writer.WriteProperty(key, varAss.variableName);
+
+                // Reassignment?
+                if (!varAss.isNewDeclaration)
+                    writer.WriteProperty("re", true);
+
+                writer.WriteObjectEnd();
+
+                return;
+            }
+
+            // Void
+            var voidObj = obj as Void;
+            if (voidObj) {
+                writer.Write("void");
+                return;
+            }
+
+            // Tag
+            var tag = obj as Tag;
+            if (tag)
+            {
+                writer.WriteObjectStart();
+                writer.WriteProperty("#", tag.text);
+                writer.WriteObjectEnd();
+                return;
+            }
+
+            // Used when serialising save state only
+            var choice = obj as Choice;
+            if (choice) {
+                WriteChoice(writer, choice);
+                return;
+            }
+
+            throw new System.Exception("Failed to write runtime object to JSON: " + obj);
+        }
+
+        public static Dictionary<string, Runtime.Object> JObjectToDictionaryRuntimeObjs(Dictionary<string, object> jObject)
+        {
+            var dict = new Dictionary<string, Runtime.Object> (jObject.Count);
+
+            foreach (var keyVal in jObject) {
+                dict [keyVal.Key] = JTokenToRuntimeObject(keyVal.Value);
+            }
+
+            return dict;
+        }
+
+        public static Dictionary<string, int> JObjectToIntDictionary(Dictionary<string, object> jObject)
+        {
+            var dict = new Dictionary<string, int> (jObject.Count);
+            foreach (var keyVal in jObject) {
+                dict [keyVal.Key] = (int)keyVal.Value;
+            }
+            return dict;
+        }
+
+        // ----------------------
+        // JSON ENCODING SCHEME
+        // ----------------------
+        //
+        // Glue:           "<>", "G<", "G>"
+        // 
+        // ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop", 
+        //                 "choiceCnt", "turns", "visit", "seq", "thread", "done", "end"
+        // 
+        // NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc
+        // 
+        // Void:           "void"
+        // 
+        // Value:          "^string value", "^^string value beginning with ^"
+        //                 5, 5.2
+        //                 {"^->": "path.target"}
+        //                 {"^var": "varname", "ci": 0}
+        // 
+        // Container:      [...]
+        //                 [..., 
+        //                     {
+        //                         "subContainerName": ..., 
+        //                         "#f": 5,                    // flags
+        //                         "#n": "containerOwnName"    // only if not redundant
+        //                     }
+        //                 ]
+        // 
+        // Divert:         {"->": "path.target", "c": true }
+        //                 {"->": "path.target", "var": true}
+        //                 {"f()": "path.func"}
+        //                 {"->t->": "path.tunnel"}
+        //                 {"x()": "externalFuncName", "exArgs": 5}
+        // 
+        // Var Assign:     {"VAR=": "varName", "re": true}   // reassignment
+        //                 {"temp=": "varName"}
+        // 
+        // Var ref:        {"VAR?": "varName"}
+        //                 {"CNT?": "stitch name"}
+        // 
+        // ChoicePoint:    {"*": pathString,
+        //                  "flg": 18 }
+        //
+        // Choice:         Nothing too clever, it's only used in the save state,
+        //                 there's not likely to be many of them.
+        // 
+        // Tag:            {"#": "the tag text"}
+        public static Runtime.Object JTokenToRuntimeObject(object token)
+        {
+            if (token is int || token is float || token is bool) {
+                return Value.Create (token);
+            }
+            
+            if (token is string) {
+                string str = (string)token;
+
+                // String value
+                char firstChar = str[0];
+                if (firstChar == '^')
+                    return new StringValue (str.Substring (1));
+                else if( firstChar == '\n' && str.Length == 1)
+                    return new StringValue ("\n");
+
+                // Glue
+                if (str == "<>") return new Runtime.Glue ();
+
+                // Control commands (would looking up in a hash set be faster?)
+                for (int i = 0; i < _controlCommandNames.Length; ++i) {
+                    string cmdName = _controlCommandNames [i];
+                    if (str == cmdName) {
+                        return new Runtime.ControlCommand ((ControlCommand.CommandType)i);
+                    }
+                }
+
+                // Native functions
+                // "^" conflicts with the way to identify strings, so now
+                // we know it's not a string, we can convert back to the proper
+                // symbol for the operator.
+                if (str == "L^") str = "^";
+                if( NativeFunctionCall.CallExistsWithName(str) )
+                    return NativeFunctionCall.CallWithName (str);
+
+                // Pop
+                if (str == "->->")
+                    return Runtime.ControlCommand.PopTunnel ();
+                else if (str == "~ret")
+                    return Runtime.ControlCommand.PopFunction ();
+
+                // Void
+                if (str == "void")
+                    return new Runtime.Void ();
+            }
+
+            if (token is Dictionary<string, object>) {
+
+                var obj = (Dictionary < string, object> )token;
+                object propValue;
+
+                // Divert target value to path
+                if (obj.TryGetValue ("^->", out propValue))
+                    return new DivertTargetValue (new Path ((string)propValue));
+
+                // VariablePointerValue
+                if (obj.TryGetValue ("^var", out propValue)) {
+                    var varPtr = new VariablePointerValue ((string)propValue);
+                    if (obj.TryGetValue ("ci", out propValue))
+                        varPtr.contextIndex = (int)propValue;
+                    return varPtr;
+                }
+
+                // Divert
+                bool isDivert = false;
+                bool pushesToStack = false;
+                PushPopType divPushType = PushPopType.Function;
+                bool external = false;
+                if (obj.TryGetValue ("->", out propValue)) {
+                    isDivert = true;
+                }
+                else if (obj.TryGetValue ("f()", out propValue)) {
+                    isDivert = true;
+                    pushesToStack = true;
+                    divPushType = PushPopType.Function;
+                }
+                else if (obj.TryGetValue ("->t->", out propValue)) {
+                    isDivert = true;
+                    pushesToStack = true;
+                    divPushType = PushPopType.Tunnel;
+                }
+                else if (obj.TryGetValue ("x()", out propValue)) {
+                    isDivert = true;
+                    external = true;
+                    pushesToStack = false;
+                    divPushType = PushPopType.Function;
+                }
+                if (isDivert) {
+                    var divert = new Divert ();
+                    divert.pushesToStack = pushesToStack;
+                    divert.stackPushType = divPushType;
+                    divert.isExternal = external;
+
+                    string target = propValue.ToString ();
+
+                    if (obj.TryGetValue ("var", out propValue))
+                        divert.variableDivertName = target;
+                    else
+                        divert.targetPathString = target;
+
+                    divert.isConditional = obj.TryGetValue("c", out propValue);
+
+                    if (external) {
+                        if (obj.TryGetValue ("exArgs", out propValue))
+                            divert.externalArgs = (int)propValue;
+                    }
+
+                    return divert;
+                }
+                    
+                // Choice
+                if (obj.TryGetValue ("*", out propValue)) {
+                    var choice = new ChoicePoint ();
+                    choice.pathStringOnChoice = propValue.ToString();
+
+                    if (obj.TryGetValue ("flg", out propValue))
+                        choice.flags = (int)propValue;
+
+                    return choice;
+                }
+
+                // Variable reference
+                if (obj.TryGetValue ("VAR?", out propValue)) {
+                    return new VariableReference (propValue.ToString ());
+                } else if (obj.TryGetValue ("CNT?", out propValue)) {
+                    var readCountVarRef = new VariableReference ();
+                    readCountVarRef.pathStringForCount = propValue.ToString ();
+                    return readCountVarRef;
+                }
+
+                // Variable assignment
+                bool isVarAss = false;
+                bool isGlobalVar = false;
+                if (obj.TryGetValue ("VAR=", out propValue)) {
+                    isVarAss = true;
+                    isGlobalVar = true;
+                } else if (obj.TryGetValue ("temp=", out propValue)) {
+                    isVarAss = true;
+                    isGlobalVar = false;
+                }
+                if (isVarAss) {
+                    var varName = propValue.ToString ();
+                    var isNewDecl = !obj.TryGetValue("re", out propValue);
+                    var varAss = new VariableAssignment (varName, isNewDecl);
+                    varAss.isGlobal = isGlobalVar;
+                    return varAss;
+                }
+
+                // Tag
+                if (obj.TryGetValue ("#", out propValue)) {
+                    return new Runtime.Tag ((string)propValue);
+                }
+
+                // List value
+                if (obj.TryGetValue ("list", out propValue)) {
+                    var listContent = (Dictionary<string, object>)propValue;
+                    var rawList = new InkList ();
+                    if (obj.TryGetValue ("origins", out propValue)) {
+                        var namesAsObjs = (List<object>)propValue;
+                        rawList.SetInitialOriginNames (namesAsObjs.Cast<string>().ToList());
+                    }
+                    foreach (var nameToVal in listContent) {
+                        var item = new InkListItem (nameToVal.Key);
+                        var val = (int)nameToVal.Value;
+                        rawList.Add (item, val);
+                    }
+                    return new ListValue (rawList);
+                }
+
+                // Used when serialising save state only
+                if (obj ["originalChoicePath"] != null)
+                    return JObjectToChoice (obj);
+            }
+
+            // Array is always a Runtime.Container
+            if (token is List<object>) {
+                return JArrayToContainer((List<object>)token);
+            }
+
+            if (token == null)
+                return null;
+
+            throw new System.Exception ("Failed to convert token to runtime object: " + token);
+        }
+
+        public static void WriteRuntimeContainer(SimpleJson.Writer writer, Container container, bool withoutName = false)
+        {
+            writer.WriteArrayStart();
+
+            foreach (var c in container.content)
+                WriteRuntimeObject(writer, c);
+
+            // Container is always an array [...]
+            // But the final element is always either:
+            //  - a dictionary containing the named content, as well as possibly
+            //    the key "#" with the count flags
+            //  - null, if neither of the above
+            var namedOnlyContent = container.namedOnlyContent;
+            var countFlags = container.countFlags;
+            var hasNameProperty = container.name != null && !withoutName;
+
+            bool hasTerminator = namedOnlyContent != null || countFlags > 0 || hasNameProperty;
+
+            if( hasTerminator )
+                writer.WriteObjectStart();
+
+            if ( namedOnlyContent != null ) {
+                foreach(var namedContent in namedOnlyContent) {
+                    var name = namedContent.Key;
+                    var namedContainer = namedContent.Value as Container;
+                    writer.WritePropertyStart(name);
+                    WriteRuntimeContainer(writer, namedContainer, withoutName:true);
+                    writer.WritePropertyEnd();
+                }
+            }
+
+            if (countFlags > 0)
+                writer.WriteProperty("#f", countFlags);
+
+            if (hasNameProperty)
+                writer.WriteProperty("#n", container.name);
+
+            if (hasTerminator)
+                writer.WriteObjectEnd();
+            else
+                writer.WriteNull();
+
+            writer.WriteArrayEnd();
+        }
+
+        static Container JArrayToContainer(List<object> jArray)
+        {
+            var container = new Container ();
+            container.content = JArrayToRuntimeObjList (jArray, skipLast:true);
+
+            // Final object in the array is always a combination of
+            //  - named content
+            //  - a "#f" key with the countFlags
+            // (if either exists at all, otherwise null)
+            var terminatingObj = jArray [jArray.Count - 1] as Dictionary<string, object>;
+            if (terminatingObj != null) {
+
+                var namedOnlyContent = new Dictionary<string, Runtime.Object> (terminatingObj.Count);
+
+                foreach (var keyVal in terminatingObj) {
+                    if (keyVal.Key == "#f") {
+                        container.countFlags = (int)keyVal.Value;
+                    } else if (keyVal.Key == "#n") {
+                        container.name = keyVal.Value.ToString ();
+                    } else {
+                        var namedContentItem = JTokenToRuntimeObject(keyVal.Value);
+                        var namedSubContainer = namedContentItem as Container;
+                        if (namedSubContainer)
+                            namedSubContainer.name = keyVal.Key;
+                        namedOnlyContent [keyVal.Key] = namedContentItem;
+                    }
+                }
+
+                container.namedOnlyContent = namedOnlyContent;
+            }
+
+            return container;
+        }
+
+        static Choice JObjectToChoice(Dictionary<string, object> jObj)
+        {
+            var choice = new Choice();
+            choice.text = jObj ["text"].ToString();
+            choice.index = (int)jObj ["index"];
+            choice.sourcePath = jObj ["originalChoicePath"].ToString();
+            choice.originalThreadIndex = (int)jObj ["originalThreadIndex"];
+            choice.pathStringOnChoice = jObj ["targetPath"].ToString();
+            return choice;
+        }
+        public static void WriteChoice(SimpleJson.Writer writer, Choice choice)
+        {
+            writer.WriteObjectStart();
+            writer.WriteProperty("text", choice.text);
+            writer.WriteProperty("index", choice.index);
+            writer.WriteProperty("originalChoicePath", choice.sourcePath);
+            writer.WriteProperty("originalThreadIndex", choice.originalThreadIndex);
+            writer.WriteProperty("targetPath", choice.pathStringOnChoice);
+            writer.WriteObjectEnd();
+        }
+
+        static void WriteInkList(SimpleJson.Writer writer, ListValue listVal)
+        {
+            var rawList = listVal.value;
+
+            writer.WriteObjectStart();
+
+            writer.WritePropertyStart("list");
+
+            writer.WriteObjectStart();
+
+            foreach (var itemAndValue in rawList)
+            {
+                var item = itemAndValue.Key;
+                int itemVal = itemAndValue.Value;
+
+                writer.WritePropertyNameStart();
+                writer.WritePropertyNameInner(item.originName ?? "?");
+                writer.WritePropertyNameInner(".");
+                writer.WritePropertyNameInner(item.itemName);
+                writer.WritePropertyNameEnd();
+
+                writer.Write(itemVal);
+
+                writer.WritePropertyEnd();
+            }
+
+            writer.WriteObjectEnd();
+
+            writer.WritePropertyEnd();
+
+            if (rawList.Count == 0 && rawList.originNames != null && rawList.originNames.Count > 0)
+            {
+                writer.WritePropertyStart("origins");
+                writer.WriteArrayStart();
+                foreach (var name in rawList.originNames)
+                    writer.Write(name);
+                writer.WriteArrayEnd();
+                writer.WritePropertyEnd();
+            }
+
+            writer.WriteObjectEnd();
+        }
+
+        public static ListDefinitionsOrigin JTokenToListDefinitions (object obj)
+        {
+            var defsObj = (Dictionary<string, object>)obj;
+
+            var allDefs = new List<ListDefinition> ();
+
+            foreach (var kv in defsObj) {
+                var name = (string) kv.Key;
+                var listDefJson = (Dictionary<string, object>)kv.Value;
+
+                // Cast (string, object) to (string, int) for items
+                var items = new Dictionary<string, int> ();
+                foreach (var nameValue in listDefJson)
+                    items.Add(nameValue.Key, (int)nameValue.Value);
+
+                var def = new ListDefinition (name, items);
+                allDefs.Add (def);
+            }
+
+            return new ListDefinitionsOrigin (allDefs);
+        }
+
+        static Json() 
+        {
+            _controlCommandNames = new string[(int)ControlCommand.CommandType.TOTAL_VALUES];
+
+            _controlCommandNames [(int)ControlCommand.CommandType.EvalStart] = "ev";
+            _controlCommandNames [(int)ControlCommand.CommandType.EvalOutput] = "out";
+            _controlCommandNames [(int)ControlCommand.CommandType.EvalEnd] = "/ev";
+            _controlCommandNames [(int)ControlCommand.CommandType.Duplicate] = "du";
+            _controlCommandNames [(int)ControlCommand.CommandType.PopEvaluatedValue] = "pop";
+            _controlCommandNames [(int)ControlCommand.CommandType.PopFunction] = "~ret";
+            _controlCommandNames [(int)ControlCommand.CommandType.PopTunnel] = "->->";
+            _controlCommandNames [(int)ControlCommand.CommandType.BeginString] = "str";
+            _controlCommandNames [(int)ControlCommand.CommandType.EndString] = "/str";
+            _controlCommandNames [(int)ControlCommand.CommandType.NoOp] = "nop";
+            _controlCommandNames [(int)ControlCommand.CommandType.ChoiceCount] = "choiceCnt";
+            _controlCommandNames [(int)ControlCommand.CommandType.Turns] = "turn";
+            _controlCommandNames [(int)ControlCommand.CommandType.TurnsSince] = "turns";
+            _controlCommandNames [(int)ControlCommand.CommandType.ReadCount] = "readc";
+            _controlCommandNames [(int)ControlCommand.CommandType.Random] = "rnd";
+            _controlCommandNames [(int)ControlCommand.CommandType.SeedRandom] = "srnd";
+            _controlCommandNames [(int)ControlCommand.CommandType.VisitIndex] = "visit";
+            _controlCommandNames [(int)ControlCommand.CommandType.SequenceShuffleIndex] = "seq";
+            _controlCommandNames [(int)ControlCommand.CommandType.StartThread] = "thread";
+            _controlCommandNames [(int)ControlCommand.CommandType.Done] = "done";
+            _controlCommandNames [(int)ControlCommand.CommandType.End] = "end";
+            _controlCommandNames [(int)ControlCommand.CommandType.ListFromInt] = "listInt";
+            _controlCommandNames [(int)ControlCommand.CommandType.ListRange] = "range";
+            _controlCommandNames [(int)ControlCommand.CommandType.ListRandom] = "lrnd";
+
+            for (int i = 0; i < (int)ControlCommand.CommandType.TOTAL_VALUES; ++i) {
+                if (_controlCommandNames [i] == null)
+                    throw new System.Exception ("Control command not accounted for in serialisation");
+            }
+        }
+
+        static string[] _controlCommandNames;
+    }
+}
+
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs.meta b/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs.meta
new file mode 100644
index 0000000..c639de5
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/JsonSerialisation.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1365b619ff45f4d12b7bf20cc52a4173
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs b/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs
new file mode 100644
index 0000000..94370bd
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public class ListDefinition
+    {
+        public string name { get { return _name; } }
+
+        public Dictionary<InkListItem, int> items {
+            get {
+                if (_items == null) {
+                    _items = new Dictionary<InkListItem, int> ();
+                    foreach (var itemNameAndValue in _itemNameToValues) {
+                        var item = new InkListItem (name, itemNameAndValue.Key);
+                        _items [item] = itemNameAndValue.Value;
+                    }
+                }
+                return _items;
+            }
+        }
+        Dictionary<InkListItem, int> _items;
+
+        public int ValueForItem (InkListItem item)
+        {
+            int intVal;
+            if (_itemNameToValues.TryGetValue (item.itemName, out intVal))
+                return intVal;
+            else
+                return 0;
+        }
+
+        public bool ContainsItem (InkListItem item)
+        {
+            if (item.originName != name) return false;
+
+            return _itemNameToValues.ContainsKey (item.itemName);
+        }
+
+        public bool ContainsItemWithName (string itemName)
+        {
+            return _itemNameToValues.ContainsKey (itemName);
+        }
+
+        public bool TryGetItemWithValue (int val, out InkListItem item)
+        {
+            foreach (var namedItem in _itemNameToValues) {
+                if (namedItem.Value == val) {
+                    item = new InkListItem (name, namedItem.Key);
+                    return true;
+                }
+            }
+
+            item = InkListItem.Null;
+            return false;
+        }
+
+        public bool TryGetValueForItem (InkListItem item, out int intVal)
+        {
+            return _itemNameToValues.TryGetValue (item.itemName, out intVal);
+        }
+
+        public ListDefinition (string name, Dictionary<string, int> items)
+        {
+            _name = name;
+            _itemNameToValues = items;
+        }
+
+        string _name;
+
+        // The main representation should be simple item names rather than a RawListItem,
+        // since we mainly want to access items based on their simple name, since that's
+        // how they'll be most commonly requested from ink.
+        Dictionary<string, int> _itemNameToValues;
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs.meta b/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs.meta
new file mode 100644
index 0000000..c0fddde
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ListDefinition.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: efacbd9aad3e9480d9c82bbd58922944
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs b/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs
new file mode 100644
index 0000000..9fc3389
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public class ListDefinitionsOrigin
+    {
+        public List<Runtime.ListDefinition> lists {
+            get {
+                var listOfLists = new List<Runtime.ListDefinition> ();
+                foreach (var namedList in _lists) {
+                    listOfLists.Add (namedList.Value);
+                }
+                return listOfLists;
+            }
+        }
+
+        public ListDefinitionsOrigin (List<Runtime.ListDefinition> lists)
+        {
+            _lists = new Dictionary<string, ListDefinition> ();
+			_allUnambiguousListValueCache = new Dictionary<string, ListValue>();
+
+            foreach (var list in lists) {
+                _lists [list.name] = list;
+
+				foreach(var itemWithValue in list.items) {
+					var item = itemWithValue.Key;
+					var val = itemWithValue.Value;
+					var listValue = new ListValue(item, val);
+
+					// May be ambiguous, but compiler should've caught that,
+					// so we may be doing some replacement here, but that's okay.
+					_allUnambiguousListValueCache[item.itemName] = listValue;
+					_allUnambiguousListValueCache[item.fullName] = listValue;
+				}
+            }
+        }
+
+        public bool TryListGetDefinition (string name, out ListDefinition def)
+        {
+            return _lists.TryGetValue (name, out def);
+        }
+
+        public ListValue FindSingleItemListWithName (string name)
+        {
+			ListValue val = null;
+			_allUnambiguousListValueCache.TryGetValue(name, out val);
+			return val;
+        }
+
+        Dictionary<string, Runtime.ListDefinition> _lists;
+		Dictionary<string, ListValue> _allUnambiguousListValueCache;
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs.meta b/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs.meta
new file mode 100644
index 0000000..687f71d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/ListDefinitionsOrigin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ec881a45ed342471fb87e412417b6343
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs b/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs
new file mode 100644
index 0000000..b610651
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs
@@ -0,0 +1,508 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public class NativeFunctionCall : Runtime.Object
+    {
+        public const string Add      = "+";
+        public const string Subtract = "-";
+        public const string Divide   = "/";
+        public const string Multiply = "*";
+        public const string Mod      = "%";
+        public const string Negate   = "_"; // distinguish from "-" for subtraction
+
+        public const string Equal    = "==";
+        public const string Greater  = ">";
+        public const string Less     = "<";
+        public const string GreaterThanOrEquals = ">=";
+        public const string LessThanOrEquals = "<=";
+        public const string NotEquals   = "!=";
+        public const string Not      = "!";
+
+
+
+        public const string And      = "&&";
+        public const string Or       = "||";
+
+        public const string Min      = "MIN";
+        public const string Max      = "MAX";
+
+        public const string Pow      = "POW";
+        public const string Floor    = "FLOOR";
+        public const string Ceiling  = "CEILING";
+        public const string Int      = "INT";
+        public const string Float    = "FLOAT";
+
+        public const string Has      = "?";
+        public const string Hasnt    = "!?";
+        public const string Intersect = "^";
+
+        public const string ListMin   = "LIST_MIN";
+        public const string ListMax   = "LIST_MAX";
+        public const string All       = "LIST_ALL";
+        public const string Count     = "LIST_COUNT";
+        public const string ValueOfList = "LIST_VALUE";
+        public const string Invert    = "LIST_INVERT";
+
+        public static NativeFunctionCall CallWithName(string functionName)
+        {
+            return new NativeFunctionCall (functionName);
+        }
+
+        public static bool CallExistsWithName(string functionName)
+        {
+            GenerateNativeFunctionsIfNecessary ();
+            return _nativeFunctions.ContainsKey (functionName);
+        }
+            
+        public string name { 
+            get { 
+                return _name;
+            } 
+            protected set {
+                _name = value;
+                if( !_isPrototype )
+                    _prototype = _nativeFunctions [_name];
+            }
+        }
+        string _name;
+
+        public int numberOfParameters { 
+            get {
+                if (_prototype) {
+                    return _prototype.numberOfParameters;
+                } else {
+                    return _numberOfParameters;
+                }
+            }
+            protected set {
+                _numberOfParameters = value;
+            }
+        }
+
+        int _numberOfParameters;
+
+        public Runtime.Object Call(List<Runtime.Object> parameters)
+        {
+            if (_prototype) {
+                return _prototype.Call(parameters);
+            }
+
+            if (numberOfParameters != parameters.Count) {
+                throw new System.Exception ("Unexpected number of parameters");
+            }
+
+            bool hasList = false;
+            foreach (var p in parameters) {
+                if (p is Void)
+                    throw new StoryException ("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?");
+                if (p is ListValue)
+                    hasList = true;
+            }
+
+            // Binary operations on lists are treated outside of the standard coerscion rules
+            if( parameters.Count == 2 && hasList )
+                return CallBinaryListOperation (parameters);
+
+            var coercedParams = CoerceValuesToSingleType (parameters);
+            ValueType coercedType = coercedParams[0].valueType;
+
+            if (coercedType == ValueType.Int) {
+                return Call<int> (coercedParams);
+            } else if (coercedType == ValueType.Float) {
+                return Call<float> (coercedParams);
+            } else if (coercedType == ValueType.String) {
+                return Call<string> (coercedParams);
+            } else if (coercedType == ValueType.DivertTarget) {
+                return Call<Path> (coercedParams);
+            } else if (coercedType == ValueType.List) {
+                return Call<InkList> (coercedParams);
+            }
+
+            return null;
+        }
+
+        Value Call<T>(List<Value> parametersOfSingleType)
+        {
+            Value param1 = (Value) parametersOfSingleType [0];
+            ValueType valType = param1.valueType;
+
+            var val1 = (Value<T>)param1;
+
+            int paramCount = parametersOfSingleType.Count;
+
+            if (paramCount == 2 || paramCount == 1) {
+
+                object opForTypeObj = null;
+                if (!_operationFuncs.TryGetValue (valType, out opForTypeObj)) {
+                    throw new StoryException ("Cannot perform operation '"+this.name+"' on "+valType);
+                }
+
+                // Binary
+                if (paramCount == 2) {
+                    Value param2 = (Value) parametersOfSingleType [1];
+
+                    var val2 = (Value<T>)param2;
+
+                    var opForType = (BinaryOp<T>)opForTypeObj;
+
+                    // Return value unknown until it's evaluated
+                    object resultVal = opForType (val1.value, val2.value);
+
+                    return Value.Create (resultVal);
+                } 
+
+                // Unary
+                else {
+
+                    var opForType = (UnaryOp<T>)opForTypeObj;
+
+                    var resultVal = opForType (val1.value);
+
+                    return Value.Create (resultVal);
+                }  
+            }
+                
+            else {
+                throw new System.Exception ("Unexpected number of parameters to NativeFunctionCall: " + parametersOfSingleType.Count);
+            }
+        }
+
+        Value CallBinaryListOperation (List<Runtime.Object> parameters)
+        {
+            // List-Int addition/subtraction returns a List (e.g. "alpha" + 1 = "beta")
+            if ((name == "+" || name == "-") && parameters [0] is ListValue && parameters [1] is IntValue)
+                return CallListIncrementOperation (parameters);
+
+            var v1 = parameters [0] as Value;
+            var v2 = parameters [1] as Value;
+
+            // And/or with any other type requires coerscion to bool (int)
+            if ((name == "&&" || name == "||") && (v1.valueType != ValueType.List || v2.valueType != ValueType.List)) {
+                var op = _operationFuncs [ValueType.Int] as BinaryOp<int>;
+                var result = (bool)op (v1.isTruthy ? 1 : 0, v2.isTruthy ? 1 : 0);
+                return new BoolValue (result);
+            }
+
+            // Normal (list • list) operation
+            if (v1.valueType == ValueType.List && v2.valueType == ValueType.List)
+                return Call<InkList> (new List<Value> { v1, v2 });
+
+            throw new StoryException ("Can not call use '" + name + "' operation on " + v1.valueType + " and " + v2.valueType);
+        }
+
+        Value CallListIncrementOperation (List<Runtime.Object> listIntParams)
+        {
+            var listVal = (ListValue)listIntParams [0];
+            var intVal = (IntValue)listIntParams [1];
+
+
+            var resultRawList = new InkList ();
+
+            foreach (var listItemWithValue in listVal.value) {
+                var listItem = listItemWithValue.Key;
+                var listItemValue = listItemWithValue.Value;
+
+                // Find + or - operation
+                var intOp = (BinaryOp<int>)_operationFuncs [ValueType.Int];
+
+                // Return value unknown until it's evaluated
+                int targetInt = (int) intOp (listItemValue, intVal.value);
+
+                // Find this item's origin (linear search should be ok, should be short haha)
+                ListDefinition itemOrigin = null;
+                foreach (var origin in listVal.value.origins) {
+                    if (origin.name == listItem.originName) {
+                        itemOrigin = origin;
+                        break;
+                    }
+                }
+                if (itemOrigin != null) {
+                    InkListItem incrementedItem;
+                    if (itemOrigin.TryGetItemWithValue (targetInt, out incrementedItem))
+                        resultRawList.Add (incrementedItem, targetInt);
+                }
+            }
+
+            return new ListValue (resultRawList);
+        }
+
+        List<Value> CoerceValuesToSingleType(List<Runtime.Object> parametersIn)
+        {
+            ValueType valType = ValueType.Int;
+
+            ListValue specialCaseList = null;
+
+            // Find out what the output type is
+            // "higher level" types infect both so that binary operations
+            // use the same type on both sides. e.g. binary operation of
+            // int and float causes the int to be casted to a float.
+            foreach (var obj in parametersIn) {
+                var val = (Value)obj;
+                if (val.valueType > valType) {
+                    valType = val.valueType;
+                }
+
+                if (val.valueType == ValueType.List) {
+                    specialCaseList = val as ListValue;
+                }
+            }
+
+            // Coerce to this chosen type
+            var parametersOut = new List<Value> ();
+
+            // Special case: Coercing to Ints to Lists
+            // We have to do it early when we have both parameters
+            // to hand - so that we can make use of the List's origin
+            if (valType == ValueType.List) {
+                
+                foreach (Value val in parametersIn) {
+                    if (val.valueType == ValueType.List) {
+                        parametersOut.Add (val);
+                    } else if (val.valueType == ValueType.Int) {
+                        int intVal = (int)val.valueObject;
+                        var list = specialCaseList.value.originOfMaxItem;
+
+                        InkListItem item;
+                        if (list.TryGetItemWithValue (intVal, out item)) {
+                            var castedValue = new ListValue (item, intVal);
+                            parametersOut.Add (castedValue);
+                        } else
+                            throw new StoryException ("Could not find List item with the value " + intVal + " in " + list.name);
+                    } else
+                        throw new StoryException ("Cannot mix Lists and " + val.valueType + " values in this operation");
+                }
+                
+            } 
+
+            // Normal Coercing (with standard casting)
+            else {
+                foreach (Value val in parametersIn) {
+                    var castedValue = val.Cast (valType);
+                    parametersOut.Add (castedValue);
+                }
+            }
+
+            return parametersOut;
+        }
+
+        public NativeFunctionCall(string name)
+        {
+            GenerateNativeFunctionsIfNecessary ();
+
+            this.name = name;
+        }
+
+        // Require default constructor for serialisation
+        public NativeFunctionCall() { 
+            GenerateNativeFunctionsIfNecessary ();
+        }
+
+        // Only called internally to generate prototypes
+        NativeFunctionCall (string name, int numberOfParameters)
+        {
+            _isPrototype = true;
+            this.name = name;
+            this.numberOfParameters = numberOfParameters;
+        }
+
+        // For defining operations that do nothing to the specific type
+        // (but are still supported), such as floor/ceil on int and float
+        // cast on float.
+        static object Identity<T>(T t) {
+            return t;
+        }
+
+        static void GenerateNativeFunctionsIfNecessary()
+        {
+            if (_nativeFunctions == null) {
+                _nativeFunctions = new Dictionary<string, NativeFunctionCall> ();
+
+                // Why no bool operations?
+                // Before evaluation, all bools are coerced to ints in
+                // CoerceValuesToSingleType (see default value for valType at top).
+                // So, no operations are ever directly done in bools themselves.
+                // This also means that 1 == true works, since true is always converted
+                // to 1 first.
+                // However, many operations return a "native" bool (equals, etc).
+
+                // Int operations
+                AddIntBinaryOp(Add,      (x, y) => x + y);
+                AddIntBinaryOp(Subtract, (x, y) => x - y);
+                AddIntBinaryOp(Multiply, (x, y) => x * y);
+                AddIntBinaryOp(Divide,   (x, y) => x / y);
+                AddIntBinaryOp(Mod,      (x, y) => x % y); 
+                AddIntUnaryOp (Negate,   x => -x); 
+
+                AddIntBinaryOp(Equal,    (x, y) => x == y);
+                AddIntBinaryOp(Greater,  (x, y) => x > y);
+                AddIntBinaryOp(Less,     (x, y) => x < y);
+                AddIntBinaryOp(GreaterThanOrEquals, (x, y) => x >= y);
+                AddIntBinaryOp(LessThanOrEquals, (x, y) => x <= y);
+                AddIntBinaryOp(NotEquals, (x, y) => x != y);
+                AddIntUnaryOp (Not,       x => x == 0); 
+
+                AddIntBinaryOp(And,      (x, y) => x != 0 && y != 0);
+                AddIntBinaryOp(Or,       (x, y) => x != 0 || y != 0);
+
+                AddIntBinaryOp(Max,      (x, y) => Math.Max(x, y));
+                AddIntBinaryOp(Min,      (x, y) => Math.Min(x, y));
+
+                // Have to cast to float since you could do POW(2, -1)
+                AddIntBinaryOp (Pow,      (x, y) => (float) Math.Pow(x, y));
+                AddIntUnaryOp(Floor,      Identity);
+                AddIntUnaryOp(Ceiling,    Identity);
+                AddIntUnaryOp(Int,        Identity);
+                AddIntUnaryOp (Float,     x => (float)x);
+
+                // Float operations
+                AddFloatBinaryOp(Add,      (x, y) => x + y);
+                AddFloatBinaryOp(Subtract, (x, y) => x - y);
+                AddFloatBinaryOp(Multiply, (x, y) => x * y);
+                AddFloatBinaryOp(Divide,   (x, y) => x / y);
+                AddFloatBinaryOp(Mod,      (x, y) => x % y); // TODO: Is this the operation we want for floats?
+                AddFloatUnaryOp (Negate,   x => -x); 
+
+                AddFloatBinaryOp(Equal,    (x, y) => x == y);
+                AddFloatBinaryOp(Greater,  (x, y) => x > y);
+                AddFloatBinaryOp(Less,     (x, y) => x < y);
+                AddFloatBinaryOp(GreaterThanOrEquals, (x, y) => x >= y);
+                AddFloatBinaryOp(LessThanOrEquals, (x, y) => x <= y);
+                AddFloatBinaryOp(NotEquals, (x, y) => x != y);
+                AddFloatUnaryOp (Not,       x => (x == 0.0f)); 
+
+                AddFloatBinaryOp(And,      (x, y) => x != 0.0f && y != 0.0f);
+                AddFloatBinaryOp(Or,       (x, y) => x != 0.0f || y != 0.0f);
+
+                AddFloatBinaryOp(Max,      (x, y) => Math.Max(x, y));
+                AddFloatBinaryOp(Min,      (x, y) => Math.Min(x, y));
+
+                AddFloatBinaryOp (Pow,      (x, y) => (float)Math.Pow(x, y));
+                AddFloatUnaryOp(Floor,      x => (float)Math.Floor(x));
+                AddFloatUnaryOp(Ceiling,    x => (float)Math.Ceiling(x));
+                AddFloatUnaryOp(Int,        x => (int)x);
+                AddFloatUnaryOp(Float,      Identity);
+
+                // String operations
+                AddStringBinaryOp(Add,     (x, y) => x + y); // concat
+                AddStringBinaryOp(Equal,   (x, y) => x.Equals(y));
+                AddStringBinaryOp (NotEquals, (x, y) => !x.Equals (y));
+                AddStringBinaryOp (Has,    (x, y) => x.Contains(y));
+                AddStringBinaryOp (Hasnt,   (x, y) => !x.Contains(y));
+
+                // List operations
+                AddListBinaryOp (Add, (x, y) => x.Union (y));
+                AddListBinaryOp (Subtract, (x, y) => x.Without(y));
+                AddListBinaryOp (Has, (x, y) => x.Contains (y));
+                AddListBinaryOp (Hasnt, (x, y) => !x.Contains (y));
+                AddListBinaryOp (Intersect, (x, y) => x.Intersect (y));
+
+                AddListBinaryOp (Equal, (x, y) => x.Equals(y));
+                AddListBinaryOp (Greater, (x, y) => x.GreaterThan(y));
+                AddListBinaryOp (Less, (x, y) => x.LessThan(y));
+                AddListBinaryOp (GreaterThanOrEquals, (x, y) => x.GreaterThanOrEquals(y));
+                AddListBinaryOp (LessThanOrEquals, (x, y) => x.LessThanOrEquals(y));
+                AddListBinaryOp (NotEquals, (x, y) => !x.Equals(y));
+
+                AddListBinaryOp (And, (x, y) => x.Count > 0 && y.Count > 0);
+                AddListBinaryOp (Or,  (x, y) => x.Count > 0 || y.Count > 0);
+
+                AddListUnaryOp (Not, x => x.Count == 0 ? (int)1 : (int)0);
+
+                // Placeholders to ensure that these special case functions can exist,
+                // since these function is never actually run, and is special cased in Call
+                AddListUnaryOp (Invert, x => x.inverse);
+                AddListUnaryOp (All, x => x.all);
+                AddListUnaryOp (ListMin, (x) => x.MinAsList());
+                AddListUnaryOp (ListMax, (x) => x.MaxAsList());
+                AddListUnaryOp (Count,  (x) => x.Count);
+                AddListUnaryOp (ValueOfList,  (x) => x.maxItem.Value);
+
+                // Special case: The only operations you can do on divert target values
+                BinaryOp<Path> divertTargetsEqual = (Path d1, Path d2) => {
+                    return d1.Equals (d2);
+                };
+                BinaryOp<Path> divertTargetsNotEqual = (Path d1, Path d2) => {
+                	return !d1.Equals (d2);
+                };
+                AddOpToNativeFunc (Equal, 2, ValueType.DivertTarget, divertTargetsEqual);
+                AddOpToNativeFunc (NotEquals, 2, ValueType.DivertTarget, divertTargetsNotEqual);
+
+            }
+        }
+
+        void AddOpFuncForType(ValueType valType, object op)
+        {
+            if (_operationFuncs == null) {
+                _operationFuncs = new Dictionary<ValueType, object> ();
+            }
+
+            _operationFuncs [valType] = op;
+        }
+
+        static void AddOpToNativeFunc(string name, int args, ValueType valType, object op)
+        {
+            NativeFunctionCall nativeFunc = null;
+            if (!_nativeFunctions.TryGetValue (name, out nativeFunc)) {
+                nativeFunc = new NativeFunctionCall (name, args);
+                _nativeFunctions [name] = nativeFunc;
+            }
+
+            nativeFunc.AddOpFuncForType (valType, op);
+        }
+
+        static void AddIntBinaryOp(string name, BinaryOp<int> op)
+        {
+            AddOpToNativeFunc (name, 2, ValueType.Int, op);
+        }
+
+        static void AddIntUnaryOp(string name, UnaryOp<int> op)
+        {
+            AddOpToNativeFunc (name, 1, ValueType.Int, op);
+        }
+
+        static void AddFloatBinaryOp(string name, BinaryOp<float> op)
+        {
+            AddOpToNativeFunc (name, 2, ValueType.Float, op);
+        }
+
+        static void AddStringBinaryOp(string name, BinaryOp<string> op)
+        {
+            AddOpToNativeFunc (name, 2, ValueType.String, op);
+        }
+
+        static void AddListBinaryOp (string name, BinaryOp<InkList> op)
+        {
+            AddOpToNativeFunc (name, 2, ValueType.List, op);
+        }
+
+        static void AddListUnaryOp (string name, UnaryOp<InkList> op)
+        {
+            AddOpToNativeFunc (name, 1, ValueType.List, op);
+        }
+
+        static void AddFloatUnaryOp(string name, UnaryOp<float> op)
+        {
+            AddOpToNativeFunc (name, 1, ValueType.Float, op);
+        }
+
+        public override string ToString ()
+        {
+            return "Native '" + name + "'";
+        }
+
+        delegate object BinaryOp<T>(T left, T right);
+        delegate object UnaryOp<T>(T val);
+
+        NativeFunctionCall _prototype;
+        bool _isPrototype;
+
+        // Operations for each data type, for a single operation (e.g. "+")
+        Dictionary<ValueType, object> _operationFuncs;
+
+        static Dictionary<string, NativeFunctionCall> _nativeFunctions;
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs.meta b/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs.meta
new file mode 100644
index 0000000..1f003e9
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/NativeFunctionCall.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b713d1356fec8458380a308bdc2e8b8f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Object.cs b/Assets/Ink/InkLibs/InkRuntime/Object.cs
new file mode 100644
index 0000000..8a5c41b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Object.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// Base class for all ink runtime content.
+    /// </summary>
+    public /* TODO: abstract */ class Object
+	{
+        /// <summary>
+        /// Runtime.Objects can be included in the main Story as a hierarchy.
+        /// Usually parents are Container objects. (TODO: Always?)
+        /// </summary>
+        /// <value>The parent.</value>
+		public Runtime.Object parent { get; set; }
+
+        public Runtime.DebugMetadata debugMetadata { 
+            get {
+                if (_debugMetadata == null) {
+                    if (parent) {
+                        return parent.debugMetadata;
+                    }
+                }
+
+                return _debugMetadata;
+            }
+
+            set {
+                _debugMetadata = value;
+            }
+        }
+
+        public Runtime.DebugMetadata ownDebugMetadata {
+            get {
+                return _debugMetadata;
+            }
+        }
+
+        // TODO: Come up with some clever solution for not having
+        // to have debug metadata on the object itself, perhaps
+        // for serialisation purposes at least.
+        DebugMetadata _debugMetadata;
+
+        public int? DebugLineNumberOfPath(Path path)
+        {
+            if (path == null)
+                return null;
+            
+            // Try to get a line number from debug metadata
+            var root = this.rootContentContainer;
+            if (root) {
+                Runtime.Object targetContent = root.ContentAtPath (path).obj;
+                if (targetContent) {
+                    var dm = targetContent.debugMetadata;
+                    if (dm != null) {
+                        return dm.startLineNumber;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+		public Path path 
+		{ 
+			get 
+			{
+                if (_path == null) {
+
+                    if (parent == null) {
+                        _path = new Path ();
+                    } else {
+                        // Maintain a Stack so that the order of the components
+                        // is reversed when they're added to the Path.
+                        // We're iterating up the hierarchy from the leaves/children to the root.
+                        var comps = new Stack<Path.Component> ();
+
+                        var child = this;
+                        Container container = child.parent as Container;
+
+                        while (container) {
+
+                            var namedChild = child as INamedContent;
+                            if (namedChild != null && namedChild.hasValidName) {
+                                comps.Push (new Path.Component (namedChild.name));
+                            } else {
+                                comps.Push (new Path.Component (container.content.IndexOf(child)));
+                            }
+
+                            child = container;
+                            container = container.parent as Container;
+                        }
+
+                        _path = new Path (comps);
+                    }
+
+                }
+				
+                return _path;
+			}
+		}
+        Path _path;
+
+        public SearchResult ResolvePath(Path path)
+        {
+            if (path.isRelative) {
+
+                Container nearestContainer = this as Container;
+                if (!nearestContainer) {
+                    Debug.Assert (this.parent != null, "Can't resolve relative path because we don't have a parent");
+                    nearestContainer = this.parent as Container;
+                    Debug.Assert (nearestContainer != null, "Expected parent to be a container");
+                    Debug.Assert (path.GetComponent(0).isParent);
+                    path = path.tail;
+                }
+
+                return nearestContainer.ContentAtPath (path);
+            } else {
+                return this.rootContentContainer.ContentAtPath (path);
+            }
+        }
+
+        public Path ConvertPathToRelative(Path globalPath)
+        {
+            // 1. Find last shared ancestor
+            // 2. Drill up using ".." style (actually represented as "^")
+            // 3. Re-build downward chain from common ancestor
+
+            var ownPath = this.path;
+
+			int minPathLength = Math.Min (globalPath.length, ownPath.length);
+            int lastSharedPathCompIndex = -1;
+
+            for (int i = 0; i < minPathLength; ++i) {
+                var ownComp = ownPath.GetComponent(i);
+                var otherComp = globalPath.GetComponent(i);
+
+                if (ownComp.Equals (otherComp)) {
+                    lastSharedPathCompIndex = i;
+                } else {
+                    break;
+                }
+            }
+
+            // No shared path components, so just use global path
+            if (lastSharedPathCompIndex == -1)
+                return globalPath;
+
+            int numUpwardsMoves = (ownPath.length-1) - lastSharedPathCompIndex;
+
+            var newPathComps = new List<Path.Component> ();
+
+            for(int up=0; up<numUpwardsMoves; ++up)
+                newPathComps.Add (Path.Component.ToParent ());
+
+			for (int down = lastSharedPathCompIndex + 1; down < globalPath.length; ++down)
+				newPathComps.Add (globalPath.GetComponent(down));
+
+            var relativePath = new Path (newPathComps, relative:true);
+            return relativePath;
+        }
+
+        // Find most compact representation for a path, whether relative or global
+        public string CompactPathString(Path otherPath)
+        {
+            string globalPathStr = null;
+            string relativePathStr = null;
+            if (otherPath.isRelative) {
+                relativePathStr = otherPath.componentsString;
+                globalPathStr = this.path.PathByAppendingPath(otherPath).componentsString;
+            } else {
+                var relativePath = ConvertPathToRelative (otherPath);
+                relativePathStr = relativePath.componentsString;
+                globalPathStr = otherPath.componentsString;
+            }
+
+            if (relativePathStr.Length < globalPathStr.Length) 
+                return relativePathStr;
+            else
+                return globalPathStr;
+        }
+
+        public Container rootContentContainer
+        {
+            get 
+            {
+                Runtime.Object ancestor = this;
+                while (ancestor.parent) {
+                    ancestor = ancestor.parent;
+                }
+                return ancestor as Container;
+            }
+        }
+
+		public Object ()
+		{
+		}
+
+        public virtual Object Copy()
+        {
+            throw new System.NotImplementedException (GetType ().Name + " doesn't support copying");
+        }
+
+        public void SetChild<T>(ref T obj, T value) where T : Runtime.Object
+        {
+            if (obj)
+                obj.parent = null;
+
+            obj = value;
+
+            if( obj )
+                obj.parent = this;
+        }
+            
+        /// Allow implicit conversion to bool so you don't have to do:
+        /// if( myObj != null ) ...
+        public static implicit operator bool (Object obj)
+        {
+            var isNull = object.ReferenceEquals (obj, null);
+            return !isNull;
+        }
+
+        /// Required for implicit bool comparison
+        public static bool operator ==(Object a, Object b)
+        {
+            return object.ReferenceEquals (a, b);
+        }
+
+        /// Required for implicit bool comparison
+        public static bool operator !=(Object a, Object b)
+        {
+            return !(a == b);
+        }
+
+        /// Required for implicit bool comparison
+        public override bool Equals (object obj)
+        {
+            return object.ReferenceEquals (obj, this);
+        }
+
+        /// Required for implicit bool comparison
+        public override int GetHashCode ()
+        {
+            return base.GetHashCode ();
+        }
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Object.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Object.cs.meta
new file mode 100644
index 0000000..222b2c5
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Object.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 20b56f60ae0f44147bccbae6ef3d0054
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Path.cs b/Assets/Ink/InkLibs/InkRuntime/Path.cs
new file mode 100644
index 0000000..428b2fc
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Path.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Diagnostics;
+using Ink.Runtime;
+
+namespace Ink.Runtime
+{
+    public class Path : IEquatable<Path>
+	{
+        static string parentId = "^";
+
+        // Immutable Component
+        public class Component : IEquatable<Component>
+		{
+			public int index { get; private set; }
+			public string name { get; private set; }
+			public bool isIndex { get { return index >= 0; } }
+            public bool isParent {
+                get {
+                    return name == Path.parentId;
+                }
+            }
+
+			public Component(int index)
+			{
+				Debug.Assert(index >= 0);
+				this.index = index;
+				this.name = null;
+			}
+
+			public Component(string name)
+			{
+				Debug.Assert(name != null && name.Length > 0);
+				this.name = name;
+				this.index = -1;
+			}
+
+            public static Component ToParent()
+            {
+                return new Component (parentId);
+            }
+
+			public override string ToString ()
+			{
+				if (isIndex) {
+					return index.ToString ();
+				} else {
+					return name;
+				}
+			}
+
+            public override bool Equals (object obj)
+            {
+                return Equals (obj as Component);
+            }
+
+            public bool Equals(Component otherComp)
+            {
+                if (otherComp != null && otherComp.isIndex == this.isIndex) {
+                    if (isIndex) {
+                        return index == otherComp.index;   
+                    } else {
+                        return name == otherComp.name;
+                    }
+                }
+
+                return false;
+            }
+
+            public override int GetHashCode ()
+            {
+                if (isIndex)
+                    return this.index;
+                else
+                    return this.name.GetHashCode ();
+            }
+		}
+
+		public Component GetComponent(int index)
+		{
+			return _components[index];
+		}
+
+        public bool isRelative { get; private set; }
+
+		public Component head 
+		{ 
+			get 
+			{ 
+				if (_components.Count > 0) {
+					return _components.First ();
+				} else {
+					return null;
+				}
+			} 
+		}
+
+		public Path tail 
+		{ 
+			get 
+			{
+				if (_components.Count >= 2) {
+					List<Component> tailComps = _components.GetRange (1, _components.Count - 1);
+					return new Path(tailComps);
+				} 
+
+                else {
+                    return Path.self;
+				}
+
+			}
+		}
+            
+		public int length { get { return _components.Count; } }
+
+		public Component lastComponent 
+		{ 
+			get 
+			{ 
+				var lastComponentIdx = _components.Count-1;
+				if( lastComponentIdx >= 0 )
+					return _components[lastComponentIdx];
+				else
+					return null;
+			} 
+		}
+
+        public bool containsNamedComponent {
+            get {
+                foreach(var comp in _components) {
+                    if( !comp.isIndex ) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        }
+
+		public Path()
+		{
+			_components = new List<Component> ();
+		}
+
+		public Path(Component head, Path tail) : this()
+		{
+			_components.Add (head);
+			_components.AddRange (tail._components);
+		}
+
+		public Path(IEnumerable<Component> components, bool relative = false) : this()
+		{
+			this._components.AddRange (components);
+            this.isRelative = relative;
+		}
+
+        public Path(string componentsString) : this()
+        {
+            this.componentsString = componentsString;
+        }
+
+        public static Path self {
+            get {
+                var path = new Path ();
+                path.isRelative = true;
+                return path;
+            }
+        }
+
+		public Path PathByAppendingPath(Path pathToAppend)
+		{
+            Path p = new Path ();
+
+            int upwardMoves = 0;
+            for (int i = 0; i < pathToAppend._components.Count; ++i) {
+                if (pathToAppend._components [i].isParent) {
+                    upwardMoves++;
+                } else {
+                    break;
+                }
+            }
+
+            for (int i = 0; i < this._components.Count - upwardMoves; ++i) {
+                p._components.Add (this._components [i]);
+            }
+
+            for(int i=upwardMoves; i<pathToAppend._components.Count; ++i) {
+                p._components.Add (pathToAppend._components [i]);
+            }
+
+			return p;
+		}
+
+        public Path PathByAppendingComponent (Component c)
+        {
+            Path p = new Path ();
+            p._components.AddRange (_components);
+            p._components.Add (c);
+            return p;
+        }
+
+        public string componentsString {
+            get {
+				if( _componentsString == null ) {
+					_componentsString = StringExt.Join (".", _components);
+					if (isRelative) _componentsString = "." + _componentsString;
+				}
+				return _componentsString;
+            }
+            private set {
+                _components.Clear ();
+
+				_componentsString = value;
+
+                // Empty path, empty components
+                // (path is to root, like "/" in file system)
+                if (string.IsNullOrEmpty(_componentsString))
+                    return;
+
+                // When components start with ".", it indicates a relative path, e.g.
+                //   .^.^.hello.5
+                // is equivalent to file system style path:
+                //  ../../hello/5
+                if (_componentsString [0] == '.') {
+                    this.isRelative = true;
+                    _componentsString = _componentsString.Substring (1);
+                } else {
+                    this.isRelative = false;
+                }
+
+                var componentStrings = _componentsString.Split('.');
+                foreach (var str in componentStrings) {
+                    int index;
+                    if (int.TryParse (str , out index)) {
+                        _components.Add (new Component (index));
+                    } else {
+                        _components.Add (new Component (str));
+                    }
+                }
+            }
+        }
+		string _componentsString;
+
+		public override string ToString()
+		{
+            return componentsString;
+		}
+
+        public override bool Equals (object obj)
+        {
+            return Equals (obj as Path);
+        }
+
+        public bool Equals (Path otherPath)
+        {
+            if (otherPath == null)
+                return false;
+
+            if (otherPath._components.Count != this._components.Count)
+                return false;
+
+            if (otherPath.isRelative != this.isRelative)
+                return false;
+
+            return otherPath._components.SequenceEqual (this._components);
+        }
+
+        public override int GetHashCode ()
+        {
+            // TODO: Better way to make a hash code!
+            return this.ToString ().GetHashCode ();
+        }
+
+		List<Component> _components;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Path.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Path.cs.meta
new file mode 100644
index 0000000..11e1bb0
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Path.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 62ce05aceee3c42cc90313aec848e171
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Pointer.cs b/Assets/Ink/InkLibs/InkRuntime/Pointer.cs
new file mode 100644
index 0000000..66fec9a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Pointer.cs
@@ -0,0 +1,70 @@
+using Ink.Runtime;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// Internal structure used to point to a particular / current point in the story.
+    /// Where Path is a set of components that make content fully addressable, this is
+    /// a reference to the current container, and the index of the current piece of 
+    /// content within that container. This scheme makes it as fast and efficient as
+    /// possible to increment the pointer (move the story forwards) in a way that's as
+    /// native to the internal engine as possible.
+    /// </summary>
+	public struct Pointer
+	{
+		public Container container;
+		public int index;
+
+        public Pointer (Container container, int index)
+        {
+            this.container = container;
+            this.index = index;
+        }
+
+		public Runtime.Object Resolve ()
+		{
+            if (index < 0) return container;
+            if (container == null) return null;
+            if (container.content.Count == 0) return container;
+            if (index >= container.content.Count) return null;
+            return container.content [index];
+
+		}
+
+        public bool isNull {
+            get {
+                return container == null;
+            }
+        }
+
+        public Path path {
+            get {
+                if( isNull ) return null;
+
+                if (index >= 0)
+                    return container.path.PathByAppendingComponent (new Path.Component(index));
+                else
+                    return container.path;
+            }
+        }
+
+        public override string ToString ()
+        {
+            if (container == null)
+                return "Ink Pointer (null)";
+
+            return "Ink Pointer -> " + container.path.ToString () + " -- index " + index;
+        }
+
+        public static Pointer StartOf (Container container)
+        {
+            return new Pointer {
+                container = container,
+                index = 0
+            };
+        }
+
+        public static Pointer Null = new Pointer { container = null, index = -1 };
+
+	}
+}
\ No newline at end of file
diff --git a/Assets/Ink/InkLibs/InkRuntime/Pointer.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Pointer.cs.meta
new file mode 100644
index 0000000..7db3d2e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Pointer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 802f84ed94f0e4f518e4be47877e9a33
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Profiler.cs b/Assets/Ink/InkLibs/InkRuntime/Profiler.cs
new file mode 100644
index 0000000..5de70a1
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Profiler.cs
@@ -0,0 +1,394 @@
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+
+namespace Ink.Runtime
+{
+	/// <summary>
+	/// Simple ink profiler that logs every instruction in the story and counts frequency and timing.
+	/// To use:
+	///  
+	///   var profiler = story.StartProfiling(), 
+	/// 
+	///   (play your story for a bit)
+	/// 
+	///   var reportStr = profiler.Report();
+	/// 
+	///   story.EndProfiling();
+	/// 
+	/// </summary>
+	public class Profiler
+	{
+        /// <summary>
+        /// The root node in the hierarchical tree of recorded ink timings.
+        /// </summary>
+		public ProfileNode rootNode {
+			get {
+				return _rootNode;
+			}
+		}
+
+		public Profiler() {
+			_rootNode = new ProfileNode();
+		}
+
+        /// <summary>
+        /// Generate a printable report based on the data recording during profiling.
+        /// </summary>
+		public string Report() {
+			var sb = new StringBuilder();
+			sb.AppendFormat("{0} CONTINUES / LINES:\n", _numContinues);
+			sb.AppendFormat("TOTAL TIME: {0}\n", FormatMillisecs(_continueTotal));
+			sb.AppendFormat("SNAPSHOTTING: {0}\n", FormatMillisecs(_snapTotal));
+			sb.AppendFormat("OTHER: {0}\n", FormatMillisecs(_continueTotal - (_stepTotal + _snapTotal)));
+			sb.Append(_rootNode.ToString());
+			return sb.ToString();
+		}
+
+		public void PreContinue() {
+			_continueWatch.Reset();
+			_continueWatch.Start();
+		}
+
+		public void PostContinue() {
+			_continueWatch.Stop();
+			_continueTotal += Millisecs(_continueWatch);
+			_numContinues++;
+		}
+
+		public void PreStep() {
+			_currStepStack = null;
+			_stepWatch.Reset();
+			_stepWatch.Start();
+		}
+
+		public void Step(CallStack callstack) 
+		{
+			_stepWatch.Stop();
+
+			var stack = new string[callstack.elements.Count];
+			for(int i=0; i<stack.Length; i++) {
+                string stackElementName = "";
+                if(!callstack.elements[i].currentPointer.isNull) {
+                    var objPath = callstack.elements[i].currentPointer.path;
+
+                    for(int c=0; c<objPath.length; c++) {
+                        var comp = objPath.GetComponent(c);
+                        if( !comp.isIndex ) {
+                            stackElementName = comp.name;
+                            break;
+                        }
+                    }
+
+                }
+                stack[i] = stackElementName;
+			}
+				
+			_currStepStack = stack;
+
+			var currObj = callstack.currentElement.currentPointer.Resolve();
+
+			string stepType = null;
+			var controlCommandStep = currObj as ControlCommand;
+			if( controlCommandStep )
+				stepType = controlCommandStep.commandType.ToString() + " CC";
+			else
+				stepType = currObj.GetType().Name;
+
+			_currStepDetails = new StepDetails {
+				type = stepType,
+				obj = currObj
+			};
+
+			_stepWatch.Start();
+		}
+
+		public void PostStep() {
+			_stepWatch.Stop();
+
+			var duration = Millisecs(_stepWatch);
+			_stepTotal += duration;
+
+			_rootNode.AddSample(_currStepStack, duration);
+
+			_currStepDetails.time = duration;
+			_stepDetails.Add(_currStepDetails);
+		}
+
+        /// <summary>
+        /// Generate a printable report specifying the average and maximum times spent
+        /// stepping over different internal ink instruction types.
+        /// This report type is primarily used to profile the ink engine itself rather
+        /// than your own specific ink.
+        /// </summary>
+		public string StepLengthReport()
+		{
+			var sb = new StringBuilder();
+
+			sb.AppendLine("TOTAL: "+_rootNode.totalMillisecs+"ms");
+
+			var averageStepTimes = _stepDetails
+				.GroupBy(s => s.type)
+				.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key, typeToDetails.Average(d => d.time)))
+				.OrderByDescending(stepTypeToAverage => stepTypeToAverage.Value)
+				.Select(stepTypeToAverage => {
+					var typeName = stepTypeToAverage.Key;
+					var time = stepTypeToAverage.Value;
+					return typeName + ": " + time + "ms";
+				})
+				.ToArray();
+
+			sb.AppendLine("AVERAGE STEP TIMES: "+string.Join(", ", averageStepTimes));
+
+			var accumStepTimes = _stepDetails
+				.GroupBy(s => s.type)
+				.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key + " (x"+typeToDetails.Count()+")", typeToDetails.Sum(d => d.time)))
+				.OrderByDescending(stepTypeToAccum => stepTypeToAccum.Value)
+				.Select(stepTypeToAccum => {
+					var typeName = stepTypeToAccum.Key;
+					var time = stepTypeToAccum.Value;
+					return typeName + ": " + time;
+				})
+				.ToArray();
+
+			sb.AppendLine("ACCUMULATED STEP TIMES: "+string.Join(", ", accumStepTimes));
+
+			return sb.ToString();
+		}
+
+        /// <summary>
+        /// Create a large log of all the internal instructions that were evaluated while profiling was active.
+        /// Log is in a tab-separated format, for easy loading into a spreadsheet application.
+        /// </summary>
+		public string Megalog()
+		{
+			var sb = new StringBuilder();
+
+			sb.AppendLine("Step type\tDescription\tPath\tTime");
+
+			foreach(var step in _stepDetails) {
+				sb.Append(step.type);
+				sb.Append("\t");
+				sb.Append(step.obj.ToString());
+				sb.Append("\t");
+				sb.Append(step.obj.path);
+				sb.Append("\t");
+				sb.AppendLine(step.time.ToString("F8"));
+			}
+
+			return sb.ToString();
+		}
+
+		public void PreSnapshot() {
+			_snapWatch.Reset();
+			_snapWatch.Start();
+		}
+
+		public void PostSnapshot() {
+			_snapWatch.Stop();
+			_snapTotal += Millisecs(_snapWatch);
+		}
+
+		double Millisecs(Stopwatch watch)
+		{
+			var ticks = watch.ElapsedTicks;
+			return ticks * _millisecsPerTick;
+		}
+
+		public static string FormatMillisecs(double num) {
+			if( num > 5000 ) {
+				return string.Format("{0:N1} secs", num / 1000.0);
+			} if( num > 1000 ) {
+				return string.Format("{0:N2} secs", num / 1000.0);
+			} else if( num > 100 ) {
+				return string.Format("{0:N0} ms", num);
+			} else if( num > 1 ) {
+				return string.Format("{0:N1} ms", num);
+			} else if( num > 0.01 ) {
+				return string.Format("{0:N3} ms", num);
+			} else {
+				return string.Format("{0:N} ms", num);
+			}
+		}
+
+		Stopwatch _continueWatch = new Stopwatch();
+		Stopwatch _stepWatch = new Stopwatch();
+		Stopwatch _snapWatch = new Stopwatch();
+
+		double _continueTotal;
+		double _snapTotal;
+		double _stepTotal;
+
+		string[] _currStepStack;
+		StepDetails _currStepDetails;
+		ProfileNode _rootNode;
+		int _numContinues;
+
+		struct StepDetails {
+			public string type;
+			public Runtime.Object obj;
+			public double time;
+		}
+		List<StepDetails> _stepDetails = new List<StepDetails>();
+
+		static double _millisecsPerTick = 1000.0 / Stopwatch.Frequency;
+	}
+
+
+    /// <summary>
+    /// Node used in the hierarchical tree of timings used by the Profiler.
+    /// Each node corresponds to a single line viewable in a UI-based representation.
+    /// </summary>
+	public class ProfileNode {
+
+        /// <summary>
+        /// The key for the node corresponds to the printable name of the callstack element.
+        /// </summary>		
+        public readonly string key;
+
+
+        #pragma warning disable 0649
+        /// <summary>
+        /// Horribly hacky field only used by ink unity integration,
+        /// but saves constructing an entire data structure that mirrors
+        /// the one in here purely to store the state of whether each
+        /// node in the UI has been opened or not  /// </summary>
+        public bool openInUI;
+        #pragma warning restore 0649
+
+        /// <summary>
+        /// Whether this node contains any sub-nodes - i.e. does it call anything else
+        /// that has been recorded?
+        /// </summary>
+        /// <value><c>true</c> if has children; otherwise, <c>false</c>.</value>
+		public bool hasChildren {
+			get {
+				return _nodes != null && _nodes.Count > 0;
+			}
+		}
+
+        /// <summary>
+        /// Total number of milliseconds this node has been active for.
+        /// </summary>
+		public int totalMillisecs {
+			get {
+				return (int)_totalMillisecs;
+			}
+		}
+
+		public ProfileNode() {
+
+		}
+
+		public ProfileNode(string key) {
+			this.key = key;
+		}
+
+		public void AddSample(string[] stack, double duration) {
+			AddSample(stack, -1, duration);
+		}
+
+		void AddSample(string[] stack, int stackIdx, double duration) {
+
+			_totalSampleCount++;
+			_totalMillisecs += duration;
+
+			if( stackIdx == stack.Length-1 ) {
+				_selfSampleCount++;
+				_selfMillisecs += duration;
+			}
+
+			if( stackIdx+1 < stack.Length )
+				AddSampleToNode(stack, stackIdx+1, duration);
+		}
+
+		void AddSampleToNode(string[] stack, int stackIdx, double duration)
+		{
+			var nodeKey = stack[stackIdx];
+			if( _nodes == null ) _nodes = new Dictionary<string, ProfileNode>();
+
+			ProfileNode node;
+			if( !_nodes.TryGetValue(nodeKey, out node) ) {
+				node = new ProfileNode(nodeKey);
+				_nodes[nodeKey] = node;
+			}
+
+			node.AddSample(stack, stackIdx, duration);
+		}
+
+        /// <summary>
+        /// Returns a sorted enumerable of the nodes in descending order of
+        /// how long they took to run.
+        /// </summary>
+		public IEnumerable<KeyValuePair<string, ProfileNode>> descendingOrderedNodes {
+			get {
+				if( _nodes == null ) return null;
+				return _nodes.OrderByDescending(keyNode => keyNode.Value._totalMillisecs);
+			}
+		}
+
+		void PrintHierarchy(StringBuilder sb, int indent)
+		{
+			Pad(sb, indent);
+
+			sb.Append(key);
+			sb.Append(": ");
+			sb.AppendLine(ownReport);
+
+			if( _nodes == null ) return;
+
+			foreach(var keyNode in descendingOrderedNodes) {
+				keyNode.Value.PrintHierarchy(sb, indent+1);
+			}
+		}
+
+        /// <summary>
+        /// Generates a string giving timing information for this single node, including
+        /// total milliseconds spent on the piece of ink, the time spent within itself
+        /// (v.s. spent in children), as well as the number of samples (instruction steps)
+        /// recorded for both too.
+        /// </summary>
+        /// <value>The own report.</value>
+		public string ownReport {
+			get {
+				var sb = new StringBuilder();
+				sb.Append("total ");
+				sb.Append(Profiler.FormatMillisecs(_totalMillisecs));
+				sb.Append(", self ");
+				sb.Append(Profiler.FormatMillisecs(_selfMillisecs));
+				sb.Append(" (");
+				sb.Append(_selfSampleCount);
+				sb.Append(" self samples, ");
+				sb.Append(_totalSampleCount);
+				sb.Append(" total)");
+				return sb.ToString();
+			}
+			
+		}
+
+		void Pad(StringBuilder sb, int spaces)
+		{
+			for(int i=0; i<spaces; i++) sb.Append("   ");
+		}
+
+        /// <summary>
+        /// String is a report of the sub-tree from this node, but without any of the header information
+        /// that's prepended by the Profiler in its Report() method.
+        /// </summary>
+		public override string ToString ()
+		{
+			var sb = new StringBuilder();
+			PrintHierarchy(sb, 0);
+			return sb.ToString();
+		}
+
+		Dictionary<string, ProfileNode> _nodes;
+		double _selfMillisecs;
+		double _totalMillisecs;
+		int _selfSampleCount;
+		int _totalSampleCount;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Profiler.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Profiler.cs.meta
new file mode 100644
index 0000000..f80ec08
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Profiler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8323a1136a1484abebab14150a06453a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/PushPop.cs b/Assets/Ink/InkLibs/InkRuntime/PushPop.cs
new file mode 100644
index 0000000..42a29bf
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/PushPop.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public enum PushPopType 
+    {
+        Tunnel,
+        Function,
+        FunctionEvaluationFromGame
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/PushPop.cs.meta b/Assets/Ink/InkLibs/InkRuntime/PushPop.cs.meta
new file mode 100644
index 0000000..63d9c16
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/PushPop.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b6f42c73ddbc24b8fadf5f6c84287fc0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs b/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs
new file mode 100644
index 0000000..e3a8c24
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs
@@ -0,0 +1,18 @@
+using System;
+namespace Ink.Runtime
+{
+    // When looking up content within the story (e.g. in Container.ContentAtPath),
+    // the result is generally found, but if the story is modified, then when loading
+    // up an old save state, then some old paths may still exist. In this case we
+    // try to recover by finding an approximate result by working up the story hierarchy
+    // in the path to find the closest valid container. Instead of crashing horribly,
+    // we might see some slight oddness in the content, but hopefully it recovers!
+    public struct SearchResult
+    {
+        public Runtime.Object obj;
+        public bool approximate;
+
+        public Runtime.Object correctObj { get { return approximate ? null : obj; } }
+        public Container container { get { return obj as Container; } }
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs.meta b/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs.meta
new file mode 100644
index 0000000..b15c73e
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/SearchResult.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 40f07931ec6ee409e97714e5567fd5c6
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs b/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs
new file mode 100644
index 0000000..7494410
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs
@@ -0,0 +1,647 @@
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// Simple custom JSON serialisation implementation that takes JSON-able System.Collections that
+    /// are produced by the ink engine and converts to and from JSON text.
+    /// </summary>
+    public static class SimpleJson
+    {
+        public static Dictionary<string, object> TextToDictionary (string text)
+        {
+            return new Reader (text).ToDictionary ();
+        }
+
+        public static List<object> TextToArray(string text)
+        {
+            return new Reader(text).ToArray();
+        }
+
+        class Reader
+        {
+            public Reader (string text)
+            {
+                _text = text;
+                _offset = 0;
+
+                SkipWhitespace ();
+
+                _rootObject = ReadObject ();
+            }
+
+            public Dictionary<string, object> ToDictionary ()
+            {
+                return (Dictionary<string, object>)_rootObject;
+            }
+
+            public List<object> ToArray()
+            {
+                return (List<object>)_rootObject;
+            }
+
+            bool IsNumberChar (char c)
+            {
+                return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e';
+            }
+
+            bool IsFirstNumberChar(char c)
+            {
+                return c >= '0' && c <= '9' || c == '-' || c == '+';
+            }
+
+            object ReadObject ()
+            {
+                var currentChar = _text [_offset];
+
+                if( currentChar == '{' )
+                    return ReadDictionary ();
+                
+                else if (currentChar == '[')
+                    return ReadArray ();
+
+                else if (currentChar == '"')
+                    return ReadString ();
+
+                else if (IsFirstNumberChar(currentChar))
+                    return ReadNumber ();
+
+                else if (TryRead ("true"))
+                    return true;
+
+                else if (TryRead ("false"))
+                    return false;
+
+                else if (TryRead ("null"))
+                    return null;
+
+                throw new System.Exception ("Unhandled object type in JSON: "+_text.Substring (_offset, 30));
+            }
+
+            Dictionary<string, object> ReadDictionary ()
+            {
+                var dict = new Dictionary<string, object> ();
+
+                Expect ("{");
+
+                SkipWhitespace ();
+
+                // Empty dictionary?
+                if (TryRead ("}"))
+                    return dict;
+
+                do {
+
+                    SkipWhitespace ();
+
+                    // Key
+                    var key = ReadString ();
+                    Expect (key != null, "dictionary key");
+
+                    SkipWhitespace ();
+
+                    // :
+                    Expect (":");
+
+                    SkipWhitespace ();
+
+                    // Value
+                    var val = ReadObject ();
+                    Expect (val != null, "dictionary value");
+
+                    // Add to dictionary
+                    dict [key] = val;
+
+                    SkipWhitespace ();
+
+                } while ( TryRead (",") );
+
+                Expect ("}");
+
+                return dict;
+            }
+
+            List<object> ReadArray ()
+            {
+                var list = new List<object> ();
+
+                Expect ("[");
+
+                SkipWhitespace ();
+
+                // Empty list?
+                if (TryRead ("]"))
+                    return list;
+
+                do {
+
+                    SkipWhitespace ();
+
+                    // Value
+                    var val = ReadObject ();
+
+                    // Add to array
+                    list.Add (val);
+
+                    SkipWhitespace ();
+
+                } while (TryRead (","));
+
+                Expect ("]");
+
+                return list;
+            }
+
+            string ReadString ()
+            {
+                Expect ("\"");
+
+                var sb = new StringBuilder();
+
+                for (; _offset < _text.Length; _offset++) {
+                    var c = _text [_offset];
+
+                    if (c == '\\') {
+                        // Escaped character
+                        _offset++;
+                        if (_offset >= _text.Length) {
+                            throw new Exception("Unexpected EOF while reading string");
+                        }
+                        c = _text[_offset];
+                        switch (c)
+                        {
+                            case '"':
+                            case '\\':
+                            case '/': // Yes, JSON allows this to be escaped
+                                sb.Append(c);
+                                break;
+                            case 'n':
+                                sb.Append('\n');
+                                break;
+                            case 't':
+                                sb.Append('\t');
+                                break;
+                            case 'r':
+                            case 'b':
+                            case 'f':
+                                // Ignore other control characters
+                                break;
+                            case 'u':
+                                // 4-digit Unicode
+                                if (_offset + 4 >=_text.Length) {
+                                    throw new Exception("Unexpected EOF while reading string");
+                                }
+                                var digits = _text.Substring(_offset + 1, 4);
+                                int uchar;
+                                if (int.TryParse(digits, System.Globalization.NumberStyles.AllowHexSpecifier, System.Globalization.CultureInfo.InvariantCulture, out uchar)) {
+                                    sb.Append((char)uchar);
+                                    _offset += 4;
+                                } else {
+                                    throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
+                                }
+                                break;
+                            default:
+                                // The escaped character is invalid per json spec
+                                throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
+                        }
+                    } else if( c == '"' ) {
+                        break;
+                    } else {
+                        sb.Append(c);
+                    }
+                }
+
+                Expect ("\"");
+                return sb.ToString();
+            }
+
+            object ReadNumber ()
+            {
+                var startOffset = _offset;
+
+                bool isFloat = false;
+                for (; _offset < _text.Length; _offset++) {
+                    var c = _text [_offset];
+                    if (c == '.' || c == 'e' || c == 'E') isFloat = true;
+                    if (IsNumberChar (c))
+                        continue;
+                    else
+                        break;
+                }
+
+                string numStr = _text.Substring (startOffset, _offset - startOffset);
+
+                if (isFloat) {
+                    float f;
+                    if (float.TryParse (numStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) {
+                        return f;
+                    }
+                } else {
+                    int i;
+                    if (int.TryParse (numStr, out i)) {
+                        return i;
+                    }
+                }
+
+                throw new System.Exception ("Failed to parse number value: "+numStr);
+            }
+
+            bool TryRead (string textToRead)
+            {
+                if (_offset + textToRead.Length > _text.Length)
+                    return false;
+                
+                for (int i = 0; i < textToRead.Length; i++) {
+                    if (textToRead [i] != _text [_offset + i])
+                        return false;
+                }
+
+                _offset += textToRead.Length;
+
+                return true;
+            }
+
+            void Expect (string expectedStr)
+            {
+                if (!TryRead (expectedStr))
+                    Expect (false, expectedStr);
+            }
+
+            void Expect (bool condition, string message = null)
+            {
+                if (!condition) {
+                    if (message == null) {
+                        message = "Unexpected token";
+                    } else {
+                        message = "Expected " + message;
+                    }
+                    message += " at offset " + _offset;
+
+                    throw new System.Exception (message);
+                }
+            }
+
+            void SkipWhitespace ()
+            {
+                while (_offset < _text.Length) {
+                    var c = _text [_offset];
+                    if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
+                        _offset++;
+                    else
+                        break;
+                }
+            }
+
+            string _text;
+            int _offset;
+
+            object _rootObject;
+        }
+
+
+        public class Writer
+        {
+            public Writer()
+            {
+                _writer = new StringWriter();
+            }
+
+            public Writer(Stream stream)
+            {
+                _writer = new System.IO.StreamWriter(stream, Encoding.UTF8);
+            }
+
+            public void WriteObject(Action<Writer> inner)
+            {
+                WriteObjectStart();
+                inner(this);
+                WriteObjectEnd();
+            }
+
+            public void WriteObjectStart()
+            {
+                StartNewObject(container: true);
+                _stateStack.Push(new StateElement { type = State.Object });
+                _writer.Write("{");
+            }
+
+            public void WriteObjectEnd()
+            {
+                Assert(state == State.Object);
+                _writer.Write("}");
+                _stateStack.Pop();
+            }
+
+            public void WriteProperty(string name, Action<Writer> inner)
+            {
+                WriteProperty<string>(name, inner);
+            }
+
+            public void WriteProperty(int id, Action<Writer> inner)
+            {
+                WriteProperty<int>(id, inner);
+            }
+
+            public void WriteProperty(string name, string content)
+            {
+                WritePropertyStart(name);
+                Write(content);
+                WritePropertyEnd();
+            }
+
+            public void WriteProperty(string name, int content)
+            {
+                WritePropertyStart(name);
+                Write(content);
+                WritePropertyEnd();
+            }
+
+            public void WriteProperty(string name, bool content)
+            {
+                WritePropertyStart(name);
+                Write(content);
+                WritePropertyEnd();
+            }
+
+            public void WritePropertyStart(string name)
+            {
+                WritePropertyStart<string>(name);
+            }
+
+            public void WritePropertyStart(int id)
+            {
+                WritePropertyStart<int>(id);
+            }
+
+            public void WritePropertyEnd()
+            {
+                Assert(state == State.Property);
+                Assert(childCount == 1);
+                _stateStack.Pop();
+            }
+
+            public void WritePropertyNameStart()
+            {
+                Assert(state == State.Object);
+
+                if (childCount > 0)
+                    _writer.Write(",");
+
+                _writer.Write("\"");
+
+                IncrementChildCount();
+
+                _stateStack.Push(new StateElement { type = State.Property });
+                _stateStack.Push(new StateElement { type = State.PropertyName });
+            }
+
+            public void WritePropertyNameEnd()
+            {
+                Assert(state == State.PropertyName);
+
+                _writer.Write("\":");
+
+                // Pop PropertyName, leaving Property state
+                _stateStack.Pop();
+            }
+
+            public void WritePropertyNameInner(string str)
+            {
+                Assert(state == State.PropertyName);
+                _writer.Write(str);
+            }
+
+            void WritePropertyStart<T>(T name)
+            {
+                Assert(state == State.Object);
+
+                if (childCount > 0)
+                    _writer.Write(",");
+
+                _writer.Write("\"");
+                _writer.Write(name);
+                _writer.Write("\":");
+
+                IncrementChildCount();
+
+                _stateStack.Push(new StateElement { type = State.Property });
+            }
+
+
+            // allow name to be string or int
+            void WriteProperty<T>(T name, Action<Writer> inner)
+            {
+                WritePropertyStart(name);
+
+                inner(this);
+
+                WritePropertyEnd();
+            }
+
+            public void WriteArrayStart()
+            {
+                StartNewObject(container: true);
+                _stateStack.Push(new StateElement { type = State.Array });
+                _writer.Write("[");
+            }
+
+            public void WriteArrayEnd()
+            {
+                Assert(state == State.Array);
+                _writer.Write("]");
+                _stateStack.Pop();
+            }
+
+            public void Write(int i)
+            {
+                StartNewObject(container: false);
+                _writer.Write(i);
+            }
+
+            public void Write(float f)
+            {
+                StartNewObject(container: false);
+
+                // TODO: Find an heap-allocation-free way to do this please!
+                // _writer.Write(formatStr, obj (the float)) requires boxing
+                // Following implementation seems to work ok but requires creating temporary garbage string.
+                string floatStr = f.ToString(System.Globalization.CultureInfo.InvariantCulture);
+                if( floatStr == "Infinity" ) {
+                    _writer.Write("3.4E+38"); // JSON doesn't support, do our best alternative
+                } else if (floatStr == "-Infinity") {
+                    _writer.Write("-3.4E+38"); // JSON doesn't support, do our best alternative
+                } else if ( floatStr == "NaN" ) {
+                    _writer.Write("0.0"); // JSON doesn't support, not much we can do
+                } else {
+                    _writer.Write(floatStr);
+                    if (!floatStr.Contains(".") && !floatStr.Contains("E")) 
+                        _writer.Write(".0"); // ensure it gets read back in as a floating point value
+                }
+            }
+
+            public void Write(string str, bool escape = true)
+            {
+                StartNewObject(container: false);
+
+                _writer.Write("\"");
+                if (escape)
+                    WriteEscapedString(str);
+                else
+                    _writer.Write(str);
+                _writer.Write("\"");
+            }
+
+            public void Write(bool b)
+            {
+                StartNewObject(container: false);
+                _writer.Write(b ? "true" : "false");
+            }
+
+            public void WriteNull()
+            {
+                StartNewObject(container: false);
+                _writer.Write("null");
+            }
+
+            public void WriteStringStart()
+            {
+                StartNewObject(container: false);
+                _stateStack.Push(new StateElement { type = State.String });
+                _writer.Write("\"");
+            }
+
+            public void WriteStringEnd()
+            {
+                Assert(state == State.String);
+                _writer.Write("\"");
+                _stateStack.Pop();
+            }
+
+            public void WriteStringInner(string str, bool escape = true)
+            {
+                Assert(state == State.String);
+                if (escape)
+                    WriteEscapedString(str);
+                else
+                    _writer.Write(str);
+            }
+
+            void WriteEscapedString(string str)
+            {
+                foreach (var c in str)
+                {
+                    if (c < ' ')
+                    {
+                        // Don't write any control characters except \n and \t
+                        switch (c)
+                        {
+                            case '\n':
+                                _writer.Write("\\n");
+                                break;
+                            case '\t':
+                                _writer.Write("\\t");
+                                break;
+                        }
+                    }
+                    else
+                    {
+                        switch (c)
+                        {
+                            case '\\':
+                            case '"':
+                                _writer.Write("\\");
+                                _writer.Write(c);
+                                break;
+                            default:
+                                _writer.Write(c);
+                                break;
+                        }
+                    }
+                }
+            }
+
+            void StartNewObject(bool container)
+            {
+
+                if (container)
+                    Assert(state == State.None || state == State.Property || state == State.Array);
+                else
+                    Assert(state == State.Property || state == State.Array);
+
+                if (state == State.Array && childCount > 0)
+                    _writer.Write(",");
+
+                if (state == State.Property)
+                    Assert(childCount == 0);
+
+                if (state == State.Array || state == State.Property)
+                    IncrementChildCount();
+            }
+
+            State state
+            {
+                get
+                {
+                    if (_stateStack.Count > 0) return _stateStack.Peek().type;
+                    else return State.None;
+                }
+            }
+
+            int childCount
+            {
+                get
+                {
+                    if (_stateStack.Count > 0) return _stateStack.Peek().childCount;
+                    else return 0;
+                }
+            }
+
+            void IncrementChildCount()
+            {
+                Assert(_stateStack.Count > 0);
+                var currEl = _stateStack.Pop();
+                currEl.childCount++;
+                _stateStack.Push(currEl);
+            }
+
+            // Shouldn't hit this assert outside of initial JSON development,
+            // so it's save to make it debug-only.
+            [System.Diagnostics.Conditional("DEBUG")]
+            void Assert(bool condition)
+            {
+                if (!condition)
+                    throw new System.Exception("Assert failed while writing JSON");
+            }
+
+            public override string ToString()
+            {
+                return _writer.ToString();
+            }
+
+            enum State
+            {
+                None,
+                Object,
+                Array,
+                Property,
+                PropertyName,
+                String
+            };
+
+            struct StateElement
+            {
+                public State type;
+                public int childCount;
+            }
+
+            Stack<StateElement> _stateStack = new Stack<StateElement>();
+            TextWriter _writer;
+        }
+
+
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs.meta b/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs.meta
new file mode 100644
index 0000000..1d877b3
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/SimpleJson.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5f2b378ccb9e148929c90bae41bb2c9d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs b/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs
new file mode 100644
index 0000000..2cc5640
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    public class StatePatch
+    {
+        public Dictionary<string, Runtime.Object> globals { get { return _globals;  } }
+        public HashSet<string> changedVariables { get { return _changedVariables;  } }
+        public Dictionary<Container, int> visitCounts { get { return _visitCounts;  } }
+        public Dictionary<Container, int> turnIndices { get { return _turnIndices;  } }
+
+        public StatePatch(StatePatch toCopy)
+        {
+            if( toCopy != null ) {
+                _globals = new Dictionary<string, Object>(toCopy._globals);
+                _changedVariables = new HashSet<string>(toCopy._changedVariables);
+                _visitCounts = new Dictionary<Container, int>(toCopy._visitCounts);
+                _turnIndices = new Dictionary<Container, int>(toCopy._turnIndices);
+            } else {
+                _globals = new Dictionary<string, Object>();
+                _changedVariables = new HashSet<string>();
+                _visitCounts = new Dictionary<Container, int>();
+                _turnIndices = new Dictionary<Container, int>();
+            }
+        }
+
+        public bool TryGetGlobal(string name, out Runtime.Object value)
+        {
+            return _globals.TryGetValue(name, out value);
+        }
+
+        public void SetGlobal(string name, Runtime.Object value){
+            _globals[name] = value;
+        }
+
+        public void AddChangedVariable(string name)
+        {
+            _changedVariables.Add(name);
+        }
+
+        public bool TryGetVisitCount(Container container, out int count)
+        {
+            return _visitCounts.TryGetValue(container, out count);
+        }
+
+        public void SetVisitCount(Container container, int count)
+        {
+            _visitCounts[container] = count;
+        }
+
+        public void SetTurnIndex(Container container, int index)
+        {
+            _turnIndices[container] = index;
+        }
+
+        public bool TryGetTurnIndex(Container container, out int index)
+        {
+            return _turnIndices.TryGetValue(container, out index);
+        }
+
+        Dictionary<string, Runtime.Object> _globals;
+        HashSet<string> _changedVariables = new HashSet<string>();
+        Dictionary<Container, int> _visitCounts = new Dictionary<Container, int>();
+        Dictionary<Container, int> _turnIndices = new Dictionary<Container, int>();
+    }
+}
diff --git a/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs.meta b/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs.meta
new file mode 100644
index 0000000..9568d40
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StatePatch.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: be1efe20e2058440e8efa8927902041f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Story.cs b/Assets/Ink/InkLibs/InkRuntime/Story.cs
new file mode 100644
index 0000000..1124636
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Story.cs
@@ -0,0 +1,2763 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.IO;
+using System.Diagnostics;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// A Story is the core class that represents a complete Ink narrative, and
+    /// manages the evaluation and state of it.
+    /// </summary>
+	public class Story : Runtime.Object
+	{
+        /// <summary>
+        /// The current version of the ink story file format.
+        /// </summary>
+        public const int inkVersionCurrent = 20;
+
+        // Version numbers are for engine itself and story file, rather
+        // than the story state save format
+        //  -- old engine, new format: always fail
+        //  -- new engine, old format: possibly cope, based on this number
+        // When incrementing the version number above, the question you
+        // should ask yourself is:
+        //  -- Will the engine be able to load an old story file from 
+        //     before I made these changes to the engine?
+        //     If possible, you should support it, though it's not as
+        //     critical as loading old save games, since it's an
+        //     in-development problem only.
+
+        /// <summary>
+        /// The minimum legacy version of ink that can be loaded by the current version of the code.
+        /// </summary>
+        const int inkVersionMinimumCompatible = 18;
+
+        /// <summary>
+        /// The list of Choice objects available at the current point in
+        /// the Story. This list will be populated as the Story is stepped
+        /// through with the Continue() method. Once canContinue becomes
+        /// false, this list will be populated, and is usually
+        /// (but not always) on the final Continue() step.
+        /// </summary>
+        public List<Choice> currentChoices
+		{
+			get 
+			{
+                // Don't include invisible choices for external usage.
+                var choices = new List<Choice>();
+                foreach (var c in _state.currentChoices) {
+                    if (!c.isInvisibleDefault) {
+                        c.index = choices.Count;
+                        choices.Add (c);
+                    }
+                }
+                return choices;
+			}
+		}
+            
+        /// <summary>
+        /// The latest line of text to be generated from a Continue() call.
+        /// </summary>
+		public string currentText { 
+            get  { 
+                IfAsyncWeCant ("call currentText since it's a work in progress");
+                return state.currentText; 
+            } 
+        }
+
+        /// <summary>
+        /// Gets a list of tags as defined with '#' in source that were seen
+        /// during the latest Continue() call.
+        /// </summary>
+        public List<string> currentTags { 
+            get { 
+                IfAsyncWeCant ("call currentTags since it's a work in progress");
+                return state.currentTags; 
+            } 
+        }
+
+        /// <summary>
+        /// Any errors generated during evaluation of the Story.
+        /// </summary>
+        public List<string> currentErrors { get { return state.currentErrors; } }
+
+        /// <summary>
+        /// Any warnings generated during evaluation of the Story.
+        /// </summary>
+        public List<string> currentWarnings { get { return state.currentWarnings; } }
+
+        /// <summary>
+        /// The current flow name if using multi-flow funtionality - see SwitchFlow
+        /// </summary>
+        public string currentFlowName => state.currentFlowName;
+
+        /// <summary>
+        /// Whether the currentErrors list contains any errors.
+        /// THIS MAY BE REMOVED - you should be setting an error handler directly
+        /// using Story.onError.
+        /// </summary>
+        public bool hasError { get { return state.hasError; } }
+
+        /// <summary>
+        /// Whether the currentWarnings list contains any warnings.
+        /// </summary>
+        public bool hasWarning { get { return state.hasWarning; } }
+
+        /// <summary>
+        /// The VariablesState object contains all the global variables in the story.
+        /// However, note that there's more to the state of a Story than just the
+        /// global variables. This is a convenience accessor to the full state object.
+        /// </summary>
+        public VariablesState variablesState{ get { return state.variablesState; } }
+
+        public ListDefinitionsOrigin listDefinitions {
+            get {
+                return _listDefinitions;
+            }
+        }
+
+        /// <summary>
+        /// The entire current state of the story including (but not limited to):
+        /// 
+        ///  * Global variables
+        ///  * Temporary variables
+        ///  * Read/visit and turn counts
+        ///  * The callstack and evaluation stacks
+        ///  * The current threads
+        /// 
+        /// </summary>
+        public StoryState state { get { return _state; } }
+        
+        /// <summary>
+        /// Error handler for all runtime errors in ink - i.e. problems
+        /// with the source ink itself that are only discovered when playing
+        /// the story.
+        /// It's strongly recommended that you assign an error handler to your
+        /// story instance to avoid getting exceptions for ink errors.
+        /// </summary>
+        public event Ink.ErrorHandler onError;
+        
+        /// <summary>
+        /// Callback for when ContinueInternal is complete
+        /// </summary>
+        public event Action onDidContinue;
+        /// <summary>
+        /// Callback for when a choice is about to be executed
+        /// </summary>
+        public event Action<Choice> onMakeChoice;
+        /// <summary>
+        /// Callback for when a function is about to be evaluated
+        /// </summary>
+        public event Action<string, object[]> onEvaluateFunction;
+        /// <summary>
+        /// Callback for when a function has been evaluated
+        /// This is necessary because evaluating a function can cause continuing
+        /// </summary>
+        public event Action<string, object[], string, object> onCompleteEvaluateFunction;
+        /// <summary>
+        /// Callback for when a path string is chosen
+        /// </summary>
+        public event Action<string, object[]> onChoosePathString;
+
+        /// <summary>
+        /// Start recording ink profiling information during calls to Continue on Story.
+        /// Return a Profiler instance that you can request a report from when you're finished.
+        /// </summary>
+		public Profiler StartProfiling() {
+            IfAsyncWeCant ("start profiling");
+			_profiler = new Profiler();
+			return _profiler;
+		}
+
+        /// <summary>
+        /// Stop recording ink profiling information during calls to Continue on Story.
+        /// To generate a report from the profiler, call 
+        /// </summary>
+		public void EndProfiling() {
+			_profiler = null;
+		}
+            
+        // Warning: When creating a Story using this constructor, you need to
+        // call ResetState on it before use. Intended for compiler use only.
+        // For normal use, use the constructor that takes a json string.
+        public Story (Container contentContainer, List<Runtime.ListDefinition> lists = null)
+		{
+			_mainContentContainer = contentContainer;
+
+            if (lists != null)
+                _listDefinitions = new ListDefinitionsOrigin (lists);
+
+            _externals = new Dictionary<string, ExternalFunctionDef> ();
+		}
+
+        /// <summary>
+        /// Construct a Story object using a JSON string compiled through inklecate.
+        /// </summary>
+        public Story(string jsonString) : this((Container)null)
+        {
+            Dictionary<string, object> rootObject = SimpleJson.TextToDictionary (jsonString);
+
+            object versionObj = rootObject ["inkVersion"];
+            if (versionObj == null)
+                throw new System.Exception ("ink version number not found. Are you sure it's a valid .ink.json file?");
+
+            int formatFromFile = (int)versionObj;
+            if (formatFromFile > inkVersionCurrent) {
+                throw new System.Exception ("Version of ink used to build story was newer than the current version of the engine");
+            } else if (formatFromFile < inkVersionMinimumCompatible) {
+                throw new System.Exception ("Version of ink used to build story is too old to be loaded by this version of the engine");
+            } else if (formatFromFile != inkVersionCurrent) {
+                System.Diagnostics.Debug.WriteLine ("WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising.");
+            }
+                
+            var rootToken = rootObject ["root"];
+            if (rootToken == null)
+                throw new System.Exception ("Root node for ink not found. Are you sure it's a valid .ink.json file?");
+
+            object listDefsObj;
+            if (rootObject.TryGetValue ("listDefs", out listDefsObj)) {
+                _listDefinitions = Json.JTokenToListDefinitions (listDefsObj);
+            }
+
+            _mainContentContainer = Json.JTokenToRuntimeObject (rootToken) as Container;
+
+            ResetState ();
+        }
+
+        /// <summary>
+        /// The Story itself in JSON representation.
+        /// </summary>
+        public string ToJson()
+        {
+            //return ToJsonOld();
+            var writer = new SimpleJson.Writer();
+            ToJson(writer);
+            return writer.ToString();
+        }
+
+        /// <summary>
+        /// The Story itself in JSON representation.
+        /// </summary>
+        public void ToJson(Stream stream)
+        {
+            var writer = new SimpleJson.Writer(stream);
+            ToJson(writer);
+        }
+
+        void ToJson(SimpleJson.Writer writer)
+        {
+            writer.WriteObjectStart();
+
+            writer.WriteProperty("inkVersion", inkVersionCurrent);
+
+            // Main container content
+            writer.WriteProperty("root", w => Json.WriteRuntimeContainer(w, _mainContentContainer));
+
+            // List definitions
+            if (_listDefinitions != null) {
+
+                writer.WritePropertyStart("listDefs");
+                writer.WriteObjectStart();
+
+                foreach (ListDefinition def in _listDefinitions.lists)
+                {
+                    writer.WritePropertyStart(def.name);
+                    writer.WriteObjectStart();
+
+                    foreach (var itemToVal in def.items)
+                    {
+                        InkListItem item = itemToVal.Key;
+                        int val = itemToVal.Value;
+                        writer.WriteProperty(item.itemName, val);
+                    }
+
+                    writer.WriteObjectEnd();
+                    writer.WritePropertyEnd();
+                }
+
+                writer.WriteObjectEnd();
+                writer.WritePropertyEnd();
+            }
+
+            writer.WriteObjectEnd();
+        }
+            
+        /// <summary>
+        /// Reset the Story back to its initial state as it was when it was
+        /// first constructed.
+        /// </summary>
+        public void ResetState()
+        {
+            // TODO: Could make this possible
+            IfAsyncWeCant ("ResetState");
+
+            _state = new StoryState (this);
+            _state.variablesState.variableChangedEvent += VariableStateDidChangeEvent;
+
+            ResetGlobals ();
+        }
+
+        void ResetErrors()
+        {
+            _state.ResetErrors ();
+        }
+
+        /// <summary>
+        /// Unwinds the callstack. Useful to reset the Story's evaluation
+        /// without actually changing any meaningful state, for example if
+        /// you want to exit a section of story prematurely and tell it to
+        /// go elsewhere with a call to ChoosePathString(...).
+        /// Doing so without calling ResetCallstack() could cause unexpected
+        /// issues if, for example, the Story was in a tunnel already.
+        /// </summary>
+        public void ResetCallstack()
+        {
+            IfAsyncWeCant ("ResetCallstack");
+
+            _state.ForceEnd ();
+        }
+
+        void ResetGlobals()
+        {
+            if (_mainContentContainer.namedContent.ContainsKey ("global decl")) {
+                var originalPointer = state.currentPointer;
+
+                ChoosePath (new Path ("global decl"), incrementingTurnIndex: false);
+
+                // Continue, but without validating external bindings,
+                // since we may be doing this reset at initialisation time.
+                ContinueInternal ();
+
+                state.currentPointer = originalPointer;
+            }
+
+            state.variablesState.SnapshotDefaultGlobals ();
+        }
+
+        public void SwitchFlow(string flowName)
+        {
+            IfAsyncWeCant("switch flow");
+            if (_asyncSaving) throw new System.Exception("Story is already in background saving mode, can't switch flow to "+flowName);
+
+            state.SwitchFlow_Internal(flowName);
+        }
+
+        public void RemoveFlow(string flowName)
+        {
+            state.RemoveFlow_Internal(flowName);
+        }
+
+        public void SwitchToDefaultFlow()
+        {
+            state.SwitchToDefaultFlow_Internal();
+        }
+
+
+        /// <summary>
+        /// Continue the story for one line of content, if possible.
+        /// If you're not sure if there's more content available, for example if you
+        /// want to check whether you're at a choice point or at the end of the story,
+        /// you should call <c>canContinue</c> before calling this function.
+        /// </summary>
+        /// <returns>The line of text content.</returns>
+        public string Continue()
+        {
+            ContinueAsync(0);
+            return currentText;
+        }
+
+
+        /// <summary>
+        /// Check whether more content is available if you were to call <c>Continue()</c> - i.e.
+        /// are we mid story rather than at a choice point or at the end.
+        /// </summary>
+        /// <value><c>true</c> if it's possible to call <c>Continue()</c>.</value>
+        public bool canContinue {
+        	get {
+                return state.canContinue;
+            }
+        }
+
+        /// <summary>
+        /// If ContinueAsync was called (with milliseconds limit > 0) then this property
+        /// will return false if the ink evaluation isn't yet finished, and you need to call 
+        /// it again in order for the Continue to fully complete.
+        /// </summary>
+        public bool asyncContinueComplete {
+            get {
+                return !_asyncContinueActive;
+            }
+        }
+
+        /// <summary>
+        /// An "asnychronous" version of Continue that only partially evaluates the ink,
+        /// with a budget of a certain time limit. It will exit ink evaluation early if
+        /// the evaluation isn't complete within the time limit, with the
+        /// asyncContinueComplete property being false.
+        /// This is useful if ink evaluation takes a long time, and you want to distribute
+        /// it over multiple game frames for smoother animation.
+        /// If you pass a limit of zero, then it will fully evaluate the ink in the same
+        /// way as calling Continue (and in fact, this exactly what Continue does internally).
+        /// </summary>
+        public void ContinueAsync (float millisecsLimitAsync)
+        {
+            if( !_hasValidatedExternals )
+                ValidateExternalBindings ();
+
+            ContinueInternal (millisecsLimitAsync);
+        }
+
+        void ContinueInternal (float millisecsLimitAsync = 0)
+        {
+            if( _profiler != null )
+                _profiler.PreContinue();
+            
+            var isAsyncTimeLimited = millisecsLimitAsync > 0;
+
+            _recursiveContinueCount++;
+
+            // Doing either:
+            //  - full run through non-async (so not active and don't want to be)
+            //  - Starting async run-through
+            if (!_asyncContinueActive) {
+                _asyncContinueActive = isAsyncTimeLimited;
+				
+                if (!canContinue) {
+                    throw new Exception ("Can't continue - should check canContinue before calling Continue");
+                }
+
+                _state.didSafeExit = false;
+                _state.ResetOutput ();
+
+                // It's possible for ink to call game to call ink to call game etc
+                // In this case, we only want to batch observe variable changes
+                // for the outermost call.
+                if (_recursiveContinueCount == 1)
+                    _state.variablesState.batchObservingVariableChanges = true;
+            }
+
+            // Start timing
+            var durationStopwatch = new Stopwatch ();
+            durationStopwatch.Start ();
+
+            bool outputStreamEndsInNewline = false;
+            _sawLookaheadUnsafeFunctionAfterNewline = false;
+            do {
+
+                try {
+                    outputStreamEndsInNewline = ContinueSingleStep ();
+                } catch(StoryException e) {
+                    AddError (e.Message, useEndLineNumber:e.useEndLineNumber);
+                    break;
+                }
+                
+                if (outputStreamEndsInNewline) 
+                    break;
+
+                // Run out of async time?
+                if (_asyncContinueActive && durationStopwatch.ElapsedMilliseconds > millisecsLimitAsync) {
+                    break;
+                }
+
+            } while(canContinue);
+
+            durationStopwatch.Stop ();
+
+            // 4 outcomes:
+            //  - got newline (so finished this line of text)
+            //  - can't continue (e.g. choices or ending)
+            //  - ran out of time during evaluation
+            //  - error
+            //
+            // Successfully finished evaluation in time (or in error)
+            if (outputStreamEndsInNewline || !canContinue) {
+
+                // Need to rewind, due to evaluating further than we should?
+                if( _stateSnapshotAtLastNewline != null ) {
+    				RestoreStateSnapshot ();
+                }
+
+                // Finished a section of content / reached a choice point?
+                if( !canContinue ) {
+					if (state.callStack.canPopThread)
+                        AddError ("Thread available to pop, threads should always be flat by the end of evaluation?");
+
+                    if (state.generatedChoices.Count == 0 && !state.didSafeExit && _temporaryEvaluationContainer == null) {
+                        if (state.callStack.CanPop (PushPopType.Tunnel))
+                            AddError ("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?");
+                        else if (state.callStack.CanPop (PushPopType.Function))
+                            AddError ("unexpectedly reached end of content. Do you need a '~ return'?");
+                        else if (!state.callStack.canPop)
+                            AddError ("ran out of content. Do you need a '-> DONE' or '-> END'?");
+                        else
+                            AddError ("unexpectedly reached end of content for unknown reason. Please debug compiler!");
+                    }
+                }
+
+                state.didSafeExit = false;
+                _sawLookaheadUnsafeFunctionAfterNewline = false;
+
+                if (_recursiveContinueCount == 1)
+                    _state.variablesState.batchObservingVariableChanges = false;
+
+                _asyncContinueActive = false;
+                if(onDidContinue != null) onDidContinue();
+            }
+
+            _recursiveContinueCount--;
+
+            if( _profiler != null )
+                _profiler.PostContinue();
+
+            // Report any errors that occured during evaluation.
+            // This may either have been StoryExceptions that were thrown
+            // and caught during evaluation, or directly added with AddError.
+            if( state.hasError || state.hasWarning ) {
+                if( onError != null ) {
+                    if( state.hasError ) {
+                        foreach(var err in state.currentErrors) {
+                            onError(err, ErrorType.Error);
+                        }
+                    }
+                    if( state.hasWarning ) {
+                        foreach(var err in state.currentWarnings) {
+                            onError(err, ErrorType.Warning);
+                        }
+                    }
+                    ResetErrors();
+                } 
+                
+                // Throw an exception since there's no error handler
+                else {
+                    var sb = new StringBuilder();
+                    sb.Append("Ink had ");
+                    if( state.hasError ) {
+                        sb.Append(state.currentErrors.Count);
+                        sb.Append(state.currentErrors.Count == 1 ? " error" : " errors");
+                        if( state.hasWarning ) sb.Append(" and ");
+                    }
+                    if( state.hasWarning ) {
+                        sb.Append(state.currentWarnings.Count);
+                        sb.Append(state.currentWarnings.Count == 1 ? " warning" : " warnings");
+                    }
+                    sb.Append(". It is strongly suggested that you assign an error handler to story.onError. The first issue was: ");
+                    sb.Append(state.hasError ? state.currentErrors[0] : state.currentWarnings[0]);
+
+                    // If you get this exception, please assign an error handler to your story.
+                    // If you're using Unity, you can do something like this when you create
+                    // your story:
+                    //
+                    // var story = new Ink.Runtime.Story(jsonTxt);
+                    // story.onError = (errorMessage, errorType) => {
+                    //     if( errorType == ErrorType.Warning )
+                    //         Debug.LogWarning(errorMessage);
+                    //     else
+                    //         Debug.LogError(errorMessage);
+                    // };
+                    //
+                    // 
+                    throw new StoryException(sb.ToString());
+                }
+            }
+        }
+
+        bool ContinueSingleStep ()
+        {
+            if (_profiler != null)
+                _profiler.PreStep ();
+
+            // Run main step function (walks through content)
+            Step ();
+
+            if (_profiler != null)
+                _profiler.PostStep ();
+
+            // Run out of content and we have a default invisible choice that we can follow?
+			if (!canContinue && !state.callStack.elementIsEvaluateFromGame) {
+                TryFollowDefaultInvisibleChoice ();
+            }
+
+            if (_profiler != null)
+                _profiler.PreSnapshot ();
+
+            // Don't save/rewind during string evaluation, which is e.g. used for choices
+            if (!state.inStringEvaluation) {
+
+                // We previously found a newline, but were we just double checking that
+                // it wouldn't immediately be removed by glue?
+                if (_stateSnapshotAtLastNewline != null) {
+
+                    // Has proper text or a tag been added? Then we know that the newline
+                    // that was previously added is definitely the end of the line.
+                    var change = CalculateNewlineOutputStateChange (
+                        _stateSnapshotAtLastNewline.currentText,       state.currentText, 
+                        _stateSnapshotAtLastNewline.currentTags.Count, state.currentTags.Count
+                    );
+
+                    // The last time we saw a newline, it was definitely the end of the line, so we
+                    // want to rewind to that point.
+                    if (change == OutputStateChange.ExtendedBeyondNewline || _sawLookaheadUnsafeFunctionAfterNewline) {
+                        RestoreStateSnapshot ();
+
+                        // Hit a newline for sure, we're done
+                        return true;
+                    } 
+
+                    // Newline that previously existed is no longer valid - e.g.
+                    // glue was encounted that caused it to be removed.
+                    else if (change == OutputStateChange.NewlineRemoved) {
+                        DiscardSnapshot();
+                    }
+                }
+
+                // Current content ends in a newline - approaching end of our evaluation
+                if (state.outputStreamEndsInNewline) {
+
+                    // If we can continue evaluation for a bit:
+                    // Create a snapshot in case we need to rewind.
+                    // We're going to continue stepping in case we see glue or some
+                    // non-text content such as choices.
+                    if (canContinue) {
+
+                        // Don't bother to record the state beyond the current newline.
+                        // e.g.:
+                        // Hello world\n            // record state at the end of here
+                        // ~ complexCalculation()   // don't actually need this unless it generates text
+                        if (_stateSnapshotAtLastNewline == null)
+                            StateSnapshot ();
+                    }
+
+                    // Can't continue, so we're about to exit - make sure we
+                    // don't have an old state hanging around.
+                    else {
+                        DiscardSnapshot();
+                    }
+
+                }
+
+            }
+
+            if (_profiler != null)
+                _profiler.PostSnapshot ();
+
+            // outputStreamEndsInNewline = false
+            return false;
+        }
+
+
+
+
+        // Assumption: prevText is the snapshot where we saw a newline, and we're checking whether we're really done
+        //             with that line. Therefore prevText will definitely end in a newline.
+        //
+        // We take tags into account too, so that a tag following a content line:
+        //   Content
+        //   # tag
+        // ... doesn't cause the tag to be wrongly associated with the content above.
+        enum OutputStateChange
+        {
+        	NoChange,
+        	ExtendedBeyondNewline,
+        	NewlineRemoved
+        }
+        OutputStateChange CalculateNewlineOutputStateChange (string prevText, string currText, int prevTagCount, int currTagCount)
+        {
+            // Simple case: nothing's changed, and we still have a newline
+            // at the end of the current content
+            var newlineStillExists = currText.Length >= prevText.Length && currText [prevText.Length - 1] == '\n';
+            if (prevTagCount == currTagCount && prevText.Length == currText.Length 
+                && newlineStillExists)
+                return OutputStateChange.NoChange;
+
+            // Old newline has been removed, it wasn't the end of the line after all
+            if (!newlineStillExists) {
+                return OutputStateChange.NewlineRemoved;
+            }
+
+            // Tag added - definitely the start of a new line
+            if (currTagCount > prevTagCount)
+                return OutputStateChange.ExtendedBeyondNewline;
+
+            // There must be new content - check whether it's just whitespace
+            for (int i = prevText.Length; i < currText.Length; i++) {
+                var c = currText [i];
+                if (c != ' ' && c != '\t') {
+                    return OutputStateChange.ExtendedBeyondNewline;
+                }
+            }
+
+            // There's new text but it's just spaces and tabs, so there's still the potential
+            // for glue to kill the newline.
+            return OutputStateChange.NoChange;
+        }
+
+
+        /// <summary>
+        /// Continue the story until the next choice point or until it runs out of content.
+        /// This is as opposed to the Continue() method which only evaluates one line of
+        /// output at a time.
+        /// </summary>
+        /// <returns>The resulting text evaluated by the ink engine, concatenated together.</returns>
+        public string ContinueMaximally()
+        {
+            IfAsyncWeCant ("ContinueMaximally");
+
+            var sb = new StringBuilder ();
+
+            while (canContinue) {
+                sb.Append (Continue ());
+            }
+
+            return sb.ToString ();
+        }
+
+        public SearchResult ContentAtPath(Path path)
+        {
+            return mainContentContainer.ContentAtPath (path);
+        }
+
+        public Runtime.Container KnotContainerWithName (string name)
+        {
+            INamedContent namedContainer;
+            if (mainContentContainer.namedContent.TryGetValue (name, out namedContainer))
+                return namedContainer as Container;
+            else
+                return null;
+        }
+
+        public Pointer PointerAtPath (Path path)
+        {
+            if (path.length == 0)
+                return Pointer.Null;
+
+            var p = new Pointer ();
+
+            int pathLengthToUse = path.length;
+
+            SearchResult result;
+            if( path.lastComponent.isIndex ) {
+                pathLengthToUse = path.length - 1;
+                result = mainContentContainer.ContentAtPath (path, partialPathLength:pathLengthToUse);
+                p.container = result.container;
+                p.index = path.lastComponent.index;
+            } else {
+                result = mainContentContainer.ContentAtPath (path);
+                p.container = result.container;
+                p.index = -1;
+            }
+
+            if (result.obj == null || result.obj == mainContentContainer && pathLengthToUse > 0)
+                Error ("Failed to find content at path '" + path + "', and no approximation of it was possible.");
+            else if (result.approximate)
+                Warning ("Failed to find content at path '" + path + "', so it was approximated to: '"+result.obj.path+"'.");
+
+            return p;
+        }
+
+        // Maximum snapshot stack:
+        //  - stateSnapshotDuringSave -- not retained, but returned to game code
+        //  - _stateSnapshotAtLastNewline (has older patch)
+        //  - _state (current, being patched)
+
+        void StateSnapshot()
+        {
+            _stateSnapshotAtLastNewline = _state;
+            _state = _state.CopyAndStartPatching();
+        }
+
+        void RestoreStateSnapshot()
+        {
+            // Patched state had temporarily hijacked our
+            // VariablesState and set its own callstack on it,
+            // so we need to restore that.
+            // If we're in the middle of saving, we may also
+            // need to give the VariablesState the old patch.
+            _stateSnapshotAtLastNewline.RestoreAfterPatch();
+
+            _state = _stateSnapshotAtLastNewline;
+            _stateSnapshotAtLastNewline = null;
+
+            // If save completed while the above snapshot was
+            // active, we need to apply any changes made since
+            // the save was started but before the snapshot was made.
+            if( !_asyncSaving ) {
+                _state.ApplyAnyPatch();
+            }
+        }
+
+        void DiscardSnapshot()
+        {
+            // Normally we want to integrate the patch
+            // into the main global/counts dictionaries.
+            // However, if we're in the middle of async
+            // saving, we simply stay in a "patching" state,
+            // albeit with the newer cloned patch.
+            if( !_asyncSaving )
+                _state.ApplyAnyPatch();
+
+            // No longer need the snapshot.
+            _stateSnapshotAtLastNewline = null;
+        }
+
+        /// <summary>
+        /// Advanced usage!
+        /// If you have a large story, and saving state to JSON takes too long for your
+        /// framerate, you can temporarily freeze a copy of the state for saving on 
+        /// a separate thread. Internally, the engine maintains a "diff patch".
+        /// When you've finished saving your state, call BackgroundSaveComplete()
+        /// and that diff patch will be applied, allowing the story to continue
+        /// in its usual mode.
+        /// </summary>
+        /// <returns>The state for background thread save.</returns>
+        public StoryState CopyStateForBackgroundThreadSave()
+        {
+            IfAsyncWeCant("start saving on a background thread");
+            if (_asyncSaving) throw new System.Exception("Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!");
+            var stateToSave = _state;
+            _state = _state.CopyAndStartPatching();
+            _asyncSaving = true;
+            return stateToSave;
+        }
+
+        /// <summary>
+        /// See CopyStateForBackgroundThreadSave. This method releases the
+        /// "frozen" save state, applying its patch that it was using internally.
+        /// </summary>
+        public void BackgroundSaveComplete()
+        {
+            // CopyStateForBackgroundThreadSave must be called outside
+            // of any async ink evaluation, since otherwise you'd be saving
+            // during an intermediate state.
+            // However, it's possible to *complete* the save in the middle of
+            // a glue-lookahead when there's a state stored in _stateSnapshotAtLastNewline.
+            // This state will have its own patch that is newer than the save patch.
+            // We hold off on the final apply until the glue-lookahead is finished.
+            // In that case, the apply is always done, it's just that it may
+            // apply the looked-ahead changes OR it may simply apply the changes
+            // made during the save process to the old _stateSnapshotAtLastNewline state.
+            if ( _stateSnapshotAtLastNewline == null ) {
+                _state.ApplyAnyPatch();
+            }
+
+            _asyncSaving = false;
+        }
+
+
+
+        void Step ()
+        {
+            bool shouldAddToStream = true;
+
+            // Get current content
+            var pointer = state.currentPointer;
+            if (pointer.isNull) {
+                return;
+            }
+
+            // Step directly to the first element of content in a container (if necessary)
+            Container containerToEnter = pointer.Resolve () as Container;
+            while(containerToEnter) {
+
+                // Mark container as being entered
+                VisitContainer (containerToEnter, atStart:true);
+
+                // No content? the most we can do is step past it
+                if (containerToEnter.content.Count == 0)
+                    break;
+
+
+                pointer = Pointer.StartOf (containerToEnter);
+                containerToEnter = pointer.Resolve() as Container;
+            }
+            state.currentPointer = pointer;
+
+			if( _profiler != null ) {
+				_profiler.Step(state.callStack);
+			}
+
+            // Is the current content object:
+            //  - Normal content
+            //  - Or a logic/flow statement - if so, do it
+            // Stop flow if we hit a stack pop when we're unable to pop (e.g. return/done statement in knot
+            // that was diverted to rather than called as a function)
+            var currentContentObj = pointer.Resolve ();
+            bool isLogicOrFlowControl = PerformLogicAndFlowControl (currentContentObj);
+
+            // Has flow been forced to end by flow control above?
+            if (state.currentPointer.isNull) {
+                return;
+            }
+
+            if (isLogicOrFlowControl) {
+                shouldAddToStream = false;
+            }
+
+            // Choice with condition?
+            var choicePoint = currentContentObj as ChoicePoint;
+            if (choicePoint) {
+                var choice = ProcessChoice (choicePoint);
+                if (choice) {
+                    state.generatedChoices.Add (choice);
+                }
+
+                currentContentObj = null;
+                shouldAddToStream = false;
+            }
+
+            // If the container has no content, then it will be
+            // the "content" itself, but we skip over it.
+            if (currentContentObj is Container) {
+                shouldAddToStream = false;
+            }
+
+            // Content to add to evaluation stack or the output stream
+            if (shouldAddToStream) {
+
+                // If we're pushing a variable pointer onto the evaluation stack, ensure that it's specific
+                // to our current (possibly temporary) context index. And make a copy of the pointer
+                // so that we're not editing the original runtime object.
+                var varPointer = currentContentObj as VariablePointerValue;
+                if (varPointer && varPointer.contextIndex == -1) {
+
+                    // Create new object so we're not overwriting the story's own data
+                    var contextIdx = state.callStack.ContextForVariableNamed(varPointer.variableName);
+                    currentContentObj = new VariablePointerValue (varPointer.variableName, contextIdx);
+                }
+
+                // Expression evaluation content
+                if (state.inExpressionEvaluation) {
+                    state.PushEvaluationStack (currentContentObj);
+                }
+                // Output stream content (i.e. not expression evaluation)
+                else {
+                    state.PushToOutputStream (currentContentObj);
+                }
+            }
+
+            // Increment the content pointer, following diverts if necessary
+            NextContent ();
+
+            // Starting a thread should be done after the increment to the content pointer,
+            // so that when returning from the thread, it returns to the content after this instruction.
+            var controlCmd = currentContentObj as ControlCommand;
+            if (controlCmd && controlCmd.commandType == ControlCommand.CommandType.StartThread) {
+                state.callStack.PushThread ();
+            }
+        }
+
+        // Mark a container as having been visited
+        void VisitContainer(Container container, bool atStart)
+        {
+            if ( !container.countingAtStartOnly || atStart ) {
+                if( container.visitsShouldBeCounted )
+                    state.IncrementVisitCountForContainer (container);
+
+                if (container.turnIndexShouldBeCounted)
+                    state.RecordTurnIndexVisitToContainer (container);
+            }
+        }
+
+        List<Container> _prevContainers = new List<Container>();
+        void VisitChangedContainersDueToDivert()
+        {
+            var previousPointer = state.previousPointer;
+            var pointer = state.currentPointer;
+
+            // Unless we're pointing *directly* at a piece of content, we don't do
+            // counting here. Otherwise, the main stepping function will do the counting.
+            if (pointer.isNull || pointer.index == -1)
+                return;
+            
+            // First, find the previously open set of containers
+			_prevContainers.Clear();
+            if (!previousPointer.isNull) {
+                Container prevAncestor = previousPointer.Resolve() as Container ?? previousPointer.container as Container;
+                while (prevAncestor) {
+					_prevContainers.Add (prevAncestor);
+                    prevAncestor = prevAncestor.parent as Container;
+                }
+            }
+
+            // If the new object is a container itself, it will be visited automatically at the next actual
+            // content step. However, we need to walk up the new ancestry to see if there are more new containers
+            Runtime.Object currentChildOfContainer = pointer.Resolve();
+
+            // Invalid pointer? May happen if attemptingto 
+            if (currentChildOfContainer == null) return;
+
+            Container currentContainerAncestor = currentChildOfContainer.parent as Container;
+
+            bool allChildrenEnteredAtStart = true;
+            while (currentContainerAncestor && (!_prevContainers.Contains(currentContainerAncestor) || currentContainerAncestor.countingAtStartOnly)) {
+
+                // Check whether this ancestor container is being entered at the start,
+                // by checking whether the child object is the first.
+                bool enteringAtStart = currentContainerAncestor.content.Count > 0 
+                    && currentChildOfContainer == currentContainerAncestor.content [0]
+                    && allChildrenEnteredAtStart;
+
+                // Don't count it as entering at start if we're entering random somewhere within
+                // a container B that happens to be nested at index 0 of container A. It only counts
+                // if we're diverting directly to the first leaf node.
+                if (!enteringAtStart)
+                    allChildrenEnteredAtStart = false;
+
+                // Mark a visit to this container
+                VisitContainer (currentContainerAncestor, enteringAtStart);
+
+                currentChildOfContainer = currentContainerAncestor;
+                currentContainerAncestor = currentContainerAncestor.parent as Container;
+            }
+        }
+            
+        Choice ProcessChoice(ChoicePoint choicePoint)
+        {
+            bool showChoice = true;
+
+            // Don't create choice if choice point doesn't pass conditional
+            if (choicePoint.hasCondition) {
+                var conditionValue = state.PopEvaluationStack ();
+                if (!IsTruthy (conditionValue)) {
+                    showChoice = false;
+                }
+            }
+
+            string startText = "";
+            string choiceOnlyText = "";
+
+            if (choicePoint.hasChoiceOnlyContent) {
+                var choiceOnlyStrVal = state.PopEvaluationStack () as StringValue;
+                choiceOnlyText = choiceOnlyStrVal.value;
+            }
+
+            if (choicePoint.hasStartContent) {
+                var startStrVal = state.PopEvaluationStack () as StringValue;
+                startText = startStrVal.value;
+            }
+
+            // Don't create choice if player has already read this content
+            if (choicePoint.onceOnly) {
+                var visitCount = state.VisitCountForContainer (choicePoint.choiceTarget);
+                if (visitCount > 0) {
+                    showChoice = false;
+                }
+            }
+
+            // We go through the full process of creating the choice above so
+            // that we consume the content for it, since otherwise it'll
+            // be shown on the output stream.
+            if (!showChoice) {
+                return null;
+            }
+
+            var choice = new Choice ();
+            choice.targetPath = choicePoint.pathOnChoice;
+            choice.sourcePath = choicePoint.path.ToString ();
+            choice.isInvisibleDefault = choicePoint.isInvisibleDefault;
+
+            // We need to capture the state of the callstack at the point where
+            // the choice was generated, since after the generation of this choice
+            // we may go on to pop out from a tunnel (possible if the choice was
+            // wrapped in a conditional), or we may pop out from a thread,
+            // at which point that thread is discarded.
+            // Fork clones the thread, gives it a new ID, but without affecting
+            // the thread stack itself.
+            choice.threadAtGeneration = state.callStack.ForkThread();
+
+            // Set final text for the choice
+            choice.text = (startText + choiceOnlyText).Trim(' ', '\t');
+
+            return choice;
+        }
+
+        // Does the expression result represented by this object evaluate to true?
+        // e.g. is it a Number that's not equal to 1?
+        bool IsTruthy(Runtime.Object obj)
+        {
+            bool truthy = false;
+            if (obj is Value) {
+                var val = (Value)obj;
+
+                if (val is DivertTargetValue) {
+                    var divTarget = (DivertTargetValue)val;
+                    Error ("Shouldn't use a divert target (to " + divTarget.targetPath + ") as a conditional value. Did you intend a function call 'likeThis()' or a read count check 'likeThis'? (no arrows)");
+                    return false;
+                }
+
+                return val.isTruthy;
+            }
+            return truthy;
+        }
+
+        /// <summary>
+        /// Checks whether contentObj is a control or flow object rather than a piece of content, 
+        /// and performs the required command if necessary.
+        /// </summary>
+        /// <returns><c>true</c> if object was logic or flow control, <c>false</c> if it's normal content.</returns>
+        /// <param name="contentObj">Content object.</param>
+        bool PerformLogicAndFlowControl(Runtime.Object contentObj)
+        {
+            if( contentObj == null ) {
+                return false;
+            }
+
+            // Divert
+            if (contentObj is Divert) {
+                
+                Divert currentDivert = (Divert)contentObj;
+
+                if (currentDivert.isConditional) {
+                    var conditionValue = state.PopEvaluationStack ();
+
+                    // False conditional? Cancel divert
+                    if (!IsTruthy (conditionValue))
+                        return true;
+                }
+
+                if (currentDivert.hasVariableTarget) {
+                    var varName = currentDivert.variableDivertName;
+
+                    var varContents = state.variablesState.GetVariableWithName (varName);
+
+                    if (varContents == null) {
+                        Error ("Tried to divert using a target from a variable that could not be found (" + varName + ")");
+                    }
+                    else if (!(varContents is DivertTargetValue)) {
+
+                        var intContent = varContents as IntValue;
+
+                        string errorMessage = "Tried to divert to a target from a variable, but the variable (" + varName + ") didn't contain a divert target, it ";
+                        if (intContent && intContent.value == 0) {
+                            errorMessage += "was empty/null (the value 0).";
+                        } else {
+                            errorMessage += "contained '" + varContents + "'.";
+                        }
+
+                        Error (errorMessage);
+                    }
+
+                    var target = (DivertTargetValue)varContents;
+                    state.divertedPointer = PointerAtPath(target.targetPath);
+
+                } else if (currentDivert.isExternal) {
+                    CallExternalFunction (currentDivert.targetPathString, currentDivert.externalArgs);
+                    return true;
+                } else {
+                    state.divertedPointer = currentDivert.targetPointer;
+                }
+
+                if (currentDivert.pushesToStack) {
+                    state.callStack.Push (
+                        currentDivert.stackPushType, 
+                        outputStreamLengthWithPushed:state.outputStream.Count
+                    );
+                }
+
+                if (state.divertedPointer.isNull && !currentDivert.isExternal) {
+
+                    // Human readable name available - runtime divert is part of a hard-written divert that to missing content
+                    if (currentDivert && currentDivert.debugMetadata.sourceName != null) {
+                        Error ("Divert target doesn't exist: " + currentDivert.debugMetadata.sourceName);
+                    } else {
+                        Error ("Divert resolution failed: " + currentDivert);
+                    }
+                }
+
+                return true;
+            } 
+
+            // Start/end an expression evaluation? Or print out the result?
+            else if( contentObj is ControlCommand ) {
+                var evalCommand = (ControlCommand) contentObj;
+
+                switch (evalCommand.commandType) {
+
+                case ControlCommand.CommandType.EvalStart:
+                    Assert (state.inExpressionEvaluation == false, "Already in expression evaluation?");
+                    state.inExpressionEvaluation = true;
+                    break;
+
+                case ControlCommand.CommandType.EvalEnd:
+                    Assert (state.inExpressionEvaluation == true, "Not in expression evaluation mode");
+                    state.inExpressionEvaluation = false;
+                    break;
+
+                case ControlCommand.CommandType.EvalOutput:
+
+                    // If the expression turned out to be empty, there may not be anything on the stack
+                    if (state.evaluationStack.Count > 0) {
+                        
+                        var output = state.PopEvaluationStack ();
+
+                        // Functions may evaluate to Void, in which case we skip output
+                        if (!(output is Void)) {
+                            // TODO: Should we really always blanket convert to string?
+                            // It would be okay to have numbers in the output stream the
+                            // only problem is when exporting text for viewing, it skips over numbers etc.
+                            var text = new StringValue (output.ToString ());
+
+                            state.PushToOutputStream (text);
+                        }
+
+                    }
+                    break;
+
+                case ControlCommand.CommandType.NoOp:
+                    break;
+
+                case ControlCommand.CommandType.Duplicate:
+                    state.PushEvaluationStack (state.PeekEvaluationStack ());
+                    break;
+
+                case ControlCommand.CommandType.PopEvaluatedValue:
+                    state.PopEvaluationStack ();
+                    break;
+
+                case ControlCommand.CommandType.PopFunction:
+                case ControlCommand.CommandType.PopTunnel:
+
+                    var popType = evalCommand.commandType == ControlCommand.CommandType.PopFunction ?
+                        PushPopType.Function : PushPopType.Tunnel;
+
+                    // Tunnel onwards is allowed to specify an optional override
+                    // divert to go to immediately after returning: ->-> target
+                    DivertTargetValue overrideTunnelReturnTarget = null;
+                    if (popType == PushPopType.Tunnel) {
+                        var popped = state.PopEvaluationStack ();
+                        overrideTunnelReturnTarget = popped as DivertTargetValue;
+                        if (overrideTunnelReturnTarget == null) {
+                            Assert (popped is Void, "Expected void if ->-> doesn't override target");
+                        }
+                    }
+
+                    if (state.TryExitFunctionEvaluationFromGame ()) {
+                        break;
+                    }
+                    else if (state.callStack.currentElement.type != popType || !state.callStack.canPop) {
+
+                        var names = new Dictionary<PushPopType, string> ();
+                        names [PushPopType.Function] = "function return statement (~ return)";
+                        names [PushPopType.Tunnel] = "tunnel onwards statement (->->)";
+
+                        string expected = names [state.callStack.currentElement.type];
+                        if (!state.callStack.canPop) {
+                            expected = "end of flow (-> END or choice)";
+                        }
+
+                        var errorMsg = string.Format ("Found {0}, when expected {1}", names [popType], expected);
+
+                        Error (errorMsg);
+                    } 
+
+                    else {
+                        state.PopCallstack ();
+
+                        // Does tunnel onwards override by diverting to a new ->-> target?
+                        if( overrideTunnelReturnTarget )
+                            state.divertedPointer = PointerAtPath (overrideTunnelReturnTarget.targetPath);
+                    }
+
+                    break;
+
+                case ControlCommand.CommandType.BeginString:
+                    state.PushToOutputStream (evalCommand);
+
+                    Assert (state.inExpressionEvaluation == true, "Expected to be in an expression when evaluating a string");
+                    state.inExpressionEvaluation = false;
+                    break;
+
+                case ControlCommand.CommandType.EndString:
+                    
+                    // Since we're iterating backward through the content,
+                    // build a stack so that when we build the string,
+                    // it's in the right order
+                    var contentStackForString = new Stack<Runtime.Object> ();
+
+                    int outputCountConsumed = 0;
+                    for (int i = state.outputStream.Count - 1; i >= 0; --i) {
+                        var obj = state.outputStream [i];
+
+                        outputCountConsumed++;
+
+                        var command = obj as ControlCommand;
+                        if (command != null && command.commandType == ControlCommand.CommandType.BeginString) {
+                            break;
+                        }
+
+                        if( obj is StringValue )
+                            contentStackForString.Push (obj);
+                    }
+
+                    // Consume the content that was produced for this string
+                    state.PopFromOutputStream (outputCountConsumed);
+
+                    // Build string out of the content we collected
+                    var sb = new StringBuilder ();
+                    foreach (var c in contentStackForString) {
+                        sb.Append (c.ToString ());
+                    }
+
+                    // Return to expression evaluation (from content mode)
+                    state.inExpressionEvaluation = true;
+                    state.PushEvaluationStack (new StringValue (sb.ToString ()));
+                    break;
+
+                case ControlCommand.CommandType.ChoiceCount:
+					var choiceCount = state.generatedChoices.Count;
+                    state.PushEvaluationStack (new Runtime.IntValue (choiceCount));
+                    break;
+
+                case ControlCommand.CommandType.Turns:
+                    state.PushEvaluationStack (new IntValue (state.currentTurnIndex+1));
+                    break;
+
+                case ControlCommand.CommandType.TurnsSince:
+                case ControlCommand.CommandType.ReadCount:
+                    var target = state.PopEvaluationStack();
+                    if( !(target is DivertTargetValue) ) {
+                        string extraNote = "";
+                        if( target is IntValue )
+                            extraNote = ". Did you accidentally pass a read count ('knot_name') instead of a target ('-> knot_name')?";
+                        Error("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw "+target+extraNote);
+                        break;
+                    }
+                        
+                    var divertTarget = target as DivertTargetValue;
+                    var container = ContentAtPath (divertTarget.targetPath).correctObj as Container;
+
+                    int eitherCount;
+                    if (container != null) {
+                        if (evalCommand.commandType == ControlCommand.CommandType.TurnsSince)
+                            eitherCount = state.TurnsSinceForContainer (container);
+                        else
+                            eitherCount = state.VisitCountForContainer (container);
+                    } else {
+                        if (evalCommand.commandType == ControlCommand.CommandType.TurnsSince)
+                            eitherCount = -1; // turn count, default to never/unknown
+                        else
+                            eitherCount = 0; // visit count, assume 0 to default to allowing entry
+
+                        Warning ("Failed to find container for " + evalCommand.ToString () + " lookup at " + divertTarget.targetPath.ToString ());
+                    }
+                    
+                    state.PushEvaluationStack (new IntValue (eitherCount));
+                    break;
+                    
+
+                case ControlCommand.CommandType.Random: {
+                        var maxInt = state.PopEvaluationStack () as IntValue;
+                        var minInt = state.PopEvaluationStack () as IntValue;
+
+                        if (minInt == null)
+                            Error ("Invalid value for minimum parameter of RANDOM(min, max)");
+
+                        if (maxInt == null)
+                            Error ("Invalid value for maximum parameter of RANDOM(min, max)");
+
+                        // +1 because it's inclusive of min and max, for e.g. RANDOM(1,6) for a dice roll.
+                        int randomRange;
+                        try {
+                            randomRange = checked(maxInt.value - minInt.value + 1);
+                        } catch (System.OverflowException) {
+                            randomRange = int.MaxValue;
+                            Error("RANDOM was called with a range that exceeds the size that ink numbers can use.");
+                        }
+                        if (randomRange <= 0)
+                            Error ("RANDOM was called with minimum as " + minInt.value + " and maximum as " + maxInt.value + ". The maximum must be larger");
+
+                        var resultSeed = state.storySeed + state.previousRandom;
+                        var random = new Random (resultSeed);
+
+                        var nextRandom = random.Next ();
+                        var chosenValue = (nextRandom % randomRange) + minInt.value;
+                        state.PushEvaluationStack (new IntValue (chosenValue));
+
+                        // Next random number (rather than keeping the Random object around)
+                        state.previousRandom = nextRandom;
+                        break;
+                    }
+
+                case ControlCommand.CommandType.SeedRandom:
+                    var seed = state.PopEvaluationStack () as IntValue;
+                    if (seed == null)
+                        Error ("Invalid value passed to SEED_RANDOM");
+
+                    // Story seed affects both RANDOM and shuffle behaviour
+                    state.storySeed = seed.value;
+                    state.previousRandom = 0;
+
+                    // SEED_RANDOM returns nothing.
+                    state.PushEvaluationStack (new Runtime.Void ());
+                    break;
+
+                case ControlCommand.CommandType.VisitIndex:
+                    var count = state.VisitCountForContainer(state.currentPointer.container) - 1; // index not count
+                    state.PushEvaluationStack (new IntValue (count));
+                    break;
+
+                case ControlCommand.CommandType.SequenceShuffleIndex:
+                    var shuffleIndex = NextSequenceShuffleIndex ();
+                    state.PushEvaluationStack (new IntValue (shuffleIndex));
+                    break;
+
+                case ControlCommand.CommandType.StartThread:
+                    // Handled in main step function
+                    break;
+
+                case ControlCommand.CommandType.Done:
+                    
+                    // We may exist in the context of the initial
+                    // act of creating the thread, or in the context of
+                    // evaluating the content.
+                    if (state.callStack.canPopThread) {
+                        state.callStack.PopThread ();
+                    } 
+
+                    // In normal flow - allow safe exit without warning
+                    else {
+                        state.didSafeExit = true;
+
+                        // Stop flow in current thread
+                        state.currentPointer = Pointer.Null;
+                    }
+
+                    break;
+                
+                // Force flow to end completely
+                case ControlCommand.CommandType.End:
+                    state.ForceEnd ();
+                    break;
+
+                case ControlCommand.CommandType.ListFromInt:
+                    var intVal = state.PopEvaluationStack () as IntValue;
+                    var listNameVal = state.PopEvaluationStack () as StringValue;
+
+					if (intVal == null) { 
+						throw new StoryException ("Passed non-integer when creating a list element from a numerical value."); 
+					}
+
+                    ListValue generatedListValue = null;
+
+                    ListDefinition foundListDef;
+                    if (listDefinitions.TryListGetDefinition (listNameVal.value, out foundListDef)) {
+                        InkListItem foundItem;
+                        if (foundListDef.TryGetItemWithValue (intVal.value, out foundItem)) {
+                            generatedListValue = new ListValue (foundItem, intVal.value);
+                        }
+                    } else {
+                        throw new StoryException ("Failed to find LIST called " + listNameVal.value);
+                    }
+
+                    if (generatedListValue == null)
+                        generatedListValue = new ListValue ();
+
+                    state.PushEvaluationStack (generatedListValue);
+                    break;
+
+                case ControlCommand.CommandType.ListRange: {
+                        var max = state.PopEvaluationStack () as Value;
+                        var min = state.PopEvaluationStack () as Value;
+
+                        var targetList = state.PopEvaluationStack () as ListValue;
+
+                        if (targetList == null || min == null || max == null)
+                            throw new StoryException ("Expected list, minimum and maximum for LIST_RANGE");
+
+                        var result = targetList.value.ListWithSubRange(min.valueObject, max.valueObject);
+
+                        state.PushEvaluationStack (new ListValue(result));
+                        break;
+                    }
+
+                case ControlCommand.CommandType.ListRandom: {
+
+                        var listVal = state.PopEvaluationStack () as ListValue;
+                        if (listVal == null)
+                            throw new StoryException ("Expected list for LIST_RANDOM");
+                        
+                        var list = listVal.value;
+
+                        InkList newList = null;
+
+                        // List was empty: return empty list
+                        if (list.Count == 0) {
+                            newList = new InkList ();
+                        } 
+
+                        // Non-empty source list
+                        else {
+                            // Generate a random index for the element to take
+                            var resultSeed = state.storySeed + state.previousRandom;
+                            var random = new Random (resultSeed);
+
+                            var nextRandom = random.Next ();
+                            var listItemIndex = nextRandom % list.Count;
+
+                            // Iterate through to get the random element
+                            var listEnumerator = list.GetEnumerator ();
+                            for (int i = 0; i <= listItemIndex; i++) {
+                                listEnumerator.MoveNext ();
+                            }
+                            var randomItem = listEnumerator.Current;
+
+                            // Origin list is simply the origin of the one element
+                            newList = new InkList (randomItem.Key.originName, this);
+                            newList.Add (randomItem.Key, randomItem.Value);
+
+                            state.previousRandom = nextRandom;
+                        }
+
+                        state.PushEvaluationStack (new ListValue(newList));
+                        break;
+                    }
+
+                default:
+                    Error ("unhandled ControlCommand: " + evalCommand);
+                    break;
+                }
+
+                return true;
+            }
+
+            // Variable assignment
+            else if( contentObj is VariableAssignment ) {
+                var varAss = (VariableAssignment) contentObj;
+                var assignedVal = state.PopEvaluationStack();
+
+                // When in temporary evaluation, don't create new variables purely within
+                // the temporary context, but attempt to create them globally
+                //var prioritiseHigherInCallStack = _temporaryEvaluationContainer != null;
+
+                state.variablesState.Assign (varAss, assignedVal);
+
+                return true;
+            }
+
+            // Variable reference
+            else if( contentObj is VariableReference ) {
+                var varRef = (VariableReference)contentObj;
+                Runtime.Object foundValue = null;
+
+
+                // Explicit read count value
+                if (varRef.pathForCount != null) {
+
+                    var container = varRef.containerForCount;
+                    int count = state.VisitCountForContainer (container);
+                    foundValue = new IntValue (count);
+                }
+
+                // Normal variable reference
+                else {
+
+                    foundValue = state.variablesState.GetVariableWithName (varRef.name);
+
+                    if (foundValue == null) {
+                        Warning ("Variable not found: '" + varRef.name + "'. Using default value of 0 (false). This can happen with temporary variables if the declaration hasn't yet been hit. Globals are always given a default value on load if a value doesn't exist in the save state.");
+                        foundValue = new IntValue (0);
+                    }
+                }
+
+                state.PushEvaluationStack (foundValue);
+
+                return true;
+            }
+
+            // Native function call
+            else if (contentObj is NativeFunctionCall) {
+                var func = (NativeFunctionCall)contentObj;
+                var funcParams = state.PopEvaluationStack (func.numberOfParameters);
+                var result = func.Call (funcParams);
+                state.PushEvaluationStack (result);
+                return true;
+            } 
+
+            // No control content, must be ordinary content
+            return false;
+        }
+
+        /// <summary>
+        /// Change the current position of the story to the given path. From here you can 
+        /// call Continue() to evaluate the next line.
+        /// 
+        /// The path string is a dot-separated path as used internally by the engine.
+        /// These examples should work:
+        /// 
+        ///    myKnot
+        ///    myKnot.myStitch
+        /// 
+        /// Note however that this won't necessarily work:
+        /// 
+        ///    myKnot.myStitch.myLabelledChoice
+        /// 
+        /// ...because of the way that content is nested within a weave structure.
+        /// 
+        /// By default this will reset the callstack beforehand, which means that any
+        /// tunnels, threads or functions you were in at the time of calling will be
+        /// discarded. This is different from the behaviour of ChooseChoiceIndex, which
+        /// will always keep the callstack, since the choices are known to come from the
+        /// correct state, and known their source thread.
+        /// 
+        /// You have the option of passing false to the resetCallstack parameter if you
+        /// don't want this behaviour, and will leave any active threads, tunnels or
+        /// function calls in-tact.
+        /// 
+        /// This is potentially dangerous! If you're in the middle of a tunnel,
+        /// it'll redirect only the inner-most tunnel, meaning that when you tunnel-return
+        /// using '->->', it'll return to where you were before. This may be what you
+        /// want though. However, if you're in the middle of a function, ChoosePathString
+        /// will throw an exception.
+        /// 
+        /// </summary>
+        /// <param name="path">A dot-separted path string, as specified above.</param>
+        /// <param name="resetCallstack">Whether to reset the callstack first (see summary description).</param>
+        /// <param name="arguments">Optional set of arguments to pass, if path is to a knot that takes them.</param>
+        public void ChoosePathString (string path, bool resetCallstack = true, params object [] arguments)
+        {
+            IfAsyncWeCant ("call ChoosePathString right now");
+            if(onChoosePathString != null) onChoosePathString(path, arguments);
+            if (resetCallstack) {
+                ResetCallstack ();
+            } else {
+                // ChoosePathString is potentially dangerous since you can call it when the stack is
+                // pretty much in any state. Let's catch one of the worst offenders.
+                if (state.callStack.currentElement.type == PushPopType.Function) {
+                    string funcDetail = "";
+                    var container = state.callStack.currentElement.currentPointer.container;
+                    if (container != null) {
+                        funcDetail = "("+container.path.ToString ()+") ";
+                    }
+                    throw new System.Exception ("Story was running a function "+funcDetail+"when you called ChoosePathString("+path+") - this is almost certainly not not what you want! Full stack trace: \n"+state.callStack.callStackTrace);
+                }
+            }
+
+            state.PassArgumentsToEvaluationStack (arguments);
+            ChoosePath (new Path (path));
+        }
+
+        void IfAsyncWeCant (string activityStr)
+        {
+            if (_asyncContinueActive)
+                throw new System.Exception ("Can't " + activityStr + ". Story is in the middle of a ContinueAsync(). Make more ContinueAsync() calls or a single Continue() call beforehand.");
+        }
+            
+        public void ChoosePath(Path p, bool incrementingTurnIndex = true)
+        {
+            state.SetChosenPath (p, incrementingTurnIndex);
+
+            // Take a note of newly visited containers for read counts etc
+            VisitChangedContainersDueToDivert ();
+        }
+
+        /// <summary>
+        /// Chooses the Choice from the currentChoices list with the given
+        /// index. Internally, this sets the current content path to that
+        /// pointed to by the Choice, ready to continue story evaluation.
+        /// </summary>
+        public void ChooseChoiceIndex(int choiceIdx)
+        {
+            var choices = currentChoices;
+            Assert (choiceIdx >= 0 && choiceIdx < choices.Count, "choice out of range");
+
+            // Replace callstack with the one from the thread at the choosing point, 
+            // so that we can jump into the right place in the flow.
+            // This is important in case the flow was forked by a new thread, which
+            // can create multiple leading edges for the story, each of
+            // which has its own context.
+            var choiceToChoose = choices [choiceIdx];
+            if(onMakeChoice != null) onMakeChoice(choiceToChoose);
+            state.callStack.currentThread = choiceToChoose.threadAtGeneration;
+
+            ChoosePath (choiceToChoose.targetPath);
+        }
+
+        /// <summary>
+        /// Checks if a function exists.
+        /// </summary>
+        /// <returns>True if the function exists, else false.</returns>
+        /// <param name="functionName">The name of the function as declared in ink.</param>
+        public bool HasFunction (string functionName)
+        {
+            try {
+                return KnotContainerWithName (functionName) != null;
+            } catch {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Evaluates a function defined in ink.
+        /// </summary>
+        /// <returns>The return value as returned from the ink function with `~ return myValue`, or null if nothing is returned.</returns>
+        /// <param name="functionName">The name of the function as declared in ink.</param>
+        /// <param name="arguments">The arguments that the ink function takes, if any. Note that we don't (can't) do any validation on the number of arguments right now, so make sure you get it right!</param>
+        public object EvaluateFunction (string functionName, params object [] arguments)
+        {
+            string _;
+            return EvaluateFunction (functionName, out _, arguments);
+        }
+
+        /// <summary>
+        /// Evaluates a function defined in ink, and gathers the possibly multi-line text as generated by the function.
+        /// This text output is any text written as normal content within the function, as opposed to the return value, as returned with `~ return`.
+        /// </summary>
+        /// <returns>The return value as returned from the ink function with `~ return myValue`, or null if nothing is returned.</returns>
+        /// <param name="functionName">The name of the function as declared in ink.</param>
+        /// <param name="textOutput">The text content produced by the function via normal ink, if any.</param>
+        /// <param name="arguments">The arguments that the ink function takes, if any. Note that we don't (can't) do any validation on the number of arguments right now, so make sure you get it right!</param>
+        public object EvaluateFunction (string functionName, out string textOutput, params object [] arguments)
+        {
+            if(onEvaluateFunction != null) onEvaluateFunction(functionName, arguments);
+            IfAsyncWeCant ("evaluate a function");
+
+			if(functionName == null) {
+				throw new System.Exception ("Function is null");
+			} else if(functionName == string.Empty || functionName.Trim() == string.Empty) {
+				throw new System.Exception ("Function is empty or white space.");
+			}
+
+            // Get the content that we need to run
+            var funcContainer = KnotContainerWithName (functionName);
+            if( funcContainer == null )
+                throw new System.Exception ("Function doesn't exist: '" + functionName + "'");
+
+            // Snapshot the output stream
+            var outputStreamBefore = new List<Runtime.Object>(state.outputStream);
+            _state.ResetOutput ();
+
+            // State will temporarily replace the callstack in order to evaluate
+            state.StartFunctionEvaluationFromGame (funcContainer, arguments);
+
+            // Evaluate the function, and collect the string output
+            var stringOutput = new StringBuilder ();
+            while (canContinue) {
+                stringOutput.Append (Continue ());
+            }
+            textOutput = stringOutput.ToString ();
+
+            // Restore the output stream in case this was called
+            // during main story evaluation.
+            _state.ResetOutput (outputStreamBefore);
+
+            // Finish evaluation, and see whether anything was produced
+            var result = state.CompleteFunctionEvaluationFromGame ();
+            if(onCompleteEvaluateFunction != null) onCompleteEvaluateFunction(functionName, arguments, textOutput, result);
+            return result;
+        }
+
+        // Evaluate a "hot compiled" piece of ink content, as used by the REPL-like
+        // CommandLinePlayer.
+        public Runtime.Object EvaluateExpression(Runtime.Container exprContainer)
+        {
+            int startCallStackHeight = state.callStack.elements.Count;
+
+            state.callStack.Push (PushPopType.Tunnel);
+
+            _temporaryEvaluationContainer = exprContainer;
+
+            state.GoToStart ();
+
+            int evalStackHeight = state.evaluationStack.Count;
+
+            Continue ();
+
+            _temporaryEvaluationContainer = null;
+
+            // Should have fallen off the end of the Container, which should
+            // have auto-popped, but just in case we didn't for some reason,
+            // manually pop to restore the state (including currentPath).
+            if (state.callStack.elements.Count > startCallStackHeight) {
+                state.PopCallstack ();
+            }
+
+            int endStackHeight = state.evaluationStack.Count;
+            if (endStackHeight > evalStackHeight) {
+                return state.PopEvaluationStack ();
+            } else {
+                return null;
+            }
+
+        }
+
+        /// <summary>
+        /// An ink file can provide a fallback functions for when when an EXTERNAL has been left
+        /// unbound by the client, and the fallback function will be called instead. Useful when
+        /// testing a story in playmode, when it's not possible to write a client-side C# external
+        /// function, but you don't want it to fail to run.
+        /// </summary>
+        public bool allowExternalFunctionFallbacks { get; set; }
+
+        public void CallExternalFunction(string funcName, int numberOfArguments)
+        {
+            ExternalFunctionDef funcDef;
+            Container fallbackFunctionContainer = null;
+
+            var foundExternal = _externals.TryGetValue (funcName, out funcDef);
+
+            // Should this function break glue? Abort run if we've already seen a newline.
+            // Set a bool to tell it to restore the snapshot at the end of this instruction.
+            if( foundExternal && !funcDef.lookaheadSafe && _stateSnapshotAtLastNewline != null ) {
+                _sawLookaheadUnsafeFunctionAfterNewline = true;
+                return;
+            }
+
+            // Try to use fallback function?
+            if (!foundExternal) {
+                if (allowExternalFunctionFallbacks) {
+                    fallbackFunctionContainer = KnotContainerWithName (funcName);
+                    Assert (fallbackFunctionContainer != null, "Trying to call EXTERNAL function '" + funcName + "' which has not been bound, and fallback ink function could not be found.");
+
+                    // Divert direct into fallback function and we're done
+                    state.callStack.Push (
+                        PushPopType.Function, 
+                        outputStreamLengthWithPushed:state.outputStream.Count
+                    );
+                    state.divertedPointer = Pointer.StartOf(fallbackFunctionContainer);
+                    return;
+
+                } else {
+                    Assert (false, "Trying to call EXTERNAL function '" + funcName + "' which has not been bound (and ink fallbacks disabled).");
+                }
+            }
+
+            // Pop arguments
+            var arguments = new List<object>();
+            for (int i = 0; i < numberOfArguments; ++i) {
+                var poppedObj = state.PopEvaluationStack () as Value;
+                var valueObj = poppedObj.valueObject;
+                arguments.Add (valueObj);
+            }
+
+            // Reverse arguments from the order they were popped,
+            // so they're the right way round again.
+            arguments.Reverse ();
+
+            // Run the function!
+            object funcResult = funcDef.function (arguments.ToArray());
+
+            // Convert return value (if any) to the a type that the ink engine can use
+            Runtime.Object returnObj = null;
+            if (funcResult != null) {
+                returnObj = Value.Create (funcResult);
+                Assert (returnObj != null, "Could not create ink value from returned object of type " + funcResult.GetType());
+            } else {
+                returnObj = new Runtime.Void ();
+            }
+                
+            state.PushEvaluationStack (returnObj);
+        }
+
+        /// <summary>
+        /// General purpose delegate definition for bound EXTERNAL function definitions
+        /// from ink. Note that this version isn't necessary if you have a function
+        /// with three arguments or less - see the overloads of BindExternalFunction.
+        /// </summary>
+        public delegate object ExternalFunction(object[] args);
+
+        /// <summary>
+        /// Most general form of function binding that returns an object
+        /// and takes an array of object parameters.
+        /// The only way to bind a function with more than 3 arguments.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunctionGeneral(string funcName, ExternalFunction func, bool lookaheadSafe = true)
+        {
+            IfAsyncWeCant ("bind an external function");
+            Assert (!_externals.ContainsKey (funcName), "Function '" + funcName + "' has already been bound.");
+            _externals [funcName] = new ExternalFunctionDef {
+                function = func,
+                lookaheadSafe = lookaheadSafe
+            };
+        }
+
+        object TryCoerce<T>(object value)
+        {  
+            if (value == null)
+                return null;
+
+            if (value is T)
+                return (T) value;
+
+            if (value is float && typeof(T) == typeof(int)) {
+                int intVal = (int)Math.Round ((float)value);
+                return intVal;
+            }
+
+            if (value is int && typeof(T) == typeof(float)) {
+                float floatVal = (float)(int)value;
+                return floatVal;
+            }
+
+            if (value is int && typeof(T) == typeof(bool)) {
+                int intVal = (int)value;
+                return intVal == 0 ? false : true;
+            }
+
+            if (value is bool && typeof(T) == typeof(int)) {
+                bool boolVal = (bool)value;
+                return boolVal ? 1 : 0;
+            }
+
+            if (typeof(T) == typeof(string)) {
+                return value.ToString ();
+            }
+
+            Assert (false, "Failed to cast " + value.GetType ().Name + " to " + typeof(T).Name);
+
+            return null;
+        }
+
+        // Convenience overloads for standard functions and actions of various arities
+        // Is there a better way of doing this?!
+
+        /// <summary>
+        /// Bind a C# function to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction(string funcName, Func<object> func, bool lookaheadSafe=false)
+        {
+			Assert(func != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 0, "External function expected no arguments");
+                return func();
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# Action to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="act">The C# action to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction(string funcName, Action act, bool lookaheadSafe=false)
+        {
+			Assert(act != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 0, "External function expected no arguments");
+                act();
+                return null;
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# function to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T>(string funcName, Func<T, object> func, bool lookaheadSafe=false)
+        {
+			Assert(func != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 1, "External function expected one argument");
+                return func( (T)TryCoerce<T>(args[0]) );
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# action to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="act">The C# action to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T>(string funcName, Action<T> act, bool lookaheadSafe=false)
+        {
+			Assert(act != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 1, "External function expected one argument");
+                act( (T)TryCoerce<T>(args[0]) );
+                return null;
+            }, lookaheadSafe);
+        }
+
+
+        /// <summary>
+        /// Bind a C# function to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2>(string funcName, Func<T1, T2, object> func, bool lookaheadSafe = false)
+        {
+			Assert(func != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 2, "External function expected two arguments");
+                return func(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1])
+                );
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# action to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="act">The C# action to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2>(string funcName, Action<T1, T2> act, bool lookaheadSafe=false)
+        {
+			Assert(act != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 2, "External function expected two arguments");
+                act(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1])
+                );
+                return null;
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# function to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2, T3>(string funcName, Func<T1, T2, T3, object> func, bool lookaheadSafe=false)
+        {
+			Assert(func != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 3, "External function expected three arguments");
+                return func(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1]),
+                    (T3)TryCoerce<T3>(args[2])
+                );
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# action to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="act">The C# action to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2, T3>(string funcName, Action<T1, T2, T3> act, bool lookaheadSafe=false)
+        {
+			Assert(act != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 3, "External function expected three arguments");
+                act(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1]),
+                    (T3)TryCoerce<T3>(args[2])
+                );
+                return null;
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# function to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="func">The C# function to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2, T3, T4>(string funcName, Func<T1, T2, T3, T4, object> func, bool lookaheadSafe=false)
+        {
+			Assert(func != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 4, "External function expected four arguments");
+                return func(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1]),
+                    (T3)TryCoerce<T3>(args[2]),
+                    (T4)TryCoerce<T4>(args[3])
+                );
+            }, lookaheadSafe);
+        }
+
+        /// <summary>
+        /// Bind a C# action to an ink EXTERNAL function declaration.
+        /// </summary>
+        /// <param name="funcName">EXTERNAL ink function name to bind to.</param>
+        /// <param name="act">The C# action to bind.</param>
+        /// <param name="lookaheadSafe">The ink engine often evaluates further 
+        /// than you might expect beyond the current line just in case it sees 
+        /// glue that will cause the two lines to become one. In this case it's 
+        /// possible that a function can appear to be called twice instead of 
+        /// just once, and earlier than you expect. If it's safe for your 
+        /// function to be called in this way (since the result and side effect 
+        /// of the function will not change), then you can pass 'true'. 
+        /// Usually, you want to pass 'false', especially if you want some action 
+        /// to be performed in game code when this function is called.</param>
+        public void BindExternalFunction<T1, T2, T3, T4>(string funcName, Action<T1, T2, T3, T4> act, bool lookaheadSafe=false)
+        {
+			Assert(act != null, "Can't bind a null function");
+
+            BindExternalFunctionGeneral (funcName, (object[] args) => {
+                Assert(args.Length == 4, "External function expected four arguments");
+                act(
+                    (T1)TryCoerce<T1>(args[0]), 
+                    (T2)TryCoerce<T2>(args[1]),
+                    (T3)TryCoerce<T3>(args[2]),
+                    (T4)TryCoerce<T4>(args[3])
+                );
+                return null;
+            }, lookaheadSafe);
+        }
+        
+        /// <summary>
+        /// Remove a binding for a named EXTERNAL ink function.
+        /// </summary>
+        public void UnbindExternalFunction(string funcName)
+        {
+            IfAsyncWeCant ("unbind an external a function");
+            Assert (_externals.ContainsKey (funcName), "Function '" + funcName + "' has not been bound.");
+            _externals.Remove (funcName);
+        }
+
+        /// <summary>
+        /// Check that all EXTERNAL ink functions have a valid bound C# function.
+        /// Note that this is automatically called on the first call to Continue().
+        /// </summary>
+        public void ValidateExternalBindings()
+        {
+			var missingExternals = new HashSet<string>();
+
+			ValidateExternalBindings (_mainContentContainer, missingExternals);
+            _hasValidatedExternals = true;
+
+			// No problem! Validation complete
+			if( missingExternals.Count == 0 ) {
+				_hasValidatedExternals = true;
+			} 
+
+			// Error for all missing externals
+			else {
+				var message = string.Format("ERROR: Missing function binding for external{0}: '{1}' {2}",
+					missingExternals.Count > 1 ? "s" : string.Empty,
+					string.Join("', '", missingExternals.ToArray()),
+					allowExternalFunctionFallbacks ? ", and no fallback ink function found." : " (ink fallbacks disabled)"
+				);
+					
+				Error(message);
+			}
+        }
+
+		void ValidateExternalBindings(Container c, HashSet<string> missingExternals)
+        {
+            foreach (var innerContent in c.content) {
+				var container = innerContent as Container;
+				if( container == null || !container.hasValidName )
+					ValidateExternalBindings (innerContent, missingExternals);
+            }
+            foreach (var innerKeyValue in c.namedContent) {
+				ValidateExternalBindings (innerKeyValue.Value as Runtime.Object, missingExternals);
+            }
+        }
+
+		void ValidateExternalBindings(Runtime.Object o, HashSet<string> missingExternals)
+        {
+            var container = o as Container;
+            if (container) {
+                ValidateExternalBindings (container, missingExternals);
+                return;
+            }
+
+            var divert = o as Divert;
+            if (divert && divert.isExternal) {
+                var name = divert.targetPathString;
+
+                if (!_externals.ContainsKey (name)) {
+					if( allowExternalFunctionFallbacks ) {
+						bool fallbackFound = mainContentContainer.namedContent.ContainsKey(name);
+						if( !fallbackFound ) {
+							missingExternals.Add(name);
+						}
+					} else {
+						missingExternals.Add(name);
+					}
+                }
+            }
+        }
+           
+        /// <summary>
+        /// Delegate definition for variable observation - see ObserveVariable.
+        /// </summary>
+        public delegate void VariableObserver(string variableName, object newValue);
+
+        /// <summary>
+        /// When the named global variable changes it's value, the observer will be
+        /// called to notify it of the change. Note that if the value changes multiple
+        /// times within the ink, the observer will only be called once, at the end
+        /// of the ink's evaluation. If, during the evaluation, it changes and then
+        /// changes back again to its original value, it will still be called.
+        /// Note that the observer will also be fired if the value of the variable
+        /// is changed externally to the ink, by directly setting a value in
+        /// story.variablesState.
+        /// </summary>
+        /// <param name="variableName">The name of the global variable to observe.</param>
+        /// <param name="observer">A delegate function to call when the variable changes.</param>
+        public void ObserveVariable(string variableName, VariableObserver observer)
+        {
+            IfAsyncWeCant ("observe a new variable");
+
+            if (_variableObservers == null)
+                _variableObservers = new Dictionary<string, VariableObserver> ();
+
+			if( !state.variablesState.GlobalVariableExistsWithName(variableName) ) 
+				throw new Exception("Cannot observe variable '"+variableName+"' because it wasn't declared in the ink story.");
+
+            if (_variableObservers.ContainsKey (variableName)) {
+                _variableObservers[variableName] += observer;
+            } else {
+                _variableObservers[variableName] = observer;
+            }
+        }
+
+        /// <summary>
+        /// Convenience function to allow multiple variables to be observed with the same
+        /// observer delegate function. See the singular ObserveVariable for details.
+        /// The observer will get one call for every variable that has changed.
+        /// </summary>
+        /// <param name="variableNames">The set of variables to observe.</param>
+        /// <param name="observer">The delegate function to call when any of the named variables change.</param>
+        public void ObserveVariables(IList<string> variableNames, VariableObserver observer)
+        {
+            foreach (var varName in variableNames) {
+                ObserveVariable (varName, observer);
+            }
+        }
+
+        /// <summary>
+        /// Removes the variable observer, to stop getting variable change notifications.
+        /// If you pass a specific variable name, it will stop observing that particular one. If you
+        /// pass null (or leave it blank, since it's optional), then the observer will be removed
+        /// from all variables that it's subscribed to. If you pass in a specific variable name and
+        /// null for the the observer, all observers for that variable will be removed. 
+        /// </summary>
+        /// <param name="observer">(Optional) The observer to stop observing.</param>
+        /// <param name="specificVariableName">(Optional) Specific variable name to stop observing.</param>
+        public void RemoveVariableObserver(VariableObserver observer = null, string specificVariableName = null)
+        {
+            IfAsyncWeCant ("remove a variable observer");
+
+            if (_variableObservers == null)
+                return;
+
+            // Remove observer for this specific variable
+            if (specificVariableName != null) {
+                if (_variableObservers.ContainsKey (specificVariableName)) {
+                    if( observer != null) {
+                        _variableObservers [specificVariableName] -= observer;
+                        if (_variableObservers[specificVariableName] == null) {
+                            _variableObservers.Remove(specificVariableName);
+                        }
+                    }
+                    else {
+                        _variableObservers.Remove(specificVariableName);
+                    }
+                }
+            } 
+
+            // Remove observer for all variables
+            else if( observer != null) {
+                var keys = new List<string>(_variableObservers.Keys);
+                foreach (var varName in keys) {
+                    _variableObservers[varName] -= observer;
+                    if (_variableObservers[varName] == null) {
+                        _variableObservers.Remove(varName);
+                    }
+                }
+            }
+        }
+
+        void VariableStateDidChangeEvent(string variableName, Runtime.Object newValueObj)
+        {
+            if (_variableObservers == null)
+                return;
+            
+            VariableObserver observers = null;
+            if (_variableObservers.TryGetValue (variableName, out observers)) {
+
+                if (!(newValueObj is Value)) {
+                    throw new System.Exception ("Tried to get the value of a variable that isn't a standard type");
+                }
+                var val = newValueObj as Value;
+
+                observers (variableName, val.valueObject);
+            }
+        }
+
+        /// <summary>
+        /// Get any global tags associated with the story. These are defined as
+        /// hash tags defined at the very top of the story.
+        /// </summary>
+        public List<string> globalTags {
+            get {
+                return TagsAtStartOfFlowContainerWithPathString ("");
+            }
+        }
+
+        /// <summary>
+        /// Gets any tags associated with a particular knot or knot.stitch.
+        /// These are defined as hash tags defined at the very top of a 
+        /// knot or stitch.
+        /// </summary>
+        /// <param name="path">The path of the knot or stitch, in the form "knot" or "knot.stitch".</param>
+        public List<string> TagsForContentAtPath (string path)
+        {
+            return TagsAtStartOfFlowContainerWithPathString (path);
+        }
+
+        List<string> TagsAtStartOfFlowContainerWithPathString (string pathString)
+        {
+            var path = new Runtime.Path (pathString);
+
+            // Expected to be global story, knot or stitch
+            var flowContainer = ContentAtPath (path).container;
+            while(true) {
+                var firstContent = flowContainer.content [0];
+                if (firstContent is Container)
+                    flowContainer = (Container)firstContent;
+                else break;
+            }
+
+            // Any initial tag objects count as the "main tags" associated with that story/knot/stitch
+            List<string> tags = null;
+            foreach (var c in flowContainer.content) {
+                var tag = c as Runtime.Tag;
+                if (tag) {
+                    if (tags == null) tags = new List<string> ();
+                    tags.Add (tag.text);
+                } else break;
+            }
+
+            return tags;
+        }
+
+        /// <summary>
+        /// Useful when debugging a (very short) story, to visualise the state of the
+        /// story. Add this call as a watch and open the extended text. A left-arrow mark
+        /// will denote the current point of the story.
+        /// It's only recommended that this is used on very short debug stories, since
+        /// it can end up generate a large quantity of text otherwise.
+        /// </summary>
+        public virtual string BuildStringOfHierarchy()
+        {
+            var sb = new StringBuilder ();
+
+            mainContentContainer.BuildStringOfHierarchy (sb, 0, state.currentPointer.Resolve());
+
+            return sb.ToString ();
+        }
+
+        string BuildStringOfContainer (Container container)
+        {
+        	var sb = new StringBuilder ();
+
+            container.BuildStringOfHierarchy (sb, 0, state.currentPointer.Resolve());
+
+        	return sb.ToString();
+        }
+
+		private void NextContent()
+		{
+            // Setting previousContentObject is critical for VisitChangedContainersDueToDivert
+            state.previousPointer = state.currentPointer;
+
+			// Divert step?
+			if (!state.divertedPointer.isNull) {
+
+                state.currentPointer = state.divertedPointer;
+                state.divertedPointer = Pointer.Null;
+
+                // Internally uses state.previousContentObject and state.currentContentObject
+                VisitChangedContainersDueToDivert ();
+
+                // Diverted location has valid content?
+                if (!state.currentPointer.isNull) {
+                    return;
+                }
+				
+                // Otherwise, if diverted location doesn't have valid content,
+                // drop down and attempt to increment.
+                // This can happen if the diverted path is intentionally jumping
+                // to the end of a container - e.g. a Conditional that's re-joining
+			}
+
+            bool successfulPointerIncrement = IncrementContentPointer ();
+
+            // Ran out of content? Try to auto-exit from a function,
+            // or finish evaluating the content of a thread
+            if (!successfulPointerIncrement) {
+
+                bool didPop = false;
+
+                if (state.callStack.CanPop (PushPopType.Function)) {
+
+                    // Pop from the call stack
+                    state.PopCallstack (PushPopType.Function);
+
+                    // This pop was due to dropping off the end of a function that didn't return anything,
+                    // so in this case, we make sure that the evaluator has something to chomp on if it needs it
+                    if (state.inExpressionEvaluation) {
+                        state.PushEvaluationStack (new Runtime.Void ());
+                    }
+
+                    didPop = true;
+                } else if (state.callStack.canPopThread) {
+                    state.callStack.PopThread ();
+
+                    didPop = true;
+                } else {
+                    state.TryExitFunctionEvaluationFromGame ();
+                }
+
+                // Step past the point where we last called out
+                if (didPop && !state.currentPointer.isNull) {
+                    NextContent ();
+                }
+			}
+		}
+
+        bool IncrementContentPointer()
+        {
+            bool successfulIncrement = true;
+
+            var pointer = state.callStack.currentElement.currentPointer;
+            pointer.index++;
+
+            // Each time we step off the end, we fall out to the next container, all the
+            // while we're in indexed rather than named content
+            while (pointer.index >= pointer.container.content.Count) {
+
+                successfulIncrement = false;
+
+                Container nextAncestor = pointer.container.parent as Container;
+                if (!nextAncestor) {
+                    break;
+                }
+
+                var indexInAncestor = nextAncestor.content.IndexOf (pointer.container);
+                if (indexInAncestor == -1) {
+                    break;
+                }
+
+                pointer = new Pointer (nextAncestor, indexInAncestor);
+
+                // Increment to next content in outer container
+                pointer.index++;
+
+                successfulIncrement = true;
+            }
+
+            if (!successfulIncrement) pointer = Pointer.Null;
+
+            state.callStack.currentElement.currentPointer = pointer;
+
+            return successfulIncrement;
+        }
+            
+        bool TryFollowDefaultInvisibleChoice()
+        {
+            var allChoices = _state.currentChoices;
+
+            // Is a default invisible choice the ONLY choice?
+            var invisibleChoices = allChoices.Where (c => c.isInvisibleDefault).ToList();
+            if (invisibleChoices.Count == 0 || allChoices.Count > invisibleChoices.Count)
+                return false;
+
+            var choice = invisibleChoices [0];
+
+            // Invisible choice may have been generated on a different thread,
+            // in which case we need to restore it before we continue
+            state.callStack.currentThread = choice.threadAtGeneration;
+
+            // If there's a chance that this state will be rolled back to before
+            // the invisible choice then make sure that the choice thread is
+            // left intact, and it isn't re-entered in an old state.
+            if ( _stateSnapshotAtLastNewline != null )
+                state.callStack.currentThread = state.callStack.ForkThread();
+
+            ChoosePath (choice.targetPath, incrementingTurnIndex: false);
+
+            return true;
+        }
+            
+
+        // Note that this is O(n), since it re-evaluates the shuffle indices
+        // from a consistent seed each time.
+        // TODO: Is this the best algorithm it can be?
+        int NextSequenceShuffleIndex()
+        {
+            var numElementsIntVal = state.PopEvaluationStack () as IntValue;
+            if (numElementsIntVal == null) {
+                Error ("expected number of elements in sequence for shuffle index");
+                return 0;
+            }
+
+            var seqContainer = state.currentPointer.container;
+
+            int numElements = numElementsIntVal.value;
+
+            var seqCountVal = state.PopEvaluationStack () as IntValue;
+            var seqCount = seqCountVal.value;
+            var loopIndex = seqCount / numElements;
+            var iterationIndex = seqCount % numElements;
+
+            // Generate the same shuffle based on:
+            //  - The hash of this container, to make sure it's consistent
+            //    each time the runtime returns to the sequence
+            //  - How many times the runtime has looped around this full shuffle
+            var seqPathStr = seqContainer.path.ToString();
+            int sequenceHash = 0;
+            foreach (char c in seqPathStr) {
+                sequenceHash += c;
+            }
+            var randomSeed = sequenceHash + loopIndex + state.storySeed;
+            var random = new Random (randomSeed);
+
+            var unpickedIndices = new List<int> ();
+            for (int i = 0; i < numElements; ++i) {
+                unpickedIndices.Add (i);
+            }
+
+            for (int i = 0; i <= iterationIndex; ++i) {
+                var chosen = random.Next () % unpickedIndices.Count;
+                var chosenIndex = unpickedIndices [chosen];
+                unpickedIndices.RemoveAt (chosen);
+
+                if (i == iterationIndex) {
+                    return chosenIndex;
+                }
+            }
+
+            throw new System.Exception ("Should never reach here");
+        }
+
+        // Throw an exception that gets caught and causes AddError to be called,
+        // then exits the flow.
+        public void Error(string message, bool useEndLineNumber = false)
+        {
+            var e = new StoryException (message);
+            e.useEndLineNumber = useEndLineNumber;
+            throw e;
+        }
+
+        public void Warning (string message)
+        {
+            AddError (message, isWarning:true);
+        }
+
+        void AddError (string message, bool isWarning = false, bool useEndLineNumber = false)
+        {
+            var dm = currentDebugMetadata;
+
+            var errorTypeStr = isWarning ? "WARNING" : "ERROR";
+
+            if (dm != null) {
+                int lineNum = useEndLineNumber ? dm.endLineNumber : dm.startLineNumber;
+                message = string.Format ("RUNTIME {0}: '{1}' line {2}: {3}", errorTypeStr, dm.fileName, lineNum, message);
+            } else if( !state.currentPointer.isNull  ) {
+                message = string.Format ("RUNTIME {0}: ({1}): {2}", errorTypeStr, state.currentPointer.path, message);
+			} else {
+                message = "RUNTIME "+errorTypeStr+": " + message;
+            }
+
+            state.AddError (message, isWarning);
+
+            // In a broken state don't need to know about any other errors.
+            if( !isWarning )
+                state.ForceEnd ();
+        }
+
+        void Assert(bool condition, string message = null, params object[] formatParams)
+        {
+            if (condition == false) {
+                if (message == null) {
+                    message = "Story assert";
+                }
+                if (formatParams != null && formatParams.Count() > 0) {
+                    message = string.Format (message, formatParams);
+                }
+                    
+                throw new System.Exception (message + " " + currentDebugMetadata);
+            }
+        }
+
+        DebugMetadata currentDebugMetadata
+        {
+            get {
+                DebugMetadata dm;
+
+                // Try to get from the current path first
+                var pointer = state.currentPointer;
+                if (!pointer.isNull) {
+                    dm = pointer.Resolve().debugMetadata;
+                    if (dm != null) {
+                        return dm;
+                    }
+                }
+                    
+                // Move up callstack if possible
+                for (int i = state.callStack.elements.Count - 1; i >= 0; --i) {
+                    pointer = state.callStack.elements [i].currentPointer;
+                    if (!pointer.isNull && pointer.Resolve() != null) {
+                        dm = pointer.Resolve().debugMetadata;
+                        if (dm != null) {
+                            return dm;
+                        }
+                    }
+                }
+
+                // Current/previous path may not be valid if we've just had an error,
+                // or if we've simply run out of content.
+                // As a last resort, try to grab something from the output stream
+                for (int i = state.outputStream.Count - 1; i >= 0; --i) {
+                    var outputObj = state.outputStream [i];
+                    dm = outputObj.debugMetadata;
+                    if (dm != null) {
+                        return dm;
+                    }
+                }
+
+                return null;
+            }
+        }
+
+        int currentLineNumber 
+        {
+            get {
+                var dm = currentDebugMetadata;
+                if (dm != null) {
+                    return dm.startLineNumber;
+                }
+                return 0;
+            }
+        }
+
+        public Container mainContentContainer {
+            get {
+                if (_temporaryEvaluationContainer) {
+                    return _temporaryEvaluationContainer;
+                } else {
+                    return _mainContentContainer;
+                }
+            }
+        }
+
+        Container _mainContentContainer;
+        ListDefinitionsOrigin _listDefinitions;
+
+        struct ExternalFunctionDef {
+            public ExternalFunction function;
+            public bool lookaheadSafe;
+        }
+        Dictionary<string, ExternalFunctionDef> _externals;
+        Dictionary<string, VariableObserver> _variableObservers;
+        bool _hasValidatedExternals;
+
+        Container _temporaryEvaluationContainer;
+
+        StoryState _state;
+
+        bool _asyncContinueActive;
+        StoryState _stateSnapshotAtLastNewline = null;
+        bool _sawLookaheadUnsafeFunctionAfterNewline = false;
+
+        int _recursiveContinueCount = 0;
+
+        bool _asyncSaving;
+
+        Profiler _profiler;
+	}
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Story.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Story.cs.meta
new file mode 100644
index 0000000..6575d5a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Story.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9996ae35e3e4647e8b8ee0c5aeb61489
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/StoryException.cs b/Assets/Ink/InkLibs/InkRuntime/StoryException.cs
new file mode 100644
index 0000000..5b0a20f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StoryException.cs
@@ -0,0 +1,24 @@
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// Exception that represents an error when running a Story at runtime.
+    /// An exception being thrown of this type is typically when there's
+    /// a bug in your ink, rather than in the ink engine itself!
+    /// </summary>
+    public class StoryException : System.Exception
+    {
+        public bool useEndLineNumber;
+
+        /// <summary>
+        /// Constructs a default instance of a StoryException without a message.
+        /// </summary>
+        public StoryException () { }
+
+        /// <summary>
+        /// Constructs an instance of a StoryException with a message.
+        /// </summary>
+        /// <param name="message">The error message.</param>
+        public StoryException(string message) : base(message) {}
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/StoryException.cs.meta b/Assets/Ink/InkLibs/InkRuntime/StoryException.cs.meta
new file mode 100644
index 0000000..4a34afe
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StoryException.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 83b6443aded8c49e1960594d5f64e114
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/StoryState.cs b/Assets/Ink/InkLibs/InkRuntime/StoryState.cs
new file mode 100644
index 0000000..0d82011
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StoryState.cs
@@ -0,0 +1,1250 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Diagnostics;
+using System.IO;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// All story state information is included in the StoryState class,
+    /// including global variables, read counts, the pointer to the current
+    /// point in the story, the call stack (for tunnels, functions, etc),
+    /// and a few other smaller bits and pieces. You can save the current
+    /// state using the json serialisation functions ToJson and LoadJson.
+    /// </summary>
+    public class StoryState
+    {
+        /// <summary>
+        /// The current version of the state save file JSON-based format.
+        /// </summary>
+        public const int kInkSaveStateVersion = 9; // new: multi-flows, but backward compatible
+        const int kMinCompatibleLoadVersion = 8;
+
+        /// <summary>
+        /// Callback for when a state is loaded
+        /// </summary>
+        public event Action onDidLoadState;
+
+        /// <summary>
+        /// Exports the current state to json format, in order to save the game.
+        /// </summary>
+        /// <returns>The save state in json format.</returns>
+        public string ToJson() {
+            var writer = new SimpleJson.Writer();
+            WriteJson(writer);
+            return writer.ToString();
+        }
+
+        /// <summary>
+        /// Exports the current state to json format, in order to save the game.
+        /// For this overload you can pass in a custom stream, such as a FileStream.
+        /// </summary>
+        public void ToJson(Stream stream) {
+            var writer = new SimpleJson.Writer(stream);
+            WriteJson(writer);
+        }
+
+        /// <summary>
+        /// Loads a previously saved state in JSON format.
+        /// </summary>
+        /// <param name="json">The JSON string to load.</param>
+        public void LoadJson(string json)
+        {
+            var jObject = SimpleJson.TextToDictionary (json);
+            LoadJsonObj(jObject);
+            if(onDidLoadState != null) onDidLoadState();
+        }
+
+        /// <summary>
+        /// Gets the visit/read count of a particular Container at the given path.
+        /// For a knot or stitch, that path string will be in the form:
+        /// 
+        ///     knot
+        ///     knot.stitch
+        /// 
+        /// </summary>
+        /// <returns>The number of times the specific knot or stitch has
+        /// been enountered by the ink engine.</returns>
+        /// <param name="pathString">The dot-separated path string of
+        /// the specific knot or stitch.</param>
+        public int VisitCountAtPathString(string pathString)
+        {
+            int visitCountOut;
+
+            if ( _patch != null ) {
+                var container = story.ContentAtPath(new Path(pathString)).container;
+                if (container == null)
+                    throw new Exception("Content at path not found: " + pathString);
+
+                if( _patch.TryGetVisitCount(container, out visitCountOut) )
+                    return visitCountOut;
+            }
+
+            if (_visitCounts.TryGetValue(pathString, out visitCountOut))
+                return visitCountOut;
+
+            return 0;
+        }
+
+        public int VisitCountForContainer(Container container)
+        {
+            if (!container.visitsShouldBeCounted)
+            {
+                story.Error("Read count for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
+                return 0;
+            }
+
+            int count = 0;
+            if (_patch != null && _patch.TryGetVisitCount(container, out count))
+                return count;
+                
+            var containerPathStr = container.path.ToString();
+            _visitCounts.TryGetValue(containerPathStr, out count);
+            return count;
+        }
+
+        public void IncrementVisitCountForContainer(Container container)
+        {
+            if( _patch != null ) {
+                var currCount = VisitCountForContainer(container);
+                currCount++;
+                _patch.SetVisitCount(container, currCount);
+                return;
+            }
+
+            int count = 0;
+            var containerPathStr = container.path.ToString();
+            _visitCounts.TryGetValue(containerPathStr, out count);
+            count++;
+            _visitCounts[containerPathStr] = count;
+        }
+
+        public void RecordTurnIndexVisitToContainer(Container container)
+        {
+            if( _patch != null ) {
+                _patch.SetTurnIndex(container, currentTurnIndex);
+                return;
+            }
+
+            var containerPathStr = container.path.ToString();
+            _turnIndices[containerPathStr] = currentTurnIndex;
+        }
+
+        public int TurnsSinceForContainer(Container container)
+        {
+            if (!container.turnIndexShouldBeCounted)
+            {
+                story.Error("TURNS_SINCE() for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
+            }
+
+            int index = 0;
+
+            if ( _patch != null && _patch.TryGetTurnIndex(container, out index) ) {
+                return currentTurnIndex - index;
+            }
+
+            var containerPathStr = container.path.ToString();
+            if (_turnIndices.TryGetValue(containerPathStr, out index))
+            {
+                return currentTurnIndex - index;
+            }
+            else
+            {
+                return -1;
+            }
+        }
+
+        public int callstackDepth {
+			get {
+				return callStack.depth;
+			}
+		}
+
+        // REMEMBER! REMEMBER! REMEMBER!
+        // When adding state, update the Copy method, and serialisation.
+        // REMEMBER! REMEMBER! REMEMBER!
+
+        public List<Runtime.Object> outputStream { 
+            get { 
+                return _currentFlow.outputStream; 
+            } 
+        }
+
+        
+
+		public List<Choice> currentChoices { 
+			get { 
+				// If we can continue generating text content rather than choices,
+				// then we reflect the choice list as being empty, since choices
+				// should always come at the end.
+				if( canContinue ) return new List<Choice>();
+				return _currentFlow.currentChoices;
+			} 
+		}
+		public List<Choice> generatedChoices {
+			get {
+				return _currentFlow.currentChoices;
+			}
+		}
+
+        // TODO: Consider removing currentErrors / currentWarnings altogether
+        // and relying on client error handler code immediately handling StoryExceptions etc
+        // Or is there a specific reason we need to collect potentially multiple
+        // errors before throwing/exiting?
+        public List<string> currentErrors { get; private set; }
+        public List<string> currentWarnings { get; private set; }
+        public VariablesState variablesState { get; private set; }
+        public CallStack callStack { 
+            get { 
+                return _currentFlow.callStack;
+            }
+            // set {
+            //     _currentFlow.callStack = value;
+            // } 
+        }
+
+        public List<Runtime.Object> evaluationStack { get; private set; }
+        public Pointer divertedPointer { get; set; }
+
+        public int currentTurnIndex { get; private set; }
+        public int storySeed { get; set; }
+        public int previousRandom { get; set; }
+        public bool didSafeExit { get; set; }
+
+        public Story story { get; set; }
+
+        /// <summary>
+        /// String representation of the location where the story currently is.
+        /// </summary>
+        public string currentPathString {
+            get {
+                var pointer = currentPointer;
+                if (pointer.isNull)
+                    return null;
+                else
+                    return pointer.path.ToString();
+            }
+        }
+
+        public Runtime.Pointer currentPointer {
+            get {
+                return callStack.currentElement.currentPointer;
+            }
+            set {
+                callStack.currentElement.currentPointer = value;
+            }
+        }
+
+        public Pointer previousPointer { 
+            get {
+                return callStack.currentThread.previousPointer;
+            }
+            set {
+                callStack.currentThread.previousPointer = value;
+            }
+        }
+
+		public bool canContinue {
+			get {
+				return !currentPointer.isNull && !hasError;
+			}
+		}
+            
+        public bool hasError
+        {
+            get {
+                return currentErrors != null && currentErrors.Count > 0;
+            }
+        }
+
+        public bool hasWarning {
+            get {
+                return currentWarnings != null && currentWarnings.Count > 0;
+            }
+        }
+
+        public string currentText
+        {
+            get 
+            {
+				if( _outputStreamTextDirty ) {
+					var sb = new StringBuilder ();
+
+					foreach (var outputObj in outputStream) {
+						var textContent = outputObj as StringValue;
+						if (textContent != null) {
+							sb.Append(textContent.value);
+						}
+					}
+
+                    _currentText = CleanOutputWhitespace (sb.ToString ());
+
+					_outputStreamTextDirty = false;
+				}
+
+				return _currentText;
+            }
+        }
+		string _currentText;
+
+        // Cleans inline whitespace in the following way:
+        //  - Removes all whitespace from the start and end of line (including just before a \n)
+        //  - Turns all consecutive space and tab runs into single spaces (HTML style)
+        string CleanOutputWhitespace(string str)
+        {
+            var sb = new StringBuilder(str.Length);
+
+            int currentWhitespaceStart = -1;
+            int startOfLine = 0;
+
+            for (int i = 0; i < str.Length; i++) {
+                var c = str[i];
+
+                bool isInlineWhitespace = c == ' ' || c == '\t';
+
+                if (isInlineWhitespace && currentWhitespaceStart == -1)
+                    currentWhitespaceStart = i;
+
+                if (!isInlineWhitespace) {
+                    if (c != '\n' && currentWhitespaceStart > 0 && currentWhitespaceStart != startOfLine) {
+                        sb.Append(' ');
+                    }
+                    currentWhitespaceStart = -1;
+                }
+
+                if (c == '\n')
+                    startOfLine = i + 1;
+
+                if (!isInlineWhitespace)
+                    sb.Append(c);
+            }
+
+            return sb.ToString();
+        }
+
+        public List<string> currentTags 
+        {
+            get 
+            {
+				if( _outputStreamTagsDirty ) {
+					_currentTags = new List<string>();
+
+					foreach (var outputObj in outputStream) {
+						var tag = outputObj as Tag;
+						if (tag != null) {
+							_currentTags.Add (tag.text);
+						}
+					}
+
+					_outputStreamTagsDirty = false;
+				}
+
+				return _currentTags;
+            }
+        }
+		List<string> _currentTags;
+
+        public string currentFlowName {
+            get {
+                return _currentFlow.name;
+            }
+        }
+
+        public bool inExpressionEvaluation {
+            get {
+                return callStack.currentElement.inExpressionEvaluation;
+            }
+            set {
+                callStack.currentElement.inExpressionEvaluation = value;
+            }
+        }
+            
+        public StoryState (Story story)
+        {
+            this.story = story;
+
+            _currentFlow = new Flow(kDefaultFlowName, story);
+            
+			OutputStreamDirty();
+
+            evaluationStack = new List<Runtime.Object> ();
+
+            variablesState = new VariablesState (callStack, story.listDefinitions);
+
+            _visitCounts = new Dictionary<string, int> ();
+            _turnIndices = new Dictionary<string, int> ();
+
+            currentTurnIndex = -1;
+
+            // Seed the shuffle random numbers
+            int timeSeed = DateTime.Now.Millisecond;
+            storySeed = (new Random (timeSeed)).Next () % 100;
+            previousRandom = 0;
+
+			
+
+            GoToStart();
+        }
+
+        public void GoToStart()
+        {
+            callStack.currentElement.currentPointer = Pointer.StartOf (story.mainContentContainer);
+        }
+
+        internal void SwitchFlow_Internal(string flowName)
+        {
+            if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.SwitchFlow");
+            
+            if( _namedFlows == null ) {
+                _namedFlows = new Dictionary<string, Flow>();
+                _namedFlows[kDefaultFlowName] = _currentFlow;
+            }
+
+            if( flowName == _currentFlow.name ) {
+                return;
+            }
+
+            Flow flow;
+            if( !_namedFlows.TryGetValue(flowName, out flow) ) {
+                flow = new Flow(flowName, story);
+                _namedFlows[flowName] = flow;
+            }
+
+            _currentFlow = flow;
+            variablesState.callStack = _currentFlow.callStack;
+
+            // Cause text to be regenerated from output stream if necessary
+            OutputStreamDirty();
+        }
+
+        internal void SwitchToDefaultFlow_Internal()
+        {
+            if( _namedFlows == null ) return;
+            SwitchFlow_Internal(kDefaultFlowName);
+        }
+
+        internal void RemoveFlow_Internal(string flowName)
+        {
+            if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.DestroyFlow");
+            if(flowName == kDefaultFlowName) throw new System.Exception("Cannot destroy default flow");
+
+            // If we're currently in the flow that's being removed, switch back to default
+            if( _currentFlow.name == flowName ) {
+                SwitchToDefaultFlow_Internal();
+            }
+
+            _namedFlows.Remove(flowName);
+        }
+
+        // Warning: Any Runtime.Object content referenced within the StoryState will
+        // be re-referenced rather than cloned. This is generally okay though since
+        // Runtime.Objects are treated as immutable after they've been set up.
+        // (e.g. we don't edit a Runtime.StringValue after it's been created an added.)
+        // I wonder if there's a sensible way to enforce that..??
+        public StoryState CopyAndStartPatching()
+        {
+            var copy = new StoryState(story);
+
+            copy._patch = new StatePatch(_patch);
+
+            // Hijack the new default flow to become a copy of our current one
+            // If the patch is applied, then this new flow will replace the old one in _namedFlows
+            copy._currentFlow.name = _currentFlow.name;
+            copy._currentFlow.callStack = new CallStack (_currentFlow.callStack);
+            copy._currentFlow.currentChoices.AddRange(_currentFlow.currentChoices);
+            copy._currentFlow.outputStream.AddRange(_currentFlow.outputStream);
+            copy.OutputStreamDirty();
+
+            // The copy of the state has its own copy of the named flows dictionary,
+            // except with the current flow replaced with the copy above
+            // (Assuming we're in multi-flow mode at all. If we're not then
+            // the above copy is simply the default flow copy and we're done)
+            if( _namedFlows != null ) {
+                copy._namedFlows = new Dictionary<string, Flow>();
+                foreach(var namedFlow in _namedFlows)
+                    copy._namedFlows[namedFlow.Key] = namedFlow.Value;
+                copy._namedFlows[_currentFlow.name] = copy._currentFlow;
+            }
+
+            if (hasError) {
+                copy.currentErrors = new List<string> ();
+                copy.currentErrors.AddRange (currentErrors); 
+            }
+            if (hasWarning) {
+                copy.currentWarnings = new List<string> ();
+                copy.currentWarnings.AddRange (currentWarnings); 
+            }
+
+            
+            // ref copy - exactly the same variables state!
+            // we're expecting not to read it only while in patch mode
+            // (though the callstack will be modified)
+            copy.variablesState = variablesState;
+            copy.variablesState.callStack = copy.callStack;
+            copy.variablesState.patch = copy._patch;
+
+            copy.evaluationStack.AddRange (evaluationStack);
+
+            if (!divertedPointer.isNull)
+                copy.divertedPointer = divertedPointer;
+
+            copy.previousPointer = previousPointer;
+
+            // visit counts and turn indicies will be read only, not modified
+            // while in patch mode
+            copy._visitCounts = _visitCounts;
+            copy._turnIndices = _turnIndices;
+
+            copy.currentTurnIndex = currentTurnIndex;
+            copy.storySeed = storySeed;
+            copy.previousRandom = previousRandom;
+
+            copy.didSafeExit = didSafeExit;
+
+            return copy;
+        }
+
+        public void RestoreAfterPatch()
+        {
+            // VariablesState was being borrowed by the patched
+            // state, so restore it with our own callstack.
+            // _patch will be null normally, but if you're in the
+            // middle of a save, it may contain a _patch for save purpsoes.
+            variablesState.callStack = callStack;
+            variablesState.patch = _patch; // usually null
+        }
+
+        public void ApplyAnyPatch()
+        {
+            if (_patch == null) return;
+
+            variablesState.ApplyPatch();
+
+            foreach(var pathToCount in _patch.visitCounts)
+                ApplyCountChanges(pathToCount.Key, pathToCount.Value, isVisit:true);
+
+            foreach (var pathToIndex in _patch.turnIndices)
+                ApplyCountChanges(pathToIndex.Key, pathToIndex.Value, isVisit:false);
+
+            _patch = null;
+        }
+
+        void ApplyCountChanges(Container container, int newCount, bool isVisit)
+        {
+            var counts = isVisit ? _visitCounts : _turnIndices;
+            counts[container.path.ToString()] = newCount;
+        }
+
+        void WriteJson(SimpleJson.Writer writer)
+        {
+            writer.WriteObjectStart();
+
+            // Flows
+            writer.WritePropertyStart("flows");
+            writer.WriteObjectStart();
+
+            // Multi-flow
+            if( _namedFlows != null ) {
+                foreach(var namedFlow in _namedFlows) {
+                    writer.WriteProperty(namedFlow.Key, namedFlow.Value.WriteJson);
+                }
+            } 
+            
+            // Single flow
+            else {
+                writer.WriteProperty(_currentFlow.name, _currentFlow.WriteJson);
+            }
+
+            writer.WriteObjectEnd();
+            writer.WritePropertyEnd(); // end of flows
+
+            writer.WriteProperty("currentFlowName", _currentFlow.name);
+
+            writer.WriteProperty("variablesState", variablesState.WriteJson);
+
+            writer.WriteProperty("evalStack", w => Json.WriteListRuntimeObjs(w, evaluationStack));
+
+
+            if (!divertedPointer.isNull)
+                writer.WriteProperty("currentDivertTarget", divertedPointer.path.componentsString);
+                
+            writer.WriteProperty("visitCounts", w => Json.WriteIntDictionary(w, _visitCounts));
+            writer.WriteProperty("turnIndices", w => Json.WriteIntDictionary(w, _turnIndices));
+
+
+            writer.WriteProperty("turnIdx", currentTurnIndex);
+            writer.WriteProperty("storySeed", storySeed);
+            writer.WriteProperty("previousRandom", previousRandom);
+
+            writer.WriteProperty("inkSaveVersion", kInkSaveStateVersion);
+
+            // Not using this right now, but could do in future.
+            writer.WriteProperty("inkFormatVersion", Story.inkVersionCurrent);
+
+            writer.WriteObjectEnd();
+        }
+
+
+        void LoadJsonObj(Dictionary<string, object> jObject)
+        {
+			object jSaveVersion = null;
+			if (!jObject.TryGetValue("inkSaveVersion", out jSaveVersion)) {
+                throw new Exception ("ink save format incorrect, can't load.");
+            }
+            else if ((int)jSaveVersion < kMinCompatibleLoadVersion) {
+                throw new Exception("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+kMinCompatibleLoadVersion+"), so can't load.");
+            }
+
+            // Flows: Always exists in latest format (even if there's just one default)
+            // but this dictionary doesn't exist in prev format
+            object flowsObj = null;
+            if (jObject.TryGetValue("flows", out flowsObj)) {
+                var flowsObjDict = (Dictionary<string, object>)flowsObj;
+                
+                // Single default flow
+                if( flowsObjDict.Count == 1 )
+                    _namedFlows = null;
+
+                // Multi-flow, need to create flows dict
+                else if( _namedFlows == null )
+                    _namedFlows = new Dictionary<string, Flow>();
+
+                // Multi-flow, already have a flows dict
+                else
+                    _namedFlows.Clear();
+
+                // Load up each flow (there may only be one)
+                foreach(var namedFlowObj in flowsObjDict) {
+                    var name = namedFlowObj.Key;
+                    var flowObj = (Dictionary<string, object>)namedFlowObj.Value;
+
+                    // Load up this flow using JSON data
+                    var flow = new Flow(name, story, flowObj);
+
+                    if( flowsObjDict.Count == 1 ) {
+                        _currentFlow = new Flow(name, story, flowObj);
+                    } else {
+                        _namedFlows[name] = flow;
+                    }
+                }
+
+                if( _namedFlows != null && _namedFlows.Count > 1 ) {
+                    var currFlowName = (string)jObject["currentFlowName"];
+                    _currentFlow = _namedFlows[currFlowName];
+                }
+            }
+
+            // Old format: individually load up callstack, output stream, choices in current/default flow
+            else {
+                _namedFlows = null;
+                _currentFlow.name = kDefaultFlowName;
+                _currentFlow.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstackThreads"], story);
+                _currentFlow.outputStream = Json.JArrayToRuntimeObjList ((List<object>)jObject ["outputStream"]);
+                _currentFlow.currentChoices = Json.JArrayToRuntimeObjList<Choice>((List<object>)jObject ["currentChoices"]);
+
+                object jChoiceThreadsObj = null;
+                jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj);
+                _currentFlow.LoadFlowChoiceThreads((Dictionary<string, object>)jChoiceThreadsObj, story);
+            }
+
+            OutputStreamDirty();
+
+            variablesState.SetJsonToken((Dictionary < string, object> )jObject["variablesState"]);
+            variablesState.callStack = _currentFlow.callStack;
+
+            evaluationStack = Json.JArrayToRuntimeObjList ((List<object>)jObject ["evalStack"]);
+
+
+			object currentDivertTargetPath;
+			if (jObject.TryGetValue("currentDivertTarget", out currentDivertTargetPath)) {
+                var divertPath = new Path (currentDivertTargetPath.ToString ());
+                divertedPointer = story.PointerAtPath (divertPath);
+            }
+                
+            _visitCounts = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["visitCounts"]);
+            _turnIndices = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["turnIndices"]);
+
+            currentTurnIndex = (int)jObject ["turnIdx"];
+            storySeed = (int)jObject ["storySeed"];
+
+            // Not optional, but bug in inkjs means it's actually missing in inkjs saves
+            object previousRandomObj = null;
+            if( jObject.TryGetValue("previousRandom", out previousRandomObj) ) {
+                previousRandom = (int)previousRandomObj;
+            } else {
+                previousRandom = 0;
+            }
+        }
+            
+        public void ResetErrors()
+        {
+            currentErrors = null;
+            currentWarnings = null;
+        }
+            
+        public void ResetOutput(List<Runtime.Object> objs = null)
+        {
+            outputStream.Clear ();
+            if( objs != null ) outputStream.AddRange (objs);
+			OutputStreamDirty();
+        }
+
+        // Push to output stream, but split out newlines in text for consistency
+        // in dealing with them later.
+        public void PushToOutputStream(Runtime.Object obj)
+        {
+            var text = obj as StringValue;
+            if (text) {
+                var listText = TrySplittingHeadTailWhitespace (text);
+                if (listText != null) {
+                    foreach (var textObj in listText) {
+                        PushToOutputStreamIndividual (textObj);
+                    }
+                    OutputStreamDirty();
+                    return;
+                }
+            }
+
+            PushToOutputStreamIndividual (obj);
+
+			OutputStreamDirty();
+        }
+
+        public void PopFromOutputStream (int count)
+        {
+            outputStream.RemoveRange (outputStream.Count - count, count);
+            OutputStreamDirty ();
+        }
+
+
+        // At both the start and the end of the string, split out the new lines like so:
+        //
+        //  "   \n  \n     \n  the string \n is awesome \n     \n     "
+        //      ^-----------^                           ^-------^
+        // 
+        // Excess newlines are converted into single newlines, and spaces discarded.
+        // Outside spaces are significant and retained. "Interior" newlines within 
+        // the main string are ignored, since this is for the purpose of gluing only.
+        //
+        //  - If no splitting is necessary, null is returned.
+        //  - A newline on its own is returned in a list for consistency.
+        List<Runtime.StringValue> TrySplittingHeadTailWhitespace(Runtime.StringValue single)
+        {
+            string str = single.value;
+
+            int headFirstNewlineIdx = -1;
+            int headLastNewlineIdx = -1;
+            for (int i = 0; i < str.Length; i++) {
+                char c = str [i];
+                if (c == '\n') {
+                    if (headFirstNewlineIdx == -1)
+                        headFirstNewlineIdx = i;
+                    headLastNewlineIdx = i;
+                }
+                else if (c == ' ' || c == '\t')
+                    continue;
+                else
+                    break;
+            }
+
+            int tailLastNewlineIdx = -1;
+            int tailFirstNewlineIdx = -1;
+            for (int i = str.Length-1; i >= 0; i--) {
+                char c = str [i];
+                if (c == '\n') {
+                    if (tailLastNewlineIdx == -1)
+                        tailLastNewlineIdx = i;
+                    tailFirstNewlineIdx = i;
+                }
+                else if (c == ' ' || c == '\t')
+                    continue;
+                else
+                    break;
+            }
+
+            // No splitting to be done?
+            if (headFirstNewlineIdx == -1 && tailLastNewlineIdx == -1)
+                return null;
+                
+            var listTexts = new List<Runtime.StringValue> ();
+            int innerStrStart = 0;
+            int innerStrEnd = str.Length;
+
+            if (headFirstNewlineIdx != -1) {
+                if (headFirstNewlineIdx > 0) {
+                    var leadingSpaces = new StringValue (str.Substring (0, headFirstNewlineIdx));
+                    listTexts.Add(leadingSpaces);
+                }
+                listTexts.Add (new StringValue ("\n"));
+                innerStrStart = headLastNewlineIdx + 1;
+            }
+
+            if (tailLastNewlineIdx != -1) {
+                innerStrEnd = tailFirstNewlineIdx;
+            }
+
+            if (innerStrEnd > innerStrStart) {
+                var innerStrText = str.Substring (innerStrStart, innerStrEnd - innerStrStart);
+                listTexts.Add (new StringValue (innerStrText));
+            }
+
+            if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) {
+                listTexts.Add (new StringValue ("\n"));
+                if (tailLastNewlineIdx < str.Length - 1) {
+                    int numSpaces = (str.Length - tailLastNewlineIdx) - 1;
+                    var trailingSpaces = new StringValue (str.Substring (tailLastNewlineIdx + 1, numSpaces));
+                    listTexts.Add(trailingSpaces);
+                }
+            }
+
+            return listTexts;
+        }
+
+        void PushToOutputStreamIndividual(Runtime.Object obj)
+        {
+            var glue = obj as Runtime.Glue;
+            var text = obj as Runtime.StringValue;
+
+            bool includeInOutput = true;
+
+            // New glue, so chomp away any whitespace from the end of the stream
+            if (glue) {
+                TrimNewlinesFromOutputStream();
+                includeInOutput = true;
+            }
+
+            // New text: do we really want to append it, if it's whitespace?
+            // Two different reasons for whitespace to be thrown away:
+            //   - Function start/end trimming
+            //   - User defined glue: <>
+            // We also need to know when to stop trimming, when there's non-whitespace.
+            else if( text ) {
+
+                // Where does the current function call begin?
+                var functionTrimIndex = -1;
+                var currEl = callStack.currentElement;
+                if (currEl.type == PushPopType.Function) {
+                    functionTrimIndex = currEl.functionStartInOuputStream;
+                }
+
+                // Do 2 things:
+                //  - Find latest glue
+                //  - Check whether we're in the middle of string evaluation
+                // If we're in string eval within the current function, we
+                // don't want to trim back further than the length of the current string.
+                int glueTrimIndex = -1;
+                for (int i = outputStream.Count - 1; i >= 0; i--) {
+                    var o = outputStream [i];
+                    var c = o as ControlCommand;
+                    var g = o as Glue;
+
+                    // Find latest glue
+                    if (g) {
+                        glueTrimIndex = i;
+                        break;
+                    } 
+
+                    // Don't function-trim past the start of a string evaluation section
+                    else if (c && c.commandType == ControlCommand.CommandType.BeginString) {
+                        if (i >= functionTrimIndex) {
+                            functionTrimIndex = -1;
+                        }
+                        break;
+                    }
+                }
+
+                // Where is the most agressive (earliest) trim point?
+                var trimIndex = -1;
+                if (glueTrimIndex != -1 && functionTrimIndex != -1)
+                    trimIndex = Math.Min (functionTrimIndex, glueTrimIndex);
+                else if (glueTrimIndex != -1)
+                    trimIndex = glueTrimIndex;
+                else
+                    trimIndex = functionTrimIndex;
+
+                // So, are we trimming then?
+                if (trimIndex != -1) {
+
+                    // While trimming, we want to throw all newlines away,
+                    // whether due to glue or the start of a function
+                    if (text.isNewline) {
+                        includeInOutput = false;
+                    } 
+
+                    // Able to completely reset when normal text is pushed
+                    else if (text.isNonWhitespace) {
+
+                        if( glueTrimIndex > -1 )
+                            RemoveExistingGlue ();
+
+                        // Tell all functions in callstack that we have seen proper text,
+                        // so trimming whitespace at the start is done.
+                        if (functionTrimIndex > -1) {
+                            var callstackElements = callStack.elements;
+                            for (int i = callstackElements.Count - 1; i >= 0; i--) {
+                                var el = callstackElements [i];
+                                if (el.type == PushPopType.Function) {
+                                    el.functionStartInOuputStream = -1;
+                                } else {
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                } 
+
+                // De-duplicate newlines, and don't ever lead with a newline
+                else if (text.isNewline) {
+                    if (outputStreamEndsInNewline || !outputStreamContainsContent)
+                        includeInOutput = false;
+                }
+            }
+
+            if (includeInOutput) {
+                outputStream.Add (obj);
+                OutputStreamDirty();
+            }
+        }
+
+        void TrimNewlinesFromOutputStream()
+        {
+            int removeWhitespaceFrom = -1;
+
+            // Work back from the end, and try to find the point where
+            // we need to start removing content.
+            //  - Simply work backwards to find the first newline in a string of whitespace
+            // e.g. This is the content   \n   \n\n
+            //                            ^---------^ whitespace to remove
+            //                        ^--- first while loop stops here
+            int i = outputStream.Count-1;
+            while (i >= 0) {
+                var obj = outputStream [i];
+                var cmd = obj as ControlCommand;
+                var txt = obj as StringValue;
+
+                if (cmd || (txt && txt.isNonWhitespace)) {
+                    break;
+                } 
+                else if (txt && txt.isNewline) {
+                    removeWhitespaceFrom = i;
+                }
+                i--;
+            }
+
+            // Remove the whitespace
+            if (removeWhitespaceFrom >= 0) {
+                i=removeWhitespaceFrom;
+                while(i < outputStream.Count) {
+                    var text = outputStream [i] as StringValue;
+                    if (text) {
+                        outputStream.RemoveAt (i);
+                    } else {
+                        i++;
+                    }
+                }
+            }
+
+			OutputStreamDirty();
+        }
+
+        // Only called when non-whitespace is appended
+        void RemoveExistingGlue()
+        {
+            for (int i = outputStream.Count - 1; i >= 0; i--) {
+                var c = outputStream [i];
+                if (c is Glue) {
+                    outputStream.RemoveAt (i);
+                } else if( c is ControlCommand ) { // e.g. BeginString
+                    break;
+                }
+            }
+
+			OutputStreamDirty();
+        }
+
+        public bool outputStreamEndsInNewline {
+            get {
+                if (outputStream.Count > 0) {
+
+                    for (int i = outputStream.Count - 1; i >= 0; i--) {
+                        var obj = outputStream [i];
+                        if (obj is ControlCommand) // e.g. BeginString
+                            break;
+                        var text = outputStream [i] as StringValue;
+                        if (text) {
+                            if (text.isNewline)
+                                return true;
+                            else if (text.isNonWhitespace)
+                                break;
+                        }
+                    }
+                }
+
+                return false;
+            }
+        }
+
+        public bool outputStreamContainsContent {
+            get {
+                foreach (var content in outputStream) {
+                    if (content is StringValue)
+                        return true;
+                }
+                return false;
+            }
+        }
+
+        public bool inStringEvaluation {
+            get {
+                for (int i = outputStream.Count - 1; i >= 0; i--) {
+                    var cmd = outputStream [i] as ControlCommand;
+                    if (cmd && cmd.commandType == ControlCommand.CommandType.BeginString) {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+        }
+
+        public void PushEvaluationStack(Runtime.Object obj)
+        {
+            // Include metadata about the origin List for list values when
+            // they're used, so that lower level functions can make use
+            // of the origin list to get related items, or make comparisons
+            // with the integer values etc.
+            var listValue = obj as ListValue;
+            if (listValue) {
+                
+                // Update origin when list is has something to indicate the list origin
+                var rawList = listValue.value;
+				if (rawList.originNames != null) {
+					if( rawList.origins == null ) rawList.origins = new List<ListDefinition>();
+					rawList.origins.Clear();
+
+					foreach (var n in rawList.originNames) {
+                        ListDefinition def = null;
+                        story.listDefinitions.TryListGetDefinition (n, out def);
+						if( !rawList.origins.Contains(def) )
+							rawList.origins.Add (def);
+                    }
+                }
+            }
+
+            evaluationStack.Add(obj);
+        }
+
+        public Runtime.Object PopEvaluationStack()
+        {
+            var obj = evaluationStack [evaluationStack.Count - 1];
+            evaluationStack.RemoveAt (evaluationStack.Count - 1);
+            return obj;
+        }
+
+        public Runtime.Object PeekEvaluationStack()
+        {
+            return evaluationStack [evaluationStack.Count - 1];
+        }
+
+        public List<Runtime.Object> PopEvaluationStack(int numberOfObjects)
+        {
+            if(numberOfObjects > evaluationStack.Count) {
+                throw new System.Exception ("trying to pop too many objects");
+            }
+
+            var popped = evaluationStack.GetRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
+            evaluationStack.RemoveRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
+            return popped;
+        }
+
+        /// <summary>
+        /// Ends the current ink flow, unwrapping the callstack but without
+        /// affecting any variables. Useful if the ink is (say) in the middle
+        /// a nested tunnel, and you want it to reset so that you can divert
+        /// elsewhere using ChoosePathString(). Otherwise, after finishing
+        /// the content you diverted to, it would continue where it left off.
+        /// Calling this is equivalent to calling -> END in ink.
+        /// </summary>
+        public void ForceEnd()
+        {
+            callStack.Reset();
+
+			_currentFlow.currentChoices.Clear();
+
+            currentPointer = Pointer.Null;
+            previousPointer = Pointer.Null;
+
+            didSafeExit = true;
+        }
+
+        // Add the end of a function call, trim any whitespace from the end.
+        // We always trim the start and end of the text that a function produces.
+        // The start whitespace is discard as it is generated, and the end
+        // whitespace is trimmed in one go here when we pop the function.
+        void TrimWhitespaceFromFunctionEnd ()
+        {
+            Debug.Assert (callStack.currentElement.type == PushPopType.Function);
+
+            var functionStartPoint = callStack.currentElement.functionStartInOuputStream;
+
+            // If the start point has become -1, it means that some non-whitespace
+            // text has been pushed, so it's safe to go as far back as we're able.
+            if (functionStartPoint == -1) {
+                functionStartPoint = 0;
+            }
+
+            // Trim whitespace from END of function call
+            for (int i = outputStream.Count - 1; i >= functionStartPoint; i--) {
+                var obj = outputStream [i];
+                var txt = obj as StringValue;
+                var cmd = obj as ControlCommand;
+                if (!txt) continue;
+                if (cmd) break;
+
+                if (txt.isNewline || txt.isInlineWhitespace) {
+                    outputStream.RemoveAt (i);
+                    OutputStreamDirty ();
+                } else {
+                    break;
+                }
+            }
+        }
+
+        public void PopCallstack (PushPopType? popType = null)
+        {
+            // Add the end of a function call, trim any whitespace from the end.
+            if (callStack.currentElement.type == PushPopType.Function)
+                TrimWhitespaceFromFunctionEnd ();
+
+            callStack.Pop (popType);
+        }
+
+        // Don't make public since the method need to be wrapped in Story for visit counting
+        public void SetChosenPath(Path path, bool incrementingTurnIndex)
+        {
+            // Changing direction, assume we need to clear current set of choices
+			_currentFlow.currentChoices.Clear ();
+
+            var newPointer = story.PointerAtPath (path);
+            if (!newPointer.isNull && newPointer.index == -1)
+                newPointer.index = 0;
+
+            currentPointer = newPointer;
+
+            if( incrementingTurnIndex )
+                currentTurnIndex++;
+        }
+
+        public void StartFunctionEvaluationFromGame (Container funcContainer, params object[] arguments)
+        {
+            callStack.Push (PushPopType.FunctionEvaluationFromGame, evaluationStack.Count);
+            callStack.currentElement.currentPointer = Pointer.StartOf (funcContainer);
+
+            PassArgumentsToEvaluationStack (arguments);
+        }
+
+        public void PassArgumentsToEvaluationStack (params object [] arguments)
+        {
+            // Pass arguments onto the evaluation stack
+            if (arguments != null) {
+                for (int i = 0; i < arguments.Length; i++) {
+                    if (!(arguments [i] is int || arguments [i] is float || arguments [i] is string || arguments [i] is InkList)) {
+                        throw new System.ArgumentException ("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be int, float, string or InkList. Argument was "+(arguments [i] == null ? "null" : arguments [i].GetType().Name));
+                    }
+
+                    PushEvaluationStack (Runtime.Value.Create (arguments [i]));
+                }
+            }
+        }
+            
+        public bool TryExitFunctionEvaluationFromGame ()
+        {
+            if( callStack.currentElement.type == PushPopType.FunctionEvaluationFromGame ) {
+                currentPointer = Pointer.Null;
+                didSafeExit = true;
+                return true;
+            }
+
+            return false;
+        }
+
+        public object CompleteFunctionEvaluationFromGame ()
+        {
+            if (callStack.currentElement.type != PushPopType.FunctionEvaluationFromGame) {
+                throw new Exception ("Expected external function evaluation to be complete. Stack trace: "+callStack.callStackTrace);
+            }
+
+            int originalEvaluationStackHeight = callStack.currentElement.evaluationStackHeightWhenPushed;
+            
+            // Do we have a returned value?
+            // Potentially pop multiple values off the stack, in case we need
+            // to clean up after ourselves (e.g. caller of EvaluateFunction may 
+            // have passed too many arguments, and we currently have no way to check for that)
+            Runtime.Object returnedObj = null;
+            while (evaluationStack.Count > originalEvaluationStackHeight) {
+                var poppedObj = PopEvaluationStack ();
+                if (returnedObj == null)
+                    returnedObj = poppedObj;
+            }
+
+            // Finally, pop the external function evaluation
+            PopCallstack (PushPopType.FunctionEvaluationFromGame);
+
+            // What did we get back?
+            if (returnedObj) {
+                if (returnedObj is Runtime.Void)
+                    return null;
+
+                // Some kind of value, if not void
+                var returnVal = returnedObj as Runtime.Value;
+
+                // DivertTargets get returned as the string of components
+                // (rather than a Path, which isn't public)
+                if (returnVal.valueType == ValueType.DivertTarget) {
+                    return returnVal.valueObject.ToString ();
+                }
+
+                // Other types can just have their exact object type:
+                // int, float, string. VariablePointers get returned as strings.
+                return returnVal.valueObject;
+            }
+
+            return null;
+        }
+
+        public void AddError(string message, bool isWarning)
+        {
+            if (!isWarning) {
+                if (currentErrors == null) currentErrors = new List<string> ();
+                currentErrors.Add (message);
+            } else {
+                if (currentWarnings == null) currentWarnings = new List<string> ();
+                currentWarnings.Add (message);
+            }
+        }
+
+		void OutputStreamDirty()
+		{
+			_outputStreamTextDirty = true;
+			_outputStreamTagsDirty = true;
+		}
+
+        // REMEMBER! REMEMBER! REMEMBER!
+        // When adding state, update the Copy method and serialisation
+        // REMEMBER! REMEMBER! REMEMBER!
+
+
+        Dictionary<string, int> _visitCounts;
+        Dictionary<string, int> _turnIndices;
+		bool _outputStreamTextDirty = true;
+		bool _outputStreamTagsDirty = true;
+
+        StatePatch _patch;
+
+        Flow _currentFlow;
+        Dictionary<string, Flow> _namedFlows;
+        const string kDefaultFlowName = "DEFAULT_FLOW";
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/StoryState.cs.meta b/Assets/Ink/InkLibs/InkRuntime/StoryState.cs.meta
new file mode 100644
index 0000000..e84e913
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StoryState.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 69b5ba5009dea4a3d8f67fbe1ebbc902
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs b/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs
new file mode 100644
index 0000000..d650fae
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Ink.Runtime
+{
+    public static class StringExt
+    {
+        public static string Join<T>(string separator, List<T> objects)
+        {
+            var sb = new StringBuilder ();
+
+            var isFirst = true;
+            foreach (var o in objects) {
+
+                if (!isFirst)
+                    sb.Append (separator);
+
+                sb.Append (o.ToString ());
+
+                isFirst = false;
+            }
+
+            return sb.ToString ();
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs.meta b/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs.meta
new file mode 100644
index 0000000..170d0d4
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/StringJoinExtension.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4c907c30d0b7c4e5aa2dfe477bba8a89
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Tag.cs b/Assets/Ink/InkLibs/InkRuntime/Tag.cs
new file mode 100644
index 0000000..04c31fd
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Tag.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Ink.Runtime
+{
+    public class Tag : Runtime.Object
+    {
+        public string text { get; private set; }
+
+        public Tag (string tagText)
+        {
+            this.text = tagText;
+        }
+
+        public override string ToString ()
+        {
+            return "# " + text;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Tag.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Tag.cs.meta
new file mode 100644
index 0000000..8978b1f
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Tag.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b8a631dc795b748dfbe8f53304cd9675
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Value.cs b/Assets/Ink/InkLibs/InkRuntime/Value.cs
new file mode 100644
index 0000000..f870dff
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Value.cs
@@ -0,0 +1,405 @@
+using System.ComponentModel;
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    // Order is significant for type coersion.
+    // If types aren't directly compatible for an operation,
+    // they're coerced to the same type, downward.
+    // Higher value types "infect" an operation.
+    // (This may not be the most sensible thing to do, but it's worked so far!)
+    public enum ValueType
+    {
+        // Bool is new addition, keep enum values the same, with Int==0, Float==1 etc,
+        // but for coersion rules, we want to keep bool with a lower value than Int
+        // so that it converts in the right direction
+        Bool = -1, 
+        // Used in coersion
+        Int,
+        Float,
+        List,
+        String,
+
+        // Not used for coersion described above
+        DivertTarget,
+        VariablePointer
+    }
+
+    public abstract class Value : Runtime.Object
+    {
+        public abstract ValueType valueType { get; }
+        public abstract bool isTruthy { get; }
+
+        public abstract Value Cast(ValueType newType);
+
+        public abstract object valueObject { get; }
+
+        public static Value Create(object val)
+        {
+            // Implicitly lose precision from any doubles we get passed in
+            if (val is double) {
+                double doub = (double)val;
+                val = (float)doub;
+            }
+
+            if( val is bool ) {
+                return new BoolValue((bool)val);
+            } else if (val is int) {
+                return new IntValue ((int)val);
+            } else if (val is long) {
+                return new IntValue ((int)(long)val);
+            } else if (val is float) {
+                return new FloatValue ((float)val);
+            } else if (val is double) {
+                return new FloatValue ((float)(double)val);
+            } else if (val is string) {
+                return new StringValue ((string)val);
+            } else if (val is Path) {
+                return new DivertTargetValue ((Path)val);
+            } else if (val is InkList) {
+                return new ListValue ((InkList)val);
+            }
+
+            return null;
+        }
+
+        public override Object Copy()
+        {
+            return Create (valueObject);
+        }
+
+        protected StoryException BadCastException (ValueType targetType)
+        {
+            return new StoryException ("Can't cast "+this.valueObject+" from " + this.valueType+" to "+targetType);
+        }
+    }
+
+    public abstract class Value<T> : Value
+    {
+        public T value { get; set; }
+
+        public override object valueObject {
+            get {
+                return (object)value;
+            }
+        }
+
+        public Value (T val)
+        {
+            value = val;
+        }
+
+        public override string ToString ()
+        {
+            return value.ToString();
+        }
+    }
+
+    public class BoolValue : Value<bool>
+    {
+        public override ValueType valueType { get { return ValueType.Bool; } }
+        public override bool isTruthy { get { return value; } }
+
+        public BoolValue(bool boolVal) : base(boolVal)
+        {
+        }
+
+        public BoolValue() : this(false) {}
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType) {
+                return this;
+            }
+
+            if (newType == ValueType.Int) {
+                return new IntValue (this.value ? 1 : 0);
+            }
+
+            if (newType == ValueType.Float) {
+                return new FloatValue (this.value ? 1.0f : 0.0f);
+            }
+
+            if (newType == ValueType.String) {
+                return new StringValue(this.value ? "true" : "false");
+            }
+
+            throw BadCastException (newType);
+        }
+
+        public override string ToString ()
+        {
+            // Instead of C# "True" / "False"
+            return value ? "true" : "false";
+        }
+    }
+
+    public class IntValue : Value<int>
+    {
+        public override ValueType valueType { get { return ValueType.Int; } }
+        public override bool isTruthy { get { return value != 0; } }
+
+        public IntValue(int intVal) : base(intVal)
+        {
+        }
+
+        public IntValue() : this(0) {}
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType) {
+                return this;
+            }
+
+            if (newType == ValueType.Bool) {
+                return new BoolValue (this.value == 0 ? false : true);
+            }
+
+            if (newType == ValueType.Float) {
+                return new FloatValue ((float)this.value);
+            }
+
+            if (newType == ValueType.String) {
+                return new StringValue("" + this.value);
+            }
+
+            throw BadCastException (newType);
+        }
+    }
+
+    public class FloatValue : Value<float>
+    {
+        public override ValueType valueType { get { return ValueType.Float; } }
+        public override bool isTruthy { get { return value != 0.0f; } }
+
+        public FloatValue(float val) : base(val)
+        {
+        }
+
+        public FloatValue() : this(0.0f) {}
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType) {
+                return this;
+            }
+
+            if (newType == ValueType.Bool) {
+                return new BoolValue (this.value == 0.0f ? false : true);
+            }
+
+            if (newType == ValueType.Int) {
+                return new IntValue ((int)this.value);
+            }
+
+            if (newType == ValueType.String) {
+                return new StringValue("" + this.value.ToString(System.Globalization.CultureInfo.InvariantCulture));
+            }
+
+            throw BadCastException (newType);
+        }
+    }
+
+    public class StringValue : Value<string>
+    {
+        public override ValueType valueType { get { return ValueType.String; } }
+        public override bool isTruthy { get { return value.Length > 0; } }
+
+        public bool isNewline { get; private set; }
+        public bool isInlineWhitespace { get; private set; }
+        public bool isNonWhitespace {
+            get {
+                return !isNewline && !isInlineWhitespace;
+            }
+        }
+
+        public StringValue(string str) : base(str)
+        {
+            // Classify whitespace status
+            isNewline = value == "\n";
+            isInlineWhitespace = true;
+            foreach (var c in value) {
+                if (c != ' ' && c != '\t') {
+                    isInlineWhitespace = false;
+                    break;
+                }
+            }
+        }
+
+        public StringValue() : this("") {}
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType) {
+                return this;
+            }
+
+            if (newType == ValueType.Int) {
+
+                int parsedInt;
+                if (int.TryParse (value, out parsedInt)) {
+                    return new IntValue (parsedInt);
+                } else {
+                    return null;
+                }
+            }
+
+            if (newType == ValueType.Float) {
+                float parsedFloat;
+                if (float.TryParse (value, System.Globalization.NumberStyles.Float ,System.Globalization.CultureInfo.InvariantCulture, out parsedFloat)) {
+                    return new FloatValue (parsedFloat);
+                } else {
+                    return null;
+                }
+            }
+
+            throw BadCastException (newType);
+        }
+    }
+
+    public class DivertTargetValue : Value<Path>
+    {
+        public Path targetPath { get { return this.value; } set { this.value = value; } }
+        public override ValueType valueType { get { return ValueType.DivertTarget; } }
+        public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a divert target"); } }
+            
+        public DivertTargetValue(Path targetPath) : base(targetPath)
+        {
+        }
+
+        public DivertTargetValue() : base(null)
+        {}
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType)
+                return this;
+            
+            throw BadCastException (newType);
+        }
+
+        public override string ToString ()
+        {
+            return "DivertTargetValue(" + targetPath + ")";
+        }
+    }
+
+    // TODO: Think: Erm, I get that this contains a string, but should
+    // we really derive from Value<string>? That seems a bit misleading to me.
+    public class VariablePointerValue : Value<string>
+    {
+        public string variableName { get { return this.value; } set { this.value = value; } }
+        public override ValueType valueType { get { return ValueType.VariablePointer; } }
+        public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a variable pointer"); } }
+
+        // Where the variable is located
+        // -1 = default, unknown, yet to be determined
+        // 0  = in global scope
+        // 1+ = callstack element index + 1 (so that the first doesn't conflict with special global scope)
+        public int contextIndex { get; set; }
+
+        public VariablePointerValue(string variableName, int contextIndex = -1) : base(variableName)
+        {
+            this.contextIndex = contextIndex;
+        }
+
+        public VariablePointerValue() : this(null)
+        {
+        }
+
+        public override Value Cast(ValueType newType)
+        {
+            if (newType == valueType)
+                return this;
+
+            throw BadCastException (newType);
+        }
+
+        public override string ToString ()
+        {
+            return "VariablePointerValue(" + variableName + ")";
+        }
+
+        public override Object Copy()
+        {
+            return new VariablePointerValue (variableName, contextIndex);
+        }
+    }
+
+    public class ListValue : Value<InkList>
+    {
+        public override ValueType valueType {
+            get {
+                return ValueType.List;
+            }
+        }
+
+        // Truthy if it is non-empty
+        public override bool isTruthy {
+            get {
+                return value.Count > 0;
+            }
+        }
+                
+        public override Value Cast (ValueType newType)
+        {
+            if (newType == ValueType.Int) {
+                var max = value.maxItem;
+                if( max.Key.isNull )
+                    return new IntValue (0);
+                else
+                    return new IntValue (max.Value);
+            }
+
+            else if (newType == ValueType.Float) {
+                var max = value.maxItem;
+                if (max.Key.isNull)
+                    return new FloatValue (0.0f);
+                else
+                    return new FloatValue ((float)max.Value);
+            }
+
+            else if (newType == ValueType.String) {
+                var max = value.maxItem;
+                if (max.Key.isNull)
+                    return new StringValue ("");
+                else {
+                    return new StringValue (max.Key.ToString());
+                }
+            }
+
+            if (newType == valueType)
+                return this;
+
+            throw BadCastException (newType);
+        }
+
+        public ListValue () : base(null) {
+            value = new InkList ();
+        }
+
+        public ListValue (InkList list) : base (null)
+        {
+            value = new InkList (list);
+        }
+
+        public ListValue (InkListItem singleItem, int singleValue) : base (null)
+        {
+            value = new InkList {
+                {singleItem, singleValue}
+            };
+        }
+
+        public static void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue)
+        {
+            var oldList = oldValue as ListValue;
+            var newList = newValue as ListValue;
+
+            // When assigning the emtpy list, try to retain any initial origin names
+            if (oldList && newList && newList.value.Count == 0)
+                newList.value.SetInitialOriginNames (oldList.value.originNames);
+        }
+    }
+        
+}
+ 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Value.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Value.cs.meta
new file mode 100644
index 0000000..7131cdb
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Value.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57357686b389f45c9ad0659798094c5d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs b/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs
new file mode 100644
index 0000000..1c3a433
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+
+namespace Ink.Runtime
+{
+    // The value to be assigned is popped off the evaluation stack, so no need to keep it here
+    public class VariableAssignment : Runtime.Object
+    {
+        public string variableName { get; protected set; }
+        public bool isNewDeclaration { get; protected set; }
+        public bool isGlobal { get; set; }
+
+        public VariableAssignment (string variableName, bool isNewDeclaration)
+        {
+            this.variableName = variableName;
+            this.isNewDeclaration = isNewDeclaration;
+        }
+
+        // Require default constructor for serialisation
+        public VariableAssignment() : this(null, false) {}
+
+        public override string ToString ()
+        {
+            return "VarAssign to " + variableName;
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs.meta b/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs.meta
new file mode 100644
index 0000000..200a51a
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariableAssignment.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fb2c978a16e604e18b1c0ca3ea64db84
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs b/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs
new file mode 100644
index 0000000..7403312
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs
@@ -0,0 +1,51 @@
+namespace Ink.Runtime
+{
+    public class VariableReference : Runtime.Object
+    {
+        // Normal named variable
+        public string name { get; set; }
+
+        // Variable reference is actually a path for a visit (read) count
+        public Path pathForCount { get; set; }
+
+        public Container containerForCount {
+            get {
+                return this.ResolvePath (pathForCount).container;
+            }
+        }
+            
+        public string pathStringForCount { 
+            get {
+                if( pathForCount == null )
+                    return null;
+
+                return CompactPathString(pathForCount);
+            }
+            set {
+                if (value == null)
+                    pathForCount = null;
+                else
+                    pathForCount = new Path (value);
+            }
+        }
+
+        public VariableReference (string name)
+        {
+            this.name = name;
+        }
+
+        // Require default constructor for serialisation
+        public VariableReference() {}
+
+        public override string ToString ()
+        {
+            if (name != null) {
+                return string.Format ("var({0})", name);
+            } else {
+                var pathStr = pathStringForCount;
+                return string.Format("read_count({0})", pathStr);
+            }
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs.meta b/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs.meta
new file mode 100644
index 0000000..a7a515c
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariableReference.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c9a8ec523da34a6196730896d7a2c1d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs b/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs
new file mode 100644
index 0000000..7ddab78
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs
@@ -0,0 +1,417 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ink.Runtime
+{
+    /// <summary>
+    /// Encompasses all the global variables in an ink Story, and
+    /// allows binding of a VariableChanged event so that that game
+    /// code can be notified whenever the global variables change.
+    /// </summary>
+	public class VariablesState : IEnumerable<string>
+    {
+        public delegate void VariableChanged(string variableName, Runtime.Object newValue);
+        public event VariableChanged variableChangedEvent;
+
+        public StatePatch patch;
+
+        public bool batchObservingVariableChanges 
+        { 
+            get {
+                return _batchObservingVariableChanges;
+            }
+            set { 
+                _batchObservingVariableChanges = value;
+                if (value) {
+                    _changedVariablesForBatchObs = new HashSet<string> ();
+                } 
+
+                // Finished observing variables in a batch - now send 
+                // notifications for changed variables all in one go.
+                else {
+                    if (_changedVariablesForBatchObs != null) {
+                        foreach (var variableName in _changedVariablesForBatchObs) {
+                            var currentValue = _globalVariables [variableName];
+                            variableChangedEvent (variableName, currentValue);
+                        }
+                    }
+
+                    _changedVariablesForBatchObs = null;
+                }
+            }
+        }
+        bool _batchObservingVariableChanges;
+
+        // Allow StoryState to change the current callstack, e.g. for
+        // temporary function evaluation.
+        public CallStack callStack {
+            get {
+                return _callStack;
+            }
+            set {
+                _callStack = value;
+            }
+        }
+
+        /// <summary>
+        /// Get or set the value of a named global ink variable.
+        /// The types available are the standard ink types. Certain
+        /// types will be implicitly casted when setting.
+        /// For example, doubles to floats, longs to ints, and bools
+        /// to ints.
+        /// </summary>
+        public object this[string variableName]
+        {
+            get {
+                Runtime.Object varContents;
+
+                if (patch != null && patch.TryGetGlobal(variableName, out varContents))
+                    return (varContents as Runtime.Value).valueObject;
+
+                // Search main dictionary first.
+                // If it's not found, it might be because the story content has changed,
+                // and the original default value hasn't be instantiated.
+                // Should really warn somehow, but it's difficult to see how...!
+                if ( _globalVariables.TryGetValue (variableName, out varContents) || 
+                     _defaultGlobalVariables.TryGetValue(variableName, out varContents) )
+                    return (varContents as Runtime.Value).valueObject;
+                else {
+                    return null;
+                }
+            }
+            set {
+                if (!_defaultGlobalVariables.ContainsKey (variableName))
+                    throw new StoryException ("Cannot assign to a variable ("+variableName+") that hasn't been declared in the story");
+                
+                var val = Runtime.Value.Create(value);
+                if (val == null) {
+                    if (value == null) {
+                        throw new Exception ("Cannot pass null to VariableState");
+                    } else {
+                        throw new Exception ("Invalid value passed to VariableState: "+value.ToString());
+                    }
+                }
+
+                SetGlobal (variableName, val);
+            }
+        }
+
+		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+		{
+			return GetEnumerator();
+		}
+
+        /// <summary>
+        /// Enumerator to allow iteration over all global variables by name.
+        /// </summary>
+		public IEnumerator<string> GetEnumerator()
+		{
+			return _globalVariables.Keys.GetEnumerator();
+		}
+
+        public VariablesState (CallStack callStack, ListDefinitionsOrigin listDefsOrigin)
+        {
+            _globalVariables = new Dictionary<string, Object> ();
+            _callStack = callStack;
+            _listDefsOrigin = listDefsOrigin;
+        }
+
+        public void ApplyPatch()
+        {
+            foreach(var namedVar in patch.globals) {
+                _globalVariables[namedVar.Key] = namedVar.Value;
+            }
+
+            if(_changedVariablesForBatchObs != null ) {
+                foreach (var name in patch.changedVariables)
+                    _changedVariablesForBatchObs.Add(name);
+            }
+
+            patch = null;
+        }
+
+        public void SetJsonToken(Dictionary<string, object> jToken)
+        {
+            _globalVariables.Clear();
+
+            foreach (var varVal in _defaultGlobalVariables) {
+                object loadedToken;
+                if( jToken.TryGetValue(varVal.Key, out loadedToken) ) {
+                    _globalVariables[varVal.Key] = Json.JTokenToRuntimeObject(loadedToken);
+                } else {
+                    _globalVariables[varVal.Key] = varVal.Value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// When saving out JSON state, we can skip saving global values that
+        /// remain equal to the initial values that were declared in ink.
+        /// This makes the save file (potentially) much smaller assuming that
+        /// at least a portion of the globals haven't changed. However, it
+        /// can also take marginally longer to save in the case that the 
+        /// majority HAVE changed, since it has to compare all globals.
+        /// It may also be useful to turn this off for testing worst case
+        /// save timing.
+        /// </summary>
+        public static bool dontSaveDefaultValues = true;
+
+        public void WriteJson(SimpleJson.Writer writer)
+        {
+            writer.WriteObjectStart();
+            foreach (var keyVal in _globalVariables)
+            {
+                var name = keyVal.Key;
+                var val = keyVal.Value;
+
+                if(dontSaveDefaultValues) {
+                    // Don't write out values that are the same as the default global values
+                    Runtime.Object defaultVal;
+                    if (_defaultGlobalVariables.TryGetValue(name, out defaultVal))
+                    {
+                        if (RuntimeObjectsEqual(val, defaultVal))
+                            continue;
+                    }
+                }
+
+
+                writer.WritePropertyStart(name);
+                Json.WriteRuntimeObject(writer, val);
+                writer.WritePropertyEnd();
+            }
+            writer.WriteObjectEnd();
+        }
+
+        public bool RuntimeObjectsEqual(Runtime.Object obj1, Runtime.Object obj2)
+        {
+            if (obj1.GetType() != obj2.GetType()) return false;
+
+            // Perform equality on int/float/bool manually to avoid boxing
+            var boolVal = obj1 as BoolValue;
+            if( boolVal != null ) {
+                return boolVal.value == ((BoolValue)obj2).value;
+            }
+
+            var intVal = obj1 as IntValue;
+            if( intVal != null ) {
+                return intVal.value == ((IntValue)obj2).value;
+            }
+
+            var floatVal = obj1 as FloatValue;
+            if (floatVal != null)
+            {
+                return floatVal.value == ((FloatValue)obj2).value;
+            }
+
+            // Other Value type (using proper Equals: list, string, divert path)
+            var val1 = obj1 as Value;
+            var val2 = obj2 as Value;
+            if( val1 != null ) {
+                return val1.valueObject.Equals(val2.valueObject);
+            }
+
+            throw new System.Exception("FastRoughDefinitelyEquals: Unsupported runtime object type: "+obj1.GetType());
+        }
+
+        public Runtime.Object GetVariableWithName(string name)
+        {
+            return GetVariableWithName (name, -1);
+        }
+
+        public Runtime.Object TryGetDefaultVariableValue (string name)
+        {
+            Runtime.Object val = null;
+            _defaultGlobalVariables.TryGetValue (name, out val);
+            return val;
+        }
+
+		public bool GlobalVariableExistsWithName(string name)
+		{
+			return _globalVariables.ContainsKey(name) || _defaultGlobalVariables != null && _defaultGlobalVariables.ContainsKey(name);
+		}
+
+        Runtime.Object GetVariableWithName(string name, int contextIndex)
+        {
+            Runtime.Object varValue = GetRawVariableWithName (name, contextIndex);
+
+            // Get value from pointer?
+            var varPointer = varValue as VariablePointerValue;
+            if (varPointer) {
+                varValue = ValueAtVariablePointer (varPointer);
+            }
+
+            return varValue;
+        }
+
+        Runtime.Object GetRawVariableWithName(string name, int contextIndex)
+        {
+            Runtime.Object varValue = null;
+
+            // 0 context = global
+            if (contextIndex == 0 || contextIndex == -1) {
+                if (patch != null && patch.TryGetGlobal(name, out varValue))
+                    return varValue;
+
+                if ( _globalVariables.TryGetValue (name, out varValue) )
+                    return varValue;
+
+                // Getting variables can actually happen during globals set up since you can do
+                //  VAR x = A_LIST_ITEM
+                // So _defaultGlobalVariables may be null.
+                // We need to do this check though in case a new global is added, so we need to
+                // revert to the default globals dictionary since an initial value hasn't yet been set.
+                if( _defaultGlobalVariables != null && _defaultGlobalVariables.TryGetValue(name, out varValue) ) {
+                    return varValue;
+                }
+
+                var listItemValue = _listDefsOrigin.FindSingleItemListWithName (name);
+                if (listItemValue)
+                    return listItemValue;
+            } 
+
+            // Temporary
+            varValue = _callStack.GetTemporaryVariableWithName (name, contextIndex);
+
+            return varValue;
+        }
+
+        public Runtime.Object ValueAtVariablePointer(VariablePointerValue pointer)
+        {
+            return GetVariableWithName (pointer.variableName, pointer.contextIndex);
+        }
+
+        public void Assign(VariableAssignment varAss, Runtime.Object value)
+        {
+            var name = varAss.variableName;
+            int contextIndex = -1;
+
+            // Are we assigning to a global variable?
+            bool setGlobal = false;
+            if (varAss.isNewDeclaration) {
+                setGlobal = varAss.isGlobal;
+            } else {
+                setGlobal = GlobalVariableExistsWithName (name);
+            }
+
+            // Constructing new variable pointer reference
+            if (varAss.isNewDeclaration) {
+                var varPointer = value as VariablePointerValue;
+                if (varPointer) {
+                    var fullyResolvedVariablePointer = ResolveVariablePointer (varPointer);
+                    value = fullyResolvedVariablePointer;
+                }
+
+            } 
+
+            // Assign to existing variable pointer?
+            // Then assign to the variable that the pointer is pointing to by name.
+            else {
+
+                // De-reference variable reference to point to
+                VariablePointerValue existingPointer = null;
+                do {
+                    existingPointer = GetRawVariableWithName (name, contextIndex) as VariablePointerValue;
+                    if (existingPointer) {
+                        name = existingPointer.variableName;
+                        contextIndex = existingPointer.contextIndex;
+                        setGlobal = (contextIndex == 0);
+                    }
+                } while(existingPointer);
+            }
+
+
+            if (setGlobal) {
+                SetGlobal (name, value);
+            } else {
+                _callStack.SetTemporaryVariable (name, value, varAss.isNewDeclaration, contextIndex);
+            }
+        }
+
+        public void SnapshotDefaultGlobals ()
+        {
+            _defaultGlobalVariables = new Dictionary<string, Object> (_globalVariables);
+        }
+
+        void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue)
+        {
+            var oldList = oldValue as ListValue;
+            var newList = newValue as ListValue;
+            if (oldList && newList && newList.value.Count == 0)
+                newList.value.SetInitialOriginNames (oldList.value.originNames);
+        }
+
+        public void SetGlobal(string variableName, Runtime.Object value)
+        {
+            Runtime.Object oldValue = null;
+            if( patch == null || !patch.TryGetGlobal(variableName, out oldValue) )
+                _globalVariables.TryGetValue (variableName, out oldValue);
+
+            ListValue.RetainListOriginsForAssignment (oldValue, value);
+
+            if (patch != null)
+                patch.SetGlobal(variableName, value);
+            else
+                _globalVariables [variableName] = value;
+
+            if (variableChangedEvent != null && !value.Equals (oldValue)) {
+
+                if (batchObservingVariableChanges) {
+                    if (patch != null)
+                        patch.AddChangedVariable(variableName);
+                    else if(_changedVariablesForBatchObs != null)
+                        _changedVariablesForBatchObs.Add (variableName);
+                } else {
+                    variableChangedEvent (variableName, value);
+                }
+            }
+        }
+
+        // Given a variable pointer with just the name of the target known, resolve to a variable
+        // pointer that more specifically points to the exact instance: whether it's global,
+        // or the exact position of a temporary on the callstack.
+        VariablePointerValue ResolveVariablePointer(VariablePointerValue varPointer)
+        {
+            int contextIndex = varPointer.contextIndex;
+
+            if( contextIndex == -1 )
+                contextIndex = GetContextIndexOfVariableNamed (varPointer.variableName);
+
+            var valueOfVariablePointedTo = GetRawVariableWithName (varPointer.variableName, contextIndex);
+
+            // Extra layer of indirection:
+            // When accessing a pointer to a pointer (e.g. when calling nested or 
+            // recursive functions that take a variable references, ensure we don't create
+            // a chain of indirection by just returning the final target.
+            var doubleRedirectionPointer = valueOfVariablePointedTo as VariablePointerValue;
+            if (doubleRedirectionPointer) {
+                return doubleRedirectionPointer;
+            } 
+
+            // Make copy of the variable pointer so we're not using the value direct from
+            // the runtime. Temporary must be local to the current scope.
+            else {
+                return new VariablePointerValue (varPointer.variableName, contextIndex);
+            }
+        }
+
+        // 0  if named variable is global
+        // 1+ if named variable is a temporary in a particular call stack element
+        int GetContextIndexOfVariableNamed(string varName)
+        {
+            if (GlobalVariableExistsWithName(varName))
+                return 0;
+
+            return _callStack.currentElementIndex;
+        }
+
+        Dictionary<string, Runtime.Object> _globalVariables;
+
+        Dictionary<string, Runtime.Object> _defaultGlobalVariables;
+
+        // Used for accessing temporary variables
+        CallStack _callStack;
+        HashSet<string> _changedVariablesForBatchObs;
+        ListDefinitionsOrigin _listDefsOrigin;
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs.meta b/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs.meta
new file mode 100644
index 0000000..5ec2b19
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 98a3826d0e499440992195956701397e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/InkLibs/InkRuntime/Void.cs b/Assets/Ink/InkLibs/InkRuntime/Void.cs
new file mode 100644
index 0000000..340570d
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Void.cs
@@ -0,0 +1,10 @@
+namespace Ink.Runtime
+{
+    public class Void : Runtime.Object
+    {
+        public Void ()
+        {
+        }
+    }
+}
+
diff --git a/Assets/Ink/InkLibs/InkRuntime/Void.cs.meta b/Assets/Ink/InkLibs/InkRuntime/Void.cs.meta
new file mode 100644
index 0000000..c41ae8b
--- /dev/null
+++ b/Assets/Ink/InkLibs/InkRuntime/Void.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 28e3cd581fd59451fb8dacea1b19f477
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/README.md b/Assets/Ink/README.md
new file mode 100644
index 0000000..e627ed6
--- /dev/null
+++ b/Assets/Ink/README.md
@@ -0,0 +1,158 @@
+# ink-Unity integration
+
+This Unity package allows you to integrate inkle's [ink narrative scripting language](http://www.inklestudios.com/ink) with Unity and provides tools to **compile**, **play** and **debug** your stories.
+
+# Overview
+
+ - **Using ink in your game**: Allows running and controlling ink files in Unity via the [C# runtime API](https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md).
+ 	
+ - **ink player**: Provides a powerful [Ink Player Window](https://github.com/inkle/ink-unity-integration/blob/master/Documentation/InkPlayerWindow.md) for playing and debugging stories.
+ 
+ - **Auto compilation**: Instantly creates and updates a JSON story file when a `.ink` is updated.
+ 	
+ - **Inspector tools**: Provides an icon for ink files, and a custom inspector that provides information about a file.
+
+# Getting started
+
+## :inbox_tray: Downloading the package
+
+### :star:As a UPM Package (Recommended):star:
+* Navigate to [OpenUPM](https://openupm.com/packages/com.inklestudios.ink-unity-integration/) and click "Get installer.unitypackage".
+* Open the downloaded file. The installer will do the rest!
+* The project will have installed at Packages > Ink Unity Integration.
+* Demo projects can be imported from Packages > Ink Unity Integration > Examples
+
+### As a .UnityPackage
+This will import the source into your Assets folder. This is a good option if you intend to edit the source for your own needs.
+* [Download the latest .UnityPackage](https://github.com/inkle/ink-unity-integration/releases).
+* Open the downloaded file to import it into your Unity project.
+
+### Via the Asset Store
+For convinience a .UnityPackage is hosted at the [Unity Asset Store](https://assetstore.unity.com/packages/tools/integration/ink-unity-integration-60055).
+**This version is updated rarely, and so is not recommended.**
+This will import the source into your Assets folder. This is a good option if you intend to edit the source for your own needs.
+
+### From GitHub
+You can fork the project on [GitHub](https://github.com/inkle/ink-unity-integration).
+
+
+
+## :video_game: Demos
+This project includes a demo scene, providing a simple example of how to control an ink story with C# code using Unity UI.
+
+(If you imported this package as a UPM (recommended), then you must first import the demos from Packages > Ink Unity Integration > Examples)
+
+To run a demo, double-click the scene file at the root of the demo folder to open it, and press the Play button at the top of the screen to start it.
+
+## :page_facing_up: C# API
+The C# API provides all you need to control ink stories in code; advancing your story, making choices, diverting to knots, saving and loading, and much more.
+[It is documented in the main ink repo](https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md#getting-started-with-the-runtime-api)
+For convenience, the package also creates an (**Help > Ink > API Documentation**) menu option.
+
+## :pencil2: Writing ink
+For more information on writing with **ink**, see [the documentation in the main ink repo](https://github.com/inkle/ink). 
+For convenience, the package also creates an (**Help > Ink > Writing Tutorial**) menu option.
+
+
+## :question: Further Help
+For assistance with writing or code, [Inkle's Discord forum](https://discord.gg/tD8Am2K) is full of lovely people who can help you out!
+
+To keep up to date with the latest news about ink [sign up for the mailing list](http://www.inklestudios.com/ink#signup).
+
+
+# Features
+
+## Compilation
+	
+Ink files must be compiled to JSON before they can be used in-game. 
+**This package compiles all edited ink files automatically.**
+By default, compiled files are created next to their ink file.
+
+### Editor Compilation
+This package provides tools to automate this process when a .ink file is edited. 
+
+**Disabling auto-compilation**: You might want to have manual control over ink compilation. If this is the case, you can disable "Compile ink automatically" in the InkSettings file or delete the InkPostProcessor class.
+
+**Manual compilation**: If you have disabled auto-compilation, you can manually compile all ink files using the **Assets > Recompile Ink** menu item, individually via the inspector of an ink file, or via code using InkCompiler.CompileInk().
+
+**Play mode delay**: By default, ink does not compile while in play mode. This can be disabled in the InkSettings file.
+
+### In-game Compilation
+
+The compiler is included in builds (See [WebGL best practices](#WebGLBestPractices) for information on removing it), enabling you to allow the editing of ink files as part of your game.
+
+
+## <a name="InkPlayerWindow"></a>Ink Player Window
+
+The Ink Player Window (**Window > Ink Player**) allows you to play stories in an editor window, and provides functionality to edit variables on the fly, test functions, profile performance, save and load states, and divert.
+
+To play a story, click the "play" button shown on the inspector of a compiled ink file, or drag a compiled ink story TextAsset into the window.
+
+**Editor Attaching**: Attaching the InkStory instance used by your game to the Ink Player window allows you to view and edit your story as it runs in game. 
+
+See BasicInkExampleEditor.cs in the Examples folder for an example of how to:
+* Show an attach/detach button on an inspector
+* Automatically attach on entering play mode
+
+[More information on using and extending Ink Player Window](https://github.com/inkle/ink-unity-integration/blob/master/Documentation/InkPlayerWindow.md)
+
+
+## Inspector tools
+
+This package replaces the icon for ink files to make them easier to spot, and adds a custom inspector for a selected ink file.
+
+**The Inspector**: Selecting an ink file displays its last compile time; lists any include files; and shows any errors, warnings or todos. It also shows a Play button which runs the story in the Ink Player Window.
+
+
+# Visual Scripting Support
+
+## Bolt
+There is currently no support for Bolt, Unity's official visual scripting tool. If you're interested in building one, we'd love to see it!
+
+## PlayMaker
+There's [unofficial support for PlayMaker here.](https://github.com/inkle/ink-unity-integration/issues/22) 
+
+
+We'd love to see this supported more if you'd like to assist the effort!
+
+
+# Source control tips
+
+When you edit ink files, the compiler will also update the corresponding compiled .json file. If no compiled file existed before, Unity will also create a meta file for it. It is recommended that you always commit both ink and json files at the same time to avoid the file being re-compiled by your team members.
+
+Adding or removing ink files will also make changes to the InkLibrary file, and we could recommend authors also commit this file for the same reasons.
+
+
+# <a name="WebGLBestPractices"></a>WebGL best practices
+
+WebGL builds should be as small as possible. The ink compiler is included in builds, but is typically only used in the editor. 
+If your game doesn't require compiling ink at runtime we recommend adding a .asmdef at Ink Unity Integration > InkLibs > InkCompiler that only functions in the editor.
+
+
+# FAQ
+
+* Is the Linux Unity Editor supported?
+
+  *Yes!*
+
+* What versions of Unity are supported?
+
+  We officially support 2018 LTS and above, but it should work going back to at least Unity 5.
+
+# Support us!:heart:
+
+Ink is free, forever; but we'd really appreciate your support!
+If you're able to give back, generous donations at our [Patreon](https://www.patreon.com/inkle) mean the world to us. 
+
+# License
+
+**ink** and this package is released under the MIT license. Although we don't require attribution, we'd love to know if you decide to use **ink** a project! Let us know on [Twitter](http://www.twitter.com/inkleStudios) or [by email](mailto:info@inklestudios.com).
+
+### The MIT License (MIT)
+Copyright (c) 2016 inkle Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Assets/Ink/README.md.meta b/Assets/Ink/README.md.meta
new file mode 100644
index 0000000..23526ff
--- /dev/null
+++ b/Assets/Ink/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 85254203c0564478cab552275fc29e5f
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Ink/package.json b/Assets/Ink/package.json
new file mode 100644
index 0000000..f749468
--- /dev/null
+++ b/Assets/Ink/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "com.inklestudios.ink-unity-integration",
+  "version": "1.0.0",
+  "displayName": "Ink Unity Integration",
+  "description": "Unity integration for the open source ink narrative scripting language.",
+  "unity": "2018.4",
+  "dependencies": {},
+  "keywords": [
+    "ink",
+    "inky",
+    "inkle",
+    "story",
+    "narrative"
+  ],
+  "author": {
+    "name": "Inkle",
+    "email": "info@inklestudios.com",
+    "url": "https://www.inklestudios.com/"
+  }
+}
diff --git a/Assets/Ink/package.json.meta b/Assets/Ink/package.json.meta
new file mode 100644
index 0000000..c310540
--- /dev/null
+++ b/Assets/Ink/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8891cdb49548241978b24997930b1314
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs b/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs
new file mode 100644
index 0000000..16bf575
--- /dev/null
+++ b/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs
@@ -0,0 +1,11 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class DialogueHolder : MonoBehaviour, IInteractable {
+    [SerializeField] private TextAsset inkJSON;
+
+    public void Interact() {
+        Debug.Log(inkJSON.text);
+    }
+}
diff --git a/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs.meta b/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs.meta
new file mode 100644
index 0000000..7713528
--- /dev/null
+++ b/Assets/SZZ/Code/Scripts/NPCScripts/DialogueHolder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 063913b232069524eb0bb1c306582ea0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Code/Scripts/Player/PlayerActionsController.cs b/Assets/SZZ/Code/Scripts/Player/PlayerActionsController.cs
index 1e03be6..af69fb4 100644
--- a/Assets/SZZ/Code/Scripts/Player/PlayerActionsController.cs
+++ b/Assets/SZZ/Code/Scripts/Player/PlayerActionsController.cs
@@ -8,6 +8,7 @@ public class PlayerActionsController : MonoBehaviour {
     [Header("Interaction")]
     [SerializeField] private float interactionDistance = 1.8f;
     [SerializeField] private LayerMask interactionLayerMask;
+    public GameObject interactionTrigger { get; private set; } = null;
 
     private FirstPersonController _firstPersonController;
     private NoClipController _noClipController;
@@ -37,17 +38,36 @@ private void OnDisable() {
         _noClipAction.started -= NoClip;
     }
 
-    private void Interact(InputAction.CallbackContext context) {
+    private void OnTriggerEnter(Collider other) {
+        if (other.gameObject.CompareTag("Interactable"))
+            interactionTrigger = other.gameObject;
+    }
+
+    private void OnTriggerExit(Collider other) {
+        if (other.gameObject == other.gameObject)
+            interactionTrigger = null;
+    }
+
+    public GameObject ShootInteractionRay() {
         // TODO more reach on ground, less horizontally and upwards
         RaycastHit hitData;
         Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
+
         Debug.DrawRay(ray.origin, ray.direction * interactionDistance, Color.magenta, 3);
-        if (Physics.Raycast(ray, out hitData, interactionDistance)) {
-            var hitObject = hitData.transform.gameObject;
-            if (hitObject.CompareTag("Interactable")) {
-                hitObject.GetComponent<IInteractable>()?.Interact();
-            }
-        }
+
+        if (Physics.Raycast(ray, out hitData, interactionDistance, interactionLayerMask))
+            return hitData.transform.gameObject;
+
+        return null;
+    }
+
+    private void Interact(InputAction.CallbackContext context) {
+        var hitObject = ShootInteractionRay();
+
+        if (hitObject && hitObject.CompareTag("Interactable"))
+            hitObject.GetComponent<IInteractable>()?.Interact();
+        else
+            interactionTrigger?.GetComponent<IInteractable>()?.Interact();
     }
 
     private void PayRespects(InputAction.CallbackContext context) {
diff --git a/Assets/SZZ/Code/Scripts/Player/PlayerInteractUI.cs b/Assets/SZZ/Code/Scripts/Player/PlayerInteractUI.cs
index 155b719..40475d5 100644
--- a/Assets/SZZ/Code/Scripts/Player/PlayerInteractUI.cs
+++ b/Assets/SZZ/Code/Scripts/Player/PlayerInteractUI.cs
@@ -2,13 +2,36 @@
 using System.Collections.Generic;
 using UnityEngine;
 
-public class PlayerInteractUI : MonoBehaviour
-{
-    [SerializeField] private GameObject containerGameObjectE;
-    [SerializeField] private GameObject containerGameObjectF;
-    [SerializeField] private PlayerInteract playerInteract;
+[RequireComponent(typeof(PlayerActionsController))]
+public class PlayerInteractUI : MonoBehaviour {
+    [SerializeField] private GameObject interactPrompt;
+    [SerializeField] private GameObject payRespectsPrompt;
 
-    private void Update(){
+    private bool _showInteractionPrompt = false;
+    private bool _showPayRespactsPrompt = false;
+
+    private PlayerActionsController _playerActionsController;
+
+    private void Awake() {
+        _playerActionsController = GetComponent<PlayerActionsController>();
+    }
+
+    private void FixedUpdate() {
+        _showInteractionPrompt = false;
+        _showPayRespactsPrompt = false;
+
+        if (_playerActionsController.interactionTrigger)
+            _showInteractionPrompt = true;
+        else {
+            var lookedAt = _playerActionsController.ShootInteractionRay();
+            if (lookedAt && lookedAt.CompareTag("Interactable"))
+                _showInteractionPrompt = true;
+        }
+    }
+
+    private void Update() {
+        // TODO payrespects tag
+        /*
         if(playerInteract.GetNPCDying() != null){
             Show(containerGameObjectF);
             Hide(containerGameObjectE);
@@ -19,13 +42,29 @@ private void Update(){
             Hide(containerGameObjectE);
             Hide(containerGameObjectF);
         }
+        */
+
+        if (_showPayRespactsPrompt) {
+            Show(payRespectsPrompt);
+            Hide(interactPrompt);
+        }
+        else if (_showInteractionPrompt) {
+            Show(interactPrompt);
+            Hide(payRespectsPrompt);
+        }
+        else {
+            Hide(interactPrompt);
+            Hide(payRespectsPrompt);
+        }
     }
 
-    private void Show(GameObject o){
-        o.SetActive(true);
+    private void Show(GameObject o) {
+        if (!o.activeSelf)
+            o.SetActive(true);
     }
 
-    private void Hide(GameObject o){
-        o.SetActive(false);
+    private void Hide(GameObject o) {
+        if (o.activeSelf)
+            o.SetActive(false);
     }
 }
diff --git a/Assets/SZZ/Dialogue.meta b/Assets/SZZ/Dialogue.meta
new file mode 100644
index 0000000..2cef9a8
--- /dev/null
+++ b/Assets/SZZ/Dialogue.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1f5d457f460c42e44b9b91c78ca85329
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Dialogue/test.ink b/Assets/SZZ/Dialogue/test.ink
new file mode 100644
index 0000000..df598c9
--- /dev/null
+++ b/Assets/SZZ/Dialogue/test.ink
@@ -0,0 +1,3 @@
+This is a test!
+another line lol
+cya :)
\ No newline at end of file
diff --git a/Assets/SZZ/Dialogue/test.ink.meta b/Assets/SZZ/Dialogue/test.ink.meta
new file mode 100644
index 0000000..1cd1808
--- /dev/null
+++ b/Assets/SZZ/Dialogue/test.ink.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e8eb7f42eeca6044f85aeaa183a95201
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Dialogue/test.json b/Assets/SZZ/Dialogue/test.json
new file mode 100644
index 0000000..ee57d2d
--- /dev/null
+++ b/Assets/SZZ/Dialogue/test.json
@@ -0,0 +1 @@
+{"inkVersion":20,"root":[["^This is a test!","\n","^another line lol","\n","^cya :)","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"#f":1}],"listDefs":{}}
\ No newline at end of file
diff --git a/Assets/SZZ/Dialogue/test.json.meta b/Assets/SZZ/Dialogue/test.json.meta
new file mode 100644
index 0000000..282dc15
--- /dev/null
+++ b/Assets/SZZ/Dialogue/test.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f155cfb465e063c44ac75fc3bd37e7a3
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Level/Prefabs/test room.prefab b/Assets/SZZ/Level/Prefabs/test room.prefab
index d420fcf..3a3bc49 100644
--- a/Assets/SZZ/Level/Prefabs/test room.prefab	
+++ b/Assets/SZZ/Level/Prefabs/test room.prefab	
@@ -152,7 +152,7 @@ Transform:
   m_ConstrainProportionsScale: 0
   m_Children: []
   m_Father: {fileID: 4428170862749948677}
-  m_RootOrder: 5
+  m_RootOrder: 4
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!220 &6828982596036911955
 LightProbeGroup:
@@ -334,10 +334,10 @@ Transform:
   m_Children:
   - {fileID: 6468461392547119398}
   - {fileID: 8296855263913118645}
-  - {fileID: 2775218916642988858}
   - {fileID: 4166236409223451946}
   - {fileID: 2200708919421361993}
   - {fileID: 7952025342885309859}
+  - {fileID: 926095796605952819}
   m_Father: {fileID: 4343901224938426690}
   m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -578,7 +578,7 @@ PrefabInstance:
     - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
         type: 3}
       propertyPath: m_RootOrder
-      value: 3
+      value: 2
       objectReference: {fileID: 0}
     - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
         type: 3}
@@ -713,81 +713,6 @@ Transform:
     type: 3}
   m_PrefabInstance: {fileID: 303026375731366777}
   m_PrefabAsset: {fileID: 0}
---- !u!1001 &1969122146515704170
-PrefabInstance:
-  m_ObjectHideFlags: 0
-  serializedVersion: 2
-  m_Modification:
-    m_TransformParent: {fileID: 4428170862749948677}
-    m_Modifications:
-    - target: {fileID: 3934989813034197226, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_Name
-      value: fake recessed light (2)
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_RootOrder
-      value: 2
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalPosition.x
-      value: 26.25
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalPosition.y
-      value: 2.75
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalPosition.z
-      value: -3.16
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalRotation.w
-      value: 1
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalRotation.x
-      value: -0
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalRotation.y
-      value: -0
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalRotation.z
-      value: -0
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.x
-      value: 0
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.y
-      value: 0
-      objectReference: {fileID: 0}
-    - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.z
-      value: 0
-      objectReference: {fileID: 0}
-    m_RemovedComponents: []
-  m_SourcePrefab: {fileID: 100100000, guid: 6e85d739d9901524e9e30c58c958332a, type: 3}
---- !u!4 &2775218916642988858 stripped
-Transform:
-  m_CorrespondingSourceObject: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
-    type: 3}
-  m_PrefabInstance: {fileID: 1969122146515704170}
-  m_PrefabAsset: {fileID: 0}
 --- !u!1001 &2148963506179588445
 PrefabInstance:
   m_ObjectHideFlags: 0
@@ -958,7 +883,7 @@ PrefabInstance:
     - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
         type: 3}
       propertyPath: m_RootOrder
-      value: 4
+      value: 3
       objectReference: {fileID: 0}
     - target: {fileID: 4454118092617775696, guid: 6e85d739d9901524e9e30c58c958332a,
         type: 3}
@@ -1318,6 +1243,81 @@ Transform:
     type: 3}
   m_PrefabInstance: {fileID: 3914732315207679809}
   m_PrefabAsset: {fileID: 0}
+--- !u!1001 &4100230139869917504
+PrefabInstance:
+  m_ObjectHideFlags: 0
+  serializedVersion: 2
+  m_Modification:
+    m_TransformParent: {fileID: 4428170862749948677}
+    m_Modifications:
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_RootOrder
+      value: 5
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalPosition.x
+      value: 26.25
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalPosition.y
+      value: 2.75
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalPosition.z
+      value: -3.16
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalRotation.w
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalRotation.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalRotation.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalRotation.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 4573130554061719753, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+        type: 3}
+      propertyPath: m_Name
+      value: fake recessed light flickering
+      objectReference: {fileID: 0}
+    m_RemovedComponents: []
+  m_SourcePrefab: {fileID: 100100000, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6, type: 3}
+--- !u!4 &926095796605952819 stripped
+Transform:
+  m_CorrespondingSourceObject: {fileID: 3764119435429139059, guid: 47cfe3eb295ab124eb2cbe2e9bd0c5d6,
+    type: 3}
+  m_PrefabInstance: {fileID: 4100230139869917504}
+  m_PrefabAsset: {fileID: 0}
 --- !u!1001 &4641832486476185389
 PrefabInstance:
   m_ObjectHideFlags: 0
diff --git a/Assets/SZZ/Level/Scenes/building a.unity b/Assets/SZZ/Level/Scenes/building a.unity
index 98c749e..7a9f1fc 100644
--- a/Assets/SZZ/Level/Scenes/building a.unity	
+++ b/Assets/SZZ/Level/Scenes/building a.unity	
@@ -4110,7 +4110,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 49
+      value: 47
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -4564,7 +4564,7 @@ PrefabInstance:
     - target: {fileID: -8679921383154817045, guid: 59c8186ffd69b8f4b8f30ecfbb647701,
         type: 3}
       propertyPath: m_RootOrder
-      value: 33
+      value: 31
       objectReference: {fileID: 0}
     - target: {fileID: -8679921383154817045, guid: 59c8186ffd69b8f4b8f30ecfbb647701,
         type: 3}
@@ -6916,7 +6916,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 36
+      value: 34
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -7901,7 +7901,7 @@ PrefabInstance:
     - target: {fileID: 8182742340936260200, guid: f71f59b81f7339447a48468f0883d213,
         type: 3}
       propertyPath: m_RootOrder
-      value: 45
+      value: 43
       objectReference: {fileID: 0}
     - target: {fileID: 8182742340936260200, guid: f71f59b81f7339447a48468f0883d213,
         type: 3}
@@ -9550,7 +9550,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 52
+      value: 50
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -9761,7 +9761,7 @@ PrefabInstance:
     - target: {fileID: 3686555598604687940, guid: 4e2697ca2dc4a8543ba5c576d82597b0,
         type: 3}
       propertyPath: m_RootOrder
-      value: 28
+      value: 26
       objectReference: {fileID: 0}
     - target: {fileID: 3686555598604687940, guid: 4e2697ca2dc4a8543ba5c576d82597b0,
         type: 3}
@@ -10532,80 +10532,6 @@ PrefabInstance:
       objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: f71f59b81f7339447a48468f0883d213, type: 3}
---- !u!1001 &441145662
-PrefabInstance:
-  m_ObjectHideFlags: 0
-  serializedVersion: 2
-  m_Modification:
-    m_TransformParent: {fileID: 1858346386}
-    m_Modifications:
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_RootOrder
-      value: 19
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalPosition.x
-      value: -27.850544
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalPosition.y
-      value: -0.7753404
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalPosition.z
-      value: 26.773067
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalRotation.w
-      value: 0.07703875
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalRotation.x
-      value: -0
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalRotation.y
-      value: 0.9970282
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalRotation.z
-      value: -0
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.x
-      value: 0
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.y
-      value: 171.163
-      objectReference: {fileID: 0}
-    - target: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_LocalEulerAnglesHint.z
-      value: 0
-      objectReference: {fileID: 0}
-    - target: {fileID: 919132149155446097, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_Name
-      value: rp_claudia_rigged_002_yup_t
-      objectReference: {fileID: 0}
-    - target: {fileID: 919132149155446097, guid: fe48b53dab70cbf429029311afb3201a,
-        type: 3}
-      propertyPath: m_TagString
-      value: NPCAdela
-      objectReference: {fileID: 0}
-    m_RemovedComponents: []
-  m_SourcePrefab: {fileID: 100100000, guid: fe48b53dab70cbf429029311afb3201a, type: 3}
 --- !u!1001 &443314068
 PrefabInstance:
   m_ObjectHideFlags: 0
@@ -11440,7 +11366,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 40
+      value: 38
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -11565,7 +11491,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 66
+      value: 65
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -13751,7 +13677,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 35
+      value: 33
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -14301,7 +14227,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 42
+      value: 40
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -16002,7 +15928,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 48
+      value: 46
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -18401,7 +18327,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 51
+      value: 49
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -19785,7 +19711,7 @@ PrefabInstance:
     - target: {fileID: 4580154537569598494, guid: ea5dbed22855f6643ae5161cc71c250d,
         type: 3}
       propertyPath: m_RootOrder
-      value: 66
+      value: 62
       objectReference: {fileID: 0}
     - target: {fileID: 4580154537569598494, guid: ea5dbed22855f6643ae5161cc71c250d,
         type: 3}
@@ -20098,7 +20024,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 60
+      value: 58
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -20440,46 +20366,6 @@ Transform:
     type: 3}
   m_PrefabInstance: {fileID: 28287951}
   m_PrefabAsset: {fileID: 0}
---- !u!1 &752435254 stripped
-GameObject:
-  m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: fe48b53dab70cbf429029311afb3201a,
-    type: 3}
-  m_PrefabInstance: {fileID: 441145662}
-  m_PrefabAsset: {fileID: 0}
---- !u!136 &752435257
-CapsuleCollider:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 752435254}
-  m_Material: {fileID: 0}
-  m_IsTrigger: 0
-  m_Enabled: 1
-  m_Radius: 0.5
-  m_Height: 1
-  m_Direction: 1
-  m_Center: {x: 0, y: 0, z: 0}
---- !u!114 &752435258
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 752435254}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: 12e79cf8a158df3458a65a54273f6704, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  level: {fileID: 468824145}
-  line: {fileID: 1610937609}
---- !u!4 &752435259 stripped
-Transform:
-  m_CorrespondingSourceObject: {fileID: -8679921383154817045, guid: fe48b53dab70cbf429029311afb3201a,
-    type: 3}
-  m_PrefabInstance: {fileID: 441145662}
-  m_PrefabAsset: {fileID: 0}
 --- !u!1001 &754115791
 PrefabInstance:
   m_ObjectHideFlags: 0
@@ -21679,7 +21565,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 47
+      value: 45
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -23567,7 +23453,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 57
+      value: 55
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -24598,7 +24484,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 27
+      value: 25
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -24955,7 +24841,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 65
+      value: 63
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -26368,7 +26254,7 @@ PrefabInstance:
     - target: {fileID: 3929754532098700848, guid: f75c34c67e0eaa34e872173a2344e4bd,
         type: 3}
       propertyPath: m_RootOrder
-      value: 1
+      value: 0
       objectReference: {fileID: 0}
     - target: {fileID: 3929754532098700848, guid: f75c34c67e0eaa34e872173a2344e4bd,
         type: 3}
@@ -27769,7 +27655,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 56
+      value: 54
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -28842,7 +28728,7 @@ PrefabInstance:
     - target: {fileID: 3686555598604687940, guid: 4e2697ca2dc4a8543ba5c576d82597b0,
         type: 3}
       propertyPath: m_RootOrder
-      value: 24
+      value: 22
       objectReference: {fileID: 0}
     - target: {fileID: 3686555598604687940, guid: 4e2697ca2dc4a8543ba5c576d82597b0,
         type: 3}
@@ -30473,7 +30359,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 67
+      value: 66
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -33071,7 +32957,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 53
+      value: 51
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -33146,7 +33032,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 64
+      value: 61
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -34223,7 +34109,7 @@ PrefabInstance:
     - target: {fileID: 4580154537569598494, guid: ea5dbed22855f6643ae5161cc71c250d,
         type: 3}
       propertyPath: m_RootOrder
-      value: 65
+      value: 64
       objectReference: {fileID: 0}
     - target: {fileID: 4580154537569598494, guid: ea5dbed22855f6643ae5161cc71c250d,
         type: 3}
@@ -34298,7 +34184,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 37
+      value: 35
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -35632,7 +35518,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 55
+      value: 53
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -36100,7 +35986,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 58
+      value: 56
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -36180,7 +36066,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 41
+      value: 39
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -37742,7 +37628,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 43
+      value: 41
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -37997,7 +37883,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 46
+      value: 44
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -38698,7 +38584,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 59
+      value: 57
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -38923,7 +38809,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 62
+      value: 60
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -42421,7 +42307,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 54
+      value: 52
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -43875,7 +43761,7 @@ PrefabInstance:
     - target: {fileID: 5090853617830975863, guid: 765190b972db72941a4518e6881d9efd,
         type: 3}
       propertyPath: m_RootOrder
-      value: 44
+      value: 42
       objectReference: {fileID: 0}
     - target: {fileID: 5090853617830975863, guid: 765190b972db72941a4518e6881d9efd,
         type: 3}
@@ -45622,7 +45508,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 39
+      value: 37
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -45729,7 +45615,7 @@ Transform:
   - {fileID: 311782151}
   - {fileID: 656490631}
   - {fileID: 506066168}
-  - {fileID: 752435259}
+  - {fileID: 543450653267476181}
   - {fileID: 2137374397}
   - {fileID: 154576854}
   - {fileID: 529807775}
@@ -46209,7 +46095,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 38
+      value: 36
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -46677,7 +46563,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 29
+      value: 27
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -49093,7 +48979,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 61
+      value: 59
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -51557,7 +51443,7 @@ PrefabInstance:
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
       propertyPath: m_RootOrder
-      value: 50
+      value: 48
       objectReference: {fileID: 0}
     - target: {fileID: 4755639943237720139, guid: 509a21faa5605664fbdffb10f41fbd77,
         type: 3}
@@ -52633,6 +52519,12 @@ Transform:
     type: 3}
   m_PrefabInstance: {fileID: 7821499719969688535}
   m_PrefabAsset: {fileID: 0}
+--- !u!4 &543450653267476181 stripped
+Transform:
+  m_CorrespondingSourceObject: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+    type: 3}
+  m_PrefabInstance: {fileID: 3298639667024475595}
+  m_PrefabAsset: {fileID: 0}
 --- !u!4 &543450653390531075 stripped
 Transform:
   m_CorrespondingSourceObject: {fileID: 8182742340936260200, guid: f71f59b81f7339447a48468f0883d213,
@@ -53184,7 +53076,7 @@ PrefabInstance:
     - target: {fileID: 5340678783447132603, guid: 1326ee9d1791d954d8019763a89b433e,
         type: 3}
       propertyPath: m_RootOrder
-      value: 31
+      value: 29
       objectReference: {fileID: 0}
     - target: {fileID: 5340678783447132603, guid: 1326ee9d1791d954d8019763a89b433e,
         type: 3}
@@ -53238,6 +53130,85 @@ PrefabInstance:
       objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: 1326ee9d1791d954d8019763a89b433e, type: 3}
+--- !u!1001 &3298639667024475595
+PrefabInstance:
+  m_ObjectHideFlags: 0
+  serializedVersion: 2
+  m_Modification:
+    m_TransformParent: {fileID: 1858346386}
+    m_Modifications:
+    - target: {fileID: 2379719870419292580, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_Name
+      value: Adela
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_RootOrder
+      value: 19
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.x
+      value: -27.850544
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.y
+      value: -0.7753404
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.z
+      value: 26.773067
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.w
+      value: 0.07703875
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.x
+      value: -0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.y
+      value: 0.9970282
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.z
+      value: -0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.y
+      value: 171.163
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3298639667508344305, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: line
+      value: 
+      objectReference: {fileID: 1610937609}
+    - target: {fileID: 3298639667508344305, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: level
+      value: 
+      objectReference: {fileID: 468824145}
+    m_RemovedComponents: []
+  m_SourcePrefab: {fileID: 100100000, guid: 3f8c48c33c3c37e44af130793093f4c3, type: 3}
 --- !u!1001 &3370165210813158646
 PrefabInstance:
   m_ObjectHideFlags: 0
@@ -53545,7 +53516,7 @@ PrefabInstance:
     - target: {fileID: 8349983867958448129, guid: 8704d66d3a2a8814e9f1f40ce6b9c4b2,
         type: 3}
       propertyPath: m_RootOrder
-      value: 32
+      value: 30
       objectReference: {fileID: 0}
     - target: {fileID: 8349983867958448129, guid: 8704d66d3a2a8814e9f1f40ce6b9c4b2,
         type: 3}
@@ -53626,7 +53597,7 @@ PrefabInstance:
     - target: {fileID: 3781732642653186414, guid: 881b0d1859d990b4dae34f92123161cd,
         type: 3}
       propertyPath: m_RootOrder
-      value: 30
+      value: 28
       objectReference: {fileID: 0}
     - target: {fileID: 3781732642653186414, guid: 881b0d1859d990b4dae34f92123161cd,
         type: 3}
@@ -53919,7 +53890,7 @@ PrefabInstance:
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
       propertyPath: m_RootOrder
-      value: 34
+      value: 32
       objectReference: {fileID: 0}
     - target: {fileID: 4302461579010940651, guid: c203df785ba04cf46bbbed6fb1abd888,
         type: 3}
@@ -54137,7 +54108,7 @@ PrefabInstance:
     - target: {fileID: 3929754532098700848, guid: f75c34c67e0eaa34e872173a2344e4bd,
         type: 3}
       propertyPath: m_RootOrder
-      value: 25
+      value: 23
       objectReference: {fileID: 0}
     - target: {fileID: 3929754532098700848, guid: f75c34c67e0eaa34e872173a2344e4bd,
         type: 3}
@@ -54709,7 +54680,7 @@ PrefabInstance:
     - target: {fileID: 7134618828506801380, guid: e8b3d02710ae7d74f883c3174423e6db,
         type: 3}
       propertyPath: m_RootOrder
-      value: 26
+      value: 24
       objectReference: {fileID: 0}
     - target: {fileID: 7134618828506801380, guid: e8b3d02710ae7d74f883c3174423e6db,
         type: 3}
diff --git a/Assets/SZZ/Level/Scenes/sandbox michal.unity b/Assets/SZZ/Level/Scenes/sandbox michal.unity
index 40453b9..d23d4a0 100644
--- a/Assets/SZZ/Level/Scenes/sandbox michal.unity	
+++ b/Assets/SZZ/Level/Scenes/sandbox michal.unity	
@@ -349,7 +349,7 @@ Transform:
   - {fileID: 1493283122}
   - {fileID: 758983161}
   m_Father: {fileID: 0}
-  m_RootOrder: 4
+  m_RootOrder: 5
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!1001 &221374721
 PrefabInstance:
@@ -358,6 +358,26 @@ PrefabInstance:
   m_Modification:
     m_TransformParent: {fileID: 1866770146}
     m_Modifications:
+    - target: {fileID: 3976937685011215316, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: 4.4657194e+30
+      objectReference: {fileID: 0}
+    - target: {fileID: 3976937685011215316, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.x
+      value: 1.05e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 3976937685011215316, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.0286167e-11
+      objectReference: {fileID: 0}
+    - target: {fileID: 3976937685011215316, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 1.6897273e+22
+      objectReference: {fileID: 0}
     - target: {fileID: 6158433212702794927, guid: 7319b941f4d9acb47a81463f6bcf8e91,
         type: 3}
       propertyPath: m_RootOrder
@@ -418,6 +438,21 @@ PrefabInstance:
       propertyPath: m_Name
       value: stairs (5)
       objectReference: {fileID: 0}
+    - target: {fileID: 8659164324298483115, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: 1.3e-44
+      objectReference: {fileID: 0}
+    - target: {fileID: 8659164324298483115, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.x
+      value: 4.700279e-39
+      objectReference: {fileID: 0}
+    - target: {fileID: 8659164324298483115, guid: 7319b941f4d9acb47a81463f6bcf8e91,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 1.758926e+22
+      objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: 7319b941f4d9acb47a81463f6bcf8e91, type: 3}
 --- !u!4 &221374722 stripped
@@ -845,7 +880,7 @@ Transform:
   m_ConstrainProportionsScale: 0
   m_Children: []
   m_Father: {fileID: 0}
-  m_RootOrder: 3
+  m_RootOrder: 4
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!1 &758983160
 GameObject:
@@ -910,6 +945,21 @@ PrefabInstance:
       propertyPath: m_LocalRotation.y
       value: 1
       objectReference: {fileID: 0}
+    - target: {fileID: 2554792372440262584, guid: 39967ec940f6f104eabe57a2974f7ab0,
+        type: 3}
+      propertyPath: interactPrompt
+      value: 
+      objectReference: {fileID: 947558919}
+    - target: {fileID: 2554792372440262584, guid: 39967ec940f6f104eabe57a2974f7ab0,
+        type: 3}
+      propertyPath: payRespectsPrompt
+      value: 
+      objectReference: {fileID: 947558920}
+    - target: {fileID: 4835453592825462427, guid: 39967ec940f6f104eabe57a2974f7ab0,
+        type: 3}
+      propertyPath: m_IsTrigger
+      value: 1
+      objectReference: {fileID: 0}
     - target: {fileID: 4853108127541422087, guid: 39967ec940f6f104eabe57a2974f7ab0,
         type: 3}
       propertyPath: m_LocalPosition.x
@@ -933,7 +983,7 @@ PrefabInstance:
     - target: {fileID: 6909880373530918866, guid: 39967ec940f6f104eabe57a2974f7ab0,
         type: 3}
       propertyPath: m_RootOrder
-      value: 5
+      value: 6
       objectReference: {fileID: 0}
     - target: {fileID: 6909880373530918866, guid: 39967ec940f6f104eabe57a2974f7ab0,
         type: 3}
@@ -1027,6 +1077,18 @@ PrefabInstance:
       objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: 39967ec940f6f104eabe57a2974f7ab0, type: 3}
+--- !u!1 &947558919 stripped
+GameObject:
+  m_CorrespondingSourceObject: {fileID: 6909880372810032739, guid: 39967ec940f6f104eabe57a2974f7ab0,
+    type: 3}
+  m_PrefabInstance: {fileID: 947558918}
+  m_PrefabAsset: {fileID: 0}
+--- !u!1 &947558920 stripped
+GameObject:
+  m_CorrespondingSourceObject: {fileID: 6909880373533192475, guid: 39967ec940f6f104eabe57a2974f7ab0,
+    type: 3}
+  m_PrefabInstance: {fileID: 947558918}
+  m_PrefabAsset: {fileID: 0}
 --- !u!1 &1014348842
 GameObject:
   m_ObjectHideFlags: 0
@@ -1067,6 +1129,75 @@ Transform:
   m_Father: {fileID: 1866770146}
   m_RootOrder: 2
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1001 &1388381973
+PrefabInstance:
+  m_ObjectHideFlags: 0
+  serializedVersion: 2
+  m_Modification:
+    m_TransformParent: {fileID: 0}
+    m_Modifications:
+    - target: {fileID: 2379719870419292580, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_Name
+      value: Adela
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_RootOrder
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.x
+      value: -8.005976
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.y
+      value: 0.00000011920929
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalPosition.z
+      value: -4.4423523
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.w
+      value: 0.7071068
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.y
+      value: -0.7071068
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalRotation.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.y
+      value: -90
+      objectReference: {fileID: 0}
+    - target: {fileID: 3048275255403973406, guid: 3f8c48c33c3c37e44af130793093f4c3,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.z
+      value: 0
+      objectReference: {fileID: 0}
+    m_RemovedComponents: []
+  m_SourcePrefab: {fileID: 100100000, guid: 3f8c48c33c3c37e44af130793093f4c3, type: 3}
 --- !u!1 &1493283121
 GameObject:
   m_ObjectHideFlags: 0
@@ -1108,12 +1239,162 @@ PrefabInstance:
     - target: {fileID: 291189437020376006, guid: 4b80444d25ea42a469386ec98563720d,
         type: 3}
       propertyPath: m_Intensity
-      value: 100
+      value: 2
+      objectReference: {fileID: 0}
+    - target: {fileID: 291189437020376006, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.0058797e-11
+      objectReference: {fileID: 0}
+    - target: {fileID: 291189437020376006, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 13.562554
+      objectReference: {fileID: 0}
+    - target: {fileID: 626501285538079264, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 1.1e-44
+      objectReference: {fileID: 0}
+    - target: {fileID: 841854937478193098, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 6.54e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 1107345712045813743, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: -1.4391552e+26
+      objectReference: {fileID: 0}
+    - target: {fileID: 1107345712045813743, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 8.74e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 1898179926562141623, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.0058793e-11
+      objectReference: {fileID: 0}
+    - target: {fileID: 1898179926562141623, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 6.7e-44
+      objectReference: {fileID: 0}
+    - target: {fileID: 2883739603716416908, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: -1.2785946e+25
+      objectReference: {fileID: 0}
+    - target: {fileID: 2883739603716416908, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: NaN
+      objectReference: {fileID: 0}
+    - target: {fileID: 2896039104095158671, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 2.9393042e+29
+      objectReference: {fileID: 0}
+    - target: {fileID: 2896039104095158671, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 7.3983547e+31
+      objectReference: {fileID: 0}
+    - target: {fileID: 2902258742918767660, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: 6.890596e+22
+      objectReference: {fileID: 0}
+    - target: {fileID: 2902258742918767660, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.x
+      value: 1.05e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 2902258742918767660, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.0058793e-11
+      objectReference: {fileID: 0}
+    - target: {fileID: 2902258742918767660, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 4.757186e+30
+      objectReference: {fileID: 0}
+    - target: {fileID: 3591332866621803435, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.051354e-11
+      objectReference: {fileID: 0}
+    - target: {fileID: 3591332866621803435, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 13.562551
+      objectReference: {fileID: 0}
+    - target: {fileID: 5192931062245208448, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: -7.13175e+22
+      objectReference: {fileID: 0}
+    - target: {fileID: 5192931062245208448, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.x
+      value: 8.74e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 5192931062245208448, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: NaN
+      objectReference: {fileID: 0}
+    - target: {fileID: 5192931062245208448, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 1e-45
+      objectReference: {fileID: 0}
+    - target: {fileID: 7129253132834519827, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 8e-45
       objectReference: {fileID: 0}
     - target: {fileID: 7385929278726665035, guid: 4b80444d25ea42a469386ec98563720d,
         type: 3}
       propertyPath: m_Intensity
-      value: 100
+      value: 2
+      objectReference: {fileID: 0}
+    - target: {fileID: 7385929278726665035, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 4.5434276e+30
+      objectReference: {fileID: 0}
+    - target: {fileID: 7385929278726665035, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 2.028251e-19
+      objectReference: {fileID: 0}
+    - target: {fileID: 7448257934285288019, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.w
+      value: -2.7326431e+26
+      objectReference: {fileID: 0}
+    - target: {fileID: 7448257934285288019, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.x
+      value: 8.74e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 7448257934285288019, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 1.61e-43
+      objectReference: {fileID: 0}
+    - target: {fileID: 7813541768606430683, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.y
+      value: 6.878344e+11
+      objectReference: {fileID: 0}
+    - target: {fileID: 7813541768606430683, guid: 4b80444d25ea42a469386ec98563720d,
+        type: 3}
+      propertyPath: m_BoundingSphereOverride.z
+      value: 7.3982314e+31
       objectReference: {fileID: 0}
     - target: {fileID: 8569302544693158594, guid: 4b80444d25ea42a469386ec98563720d,
         type: 3}
@@ -1375,7 +1656,7 @@ Transform:
   - {fileID: 1590433461}
   - {fileID: 1014348843}
   m_Father: {fileID: 0}
-  m_RootOrder: 1
+  m_RootOrder: 2
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
 --- !u!1001 &2104078510
 PrefabInstance:
@@ -1472,10 +1753,15 @@ PrefabInstance:
       propertyPath: m_Name
       value: Supervisor
       objectReference: {fileID: 0}
+    - target: {fileID: 2632451753775102028, guid: 5e581040a02cc9e4eb9f08baaeb185c3,
+        type: 3}
+      propertyPath: m_IsActive
+      value: 0
+      objectReference: {fileID: 0}
     - target: {fileID: 2632451753775102032, guid: 5e581040a02cc9e4eb9f08baaeb185c3,
         type: 3}
       propertyPath: m_RootOrder
-      value: 2
+      value: 3
       objectReference: {fileID: 0}
     - target: {fileID: 2632451753775102032, guid: 5e581040a02cc9e4eb9f08baaeb185c3,
         type: 3}
diff --git a/Assets/SZZ/Level/Scenes/sandbox michal/LightingData.asset b/Assets/SZZ/Level/Scenes/sandbox michal/LightingData.asset
index 64861a9..afb7d1b 100644
--- a/Assets/SZZ/Level/Scenes/sandbox michal/LightingData.asset	
+++ b/Assets/SZZ/Level/Scenes/sandbox michal/LightingData.asset	
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:84a28ddbf331c85540979b11a5884dd112002c213db6fa899a3931cdb997a9fd
-size 83064
+oid sha256:38dd94189b0355c3863b745a53ca9bc24555f98c9eff8a65ca783e55877f3e70
+size 96728
diff --git a/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_dir.png b/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_dir.png
index c647fcb..02cb6db 100644
--- a/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_dir.png	
+++ b/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_dir.png	
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:a40c05965380f3125056423f6f415bd3ec761d879a05b83a46ebab4681aa40a8
-size 1100790
+oid sha256:36b44ba8c3676882cd9d69d00c42e428db5378ba866da1f24aaae44b0ce73436
+size 1203289
diff --git a/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_light.exr b/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_light.exr
index da05cfe..34b1fc5 100644
--- a/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_light.exr	
+++ b/Assets/SZZ/Level/Scenes/sandbox michal/Lightmap-0_comp_light.exr	
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:09dd6a065736c6fc8f85b593e72cf4e50b378c0799c2bcd708cb718c6c150403
-size 3843148
+oid sha256:34c82554ca883b7812bd78832590130a84950a0e76385dbaa39f40226858dcc1
+size 4071558
diff --git a/Assets/SZZ/Prefabs/Adela.prefab b/Assets/SZZ/Prefabs/Adela.prefab
new file mode 100644
index 0000000..f8f5f61
--- /dev/null
+++ b/Assets/SZZ/Prefabs/Adela.prefab
@@ -0,0 +1,3129 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &379727409509034652
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2333377255903728639}
+  m_Layer: 0
+  m_Name: pinky_01_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2333377255903728639
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 379727409509034652}
+  m_LocalRotation: {x: -6.162976e-33, y: -0, z: 1.1055593e-32, w: 1}
+  m_LocalPosition: {x: 0.064088896, y: 0.0034781592, z: -0.023178494}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5683005500533539273}
+  m_Father: {fileID: 1235654288974120821}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &675471732147146461
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2706965206348296268}
+  m_Layer: 0
+  m_Name: pinky_01_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2706965206348296268
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 675471732147146461}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.062418155, y: 0.0005166338, z: 0.030159093}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5734732099825944587}
+  m_Father: {fileID: 2381871880633378125}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &941526586370067865
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1229326576785646757}
+  m_Layer: 0
+  m_Name: middle_02_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1229326576785646757
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 941526586370067865}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.032511543, y: 5.684342e-16, z: -1.2656542e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7251348262452567251}
+  m_Father: {fileID: 7085302112731782838}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &965087136108849892
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6435498278666147816}
+  m_Layer: 0
+  m_Name: mouth_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6435498278666147816
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 965087136108849892}
+  m_LocalRotation: {x: -0.27059805, y: 0.27059805, z: 0.6532815, w: 0.6532815}
+  m_LocalPosition: {x: 0.0019836398, y: -0.08318037, z: -0.026809031}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 8
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1245860822247885338
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8717656615609786348}
+  m_Layer: 0
+  m_Name: thumb_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8717656615609786348
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1245860822247885338}
+  m_LocalRotation: {x: -1.0408341e-17, y: 8.326673e-17, z: -6.938894e-17, w: 1}
+  m_LocalPosition: {x: -0.016968556, y: 0, z: -5.684342e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 1399307818516367495}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1276181074741647128
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 9011662874298370284}
+  m_Layer: 0
+  m_Name: lowerleg_twist_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &9011662874298370284
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1276181074741647128}
+  m_LocalRotation: {x: -8.4703295e-22, y: 6.2733378e-21, z: 1.7347235e-18, w: 1}
+  m_LocalPosition: {x: -0.20236774, y: -4.063416e-16, z: -1.0658141e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 5797689457763641028}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1288154011259633317
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1399307818516367495}
+  m_Layer: 0
+  m_Name: thumb_03_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1399307818516367495
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1288154011259633317}
+  m_LocalRotation: {x: -1.0408341e-17, y: 8.326673e-17, z: -6.938894e-17, w: 1}
+  m_LocalPosition: {x: -0.02211393, y: -5.3290704e-17, z: 5.684342e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8717656615609786348}
+  m_Father: {fileID: 7425529535134085500}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1300082582496039443
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8732582027860113579}
+  m_Layer: 0
+  m_Name: index_03_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8732582027860113579
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1300082582496039443}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.021776097, y: -2.842171e-16, z: 2.1316282e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1093144528369578063}
+  m_Father: {fileID: 7396447316078992807}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1384149698259062147
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4765968336605576769}
+  m_Layer: 0
+  m_Name: head
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4765968336605576769
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1384149698259062147}
+  m_LocalRotation: {x: 9.862232e-33, y: -2.573678e-32, z: -0.09755537, w: 0.9952301}
+  m_LocalPosition: {x: -0.11730746, y: 0, z: -8.0480544e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3632964819607652838}
+  - {fileID: 8548305002072694430}
+  - {fileID: 1611322927906086111}
+  - {fileID: 5963347724840074016}
+  - {fileID: 630057863270563331}
+  - {fileID: 1120541036835219435}
+  - {fileID: 8652725518979701653}
+  - {fileID: 4005615722419245347}
+  - {fileID: 6435498278666147816}
+  - {fileID: 7486482435178862634}
+  m_Father: {fileID: 4036541441739487284}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1500375392436005359
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1093144528369578063}
+  m_Layer: 0
+  m_Name: index_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1093144528369578063
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1500375392436005359}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.016149597, y: -2.842171e-16, z: 2.6645352e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 8732582027860113579}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1520242453525341770
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5232870943066053356}
+  m_Layer: 0
+  m_Name: spine_01
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5232870943066053356
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1520242453525341770}
+  m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: -0.144139, y: 2.7423648e-17, z: 3.281517e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8849924193192343790}
+  m_Father: {fileID: 6057189240968101349}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1532674998865169422
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1177036486261145738}
+  m_Layer: 0
+  m_Name: root
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1177036486261145738
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1532674998865169422}
+  m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: -0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6057189240968101349}
+  m_Father: {fileID: 3048275255403973406}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1659054249789945871
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1914784906034674677}
+  m_Layer: 0
+  m_Name: foot_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1914784906034674677
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1659054249789945871}
+  m_LocalRotation: {x: -2.6472413e-22, y: -0.000017844362, z: -0.014105773, w: 0.9999005}
+  m_LocalPosition: {x: 0.40469593, y: 3.9968028e-16, z: 3.5527136e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6121226893598170575}
+  m_Father: {fileID: 3526673289307765740}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1892457620925601604
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3632964819607652838}
+  m_Layer: 0
+  m_Name: eye_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3632964819607652838
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1892457620925601604}
+  m_LocalRotation: {x: -9.35845e-24, y: 0.000000058196388, z: 0.7071045, w: 0.7071091}
+  m_LocalPosition: {x: -0.06040836, y: -0.07730016, z: -0.03092631}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4878582967942150908}
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2036010733094633182
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6085701098231779569}
+  m_Layer: 0
+  m_Name: middle_01_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6085701098231779569
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2036010733094633182}
+  m_LocalRotation: {x: -6.162976e-33, y: -0, z: 1.1055593e-32, w: 1}
+  m_LocalPosition: {x: 0.07048816, y: 0.014075107, z: 0.00442146}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5579083276472945046}
+  m_Father: {fileID: 1235654288974120821}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2040791951484425846
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3493033813247087006}
+  m_Layer: 0
+  m_Name: eyelid_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3493033813247087006
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2040791951484425846}
+  m_LocalRotation: {x: -1.0842022e-19, y: 3.0092655e-36, z: -2.7755576e-17, w: 1}
+  m_LocalPosition: {x: -0.013280203, y: 0.004055402, z: -0.000049249007}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 630057863270563331}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2067707712791358541
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8173459002536225327}
+  m_Layer: 0
+  m_Name: upperleg_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8173459002536225327
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2067707712791358541}
+  m_LocalRotation: {x: 0, y: 0, z: 0.000055306893, w: 1}
+  m_LocalPosition: {x: 2.842171e-16, y: -5.5844522e-18, z: -0.08740764}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3526673289307765740}
+  - {fileID: 4937483991207633016}
+  m_Father: {fileID: 6057189240968101349}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2159995794789425459
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7425529535134085500}
+  m_Layer: 0
+  m_Name: thumb_02_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7425529535134085500
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2159995794789425459}
+  m_LocalRotation: {x: 0.012267208, y: 0.09777105, z: -0.12388677, w: 0.9873917}
+  m_LocalPosition: {x: -0.037411258, y: 2.6645352e-17, z: -2.842171e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1399307818516367495}
+  m_Father: {fileID: 8131524779932594774}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2379719870419292580
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3048275255403973406}
+  - component: {fileID: 3298639667508344306}
+  - component: {fileID: 3298639667508344305}
+  m_Layer: 0
+  m_Name: Adela
+  m_TagString: NPCAdela
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3048275255403973406
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2379719870419292580}
+  m_LocalRotation: {x: 0, y: 0.99702793, z: 0, w: 0.077041045}
+  m_LocalPosition: {x: -27.850544, y: -0.7753404, z: 26.773067}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1177036486261145738}
+  - {fileID: 7851388775404421171}
+  - {fileID: 1833431233}
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 171.163, z: 0}
+--- !u!136 &3298639667508344306
+CapsuleCollider:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2379719870419292580}
+  m_Material: {fileID: 0}
+  m_IsTrigger: 0
+  m_Enabled: 1
+  m_Radius: 0.5
+  m_Height: 1
+  m_Direction: 1
+  m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &3298639667508344305
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2379719870419292580}
+  m_Enabled: 0
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 12e79cf8a158df3458a65a54273f6704, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  level: {fileID: 0}
+  line: {fileID: 0}
+--- !u!1 &2434332166365921294
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 630057863270563331}
+  m_Layer: 0
+  m_Name: eyelid_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &630057863270563331
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2434332166365921294}
+  m_LocalRotation: {x: -0.0017496326, y: 0.0012722076, z: 0.58809394, w: 0.80878973}
+  m_LocalPosition: {x: -0.06040836, y: -0.07730016, z: -0.03092631}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3493033813247087006}
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 4
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2586739552486513745
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1205757794771949366}
+  m_Layer: 0
+  m_Name: middle_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1205757794771949366
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2586739552486513745}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.017884577, y: -2.842171e-16, z: -1.5987211e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 7251348262452567251}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2703513453991519720
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2381871880633378125}
+  m_Layer: 0
+  m_Name: hand_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2381871880633378125
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2703513453991519720}
+  m_LocalRotation: {x: -0.7071068, y: 3.9252314e-17, z: 3.9252314e-17, w: 0.7071068}
+  m_LocalPosition: {x: -0.25611728, y: 2.6867395e-16, z: -3.126388e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8232029548070193746}
+  - {fileID: 7085302112731782838}
+  - {fileID: 2706965206348296268}
+  - {fileID: 2510794366689268345}
+  - {fileID: 8131524779932594774}
+  m_Father: {fileID: 8501821640402845930}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2808047418840240947
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4036541441739487284}
+  m_Layer: 0
+  m_Name: neck
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4036541441739487284
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2808047418840240947}
+  m_LocalRotation: {x: 0, y: 0, z: 0.09755536, w: 0.9952301}
+  m_LocalPosition: {x: -0.11496538, y: -6.5348675e-18, z: -5.6280105e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4765968336605576769}
+  m_Father: {fileID: 6549102974271056664}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2909642978647179328
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5579083276472945046}
+  m_Layer: 0
+  m_Name: middle_02_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5579083276472945046
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 2909642978647179328}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.032090172, y: 5.684342e-16, z: -1.1990408e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6508795214263027224}
+  m_Father: {fileID: 6085701098231779569}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3301327622561132936
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7019624253871538769}
+  m_Layer: 0
+  m_Name: thumb_03_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7019624253871538769
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3301327622561132936}
+  m_LocalRotation: {x: -3.469447e-18, y: -7.896287e-31, z: -2.7755576e-17, w: 1}
+  m_LocalPosition: {x: 0.0216141, y: 1.7763568e-17, z: 2.842171e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1555873079980989295}
+  m_Father: {fileID: 5446666112866949422}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3316651480253091541
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2598610365053467465}
+  m_Layer: 0
+  m_Name: foot_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2598610365053467465
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3316651480253091541}
+  m_LocalRotation: {x: -1.3552527e-20, y: -4.3368087e-19, z: -5.877472e-39, w: 1}
+  m_LocalPosition: {x: -0.06604811, y: 4.7739588e-17, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 6121226893598170575}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3324816383907017944
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6063728074726562360}
+  m_Layer: 0
+  m_Name: middle_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6063728074726562360
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3324816383907017944}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.018363109, y: 0, z: 1.9095835e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 6508795214263027224}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3328121927105089529
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7085302112731782838}
+  m_Layer: 0
+  m_Name: middle_01_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7085302112731782838
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3328121927105089529}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.07086872, y: -0.0069440035, z: 0.005015406}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1229326576785646757}
+  m_Father: {fileID: 2381871880633378125}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3363670294077184221
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5683005500533539273}
+  m_Layer: 0
+  m_Name: pinky_02_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5683005500533539273
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3363670294077184221}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.021970283, y: 0, z: -1.155187e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6192457538278484708}
+  m_Father: {fileID: 2333377255903728639}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3394425814870398845
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7566458759318757895}
+  m_Layer: 0
+  m_Name: eyelid_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7566458759318757895
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3394425814870398845}
+  m_LocalRotation: {x: 0.0109661175, y: 0.010199106, z: -0.5485404, w: 0.8359899}
+  m_LocalPosition: {x: -0.012670721, y: 0.0034062627, z: -0.00004760648}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 1120541036835219435}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3497497521138454366
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8548305002072694430}
+  m_Layer: 0
+  m_Name: eye_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8548305002072694430
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3497497521138454366}
+  m_LocalRotation: {x: 4.679254e-24, y: 0.000000047187765, z: 0.70710886, w: 0.70710474}
+  m_LocalPosition: {x: -0.061466966, y: -0.07643084, z: 0.030879663}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8141532262376350942}
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3497725119550956868
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8849924193192343790}
+  m_Layer: 0
+  m_Name: spine_02
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8849924193192343790
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3497725119550956868}
+  m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: -0.09970581, y: -1.6826538e-18, z: -7.7241484e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6549102974271056664}
+  m_Father: {fileID: 5232870943066053356}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3633611172236366700
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3086696189437197051}
+  m_Layer: 0
+  m_Name: ring_03_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3086696189437197051
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3633611172236366700}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.022399196, y: 0, z: 6.6335825e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4483765489176736329}
+  m_Father: {fileID: 2728314977223220296}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3644088884473622794
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3703143824787509052}
+  m_Layer: 0
+  m_Name: index_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3703143824787509052
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3644088884473622794}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.015656965, y: 0, z: -1.2700951e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 7789716133437972959}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3716733590526910452
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 990906300371023220}
+  m_Layer: 0
+  m_Name: lowerarm_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &990906300371023220
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3716733590526910452}
+  m_LocalRotation: {x: 0, y: -1.3596311e-32, z: -0, w: 1}
+  m_LocalPosition: {x: 0.27836433, y: -1.22413185e-14, z: -3.9790393e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1235654288974120821}
+  - {fileID: 6297065865115258144}
+  m_Father: {fileID: 5371424928626673232}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3770605870673730475
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7251348262452567251}
+  m_Layer: 0
+  m_Name: middle_03_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7251348262452567251
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3770605870673730475}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.024235977, y: -5.684342e-16, z: 9.325873e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1205757794771949366}
+  m_Father: {fileID: 1229326576785646757}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3842192035693038441
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1384636291380193848}
+  m_Layer: 0
+  m_Name: pinky_03_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1384636291380193848
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3842192035693038441}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.016199136, y: -2.842171e-16, z: 4.2188474e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 805552286481881691}
+  m_Father: {fileID: 5734732099825944587}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &3889406302072827820
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7396447316078992807}
+  m_Layer: 0
+  m_Name: index_02_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7396447316078992807
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3889406302072827820}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.028745528, y: 0, z: -8.8817837e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8732582027860113579}
+  m_Father: {fileID: 3699206521418305661}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4091075075194963918
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2510794366689268345}
+  m_Layer: 0
+  m_Name: ring_01_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2510794366689268345
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4091075075194963918}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.067621246, y: -0.0056693503, z: 0.018652089}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 2728314977223220296}
+  m_Father: {fileID: 2381871880633378125}
+  m_RootOrder: 3
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4104135424923591643
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2728314977223220296}
+  m_Layer: 0
+  m_Name: ring_02_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2728314977223220296
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4104135424923591643}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.02836622, y: 5.684342e-16, z: -8.3766323e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3086696189437197051}
+  m_Father: {fileID: 2510794366689268345}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4334365935826787427
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5734732099825944587}
+  m_Layer: 0
+  m_Name: pinky_02_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5734732099825944587
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4334365935826787427}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.021235589, y: 5.684342e-16, z: -4.2188474e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1384636291380193848}
+  m_Father: {fileID: 2706965206348296268}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4499203677319251574
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8652725518979701653}
+  m_Layer: 0
+  m_Name: head_end
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8652725518979701653
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4499203677319251574}
+  m_LocalRotation: {x: 1.2325952e-32, y: -1.540744e-32, z: 4.135903e-25, w: 1}
+  m_LocalPosition: {x: -0.16172896, y: -6.661338e-17, z: 3.770767e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 6
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4505596580003276185
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3699206521418305661}
+  m_Layer: 0
+  m_Name: index_01_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3699206521418305661
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4505596580003276185}
+  m_LocalRotation: {x: -6.162976e-33, y: -0, z: 1.1055593e-32, w: 1}
+  m_LocalPosition: {x: 0.07225355, y: 0.007858025, z: 0.019997204}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7396447316078992807}
+  m_Father: {fileID: 1235654288974120821}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4612622841448834914
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8141532262376350942}
+  m_Layer: 0
+  m_Name: eye_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8141532262376350942
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4612622841448834914}
+  m_LocalRotation: {x: -1.6793019e-24, y: -6.617445e-24, z: -0, w: 1}
+  m_LocalPosition: {x: -0.010719967, y: -1.8019363e-13, z: 8.881784e-18}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 8548305002072694430}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4787569635857434928
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4937483991207633016}
+  m_Layer: 0
+  m_Name: upperleg_twist_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4937483991207633016
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4787569635857434928}
+  m_LocalRotation: {x: 0, y: -4.3739675e-33, z: -0, w: 1}
+  m_LocalPosition: {x: 0.24532321, y: 2.6784008e-14, z: -9.180212e-14}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 8173459002536225327}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &4798973474144671985
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6057189240968101349}
+  m_Layer: 0
+  m_Name: hip
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6057189240968101349
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 4798973474144671985}
+  m_LocalRotation: {x: -0.5, y: 0.5, z: -0.5, w: 0.5}
+  m_LocalPosition: {x: -0, y: 1.0428139, z: 1.8175173e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5232870943066053356}
+  - {fileID: 8173459002536225327}
+  - {fileID: 6056169477193609007}
+  m_Father: {fileID: 1177036486261145738}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5004198440867160901
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5963347724840074016}
+  m_Layer: 0
+  m_Name: eyebrow_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5963347724840074016
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5004198440867160901}
+  m_LocalRotation: {x: 0.27059805, y: -0.6532815, z: 0.6532815, w: 0.27059805}
+  m_LocalPosition: {x: -0.08243206, y: -0.10192263, z: 0.022408087}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 3
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5329693526255344124
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4483765489176736329}
+  m_Layer: 0
+  m_Name: ring_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4483765489176736329
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5329693526255344124}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.016921526, y: 2.842171e-16, z: 7.0221606e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 3086696189437197051}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5464028284334996836
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6121226893598170575}
+  m_Layer: 0
+  m_Name: ball_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6121226893598170575
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5464028284334996836}
+  m_LocalRotation: {x: 0.001910514, y: -0.001950124, z: 0.7143215, w: 0.6998124}
+  m_LocalPosition: {x: 0.14122562, y: -0.07719882, z: 0.0000059755184}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 2598610365053467465}
+  m_Father: {fileID: 1914784906034674677}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5544043307979086622
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1611322927906086111}
+  m_Layer: 0
+  m_Name: eyebrow_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1611322927906086111
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5544043307979086622}
+  m_LocalRotation: {x: -0.27059805, y: 0.6532815, z: 0.6532815, w: 0.27059805}
+  m_LocalPosition: {x: -0.08257198, y: -0.1027443, z: -0.021922244}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5634485349363937810
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5467602459146927714}
+  m_Layer: 0
+  m_Name: upperarm_twist_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5467602459146927714
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5634485349363937810}
+  m_LocalRotation: {x: 0, y: -1.3596311e-32, z: -0, w: 1}
+  m_LocalPosition: {x: 0.13918217, y: -6.1128877e-15, z: -1.9895197e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 5371424928626673232}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5744942044164140600
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4878582967942150908}
+  m_Layer: 0
+  m_Name: eye_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4878582967942150908
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5744942044164140600}
+  m_LocalRotation: {x: -1.0442246e-24, y: 0, z: -4.2351647e-22, w: 1}
+  m_LocalPosition: {x: -0.010620728, y: -2.2367885e-13, z: 8.881784e-18}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 3632964819607652838}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5871525005141805475
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6330925767624484876}
+  m_Layer: 0
+  m_Name: lowerleg_twist_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6330925767624484876
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5871525005141805475}
+  m_LocalRotation: {x: -1.0587912e-22, y: -5.5396286e-21, z: -5.87e-43, w: 1}
+  m_LocalPosition: {x: 0.20234796, y: 2.1316282e-16, z: 7.105427e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 3526673289307765740}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5989470711784412856
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5797689457763641028}
+  m_Layer: 0
+  m_Name: lowerleg_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5797689457763641028
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5989470711784412856}
+  m_LocalRotation: {x: -3.8120845e-21, y: 0.00025548242, z: 0.015125982, w: 0.99988556}
+  m_LocalPosition: {x: -0.49525276, y: 8.0280326e-14, z: -0.0000000013120741}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 944637072798990781}
+  - {fileID: 9011662874298370284}
+  m_Father: {fileID: 6056169477193609007}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &5989809495221754017
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 944637072798990781}
+  m_Layer: 0
+  m_Name: foot_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &944637072798990781
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5989809495221754017}
+  m_LocalRotation: {x: -3.388513e-21, y: -0.00025548288, z: -0.015000927, w: 0.99988747}
+  m_LocalPosition: {x: -0.40473548, y: -8.171241e-16, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4773792029926832471}
+  m_Father: {fileID: 5797689457763641028}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6072873166038203041
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6508795214263027224}
+  m_Layer: 0
+  m_Name: middle_03_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6508795214263027224
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6072873166038203041}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.024449248, y: -2.842171e-16, z: -7.5495166e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6063728074726562360}
+  m_Father: {fileID: 5579083276472945046}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6117046051327150587
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 805552286481881691}
+  m_Layer: 0
+  m_Name: pinky_end_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &805552286481881691
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6117046051327150587}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.012699278, y: 0, z: 1.04360964e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 1384636291380193848}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6227119209750102184
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8397595691551312658}
+  m_Layer: 0
+  m_Name: index_02_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8397595691551312658
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6227119209750102184}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.028341558, y: 0, z: -5.506706e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7789716133437972959}
+  m_Father: {fileID: 8232029548070193746}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6271459403690338065
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7837601993634560944}
+  m_Layer: 0
+  m_Name: ring_01_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7837601993634560944
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6271459403690338065}
+  m_LocalRotation: {x: -6.162976e-33, y: -0, z: 1.1055593e-32, w: 1}
+  m_LocalPosition: {x: 0.06654651, y: 0.011661983, z: -0.011422721}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5290159138084751146}
+  m_Father: {fileID: 1235654288974120821}
+  m_RootOrder: 3
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6346408185445617978
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1944928418732748329}
+  m_Layer: 0
+  m_Name: pinky_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1944928418732748329
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6346408185445617978}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.012459457, y: 2.842171e-16, z: 3.3928415e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 6192457538278484708}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6425440582158911095
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6549102974271056664}
+  m_Layer: 0
+  m_Name: spine_03
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6549102974271056664
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6425440582158911095}
+  m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: -0.13022494, y: -2.2261755e-18, z: -7.879215e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4036541441739487284}
+  - {fileID: 8328599160947734887}
+  - {fileID: 2636606874439261541}
+  m_Father: {fileID: 8849924193192343790}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6514853058118864798
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2636606874439261541}
+  m_Layer: 0
+  m_Name: shoulder_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2636606874439261541
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6514853058118864798}
+  m_LocalRotation: {x: 0.7071068, y: -4.3297806e-17, z: 0.7071068, w: 4.3297806e-17}
+  m_LocalPosition: {x: -0.05727343, y: -0.019993229, z: 0.031052308}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5371424928626673232}
+  m_Father: {fileID: 6549102974271056664}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6522783482989596174
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2872556629755329813}
+  m_Layer: 0
+  m_Name: ring_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2872556629755329813
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6522783482989596174}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.017390132, y: 0, z: 3.0087044e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4308802083595323723}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6545788732741508948
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4005615722419245347}
+  m_Layer: 0
+  m_Name: jaw
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4005615722419245347
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6545788732741508948}
+  m_LocalRotation: {x: 9.96544e-28, y: -1.5065033e-27, z: 0.86645687, w: 0.49925193}
+  m_LocalPosition: {x: -0.00937338, y: -0.016478842, z: 0.00032256785}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7586176476468479897}
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 7
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6742279389967054361
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1555873079980989295}
+  m_Layer: 0
+  m_Name: thumb_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1555873079980989295
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6742279389967054361}
+  m_LocalRotation: {x: -3.469447e-18, y: -3.9450723e-30, z: -2.7755576e-17, w: 1}
+  m_LocalPosition: {x: 0.016219752, y: 3.5527136e-17, z: -5.684342e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 7019624253871538769}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6915012426975172168
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2711041118277527108}
+  m_Layer: 0
+  m_Name: upperarm_twist_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2711041118277527108
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6915012426975172168}
+  m_LocalRotation: {x: 0, y: 5.551115e-17, z: -0, w: 1}
+  m_LocalPosition: {x: -0.1361888, y: 1.827427e-15, z: -2.842171e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 6891101649567858718}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &6916364700140526561
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5446666112866949422}
+  m_Layer: 0
+  m_Name: thumb_02_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5446666112866949422
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6916364700140526561}
+  m_LocalRotation: {x: 0.012267208, y: 0.09777105, z: -0.12388677, w: 0.9873917}
+  m_LocalPosition: {x: 0.03736786, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7019624253871538769}
+  m_Father: {fileID: 7766707717875594074}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7121904016237745335
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8131524779932594774}
+  m_Layer: 0
+  m_Name: thumb_01_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8131524779932594774
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7121904016237745335}
+  m_LocalRotation: {x: 0.68951714, y: -0.15673575, z: 0.018466607, w: 0.7068656}
+  m_LocalPosition: {x: -0.026103973, y: 0.01112954, z: -0.015895331}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7425529535134085500}
+  m_Father: {fileID: 2381871880633378125}
+  m_RootOrder: 4
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7130349609533785938
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6891101649567858718}
+  m_Layer: 0
+  m_Name: upperarm_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6891101649567858718
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7130349609533785938}
+  m_LocalRotation: {x: 0, y: 5.551115e-17, z: -0, w: 1}
+  m_LocalPosition: {x: -0.14487943, y: -1.11022296e-17, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8501821640402845930}
+  - {fileID: 2711041118277527108}
+  m_Father: {fileID: 8328599160947734887}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7550055102976452673
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7789716133437972959}
+  m_Layer: 0
+  m_Name: index_03_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7789716133437972959
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7550055102976452673}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.021378875, y: 0, z: -6.217249e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3703143824787509052}
+  m_Father: {fileID: 8397595691551312658}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7625519603523565268
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6192457538278484708}
+  m_Layer: 0
+  m_Name: pinky_03_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6192457538278484708
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7625519603523565268}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.016703656, y: -8.5265126e-16, z: -2.1643797e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1944928418732748329}
+  m_Father: {fileID: 5683005500533539273}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7671135813531876400
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4308802083595323723}
+  m_Layer: 0
+  m_Name: ring_03_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4308802083595323723
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7671135813531876400}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.02281481, y: 0, z: -5.662137e-17}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 2872556629755329813}
+  m_Father: {fileID: 5290159138084751146}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7715674847474014800
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5290159138084751146}
+  m_Layer: 0
+  m_Name: ring_02_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5290159138084751146
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7715674847474014800}
+  m_LocalRotation: {x: 0, y: 0, z: -7.7780533e-31, w: 1}
+  m_LocalPosition: {x: 0.030086216, y: 0, z: -1.050271e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4308802083595323723}
+  m_Father: {fileID: 7837601993634560944}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7786028367846991431
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7486482435178862634}
+  m_Layer: 0
+  m_Name: mouth_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7486482435178862634
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7786028367846991431}
+  m_LocalRotation: {x: 0.27059805, y: -0.27059805, z: 0.6532815, w: 0.6532815}
+  m_LocalPosition: {x: 0.002089301, y: -0.08510672, z: 0.02683015}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 9
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7970968868289428737
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1235654288974120821}
+  m_Layer: 0
+  m_Name: hand_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1235654288974120821
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7970968868289428737}
+  m_LocalRotation: {x: -0.7071068, y: -1.0740573e-32, z: -1.0740573e-32, w: 0.7071068}
+  m_LocalPosition: {x: 0.26139042, y: -2.5979218e-16, z: -1.7053025e-15}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 3699206521418305661}
+  - {fileID: 6085701098231779569}
+  - {fileID: 2333377255903728639}
+  - {fileID: 7837601993634560944}
+  - {fileID: 7766707717875594074}
+  m_Father: {fileID: 990906300371023220}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &7973204751558769331
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2949085971857437748}
+  m_Layer: 0
+  m_Name: lowerarm_twist_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2949085971857437748
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7973204751558769331}
+  m_LocalRotation: {x: 0, y: 5.551115e-17, z: -0, w: 1}
+  m_LocalPosition: {x: -0.12805864, y: 1.398881e-16, z: -5.684342e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 8501821640402845930}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8063690618393414639
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7766707717875594074}
+  m_Layer: 0
+  m_Name: thumb_01_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7766707717875594074
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8063690618393414639}
+  m_LocalRotation: {x: 0.68951714, y: -0.15673575, z: 0.018466607, w: 0.7068656}
+  m_LocalPosition: {x: 0.028872468, y: -0.005484139, z: 0.023446606}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5446666112866949422}
+  m_Father: {fileID: 1235654288974120821}
+  m_RootOrder: 4
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8110287498807290464
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5371424928626673232}
+  m_Layer: 0
+  m_Name: upperarm_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5371424928626673232
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8110287498807290464}
+  m_LocalRotation: {x: 0, y: -1.3596311e-32, z: -0, w: 1}
+  m_LocalPosition: {x: 0.1374886, y: 8.881784e-18, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 990906300371023220}
+  - {fileID: 5467602459146927714}
+  m_Father: {fileID: 2636606874439261541}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8205756566230551705
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8501821640402845930}
+  m_Layer: 0
+  m_Name: lowerarm_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8501821640402845930
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8205756566230551705}
+  m_LocalRotation: {x: 0, y: 5.551115e-17, z: -0, w: 1}
+  m_LocalPosition: {x: -0.2723776, y: 3.639311e-15, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 2381871880633378125}
+  - {fileID: 2949085971857437748}
+  m_Father: {fileID: 6891101649567858718}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8233492288097718082
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8232029548070193746}
+  m_Layer: 0
+  m_Name: index_01_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8232029548070193746
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8233492288097718082}
+  m_LocalRotation: {x: 0, y: 0, z: 5.551115e-17, w: 1}
+  m_LocalPosition: {x: -0.07285818, y: -0.0022132832, z: -0.009658413}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8397595691551312658}
+  m_Father: {fileID: 2381871880633378125}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8355339764653902194
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6056169477193609007}
+  m_Layer: 0
+  m_Name: upperleg_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6056169477193609007
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8355339764653902194}
+  m_LocalRotation: {x: 0.0000000013246513, y: 1.7977961e-12, z: 1, w: 0.00012506921}
+  m_LocalPosition: {x: -0.0000046709124, y: -0.00000063650634, z: 0.08740767}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5797689457763641028}
+  - {fileID: 2457292444525664468}
+  m_Father: {fileID: 6057189240968101349}
+  m_RootOrder: 2
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8376084579985539296
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 1120541036835219435}
+  m_Layer: 0
+  m_Name: eyelid_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &1120541036835219435
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8376084579985539296}
+  m_LocalRotation: {x: -0.0017496326, y: 0.0012722076, z: 0.58809394, w: 0.80878973}
+  m_LocalPosition: {x: -0.061466966, y: -0.07643084, z: 0.030879663}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 7566458759318757895}
+  m_Father: {fileID: 4765968336605576769}
+  m_RootOrder: 5
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8588559721898743378
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8328599160947734887}
+  m_Layer: 0
+  m_Name: shoulder_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &8328599160947734887
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8588559721898743378}
+  m_LocalRotation: {x: -0, y: -0.7071068, z: 0, w: 0.7071068}
+  m_LocalPosition: {x: -0.05727343, y: -0.019993229, z: -0.029778821}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 6891101649567858718}
+  m_Father: {fileID: 6549102974271056664}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8601109527932264964
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2457292444525664468}
+  m_Layer: 0
+  m_Name: upperleg_twist_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &2457292444525664468
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8601109527932264964}
+  m_LocalRotation: {x: 0, y: -1.643059e-33, z: 2.7105054e-20, w: 1}
+  m_LocalPosition: {x: -0.24762638, y: 4.0139834e-14, z: -6.560371e-10}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 6056169477193609007}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8656647538822277210
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7586176476468479897}
+  m_Layer: 0
+  m_Name: jaw_end
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7586176476468479897
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8656647538822277210}
+  m_LocalRotation: {x: -3.6977855e-32, y: 0, z: -2.7755576e-17, w: 1}
+  m_LocalPosition: {x: -0.087921016, y: -2.842171e-16, z: 5.0519316e-13}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4005615722419245347}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8771896191595599921
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3526673289307765740}
+  m_Layer: 0
+  m_Name: lowerleg_l
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3526673289307765740
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8771896191595599921}
+  m_LocalRotation: {x: -2.6472392e-23, y: 0.000017844377, z: 0.014050471, w: 0.9999013}
+  m_LocalPosition: {x: 0.49064642, y: 5.356793e-14, z: -1.8358648e-13}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 1914784906034674677}
+  - {fileID: 6330925767624484876}
+  m_Father: {fileID: 8173459002536225327}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8805611655738709981
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4773792029926832471}
+  m_Layer: 0
+  m_Name: ball_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &4773792029926832471
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8805611655738709981}
+  m_LocalRotation: {x: 0.0010808189, y: -0.0010444788, z: 0.69491374, w: 0.71909153}
+  m_LocalPosition: {x: -0.13590692, y: 0.078868814, z: 0.000007632168}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 5956666630224227110}
+  m_Father: {fileID: 944637072798990781}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &8821256188286696894
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7851388775404421171}
+  - component: {fileID: 5673914869434077079}
+  m_Layer: 0
+  m_Name: rp_claudia_rigged_002_geo
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &7851388775404421171
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8821256188286696894}
+  m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: -0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 3048275255403973406}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!137 &5673914869434077079
+SkinnedMeshRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8821256188286696894}
+  m_Enabled: 1
+  m_CastShadows: 1
+  m_ReceiveShadows: 1
+  m_DynamicOccludee: 1
+  m_StaticShadowCaster: 0
+  m_MotionVectors: 1
+  m_LightProbeUsage: 1
+  m_ReflectionProbeUsage: 1
+  m_RayTracingMode: 3
+  m_RayTraceProcedural: 0
+  m_RenderingLayerMask: 1
+  m_RendererPriority: 0
+  m_Materials:
+  - {fileID: -8276828155560707188, guid: fe48b53dab70cbf429029311afb3201a, type: 3}
+  m_StaticBatchInfo:
+    firstSubMesh: 0
+    subMeshCount: 0
+  m_StaticBatchRoot: {fileID: 0}
+  m_ProbeAnchor: {fileID: 0}
+  m_LightProbeVolumeOverride: {fileID: 0}
+  m_ScaleInLightmap: 1
+  m_ReceiveGI: 1
+  m_PreserveUVs: 0
+  m_IgnoreNormalsForChartDetection: 0
+  m_ImportantGI: 0
+  m_StitchLightmapSeams: 1
+  m_SelectedEditorRenderState: 3
+  m_MinimumChartSize: 4
+  m_AutoUVMaxDistance: 0.5
+  m_AutoUVMaxAngle: 89
+  m_LightmapParameters: {fileID: 0}
+  m_SortingLayerID: 0
+  m_SortingLayer: 0
+  m_SortingOrder: 0
+  serializedVersion: 2
+  m_Quality: 0
+  m_UpdateWhenOffscreen: 0
+  m_SkinnedMotionVectors: 1
+  m_Mesh: {fileID: 8906223023160854984, guid: fe48b53dab70cbf429029311afb3201a, type: 3}
+  m_Bones:
+  - {fileID: 1177036486261145738}
+  - {fileID: 6057189240968101349}
+  - {fileID: 5232870943066053356}
+  - {fileID: 8849924193192343790}
+  - {fileID: 6549102974271056664}
+  - {fileID: 4036541441739487284}
+  - {fileID: 4765968336605576769}
+  - {fileID: 8652725518979701653}
+  - {fileID: 4005615722419245347}
+  - {fileID: 7586176476468479897}
+  - {fileID: 3632964819607652838}
+  - {fileID: 4878582967942150908}
+  - {fileID: 8548305002072694430}
+  - {fileID: 8141532262376350942}
+  - {fileID: 8328599160947734887}
+  - {fileID: 6891101649567858718}
+  - {fileID: 8501821640402845930}
+  - {fileID: 2381871880633378125}
+  - {fileID: 8131524779932594774}
+  - {fileID: 7425529535134085500}
+  - {fileID: 1399307818516367495}
+  - {fileID: 8717656615609786348}
+  - {fileID: 8232029548070193746}
+  - {fileID: 8397595691551312658}
+  - {fileID: 7789716133437972959}
+  - {fileID: 3703143824787509052}
+  - {fileID: 7085302112731782838}
+  - {fileID: 1229326576785646757}
+  - {fileID: 7251348262452567251}
+  - {fileID: 1205757794771949366}
+  - {fileID: 2510794366689268345}
+  - {fileID: 2728314977223220296}
+  - {fileID: 3086696189437197051}
+  - {fileID: 4483765489176736329}
+  - {fileID: 2706965206348296268}
+  - {fileID: 5734732099825944587}
+  - {fileID: 1384636291380193848}
+  - {fileID: 805552286481881691}
+  - {fileID: 2949085971857437748}
+  - {fileID: 2711041118277527108}
+  - {fileID: 2636606874439261541}
+  - {fileID: 5371424928626673232}
+  - {fileID: 990906300371023220}
+  - {fileID: 1235654288974120821}
+  - {fileID: 7766707717875594074}
+  - {fileID: 5446666112866949422}
+  - {fileID: 7019624253871538769}
+  - {fileID: 1555873079980989295}
+  - {fileID: 3699206521418305661}
+  - {fileID: 7396447316078992807}
+  - {fileID: 8732582027860113579}
+  - {fileID: 1093144528369578063}
+  - {fileID: 6085701098231779569}
+  - {fileID: 5579083276472945046}
+  - {fileID: 6508795214263027224}
+  - {fileID: 6063728074726562360}
+  - {fileID: 7837601993634560944}
+  - {fileID: 5290159138084751146}
+  - {fileID: 4308802083595323723}
+  - {fileID: 2872556629755329813}
+  - {fileID: 2333377255903728639}
+  - {fileID: 5683005500533539273}
+  - {fileID: 6192457538278484708}
+  - {fileID: 1944928418732748329}
+  - {fileID: 6297065865115258144}
+  - {fileID: 5467602459146927714}
+  - {fileID: 8173459002536225327}
+  - {fileID: 3526673289307765740}
+  - {fileID: 1914784906034674677}
+  - {fileID: 6121226893598170575}
+  - {fileID: 2598610365053467465}
+  - {fileID: 6330925767624484876}
+  - {fileID: 4937483991207633016}
+  - {fileID: 6056169477193609007}
+  - {fileID: 5797689457763641028}
+  - {fileID: 944637072798990781}
+  - {fileID: 4773792029926832471}
+  - {fileID: 5956666630224227110}
+  - {fileID: 9011662874298370284}
+  - {fileID: 2457292444525664468}
+  - {fileID: 6435498278666147816}
+  - {fileID: 7486482435178862634}
+  - {fileID: 1120541036835219435}
+  - {fileID: 630057863270563331}
+  - {fileID: 3493033813247087006}
+  - {fileID: 7566458759318757895}
+  - {fileID: 1611322927906086111}
+  - {fileID: 5963347724840074016}
+  m_BlendShapeWeights: []
+  m_RootBone: {fileID: 1177036486261145738}
+  m_AABB:
+    m_Center: {x: 0.0015544593, y: 0.9115197, z: 0.010714211}
+    m_Extent: {x: 0.85526705, y: 0.9208743, z: 0.14658794}
+  m_DirtyAABB: 0
+--- !u!1 &9130755989438875117
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 6297065865115258144}
+  m_Layer: 0
+  m_Name: lowerarm_twist_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &6297065865115258144
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 9130755989438875117}
+  m_LocalRotation: {x: 0, y: -1.3596311e-32, z: -0, w: 1}
+  m_LocalPosition: {x: 0.13069521, y: -1.1324275e-16, z: 5.684342e-16}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 990906300371023220}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &9205267878716793024
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 5956666630224227110}
+  m_Layer: 0
+  m_Name: foot_end_r
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &5956666630224227110
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 9205267878716793024}
+  m_LocalRotation: {x: 1.3552527e-20, y: -4.3368087e-19, z: 3.469447e-18, w: 1}
+  m_LocalPosition: {x: 0.055463985, y: -3.330669e-18, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 4773792029926832471}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1001 &3030729321320720810
+PrefabInstance:
+  m_ObjectHideFlags: 0
+  serializedVersion: 2
+  m_Modification:
+    m_TransformParent: {fileID: 3048275255403973406}
+    m_Modifications:
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_RootOrder
+      value: 2
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalPosition.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalPosition.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalPosition.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalRotation.w
+      value: 1
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalRotation.x
+      value: -0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalRotation.y
+      value: -0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalRotation.z
+      value: -0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.x
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.y
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_LocalEulerAnglesHint.z
+      value: 0
+      objectReference: {fileID: 0}
+    - target: {fileID: 3030729322717075820, guid: c8f52536e66c8874995464afe5ee6130,
+        type: 3}
+      propertyPath: m_Name
+      value: DialogueTrigger
+      objectReference: {fileID: 0}
+    m_RemovedComponents: []
+  m_SourcePrefab: {fileID: 100100000, guid: c8f52536e66c8874995464afe5ee6130, type: 3}
+--- !u!4 &1833431233 stripped
+Transform:
+  m_CorrespondingSourceObject: {fileID: 3030729322717075819, guid: c8f52536e66c8874995464afe5ee6130,
+    type: 3}
+  m_PrefabInstance: {fileID: 3030729321320720810}
+  m_PrefabAsset: {fileID: 0}
diff --git a/Assets/SZZ/Prefabs/Adela.prefab.meta b/Assets/SZZ/Prefabs/Adela.prefab.meta
new file mode 100644
index 0000000..b2e351e
--- /dev/null
+++ b/Assets/SZZ/Prefabs/Adela.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3f8c48c33c3c37e44af130793093f4c3
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Prefabs/DialogueTrigger.prefab b/Assets/SZZ/Prefabs/DialogueTrigger.prefab
new file mode 100644
index 0000000..b022d92
--- /dev/null
+++ b/Assets/SZZ/Prefabs/DialogueTrigger.prefab
@@ -0,0 +1,61 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &3030729322717075820
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 3030729322717075819}
+  - component: {fileID: 3030729322717075817}
+  - component: {fileID: 3030729322717075818}
+  m_Layer: 10
+  m_Name: DialogueTrigger
+  m_TagString: Interactable
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!4 &3030729322717075819
+Transform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3030729322717075820}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!65 &3030729322717075817
+BoxCollider:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3030729322717075820}
+  m_Material: {fileID: 0}
+  m_IsTrigger: 1
+  m_Enabled: 1
+  serializedVersion: 2
+  m_Size: {x: 2, y: 1, z: 2}
+  m_Center: {x: 0, y: 1.13, z: 0.7}
+--- !u!114 &3030729322717075818
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3030729322717075820}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 063913b232069524eb0bb1c306582ea0, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  inkJSON: {fileID: 4900000, guid: f155cfb465e063c44ac75fc3bd37e7a3, type: 3}
diff --git a/Assets/SZZ/Prefabs/DialogueTrigger.prefab.meta b/Assets/SZZ/Prefabs/DialogueTrigger.prefab.meta
new file mode 100644
index 0000000..1e55b8f
--- /dev/null
+++ b/Assets/SZZ/Prefabs/DialogueTrigger.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c8f52536e66c8874995464afe5ee6130
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/SZZ/Prefabs/PlayerCapsule.prefab b/Assets/SZZ/Prefabs/PlayerCapsule.prefab
index 0644c78..864965f 100644
--- a/Assets/SZZ/Prefabs/PlayerCapsule.prefab
+++ b/Assets/SZZ/Prefabs/PlayerCapsule.prefab
@@ -279,7 +279,7 @@ MonoBehaviour:
   m_PrefabInstance: {fileID: 0}
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 8616685848737228372}
-  m_Enabled: 1
+  m_Enabled: 0
   m_EditorHideFlags: 0
   m_Script: {fileID: 11500000, guid: a30ace0f9b15e2c4b8aa89a1aab08c0a, type: 3}
   m_Name: 
@@ -289,3 +289,7 @@ MonoBehaviour:
   MAX_CHEATS: 5
   AdviceAmount: 0
   AvailableMistakes: 0
+  level: {fileID: 0}
+  imageContainer: {fileID: 0}
+  images: []
+  line: {fileID: 0}
diff --git a/Assets/SZZ/Prefabs/View.prefab b/Assets/SZZ/Prefabs/View.prefab
index 7201e93..416cfe6 100644
--- a/Assets/SZZ/Prefabs/View.prefab
+++ b/Assets/SZZ/Prefabs/View.prefab
@@ -871,7 +871,7 @@ GameObject:
   m_Component:
   - component: {fileID: 6909880372566592212}
   m_Layer: 5
-  m_Name: PlayerUI
+  m_Name: HUD
   m_TagString: Untagged
   m_Icon: {fileID: 0}
   m_NavMeshLayer: 0
@@ -954,7 +954,7 @@ GameObject:
   m_Component:
   - component: {fileID: 6909880372810032764}
   m_Layer: 5
-  m_Name: Container E
+  m_Name: Interact
   m_TagString: Untagged
   m_Icon: {fileID: 0}
   m_NavMeshLayer: 0
@@ -969,14 +969,14 @@ RectTransform:
   m_GameObject: {fileID: 6909880372810032739}
   m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
   m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 2.6375, y: 2.6375, z: 2.6375}
+  m_LocalScale: {x: 2.6375003, y: 2.6375003, z: 2.6375003}
   m_ConstrainProportionsScale: 1
   m_Children:
   - {fileID: 6909880372416141384}
   - {fileID: 6909880372232532608}
   - {fileID: 6909880373659940258}
-  m_Father: {fileID: 6909880373219561751}
-  m_RootOrder: 1
+  m_Father: {fileID: 6909880373574815807}
+  m_RootOrder: 2
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 0.5}
   m_AnchorMax: {x: 0.5, y: 0.5}
@@ -1013,7 +1013,7 @@ RectTransform:
   m_Children:
   - {fileID: 6909880372489281636}
   m_Father: {fileID: 6909880373574815807}
-  m_RootOrder: 1
+  m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 0}
   m_AnchorMax: {x: 0.5, y: 0}
@@ -1306,60 +1306,6 @@ MonoBehaviour:
   m_FillOrigin: 0
   m_UseSpriteMesh: 0
   m_PixelsPerUnitMultiplier: 1
---- !u!1 &6909880373219561750
-GameObject:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  serializedVersion: 6
-  m_Component:
-  - component: {fileID: 6909880373219561751}
-  - component: {fileID: 6909880373219561744}
-  m_Layer: 5
-  m_Name: PlayerInteractUI
-  m_TagString: Untagged
-  m_Icon: {fileID: 0}
-  m_NavMeshLayer: 0
-  m_StaticEditorFlags: 0
-  m_IsActive: 1
---- !u!224 &6909880373219561751
-RectTransform:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6909880373219561750}
-  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
-  m_Children:
-  - {fileID: 6909880373533192468}
-  - {fileID: 6909880372810032764}
-  m_Father: {fileID: 6909880373574815807}
-  m_RootOrder: 0
-  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0.5, y: 0.5}
-  m_AnchorMax: {x: 0.5, y: 0.5}
-  m_AnchoredPosition: {x: 0, y: 0}
-  m_SizeDelta: {x: 0, y: 0}
-  m_Pivot: {x: 0.5, y: 0.5}
---- !u!114 &6909880373219561744
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6909880373219561750}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: d912b1907980aa548b019067951c21ff, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  containerGameObjectE: {fileID: 6909880372810032739}
-  containerGameObjectF: {fileID: 6909880373533192475}
-  playerInteract: {fileID: 1503659779496013627}
 --- !u!1 &6909880373295203197
 GameObject:
   m_ObjectHideFlags: 0
@@ -1892,7 +1838,7 @@ GameObject:
   m_Component:
   - component: {fileID: 6909880373533192468}
   m_Layer: 5
-  m_Name: Container F
+  m_Name: PayRespects
   m_TagString: Untagged
   m_Icon: {fileID: 0}
   m_NavMeshLayer: 0
@@ -1907,18 +1853,18 @@ RectTransform:
   m_GameObject: {fileID: 6909880373533192475}
   m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
   m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 2.6375, y: 2.6375, z: 2.6375}
+  m_LocalScale: {x: 2.6375003, y: 2.6375003, z: 2.6375003}
   m_ConstrainProportionsScale: 0
   m_Children:
   - {fileID: 6909880371816490731}
   - {fileID: 6909880373295203198}
   - {fileID: 6909880372113319953}
-  m_Father: {fileID: 6909880373219561751}
-  m_RootOrder: 0
+  m_Father: {fileID: 6909880373574815807}
+  m_RootOrder: 1
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 1, y: 0}
   m_AnchorMax: {x: 1, y: 0}
-  m_AnchoredPosition: {x: 277.66, y: -351}
+  m_AnchoredPosition: {x: -1450.3402, y: 621.0001}
   m_SizeDelta: {x: 0, y: 0}
   m_Pivot: {x: 0.5, y: 0.5}
 --- !u!1 &6909880373539580040
@@ -2068,7 +2014,7 @@ GameObject:
   - component: {fileID: 6909880373574815805}
   - component: {fileID: 6909880373574815804}
   m_Layer: 5
-  m_Name: InteractUI
+  m_Name: Interactions
   m_TagString: Untagged
   m_Icon: {fileID: 0}
   m_NavMeshLayer: 0
@@ -2086,8 +2032,9 @@ RectTransform:
   m_LocalScale: {x: 0.5555556, y: 0.5555556, z: 0.5555556}
   m_ConstrainProportionsScale: 0
   m_Children:
-  - {fileID: 6909880373219561751}
   - {fileID: 6909880372860330797}
+  - {fileID: 6909880373533192468}
+  - {fileID: 6909880372810032764}
   m_Father: {fileID: 6909880372566592212}
   m_RootOrder: 5
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -2351,7 +2298,7 @@ Canvas:
   m_OverrideSorting: 0
   m_OverridePixelPerfect: 0
   m_SortingBucketNormalizedSize: 0
-  m_AdditionalShaderChannelsFlag: 1
+  m_AdditionalShaderChannelsFlag: 25
   m_SortingLayerID: 0
   m_SortingOrder: 0
   m_TargetDisplay: 0
@@ -2833,11 +2780,25 @@ PrefabInstance:
       propertyPath: _playerInput
       value: 
       objectReference: {fileID: 6909880373530918865}
+    - target: {fileID: 1282553599, guid: c5efc39a8aaf6e64ea40e9ad573e9b47, type: 3}
+      propertyPath: interactionLayerMask.m_Bits
+      value: 1847
+      objectReference: {fileID: 0}
     - target: {fileID: 135756641613574976, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
         type: 3}
       propertyPath: CheatTxt
       value: 
       objectReference: {fileID: 6909880373761857717}
+    - target: {fileID: 3911426063876357059, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
+        type: 3}
+      propertyPath: containerGameObjectE
+      value: 
+      objectReference: {fileID: 6909880372810032739}
+    - target: {fileID: 3911426063876357059, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
+        type: 3}
+      propertyPath: containerGameObjectF
+      value: 
+      objectReference: {fileID: 6909880373533192475}
     - target: {fileID: 8616685848737228371, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
         type: 3}
       propertyPath: _input
@@ -2910,24 +2871,32 @@ PrefabInstance:
       objectReference: {fileID: 0}
     m_RemovedComponents: []
   m_SourcePrefab: {fileID: 100100000, guid: c5efc39a8aaf6e64ea40e9ad573e9b47, type: 3}
---- !u!114 &1503659779496013627 stripped
-MonoBehaviour:
-  m_CorrespondingSourceObject: {fileID: 135756641613574976, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
+--- !u!4 &3197233019988198259 stripped
+Transform:
+  m_CorrespondingSourceObject: {fileID: 4135013735270702856, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
     type: 3}
   m_PrefabInstance: {fileID: 1530202875637013627}
   m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 0}
+--- !u!1 &7109144527452912175 stripped
+GameObject:
+  m_CorrespondingSourceObject: {fileID: 8616685848737228372, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
+    type: 3}
+  m_PrefabInstance: {fileID: 1530202875637013627}
+  m_PrefabAsset: {fileID: 0}
+--- !u!114 &2703034388931117196
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7109144527452912175}
   m_Enabled: 1
   m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: a30ace0f9b15e2c4b8aa89a1aab08c0a, type: 3}
+  m_Script: {fileID: 11500000, guid: d912b1907980aa548b019067951c21ff, type: 3}
   m_Name: 
   m_EditorClassIdentifier: 
---- !u!4 &3197233019988198259 stripped
-Transform:
-  m_CorrespondingSourceObject: {fileID: 4135013735270702856, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
-    type: 3}
-  m_PrefabInstance: {fileID: 1530202875637013627}
-  m_PrefabAsset: {fileID: 0}
+  interactPrompt: {fileID: 6909880372810032739}
+  payRespectsPrompt: {fileID: 6909880373533192475}
 --- !u!4 &7629961867817803925 stripped
 Transform:
   m_CorrespondingSourceObject: {fileID: 8997996947095583982, guid: c5efc39a8aaf6e64ea40e9ad573e9b47,
@@ -3029,22 +2998,22 @@ PrefabInstance:
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
       propertyPath: m_LocalPosition.x
-      value: 0
+      value: -8.303
       objectReference: {fileID: 0}
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
       propertyPath: m_LocalPosition.y
-      value: 1.46
+      value: 1.5630001
       objectReference: {fileID: 0}
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
       propertyPath: m_LocalPosition.z
-      value: 0
+      value: 5.122
       objectReference: {fileID: 0}
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
       propertyPath: m_LocalRotation.w
-      value: 1
+      value: 0
       objectReference: {fileID: 0}
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
@@ -3054,7 +3023,7 @@ PrefabInstance:
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
       propertyPath: m_LocalRotation.y
-      value: -0
+      value: 1
       objectReference: {fileID: 0}
     - target: {fileID: 9005220659476430818, guid: 2d3a85ecde41a8246a79669975912b74,
         type: 3}
diff --git a/Assets/SZZ/Settings/InputMap.inputactions b/Assets/SZZ/Settings/InputMap.inputactions
index 2e94e91..78db489 100644
--- a/Assets/SZZ/Settings/InputMap.inputactions
+++ b/Assets/SZZ/Settings/InputMap.inputactions
@@ -76,6 +76,15 @@
                     "processors": "",
                     "interactions": "",
                     "initialStateCheck": false
+                },
+                {
+                    "name": "MenuSelect",
+                    "type": "Value",
+                    "id": "4dc1e763-f09c-4cd1-b90f-4c3b569c82f6",
+                    "expectedControlType": "Axis",
+                    "processors": "",
+                    "interactions": "",
+                    "initialStateCheck": true
                 }
             ],
             "bindings": [
@@ -309,6 +318,17 @@
                     "action": "NoClip",
                     "isComposite": false,
                     "isPartOfComposite": false
+                },
+                {
+                    "name": "",
+                    "id": "e3953ee8-360b-4a94-9fd1-46aea7c98696",
+                    "path": "<Mouse>/scroll/down",
+                    "interactions": "",
+                    "processors": "",
+                    "groups": "KeyboardMouse",
+                    "action": "MenuSelect",
+                    "isComposite": false,
+                    "isPartOfComposite": false
                 }
             ]
         },
diff --git a/Assets/SZZ/UI/continue-icon.png b/Assets/SZZ/UI/continue-icon.png
new file mode 100644
index 0000000..8c93642
--- /dev/null
+++ b/Assets/SZZ/UI/continue-icon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6bc2ebe9a66d0c5ad09202e7b7b61500761ee8f92897f4ceb8a553c2a3af4024
+size 161
diff --git a/Assets/SZZ/UI/continue-icon.png.meta b/Assets/SZZ/UI/continue-icon.png.meta
new file mode 100644
index 0000000..b81a4fc
--- /dev/null
+++ b/Assets/SZZ/UI/continue-icon.png.meta
@@ -0,0 +1,135 @@
+fileFormatVersion: 2
+guid: a92e114f1b8f59641adc4769ae09c61c
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 12
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  vTOnly: 0
+  ignoreMasterTextureLimit: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: 1
+    aniso: 1
+    mipBias: 0
+    wrapU: 0
+    wrapV: 0
+    wrapW: 0
+  nPOTScale: 1
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  singleChannelComponent: 0
+  flipbookRows: 1
+  flipbookColumns: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  ignorePngGamma: 0
+  applyGammaDecoding: 0
+  cookieLightType: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Standalone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Server
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: WebGL
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+    nameFileIdTable: {}
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/ProjectSettings/DynamicsManager.asset b/ProjectSettings/DynamicsManager.asset
index cdc1f3e..7d68edd 100644
--- a/ProjectSettings/DynamicsManager.asset
+++ b/ProjectSettings/DynamicsManager.asset
@@ -3,10 +3,11 @@
 --- !u!55 &1
 PhysicsManager:
   m_ObjectHideFlags: 0
-  serializedVersion: 11
+  serializedVersion: 13
   m_Gravity: {x: 0, y: -9.81, z: 0}
   m_DefaultMaterial: {fileID: 0}
   m_BounceThreshold: 2
+  m_DefaultMaxDepenetrationVelocity: 10
   m_SleepThreshold: 0.005
   m_DefaultContactOffset: 0.01
   m_DefaultSolverIterations: 6
@@ -17,11 +18,12 @@ PhysicsManager:
   m_ClothInterCollisionDistance: 0
   m_ClothInterCollisionStiffness: 0
   m_ContactsGeneration: 1
-  m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+  m_LayerCollisionMatrix: fff3fffffff3fffffff3fffffffffffffff3fffffff3fffffffffffffff7fffffff3fffffff3ffffc8f8ffff48fcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
   m_AutoSimulation: 1
   m_AutoSyncTransforms: 0
   m_ReuseCollisionCallbacks: 1
   m_ClothInterCollisionSettingsToggle: 0
+  m_ClothGravity: {x: 0, y: -9.81, z: 0}
   m_ContactPairsMode: 0
   m_BroadphaseType: 0
   m_WorldBounds:
@@ -31,4 +33,6 @@ PhysicsManager:
   m_FrictionType: 0
   m_EnableEnhancedDeterminism: 0
   m_EnableUnifiedHeightmaps: 1
-  m_DefaultMaxAngluarSpeed: 7
+  m_ImprovedPatchFriction: 0
+  m_SolverType: 0
+  m_DefaultMaxAngularSpeed: 7
diff --git a/ProjectSettings/InkSettings.asset b/ProjectSettings/InkSettings.asset
new file mode 100644
index 0000000..3f63202
--- /dev/null
+++ b/ProjectSettings/InkSettings.asset
@@ -0,0 +1,21 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &1
+MonoBehaviour:
+  m_ObjectHideFlags: 61
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 0}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 983af1b6602bf416b9928d40b37a38bd, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  templateFile: {fileID: 0}
+  defaultJsonAssetPath: {fileID: 0}
+  compileAutomatically: 1
+  delayInPlayMode: 1
+  handleJSONFilesAutomatically: 1
+  compileTimeout: 30
+  printInkLogsInConsoleOnCompile: 0
diff --git a/ProjectSettings/QualitySettings.asset b/ProjectSettings/QualitySettings.asset
index 2880694..7e7b554 100644
--- a/ProjectSettings/QualitySettings.asset
+++ b/ProjectSettings/QualitySettings.asset
@@ -225,3 +225,4 @@ QualitySettings:
   m_PerPlatformDefaultQuality:
     Server: 0
     Standalone: 3
+    WebGL: 0
diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset
index c27162e..21a3f42 100644
--- a/ProjectSettings/TagManager.asset
+++ b/ProjectSettings/TagManager.asset
@@ -25,7 +25,7 @@ TagManager:
   - Player
   - Character
   - Ground
-  - 
+  - DialogueTrigger
   - 
   - 
   - 
-- 
GitLab