mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 10:34:27 +11:00
Committed everything
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class AuthorWarning : Parsed.Object
|
||||
{
|
||||
public string warningMessage;
|
||||
|
||||
public AuthorWarning(string message)
|
||||
{
|
||||
warningMessage = message;
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
Warning (warningMessage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36d3c5151e15a45b4b7a2ac7b355bff9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,290 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Choice : Parsed.Object, IWeavePoint, INamedContent
|
||||
{
|
||||
public ContentList startContent { get; protected set; }
|
||||
public ContentList choiceOnlyContent { get; protected set; }
|
||||
public ContentList innerContent { get; protected set; }
|
||||
|
||||
public string name
|
||||
{
|
||||
get { return identifier?.name; }
|
||||
}
|
||||
public Identifier identifier { get; set; }
|
||||
|
||||
public Expression condition {
|
||||
get {
|
||||
return _condition;
|
||||
}
|
||||
set {
|
||||
_condition = value;
|
||||
if( _condition )
|
||||
AddContent (_condition);
|
||||
}
|
||||
}
|
||||
|
||||
public bool onceOnly { get; set; }
|
||||
public bool isInvisibleDefault { get; set; }
|
||||
|
||||
public int indentationDepth { get; set; }// = 1;
|
||||
public bool hasWeaveStyleInlineBrackets { get; set; }
|
||||
|
||||
// Required for IWeavePoint interface
|
||||
// Choice's target container. Used by weave to append any extra
|
||||
// nested weave content into.
|
||||
public Runtime.Container runtimeContainer { get { return _innerContentContainer; } }
|
||||
|
||||
|
||||
public Runtime.Container innerContentContainer {
|
||||
get {
|
||||
return _innerContentContainer;
|
||||
}
|
||||
}
|
||||
|
||||
public override Runtime.Container containerForCounting {
|
||||
get {
|
||||
return _innerContentContainer;
|
||||
}
|
||||
}
|
||||
|
||||
// Override runtimePath to point to the Choice's target content (after it's chosen),
|
||||
// as opposed to the default implementation which would point to the choice itself
|
||||
// (or it's outer container), which is what runtimeObject is.
|
||||
public override Runtime.Path runtimePath
|
||||
{
|
||||
get {
|
||||
return _innerContentContainer.path;
|
||||
}
|
||||
}
|
||||
|
||||
public Choice (ContentList startContent, ContentList choiceOnlyContent, ContentList innerContent)
|
||||
{
|
||||
this.startContent = startContent;
|
||||
this.choiceOnlyContent = choiceOnlyContent;
|
||||
this.innerContent = innerContent;
|
||||
this.indentationDepth = 1;
|
||||
|
||||
if (startContent)
|
||||
AddContent (this.startContent);
|
||||
|
||||
if (choiceOnlyContent)
|
||||
AddContent (this.choiceOnlyContent);
|
||||
|
||||
if( innerContent )
|
||||
AddContent (this.innerContent);
|
||||
|
||||
this.onceOnly = true; // default
|
||||
}
|
||||
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
_outerContainer = new Runtime.Container ();
|
||||
|
||||
// Content names for different types of choice:
|
||||
// * start content [choice only content] inner content
|
||||
// * start content -> divert
|
||||
// * start content
|
||||
// * [choice only content]
|
||||
|
||||
// Hmm, this structure has become slightly insane!
|
||||
//
|
||||
// [
|
||||
// EvalStart
|
||||
// assign $r = $r1 -- return target = return label 1
|
||||
// BeginString
|
||||
// -> s
|
||||
// [(r1)] -- return label 1 (after start content)
|
||||
// EndString
|
||||
// BeginString
|
||||
// ... choice only content
|
||||
// EndEval
|
||||
// Condition expression
|
||||
// choice: -> "c-0"
|
||||
// (s) = [
|
||||
// start content
|
||||
// -> r -- goto return label 1 or 2
|
||||
// ]
|
||||
// ]
|
||||
//
|
||||
// in parent's container: (the inner content for the choice)
|
||||
//
|
||||
// (c-0) = [
|
||||
// EvalStart
|
||||
// assign $r = $r2 -- return target = return label 2
|
||||
// EndEval
|
||||
// -> s
|
||||
// [(r2)] -- return label 1 (after start content)
|
||||
// inner content
|
||||
// ]
|
||||
//
|
||||
|
||||
_runtimeChoice = new Runtime.ChoicePoint (onceOnly);
|
||||
_runtimeChoice.isInvisibleDefault = this.isInvisibleDefault;
|
||||
|
||||
if (startContent || choiceOnlyContent || condition) {
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.EvalStart ());
|
||||
}
|
||||
|
||||
// Start content is put into a named container that's referenced both
|
||||
// when displaying the choice initially, and when generating the text
|
||||
// when the choice is chosen.
|
||||
if (startContent) {
|
||||
|
||||
// Generate start content and return
|
||||
// - We can't use a function since it uses a call stack element, which would
|
||||
// put temporary values out of scope. Instead we manually divert around.
|
||||
// - $r is a variable divert target contains the return point
|
||||
_returnToR1 = new Runtime.DivertTargetValue ();
|
||||
_outerContainer.AddContent (_returnToR1);
|
||||
var varAssign = new Runtime.VariableAssignment ("$r", true);
|
||||
_outerContainer.AddContent (varAssign);
|
||||
|
||||
// Mark the start of the choice text generation, so that the runtime
|
||||
// knows where to rewind to to extract the content from the output stream.
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
|
||||
|
||||
_divertToStartContentOuter = new Runtime.Divert ();
|
||||
_outerContainer.AddContent (_divertToStartContentOuter);
|
||||
|
||||
// Start content itself in a named container
|
||||
_startContentRuntimeContainer = startContent.GenerateRuntimeObject () as Runtime.Container;
|
||||
_startContentRuntimeContainer.name = "s";
|
||||
|
||||
// Effectively, the "return" statement - return to the point specified by $r
|
||||
var varDivert = new Runtime.Divert ();
|
||||
varDivert.variableDivertName = "$r";
|
||||
_startContentRuntimeContainer.AddContent (varDivert);
|
||||
|
||||
// Add the container
|
||||
_outerContainer.AddToNamedContentOnly (_startContentRuntimeContainer);
|
||||
|
||||
// This is the label to return to
|
||||
_r1Label = new Runtime.Container ();
|
||||
_r1Label.name = "$r1";
|
||||
_outerContainer.AddContent (_r1Label);
|
||||
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.EndString ());
|
||||
|
||||
_runtimeChoice.hasStartContent = true;
|
||||
}
|
||||
|
||||
// Choice only content - mark the start, then generate it directly into the outer container
|
||||
if (choiceOnlyContent) {
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.BeginString ());
|
||||
|
||||
var choiceOnlyRuntimeContent = choiceOnlyContent.GenerateRuntimeObject () as Runtime.Container;
|
||||
_outerContainer.AddContentsOfContainer (choiceOnlyRuntimeContent);
|
||||
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.EndString ());
|
||||
|
||||
_runtimeChoice.hasChoiceOnlyContent = true;
|
||||
}
|
||||
|
||||
// Generate any condition for this choice
|
||||
if (condition) {
|
||||
condition.GenerateIntoContainer (_outerContainer);
|
||||
_runtimeChoice.hasCondition = true;
|
||||
}
|
||||
|
||||
if (startContent || choiceOnlyContent || condition) {
|
||||
_outerContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
|
||||
}
|
||||
|
||||
// Add choice itself
|
||||
_outerContainer.AddContent (_runtimeChoice);
|
||||
|
||||
// Container that choice points to for when it's chosen
|
||||
_innerContentContainer = new Runtime.Container ();
|
||||
|
||||
// Repeat start content by diverting to its container
|
||||
if (startContent) {
|
||||
|
||||
// Set the return point when jumping back into the start content
|
||||
// - In this case, it's the $r2 point, within the choice content "c".
|
||||
_returnToR2 = new Runtime.DivertTargetValue ();
|
||||
_innerContentContainer.AddContent (Runtime.ControlCommand.EvalStart ());
|
||||
_innerContentContainer.AddContent (_returnToR2);
|
||||
_innerContentContainer.AddContent (Runtime.ControlCommand.EvalEnd ());
|
||||
var varAssign = new Runtime.VariableAssignment ("$r", true);
|
||||
_innerContentContainer.AddContent (varAssign);
|
||||
|
||||
// Main divert into start content
|
||||
_divertToStartContentInner = new Runtime.Divert ();
|
||||
_innerContentContainer.AddContent (_divertToStartContentInner);
|
||||
|
||||
// Define label to return to
|
||||
_r2Label = new Runtime.Container ();
|
||||
_r2Label.name = "$r2";
|
||||
_innerContentContainer.AddContent (_r2Label);
|
||||
}
|
||||
|
||||
// Choice's own inner content
|
||||
if (innerContent) {
|
||||
var innerChoiceOnlyContent = innerContent.GenerateRuntimeObject () as Runtime.Container;
|
||||
_innerContentContainer.AddContentsOfContainer (innerChoiceOnlyContent);
|
||||
}
|
||||
|
||||
if (this.story.countAllVisits) {
|
||||
_innerContentContainer.visitsShouldBeCounted = true;
|
||||
}
|
||||
|
||||
_innerContentContainer.countingAtStartOnly = true;
|
||||
|
||||
return _outerContainer;
|
||||
}
|
||||
|
||||
public override void ResolveReferences(Story context)
|
||||
{
|
||||
// Weave style choice - target own content container
|
||||
if (_innerContentContainer) {
|
||||
_runtimeChoice.pathOnChoice = _innerContentContainer.path;
|
||||
|
||||
if (onceOnly)
|
||||
_innerContentContainer.visitsShouldBeCounted = true;
|
||||
}
|
||||
|
||||
if (_returnToR1)
|
||||
_returnToR1.targetPath = _r1Label.path;
|
||||
|
||||
if (_returnToR2)
|
||||
_returnToR2.targetPath = _r2Label.path;
|
||||
|
||||
if( _divertToStartContentOuter )
|
||||
_divertToStartContentOuter.targetPath = _startContentRuntimeContainer.path;
|
||||
|
||||
if( _divertToStartContentInner )
|
||||
_divertToStartContentInner.targetPath = _startContentRuntimeContainer.path;
|
||||
|
||||
base.ResolveReferences (context);
|
||||
|
||||
if( identifier != null && identifier.name.Length > 0 )
|
||||
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (choiceOnlyContent != null) {
|
||||
return string.Format ("* {0}[{1}]...", startContent, choiceOnlyContent);
|
||||
} else {
|
||||
return string.Format ("* {0}...", startContent);
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.ChoicePoint _runtimeChoice;
|
||||
Runtime.Container _innerContentContainer;
|
||||
Runtime.Container _outerContainer;
|
||||
Runtime.Container _startContentRuntimeContainer;
|
||||
Runtime.Divert _divertToStartContentOuter;
|
||||
Runtime.Divert _divertToStartContentInner;
|
||||
Runtime.Container _r1Label;
|
||||
Runtime.Container _r2Label;
|
||||
Runtime.DivertTargetValue _returnToR1;
|
||||
Runtime.DivertTargetValue _returnToR2;
|
||||
Expression _condition;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24c93c5d49d7f498f9b658d38f48e0b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Ink.Runtime;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Conditional : Parsed.Object
|
||||
{
|
||||
public Expression initialCondition { get; private set; }
|
||||
public List<ConditionalSingleBranch> branches { get; private set; }
|
||||
|
||||
public Conditional (Expression condition, List<ConditionalSingleBranch> branches)
|
||||
{
|
||||
this.initialCondition = condition;
|
||||
if (this.initialCondition) {
|
||||
AddContent (condition);
|
||||
}
|
||||
|
||||
this.branches = branches;
|
||||
if (this.branches != null) {
|
||||
AddContent (this.branches.Cast<Parsed.Object> ().ToList ());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var container = new Runtime.Container ();
|
||||
|
||||
// Initial condition
|
||||
if (this.initialCondition) {
|
||||
container.AddContent (initialCondition.runtimeObject);
|
||||
}
|
||||
|
||||
// Individual branches
|
||||
foreach (var branch in branches) {
|
||||
var branchContainer = (Container) branch.runtimeObject;
|
||||
container.AddContent (branchContainer);
|
||||
}
|
||||
|
||||
// If it's a switch-like conditional, each branch
|
||||
// will have a "duplicate" operation for the original
|
||||
// switched value. If there's no final else clause
|
||||
// and we fall all the way through, we need to clean up.
|
||||
// (An else clause doesn't dup but it *does* pop)
|
||||
if (this.initialCondition != null && branches [0].ownExpression != null && !branches [branches.Count - 1].isElse) {
|
||||
container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
|
||||
}
|
||||
|
||||
// Target for branches to rejoin to
|
||||
_reJoinTarget = Runtime.ControlCommand.NoOp ();
|
||||
container.AddContent (_reJoinTarget);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
var pathToReJoin = _reJoinTarget.path;
|
||||
|
||||
foreach (var branch in branches) {
|
||||
branch.returnDivert.targetPath = pathToReJoin;
|
||||
}
|
||||
|
||||
base.ResolveReferences (context);
|
||||
}
|
||||
|
||||
Runtime.ControlCommand _reJoinTarget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6cda85f80c124e12b215fdbc95b33bb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class ConditionalSingleBranch : Parsed.Object
|
||||
{
|
||||
// bool condition, e.g.:
|
||||
// { 5 == 4:
|
||||
// - the true branch
|
||||
// - the false branch
|
||||
// }
|
||||
public bool isTrueBranch { get; set; }
|
||||
|
||||
// When each branch has its own expression like a switch statement,
|
||||
// this is non-null. e.g.
|
||||
// { x:
|
||||
// - 4: the value of x is four (ownExpression is the value 4)
|
||||
// - 3: the value of x is three
|
||||
// }
|
||||
public Expression ownExpression {
|
||||
get {
|
||||
return _ownExpression;
|
||||
}
|
||||
set {
|
||||
_ownExpression = value;
|
||||
if (_ownExpression) {
|
||||
AddContent (_ownExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In the above example, match equality of x with 4 for the first branch.
|
||||
// This is as opposed to simply evaluating boolean equality for each branch,
|
||||
// example when shouldMatchEqualtity is FALSE:
|
||||
// {
|
||||
// 3 > 2: This will happen
|
||||
// 2 > 3: This won't happen
|
||||
// }
|
||||
public bool matchingEquality { get; set; }
|
||||
|
||||
public bool isElse { get; set; }
|
||||
|
||||
public bool isInline { get; set; }
|
||||
|
||||
public Runtime.Divert returnDivert { get; protected set; }
|
||||
|
||||
public ConditionalSingleBranch (List<Parsed.Object> content)
|
||||
{
|
||||
// Branches are allowed to be empty
|
||||
if (content != null) {
|
||||
_innerWeave = new Weave (content);
|
||||
AddContent (_innerWeave);
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime content can be summarised as follows:
|
||||
// - Evaluate an expression if necessary to branch on
|
||||
// - Branch to a named container if true
|
||||
// - Divert back to main flow
|
||||
// (owner Conditional is in control of this target point)
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
// Check for common mistake, of putting "else:" instead of "- else:"
|
||||
if (_innerWeave) {
|
||||
foreach (var c in _innerWeave.content) {
|
||||
var text = c as Parsed.Text;
|
||||
if (text) {
|
||||
// Don't need to trim at the start since the parser handles that already
|
||||
if (text.text.StartsWith ("else:")) {
|
||||
Warning ("Saw the text 'else:' which is being treated as content. Did you mean '- else:'?", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var container = new Runtime.Container ();
|
||||
|
||||
// Are we testing against a condition that's used for more than just this
|
||||
// branch? If so, the first thing we need to do is replicate the value that's
|
||||
// on the evaluation stack so that we don't fully consume it, in case other
|
||||
// branches need to use it.
|
||||
bool duplicatesStackValue = matchingEquality && !isElse;
|
||||
if ( duplicatesStackValue )
|
||||
container.AddContent (Runtime.ControlCommand.Duplicate ());
|
||||
|
||||
_conditionalDivert = new Runtime.Divert ();
|
||||
|
||||
// else clause is unconditional catch-all, otherwise the divert is conditional
|
||||
_conditionalDivert.isConditional = !isElse;
|
||||
|
||||
// Need extra evaluation?
|
||||
if( !isTrueBranch && !isElse ) {
|
||||
|
||||
bool needsEval = ownExpression != null;
|
||||
if( needsEval )
|
||||
container.AddContent (Runtime.ControlCommand.EvalStart ());
|
||||
|
||||
if (ownExpression)
|
||||
ownExpression.GenerateIntoContainer (container);
|
||||
|
||||
// Uses existing duplicated value
|
||||
if (matchingEquality)
|
||||
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("=="));
|
||||
|
||||
if( needsEval )
|
||||
container.AddContent (Runtime.ControlCommand.EvalEnd ());
|
||||
}
|
||||
|
||||
// Will pop from stack if conditional
|
||||
container.AddContent (_conditionalDivert);
|
||||
|
||||
_contentContainer = GenerateRuntimeForContent ();
|
||||
_contentContainer.name = "b";
|
||||
|
||||
// Multi-line conditionals get a newline at the start of each branch
|
||||
// (as opposed to the start of the multi-line conditional since the condition
|
||||
// may evaluate to false.)
|
||||
if (!isInline) {
|
||||
_contentContainer.InsertContent (new Runtime.StringValue ("\n"), 0);
|
||||
}
|
||||
|
||||
if( duplicatesStackValue || (isElse && matchingEquality) )
|
||||
_contentContainer.InsertContent (Runtime.ControlCommand.PopEvaluatedValue (), 0);
|
||||
|
||||
container.AddToNamedContentOnly (_contentContainer);
|
||||
|
||||
returnDivert = new Runtime.Divert ();
|
||||
_contentContainer.AddContent (returnDivert);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
Runtime.Container GenerateRuntimeForContent()
|
||||
{
|
||||
// Empty branch - create empty container
|
||||
if (_innerWeave == null) {
|
||||
return new Runtime.Container ();
|
||||
}
|
||||
|
||||
return _innerWeave.rootContainer;
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
_conditionalDivert.targetPath = _contentContainer.path;
|
||||
|
||||
base.ResolveReferences (context);
|
||||
}
|
||||
|
||||
Runtime.Container _contentContainer;
|
||||
Runtime.Divert _conditionalDivert;
|
||||
Expression _ownExpression;
|
||||
|
||||
Weave _innerWeave;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9e4e9e3b695418f92d259e9d047fb1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
//using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class ConstantDeclaration : Parsed.Object
|
||||
{
|
||||
public string constantName
|
||||
{
|
||||
get { return constantIdentifier?.name; }
|
||||
}
|
||||
public Identifier constantIdentifier { get; protected set; }
|
||||
public Expression expression { get; protected set; }
|
||||
|
||||
public ConstantDeclaration (Identifier name, Expression assignedExpression)
|
||||
{
|
||||
this.constantIdentifier = name;
|
||||
|
||||
// Defensive programming in case parsing of assignedExpression failed
|
||||
if( assignedExpression )
|
||||
this.expression = AddContent(assignedExpression);
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
// Global declarations don't generate actual procedural
|
||||
// runtime objects, but instead add a global variable to the story itself.
|
||||
// The story then initialises them all in one go at the start of the game.
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
context.CheckForNamingCollisions (this, constantIdentifier, Story.SymbolType.Var);
|
||||
}
|
||||
|
||||
public override string typeName {
|
||||
get {
|
||||
return "Constant";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6c599566895c444da36111b8aa6bd5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class ContentList : Parsed.Object
|
||||
{
|
||||
public bool dontFlatten { get; set; }
|
||||
|
||||
public Runtime.Container runtimeContainer {
|
||||
get {
|
||||
return (Runtime.Container) this.runtimeObject;
|
||||
}
|
||||
}
|
||||
|
||||
public ContentList (List<Parsed.Object> objects)
|
||||
{
|
||||
if( objects != null )
|
||||
AddContent (objects);
|
||||
}
|
||||
|
||||
public ContentList (params Parsed.Object[] objects)
|
||||
{
|
||||
if (objects != null) {
|
||||
var objList = new List<Parsed.Object> (objects);
|
||||
AddContent (objList);
|
||||
}
|
||||
}
|
||||
|
||||
public ContentList()
|
||||
{
|
||||
}
|
||||
|
||||
public void TrimTrailingWhitespace()
|
||||
{
|
||||
for (int i = this.content.Count - 1; i >= 0; --i) {
|
||||
var text = this.content [i] as Text;
|
||||
if (text == null)
|
||||
break;
|
||||
|
||||
text.text = text.text.TrimEnd (' ', '\t');
|
||||
if (text.text.Length == 0)
|
||||
this.content.RemoveAt (i);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var container = new Runtime.Container ();
|
||||
if (content != null) {
|
||||
foreach (var obj in content) {
|
||||
var contentObjRuntime = obj.runtimeObject;
|
||||
|
||||
// Some objects (e.g. author warnings) don't generate runtime objects
|
||||
if( contentObjRuntime )
|
||||
container.AddContent (contentObjRuntime);
|
||||
}
|
||||
}
|
||||
|
||||
if( dontFlatten )
|
||||
story.DontFlattenContainer (container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
sb.Append ("ContentList(");
|
||||
sb.Append(string.Join (", ", content.ToStringsArray()));
|
||||
sb.Append (")");
|
||||
return sb.ToString ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d310eb8e3c23e41db891c939a1313cda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,403 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Divert : Parsed.Object
|
||||
{
|
||||
public Parsed.Path target { get; protected set; }
|
||||
public Parsed.Object targetContent { get; protected set; }
|
||||
public List<Expression> arguments { get; protected set; }
|
||||
public Runtime.Divert runtimeDivert { get; protected set; }
|
||||
public bool isFunctionCall { get; set; }
|
||||
public bool isEmpty { get; set; }
|
||||
public bool isTunnel { get; set; }
|
||||
public bool isThread { get; set; }
|
||||
public bool isEnd {
|
||||
get {
|
||||
return target != null && target.dotSeparatedComponents == "END";
|
||||
}
|
||||
}
|
||||
public bool isDone {
|
||||
get {
|
||||
return target != null && target.dotSeparatedComponents == "DONE";
|
||||
}
|
||||
}
|
||||
|
||||
public Divert (Parsed.Path target, List<Expression> arguments = null)
|
||||
{
|
||||
this.target = target;
|
||||
this.arguments = arguments;
|
||||
|
||||
if (arguments != null) {
|
||||
AddContent (arguments.Cast<Parsed.Object> ().ToList ());
|
||||
}
|
||||
}
|
||||
|
||||
public Divert (Parsed.Object targetContent)
|
||||
{
|
||||
this.targetContent = targetContent;
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
// End = end flow immediately
|
||||
// Done = return from thread or instruct the flow that it's safe to exit
|
||||
if (isEnd) {
|
||||
return Runtime.ControlCommand.End ();
|
||||
}
|
||||
if (isDone) {
|
||||
return Runtime.ControlCommand.Done ();
|
||||
}
|
||||
|
||||
runtimeDivert = new Runtime.Divert ();
|
||||
|
||||
// Normally we resolve the target content during the
|
||||
// Resolve phase, since we expect all runtime objects to
|
||||
// be available in order to find the final runtime path for
|
||||
// the destination. However, we need to resolve the target
|
||||
// (albeit without the runtime target) early so that
|
||||
// we can get information about the arguments - whether
|
||||
// they're by reference - since it affects the code we
|
||||
// generate here.
|
||||
ResolveTargetContent ();
|
||||
|
||||
|
||||
CheckArgumentValidity ();
|
||||
|
||||
// Passing arguments to the knot
|
||||
bool requiresArgCodeGen = arguments != null && arguments.Count > 0;
|
||||
if ( requiresArgCodeGen || isFunctionCall || isTunnel || isThread ) {
|
||||
|
||||
var container = new Runtime.Container ();
|
||||
|
||||
// Generate code for argument evaluation
|
||||
// This argument generation is coded defensively - it should
|
||||
// attempt to generate the code for all the parameters, even if
|
||||
// they don't match the expected arguments. This is so that the
|
||||
// parameter objects themselves are generated correctly and don't
|
||||
// get into a state of attempting to resolve references etc
|
||||
// without being generated.
|
||||
if (requiresArgCodeGen) {
|
||||
|
||||
// Function calls already in an evaluation context
|
||||
if (!isFunctionCall) {
|
||||
container.AddContent (Runtime.ControlCommand.EvalStart());
|
||||
}
|
||||
|
||||
List<FlowBase.Argument> targetArguments = null;
|
||||
if( targetContent )
|
||||
targetArguments = (targetContent as FlowBase).arguments;
|
||||
|
||||
for (var i = 0; i < arguments.Count; ++i) {
|
||||
Expression argToPass = arguments [i];
|
||||
FlowBase.Argument argExpected = null;
|
||||
if( targetArguments != null && i < targetArguments.Count )
|
||||
argExpected = targetArguments [i];
|
||||
|
||||
// Pass by reference: argument needs to be a variable reference
|
||||
if (argExpected != null && argExpected.isByReference) {
|
||||
|
||||
var varRef = argToPass as VariableReference;
|
||||
if (varRef == null) {
|
||||
Error ("Expected variable name to pass by reference to 'ref " + argExpected.identifier + "' but saw " + argToPass.ToString ());
|
||||
break;
|
||||
}
|
||||
|
||||
// Check that we're not attempting to pass a read count by reference
|
||||
var targetPath = new Path(varRef.pathIdentifiers);
|
||||
Parsed.Object targetForCount = targetPath.ResolveFromContext (this);
|
||||
if (targetForCount != null) {
|
||||
Error ("can't pass a read count by reference. '" + targetPath.dotSeparatedComponents+"' is a knot/stitch/label, but '"+target.dotSeparatedComponents+"' requires the name of a VAR to be passed.");
|
||||
break;
|
||||
}
|
||||
|
||||
var varPointer = new Runtime.VariablePointerValue (varRef.name);
|
||||
container.AddContent (varPointer);
|
||||
}
|
||||
|
||||
// Normal value being passed: evaluate it as normal
|
||||
else {
|
||||
argToPass.GenerateIntoContainer (container);
|
||||
}
|
||||
}
|
||||
|
||||
// Function calls were already in an evaluation context
|
||||
if (!isFunctionCall) {
|
||||
container.AddContent (Runtime.ControlCommand.EvalEnd());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Starting a thread? A bit like a push to the call stack below... but not.
|
||||
// It sort of puts the call stack on a thread stack (argh!) - forks the full flow.
|
||||
if (isThread) {
|
||||
container.AddContent(Runtime.ControlCommand.StartThread());
|
||||
}
|
||||
|
||||
// If this divert is a function call, tunnel, we push to the call stack
|
||||
// so we can return again
|
||||
else if (isFunctionCall || isTunnel) {
|
||||
runtimeDivert.pushesToStack = true;
|
||||
runtimeDivert.stackPushType = isFunctionCall ? Runtime.PushPopType.Function : Runtime.PushPopType.Tunnel;
|
||||
}
|
||||
|
||||
// Jump into the "function" (knot/stitch)
|
||||
container.AddContent (runtimeDivert);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Simple divert
|
||||
else {
|
||||
return runtimeDivert;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// When the divert is to a target that's actually a variable name
|
||||
// rather than an explicit knot/stitch name, try interpretting it
|
||||
// as such by getting the variable name.
|
||||
public string PathAsVariableName()
|
||||
{
|
||||
return target.firstComponent;
|
||||
}
|
||||
|
||||
|
||||
void ResolveTargetContent()
|
||||
{
|
||||
if (isEmpty || isEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetContent == null) {
|
||||
|
||||
// Is target of this divert a variable name that will be de-referenced
|
||||
// at runtime? If so, there won't be any further reference resolution
|
||||
// we can do at this point.
|
||||
var variableTargetName = PathAsVariableName ();
|
||||
if (variableTargetName != null) {
|
||||
var flowBaseScope = ClosestFlowBase ();
|
||||
var resolveResult = flowBaseScope.ResolveVariableWithName (variableTargetName, fromNode: this);
|
||||
if (resolveResult.found) {
|
||||
|
||||
// Make sure that the flow was typed correctly, given that we know that this
|
||||
// is meant to be a divert target
|
||||
if (resolveResult.isArgument) {
|
||||
var argument = resolveResult.ownerFlow.arguments.Where (a => a.identifier.name == variableTargetName).First();
|
||||
if ( !argument.isDivertTarget ) {
|
||||
Error ("Since '" + argument.identifier + "' is used as a variable divert target (on "+this.debugMetadata+"), it should be marked as: -> " + argument.identifier, resolveResult.ownerFlow);
|
||||
}
|
||||
}
|
||||
|
||||
runtimeDivert.variableDivertName = variableTargetName;
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
targetContent = target.ResolveFromContext (this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResolveReferences(Story context)
|
||||
{
|
||||
if (isEmpty || isEnd || isDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetContent) {
|
||||
runtimeDivert.targetPath = targetContent.runtimePath;
|
||||
}
|
||||
|
||||
// Resolve children (the arguments)
|
||||
base.ResolveReferences (context);
|
||||
|
||||
// May be null if it's a built in function (e.g. TURNS_SINCE)
|
||||
// or if it's a variable target.
|
||||
var targetFlow = targetContent as FlowBase;
|
||||
if (targetFlow) {
|
||||
if (!targetFlow.isFunction && this.isFunctionCall) {
|
||||
base.Error (targetFlow.identifier + " hasn't been marked as a function, but it's being called as one. Do you need to delcare the knot as '== function " + targetFlow.identifier + " =='?");
|
||||
} else if (targetFlow.isFunction && !this.isFunctionCall && !(this.parent is DivertTarget)) {
|
||||
base.Error (targetFlow.identifier + " can't be diverted to. It can only be called as a function since it's been marked as such: '" + targetFlow.identifier + "(...)'");
|
||||
}
|
||||
}
|
||||
|
||||
// Check validity of target content
|
||||
bool targetWasFound = targetContent != null;
|
||||
bool isBuiltIn = false;
|
||||
bool isExternal = false;
|
||||
|
||||
if (target.numberOfComponents == 1 ) {
|
||||
|
||||
// BuiltIn means TURNS_SINCE, CHOICE_COUNT, RANDOM or SEED_RANDOM
|
||||
isBuiltIn = FunctionCall.IsBuiltIn (target.firstComponent);
|
||||
|
||||
// Client-bound function?
|
||||
isExternal = context.IsExternal (target.firstComponent);
|
||||
|
||||
if (isBuiltIn || isExternal) {
|
||||
if (!isFunctionCall) {
|
||||
base.Error (target.firstComponent + " must be called as a function: ~ " + target.firstComponent + "()");
|
||||
}
|
||||
if (isExternal) {
|
||||
runtimeDivert.isExternal = true;
|
||||
if( arguments != null )
|
||||
runtimeDivert.externalArgs = arguments.Count;
|
||||
runtimeDivert.pushesToStack = false;
|
||||
runtimeDivert.targetPath = new Runtime.Path (this.target.firstComponent);
|
||||
CheckExternalArgumentValidity (context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Variable target?
|
||||
if (runtimeDivert.variableDivertName != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if( !targetWasFound && !isBuiltIn && !isExternal )
|
||||
Error ("target not found: '" + target + "'");
|
||||
}
|
||||
|
||||
// Returns false if there's an error
|
||||
void CheckArgumentValidity()
|
||||
{
|
||||
if (isEmpty)
|
||||
return;
|
||||
|
||||
// Argument passing: Check for errors in number of arguments
|
||||
var numArgs = 0;
|
||||
if (arguments != null && arguments.Count > 0)
|
||||
numArgs = arguments.Count;
|
||||
|
||||
// Missing content?
|
||||
// Can't check arguments properly. It'll be due to some
|
||||
// other error though, so although there's a problem and
|
||||
// we report false, we don't need to report a specific error.
|
||||
// It may also be because it's a valid call to an external
|
||||
// function, that we check at the resolve stage.
|
||||
if (targetContent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlowBase targetFlow = targetContent as FlowBase;
|
||||
|
||||
// No error, crikey!
|
||||
if (numArgs == 0 && (targetFlow == null || !targetFlow.hasParameters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetFlow == null && numArgs > 0) {
|
||||
Error ("target needs to be a knot or stitch in order to pass arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetFlow.arguments == null && numArgs > 0) {
|
||||
Error ("target (" + targetFlow.name + ") doesn't take parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
if( this.parent is DivertTarget ) {
|
||||
if (numArgs > 0)
|
||||
Error ("can't store arguments in a divert target variable");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramCount = targetFlow.arguments.Count;
|
||||
if (paramCount != numArgs) {
|
||||
|
||||
string butClause;
|
||||
if (numArgs == 0) {
|
||||
butClause = "but there weren't any passed to it";
|
||||
} else if (numArgs < paramCount) {
|
||||
butClause = "but only got " + numArgs;
|
||||
} else {
|
||||
butClause = "but got " + numArgs;
|
||||
}
|
||||
Error ("to '" + targetFlow.identifier + "' requires " + paramCount + " arguments, "+butClause);
|
||||
return;
|
||||
}
|
||||
|
||||
// Light type-checking for divert target arguments
|
||||
for (int i = 0; i < paramCount; ++i) {
|
||||
FlowBase.Argument flowArg = targetFlow.arguments [i];
|
||||
Parsed.Expression divArgExpr = arguments [i];
|
||||
|
||||
// Expecting a divert target as an argument, let's do some basic type checking
|
||||
if (flowArg.isDivertTarget) {
|
||||
|
||||
// Not passing a divert target or any kind of variable reference?
|
||||
var varRef = divArgExpr as VariableReference;
|
||||
if (!(divArgExpr is DivertTarget) && varRef == null ) {
|
||||
Error ("Target '" + targetFlow.identifier + "' expects a divert target for the parameter named -> " + flowArg.identifier + " but saw " + divArgExpr, divArgExpr);
|
||||
}
|
||||
|
||||
// Passing 'a' instead of '-> a'?
|
||||
// i.e. read count instead of divert target
|
||||
else if (varRef != null) {
|
||||
|
||||
// Unfortunately have to manually resolve here since we're still in code gen
|
||||
var knotCountPath = new Path(varRef.pathIdentifiers);
|
||||
Parsed.Object targetForCount = knotCountPath.ResolveFromContext (varRef);
|
||||
if (targetForCount != null) {
|
||||
Error ("Passing read count of '" + knotCountPath.dotSeparatedComponents + "' instead of a divert target. You probably meant '" + knotCountPath + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetFlow == null) {
|
||||
Error ("Can't call as a function or with arguments unless it's a knot or stitch");
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void CheckExternalArgumentValidity(Story context)
|
||||
{
|
||||
string externalName = target.firstComponent;
|
||||
ExternalDeclaration external = null;
|
||||
var found = context.externals.TryGetValue(externalName, out external);
|
||||
System.Diagnostics.Debug.Assert (found, "external not found");
|
||||
|
||||
int externalArgCount = external.argumentNames.Count;
|
||||
int ownArgCount = 0;
|
||||
if (arguments != null) {
|
||||
ownArgCount = arguments.Count;
|
||||
}
|
||||
|
||||
if (ownArgCount != externalArgCount) {
|
||||
Error ("incorrect number of arguments sent to external function '" + externalName + "'. Expected " + externalArgCount + " but got " + ownArgCount);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Error (string message, Object source = null, bool isWarning = false)
|
||||
{
|
||||
// Could be getting an error from a nested Divert
|
||||
if (source != this && source) {
|
||||
base.Error (message, source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFunctionCall) {
|
||||
base.Error ("Function call " + message, source, isWarning);
|
||||
} else {
|
||||
base.Error ("Divert " + message, source, isWarning);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (target != null)
|
||||
return target.ToString ();
|
||||
else
|
||||
return "-> <empty divert>";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26c47b19962e641869a39b85cd86f9e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,172 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class DivertTarget : Expression
|
||||
{
|
||||
public Divert divert;
|
||||
|
||||
public DivertTarget (Divert divert)
|
||||
{
|
||||
this.divert = AddContent(divert);
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer (Runtime.Container container)
|
||||
{
|
||||
divert.GenerateRuntimeObject();
|
||||
|
||||
_runtimeDivert = (Runtime.Divert) divert.runtimeDivert;
|
||||
_runtimeDivertTargetValue = new Runtime.DivertTargetValue ();
|
||||
|
||||
container.AddContent (_runtimeDivertTargetValue);
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
if( divert.isDone || divert.isEnd )
|
||||
{
|
||||
Error("Can't Can't use -> DONE or -> END as variable divert targets", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Parsed.Object usageContext = this;
|
||||
while (usageContext && usageContext is Expression) {
|
||||
|
||||
bool badUsage = false;
|
||||
bool foundUsage = false;
|
||||
|
||||
var usageParent = usageContext.parent;
|
||||
if (usageParent is BinaryExpression) {
|
||||
|
||||
// Only allowed to compare for equality
|
||||
|
||||
var binaryExprParent = usageParent as BinaryExpression;
|
||||
if (binaryExprParent.opName != "==" && binaryExprParent.opName != "!=") {
|
||||
badUsage = true;
|
||||
} else {
|
||||
if (!(binaryExprParent.leftExpression is DivertTarget || binaryExprParent.leftExpression is VariableReference)) {
|
||||
badUsage = true;
|
||||
}
|
||||
if (!(binaryExprParent.rightExpression is DivertTarget || binaryExprParent.rightExpression is VariableReference)) {
|
||||
badUsage = true;
|
||||
}
|
||||
}
|
||||
foundUsage = true;
|
||||
}
|
||||
else if( usageParent is FunctionCall ) {
|
||||
var funcCall = usageParent as FunctionCall;
|
||||
if( !funcCall.isTurnsSince && !funcCall.isReadCount ) {
|
||||
badUsage = true;
|
||||
}
|
||||
foundUsage = true;
|
||||
}
|
||||
else if (usageParent is Expression) {
|
||||
badUsage = true;
|
||||
foundUsage = true;
|
||||
}
|
||||
else if (usageParent is MultipleConditionExpression) {
|
||||
badUsage = true;
|
||||
foundUsage = true;
|
||||
} else if (usageParent is Choice && ((Choice)usageParent).condition == usageContext) {
|
||||
badUsage = true;
|
||||
foundUsage = true;
|
||||
} else if (usageParent is Conditional || usageParent is ConditionalSingleBranch) {
|
||||
badUsage = true;
|
||||
foundUsage = true;
|
||||
}
|
||||
|
||||
if (badUsage) {
|
||||
Error ("Can't use a divert target like that. Did you intend to call '" + divert.target + "' as a function: likeThis(), or check the read count: likeThis, with no arrows?", this);
|
||||
}
|
||||
|
||||
if (foundUsage)
|
||||
break;
|
||||
|
||||
usageContext = usageParent;
|
||||
}
|
||||
|
||||
// Example ink for this case:
|
||||
//
|
||||
// VAR x = -> blah
|
||||
//
|
||||
// ...which means that "blah" is expected to be a literal stitch target rather
|
||||
// than a variable name. We can't really intelligently recover from this (e.g. if blah happens to
|
||||
// contain a divert target itself) since really we should be generating a variable reference
|
||||
// rather than a concrete DivertTarget, so we list it as an error.
|
||||
if (_runtimeDivert.hasVariableTarget)
|
||||
Error ("Since '"+divert.target.dotSeparatedComponents+"' is a variable, it shouldn't be preceded by '->' here.");
|
||||
|
||||
// Main resolve
|
||||
_runtimeDivertTargetValue.targetPath = _runtimeDivert.targetPath;
|
||||
|
||||
// Tell hard coded (yet variable) divert targets that they also need to be counted
|
||||
// TODO: Only detect DivertTargets that are values rather than being used directly for
|
||||
// read or turn counts. Should be able to detect this by looking for other uses of containerForCounting
|
||||
var targetContent = this.divert.targetContent;
|
||||
if (targetContent != null ) {
|
||||
var target = targetContent.containerForCounting;
|
||||
if (target != null)
|
||||
{
|
||||
// Purpose is known: used directly in TURNS_SINCE(-> divTarg)
|
||||
var parentFunc = this.parent as FunctionCall;
|
||||
if( parentFunc && parentFunc.isTurnsSince ) {
|
||||
target.turnIndexShouldBeCounted = true;
|
||||
}
|
||||
|
||||
// Unknown purpose, count everything
|
||||
else {
|
||||
target.visitsShouldBeCounted = true;
|
||||
target.turnIndexShouldBeCounted = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Unfortunately not possible:
|
||||
// https://github.com/inkle/ink/issues/538
|
||||
//
|
||||
// VAR func = -> double
|
||||
//
|
||||
// === function double(ref x)
|
||||
// ~ x = x * 2
|
||||
//
|
||||
// Because when generating the parameters for a function
|
||||
// to be called, it needs to know ahead of time when
|
||||
// compiling whether to pass a variable reference or value.
|
||||
//
|
||||
var targetFlow = (targetContent as FlowBase);
|
||||
if (targetFlow != null && targetFlow.arguments != null)
|
||||
{
|
||||
foreach(var arg in targetFlow.arguments) {
|
||||
if(arg.isByReference)
|
||||
{
|
||||
Error("Can't store a divert target to a knot or function that has by-reference arguments ('"+targetFlow.identifier+"' has 'ref "+arg.identifier+"').");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Equals override necessary in order to check for CONST multiple definition equality
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
var otherDivTarget = obj as DivertTarget;
|
||||
if (otherDivTarget == null) return false;
|
||||
|
||||
var targetStr = this.divert.target.dotSeparatedComponents;
|
||||
var otherTargetStr = otherDivTarget.divert.target.dotSeparatedComponents;
|
||||
|
||||
return targetStr.Equals (otherTargetStr);
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
var targetStr = this.divert.target.dotSeparatedComponents;
|
||||
return targetStr.GetHashCode ();
|
||||
}
|
||||
|
||||
Runtime.DivertTargetValue _runtimeDivertTargetValue;
|
||||
Runtime.Divert _runtimeDivert;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8a428e7434204b02921e47651c42329
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,307 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public abstract class Expression : Parsed.Object
|
||||
{
|
||||
public bool outputWhenComplete { get; set; }
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var container = new Runtime.Container ();
|
||||
|
||||
// Tell Runtime to start evaluating the following content as an expression
|
||||
container.AddContent (Runtime.ControlCommand.EvalStart());
|
||||
|
||||
GenerateIntoContainer (container);
|
||||
|
||||
// Tell Runtime to output the result of the expression evaluation to the output stream
|
||||
if (outputWhenComplete) {
|
||||
container.AddContent (Runtime.ControlCommand.EvalOutput());
|
||||
}
|
||||
|
||||
// Tell Runtime to stop evaluating the content as an expression
|
||||
container.AddContent (Runtime.ControlCommand.EvalEnd());
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// When generating the value of a constant expression,
|
||||
// we can't just keep generating the same constant expression into
|
||||
// different places where the constant value is referenced, since then
|
||||
// the same runtime objects would be used in multiple places, which
|
||||
// is impossible since each runtime object should have one parent.
|
||||
// Instead, we generate a prototype of the runtime object(s), then
|
||||
// copy them each time they're used.
|
||||
public void GenerateConstantIntoContainer(Runtime.Container container)
|
||||
{
|
||||
if( _prototypeRuntimeConstantExpression == null ) {
|
||||
_prototypeRuntimeConstantExpression = new Runtime.Container ();
|
||||
GenerateIntoContainer (_prototypeRuntimeConstantExpression);
|
||||
}
|
||||
|
||||
foreach (var runtimeObj in _prototypeRuntimeConstantExpression.content) {
|
||||
container.AddContent (runtimeObj.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void GenerateIntoContainer (Runtime.Container container);
|
||||
|
||||
Runtime.Container _prototypeRuntimeConstantExpression;
|
||||
}
|
||||
|
||||
public class BinaryExpression : Expression
|
||||
{
|
||||
public Expression leftExpression;
|
||||
public Expression rightExpression;
|
||||
public string opName;
|
||||
|
||||
public BinaryExpression(Expression left, Expression right, string opName)
|
||||
{
|
||||
leftExpression = AddContent(left);
|
||||
rightExpression = AddContent(right);
|
||||
this.opName = opName;
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer(Runtime.Container container)
|
||||
{
|
||||
leftExpression.GenerateIntoContainer (container);
|
||||
rightExpression.GenerateIntoContainer (container);
|
||||
|
||||
opName = NativeNameForOp (opName);
|
||||
|
||||
container.AddContent(Runtime.NativeFunctionCall.CallWithName(opName));
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
// Check for the following case:
|
||||
//
|
||||
// (not A) ? B
|
||||
//
|
||||
// Since this easy to accidentally do:
|
||||
//
|
||||
// not A ? B
|
||||
//
|
||||
// when you intend:
|
||||
//
|
||||
// not (A ? B)
|
||||
if (NativeNameForOp (opName) == "?") {
|
||||
var leftUnary = leftExpression as UnaryExpression;
|
||||
if( leftUnary != null && (leftUnary.op == "not" || leftUnary.op == "!") ) {
|
||||
Error ("Using 'not' or '!' here negates '"+leftUnary.innerExpression+"' rather than the result of the '?' or 'has' operator. You need to add parentheses around the (A ? B) expression.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string NativeNameForOp(string opName)
|
||||
{
|
||||
if (opName == "and")
|
||||
return "&&";
|
||||
|
||||
if (opName == "or")
|
||||
return "||";
|
||||
|
||||
if (opName == "mod")
|
||||
return "%";
|
||||
|
||||
if (opName == "has")
|
||||
return "?";
|
||||
|
||||
if (opName == "hasnt")
|
||||
return "!?";
|
||||
|
||||
return opName;
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return string.Format ("({0} {1} {2})", leftExpression, opName, rightExpression);
|
||||
}
|
||||
}
|
||||
|
||||
public class UnaryExpression : Expression
|
||||
{
|
||||
public Expression innerExpression;
|
||||
public string op;
|
||||
|
||||
// Attempt to flatten inner expression immediately
|
||||
// e.g. convert (-(5)) into (-5)
|
||||
public static Expression WithInner(Expression inner, string op) {
|
||||
|
||||
var innerNumber = inner as Number;
|
||||
if( innerNumber ) {
|
||||
|
||||
if( op == "-" ) {
|
||||
if( innerNumber.value is int ) {
|
||||
return new Number( -((int)innerNumber.value) );
|
||||
} else if( innerNumber.value is float ) {
|
||||
return new Number( -((float)innerNumber.value) );
|
||||
}
|
||||
}
|
||||
|
||||
else if( op == "!" || op == "not" ) {
|
||||
if( innerNumber.value is int ) {
|
||||
return new Number( (int)innerNumber.value == 0 );
|
||||
} else if( innerNumber.value is float ) {
|
||||
return new Number( (float)innerNumber.value == 0.0f );
|
||||
} else if( innerNumber.value is bool ) {
|
||||
return new Number( !(bool)innerNumber.value );
|
||||
}
|
||||
}
|
||||
|
||||
throw new System.Exception ("Unexpected operation or number type");
|
||||
}
|
||||
|
||||
// Normal fallback
|
||||
var unary = new UnaryExpression (inner, op);
|
||||
return unary;
|
||||
}
|
||||
|
||||
public UnaryExpression(Expression inner, string op)
|
||||
{
|
||||
this.innerExpression = AddContent(inner);
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer(Runtime.Container container)
|
||||
{
|
||||
innerExpression.GenerateIntoContainer (container);
|
||||
|
||||
container.AddContent(Runtime.NativeFunctionCall.CallWithName(nativeNameForOp));
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return nativeNameForOp + innerExpression;
|
||||
}
|
||||
|
||||
string nativeNameForOp
|
||||
{
|
||||
get {
|
||||
// Replace "-" with "_" to make it unique (compared to subtraction)
|
||||
if (op == "-")
|
||||
return "_";
|
||||
if (op == "not")
|
||||
return "!";
|
||||
return op;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IncDecExpression : Expression
|
||||
{
|
||||
public Identifier varIdentifier;
|
||||
public bool isInc;
|
||||
public Expression expression;
|
||||
|
||||
public IncDecExpression(Identifier varIdentifier, bool isInc)
|
||||
{
|
||||
this.varIdentifier = varIdentifier;
|
||||
this.isInc = isInc;
|
||||
}
|
||||
|
||||
public IncDecExpression (Identifier varIdentifier, Expression expression, bool isInc) : this(varIdentifier, isInc)
|
||||
{
|
||||
this.expression = expression;
|
||||
AddContent (expression);
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer(Runtime.Container container)
|
||||
{
|
||||
// x = x + y
|
||||
// ^^^ ^ ^ ^
|
||||
// 4 1 3 2
|
||||
// Reverse polish notation: (x 1 +) (assign to x)
|
||||
|
||||
// 1.
|
||||
container.AddContent (new Runtime.VariableReference (varIdentifier?.name));
|
||||
|
||||
// 2.
|
||||
// - Expression used in the form ~ x += y
|
||||
// - Simple version: ~ x++
|
||||
if (expression)
|
||||
expression.GenerateIntoContainer (container);
|
||||
else
|
||||
container.AddContent (new Runtime.IntValue (1));
|
||||
|
||||
// 3.
|
||||
container.AddContent (Runtime.NativeFunctionCall.CallWithName (isInc ? "+" : "-"));
|
||||
|
||||
// 4.
|
||||
_runtimeAssignment = new Runtime.VariableAssignment(varIdentifier?.name, false);
|
||||
container.AddContent (_runtimeAssignment);
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
var varResolveResult = context.ResolveVariableWithName(varIdentifier?.name, fromNode: this);
|
||||
if (!varResolveResult.found) {
|
||||
Error ("variable for "+incrementDecrementWord+" could not be found: '"+varIdentifier+"' after searching: "+this.descriptionOfScope);
|
||||
}
|
||||
|
||||
_runtimeAssignment.isGlobal = varResolveResult.isGlobal;
|
||||
|
||||
if (!(parent is Weave) && !(parent is FlowBase) && !(parent is ContentList)) {
|
||||
Error ("Can't use " + incrementDecrementWord + " as sub-expression");
|
||||
}
|
||||
}
|
||||
|
||||
string incrementDecrementWord {
|
||||
get {
|
||||
if (isInc)
|
||||
return "increment";
|
||||
else
|
||||
return "decrement";
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (expression)
|
||||
return varIdentifier + (isInc ? " += " : " -= ") + expression.ToString ();
|
||||
else
|
||||
return varIdentifier + (isInc ? "++" : "--");
|
||||
}
|
||||
|
||||
Runtime.VariableAssignment _runtimeAssignment;
|
||||
}
|
||||
|
||||
public class MultipleConditionExpression : Expression
|
||||
{
|
||||
public List<Expression> subExpressions {
|
||||
get {
|
||||
return this.content.Cast<Expression> ().ToList ();
|
||||
}
|
||||
}
|
||||
|
||||
public MultipleConditionExpression(List<Expression> conditionExpressions)
|
||||
{
|
||||
AddContent (conditionExpressions);
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer(Runtime.Container container)
|
||||
{
|
||||
// A && B && C && D
|
||||
// => (((A B &&) C &&) D &&) etc
|
||||
bool isFirst = true;
|
||||
foreach (var conditionExpr in subExpressions) {
|
||||
|
||||
conditionExpr.GenerateIntoContainer (container);
|
||||
|
||||
if (!isFirst) {
|
||||
container.AddContent (Runtime.NativeFunctionCall.CallWithName ("&&"));
|
||||
}
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 476809ca3ec8f4743afd6fa33bd6e442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class ExternalDeclaration : Parsed.Object, INamedContent
|
||||
{
|
||||
public string name
|
||||
{
|
||||
get { return identifier?.name; }
|
||||
}
|
||||
public Identifier identifier { get; set; }
|
||||
public List<string> argumentNames { get; set; }
|
||||
|
||||
public ExternalDeclaration (Identifier identifier, List<string> argumentNames)
|
||||
{
|
||||
this.identifier = identifier;
|
||||
this.argumentNames = argumentNames;
|
||||
}
|
||||
|
||||
public override Ink.Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
story.AddExternal (this);
|
||||
|
||||
// No runtime code exists for an external, only metadata
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca4476886042f471a9771284a027b45f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,439 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
// Base class for Knots and Stitches
|
||||
public abstract class FlowBase : Parsed.Object, INamedContent
|
||||
{
|
||||
public class Argument
|
||||
{
|
||||
public Identifier identifier;
|
||||
public bool isByReference;
|
||||
public bool isDivertTarget;
|
||||
}
|
||||
|
||||
public string name
|
||||
{
|
||||
get { return identifier?.name; }
|
||||
}
|
||||
public Identifier identifier { get; set; }
|
||||
public List<Argument> arguments { get; protected set; }
|
||||
public bool hasParameters { get { return arguments != null && arguments.Count > 0; } }
|
||||
public Dictionary<string, VariableAssignment> variableDeclarations;
|
||||
|
||||
public abstract FlowLevel flowLevel { get; }
|
||||
public bool isFunction { get; protected set; }
|
||||
|
||||
public FlowBase (Identifier name = null, List<Parsed.Object> topLevelObjects = null, List<Argument> arguments = null, bool isFunction = false, bool isIncludedStory = false)
|
||||
{
|
||||
this.identifier = name;
|
||||
|
||||
if (topLevelObjects == null) {
|
||||
topLevelObjects = new List<Parsed.Object> ();
|
||||
}
|
||||
|
||||
// Used by story to add includes
|
||||
PreProcessTopLevelObjects (topLevelObjects);
|
||||
|
||||
topLevelObjects = SplitWeaveAndSubFlowContent (topLevelObjects, isRootStory:this is Story && !isIncludedStory);
|
||||
|
||||
AddContent(topLevelObjects);
|
||||
|
||||
this.arguments = arguments;
|
||||
this.isFunction = isFunction;
|
||||
this.variableDeclarations = new Dictionary<string, VariableAssignment> ();
|
||||
}
|
||||
|
||||
List<Parsed.Object> SplitWeaveAndSubFlowContent(List<Parsed.Object> contentObjs, bool isRootStory)
|
||||
{
|
||||
var weaveObjs = new List<Parsed.Object> ();
|
||||
var subFlowObjs = new List<Parsed.Object> ();
|
||||
|
||||
_subFlowsByName = new Dictionary<string, FlowBase> ();
|
||||
|
||||
foreach (var obj in contentObjs) {
|
||||
|
||||
var subFlow = obj as FlowBase;
|
||||
if (subFlow) {
|
||||
if (_firstChildFlow == null)
|
||||
_firstChildFlow = subFlow;
|
||||
|
||||
subFlowObjs.Add (obj);
|
||||
_subFlowsByName [subFlow.identifier?.name] = subFlow;
|
||||
} else {
|
||||
weaveObjs.Add (obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Implicit final gather in top level story for ending without warning that you run out of content
|
||||
if (isRootStory) {
|
||||
weaveObjs.Add (new Gather (null, 1));
|
||||
weaveObjs.Add (new Divert (new Path (Identifier.Done)));
|
||||
}
|
||||
|
||||
var finalContent = new List<Parsed.Object> ();
|
||||
|
||||
if (weaveObjs.Count > 0) {
|
||||
_rootWeave = new Weave (weaveObjs, 0);
|
||||
finalContent.Add (_rootWeave);
|
||||
}
|
||||
|
||||
if (subFlowObjs.Count > 0) {
|
||||
finalContent.AddRange (subFlowObjs);
|
||||
}
|
||||
|
||||
return finalContent;
|
||||
}
|
||||
|
||||
protected virtual void PreProcessTopLevelObjects(List<Parsed.Object> topLevelObjects)
|
||||
{
|
||||
// empty by default, used by Story to process included file references
|
||||
}
|
||||
|
||||
public struct VariableResolveResult
|
||||
{
|
||||
public bool found;
|
||||
public bool isGlobal;
|
||||
public bool isArgument;
|
||||
public bool isTemporary;
|
||||
public FlowBase ownerFlow;
|
||||
}
|
||||
|
||||
public VariableResolveResult ResolveVariableWithName(string varName, Parsed.Object fromNode)
|
||||
{
|
||||
var result = new VariableResolveResult ();
|
||||
|
||||
// Search in the stitch / knot that owns the node first
|
||||
var ownerFlow = fromNode == null ? this : fromNode.ClosestFlowBase ();
|
||||
|
||||
// Argument
|
||||
if (ownerFlow.arguments != null ) {
|
||||
foreach (var arg in ownerFlow.arguments) {
|
||||
if (arg.identifier.name.Equals (varName)) {
|
||||
result.found = true;
|
||||
result.isArgument = true;
|
||||
result.ownerFlow = ownerFlow;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temp
|
||||
var story = this.story; // optimisation
|
||||
if (ownerFlow != story && ownerFlow.variableDeclarations.ContainsKey (varName)) {
|
||||
result.found = true;
|
||||
result.ownerFlow = ownerFlow;
|
||||
result.isTemporary = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Global
|
||||
if (story.variableDeclarations.ContainsKey (varName)) {
|
||||
result.found = true;
|
||||
result.ownerFlow = story;
|
||||
result.isGlobal = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.found = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public void TryAddNewVariableDeclaration(VariableAssignment varDecl)
|
||||
{
|
||||
var varName = varDecl.variableName;
|
||||
if (variableDeclarations.ContainsKey (varName)) {
|
||||
|
||||
var prevDeclError = "";
|
||||
var debugMetadata = variableDeclarations [varName].debugMetadata;
|
||||
if (debugMetadata != null) {
|
||||
prevDeclError = " ("+variableDeclarations [varName].debugMetadata+")";
|
||||
}
|
||||
Error("found declaration variable '"+varName+"' that was already declared"+prevDeclError, varDecl, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
variableDeclarations [varDecl.variableName] = varDecl;
|
||||
}
|
||||
|
||||
public void ResolveWeavePointNaming ()
|
||||
{
|
||||
// Find all weave points and organise them by name ready for
|
||||
// diverting. Also detect naming collisions.
|
||||
if( _rootWeave )
|
||||
_rootWeave.ResolveWeavePointNaming ();
|
||||
|
||||
if (_subFlowsByName != null) {
|
||||
foreach (var namedSubFlow in _subFlowsByName) {
|
||||
namedSubFlow.Value.ResolveWeavePointNaming ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
Return foundReturn = null;
|
||||
if (isFunction) {
|
||||
CheckForDisallowedFunctionFlowControl ();
|
||||
}
|
||||
|
||||
// Non-functon: Make sure knots and stitches don't attempt to use Return statement
|
||||
else if( flowLevel == FlowLevel.Knot || flowLevel == FlowLevel.Stitch ) {
|
||||
foundReturn = Find<Return> ();
|
||||
if (foundReturn != null) {
|
||||
Error ("Return statements can only be used in knots that are declared as functions: == function " + this.identifier + " ==", foundReturn);
|
||||
}
|
||||
}
|
||||
|
||||
var container = new Runtime.Container ();
|
||||
container.name = identifier?.name;
|
||||
|
||||
if( this.story.countAllVisits ) {
|
||||
container.visitsShouldBeCounted = true;
|
||||
}
|
||||
|
||||
GenerateArgumentVariableAssignments (container);
|
||||
|
||||
// Run through content defined for this knot/stitch:
|
||||
// - First of all, any initial content before a sub-stitch
|
||||
// or any weave content is added to the main content container
|
||||
// - The first inner knot/stitch is automatically entered, while
|
||||
// the others are only accessible by an explicit divert
|
||||
// - The exception to this rule is if the knot/stitch takes
|
||||
// parameters, in which case it can't be auto-entered.
|
||||
// - Any Choices and Gathers (i.e. IWeavePoint) found are
|
||||
// processsed by GenerateFlowContent.
|
||||
int contentIdx = 0;
|
||||
while (content != null && contentIdx < content.Count) {
|
||||
|
||||
Parsed.Object obj = content [contentIdx];
|
||||
|
||||
// Inner knots and stitches
|
||||
if (obj is FlowBase) {
|
||||
|
||||
var childFlow = (FlowBase)obj;
|
||||
|
||||
var childFlowRuntime = childFlow.runtimeObject;
|
||||
|
||||
// First inner stitch - automatically step into it
|
||||
// 20/09/2016 - let's not auto step into knots
|
||||
if (contentIdx == 0 && !childFlow.hasParameters
|
||||
&& this.flowLevel == FlowLevel.Knot) {
|
||||
_startingSubFlowDivert = new Runtime.Divert ();
|
||||
container.AddContent(_startingSubFlowDivert);
|
||||
_startingSubFlowRuntime = childFlowRuntime;
|
||||
}
|
||||
|
||||
// Check for duplicate knots/stitches with same name
|
||||
var namedChild = (Runtime.INamedContent)childFlowRuntime;
|
||||
Runtime.INamedContent existingChild = null;
|
||||
if (container.namedContent.TryGetValue(namedChild.name, out existingChild) ) {
|
||||
var errorMsg = string.Format ("{0} already contains flow named '{1}' (at {2})",
|
||||
this.GetType().Name,
|
||||
namedChild.name,
|
||||
(existingChild as Runtime.Object).debugMetadata);
|
||||
|
||||
Error (errorMsg, childFlow);
|
||||
}
|
||||
|
||||
container.AddToNamedContentOnly (namedChild);
|
||||
}
|
||||
|
||||
// Other content (including entire Weaves that were grouped in the constructor)
|
||||
// At the time of writing, all FlowBases have a maximum of one piece of "other content"
|
||||
// and it's always the root Weave
|
||||
else {
|
||||
container.AddContent (obj.runtimeObject);
|
||||
}
|
||||
|
||||
contentIdx++;
|
||||
}
|
||||
|
||||
// CHECK FOR FINAL LOOSE ENDS!
|
||||
// Notes:
|
||||
// - Functions don't need to terminate - they just implicitly return
|
||||
// - If return statement was found, don't continue finding warnings for missing control flow,
|
||||
// since it's likely that a return statement has been used instead of a ->-> or something,
|
||||
// or the writer failed to mark the knot as a function.
|
||||
// - _rootWeave may be null if it's a knot that only has stitches
|
||||
if (flowLevel != FlowLevel.Story && !this.isFunction && _rootWeave != null && foundReturn == null) {
|
||||
_rootWeave.ValidateTermination (WarningInTermination);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
void GenerateArgumentVariableAssignments(Runtime.Container container)
|
||||
{
|
||||
if (this.arguments == null || this.arguments.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign parameters in reverse since they'll be popped off the evaluation stack
|
||||
// No need to generate EvalStart and EvalEnd since there's nothing being pushed
|
||||
// back onto the evaluation stack.
|
||||
for (int i = arguments.Count - 1; i >= 0; --i) {
|
||||
var paramName = arguments [i].identifier?.name;
|
||||
|
||||
var assign = new Runtime.VariableAssignment (paramName, isNewDeclaration:true);
|
||||
container.AddContent (assign);
|
||||
}
|
||||
}
|
||||
|
||||
public Parsed.Object ContentWithNameAtLevel(string name, FlowLevel? level = null, bool deepSearch = false)
|
||||
{
|
||||
// Referencing self?
|
||||
if (level == this.flowLevel || level == null) {
|
||||
if (name == this.identifier?.name) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
if ( level == FlowLevel.WeavePoint || level == null ) {
|
||||
|
||||
Parsed.Object weavePointResult = null;
|
||||
|
||||
if (_rootWeave) {
|
||||
weavePointResult = (Parsed.Object)_rootWeave.WeavePointNamed (name);
|
||||
if (weavePointResult)
|
||||
return weavePointResult;
|
||||
}
|
||||
|
||||
// Stop now if we only wanted a result if it's a weave point?
|
||||
if (level == FlowLevel.WeavePoint)
|
||||
return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
|
||||
}
|
||||
|
||||
// If this flow would be incapable of containing the requested level, early out
|
||||
// (e.g. asking for a Knot from a Stitch)
|
||||
if (level != null && level < this.flowLevel)
|
||||
return null;
|
||||
|
||||
FlowBase subFlow = null;
|
||||
|
||||
if (_subFlowsByName.TryGetValue (name, out subFlow)) {
|
||||
if (level == null || level == subFlow.flowLevel)
|
||||
return subFlow;
|
||||
}
|
||||
|
||||
return deepSearch ? DeepSearchForAnyLevelContent(name) : null;
|
||||
}
|
||||
|
||||
Parsed.Object DeepSearchForAnyLevelContent(string name)
|
||||
{
|
||||
var weaveResultSelf = ContentWithNameAtLevel (name, level:FlowLevel.WeavePoint, deepSearch: false);
|
||||
if (weaveResultSelf) {
|
||||
return weaveResultSelf;
|
||||
}
|
||||
|
||||
foreach (var subFlowNamePair in _subFlowsByName) {
|
||||
var subFlow = subFlowNamePair.Value;
|
||||
var deepResult = subFlow.ContentWithNameAtLevel (name, level:null, deepSearch: true);
|
||||
if (deepResult)
|
||||
return deepResult;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
if (_startingSubFlowDivert) {
|
||||
_startingSubFlowDivert.targetPath = _startingSubFlowRuntime.path;
|
||||
}
|
||||
|
||||
base.ResolveReferences(context);
|
||||
|
||||
// Check validity of parameter names
|
||||
if (arguments != null) {
|
||||
|
||||
foreach (var arg in arguments)
|
||||
context.CheckForNamingCollisions (this, arg.identifier, Story.SymbolType.Arg, "argument");
|
||||
|
||||
// Separately, check for duplicate arugment names, since they aren't Parsed.Objects,
|
||||
// so have to be checked independently.
|
||||
for (int i = 0; i < arguments.Count; i++) {
|
||||
for (int j = i + 1; j < arguments.Count; j++) {
|
||||
if (arguments [i].identifier?.name == arguments [j].identifier?.name) {
|
||||
Error ("Multiple arguments with the same name: '" + arguments [i].identifier + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check naming collisions for knots and stitches
|
||||
if (flowLevel != FlowLevel.Story) {
|
||||
// Weave points aren't FlowBases, so this will only be knot or stitch
|
||||
var symbolType = flowLevel == FlowLevel.Knot ? Story.SymbolType.Knot : Story.SymbolType.SubFlowAndWeave;
|
||||
context.CheckForNamingCollisions (this, identifier, symbolType);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckForDisallowedFunctionFlowControl()
|
||||
{
|
||||
if (!(this is Knot)) {
|
||||
Error ("Functions cannot be stitches - i.e. they should be defined as '== function myFunc ==' rather than public to another knot.");
|
||||
}
|
||||
|
||||
// Not allowed sub-flows
|
||||
foreach (var subFlowAndName in _subFlowsByName) {
|
||||
var name = subFlowAndName.Key;
|
||||
var subFlow = subFlowAndName.Value;
|
||||
Error ("Functions may not contain stitches, but saw '"+name+"' within the function '"+this.identifier+"'", subFlow);
|
||||
}
|
||||
|
||||
var allDiverts = _rootWeave.FindAll<Divert> ();
|
||||
foreach (var divert in allDiverts) {
|
||||
if( !divert.isFunctionCall && !(divert.parent is DivertTarget) )
|
||||
Error ("Functions may not contain diverts, but saw '"+divert.ToString()+"'", divert);
|
||||
}
|
||||
|
||||
var allChoices = _rootWeave.FindAll<Choice> ();
|
||||
foreach (var choice in allChoices) {
|
||||
Error ("Functions may not contain choices, but saw '"+choice.ToString()+"'", choice);
|
||||
}
|
||||
}
|
||||
|
||||
void WarningInTermination(Parsed.Object terminatingObject)
|
||||
{
|
||||
string message = "Apparent loose end exists where the flow runs out. Do you need a '-> DONE' statement, choice or divert?";
|
||||
if (terminatingObject.parent == _rootWeave && _firstChildFlow) {
|
||||
message = message + " Note that if you intend to enter '"+_firstChildFlow.identifier+"' next, you need to divert to it explicitly.";
|
||||
}
|
||||
|
||||
var terminatingDivert = terminatingObject as Divert;
|
||||
if (terminatingDivert && terminatingDivert.isTunnel) {
|
||||
message = message + " When final tunnel to '"+terminatingDivert.target+" ->' returns it won't have anywhere to go.";
|
||||
}
|
||||
|
||||
Warning (message, terminatingObject);
|
||||
}
|
||||
|
||||
protected Dictionary<string, FlowBase> subFlowsByName {
|
||||
get {
|
||||
return _subFlowsByName;
|
||||
}
|
||||
}
|
||||
|
||||
public override string typeName {
|
||||
get {
|
||||
if (isFunction) return "Function";
|
||||
else return flowLevel.ToString ();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return typeName+" '" + identifier + "'";
|
||||
}
|
||||
|
||||
Weave _rootWeave;
|
||||
Dictionary<string, FlowBase> _subFlowsByName;
|
||||
Runtime.Divert _startingSubFlowDivert;
|
||||
Runtime.Object _startingSubFlowRuntime;
|
||||
FlowBase _firstChildFlow;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 573f03d14273b46a29e16c5ff9ec8059
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public enum FlowLevel
|
||||
{
|
||||
Story,
|
||||
Knot,
|
||||
Stitch,
|
||||
WeavePoint // not actually a FlowBase, but used for diverts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3e0618d06b104e3a82b27f11b68ad54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class FunctionCall : Expression
|
||||
{
|
||||
public string name { get { return _proxyDivert.target.firstComponent; } }
|
||||
public Divert proxyDivert { get { return _proxyDivert; } }
|
||||
public List<Expression> arguments { get { return _proxyDivert.arguments; } }
|
||||
public Runtime.Divert runtimeDivert { get { return _proxyDivert.runtimeDivert; } }
|
||||
public bool isChoiceCount { get { return name == "CHOICE_COUNT"; } }
|
||||
public bool isTurns { get { return name == "TURNS"; } }
|
||||
public bool isTurnsSince { get { return name == "TURNS_SINCE"; } }
|
||||
public bool isRandom { get { return name == "RANDOM"; } }
|
||||
public bool isSeedRandom { get { return name == "SEED_RANDOM"; } }
|
||||
public bool isListRange { get { return name == "LIST_RANGE"; } }
|
||||
public bool isListRandom { get { return name == "LIST_RANDOM"; } }
|
||||
public bool isReadCount { get { return name == "READ_COUNT"; } }
|
||||
|
||||
public bool shouldPopReturnedValue;
|
||||
|
||||
public FunctionCall (Identifier functionName, List<Expression> arguments)
|
||||
{
|
||||
_proxyDivert = new Parsed.Divert(new Path(functionName), arguments);
|
||||
_proxyDivert.isFunctionCall = true;
|
||||
AddContent (_proxyDivert);
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer (Runtime.Container container)
|
||||
{
|
||||
var foundList = story.ResolveList (name);
|
||||
|
||||
bool usingProxyDivert = false;
|
||||
|
||||
if (isChoiceCount) {
|
||||
|
||||
if (arguments.Count > 0)
|
||||
Error ("The CHOICE_COUNT() function shouldn't take any arguments");
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.ChoiceCount ());
|
||||
|
||||
} else if (isTurns) {
|
||||
|
||||
if (arguments.Count > 0)
|
||||
Error ("The TURNS() function shouldn't take any arguments");
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.Turns ());
|
||||
|
||||
} else if (isTurnsSince || isReadCount) {
|
||||
|
||||
var divertTarget = arguments [0] as DivertTarget;
|
||||
var variableDivertTarget = arguments [0] as VariableReference;
|
||||
|
||||
if (arguments.Count != 1 || (divertTarget == null && variableDivertTarget == null)) {
|
||||
Error ("The " + name + "() function should take one argument: a divert target to the target knot, stitch, gather or choice you want to check. e.g. TURNS_SINCE(-> myKnot)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (divertTarget) {
|
||||
_divertTargetToCount = divertTarget;
|
||||
AddContent (_divertTargetToCount);
|
||||
|
||||
_divertTargetToCount.GenerateIntoContainer (container);
|
||||
} else {
|
||||
_variableReferenceToCount = variableDivertTarget;
|
||||
AddContent (_variableReferenceToCount);
|
||||
|
||||
_variableReferenceToCount.GenerateIntoContainer (container);
|
||||
}
|
||||
|
||||
if (isTurnsSince)
|
||||
container.AddContent (Runtime.ControlCommand.TurnsSince ());
|
||||
else
|
||||
container.AddContent (Runtime.ControlCommand.ReadCount ());
|
||||
|
||||
} else if (isRandom) {
|
||||
if (arguments.Count != 2)
|
||||
Error ("RANDOM should take 2 parameters: a minimum and a maximum integer");
|
||||
|
||||
// We can type check single values, but not complex expressions
|
||||
for (int arg = 0; arg < arguments.Count; arg++) {
|
||||
if (arguments [arg] is Number) {
|
||||
var num = arguments [arg] as Number;
|
||||
if (!(num.value is int)) {
|
||||
string paramName = arg == 0 ? "minimum" : "maximum";
|
||||
Error ("RANDOM's " + paramName + " parameter should be an integer");
|
||||
}
|
||||
}
|
||||
|
||||
arguments [arg].GenerateIntoContainer (container);
|
||||
}
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.Random ());
|
||||
|
||||
} else if (isSeedRandom) {
|
||||
if (arguments.Count != 1)
|
||||
Error ("SEED_RANDOM should take 1 parameter - an integer seed");
|
||||
|
||||
var num = arguments [0] as Number;
|
||||
if (num && !(num.value is int)) {
|
||||
Error ("SEED_RANDOM's parameter should be an integer seed");
|
||||
}
|
||||
|
||||
arguments [0].GenerateIntoContainer (container);
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.SeedRandom ());
|
||||
|
||||
} else if (isListRange) {
|
||||
if (arguments.Count != 3)
|
||||
Error ("LIST_RANGE should take 3 parameters - a list, a min and a max");
|
||||
|
||||
for (int arg = 0; arg < arguments.Count; arg++)
|
||||
arguments [arg].GenerateIntoContainer (container);
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.ListRange ());
|
||||
|
||||
} else if( isListRandom ) {
|
||||
if (arguments.Count != 1)
|
||||
Error ("LIST_RANDOM should take 1 parameter - a list");
|
||||
|
||||
arguments [0].GenerateIntoContainer (container);
|
||||
|
||||
container.AddContent (Runtime.ControlCommand.ListRandom ());
|
||||
|
||||
} else if (Runtime.NativeFunctionCall.CallExistsWithName (name)) {
|
||||
|
||||
var nativeCall = Runtime.NativeFunctionCall.CallWithName (name);
|
||||
|
||||
if (nativeCall.numberOfParameters != arguments.Count) {
|
||||
var msg = name + " should take " + nativeCall.numberOfParameters + " parameter";
|
||||
if (nativeCall.numberOfParameters > 1)
|
||||
msg += "s";
|
||||
Error (msg);
|
||||
}
|
||||
|
||||
for (int arg = 0; arg < arguments.Count; arg++)
|
||||
arguments [arg].GenerateIntoContainer (container);
|
||||
|
||||
container.AddContent (Runtime.NativeFunctionCall.CallWithName (name));
|
||||
} else if (foundList != null) {
|
||||
if (arguments.Count > 1)
|
||||
Error ("Can currently only construct a list from one integer (or an empty list from a given list definition)");
|
||||
|
||||
// List item from given int
|
||||
if (arguments.Count == 1) {
|
||||
container.AddContent (new Runtime.StringValue (name));
|
||||
arguments [0].GenerateIntoContainer (container);
|
||||
container.AddContent (Runtime.ControlCommand.ListFromInt ());
|
||||
}
|
||||
|
||||
// Empty list with given origin.
|
||||
else {
|
||||
var list = new Runtime.InkList ();
|
||||
list.SetInitialOriginName (name);
|
||||
container.AddContent (new Runtime.ListValue (list));
|
||||
}
|
||||
}
|
||||
|
||||
// Normal function call
|
||||
else {
|
||||
container.AddContent (_proxyDivert.runtimeObject);
|
||||
usingProxyDivert = true;
|
||||
}
|
||||
|
||||
// Don't attempt to resolve as a divert if we're not doing a normal function call
|
||||
if( !usingProxyDivert ) content.Remove (_proxyDivert);
|
||||
|
||||
// Function calls that are used alone on a tilda-based line:
|
||||
// ~ func()
|
||||
// Should tidy up any returned value from the evaluation stack,
|
||||
// since it's unused.
|
||||
if (shouldPopReturnedValue)
|
||||
container.AddContent (Runtime.ControlCommand.PopEvaluatedValue ());
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
// If we aren't using the proxy divert after all (e.g. if
|
||||
// it's a native function call), but we still have arguments,
|
||||
// we need to make sure they get resolved since the proxy divert
|
||||
// is no longer in the content array.
|
||||
if (!content.Contains(_proxyDivert) && arguments != null) {
|
||||
foreach (var arg in arguments)
|
||||
arg.ResolveReferences (context);
|
||||
}
|
||||
|
||||
if( _divertTargetToCount ) {
|
||||
var divert = _divertTargetToCount.divert;
|
||||
var attemptingTurnCountOfVariableTarget = divert.runtimeDivert.variableDivertName != null;
|
||||
|
||||
if( attemptingTurnCountOfVariableTarget ) {
|
||||
Error("When getting the TURNS_SINCE() of a variable target, remove the '->' - i.e. it should just be TURNS_SINCE("+divert.runtimeDivert.variableDivertName+")");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetObject = divert.targetContent;
|
||||
if( targetObject == null ) {
|
||||
if( !attemptingTurnCountOfVariableTarget ) {
|
||||
Error("Failed to find target for TURNS_SINCE: '"+divert.target+"'");
|
||||
}
|
||||
} else {
|
||||
targetObject.containerForCounting.turnIndexShouldBeCounted = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if( _variableReferenceToCount ) {
|
||||
var runtimeVarRef = _variableReferenceToCount.runtimeVarRef;
|
||||
if( runtimeVarRef.pathForCount != null ) {
|
||||
Error("Should be "+name+"(-> "+_variableReferenceToCount.name+"). Usage without the '->' only makes sense for variable targets.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsBuiltIn(string name)
|
||||
{
|
||||
if (Runtime.NativeFunctionCall.CallExistsWithName (name))
|
||||
return true;
|
||||
|
||||
return name == "CHOICE_COUNT"
|
||||
|| name == "TURNS_SINCE"
|
||||
|| name == "TURNS"
|
||||
|| name == "RANDOM"
|
||||
|| name == "SEED_RANDOM"
|
||||
|| name == "LIST_VALUE"
|
||||
|| name == "LIST_RANDOM"
|
||||
|| name == "READ_COUNT";
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
var strArgs = string.Join (", ", arguments.ToStringsArray());
|
||||
return string.Format ("{0}({1})", name, strArgs);
|
||||
}
|
||||
|
||||
Parsed.Divert _proxyDivert;
|
||||
Parsed.DivertTarget _divertTargetToCount;
|
||||
Parsed.VariableReference _variableReferenceToCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1308e59f4c6e3430797ed5278369c5a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Gather : Parsed.Object, IWeavePoint, INamedContent
|
||||
{
|
||||
public string name
|
||||
{
|
||||
get { return identifier?.name; }
|
||||
}
|
||||
public Identifier identifier { get; set; }
|
||||
public int indentationDepth { get; protected set; }
|
||||
|
||||
public Runtime.Container runtimeContainer { get { return (Runtime.Container) runtimeObject; } }
|
||||
|
||||
public Gather (Identifier identifier, int indentationDepth)
|
||||
{
|
||||
this.identifier = identifier;
|
||||
this.indentationDepth = indentationDepth;
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var container = new Runtime.Container ();
|
||||
container.name = name;
|
||||
|
||||
if (this.story.countAllVisits) {
|
||||
container.visitsShouldBeCounted = true;
|
||||
}
|
||||
|
||||
container.countingAtStartOnly = true;
|
||||
|
||||
// A gather can have null content, e.g. it's just purely a line with "-"
|
||||
if (content != null) {
|
||||
foreach (var c in content) {
|
||||
container.AddContent (c.runtimeObject);
|
||||
}
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
if( identifier != null && identifier.name.Length > 0 )
|
||||
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.SubFlowAndWeave);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c820ea7ecd7524dfa9c467fa4e72e054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public interface INamedContent
|
||||
{
|
||||
string name { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 262d1f7783ec640e4b3aeef875166a20
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public interface IWeavePoint
|
||||
{
|
||||
int indentationDepth { get; }
|
||||
Runtime.Container runtimeContainer { get; }
|
||||
List<Parsed.Object> content { get; }
|
||||
string name { get; }
|
||||
Identifier identifier { get; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c7b0b5c8842a4c17ba25edb46903749
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Ink.Parsed {
|
||||
public class Identifier {
|
||||
public string name;
|
||||
public Runtime.DebugMetadata debugMetadata;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public static Identifier Done = new Identifier { name = "DONE", debugMetadata = null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50e035c7d009a254c8d832c3863be7b6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class IncludedFile : Parsed.Object
|
||||
{
|
||||
public Parsed.Story includedStory { get; private set; }
|
||||
|
||||
public IncludedFile (Parsed.Story includedStory)
|
||||
{
|
||||
this.includedStory = includedStory;
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
// Left to the main story to process
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9cbcf6c1e16545b2be5a72f4064d93b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Knot : FlowBase
|
||||
{
|
||||
public override FlowLevel flowLevel { get { return FlowLevel.Knot; } }
|
||||
|
||||
public Knot (Identifier name, List<Parsed.Object> topLevelObjects, List<Argument> arguments, bool isFunction) : base(name, topLevelObjects, arguments, isFunction)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
var parentStory = this.story;
|
||||
|
||||
// Enforce rule that stitches must not have the same
|
||||
// name as any knots that exist in the story
|
||||
foreach (var stitchNamePair in subFlowsByName) {
|
||||
var stitchName = stitchNamePair.Key;
|
||||
|
||||
var knotWithStitchName = parentStory.ContentWithNameAtLevel (stitchName, FlowLevel.Knot, false);
|
||||
if (knotWithStitchName) {
|
||||
var stitch = stitchNamePair.Value;
|
||||
var errorMsg = string.Format ("Stitch '{0}' has the same name as a knot (on {1})", stitch.identifier, knotWithStitchName.debugMetadata);
|
||||
Error(errorMsg, stitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 004df9f5aa1804ff1b359d44aa670019
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class List : Parsed.Expression
|
||||
{
|
||||
public List<Identifier> itemIdentifierList;
|
||||
|
||||
public List (List<Identifier> itemIdentifierList)
|
||||
{
|
||||
this.itemIdentifierList = itemIdentifierList;
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer (Runtime.Container container)
|
||||
{
|
||||
var runtimeRawList = new Runtime.InkList ();
|
||||
|
||||
if (itemIdentifierList != null) {
|
||||
foreach (var itemIdentifier in itemIdentifierList) {
|
||||
var nameParts = itemIdentifier?.name.Split ('.');
|
||||
|
||||
string listName = null;
|
||||
string listItemName = null;
|
||||
if (nameParts.Length > 1) {
|
||||
listName = nameParts [0];
|
||||
listItemName = nameParts [1];
|
||||
} else {
|
||||
listItemName = nameParts [0];
|
||||
}
|
||||
|
||||
var listItem = story.ResolveListItem (listName, listItemName, this);
|
||||
if (listItem == null) {
|
||||
if (listName == null)
|
||||
Error ("Could not find list definition that contains item '" + itemIdentifier + "'");
|
||||
else
|
||||
Error ("Could not find list item " + itemIdentifier);
|
||||
} else {
|
||||
if (listName == null)
|
||||
listName = ((ListDefinition)listItem.parent).identifier?.name;
|
||||
var item = new Runtime.InkListItem (listName, listItem.name);
|
||||
|
||||
if (runtimeRawList.ContainsKey (item))
|
||||
Warning ("Duplicate of item '"+itemIdentifier+"' in list.");
|
||||
else
|
||||
runtimeRawList [item] = listItem.seriesValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container.AddContent(new Runtime.ListValue (runtimeRawList));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 672f1903584d34411b719aea4d59f1b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class ListDefinition : Parsed.Object
|
||||
{
|
||||
public Identifier identifier;
|
||||
public List<ListElementDefinition> itemDefinitions;
|
||||
|
||||
public VariableAssignment variableAssignment;
|
||||
|
||||
public Runtime.ListDefinition runtimeListDefinition {
|
||||
get {
|
||||
var allItems = new Dictionary<string, int> ();
|
||||
foreach (var e in itemDefinitions) {
|
||||
if( !allItems.ContainsKey(e.name) )
|
||||
allItems.Add (e.name, e.seriesValue);
|
||||
else
|
||||
Error("List '"+identifier+"' contains dupicate items called '"+e.name+"'");
|
||||
}
|
||||
|
||||
return new Runtime.ListDefinition (identifier?.name, allItems);
|
||||
}
|
||||
}
|
||||
|
||||
public ListElementDefinition ItemNamed (string itemName)
|
||||
{
|
||||
if (_elementsByName == null) {
|
||||
_elementsByName = new Dictionary<string, ListElementDefinition> ();
|
||||
foreach (var el in itemDefinitions) {
|
||||
_elementsByName [el.name] = el;
|
||||
}
|
||||
}
|
||||
|
||||
ListElementDefinition foundElement;
|
||||
if (_elementsByName.TryGetValue (itemName, out foundElement))
|
||||
return foundElement;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ListDefinition (List<ListElementDefinition> elements)
|
||||
{
|
||||
this.itemDefinitions = elements;
|
||||
|
||||
int currentValue = 1;
|
||||
foreach (var e in this.itemDefinitions) {
|
||||
if (e.explicitValue != null)
|
||||
currentValue = e.explicitValue.Value;
|
||||
|
||||
e.seriesValue = currentValue;
|
||||
|
||||
currentValue++;
|
||||
}
|
||||
|
||||
AddContent (elements);
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var initialValues = new Runtime.InkList ();
|
||||
foreach (var itemDef in itemDefinitions) {
|
||||
if (itemDef.inInitialList) {
|
||||
var item = new Runtime.InkListItem (this.identifier?.name, itemDef.name);
|
||||
initialValues [item] = itemDef.seriesValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Set origin name, so
|
||||
initialValues.SetInitialOriginName (identifier?.name);
|
||||
|
||||
return new Runtime.ListValue (initialValues);
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.List);
|
||||
}
|
||||
|
||||
public override string typeName {
|
||||
get {
|
||||
return "List definition";
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, ListElementDefinition> _elementsByName;
|
||||
}
|
||||
|
||||
public class ListElementDefinition : Parsed.Object
|
||||
{
|
||||
public string name
|
||||
{
|
||||
get { return identifier?.name; }
|
||||
}
|
||||
public Identifier identifier;
|
||||
public int? explicitValue;
|
||||
public int seriesValue;
|
||||
public bool inInitialList;
|
||||
|
||||
public string fullName {
|
||||
get {
|
||||
var parentList = parent as ListDefinition;
|
||||
if (parentList == null)
|
||||
throw new System.Exception ("Can't get full name without a parent list");
|
||||
|
||||
return parentList.identifier + "." + name;
|
||||
}
|
||||
}
|
||||
|
||||
public ListElementDefinition (Identifier identifier, bool inInitialList, int? explicitValue = null)
|
||||
{
|
||||
this.identifier = identifier;
|
||||
this.inInitialList = inInitialList;
|
||||
this.explicitValue = explicitValue;
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
throw new System.NotImplementedException ();
|
||||
}
|
||||
|
||||
public override void ResolveReferences (Story context)
|
||||
{
|
||||
base.ResolveReferences (context);
|
||||
|
||||
context.CheckForNamingCollisions (this, identifier, Story.SymbolType.ListItem);
|
||||
}
|
||||
|
||||
public override string typeName {
|
||||
get {
|
||||
return "List element";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 848e06b169a60427cbf371a506af2b8d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Number : Parsed.Expression
|
||||
{
|
||||
public object value;
|
||||
|
||||
public Number(object value)
|
||||
{
|
||||
if (value is int || value is float || value is bool) {
|
||||
this.value = value;
|
||||
} else {
|
||||
throw new System.Exception ("Unexpected object type in Number");
|
||||
}
|
||||
}
|
||||
|
||||
public override void GenerateIntoContainer (Runtime.Container container)
|
||||
{
|
||||
if (value is int) {
|
||||
container.AddContent (new Runtime.IntValue ((int)value));
|
||||
} else if (value is float) {
|
||||
container.AddContent (new Runtime.FloatValue ((float)value));
|
||||
} else if(value is bool) {
|
||||
container.AddContent (new Runtime.BoolValue ((bool)value));
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (value is float) {
|
||||
return ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
} else {
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Equals override necessary in order to check for CONST multiple definition equality
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
var otherNum = obj as Number;
|
||||
if (otherNum == null) return false;
|
||||
|
||||
return this.value.Equals (otherNum.value);
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
return this.value.GetHashCode ();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 193db1f0576f340febab521eb678105e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,363 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public abstract class Object
|
||||
{
|
||||
public Runtime.DebugMetadata debugMetadata {
|
||||
get {
|
||||
if (_debugMetadata == null) {
|
||||
if (parent) {
|
||||
return parent.debugMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return _debugMetadata;
|
||||
}
|
||||
|
||||
set {
|
||||
_debugMetadata = value;
|
||||
}
|
||||
}
|
||||
private Runtime.DebugMetadata _debugMetadata;
|
||||
|
||||
public bool hasOwnDebugMetadata {
|
||||
get {
|
||||
return _debugMetadata != null;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string typeName {
|
||||
get {
|
||||
return GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
public Parsed.Object parent { get; set; }
|
||||
public List<Parsed.Object> content { get; protected set; }
|
||||
|
||||
public Parsed.Story story {
|
||||
get {
|
||||
Parsed.Object ancestor = this;
|
||||
while (ancestor.parent) {
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return ancestor as Parsed.Story;
|
||||
}
|
||||
}
|
||||
|
||||
private Runtime.Object _runtimeObject;
|
||||
public Runtime.Object runtimeObject
|
||||
{
|
||||
get {
|
||||
if (_runtimeObject == null) {
|
||||
_runtimeObject = GenerateRuntimeObject ();
|
||||
if( _runtimeObject )
|
||||
_runtimeObject.debugMetadata = debugMetadata;
|
||||
}
|
||||
return _runtimeObject;
|
||||
}
|
||||
|
||||
set {
|
||||
_runtimeObject = value;
|
||||
}
|
||||
}
|
||||
|
||||
// virtual so that certian object types can return a different
|
||||
// path than just the path to the main runtimeObject.
|
||||
// e.g. a Choice returns a path to its content rather than
|
||||
// its outer container.
|
||||
public virtual Runtime.Path runtimePath
|
||||
{
|
||||
get {
|
||||
return runtimeObject.path;
|
||||
}
|
||||
}
|
||||
|
||||
// When counting visits and turns since, different object
|
||||
// types may have different containers that needs to be counted.
|
||||
// For most it'll just be the object's main runtime object,
|
||||
// but for e.g. choices, it'll be the target container.
|
||||
public virtual Runtime.Container containerForCounting
|
||||
{
|
||||
get {
|
||||
return this.runtimeObject as Runtime.Container;
|
||||
}
|
||||
}
|
||||
|
||||
public Parsed.Path PathRelativeTo(Parsed.Object otherObj)
|
||||
{
|
||||
var ownAncestry = ancestry;
|
||||
var otherAncestry = otherObj.ancestry;
|
||||
|
||||
Parsed.Object highestCommonAncestor = null;
|
||||
int minLength = System.Math.Min (ownAncestry.Count, otherAncestry.Count);
|
||||
for (int i = 0; i < minLength; ++i) {
|
||||
var a1 = ancestry [i];
|
||||
var a2 = otherAncestry [i];
|
||||
if (a1 == a2)
|
||||
highestCommonAncestor = a1;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
FlowBase commonFlowAncestor = highestCommonAncestor as FlowBase;
|
||||
if (commonFlowAncestor == null)
|
||||
commonFlowAncestor = highestCommonAncestor.ClosestFlowBase ();
|
||||
|
||||
|
||||
var pathComponents = new List<Identifier> ();
|
||||
bool hasWeavePoint = false;
|
||||
FlowLevel baseFlow = FlowLevel.WeavePoint;
|
||||
|
||||
var ancestor = this;
|
||||
while(ancestor && (ancestor != commonFlowAncestor) && !(ancestor is Story)) {
|
||||
|
||||
if (ancestor == commonFlowAncestor)
|
||||
break;
|
||||
|
||||
if (!hasWeavePoint) {
|
||||
var weavePointAncestor = ancestor as IWeavePoint;
|
||||
if (weavePointAncestor != null && weavePointAncestor.identifier != null) {
|
||||
pathComponents.Add (weavePointAncestor.identifier);
|
||||
hasWeavePoint = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var flowAncestor = ancestor as FlowBase;
|
||||
if (flowAncestor) {
|
||||
pathComponents.Add (flowAncestor.identifier);
|
||||
baseFlow = flowAncestor.flowLevel;
|
||||
}
|
||||
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
pathComponents.Reverse ();
|
||||
|
||||
if (pathComponents.Count > 0) {
|
||||
return new Path (baseFlow, pathComponents);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Parsed.Object> ancestry
|
||||
{
|
||||
get {
|
||||
var result = new List<Parsed.Object> ();
|
||||
|
||||
var ancestor = this.parent;
|
||||
while(ancestor) {
|
||||
result.Add (ancestor);
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
result.Reverse ();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public string descriptionOfScope
|
||||
{
|
||||
get {
|
||||
var locationNames = new List<string> ();
|
||||
|
||||
Parsed.Object ancestor = this;
|
||||
while (ancestor) {
|
||||
var ancestorFlow = ancestor as FlowBase;
|
||||
if (ancestorFlow && ancestorFlow.identifier != null) {
|
||||
locationNames.Add ("'" + ancestorFlow.identifier + "'");
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
var scopeSB = new StringBuilder ();
|
||||
if (locationNames.Count > 0) {
|
||||
var locationsListStr = string.Join (", ", locationNames.ToArray());
|
||||
scopeSB.Append (locationsListStr);
|
||||
scopeSB.Append (" and ");
|
||||
}
|
||||
|
||||
scopeSB.Append ("at top scope");
|
||||
|
||||
return scopeSB.ToString ();
|
||||
}
|
||||
}
|
||||
|
||||
// Return the object so that method can be chained easily
|
||||
public T AddContent<T>(T subContent) where T : Parsed.Object
|
||||
{
|
||||
if (content == null) {
|
||||
content = new List<Parsed.Object> ();
|
||||
}
|
||||
|
||||
// Make resilient to content not existing, which can happen
|
||||
// in the case of parse errors where we've already reported
|
||||
// an error but still want a valid structure so we can
|
||||
// carry on parsing.
|
||||
if( subContent ) {
|
||||
subContent.parent = this;
|
||||
content.Add(subContent);
|
||||
}
|
||||
|
||||
return subContent;
|
||||
}
|
||||
|
||||
public void AddContent<T>(List<T> listContent) where T : Parsed.Object
|
||||
{
|
||||
foreach (var obj in listContent) {
|
||||
AddContent (obj);
|
||||
}
|
||||
}
|
||||
|
||||
public T InsertContent<T>(int index, T subContent) where T : Parsed.Object
|
||||
{
|
||||
if (content == null) {
|
||||
content = new List<Parsed.Object> ();
|
||||
}
|
||||
|
||||
subContent.parent = this;
|
||||
content.Insert (index, subContent);
|
||||
|
||||
return subContent;
|
||||
}
|
||||
|
||||
public delegate bool FindQueryFunc<T>(T obj);
|
||||
public T Find<T>(FindQueryFunc<T> queryFunc = null) where T : class
|
||||
{
|
||||
var tObj = this as T;
|
||||
if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
|
||||
return tObj;
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
return null;
|
||||
|
||||
foreach (var obj in content) {
|
||||
var nestedResult = obj.Find (queryFunc);
|
||||
if (nestedResult != null)
|
||||
return nestedResult;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public List<T> FindAll<T>(FindQueryFunc<T> queryFunc = null) where T : class
|
||||
{
|
||||
var found = new List<T> ();
|
||||
|
||||
FindAll (queryFunc, found);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void FindAll<T>(FindQueryFunc<T> queryFunc, List<T> foundSoFar) where T : class
|
||||
{
|
||||
var tObj = this as T;
|
||||
if (tObj != null && (queryFunc == null || queryFunc (tObj) == true)) {
|
||||
foundSoFar.Add (tObj);
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
foreach (var obj in content) {
|
||||
obj.FindAll (queryFunc, foundSoFar);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Runtime.Object GenerateRuntimeObject ();
|
||||
|
||||
public virtual void ResolveReferences(Story context)
|
||||
{
|
||||
if (content != null) {
|
||||
foreach(var obj in content) {
|
||||
obj.ResolveReferences (context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FlowBase ClosestFlowBase()
|
||||
{
|
||||
var ancestor = this.parent;
|
||||
while (ancestor) {
|
||||
if (ancestor is FlowBase) {
|
||||
return (FlowBase)ancestor;
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual void Error(string message, Parsed.Object source = null, bool isWarning = false)
|
||||
{
|
||||
if (source == null) {
|
||||
source = this;
|
||||
}
|
||||
|
||||
// Only allow a single parsed object to have a single error *directly* associated with it
|
||||
if (source._alreadyHadError && !isWarning) {
|
||||
return;
|
||||
}
|
||||
if (source._alreadyHadWarning && isWarning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.parent) {
|
||||
this.parent.Error (message, source, isWarning);
|
||||
} else {
|
||||
throw new System.Exception ("No parent object to send error to: "+message);
|
||||
}
|
||||
|
||||
if (isWarning) {
|
||||
source._alreadyHadWarning = true;
|
||||
} else {
|
||||
source._alreadyHadError = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void Warning(string message, Parsed.Object source = null)
|
||||
{
|
||||
Error (message, source, isWarning: true);
|
||||
}
|
||||
|
||||
// Allow implicit conversion to bool so you don't have to do:
|
||||
// if( myObj != null ) ...
|
||||
public static implicit operator bool (Object obj)
|
||||
{
|
||||
var isNull = object.ReferenceEquals (obj, null);
|
||||
return !isNull;
|
||||
}
|
||||
|
||||
public static bool operator ==(Object a, Object b)
|
||||
{
|
||||
return object.ReferenceEquals (a, b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Object a, Object b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
return object.ReferenceEquals (obj, this);
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
return base.GetHashCode ();
|
||||
}
|
||||
|
||||
bool _alreadyHadError;
|
||||
bool _alreadyHadWarning;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfde668c261f944e08e08a5b097043ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Path
|
||||
{
|
||||
public FlowLevel baseTargetLevel {
|
||||
get {
|
||||
if (baseLevelIsAmbiguous)
|
||||
return FlowLevel.Story;
|
||||
else
|
||||
return (FlowLevel) _baseTargetLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public bool baseLevelIsAmbiguous {
|
||||
get {
|
||||
return _baseTargetLevel == null;
|
||||
}
|
||||
}
|
||||
|
||||
public string firstComponent {
|
||||
get {
|
||||
if (components == null || components.Count == 0)
|
||||
return null;
|
||||
|
||||
return components [0].name;
|
||||
}
|
||||
}
|
||||
|
||||
public int numberOfComponents {
|
||||
get {
|
||||
return components.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public string dotSeparatedComponents {
|
||||
get {
|
||||
if( _dotSeparatedComponents == null ) {
|
||||
_dotSeparatedComponents = string.Join(".", components.Select(c => c?.name));
|
||||
}
|
||||
|
||||
return _dotSeparatedComponents;
|
||||
}
|
||||
}
|
||||
string _dotSeparatedComponents;
|
||||
|
||||
public List<Identifier> components { get; }
|
||||
|
||||
public Path(FlowLevel baseFlowLevel, List<Identifier> components)
|
||||
{
|
||||
_baseTargetLevel = baseFlowLevel;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public Path(List<Identifier> components)
|
||||
{
|
||||
_baseTargetLevel = null;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
public Path(Identifier ambiguousName)
|
||||
{
|
||||
_baseTargetLevel = null;
|
||||
components = new List<Identifier> ();
|
||||
components.Add (ambiguousName);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (components == null || components.Count == 0) {
|
||||
if (baseTargetLevel == FlowLevel.WeavePoint)
|
||||
return "-> <next gather point>";
|
||||
else
|
||||
return "<invalid Path>";
|
||||
}
|
||||
|
||||
return "-> " + dotSeparatedComponents;
|
||||
}
|
||||
|
||||
public Parsed.Object ResolveFromContext(Parsed.Object context)
|
||||
{
|
||||
if (components == null || components.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find base target of path from current context. e.g.
|
||||
// ==> BASE.sub.sub
|
||||
var baseTargetObject = ResolveBaseTarget (context);
|
||||
if (baseTargetObject == null) {
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
// Given base of path, resolve final target by working deeper into hierarchy
|
||||
// e.g. ==> base.mid.FINAL
|
||||
if (components.Count > 1) {
|
||||
return ResolveTailComponents (baseTargetObject);
|
||||
}
|
||||
|
||||
return baseTargetObject;
|
||||
}
|
||||
|
||||
// Find the root object from the base, i.e. root from:
|
||||
// root.sub1.sub2
|
||||
Parsed.Object ResolveBaseTarget(Parsed.Object originalContext)
|
||||
{
|
||||
var firstComp = firstComponent;
|
||||
|
||||
// Work up the ancestry to find the node that has the named object
|
||||
Parsed.Object ancestorContext = originalContext;
|
||||
while (ancestorContext != null) {
|
||||
|
||||
// Only allow deep search when searching deeper from original context.
|
||||
// Don't allow search upward *then* downward, since that's searching *everywhere*!
|
||||
// Allowed examples:
|
||||
// - From an inner gather of a stitch, you should search up to find a knot called 'x'
|
||||
// at the root of a story, but not a stitch called 'x' in that knot.
|
||||
// - However, from within a knot, you should be able to find a gather/choice
|
||||
// anywhere called 'x'
|
||||
// (that latter example is quite loose, but we allow it)
|
||||
bool deepSearch = ancestorContext == originalContext;
|
||||
|
||||
var foundBase = TryGetChildFromContext (ancestorContext, firstComp, null, deepSearch);
|
||||
if (foundBase != null)
|
||||
return foundBase;
|
||||
|
||||
ancestorContext = ancestorContext.parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the final child from path given root, i.e.:
|
||||
// root.sub.finalChild
|
||||
Parsed.Object ResolveTailComponents(Parsed.Object rootTarget)
|
||||
{
|
||||
Parsed.Object foundComponent = rootTarget;
|
||||
for (int i = 1; i < components.Count; ++i) {
|
||||
var compName = components [i].name;
|
||||
|
||||
FlowLevel minimumExpectedLevel;
|
||||
var foundFlow = foundComponent as FlowBase;
|
||||
if (foundFlow != null)
|
||||
minimumExpectedLevel = (FlowLevel)(foundFlow.flowLevel + 1);
|
||||
else
|
||||
minimumExpectedLevel = FlowLevel.WeavePoint;
|
||||
|
||||
|
||||
foundComponent = TryGetChildFromContext (foundComponent, compName, minimumExpectedLevel);
|
||||
if (foundComponent == null)
|
||||
break;
|
||||
}
|
||||
|
||||
return foundComponent;
|
||||
}
|
||||
|
||||
// See whether "context" contains a child with a given name at a given flow level
|
||||
// Can either be a named knot/stitch (a FlowBase) or a weave point within a Weave (Choice or Gather)
|
||||
// This function also ignores any other object types that are neither FlowBase nor Weave.
|
||||
// Called from both ResolveBase (force deep) and ResolveTail for the individual components.
|
||||
Parsed.Object TryGetChildFromContext(Parsed.Object context, string childName, FlowLevel? minimumLevel, bool forceDeepSearch = false)
|
||||
{
|
||||
// null childLevel means that we don't know where to find it
|
||||
bool ambiguousChildLevel = minimumLevel == null;
|
||||
|
||||
// Search for WeavePoint within Weave
|
||||
var weaveContext = context as Weave;
|
||||
if ( weaveContext != null && (ambiguousChildLevel || minimumLevel == FlowLevel.WeavePoint)) {
|
||||
return (Parsed.Object) weaveContext.WeavePointNamed (childName);
|
||||
}
|
||||
|
||||
// Search for content within Flow (either a sub-Flow or a WeavePoint)
|
||||
var flowContext = context as FlowBase;
|
||||
if (flowContext != null) {
|
||||
|
||||
// When searching within a Knot, allow a deep searches so that
|
||||
// named weave points (choices and gathers) can be found within any stitch
|
||||
// Otherwise, we just search within the immediate object.
|
||||
var shouldDeepSearch = forceDeepSearch || flowContext.flowLevel == FlowLevel.Knot;
|
||||
return flowContext.ContentWithNameAtLevel (childName, minimumLevel, shouldDeepSearch);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
FlowLevel? _baseTargetLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc7b7f981067b4ab7ac3fd451c958785
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace Ink.Parsed
|
||||
{
|
||||
public class Return : Parsed.Object
|
||||
{
|
||||
public Expression returnedExpression { get; protected set; }
|
||||
|
||||
public Return (Expression returnedExpression = null)
|
||||
{
|
||||
if (returnedExpression) {
|
||||
this.returnedExpression = AddContent(returnedExpression);
|
||||
}
|
||||
}
|
||||
|
||||
public override Runtime.Object GenerateRuntimeObject ()
|
||||
{
|
||||
var container = new Runtime.Container ();
|
||||
|
||||
// Evaluate expression
|
||||
if (returnedExpression) {
|
||||
container.AddContent (returnedExpression.runtimeObject);
|
||||
}
|
||||
|
||||
// Return Runtime.Void when there's no expression to evaluate
|
||||
// (This evaluation will just add the Void object to the evaluation stack)
|
||||
else {
|
||||
container.AddContent (Runtime.ControlCommand.EvalStart ());
|
||||
container.AddContent (new Runtime.Void());
|
||||
container.AddContent (Runtime.ControlCommand.EvalEnd ());
|
||||
}
|
||||
|
||||
// Then pop the call stack
|
||||
// (the evaluated expression will leave the return value on the evaluation stack)
|
||||
container.AddContent (Runtime.ControlCommand.PopFunction());
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4af7a89511e7e44b1b38d2bdafdeaad1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c69e88da4e89a46e7a7f0ae08b5b08e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc4bd341820aa4690b360e54951da250
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ea84180e05064c52bb98a05546a7ddb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0fdc424d743642aeae05b3b736e4bfb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5cce79a72f1f94786a6bb0297db32376
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73a38744ac93c440794944efec87b970
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6399748d4c08492e863106c86944abf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df944176b643540b3b7c9a1bb01d3780
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94f6b0e0f70d94e05b4292888083b3c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01c402ee24cb74892a82fb28f502dfac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user