Committed everything

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,206 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
[System.Flags]
public enum SequenceType
{
Stopping = 1, // default
Cycle = 2,
Shuffle = 4,
Once = 8
}
public class Sequence : Parsed.Object
{
public List<Parsed.Object> sequenceElements;
public SequenceType sequenceType;
public Sequence (List<ContentList> elementContentLists, SequenceType sequenceType)
{
this.sequenceType = sequenceType;
this.sequenceElements = new List<Parsed.Object> ();
foreach (var elementContentList in elementContentLists) {
var contentObjs = elementContentList.content;
Parsed.Object seqElObject = null;
// Don't attempt to create a weave for the sequence element
// if the content list is empty. Weaves don't like it!
if (contentObjs == null || contentObjs.Count == 0)
seqElObject = elementContentList;
else
seqElObject = new Weave (contentObjs);
this.sequenceElements.Add (seqElObject);
AddContent (seqElObject);
}
}
// Generate runtime code that looks like:
//
// chosenIndex = MIN(sequence counter, num elements) e.g. for "Stopping"
// if chosenIndex == 0, divert to s0
// if chosenIndex == 1, divert to s1 [etc]
//
// - s0:
// <content for sequence element>
// divert to no-op
// - s1:
// <content for sequence element>
// divert to no-op
// - s2:
// empty branch if using "once"
// divert to no-op
//
// no-op
//
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
container.visitsShouldBeCounted = true;
container.countingAtStartOnly = true;
_sequenceDivertsToResove = new List<SequenceDivertToResolve> ();
// Get sequence read count
container.AddContent (Runtime.ControlCommand.EvalStart ());
container.AddContent (Runtime.ControlCommand.VisitIndex ());
bool once = (sequenceType & SequenceType.Once) > 0;
bool cycle = (sequenceType & SequenceType.Cycle) > 0;
bool stopping = (sequenceType & SequenceType.Stopping) > 0;
bool shuffle = (sequenceType & SequenceType.Shuffle) > 0;
var seqBranchCount = sequenceElements.Count;
if (once) seqBranchCount++;
// Chosen sequence index:
// - Stopping: take the MIN(read count, num elements - 1)
// - Once: take the MIN(read count, num elements)
// (the last one being empty)
if (stopping || once) {
//var limit = stopping ? seqBranchCount-1 : seqBranchCount;
container.AddContent (new Runtime.IntValue (seqBranchCount-1));
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("MIN"));
}
// - Cycle: take (read count % num elements)
else if (cycle) {
container.AddContent (new Runtime.IntValue (sequenceElements.Count));
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("%"));
}
// Shuffle
if (shuffle) {
// Create point to return to when sequence is complete
var postShuffleNoOp = Runtime.ControlCommand.NoOp();
// When visitIndex == lastIdx, we skip the shuffle
if ( once || stopping )
{
// if( visitIndex == lastIdx ) -> skipShuffle
int lastIdx = stopping ? sequenceElements.Count - 1 : sequenceElements.Count;
container.AddContent(Runtime.ControlCommand.Duplicate());
container.AddContent(new Runtime.IntValue(lastIdx));
container.AddContent(Runtime.NativeFunctionCall.CallWithName("=="));
var skipShuffleDivert = new Runtime.Divert();
skipShuffleDivert.isConditional = true;
container.AddContent(skipShuffleDivert);
AddDivertToResolve(skipShuffleDivert, postShuffleNoOp);
}
// This one's a bit more complex! Choose the index at runtime.
var elementCountToShuffle = sequenceElements.Count;
if (stopping) elementCountToShuffle--;
container.AddContent (new Runtime.IntValue (elementCountToShuffle));
container.AddContent (Runtime.ControlCommand.SequenceShuffleIndex ());
if (once || stopping) container.AddContent(postShuffleNoOp);
}
container.AddContent (Runtime.ControlCommand.EvalEnd ());
// Create point to return to when sequence is complete
var postSequenceNoOp = Runtime.ControlCommand.NoOp();
// Each of the main sequence branches, and one extra empty branch if
// we have a "once" sequence.
for (var elIndex=0; elIndex<seqBranchCount; elIndex++) {
// This sequence element:
// if( chosenIndex == this index ) divert to this sequence element
// duplicate chosen sequence index, since it'll be consumed by "=="
container.AddContent (Runtime.ControlCommand.EvalStart ());
container.AddContent (Runtime.ControlCommand.Duplicate ());
container.AddContent (new Runtime.IntValue (elIndex));
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("=="));
container.AddContent (Runtime.ControlCommand.EvalEnd ());
// Divert branch for this sequence element
var sequenceDivert = new Runtime.Divert ();
sequenceDivert.isConditional = true;
container.AddContent (sequenceDivert);
Runtime.Container contentContainerForSequenceBranch;
// Generate content for this sequence element
if ( elIndex < sequenceElements.Count ) {
var el = sequenceElements[elIndex];
contentContainerForSequenceBranch = (Runtime.Container)el.runtimeObject;
}
// Final empty branch for "once" sequences
else {
contentContainerForSequenceBranch = new Runtime.Container();
}
contentContainerForSequenceBranch.name = "s" + elIndex;
contentContainerForSequenceBranch.InsertContent(Runtime.ControlCommand.PopEvaluatedValue(), 0);
// When sequence element is complete, divert back to end of sequence
var seqBranchCompleteDivert = new Runtime.Divert ();
contentContainerForSequenceBranch.AddContent (seqBranchCompleteDivert);
container.AddToNamedContentOnly (contentContainerForSequenceBranch);
// Save the diverts for reference resolution later (in ResolveReferences)
AddDivertToResolve (sequenceDivert, contentContainerForSequenceBranch);
AddDivertToResolve (seqBranchCompleteDivert, postSequenceNoOp);
}
container.AddContent (postSequenceNoOp);
return container;
}
void AddDivertToResolve(Runtime.Divert divert, Runtime.Object targetContent)
{
_sequenceDivertsToResove.Add( new SequenceDivertToResolve() {
divert = divert,
targetContent = targetContent
});
}
public override void ResolveReferences(Story context)
{
base.ResolveReferences (context);
foreach (var toResolve in _sequenceDivertsToResove) {
toResolve.divert.targetPath = toResolve.targetContent.path;
}
}
class SequenceDivertToResolve
{
public Runtime.Divert divert;
public Runtime.Object targetContent;
}
List<SequenceDivertToResolve> _sequenceDivertsToResove;
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class Stitch : FlowBase
{
public override FlowLevel flowLevel { get { return FlowLevel.Stitch; } }
public Stitch (Identifier name, List<Parsed.Object> topLevelObjects, List<Argument> arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction)
{
}
}
}

View File

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

View File

