├── tests ├── test_included_file.ink ├── test_included_file2.ink ├── test_included_file3.ink ├── test_included_file4.ink └── tests.csproj ├── InkTestBed ├── test_included_file.ink ├── test_included_file2.ink ├── test.ink └── InkTestBed.csproj ├── Epic_MegaGrants_Recipient_logo_horizontal.png ├── Sublime3Syntax ├── ink.sublime-settings ├── LiveWatchAndInstallOnEdit.command ├── ink-local-symbols.tmPreferences ├── ink-global-symbols.tmPreferences ├── ink-comments.tmPreferences ├── install_for_sublime2and3.command ├── README.md ├── ink.tmTheme ├── ink-dark.tmTheme └── ink.YAML-tmLanguage ├── written-in-ink-logos ├── written-in-ink-on-white-only.png ├── written-in-ink-black-mid-and-colour.png └── README.md ├── compiler ├── ParsedHierarchy │ ├── INamedContent.cs │ ├── FlowLevel.cs │ ├── IWeavePoint.cs │ ├── Identifier.cs │ ├── Stitch.cs │ ├── AuthorWarning.cs │ ├── Text.cs │ ├── IncludedFile.cs │ ├── Wrap.cs │ ├── ExternalDeclaration.cs │ ├── Knot.cs │ ├── Return.cs │ ├── ConstantDeclaration.cs │ ├── Number.cs │ ├── Gather.cs │ ├── List.cs │ ├── StringExpression.cs │ ├── ContentList.cs │ ├── Conditional.cs │ ├── TunnelOnwards.cs │ ├── ListDefinition.cs │ ├── VariableAssignment.cs │ ├── ConditionalSingleBranch.cs │ ├── VariableReference.cs │ ├── Path.cs │ └── DivertTarget.cs ├── CommandLineInput.cs ├── InkStringConversionExtensions.cs ├── InkParser │ ├── InkParser_AuthorWarning.cs │ ├── InkParser_Tags.cs │ ├── InkParser_Include.cs │ ├── InkParser_CharacterRanges.cs │ ├── InkParser_Whitespace.cs │ ├── CommentEliminator.cs │ ├── InkParser_CommandLineInput.cs │ ├── InkParser_Statements.cs │ ├── InkParser.cs │ └── InkParser_Divert.cs ├── FileHandler.cs ├── Plugins │ ├── Plugin.cs │ └── PluginManager.cs ├── CharacterSet.cs ├── ink_compiler.csproj ├── CharacterRange.cs ├── Stats.cs └── StringParser │ └── StringParserState.cs ├── ink-engine-runtime ├── Void.cs ├── INamedContent.cs ├── PushPop.cs ├── Glue.cs ├── Tag.cs ├── StringJoinExtension.cs ├── Error.cs ├── StoryException.cs ├── SearchResult.cs ├── VariableAssignment.cs ├── VariableReference.cs ├── Choice.cs ├── ListDefinitionsOrigin.cs ├── Pointer.cs ├── ink-engine-runtime.csproj ├── ListDefinition.cs ├── StatePatch.cs ├── DebugMetadata.cs ├── ChoicePoint.cs ├── Flow.cs ├── Divert.cs └── ControlCommand.cs ├── cleanup-binobj.ps1 ├── profiler.command ├── .vscode ├── ink.code-workspace ├── launch.json └── tasks.json ├── .travis.yml ├── LICENSE.txt ├── inklecate └── inklecate.csproj ├── publish-nuget.ps1 └── .gitignore /tests/test_included_file.ink: -------------------------------------------------------------------------------- 1 | This is include 1. -------------------------------------------------------------------------------- /tests/test_included_file2.ink: -------------------------------------------------------------------------------- 1 | This is include 2. -------------------------------------------------------------------------------- /InkTestBed/test_included_file.ink: -------------------------------------------------------------------------------- 1 | This is include 1. -------------------------------------------------------------------------------- /InkTestBed/test_included_file2.ink: -------------------------------------------------------------------------------- 1 | This is include 2. -------------------------------------------------------------------------------- /tests/test_included_file3.ink: -------------------------------------------------------------------------------- 1 | INCLUDE test_included_file4.ink -------------------------------------------------------------------------------- /InkTestBed/test.ink: -------------------------------------------------------------------------------- 1 | This is a convenience test file for InkTestBed. -------------------------------------------------------------------------------- /Epic_MegaGrants_Recipient_logo_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbutter/ink/master/Epic_MegaGrants_Recipient_logo_horizontal.png -------------------------------------------------------------------------------- /Sublime3Syntax/ink.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // nothing 3 | "color_scheme": "Packages/User/ink.tmTheme", 4 | "word_wrap": true 5 | } 6 | -------------------------------------------------------------------------------- /written-in-ink-logos/written-in-ink-on-white-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbutter/ink/master/written-in-ink-logos/written-in-ink-on-white-only.png -------------------------------------------------------------------------------- /written-in-ink-logos/written-in-ink-black-mid-and-colour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitbutter/ink/master/written-in-ink-logos/written-in-ink-black-mid-and-colour.png -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/INamedContent.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public interface INamedContent 5 | { 6 | string name { get; } 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/test_included_file4.ink: -------------------------------------------------------------------------------- 1 | VAR t2 = 5 2 | 3 | The value of a variable in test file 2 is { t2 }. 4 | 5 | == knot_in_2 == 6 | The value when accessed from knot_in_2 is { t2 }. 7 | -> END -------------------------------------------------------------------------------- /ink-engine-runtime/Void.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | public class Void : Runtime.Object 4 | { 5 | public Void () 6 | { 7 | } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /ink-engine-runtime/INamedContent.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Runtime 3 | { 4 | public interface INamedContent 5 | { 6 | string name { get; } 7 | bool hasValidName { get; } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /cleanup-binobj.ps1: -------------------------------------------------------------------------------- 1 | $binobj = @(Get-ChildItem .\ -Recurse -Depth 3 -Attributes Directory -Include bin, obj) 2 | foreach ($d in $binobj) 3 | { 4 | Write-Host "Removing $($d.FullName)" 5 | Remove-Item $d.FullName -Recurse -Force -ErrorAction Ignore 6 | } -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/FlowLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Parsed 2 | { 3 | public enum FlowLevel 4 | { 5 | Story, 6 | Knot, 7 | Stitch, 8 | WeavePoint // not actually a FlowBase, but used for diverts 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ink-engine-runtime/PushPop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Runtime 5 | { 6 | public enum PushPopType 7 | { 8 | Tunnel, 9 | Function, 10 | FunctionEvaluationFromGame 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /ink-engine-runtime/Glue.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | public class Glue : Runtime.Object 4 | { 5 | public Glue() { } 6 | 7 | public override string ToString () 8 | { 9 | return "Glue"; 10 | } 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /compiler/CommandLineInput.cs: -------------------------------------------------------------------------------- 1 | namespace Ink 2 | { 3 | public class CommandLineInput 4 | { 5 | public bool isHelp; 6 | public bool isExit; 7 | public int? choiceInput; 8 | public int? debugSource; 9 | public string debugPathLookup; 10 | public object userImmediateModeStatement; 11 | } 12 | } -------------------------------------------------------------------------------- /profiler.command: -------------------------------------------------------------------------------- 1 | cd "`dirname "$0"`" 2 | mono --profile=log:output=output.mlpd inklecate/bin/Release/netcoreapp3.1/inklecate.dll -s > report-in-progress.txt 3 | echo "----------------------------" >> report-in-progress.txt 4 | mprof-report --verbose output.mlpd >> report-in-progress.txt # use --time=10.0-20.0 to select a particular time period 5 | rm output.mlpd 6 | mv report-in-progress.txt report.txt -------------------------------------------------------------------------------- /.vscode/ink.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": { 8 | "files.exclude": { 9 | "**/bin": true, 10 | "**/obj": true, 11 | "packages/": true, 12 | "ReleaseBinary/": true 13 | }, 14 | "files.watcherExclude": { 15 | "**/bin/**": true, 16 | "**/obj/**": true 17 | }, 18 | "omnisharp.enableRoslynAnalyzers": true 19 | } 20 | } -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/IWeavePoint.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public interface IWeavePoint 6 | { 7 | int indentationDepth { get; } 8 | Runtime.Container runtimeContainer { get; } 9 | List content { get; } 10 | string name { get; } 11 | Identifier identifier { get; } 12 | 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Identifier.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Parsed { 2 | public class Identifier { 3 | public string name; 4 | public Runtime.DebugMetadata debugMetadata; 5 | 6 | public override string ToString() 7 | { 8 | return name; 9 | } 10 | 11 | public static Identifier Done = new Identifier { name = "DONE", debugMetadata = null }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sublime3Syntax/LiveWatchAndInstallOnEdit.command: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "`dirname "$0"`" 3 | 4 | # Check for existence of fswatch 5 | command -v fswatch >/dev/null 2>&1 || { echo >&2 "ERROR: 'fswatch' is required to run this script! Please see LiveUpdateReadme.md"; exit 1; } 6 | 7 | # Run an initial make, then watch for ink files changing 8 | ./install_for_sublime2and3.command 9 | fswatch -0 . | xargs -0 -n 1 -I{} ./install_for_sublime2and3.command -------------------------------------------------------------------------------- /ink-engine-runtime/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class Tag : Runtime.Object 6 | { 7 | public string text { get; private set; } 8 | 9 | public Tag (string tagText) 10 | { 11 | this.text = tagText; 12 | } 13 | 14 | public override string ToString () 15 | { 16 | return "# " + text; 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Stitch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class Stitch : FlowBase 6 | { 7 | public override FlowLevel flowLevel { get { return FlowLevel.Stitch; } } 8 | 9 | public Stitch (Identifier name, List topLevelObjects, List arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction) 10 | { 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/AuthorWarning.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class AuthorWarning : Parsed.Object 5 | { 6 | public string warningMessage; 7 | 8 | public AuthorWarning(string message) 9 | { 10 | warningMessage = message; 11 | } 12 | 13 | public override Runtime.Object GenerateRuntimeObject () 14 | { 15 | Warning (warningMessage); 16 | return null; 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Text.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class Text : Parsed.Object 5 | { 6 | public string text { get; set; } 7 | 8 | public Text (string str) 9 | { 10 | text = str; 11 | } 12 | 13 | public override Runtime.Object GenerateRuntimeObject () 14 | { 15 | return new Runtime.StringValue(this.text); 16 | } 17 | 18 | public override string ToString () 19 | { 20 | return this.text; 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /compiler/InkStringConversionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink 4 | { 5 | public static class InkStringConversionExtensions 6 | { 7 | public static string[] ToStringsArray(this List list) { 8 | int count = list.Count; 9 | var strings = new string[count]; 10 | 11 | for(int i = 0; i < count; i++) { 12 | strings[i] = list[i].ToString(); 13 | } 14 | 15 | return strings; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/IncludedFile.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class IncludedFile : Parsed.Object 5 | { 6 | public Parsed.Story includedStory { get; private set; } 7 | 8 | public IncludedFile (Parsed.Story includedStory) 9 | { 10 | this.includedStory = includedStory; 11 | } 12 | 13 | public override Runtime.Object GenerateRuntimeObject () 14 | { 15 | // Left to the main story to process 16 | return null; 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink-local-symbols.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Symbol List 7 | scope 8 | meta.stitch.declaration, meta.label 9 | settings 10 | 11 | showInSymbolList 12 | 1 13 | symbolTransformation 14 | 15 | s/=\s*([^=]+)=*/ $1/g; 16 | s/\(\w+\)/ $0/g; 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_AuthorWarning.cs: -------------------------------------------------------------------------------- 1 | using Ink.Parsed; 2 | 3 | namespace Ink 4 | { 5 | public partial class InkParser 6 | { 7 | protected AuthorWarning AuthorWarning() 8 | { 9 | Whitespace (); 10 | 11 | var identifier = Parse (IdentifierWithMetadata); 12 | if (identifier == null || identifier.name != "TODO") 13 | return null; 14 | 15 | Whitespace (); 16 | 17 | ParseString (":"); 18 | 19 | Whitespace (); 20 | 21 | var message = ParseUntilCharactersFromString ("\n\r"); 22 | 23 | return new AuthorWarning (message); 24 | } 25 | 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /ink-engine-runtime/StringJoinExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Ink.Runtime 6 | { 7 | public static class StringExt 8 | { 9 | public static string Join(string separator, List objects) 10 | { 11 | var sb = new StringBuilder (); 12 | 13 | var isFirst = true; 14 | foreach (var o in objects) { 15 | 16 | if (!isFirst) 17 | sb.Append (separator); 18 | 19 | sb.Append (o.ToString ()); 20 | 21 | isFirst = false; 22 | } 23 | 24 | return sb.ToString (); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /written-in-ink-logos/README.md: -------------------------------------------------------------------------------- 1 | # Written in ink logos 2 | 3 | We'd be delighted if you would like to include one of the *Written in ink* logos in your game or other creative work. You can find them below. 4 | 5 | You can also find the [inkle logos on our press page](https://www.inklestudios.com/press/). 6 | 7 | 8 | 9 | ![](https://github.com/inkle/ink/blob/master/written-in-ink-logos/written-in-ink-on-white-only.png) 10 | 11 | 12 | 13 | ![](https://github.com/inkle/ink/blob/master/written-in-ink-logos/written-in-ink-black-mid-and-colour.png) 14 | 15 | -------------------------------------------------------------------------------- /compiler/FileHandler.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Ink 4 | { 5 | public interface IFileHandler 6 | { 7 | string ResolveInkFilename (string includeName); 8 | string LoadInkFileContents (string fullFilename); 9 | } 10 | 11 | public class DefaultFileHandler : Ink.IFileHandler { 12 | public string ResolveInkFilename (string includeName) 13 | { 14 | var workingDir = Directory.GetCurrentDirectory (); 15 | var fullRootInkPath = Path.Combine (workingDir, includeName); 16 | return fullRootInkPath; 17 | } 18 | 19 | public string LoadInkFileContents (string fullFilename) 20 | { 21 | return File.ReadAllText (fullFilename); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Wrap.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class Wrap : Parsed.Object where T : Runtime.Object 5 | { 6 | public Wrap (T objToWrap) 7 | { 8 | _objToWrap = objToWrap; 9 | } 10 | 11 | public override Runtime.Object GenerateRuntimeObject () 12 | { 13 | return _objToWrap; 14 | } 15 | 16 | T _objToWrap; 17 | } 18 | 19 | // Shorthand for writing Parsed.Wrap and Parsed.Wrap 20 | public class Glue : Wrap { 21 | public Glue (Runtime.Glue glue) : base(glue) {} 22 | } 23 | public class Tag : Wrap { 24 | public Tag (Runtime.Tag tag) : base (tag) { } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /compiler/Plugins/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink 4 | { 5 | public interface IPlugin 6 | { 7 | // Hooks: if in doubt use PostExport, since the parsedStory is in a more finalised state. 8 | 9 | // Hook for immediately after the story has been parsed into its basic Parsed hierarchy. 10 | // Could be useful for modifying the story before it's exported. 11 | void PostParse(Parsed.Story parsedStory); 12 | 13 | // Hook for after parsed story has been converted into its runtime equivalent. Note that 14 | // during this process the parsed story will have changed structure too, to take into 15 | // account analysis of the structure of Weave, for example. 16 | void PostExport(Parsed.Story parsedStory, Runtime.Story runtimeStory); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink-global-symbols.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Symbol List 7 | scope 8 | source.ink meta.knot.declaration, source.ink meta.variable.declaration 9 | settings 10 | 11 | showInIndexedSymbolList 12 | 1 13 | showInSymbolList 14 | 1 15 | symbolTransformation 16 | 17 | s/={2,}\s*([^=]+)=*/$1/g; 18 | 19 | symbolIndexTransformation 20 | 21 | s/={2,}\s*([^=]+)=*/$1/g; 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ink-engine-runtime/Error.cs: -------------------------------------------------------------------------------- 1 | namespace Ink 2 | { 3 | /// 4 | /// Callback for errors throughout both the ink runtime and compiler. 5 | /// 6 | public delegate void ErrorHandler(string message, ErrorType type); 7 | 8 | /// 9 | /// Author errors will only ever come from the compiler so don't need to be handled 10 | /// by your Story error handler. The "Error" ErrorType is by far the most common 11 | /// for a runtime story error (rather than compiler error), though the Warning type 12 | /// is also possible. 13 | /// 14 | public enum ErrorType 15 | { 16 | /// Generated by a "TODO" note in the ink source 17 | Author, 18 | /// You should probably fix this, but it's not critical 19 | Warning, 20 | /// Critical error that can't be recovered from 21 | Error 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ink-engine-runtime/StoryException.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | /// 4 | /// Exception that represents an error when running a Story at runtime. 5 | /// An exception being thrown of this type is typically when there's 6 | /// a bug in your ink, rather than in the ink engine itself! 7 | /// 8 | public class StoryException : System.Exception 9 | { 10 | public bool useEndLineNumber; 11 | 12 | /// 13 | /// Constructs a default instance of a StoryException without a message. 14 | /// 15 | public StoryException () { } 16 | 17 | /// 18 | /// Constructs an instance of a StoryException with a message. 19 | /// 20 | /// The error message. 21 | public StoryException(string message) : base(message) {} 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ink-engine-runtime/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Ink.Runtime 3 | { 4 | // When looking up content within the story (e.g. in Container.ContentAtPath), 5 | // the result is generally found, but if the story is modified, then when loading 6 | // up an old save state, then some old paths may still exist. In this case we 7 | // try to recover by finding an approximate result by working up the story hierarchy 8 | // in the path to find the closest valid container. Instead of crashing horribly, 9 | // we might see some slight oddness in the content, but hopefully it recovers! 10 | public struct SearchResult 11 | { 12 | public Runtime.Object obj; 13 | public bool approximate; 14 | 15 | public Runtime.Object correctObj { get { return approximate ? null : obj; } } 16 | public Container container { get { return obj as Container; } } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/ExternalDeclaration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Parsed 5 | { 6 | public class ExternalDeclaration : Parsed.Object, INamedContent 7 | { 8 | public string name 9 | { 10 | get { return identifier?.name; } 11 | } 12 | public Identifier identifier { get; set; } 13 | public List argumentNames { get; set; } 14 | 15 | public ExternalDeclaration (Identifier identifier, List argumentNames) 16 | { 17 | this.identifier = identifier; 18 | this.argumentNames = argumentNames; 19 | } 20 | 21 | public override Ink.Runtime.Object GenerateRuntimeObject () 22 | { 23 | story.AddExternal (this); 24 | 25 | // No runtime code exists for an external, only metadata 26 | return null; 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /ink-engine-runtime/VariableAssignment.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Ink.Runtime 4 | { 5 | // The value to be assigned is popped off the evaluation stack, so no need to keep it here 6 | public class VariableAssignment : Runtime.Object 7 | { 8 | public string variableName { get; protected set; } 9 | public bool isNewDeclaration { get; protected set; } 10 | public bool isGlobal { get; set; } 11 | 12 | public VariableAssignment (string variableName, bool isNewDeclaration) 13 | { 14 | this.variableName = variableName; 15 | this.isNewDeclaration = isNewDeclaration; 16 | } 17 | 18 | // Require default constructor for serialisation 19 | public VariableAssignment() : this(null, false) {} 20 | 21 | public override string ToString () 22 | { 23 | return "VarAssign to " + variableName; 24 | } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | addons: 3 | snaps: 4 | - name: powershell 5 | confinement: classic 6 | language: csharp 7 | solution: ink.sln 8 | mono: none 9 | dotnet: 3.1 10 | cache: 11 | directories: 12 | - /home/travis/.nuget/packages/ 13 | script: 14 | - dotnet test tests/tests.csproj 15 | - pwsh -File ./publish-nuget.ps1 16 | 17 | # you can define following variables in the web interface, NOT in this file: 18 | # RELEASE_BRANCH to specify what branch should be considered a release branch (optional, default is master) 19 | # for nightly builds: 20 | # NIGHTLY_FEED_SOURCE_URL and NIGHTLY_FEED_APIKEY (e.g. https://www.myget.org/F/13xforever-inkle-ink-engine/) 21 | # PUBLISH_MASTER_BUILDS = true (to also build and push every new commit in master branch) 22 | # for release builds: 23 | # RELEASE_FEED_SOURCE_URL and RELEASE_FEED_APIKEY (e.g. https://www.nuget.org/) 24 | # release will be build if commit has version tag, using that version tag 25 | # see .travis.build_nuget.sh for details -------------------------------------------------------------------------------- /tests/tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | tests 6 | ink-tests 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Always 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink-comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | ink comment settings 7 | scope 8 | source.ink 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | // 18 | 19 | 20 | 21 | name 22 | TM_COMMENT_START_2 23 | value 24 | /* 25 | 26 | 27 | name 28 | TM_COMMENT_END_2 29 | value 30 | */ 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 inkle Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /compiler/CharacterSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink 4 | { 5 | 6 | public class CharacterSet : HashSet 7 | { 8 | public static CharacterSet FromRange(char start, char end) 9 | { 10 | return new CharacterSet ().AddRange (start, end); 11 | } 12 | 13 | public CharacterSet () 14 | { 15 | } 16 | 17 | public CharacterSet(string str) 18 | { 19 | AddCharacters (str); 20 | } 21 | 22 | public CharacterSet(CharacterSet charSetToCopy) 23 | { 24 | AddCharacters (charSetToCopy); 25 | } 26 | 27 | public CharacterSet AddRange(char start, char end) 28 | { 29 | for(char c=start; c<=end; ++c) { 30 | Add (c); 31 | } 32 | return this; 33 | } 34 | 35 | public CharacterSet AddCharacters(IEnumerable chars) 36 | { 37 | foreach (char c in chars) { 38 | Add (c); 39 | } 40 | return this; 41 | } 42 | 43 | public CharacterSet AddCharacters (string chars) 44 | { 45 | foreach (char c in chars) { 46 | Add (c); 47 | } 48 | return this; 49 | } 50 | 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /compiler/Plugins/PluginManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Ink 5 | { 6 | public class PluginManager 7 | { 8 | public PluginManager (List pluginNames) 9 | { 10 | _plugins = new List (); 11 | 12 | // TODO: Make these plugin names DLL filenames, and load up their assemblies 13 | foreach (string pluginName in pluginNames) { 14 | //if (pluginName == "ChoiceListPlugin") { 15 | // _plugins.Add (new InkPlugin.ChoiceListPlugin ()); 16 | //}else 17 | { 18 | throw new System.Exception ("Plugin not found"); 19 | } 20 | } 21 | } 22 | 23 | public void PostParse(Parsed.Story parsedStory) 24 | { 25 | foreach (var plugin in _plugins) { 26 | plugin.PostParse (parsedStory); 27 | } 28 | } 29 | 30 | public void PostExport(Parsed.Story parsedStory, Runtime.Story runtimeStory) 31 | { 32 | foreach (var plugin in _plugins) { 33 | plugin.PostExport (parsedStory, runtimeStory); 34 | } 35 | } 36 | 37 | List _plugins; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Knot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class Knot : FlowBase 6 | { 7 | public override FlowLevel flowLevel { get { return FlowLevel.Knot; } } 8 | 9 | public Knot (Identifier name, List topLevelObjects, List arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction) 10 | { 11 | } 12 | 13 | public override void ResolveReferences (Story context) 14 | { 15 | base.ResolveReferences (context); 16 | 17 | var parentStory = this.story; 18 | 19 | // Enforce rule that stitches must not have the same 20 | // name as any knots that exist in the story 21 | foreach (var stitchNamePair in subFlowsByName) { 22 | var stitchName = stitchNamePair.Key; 23 | 24 | var knotWithStitchName = parentStory.ContentWithNameAtLevel (stitchName, FlowLevel.Knot, false); 25 | if (knotWithStitchName) { 26 | var stitch = stitchNamePair.Value; 27 | var errorMsg = string.Format ("Stitch '{0}' has the same name as a knot (on {1})", stitch.identifier, knotWithStitchName.debugMetadata); 28 | Error(errorMsg, stitch); 29 | } 30 | } 31 | } 32 | 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /InkTestBed/InkTestBed.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | true 7 | InkTestBed 8 | InkTestBed 9 | 10 | 11 | 12 | inkle Ltd 13 | InkleStudios 14 | phish 15 | https://github.com/inkle/ink 16 | https://github.com/inkle/ink 17 | https://github.com/inkle/ink/blob/master/LICENSE.txt 18 | git 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /inklecate/inklecate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | Ink 7 | inklecate 8 | 9 | 10 | 11 | inkle Ltd 12 | InkleStudios 13 | inkle Ltd 14 | https://github.com/inkle/ink 15 | https://github.com/inkle/ink 16 | https://github.com/inkle/ink/blob/master/LICENSE.txt 17 | git 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | <_Parameter1>ink-tests 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Return.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Parsed 2 | { 3 | public class Return : Parsed.Object 4 | { 5 | public Expression returnedExpression { get; protected set; } 6 | 7 | public Return (Expression returnedExpression = null) 8 | { 9 | if (returnedExpression) { 10 | this.returnedExpression = AddContent(returnedExpression); 11 | } 12 | } 13 | 14 | public override Runtime.Object GenerateRuntimeObject () 15 | { 16 | var container = new Runtime.Container (); 17 | 18 | // Evaluate expression 19 | if (returnedExpression) { 20 | container.AddContent (returnedExpression.runtimeObject); 21 | } 22 | 23 | // Return Runtime.Void when there's no expression to evaluate 24 | // (This evaluation will just add the Void object to the evaluation stack) 25 | else { 26 | container.AddContent (Runtime.ControlCommand.EvalStart ()); 27 | container.AddContent (new Runtime.Void()); 28 | container.AddContent (Runtime.ControlCommand.EvalEnd ()); 29 | } 30 | 31 | // Then pop the call stack 32 | // (the evaluated expression will leave the return value on the evaluation stack) 33 | container.AddContent (Runtime.ControlCommand.PopFunction()); 34 | 35 | return container; 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_Tags.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Ink 6 | { 7 | public partial class InkParser 8 | { 9 | protected Parsed.Tag Tag () 10 | { 11 | Whitespace (); 12 | 13 | if (ParseString ("#") == null) 14 | return null; 15 | 16 | Whitespace (); 17 | 18 | var sb = new StringBuilder (); 19 | do { 20 | // Read up to another #, end of input or newline 21 | string tagText = ParseUntilCharactersFromCharSet (_endOfTagCharSet); 22 | sb.Append (tagText); 23 | 24 | // Escape character 25 | if (ParseString ("\\") != null) { 26 | char c = ParseSingleCharacter (); 27 | if( c != (char)0 ) sb.Append(c); 28 | continue; 29 | } 30 | 31 | break; 32 | } while ( true ); 33 | 34 | var fullTagText = sb.ToString ().Trim(); 35 | 36 | return new Parsed.Tag (new Runtime.Tag (fullTagText)); 37 | } 38 | 39 | protected List Tags () 40 | { 41 | var tags = OneOrMore (Tag); 42 | if (tags == null) return null; 43 | 44 | return tags.Cast().ToList(); 45 | } 46 | 47 | CharacterSet _endOfTagCharSet = new CharacterSet ("#\n\r\\"); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Sublime3Syntax/install_for_sublime2and3.command: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Fail if any individual command fails 3 | # http://stackoverflow.com/questions/5195607/checking-bash-exit-status-of-several-commands-efficiently 4 | set -e 5 | 6 | cd "`dirname "$0"`" 7 | 8 | 9 | # Copy latest syntax highlighting grammar into place for Sublime Text 2 and 3 10 | sublime2Folder="$HOME/Library/Application Support/Sublime Text 2" 11 | if [ -d "$sublime2Folder" ]; then 12 | sublime2Packages="$sublime2Folder/Packages/User" 13 | mkdir -p "$sublime2Packages" 14 | cp ./ink.tmLanguage "$sublime2Packages" 15 | cp ./ink.tmTheme "$sublime2Packages" 16 | cp ./ink-dark.tmTheme "$sublime2Packages" 17 | cp ./ink.sublime-settings "$sublime2Packages" 18 | cp ./ink-global-symbols.tmPreferences "$sublime2Packages" 19 | cp ./ink-local-symbols.tmPreferences "$sublime2Packages" 20 | cp ./ink-comments.tmPreferences "$sublime2Packages" 21 | fi 22 | 23 | sublime3Folder="$HOME/Library/Application Support/Sublime Text 3" 24 | if [ -d "$sublime3Folder" ]; then 25 | sublime3Packages="$sublime3Folder/Packages/User" 26 | mkdir -p "$sublime3Packages" 27 | cp ./ink.tmLanguage "$sublime3Packages" 28 | cp ./ink.tmTheme "$sublime3Packages" 29 | cp ./ink-dark.tmTheme "$sublime3Packages" 30 | cp ./ink.sublime-settings "$sublime3Packages" 31 | cp ./ink-global-symbols.tmPreferences "$sublime3Packages" 32 | cp ./ink-local-symbols.tmPreferences "$sublime3Packages" 33 | cp ./ink-comments.tmPreferences "$sublime3Packages" 34 | fi 35 | -------------------------------------------------------------------------------- /ink-engine-runtime/VariableReference.cs: -------------------------------------------------------------------------------- 1 | namespace Ink.Runtime 2 | { 3 | public class VariableReference : Runtime.Object 4 | { 5 | // Normal named variable 6 | public string name { get; set; } 7 | 8 | // Variable reference is actually a path for a visit (read) count 9 | public Path pathForCount { get; set; } 10 | 11 | public Container containerForCount { 12 | get { 13 | return this.ResolvePath (pathForCount).container; 14 | } 15 | } 16 | 17 | public string pathStringForCount { 18 | get { 19 | if( pathForCount == null ) 20 | return null; 21 | 22 | return CompactPathString(pathForCount); 23 | } 24 | set { 25 | if (value == null) 26 | pathForCount = null; 27 | else 28 | pathForCount = new Path (value); 29 | } 30 | } 31 | 32 | public VariableReference (string name) 33 | { 34 | this.name = name; 35 | } 36 | 37 | // Require default constructor for serialisation 38 | public VariableReference() {} 39 | 40 | public override string ToString () 41 | { 42 | if (name != null) { 43 | return string.Format ("var({0})", name); 44 | } else { 45 | var pathStr = pathStringForCount; 46 | return string.Format("read_count({0})", pathStr); 47 | } 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/ConstantDeclaration.cs: -------------------------------------------------------------------------------- 1 | //using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class ConstantDeclaration : Parsed.Object 6 | { 7 | public string constantName 8 | { 9 | get { return constantIdentifier?.name; } 10 | } 11 | public Identifier constantIdentifier { get; protected set; } 12 | public Expression expression { get; protected set; } 13 | 14 | public ConstantDeclaration (Identifier name, Expression assignedExpression) 15 | { 16 | this.constantIdentifier = name; 17 | 18 | // Defensive programming in case parsing of assignedExpression failed 19 | if( assignedExpression ) 20 | this.expression = AddContent(assignedExpression); 21 | } 22 | 23 | public override Runtime.Object GenerateRuntimeObject () 24 | { 25 | // Global declarations don't generate actual procedural 26 | // runtime objects, but instead add a global variable to the story itself. 27 | // The story then initialises them all in one go at the start of the game. 28 | return null; 29 | } 30 | 31 | public override void ResolveReferences (Story context) 32 | { 33 | base.ResolveReferences (context); 34 | 35 | context.CheckForNamingCollisions (this, constantIdentifier, Story.SymbolType.Var); 36 | } 37 | 38 | public override string typeName { 39 | get { 40 | return "Constant"; 41 | } 42 | } 43 | 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /compiler/ink_compiler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Ink 6 | ink_compiler 7 | 8 | 9 | 10 | inkle Ltd 11 | InkleStudios 12 | phish 13 | https://github.com/inkle/ink 14 | https://github.com/inkle/ink 15 | https://github.com/inkle/ink/blob/master/LICENSE.txt 16 | git 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | <_Parameter1>inklecate 35 | 36 | 37 | <_Parameter1>ink-tests 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Number.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class Number : Parsed.Expression 5 | { 6 | public object value; 7 | 8 | public Number(object value) 9 | { 10 | if (value is int || value is float || value is bool) { 11 | this.value = value; 12 | } else { 13 | throw new System.Exception ("Unexpected object type in Number"); 14 | } 15 | } 16 | 17 | public override void GenerateIntoContainer (Runtime.Container container) 18 | { 19 | if (value is int) { 20 | container.AddContent (new Runtime.IntValue ((int)value)); 21 | } else if (value is float) { 22 | container.AddContent (new Runtime.FloatValue ((float)value)); 23 | } else if(value is bool) { 24 | container.AddContent (new Runtime.BoolValue ((bool)value)); 25 | } 26 | } 27 | 28 | public override string ToString () 29 | { 30 | if (value is float) { 31 | return ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture); 32 | } else { 33 | return value.ToString(); 34 | } 35 | } 36 | 37 | // Equals override necessary in order to check for CONST multiple definition equality 38 | public override bool Equals (object obj) 39 | { 40 | var otherNum = obj as Number; 41 | if (otherNum == null) return false; 42 | 43 | return this.value.Equals (otherNum.value); 44 | } 45 | 46 | public override int GetHashCode () 47 | { 48 | return this.value.GetHashCode (); 49 | } 50 | 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ink-engine-runtime/Choice.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Runtime 3 | { 4 | /// 5 | /// A generated Choice from the story. 6 | /// A single ChoicePoint in the Story could potentially generate 7 | /// different Choices dynamically dependent on state, so they're 8 | /// separated. 9 | /// 10 | public class Choice : Runtime.Object 11 | { 12 | /// 13 | /// The main text to presented to the player for this Choice. 14 | /// 15 | public string text { get; set; } 16 | 17 | /// 18 | /// The target path that the Story should be diverted to if 19 | /// this Choice is chosen. 20 | /// 21 | public string pathStringOnChoice { 22 | get { 23 | return targetPath.ToString (); 24 | } 25 | set { 26 | targetPath = new Path (value); 27 | } 28 | } 29 | 30 | /// 31 | /// Get the path to the original choice point - where was this choice defined in the story? 32 | /// 33 | /// A dot separated path into the story data. 34 | public string sourcePath; 35 | 36 | /// 37 | /// The original index into currentChoices list on the Story when 38 | /// this Choice was generated, for convenience. 39 | /// 40 | public int index { get; set; } 41 | 42 | public Path targetPath; 43 | 44 | public CallStack.Thread threadAtGeneration { get; set; } 45 | public int originalThreadIndex; 46 | 47 | public bool isInvisibleDefault; 48 | 49 | public Choice() 50 | { 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Gather.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class Gather : Parsed.Object, IWeavePoint, INamedContent 5 | { 6 | public string name 7 | { 8 | get { return identifier?.name; } 9 | } 10 | public Identifier identifier { get; set; } 11 | public int indentationDepth { get; protected set; } 12 | 13 | public Runtime.Container runtimeContainer { get { return (Runtime.Container) runtimeObject; } } 14 | 15 | public Gather (Identifier identifier, int indentationDepth) 16 | { 17 | this.identifier = identifier; 18 | this.indentationDepth = indentationDepth; 19 | } 20 | 21 | public override Runtime.Object GenerateRuntimeObject () 22 | { 23 | var container = new Runtime.Container (); 24 | container.name = name; 25 | 26 | if (this.story.countAllVisits) { 27 | container.visitsShouldBeCounted = true; 28 | } 29 | 30 | container.countingAtStartOnly = true; 31 | 32 | // A gather can have null content, e.g. it's just purely a line with "-" 33 | if (content != null) { 34 | foreach (var c in content) { 35 | container.AddContent (c.runtimeObject); 36 | } 37 | } 38 | 39 | return container; 40 | } 41 | 42 | public override void ResolveReferences (Story context) 43 | { 44 | base.ResolveReferences (context); 45 | 46 | if( identifier != null && identifier.name.Length > 0 ) 47 | context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave); 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ink-engine-runtime/ListDefinitionsOrigin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class ListDefinitionsOrigin 6 | { 7 | public List lists { 8 | get { 9 | var listOfLists = new List (); 10 | foreach (var namedList in _lists) { 11 | listOfLists.Add (namedList.Value); 12 | } 13 | return listOfLists; 14 | } 15 | } 16 | 17 | public ListDefinitionsOrigin (List lists) 18 | { 19 | _lists = new Dictionary (); 20 | _allUnambiguousListValueCache = new Dictionary(); 21 | 22 | foreach (var list in lists) { 23 | _lists [list.name] = list; 24 | 25 | foreach(var itemWithValue in list.items) { 26 | var item = itemWithValue.Key; 27 | var val = itemWithValue.Value; 28 | var listValue = new ListValue(item, val); 29 | 30 | // May be ambiguous, but compiler should've caught that, 31 | // so we may be doing some replacement here, but that's okay. 32 | _allUnambiguousListValueCache[item.itemName] = listValue; 33 | _allUnambiguousListValueCache[item.fullName] = listValue; 34 | } 35 | } 36 | } 37 | 38 | public bool TryListGetDefinition (string name, out ListDefinition def) 39 | { 40 | return _lists.TryGetValue (name, out def); 41 | } 42 | 43 | public ListValue FindSingleItemListWithName (string name) 44 | { 45 | ListValue val = null; 46 | _allUnambiguousListValueCache.TryGetValue(name, out val); 47 | return val; 48 | } 49 | 50 | Dictionary _lists; 51 | Dictionary _allUnambiguousListValueCache; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "ink Testbed (output only)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "Build InkTestBed", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/InkTestBed/bin/Debug/netcoreapp3.1/InkTestBed.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/InkTestBed", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": "ink Testbed (choices in terminal)", 23 | "type": "coreclr", 24 | "request": "launch", 25 | "preLaunchTask": "Build InkTestBed", 26 | // If you have changed target frameworks, make sure to update the program path. 27 | "program": "${workspaceFolder}/InkTestBed/bin/Debug/netcoreapp3.1/InkTestBed.dll", 28 | "args": [], 29 | "cwd": "${workspaceFolder}/InkTestBed", 30 | "console": "integratedTerminal", 31 | "stopAtEntry": false, 32 | "internalConsoleOptions": "neverOpen" 33 | }, 34 | { 35 | "name": ".NET Core Attach", 36 | "type": "coreclr", 37 | "request": "attach", 38 | "processId": "${command:pickProcess}" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /compiler/CharacterRange.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Ink 5 | { 6 | /// 7 | /// A class representing a character range. Allows for lazy-loading a corresponding character set. 8 | /// 9 | public sealed class CharacterRange 10 | { 11 | public static CharacterRange Define(char start, char end, IEnumerable excludes = null) 12 | { 13 | return new CharacterRange (start, end, excludes); 14 | } 15 | 16 | /// 17 | /// Returns a character set instance corresponding to the character range 18 | /// represented by the current instance. 19 | /// 20 | /// 21 | /// The internal character set is created once and cached in memory. 22 | /// 23 | /// The char set. 24 | public CharacterSet ToCharacterSet () 25 | { 26 | if (_correspondingCharSet.Count == 0) 27 | { 28 | for (char c = _start; c <= _end; c++) 29 | { 30 | if (!_excludes.Contains (c)) 31 | { 32 | _correspondingCharSet.Add (c); 33 | } 34 | } 35 | } 36 | return _correspondingCharSet; 37 | } 38 | 39 | public char start { get { return _start; } } 40 | public char end { get { return _end; } } 41 | 42 | CharacterRange (char start, char end, IEnumerable excludes) 43 | { 44 | _start = start; 45 | _end = end; 46 | _excludes = excludes == null ? new HashSet() : new HashSet (excludes); 47 | } 48 | 49 | char _start; 50 | char _end; 51 | ICollection _excludes; 52 | CharacterSet _correspondingCharSet = new CharacterSet(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/List.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class List : Parsed.Expression 6 | { 7 | public List itemIdentifierList; 8 | 9 | public List (List itemIdentifierList) 10 | { 11 | this.itemIdentifierList = itemIdentifierList; 12 | } 13 | 14 | public override void GenerateIntoContainer (Runtime.Container container) 15 | { 16 | var runtimeRawList = new Runtime.InkList (); 17 | 18 | if (itemIdentifierList != null) { 19 | foreach (var itemIdentifier in itemIdentifierList) { 20 | var nameParts = itemIdentifier?.name.Split ('.'); 21 | 22 | string listName = null; 23 | string listItemName = null; 24 | if (nameParts.Length > 1) { 25 | listName = nameParts [0]; 26 | listItemName = nameParts [1]; 27 | } else { 28 | listItemName = nameParts [0]; 29 | } 30 | 31 | var listItem = story.ResolveListItem (listName, listItemName, this); 32 | if (listItem == null) { 33 | if (listName == null) 34 | Error ("Could not find list definition that contains item '" + itemIdentifier + "'"); 35 | else 36 | Error ("Could not find list item " + itemIdentifier); 37 | } else { 38 | if (listName == null) 39 | listName = ((ListDefinition)listItem.parent).identifier?.name; 40 | var item = new Runtime.InkListItem (listName, listItem.name); 41 | 42 | if (runtimeRawList.ContainsKey (item)) 43 | Warning ("Duplicate of item '"+itemIdentifier+"' in list."); 44 | else 45 | runtimeRawList [item] = listItem.seriesValue; 46 | } 47 | } 48 | } 49 | 50 | container.AddContent(new Runtime.ListValue (runtimeRawList)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/StringExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Ink.Parsed 6 | { 7 | public class StringExpression : Parsed.Expression 8 | { 9 | public bool isSingleString { 10 | get { 11 | if (content.Count != 1) 12 | return false; 13 | 14 | var c = content [0]; 15 | if (!(c is Text)) 16 | return false; 17 | 18 | return true; 19 | } 20 | } 21 | 22 | public StringExpression (List content) 23 | { 24 | AddContent (content); 25 | } 26 | 27 | public override void GenerateIntoContainer (Runtime.Container container) 28 | { 29 | container.AddContent (Runtime.ControlCommand.BeginString()); 30 | 31 | foreach (var c in content) { 32 | container.AddContent (c.runtimeObject); 33 | } 34 | 35 | container.AddContent (Runtime.ControlCommand.EndString()); 36 | } 37 | 38 | public override string ToString () 39 | { 40 | var sb = new StringBuilder (); 41 | foreach (var c in content) { 42 | sb.Append (c.ToString ()); 43 | } 44 | return sb.ToString (); 45 | } 46 | 47 | // Equals override necessary in order to check for CONST multiple definition equality 48 | public override bool Equals (object obj) 49 | { 50 | var otherStr = obj as StringExpression; 51 | if (otherStr == null) return false; 52 | 53 | // Can only compare direct equality on single strings rather than 54 | // complex string expressions that contain dynamic logic 55 | if (!this.isSingleString || !otherStr.isSingleString) { 56 | return false; 57 | } 58 | 59 | var thisTxt = this.ToString (); 60 | var otherTxt = otherStr.ToString (); 61 | return thisTxt.Equals (otherTxt); 62 | } 63 | 64 | public override int GetHashCode () 65 | { 66 | return this.ToString ().GetHashCode (); 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /ink-engine-runtime/Pointer.cs: -------------------------------------------------------------------------------- 1 | using Ink.Runtime; 2 | 3 | namespace Ink.Runtime 4 | { 5 | /// 6 | /// Internal structure used to point to a particular / current point in the story. 7 | /// Where Path is a set of components that make content fully addressable, this is 8 | /// a reference to the current container, and the index of the current piece of 9 | /// content within that container. This scheme makes it as fast and efficient as 10 | /// possible to increment the pointer (move the story forwards) in a way that's as 11 | /// native to the internal engine as possible. 12 | /// 13 | public struct Pointer 14 | { 15 | public Container container; 16 | public int index; 17 | 18 | public Pointer (Container container, int index) 19 | { 20 | this.container = container; 21 | this.index = index; 22 | } 23 | 24 | public Runtime.Object Resolve () 25 | { 26 | if (index < 0) return container; 27 | if (container == null) return null; 28 | if (container.content.Count == 0) return container; 29 | if (index >= container.content.Count) return null; 30 | return container.content [index]; 31 | 32 | } 33 | 34 | public bool isNull { 35 | get { 36 | return container == null; 37 | } 38 | } 39 | 40 | public Path path { 41 | get { 42 | if( isNull ) return null; 43 | 44 | if (index >= 0) 45 | return container.path.PathByAppendingComponent (new Path.Component(index)); 46 | else 47 | return container.path; 48 | } 49 | } 50 | 51 | public override string ToString () 52 | { 53 | if (container == null) 54 | return "Ink Pointer (null)"; 55 | 56 | return "Ink Pointer -> " + container.path.ToString () + " -- index " + index; 57 | } 58 | 59 | public static Pointer StartOf (Container container) 60 | { 61 | return new Pointer { 62 | container = container, 63 | index = 0 64 | }; 65 | } 66 | 67 | public static Pointer Null = new Pointer { container = null, index = -1 }; 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /ink-engine-runtime/ink-engine-runtime.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.0;netstandard2.0 5 | Ink.Runtime 6 | ink-engine-runtime 7 | 8 | 9 | 10 | bin\$(Configuration)\$(TargetFramework)\ink-engine-runtime.xml 11 | inkle Ltd 12 | InkleStudios 13 | inkle Ltd 14 | https://github.com/inkle/ink 15 | https://www.inklestudios.com/ink/ 16 | 0.9.0 17 | https://avatars2.githubusercontent.com/u/1987090 18 | Runtime engine for the ink scripting language 19 | git 20 | true 21 | Inkle.Ink.Engine 22 | LICENSE.txt 23 | 1701;1702;1591 24 | 25 | 26 | 30 | 31 | 32 | 33 | True 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | <_Parameter1>ink_compiler 46 | 47 | 48 | <_Parameter1>inklecate 49 | 50 | 51 | <_Parameter1>ink-tests 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/ContentList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace Ink.Parsed 5 | { 6 | public class ContentList : Parsed.Object 7 | { 8 | public bool dontFlatten { get; set; } 9 | 10 | public Runtime.Container runtimeContainer { 11 | get { 12 | return (Runtime.Container) this.runtimeObject; 13 | } 14 | } 15 | 16 | public ContentList (List objects) 17 | { 18 | if( objects != null ) 19 | AddContent (objects); 20 | } 21 | 22 | public ContentList (params Parsed.Object[] objects) 23 | { 24 | if (objects != null) { 25 | var objList = new List (objects); 26 | AddContent (objList); 27 | } 28 | } 29 | 30 | public ContentList() 31 | { 32 | } 33 | 34 | public void TrimTrailingWhitespace() 35 | { 36 | for (int i = this.content.Count - 1; i >= 0; --i) { 37 | var text = this.content [i] as Text; 38 | if (text == null) 39 | break; 40 | 41 | text.text = text.text.TrimEnd (' ', '\t'); 42 | if (text.text.Length == 0) 43 | this.content.RemoveAt (i); 44 | else 45 | break; 46 | } 47 | } 48 | 49 | public override Runtime.Object GenerateRuntimeObject () 50 | { 51 | var container = new Runtime.Container (); 52 | if (content != null) { 53 | foreach (var obj in content) { 54 | var contentObjRuntime = obj.runtimeObject; 55 | 56 | // Some objects (e.g. author warnings) don't generate runtime objects 57 | if( contentObjRuntime ) 58 | container.AddContent (contentObjRuntime); 59 | } 60 | } 61 | 62 | if( dontFlatten ) 63 | story.DontFlattenContainer (container); 64 | 65 | return container; 66 | } 67 | 68 | public override string ToString () 69 | { 70 | var sb = new StringBuilder (); 71 | sb.Append ("ContentList("); 72 | sb.Append(string.Join (", ", content.ToStringsArray())); 73 | sb.Append (")"); 74 | return sb.ToString (); 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /compiler/Stats.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink { 3 | public struct Stats { 4 | 5 | public int words; 6 | public int knots; 7 | public int stitches; 8 | public int functions; 9 | public int choices; 10 | public int gathers; 11 | public int diverts; 12 | 13 | public static Stats Generate(Ink.Parsed.Story story) { 14 | var stats = new Stats(); 15 | 16 | var allText = story.FindAll(); 17 | 18 | // Count all the words across all strings 19 | stats.words = 0; 20 | foreach(var text in allText) { 21 | 22 | var wordsInThisStr = 0; 23 | var wasWhiteSpace = true; 24 | foreach(var c in text.text) { 25 | if( c == ' ' || c == '\t' || c == '\n' || c == '\r' ) { 26 | wasWhiteSpace = true; 27 | } else if( wasWhiteSpace ) { 28 | wordsInThisStr++; 29 | wasWhiteSpace = false; 30 | } 31 | } 32 | 33 | stats.words += wordsInThisStr; 34 | } 35 | 36 | var knots = story.FindAll(); 37 | stats.knots = knots.Count; 38 | 39 | stats.functions = 0; 40 | foreach(var knot in knots) 41 | if (knot.isFunction) stats.functions++; 42 | 43 | var stitches = story.FindAll(); 44 | stats.stitches = stitches.Count; 45 | 46 | var choices = story.FindAll(); 47 | stats.choices = choices.Count; 48 | 49 | // Skip implicit gather that's generated at top of story 50 | // (we know which it is because it isn't assigned debug metadata) 51 | var gathers = story.FindAll(g => g.debugMetadata != null); 52 | stats.gathers = gathers.Count; 53 | 54 | // May not be entirely what you expect. 55 | // Does it nevertheless have value? 56 | // Includes: 57 | // - DONE, END 58 | // - Function calls 59 | // - Some implicitly generated weave diverts 60 | // But we subtract one for the implicit DONE 61 | // at the end of the main flow outside of knots. 62 | var diverts = story.FindAll(); 63 | stats.diverts = diverts.Count - 1; 64 | 65 | return stats; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /ink-engine-runtime/ListDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class ListDefinition 6 | { 7 | public string name { get { return _name; } } 8 | 9 | public Dictionary items { 10 | get { 11 | if (_items == null) { 12 | _items = new Dictionary (); 13 | foreach (var itemNameAndValue in _itemNameToValues) { 14 | var item = new InkListItem (name, itemNameAndValue.Key); 15 | _items [item] = itemNameAndValue.Value; 16 | } 17 | } 18 | return _items; 19 | } 20 | } 21 | Dictionary _items; 22 | 23 | public int ValueForItem (InkListItem item) 24 | { 25 | int intVal; 26 | if (_itemNameToValues.TryGetValue (item.itemName, out intVal)) 27 | return intVal; 28 | else 29 | return 0; 30 | } 31 | 32 | public bool ContainsItem (InkListItem item) 33 | { 34 | if (item.originName != name) return false; 35 | 36 | return _itemNameToValues.ContainsKey (item.itemName); 37 | } 38 | 39 | public bool ContainsItemWithName (string itemName) 40 | { 41 | return _itemNameToValues.ContainsKey (itemName); 42 | } 43 | 44 | public bool TryGetItemWithValue (int val, out InkListItem item) 45 | { 46 | foreach (var namedItem in _itemNameToValues) { 47 | if (namedItem.Value == val) { 48 | item = new InkListItem (name, namedItem.Key); 49 | return true; 50 | } 51 | } 52 | 53 | item = InkListItem.Null; 54 | return false; 55 | } 56 | 57 | public bool TryGetValueForItem (InkListItem item, out int intVal) 58 | { 59 | return _itemNameToValues.TryGetValue (item.itemName, out intVal); 60 | } 61 | 62 | public ListDefinition (string name, Dictionary items) 63 | { 64 | _name = name; 65 | _itemNameToValues = items; 66 | } 67 | 68 | string _name; 69 | 70 | // The main representation should be simple item names rather than a RawListItem, 71 | // since we mainly want to access items based on their simple name, since that's 72 | // how they'll be most commonly requested from ink. 73 | Dictionary _itemNameToValues; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Conditional.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Ink.Runtime; 4 | 5 | namespace Ink.Parsed 6 | { 7 | public class Conditional : Parsed.Object 8 | { 9 | public Expression initialCondition { get; private set; } 10 | public List branches { get; private set; } 11 | 12 | public Conditional (Expression condition, List branches) 13 | { 14 | this.initialCondition = condition; 15 | if (this.initialCondition) { 16 | AddContent (condition); 17 | } 18 | 19 | this.branches = branches; 20 | if (this.branches != null) { 21 | AddContent (this.branches.Cast ().ToList ()); 22 | } 23 | 24 | } 25 | 26 | public override Runtime.Object GenerateRuntimeObject () 27 | { 28 | var container = new Runtime.Container (); 29 | 30 | // Initial condition 31 | if (this.initialCondition) { 32 | container.AddContent (initialCondition.runtimeObject); 33 | } 34 | 35 | // Individual branches 36 | foreach (var branch in branches) { 37 | var branchContainer = (Container) branch.runtimeObject; 38 | container.AddContent (branchContainer); 39 | } 40 | 41 | // If it's a switch-like conditional, each branch 42 | // will have a "duplicate" operation for the original 43 | // switched value. If there's no final else clause 44 | // and we fall all the way through, we need to clean up. 45 | // (An else clause doesn't dup but it *does* pop) 46 | if (this.initialCondition != null && branches [0].ownExpression != null && !branches [branches.Count - 1].isElse) { 47 | container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ()); 48 | } 49 | 50 | // Target for branches to rejoin to 51 | _reJoinTarget = Runtime.ControlCommand.NoOp (); 52 | container.AddContent (_reJoinTarget); 53 | 54 | return container; 55 | } 56 | 57 | public override void ResolveReferences (Story context) 58 | { 59 | var pathToReJoin = _reJoinTarget.path; 60 | 61 | foreach (var branch in branches) { 62 | branch.returnDivert.targetPath = pathToReJoin; 63 | } 64 | 65 | base.ResolveReferences (context); 66 | } 67 | 68 | Runtime.ControlCommand _reJoinTarget; 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /ink-engine-runtime/StatePatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class StatePatch 6 | { 7 | public Dictionary globals { get { return _globals; } } 8 | public HashSet changedVariables { get { return _changedVariables; } } 9 | public Dictionary visitCounts { get { return _visitCounts; } } 10 | public Dictionary turnIndices { get { return _turnIndices; } } 11 | 12 | public StatePatch(StatePatch toCopy) 13 | { 14 | if( toCopy != null ) { 15 | _globals = new Dictionary(toCopy._globals); 16 | _changedVariables = new HashSet(toCopy._changedVariables); 17 | _visitCounts = new Dictionary(toCopy._visitCounts); 18 | _turnIndices = new Dictionary(toCopy._turnIndices); 19 | } else { 20 | _globals = new Dictionary(); 21 | _changedVariables = new HashSet(); 22 | _visitCounts = new Dictionary(); 23 | _turnIndices = new Dictionary(); 24 | } 25 | } 26 | 27 | public bool TryGetGlobal(string name, out Runtime.Object value) 28 | { 29 | return _globals.TryGetValue(name, out value); 30 | } 31 | 32 | public void SetGlobal(string name, Runtime.Object value){ 33 | _globals[name] = value; 34 | } 35 | 36 | public void AddChangedVariable(string name) 37 | { 38 | _changedVariables.Add(name); 39 | } 40 | 41 | public bool TryGetVisitCount(Container container, out int count) 42 | { 43 | return _visitCounts.TryGetValue(container, out count); 44 | } 45 | 46 | public void SetVisitCount(Container container, int count) 47 | { 48 | _visitCounts[container] = count; 49 | } 50 | 51 | public void SetTurnIndex(Container container, int index) 52 | { 53 | _turnIndices[container] = index; 54 | } 55 | 56 | public bool TryGetTurnIndex(Container container, out int index) 57 | { 58 | return _turnIndices.TryGetValue(container, out index); 59 | } 60 | 61 | Dictionary _globals; 62 | HashSet _changedVariables = new HashSet(); 63 | Dictionary _visitCounts = new Dictionary(); 64 | Dictionary _turnIndices = new Dictionary(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_Include.cs: -------------------------------------------------------------------------------- 1 | using Ink.Parsed; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | 6 | namespace Ink 7 | { 8 | public partial class InkParser 9 | { 10 | protected object IncludeStatement() 11 | { 12 | Whitespace (); 13 | 14 | if (ParseString ("INCLUDE") == null) 15 | return null; 16 | 17 | Whitespace (); 18 | 19 | var filename = (string) Expect(() => ParseUntilCharactersFromString ("\n\r"), "filename for include statement"); 20 | filename = filename.TrimEnd (' ', '\t'); 21 | 22 | // Working directory should already have been set up relative to the root ink file. 23 | var fullFilename = _rootParser._fileHandler.ResolveInkFilename (filename); 24 | 25 | if (FilenameIsAlreadyOpen (fullFilename)) { 26 | Error ("Recursive INCLUDE detected: '" + fullFilename + "' is already open."); 27 | ParseUntilCharactersFromString("\r\n"); 28 | return new IncludedFile(null); 29 | } else { 30 | AddOpenFilename (fullFilename); 31 | } 32 | 33 | Parsed.Story includedStory = null; 34 | string includedString = null; 35 | try { 36 | includedString = _rootParser._fileHandler.LoadInkFileContents(fullFilename); 37 | } 38 | catch { 39 | Error ("Failed to load: '"+filename+"'"); 40 | } 41 | 42 | 43 | if (includedString != null ) { 44 | InkParser parser = new InkParser(includedString, filename, _externalErrorHandler, _rootParser); 45 | includedStory = parser.Parse(); 46 | } 47 | 48 | RemoveOpenFilename (fullFilename); 49 | 50 | // Return valid IncludedFile object even if there were errors when parsing. 51 | // We don't want to attempt to re-parse the include line as something else, 52 | // and we want to include the bits that *are* valid, so we don't generate 53 | // more errors than necessary. 54 | return new IncludedFile (includedStory); 55 | } 56 | 57 | bool FilenameIsAlreadyOpen(string fullFilename) 58 | { 59 | return _rootParser._openFilenames.Contains (fullFilename); 60 | } 61 | 62 | void AddOpenFilename(string fullFilename) 63 | { 64 | _rootParser._openFilenames.Add (fullFilename); 65 | } 66 | 67 | void RemoveOpenFilename(string fullFilename) 68 | { 69 | _rootParser._openFilenames.Remove (fullFilename); 70 | } 71 | 72 | InkParser _rootParser; 73 | HashSet _openFilenames; 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /ink-engine-runtime/DebugMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class DebugMetadata 6 | { 7 | public int startLineNumber = 0; 8 | public int endLineNumber = 0; 9 | public int startCharacterNumber = 0; 10 | public int endCharacterNumber = 0; 11 | public string fileName = null; 12 | public string sourceName = null; 13 | 14 | public DebugMetadata () 15 | { 16 | } 17 | 18 | // Currently only used in VariableReference in order to 19 | // merge the debug metadata of a Path.Of.Indentifiers into 20 | // one single range. 21 | public DebugMetadata Merge(DebugMetadata dm) 22 | { 23 | var newDebugMetadata = new DebugMetadata(); 24 | 25 | // These are not supposed to be differ between 'this' and 'dm'. 26 | newDebugMetadata.fileName = fileName; 27 | newDebugMetadata.sourceName = sourceName; 28 | 29 | if (startLineNumber < dm.startLineNumber) 30 | { 31 | newDebugMetadata.startLineNumber = startLineNumber; 32 | newDebugMetadata.startCharacterNumber = startCharacterNumber; 33 | } 34 | else if (startLineNumber > dm.startLineNumber) 35 | { 36 | newDebugMetadata.startLineNumber = dm.startLineNumber; 37 | newDebugMetadata.startCharacterNumber = dm.startCharacterNumber; 38 | } 39 | else 40 | { 41 | newDebugMetadata.startLineNumber = startLineNumber; 42 | newDebugMetadata.startCharacterNumber = Math.Min(startCharacterNumber, dm.startCharacterNumber); 43 | } 44 | 45 | if (endLineNumber > dm.endLineNumber) 46 | { 47 | newDebugMetadata.endLineNumber = endLineNumber; 48 | newDebugMetadata.endCharacterNumber = endCharacterNumber; 49 | } 50 | else if (endLineNumber < dm.endLineNumber) 51 | { 52 | newDebugMetadata.endLineNumber = dm.endLineNumber; 53 | newDebugMetadata.endCharacterNumber = dm.endCharacterNumber; 54 | } 55 | else 56 | { 57 | newDebugMetadata.endLineNumber = endLineNumber; 58 | newDebugMetadata.endCharacterNumber = Math.Max(endCharacterNumber, dm.endCharacterNumber); 59 | } 60 | 61 | return newDebugMetadata; 62 | } 63 | 64 | public override string ToString () 65 | { 66 | if (fileName != null) { 67 | return string.Format ("line {0} of {1}", startLineNumber, fileName); 68 | } else { 69 | return "line " + startLineNumber; 70 | } 71 | 72 | } 73 | 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_CharacterRanges.cs: -------------------------------------------------------------------------------- 1 | using Ink.Parsed; 2 | using System; 3 | using System.Text; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace Ink 8 | { 9 | public partial class InkParser 10 | { 11 | public static readonly CharacterRange LatinBasic = 12 | CharacterRange.Define ('\u0041', '\u007A', excludes: new CharacterSet().AddRange('\u005B', '\u0060')); 13 | public static readonly CharacterRange LatinExtendedA = CharacterRange.Define('\u0100', '\u017F'); // no excludes here 14 | public static readonly CharacterRange LatinExtendedB = CharacterRange.Define('\u0180', '\u024F'); // no excludes here 15 | public static readonly CharacterRange Greek = 16 | CharacterRange.Define('\u0370', '\u03FF', excludes: new CharacterSet().AddRange('\u0378','\u0385').AddCharacters("\u0374\u0375\u0378\u0387\u038B\u038D\u03A2")); 17 | public static readonly CharacterRange Cyrillic = 18 | CharacterRange.Define('\u0400', '\u04FF', excludes: new CharacterSet().AddRange('\u0482', '\u0489')); 19 | public static readonly CharacterRange Armenian = 20 | CharacterRange.Define('\u0530', '\u058F', excludes: new CharacterSet().AddCharacters("\u0530").AddRange('\u0557', '\u0560').AddRange('\u0588', '\u058E')); 21 | public static readonly CharacterRange Hebrew = 22 | CharacterRange.Define('\u0590', '\u05FF', excludes: new CharacterSet()); 23 | public static readonly CharacterRange Arabic = 24 | CharacterRange.Define('\u0600', '\u06FF', excludes: new CharacterSet()); 25 | public static readonly CharacterRange Korean = 26 | CharacterRange.Define('\uAC00', '\uD7AF', excludes: new CharacterSet()); 27 | public static readonly CharacterRange Latin1Supplement = 28 | CharacterRange.Define('\u0080', '\u00FF', excludes: new CharacterSet()); 29 | 30 | private void ExtendIdentifierCharacterRanges(CharacterSet identifierCharSet) 31 | { 32 | var characterRanges = ListAllCharacterRanges(); 33 | 34 | foreach (var charRange in characterRanges) 35 | { 36 | identifierCharSet.AddCharacters(charRange.ToCharacterSet()); 37 | } 38 | } 39 | 40 | /// 41 | /// Gets an array of representing all of the currently supported 42 | /// non-ASCII character ranges that can be used in identifier names. 43 | /// 44 | /// 45 | /// An array of representing all of the currently supported 46 | /// non-ASCII character ranges that can be used in identifier names. 47 | /// 48 | public static CharacterRange[] ListAllCharacterRanges() { 49 | return new CharacterRange[] { 50 | LatinBasic, 51 | LatinExtendedA, 52 | LatinExtendedB, 53 | Arabic, 54 | Armenian, 55 | Cyrillic, 56 | Greek, 57 | Hebrew, 58 | Korean, 59 | Latin1Supplement, 60 | }; 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_Whitespace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink 4 | { 5 | public partial class InkParser 6 | { 7 | // Handles both newline and endOfFile 8 | protected object EndOfLine() 9 | { 10 | return OneOf(Newline, EndOfFile); 11 | } 12 | 13 | // Allow whitespace before the actual newline 14 | protected object Newline() 15 | { 16 | Whitespace(); 17 | 18 | bool gotNewline = ParseNewline () != null; 19 | 20 | // Optional \r, definite \n to support Windows (\r\n) and Mac/Unix (\n) 21 | 22 | if( !gotNewline ) { 23 | return null; 24 | } else { 25 | return ParseSuccess; 26 | } 27 | } 28 | 29 | protected object EndOfFile() 30 | { 31 | Whitespace(); 32 | 33 | if (!endOfInput) 34 | return null; 35 | 36 | return ParseSuccess; 37 | } 38 | 39 | 40 | // General purpose space, returns N-count newlines (fails if no newlines) 41 | protected object MultilineWhitespace() 42 | { 43 | List newlines = OneOrMore(Newline); 44 | if (newlines == null) 45 | return null; 46 | 47 | // Use content field of Token to say how many newlines there were 48 | // (in most circumstances it's unimportant) 49 | int numNewlines = newlines.Count; 50 | if (numNewlines >= 1) { 51 | return ParseSuccess; 52 | } else { 53 | return null; 54 | } 55 | } 56 | 57 | protected object Whitespace() 58 | { 59 | if( ParseCharactersFromCharSet(_inlineWhitespaceChars) != null ) { 60 | return ParseSuccess; 61 | } 62 | 63 | return null; 64 | } 65 | 66 | protected ParseRule Spaced(ParseRule rule) 67 | { 68 | return () => { 69 | 70 | Whitespace (); 71 | 72 | var result = ParseObject(rule); 73 | if (result == null) { 74 | return null; 75 | } 76 | 77 | Whitespace (); 78 | 79 | return result; 80 | }; 81 | } 82 | 83 | protected object AnyWhitespace () 84 | { 85 | bool anyWhitespace = false; 86 | while (OneOf (Whitespace, MultilineWhitespace) != null) { 87 | anyWhitespace = true; 88 | } 89 | return anyWhitespace ? ParseSuccess : null; 90 | } 91 | 92 | protected ParseRule MultiSpaced (ParseRule rule) 93 | { 94 | return () => { 95 | 96 | AnyWhitespace (); 97 | 98 | var result = ParseObject (rule); 99 | if (result == null) { 100 | return null; 101 | } 102 | 103 | AnyWhitespace (); 104 | 105 | return result; 106 | }; 107 | } 108 | 109 | private CharacterSet _inlineWhitespaceChars = new CharacterSet(" \t"); 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /ink-engine-runtime/ChoicePoint.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Ink.Runtime 4 | { 5 | /// 6 | /// The ChoicePoint represents the point within the Story where 7 | /// a Choice instance gets generated. The distinction is made 8 | /// because the text of the Choice can be dynamically generated. 9 | /// 10 | public class ChoicePoint : Runtime.Object 11 | { 12 | public Path pathOnChoice { 13 | get { 14 | // Resolve any relative paths to global ones as we come across them 15 | if (_pathOnChoice != null && _pathOnChoice.isRelative) { 16 | var choiceTargetObj = choiceTarget; 17 | if (choiceTargetObj) { 18 | _pathOnChoice = choiceTargetObj.path; 19 | } 20 | } 21 | return _pathOnChoice; 22 | } 23 | set { 24 | _pathOnChoice = value; 25 | } 26 | } 27 | Path _pathOnChoice; 28 | 29 | public Container choiceTarget { 30 | get { 31 | return this.ResolvePath (_pathOnChoice).container; 32 | } 33 | } 34 | 35 | public string pathStringOnChoice { 36 | get { 37 | return CompactPathString (pathOnChoice); 38 | } 39 | set { 40 | pathOnChoice = new Path (value); 41 | } 42 | } 43 | 44 | public bool hasCondition { get; set; } 45 | public bool hasStartContent { get; set; } 46 | public bool hasChoiceOnlyContent { get; set; } 47 | public bool onceOnly { get; set; } 48 | public bool isInvisibleDefault { get; set; } 49 | 50 | public int flags { 51 | get { 52 | int flags = 0; 53 | if (hasCondition) flags |= 1; 54 | if (hasStartContent) flags |= 2; 55 | if (hasChoiceOnlyContent) flags |= 4; 56 | if (isInvisibleDefault) flags |= 8; 57 | if (onceOnly) flags |= 16; 58 | return flags; 59 | } 60 | set { 61 | hasCondition = (value & 1) > 0; 62 | hasStartContent = (value & 2) > 0; 63 | hasChoiceOnlyContent = (value & 4) > 0; 64 | isInvisibleDefault = (value & 8) > 0; 65 | onceOnly = (value & 16) > 0; 66 | } 67 | } 68 | 69 | public ChoicePoint (bool onceOnly) 70 | { 71 | this.onceOnly = onceOnly; 72 | } 73 | 74 | public ChoicePoint() : this(true) {} 75 | 76 | public override string ToString () 77 | { 78 | int? targetLineNum = DebugLineNumberOfPath (pathOnChoice); 79 | string targetString = pathOnChoice.ToString (); 80 | 81 | if (targetLineNum != null) { 82 | targetString = " line " + targetLineNum + "("+targetString+")"; 83 | } 84 | 85 | return "Choice: -> " + targetString; 86 | } 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /compiler/InkParser/CommentEliminator.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink 3 | { 4 | /// 5 | /// Pre-pass before main ink parser runs. It actually performs two main tasks: 6 | /// - comment elimination to simplify the parse rules in the main parser 7 | /// - Conversion of Windows line endings (\r\n) to the simpler Unix style (\n), so 8 | /// we don't have to worry about them later. 9 | /// 10 | public class CommentEliminator : StringParser 11 | { 12 | public CommentEliminator (string input) : base(input) 13 | { 14 | } 15 | 16 | public string Process() 17 | { 18 | // Make both comments and non-comments optional to handle trivial empty file case (or *only* comments) 19 | var stringList = Interleave(Optional (CommentsAndNewlines), Optional(MainInk)); 20 | 21 | if (stringList != null) { 22 | return string.Join("", stringList.ToArray()); 23 | } else { 24 | return null; 25 | } 26 | } 27 | 28 | string MainInk() 29 | { 30 | return ParseUntil (CommentsAndNewlines, _commentOrNewlineStartCharacter, null); 31 | } 32 | 33 | string CommentsAndNewlines() 34 | { 35 | var newlines = Interleave (Optional (ParseNewline), Optional (ParseSingleComment)); 36 | 37 | if (newlines != null) { 38 | return string.Join ("", newlines.ToArray()); 39 | } else { 40 | return null; 41 | } 42 | } 43 | 44 | // Valid comments always return either an empty string or pure newlines, 45 | // which we want to keep so that line numbers stay the same 46 | string ParseSingleComment() 47 | { 48 | return (string) OneOf (EndOfLineComment, BlockComment); 49 | } 50 | 51 | string EndOfLineComment() 52 | { 53 | if (ParseString ("//") == null) { 54 | return null; 55 | } 56 | 57 | ParseUntilCharactersFromCharSet (_newlineCharacters); 58 | 59 | return ""; 60 | } 61 | 62 | string BlockComment() 63 | { 64 | if (ParseString ("/*") == null) { 65 | return null; 66 | } 67 | 68 | int startLineIndex = lineIndex; 69 | 70 | var commentResult = ParseUntil (String("*/"), _commentBlockEndCharacter, null); 71 | 72 | if (!endOfInput) { 73 | ParseString ("*/"); 74 | } 75 | 76 | // Count the number of lines that were inside the block, and replicate them as newlines 77 | // so that the line indexing still works from the original source 78 | if (commentResult != null) { 79 | return new string ('\n', lineIndex - startLineIndex); 80 | } 81 | 82 | // No comment at all 83 | else { 84 | return null; 85 | } 86 | } 87 | 88 | CharacterSet _commentOrNewlineStartCharacter = new CharacterSet ("/\r\n"); 89 | CharacterSet _commentBlockEndCharacter = new CharacterSet("*"); 90 | CharacterSet _newlineCharacters = new CharacterSet ("\n\r"); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /Sublime3Syntax/README.md: -------------------------------------------------------------------------------- 1 | # ink sublime syntax 2 | 3 | ## Quickstart 4 | 5 | **Mac**: Double-click the `install_for_sublime2and3.command` script. This will copy the right files into the right place for Sublime Text 2 and/or 3. 6 | 7 | **Windows** (untested): Copy the "files to be installed" into Sublime's `Packages/User` directory. 8 | 9 | (TODO: We should get this added to [Sublime Text's Package Control](https://packagecontrol.io/).) 10 | 11 | ## What's included 12 | 13 | ### Files to be installed 14 | 15 | * `ink.tmLanguage`: This is the file compiled using the AAAPackageDev package in *Sublime Text 3*, and is simply an uglier plist-based XML version of the YAML grammar. 16 | * `ink.tmTheme`: A custom colour scheme for using ink. Unfortunately, it's necessary to use this since ink requires unique semantic markup that doesn't map very nicely to standard programming and markup concepts. We'd welcome other themes (like the dark version `ink-dark.thTheme`) that use the ink symbol names. 17 | * `ink.sublime-settings`: Choose the above colour scheme by default and turns on word wrapping by default for ink. If you want to use the alternate dark scheme, you may change it there. 18 | * `ink-comments.tmPreferences`: Defines characters to insert when user uses comment shortcut in Sublime. 19 | * `ink-global-symbols.tmPreferences` and `ink-local-symbols.tmPreferences`: Defines which symbols appear in Sublime's *Goto Symbol...* and *Goto Symbol In Project...* options. 20 | 21 | ### Other files 22 | 23 | * `ink.YAML-tmLanguage`: This is the main source file for the syntax 24 | * `LiveWatchAndInstallOnEdit.command` - when continuously editing the above the files, you can run this script so that it installs them automatically as you save changes to them (Mac only). 25 | 26 | (Note: Unfortunately we can't use the alternative `.sublime-syntax` ([documentation here](https://www.sublimetext.com/docs/3/syntax.html)) just yet since it's not available for non-dev builds of Sublime Text 3 yet.) 27 | 28 | 29 | ## Syntax file development 30 | 31 | (Workflow designed for Mac.) 32 | 33 | 1. Install the AAAPackageDev file in Sublime Text 34 | 2. Run `LiveWatchAndInstallOnEdit.command`. 35 | 3. Make edits to the `ink2.YAML-tmLanguage` file (or other files listed above). 36 | 4. CMD-B to build the language file. The first time after opening it, it'll ask you which file type to compile to - choose **Propery List**. It will then generate the compiled `.tmLanguage` file. 37 | 5. The live watch script will copy the built files into the right place (or alternatively if you don't want to install `fswatch`, you can just run the manual install script or do it yourself.) 38 | 39 | Some helpful links: 40 | 41 | - - Despite being apparently out of date, I found this to be a helpful and clear tutorial 42 | - - Original TextMate tutorial that Sublime Text's grammars are based off 43 | - - Mirror of all the scope names available for colour highlighting 44 | - - The most up to date reference available 45 | - - Sublime Text uses Ruby flavoured regexes 46 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/TunnelOnwards.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class TunnelOnwards : Parsed.Object 6 | { 7 | public Divert divertAfter { 8 | get { 9 | return _divertAfter; 10 | } 11 | set { 12 | _divertAfter = value; 13 | if (_divertAfter) AddContent (_divertAfter); 14 | } 15 | } 16 | Divert _divertAfter; 17 | 18 | public override Runtime.Object GenerateRuntimeObject () 19 | { 20 | var container = new Runtime.Container (); 21 | 22 | // Set override path for tunnel onwards (or nothing) 23 | container.AddContent (Runtime.ControlCommand.EvalStart ()); 24 | 25 | if (divertAfter) { 26 | 27 | // Generate runtime object's generated code and steal the arguments runtime code 28 | var returnRuntimeObj = divertAfter.GenerateRuntimeObject (); 29 | var returnRuntimeContainer = returnRuntimeObj as Runtime.Container; 30 | if (returnRuntimeContainer) { 31 | 32 | // Steal all code for generating arguments from the divert 33 | var args = divertAfter.arguments; 34 | if (args != null && args.Count > 0) { 35 | 36 | // Steal everything betwen eval start and eval end 37 | int evalStart = -1; 38 | int evalEnd = -1; 39 | for (int i = 0; i < returnRuntimeContainer.content.Count; i++) { 40 | var cmd = returnRuntimeContainer.content [i] as Runtime.ControlCommand; 41 | if (cmd) { 42 | if (evalStart == -1 && cmd.commandType == Runtime.ControlCommand.CommandType.EvalStart) 43 | evalStart = i; 44 | else if (cmd.commandType == Runtime.ControlCommand.CommandType.EvalEnd) 45 | evalEnd = i; 46 | } 47 | } 48 | 49 | for (int i = evalStart + 1; i < evalEnd; i++) { 50 | var obj = returnRuntimeContainer.content [i]; 51 | obj.parent = null; // prevent error of being moved between owners 52 | container.AddContent (returnRuntimeContainer.content [i]); 53 | } 54 | } 55 | } 56 | 57 | // Finally, divert to the requested target 58 | _overrideDivertTarget = new Runtime.DivertTargetValue (); 59 | container.AddContent (_overrideDivertTarget); 60 | } 61 | 62 | // No divert after tunnel onwards 63 | else { 64 | container.AddContent (new Runtime.Void ()); 65 | } 66 | 67 | container.AddContent (Runtime.ControlCommand.EvalEnd ()); 68 | 69 | container.AddContent (Runtime.ControlCommand.PopTunnel ()); 70 | 71 | return container; 72 | } 73 | 74 | public override void ResolveReferences (Story context) 75 | { 76 | base.ResolveReferences (context); 77 | 78 | if (divertAfter && divertAfter.targetContent) 79 | _overrideDivertTarget.targetPath = divertAfter.targetContent.runtimePath; 80 | } 81 | 82 | Runtime.DivertTargetValue _overrideDivertTarget; 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /ink-engine-runtime/Flow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class Flow { 6 | public string name; 7 | public CallStack callStack; 8 | public List outputStream; 9 | public List currentChoices; 10 | 11 | public Flow(string name, Story story) { 12 | this.name = name; 13 | this.callStack = new CallStack(story); 14 | this.outputStream = new List(); 15 | this.currentChoices = new List(); 16 | } 17 | 18 | public Flow(string name, Story story, Dictionary jObject) { 19 | this.name = name; 20 | this.callStack = new CallStack(story); 21 | this.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstack"], story); 22 | this.outputStream = Json.JArrayToRuntimeObjList ((List)jObject ["outputStream"]); 23 | this.currentChoices = Json.JArrayToRuntimeObjList((List)jObject ["currentChoices"]); 24 | 25 | // choiceThreads is optional 26 | object jChoiceThreadsObj; 27 | jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj); 28 | LoadFlowChoiceThreads((Dictionary)jChoiceThreadsObj, story); 29 | } 30 | 31 | public void WriteJson(SimpleJson.Writer writer) 32 | { 33 | writer.WriteObjectStart(); 34 | 35 | writer.WriteProperty("callstack", callStack.WriteJson); 36 | writer.WriteProperty("outputStream", w => Json.WriteListRuntimeObjs(w, outputStream)); 37 | 38 | // choiceThreads: optional 39 | // Has to come BEFORE the choices themselves are written out 40 | // since the originalThreadIndex of each choice needs to be set 41 | bool hasChoiceThreads = false; 42 | foreach (Choice c in currentChoices) 43 | { 44 | c.originalThreadIndex = c.threadAtGeneration.threadIndex; 45 | 46 | if (callStack.ThreadWithIndex(c.originalThreadIndex) == null) 47 | { 48 | if (!hasChoiceThreads) 49 | { 50 | hasChoiceThreads = true; 51 | writer.WritePropertyStart("choiceThreads"); 52 | writer.WriteObjectStart(); 53 | } 54 | 55 | writer.WritePropertyStart(c.originalThreadIndex); 56 | c.threadAtGeneration.WriteJson(writer); 57 | writer.WritePropertyEnd(); 58 | } 59 | } 60 | 61 | if (hasChoiceThreads) 62 | { 63 | writer.WriteObjectEnd(); 64 | writer.WritePropertyEnd(); 65 | } 66 | 67 | 68 | writer.WriteProperty("currentChoices", w => { 69 | w.WriteArrayStart(); 70 | foreach (var c in currentChoices) 71 | Json.WriteChoice(w, c); 72 | w.WriteArrayEnd(); 73 | }); 74 | 75 | 76 | writer.WriteObjectEnd(); 77 | } 78 | 79 | // Used both to load old format and current 80 | public void LoadFlowChoiceThreads(Dictionary jChoiceThreads, Story story) 81 | { 82 | foreach (var choice in currentChoices) { 83 | var foundActiveThread = callStack.ThreadWithIndex(choice.originalThreadIndex); 84 | if( foundActiveThread != null ) { 85 | choice.threadAtGeneration = foundActiveThread.Copy (); 86 | } else { 87 | var jSavedChoiceThread = (Dictionary ) jChoiceThreads[choice.originalThreadIndex.ToString()]; 88 | choice.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, story); 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_CommandLineInput.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink 3 | { 4 | public partial class InkParser 5 | { 6 | // Valid returned objects: 7 | // - "help" 8 | // - int: for choice number 9 | // - Parsed.Divert 10 | // - Variable declaration/assignment 11 | // - Epression 12 | // - Lookup debug source for character offset 13 | // - Lookup debug source for runtime path 14 | public CommandLineInput CommandLineUserInput() 15 | { 16 | CommandLineInput result = new CommandLineInput (); 17 | 18 | Whitespace (); 19 | 20 | if (ParseString ("help") != null) { 21 | result.isHelp = true; 22 | return result; 23 | } 24 | 25 | if (ParseString ("exit") != null || ParseString ("quit") != null) { 26 | result.isExit = true; 27 | return result; 28 | } 29 | 30 | return (CommandLineInput) OneOf ( 31 | DebugSource, 32 | DebugPathLookup, 33 | UserChoiceNumber, 34 | UserImmediateModeStatement 35 | ); 36 | } 37 | 38 | CommandLineInput DebugSource () 39 | { 40 | Whitespace (); 41 | 42 | if (ParseString ("DebugSource") == null) 43 | return null; 44 | 45 | Whitespace (); 46 | 47 | var expectMsg = "character offset in parentheses, e.g. DebugSource(5)"; 48 | if (Expect (String ("("), expectMsg) == null) 49 | return null; 50 | 51 | Whitespace (); 52 | 53 | int? characterOffset = ParseInt (); 54 | if (characterOffset == null) { 55 | Error (expectMsg); 56 | return null; 57 | } 58 | 59 | Whitespace (); 60 | 61 | Expect (String (")"), "closing parenthesis"); 62 | 63 | var inputStruct = new CommandLineInput (); 64 | inputStruct.debugSource = characterOffset; 65 | return inputStruct; 66 | } 67 | 68 | CommandLineInput DebugPathLookup () 69 | { 70 | Whitespace (); 71 | 72 | if (ParseString ("DebugPath") == null) 73 | return null; 74 | 75 | if (Whitespace () == null) 76 | return null; 77 | 78 | var pathStr = Expect (RuntimePath, "path") as string; 79 | 80 | var inputStruct = new CommandLineInput (); 81 | inputStruct.debugPathLookup = pathStr; 82 | return inputStruct; 83 | } 84 | 85 | string RuntimePath () 86 | { 87 | if (_runtimePathCharacterSet == null) { 88 | _runtimePathCharacterSet = new CharacterSet (identifierCharSet); 89 | _runtimePathCharacterSet.Add ('-'); // for c-0, g-0 etc 90 | _runtimePathCharacterSet.Add ('.'); 91 | 92 | } 93 | 94 | return ParseCharactersFromCharSet (_runtimePathCharacterSet); 95 | } 96 | 97 | CommandLineInput UserChoiceNumber() 98 | { 99 | Whitespace (); 100 | 101 | int? number = ParseInt (); 102 | if (number == null) { 103 | return null; 104 | } 105 | 106 | Whitespace (); 107 | 108 | if (Parse(EndOfLine) == null) { 109 | return null; 110 | } 111 | 112 | var inputStruct = new CommandLineInput (); 113 | inputStruct.choiceInput = number; 114 | return inputStruct; 115 | } 116 | 117 | CommandLineInput UserImmediateModeStatement() 118 | { 119 | var statement = OneOf (SingleDivert, TempDeclarationOrAssignment, Expression); 120 | 121 | var inputStruct = new CommandLineInput (); 122 | inputStruct.userImmediateModeStatement = statement; 123 | return inputStruct; 124 | } 125 | 126 | CharacterSet _runtimePathCharacterSet; 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /publish-nuget.ps1: -------------------------------------------------------------------------------- 1 | #!/bin/pwsh 2 | 3 | # do not bother with pull requests 4 | if ($env:TRAVIS_PULL_REQUEST -ne "false") { 5 | Write-Host "Skipping NuGet publish on pull request" 6 | exit 0 7 | } 8 | 9 | # set release branch to master by default 10 | $releaseBranch = $env:RELEASE_BRANCH 11 | if ("$releaseBranch" -eq "") { 12 | $releaseBranch = "master" 13 | } 14 | 15 | # check if we're building a tagged release build 16 | $tag = "$($env:TRAVIS_TAG)" 17 | if (("$($env:TRAVIS_BRANCH)" -ne $tag) -and ("$($env:TRAVIS_BRANCH)" -ne $releaseBranch)) { 18 | Write-Host "Skipping NuGet publish for the branch $($env:TRAVIS_BRANCH)" 19 | exit 0 20 | } 21 | 22 | # extract version number in case it has prefix (like v1.2.3) 23 | if (($tag -ne "") -and ($tag -match '\d+(\.\d+)+$')) { 24 | $tag = $Matches[0] 25 | } 26 | 27 | # delete existing nuget packages, although this should never happen on travis 28 | if ((Test-Path ink-engine-runtime/bin/Release) -and (@(Get-ChildItem ink-engine-runtime/bin/Release/*.nupkg).Count -gt 0)) { 29 | Remove-Item ink-engine-runtime/bin/Release/*.nupkg 30 | } 31 | 32 | if ($tag -eq "") { 33 | # if it's not a tagged build, then it's either a nightly or a master push 34 | $feedUrl = "$($env:NIGHTLY_FEED_SOURCE_URL)" 35 | $apiKey = "$($env:NIGHTLY_FEED_APIKEY)" 36 | if (($feedUrl -eq "") -or ($apiKey -eq "")) { 37 | Write-Host "Nightly feed is not set up, skipping pre-release build" 38 | exit 0 39 | } 40 | 41 | # check if it's a scheduled nightly build 42 | if ($env:TRAVIS_EVENT_TYPE -eq "cron") { 43 | # check if there were any commits in the last 24 hours 44 | $commits = "$(git log -1 --since=1.day --pretty=format:"%h %s")" 45 | $suffix = "" 46 | if ($commits -ne "") { 47 | Write-Host "Found commit: $commits" 48 | $timestamp = [DateTime]::UtcNow.ToString("yyMMddHH") 49 | $suffix = "nightly-$timestamp" 50 | } 51 | } 52 | elseif ("$($env:PUBLISH_MASTER_BUILDS)" -eq "true") { 53 | $timestamp = [DateTime]::UtcNow.ToString("yyMMddHHmmss") 54 | $suffix = "master-$timestamp" 55 | } 56 | if ("$suffix" -eq "") { 57 | Write-Host "Skipping publishing the pre-release build" 58 | exit 0 59 | } 60 | 61 | # get the latest tag in the branch history 62 | $tag = "$(git describe --tags $(git rev-list --tags --max-count=1))" 63 | # extract version number in case it has prefix (like v1.2.3) 64 | if (($tag -ne "") -and ($tag -match '\d+(\.\d+)+$')) { 65 | $tag = $Matches[0] 66 | } 67 | if ($tag -ne "") { 68 | Write-Host "Building a pre-release build $tag-$suffix..." 69 | dotnet pack -c Release /p:VersionPrefix=$tag --version-suffix "$suffix" ink-engine-runtime/ink-engine-runtime.csproj 70 | } else { 71 | Write-Host "Building a pre-release build $suffix..." 72 | dotnet pack -c Release --version-suffix "$suffix" ink-engine-runtime/ink-engine-runtime.csproj 73 | } 74 | if ($LASTEXITCODE -ne 0) { 75 | exit $LASTEXITCODE 76 | } 77 | 78 | Write-Host "Publishing pre-release build..." 79 | $packageName = @(Get-ChildItem ink-engine-runtime/bin/Release/*.nupkg)[0] 80 | } 81 | else { 82 | # tagged build will only happen on a tag push, so this means we're building a release package 83 | $feedUrl = "$($env:RELEASE_FEED_SOURCE_URL)" 84 | $apiKey = "$($env:RELEASE_FEED_APIKEY)" 85 | if (($feedUrl -eq "") -or ($apiKey -eq "")) { 86 | Write-Host "Release feed is not set up, skipping release build" 87 | exit 0 88 | } 89 | 90 | Write-Host "Building a release build..." 91 | dotnet pack -c Release /p:VersionPrefix=$tag ink-engine-runtime/ink-engine-runtime.csproj 92 | if ($LASTEXITCODE -ne 0) { 93 | exit $LASTEXITCODE 94 | } 95 | 96 | Write-Host "Publishing release build..." 97 | $packageName = @(Get-ChildItem ink-engine-runtime/bin/Release/*.nupkg)[0] 98 | } 99 | Write-Host "Pushing package $($packageName.FullName)..." 100 | dotnet nuget push $packageName.FullName --api-key $apiKey --source $feedUrl 101 | exit $LASTEXITCODE 102 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/ListDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Ink.Parsed 6 | { 7 | public class ListDefinition : Parsed.Object 8 | { 9 | public Identifier identifier; 10 | public List itemDefinitions; 11 | 12 | public VariableAssignment variableAssignment; 13 | 14 | public Runtime.ListDefinition runtimeListDefinition { 15 | get { 16 | var allItems = new Dictionary (); 17 | foreach (var e in itemDefinitions) { 18 | if( !allItems.ContainsKey(e.name) ) 19 | allItems.Add (e.name, e.seriesValue); 20 | else 21 | Error("List '"+identifier+"' contains dupicate items called '"+e.name+"'"); 22 | } 23 | 24 | return new Runtime.ListDefinition (identifier?.name, allItems); 25 | } 26 | } 27 | 28 | public ListElementDefinition ItemNamed (string itemName) 29 | { 30 | if (_elementsByName == null) { 31 | _elementsByName = new Dictionary (); 32 | foreach (var el in itemDefinitions) { 33 | _elementsByName [el.name] = el; 34 | } 35 | } 36 | 37 | ListElementDefinition foundElement; 38 | if (_elementsByName.TryGetValue (itemName, out foundElement)) 39 | return foundElement; 40 | 41 | return null; 42 | } 43 | 44 | public ListDefinition (List elements) 45 | { 46 | this.itemDefinitions = elements; 47 | 48 | int currentValue = 1; 49 | foreach (var e in this.itemDefinitions) { 50 | if (e.explicitValue != null) 51 | currentValue = e.explicitValue.Value; 52 | 53 | e.seriesValue = currentValue; 54 | 55 | currentValue++; 56 | } 57 | 58 | AddContent (elements); 59 | } 60 | 61 | public override Runtime.Object GenerateRuntimeObject () 62 | { 63 | var initialValues = new Runtime.InkList (); 64 | foreach (var itemDef in itemDefinitions) { 65 | if (itemDef.inInitialList) { 66 | var item = new Runtime.InkListItem (this.identifier?.name, itemDef.name); 67 | initialValues [item] = itemDef.seriesValue; 68 | } 69 | } 70 | 71 | // Set origin name, so 72 | initialValues.SetInitialOriginName (identifier?.name); 73 | 74 | return new Runtime.ListValue (initialValues); 75 | } 76 | 77 | public override void ResolveReferences (Story context) 78 | { 79 | base.ResolveReferences (context); 80 | 81 | context.CheckForNamingCollisions (this, identifier, Story.SymbolType.List); 82 | } 83 | 84 | public override string typeName { 85 | get { 86 | return "List definition"; 87 | } 88 | } 89 | 90 | Dictionary _elementsByName; 91 | } 92 | 93 | public class ListElementDefinition : Parsed.Object 94 | { 95 | public string name 96 | { 97 | get { return identifier?.name; } 98 | } 99 | public Identifier identifier; 100 | public int? explicitValue; 101 | public int seriesValue; 102 | public bool inInitialList; 103 | 104 | public string fullName { 105 | get { 106 | var parentList = parent as ListDefinition; 107 | if (parentList == null) 108 | throw new System.Exception ("Can't get full name without a parent list"); 109 | 110 | return parentList.identifier + "." + name; 111 | } 112 | } 113 | 114 | public ListElementDefinition (Identifier identifier, bool inInitialList, int? explicitValue = null) 115 | { 116 | this.identifier = identifier; 117 | this.inInitialList = inInitialList; 118 | this.explicitValue = explicitValue; 119 | } 120 | 121 | public override Runtime.Object GenerateRuntimeObject () 122 | { 123 | throw new System.NotImplementedException (); 124 | } 125 | 126 | public override void ResolveReferences (Story context) 127 | { 128 | base.ResolveReferences (context); 129 | 130 | context.CheckForNamingCollisions (this, identifier, Story.SymbolType.ListItem); 131 | } 132 | 133 | public override string typeName { 134 | get { 135 | return "List element"; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Autosave files 2 | *~ 3 | 4 | # Build results 5 | [Dd]ebug/ 6 | [Dd]ebugPublic/ 7 | [Rr]elease/ 8 | [Rr]eleases/ 9 | x64/ 10 | x86/ 11 | bld/ 12 | [Bb]in/ 13 | [Oo]bj/ 14 | [Ll]og/ 15 | 16 | 17 | packages/ 18 | 19 | 20 | # globs 21 | Makefile.in 22 | *.DS_Store 23 | *.sln.cache 24 | *.suo 25 | *.cache 26 | *.pidb 27 | *.userprefs 28 | *.usertasks 29 | config.log 30 | config.make 31 | config.status 32 | aclocal.m4 33 | install-sh 34 | autom4te.cache/ 35 | *.user 36 | *.tar.gz 37 | tarballs/ 38 | test-results/ 39 | Thumbs.db 40 | 41 | #Mac bundle stuff 42 | *.dmg 43 | *.app 44 | 45 | #resharper 46 | *_Resharper.* 47 | *.Resharper 48 | 49 | #dotCover 50 | *.dotCover 51 | 52 | ReleaseBinary 53 | BuildForInky 54 | PerformanceReports 55 | RuntimeDLL 56 | 57 | 58 | 59 | # Visual Studio 2015 cache/options directory 60 | .vs/ 61 | # Uncomment if you have tasks that create the project's static files in wwwroot 62 | #wwwroot/ 63 | 64 | # MSTest test Results 65 | [Tt]est[Rr]esult*/ 66 | [Bb]uild[Ll]og.* 67 | 68 | # NUNIT 69 | *.VisualState.xml 70 | TestResult.xml 71 | 72 | # Build Results of an ATL Project 73 | [Dd]ebugPS/ 74 | [Rr]eleasePS/ 75 | dlldata.c 76 | 77 | # DNX 78 | project.lock.json 79 | artifacts/ 80 | 81 | *_i.c 82 | *_p.c 83 | *_i.h 84 | *.ilk 85 | *.meta 86 | *.obj 87 | *.pch 88 | *.pdb 89 | *.pgc 90 | *.pgd 91 | *.rsp 92 | *.sbr 93 | *.tlb 94 | *.tli 95 | *.tlh 96 | *.tmp 97 | *.tmp_proj 98 | *.log 99 | *.vspscc 100 | *.vssscc 101 | .builds 102 | *.pidb 103 | *.svclog 104 | *.scc 105 | 106 | # Chutzpah Test files 107 | _Chutzpah* 108 | 109 | # Visual C++ cache files 110 | ipch/ 111 | *.aps 112 | *.ncb 113 | *.opendb 114 | *.opensdf 115 | *.sdf 116 | *.cachefile 117 | *.VC.db 118 | *.VC.VC.opendb 119 | 120 | # Visual Studio profiler 121 | *.psess 122 | *.vsp 123 | *.vspx 124 | *.sap 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # JustCode is a .NET coding add-in 138 | .JustCode 139 | 140 | # TeamCity is a build add-in 141 | _TeamCity* 142 | 143 | # DotCover is a Code Coverage Tool 144 | *.dotCover 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # TODO: Comment the next line if you want to checkin your web deploy settings 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | 188 | 189 | # Microsoft Azure Build Output 190 | csx/ 191 | *.build.csdef 192 | 193 | # Microsoft Azure Emulator 194 | ecf/ 195 | rcf/ 196 | 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | 204 | # Visual Studio cache files 205 | # files ending in .cache can be ignored 206 | *.[Cc]ache 207 | # but keep track of directories ending in .cache 208 | !*.[Cc]ache/ 209 | 210 | # Others 211 | ClientBin/ 212 | ~$* 213 | *~ 214 | *.dbmdl 215 | *.dbproj.schemaview 216 | *.pfx 217 | *.publishsettings 218 | node_modules/ 219 | orleans.codegen.cs 220 | 221 | 222 | # Backup & report files from converting an old project file 223 | # to a newer Visual Studio version. Backup files are not needed, 224 | # because we have git ;-) 225 | _UpgradeReport_Files/ 226 | Backup*/ 227 | UpgradeLog*.XML 228 | UpgradeLog*.htm 229 | 230 | # SQL Server files 231 | *.mdf 232 | *.ldf 233 | 234 | # Business Intelligence projects 235 | *.rdl.data 236 | *.bim.layout 237 | *.bim_*.settings 238 | 239 | # Microsoft Fakes 240 | FakesAssemblies/ 241 | 242 | # GhostDoc plugin setting file 243 | *.GhostDoc.xml 244 | 245 | # Node.js Tools for Visual Studio 246 | .ntvs_analysis.dat 247 | 248 | # Visual Studio 6 build log 249 | *.plg 250 | 251 | # Visual Studio 6 workspace options file 252 | *.opt 253 | 254 | # Visual Studio LightSwitch build output 255 | **/*.HTMLClient/GeneratedArtifacts 256 | **/*.DesktopClient/GeneratedArtifacts 257 | **/*.DesktopClient/ModelManifest.xml 258 | **/*.Server/GeneratedArtifacts 259 | **/*.Server/ModelManifest.xml 260 | _Pvt_Extensions 261 | 262 | # Paket dependency manager 263 | .paket/paket.exe 264 | paket-files/ 265 | 266 | # FAKE - F# Make 267 | .fake/ 268 | 269 | # JetBrains Rider 270 | .idea/ 271 | *.sln.iml 272 | .ionide/ 273 | -------------------------------------------------------------------------------- /ink-engine-runtime/Divert.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class Divert : Runtime.Object 6 | { 7 | public Path targetPath { 8 | get { 9 | // Resolve any relative paths to global ones as we come across them 10 | if (_targetPath != null && _targetPath.isRelative) { 11 | var targetObj = targetPointer.Resolve(); 12 | if (targetObj) { 13 | _targetPath = targetObj.path; 14 | } 15 | } 16 | return _targetPath; 17 | } 18 | set { 19 | _targetPath = value; 20 | _targetPointer = Pointer.Null; 21 | } 22 | } 23 | Path _targetPath; 24 | 25 | public Pointer targetPointer { 26 | get { 27 | if (_targetPointer.isNull) { 28 | var targetObj = ResolvePath (_targetPath).obj; 29 | 30 | if (_targetPath.lastComponent.isIndex) { 31 | _targetPointer.container = targetObj.parent as Container; 32 | _targetPointer.index = _targetPath.lastComponent.index; 33 | } else { 34 | _targetPointer = Pointer.StartOf (targetObj as Container); 35 | } 36 | } 37 | return _targetPointer; 38 | } 39 | } 40 | Pointer _targetPointer; 41 | 42 | 43 | public string targetPathString { 44 | get { 45 | if (targetPath == null) 46 | return null; 47 | 48 | return CompactPathString (targetPath); 49 | } 50 | set { 51 | if (value == null) { 52 | targetPath = null; 53 | } else { 54 | targetPath = new Path (value); 55 | } 56 | } 57 | } 58 | 59 | public string variableDivertName { get; set; } 60 | public bool hasVariableTarget { get { return variableDivertName != null; } } 61 | 62 | public bool pushesToStack { get; set; } 63 | public PushPopType stackPushType; 64 | 65 | public bool isExternal { get; set; } 66 | public int externalArgs { get; set; } 67 | 68 | public bool isConditional { get; set; } 69 | 70 | public Divert () 71 | { 72 | pushesToStack = false; 73 | } 74 | 75 | public Divert(PushPopType stackPushType) 76 | { 77 | pushesToStack = true; 78 | this.stackPushType = stackPushType; 79 | } 80 | 81 | public override bool Equals (object obj) 82 | { 83 | var otherDivert = obj as Divert; 84 | if (otherDivert) { 85 | if (this.hasVariableTarget == otherDivert.hasVariableTarget) { 86 | if (this.hasVariableTarget) { 87 | return this.variableDivertName == otherDivert.variableDivertName; 88 | } else { 89 | return this.targetPath.Equals(otherDivert.targetPath); 90 | } 91 | } 92 | } 93 | return false; 94 | } 95 | 96 | public override int GetHashCode () 97 | { 98 | if (hasVariableTarget) { 99 | const int variableTargetSalt = 12345; 100 | return variableDivertName.GetHashCode() + variableTargetSalt; 101 | } else { 102 | const int pathTargetSalt = 54321; 103 | return targetPath.GetHashCode() + pathTargetSalt; 104 | } 105 | } 106 | 107 | public override string ToString () 108 | { 109 | if (hasVariableTarget) { 110 | return "Divert(variable: " + variableDivertName + ")"; 111 | } 112 | else if (targetPath == null) { 113 | return "Divert(null)"; 114 | } else { 115 | 116 | var sb = new StringBuilder (); 117 | 118 | string targetStr = targetPath.ToString (); 119 | int? targetLineNum = DebugLineNumberOfPath (targetPath); 120 | if (targetLineNum != null) { 121 | targetStr = "line " + targetLineNum; 122 | } 123 | 124 | sb.Append ("Divert"); 125 | 126 | if (isConditional) 127 | sb.Append ("?"); 128 | 129 | if (pushesToStack) { 130 | if (stackPushType == PushPopType.Function) { 131 | sb.Append (" function"); 132 | } else { 133 | sb.Append (" tunnel"); 134 | } 135 | } 136 | 137 | sb.Append (" -> "); 138 | sb.Append (targetPathString); 139 | 140 | sb.Append (" ("); 141 | sb.Append (targetStr); 142 | sb.Append (")"); 143 | 144 | return sb.ToString (); 145 | } 146 | } 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/VariableAssignment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ink.Parsed 4 | { 5 | public class VariableAssignment : Parsed.Object 6 | { 7 | public string variableName 8 | { 9 | get { return variableIdentifier.name; } 10 | } 11 | public Identifier variableIdentifier { get; protected set; } 12 | public Expression expression { get; protected set; } 13 | public ListDefinition listDefinition { get; protected set; } 14 | 15 | public bool isGlobalDeclaration { get; set; } 16 | public bool isNewTemporaryDeclaration { get; set; } 17 | 18 | public bool isDeclaration { 19 | get { 20 | return isGlobalDeclaration || isNewTemporaryDeclaration; 21 | } 22 | } 23 | 24 | public VariableAssignment (Identifier identifier, Expression assignedExpression) 25 | { 26 | this.variableIdentifier = identifier; 27 | 28 | // Defensive programming in case parsing of assignedExpression failed 29 | if( assignedExpression ) 30 | this.expression = AddContent(assignedExpression); 31 | } 32 | 33 | public VariableAssignment (Identifier identifier, ListDefinition listDef) 34 | { 35 | this.variableIdentifier = identifier; 36 | 37 | if (listDef) { 38 | this.listDefinition = AddContent (listDef); 39 | this.listDefinition.variableAssignment = this; 40 | } 41 | 42 | // List definitions are always global 43 | isGlobalDeclaration = true; 44 | } 45 | 46 | public override Runtime.Object GenerateRuntimeObject () 47 | { 48 | FlowBase newDeclScope = null; 49 | if (isGlobalDeclaration) { 50 | newDeclScope = story; 51 | } else if(isNewTemporaryDeclaration) { 52 | newDeclScope = ClosestFlowBase (); 53 | } 54 | 55 | if( newDeclScope ) 56 | newDeclScope.TryAddNewVariableDeclaration (this); 57 | 58 | // Global declarations don't generate actual procedural 59 | // runtime objects, but instead add a global variable to the story itself. 60 | // The story then initialises them all in one go at the start of the game. 61 | if( isGlobalDeclaration ) 62 | return null; 63 | 64 | var container = new Runtime.Container (); 65 | 66 | // The expression's runtimeObject is actually another nested container 67 | if( expression != null ) 68 | container.AddContent (expression.runtimeObject); 69 | else if( listDefinition != null ) 70 | container.AddContent (listDefinition.runtimeObject); 71 | 72 | _runtimeAssignment = new Runtime.VariableAssignment(variableName, isNewTemporaryDeclaration); 73 | container.AddContent (_runtimeAssignment); 74 | 75 | return container; 76 | } 77 | 78 | public override void ResolveReferences (Story context) 79 | { 80 | base.ResolveReferences (context); 81 | 82 | // List definitions are checked for conflicts separately 83 | if( this.isDeclaration && listDefinition == null ) 84 | context.CheckForNamingCollisions (this, variableIdentifier, this.isGlobalDeclaration ? Story.SymbolType.Var : Story.SymbolType.Temp); 85 | 86 | // Initial VAR x = [intialValue] declaration, not re-assignment 87 | if (this.isGlobalDeclaration) { 88 | var variableReference = expression as VariableReference; 89 | if (variableReference && !variableReference.isConstantReference && !variableReference.isListItemReference) { 90 | Error ("global variable assignments cannot refer to other variables, only literal values, constants and list items"); 91 | } 92 | } 93 | 94 | if (!this.isNewTemporaryDeclaration) { 95 | var resolvedVarAssignment = context.ResolveVariableWithName(this.variableName, fromNode: this); 96 | if (!resolvedVarAssignment.found) { 97 | if (story.constants.ContainsKey (variableName)) { 98 | Error ("Can't re-assign to a constant (do you need to use VAR when declaring '" + this.variableName + "'?)", this); 99 | } else { 100 | Error ("Variable could not be found to assign to: '" + this.variableName + "'", this); 101 | } 102 | } 103 | 104 | // A runtime assignment may not have been generated if it's the initial global declaration, 105 | // since these are hoisted out and handled specially in Story.ExportRuntime. 106 | if( _runtimeAssignment != null ) 107 | _runtimeAssignment.isGlobal = resolvedVarAssignment.isGlobal; 108 | } 109 | } 110 | 111 | 112 | public override string typeName { 113 | get { 114 | if (isNewTemporaryDeclaration) return "temp"; 115 | else if (isGlobalDeclaration) return "VAR"; 116 | else return "variable assignment"; 117 | } 118 | } 119 | 120 | Runtime.VariableAssignment _runtimeAssignment; 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /ink-engine-runtime/ControlCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ink.Runtime 4 | { 5 | public class ControlCommand : Runtime.Object 6 | { 7 | public enum CommandType 8 | { 9 | NotSet = -1, 10 | EvalStart, 11 | EvalOutput, 12 | EvalEnd, 13 | Duplicate, 14 | PopEvaluatedValue, 15 | PopFunction, 16 | PopTunnel, 17 | BeginString, 18 | EndString, 19 | NoOp, 20 | ChoiceCount, 21 | Turns, 22 | TurnsSince, 23 | ReadCount, 24 | Random, 25 | SeedRandom, 26 | VisitIndex, 27 | SequenceShuffleIndex, 28 | StartThread, 29 | Done, 30 | End, 31 | ListFromInt, 32 | ListRange, 33 | ListRandom, 34 | //---- 35 | TOTAL_VALUES 36 | } 37 | 38 | public CommandType commandType { get; protected set; } 39 | 40 | public ControlCommand (CommandType commandType) 41 | { 42 | this.commandType = commandType; 43 | } 44 | 45 | // Require default constructor for serialisation 46 | public ControlCommand() : this(CommandType.NotSet) {} 47 | 48 | public override Object Copy() 49 | { 50 | return new ControlCommand (commandType); 51 | } 52 | 53 | // The following static factory methods are to make generating these objects 54 | // slightly more succinct. Without these, the code gets pretty massive! e.g. 55 | // 56 | // var c = new Runtime.ControlCommand(Runtime.ControlCommand.CommandType.EvalStart) 57 | // 58 | // as opposed to 59 | // 60 | // var c = Runtime.ControlCommand.EvalStart() 61 | 62 | public static ControlCommand EvalStart() { 63 | return new ControlCommand(CommandType.EvalStart); 64 | } 65 | 66 | public static ControlCommand EvalOutput() { 67 | return new ControlCommand(CommandType.EvalOutput); 68 | } 69 | 70 | public static ControlCommand EvalEnd() { 71 | return new ControlCommand(CommandType.EvalEnd); 72 | } 73 | 74 | public static ControlCommand Duplicate() { 75 | return new ControlCommand(CommandType.Duplicate); 76 | } 77 | 78 | public static ControlCommand PopEvaluatedValue() { 79 | return new ControlCommand (CommandType.PopEvaluatedValue); 80 | } 81 | 82 | public static ControlCommand PopFunction() { 83 | return new ControlCommand (CommandType.PopFunction); 84 | } 85 | 86 | public static ControlCommand PopTunnel() { 87 | return new ControlCommand (CommandType.PopTunnel); 88 | } 89 | 90 | public static ControlCommand BeginString() { 91 | return new ControlCommand (CommandType.BeginString); 92 | } 93 | 94 | public static ControlCommand EndString() { 95 | return new ControlCommand (CommandType.EndString); 96 | } 97 | 98 | public static ControlCommand NoOp() { 99 | return new ControlCommand(CommandType.NoOp); 100 | } 101 | 102 | public static ControlCommand ChoiceCount() { 103 | return new ControlCommand(CommandType.ChoiceCount); 104 | } 105 | 106 | public static ControlCommand Turns () 107 | { 108 | return new ControlCommand (CommandType.Turns); 109 | } 110 | 111 | public static ControlCommand TurnsSince() { 112 | return new ControlCommand(CommandType.TurnsSince); 113 | } 114 | 115 | public static ControlCommand ReadCount () 116 | { 117 | return new ControlCommand (CommandType.ReadCount); 118 | } 119 | 120 | public static ControlCommand Random () 121 | { 122 | return new ControlCommand (CommandType.Random); 123 | } 124 | 125 | public static ControlCommand SeedRandom () 126 | { 127 | return new ControlCommand (CommandType.SeedRandom); 128 | } 129 | 130 | public static ControlCommand VisitIndex() { 131 | return new ControlCommand(CommandType.VisitIndex); 132 | } 133 | 134 | public static ControlCommand SequenceShuffleIndex() { 135 | return new ControlCommand(CommandType.SequenceShuffleIndex); 136 | } 137 | 138 | public static ControlCommand StartThread() { 139 | return new ControlCommand (CommandType.StartThread); 140 | } 141 | 142 | public static ControlCommand Done() { 143 | return new ControlCommand (CommandType.Done); 144 | } 145 | 146 | public static ControlCommand End() { 147 | return new ControlCommand (CommandType.End); 148 | } 149 | 150 | public static ControlCommand ListFromInt () { 151 | return new ControlCommand (CommandType.ListFromInt); 152 | } 153 | 154 | public static ControlCommand ListRange () 155 | { 156 | return new ControlCommand (CommandType.ListRange); 157 | } 158 | 159 | public static ControlCommand ListRandom () 160 | { 161 | return new ControlCommand (CommandType.ListRandom); 162 | } 163 | 164 | public override string ToString () 165 | { 166 | return commandType.ToString(); 167 | } 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /compiler/StringParser/StringParserState.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink 3 | { 4 | public class StringParserState 5 | { 6 | public int lineIndex { 7 | get { return currentElement.lineIndex; } 8 | set { currentElement.lineIndex = value; } 9 | } 10 | 11 | public int characterIndex { 12 | get { return currentElement.characterIndex; } 13 | set { currentElement.characterIndex = value; } 14 | } 15 | 16 | public int characterInLineIndex { 17 | get { return currentElement.characterInLineIndex; } 18 | set { currentElement.characterInLineIndex = value; } 19 | } 20 | 21 | public uint customFlags { 22 | get { return currentElement.customFlags; } 23 | set { currentElement.customFlags = value; } 24 | } 25 | 26 | public bool errorReportedAlreadyInScope { 27 | get { 28 | return currentElement.reportedErrorInScope; 29 | } 30 | } 31 | 32 | public int stackHeight { 33 | get { 34 | return _numElements; 35 | } 36 | } 37 | 38 | public class Element { 39 | public int characterIndex; 40 | public int characterInLineIndex; 41 | public int lineIndex; 42 | public bool reportedErrorInScope; 43 | public int uniqueId; 44 | public uint customFlags; 45 | 46 | public Element() { 47 | 48 | } 49 | 50 | public void CopyFrom(Element fromElement) 51 | { 52 | _uniqueIdCounter++; 53 | this.uniqueId = _uniqueIdCounter; 54 | this.characterIndex = fromElement.characterIndex; 55 | this.characterInLineIndex = fromElement.characterInLineIndex; 56 | this.lineIndex = fromElement.lineIndex; 57 | this.customFlags = fromElement.customFlags; 58 | this.reportedErrorInScope = false; 59 | } 60 | 61 | // Squash is used when succeeding from a rule, 62 | // so only the state information we wanted to carry forward is 63 | // retained. e.g. characterIndex and lineIndex are global, 64 | // however uniqueId is specific to the individual rule, 65 | // and likewise, custom flags are designed for the temporary 66 | // state of the individual rule too. 67 | public void SquashFrom(Element fromElement) 68 | { 69 | this.characterIndex = fromElement.characterIndex; 70 | this.characterInLineIndex = fromElement.characterInLineIndex; 71 | this.lineIndex = fromElement.lineIndex; 72 | this.reportedErrorInScope = fromElement.reportedErrorInScope; 73 | } 74 | 75 | static int _uniqueIdCounter; 76 | } 77 | 78 | public StringParserState () 79 | { 80 | const int kExpectedMaxStackDepth = 200; 81 | _stack = new Element[kExpectedMaxStackDepth]; 82 | 83 | for (int i = 0; i < kExpectedMaxStackDepth; ++i) { 84 | _stack [i] = new Element (); 85 | } 86 | 87 | _numElements = 1; 88 | } 89 | 90 | public int Push() 91 | { 92 | if (_numElements >= _stack.Length) 93 | throw new System.Exception ("Stack overflow in parser state"); 94 | 95 | var prevElement = _stack [_numElements - 1]; 96 | var newElement = _stack[_numElements]; 97 | _numElements++; 98 | 99 | newElement.CopyFrom (prevElement); 100 | 101 | return newElement.uniqueId; 102 | } 103 | 104 | public void Pop(int expectedRuleId) 105 | { 106 | if (_numElements == 1) { 107 | throw new System.Exception ("Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?"); 108 | } 109 | 110 | if ( currentElement.uniqueId != expectedRuleId) 111 | throw new System.Exception ("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?"); 112 | 113 | // Restore state 114 | _numElements--; 115 | } 116 | 117 | public Element Peek(int expectedRuleId) 118 | { 119 | if (currentElement.uniqueId != expectedRuleId) 120 | throw new System.Exception ("Mismatched rule IDs - do you have mismatched Begin/Succeed/Fail?"); 121 | 122 | return _stack[_numElements-1]; 123 | } 124 | 125 | public Element PeekPenultimate() 126 | { 127 | if (_numElements >= 2) { 128 | return _stack [_numElements - 2]; 129 | } else { 130 | return null; 131 | } 132 | } 133 | 134 | // Reduce stack height while maintaining currentElement 135 | // Remove second last element: i.e. "squash last two elements together" 136 | // Used when succeeding from a rule (and ONLY when succeeding, since 137 | // the state of the top element is retained). 138 | public void Squash() 139 | { 140 | if (_numElements < 2) { 141 | throw new System.Exception ("Attempting to remove final stack element is illegal! Mismatched Begin/Succceed/Fail?"); 142 | } 143 | 144 | var penultimateEl = _stack [_numElements - 2]; 145 | var lastEl = _stack [_numElements - 1]; 146 | 147 | penultimateEl.SquashFrom (lastEl); 148 | 149 | _numElements--; 150 | } 151 | 152 | public void NoteErrorReported() 153 | { 154 | foreach (var el in _stack) { 155 | el.reportedErrorInScope = true; 156 | } 157 | } 158 | 159 | protected Element currentElement 160 | { 161 | get { 162 | return _stack [_numElements - 1]; 163 | } 164 | } 165 | 166 | private Element[] _stack; 167 | private int _numElements; 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | author 16 | Joseph Humfrey, inkle 17 | name 18 | ink light theme 19 | comment 20 | https://github.com/inkle/ink 21 | semanticClass 22 | theme.light.ink 23 | colorSpaceName 24 | sRGB 25 | settings 26 | 27 | 28 | settings 29 | 30 | background 31 | #ffffff 32 | caret 33 | #4a4543 34 | foreground 35 | #4a4543 36 | invisibles 37 | #d6d5d4 38 | lineHighlight 39 | #d6d5d4 40 | selection 41 | #d6d5d4 42 | 43 | 44 | 45 | 46 | 47 | name 48 | Knot and stitch declarations 49 | scope 50 | meta.knot.declaration, meta.stitch.declaration 51 | settings 52 | 53 | fontStyle 54 | bold 55 | foreground 56 | #11D 57 | 58 | 59 | 60 | 61 | name 62 | Weave punctuation - choices, gather bullets and brackets 63 | scope 64 | keyword.operator.weaveBullet, keyword.operator.weaveBracket 65 | settings 66 | 67 | foreground 68 | #cc0000 69 | 70 | 71 | 72 | 73 | name 74 | Label for choice or gather 75 | scope 76 | meta.label, entity.name.label 77 | settings 78 | 79 | foreground 80 | #11D 81 | 82 | 83 | 84 | 85 | name 86 | Divert 87 | scope 88 | meta.divert, keyword.operator.divert, variable.divertTarget 89 | settings 90 | 91 | foreground 92 | #11D 93 | 94 | 95 | 96 | 97 | name 98 | General logic 99 | scope 100 | meta.logic, keyword.operator, meta.multilineLogic, meta.logicBegin, entity.inlineConditional 101 | settings 102 | 103 | foreground 104 | #228b22 105 | 106 | 107 | 108 | 109 | name 110 | Global 111 | scope 112 | meta.variable 113 | settings 114 | 115 | foreground 116 | #44D 117 | 118 | 119 | 120 | 121 | name 122 | TODO 123 | scope 124 | comment.todo 125 | settings 126 | 127 | foreground 128 | #F00 129 | 130 | 131 | 132 | 133 | name 134 | TODO 135 | scope 136 | comment.todo.TODO 137 | settings 138 | 139 | background 140 | #FDD 141 | 142 | 143 | 144 | 145 | name 146 | Comments 147 | scope 148 | comment, punctuation.definition.comment 149 | settings 150 | 151 | foreground 152 | #888 153 | 154 | 155 | 156 | 157 | name 158 | Operators 159 | scope 160 | keyword.done, keyword.end 161 | settings 162 | 163 | foreground 164 | #00F 165 | 166 | 167 | 168 | 169 | name 170 | Bold-italic text 171 | scope 172 | string.boldItalic 173 | settings 174 | 175 | fontStyle 176 | bold, italic 177 | foreground 178 | #000 179 | 180 | 181 | 182 | 183 | name 184 | Bold text 185 | scope 186 | string.bold 187 | settings 188 | 189 | fontStyle 190 | bold 191 | foreground 192 | #000 193 | 194 | 195 | 196 | 197 | 198 | name 199 | Italic text 200 | scope 201 | string.italic 202 | settings 203 | 204 | fontStyle 205 | italic 206 | 207 | 208 | 209 | 210 | 211 | name 212 | Main content 213 | scope 214 | string 215 | settings 216 | 217 | foreground 218 | #000 219 | 220 | 221 | 222 | 223 | uuid 224 | 14875ac8-6a02-493d-9cfa-1701c764e24b 225 | 226 | 227 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink-dark.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | author 17 | Joseph Humfrey, inkle 18 | name 19 | ink dark theme 20 | comment 21 | https://github.com/inkle/ink 22 | semanticClass 23 | theme.dark.ink 24 | colorSpaceName 25 | sRGB 26 | settings 27 | 28 | 29 | settings 30 | 31 | background 32 | #282828 33 | caret 34 | #F8F8F0 35 | foreground 36 | #F8F8F2 37 | invisibles 38 | #49483E 39 | lineHighlight 40 | #49483E 41 | selection 42 | #49483E 43 | 44 | 45 | 46 | 47 | 48 | name 49 | Knot and stitch declarations 50 | scope 51 | meta.knot.declaration, meta.stitch.declaration 52 | settings 53 | 54 | fontStyle 55 | bold 56 | foreground 57 | #66D9EF 58 | 59 | 60 | 61 | 62 | name 63 | Weave punctuation - choices, gather bullets and brackets 64 | scope 65 | keyword.operator.weaveBullet, keyword.operator.weaveBracket 66 | settings 67 | 68 | foreground 69 | #F92672 70 | 71 | 72 | 73 | 74 | name 75 | Label for choice or gather 76 | scope 77 | meta.label, entity.name.label 78 | settings 79 | 80 | foreground 81 | #66D9EF 82 | 83 | 84 | 85 | 86 | name 87 | Divert 88 | scope 89 | meta.divert, keyword.operator.divert, variable.divertTarget 90 | settings 91 | 92 | foreground 93 | #66D9EF 94 | 95 | 96 | 97 | 98 | name 99 | General logic 100 | scope 101 | meta.logic, keyword.operator, meta.multilineLogic, meta.logicBegin, entity.inlineConditional 102 | settings 103 | 104 | foreground 105 | #A6E22E 106 | 107 | 108 | 109 | 110 | name 111 | Global 112 | scope 113 | meta.variable 114 | settings 115 | 116 | foreground 117 | #AE81FF 118 | 119 | 120 | 121 | 122 | name 123 | TODO 124 | scope 125 | comment.todo 126 | settings 127 | 128 | foreground 129 | #FD971F 130 | 131 | 132 | 133 | 134 | name 135 | TODO 136 | scope 137 | comment.todo.TODO 138 | settings 139 | 140 | background 141 | #FFD569 142 | 143 | 144 | 145 | 146 | name 147 | Comments 148 | scope 149 | comment, punctuation.definition.comment 150 | settings 151 | 152 | foreground 153 | #888 154 | 155 | 156 | 157 | 158 | name 159 | Operators 160 | scope 161 | keyword.done, keyword.end 162 | settings 163 | 164 | foreground 165 | #66D9EF 166 | 167 | 168 | 169 | 170 | name 171 | Bold-italic text 172 | scope 173 | string.boldItalic 174 | settings 175 | 176 | fontStyle 177 | bold, italic 178 | foreground 179 | #FFF 180 | 181 | 182 | 183 | 184 | name 185 | Bold text 186 | scope 187 | string.bold 188 | settings 189 | 190 | fontStyle 191 | bold 192 | foreground 193 | #FFF 194 | 195 | 196 | 197 | 198 | 199 | name 200 | Italic text 201 | scope 202 | string.italic 203 | settings 204 | 205 | fontStyle 206 | italic 207 | 208 | 209 | 210 | 211 | 212 | name 213 | Main content 214 | scope 215 | string 216 | settings 217 | 218 | foreground 219 | #fff 220 | 221 | 222 | 223 | 224 | uuid 225 | 14875ac8-6a02-493d-9cfa-1701c764e24b 226 | 227 | 228 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | // subtasks that are not shown in GUI 5 | { 6 | "label": "Create inklecate build for Windows", 7 | "command": "dotnet", 8 | "type": "shell", 9 | "args": [ 10 | "publish", 11 | "--configuration", "Release", 12 | "--runtime", "win-x86", 13 | "/p:PublishTrimmed=true", 14 | "/p:PublishSingleFile=true", 15 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate-win32", 16 | "${workspaceFolder}/inklecate/inklecate.csproj" 17 | ], 18 | "problemMatcher": "$msCompile", 19 | "group": "none" 20 | }, 21 | { 22 | "label": "Create inklecate build for Linux", 23 | "dependsOn": [ "Create inklecate build for Windows" ], 24 | "command": "dotnet", 25 | "type": "shell", 26 | "args": [ 27 | "publish", 28 | "--configuration", "Release", 29 | "--runtime", "linux-x64", 30 | "/p:PublishTrimmed=true", 31 | "/p:PublishSingleFile=true", 32 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate-lin64", 33 | "${workspaceFolder}/inklecate/inklecate.csproj" 34 | ], 35 | "problemMatcher": "$msCompile", 36 | "group": "none" 37 | }, 38 | { 39 | "label": "Create inklecate build for OSX", 40 | "dependsOn": [ "Create inklecate build for Linux" ], 41 | "command": "dotnet", 42 | "type": "shell", 43 | "args": [ 44 | "publish", 45 | "--configuration", "Release", 46 | "--runtime", "osx-x64", 47 | "/p:PublishTrimmed=true", 48 | "/p:PublishSingleFile=true", 49 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate-osx64", 50 | "${workspaceFolder}/inklecate/inklecate.csproj" 51 | ], 52 | "problemMatcher": "$msCompile", 53 | "group": "none" 54 | }, 55 | { 56 | "label": "Build ink-engine-runtime", 57 | "command": "dotnet", 58 | "type": "shell", 59 | "args": [ 60 | "build", 61 | "--configuration", "Release", 62 | "${workspaceFolder}/ink-engine-runtime/ink-engine-runtime.csproj" 63 | ], 64 | "problemMatcher": "$msCompile", 65 | "group": "none" 66 | }, 67 | // use Ctrl+Shift+B to select a build task 68 | { 69 | "label": "Create inklecate release", 70 | "type": "shell", 71 | "problemMatcher": "$msCompile", 72 | "group": "build", 73 | // build executables for every platform 74 | "dependsOn": [ "Create inklecate build for OSX" ], 75 | // rebuild project for current platform so that intellisense won't complain 76 | "windows": { 77 | "command": "dotnet", 78 | "args": [ 79 | "publish", 80 | "--configuration", "Release", 81 | "--runtime", "win-x64", 82 | "/p:PublishTrimmed=true", 83 | "/p:PublishSingleFile=true", 84 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate", 85 | "${workspaceFolder}/inklecate/inklecate.csproj" 86 | ] 87 | }, 88 | "linux": { 89 | "command": "dotnet", 90 | "args": [ 91 | "publish", 92 | "--configuration", "Release", 93 | "--runtime", "linux-x64", 94 | "/p:PublishTrimmed=true", 95 | "/p:PublishSingleFile=true", 96 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate", 97 | "${workspaceFolder}/inklecate/inklecate.csproj" 98 | ] 99 | }, 100 | "osx": { 101 | "command": "dotnet", 102 | "args": [ 103 | "publish", 104 | "--configuration", "Release", 105 | "--runtime", "osx-x64", 106 | "/p:PublishTrimmed=true", 107 | "/p:PublishSingleFile=true", 108 | "--output", "${workspaceFolder}/ReleaseBinary/inklecate", 109 | "${workspaceFolder}/inklecate/inklecate.csproj" 110 | ] 111 | } 112 | }, 113 | { 114 | "label": "Create ink-engine-runtime NuGet package", 115 | "type": "shell", 116 | "problemMatcher": "$msCompile", 117 | "group": "build", 118 | "dependsOn": [ "Build ink-engine-runtime" ], 119 | "command": "dotnet", 120 | "args": [ 121 | "pack", 122 | "--configuration", "Release", 123 | "--output", "${workspaceFolder}/ReleaseBinary", 124 | "${workspaceFolder}/ink-engine-runtime/ink-engine-runtime.csproj" 125 | ] 126 | }, 127 | { 128 | "label": "Run tests", 129 | "command": "dotnet", 130 | "type": "shell", 131 | "args": [ 132 | "test", 133 | "${workspaceFolder}/tests/tests.csproj" 134 | ], 135 | "problemMatcher": "$msCompile", 136 | "group": { 137 | "kind": "test", 138 | "isDefault": true 139 | } 140 | }, 141 | { 142 | "label": "Build InkTestBed", 143 | "type": "shell", 144 | "problemMatcher": "$msCompile", 145 | "command": "dotnet", 146 | "args": [ 147 | "build", 148 | "--configuration", "Debug", 149 | "${workspaceFolder}/InkTestBed/InkTestBed.csproj" 150 | ], 151 | "group": "none" 152 | }, 153 | ] 154 | } -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_Statements.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Ink.Parsed; 5 | 6 | namespace Ink 7 | { 8 | public partial class InkParser 9 | { 10 | protected enum StatementLevel 11 | { 12 | InnerBlock, 13 | Stitch, 14 | Knot, 15 | Top 16 | } 17 | 18 | protected List StatementsAtLevel(StatementLevel level) 19 | { 20 | // Check for error: Should not be allowed gather dashes within an inner block 21 | if (level == StatementLevel.InnerBlock) { 22 | object badGatherDashCount = Parse(GatherDashes); 23 | if (badGatherDashCount != null) { 24 | 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."); 25 | } 26 | } 27 | 28 | return Interleave( 29 | Optional (MultilineWhitespace), 30 | () => StatementAtLevel (level), 31 | untilTerminator: () => StatementsBreakForLevel(level)); 32 | } 33 | 34 | protected object StatementAtLevel(StatementLevel level) 35 | { 36 | ParseRule[] rulesAtLevel = _statementRulesAtLevel[(int)level]; 37 | 38 | var statement = OneOf (rulesAtLevel); 39 | 40 | // For some statements, allow them to parse, but create errors, since 41 | // writers may think they can use the statement, so it's useful to have 42 | // the error message. 43 | if (level == StatementLevel.Top) { 44 | if( statement is Return ) 45 | Error ("should not have return statement outside of a knot"); 46 | } 47 | 48 | return statement; 49 | } 50 | 51 | protected object StatementsBreakForLevel(StatementLevel level) 52 | { 53 | Whitespace (); 54 | 55 | ParseRule[] breakRules = _statementBreakRulesAtLevel[(int)level]; 56 | 57 | var breakRuleResult = OneOf (breakRules); 58 | if (breakRuleResult == null) 59 | return null; 60 | 61 | return breakRuleResult; 62 | } 63 | 64 | void GenerateStatementLevelRules() 65 | { 66 | var levels = Enum.GetValues (typeof(StatementLevel)).Cast ().ToList(); 67 | 68 | _statementRulesAtLevel = new ParseRule[levels.Count][]; 69 | _statementBreakRulesAtLevel = new ParseRule[levels.Count][]; 70 | 71 | foreach (var level in levels) { 72 | List rulesAtLevel = new List (); 73 | List breakingRules = new List (); 74 | 75 | // Diverts can go anywhere 76 | rulesAtLevel.Add(Line(MultiDivert)); 77 | 78 | // Knots can only be parsed at Top/Global scope 79 | if (level >= StatementLevel.Top) 80 | rulesAtLevel.Add (KnotDefinition); 81 | 82 | rulesAtLevel.Add(Line(Choice)); 83 | 84 | rulesAtLevel.Add(Line(AuthorWarning)); 85 | 86 | // Gather lines would be confused with multi-line block separators, like 87 | // within a multi-line if statement 88 | if (level > StatementLevel.InnerBlock) { 89 | rulesAtLevel.Add (Gather); 90 | } 91 | 92 | // Stitches (and gathers) can (currently) only go in Knots and top level 93 | if (level >= StatementLevel.Knot) { 94 | rulesAtLevel.Add (StitchDefinition); 95 | } 96 | 97 | // Global variable declarations can go anywhere 98 | rulesAtLevel.Add(Line(ListDeclaration)); 99 | rulesAtLevel.Add(Line(VariableDeclaration)); 100 | rulesAtLevel.Add(Line(ConstDeclaration)); 101 | rulesAtLevel.Add(Line(ExternalDeclaration)); 102 | 103 | // Global include can go anywhere 104 | rulesAtLevel.Add(Line(IncludeStatement)); 105 | 106 | // Normal logic / text can go anywhere 107 | rulesAtLevel.Add(LogicLine); 108 | rulesAtLevel.Add(LineOfMixedTextAndLogic); 109 | 110 | // -------- 111 | // Breaking rules 112 | 113 | // Break current knot with a new knot 114 | if (level <= StatementLevel.Knot) { 115 | breakingRules.Add (KnotDeclaration); 116 | } 117 | 118 | // Break current stitch with a new stitch 119 | if (level <= StatementLevel.Stitch) { 120 | breakingRules.Add (StitchDeclaration); 121 | } 122 | 123 | // Breaking an inner block (like a multi-line condition statement) 124 | if (level <= StatementLevel.InnerBlock) { 125 | breakingRules.Add (ParseDashNotArrow); 126 | breakingRules.Add (String ("}")); 127 | } 128 | 129 | _statementRulesAtLevel [(int)level] = rulesAtLevel.ToArray (); 130 | _statementBreakRulesAtLevel [(int)level] = breakingRules.ToArray (); 131 | } 132 | } 133 | 134 | protected object SkipToNextLine() 135 | { 136 | ParseUntilCharactersFromString ("\n\r"); 137 | ParseNewline (); 138 | return ParseSuccess; 139 | } 140 | 141 | // Modifier to turn a rule into one that expects a newline on the end. 142 | // e.g. anywhere you can use "MixedTextAndLogic" as a rule, you can use 143 | // "Line(MixedTextAndLogic)" to specify that it expects a newline afterwards. 144 | protected ParseRule Line(ParseRule inlineRule) 145 | { 146 | return () => { 147 | object result = ParseObject(inlineRule); 148 | if (result == null) { 149 | return null; 150 | } 151 | 152 | Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine); 153 | 154 | return result; 155 | }; 156 | } 157 | 158 | 159 | ParseRule[][] _statementRulesAtLevel; 160 | ParseRule[][] _statementBreakRulesAtLevel; 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/ConditionalSingleBranch.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Generic; 3 | 4 | namespace Ink.Parsed 5 | { 6 | public class ConditionalSingleBranch : Parsed.Object 7 | { 8 | // bool condition, e.g.: 9 | // { 5 == 4: 10 | // - the true branch 11 | // - the false branch 12 | // } 13 | public bool isTrueBranch { get; set; } 14 | 15 | // When each branch has its own expression like a switch statement, 16 | // this is non-null. e.g. 17 | // { x: 18 | // - 4: the value of x is four (ownExpression is the value 4) 19 | // - 3: the value of x is three 20 | // } 21 | public Expression ownExpression { 22 | get { 23 | return _ownExpression; 24 | } 25 | set { 26 | _ownExpression = value; 27 | if (_ownExpression) { 28 | AddContent (_ownExpression); 29 | } 30 | } 31 | } 32 | 33 | // In the above example, match equality of x with 4 for the first branch. 34 | // This is as opposed to simply evaluating boolean equality for each branch, 35 | // example when shouldMatchEqualtity is FALSE: 36 | // { 37 | // 3 > 2: This will happen 38 | // 2 > 3: This won't happen 39 | // } 40 | public bool matchingEquality { get; set; } 41 | 42 | public bool isElse { get; set; } 43 | 44 | public bool isInline { get; set; } 45 | 46 | public Runtime.Divert returnDivert { get; protected set; } 47 | 48 | public ConditionalSingleBranch (List content) 49 | { 50 | // Branches are allowed to be empty 51 | if (content != null) { 52 | _innerWeave = new Weave (content); 53 | AddContent (_innerWeave); 54 | } 55 | } 56 | 57 | // Runtime content can be summarised as follows: 58 | // - Evaluate an expression if necessary to branch on 59 | // - Branch to a named container if true 60 | // - Divert back to main flow 61 | // (owner Conditional is in control of this target point) 62 | public override Runtime.Object GenerateRuntimeObject () 63 | { 64 | // Check for common mistake, of putting "else:" instead of "- else:" 65 | if (_innerWeave) { 66 | foreach (var c in _innerWeave.content) { 67 | var text = c as Parsed.Text; 68 | if (text) { 69 | // Don't need to trim at the start since the parser handles that already 70 | if (text.text.StartsWith ("else:")) { 71 | Warning ("Saw the text 'else:' which is being treated as content. Did you mean '- else:'?", text); 72 | } 73 | } 74 | } 75 | } 76 | 77 | var container = new Runtime.Container (); 78 | 79 | // Are we testing against a condition that's used for more than just this 80 | // branch? If so, the first thing we need to do is replicate the value that's 81 | // on the evaluation stack so that we don't fully consume it, in case other 82 | // branches need to use it. 83 | bool duplicatesStackValue = matchingEquality && !isElse; 84 | if ( duplicatesStackValue ) 85 | container.AddContent (Runtime.ControlCommand.Duplicate ()); 86 | 87 | _conditionalDivert = new Runtime.Divert (); 88 | 89 | // else clause is unconditional catch-all, otherwise the divert is conditional 90 | _conditionalDivert.isConditional = !isElse; 91 | 92 | // Need extra evaluation? 93 | if( !isTrueBranch && !isElse ) { 94 | 95 | bool needsEval = ownExpression != null; 96 | if( needsEval ) 97 | container.AddContent (Runtime.ControlCommand.EvalStart ()); 98 | 99 | if (ownExpression) 100 | ownExpression.GenerateIntoContainer (container); 101 | 102 | // Uses existing duplicated value 103 | if (matchingEquality) 104 | container.AddContent (Runtime.NativeFunctionCall.CallWithName ("==")); 105 | 106 | if( needsEval ) 107 | container.AddContent (Runtime.ControlCommand.EvalEnd ()); 108 | } 109 | 110 | // Will pop from stack if conditional 111 | container.AddContent (_conditionalDivert); 112 | 113 | _contentContainer = GenerateRuntimeForContent (); 114 | _contentContainer.name = "b"; 115 | 116 | // Multi-line conditionals get a newline at the start of each branch 117 | // (as opposed to the start of the multi-line conditional since the condition 118 | // may evaluate to false.) 119 | if (!isInline) { 120 | _contentContainer.InsertContent (new Runtime.StringValue ("\n"), 0); 121 | } 122 | 123 | if( duplicatesStackValue || (isElse && matchingEquality) ) 124 | _contentContainer.InsertContent (Runtime.ControlCommand.PopEvaluatedValue (), 0); 125 | 126 | container.AddToNamedContentOnly (_contentContainer); 127 | 128 | returnDivert = new Runtime.Divert (); 129 | _contentContainer.AddContent (returnDivert); 130 | 131 | return container; 132 | } 133 | 134 | Runtime.Container GenerateRuntimeForContent() 135 | { 136 | // Empty branch - create empty container 137 | if (_innerWeave == null) { 138 | return new Runtime.Container (); 139 | } 140 | 141 | return _innerWeave.rootContainer; 142 | } 143 | 144 | public override void ResolveReferences (Story context) 145 | { 146 | _conditionalDivert.targetPath = _contentContainer.path; 147 | 148 | base.ResolveReferences (context); 149 | } 150 | 151 | Runtime.Container _contentContainer; 152 | Runtime.Divert _conditionalDivert; 153 | Expression _ownExpression; 154 | 155 | Weave _innerWeave; 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/VariableReference.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Ink.Parsed 5 | { 6 | public class VariableReference : Expression 7 | { 8 | // - Normal variables have a single item in their "path" 9 | // - Knot/stitch names for read counts are actual dot-separated paths 10 | // (though this isn't actually used at time of writing) 11 | // - List names are dot separated: listName.itemName (or just itemName) 12 | public string name { get; private set; } 13 | 14 | public Identifier identifier { 15 | get { 16 | // Merging the list of identifiers into a single identifier. 17 | // Debug metadata is also merged. 18 | if (pathIdentifiers == null || pathIdentifiers.Count == 0) { 19 | return null; 20 | } 21 | 22 | if( _singleIdentifier == null ) { 23 | var name = string.Join (".", path.ToArray()); 24 | var firstDebugMetadata = pathIdentifiers.First().debugMetadata; 25 | var debugMetadata = pathIdentifiers.Aggregate(firstDebugMetadata, (acc, id) => acc.Merge(id.debugMetadata)); 26 | _singleIdentifier = new Identifier { name = name, debugMetadata = debugMetadata }; 27 | } 28 | 29 | return _singleIdentifier; 30 | } 31 | } 32 | Identifier _singleIdentifier; 33 | 34 | public List pathIdentifiers; 35 | public List path { get; private set; } 36 | 37 | // Only known after GenerateIntoContainer has run 38 | public bool isConstantReference; 39 | public bool isListItemReference; 40 | 41 | public Runtime.VariableReference runtimeVarRef { get { return _runtimeVarRef; } } 42 | 43 | public VariableReference (List pathIdentifiers) 44 | { 45 | this.pathIdentifiers = pathIdentifiers; 46 | this.path = pathIdentifiers.Select(id => id?.name).ToList(); 47 | this.name = string.Join (".", pathIdentifiers); 48 | } 49 | 50 | public override void GenerateIntoContainer (Runtime.Container container) 51 | { 52 | Expression constantValue = null; 53 | 54 | // If it's a constant reference, just generate the literal expression value 55 | // It's okay to access the constants at code generation time, since the 56 | // first thing the ExportRuntime function does it search for all the constants 57 | // in the story hierarchy, so they're all available. 58 | if ( story.constants.TryGetValue (name, out constantValue) ) { 59 | constantValue.GenerateConstantIntoContainer (container); 60 | isConstantReference = true; 61 | return; 62 | } 63 | 64 | _runtimeVarRef = new Runtime.VariableReference (name); 65 | 66 | // List item reference? 67 | // Path might be to a list (listName.listItemName or just listItemName) 68 | if (path.Count == 1 || path.Count == 2) { 69 | string listItemName = null; 70 | string listName = null; 71 | 72 | if (path.Count == 1) listItemName = path [0]; 73 | else { 74 | listName = path [0]; 75 | listItemName = path [1]; 76 | } 77 | 78 | var listItem = story.ResolveListItem (listName, listItemName, this); 79 | if (listItem) { 80 | isListItemReference = true; 81 | } 82 | } 83 | 84 | container.AddContent (_runtimeVarRef); 85 | } 86 | 87 | public override void ResolveReferences (Story context) 88 | { 89 | base.ResolveReferences (context); 90 | 91 | // Work is already done if it's a constant or list item reference 92 | if (isConstantReference || isListItemReference) { 93 | return; 94 | } 95 | 96 | // Is it a read count? 97 | var parsedPath = new Path (pathIdentifiers); 98 | Parsed.Object targetForCount = parsedPath.ResolveFromContext (this); 99 | if (targetForCount) { 100 | 101 | targetForCount.containerForCounting.visitsShouldBeCounted = true; 102 | 103 | // If this is an argument to a function that wants a variable to be 104 | // passed by reference, then the Parsed.Divert will have generated a 105 | // Runtime.VariablePointerValue instead of allowing this object 106 | // to generate its RuntimeVariableReference. This only happens under 107 | // error condition since we shouldn't be passing a read count by 108 | // reference, but we don't want it to crash! 109 | if (_runtimeVarRef == null) return; 110 | 111 | _runtimeVarRef.pathForCount = targetForCount.runtimePath; 112 | _runtimeVarRef.name = null; 113 | 114 | // Check for very specific writer error: getting read count and 115 | // printing it as content rather than as a piece of logic 116 | // e.g. Writing {myFunc} instead of {myFunc()} 117 | var targetFlow = targetForCount as FlowBase; 118 | if (targetFlow && targetFlow.isFunction) { 119 | 120 | // Is parent context content rather than logic? 121 | if ( parent is Weave || parent is ContentList || parent is FlowBase) { 122 | Warning ("'" + targetFlow.identifier + "' being used as read count rather than being called as function. Perhaps you intended to write " + targetFlow.name + "()"); 123 | } 124 | } 125 | 126 | return; 127 | } 128 | 129 | // Couldn't find this multi-part path at all, whether as a divert 130 | // target or as a list item reference. 131 | if (path.Count > 1) { 132 | var errorMsg = "Could not find target for read count: " + parsedPath; 133 | if (path.Count <= 2) 134 | errorMsg += ", or couldn't find list item with the name " + string.Join (",", path.ToArray()); 135 | Error (errorMsg); 136 | return; 137 | } 138 | 139 | if (!context.ResolveVariableWithName (this.name, fromNode: this).found) { 140 | Error("Unresolved variable: "+this.ToString(), this); 141 | } 142 | } 143 | 144 | public override string ToString () 145 | { 146 | return string.Join(".", path.ToArray()); 147 | } 148 | 149 | Runtime.VariableReference _runtimeVarRef; 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /compiler/InkParser/InkParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Ink 6 | { 7 | public partial class InkParser : StringParser 8 | { 9 | public InkParser(string str, string filenameForMetadata = null, Ink.ErrorHandler externalErrorHandler = null, IFileHandler fileHandler = null) 10 | : this(str, filenameForMetadata, externalErrorHandler, null, fileHandler) 11 | { } 12 | 13 | InkParser(string str, string inkFilename = null, Ink.ErrorHandler externalErrorHandler = null, InkParser rootParser = null, IFileHandler fileHandler = null) : base(str) { 14 | _filename = inkFilename; 15 | RegisterExpressionOperators (); 16 | GenerateStatementLevelRules (); 17 | 18 | // Built in handler for all standard parse errors and warnings 19 | this.errorHandler = OnStringParserError; 20 | 21 | // The above parse errors are then formatted as strings and passed 22 | // to the Ink.ErrorHandler, or it throws an exception 23 | _externalErrorHandler = externalErrorHandler; 24 | 25 | _fileHandler = fileHandler ?? new DefaultFileHandler(); 26 | 27 | if (rootParser == null) { 28 | _rootParser = this; 29 | 30 | _openFilenames = new HashSet (); 31 | 32 | if (inkFilename != null) { 33 | var fullRootInkPath = _fileHandler.ResolveInkFilename (inkFilename); 34 | _openFilenames.Add (fullRootInkPath); 35 | } 36 | 37 | } else { 38 | _rootParser = rootParser; 39 | } 40 | 41 | } 42 | 43 | // Main entry point 44 | public Parsed.Story Parse() 45 | { 46 | List topLevelContent = StatementsAtLevel (StatementLevel.Top); 47 | 48 | // Note we used to return null if there were any errors, but this would mean 49 | // that include files would return completely empty rather than attempting to 50 | // continue with errors. Returning an empty include files meant that anything 51 | // that *did* compile successfully would otherwise be ignored, generating way 52 | // more errors than necessary. 53 | return new Parsed.Story (topLevelContent, isInclude:_rootParser != this); 54 | } 55 | 56 | protected List SeparatedList (SpecificParseRule mainRule, ParseRule separatorRule) where T : class 57 | { 58 | T firstElement = Parse (mainRule); 59 | if (firstElement == null) return null; 60 | 61 | var allElements = new List (); 62 | allElements.Add (firstElement); 63 | 64 | do { 65 | 66 | int nextElementRuleId = BeginRule (); 67 | 68 | var sep = separatorRule (); 69 | if (sep == null) { 70 | FailRule (nextElementRuleId); 71 | break; 72 | } 73 | 74 | var nextElement = Parse (mainRule); 75 | if (nextElement == null) { 76 | FailRule (nextElementRuleId); 77 | break; 78 | } 79 | 80 | SucceedRule (nextElementRuleId); 81 | 82 | allElements.Add (nextElement); 83 | 84 | } while (true); 85 | 86 | return allElements; 87 | } 88 | 89 | protected override string PreProcessInputString(string str) 90 | { 91 | var inputWithCommentsRemoved = (new CommentEliminator (str)).Process(); 92 | return inputWithCommentsRemoved; 93 | } 94 | 95 | protected Runtime.DebugMetadata CreateDebugMetadata(StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd) 96 | { 97 | var md = new Runtime.DebugMetadata (); 98 | md.startLineNumber = stateAtStart.lineIndex + 1; 99 | md.endLineNumber = stateAtEnd.lineIndex + 1; 100 | md.startCharacterNumber = stateAtStart.characterInLineIndex + 1; 101 | md.endCharacterNumber = stateAtEnd.characterInLineIndex + 1; 102 | md.fileName = _filename; 103 | return md; 104 | } 105 | 106 | protected override void RuleDidSucceed(object result, StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd) 107 | { 108 | // Apply DebugMetadata based on the state at the start of the rule 109 | // (i.e. use line number as it was at the start of the rule) 110 | var parsedObj = result as Parsed.Object; 111 | if ( parsedObj) { 112 | parsedObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd); 113 | return; 114 | } 115 | 116 | // A list of objects that doesn't already have metadata? 117 | var parsedListObjs = result as List; 118 | if (parsedListObjs != null) { 119 | foreach (var parsedListObj in parsedListObjs) { 120 | if (!parsedListObj.hasOwnDebugMetadata) { 121 | parsedListObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd); 122 | } 123 | } 124 | } 125 | 126 | var id = result as Parsed.Identifier; 127 | if (id != null) { 128 | id.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd); 129 | } 130 | } 131 | 132 | protected bool parsingStringExpression 133 | { 134 | get { 135 | return GetFlag ((uint)CustomFlags.ParsingString); 136 | } 137 | set { 138 | SetFlag ((uint)CustomFlags.ParsingString, value); 139 | } 140 | } 141 | 142 | protected enum CustomFlags { 143 | ParsingString = 0x1 144 | } 145 | 146 | void OnStringParserError(string message, int index, int lineIndex, bool isWarning) 147 | { 148 | var warningType = isWarning ? "WARNING:" : "ERROR:"; 149 | string fullMessage; 150 | 151 | if (_filename != null) { 152 | fullMessage = string.Format(warningType+" '{0}' line {1}: {2}", _filename, (lineIndex+1), message); 153 | } else { 154 | fullMessage = string.Format(warningType+" line {0}: {1}", (lineIndex+1), message); 155 | } 156 | 157 | if (_externalErrorHandler != null) { 158 | _externalErrorHandler (fullMessage, isWarning ? ErrorType.Warning : ErrorType.Error); 159 | } else { 160 | throw new System.Exception (fullMessage); 161 | } 162 | } 163 | 164 | IFileHandler _fileHandler; 165 | 166 | Ink.ErrorHandler _externalErrorHandler; 167 | 168 | string _filename; 169 | } 170 | } 171 | 172 | -------------------------------------------------------------------------------- /Sublime3Syntax/ink.YAML-tmLanguage: -------------------------------------------------------------------------------- 1 | # [PackageDev] target_format: plist, ext: tmLanguage 2 | --- 3 | name: ink 4 | scopeName: source.ink 5 | fileTypes: [ink, ink2] 6 | uuid: 5a0f60ba-87b8-4fa9-854c-6bf41f74bd98 7 | 8 | patterns: 9 | 10 | - {include: '#comments'} 11 | 12 | # knot declaration 13 | - match: ^\s*(={2,})\s*(function)?\s*(\w+)\s*(\([^)]*\))?\s*(={1,})? 14 | captures: 15 | '1': {name: markup.punctuation} 16 | '2': {name: keyword.function} 17 | '3': {name: entity.name.knot} 18 | '4': {name: variable.parameter} 19 | '5': {name: markup.punctuation} 20 | name: meta.knot.declaration 21 | 22 | # stitch declaration 23 | - match: ^\s*(=)\s*(\w+)\s*(\([^)\n]*\))?\s*$ 24 | captures: 25 | '1': {name: markup.punctuation} 26 | '2': {name: entity.name.stitch} 27 | '3': {name: variable.parameter} 28 | name: meta.stitch.declaration 29 | 30 | 31 | # Choice and gather lines 32 | - {include: '#choice'} 33 | - {include: '#gather'} 34 | 35 | - {include: '#statements'} 36 | 37 | repository: 38 | tag: 39 | begin: (#)\s* 40 | end: \s*$\n* 41 | captures: 42 | '1': {name: keyword.operator.hashtag} 43 | contentName: entity.name.tag 44 | patterns: 45 | - include: '#comments' 46 | comments: 47 | patterns: 48 | - begin: /\*\* 49 | captures: 50 | '0': {name: punctuation.definition.comment.json} 51 | end: \*/ 52 | name: comment.block.documentation.json 53 | - begin: /\* 54 | captures: 55 | '0': {name: punctuation.definition.comment.json} 56 | end: \*/ 57 | name: comment.block.json 58 | - captures: 59 | '1': {name: punctuation.definition.comment.json} 60 | match: (//).*$\n? 61 | name: comment.line.double-slash.js 62 | 63 | TODO: 64 | match: ^\s*(?:(TODO\s*:)|(TODO\b))\s*(.*) 65 | captures: 66 | '1': { name: comment.todo.TODO } 67 | '2': { name: comment.todo.TODO } 68 | end: $\n? 69 | name: comment.todo 70 | include: 71 | begin: ^\s*((INCLUDE)\s*(\S*))\s* 72 | beginCaptures: 73 | '1': {name: meta.include} 74 | '2': {name: keyword.control.include} 75 | '3': {name: string.other } 76 | end: $\n? 77 | external: 78 | begin: ^\s*((EXTERNAL)\s*([\w_0-9]*\(\)))\s* 79 | beginCaptures: 80 | '1': {name: meta.external} 81 | '2': {name: keyword.control.external} 82 | '3': {name: support.function } 83 | end: $\n? 84 | globalVAR: 85 | begin: ^\s*((VAR|CONST)\s*(\w+))\s* 86 | beginCaptures: 87 | '1': {name: meta.variable.declaration} 88 | '2': {name: storage} 89 | '3': {name: entity.name.variable} 90 | end: $\n? 91 | name: meta.variable.assignment 92 | 93 | choice: 94 | begin: ^\s*((?:[\*\+]\s?)+)\s*(\(\s*(\w+)\s*\))? 95 | beginCaptures: 96 | '1': {name: keyword.operator.weaveBullet.choice} 97 | '2': {name: meta.label} 98 | '3': {name: entity.name.label} 99 | end: $\n? 100 | name: choice 101 | patterns: 102 | - {include: '#comments'} 103 | - match: (\[)((?:[^\]]|\\\[|\])*)(\]) 104 | captures: 105 | '1': {name: keyword.operator.weaveBracket} 106 | '2': {name: string.content} 107 | '3': {name: keyword.operator.weaveBracket} 108 | - {include: '#divert'} 109 | - {include: '#mixedContent'} 110 | 111 | gather: 112 | match: ^\s*((?:-\s*)+)(?!>)(\(\s*(\w+)\s*\))? 113 | captures: 114 | '1': {name: keyword.operator.weaveBullet.gather} 115 | '2': {name: meta.label} 116 | '3': {name: entity.name.label} 117 | name: meta.gather 118 | 119 | multiLineLogic: 120 | begin: '^\s*(\{)([\w_\s\*\/\-\+\&\|\%\<\>\.\(\)]+(:))?(?=[^}]+$)' 121 | beginCaptures: 122 | '0': {name: meta.logicBegin} 123 | '1': {name: keyword.operator.logic} 124 | '3': {name: keyword.operator.logic} 125 | end: (\}) 126 | endCaptures: 127 | '1': {name: keyword.operator} 128 | contentName: meta.multilineLogicInner 129 | name: meta.multilineLogic 130 | patterns: 131 | - match: '^\s*else\s*\:' 132 | name: conditional.else 133 | - begin: '^\s*(-)\s?[^:]+(:)' 134 | beginCaptures: 135 | '1': {name: keyword.operator} 136 | '2': {name: keyword.operator} 137 | end: $\n? 138 | name: conditional.clause 139 | patterns: 140 | - {include: '#mixedContent'} 141 | 142 | - {include: '#statements'} 143 | 144 | inlineConditional: 145 | begin: '(\{)[^:\|\}]+:' 146 | beginCaptures: 147 | '1': {name: keyword.operator.inlineConditionalStart} 148 | end: (\}) 149 | endCaptures: 150 | '1': {name: keyword.operator.inlineConditionalEnd} 151 | name: entity.inlineConditional 152 | patterns: 153 | - match: \| 154 | name: keyword.operator.inlineConditionalBranchSeparator 155 | - {include: '#mixedContent'} 156 | 157 | inlineSequence: 158 | begin: (\{)\s*(~|&|!|\$)?(?=([^\|]*\|(?!\|)[^\}]*)\}) 159 | beginCaptures: 160 | '1': {name: keyword.operator.inlineSequenceStart} 161 | '2': {name: keyword.operator.inlineSequenceTypeChar} 162 | end: \} 163 | endCaptures: 164 | '0': {name: keyword.operator.inlineSequenceEnd} 165 | name: entity.inlineSequence 166 | patterns: 167 | - match: \|(?!\|) 168 | name: keyword.operator.inlineSequenceSeparator 169 | - {include: '#mixedContent'} 170 | 171 | inlineLogic: 172 | begin: (\{) 173 | beginCaptures: 174 | '1': {name: keyword.operator.inlineLogicStart} 175 | end: (\}) 176 | endCaptures: 177 | '1': {name: keyword.operator.inlineLogicEnd} 178 | name: meta.logic 179 | 180 | logicLine: 181 | match: \s*(~\s*.*)$ 182 | captures: 183 | '0': {name: meta.logic} 184 | 185 | divert: 186 | match: (->|<-)\s*((?:(DONE)|(END)|(\w+))(?:\s*\.\s*(?:\w+))*\s*(?:\([^\)]+\))?)? 187 | name: meta.divert 188 | captures: 189 | '1': {name: keyword.operator.divert} 190 | '3': {name: keyword.done} 191 | '4': {name: keyword.end} 192 | '5': {name: variable.divertTarget} 193 | 194 | mixedContent: 195 | patterns: 196 | - {include: '#inlineConditional'} 197 | - {include: '#inlineSequence'} 198 | - {include: '#inlineLogic'} 199 | - {include: '#divert'} 200 | - match: <> 201 | name: constant.glue 202 | 203 | # Hrm, the bold/italic stuff seems to be more trouble than it's worth :-( 204 | # - begin: \*_|_\* 205 | # end: \*_|_\*|\[|\{|->|\n 206 | # contentName: string.boldItalic 207 | # - begin: \* 208 | # end: \*|\[|\{|->|\n 209 | # contentName: string.bold 210 | # - begin: _ 211 | # end: _|\[|\{|->|\n 212 | # contentName: string.italic 213 | 214 | # Final fallback 215 | # 26/03/16 - Switch off this semantic name for the main text content, 216 | # since it messes with word wrapping, and doesn't seem to give any 217 | # particular benefit. 218 | # - match: . 219 | # name: string.content 220 | 221 | statements: 222 | patterns: 223 | - {include: '#include'} 224 | - {include: '#external'} 225 | - {include: '#comments'} 226 | - {include: '#TODO'} 227 | - {include: '#globalVAR'} 228 | - {include: '#choice'} 229 | - {include: '#gather'} 230 | - {include: '#multiLineLogic'} 231 | - {include: '#endOfSection'} 232 | - {include: '#logicLine'} 233 | - {include: '#mixedContent'} 234 | - {include: '#tag'} 235 | 236 | ... -------------------------------------------------------------------------------- /compiler/InkParser/InkParser_Divert.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Ink.Parsed; 3 | 4 | 5 | namespace Ink 6 | { 7 | public partial class InkParser 8 | { 9 | protected List MultiDivert() 10 | { 11 | Whitespace (); 12 | 13 | List diverts = null; 14 | 15 | // Try single thread first 16 | var threadDivert = Parse(StartThread); 17 | if (threadDivert) { 18 | diverts = new List (); 19 | diverts.Add (threadDivert); 20 | return diverts; 21 | } 22 | 23 | // Normal diverts and tunnels 24 | var arrowsAndDiverts = Interleave ( 25 | ParseDivertArrowOrTunnelOnwards, 26 | DivertIdentifierWithArguments); 27 | 28 | if (arrowsAndDiverts == null) 29 | return null; 30 | 31 | diverts = new List (); 32 | 33 | // Possible patterns: 34 | // -> -- explicit gather 35 | // ->-> -- tunnel onwards 36 | // -> div -- normal divert 37 | // ->-> div -- tunnel onwards, followed by override divert 38 | // -> div -> -- normal tunnel 39 | // -> div ->-> -- tunnel then tunnel continue 40 | // -> div -> div -- tunnel then divert 41 | // -> div -> div -> -- tunnel then tunnel 42 | // -> div -> div ->-> 43 | // -> div -> div ->-> div (etc) 44 | 45 | // Look at the arrows and diverts 46 | for (int i = 0; i < arrowsAndDiverts.Count; ++i) { 47 | bool isArrow = (i % 2) == 0; 48 | 49 | // Arrow string 50 | if (isArrow) { 51 | 52 | // Tunnel onwards 53 | if ((string)arrowsAndDiverts [i] == "->->") { 54 | 55 | bool tunnelOnwardsPlacementValid = (i == 0 || i == arrowsAndDiverts.Count - 1 || i == arrowsAndDiverts.Count - 2); 56 | if (!tunnelOnwardsPlacementValid) 57 | Error ("Tunnel onwards '->->' must only come at the begining or the start of a divert"); 58 | 59 | var tunnelOnwards = new TunnelOnwards (); 60 | if (i < arrowsAndDiverts.Count - 1) { 61 | var tunnelOnwardDivert = arrowsAndDiverts [i+1] as Parsed.Divert; 62 | tunnelOnwards.divertAfter = tunnelOnwardDivert; 63 | } 64 | 65 | diverts.Add (tunnelOnwards); 66 | 67 | // Not allowed to do anything after a tunnel onwards. 68 | // If we had anything left it would be caused in the above Error for 69 | // the positioning of a ->-> 70 | break; 71 | } 72 | } 73 | 74 | // Divert 75 | else { 76 | 77 | var divert = arrowsAndDiverts [i] as Divert; 78 | 79 | // More to come? (further arrows) Must be tunnelling. 80 | if (i < arrowsAndDiverts.Count - 1) { 81 | divert.isTunnel = true; 82 | } 83 | 84 | diverts.Add (divert); 85 | } 86 | } 87 | 88 | // Single -> (used for default choices) 89 | if (diverts.Count == 0 && arrowsAndDiverts.Count == 1) { 90 | var gatherDivert = new Divert ((Parsed.Object)null); 91 | gatherDivert.isEmpty = true; 92 | diverts.Add (gatherDivert); 93 | 94 | if (!_parsingChoice) 95 | Error ("Empty diverts (->) are only valid on choices"); 96 | } 97 | 98 | return diverts; 99 | } 100 | 101 | protected Divert StartThread() 102 | { 103 | Whitespace (); 104 | 105 | if (ParseThreadArrow() == null) 106 | return null; 107 | 108 | Whitespace (); 109 | 110 | var divert = Expect(DivertIdentifierWithArguments, "target for new thread", () => new Divert(null)) as Divert; 111 | divert.isThread = true; 112 | 113 | return divert; 114 | } 115 | 116 | protected Divert DivertIdentifierWithArguments() 117 | { 118 | Whitespace (); 119 | 120 | List targetComponents = Parse (DotSeparatedDivertPathComponents); 121 | if (targetComponents == null) 122 | return null; 123 | 124 | Whitespace (); 125 | 126 | var optionalArguments = Parse(ExpressionFunctionCallArguments); 127 | 128 | Whitespace (); 129 | 130 | var targetPath = new Path (targetComponents); 131 | return new Divert (targetPath, optionalArguments); 132 | } 133 | 134 | protected Divert SingleDivert() 135 | { 136 | var diverts = Parse (MultiDivert); 137 | if (diverts == null) 138 | return null; 139 | 140 | // Ideally we'd report errors if we get the 141 | // wrong kind of divert, but unfortunately we 142 | // have to hack around the fact that sequences use 143 | // a very similar syntax. 144 | // i.e. if you have a multi-divert at the start 145 | // of a sequence, it initially tries to parse it 146 | // as a divert target (part of an expression of 147 | // a conditional) and gives errors. So instead 148 | // we just have to blindly reject it as a single 149 | // divert, and give a slightly less nice error 150 | // when you DO use a multi divert as a divert taret. 151 | 152 | if (diverts.Count != 1) { 153 | return null; 154 | } 155 | 156 | var singleDivert = diverts [0]; 157 | if (singleDivert is TunnelOnwards) { 158 | return null; 159 | } 160 | 161 | var divert = diverts [0] as Divert; 162 | if (divert.isTunnel) { 163 | return null; 164 | } 165 | 166 | return divert; 167 | } 168 | 169 | List DotSeparatedDivertPathComponents() 170 | { 171 | return Interleave (Spaced (IdentifierWithMetadata), Exclude (String ("."))); 172 | } 173 | 174 | protected string ParseDivertArrowOrTunnelOnwards() 175 | { 176 | int numArrows = 0; 177 | while (ParseString ("->") != null) 178 | numArrows++; 179 | 180 | if (numArrows == 0) 181 | return null; 182 | 183 | else if (numArrows == 1) 184 | return "->"; 185 | 186 | else if (numArrows == 2) 187 | return "->->"; 188 | 189 | else { 190 | Error ("Unexpected number of arrows in divert. Should only have '->' or '->->'"); 191 | return "->->"; 192 | } 193 | } 194 | 195 | protected string ParseDivertArrow() 196 | { 197 | return ParseString ("->"); 198 | } 199 | 200 | protected string ParseThreadArrow() 201 | { 202 | return ParseString ("<-"); 203 | } 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/Path.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Ink.Parsed 6 | { 7 | public class Path 8 | { 9 | public FlowLevel baseTargetLevel { 10 | get { 11 | if (baseLevelIsAmbiguous) 12 | return FlowLevel.Story; 13 | else 14 | return (FlowLevel) _baseTargetLevel; 15 | } 16 | } 17 | 18 | public bool baseLevelIsAmbiguous { 19 | get { 20 | return _baseTargetLevel == null; 21 | } 22 | } 23 | 24 | public string firstComponent { 25 | get { 26 | if (components == null || components.Count == 0) 27 | return null; 28 | 29 | return components [0].name; 30 | } 31 | } 32 | 33 | public int numberOfComponents { 34 | get { 35 | return components.Count; 36 | } 37 | } 38 | 39 | public string dotSeparatedComponents { 40 | get { 41 | if( _dotSeparatedComponents == null ) { 42 | _dotSeparatedComponents = string.Join(".", components.Select(c => c?.name)); 43 | } 44 | 45 | return _dotSeparatedComponents; 46 | } 47 | } 48 | string _dotSeparatedComponents; 49 | 50 | public List components { get; } 51 | 52 | public Path(FlowLevel baseFlowLevel, List components) 53 | { 54 | _baseTargetLevel = baseFlowLevel; 55 | this.components = components; 56 | } 57 | 58 | public Path(List components) 59 | { 60 | _baseTargetLevel = null; 61 | this.components = components; 62 | } 63 | 64 | public Path(Identifier ambiguousName) 65 | { 66 | _baseTargetLevel = null; 67 | components = new List (); 68 | components.Add (ambiguousName); 69 | } 70 | 71 | public override string ToString () 72 | { 73 | if (components == null || components.Count == 0) { 74 | if (baseTargetLevel == FlowLevel.WeavePoint) 75 | return "-> "; 76 | else 77 | return ""; 78 | } 79 | 80 | return "-> " + dotSeparatedComponents; 81 | } 82 | 83 | public Parsed.Object ResolveFromContext(Parsed.Object context) 84 | { 85 | if (components == null || components.Count == 0) { 86 | return null; 87 | } 88 | 89 | // Find base target of path from current context. e.g. 90 | // ==> BASE.sub.sub 91 | var baseTargetObject = ResolveBaseTarget (context); 92 | if (baseTargetObject == null) { 93 | return null; 94 | 95 | } 96 | 97 | // Given base of path, resolve final target by working deeper into hierarchy 98 | // e.g. ==> base.mid.FINAL 99 | if (components.Count > 1) { 100 | return ResolveTailComponents (baseTargetObject); 101 | } 102 | 103 | return baseTargetObject; 104 | } 105 | 106 | // Find the root object from the base, i.e. root from: 107 | // root.sub1.sub2 108 | Parsed.Object ResolveBaseTarget(Parsed.Object originalContext) 109 | { 110 | var firstComp = firstComponent; 111 | 112 | // Work up the ancestry to find the node that has the named object 113 | Parsed.Object ancestorContext = originalContext; 114 | while (ancestorContext != null) { 115 | 116 | // Only allow deep search when searching deeper from original context. 117 | // Don't allow search upward *then* downward, since that's searching *everywhere*! 118 | // Allowed examples: 119 | // - From an inner gather of a stitch, you should search up to find a knot called 'x' 120 | // at the root of a story, but not a stitch called 'x' in that knot. 121 | // - However, from within a knot, you should be able to find a gather/choice 122 | // anywhere called 'x' 123 | // (that latter example is quite loose, but we allow it) 124 | bool deepSearch = ancestorContext == originalContext; 125 | 126 | var foundBase = TryGetChildFromContext (ancestorContext, firstComp, null, deepSearch); 127 | if (foundBase != null) 128 | return foundBase; 129 | 130 | ancestorContext = ancestorContext.parent; 131 | } 132 | 133 | return null; 134 | } 135 | 136 | // Find the final child from path given root, i.e.: 137 | // root.sub.finalChild 138 | Parsed.Object ResolveTailComponents(Parsed.Object rootTarget) 139 | { 140 | Parsed.Object foundComponent = rootTarget; 141 | for (int i = 1; i < components.Count; ++i) { 142 | var compName = components [i].name; 143 | 144 | FlowLevel minimumExpectedLevel; 145 | var foundFlow = foundComponent as FlowBase; 146 | if (foundFlow != null) 147 | minimumExpectedLevel = (FlowLevel)(foundFlow.flowLevel + 1); 148 | else 149 | minimumExpectedLevel = FlowLevel.WeavePoint; 150 | 151 | 152 | foundComponent = TryGetChildFromContext (foundComponent, compName, minimumExpectedLevel); 153 | if (foundComponent == null) 154 | break; 155 | } 156 | 157 | return foundComponent; 158 | } 159 | 160 | // See whether "context" contains a child with a given name at a given flow level 161 | // Can either be a named knot/stitch (a FlowBase) or a weave point within a Weave (Choice or Gather) 162 | // This function also ignores any other object types that are neither FlowBase nor Weave. 163 | // Called from both ResolveBase (force deep) and ResolveTail for the individual components. 164 | Parsed.Object TryGetChildFromContext(Parsed.Object context, string childName, FlowLevel? minimumLevel, bool forceDeepSearch = false) 165 | { 166 | // null childLevel means that we don't know where to find it 167 | bool ambiguousChildLevel = minimumLevel == null; 168 | 169 | // Search for WeavePoint within Weave 170 | var weaveContext = context as Weave; 171 | if ( weaveContext != null && (ambiguousChildLevel || minimumLevel == FlowLevel.WeavePoint)) { 172 | return (Parsed.Object) weaveContext.WeavePointNamed (childName); 173 | } 174 | 175 | // Search for content within Flow (either a sub-Flow or a WeavePoint) 176 | var flowContext = context as FlowBase; 177 | if (flowContext != null) { 178 | 179 | // When searching within a Knot, allow a deep searches so that 180 | // named weave points (choices and gathers) can be found within any stitch 181 | // Otherwise, we just search within the immediate object. 182 | var shouldDeepSearch = forceDeepSearch || flowContext.flowLevel == FlowLevel.Knot; 183 | return flowContext.ContentWithNameAtLevel (childName, minimumLevel, shouldDeepSearch); 184 | } 185 | 186 | return null; 187 | } 188 | 189 | FlowLevel? _baseTargetLevel; 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /compiler/ParsedHierarchy/DivertTarget.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Ink.Parsed 3 | { 4 | public class DivertTarget : Expression 5 | { 6 | public Divert divert; 7 | 8 | public DivertTarget (Divert divert) 9 | { 10 | this.divert = AddContent(divert); 11 | } 12 | 13 | public override void GenerateIntoContainer (Runtime.Container container) 14 | { 15 | divert.GenerateRuntimeObject(); 16 | 17 | _runtimeDivert = (Runtime.Divert) divert.runtimeDivert; 18 | _runtimeDivertTargetValue = new Runtime.DivertTargetValue (); 19 | 20 | container.AddContent (_runtimeDivertTargetValue); 21 | } 22 | 23 | public override void ResolveReferences (Story context) 24 | { 25 | base.ResolveReferences (context); 26 | 27 | if( divert.isDone || divert.isEnd ) 28 | { 29 | Error("Can't Can't use -> DONE or -> END as variable divert targets", this); 30 | return; 31 | } 32 | 33 | Parsed.Object usageContext = this; 34 | while (usageContext && usageContext is Expression) { 35 | 36 | bool badUsage = false; 37 | bool foundUsage = false; 38 | 39 | var usageParent = usageContext.parent; 40 | if (usageParent is BinaryExpression) { 41 | 42 | // Only allowed to compare for equality 43 | 44 | var binaryExprParent = usageParent as BinaryExpression; 45 | if (binaryExprParent.opName != "==" && binaryExprParent.opName != "!=") { 46 | badUsage = true; 47 | } else { 48 | if (!(binaryExprParent.leftExpression is DivertTarget || binaryExprParent.leftExpression is VariableReference)) { 49 | badUsage = true; 50 | } 51 | if (!(binaryExprParent.rightExpression is DivertTarget || binaryExprParent.rightExpression is VariableReference)) { 52 | badUsage = true; 53 | } 54 | } 55 | foundUsage = true; 56 | } 57 | else if( usageParent is FunctionCall ) { 58 | var funcCall = usageParent as FunctionCall; 59 | if( !funcCall.isTurnsSince && !funcCall.isReadCount ) { 60 | badUsage = true; 61 | } 62 | foundUsage = true; 63 | } 64 | else if (usageParent is Expression) { 65 | badUsage = true; 66 | foundUsage = true; 67 | } 68 | else if (usageParent is MultipleConditionExpression) { 69 | badUsage = true; 70 | foundUsage = true; 71 | } else if (usageParent is Choice && ((Choice)usageParent).condition == usageContext) { 72 | badUsage = true; 73 | foundUsage = true; 74 | } else if (usageParent is Conditional || usageParent is ConditionalSingleBranch) { 75 | badUsage = true; 76 | foundUsage = true; 77 | } 78 | 79 | if (badUsage) { 80 | 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); 81 | } 82 | 83 | if (foundUsage) 84 | break; 85 | 86 | usageContext = usageParent; 87 | } 88 | 89 | // Example ink for this case: 90 | // 91 | // VAR x = -> blah 92 | // 93 | // ...which means that "blah" is expected to be a literal stitch target rather 94 | // than a variable name. We can't really intelligently recover from this (e.g. if blah happens to 95 | // contain a divert target itself) since really we should be generating a variable reference 96 | // rather than a concrete DivertTarget, so we list it as an error. 97 | if (_runtimeDivert.hasVariableTarget) 98 | Error ("Since '"+divert.target.dotSeparatedComponents+"' is a variable, it shouldn't be preceded by '->' here."); 99 | 100 | // Main resolve 101 | _runtimeDivertTargetValue.targetPath = _runtimeDivert.targetPath; 102 | 103 | // Tell hard coded (yet variable) divert targets that they also need to be counted 104 | // TODO: Only detect DivertTargets that are values rather than being used directly for 105 | // read or turn counts. Should be able to detect this by looking for other uses of containerForCounting 106 | var targetContent = this.divert.targetContent; 107 | if (targetContent != null ) { 108 | var target = targetContent.containerForCounting; 109 | if (target != null) 110 | { 111 | // Purpose is known: used directly in TURNS_SINCE(-> divTarg) 112 | var parentFunc = this.parent as FunctionCall; 113 | if( parentFunc && parentFunc.isTurnsSince ) { 114 | target.turnIndexShouldBeCounted = true; 115 | } 116 | 117 | // Unknown purpose, count everything 118 | else { 119 | target.visitsShouldBeCounted = true; 120 | target.turnIndexShouldBeCounted = true; 121 | } 122 | 123 | } 124 | 125 | // Unfortunately not possible: 126 | // https://github.com/inkle/ink/issues/538 127 | // 128 | // VAR func = -> double 129 | // 130 | // === function double(ref x) 131 | // ~ x = x * 2 132 | // 133 | // Because when generating the parameters for a function 134 | // to be called, it needs to know ahead of time when 135 | // compiling whether to pass a variable reference or value. 136 | // 137 | var targetFlow = (targetContent as FlowBase); 138 | if (targetFlow != null && targetFlow.arguments != null) 139 | { 140 | foreach(var arg in targetFlow.arguments) { 141 | if(arg.isByReference) 142 | { 143 | Error("Can't store a divert target to a knot or function that has by-reference arguments ('"+targetFlow.identifier+"' has 'ref "+arg.identifier+"')."); 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | // Equals override necessary in order to check for CONST multiple definition equality 151 | public override bool Equals (object obj) 152 | { 153 | var otherDivTarget = obj as DivertTarget; 154 | if (otherDivTarget == null) return false; 155 | 156 | var targetStr = this.divert.target.dotSeparatedComponents; 157 | var otherTargetStr = otherDivTarget.divert.target.dotSeparatedComponents; 158 | 159 | return targetStr.Equals (otherTargetStr); 160 | } 161 | 162 | public override int GetHashCode () 163 | { 164 | var targetStr = this.divert.target.dotSeparatedComponents; 165 | return targetStr.GetHashCode (); 166 | } 167 | 168 | Runtime.DivertTargetValue _runtimeDivertTargetValue; 169 | Runtime.Divert _runtimeDivert; 170 | } 171 | } 172 | 173 | --------------------------------------------------------------------------------