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

440 lines
17 KiB
C#

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