@@ -0,0 +1,508 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.CompilerServices;
using System.Diagnostics;
[assembly: InternalsVisibleTo("tests")]
namespace Ink.Parsed
{
public class Story : FlowBase
{
public override FlowLevel flowLevel { get { return FlowLevel.Story; } }
/// <summary>
/// Had error during code gen, resolve references?
/// Most of the time it shouldn't be necessary to use this
/// since errors should be caught by the error handler.
/// </summary>
internal bool hadError { get { return _hadError; } }
internal bool hadWarning { get { return _hadWarning; } }
public Dictionary<string, Expression> constants;
public Dictionary<string, ExternalDeclaration> externals;
// Build setting for exporting:
// When true, the visit count for *all* knots, stitches, choices,
// and gathers is counted. When false, only those that are direclty
// referenced by the ink are recorded. Use this flag to allow game-side
// querying of arbitrary knots/stitches etc.
// Storing all counts is more robust and future proof (updates to the story file
// that reference previously uncounted visits are possible, but generates a much
// larger safe file, with a lot of potentially redundant counts.
public bool countAllVisits = false;
public Story (List<Parsed.Object> toplevelObjects, bool isInclude = false) : base(null, toplevelObjects, isIncludedStory:isInclude)
{
// Don't do anything much on construction, leave it lightweight until
// the ExportRuntime method is called.
}
// Before this function is called, we have IncludedFile objects interspersed
// in our content wherever an include statement was.
// So that the include statement can be added in a sensible place (e.g. the
// top of the file) without side-effects of jumping into a knot that was
// defined in that include, we separate knots and stitches from anything
// else defined at the top scope of the included file.
//
// Algorithm: For each IncludedFile we find, split its contents into
// knots/stiches and any other content. Insert the normal content wherever
// the include statement was, and append the knots/stitches to the very
// end of the main story.
protected override void PreProcessTopLevelObjects(List<Parsed.Object> topLevelContent)
{
var flowsFromOtherFiles = new List<FlowBase> ();
// Inject included files
int i = 0;
while (i < topLevelContent.Count) {
var obj = topLevelContent [i];
if (obj is IncludedFile) {
var file = (IncludedFile)obj;
// Remove the IncludedFile itself
topLevelContent.RemoveAt (i);
// When an included story fails to load, the include
// line itself is still valid, so we have to handle it here
if (file.includedStory) {
var nonFlowContent = new List<Parsed.Object> ();
var subStory = file.includedStory;
// Allow empty file
if (subStory.content != null) {
foreach (var subStoryObj in subStory.content) {
if (subStoryObj is FlowBase) {
flowsFromOtherFiles.Add ((FlowBase)subStoryObj);
} else {
nonFlowContent.Add (subStoryObj);
}
}
// Add newline on the end of the include
nonFlowContent.Add (new Parsed.Text ("\n"));
// Add contents of the file in its place
topLevelContent.InsertRange (i, nonFlowContent);
// Skip past the content of this sub story
// (since it will already have recursively included
// any lines from other files)
i += nonFlowContent.Count;
}
}
// Include object has been removed, with possible content inserted,
// and position of 'i' will have been determined already.
continue;
}
// Non-include: skip over it
else {
i++;
}
}
// Add the flows we collected from the included files to the
// end of our list of our content
topLevelContent.AddRange (flowsFromOtherFiles.ToArray());
}
public Runtime.Story ExportRuntime(ErrorHandler errorHandler = null)
{
_errorHandler = errorHandler;
// Find all constants before main export begins, so that VariableReferences know
// whether to generate a runtime variable reference or the literal value
constants = new Dictionary<string, Expression> ();
foreach (var constDecl in FindAll<ConstantDeclaration> ()) {
// Check for duplicate definitions
Parsed.Expression existingDefinition = null;
if (constants.TryGetValue (constDecl.constantName, out existingDefinition)) {
if (!existingDefinition.Equals (constDecl.expression)) {
var errorMsg = string.Format ("CONST '{0}' has been redefined with a different value. Multiple definitions of the same CONST are valid so long as they contain the same value. Initial definition was on {1}.", constDecl.constantName, existingDefinition.debugMetadata);
Error (errorMsg, constDecl, isWarning:false);
}
}
constants [constDecl.constantName] = constDecl.expression;
}
// List definitions are treated like constants too - they should be usable
// from other variable declarations.
_listDefs = new Dictionary<string, ListDefinition> ();
foreach (var listDef in FindAll<ListDefinition> ()) {
_listDefs [listDef.identifier?.name] = listDef;
}
externals = new Dictionary<string, ExternalDeclaration> ();
// Resolution of weave point names has to come first, before any runtime code generation
// since names have to be ready before diverts start getting created.
// (It used to be done in the constructor for a weave, but didn't allow us to generate
// errors when name resolution failed.)
ResolveWeavePointNaming ();
// Get default implementation of runtimeObject, which calls ContainerBase's generation method
var rootContainer = runtimeObject as Runtime.Container;
// Export initialisation of global variables
// TODO: We *could* add this as a declarative block to the story itself...
var variableInitialisation = new Runtime.Container ();
variableInitialisation.AddContent (Runtime.ControlCommand.EvalStart ());
// Global variables are those that are local to the story and marked as global
var runtimeLists = new List<Runtime.ListDefinition> ();
foreach (var nameDeclPair in variableDeclarations) {
var varName = nameDeclPair.Key;
var varDecl = nameDeclPair.Value;
if (varDecl.isGlobalDeclaration) {
if (varDecl.listDefinition != null) {
_listDefs[varName] = varDecl.listDefinition;
variableInitialisation.AddContent (varDecl.listDefinition.runtimeObject);
runtimeLists.Add (varDecl.listDefinition.runtimeListDefinition);
} else {
varDecl.expression.GenerateIntoContainer (variableInitialisation);
}
var runtimeVarAss = new Runtime.VariableAssignment (varName, isNewDeclaration:true);
runtimeVarAss.isGlobal = true;
variableInitialisation.AddContent (runtimeVarAss);
}
}
variableInitialisation.AddContent (Runtime.ControlCommand.EvalEnd ());
variableInitialisation.AddContent (Runtime.ControlCommand.End ());
if (variableDeclarations.Count > 0) {
variableInitialisation.name = "global decl";
rootContainer.AddToNamedContentOnly (variableInitialisation);
}
// Signal that it's safe to exit without error, even if there are no choices generated
// (this only happens at the end of top level content that isn't in any particular knot)
rootContainer.AddContent (Runtime.ControlCommand.Done ());
// Replace runtimeObject with Story object instead of the Runtime.Container generated by Parsed.ContainerBase
var runtimeStory = new Runtime.Story (rootContainer, runtimeLists);
runtimeObject = runtimeStory;
if (_hadError)
return null;
// Optimisation step - inline containers that can be
FlattenContainersIn (rootContainer);
// Now that the story has been fulled parsed into a hierarchy,
// and the derived runtime hierarchy has been built, we can
// resolve referenced symbols such as variables and paths.
// e.g. for paths " -> knotName --> stitchName" into an INKPath (knotName.stitchName)
// We don't make any assumptions that the INKPath follows the same
// conventions as the script format, so we resolve to actual objects before
// translating into an INKPath. (This also allows us to choose whether
// we want the paths to be absolute)
ResolveReferences (this);
if (_hadError)
return null;
runtimeStory.ResetState ();
return runtimeStory;
}
public ListDefinition ResolveList (string listName)
{
ListDefinition list;
if (!_listDefs.TryGetValue (listName, out list))
return null;
return list;
}
public ListElementDefinition ResolveListItem (string listName, string itemName, Parsed.Object source = null)
{
ListDefinition listDef = null;
// Search a specific list if we know its name (i.e. the form listName.itemName)
if (listName != null) {
if (!_listDefs.TryGetValue (listName, out listDef))
return null;
return listDef.ItemNamed (itemName);
}
// Otherwise, try to search all lists
else {
ListElementDefinition foundItem = null;
ListDefinition originalFoundList = null;
foreach (var namedList in _listDefs) {
var listToSearch = namedList.Value;
var itemInThisList = listToSearch.ItemNamed (itemName);
if (itemInThisList) {
if (foundItem != null) {
Error ("Ambiguous item name '" + itemName + "' found in multiple sets, including "+originalFoundList.identifier+" and "+listToSearch.identifier, source, isWarning:false);
} else {
foundItem = itemInThisList;
originalFoundList = listToSearch;
}
}
}
return foundItem;
}
}
void FlattenContainersIn (Runtime.Container container)
{
// Need to create a collection to hold the inner containers
// because otherwise we'd end up modifying during iteration
var innerContainers = new HashSet<Runtime.Container> ();
foreach (var c in container.content) {
var innerContainer = c as Runtime.Container;
if (innerContainer)
innerContainers.Add (innerContainer);
}
// Can't flatten the named inner containers, but we can at least
// iterate through their children
if (container.namedContent != null) {
foreach (var keyValue in container.namedContent) {
var namedInnerContainer = keyValue.Value as Runtime.Container;
if (namedInnerContainer)
innerContainers.Add (namedInnerContainer);
}
}
foreach (var innerContainer in innerContainers) {
TryFlattenContainer (innerContainer);
FlattenContainersIn (innerContainer);
}
}
void TryFlattenContainer (Runtime.Container container)
{
if (container.namedContent.Count > 0 || container.hasValidName || _dontFlattenContainers.Contains(container))
return;
// Inline all the content in container into the parent
var parentContainer = container.parent as Runtime.Container;
if (parentContainer) {
var contentIdx = parentContainer.content.IndexOf (container);
parentContainer.content.RemoveAt (contentIdx);
var dm = container.ownDebugMetadata;
foreach (var innerContent in container.content) {
innerContent.parent = null;
if (dm != null && innerContent.ownDebugMetadata == null)
innerContent.debugMetadata = dm;
parentContainer.InsertContent (innerContent, contentIdx);
contentIdx++;
}
}
}
public override void Error(string message, Parsed.Object source, bool isWarning)
{
ErrorType errorType = isWarning ? ErrorType.Warning : ErrorType.Error;
var sb = new StringBuilder ();
if (source is AuthorWarning) {
sb.Append ("TODO: ");
errorType = ErrorType.Author;
} else if (isWarning) {
sb.Append ("WARNING: ");
} else {
sb.Append ("ERROR: ");
}
if (source && source.debugMetadata != null && source.debugMetadata.startLineNumber >= 1 ) {
if (source.debugMetadata.fileName != null) {
sb.AppendFormat ("'{0}' ", source.debugMetadata.fileName);
}
sb.AppendFormat ("line {0}: ", source.debugMetadata.startLineNumber);
}
sb.Append (message);
message = sb.ToString ();
if (_errorHandler != null) {
_hadError = errorType == ErrorType.Error;
_hadWarning = errorType == ErrorType.Warning;
_errorHandler (message, errorType);
} else {
throw new System.Exception (message);
}
}
public void ResetError()
{
_hadError = false;
_hadWarning = false;
}
public bool IsExternal(string namedFuncTarget)
{
return externals.ContainsKey (namedFuncTarget);
}
public void AddExternal(ExternalDeclaration decl)
{
if (externals.ContainsKey (decl.name)) {
Error ("Duplicate EXTERNAL definition of '"+decl.name+"'", decl, false);
} else {
externals [decl.name] = decl;
}
}
public void DontFlattenContainer (Runtime.Container container)
{
_dontFlattenContainers.Add (container);
}
void NameConflictError (Parsed.Object obj, string name, Parsed.Object existingObj, string typeNameToPrint)
{
obj.Error (typeNameToPrint+" '" + name + "': name has already been used for a " + existingObj.typeName.ToLower() + " on " +existingObj.debugMetadata);
}
public static bool IsReservedKeyword (string name)
{
switch (name) {
case "true":
case "false":
case "not":
case "return":
case "else":
case "VAR":
case "CONST":
case "temp":
case "LIST":
case "function":
return true;
}
return false;
}
public enum SymbolType : uint
{
Knot,
List,
ListItem,
Var,
SubFlowAndWeave,
Arg,
Temp
}
// Check given symbol type against everything that's of a higher priority in the ordered SymbolType enum (above).
// When the given symbol type level is reached, we early-out / return.
public void CheckForNamingCollisions (Parsed.Object obj, Identifier identifier, SymbolType symbolType, string typeNameOverride = null)
{
string typeNameToPrint = typeNameOverride ?? obj.typeName;
if (IsReservedKeyword (identifier?.name)) {
obj.Error ("'"+name + "' cannot be used for the name of a " + typeNameToPrint.ToLower() + " because it's a reserved keyword");
return;
}
if (FunctionCall.IsBuiltIn (identifier?.name)) {
obj.Error ("'"+name + "' cannot be used for the name of a " + typeNameToPrint.ToLower() + " because it's a built in function");
return;
}
// Top level knots
FlowBase knotOrFunction = ContentWithNameAtLevel (identifier?.name, FlowLevel.Knot) as FlowBase;
if (knotOrFunction && (knotOrFunction != obj || symbolType == SymbolType.Arg)) {
NameConflictError (obj, identifier?.name, knotOrFunction, typeNameToPrint);
return;
}
if (symbolType < SymbolType.List) return;
// Lists
foreach (var namedListDef in _listDefs) {
var listDefName = namedListDef.Key;
var listDef = namedListDef.Value;
if (identifier?.name == listDefName && obj != listDef && listDef.variableAssignment != obj) {
NameConflictError (obj, identifier?.name, listDef, typeNameToPrint);
}
// We don't check for conflicts between individual elements in
// different lists because they are namespaced.
if (!(obj is ListElementDefinition)) {
foreach (var item in listDef.itemDefinitions) {
if (identifier?.name == item.name) {
NameConflictError (obj, identifier?.name, item, typeNameToPrint);
}
}
}
}
// Don't check for VAR->VAR conflicts because that's handled separately
// (necessary since checking looks up in a dictionary)
if (symbolType <= SymbolType.Var) return;
// Global variable collision
VariableAssignment varDecl = null;
if (variableDeclarations.TryGetValue(identifier?.name, out varDecl) ) {
if (varDecl != obj && varDecl.isGlobalDeclaration && varDecl.listDefinition == null) {
NameConflictError (obj, identifier?.name, varDecl, typeNameToPrint);
}
}
if (symbolType < SymbolType.SubFlowAndWeave) return;
// Stitches, Choices and Gathers
var path = new Path (identifier);
var targetContent = path.ResolveFromContext (obj);
if (targetContent && targetContent != obj) {
NameConflictError (obj, identifier?.name, targetContent, typeNameToPrint);
return;
}
if (symbolType < SymbolType.Arg) return;
// Arguments to the current flow
if (symbolType != SymbolType.Arg) {
FlowBase flow = obj as FlowBase;
if( flow == null ) flow = obj.ClosestFlowBase ();
if (flow && flow.hasParameters) {
foreach (var arg in flow.arguments) {
if (arg.identifier?.name == identifier?.name) {
obj.Error (typeNameToPrint+" '" + name + "': Name has already been used for a argument to "+flow.identifier+" on " +flow.debugMetadata);
return;
}
}
}
}
}
ErrorHandler _errorHandler;
bool _hadError;
bool _hadWarning;
HashSet<Runtime.Container> _dontFlattenContainers = new HashSet<Runtime.Container>();
Dictionary<string, Parsed.ListDefinition> _listDefs;
}
}

