Files
Mementos/Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkCompiler/ParsedHierarchy/Weave.cs
2021-06-30 21:39:19 +10:00

731 lines
28 KiB
C#

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;
}
}