mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
Committed everything
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Ink-Libraries",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58bed0e7c5306824586d7eda03609289
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9d4bbcea2b35784b919f188e0d2c800
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3d8ce43e51d74eb791e0665c2ba4fc7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8d8b00ea012047a29ce7e7b4f177835
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c05786c433ce449658bbeffe36e74ffd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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> ();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 041ad81bb22094b95bec754d7d480a82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: beb7e0e9816ad714290ca610ac174808
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e0d4431a2a4b447e8ed4b5978110831
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59b3b9369a2a7481aab00ef369b7c4a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97d6f86dbb9994db6bd0572b69f53f25
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a2f15af905c94ac9bdfb197799eec41
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2383f2e1b977347c2b6fb596e9af6d97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea21bfef8977a4907b49628d9b651ebf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ca27cda0c9364bfbaf37f8db278563f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 334d80c537ee2473ea6a7cbd20e09f14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8af10c051a6c942b1a43cb31ceb18114
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ("<-");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7aec7c9fb87244b3a8c64753c8a2f9b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85c9eb24bda894235981df7a3689da51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4b92fc2dbd664298b01416321678d61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77e8119b05a284a889e4b2978c6b1617
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa9a6c9127e3f4eb8a46f532557ccd2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e33e10232e5c4587a8c460cf9d99960
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42fdd6da57c584bf58ff578d093b8529
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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\\");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fed5929e88fa3480e8f02d3f686c0c2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbdaec262d8f64dd684a79c235405a10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12f91c70015a64a2ba10b2e2fd5d25b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e3f49d19a4e4473086ad32869ce362f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36d3c5151e15a45b4b7a2ac7b355bff9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24c93c5d49d7f498f9b658d38f48e0b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6cda85f80c124e12b215fdbc95b33bb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9e4e9e3b695418f92d259e9d047fb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6c599566895c444da36111b8aa6bd5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d310eb8e3c23e41db891c939a1313cda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26c47b19962e641869a39b85cd86f9e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8a428e7434204b02921e47651c42329
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 476809ca3ec8f4743afd6fa33bd6e442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca4476886042f471a9771284a027b45f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 573f03d14273b46a29e16c5ff9ec8059
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public enum FlowLevel
|
||||
{
|
||||
Story,
|
||||
Knot,
|
||||
Stitch,
|
||||
WeavePoint // not actually a FlowBase, but used for diverts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3e0618d06b104e3a82b27f11b68ad54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1308e59f4c6e3430797ed5278369c5a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c820ea7ecd7524dfa9c467fa4e72e054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public interface INamedContent
|
||||
{
|
||||
string name { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 262d1f7783ec640e4b3aeef875166a20
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c7b0b5c8842a4c17ba25edb46903749
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50e035c7d009a254c8d832c3863be7b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9cbcf6c1e16545b2be5a72f4064d93b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 004df9f5aa1804ff1b359d44aa670019
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 672f1903584d34411b719aea4d59f1b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 848e06b169a60427cbf371a506af2b8d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 193db1f0576f340febab521eb678105e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfde668c261f944e08e08a5b097043ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc7b7f981067b4ab7ac3fd451c958785
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
Reference in New Issue
Block a user