View File

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

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ink.Parsed
{
public class StringExpression : Parsed.Expression
{
public bool isSingleString {
get {
if (content.Count != 1)
return false;
var c = content [0];
if (!(c is Text))
return false;
return true;
}
}
public StringExpression (List<Parsed.Object> content)
{
AddContent (content);
}
public override void GenerateIntoContainer (Runtime.Container container)
{
container.AddContent (Runtime.ControlCommand.BeginString());
foreach (var c in content) {
container.AddContent (c.runtimeObject);
}
container.AddContent (Runtime.ControlCommand.EndString());
}
public override string ToString ()
{
var sb = new StringBuilder ();
foreach (var c in content) {
sb.Append (c.ToString ());
}
return sb.ToString ();
}
// Equals override necessary in order to check for CONST multiple definition equality
public override bool Equals (object obj)
{
var otherStr = obj as StringExpression;
if (otherStr == null) return false;
// Can only compare direct equality on single strings rather than
// complex string expressions that contain dynamic logic
if (!this.isSingleString || !otherStr.isSingleString) {
return false;
}
var thisTxt = this.ToString ();
var otherTxt = otherStr.ToString ();
return thisTxt.Equals (otherTxt);
}
public override int GetHashCode ()
{
return this.ToString ().GetHashCode ();
}
}
}

