Committed everything

This commit is contained in:
2021-06-30 21:39:19 +10:00
commit fcfa8e7213
525 changed files with 49440 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{
"name": "Ink-Libraries",
"references": [],
"includePlatforms": [],
"excludePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 58bed0e7c5306824586d7eda03609289
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d9d4bbcea2b35784b919f188e0d2c800
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
/// <summary>
/// A class representing a character range. Allows for lazy-loading a corresponding <see cref="CharacterSet">character set</see>.
/// </summary>
public sealed class CharacterRange
{
public static CharacterRange Define(char start, char end, IEnumerable<char> excludes = null)
{
return new CharacterRange (start, end, excludes);
}
/// <summary>
/// Returns a <see cref="CharacterSet">character set</see> instance corresponding to the character range
/// represented by the current instance.
/// </summary>
/// <remarks>
/// The internal character set is created once and cached in memory.
/// </remarks>
/// <returns>The char set.</returns>
public CharacterSet ToCharacterSet ()
{
if (_correspondingCharSet.Count == 0)
{
for (char c = _start; c <= _end; c++)
{
if (!_excludes.Contains (c))
{
_correspondingCharSet.Add (c);
}
}
}
return _correspondingCharSet;
}
public char start { get { return _start; } }
public char end { get { return _end; } }
CharacterRange (char start, char end, IEnumerable<char> excludes)
{
_start = start;
_end = end;
_excludes = excludes == null ? new HashSet<char>() : new HashSet<char> (excludes);
}
char _start;
char _end;
ICollection<char> _excludes;
CharacterSet _correspondingCharSet = new CharacterSet();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3d8ce43e51d74eb791e0665c2ba4fc7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace Ink
{
public class CharacterSet : HashSet<char>
{
public static CharacterSet FromRange(char start, char end)
{
return new CharacterSet ().AddRange (start, end);
}
public CharacterSet ()
{
}
public CharacterSet(string str)
{
AddCharacters (str);
}
public CharacterSet(CharacterSet charSetToCopy)
{
AddCharacters (charSetToCopy);
}
public CharacterSet AddRange(char start, char end)
{
for(char c=start; c<=end; ++c) {
Add (c);
}
return this;
}
public CharacterSet AddCharacters(IEnumerable<char> chars)
{
foreach (char c in chars) {
Add (c);
}
return this;
}
public CharacterSet AddCharacters (string chars)
{
foreach (char c in chars) {
Add (c);
}
return this;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8d8b00ea012047a29ce7e7b4f177835
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
namespace Ink
{
public class CommandLineInput
{
public bool isHelp;
public bool isExit;
public int? choiceInput;
public int? debugSource;
public string debugPathLookup;
public object userImmediateModeStatement;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c05786c433ce449658bbeffe36e74ffd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using Ink;
namespace Ink
{
public class Compiler
{
public class Options
{
public string sourceFilename;
public List<string> pluginNames;
public bool countAllVisits;
public Ink.ErrorHandler errorHandler;
public Ink.IFileHandler fileHandler;
}
public Parsed.Story parsedStory {
get {
return _parsedStory;
}
}
public Compiler (string inkSource, Options options = null)
{
_inputString = inkSource;
_options = options ?? new Options();
if( _options.pluginNames != null )
_pluginManager = new PluginManager (_options.pluginNames);
}
public Parsed.Story Parse()
{
_parser = new InkParser(_inputString, _options.sourceFilename, OnParseError, _options.fileHandler);
_parsedStory = _parser.Parse();
return _parsedStory;
}
public Runtime.Story Compile ()
{
Parse();
if( _pluginManager != null )
_pluginManager.PostParse(_parsedStory);
if (_parsedStory != null && !_hadParseError) {
_parsedStory.countAllVisits = _options.countAllVisits;
_runtimeStory = _parsedStory.ExportRuntime (_options.errorHandler);
if( _pluginManager != null )
_pluginManager.PostExport (_parsedStory, _runtimeStory);
} else {
_runtimeStory = null;
}
return _runtimeStory;
}
public class CommandLineInputResult {
public bool requestsExit;
public int choiceIdx = -1;
public string divertedPath;
public string output;
}
public CommandLineInputResult HandleInput (CommandLineInput inputResult)
{
var result = new CommandLineInputResult ();
// Request for debug source line number
if (inputResult.debugSource != null) {
var offset = (int)inputResult.debugSource;
var dm = DebugMetadataForContentAtOffset (offset);
if (dm != null)
result.output = "DebugSource: " + dm.ToString ();
else
result.output = "DebugSource: Unknown source";
}
// Request for runtime path lookup (to line number)
else if (inputResult.debugPathLookup != null) {
var pathStr = inputResult.debugPathLookup;
var contentResult = _runtimeStory.ContentAtPath (new Runtime.Path (pathStr));
var dm = contentResult.obj.debugMetadata;
if( dm != null )
result.output = "DebugSource: " + dm.ToString ();
else
result.output = "DebugSource: Unknown source";
}
// User entered some ink
else if (inputResult.userImmediateModeStatement != null) {
var parsedObj = inputResult.userImmediateModeStatement as Parsed.Object;
return ExecuteImmediateStatement(parsedObj);
} else {
return null;
}
return result;
}
CommandLineInputResult ExecuteImmediateStatement(Parsed.Object parsedObj) {
var result = new CommandLineInputResult ();
// Variable assignment: create in Parsed.Story as well as the Runtime.Story
// so that we don't get an error message during reference resolution
if (parsedObj is Parsed.VariableAssignment) {
var varAssign = (Parsed.VariableAssignment)parsedObj;
if (varAssign.isNewTemporaryDeclaration) {
_parsedStory.TryAddNewVariableDeclaration (varAssign);
}
}
parsedObj.parent = _parsedStory;
var runtimeObj = parsedObj.runtimeObject;
parsedObj.ResolveReferences (_parsedStory);
if (!_parsedStory.hadError) {
// Divert
if (parsedObj is Parsed.Divert) {
var parsedDivert = parsedObj as Parsed.Divert;
result.divertedPath = parsedDivert.runtimeDivert.targetPath.ToString();
}
// Expression or variable assignment
else if (parsedObj is Parsed.Expression || parsedObj is Parsed.VariableAssignment) {
var evalResult = _runtimeStory.EvaluateExpression ((Runtime.Container)runtimeObj);
if (evalResult != null) {
result.output = evalResult.ToString ();
}
}
} else {
_parsedStory.ResetError ();
}
return result;
}
public void RetrieveDebugSourceForLatestContent ()
{
foreach (var outputObj in _runtimeStory.state.outputStream) {
var textContent = outputObj as Runtime.StringValue;
if (textContent != null) {
var range = new DebugSourceRange ();
range.length = textContent.value.Length;
range.debugMetadata = textContent.debugMetadata;
range.text = textContent.value;
_debugSourceRanges.Add (range);
}
}
}
Runtime.DebugMetadata DebugMetadataForContentAtOffset (int offset)
{
int currOffset = 0;
Runtime.DebugMetadata lastValidMetadata = null;
foreach (var range in _debugSourceRanges) {
if (range.debugMetadata != null)
lastValidMetadata = range.debugMetadata;
if (offset >= currOffset && offset < currOffset + range.length)
return lastValidMetadata;
currOffset += range.length;
}
return null;
}
public struct DebugSourceRange
{
public int length;
public Runtime.DebugMetadata debugMetadata;
public string text;
}
// Need to wrap the error handler so that we know
// when there was a critical error between parse and codegen stages
void OnParseError (string message, ErrorType errorType)
{
if( errorType == ErrorType.Error )
_hadParseError = true;
if (_options.errorHandler != null)
_options.errorHandler (message, errorType);
else
throw new System.Exception(message);
}
string _inputString;
Options _options;
InkParser _parser;
Parsed.Story _parsedStory;
Runtime.Story _runtimeStory;
PluginManager _pluginManager;
bool _hadParseError;
List<DebugSourceRange> _debugSourceRanges = new List<DebugSourceRange> ();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 041ad81bb22094b95bec754d7d480a82
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System.IO;
namespace Ink
{
public interface IFileHandler
{
string ResolveInkFilename (string includeName);
string LoadInkFileContents (string fullFilename);
}
public class DefaultFileHandler : Ink.IFileHandler {
public string ResolveInkFilename (string includeName)
{
var workingDir = Directory.GetCurrentDirectory ();
var fullRootInkPath = Path.Combine (workingDir, includeName);
return fullRootInkPath;
}
public string LoadInkFileContents (string fullFilename)
{
return File.ReadAllText (fullFilename);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: beb7e0e9816ad714290ca610ac174808
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9e0d4431a2a4b447e8ed4b5978110831
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,93 @@

namespace Ink
{
/// <summary>
/// Pre-pass before main ink parser runs. It actually performs two main tasks:
/// - comment elimination to simplify the parse rules in the main parser
/// - Conversion of Windows line endings (\r\n) to the simpler Unix style (\n), so
/// we don't have to worry about them later.
/// </summary>
public class CommentEliminator : StringParser
{
public CommentEliminator (string input) : base(input)
{
}
public string Process()
{
// Make both comments and non-comments optional to handle trivial empty file case (or *only* comments)
var stringList = Interleave<string>(Optional (CommentsAndNewlines), Optional(MainInk));
if (stringList != null) {
return string.Join("", stringList.ToArray());
} else {
return null;
}
}
string MainInk()
{
return ParseUntil (CommentsAndNewlines, _commentOrNewlineStartCharacter, null);
}
string CommentsAndNewlines()
{
var newlines = Interleave<string> (Optional (ParseNewline), Optional (ParseSingleComment));
if (newlines != null) {
return string.Join ("", newlines.ToArray());
} else {
return null;
}
}
// Valid comments always return either an empty string or pure newlines,
// which we want to keep so that line numbers stay the same
string ParseSingleComment()
{
return (string) OneOf (EndOfLineComment, BlockComment);
}
string EndOfLineComment()
{
if (ParseString ("//") == null) {
return null;
}
ParseUntilCharactersFromCharSet (_newlineCharacters);
return "";
}
string BlockComment()
{
if (ParseString ("/*") == null) {
return null;
}
int startLineIndex = lineIndex;
var commentResult = ParseUntil (String("*/"), _commentBlockEndCharacter, null);
if (!endOfInput) {
ParseString ("*/");
}
// Count the number of lines that were inside the block, and replicate them as newlines
// so that the line indexing still works from the original source
if (commentResult != null) {
return new string ('\n', lineIndex - startLineIndex);
}
// No comment at all
else {
return null;
}
}
CharacterSet _commentOrNewlineStartCharacter = new CharacterSet ("/\r\n");
CharacterSet _commentBlockEndCharacter = new CharacterSet("*");
CharacterSet _newlineCharacters = new CharacterSet ("\n\r");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59b3b9369a2a7481aab00ef369b7c4a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Ink
{
public partial class InkParser : StringParser
{
public InkParser(string str, string filenameForMetadata = null, Ink.ErrorHandler externalErrorHandler = null, IFileHandler fileHandler = null)
: this(str, filenameForMetadata, externalErrorHandler, null, fileHandler)
{ }
InkParser(string str, string inkFilename = null, Ink.ErrorHandler externalErrorHandler = null, InkParser rootParser = null, IFileHandler fileHandler = null) : base(str) {
_filename = inkFilename;
RegisterExpressionOperators ();
GenerateStatementLevelRules ();
// Built in handler for all standard parse errors and warnings
this.errorHandler = OnStringParserError;
// The above parse errors are then formatted as strings and passed
// to the Ink.ErrorHandler, or it throws an exception
_externalErrorHandler = externalErrorHandler;
_fileHandler = fileHandler ?? new DefaultFileHandler();
if (rootParser == null) {
_rootParser = this;
_openFilenames = new HashSet<string> ();
if (inkFilename != null) {
var fullRootInkPath = _fileHandler.ResolveInkFilename (inkFilename);
_openFilenames.Add (fullRootInkPath);
}
} else {
_rootParser = rootParser;
}
}
// Main entry point
public Parsed.Story Parse()
{
List<Parsed.Object> topLevelContent = StatementsAtLevel (StatementLevel.Top);
// Note we used to return null if there were any errors, but this would mean
// that include files would return completely empty rather than attempting to
// continue with errors. Returning an empty include files meant that anything
// that *did* compile successfully would otherwise be ignored, generating way
// more errors than necessary.
return new Parsed.Story (topLevelContent, isInclude:_rootParser != this);
}
protected List<T> SeparatedList<T> (SpecificParseRule<T> mainRule, ParseRule separatorRule) where T : class
{
T firstElement = Parse (mainRule);
if (firstElement == null) return null;
var allElements = new List<T> ();
allElements.Add (firstElement);
do {
int nextElementRuleId = BeginRule ();
var sep = separatorRule ();
if (sep == null) {
FailRule (nextElementRuleId);
break;
}
var nextElement = Parse (mainRule);
if (nextElement == null) {
FailRule (nextElementRuleId);
break;
}
SucceedRule (nextElementRuleId);
allElements.Add (nextElement);
} while (true);
return allElements;
}
protected override string PreProcessInputString(string str)
{
var inputWithCommentsRemoved = (new CommentEliminator (str)).Process();
return inputWithCommentsRemoved;
}
protected Runtime.DebugMetadata CreateDebugMetadata(StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
{
var md = new Runtime.DebugMetadata ();
md.startLineNumber = stateAtStart.lineIndex + 1;
md.endLineNumber = stateAtEnd.lineIndex + 1;
md.startCharacterNumber = stateAtStart.characterInLineIndex + 1;
md.endCharacterNumber = stateAtEnd.characterInLineIndex + 1;
md.fileName = _filename;
return md;
}
protected override void RuleDidSucceed(object result, StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
{
// Apply DebugMetadata based on the state at the start of the rule
// (i.e. use line number as it was at the start of the rule)
var parsedObj = result as Parsed.Object;
if ( parsedObj) {
parsedObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
return;
}
// A list of objects that doesn't already have metadata?
var parsedListObjs = result as List<Parsed.Object>;
if (parsedListObjs != null) {
foreach (var parsedListObj in parsedListObjs) {
if (!parsedListObj.hasOwnDebugMetadata) {
parsedListObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
}
}
}
var id = result as Parsed.Identifier;
if (id != null) {
id.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
}
}
protected bool parsingStringExpression
{
get {
return GetFlag ((uint)CustomFlags.ParsingString);
}
set {
SetFlag ((uint)CustomFlags.ParsingString, value);
}
}
protected enum CustomFlags {
ParsingString = 0x1
}
void OnStringParserError(string message, int index, int lineIndex, bool isWarning)
{
var warningType = isWarning ? "WARNING:" : "ERROR:";
string fullMessage;
if (_filename != null) {
fullMessage = string.Format(warningType+" '{0}' line {1}: {2}", _filename, (lineIndex+1), message);
} else {
fullMessage = string.Format(warningType+" line {0}: {1}", (lineIndex+1), message);
}
if (_externalErrorHandler != null) {
_externalErrorHandler (fullMessage, isWarning ? ErrorType.Warning : ErrorType.Error);
} else {
throw new System.Exception (fullMessage);
}
}
IFileHandler _fileHandler;
Ink.ErrorHandler _externalErrorHandler;
string _filename;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97d6f86dbb9994db6bd0572b69f53f25
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected AuthorWarning AuthorWarning()
{
Whitespace ();
var identifier = Parse (IdentifierWithMetadata);
if (identifier == null || identifier.name != "TODO")
return null;
Whitespace ();
ParseString (":");
Whitespace ();
var message = ParseUntilCharactersFromString ("\n\r");
return new AuthorWarning (message);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2f15af905c94ac9bdfb197799eec41
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
using Ink.Parsed;
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
public static readonly CharacterRange LatinBasic =
CharacterRange.Define ('\u0041', '\u007A', excludes: new CharacterSet().AddRange('\u005B', '\u0060'));
public static readonly CharacterRange LatinExtendedA = CharacterRange.Define('\u0100', '\u017F'); // no excludes here
public static readonly CharacterRange LatinExtendedB = CharacterRange.Define('\u0180', '\u024F'); // no excludes here
public static readonly CharacterRange Greek =
CharacterRange.Define('\u0370', '\u03FF', excludes: new CharacterSet().AddRange('\u0378','\u0385').AddCharacters("\u0374\u0375\u0378\u0387\u038B\u038D\u03A2"));
public static readonly CharacterRange Cyrillic =
CharacterRange.Define('\u0400', '\u04FF', excludes: new CharacterSet().AddRange('\u0482', '\u0489'));
public static readonly CharacterRange Armenian =
CharacterRange.Define('\u0530', '\u058F', excludes: new CharacterSet().AddCharacters("\u0530").AddRange('\u0557', '\u0560').AddRange('\u0588', '\u058E'));
public static readonly CharacterRange Hebrew =
CharacterRange.Define('\u0590', '\u05FF', excludes: new CharacterSet());
public static readonly CharacterRange Arabic =
CharacterRange.Define('\u0600', '\u06FF', excludes: new CharacterSet());
public static readonly CharacterRange Korean =
CharacterRange.Define('\uAC00', '\uD7AF', excludes: new CharacterSet());
public static readonly CharacterRange Latin1Supplement =
CharacterRange.Define('\u0080', '\u00FF', excludes: new CharacterSet());
private void ExtendIdentifierCharacterRanges(CharacterSet identifierCharSet)
{
var characterRanges = ListAllCharacterRanges();
foreach (var charRange in characterRanges)
{
identifierCharSet.AddCharacters(charRange.ToCharacterSet());
}
}
/// <summary>
/// Gets an array of <see cref="CharacterRange" /> representing all of the currently supported
/// non-ASCII character ranges that can be used in identifier names.
/// </summary>
/// <returns>
/// An array of <see cref="CharacterRange" /> representing all of the currently supported
/// non-ASCII character ranges that can be used in identifier names.
/// </returns>
public static CharacterRange[] ListAllCharacterRanges() {
return new CharacterRange[] {
LatinBasic,
LatinExtendedA,
LatinExtendedB,
Arabic,
Armenian,
Cyrillic,
Greek,
Hebrew,
Korean,
Latin1Supplement,
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2383f2e1b977347c2b6fb596e9af6d97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
using Ink.Parsed;
using System.Diagnostics;
namespace Ink
{
public partial class InkParser
{
protected Choice Choice()
{
bool onceOnlyChoice = true;
var bullets = Interleave <string>(OptionalExclude(Whitespace), String("*") );
if (bullets == null) {
bullets = Interleave <string>(OptionalExclude(Whitespace), String("+") );
if (bullets == null) {
return null;
}
onceOnlyChoice = false;
}
// Optional name for the choice
Identifier optionalName = Parse(BracketedName);
Whitespace ();
// Optional condition for whether the choice should be shown to the player
Expression conditionExpr = Parse(ChoiceCondition);
Whitespace ();
// Ordinarily we avoid parser state variables like these, since
// nesting would require us to store them in a stack. But since you should
// never be able to nest choices within choice content, it's fine here.
Debug.Assert(_parsingChoice == false, "Already parsing a choice - shouldn't have nested choices");
_parsingChoice = true;
ContentList startContent = null;
var startTextAndLogic = Parse (MixedTextAndLogic);
if (startTextAndLogic != null)
startContent = new ContentList (startTextAndLogic);
ContentList optionOnlyContent = null;
ContentList innerContent = null;
// Check for a the weave style format:
// * "Hello[."]," he said.
bool hasWeaveStyleInlineBrackets = ParseString("[") != null;
if (hasWeaveStyleInlineBrackets) {
var optionOnlyTextAndLogic = Parse (MixedTextAndLogic);
if (optionOnlyTextAndLogic != null)
optionOnlyContent = new ContentList (optionOnlyTextAndLogic);
Expect (String("]"), "closing ']' for weave-style option");
var innerTextAndLogic = Parse (MixedTextAndLogic);
if( innerTextAndLogic != null )
innerContent = new ContentList (innerTextAndLogic);
}
Whitespace ();
// Finally, now we know we're at the end of the main choice body, parse
// any diverts separately.
var diverts = Parse(MultiDivert);
_parsingChoice = false;
Whitespace ();
// Completely empty choice without even an empty divert?
bool emptyContent = !startContent && !innerContent && !optionOnlyContent;
if (emptyContent && diverts == null)
Warning ("Choice is completely empty. Interpretting as a default fallback choice. Add a divert arrow to remove this warning: * ->");
// * [] some text
else if (!startContent && hasWeaveStyleInlineBrackets && !optionOnlyContent)
Warning ("Blank choice - if you intended a default fallback choice, use the `* ->` syntax");
if (!innerContent) innerContent = new ContentList ();
var tags = Parse (Tags);
if (tags != null) {
innerContent.AddContent(tags);
}
// Normal diverts on the end of a choice - simply add to the normal content
if (diverts != null) {
foreach (var divObj in diverts) {
// may be TunnelOnwards
var div = divObj as Divert;
// Empty divert serves no purpose other than to say
// "this choice is intentionally left blank"
// (as an invisible default choice)
if (div && div.isEmpty) continue;
innerContent.AddContent (divObj);
}
}
// Terminate main content with a newline since this is the end of the line
// Note that this will be redundant if the diverts above definitely take
// the flow away permanently.
innerContent.AddContent (new Text ("\n"));
var choice = new Choice (startContent, optionOnlyContent, innerContent);
choice.identifier = optionalName;
choice.indentationDepth = bullets.Count;
choice.hasWeaveStyleInlineBrackets = hasWeaveStyleInlineBrackets;
choice.condition = conditionExpr;
choice.onceOnly = onceOnlyChoice;
choice.isInvisibleDefault = emptyContent;
return choice;
}
protected Expression ChoiceCondition()
{
var conditions = Interleave<Expression> (ChoiceSingleCondition, ChoiceConditionsSpace);
if (conditions == null)
return null;
else if (conditions.Count == 1)
return conditions [0];
else {
return new MultipleConditionExpression (conditions);
}
}
protected object ChoiceConditionsSpace()
{
// Both optional
// Newline includes initial end of line whitespace
Newline ();
Whitespace ();
return ParseSuccess;
}
protected Expression ChoiceSingleCondition()
{
if (ParseString ("{") == null)
return null;
var condExpr = Expect(Expression, "choice condition inside { }") as Expression;
DisallowIncrement (condExpr);
Expect (String ("}"), "closing '}' for choice condition");
return condExpr;
}
protected Gather Gather()
{
object gatherDashCountObj = Parse(GatherDashes);
if (gatherDashCountObj == null) {
return null;
}
int gatherDashCount = (int)gatherDashCountObj;
// Optional name for the gather
Identifier optionalName = Parse(BracketedName);
var gather = new Gather (optionalName, gatherDashCount);
// Optional newline before gather's content begins
Newline ();
return gather;
}
protected object GatherDashes()
{
Whitespace ();
int gatherDashCount = 0;
while (ParseDashNotArrow () != null) {
gatherDashCount++;
Whitespace ();
}
if (gatherDashCount == 0)
return null;
return gatherDashCount;
}
protected object ParseDashNotArrow()
{
var ruleId = BeginRule ();
if (ParseString ("->") == null && ParseSingleCharacter () == '-') {
return SucceedRule (ruleId);
} else {
return FailRule (ruleId);
}
}
protected Identifier BracketedName()
{
if (ParseString ("(") == null)
return null;
Whitespace ();
Identifier name = Parse(IdentifierWithMetadata);
if (name == null)
return null;
Whitespace ();
Expect (String (")"), "closing ')' for bracketed name");
return name;
}
bool _parsingChoice;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea21bfef8977a4907b49628d9b651ebf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,129 @@
namespace Ink
{
public partial class InkParser
{
// Valid returned objects:
// - "help"
// - int: for choice number
// - Parsed.Divert
// - Variable declaration/assignment
// - Epression
// - Lookup debug source for character offset
// - Lookup debug source for runtime path
public CommandLineInput CommandLineUserInput()
{
CommandLineInput result = new CommandLineInput ();
Whitespace ();
if (ParseString ("help") != null) {
result.isHelp = true;
return result;
}
if (ParseString ("exit") != null || ParseString ("quit") != null) {
result.isExit = true;
return result;
}
return (CommandLineInput) OneOf (
DebugSource,
DebugPathLookup,
UserChoiceNumber,
UserImmediateModeStatement
);
}
CommandLineInput DebugSource ()
{
Whitespace ();
if (ParseString ("DebugSource") == null)
return null;
Whitespace ();
var expectMsg = "character offset in parentheses, e.g. DebugSource(5)";
if (Expect (String ("("), expectMsg) == null)
return null;
Whitespace ();
int? characterOffset = ParseInt ();
if (characterOffset == null) {
Error (expectMsg);
return null;
}
Whitespace ();
Expect (String (")"), "closing parenthesis");
var inputStruct = new CommandLineInput ();
inputStruct.debugSource = characterOffset;
return inputStruct;
}
CommandLineInput DebugPathLookup ()
{
Whitespace ();
if (ParseString ("DebugPath") == null)
return null;
if (Whitespace () == null)
return null;
var pathStr = Expect (RuntimePath, "path") as string;
var inputStruct = new CommandLineInput ();
inputStruct.debugPathLookup = pathStr;
return inputStruct;
}
string RuntimePath ()
{
if (_runtimePathCharacterSet == null) {
_runtimePathCharacterSet = new CharacterSet (identifierCharSet);
_runtimePathCharacterSet.Add ('-'); // for c-0, g-0 etc
_runtimePathCharacterSet.Add ('.');
}
return ParseCharactersFromCharSet (_runtimePathCharacterSet);
}
CommandLineInput UserChoiceNumber()
{
Whitespace ();
int? number = ParseInt ();
if (number == null) {
return null;
}
Whitespace ();
if (Parse(EndOfLine) == null) {
return null;
}
var inputStruct = new CommandLineInput ();
inputStruct.choiceInput = number;
return inputStruct;
}
CommandLineInput UserImmediateModeStatement()
{
var statement = OneOf (SingleDivert, TempDeclarationOrAssignment, Expression);
var inputStruct = new CommandLineInput ();
inputStruct.userImmediateModeStatement = statement;
return inputStruct;
}
CharacterSet _runtimePathCharacterSet;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ca27cda0c9364bfbaf37f8db278563f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,288 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected Conditional InnerConditionalContent()
{
var initialQueryExpression = Parse(ConditionExpression);
var conditional = Parse(() => InnerConditionalContent (initialQueryExpression));
if (conditional == null)
return null;
return conditional;
}
protected Conditional InnerConditionalContent(Expression initialQueryExpression)
{
List<ConditionalSingleBranch> alternatives;
bool canBeInline = initialQueryExpression != null;
bool isInline = Parse(Newline) == null;
if (isInline && !canBeInline) {
return null;
}
// Inline innards
if (isInline) {
alternatives = InlineConditionalBranches ();
}
// Multiline innards
else {
alternatives = MultilineConditionalBranches ();
if (alternatives == null) {
// Allow single piece of content within multi-line expression, e.g.:
// { true:
// Some content that isn't preceded by '-'
// }
if (initialQueryExpression) {
List<Parsed.Object> soleContent = StatementsAtLevel (StatementLevel.InnerBlock);
if (soleContent != null) {
var soleBranch = new ConditionalSingleBranch (soleContent);
alternatives = new List<ConditionalSingleBranch> ();
alternatives.Add (soleBranch);
// Also allow a final "- else:" clause
var elseBranch = Parse (SingleMultilineCondition);
if (elseBranch) {
if (!elseBranch.isElse) {
ErrorWithParsedObject ("Expected an '- else:' clause here rather than an extra condition", elseBranch);
elseBranch.isElse = true;
}
alternatives.Add (elseBranch);
}
}
}
// Still null?
if (alternatives == null) {
return null;
}
}
// Empty true branch - didn't get parsed, but should insert one for semantic correctness,
// and to make sure that any evaluation stack values get tidied up correctly.
else if (alternatives.Count == 1 && alternatives [0].isElse && initialQueryExpression) {
var emptyTrueBranch = new ConditionalSingleBranch (null);
emptyTrueBranch.isTrueBranch = true;
alternatives.Insert (0, emptyTrueBranch);
}
// Like a switch statement
// { initialQueryExpression:
// ... match the expression
// }
if (initialQueryExpression) {
bool earlierBranchesHaveOwnExpression = false;
for (int i = 0; i < alternatives.Count; ++i) {
var branch = alternatives [i];
bool isLast = (i == alternatives.Count - 1);
// Matching equality with initial query expression
// We set this flag even for the "else" clause so that
// it knows to tidy up the evaluation stack at the end
// Match query
if (branch.ownExpression) {
branch.matchingEquality = true;
earlierBranchesHaveOwnExpression = true;
}
// Else (final branch)
else if (earlierBranchesHaveOwnExpression && isLast) {
branch.matchingEquality = true;
branch.isElse = true;
}
// Binary condition:
// { trueOrFalse:
// - when true
// - when false
// }
else {
if (!isLast && alternatives.Count > 2) {
ErrorWithParsedObject ("Only final branch can be an 'else'. Did you miss a ':'?", branch);
} else {
if (i == 0)
branch.isTrueBranch = true;
else
branch.isElse = true;
}
}
}
}
// No initial query, so just a multi-line conditional. e.g.:
// {
// - x > 3: greater than three
// - x == 3: equal to three
// - x < 3: less than three
// }
else {
for (int i = 0; i < alternatives.Count; ++i) {
var alt = alternatives [i];
bool isLast = (i == alternatives.Count - 1);
if (alt.ownExpression == null) {
if (isLast) {
alt.isElse = true;
} else {
if (alt.isElse) {
// Do we ALSO have a valid "else" at the end? Let's report the error there.
var finalClause = alternatives [alternatives.Count - 1];
if (finalClause.isElse) {
ErrorWithParsedObject ("Multiple 'else' cases. Can have a maximum of one, at the end.", finalClause);
} else {
ErrorWithParsedObject ("'else' case in conditional should always be the final one", alt);
}
} else {
ErrorWithParsedObject ("Branch doesn't have condition. Are you missing a ':'? ", alt);
}
}
}
}
if (alternatives.Count == 1 && alternatives [0].ownExpression == null) {
ErrorWithParsedObject ("Condition block with no conditions", alternatives [0]);
}
}
}
// TODO: Come up with water-tight error conditions... it's quite a flexible system!
// e.g.
// - inline conditionals must have exactly 1 or 2 alternatives
// - multiline expression shouldn't have mixed existence of branch-conditions?
if (alternatives == null)
return null;
foreach (var branch in alternatives) {
branch.isInline = isInline;
}
var cond = new Conditional (initialQueryExpression, alternatives);
return cond;
}
protected List<ConditionalSingleBranch> InlineConditionalBranches()
{
var listOfLists = Interleave<List<Parsed.Object>> (MixedTextAndLogic, Exclude (String ("|")), flatten: false);
if (listOfLists == null || listOfLists.Count == 0) {
return null;
}
var result = new List<ConditionalSingleBranch> ();
if (listOfLists.Count > 2) {
Error ("Expected one or two alternatives separated by '|' in inline conditional");
} else {
var trueBranch = new ConditionalSingleBranch (listOfLists[0]);
trueBranch.isTrueBranch = true;
result.Add (trueBranch);
if (listOfLists.Count > 1) {
var elseBranch = new ConditionalSingleBranch (listOfLists[1]);
elseBranch.isElse = true;
result.Add (elseBranch);
}
}
return result;
}
protected List<ConditionalSingleBranch> MultilineConditionalBranches()
{
MultilineWhitespace ();
List<object> multipleConditions = OneOrMore (SingleMultilineCondition);
if (multipleConditions == null)
return null;
MultilineWhitespace ();
return multipleConditions.Cast<ConditionalSingleBranch>().ToList();
}
protected ConditionalSingleBranch SingleMultilineCondition()
{
Whitespace ();
// Make sure we're not accidentally parsing a divert
if (ParseString ("->") != null)
return null;
if (ParseString ("-") == null)
return null;
Whitespace ();
Expression expr = null;
bool isElse = Parse(ElseExpression) != null;
if( !isElse )
expr = Parse(ConditionExpression);
List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
if (expr == null && content == null) {
Error ("expected content for the conditional branch following '-'");
// Recover
content = new List<Ink.Parsed.Object> ();
content.Add (new Text (""));
}
// Allow additional multiline whitespace, if the statements were empty (valid)
// then their surrounding multiline whitespacce needs to be handled manually.
// e.g.
// { x:
// - 1: // intentionally left blank, but newline needs to be parsed
// - 2: etc
// }
MultilineWhitespace ();
var branch = new ConditionalSingleBranch (content);
branch.ownExpression = expr;
branch.isElse = isElse;
return branch;
}
protected Expression ConditionExpression()
{
var expr = Parse(Expression);
if (expr == null)
return null;
DisallowIncrement (expr);
Whitespace ();
if (ParseString (":") == null)
return null;
return expr;
}
protected object ElseExpression()
{
if (ParseString ("else") == null)
return null;
Whitespace ();
if (ParseString (":") == null)
return null;
return ParseSuccess;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 334d80c537ee2473ea6a7cbd20e09f14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,217 @@
using Ink.Parsed;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
void TrimEndWhitespace(List<Parsed.Object> mixedTextAndLogicResults, bool terminateWithSpace)
{
// Trim whitespace from end
if (mixedTextAndLogicResults.Count > 0) {
var lastObjIdx = mixedTextAndLogicResults.Count - 1;
var lastObj = mixedTextAndLogicResults[lastObjIdx];
if (lastObj is Text) {
var text = (Text)lastObj;
text.text = text.text.TrimEnd (' ', '\t');
if (terminateWithSpace)
text.text += " ";
// No content left at all? trim the whole object
else if( text.text.Length == 0 ) {
mixedTextAndLogicResults.RemoveAt(lastObjIdx);
// Recurse in case there's more whitespace
TrimEndWhitespace(mixedTextAndLogicResults, terminateWithSpace:false);
}
}
}
}
protected List<Parsed.Object> LineOfMixedTextAndLogic()
{
// Consume any whitespace at the start of the line
// (Except for escaped whitespace)
Parse (Whitespace);
var result = Parse(MixedTextAndLogic);
// Terminating tag
bool onlyTags = false;
var tags = Parse (Tags);
if (tags != null) {
if (result == null) {
result = tags.Cast<Parsed.Object> ().ToList ();
onlyTags = true;
} else {
foreach (var tag in tags) {
result.Add(tag);
}
}
}
if (result == null || result.Count == 0)
return null;
// Warn about accidentally writing "return" without "~"
var firstText = result[0] as Text;
if (firstText) {
if (firstText.text.StartsWith ("return")) {
Warning ("Do you need a '~' before 'return'? If not, perhaps use a glue: <> (since it's lowercase) or rewrite somehow?");
}
}
if (result.Count == 0)
return null;
var lastObj = result [result.Count - 1];
if (!(lastObj is Divert)) {
TrimEndWhitespace (result, terminateWithSpace:false);
}
// Add newline since it's the end of the line
// (so long as it's a line with only tags)
if( !onlyTags )
result.Add (new Text ("\n"));
Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
return result;
}
protected List<Parsed.Object> MixedTextAndLogic()
{
// Check for disallowed "~" within this context
var disallowedTilda = ParseObject(Spaced(String("~")));
if (disallowedTilda != null)
Error ("You shouldn't use a '~' here - tildas are for logic that's on its own line. To do inline logic, use { curly braces } instead");
// Either, or both interleaved
var results = Interleave<Parsed.Object>(Optional (ContentText), Optional (InlineLogicOrGlue));
// Terminating divert?
// (When parsing content for the text of a choice, diverts aren't allowed.
// The divert on the end of the body of a choice is handled specially.)
if (!_parsingChoice) {
var diverts = Parse (MultiDivert);
if (diverts != null) {
// May not have had any results at all if there's *only* a divert!
if (results == null)
results = new List<Parsed.Object> ();
TrimEndWhitespace (results, terminateWithSpace:true);
results.AddRange (diverts);
}
}
if (results == null)
return null;
return results;
}
protected Parsed.Text ContentText()
{
return ContentTextAllowingEcapeChar ();
}
protected Parsed.Text ContentTextAllowingEcapeChar()
{
StringBuilder sb = null;
do {
var str = Parse(ContentTextNoEscape);
bool gotEscapeChar = ParseString(@"\") != null;
if( gotEscapeChar || str != null ) {
if( sb == null ) {
sb = new StringBuilder();
}
if( str != null ) {
sb.Append(str);
}
if( gotEscapeChar ) {
char c = ParseSingleCharacter();
sb.Append(c);
}
} else {
break;
}
} while(true);
if (sb != null ) {
return new Parsed.Text (sb.ToString ());
} else {
return null;
}
}
// Content text is an unusual parse rule compared with most since it's
// less about saying "this is is the small selection of stuff that we parse"
// and more "we parse ANYTHING except this small selection of stuff".
protected string ContentTextNoEscape()
{
// Eat through text, pausing at the following characters, and
// attempt to parse the nonTextRule.
// "-": possible start of divert or start of gather
// "<": possible start of glue
if (_nonTextPauseCharacters == null) {
_nonTextPauseCharacters = new CharacterSet ("-<");
}
// If we hit any of these characters, we stop *immediately* without bothering to even check the nonTextRule
// "{" for start of logic
// "|" for mid logic branch
if (_nonTextEndCharacters == null) {
_nonTextEndCharacters = new CharacterSet ("{}|\n\r\\#");
_notTextEndCharactersChoice = new CharacterSet (_nonTextEndCharacters);
_notTextEndCharactersChoice.AddCharacters ("[]");
_notTextEndCharactersString = new CharacterSet (_nonTextEndCharacters);
_notTextEndCharactersString.AddCharacters ("\"");
}
// When the ParseUntil pauses, check these rules in case they evaluate successfully
ParseRule nonTextRule = () => OneOf (ParseDivertArrow, ParseThreadArrow, EndOfLine, Glue);
CharacterSet endChars = null;
if (parsingStringExpression) {
endChars = _notTextEndCharactersString;
}
else if (_parsingChoice) {
endChars = _notTextEndCharactersChoice;
}
else {
endChars = _nonTextEndCharacters;
}
string pureTextContent = ParseUntil (nonTextRule, _nonTextPauseCharacters, endChars);
if (pureTextContent != null ) {
return pureTextContent;
} else {
return null;
}
}
CharacterSet _nonTextPauseCharacters;
CharacterSet _nonTextEndCharacters;
CharacterSet _notTextEndCharactersChoice;
CharacterSet _notTextEndCharactersString;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8af10c051a6c942b1a43cb31ceb18114
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,206 @@
using System.Collections.Generic;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected List<Parsed.Object> MultiDivert()
{
Whitespace ();
List<Parsed.Object> diverts = null;
// Try single thread first
var threadDivert = Parse(StartThread);
if (threadDivert) {
diverts = new List<Object> ();
diverts.Add (threadDivert);
return diverts;
}
// Normal diverts and tunnels
var arrowsAndDiverts = Interleave<object> (
ParseDivertArrowOrTunnelOnwards,
DivertIdentifierWithArguments);
if (arrowsAndDiverts == null)
return null;
diverts = new List<Parsed.Object> ();
// Possible patterns:
// -> -- explicit gather
// ->-> -- tunnel onwards
// -> div -- normal divert
// ->-> div -- tunnel onwards, followed by override divert
// -> div -> -- normal tunnel
// -> div ->-> -- tunnel then tunnel continue
// -> div -> div -- tunnel then divert
// -> div -> div -> -- tunnel then tunnel
// -> div -> div ->->
// -> div -> div ->-> div (etc)
// Look at the arrows and diverts
for (int i = 0; i < arrowsAndDiverts.Count; ++i) {
bool isArrow = (i % 2) == 0;
// Arrow string
if (isArrow) {
// Tunnel onwards
if ((string)arrowsAndDiverts [i] == "->->") {
bool tunnelOnwardsPlacementValid = (i == 0 || i == arrowsAndDiverts.Count - 1 || i == arrowsAndDiverts.Count - 2);
if (!tunnelOnwardsPlacementValid)
Error ("Tunnel onwards '->->' must only come at the begining or the start of a divert");
var tunnelOnwards = new TunnelOnwards ();
if (i < arrowsAndDiverts.Count - 1) {
var tunnelOnwardDivert = arrowsAndDiverts [i+1] as Parsed.Divert;
tunnelOnwards.divertAfter = tunnelOnwardDivert;
}
diverts.Add (tunnelOnwards);
// Not allowed to do anything after a tunnel onwards.
// If we had anything left it would be caused in the above Error for
// the positioning of a ->->
break;
}
}
// Divert
else {
var divert = arrowsAndDiverts [i] as Divert;
// More to come? (further arrows) Must be tunnelling.
if (i < arrowsAndDiverts.Count - 1) {
divert.isTunnel = true;
}
diverts.Add (divert);
}
}
// Single -> (used for default choices)
if (diverts.Count == 0 && arrowsAndDiverts.Count == 1) {
var gatherDivert = new Divert ((Parsed.Object)null);
gatherDivert.isEmpty = true;
diverts.Add (gatherDivert);
if (!_parsingChoice)
Error ("Empty diverts (->) are only valid on choices");
}
return diverts;
}
protected Divert StartThread()
{
Whitespace ();
if (ParseThreadArrow() == null)
return null;
Whitespace ();
var divert = Expect(DivertIdentifierWithArguments, "target for new thread", () => new Divert(null)) as Divert;
divert.isThread = true;
return divert;
}
protected Divert DivertIdentifierWithArguments()
{
Whitespace ();
List<Identifier> targetComponents = Parse (DotSeparatedDivertPathComponents);
if (targetComponents == null)
return null;
Whitespace ();
var optionalArguments = Parse(ExpressionFunctionCallArguments);
Whitespace ();
var targetPath = new Path (targetComponents);
return new Divert (targetPath, optionalArguments);
}
protected Divert SingleDivert()
{
var diverts = Parse (MultiDivert);
if (diverts == null)
return null;
// Ideally we'd report errors if we get the
// wrong kind of divert, but unfortunately we
// have to hack around the fact that sequences use
// a very similar syntax.
// i.e. if you have a multi-divert at the start
// of a sequence, it initially tries to parse it
// as a divert target (part of an expression of
// a conditional) and gives errors. So instead
// we just have to blindly reject it as a single
// divert, and give a slightly less nice error
// when you DO use a multi divert as a divert taret.
if (diverts.Count != 1) {
return null;
}
var singleDivert = diverts [0];
if (singleDivert is TunnelOnwards) {
return null;
}
var divert = diverts [0] as Divert;
if (divert.isTunnel) {
return null;
}
return divert;
}
List<Identifier> DotSeparatedDivertPathComponents()
{
return Interleave<Identifier> (Spaced (IdentifierWithMetadata), Exclude (String (".")));
}
protected string ParseDivertArrowOrTunnelOnwards()
{
int numArrows = 0;
while (ParseString ("->") != null)
numArrows++;
if (numArrows == 0)
return null;
else if (numArrows == 1)
return "->";
else if (numArrows == 2)
return "->->";
else {
Error ("Unexpected number of arrows in divert. Should only have '->' or '->->'");
return "->->";
}
}
protected string ParseDivertArrow()
{
return ParseString ("->");
}
protected string ParseThreadArrow()
{
return ParseString ("<-");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7aec7c9fb87244b3a8c64753c8a2f9b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,512 @@
using System;
using Ink.Parsed;
using System.Collections.Generic;
namespace Ink
{
public partial class InkParser
{
protected class InfixOperator
{
public string type;
public int precedence;
public bool requireWhitespace;
public InfixOperator(string type, int precedence, bool requireWhitespace) {
this.type = type;
this.precedence = precedence;
this.requireWhitespace = requireWhitespace;
}
public override string ToString ()
{
return type;
}
}
protected Parsed.Object TempDeclarationOrAssignment()
{
Whitespace ();
bool isNewDeclaration = ParseTempKeyword();
Whitespace ();
Identifier varIdentifier = null;
if (isNewDeclaration) {
varIdentifier = (Identifier)Expect (IdentifierWithMetadata, "variable name");
} else {
varIdentifier = Parse(IdentifierWithMetadata);
}
if (varIdentifier == null) {
return null;
}
Whitespace();
// += -=
bool isIncrement = ParseString ("+") != null;
bool isDecrement = ParseString ("-") != null;
if (isIncrement && isDecrement) Error ("Unexpected sequence '+-'");
if (ParseString ("=") == null) {
// Definitely in an assignment expression?
if (isNewDeclaration) Error ("Expected '='");
return null;
}
Expression assignedExpression = (Expression)Expect (Expression, "value expression to be assigned");
if (isIncrement || isDecrement) {
var result = new IncDecExpression (varIdentifier, assignedExpression, isIncrement);
return result;
} else {
var result = new VariableAssignment (varIdentifier, assignedExpression);
result.isNewTemporaryDeclaration = isNewDeclaration;
return result;
}
}
protected void DisallowIncrement (Parsed.Object expr)
{
if (expr is Parsed.IncDecExpression)
Error ("Can't use increment/decrement here. It can only be used on a ~ line");
}
protected bool ParseTempKeyword()
{
var ruleId = BeginRule ();
if (Parse (Identifier) == "temp") {
SucceedRule (ruleId);
return true;
} else {
FailRule (ruleId);
return false;
}
}
protected Parsed.Return ReturnStatement()
{
Whitespace ();
var returnOrDone = Parse(Identifier);
if (returnOrDone != "return") {
return null;
}
Whitespace ();
var expr = Parse(Expression);
var returnObj = new Return (expr);
return returnObj;
}
protected Expression Expression() {
return Expression(minimumPrecedence:0);
}
// Pratt Parser
// aka "Top down operator precedence parser"
// http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
// Algorithm overview:
// The two types of precedence are handled in two different ways:
// ((((a . b) . c) . d) . e) #1
// (a . (b . (c . (d . e)))) #2
// Where #1 is automatically handled by successive loops within the main 'while' in this function,
// so long as continuing operators have lower (or equal) precedence (e.g. imagine some series of "*"s then "+" above.
// ...and #2 is handled by recursion of the right hand term in the binary expression parser.
// (see link for advice on how to extend for postfix and mixfix operators)
protected Expression Expression(int minimumPrecedence)
{
Whitespace ();
// First parse a unary expression e.g. "-a" or parethensised "(1 + 2)"
var expr = ExpressionUnary ();
if (expr == null) {
return null;
}
Whitespace ();
// Attempt to parse (possibly multiple) continuing infix expressions (e.g. 1 + 2 + 3)
while(true) {
var ruleId = BeginRule ();
// Operator
var infixOp = ParseInfixOperator ();
if (infixOp != null && infixOp.precedence > minimumPrecedence) {
// Expect right hand side of operator
var expectationMessage = string.Format("right side of '{0}' expression", infixOp.type);
var multiaryExpr = Expect (() => ExpressionInfixRight (left: expr, op: infixOp), expectationMessage);
if (multiaryExpr == null) {
// Fail for operator and right-hand side of multiary expression
FailRule (ruleId);
return null;
}
expr = SucceedRule(ruleId, multiaryExpr) as Parsed.Expression;
continue;
}
FailRule (ruleId);
break;
}
Whitespace ();
return expr;
}
protected Expression ExpressionUnary()
{
// Divert target is a special case - it can't have any other operators
// applied to it, and we also want to check for it first so that we don't
// confuse "->" for subtraction.
var divertTarget = Parse (ExpressionDivertTarget);
if (divertTarget != null) {
return divertTarget;
}
var prefixOp = (string) OneOf (String ("-"), String ("!"));
// Don't parse like the string rules above, in case its actually
// a variable that simply starts with "not", e.g. "notable".
// This rule uses the Identifier rule, which will scan as much text
// as possible before returning.
if (prefixOp == null) {
prefixOp = Parse(ExpressionNot);
}
Whitespace ();
// - Since we allow numbers at the start of variable names, variable names are checked before literals
// - Function calls before variable names in case we see parentheses
var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression;
// Only recurse immediately if we have one of the (usually optional) unary ops
if (expr == null && prefixOp != null) {
expr = ExpressionUnary ();
}
if (expr == null)
return null;
if (prefixOp != null) {
expr = UnaryExpression.WithInner(expr, prefixOp);
}
Whitespace ();
var postfixOp = (string) OneOf (String ("++"), String ("--"));
if (postfixOp != null) {
bool isInc = postfixOp == "++";
if (!(expr is VariableReference)) {
Error ("can only increment and decrement variables, but saw '" + expr + "'");
// Drop down and succeed without the increment after reporting error
} else {
// TODO: Language Server - (Identifier combined into one vs. list of Identifiers)
var varRef = (VariableReference)expr;
expr = new IncDecExpression(varRef.identifier, isInc);
}
}
return expr;
}
protected string ExpressionNot()
{
var id = Identifier ();
if (id == "not") {
return id;
}
return null;
}
protected Expression ExpressionLiteral()
{
return (Expression) OneOf (ExpressionFloat, ExpressionInt, ExpressionBool, ExpressionString);
}
protected Expression ExpressionDivertTarget()
{
Whitespace ();
var divert = Parse(SingleDivert);
if (divert == null)
return null;
if (divert.isThread)
return null;
Whitespace ();
return new DivertTarget (divert);
}
protected Number ExpressionInt()
{
int? intOrNull = ParseInt ();
if (intOrNull == null) {
return null;
} else {
return new Number (intOrNull.Value);
}
}
protected Number ExpressionFloat()
{
float? floatOrNull = ParseFloat ();
if (floatOrNull == null) {
return null;
} else {
return new Number (floatOrNull.Value);
}
}
protected StringExpression ExpressionString()
{
var openQuote = ParseString ("\"");
if (openQuote == null)
return null;
// Set custom parser state flag so that within the text parser,
// it knows to treat the quote character (") as an end character
parsingStringExpression = true;
List<Parsed.Object> textAndLogic = Parse (MixedTextAndLogic);
Expect (String ("\""), "close quote for string expression");
parsingStringExpression = false;
if (textAndLogic == null) {
textAndLogic = new List<Ink.Parsed.Object> ();
textAndLogic.Add (new Parsed.Text (""));
}
else if (textAndLogic.Exists (c => c is Divert))
Error ("String expressions cannot contain diverts (->)");
return new StringExpression (textAndLogic);
}
protected Number ExpressionBool()
{
var id = Parse(Identifier);
if (id == "true") {
return new Number (true);
} else if (id == "false") {
return new Number (false);
}
return null;
}
protected Expression ExpressionFunctionCall()
{
var iden = Parse(IdentifierWithMetadata);
if (iden == null)
return null;
Whitespace ();
var arguments = Parse(ExpressionFunctionCallArguments);
if (arguments == null) {
return null;
}
return new FunctionCall(iden, arguments);
}
protected List<Expression> ExpressionFunctionCallArguments()
{
if (ParseString ("(") == null)
return null;
// "Exclude" requires the rule to succeed, but causes actual comma string to be excluded from the list of results
ParseRule commas = Exclude (String (","));
var arguments = Interleave<Expression>(Expression, commas);
if (arguments == null) {
arguments = new List<Expression> ();
}
Whitespace ();
Expect (String (")"), "closing ')' for function call");
return arguments;
}
protected Expression ExpressionVariableName()
{
List<Identifier> path = Interleave<Identifier> (IdentifierWithMetadata, Exclude (Spaced (String ("."))));
if (path == null || Story.IsReservedKeyword (path[0].name) )
return null;
return new VariableReference (path);
}
protected Expression ExpressionParen()
{
if (ParseString ("(") == null)
return null;
var innerExpr = Parse(Expression);
if (innerExpr == null)
return null;
Whitespace ();
Expect (String(")"), "closing parenthesis ')' for expression");
return innerExpr;
}
protected Expression ExpressionInfixRight(Parsed.Expression left, InfixOperator op)
{
Whitespace ();
var right = Parse(() => Expression (op.precedence));
if (right) {
// We assume that the character we use for the operator's type is the same
// as that used internally by e.g. Runtime.Expression.Add, Runtime.Expression.Multiply etc
var expr = new BinaryExpression (left, right, op.type);
return expr;
}
return null;
}
private InfixOperator ParseInfixOperator()
{
foreach (var op in _binaryOperators) {
int ruleId = BeginRule ();
if (ParseString (op.type) != null) {
if (op.requireWhitespace) {
if (Whitespace () == null) {
FailRule (ruleId);
continue;
}
}
return (InfixOperator) SucceedRule(ruleId, op);
}
FailRule (ruleId);
}
return null;
}
protected Parsed.List ExpressionList ()
{
Whitespace ();
if (ParseString ("(") == null)
return null;
Whitespace ();
// When list has:
// - 0 elements (null list) - this is okay, it's an empty list: "()"
// - 1 element - it could be confused for a single non-list related
// identifier expression in brackets, but this is a useless thing
// to do, so we reserve that syntax for a list with one item.
// - 2 or more elements - normal!
List<Identifier> memberNames = SeparatedList (ListMember, Spaced (String (",")));
Whitespace ();
// May have failed to parse the inner list - the parentheses may
// be for a normal expression
if (ParseString (")") == null)
return null;
return new List (memberNames);
}
protected Identifier ListMember ()
{
Whitespace ();
Identifier identifier = Parse (IdentifierWithMetadata);
if (identifier == null)
return null;
var dot = ParseString (".");
if (dot != null) {
Identifier identifier2 = Expect (IdentifierWithMetadata, "element name within the set " + identifier) as Identifier;
identifier.name = identifier.name + "." + identifier2?.name;
}
Whitespace ();
return identifier;
}
void RegisterExpressionOperators()
{
_maxBinaryOpLength = 0;
_binaryOperators = new List<InfixOperator> ();
// These will be tried in order, so we need "<=" before "<"
// for correctness
RegisterBinaryOperator ("&&", precedence:1);
RegisterBinaryOperator ("||", precedence:1);
RegisterBinaryOperator ("and", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("or", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("==", precedence:2);
RegisterBinaryOperator (">=", precedence:2);
RegisterBinaryOperator ("<=", precedence:2);
RegisterBinaryOperator ("<", precedence:2);
RegisterBinaryOperator (">", precedence:2);
RegisterBinaryOperator ("!=", precedence:2);
// (apples, oranges) + cabbages has (oranges, cabbages) == true
RegisterBinaryOperator ("?", precedence: 3);
RegisterBinaryOperator ("has", precedence: 3, requireWhitespace:true);
RegisterBinaryOperator ("!?", precedence: 3);
RegisterBinaryOperator ("hasnt", precedence: 3, requireWhitespace: true);
RegisterBinaryOperator ("^", precedence: 3);
RegisterBinaryOperator ("+", precedence:4);
RegisterBinaryOperator ("-", precedence:5);
RegisterBinaryOperator ("*", precedence:6);
RegisterBinaryOperator ("/", precedence:7);
RegisterBinaryOperator ("%", precedence:8);
RegisterBinaryOperator ("mod", precedence:8, requireWhitespace:true);
}
void RegisterBinaryOperator(string op, int precedence, bool requireWhitespace = false)
{
_binaryOperators.Add(new InfixOperator (op, precedence, requireWhitespace));
_maxBinaryOpLength = Math.Max (_maxBinaryOpLength, op.Length);
}
List<InfixOperator> _binaryOperators;
int _maxBinaryOpLength;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85c9eb24bda894235981df7a3689da51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using Ink.Parsed;
using System.Collections.Generic;
using System.IO;
namespace Ink
{
public partial class InkParser
{
protected object IncludeStatement()
{
Whitespace ();
if (ParseString ("INCLUDE") == null)
return null;
Whitespace ();
var filename = (string) Expect(() => ParseUntilCharactersFromString ("\n\r"), "filename for include statement");
filename = filename.TrimEnd (' ', '\t');
// Working directory should already have been set up relative to the root ink file.
var fullFilename = _rootParser._fileHandler.ResolveInkFilename (filename);
if (FilenameIsAlreadyOpen (fullFilename)) {
Error ("Recursive INCLUDE detected: '" + fullFilename + "' is already open.");
ParseUntilCharactersFromString("\r\n");
return new IncludedFile(null);
} else {
AddOpenFilename (fullFilename);
}
Parsed.Story includedStory = null;
string includedString = null;
try {
includedString = _rootParser._fileHandler.LoadInkFileContents(fullFilename);
}
catch {
Error ("Failed to load: '"+filename+"'");
}
if (includedString != null ) {
InkParser parser = new InkParser(includedString, filename, _externalErrorHandler, _rootParser);
includedStory = parser.Parse();
}
RemoveOpenFilename (fullFilename);
// Return valid IncludedFile object even if there were errors when parsing.
// We don't want to attempt to re-parse the include line as something else,
// and we want to include the bits that *are* valid, so we don't generate
// more errors than necessary.
return new IncludedFile (includedStory);
}
bool FilenameIsAlreadyOpen(string fullFilename)
{
return _rootParser._openFilenames.Contains (fullFilename);
}
void AddOpenFilename(string fullFilename)
{
_rootParser._openFilenames.Add (fullFilename);
}
void RemoveOpenFilename(string fullFilename)
{
_rootParser._openFilenames.Remove (fullFilename);
}
InkParser _rootParser;
HashSet<string> _openFilenames;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4b92fc2dbd664298b01416321678d61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,239 @@
using System.Collections.Generic;
using Ink.Parsed;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
protected class NameWithMetadata {
public string name;
public Runtime.DebugMetadata metadata;
}
protected class FlowDecl
{
public Identifier name;
public List<FlowBase.Argument> arguments;
public bool isFunction;
}
protected Knot KnotDefinition()
{
var knotDecl = Parse(KnotDeclaration);
if (knotDecl == null)
return null;
Expect(EndOfLine, "end of line after knot name definition", recoveryRule: SkipToNextLine);
ParseRule innerKnotStatements = () => StatementsAtLevel (StatementLevel.Knot);
var content = Expect (innerKnotStatements, "at least one line within the knot", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
return new Knot (knotDecl.name, content, knotDecl.arguments, knotDecl.isFunction);
}
protected FlowDecl KnotDeclaration()
{
Whitespace ();
if (KnotTitleEquals () == null)
return null;
Whitespace ();
Identifier identifier = Parse(IdentifierWithMetadata);
Identifier knotName;
bool isFunc = identifier?.name == "function";
if (isFunc) {
Expect (Whitespace, "whitespace after the 'function' keyword");
knotName = Parse(IdentifierWithMetadata);
} else {
knotName = identifier;
}
if (knotName == null) {
Error ("Expected the name of the " + (isFunc ? "function" : "knot"));
knotName = new Identifier { name = "" }; // prevent later null ref
}
Whitespace ();
List<FlowBase.Argument> parameterNames = Parse (BracketedKnotDeclArguments);
Whitespace ();
// Optional equals after name
Parse(KnotTitleEquals);
return new FlowDecl () { name = knotName, arguments = parameterNames, isFunction = isFunc };
}
protected string KnotTitleEquals()
{
// 2+ "=" starts a knot
var multiEquals = ParseCharactersFromString ("=");
if (multiEquals == null || multiEquals.Length <= 1) {
return null;
} else {
return multiEquals;
}
}
protected object StitchDefinition()
{
var decl = Parse(StitchDeclaration);
if (decl == null)
return null;
Expect(EndOfLine, "end of line after stitch name", recoveryRule: SkipToNextLine);
ParseRule innerStitchStatements = () => StatementsAtLevel (StatementLevel.Stitch);
var content = Expect(innerStitchStatements, "at least one line within the stitch", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
return new Stitch (decl.name, content, decl.arguments, decl.isFunction );
}
protected FlowDecl StitchDeclaration()
{
Whitespace ();
// Single "=" to define a stitch
if (ParseString ("=") == null)
return null;
// If there's more than one "=", that's actually a knot definition (or divert), so this rule should fail
if (ParseString ("=") != null)
return null;
Whitespace ();
// Stitches aren't allowed to be functions, but we parse it anyway and report the error later
bool isFunc = ParseString ("function") != null;
if ( isFunc ) {
Whitespace ();
}
Identifier stitchName = Parse(IdentifierWithMetadata);
if (stitchName == null)
return null;
Whitespace ();
List<FlowBase.Argument> flowArgs = Parse(BracketedKnotDeclArguments);
Whitespace ();
return new FlowDecl () { name = stitchName, arguments = flowArgs, isFunction = isFunc };
}
protected object KnotStitchNoContentRecoveryRule()
{
// Jump ahead to the next knot or the end of the file
ParseUntil (KnotDeclaration, new CharacterSet ("="), null);
var recoveredFlowContent = new List<Parsed.Object>();
recoveredFlowContent.Add( new Parsed.Text("<ERROR IN FLOW>" ) );
return recoveredFlowContent;
}
protected List<FlowBase.Argument> BracketedKnotDeclArguments()
{
if (ParseString ("(") == null)
return null;
var flowArguments = Interleave<FlowBase.Argument>(Spaced(FlowDeclArgument), Exclude (String(",")));
Expect (String (")"), "closing ')' for parameter list");
// If no parameters, create an empty list so that this method is type safe and
// doesn't attempt to return the ParseSuccess object
if (flowArguments == null) {
flowArguments = new List<FlowBase.Argument> ();
}
return flowArguments;
}
protected FlowBase.Argument FlowDeclArgument()
{
// Possible forms:
// name
// -> name (variable divert target argument
// ref name
// ref -> name (variable divert target by reference)
var firstIden = Parse(IdentifierWithMetadata);
Whitespace ();
var divertArrow = ParseDivertArrow ();
Whitespace ();
var secondIden = Parse(IdentifierWithMetadata);
if (firstIden == null && secondIden == null)
return null;
var flowArg = new FlowBase.Argument ();
if (divertArrow != null) {
flowArg.isDivertTarget = true;
}
// Passing by reference
if (firstIden != null && firstIden.name == "ref") {
if (secondIden == null) {
Error ("Expected an parameter name after 'ref'");
}
flowArg.identifier = secondIden;
flowArg.isByReference = true;
}
// Simple argument name
else {
if (flowArg.isDivertTarget) {
flowArg.identifier = secondIden;
} else {
flowArg.identifier = firstIden;
}
if (flowArg.identifier == null) {
Error ("Expected an parameter name");
}
flowArg.isByReference = false;
}
return flowArg;
}
protected ExternalDeclaration ExternalDeclaration()
{
Whitespace ();
Identifier external = Parse(IdentifierWithMetadata);
if (external == null || external.name != "EXTERNAL")
return null;
Whitespace ();
var funcIdentifier = Expect(IdentifierWithMetadata, "name of external function") as Identifier ?? new Identifier();
Whitespace ();
var parameterNames = Expect (BracketedKnotDeclArguments, "declaration of arguments for EXTERNAL, even if empty, i.e. 'EXTERNAL "+funcIdentifier+"()'") as List<FlowBase.Argument>;
if (parameterNames == null)
parameterNames = new List<FlowBase.Argument> ();
var argNames = parameterNames.Select (arg => arg.identifier?.name).ToList();
return new ExternalDeclaration (funcIdentifier, argNames);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 77e8119b05a284a889e4b2978c6b1617
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,421 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected Parsed.Object LogicLine()
{
Whitespace ();
if (ParseString ("~") == null) {
return null;
}
Whitespace ();
// Some example lines we need to be able to distinguish between:
// ~ temp x = 5 -- var decl + assign
// ~ temp x -- var decl
// ~ x = 5 -- var assign
// ~ x -- expr (not var decl or assign)
// ~ f() -- expr
// We don't treat variable decl/assign as an expression since we don't want an assignment
// to have a return value, or to be used in compound expressions.
ParseRule afterTilda = () => OneOf (ReturnStatement, TempDeclarationOrAssignment, Expression);
var result = Expect(afterTilda, "expression after '~'", recoveryRule: SkipToNextLine) as Parsed.Object;
// Prevent further errors, already reported expected expression and have skipped to next line.
if (result == null) return new ContentList();
// Parse all expressions, but tell the writer off if they did something useless like:
// ~ 5 + 4
// And even:
// ~ false && myFunction()
// ...since it's bad practice, and won't do what they expect if
// they're expecting C's lazy evaluation.
if (result is Expression && !(result is FunctionCall || result is IncDecExpression) ) {
// TODO: Remove this specific error message when it has expired in usefulness
var varRef = result as VariableReference;
if (varRef && varRef.name == "include") {
Error ("'~ include' is no longer the correct syntax - please use 'INCLUDE your_filename.ink', without the tilda, and in block capitals.");
}
else {
Error ("Logic following a '~' can't be that type of expression. It can only be something like:\n\t~ return\n\t~ var x = blah\n\t~ x++\n\t~ myFunction()");
}
}
// Line is pure function call? e.g.
// ~ f()
// Add extra pop to make sure we tidy up after ourselves.
// We no longer need anything on the evaluation stack.
var funCall = result as FunctionCall;
if (funCall) funCall.shouldPopReturnedValue = true;
// If the expression contains a function call, then it could produce a text side effect,
// in which case it needs a newline on the end. e.g.
// ~ printMyName()
// ~ x = 1 + returnAValueAndAlsoPrintStuff()
// If no text gets printed, then the extra newline will have to be culled later.
// Multiple newlines on the output will be removed, so there will be no "leak" for
// long running calculations. It's disappointingly messy though :-/
if (result.Find<FunctionCall>() != null ) {
result = new ContentList (result, new Parsed.Text ("\n"));
}
Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
return result as Parsed.Object;
}
protected Parsed.Object VariableDeclaration()
{
Whitespace ();
var id = Parse (Identifier);
if (id != "VAR")
return null;
Whitespace ();
var varName = Expect (IdentifierWithMetadata, "variable name") as Identifier;
Whitespace ();
Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)");
Whitespace ();
var definition = Expect (Expression, "initial value for ");
var expr = definition as Parsed.Expression;
if (expr) {
if (!(expr is Number || expr is StringExpression || expr is DivertTarget || expr is VariableReference || expr is List)) {
Error ("initial value for a variable must be a number, constant, list or divert target");
}
if (Parse (ListElementDefinitionSeparator) != null)
Error ("Unexpected ','. If you're trying to declare a new list, use the LIST keyword, not VAR");
// Ensure string expressions are simple
else if (expr is StringExpression) {
var strExpr = expr as StringExpression;
if (!strExpr.isSingleString)
Error ("Constant strings cannot contain any logic.");
}
var result = new VariableAssignment (varName, expr);
result.isGlobalDeclaration = true;
return result;
}
return null;
}
protected Parsed.VariableAssignment ListDeclaration ()
{
Whitespace ();
var id = Parse (Identifier);
if (id != "LIST")
return null;
Whitespace ();
var varName = Expect (IdentifierWithMetadata, "list name") as Identifier;
Whitespace ();
Expect (String ("="), "the '=' for an assignment of the list definition");
Whitespace ();
var definition = Expect (ListDefinition, "list item names") as ListDefinition;
if (definition) {
definition.identifier = varName;
return new VariableAssignment (varName, definition);
}
return null;
}
protected Parsed.ListDefinition ListDefinition ()
{
AnyWhitespace ();
var allElements = SeparatedList (ListElementDefinition, ListElementDefinitionSeparator);
if (allElements == null)
return null;
return new ListDefinition (allElements);
}
protected string ListElementDefinitionSeparator ()
{
AnyWhitespace ();
if (ParseString (",") == null) return null;
AnyWhitespace ();
return ",";
}
protected Parsed.ListElementDefinition ListElementDefinition ()
{
var inInitialList = ParseString ("(") != null;
var needsToCloseParen = inInitialList;
Whitespace ();
var name = Parse (IdentifierWithMetadata);
if (name == null)
return null;
Whitespace ();
if (inInitialList) {
if (ParseString (")") != null) {
needsToCloseParen = false;
Whitespace ();
}
}
int? elementValue = null;
if (ParseString ("=") != null) {
Whitespace ();
var elementValueNum = Expect (ExpressionInt, "value to be assigned to list item") as Number;
if (elementValueNum != null) {
elementValue = (int) elementValueNum.value;
}
if (needsToCloseParen) {
Whitespace ();
if (ParseString (")") != null)
needsToCloseParen = false;
}
}
if (needsToCloseParen)
Error("Expected closing ')'");
return new ListElementDefinition (name, inInitialList, elementValue);
}
protected Parsed.Object ConstDeclaration()
{
Whitespace ();
var id = Parse (Identifier);
if (id != "CONST")
return null;
Whitespace ();
var varName = Expect (IdentifierWithMetadata, "constant name") as Identifier;
Whitespace ();
Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)");
Whitespace ();
var expr = Expect (Expression, "initial value for ") as Parsed.Expression;
if (!(expr is Number || expr is DivertTarget || expr is StringExpression)) {
Error ("initial value for a constant must be a number or divert target");
}
// Ensure string expressions are simple
else if (expr is StringExpression) {
var strExpr = expr as StringExpression;
if (!strExpr.isSingleString)
Error ("Constant strings cannot contain any logic.");
}
var result = new ConstantDeclaration (varName, expr);
return result;
}
protected Parsed.Object InlineLogicOrGlue()
{
return (Parsed.Object) OneOf (InlineLogic, Glue);
}
protected Parsed.Glue Glue()
{
// Don't want to parse whitespace, since it might be important
// surrounding the glue.
var glueStr = ParseString("<>");
if (glueStr != null) {
return new Parsed.Glue (new Runtime.Glue ());
} else {
return null;
}
}
protected Parsed.Object InlineLogic()
{
if ( ParseString ("{") == null) {
return null;
}
Whitespace ();
var logic = (Parsed.Object) Expect(InnerLogic, "some kind of logic, conditional or sequence within braces: { ... }");
if (logic == null)
return null;
DisallowIncrement (logic);
ContentList contentList = logic as ContentList;
if (!contentList) {
contentList = new ContentList (logic);
}
Whitespace ();
Expect (String("}"), "closing brace '}' for inline logic");
return contentList;
}
protected Parsed.Object InnerLogic()
{
Whitespace ();
// Explicitly try the combinations of inner logic
// that could potentially have conflicts first.
// Explicit sequence annotation?
SequenceType? explicitSeqType = (SequenceType?) ParseObject(SequenceTypeAnnotation);
if (explicitSeqType != null) {
var contentLists = (List<ContentList>) Expect(InnerSequenceObjects, "sequence elements (for cycle/stoping etc)");
if (contentLists == null)
return null;
return new Sequence (contentLists, (SequenceType) explicitSeqType);
}
// Conditional with expression?
var initialQueryExpression = Parse(ConditionExpression);
if (initialQueryExpression) {
var conditional = (Conditional) Expect(() => InnerConditionalContent (initialQueryExpression), "conditional content following query");
return conditional;
}
// Now try to evaluate each of the "full" rules in turn
ParseRule[] rules = {
// Conditional still necessary, since you can have a multi-line conditional
// without an initial query expression:
// {
// - true: this is true
// - false: this is false
// }
InnerConditionalContent,
InnerSequence,
InnerExpression,
};
// Adapted from "OneOf" structuring rule except that in
// order for the rule to succeed, it has to maximally
// cover the entire string within the { }. Used to
// differentiate between:
// {myVar} -- Expression (try first)
// {my content is jolly} -- sequence with single element
foreach (ParseRule rule in rules) {
int ruleId = BeginRule ();
Parsed.Object result = ParseObject(rule) as Parsed.Object;
if (result) {
// Not yet at end?
if (Peek (Spaced (String ("}"))) == null)
FailRule (ruleId);
// Full parse of content within braces
else
return (Parsed.Object) SucceedRule (ruleId, result);
} else {
FailRule (ruleId);
}
}
return null;
}
protected Parsed.Object InnerExpression()
{
var expr = Parse(Expression);
if (expr) {
expr.outputWhenComplete = true;
}
return expr;
}
protected Identifier IdentifierWithMetadata()
{
var id = Identifier();
if( id == null ) return null;
// InkParser.RuleDidSucceed will add DebugMetadata
return new Identifier { name = id, debugMetadata = null };
}
// Note: we allow identifiers that start with a number,
// but not if they *only* comprise numbers
protected string Identifier()
{
// Parse remaining characters (if any)
var name = ParseCharactersFromCharSet (identifierCharSet);
if (name == null)
return null;
// Reject if it's just a number
bool isNumberCharsOnly = true;
foreach (var c in name) {
if ( !(c >= '0' && c <= '9') ) {
isNumberCharsOnly = false;
break;
}
}
if (isNumberCharsOnly) {
return null;
}
return name;
}
CharacterSet identifierCharSet {
get {
if (_identifierCharSet == null) {
(_identifierCharSet = new CharacterSet ())
.AddRange ('A', 'Z')
.AddRange ('a', 'z')
.AddRange ('0', '9')
.Add ('_');
// Enable non-ASCII characters for story identifiers.
ExtendIdentifierCharacterRanges (_identifierCharSet);
}
return _identifierCharSet;
}
}
private CharacterSet _identifierCharSet;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa9a6c9127e3f4eb8a46f532557ccd2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,231 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected Sequence InnerSequence()
{
Whitespace ();
// Default sequence type
SequenceType seqType = SequenceType.Stopping;
// Optional explicit sequence type
SequenceType? parsedSeqType = (SequenceType?) Parse(SequenceTypeAnnotation);
if (parsedSeqType != null)
seqType = parsedSeqType.Value;
var contentLists = Parse(InnerSequenceObjects);
if (contentLists == null || contentLists.Count <= 1) {
return null;
}
return new Sequence (contentLists, seqType);
}
protected object SequenceTypeAnnotation()
{
var annotation = (SequenceType?) Parse(SequenceTypeSymbolAnnotation);
if(annotation == null)
annotation = (SequenceType?) Parse(SequenceTypeWordAnnotation);
if (annotation == null)
return null;
switch (annotation.Value)
{
case SequenceType.Once:
case SequenceType.Cycle:
case SequenceType.Stopping:
case SequenceType.Shuffle:
case (SequenceType.Shuffle | SequenceType.Stopping):
case (SequenceType.Shuffle | SequenceType.Once):
break;
default:
Error("Sequence type combination not supported: " + annotation.Value);
return SequenceType.Stopping;
}
return annotation;
}
protected object SequenceTypeSymbolAnnotation()
{
if(_sequenceTypeSymbols == null )
_sequenceTypeSymbols = new CharacterSet("!&~$ ");
var sequenceType = (SequenceType)0;
var sequenceAnnotations = ParseCharactersFromCharSet(_sequenceTypeSymbols);
if (sequenceAnnotations == null)
return null;
foreach(char symbolChar in sequenceAnnotations) {
switch(symbolChar) {
case '!': sequenceType |= SequenceType.Once; break;
case '&': sequenceType |= SequenceType.Cycle; break;
case '~': sequenceType |= SequenceType.Shuffle; break;
case '$': sequenceType |= SequenceType.Stopping; break;
}
}
if (sequenceType == (SequenceType)0)
return null;
return sequenceType;
}
CharacterSet _sequenceTypeSymbols = new CharacterSet("!&~$");
protected object SequenceTypeWordAnnotation()
{
var sequenceTypes = Interleave<SequenceType?>(SequenceTypeSingleWord, Exclude(Whitespace));
if (sequenceTypes == null || sequenceTypes.Count == 0)
return null;
if (ParseString (":") == null)
return null;
var combinedSequenceType = (SequenceType)0;
foreach(var seqType in sequenceTypes) {
combinedSequenceType |= seqType.Value;
}
return combinedSequenceType;
}
protected object SequenceTypeSingleWord()
{
SequenceType? seqType = null;
var word = Parse(IdentifierWithMetadata);
if (word != null)
{
switch (word.name)
{
case "once":
seqType = SequenceType.Once;
break;
case "cycle":
seqType = SequenceType.Cycle;
break;
case "shuffle":
seqType = SequenceType.Shuffle;
break;
case "stopping":
seqType = SequenceType.Stopping;
break;
}
}
if (seqType == null)
return null;
return seqType;
}
protected List<ContentList> InnerSequenceObjects()
{
var multiline = Parse(Newline) != null;
List<ContentList> result = null;
if (multiline) {
result = Parse(InnerMultilineSequenceObjects);
} else {
result = Parse(InnerInlineSequenceObjects);
}
return result;
}
protected List<ContentList> InnerInlineSequenceObjects()
{
var interleavedContentAndPipes = Interleave<object> (Optional (MixedTextAndLogic), String ("|"), flatten:false);
if (interleavedContentAndPipes == null)
return null;
var result = new List<ContentList> ();
// The content and pipes won't necessarily be perfectly interleaved in the sense that
// the content can be missing, but in that case it's intended that there's blank content.
bool justHadContent = false;
foreach (object contentOrPipe in interleavedContentAndPipes) {
// Pipe/separator
if (contentOrPipe as string == "|") {
// Expected content, saw pipe - need blank content now
if (!justHadContent) {
// Add blank content
result.Add (new ContentList ());
}
justHadContent = false;
}
// Real content
else {
var content = contentOrPipe as List<Parsed.Object>;
if (content == null) {
Error ("Expected content, but got " + contentOrPipe + " (this is an ink compiler bug!)");
} else {
result.Add (new ContentList (content));
}
justHadContent = true;
}
}
// Ended in a pipe? Need to insert final blank content
if (!justHadContent)
result.Add (new ContentList ());
return result;
}
protected List<ContentList> InnerMultilineSequenceObjects()
{
MultilineWhitespace ();
var contentLists = OneOrMore (SingleMultilineSequenceElement);
if (contentLists == null)
return null;
return contentLists.Cast<ContentList> ().ToList();
}
protected ContentList SingleMultilineSequenceElement()
{
Whitespace ();
// Make sure we're not accidentally parsing a divert
if (ParseString ("->") != null)
return null;
if (ParseString ("-") == null)
return null;
Whitespace ();
List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
if (content == null)
MultilineWhitespace ();
// Add newline at the start of each branch
else {
content.Insert (0, new Parsed.Text ("\n"));
}
return new ContentList (content);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e33e10232e5c4587a8c460cf9d99960
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected enum StatementLevel
{
InnerBlock,
Stitch,
Knot,
Top
}
protected List<Parsed.Object> StatementsAtLevel(StatementLevel level)
{
// Check for error: Should not be allowed gather dashes within an inner block
if (level == StatementLevel.InnerBlock) {
object badGatherDashCount = Parse(GatherDashes);
if (badGatherDashCount != null) {
Error ("You can't use a gather (the dashes) within the { curly braces } context. For multi-line sequences and conditions, you should only use one dash.");
}
}
return Interleave<Parsed.Object>(
Optional (MultilineWhitespace),
() => StatementAtLevel (level),
untilTerminator: () => StatementsBreakForLevel(level));
}
protected object StatementAtLevel(StatementLevel level)
{
ParseRule[] rulesAtLevel = _statementRulesAtLevel[(int)level];
var statement = OneOf (rulesAtLevel);
// For some statements, allow them to parse, but create errors, since
// writers may think they can use the statement, so it's useful to have
// the error message.
if (level == StatementLevel.Top) {
if( statement is Return )
Error ("should not have return statement outside of a knot");
}
return statement;
}
protected object StatementsBreakForLevel(StatementLevel level)
{
Whitespace ();
ParseRule[] breakRules = _statementBreakRulesAtLevel[(int)level];
var breakRuleResult = OneOf (breakRules);
if (breakRuleResult == null)
return null;
return breakRuleResult;
}
void GenerateStatementLevelRules()
{
var levels = Enum.GetValues (typeof(StatementLevel)).Cast<StatementLevel> ().ToList();
_statementRulesAtLevel = new ParseRule[levels.Count][];
_statementBreakRulesAtLevel = new ParseRule[levels.Count][];
foreach (var level in levels) {
List<ParseRule> rulesAtLevel = new List<ParseRule> ();
List<ParseRule> breakingRules = new List<ParseRule> ();
// Diverts can go anywhere
rulesAtLevel.Add(Line(MultiDivert));
// Knots can only be parsed at Top/Global scope
if (level >= StatementLevel.Top)
rulesAtLevel.Add (KnotDefinition);
rulesAtLevel.Add(Line(Choice));
rulesAtLevel.Add(Line(AuthorWarning));
// Gather lines would be confused with multi-line block separators, like
// within a multi-line if statement
if (level > StatementLevel.InnerBlock) {
rulesAtLevel.Add (Gather);
}
// Stitches (and gathers) can (currently) only go in Knots and top level
if (level >= StatementLevel.Knot) {
rulesAtLevel.Add (StitchDefinition);
}
// Global variable declarations can go anywhere
rulesAtLevel.Add(Line(ListDeclaration));
rulesAtLevel.Add(Line(VariableDeclaration));
rulesAtLevel.Add(Line(ConstDeclaration));
rulesAtLevel.Add(Line(ExternalDeclaration));
// Global include can go anywhere
rulesAtLevel.Add(Line(IncludeStatement));
// Normal logic / text can go anywhere
rulesAtLevel.Add(LogicLine);
rulesAtLevel.Add(LineOfMixedTextAndLogic);
// --------
// Breaking rules
// Break current knot with a new knot
if (level <= StatementLevel.Knot) {
breakingRules.Add (KnotDeclaration);
}
// Break current stitch with a new stitch
if (level <= StatementLevel.Stitch) {
breakingRules.Add (StitchDeclaration);
}
// Breaking an inner block (like a multi-line condition statement)
if (level <= StatementLevel.InnerBlock) {
breakingRules.Add (ParseDashNotArrow);
breakingRules.Add (String ("}"));
}
_statementRulesAtLevel [(int)level] = rulesAtLevel.ToArray ();
_statementBreakRulesAtLevel [(int)level] = breakingRules.ToArray ();
}
}
protected object SkipToNextLine()
{
ParseUntilCharactersFromString ("\n\r");
ParseNewline ();
return ParseSuccess;
}
// Modifier to turn a rule into one that expects a newline on the end.
// e.g. anywhere you can use "MixedTextAndLogic" as a rule, you can use
// "Line(MixedTextAndLogic)" to specify that it expects a newline afterwards.
protected ParseRule Line(ParseRule inlineRule)
{
return () => {
object result = ParseObject(inlineRule);
if (result == null) {
return null;
}
Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
return result;
};
}
ParseRule[][] _statementRulesAtLevel;
ParseRule[][] _statementBreakRulesAtLevel;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42fdd6da57c584bf58ff578d093b8529
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
protected Parsed.Tag Tag ()
{
Whitespace ();
if (ParseString ("#") == null)
return null;
Whitespace ();
var sb = new StringBuilder ();
do {
// Read up to another #, end of input or newline
string tagText = ParseUntilCharactersFromCharSet (_endOfTagCharSet);
sb.Append (tagText);
// Escape character
if (ParseString ("\\") != null) {
char c = ParseSingleCharacter ();
if( c != (char)0 ) sb.Append(c);
continue;
}
break;
} while ( true );
var fullTagText = sb.ToString ().Trim();
return new Parsed.Tag (new Runtime.Tag (fullTagText));
}
protected List<Parsed.Tag> Tags ()
{
var tags = OneOrMore (Tag);
if (tags == null) return null;
return tags.Cast<Parsed.Tag>().ToList();
}
CharacterSet _endOfTagCharSet = new CharacterSet ("#\n\r\\");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fed5929e88fa3480e8f02d3f686c0c2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,112 @@
using System.Collections.Generic;
namespace Ink
{
public partial class InkParser
{
// Handles both newline and endOfFile
protected object EndOfLine()
{
return OneOf(Newline, EndOfFile);
}
// Allow whitespace before the actual newline
protected object Newline()
{
Whitespace();
bool gotNewline = ParseNewline () != null;
// Optional \r, definite \n to support Windows (\r\n) and Mac/Unix (\n)
if( !gotNewline ) {
return null;
} else {
return ParseSuccess;
}
}
protected object EndOfFile()
{
Whitespace();
if (!endOfInput)
return null;
return ParseSuccess;
}
// General purpose space, returns N-count newlines (fails if no newlines)
protected object MultilineWhitespace()
{
List<object> newlines = OneOrMore(Newline);
if (newlines == null)
return null;
// Use content field of Token to say how many newlines there were
// (in most circumstances it's unimportant)
int numNewlines = newlines.Count;
if (numNewlines >= 1) {
return ParseSuccess;
} else {
return null;
}
}
protected object Whitespace()
{
if( ParseCharactersFromCharSet(_inlineWhitespaceChars) != null ) {
return ParseSuccess;
}
return null;
}
protected ParseRule Spaced(ParseRule rule)
{
return () => {
Whitespace ();
var result = ParseObject(rule);
if (result == null) {
return null;
}
Whitespace ();
return result;
};
}
protected object AnyWhitespace ()
{
bool anyWhitespace = false;
while (OneOf (Whitespace, MultilineWhitespace) != null) {
anyWhitespace = true;
}
return anyWhitespace ? ParseSuccess : null;
}
protected ParseRule MultiSpaced (ParseRule rule)
{
return () => {
AnyWhitespace ();
var result = ParseObject (rule);
if (result == null) {
return null;
}
AnyWhitespace ();
return result;
};
}
private CharacterSet _inlineWhitespaceChars = new CharacterSet(" \t");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fbdaec262d8f64dd684a79c235405a10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Ink
{
public static class InkStringConversionExtensions
{
public static string[] ToStringsArray<T>(this List<T> list) {
int count = list.Count;
var strings = new string[count];
for(int i = 0; i < count; i++) {
strings[i] = list[i].ToString();
}
return strings;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12f91c70015a64a2ba10b2e2fd5d25b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e3f49d19a4e4473086ad32869ce362f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@

namespace Ink.Parsed
{
public class AuthorWarning : Parsed.Object
{
public string warningMessage;
public AuthorWarning(string message)
{
warningMessage = message;
}
public override Runtime.Object GenerateRuntimeObject ()
{
Warning (warningMessage);
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36d3c5151e15a45b4b7a2ac7b355bff9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,290 @@
using System.Text;
namespace Ink.Parsed
{
public class Choice : Parsed.Object, IWeavePoint, INamedContent
{
public ContentList startContent { get; protected set; }
public ContentList choiceOnlyContent { get; protected set; }
public ContentList innerContent { get; protected set; }
public string name
{
get { return identifier?.name; }
}
public Identifier identifier { get; set; }
public Expression condition {
get {
return _condition;
}
set {
_condition = value;
if( _condition )
AddContent (_condition);
}
}
public bool onceOnly { get; set; }
public bool isInvisibleDefault { get; set; }
public int indentationDepth { get; set; }// = 1;
public bool hasWeaveStyleInlineBrackets { get; set; }
// Required for IWeavePoint interface
// Choice's target container. Used by weave to append any extra
// nested weave content into.
public Runtime.Container runtimeContainer { get { return _innerContentContainer; } }
public Runtime.Container innerContentContainer {
get {
return _innerContentContainer;
}
}
public override Runtime.Container containerForCounting {
get {
return _innerContentContainer;
}
}
// Override runtimePath to point to the Choice's target content (after it's chosen),
// as opposed to the default implementation which would point to the choice itself
// (or it's outer container), which is what runtimeObject is.
public override Runtime.Path runtimePath
{
get {
return _innerContentContainer.path;
}
}
public Choice (ContentList startContent, ContentList choiceOnlyContent, ContentList innerContent)
{
this.startContent = startContent;
this.choiceOnlyContent = choiceOnlyContent;
this.innerContent = innerContent;
this.indentationDepth = 1;
if (startContent)
AddContent (this.startContent);
if (choiceOnlyContent)
AddContent (this.choiceOnlyContent);
if( innerContent )
AddContent (this.innerContent);
this.onceOnly = true; // default
}
public override Runtime.Object GenerateRuntimeObject ()
{
_outerContainer = new Runtime.Container ();
// Content names for different types of choice:
// * start content [choice only content] inner content
// * start content -> divert
// * start content
// * [choice only content]
// Hmm, this structure has become slightly insane!
//
// [
// EvalStart
// assign $r = $r1 -- return target = return label 1
// BeginString
// -> s
// [(r1)] -- return label 1 (after start content)
// EndString
// BeginString
// ... choice only content
// EndEval
// Condition expression
// choice: -> "c-0"
// (s) = [
// start content
// -> r -- goto return label 1 or 2
// ]
// ]
//
// in parent's container: (the inner content for the choice)
//
// (c-0) = [
// EvalStart
// assign $r = $r2 -- return target = return label 2
// EndEval
// -> s
// [(r2)] -- return label 1 (after start content)
// inner content
// ]
//
_runtimeChoice = new Runtime.ChoicePoint (onceOnly);
_runtimeChoice.isInvisibleDefault = this.isInvisibleDefault;
if (startContent || choiceOnlyContent || condition) {
_outerContainer.AddContent (Runtime.ControlCommand.EvalStart ());
}
// Start content is put into a named container that's referenced both
// when displaying the choice initially, and when generating the text
// when the choice is chosen.
if (startContent) {
// Generate start content and return
// - We can't use a function since it uses a call stack element, which would
// put temporary values out of scope. Instead we manually divert around.
// - $r is a variable divert target contains the return point
_returnToR1 = new Runtime.DivertTargetValue ();
_outerContainer.AddContent (_returnToR1);
var varAssign = new Runtime.VariableAssignment ("$r", true);
_outerContainer.AddContent (varAssign);
// Mark the start of the choice text generation, so that the runtime
// knows where to rewind to to extract the content from the output stream.
_outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
_divertToStartContentOuter = new Runtime.Divert ();
_outerContainer.AddContent (_divertToStartContentOuter);
// Start content itself in a named container
_startContentRuntimeContainer = startContent.GenerateRuntimeObject () as Runtime.Container;
_startContentRuntimeContainer.name = "s";
// Effectively, the "return" statement - return to the point specified by $r
var varDivert = new Runtime.Divert ();
varDivert.variableDivertName = "$r";
_startContentRuntimeContainer.AddContent (varDivert);
// Add the container
_outerContainer.AddToNamedContentOnly (_startContentRuntimeContainer);
// This is the label to return to
_r1Label = new Runtime.Container ();
_r1Label.name = "$r1";
_outerContainer.AddContent (_r1Label);
_outerContainer.AddContent (Runtime.ControlCommand.EndString ());
_runtimeChoice.hasStartContent = true;
}
// Choice only content - mark the start, then generate it directly into the outer container
if (choiceOnlyContent) {
_outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
var choiceOnlyRuntimeContent = choiceOnlyContent.GenerateRuntimeObject () as Runtime.Container;
_outerContainer.AddContentsOfContainer (choiceOnlyRuntimeContent);
_outerContainer.AddContent (Runtime.ControlCommand.EndString ());
_runtimeChoice.hasChoiceOnlyContent = true;
}
// Generate any condition for this choice
if (condition) {
condition.GenerateIntoContainer (_outerContainer);
_runtimeChoice.hasCondition = true;
}
if (startContent || choiceOnlyContent || condition) {
_outerContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
}
// Add choice itself
_outerContainer.AddContent (_runtimeChoice);
// Container that choice points to for when it's chosen
_innerContentContainer = new Runtime.Container ();
// Repeat start content by diverting to its container
if (startContent) {
// Set the return point when jumping back into the start content
// - In this case, it's the $r2 point, within the choice content "c".
_returnToR2 = new Runtime.DivertTargetValue ();
_innerContentContainer.AddContent (Runtime.ControlCommand.EvalStart ());
_innerContentContainer.AddContent (_returnToR2);
_innerContentContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
var varAssign = new Runtime.VariableAssignment ("$r", true);
_innerContentContainer.AddContent (varAssign);
// Main divert into start content
_divertToStartContentInner = new Runtime.Divert ();
_innerContentContainer.AddContent (_divertToStartContentInner);
// Define label to return to
_r2Label = new Runtime.Container ();
_r2Label.name = "$r2";
_innerContentContainer.AddContent (_r2Label);
}
// Choice's own inner content
if (innerContent) {
var innerChoiceOnlyContent = innerContent.GenerateRuntimeObject () as Runtime.Container;
_innerContentContainer.AddContentsOfContainer (innerChoiceOnlyContent);
}
if (this.story.countAllVisits) {
_innerContentContainer.visitsShouldBeCounted = true;
}
_innerContentContainer.countingAtStartOnly = true;
return _outerContainer;
}
public override void ResolveReferences(Story context)
{
// Weave style choice - target own content container
if (_innerContentContainer) {
_runtimeChoice.pathOnChoice = _innerContentContainer.path;
if (onceOnly)
_innerContentContainer.visitsShouldBeCounted = true;
}
if (_returnToR1)
_returnToR1.targetPath = _r1Label.path;
if (_returnToR2)
_returnToR2.targetPath = _r2Label.path;
if( _divertToStartContentOuter )
_divertToStartContentOuter.targetPath = _startContentRuntimeContainer.path;
if( _divertToStartContentInner )
_divertToStartContentInner.targetPath = _startContentRuntimeContainer.path;
base.ResolveReferences (context);
if( identifier != null && identifier.name.Length > 0 )
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
}
public override string ToString ()
{
if (choiceOnlyContent != null) {
return string.Format ("* {0}[{1}]...", startContent, choiceOnlyContent);
} else {
return string.Format ("* {0}...", startContent);
}
}
Runtime.ChoicePoint _runtimeChoice;
Runtime.Container _innerContentContainer;
Runtime.Container _outerContainer;
Runtime.Container _startContentRuntimeContainer;
Runtime.Divert _divertToStartContentOuter;
Runtime.Divert _divertToStartContentInner;
Runtime.Container _r1Label;
Runtime.Container _r2Label;
Runtime.DivertTargetValue _returnToR1;
Runtime.DivertTargetValue _returnToR2;
Expression _condition;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 24c93c5d49d7f498f9b658d38f48e0b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Runtime;
namespace Ink.Parsed
{
public class Conditional : Parsed.Object
{
public Expression initialCondition { get; private set; }
public List<ConditionalSingleBranch> branches { get; private set; }
public Conditional (Expression condition, List<ConditionalSingleBranch> branches)
{
this.initialCondition = condition;
if (this.initialCondition) {
AddContent (condition);
}
this.branches = branches;
if (this.branches != null) {
AddContent (this.branches.Cast<Parsed.Object> ().ToList ());
}
}
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
// Initial condition
if (this.initialCondition) {
container.AddContent (initialCondition.runtimeObject);
}
// Individual branches
foreach (var branch in branches) {
var branchContainer = (Container) branch.runtimeObject;
container.AddContent (branchContainer);
}
// If it's a switch-like conditional, each branch
// will have a "duplicate" operation for the original
// switched value. If there's no final else clause
// and we fall all the way through, we need to clean up.
// (An else clause doesn't dup but it *does* pop)
if (this.initialCondition != null && branches [0].ownExpression != null && !branches [branches.Count - 1].isElse) {
container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
}
// Target for branches to rejoin to
_reJoinTarget = Runtime.ControlCommand.NoOp ();
container.AddContent (_reJoinTarget);
return container;
}
public override void ResolveReferences (Story context)
{
var pathToReJoin = _reJoinTarget.path;
foreach (var branch in branches) {
branch.returnDivert.targetPath = pathToReJoin;
}
base.ResolveReferences (context);
}
Runtime.ControlCommand _reJoinTarget;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6cda85f80c124e12b215fdbc95b33bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,158 @@

using System.Collections.Generic;
namespace Ink.Parsed
{
public class ConditionalSingleBranch : Parsed.Object
{
// bool condition, e.g.:
// { 5 == 4:
// - the true branch
// - the false branch
// }
public bool isTrueBranch { get; set; }
// When each branch has its own expression like a switch statement,
// this is non-null. e.g.
// { x:
// - 4: the value of x is four (ownExpression is the value 4)
// - 3: the value of x is three
// }
public Expression ownExpression {
get {
return _ownExpression;
}
set {
_ownExpression = value;
if (_ownExpression) {
AddContent (_ownExpression);
}
}
}
// In the above example, match equality of x with 4 for the first branch.
// This is as opposed to simply evaluating boolean equality for each branch,
// example when shouldMatchEqualtity is FALSE:
// {
// 3 > 2: This will happen
// 2 > 3: This won't happen
// }
public bool matchingEquality { get; set; }
public bool isElse { get; set; }
public bool isInline { get; set; }
public Runtime.Divert returnDivert { get; protected set; }
public ConditionalSingleBranch (List<Parsed.Object> content)
{
// Branches are allowed to be empty
if (content != null) {
_innerWeave = new Weave (content);
AddContent (_innerWeave);
}
}
// Runtime content can be summarised as follows:
// - Evaluate an expression if necessary to branch on
// - Branch to a named container if true
// - Divert back to main flow
// (owner Conditional is in control of this target point)
public override Runtime.Object GenerateRuntimeObject ()
{
// Check for common mistake, of putting "else:" instead of "- else:"
if (_innerWeave) {
foreach (var c in _innerWeave.content) {
var text = c as Parsed.Text;
if (text) {
// Don't need to trim at the start since the parser handles that already
if (text.text.StartsWith ("else:")) {
Warning ("Saw the text 'else:' which is being treated as content. Did you mean '- else:'?", text);
}
}
}
}
var container = new Runtime.Container ();
// Are we testing against a condition that's used for more than just this
// branch? If so, the first thing we need to do is replicate the value that's
// on the evaluation stack so that we don't fully consume it, in case other
// branches need to use it.
bool duplicatesStackValue = matchingEquality && !isElse;
if ( duplicatesStackValue )
container.AddContent (Runtime.ControlCommand.Duplicate ());
_conditionalDivert = new Runtime.Divert ();
// else clause is unconditional catch-all, otherwise the divert is conditional
_conditionalDivert.isConditional = !isElse;
// Need extra evaluation?
if( !isTrueBranch && !isElse ) {
bool needsEval = ownExpression != null;
if( needsEval )
container.AddContent (Runtime.ControlCommand.EvalStart ());
if (ownExpression)
ownExpression.GenerateIntoContainer (container);
// Uses existing duplicated value
if (matchingEquality)
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("=="));
if( needsEval )
container.AddContent (Runtime.ControlCommand.EvalEnd ());
}
// Will pop from stack if conditional
container.AddContent (_conditionalDivert);
_contentContainer = GenerateRuntimeForContent ();
_contentContainer.name = "b";
// Multi-line conditionals get a newline at the start of each branch
// (as opposed to the start of the multi-line conditional since the condition
// may evaluate to false.)
if (!isInline) {
_contentContainer.InsertContent (new Runtime.StringValue ("\n"), 0);
}
if( duplicatesStackValue || (isElse && matchingEquality) )
_contentContainer.InsertContent (Runtime.ControlCommand.PopEvaluatedValue (), 0);
container.AddToNamedContentOnly (_contentContainer);
returnDivert = new Runtime.Divert ();
_contentContainer.AddContent (returnDivert);
return container;
}
Runtime.Container GenerateRuntimeForContent()
{
// Empty branch - create empty container
if (_innerWeave == null) {
return new Runtime.Container ();
}
return _innerWeave.rootContainer;
}
public override void ResolveReferences (Story context)
{
_conditionalDivert.targetPath = _contentContainer.path;
base.ResolveReferences (context);
}
Runtime.Container _contentContainer;
Runtime.Divert _conditionalDivert;
Expression _ownExpression;
Weave _innerWeave;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a9e4e9e3b695418f92d259e9d047fb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
//using System.Collections.Generic;
namespace Ink.Parsed
{
public class ConstantDeclaration : Parsed.Object
{
public string constantName
{
get { return constantIdentifier?.name; }
}
public Identifier constantIdentifier { get; protected set; }
public Expression expression { get; protected set; }
public ConstantDeclaration (Identifier name, Expression assignedExpression)
{
this.constantIdentifier = name;
// Defensive programming in case parsing of assignedExpression failed
if( assignedExpression )
this.expression = AddContent(assignedExpression);
}
public override Runtime.Object GenerateRuntimeObject ()
{
// Global declarations don't generate actual procedural
// runtime objects, but instead add a global variable to the story itself.
// The story then initialises them all in one go at the start of the game.
return null;
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
context.CheckForNamingCollisions (this, constantIdentifier, Story.SymbolType.Var);
}
public override string typeName {
get {
return "Constant";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6c599566895c444da36111b8aa6bd5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Text;
namespace Ink.Parsed
{
public class ContentList : Parsed.Object
{
public bool dontFlatten { get; set; }
public Runtime.Container runtimeContainer {
get {
return (Runtime.Container) this.runtimeObject;
}
}
public ContentList (List<Parsed.Object> objects)
{
if( objects != null )
AddContent (objects);
}
public ContentList (params Parsed.Object[] objects)
{
if (objects != null) {
var objList = new List<Parsed.Object> (objects);
AddContent (objList);
}
}
public ContentList()
{
}
public void TrimTrailingWhitespace()
{
for (int i = this.content.Count - 1; i >= 0; --i) {
var text = this.content [i] as Text;
if (text == null)
break;
text.text = text.text.TrimEnd (' ', '\t');
if (text.text.Length == 0)
this.content.RemoveAt (i);
else
break;
}
}
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
if (content != null) {
foreach (var obj in content) {
var contentObjRuntime = obj.runtimeObject;
// Some objects (e.g. author warnings) don't generate runtime objects
if( contentObjRuntime )
container.AddContent (contentObjRuntime);
}
}
if( dontFlatten )
story.DontFlattenContainer (container);
return container;
}
public override string ToString ()
{
var sb = new StringBuilder ();
sb.Append ("ContentList(");
sb.Append(string.Join (", ", content.ToStringsArray()));
sb.Append (")");
return sb.ToString ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d310eb8e3c23e41db891c939a1313cda
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,403 @@
using System.Collections.Generic;
using System.Linq;
namespace Ink.Parsed
{
public class Divert : Parsed.Object
{
public Parsed.Path target { get; protected set; }
public Parsed.Object targetContent { get; protected set; }
public List<Expression> arguments { get; protected set; }
public Runtime.Divert runtimeDivert { get; protected set; }
public bool isFunctionCall { get; set; }
public bool isEmpty { get; set; }
public bool isTunnel { get; set; }
public bool isThread { get; set; }
public bool isEnd {
get {
return target != null && target.dotSeparatedComponents == "END";
}
}
public bool isDone {
get {
return target != null && target.dotSeparatedComponents == "DONE";
}
}
public Divert (Parsed.Path target, List<Expression> arguments = null)
{
this.target = target;
this.arguments = arguments;
if (arguments != null) {
AddContent (arguments.Cast<Parsed.Object> ().ToList ());
}
}
public Divert (Parsed.Object targetContent)
{
this.targetContent = targetContent;
}
public override Runtime.Object GenerateRuntimeObject ()
{
// End = end flow immediately
// Done = return from thread or instruct the flow that it's safe to exit
if (isEnd) {
return Runtime.ControlCommand.End ();
}
if (isDone) {
return Runtime.ControlCommand.Done ();
}
runtimeDivert = new Runtime.Divert ();
// Normally we resolve the target content during the
// Resolve phase, since we expect all runtime objects to
// be available in order to find the final runtime path for
// the destination. However, we need to resolve the target
// (albeit without the runtime target) early so that
// we can get information about the arguments - whether
// they're by reference - since it affects the code we
// generate here.
ResolveTargetContent ();
CheckArgumentValidity ();
// Passing arguments to the knot
bool requiresArgCodeGen = arguments != null && arguments.Count > 0;
if ( requiresArgCodeGen || isFunctionCall || isTunnel || isThread ) {
var container = new Runtime.Container ();
// Generate code for argument evaluation
// This argument generation is coded defensively - it should
// attempt to generate the code for all the parameters, even if
// they don't match the expected arguments. This is so that the
// parameter objects themselves are generated correctly and don't
// get into a state of attempting to resolve references etc
// without being generated.
if (requiresArgCodeGen) {
// Function calls already in an evaluation context
if (!isFunctionCall) {
container.AddContent (Runtime.ControlCommand.EvalStart());
}
List<FlowBase.Argument> targetArguments = null;
if( targetContent )
targetArguments = (targetContent as FlowBase).arguments;
for (var i = 0; i < arguments.Count; ++i) {
Expression argToPass = arguments [i];
FlowBase.Argument argExpected = null;
if( targetArguments != null && i < targetArguments.Count )
argExpected = targetArguments [i];
// Pass by reference: argument needs to be a variable reference
if (argExpected != null && argExpected.isByReference) {
var varRef = argToPass as VariableReference;
if (varRef == null) {
Error ("Expected variable name to pass by reference to 'ref " + argExpected.identifier + "' but saw " + argToPass.ToString ());
break;
}
// Check that we're not attempting to pass a read count by reference
var targetPath = new Path(varRef.pathIdentifiers);
Parsed.Object targetForCount = targetPath.ResolveFromContext (this);
if (targetForCount != null) {
Error ("can't pass a read count by reference. '" + targetPath.dotSeparatedComponents+"' is a knot/stitch/label, but '"+target.dotSeparatedComponents+"' requires the name of a VAR to be passed.");
break;
}
var varPointer = new Runtime.VariablePointerValue (varRef.name);
container.AddContent (varPointer);
}
// Normal value being passed: evaluate it as normal
else {
argToPass.GenerateIntoContainer (container);
}
}
// Function calls were already in an evaluation context
if (!isFunctionCall) {
container.AddContent (Runtime.ControlCommand.EvalEnd());
}
}
// Starting a thread? A bit like a push to the call stack below... but not.
// It sort of puts the call stack on a thread stack (argh!) - forks the full flow.
if (isThread) {
container.AddContent(Runtime.ControlCommand.StartThread());
}
// If this divert is a function call, tunnel, we push to the call stack
// so we can return again
else if (isFunctionCall || isTunnel) {
runtimeDivert.pushesToStack = true;
runtimeDivert.stackPushType = isFunctionCall ? Runtime.PushPopType.Function : Runtime.PushPopType.Tunnel;
}
// Jump into the "function" (knot/stitch)
container.AddContent (runtimeDivert);
return container;
}
// Simple divert
else {
return runtimeDivert;
}
}
// When the divert is to a target that's actually a variable name
// rather than an explicit knot/stitch name, try interpretting it
// as such by getting the variable name.
public string PathAsVariableName()
{
return target.firstComponent;
}
void ResolveTargetContent()
{
if (isEmpty || isEnd) {
return;
}
if (targetContent == null) {
// Is target of this divert a variable name that will be de-referenced
// at runtime? If so, there won't be any further reference resolution
// we can do at this point.
var variableTargetName = PathAsVariableName ();
if (variableTargetName != null) {
var flowBaseScope = ClosestFlowBase ();
var resolveResult = flowBaseScope.ResolveVariableWithName (variableTargetName, fromNode: this);
if (resolveResult.found) {
// Make sure that the flow was typed correctly, given that we know that this
// is meant to be a divert target
if (resolveResult.isArgument) {
var argument = resolveResult.ownerFlow.arguments.Where (a => a.identifier.name == variableTargetName).First();
if ( !argument.isDivertTarget ) {
Error ("Since '" + argument.identifier + "' is used as a variable divert target (on "+this.debugMetadata+"), it should be marked as: -> " + argument.identifier, resolveResult.ownerFlow);
}
}
runtimeDivert.variableDivertName = variableTargetName;
return;
}
}
targetContent = target.ResolveFromContext (this);
}
}
public override void ResolveReferences(Story context)
{
if (isEmpty || isEnd || isDone) {
return;
}
if (targetContent) {
runtimeDivert.targetPath = targetContent.runtimePath;
}
// Resolve children (the arguments)
base.ResolveReferences (context);
// May be null if it's a built in function (e.g. TURNS_SINCE)
// or if it's a variable target.
var targetFlow = targetContent as FlowBase;
if (targetFlow) {
if (!targetFlow.isFunction && this.isFunctionCall) {
base.Error (targetFlow.identifier + " hasn't been marked as a function, but it's being called as one. Do you need to delcare the knot as '== function " + targetFlow.identifier + " =='?");
} else if (targetFlow.isFunction && !this.isFunctionCall && !(this.parent is DivertTarget)) {
base.Error (targetFlow.identifier + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + targetFlow.identifier + "(...)'");
}
}
// Check validity of target content
bool targetWasFound = targetContent != null;
bool isBuiltIn = false;
bool isExternal = false;
if (target.numberOfComponents == 1 ) {
// BuiltIn means TURNS_SINCE, CHOICE_COUNT, RANDOM or SEED_RANDOM
isBuiltIn = FunctionCall.IsBuiltIn (target.firstComponent);
// Client-bound function?
isExternal = context.IsExternal (target.firstComponent);
if (isBuiltIn || isExternal) {
if (!isFunctionCall) {
base.Error (target.firstComponent + " must be called as a function: ~ " + target.firstComponent + "()");
}
if (isExternal) {
runtimeDivert.isExternal = true;
if( arguments != null )
runtimeDivert.externalArgs = arguments.Count;
runtimeDivert.pushesToStack = false;
runtimeDivert.targetPath = new Runtime.Path (this.target.firstComponent);
CheckExternalArgumentValidity (context);
}
return;
}
}
// Variable target?
if (runtimeDivert.variableDivertName != null) {
return;
}
if( !targetWasFound && !isBuiltIn && !isExternal )
Error ("target not found: '" + target + "'");
}
// Returns false if there's an error
void CheckArgumentValidity()
{
if (isEmpty)
return;
// Argument passing: Check for errors in number of arguments
var numArgs = 0;
if (arguments != null && arguments.Count > 0)
numArgs = arguments.Count;
// Missing content?
// Can't check arguments properly. It'll be due to some
// other error though, so although there's a problem and
// we report false, we don't need to report a specific error.
// It may also be because it's a valid call to an external
// function, that we check at the resolve stage.
if (targetContent == null) {
return;
}
FlowBase targetFlow = targetContent as FlowBase;
// No error, crikey!
if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters)) {
return;
}
if (targetFlow == null && numArgs > 0) {
Error ("target needs to be a knot or stitch in order to pass arguments");
return;
}
if (targetFlow.arguments == null && numArgs > 0) {
Error ("target (" + targetFlow.name + ") doesn't take parameters");
return;
}
if( this.parent is DivertTarget ) {
if (numArgs > 0)
Error ("can't store arguments in a divert target variable");
return;
}
var paramCount = targetFlow.arguments.Count;
if (paramCount != numArgs) {
string butClause;
if (numArgs == 0) {
butClause = "but there weren't any passed to it";
} else if (numArgs < paramCount) {
butClause = "but only got " + numArgs;
} else {
butClause = "but got " + numArgs;
}
Error ("to '" + targetFlow.identifier + "' requires " + paramCount + " arguments, "+butClause);
return;
}
// Light type-checking for divert target arguments
for (int i = 0; i < paramCount; ++i) {
FlowBase.Argument flowArg = targetFlow.arguments [i];
Parsed.Expression divArgExpr = arguments [i];
// Expecting a divert target as an argument, let's do some basic type checking
if (flowArg.isDivertTarget) {
// Not passing a divert target or any kind of variable reference?
var varRef = divArgExpr as VariableReference;
if (!(divArgExpr is DivertTarget) && varRef == null ) {
Error ("Target '" + targetFlow.identifier + "' expects a divert target for the parameter named -> " + flowArg.identifier + " but saw " + divArgExpr, divArgExpr);
}
// Passing 'a' instead of '-> a'?
// i.e. read count instead of divert target
else if (varRef != null) {
// Unfortunately have to manually resolve here since we're still in code gen
var knotCountPath = new Path(varRef.pathIdentifiers);
Parsed.Object targetForCount = knotCountPath.ResolveFromContext (varRef);
if (targetForCount != null) {
Error ("Passing read count of '" + knotCountPath.dotSeparatedComponents + "' instead of a divert target. You probably meant '" + knotCountPath + "'");
}
}
}
}
if (targetFlow == null) {
Error ("Can't call as a function or with arguments unless it's a knot or stitch");
return;
}
return;
}
void CheckExternalArgumentValidity(Story context)
{
string externalName = target.firstComponent;
ExternalDeclaration external = null;
var found = context.externals.TryGetValue(externalName, out external);
System.Diagnostics.Debug.Assert (found, "external not found");
int externalArgCount = external.argumentNames.Count;
int ownArgCount = 0;
if (arguments != null) {
ownArgCount = arguments.Count;
}
if (ownArgCount != externalArgCount) {
Error ("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + externalArgCount + " but got " + ownArgCount);
}
}
public override void Error (string message, Object source = null, bool isWarning = false)
{
// Could be getting an error from a nested Divert
if (source != this && source) {
base.Error (message, source);
return;
}
if (isFunctionCall) {
base.Error ("Function call " + message, source, isWarning);
} else {
base.Error ("Divert " + message, source, isWarning);
}
}
public override string ToString ()
{
if (target != null)
return target.ToString ();
else
return "-> <empty divert>";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26c47b19962e641869a39b85cd86f9e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,172 @@

namespace Ink.Parsed
{
public class DivertTarget : Expression
{
public Divert divert;
public DivertTarget (Divert divert)
{
this.divert = AddContent(divert);
}
public override void GenerateIntoContainer (Runtime.Container container)
{
divert.GenerateRuntimeObject();
_runtimeDivert = (Runtime.Divert) divert.runtimeDivert;
_runtimeDivertTargetValue = new Runtime.DivertTargetValue ();
container.AddContent (_runtimeDivertTargetValue);
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
if( divert.isDone || divert.isEnd )
{
Error("Can't Can't use -> DONE or -> END as variable divert targets", this);
return;
}
Parsed.Object usageContext = this;
while (usageContext && usageContext is Expression) {
bool badUsage = false;
bool foundUsage = false;
var usageParent = usageContext.parent;
if (usageParent is BinaryExpression) {
// Only allowed to compare for equality
var binaryExprParent = usageParent as BinaryExpression;
if (binaryExprParent.opName != "==" && binaryExprParent.opName != "!=") {
badUsage = true;
} else {
if (!(binaryExprParent.leftExpression is DivertTarget || binaryExprParent.leftExpression is VariableReference)) {
badUsage = true;
}
if (!(binaryExprParent.rightExpression is DivertTarget || binaryExprParent.rightExpression is VariableReference)) {
badUsage = true;
}
}
foundUsage = true;
}
else if( usageParent is FunctionCall ) {
var funcCall = usageParent as FunctionCall;
if( !funcCall.isTurnsSince && !funcCall.isReadCount ) {
badUsage = true;
}
foundUsage = true;
}
else if (usageParent is Expression) {
badUsage = true;
foundUsage = true;
}
else if (usageParent is MultipleConditionExpression) {
badUsage = true;
foundUsage = true;
} else if (usageParent is Choice && ((Choice)usageParent).condition == usageContext) {
badUsage = true;
foundUsage = true;
} else if (usageParent is Conditional || usageParent is ConditionalSingleBranch) {
badUsage = true;
foundUsage = true;
}
if (badUsage) {
Error ("Can't use a divert target like that. Did you intend to call '" + divert.target + "' as a function: likeThis(), or check the read count: likeThis, with no arrows?", this);
}
if (foundUsage)
break;
usageContext = usageParent;
}
// Example ink for this case:
//
// VAR x = -> blah
//
// ...which means that "blah" is expected to be a literal stitch target rather
// than a variable name. We can't really intelligently recover from this (e.g. if blah happens to
// contain a divert target itself) since really we should be generating a variable reference
// rather than a concrete DivertTarget, so we list it as an error.
if (_runtimeDivert.hasVariableTarget)
Error ("Since '"+divert.target.dotSeparatedComponents+"' is a variable, it shouldn't be preceded by '->' here.");
// Main resolve
_runtimeDivertTargetValue.targetPath = _runtimeDivert.targetPath;
// Tell hard coded (yet variable) divert targets that they also need to be counted
// TODO: Only detect DivertTargets that are values rather than being used directly for
// read or turn counts. Should be able to detect this by looking for other uses of containerForCounting
var targetContent = this.divert.targetContent;
if (targetContent != null ) {
var target = targetContent.containerForCounting;
if (target != null)
{
// Purpose is known: used directly in TURNS_SINCE(-> divTarg)
var parentFunc = this.parent as FunctionCall;
if( parentFunc && parentFunc.isTurnsSince ) {
target.turnIndexShouldBeCounted = true;
}
// Unknown purpose, count everything
else {
target.visitsShouldBeCounted = true;
target.turnIndexShouldBeCounted = true;
}
}
// Unfortunately not possible:
// https://github.com/inkle/ink/issues/538
//
// VAR func = -> double
//
// === function double(ref x)
// ~ x = x * 2
//
// Because when generating the parameters for a function
// to be called, it needs to know ahead of time when
// compiling whether to pass a variable reference or value.
//
var targetFlow = (targetContent as FlowBase);
if (targetFlow != null && targetFlow.arguments != null)
{
foreach(var arg in targetFlow.arguments) {
if(arg.isByReference)
{
Error("Can't store a divert target to a knot or function that has by-reference arguments ('"+targetFlow.identifier+"' has 'ref "+arg.identifier+"').");
}
}
}
}
}
// Equals override necessary in order to check for CONST multiple definition equality
public override bool Equals (object obj)
{
var otherDivTarget = obj as DivertTarget;
if (otherDivTarget == null) return false;
var targetStr = this.divert.target.dotSeparatedComponents;
var otherTargetStr = otherDivTarget.divert.target.dotSeparatedComponents;
return targetStr.Equals (otherTargetStr);
}
public override int GetHashCode ()
{
var targetStr = this.divert.target.dotSeparatedComponents;
return targetStr.GetHashCode ();
}
Runtime.DivertTargetValue _runtimeDivertTargetValue;
Runtime.Divert _runtimeDivert;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d8a428e7434204b02921e47651c42329
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,307 @@
using System.Collections.Generic;
using System.Linq;
namespace Ink.Parsed
{
public abstract class Expression : Parsed.Object
{
public bool outputWhenComplete { get; set; }
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
// Tell Runtime to start evaluating the following content as an expression
container.AddContent (Runtime.ControlCommand.EvalStart());
GenerateIntoContainer (container);
// Tell Runtime to output the result of the expression evaluation to the output stream
if (outputWhenComplete) {
container.AddContent (Runtime.ControlCommand.EvalOutput());
}
// Tell Runtime to stop evaluating the content as an expression
container.AddContent (Runtime.ControlCommand.EvalEnd());
return container;
}
// When generating the value of a constant expression,
// we can't just keep generating the same constant expression into
// different places where the constant value is referenced, since then
// the same runtime objects would be used in multiple places, which
// is impossible since each runtime object should have one parent.
// Instead, we generate a prototype of the runtime object(s), then
// copy them each time they're used.
public void GenerateConstantIntoContainer(Runtime.Container container)
{
if( _prototypeRuntimeConstantExpression == null ) {
_prototypeRuntimeConstantExpression = new Runtime.Container ();
GenerateIntoContainer (_prototypeRuntimeConstantExpression);
}
foreach (var runtimeObj in _prototypeRuntimeConstantExpression.content) {
container.AddContent (runtimeObj.Copy());
}
}
public abstract void GenerateIntoContainer (Runtime.Container container);
Runtime.Container _prototypeRuntimeConstantExpression;
}
public class BinaryExpression : Expression
{
public Expression leftExpression;
public Expression rightExpression;
public string opName;
public BinaryExpression(Expression left, Expression right, string opName)
{
leftExpression = AddContent(left);
rightExpression = AddContent(right);
this.opName = opName;
}
public override void GenerateIntoContainer(Runtime.Container container)
{
leftExpression.GenerateIntoContainer (container);
rightExpression.GenerateIntoContainer (container);
opName = NativeNameForOp (opName);
container.AddContent(Runtime.NativeFunctionCall.CallWithName(opName));
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
// Check for the following case:
//
// (not A) ? B
//
// Since this easy to accidentally do:
//
// not A ? B
//
// when you intend:
//
// not (A ? B)
if (NativeNameForOp (opName) == "?") {
var leftUnary = leftExpression as UnaryExpression;
if( leftUnary != null && (leftUnary.op == "not" || leftUnary.op == "!") ) {
Error ("Using 'not' or '!' here negates '"+leftUnary.innerExpression+"' rather than the result of the '?' or 'has' operator. You need to add parentheses around the (A ? B) expression.");
}
}
}
string NativeNameForOp(string opName)
{
if (opName == "and")
return "&&";
if (opName == "or")
return "||";
if (opName == "mod")
return "%";
if (opName == "has")
return "?";
if (opName == "hasnt")
return "!?";
return opName;
}
public override string ToString ()
{
return string.Format ("({0} {1} {2})", leftExpression, opName, rightExpression);
}
}
public class UnaryExpression : Expression
{
public Expression innerExpression;
public string op;
// Attempt to flatten inner expression immediately
// e.g. convert (-(5)) into (-5)
public static Expression WithInner(Expression inner, string op) {
var innerNumber = inner as Number;
if( innerNumber ) {
if( op == "-" ) {
if( innerNumber.value is int ) {
return new Number( -((int)innerNumber.value) );
} else if( innerNumber.value is float ) {
return new Number( -((float)innerNumber.value) );
}
}
else if( op == "!" || op == "not" ) {
if( innerNumber.value is int ) {
return new Number( (int)innerNumber.value == 0 );
} else if( innerNumber.value is float ) {
return new Number( (float)innerNumber.value == 0.0f );
} else if( innerNumber.value is bool ) {
return new Number( !(bool)innerNumber.value );
}
}
throw new System.Exception ("Unexpected operation or number type");
}
// Normal fallback
var unary = new UnaryExpression (inner, op);
return unary;
}
public UnaryExpression(Expression inner, string op)
{
this.innerExpression = AddContent(inner);
this.op = op;
}
public override void GenerateIntoContainer(Runtime.Container container)
{
innerExpression.GenerateIntoContainer (container);
container.AddContent(Runtime.NativeFunctionCall.CallWithName(nativeNameForOp));
}
public override string ToString ()
{
return nativeNameForOp + innerExpression;
}
string nativeNameForOp
{
get {
// Replace "-" with "_" to make it unique (compared to subtraction)
if (op == "-")
return "_";
if (op == "not")
return "!";
return op;
}
}
}
public class IncDecExpression : Expression
{
public Identifier varIdentifier;
public bool isInc;
public Expression expression;
public IncDecExpression(Identifier varIdentifier, bool isInc)
{
this.varIdentifier = varIdentifier;
this.isInc = isInc;
}
public IncDecExpression (Identifier varIdentifier, Expression expression, bool isInc) : this(varIdentifier, isInc)
{
this.expression = expression;
AddContent (expression);
}
public override void GenerateIntoContainer(Runtime.Container container)
{
// x = x + y
// ^^^ ^ ^ ^
// 4 1 3 2
// Reverse polish notation: (x 1 +) (assign to x)
// 1.
container.AddContent (new Runtime.VariableReference (varIdentifier?.name));
// 2.
// - Expression used in the form ~ x += y
// - Simple version: ~ x++
if (expression)
expression.GenerateIntoContainer (container);
else
container.AddContent (new Runtime.IntValue (1));
// 3.
container.AddContent (Runtime.NativeFunctionCall.CallWithName (isInc ? "+" : "-"));
// 4.
_runtimeAssignment = new Runtime.VariableAssignment(varIdentifier?.name, false);
container.AddContent (_runtimeAssignment);
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
var varResolveResult = context.ResolveVariableWithName(varIdentifier?.name, fromNode: this);
if (!varResolveResult.found) {
Error ("variable for "+incrementDecrementWord+" could not be found: '"+varIdentifier+"' after searching: "+this.descriptionOfScope);
}
_runtimeAssignment.isGlobal = varResolveResult.isGlobal;
if (!(parent is Weave) && !(parent is FlowBase) && !(parent is ContentList)) {
Error ("Can't use " + incrementDecrementWord + " as sub-expression");
}
}
string incrementDecrementWord {
get {
if (isInc)
return "increment";
else
return "decrement";
}
}
public override string ToString ()
{
if (expression)
return varIdentifier + (isInc ? " += " : " -= ") + expression.ToString ();
else
return varIdentifier + (isInc ? "++" : "--");
}
Runtime.VariableAssignment _runtimeAssignment;
}
public class MultipleConditionExpression : Expression
{
public List<Expression> subExpressions {
get {
return this.content.Cast<Expression> ().ToList ();
}
}
public MultipleConditionExpression(List<Expression> conditionExpressions)
{
AddContent (conditionExpressions);
}
public override void GenerateIntoContainer(Runtime.Container container)
{
// A && B && C && D
// => (((A B &&) C &&) D &&) etc
bool isFirst = true;
foreach (var conditionExpr in subExpressions) {
conditionExpr.GenerateIntoContainer (container);
if (!isFirst) {
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("&&"));
}
isFirst = false;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 476809ca3ec8f4743afd6fa33bd6e442
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace Ink.Parsed
{
public class ExternalDeclaration : Parsed.Object, INamedContent
{
public string name
{
get { return identifier?.name; }
}
public Identifier identifier { get; set; }
public List<string> argumentNames { get; set; }
public ExternalDeclaration (Identifier identifier, List<string> argumentNames)
{
this.identifier = identifier;
this.argumentNames = argumentNames;
}
public override Ink.Runtime.Object GenerateRuntimeObject ()
{
story.AddExternal (this);
// No runtime code exists for an external, only metadata
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ca4476886042f471a9771284a027b45f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,439 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
// Base class for Knots and Stitches
public abstract class FlowBase : Parsed.Object, INamedContent
{
public class Argument
{
public Identifier identifier;
public bool isByReference;
public bool isDivertTarget;
}
public string name
{
get { return identifier?.name; }
}
public Identifier identifier { get; set; }
public List<Argument> arguments { get; protected set; }
public bool hasParameters { get { return arguments != null && arguments.Count > 0; } }
public Dictionary<string, VariableAssignment> variableDeclarations;
public abstract FlowLevel flowLevel { get; }
public bool isFunction { get; protected set; }
public FlowBase (Identifier name = null, List<Parsed.Object> topLevelObjects = null, List<Argument> arguments = null, bool isFunction = false, bool isIncludedStory = false)
{
this.identifier = name;
if (topLevelObjects == null) {
topLevelObjects = new List<Parsed.Object> ();
}
// Used by story to add includes
PreProcessTopLevelObjects (topLevelObjects);
topLevelObjects = SplitWeaveAndSubFlowContent (topLevelObjects, isRootStory:this is Story && !isIncludedStory);
AddContent(topLevelObjects);
this.arguments = arguments;
this.isFunction = isFunction;
this.variableDeclarations = new Dictionary<string, VariableAssignment> ();
}
List<Parsed.Object> SplitWeaveAndSubFlowContent(List<Parsed.Object> contentObjs, bool isRootStory)
{
var weaveObjs = new List<Parsed.Object> ();
var subFlowObjs = new List<Parsed.Object> ();
_subFlowsByName = new Dictionary<string, FlowBase> ();
foreach (var obj in contentObjs) {
var subFlow = obj as FlowBase;
if (subFlow) {
if (_firstChildFlow == null)
_firstChildFlow = subFlow;
subFlowObjs.Add (obj);
_subFlowsByName [subFlow.identifier?.name] = subFlow;
} else {
weaveObjs.Add (obj);
}
}
// Implicit final gather in top level story for ending without warning that you run out of content
if (isRootStory) {
weaveObjs.Add (new Gather (null, 1));
weaveObjs.Add (new Divert (new Path (Identifier.Done)));
}
var finalContent = new List<Parsed.Object> ();
if (weaveObjs.Count > 0) {
_rootWeave = new Weave (weaveObjs, 0);
finalContent.Add (_rootWeave);
}
if (subFlowObjs.Count > 0) {
finalContent.AddRange (subFlowObjs);
}
return finalContent;
}
protected virtual void PreProcessTopLevelObjects(List<Parsed.Object> topLevelObjects)
{
// empty by default, used by Story to process included file references
}
public struct VariableResolveResult
{
public bool found;
public bool isGlobal;
public bool isArgument;
public bool isTemporary;
public FlowBase ownerFlow;
}
public VariableResolveResult ResolveVariableWithName(string varName, Parsed.Object fromNode)
{
var result = new VariableResolveResult ();
// Search in the stitch / knot that owns the node first
var ownerFlow = fromNode == null ? this : fromNode.ClosestFlowBase ();
// Argument
if (ownerFlow.arguments != null ) {
foreach (var arg in ownerFlow.arguments) {
if (arg.identifier.name.Equals (varName)) {
result.found = true;
result.isArgument = true;
result.ownerFlow = ownerFlow;
return result;
}
}
}
// Temp
var story = this.story; // optimisation
if (ownerFlow != story && ownerFlow.variableDeclarations.ContainsKey (varName)) {
result.found = true;
result.ownerFlow = ownerFlow;
result.isTemporary = true;
return result;
}
// Global
if (story.variableDeclarations.ContainsKey (varName)) {
result.found = true;
result.ownerFlow = story;
result.isGlobal = true;
return result;
}
result.found = false;
return result;
}
public void TryAddNewVariableDeclaration(VariableAssignment varDecl)
{
var varName = varDecl.variableName;
if (variableDeclarations.ContainsKey (varName)) {
var prevDeclError = "";
var debugMetadata = variableDeclarations [varName].debugMetadata;
if (debugMetadata != null) {
prevDeclError = " ("+variableDeclarations [varName].debugMetadata+")";
}
Error("found declaration variable '"+varName+"' that was already declared"+prevDeclError, varDecl, false);
return;
}
variableDeclarations [varDecl.variableName] = varDecl;
}
public void ResolveWeavePointNaming ()
{
// Find all weave points and organise them by name ready for
// diverting. Also detect naming collisions.
if( _rootWeave )
_rootWeave.ResolveWeavePointNaming ();
if (_subFlowsByName != null) {
foreach (var namedSubFlow in _subFlowsByName) {
namedSubFlow.Value.ResolveWeavePointNaming ();
}
}
}
public override Runtime.Object GenerateRuntimeObject ()
{
Return foundReturn = null;
if (isFunction) {
CheckForDisallowedFunctionFlowControl ();
}
// Non-functon: Make sure knots and stitches don't attempt to use Return statement
else if( flowLevel == FlowLevel.Knot || flowLevel == FlowLevel.Stitch ) {
foundReturn = Find<Return> ();
if (foundReturn != null) {
Error ("Return statements can only be used in knots that are declared as functions: == function " + this.identifier + " ==", foundReturn);
}
}
var container = new Runtime.Container ();
container.name = identifier?.name;
if( this.story.countAllVisits ) {
container.visitsShouldBeCounted = true;
}
GenerateArgumentVariableAssignments (container);
// Run through content defined for this knot/stitch:
// - First of all, any initial content before a sub-stitch
// or any weave content is added to the main content container
// - The first inner knot/stitch is automatically entered, while
// the others are only accessible by an explicit divert
// - The exception to this rule is if the knot/stitch takes
// parameters, in which case it can't be auto-entered.
// - Any Choices and Gathers (i.e. IWeavePoint) found are
// processsed by GenerateFlowContent.
int contentIdx = 0;
while (content != null && contentIdx < content.Count) {
Parsed.Object obj = content [contentIdx];
// Inner knots and stitches
if (obj is FlowBase) {
var childFlow = (FlowBase)obj;
var childFlowRuntime = childFlow.runtimeObject;
// First inner stitch - automatically step into it
// 20/09/2016 - let's not auto step into knots
if (contentIdx == 0 && !childFlow.hasParameters
&& this.flowLevel == FlowLevel.Knot) {
_startingSubFlowDivert = new Runtime.Divert ();
container.AddContent(_startingSubFlowDivert);
_startingSubFlowRuntime = childFlowRuntime;
}
// Check for duplicate knots/stitches with same name
var namedChild = (Runtime.INamedContent)childFlowRuntime;
Runtime.INamedContent existingChild = null;
if (container.namedContent.TryGetValue(namedChild.name, out existingChild) ) {
var errorMsg = string.Format ("{0} already contains flow named '{1}' (at {2})",
this.GetType().Name,
namedChild.name,
(existingChild as Runtime.Object).debugMetadata);
Error (errorMsg, childFlow);
}
container.AddToNamedContentOnly (namedChild);
}
// Other content (including entire Weaves that were grouped in the constructor)
// At the time of writing, all FlowBases have a maximum of one piece of "other content"
// and it's always the root Weave
else {
container.AddContent (obj.runtimeObject);
}
contentIdx++;
}
// CHECK FOR FINAL LOOSE ENDS!
// Notes:
// - Functions don't need to terminate - they just implicitly return
// - If return statement was found, don't continue finding warnings for missing control flow,
// since it's likely that a return statement has been used instead of a ->-> or something,
// or the writer failed to mark the knot as a function.
// - _rootWeave may be null if it's a knot that only has stitches
if (flowLevel != FlowLevel.Story && !this.isFunction && _rootWeave != null && foundReturn == null) {
_rootWeave.ValidateTermination (WarningInTermination);
}
return container;
}
void GenerateArgumentVariableAssignments(Runtime.Container container)
{
if (this.arguments == null || this.arguments.Count == 0) {
return;
}
// Assign parameters in reverse since they'll be popped off the evaluation stack
// No need to generate EvalStart and EvalEnd since there's nothing being pushed
// back onto the evaluation stack.
for (int i = arguments.Count - 1; i >= 0; --i) {
var paramName = arguments [i].identifier?.name;
var assign = new Runtime.VariableAssignment (paramName, isNewDeclaration:true);
container.AddContent (assign);
}
}
public Parsed.Object ContentWithNameAtLevel(string name, FlowLevel? level = null, bool deepSearch = false)
{
// Referencing self?
if (level == this.flowLevel || level == null) {
if (name == this.identifier?.name) {
return this;
}
}
if ( level == FlowLevel.WeavePoint || level == null ) {
Parsed.Object weavePointResult = null;
if (_rootWeave) {
weavePointResult = (Parsed.Object)_rootWeave.WeavePointNamed (name);
if (weavePointResult)
return weavePointResult;
}
// Stop now if we only wanted a result if it's a weave point?
if (level == FlowLevel.WeavePoint)
return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
}
// If this flow would be incapable of containing the requested level, early out
// (e.g. asking for a Knot from a Stitch)
if (level != null && level < this.flowLevel)
return null;
FlowBase subFlow = null;
if (_subFlowsByName.TryGetValue (name, out subFlow)) {
if (level == null || level == subFlow.flowLevel)
return subFlow;
}
return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
}
Parsed.Object DeepSearchForAnyLevelContent(string name)
{
var weaveResultSelf = ContentWithNameAtLevel (name, level:FlowLevel.WeavePoint, deepSearch: false);
if (weaveResultSelf) {
return weaveResultSelf;
}
foreach (var subFlowNamePair in _subFlowsByName) {
var subFlow = subFlowNamePair.Value;
var deepResult = subFlow.ContentWithNameAtLevel (name, level:null, deepSearch: true);
if (deepResult)
return deepResult;
}
return null;
}
public override void ResolveReferences (Story context)
{
if (_startingSubFlowDivert) {
_startingSubFlowDivert.targetPath = _startingSubFlowRuntime.path;
}
base.ResolveReferences(context);
// Check validity of parameter names
if (arguments != null) {
foreach (var arg in arguments)
context.CheckForNamingCollisions (this, arg.identifier, Story.SymbolType.Arg, "argument");
// Separately, check for duplicate arugment names, since they aren't Parsed.Objects,
// so have to be checked independently.
for (int i = 0; i < arguments.Count; i++) {
for (int j = i + 1; j < arguments.Count; j++) {
if (arguments [i].identifier?.name == arguments [j].identifier?.name) {
Error ("Multiple arguments with the same name: '" + arguments [i].identifier + "'");
}
}
}
}
// Check naming collisions for knots and stitches
if (flowLevel != FlowLevel.Story) {
// Weave points aren't FlowBases, so this will only be knot or stitch
var symbolType = flowLevel == FlowLevel.Knot ? Story.SymbolType.Knot : Story.SymbolType.SubFlowAndWeave;
context.CheckForNamingCollisions (this, identifier, symbolType);
}
}
void CheckForDisallowedFunctionFlowControl()
{
if (!(this is Knot)) {
Error ("Functions cannot be stitches - i.e. they should be defined as '== function myFunc ==' rather than public to another knot.");
}
// Not allowed sub-flows
foreach (var subFlowAndName in _subFlowsByName) {
var name = subFlowAndName.Key;
var subFlow = subFlowAndName.Value;
Error ("Functions may not contain stitches, but saw '"+name+"' within the function '"+this.identifier+"'", subFlow);
}
var allDiverts = _rootWeave.FindAll<Divert> ();
foreach (var divert in allDiverts) {
if( !divert.isFunctionCall && !(divert.parent is DivertTarget) )
Error ("Functions may not contain diverts, but saw '"+divert.ToString()+"'", divert);
}
var allChoices = _rootWeave.FindAll<Choice> ();
foreach (var choice in allChoices) {
Error ("Functions may not contain choices, but saw '"+choice.ToString()+"'", choice);
}
}
void WarningInTermination(Parsed.Object terminatingObject)
{
string message = "Apparent loose end exists where the flow runs out. Do you need a '-> DONE' statement, choice or divert?";
if (terminatingObject.parent == _rootWeave && _firstChildFlow) {
message = message + " Note that if you intend to enter '"+_firstChildFlow.identifier+"' next, you need to divert to it explicitly.";
}
var terminatingDivert = terminatingObject as Divert;
if (terminatingDivert && terminatingDivert.isTunnel) {
message = message + " When final tunnel to '"+terminatingDivert.target+" ->' returns it won't have anywhere to go.";
}
Warning (message, terminatingObject);
}
protected Dictionary<string, FlowBase> subFlowsByName {
get {
return _subFlowsByName;
}
}
public override string typeName {
get {
if (isFunction) return "Function";
else return flowLevel.ToString ();
}
}
public override string ToString ()
{
return typeName+" '" + identifier + "'";
}
Weave _rootWeave;
Dictionary<string, FlowBase> _subFlowsByName;
Runtime.Divert _startingSubFlowDivert;
Runtime.Object _startingSubFlowRuntime;
FlowBase _firstChildFlow;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 573f03d14273b46a29e16c5ff9ec8059
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
namespace Ink.Parsed
{
public enum FlowLevel
{
Story,
Knot,
Stitch,
WeavePoint // not actually a FlowBase, but used for diverts
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3e0618d06b104e3a82b27f11b68ad54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,242 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class FunctionCall : Expression
{
public string name { get { return _proxyDivert.target.firstComponent; } }
public Divert proxyDivert { get { return _proxyDivert; } }
public List<Expression> arguments { get { return _proxyDivert.arguments; } }
public Runtime.Divert runtimeDivert { get { return _proxyDivert.runtimeDivert; } }
public bool isChoiceCount { get { return name == "CHOICE_COUNT"; } }
public bool isTurns { get { return name == "TURNS"; } }
public bool isTurnsSince { get { return name == "TURNS_SINCE"; } }
public bool isRandom { get { return name == "RANDOM"; } }
public bool isSeedRandom { get { return name == "SEED_RANDOM"; } }
public bool isListRange { get { return name == "LIST_RANGE"; } }
public bool isListRandom { get { return name == "LIST_RANDOM"; } }
public bool isReadCount { get { return name == "READ_COUNT"; } }
public bool shouldPopReturnedValue;
public FunctionCall (Identifier functionName, List<Expression> arguments)
{
_proxyDivert = new Parsed.Divert(new Path(functionName), arguments);
_proxyDivert.isFunctionCall = true;
AddContent (_proxyDivert);
}
public override void GenerateIntoContainer (Runtime.Container container)
{
var foundList = story.ResolveList (name);
bool usingProxyDivert = false;
if (isChoiceCount) {
if (arguments.Count > 0)
Error ("The CHOICE_COUNT() function shouldn't take any arguments");
container.AddContent (Runtime.ControlCommand.ChoiceCount ());
} else if (isTurns) {
if (arguments.Count > 0)
Error ("The TURNS() function shouldn't take any arguments");
container.AddContent (Runtime.ControlCommand.Turns ());
} else if (isTurnsSince || isReadCount) {
var divertTarget = arguments [0] as DivertTarget;
var variableDivertTarget = arguments [0] as VariableReference;
if (arguments.Count != 1 || (divertTarget == null && variableDivertTarget == null)) {
Error ("The " + name + "() function should take one argument: a divert target to the target knot, stitch, gather or choice you want to check. e.g. TURNS_SINCE(-> myKnot)");
return;
}
if (divertTarget) {
_divertTargetToCount = divertTarget;
AddContent (_divertTargetToCount);
_divertTargetToCount.GenerateIntoContainer (container);
} else {
_variableReferenceToCount = variableDivertTarget;
AddContent (_variableReferenceToCount);
_variableReferenceToCount.GenerateIntoContainer (container);
}
if (isTurnsSince)
container.AddContent (Runtime.ControlCommand.TurnsSince ());
else
container.AddContent (Runtime.ControlCommand.ReadCount ());
} else if (isRandom) {
if (arguments.Count != 2)
Error ("RANDOM should take 2 parameters: a minimum and a maximum integer");
// We can type check single values, but not complex expressions
for (int arg = 0; arg < arguments.Count; arg++) {
if (arguments [arg] is Number) {
var num = arguments [arg] as Number;
if (!(num.value is int)) {
string paramName = arg == 0 ? "minimum" : "maximum";
Error ("RANDOM's " + paramName + " parameter should be an integer");
}
}
arguments [arg].GenerateIntoContainer (container);
}
container.AddContent (Runtime.ControlCommand.Random ());
} else if (isSeedRandom) {
if (arguments.Count != 1)
Error ("SEED_RANDOM should take 1 parameter - an integer seed");
var num = arguments [0] as Number;
if (num && !(num.value is int)) {
Error ("SEED_RANDOM's parameter should be an integer seed");
}
arguments [0].GenerateIntoContainer (container);
container.AddContent (Runtime.ControlCommand.SeedRandom ());
} else if (isListRange) {
if (arguments.Count != 3)
Error ("LIST_RANGE should take 3 parameters - a list, a min and a max");
for (int arg = 0; arg < arguments.Count; arg++)
arguments [arg].GenerateIntoContainer (container);
container.AddContent (Runtime.ControlCommand.ListRange ());
} else if( isListRandom ) {
if (arguments.Count != 1)
Error ("LIST_RANDOM should take 1 parameter - a list");
arguments [0].GenerateIntoContainer (container);
container.AddContent (Runtime.ControlCommand.ListRandom ());
} else if (Runtime.NativeFunctionCall.CallExistsWithName (name)) {
var nativeCall = Runtime.NativeFunctionCall.CallWithName (name);
if (nativeCall.numberOfParameters != arguments.Count) {
var msg = name + " should take " + nativeCall.numberOfParameters + " parameter";
if (nativeCall.numberOfParameters > 1)
msg += "s";
Error (msg);
}
for (int arg = 0; arg < arguments.Count; arg++)
arguments [arg].GenerateIntoContainer (container);
container.AddContent (Runtime.NativeFunctionCall.CallWithName (name));
} else if (foundList != null) {
if (arguments.Count > 1)
Error ("Can currently only construct a list from one integer (or an empty list from a given list definition)");
// List item from given int
if (arguments.Count == 1) {
container.AddContent (new Runtime.StringValue (name));
arguments [0].GenerateIntoContainer (container);
container.AddContent (Runtime.ControlCommand.ListFromInt ());
}
// Empty list with given origin.
else {
var list = new Runtime.InkList ();
list.SetInitialOriginName (name);
container.AddContent (new Runtime.ListValue (list));
}
}
// Normal function call
else {
container.AddContent (_proxyDivert.runtimeObject);
usingProxyDivert = true;
}
// Don't attempt to resolve as a divert if we're not doing a normal function call
if( !usingProxyDivert ) content.Remove (_proxyDivert);
// Function calls that are used alone on a tilda-based line:
// ~ func()
// Should tidy up any returned value from the evaluation stack,
// since it's unused.
if (shouldPopReturnedValue)
container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
// If we aren't using the proxy divert after all (e.g. if
// it's a native function call), but we still have arguments,
// we need to make sure they get resolved since the proxy divert
// is no longer in the content array.
if (!content.Contains(_proxyDivert) && arguments != null) {
foreach (var arg in arguments)
arg.ResolveReferences (context);
}
if( _divertTargetToCount ) {
var divert = _divertTargetToCount.divert;
var attemptingTurnCountOfVariableTarget = divert.runtimeDivert.variableDivertName != null;
if( attemptingTurnCountOfVariableTarget ) {
Error("When getting the TURNS_SINCE() of a variable target, remove the '->' - i.e. it should just be TURNS_SINCE("+divert.runtimeDivert.variableDivertName+")");
return;
}
var targetObject = divert.targetContent;
if( targetObject == null ) {
if( !attemptingTurnCountOfVariableTarget ) {
Error("Failed to find target for TURNS_SINCE: '"+divert.target+"'");
}
} else {
targetObject.containerForCounting.turnIndexShouldBeCounted = true;
}
}
else if( _variableReferenceToCount ) {
var runtimeVarRef = _variableReferenceToCount.runtimeVarRef;
if( runtimeVarRef.pathForCount != null ) {
Error("Should be "+name+"(-> "+_variableReferenceToCount.name+"). Usage without the '->' only makes sense for variable targets.");
}
}
}
public static bool IsBuiltIn(string name)
{
if (Runtime.NativeFunctionCall.CallExistsWithName (name))
return true;
return name == "CHOICE_COUNT"
|| name == "TURNS_SINCE"
|| name == "TURNS"
|| name == "RANDOM"
|| name == "SEED_RANDOM"
|| name == "LIST_VALUE"
|| name == "LIST_RANDOM"
|| name == "READ_COUNT";
}
public override string ToString ()
{
var strArgs = string.Join (", ", arguments.ToStringsArray());
return string.Format ("{0}({1})", name, strArgs);
}
Parsed.Divert _proxyDivert;
Parsed.DivertTarget _divertTargetToCount;
Parsed.VariableReference _variableReferenceToCount;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1308e59f4c6e3430797ed5278369c5a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@

namespace Ink.Parsed
{
public class Gather : Parsed.Object, IWeavePoint, INamedContent
{
public string name
{
get { return identifier?.name; }
}
public Identifier identifier { get; set; }
public int indentationDepth { get; protected set; }
public Runtime.Container runtimeContainer { get { return (Runtime.Container) runtimeObject; } }
public Gather (Identifier identifier, int indentationDepth)
{
this.identifier = identifier;
this.indentationDepth = indentationDepth;
}
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
container.name = name;
if (this.story.countAllVisits) {
container.visitsShouldBeCounted = true;
}
container.countingAtStartOnly = true;
// A gather can have null content, e.g. it's just purely a line with "-"
if (content != null) {
foreach (var c in content) {
container.AddContent (c.runtimeObject);
}
}
return container;
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
if( identifier != null && identifier.name.Length > 0 )
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c820ea7ecd7524dfa9c467fa4e72e054
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@

namespace Ink.Parsed
{
public interface INamedContent
{
string name { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 262d1f7783ec640e4b3aeef875166a20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public interface IWeavePoint
{
int indentationDepth { get; }
Runtime.Container runtimeContainer { get; }
List<Parsed.Object> content { get; }
string name { get; }
Identifier identifier { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c7b0b5c8842a4c17ba25edb46903749
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Ink.Parsed {
public class Identifier {
public string name;
public Runtime.DebugMetadata debugMetadata;
public override string ToString()
{
return name;
}
public static Identifier Done = new Identifier { name = "DONE", debugMetadata = null };
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 50e035c7d009a254c8d832c3863be7b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@

namespace Ink.Parsed
{
public class IncludedFile : Parsed.Object
{
public Parsed.Story includedStory { get; private set; }
public IncludedFile (Parsed.Story includedStory)
{
this.includedStory = includedStory;
}
public override Runtime.Object GenerateRuntimeObject ()
{
// Left to the main story to process
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9cbcf6c1e16545b2be5a72f4064d93b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class Knot : FlowBase
{
public override FlowLevel flowLevel { get { return FlowLevel.Knot; } }
public Knot (Identifier name, List<Parsed.Object> topLevelObjects, List<Argument> arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction)
{
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
var parentStory = this.story;
// Enforce rule that stitches must not have the same
// name as any knots that exist in the story
foreach (var stitchNamePair in subFlowsByName) {
var stitchName = stitchNamePair.Key;
var knotWithStitchName = parentStory.ContentWithNameAtLevel (stitchName, FlowLevel.Knot, false);
if (knotWithStitchName) {
var stitch = stitchNamePair.Value;
var errorMsg = string.Format ("Stitch '{0}' has the same name as a knot (on {1})", stitch.identifier, knotWithStitchName.debugMetadata);
Error(errorMsg, stitch);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 004df9f5aa1804ff1b359d44aa670019
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class List : Parsed.Expression
{
public List<Identifier> itemIdentifierList;
public List (List<Identifier> itemIdentifierList)
{
this.itemIdentifierList = itemIdentifierList;
}
public override void GenerateIntoContainer (Runtime.Container container)
{
var runtimeRawList = new Runtime.InkList ();
if (itemIdentifierList != null) {
foreach (var itemIdentifier in itemIdentifierList) {
var nameParts = itemIdentifier?.name.Split ('.');
string listName = null;
string listItemName = null;
if (nameParts.Length > 1) {
listName = nameParts [0];
listItemName = nameParts [1];
} else {
listItemName = nameParts [0];
}
var listItem = story.ResolveListItem (listName, listItemName, this);
if (listItem == null) {
if (listName == null)
Error ("Could not find list definition that contains item '" + itemIdentifier + "'");
else
Error ("Could not find list item " + itemIdentifier);
} else {
if (listName == null)
listName = ((ListDefinition)listItem.parent).identifier?.name;
var item = new Runtime.InkListItem (listName, listItem.name);
if (runtimeRawList.ContainsKey (item))
Warning ("Duplicate of item '"+itemIdentifier+"' in list.");
else
runtimeRawList [item] = listItem.seriesValue;
}
}
}
container.AddContent(new Runtime.ListValue (runtimeRawList));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 672f1903584d34411b719aea4d59f1b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ink.Parsed
{
public class ListDefinition : Parsed.Object
{
public Identifier identifier;
public List<ListElementDefinition> itemDefinitions;
public VariableAssignment variableAssignment;
public Runtime.ListDefinition runtimeListDefinition {
get {
var allItems = new Dictionary<string, int> ();
foreach (var e in itemDefinitions) {
if( !allItems.ContainsKey(e.name) )
allItems.Add (e.name, e.seriesValue);
else
Error("List '"+identifier+"' contains dupicate items called '"+e.name+"'");
}
return new Runtime.ListDefinition (identifier?.name, allItems);
}
}
public ListElementDefinition ItemNamed (string itemName)
{
if (_elementsByName == null) {
_elementsByName = new Dictionary<string, ListElementDefinition> ();
foreach (var el in itemDefinitions) {
_elementsByName [el.name] = el;
}
}
ListElementDefinition foundElement;
if (_elementsByName.TryGetValue (itemName, out foundElement))
return foundElement;
return null;
}
public ListDefinition (List<ListElementDefinition> elements)
{
this.itemDefinitions = elements;
int currentValue = 1;
foreach (var e in this.itemDefinitions) {
if (e.explicitValue != null)
currentValue = e.explicitValue.Value;
e.seriesValue = currentValue;
currentValue++;
}
AddContent (elements);
}
public override Runtime.Object GenerateRuntimeObject ()
{
var initialValues = new Runtime.InkList ();
foreach (var itemDef in itemDefinitions) {
if (itemDef.inInitialList) {
var item = new Runtime.InkListItem (this.identifier?.name, itemDef.name);
initialValues [item] = itemDef.seriesValue;
}
}
// Set origin name, so
initialValues.SetInitialOriginName (identifier?.name);
return new Runtime.ListValue (initialValues);
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.List);
}
public override string typeName {
get {
return "List definition";
}
}
Dictionary<string, ListElementDefinition> _elementsByName;
}
public class ListElementDefinition : Parsed.Object
{
public string name
{
get { return identifier?.name; }
}
public Identifier identifier;
public int? explicitValue;
public int seriesValue;
public bool inInitialList;
public string fullName {
get {
var parentList = parent as ListDefinition;
if (parentList == null)
throw new System.Exception ("Can't get full name without a parent list");
return parentList.identifier + "." + name;
}
}
public ListElementDefinition (Identifier identifier, bool inInitialList, int? explicitValue = null)
{
this.identifier = identifier;
this.inInitialList = inInitialList;
this.explicitValue = explicitValue;
}
public override Runtime.Object GenerateRuntimeObject ()
{
throw new System.NotImplementedException ();
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.ListItem);
}
public override string typeName {
get {
return "List element";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 848e06b169a60427cbf371a506af2b8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@

namespace Ink.Parsed
{
public class Number : Parsed.Expression
{
public object value;
public Number(object value)
{
if (value is int || value is float || value is bool) {
this.value = value;
} else {
throw new System.Exception ("Unexpected object type in Number");
}
}
public override void GenerateIntoContainer (Runtime.Container container)
{
if (value is int) {
container.AddContent (new Runtime.IntValue ((int)value));
} else if (value is float) {
container.AddContent (new Runtime.FloatValue ((float)value));
} else if(value is bool) {
container.AddContent (new Runtime.BoolValue ((bool)value));
}
}
public override string ToString ()
{
if (value is float) {
return ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
} else {
return value.ToString();
}
}
// Equals override necessary in order to check for CONST multiple definition equality
public override bool Equals (object obj)
{
var otherNum = obj as Number;
if (otherNum == null) return false;
return this.value.Equals (otherNum.value);
}
public override int GetHashCode ()
{
return this.value.GetHashCode ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 193db1f0576f340febab521eb678105e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,363 @@
using System.Collections.Generic;
using System.Text;
namespace Ink.Parsed
{
public abstract class Object
{
public Runtime.DebugMetadata debugMetadata {
get {
if (_debugMetadata == null) {
if (parent) {
return parent.debugMetadata;
}
}
return _debugMetadata;
}
set {
_debugMetadata = value;
}
}
private Runtime.DebugMetadata _debugMetadata;
public bool hasOwnDebugMetadata {
get {
return _debugMetadata != null;
}
}
public virtual string typeName {
get {
return GetType().Name;
}
}
public Parsed.Object parent { get; set; }
public List<Parsed.Object> content { get; protected set; }
public Parsed.Story story {
get {
Parsed.Object ancestor = this;
while (ancestor.parent) {
ancestor = ancestor.parent;
}
return ancestor as Parsed.Story;
}
}
private Runtime.Object _runtimeObject;
public Runtime.Object runtimeObject
{
get {
if (_runtimeObject == null) {
_runtimeObject = GenerateRuntimeObject ();
if( _runtimeObject )
_runtimeObject.debugMetadata = debugMetadata;
}
return _runtimeObject;
}
set {
_runtimeObject = value;
}
}
// virtual so that certian object types can return a different
// path than just the path to the main runtimeObject.
// e.g. a Choice returns a path to its content rather than
// its outer container.
public virtual Runtime.Path runtimePath
{
get {
return runtimeObject.path;
}
}
// When counting visits and turns since, different object
// types may have different containers that needs to be counted.
// For most it'll just be the object's main runtime object,
// but for e.g. choices, it'll be the target container.
public virtual Runtime.Container containerForCounting
{
get {
return this.runtimeObject as Runtime.Container;
}
}
public Parsed.Path PathRelativeTo(Parsed.Object otherObj)
{
var ownAncestry = ancestry;
var otherAncestry = otherObj.ancestry;
Parsed.Object highestCommonAncestor = null;
int minLength = System.Math.Min (ownAncestry.Count, otherAncestry.Count);
for (int i = 0; i < minLength; ++i) {
var a1 = ancestry [i];
var a2 = otherAncestry [i];
if (a1 == a2)
highestCommonAncestor = a1;
else
break;
}
FlowBase commonFlowAncestor = highestCommonAncestor as FlowBase;
if (commonFlowAncestor == null)
commonFlowAncestor = highestCommonAncestor.ClosestFlowBase ();
var pathComponents = new List<Identifier> ();
bool hasWeavePoint = false;
FlowLevel baseFlow = FlowLevel.WeavePoint;
var ancestor = this;
while(ancestor && (ancestor != commonFlowAncestor) && !(ancestor is Story)) {
if (ancestor == commonFlowAncestor)
break;
if (!hasWeavePoint) {
var weavePointAncestor = ancestor as IWeavePoint;
if (weavePointAncestor != null && weavePointAncestor.identifier != null) {
pathComponents.Add (weavePointAncestor.identifier);
hasWeavePoint = true;
continue;
}
}
var flowAncestor = ancestor as FlowBase;
if (flowAncestor) {
pathComponents.Add (flowAncestor.identifier);
baseFlow = flowAncestor.flowLevel;
}
ancestor = ancestor.parent;
}
pathComponents.Reverse ();
if (pathComponents.Count > 0) {
return new Path (baseFlow, pathComponents);
}
return null;
}
public List<Parsed.Object> ancestry
{
get {
var result = new List<Parsed.Object> ();
var ancestor = this.parent;
while(ancestor) {
result.Add (ancestor);
ancestor = ancestor.parent;
}
result.Reverse ();
return result;
}
}
public string descriptionOfScope
{
get {
var locationNames = new List<string> ();
Parsed.Object ancestor = this;
while (ancestor) {
var ancestorFlow = ancestor as FlowBase;
if (ancestorFlow && ancestorFlow.identifier != null) {
locationNames.Add ("'" + ancestorFlow.identifier + "'");
}
ancestor = ancestor.parent;
}
var scopeSB = new StringBuilder ();
if (locationNames.Count > 0) {
var locationsListStr = string.Join (", ", locationNames.ToArray());
scopeSB.Append (locationsListStr);
scopeSB.Append (" and ");
}
scopeSB.Append ("at top scope");
return scopeSB.ToString ();
}
}
// Return the object so that method can be chained easily
public T AddContent<T>(T subContent) where T : Parsed.Object
{
if (content == null) {
content = new List<Parsed.Object> ();
}
// Make resilient to content not existing, which can happen
// in the case of parse errors where we've already reported
// an error but still want a valid structure so we can
// carry on parsing.
if( subContent ) {
subContent.parent = this;
content.Add(subContent);
}
return subContent;
}
public void AddContent<T>(List<T> listContent) where T : Parsed.Object
{
foreach (var obj in listContent) {
AddContent (obj);
}
}
public T InsertContent<T>(int index, T subContent) where T : Parsed.Object
{
if (content == null) {
content = new List<Parsed.Object> ();
}
subContent.parent = this;
content.Insert (index, subContent);
return subContent;
}
public delegate bool FindQueryFunc<T>(T obj);
public T Find<T>(FindQueryFunc<T> queryFunc = null) where T : class
{
var tObj = this as T;
if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
return tObj;
}
if (content == null)
return null;
foreach (var obj in content) {
var nestedResult = obj.Find (queryFunc);
if (nestedResult != null)
return nestedResult;
}
return null;
}
public List<T> FindAll<T>(FindQueryFunc<T> queryFunc = null) where T : class
{
var found = new List<T> ();
FindAll (queryFunc, found);
return found;
}
void FindAll<T>(FindQueryFunc<T> queryFunc, List<T> foundSoFar) where T : class
{
var tObj = this as T;
if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
foundSoFar.Add (tObj);
}
if (content == null)
return;
foreach (var obj in content) {
obj.FindAll (queryFunc, foundSoFar);
}
}
public abstract Runtime.Object GenerateRuntimeObject ();
public virtual void ResolveReferences(Story context)
{
if (content != null) {
foreach(var obj in content) {
obj.ResolveReferences (context);
}
}
}
public FlowBase ClosestFlowBase()
{
var ancestor = this.parent;
while (ancestor) {
if (ancestor is FlowBase) {
return (FlowBase)ancestor;
}
ancestor = ancestor.parent;
}
return null;
}
public virtual void Error(string message, Parsed.Object source = null, bool isWarning = false)
{
if (source == null) {
source = this;
}
// Only allow a single parsed object to have a single error *directly* associated with it
if (source._alreadyHadError && !isWarning) {
return;
}
if (source._alreadyHadWarning && isWarning) {
return;
}
if (this.parent) {
this.parent.Error (message, source, isWarning);
} else {
throw new System.Exception ("No parent object to send error to: "+message);
}
if (isWarning) {
source._alreadyHadWarning = true;
} else {
source._alreadyHadError = true;
}
}
public void Warning(string message, Parsed.Object source = null)
{
Error (message, source, isWarning: true);
}
// Allow implicit conversion to bool so you don't have to do:
// if( myObj != null ) ...
public static implicit operator bool (Object obj)
{
var isNull = object.ReferenceEquals (obj, null);
return !isNull;
}
public static bool operator ==(Object a, Object b)
{
return object.ReferenceEquals (a, b);
}
public static bool operator !=(Object a, Object b)
{
return !(a == b);
}
public override bool Equals (object obj)
{
return object.ReferenceEquals (obj, this);
}
public override int GetHashCode ()
{
return base.GetHashCode ();
}
bool _alreadyHadError;
bool _alreadyHadWarning;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bfde668c261f944e08e08a5b097043ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ink.Parsed
{
public class Path
{
public FlowLevel baseTargetLevel {
get {
if (baseLevelIsAmbiguous)
return FlowLevel.Story;
else
return (FlowLevel) _baseTargetLevel;
}
}
public bool baseLevelIsAmbiguous {
get {
return _baseTargetLevel == null;
}
}
public string firstComponent {
get {
if (components == null || components.Count == 0)
return null;
return components [0].name;
}
}
public int numberOfComponents {
get {
return components.Count;
}
}
public string dotSeparatedComponents {
get {
if( _dotSeparatedComponents == null ) {
_dotSeparatedComponents = string.Join(".", components.Select(c => c?.name));
}
return _dotSeparatedComponents;
}
}
string _dotSeparatedComponents;
public List<Identifier> components { get; }
public Path(FlowLevel baseFlowLevel, List<Identifier> components)
{
_baseTargetLevel = baseFlowLevel;
this.components = components;
}
public Path(List<Identifier> components)
{
_baseTargetLevel = null;
this.components = components;
}
public Path(Identifier ambiguousName)
{
_baseTargetLevel = null;
components = new List<Identifier> ();
components.Add (ambiguousName);
}
public override string ToString ()
{
if (components == null || components.Count == 0) {
if (baseTargetLevel == FlowLevel.WeavePoint)
return "-> <next gather point>";
else
return "<invalid Path>";
}
return "-> " + dotSeparatedComponents;
}
public Parsed.Object ResolveFromContext(Parsed.Object context)
{
if (components == null || components.Count == 0) {
return null;
}
// Find base target of path from current context. e.g.
// ==> BASE.sub.sub
var baseTargetObject = ResolveBaseTarget (context);
if (baseTargetObject == null) {
return null;
}
// Given base of path, resolve final target by working deeper into hierarchy
// e.g. ==> base.mid.FINAL
if (components.Count > 1) {
return ResolveTailComponents (baseTargetObject);
}
return baseTargetObject;
}
// Find the root object from the base, i.e. root from:
// root.sub1.sub2
Parsed.Object ResolveBaseTarget(Parsed.Object originalContext)
{
var firstComp = firstComponent;
// Work up the ancestry to find the node that has the named object
Parsed.Object ancestorContext = originalContext;
while (ancestorContext != null) {
// Only allow deep search when searching deeper from original context.
// Don't allow search upward *then* downward, since that's searching *everywhere*!
// Allowed examples:
// - From an inner gather of a stitch, you should search up to find a knot called 'x'
// at the root of a story, but not a stitch called 'x' in that knot.
// - However, from within a knot, you should be able to find a gather/choice
// anywhere called 'x'
// (that latter example is quite loose, but we allow it)
bool deepSearch = ancestorContext == originalContext;
var foundBase = TryGetChildFromContext (ancestorContext, firstComp, null, deepSearch);
if (foundBase != null)
return foundBase;
ancestorContext = ancestorContext.parent;
}
return null;
}
// Find the final child from path given root, i.e.:
// root.sub.finalChild
Parsed.Object ResolveTailComponents(Parsed.Object rootTarget)
{
Parsed.Object foundComponent = rootTarget;
for (int i = 1; i < components.Count; ++i) {
var compName = components [i].name;
FlowLevel minimumExpectedLevel;
var foundFlow = foundComponent as FlowBase;
if (foundFlow != null)
minimumExpectedLevel = (FlowLevel)(foundFlow.flowLevel + 1);
else
minimumExpectedLevel = FlowLevel.WeavePoint;
foundComponent = TryGetChildFromContext (foundComponent, compName, minimumExpectedLevel);
if (foundComponent == null)
break;
}
return foundComponent;
}
// See whether "context" contains a child with a given name at a given flow level
// Can either be a named knot/stitch (a FlowBase) or a weave point within a Weave (Choice or Gather)
// This function also ignores any other object types that are neither FlowBase nor Weave.
// Called from both ResolveBase (force deep) and ResolveTail for the individual components.
Parsed.Object TryGetChildFromContext(Parsed.Object context, string childName, FlowLevel? minimumLevel, bool forceDeepSearch = false)
{
// null childLevel means that we don't know where to find it
bool ambiguousChildLevel = minimumLevel == null;
// Search for WeavePoint within Weave
var weaveContext = context as Weave;
if ( weaveContext != null && (ambiguousChildLevel || minimumLevel == FlowLevel.WeavePoint)) {
return (Parsed.Object) weaveContext.WeavePointNamed (childName);
}
// Search for content within Flow (either a sub-Flow or a WeavePoint)
var flowContext = context as FlowBase;
if (flowContext != null) {
// When searching within a Knot, allow a deep searches so that
// named weave points (choices and gathers) can be found within any stitch
// Otherwise, we just search within the immediate object.
var shouldDeepSearch = forceDeepSearch || flowContext.flowLevel == FlowLevel.Knot;
return flowContext.ContentWithNameAtLevel (childName, minimumLevel, shouldDeepSearch);
}
return null;
}
FlowLevel? _baseTargetLevel;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc7b7f981067b4ab7ac3fd451c958785
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
namespace Ink.Parsed
{
public class Return : Parsed.Object
{
public Expression returnedExpression { get; protected set; }
public Return (Expression returnedExpression = null)
{
if (returnedExpression) {
this.returnedExpression = AddContent(returnedExpression);
}
}
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
// Evaluate expression
if (returnedExpression) {
container.AddContent (returnedExpression.runtimeObject);
}
// Return Runtime.Void when there's no expression to evaluate
// (This evaluation will just add the Void object to the evaluation stack)
else {
container.AddContent (Runtime.ControlCommand.EvalStart ());
container.AddContent (new Runtime.Void());
container.AddContent (Runtime.ControlCommand.EvalEnd ());
}
// Then pop the call stack
// (the evaluated expression will leave the return value on the evaluation stack)
container.AddContent (Runtime.ControlCommand.PopFunction());
return container;
}
}
}

Some files were not shown because too many files have changed in this diff Show More