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; } }
///
/// 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.
///
internal bool hadError { get { return _hadError; } }
internal bool hadWarning { get { return _hadWarning; } }
public Dictionary constants;
public Dictionary 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 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 topLevelContent)
{
var flowsFromOtherFiles = new List ();
// 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 ();
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 ();
foreach (var constDecl in FindAll ()) {
// 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 ();
foreach (var listDef in FindAll ()) {
_listDefs [listDef.identifier?.name] = listDef;
}
externals = new Dictionary ();
// 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 ();
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 ();
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 _dontFlattenContainers = new HashSet();
Dictionary _listDefs;
}
}