View File

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

View File

@@ -0,0 +1,24 @@

namespace Ink.Parsed
{
public class Text : Parsed.Object
{
public string text { get; set; }
public Text (string str)
{
text = str;
}
public override Runtime.Object GenerateRuntimeObject ()
{
return new Runtime.StringValue(this.text);
}
public override string ToString ()
{
return this.text;
}
}
}

View File

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

View File

@@ -0,0 +1,85 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class TunnelOnwards : Parsed.Object
{
public Divert divertAfter {
get {
return _divertAfter;
}
set {
_divertAfter = value;
if (_divertAfter) AddContent (_divertAfter);
}
}
Divert _divertAfter;
public override Runtime.Object GenerateRuntimeObject ()
{
var container = new Runtime.Container ();
// Set override path for tunnel onwards (or nothing)
container.AddContent (Runtime.ControlCommand.EvalStart ());
if (divertAfter) {
// Generate runtime object's generated code and steal the arguments runtime code
var returnRuntimeObj = divertAfter.GenerateRuntimeObject ();
var returnRuntimeContainer = returnRuntimeObj as Runtime.Container;
if (returnRuntimeContainer) {
// Steal all code for generating arguments from the divert
var args = divertAfter.arguments;
if (args != null && args.Count > 0) {
// Steal everything betwen eval start and eval end
int evalStart = -1;
int evalEnd = -1;
for (int i = 0; i < returnRuntimeContainer.content.Count; i++) {
var cmd = returnRuntimeContainer.content [i] as Runtime.ControlCommand;
if (cmd) {
if (evalStart == -1 && cmd.commandType == Runtime.ControlCommand.CommandType.EvalStart)
evalStart = i;
else if (cmd.commandType == Runtime.ControlCommand.CommandType.EvalEnd)
evalEnd = i;
}
}
for (int i = evalStart + 1; i < evalEnd; i++) {
var obj = returnRuntimeContainer.content [i];
obj.parent = null; // prevent error of being moved between owners
container.AddContent (returnRuntimeContainer.content [i]);
}
}
}
// Finally, divert to the requested target
_overrideDivertTarget = new Runtime.DivertTargetValue ();
container.AddContent (_overrideDivertTarget);
}
// No divert after tunnel onwards
else {
container.AddContent (new Runtime.Void ());
}
container.AddContent (Runtime.ControlCommand.EvalEnd ());
container.AddContent (Runtime.ControlCommand.PopTunnel ());
return container;
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
if (divertAfter && divertAfter.targetContent)
_overrideDivertTarget.targetPath = divertAfter.targetContent.runtimePath;
}
Runtime.DivertTargetValue _overrideDivertTarget;
}
}

View File

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

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
public class VariableAssignment : Parsed.Object
{
public string variableName
{
get { return variableIdentifier.name; }
}
public Identifier variableIdentifier { get; protected set; }
public Expression expression { get; protected set; }
public ListDefinition listDefinition { get; protected set; }
public bool isGlobalDeclaration { get; set; }
public bool isNewTemporaryDeclaration { get; set; }
public bool isDeclaration {
get {
return isGlobalDeclaration || isNewTemporaryDeclaration;
}
}
public VariableAssignment (Identifier identifier, Expression assignedExpression)
{
this.variableIdentifier = identifier;
// Defensive programming in case parsing of assignedExpression failed
if( assignedExpression )
this.expression = AddContent(assignedExpression);
}
public VariableAssignment (Identifier identifier, ListDefinition listDef)
{
this.variableIdentifier = identifier;
if (listDef) {
this.listDefinition = AddContent (listDef);
this.listDefinition.variableAssignment = this;
}
// List definitions are always global
isGlobalDeclaration = true;
}
public override Runtime.Object GenerateRuntimeObject ()
{
FlowBase newDeclScope = null;
if (isGlobalDeclaration) {
newDeclScope = story;
} else if(isNewTemporaryDeclaration) {
newDeclScope = ClosestFlowBase ();
}
if( newDeclScope )
newDeclScope.TryAddNewVariableDeclaration (this);
// Global declarations don't generate actual procedural
// runtime objects, but instead add a global variable to the story itself.
// The story then initialises them all in one go at the start of the game.
if( isGlobalDeclaration )
return null;
var container = new Runtime.Container ();
// The expression's runtimeObject is actually another nested container
if( expression != null )
container.AddContent (expression.runtimeObject);
else if( listDefinition != null )
container.AddContent (listDefinition.runtimeObject);
_runtimeAssignment = new Runtime.VariableAssignment(variableName, isNewTemporaryDeclaration);
container.AddContent (_runtimeAssignment);
return container;
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
// List definitions are checked for conflicts separately
if( this.isDeclaration && listDefinition == null )
context.CheckForNamingCollisions (this, variableIdentifier, this.isGlobalDeclaration ? Story.SymbolType.Var : Story.SymbolType.Temp);
// Initial VAR x = [intialValue] declaration, not re-assignment
if (this.isGlobalDeclaration) {
var variableReference = expression as VariableReference;
if (variableReference && !variableReference.isConstantReference && !variableReference.isListItemReference) {
Error ("global variable assignments cannot refer to other variables, only literal values, constants and list items");
}
}
if (!this.isNewTemporaryDeclaration) {
var resolvedVarAssignment = context.ResolveVariableWithName(this.variableName, fromNode: this);
if (!resolvedVarAssignment.found) {
if (story.constants.ContainsKey (variableName)) {
Error ("Can't re-assign to a constant (do you need to use VAR when declaring '" + this.variableName + "'?)", this);
} else {
Error ("Variable could not be found to assign to: '" + this.variableName + "'", this);
}
}
// A runtime assignment may not have been generated if it's the initial global declaration,
// since these are hoisted out and handled specially in Story.ExportRuntime.
if( _runtimeAssignment != null )
_runtimeAssignment.isGlobal = resolvedVarAssignment.isGlobal;
}
}
public override string typeName {
get {
if (isNewTemporaryDeclaration) return "temp";
else if (isGlobalDeclaration) return "VAR";
else return "variable assignment";
}
}
Runtime.VariableAssignment _runtimeAssignment;
}
}

View File

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

View File

@@ -0,0 +1,152 @@
using System.Collections.Generic;
using System.Linq;
namespace Ink.Parsed
{
public class VariableReference : Expression
{
// - Normal variables have a single item in their "path"
// - Knot/stitch names for read counts are actual dot-separated paths
// (though this isn't actually used at time of writing)
// - List names are dot separated: listName.itemName (or just itemName)
public string name { get; private set; }
public Identifier identifier {
get {
// Merging the list of identifiers into a single identifier.
// Debug metadata is also merged.
if (pathIdentifiers == null || pathIdentifiers.Count == 0) {
return null;
}
if( _singleIdentifier == null ) {
var name = string.Join (".", path.ToArray());
var firstDebugMetadata = pathIdentifiers.First().debugMetadata;
var debugMetadata = pathIdentifiers.Aggregate(firstDebugMetadata, (acc, id) => acc.Merge(id.debugMetadata));
_singleIdentifier = new Identifier { name = name, debugMetadata = debugMetadata };
}
return _singleIdentifier;
}
}
Identifier _singleIdentifier;
public List<Identifier> pathIdentifiers;
public List<string> path { get; private set; }
// Only known after GenerateIntoContainer has run
public bool isConstantReference;
public bool isListItemReference;
public Runtime.VariableReference runtimeVarRef { get { return _runtimeVarRef; } }
public VariableReference (List<Identifier> pathIdentifiers)
{
this.pathIdentifiers = pathIdentifiers;
this.path = pathIdentifiers.Select(id => id?.name).ToList();
this.name = string.Join (".", pathIdentifiers);
}
public override void GenerateIntoContainer (Runtime.Container container)
{
Expression constantValue = null;
// If it's a constant reference, just generate the literal expression value
// It's okay to access the constants at code generation time, since the
// first thing the ExportRuntime function does it search for all the constants
// in the story hierarchy, so they're all available.
if ( story.constants.TryGetValue (name, out constantValue) ) {
constantValue.GenerateConstantIntoContainer (container);
isConstantReference = true;
return;
}
_runtimeVarRef = new Runtime.VariableReference (name);
// List item reference?
// Path might be to a list (listName.listItemName or just listItemName)
if (path.Count == 1 || path.Count == 2) {
string listItemName = null;
string listName = null;
if (path.Count == 1) listItemName = path [0];
else {
listName = path [0];
listItemName = path [1];
}
var listItem = story.ResolveListItem (listName, listItemName, this);
if (listItem) {
isListItemReference = true;
}
}
container.AddContent (_runtimeVarRef);
}
public override void ResolveReferences (Story context)
{
base.ResolveReferences (context);
// Work is already done if it's a constant or list item reference
if (isConstantReference || isListItemReference) {
return;
}
// Is it a read count?
var parsedPath = new Path (pathIdentifiers);
Parsed.Object targetForCount = parsedPath.ResolveFromContext (this);
if (targetForCount) {
targetForCount.containerForCounting.visitsShouldBeCounted = true;
// If this is an argument to a function that wants a variable to be
// passed by reference, then the Parsed.Divert will have generated a
// Runtime.VariablePointerValue instead of allowing this object
// to generate its RuntimeVariableReference. This only happens under
// error condition since we shouldn't be passing a read count by
// reference, but we don't want it to crash!
if (_runtimeVarRef == null) return;
_runtimeVarRef.pathForCount = targetForCount.runtimePath;
_runtimeVarRef.name = null;
// Check for very specific writer error: getting read count and
// printing it as content rather than as a piece of logic
// e.g. Writing {myFunc} instead of {myFunc()}
var targetFlow = targetForCount as FlowBase;
if (targetFlow && targetFlow.isFunction) {
// Is parent context content rather than logic?
if ( parent is Weave || parent is ContentList || parent is FlowBase) {
Warning ("'" + targetFlow.identifier + "' being used as read count rather than being called as function. Perhaps you intended to write " + targetFlow.name + "()");
}
}
return;
}
// Couldn't find this multi-part path at all, whether as a divert
// target or as a list item reference.
if (path.Count > 1) {
var errorMsg = "Could not find target for read count: " + parsedPath;
if (path.Count <= 2)
errorMsg += ", or couldn't find list item with the name " + string.Join (",", path.ToArray());
Error (errorMsg);
return;
}
if (!context.ResolveVariableWithName (this.name, fromNode: this).found) {
Error("Unresolved variable: "+this.ToString(), this);
}
}
public override string ToString ()
{
return string.Join(".", path.ToArray());
}
Runtime.VariableReference _runtimeVarRef;
}
}

View File

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

View File

@@ -0,0 +1,730 @@
using System.Collections.Generic;
namespace Ink.Parsed
{
// Used by the FlowBase when constructing the weave flow from
// a flat list of content objects.
public class Weave : Parsed.Object
{
// Containers can be chained as multiple gather points
// get created as the same indentation level.
// rootContainer is always the first in the chain, while
// currentContainer is the latest.
public Runtime.Container rootContainer {
get {
if (_rootContainer == null) {
GenerateRuntimeObject ();
}
return _rootContainer;
}
}
Runtime.Container currentContainer { get; set; }
public int baseIndentIndex { get; private set; }
// Loose ends are:
// - Choices or Gathers that need to be joined up
// - Explicit Divert to gather points (i.e. "->" without a target)
public List<IWeavePoint> looseEnds;
public List<GatherPointToResolve> gatherPointsToResolve;
public class GatherPointToResolve
{
public Runtime.Divert divert;
public Runtime.Object targetRuntimeObj;
}
public Parsed.Object lastParsedSignificantObject
{
get {
if (content.Count == 0) return null;
// Don't count extraneous newlines or VAR/CONST declarations,
// since they're "empty" statements outside of the main flow.
Parsed.Object lastObject = null;
for (int i = content.Count - 1; i >= 0; --i) {
lastObject = content [i];
var lastText = lastObject as Parsed.Text;
if (lastText && lastText.text == "\n") {
continue;
}
if (IsGlobalDeclaration (lastObject))
continue;
break;
}
var lastWeave = lastObject as Weave;
if (lastWeave)
lastObject = lastWeave.lastParsedSignificantObject;
return lastObject;
}
}
public Weave(List<Parsed.Object> cont, int indentIndex=-1)
{
if (indentIndex == -1) {
baseIndentIndex = DetermineBaseIndentationFromContent (cont);
} else {
baseIndentIndex = indentIndex;
}
AddContent (cont);
ConstructWeaveHierarchyFromIndentation ();
}
public void ResolveWeavePointNaming ()
{
var namedWeavePoints = FindAll<IWeavePoint> (w => !string.IsNullOrEmpty (w.name));
_namedWeavePoints = new Dictionary<string, IWeavePoint> ();
foreach (var weavePoint in namedWeavePoints) {
// Check for weave point naming collisions
IWeavePoint existingWeavePoint;
if (_namedWeavePoints.TryGetValue (weavePoint.name, out existingWeavePoint)) {
var typeName = existingWeavePoint is Gather ? "gather" : "choice";
var existingObj = (Parsed.Object)existingWeavePoint;
Error ("A " + typeName + " with the same label name '" + weavePoint.name + "' already exists in this context on line " + existingObj.debugMetadata.startLineNumber, (Parsed.Object)weavePoint);
}
_namedWeavePoints [weavePoint.name] = weavePoint;
}
}
void ConstructWeaveHierarchyFromIndentation()
{
// Find nested indentation and convert to a proper object hierarchy
// (i.e. indented content is replaced with a Weave object that contains
// that nested content)
int contentIdx = 0;
while (contentIdx < content.Count) {
Parsed.Object obj = content [contentIdx];
// Choice or Gather
if (obj is IWeavePoint) {
var weavePoint = (IWeavePoint)obj;
var weaveIndentIdx = weavePoint.indentationDepth - 1;
// Inner level indentation - recurse
if (weaveIndentIdx > baseIndentIndex) {
// Step through content until indent jumps out again
int innerWeaveStartIdx = contentIdx;
while (contentIdx < content.Count) {
var innerWeaveObj = content [contentIdx] as IWeavePoint;
if (innerWeaveObj != null) {
var innerIndentIdx = innerWeaveObj.indentationDepth - 1;
if (innerIndentIdx <= baseIndentIndex) {
break;
}
}
contentIdx++;
}
int weaveContentCount = contentIdx - innerWeaveStartIdx;
var weaveContent = content.GetRange (innerWeaveStartIdx, weaveContentCount);
content.RemoveRange (innerWeaveStartIdx, weaveContentCount);
var weave = new Weave (weaveContent, weaveIndentIdx);
InsertContent (innerWeaveStartIdx, weave);
// Continue iteration from this point
contentIdx = innerWeaveStartIdx;
}
}
contentIdx++;
}
}
// When the indentation wasn't told to us at construction time using
// a choice point with a known indentation level, we may be told to
// determine the indentation level by incrementing from our closest ancestor.
public int DetermineBaseIndentationFromContent(List<Parsed.Object> contentList)
{
foreach (var obj in contentList) {
if (obj is IWeavePoint) {
return ((IWeavePoint)obj).indentationDepth - 1;
}
}
// No weave points, so it doesn't matter
return 0;
}
public override Runtime.Object GenerateRuntimeObject ()
{
_rootContainer = currentContainer = new Runtime.Container();
looseEnds = new List<IWeavePoint> ();
gatherPointsToResolve = new List<GatherPointToResolve> ();
// Iterate through content for the block at this level of indentation
// - Normal content is nested under Choices and Gathers
// - Blocks that are further indented cause recursion
// - Keep track of loose ends so that they can be diverted to Gathers
foreach(var obj in content) {
// Choice or Gather
if (obj is IWeavePoint) {
AddRuntimeForWeavePoint ((IWeavePoint)obj);
}
// Non-weave point
else {
// Nested weave
if (obj is Weave) {
var weave = (Weave)obj;
AddRuntimeForNestedWeave (weave);
gatherPointsToResolve.AddRange (weave.gatherPointsToResolve);
}
// Other object
// May be complex object that contains statements - e.g. a multi-line conditional
else {
AddGeneralRuntimeContent (obj.runtimeObject);
}
}
}
// Pass any loose ends up the hierarhcy
PassLooseEndsToAncestors();
return _rootContainer;
}
// Found gather point:
// - gather any loose ends
// - set the gather as the main container to dump new content in
void AddRuntimeForGather(Gather gather)
{
// Determine whether this Gather should be auto-entered:
// - It is auto-entered if there were no choices in the last section
// - A section is "since the previous gather" - so reset now
bool autoEnter = !hasSeenChoiceInSection;
hasSeenChoiceInSection = false;
var gatherContainer = gather.runtimeContainer;
if (gather.name == null) {
// Use disallowed character so it's impossible to have a name collision
gatherContainer.name = "g-" + _unnamedGatherCount;
_unnamedGatherCount++;
}
// Auto-enter: include in main content
if (autoEnter) {
currentContainer.AddContent (gatherContainer);
}
// Don't auto-enter:
// Add this gather to the main content, but only accessible
// by name so that it isn't stepped into automatically, but only via
// a divert from a loose end.
else {
_rootContainer.AddToNamedContentOnly (gatherContainer);
}
// Consume loose ends: divert them to this gather
foreach (IWeavePoint looseEndWeavePoint in looseEnds) {
var looseEnd = (Parsed.Object)looseEndWeavePoint;
// Skip gather loose ends that are at the same level
// since they'll be handled by the auto-enter code below
// that only jumps into the gather if (current runtime choices == 0)
if (looseEnd is Gather) {
var prevGather = (Gather)looseEnd;
if (prevGather.indentationDepth == gather.indentationDepth) {
continue;
}
}
Runtime.Divert divert = null;
if (looseEnd is Parsed.Divert) {
divert = (Runtime.Divert) looseEnd.runtimeObject;
} else {
divert = new Runtime.Divert ();
var looseWeavePoint = looseEnd as IWeavePoint;
looseWeavePoint.runtimeContainer.AddContent (divert);
}
// Pass back knowledge of this loose end being diverted
// to the FlowBase so that it can maintain a list of them,
// and resolve the divert references later
gatherPointsToResolve.Add (new GatherPointToResolve{ divert = divert, targetRuntimeObj = gatherContainer });
}
looseEnds.Clear ();
// Replace the current container itself
currentContainer = gatherContainer;
}
void AddRuntimeForWeavePoint(IWeavePoint weavePoint)
{
// Current level Gather
if (weavePoint is Gather) {
AddRuntimeForGather ((Gather)weavePoint);
}
// Current level choice
else if (weavePoint is Choice) {
// Gathers that contain choices are no longer loose ends
// (same as when weave points get nested content)
if (previousWeavePoint is Gather) {
looseEnds.Remove (previousWeavePoint);
}
// Add choice point content
var choice = (Choice)weavePoint;
currentContainer.AddContent (choice.runtimeObject);
// Add choice's inner content to self
choice.innerContentContainer.name = "c-" + _choiceCount;
currentContainer.AddToNamedContentOnly (choice.innerContentContainer);
_choiceCount++;
hasSeenChoiceInSection = true;
}
// Keep track of loose ends
addContentToPreviousWeavePoint = false; // default
if (WeavePointHasLooseEnd (weavePoint)) {
looseEnds.Add (weavePoint);
var looseChoice = weavePoint as Choice;
if (looseChoice) {
addContentToPreviousWeavePoint = true;
}
}
previousWeavePoint = weavePoint;
}
// Add nested block at a greater indentation level
public void AddRuntimeForNestedWeave(Weave nestedResult)
{
// Add this inner block to current container
// (i.e. within the main container, or within the last defined Choice/Gather)
AddGeneralRuntimeContent (nestedResult.rootContainer);
// Now there's a deeper indentation level, the previous weave point doesn't
// count as a loose end (since it will have content to go to)
if (previousWeavePoint != null) {
looseEnds.Remove (previousWeavePoint);
addContentToPreviousWeavePoint = false;
}
}
// Normal content gets added into the latest Choice or Gather by default,
// unless there hasn't been one yet.
void AddGeneralRuntimeContent(Runtime.Object content)
{
// Content is allowed to evaluate runtimeObject to null
// (e.g. AuthorWarning, which doesn't make it into the runtime)
if (content == null)
return;
if (addContentToPreviousWeavePoint) {
previousWeavePoint.runtimeContainer.AddContent (content);
} else {
currentContainer.AddContent (content);
}
}
void PassLooseEndsToAncestors()
{
if (looseEnds.Count == 0) return;
// Search for Weave ancestor to pass loose ends to for gathering.
// There are two types depending on whether the current weave
// is separated by a conditional or sequence.
// - An "inner" weave is one that is directly connected to the current
// weave - i.e. you don't have to pass through a conditional or
// sequence to get to it. We're allowed to pass all loose ends to
// one of these.
// - An "outer" weave is one that is outside of a conditional/sequence
// that the current weave is nested within. We're only allowed to
// pass gathers (i.e. 'normal flow') loose ends up there, not normal
// choices. The rule is that choices have to be diverted explicitly
// by the author since it's ambiguous where flow should go otherwise.
//
// e.g.:
//
// - top <- e.g. outer weave
// {true:
// * choice <- e.g. inner weave
// * * choice 2
// more content <- e.g. current weave
// * choice 2
// }
// - more of outer weave
//
Weave closestInnerWeaveAncestor = null;
Weave closestOuterWeaveAncestor = null;
// Find inner and outer ancestor weaves as defined above.
bool nested = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{
// Found ancestor?
var weaveAncestor = ancestor as Weave;
if (weaveAncestor != null)
{
if (!nested && closestInnerWeaveAncestor == null)
closestInnerWeaveAncestor = weaveAncestor;
if (nested && closestOuterWeaveAncestor == null)
closestOuterWeaveAncestor = weaveAncestor;
}
// Weaves nested within Sequences or Conditionals are
// "sealed" - any loose ends require explicit diverts.
if (ancestor is Sequence || ancestor is Conditional)
nested = true;
}
// No weave to pass loose ends to at all?
if (closestInnerWeaveAncestor == null && closestOuterWeaveAncestor == null)
return;
// Follow loose end passing logic as defined above
for (int i = looseEnds.Count - 1; i >= 0; i--) {
var looseEnd = looseEnds[i];
bool received = false;
// This weave is nested within a conditional or sequence:
// - choices can only be passed up to direct ancestor ("inner") weaves
// - gathers can be passed up to either, but favour the closer (inner) weave
// if there is one
if(nested) {
if( looseEnd is Choice && closestInnerWeaveAncestor != null) {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}
else if( !(looseEnd is Choice) ) {
var receivingWeave = closestInnerWeaveAncestor ?? closestOuterWeaveAncestor;
if(receivingWeave != null) {
receivingWeave.ReceiveLooseEnd(looseEnd);
received = true;
}
}
}
// No nesting, all loose ends can be safely passed up
else {
closestInnerWeaveAncestor.ReceiveLooseEnd(looseEnd);
received = true;
}
if(received) looseEnds.RemoveAt(i);
}
}
void ReceiveLooseEnd(IWeavePoint childWeaveLooseEnd)
{
looseEnds.Add(childWeaveLooseEnd);
}
public override void ResolveReferences(Story context)
{
base.ResolveReferences (context);
// Check that choices nested within conditionals and sequences are terminated
if( looseEnds != null && looseEnds.Count > 0 ) {
var isNestedWeave = false;
for (var ancestor = this.parent; ancestor != null; ancestor = ancestor.parent)
{
if (ancestor is Sequence || ancestor is Conditional)
{
isNestedWeave = true;
break;
}
}
if (isNestedWeave)
{
ValidateTermination(BadNestedTerminationHandler);
}
}
foreach(var gatherPoint in gatherPointsToResolve) {
gatherPoint.divert.targetPath = gatherPoint.targetRuntimeObj.path;
}
CheckForWeavePointNamingCollisions ();
}
public IWeavePoint WeavePointNamed(string name)
{
if (_namedWeavePoints == null)
return null;
IWeavePoint weavePointResult = null;
if (_namedWeavePoints.TryGetValue (name, out weavePointResult))
return weavePointResult;
return null;
}
// Global VARs and CONSTs are treated as "outside of the flow"
// when iterating over content that follows loose ends
bool IsGlobalDeclaration (Parsed.Object obj)
{
var varAss = obj as VariableAssignment;
if (varAss && varAss.isGlobalDeclaration && varAss.isDeclaration)
return true;
var constDecl = obj as ConstantDeclaration;
if (constDecl)
return true;
return false;
}
// While analysing final loose ends, we look to see whether there
// are any diverts etc which choices etc divert from
IEnumerable<Parsed.Object> ContentThatFollowsWeavePoint (IWeavePoint weavePoint)
{
var obj = (Parsed.Object)weavePoint;
// Inner content first (e.g. for a choice)
if (obj.content != null) {
foreach (var contentObj in obj.content) {
// Global VARs and CONSTs are treated as "outside of the flow"
if (IsGlobalDeclaration (contentObj)) continue;
yield return contentObj;
}
}
var parentWeave = obj.parent as Weave;
if (parentWeave == null) {
throw new System.Exception ("Expected weave point parent to be weave?");
}
var weavePointIdx = parentWeave.content.IndexOf (obj);
for (int i = weavePointIdx+1; i < parentWeave.content.Count; i++) {
var laterObj = parentWeave.content [i];
// Global VARs and CONSTs are treated as "outside of the flow"
if (IsGlobalDeclaration (laterObj)) continue;
// End of the current flow
if (laterObj is IWeavePoint)
break;
// Other weaves will be have their own loose ends
if (laterObj is Weave)
break;
yield return laterObj;
}
}
public delegate void BadTerminationHandler (Parsed.Object terminatingObj);
public void ValidateTermination (BadTerminationHandler badTerminationHandler)
{
// Don't worry if the last object in the flow is a "TODO",
// even if there are other loose ends in other places
if (lastParsedSignificantObject is AuthorWarning) {
return;
}
// By now, any sub-weaves will have passed loose ends up to the root weave (this).
// So there are 2 possible situations:
// - There are loose ends from somewhere in the flow.
// These aren't necessarily "real" loose ends - they're weave points
// that don't connect to any lower weave points, so we just
// have to check that they terminate properly.
// - This weave is just a list of content with no actual weave points,
// so we just need to check that the list of content terminates.
bool hasLooseEnds = looseEnds != null && looseEnds.Count > 0;
if (hasLooseEnds) {
foreach (var looseEnd in looseEnds) {
var looseEndFlow = ContentThatFollowsWeavePoint (looseEnd);
ValidateFlowOfObjectsTerminates (looseEndFlow, (Parsed.Object)looseEnd, badTerminationHandler);
}
}
// No loose ends... is there any inner weaving at all?
// If not, make sure the single content stream is terminated correctly
else {
// If there's any actual weaving, assume that content is
// terminated correctly since we would've had a loose end otherwise
foreach (var obj in content) {
if (obj is IWeavePoint) return;
}
// Straight linear flow? Check it terminates
ValidateFlowOfObjectsTerminates (content, this, badTerminationHandler);
}
}
void BadNestedTerminationHandler(Parsed.Object terminatingObj)
{
Conditional conditional = null;
for (var ancestor = terminatingObj.parent; ancestor != null; ancestor = ancestor.parent) {
if( ancestor is Sequence || ancestor is Conditional ) {
conditional = ancestor as Conditional;
break;
}
}
var errorMsg = "Choices nested in conditionals or sequences need to explicitly divert afterwards.";
// Tutorialise proper choice syntax if this looks like a single choice within a condition, e.g.
// { condition:
// * choice
// }
if (conditional != null) {
var numChoices = conditional.FindAll<Choice>().Count;
if( numChoices == 1 ) {
errorMsg = "Choices with conditions should be written: '* {condition} choice'. Otherwise, "+ errorMsg.ToLower();
}
}
Error(errorMsg, terminatingObj);
}
void ValidateFlowOfObjectsTerminates (IEnumerable<Parsed.Object> objFlow, Parsed.Object defaultObj, BadTerminationHandler badTerminationHandler)
{
bool terminated = false;
Parsed.Object terminatingObj = defaultObj;
foreach (var flowObj in objFlow) {
var divert = flowObj.Find<Divert> (d => !d.isThread && !d.isTunnel && !d.isFunctionCall && !(d.parent is DivertTarget));
if (divert != null) {
terminated = true;
}
if (flowObj.Find<TunnelOnwards> () != null) {
terminated = true;
break;
}
terminatingObj = flowObj;
}
if (!terminated) {
// Author has left a note to self here - clearly we don't need
// to leave them with another warning since they know what they're doing.
if (terminatingObj is AuthorWarning) {
return;
}
badTerminationHandler (terminatingObj);
}
}
bool WeavePointHasLooseEnd(IWeavePoint weavePoint)
{
// No content, must be a loose end.
if (weavePoint.content == null) return true;
// If a weave point is diverted from, it doesn't have a loose end.
// Detect a divert object within a weavePoint's main content
// Work backwards since we're really interested in the end,
// although it doesn't actually make a difference!
// (content after a divert will simply be inaccessible)
for (int i = weavePoint.content.Count - 1; i >= 0; --i) {
var innerDivert = weavePoint.content [i] as Divert;
if (innerDivert) {
bool willReturn = innerDivert.isThread || innerDivert.isTunnel || innerDivert.isFunctionCall;
if (!willReturn) return false;
}
}
return true;
}
// Enforce rule that weave points must not have the same
// name as any stitches or knots upwards in the hierarchy
void CheckForWeavePointNamingCollisions()
{
if (_namedWeavePoints == null)
return;
var ancestorFlows = new List<FlowBase> ();
foreach (var obj in this.ancestry) {
var flow = obj as FlowBase;
if (flow)
ancestorFlows.Add (flow);
else
break;
}
foreach (var namedWeavePointPair in _namedWeavePoints) {
var weavePointName = namedWeavePointPair.Key;
var weavePoint = (Parsed.Object) namedWeavePointPair.Value;
foreach(var flow in ancestorFlows) {
// Shallow search
var otherContentWithName = flow.ContentWithNameAtLevel (weavePointName);
if (otherContentWithName && otherContentWithName != weavePoint) {
var errorMsg = string.Format ("{0} '{1}' has the same label name as a {2} (on {3})",
weavePoint.GetType().Name,
weavePointName,
otherContentWithName.GetType().Name,
otherContentWithName.debugMetadata);
Error(errorMsg, (Parsed.Object) weavePoint);
}
}
}
}
// Keep track of previous weave point (Choice or Gather)
// at the current indentation level:
// - to add ordinary content to be nested under it
// - to add nested content under it when it's indented
// - to remove it from the list of loose ends when
// - it has indented content since it's no longer a loose end
// - it's a gather and it has a choice added to it
IWeavePoint previousWeavePoint = null;
bool addContentToPreviousWeavePoint = false;
// Used for determining whether the next Gather should auto-enter
bool hasSeenChoiceInSection = false;
int _unnamedGatherCount;
int _choiceCount;
Runtime.Container _rootContainer;
Dictionary<string, IWeavePoint> _namedWeavePoints;
}
}

View File

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

View File

@@ -0,0 +1,27 @@

namespace Ink.Parsed
{
public class Wrap<T> : Parsed.Object where T : Runtime.Object
{
public Wrap (T objToWrap)
{
_objToWrap = objToWrap;
}
public override Runtime.Object GenerateRuntimeObject ()
{
return _objToWrap;
}
T _objToWrap;
}
// Shorthand for writing Parsed.Wrap<Runtime.Glue> and Parsed.Wrap<Runtime.Tag>
public class Glue : Wrap<Runtime.Glue> {
public Glue (Runtime.Glue glue) : base(glue) {}
}
public class Tag : Wrap<Runtime.Tag> {
public Tag (Runtime.Tag tag) : base (tag) { }
}
}

View File

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