Newer
Older
TheVengeance-Project-IADE-Unity2D / Assets / Ink / InkLibs / InkRuntime / StoryState.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.IO;

namespace Ink.Runtime
{
    /// <summary>
    /// All story state information is included in the StoryState class,
    /// including global variables, read counts, the pointer to the current
    /// point in the story, the call stack (for tunnels, functions, etc),
    /// and a few other smaller bits and pieces. You can save the current
    /// state using the json serialisation functions ToJson and LoadJson.
    /// </summary>
    public class StoryState
    {
        /// <summary>
        /// The current version of the state save file JSON-based format.
        /// </summary>
        //
        // Backward compatible changes since v8:
        // v10: dynamic tags
        // v9:  multi-flows
        public const int kInkSaveStateVersion = 10;
        const int kMinCompatibleLoadVersion = 8;

        /// <summary>
        /// Callback for when a state is loaded
        /// </summary>
        public event Action onDidLoadState;

        /// <summary>
        /// Exports the current state to json format, in order to save the game.
        /// </summary>
        /// <returns>The save state in json format.</returns>
        public string ToJson() {
            var writer = new SimpleJson.Writer();
            WriteJson(writer);
            return writer.ToString();
        }

        /// <summary>
        /// Exports the current state to json format, in order to save the game.
        /// For this overload you can pass in a custom stream, such as a FileStream.
        /// </summary>
        public void ToJson(Stream stream) {
            var writer = new SimpleJson.Writer(stream);
            WriteJson(writer);
        }

        /// <summary>
        /// Loads a previously saved state in JSON format.
        /// </summary>
        /// <param name="json">The JSON string to load.</param>
        public void LoadJson(string json)
        {
            var jObject = SimpleJson.TextToDictionary (json);
            LoadJsonObj(jObject);
            if(onDidLoadState != null) onDidLoadState();
        }

        /// <summary>
        /// Gets the visit/read count of a particular Container at the given path.
        /// For a knot or stitch, that path string will be in the form:
        /// 
        ///     knot
        ///     knot.stitch
        /// 
        /// </summary>
        /// <returns>The number of times the specific knot or stitch has
        /// been enountered by the ink engine.</returns>
        /// <param name="pathString">The dot-separated path string of
        /// the specific knot or stitch.</param>
        public int VisitCountAtPathString(string pathString)
        {
            int visitCountOut;

            if ( _patch != null ) {
                var container = story.ContentAtPath(new Path(pathString)).container;
                if (container == null)
                    throw new Exception("Content at path not found: " + pathString);

                if( _patch.TryGetVisitCount(container, out visitCountOut) )
                    return visitCountOut;
            }

            if (_visitCounts.TryGetValue(pathString, out visitCountOut))
                return visitCountOut;

            return 0;
        }

        public int VisitCountForContainer(Container container)
        {
            if (!container.visitsShouldBeCounted)
            {
                story.Error("Read count for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
                return 0;
            }

            int count = 0;
            if (_patch != null && _patch.TryGetVisitCount(container, out count))
                return count;
                
            var containerPathStr = container.path.ToString();
            _visitCounts.TryGetValue(containerPathStr, out count);
            return count;
        }

        public void IncrementVisitCountForContainer(Container container)
        {
            if( _patch != null ) {
                var currCount = VisitCountForContainer(container);
                currCount++;
                _patch.SetVisitCount(container, currCount);
                return;
            }

            int count = 0;
            var containerPathStr = container.path.ToString();
            _visitCounts.TryGetValue(containerPathStr, out count);
            count++;
            _visitCounts[containerPathStr] = count;
        }

        public void RecordTurnIndexVisitToContainer(Container container)
        {
            if( _patch != null ) {
                _patch.SetTurnIndex(container, currentTurnIndex);
                return;
            }

            var containerPathStr = container.path.ToString();
            _turnIndices[containerPathStr] = currentTurnIndex;
        }

        public int TurnsSinceForContainer(Container container)
        {
            if (!container.turnIndexShouldBeCounted)
            {
                story.Error("TURNS_SINCE() for target (" + container.name + " - on " + container.debugMetadata + ") unknown.");
            }

            int index = 0;

            if ( _patch != null && _patch.TryGetTurnIndex(container, out index) ) {
                return currentTurnIndex - index;
            }

            var containerPathStr = container.path.ToString();
            if (_turnIndices.TryGetValue(containerPathStr, out index))
            {
                return currentTurnIndex - index;
            }
            else
            {
                return -1;
            }
        }

        public int callstackDepth {
			get {
				return callStack.depth;
			}
		}

        // REMEMBER! REMEMBER! REMEMBER!
        // When adding state, update the Copy method, and serialisation.
        // REMEMBER! REMEMBER! REMEMBER!

        public List<Runtime.Object> outputStream { 
            get { 
                return _currentFlow.outputStream; 
            } 
        }

        

		public List<Choice> currentChoices { 
			get { 
				// If we can continue generating text content rather than choices,
				// then we reflect the choice list as being empty, since choices
				// should always come at the end.
				if( canContinue ) return new List<Choice>();
				return _currentFlow.currentChoices;
			} 
		}
		public List<Choice> generatedChoices {
			get {
				return _currentFlow.currentChoices;
			}
		}

        // TODO: Consider removing currentErrors / currentWarnings altogether
        // and relying on client error handler code immediately handling StoryExceptions etc
        // Or is there a specific reason we need to collect potentially multiple
        // errors before throwing/exiting?
        public List<string> currentErrors { get; private set; }
        public List<string> currentWarnings { get; private set; }
        public VariablesState variablesState { get; private set; }
        public CallStack callStack { 
            get { 
                return _currentFlow.callStack;
            }
            // set {
            //     _currentFlow.callStack = value;
            // } 
        }

        public List<Runtime.Object> evaluationStack { get; private set; }
        public Pointer divertedPointer { get; set; }

        public int currentTurnIndex { get; private set; }
        public int storySeed { get; set; }
        public int previousRandom { get; set; }
        public bool didSafeExit { get; set; }

        public Story story { get; set; }

        /// <summary>
        /// String representation of the location where the story currently is.
        /// </summary>
        public string currentPathString {
            get {
                var pointer = currentPointer;
                if (pointer.isNull)
                    return null;
                else
                    return pointer.path.ToString();
            }
        }

        public Runtime.Pointer currentPointer {
            get {
                return callStack.currentElement.currentPointer;
            }
            set {
                callStack.currentElement.currentPointer = value;
            }
        }

        public Pointer previousPointer { 
            get {
                return callStack.currentThread.previousPointer;
            }
            set {
                callStack.currentThread.previousPointer = value;
            }
        }

		public bool canContinue {
			get {
				return !currentPointer.isNull && !hasError;
			}
		}
            
        public bool hasError
        {
            get {
                return currentErrors != null && currentErrors.Count > 0;
            }
        }

        public bool hasWarning {
            get {
                return currentWarnings != null && currentWarnings.Count > 0;
            }
        }

        public string currentText
        {
            get 
            {
				if( _outputStreamTextDirty ) {
					var sb = new StringBuilder ();

                    bool inTag = false;
					foreach (var outputObj in outputStream) {
						var textContent = outputObj as StringValue;
						if (!inTag && textContent != null) {
							sb.Append(textContent.value);
						} else {
                            var controlCommand = outputObj as ControlCommand;
                            if( controlCommand != null ) {
                                if( controlCommand.commandType == ControlCommand.CommandType.BeginTag ) {
                                    inTag = true;
                                } else if( controlCommand.commandType == ControlCommand.CommandType.EndTag ) {
                                    inTag = false;
                                }
                            }
                        }
					}

                    _currentText = CleanOutputWhitespace (sb.ToString ());

					_outputStreamTextDirty = false;
				}

				return _currentText;
            }
        }
		string _currentText;

        // Cleans inline whitespace in the following way:
        //  - Removes all whitespace from the start and end of line (including just before a \n)
        //  - Turns all consecutive space and tab runs into single spaces (HTML style)
        public string CleanOutputWhitespace(string str)
        {
            var sb = new StringBuilder(str.Length);

            int currentWhitespaceStart = -1;
            int startOfLine = 0;

            for (int i = 0; i < str.Length; i++) {
                var c = str[i];

                bool isInlineWhitespace = c == ' ' || c == '\t';

                if (isInlineWhitespace && currentWhitespaceStart == -1)
                    currentWhitespaceStart = i;

                if (!isInlineWhitespace) {
                    if (c != '\n' && currentWhitespaceStart > 0 && currentWhitespaceStart != startOfLine) {
                        sb.Append(' ');
                    }
                    currentWhitespaceStart = -1;
                }

                if (c == '\n')
                    startOfLine = i + 1;

                if (!isInlineWhitespace)
                    sb.Append(c);
            }

            return sb.ToString();
        }

        public List<string> currentTags 
        {
            get 
            {
				if( _outputStreamTagsDirty ) {
					_currentTags = new List<string>();

                    bool inTag = false;
                    var sb = new StringBuilder ();

					foreach (var outputObj in outputStream) {
                        var controlCommand = outputObj as ControlCommand;

                        if( controlCommand != null ) {
                            if( controlCommand.commandType == ControlCommand.CommandType.BeginTag ) {
                                if( inTag && sb.Length > 0 ) {
                                    var txt = CleanOutputWhitespace(sb.ToString());
                                    _currentTags.Add(txt);
                                    sb.Clear();
                                }
                                inTag = true;
                            }

                            else if( controlCommand.commandType == ControlCommand.CommandType.EndTag ) {
                                if( sb.Length > 0 ) {
                                    var txt = CleanOutputWhitespace(sb.ToString());
                                    _currentTags.Add(txt);
                                    sb.Clear();
                                }
                                inTag = false;
                            }
                        }

                        else if( inTag ) {
                            var strVal = outputObj as StringValue;
                            if( strVal != null ) {
                                sb.Append(strVal.value);
                            }
                        }

                        else {
                            var tag = outputObj as Tag;
                            if (tag != null && tag.text != null && tag.text.Length > 0) {
                                _currentTags.Add (tag.text); // tag.text has whitespae already cleaned
                            }
                        }

					}

                    if( sb.Length > 0 ) {
                        var txt = CleanOutputWhitespace(sb.ToString());
                        _currentTags.Add(txt);
                        sb.Clear();
                    }

					_outputStreamTagsDirty = false;
				}

				return _currentTags;
            }
        }
		List<string> _currentTags;

        public string currentFlowName {
            get {
                return _currentFlow.name;
            }
        }

        public bool currentFlowIsDefaultFlow {
            get {
                return _currentFlow.name == kDefaultFlowName;
            }
        }

        public List<string> aliveFlowNames {
            get {

                if( _aliveFlowNamesDirty ) {
					_aliveFlowNames = new List<string>();

                    if (_namedFlows != null)
                    {
                        foreach (string flowName in _namedFlows.Keys) {
                            if (flowName != kDefaultFlowName) {
                                _aliveFlowNames.Add(flowName);
                            }
                        }
                    }

					_aliveFlowNamesDirty = false;
				}

				return _aliveFlowNames;
            }
        }

        List<string> _aliveFlowNames;

        public bool inExpressionEvaluation {
            get {
                return callStack.currentElement.inExpressionEvaluation;
            }
            set {
                callStack.currentElement.inExpressionEvaluation = value;
            }
        }
            
        public StoryState (Story story)
        {
            this.story = story;

            _currentFlow = new Flow(kDefaultFlowName, story);
            
			OutputStreamDirty();
            _aliveFlowNamesDirty = true;

            evaluationStack = new List<Runtime.Object> ();

            variablesState = new VariablesState (callStack, story.listDefinitions);

            _visitCounts = new Dictionary<string, int> ();
            _turnIndices = new Dictionary<string, int> ();

            currentTurnIndex = -1;

            // Seed the shuffle random numbers
            int timeSeed = DateTime.Now.Millisecond;
            storySeed = (new Random (timeSeed)).Next () % 100;
            previousRandom = 0;

			

            GoToStart();
        }

        public void GoToStart()
        {
            callStack.currentElement.currentPointer = Pointer.StartOf (story.mainContentContainer);
        }

        internal void SwitchFlow_Internal(string flowName)
        {
            if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.SwitchFlow");
            
            if( _namedFlows == null ) {
                _namedFlows = new Dictionary<string, Flow>();
                _namedFlows[kDefaultFlowName] = _currentFlow;
            }

            if( flowName == _currentFlow.name ) {
                return;
            }

            Flow flow;
            if( !_namedFlows.TryGetValue(flowName, out flow) ) {
                flow = new Flow(flowName, story);
                _namedFlows[flowName] = flow;
                _aliveFlowNamesDirty = true;
            }

            _currentFlow = flow;
            variablesState.callStack = _currentFlow.callStack;

            // Cause text to be regenerated from output stream if necessary
            OutputStreamDirty();
        }

        internal void SwitchToDefaultFlow_Internal()
        {
            if( _namedFlows == null ) return;
            SwitchFlow_Internal(kDefaultFlowName);
        }

        internal void RemoveFlow_Internal(string flowName)
        {
            if(flowName == null) throw new System.Exception("Must pass a non-null string to Story.DestroyFlow");
            if(flowName == kDefaultFlowName) throw new System.Exception("Cannot destroy default flow");

            // If we're currently in the flow that's being removed, switch back to default
            if( _currentFlow.name == flowName ) {
                SwitchToDefaultFlow_Internal();
            }

            _namedFlows.Remove(flowName);
            _aliveFlowNamesDirty = true;
        }

        // Warning: Any Runtime.Object content referenced within the StoryState will
        // be re-referenced rather than cloned. This is generally okay though since
        // Runtime.Objects are treated as immutable after they've been set up.
        // (e.g. we don't edit a Runtime.StringValue after it's been created an added.)
        // I wonder if there's a sensible way to enforce that..??
        public StoryState CopyAndStartPatching()
        {
            var copy = new StoryState(story);

            copy._patch = new StatePatch(_patch);

            // Hijack the new default flow to become a copy of our current one
            // If the patch is applied, then this new flow will replace the old one in _namedFlows
            copy._currentFlow.name = _currentFlow.name;
            copy._currentFlow.callStack = new CallStack (_currentFlow.callStack);
            copy._currentFlow.currentChoices.AddRange(_currentFlow.currentChoices);
            copy._currentFlow.outputStream.AddRange(_currentFlow.outputStream);
            copy.OutputStreamDirty();

            // The copy of the state has its own copy of the named flows dictionary,
            // except with the current flow replaced with the copy above
            // (Assuming we're in multi-flow mode at all. If we're not then
            // the above copy is simply the default flow copy and we're done)
            if( _namedFlows != null ) {
                copy._namedFlows = new Dictionary<string, Flow>();
                foreach(var namedFlow in _namedFlows)
                    copy._namedFlows[namedFlow.Key] = namedFlow.Value;
                copy._namedFlows[_currentFlow.name] = copy._currentFlow;
                copy._aliveFlowNamesDirty = true;
            }

            if (hasError) {
                copy.currentErrors = new List<string> ();
                copy.currentErrors.AddRange (currentErrors); 
            }
            if (hasWarning) {
                copy.currentWarnings = new List<string> ();
                copy.currentWarnings.AddRange (currentWarnings); 
            }

            
            // ref copy - exactly the same variables state!
            // we're expecting not to read it only while in patch mode
            // (though the callstack will be modified)
            copy.variablesState = variablesState;
            copy.variablesState.callStack = copy.callStack;
            copy.variablesState.patch = copy._patch;

            copy.evaluationStack.AddRange (evaluationStack);

            if (!divertedPointer.isNull)
                copy.divertedPointer = divertedPointer;

            copy.previousPointer = previousPointer;

            // visit counts and turn indicies will be read only, not modified
            // while in patch mode
            copy._visitCounts = _visitCounts;
            copy._turnIndices = _turnIndices;

            copy.currentTurnIndex = currentTurnIndex;
            copy.storySeed = storySeed;
            copy.previousRandom = previousRandom;

            copy.didSafeExit = didSafeExit;

            return copy;
        }

        public void RestoreAfterPatch()
        {
            // VariablesState was being borrowed by the patched
            // state, so restore it with our own callstack.
            // _patch will be null normally, but if you're in the
            // middle of a save, it may contain a _patch for save purpsoes.
            variablesState.callStack = callStack;
            variablesState.patch = _patch; // usually null
        }

        public void ApplyAnyPatch()
        {
            if (_patch == null) return;

            variablesState.ApplyPatch();

            foreach(var pathToCount in _patch.visitCounts)
                ApplyCountChanges(pathToCount.Key, pathToCount.Value, isVisit:true);

            foreach (var pathToIndex in _patch.turnIndices)
                ApplyCountChanges(pathToIndex.Key, pathToIndex.Value, isVisit:false);

            _patch = null;
        }

        void ApplyCountChanges(Container container, int newCount, bool isVisit)
        {
            var counts = isVisit ? _visitCounts : _turnIndices;
            counts[container.path.ToString()] = newCount;
        }

        void WriteJson(SimpleJson.Writer writer)
        {
            writer.WriteObjectStart();

            // Flows
            writer.WritePropertyStart("flows");
            writer.WriteObjectStart();

            // Multi-flow
            if( _namedFlows != null ) {
                foreach(var namedFlow in _namedFlows) {
                    writer.WriteProperty(namedFlow.Key, namedFlow.Value.WriteJson);
                }
            } 
            
            // Single flow
            else {
                writer.WriteProperty(_currentFlow.name, _currentFlow.WriteJson);
            }

            writer.WriteObjectEnd();
            writer.WritePropertyEnd(); // end of flows

            writer.WriteProperty("currentFlowName", _currentFlow.name);

            writer.WriteProperty("variablesState", variablesState.WriteJson);

            writer.WriteProperty("evalStack", w => Json.WriteListRuntimeObjs(w, evaluationStack));


            if (!divertedPointer.isNull)
                writer.WriteProperty("currentDivertTarget", divertedPointer.path.componentsString);
                
            writer.WriteProperty("visitCounts", w => Json.WriteIntDictionary(w, _visitCounts));
            writer.WriteProperty("turnIndices", w => Json.WriteIntDictionary(w, _turnIndices));


            writer.WriteProperty("turnIdx", currentTurnIndex);
            writer.WriteProperty("storySeed", storySeed);
            writer.WriteProperty("previousRandom", previousRandom);

            writer.WriteProperty("inkSaveVersion", kInkSaveStateVersion);

            // Not using this right now, but could do in future.
            writer.WriteProperty("inkFormatVersion", Story.inkVersionCurrent);

            writer.WriteObjectEnd();
        }


        void LoadJsonObj(Dictionary<string, object> jObject)
        {
			object jSaveVersion = null;
			if (!jObject.TryGetValue("inkSaveVersion", out jSaveVersion)) {
                throw new Exception ("ink save format incorrect, can't load.");
            }
            else if ((int)jSaveVersion < kMinCompatibleLoadVersion) {
                throw new Exception("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+kMinCompatibleLoadVersion+"), so can't load.");
            }

            // Flows: Always exists in latest format (even if there's just one default)
            // but this dictionary doesn't exist in prev format
            object flowsObj = null;
            if (jObject.TryGetValue("flows", out flowsObj)) {
                var flowsObjDict = (Dictionary<string, object>)flowsObj;
                
                // Single default flow
                if( flowsObjDict.Count == 1 )
                    _namedFlows = null;

                // Multi-flow, need to create flows dict
                else if( _namedFlows == null )
                    _namedFlows = new Dictionary<string, Flow>();

                // Multi-flow, already have a flows dict
                else
                    _namedFlows.Clear();

                // Load up each flow (there may only be one)
                foreach(var namedFlowObj in flowsObjDict) {
                    var name = namedFlowObj.Key;
                    var flowObj = (Dictionary<string, object>)namedFlowObj.Value;

                    // Load up this flow using JSON data
                    var flow = new Flow(name, story, flowObj);

                    if( flowsObjDict.Count == 1 ) {
                        _currentFlow = new Flow(name, story, flowObj);
                    } else {
                        _namedFlows[name] = flow;
                    }
                }

                if( _namedFlows != null && _namedFlows.Count > 1 ) {
                    var currFlowName = (string)jObject["currentFlowName"];
                    _currentFlow = _namedFlows[currFlowName];
                }
            }

            // Old format: individually load up callstack, output stream, choices in current/default flow
            else {
                _namedFlows = null;
                _currentFlow.name = kDefaultFlowName;
                _currentFlow.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstackThreads"], story);
                _currentFlow.outputStream = Json.JArrayToRuntimeObjList ((List<object>)jObject ["outputStream"]);
                _currentFlow.currentChoices = Json.JArrayToRuntimeObjList<Choice>((List<object>)jObject ["currentChoices"]);

                object jChoiceThreadsObj = null;
                jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj);
                _currentFlow.LoadFlowChoiceThreads((Dictionary<string, object>)jChoiceThreadsObj, story);
            }

            OutputStreamDirty();
            _aliveFlowNamesDirty = true;

            variablesState.SetJsonToken((Dictionary < string, object> )jObject["variablesState"]);
            variablesState.callStack = _currentFlow.callStack;

            evaluationStack = Json.JArrayToRuntimeObjList ((List<object>)jObject ["evalStack"]);


			object currentDivertTargetPath;
			if (jObject.TryGetValue("currentDivertTarget", out currentDivertTargetPath)) {
                var divertPath = new Path (currentDivertTargetPath.ToString ());
                divertedPointer = story.PointerAtPath (divertPath);
            }
                
            _visitCounts = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["visitCounts"]);
            _turnIndices = Json.JObjectToIntDictionary((Dictionary<string, object>)jObject["turnIndices"]);

            currentTurnIndex = (int)jObject ["turnIdx"];
            storySeed = (int)jObject ["storySeed"];

            // Not optional, but bug in inkjs means it's actually missing in inkjs saves
            object previousRandomObj = null;
            if( jObject.TryGetValue("previousRandom", out previousRandomObj) ) {
                previousRandom = (int)previousRandomObj;
            } else {
                previousRandom = 0;
            }
        }
            
        public void ResetErrors()
        {
            currentErrors = null;
            currentWarnings = null;
        }
            
        public void ResetOutput(List<Runtime.Object> objs = null)
        {
            outputStream.Clear ();
            if( objs != null ) outputStream.AddRange (objs);
			OutputStreamDirty();
        }

        // Push to output stream, but split out newlines in text for consistency
        // in dealing with them later.
        public void PushToOutputStream(Runtime.Object obj)
        {
            var text = obj as StringValue;
            if (text) {
                var listText = TrySplittingHeadTailWhitespace (text);
                if (listText != null) {
                    foreach (var textObj in listText) {
                        PushToOutputStreamIndividual (textObj);
                    }
                    OutputStreamDirty();
                    return;
                }
            }

            PushToOutputStreamIndividual (obj);

			OutputStreamDirty();
        }

        public void PopFromOutputStream (int count)
        {
            outputStream.RemoveRange (outputStream.Count - count, count);
            OutputStreamDirty ();
        }


        // At both the start and the end of the string, split out the new lines like so:
        //
        //  "   \n  \n     \n  the string \n is awesome \n     \n     "
        //      ^-----------^                           ^-------^
        // 
        // Excess newlines are converted into single newlines, and spaces discarded.
        // Outside spaces are significant and retained. "Interior" newlines within 
        // the main string are ignored, since this is for the purpose of gluing only.
        //
        //  - If no splitting is necessary, null is returned.
        //  - A newline on its own is returned in a list for consistency.
        List<Runtime.StringValue> TrySplittingHeadTailWhitespace(Runtime.StringValue single)
        {
            string str = single.value;

            int headFirstNewlineIdx = -1;
            int headLastNewlineIdx = -1;
            for (int i = 0; i < str.Length; i++) {
                char c = str [i];
                if (c == '\n') {
                    if (headFirstNewlineIdx == -1)
                        headFirstNewlineIdx = i;
                    headLastNewlineIdx = i;
                }
                else if (c == ' ' || c == '\t')
                    continue;
                else
                    break;
            }

            int tailLastNewlineIdx = -1;
            int tailFirstNewlineIdx = -1;
            for (int i = str.Length-1; i >= 0; i--) {
                char c = str [i];
                if (c == '\n') {
                    if (tailLastNewlineIdx == -1)
                        tailLastNewlineIdx = i;
                    tailFirstNewlineIdx = i;
                }
                else if (c == ' ' || c == '\t')
                    continue;
                else
                    break;
            }

            // No splitting to be done?
            if (headFirstNewlineIdx == -1 && tailLastNewlineIdx == -1)
                return null;
                
            var listTexts = new List<Runtime.StringValue> ();
            int innerStrStart = 0;
            int innerStrEnd = str.Length;

            if (headFirstNewlineIdx != -1) {
                if (headFirstNewlineIdx > 0) {
                    var leadingSpaces = new StringValue (str.Substring (0, headFirstNewlineIdx));
                    listTexts.Add(leadingSpaces);
                }
                listTexts.Add (new StringValue ("\n"));
                innerStrStart = headLastNewlineIdx + 1;
            }

            if (tailLastNewlineIdx != -1) {
                innerStrEnd = tailFirstNewlineIdx;
            }

            if (innerStrEnd > innerStrStart) {
                var innerStrText = str.Substring (innerStrStart, innerStrEnd - innerStrStart);
                listTexts.Add (new StringValue (innerStrText));
            }

            if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) {
                listTexts.Add (new StringValue ("\n"));
                if (tailLastNewlineIdx < str.Length - 1) {
                    int numSpaces = (str.Length - tailLastNewlineIdx) - 1;
                    var trailingSpaces = new StringValue (str.Substring (tailLastNewlineIdx + 1, numSpaces));
                    listTexts.Add(trailingSpaces);
                }
            }

            return listTexts;
        }

        void PushToOutputStreamIndividual(Runtime.Object obj)
        {
            var glue = obj as Runtime.Glue;
            var text = obj as Runtime.StringValue;

            bool includeInOutput = true;

            // New glue, so chomp away any whitespace from the end of the stream
            if (glue) {
                TrimNewlinesFromOutputStream();
                includeInOutput = true;
            }

            // New text: do we really want to append it, if it's whitespace?
            // Two different reasons for whitespace to be thrown away:
            //   - Function start/end trimming
            //   - User defined glue: <>
            // We also need to know when to stop trimming, when there's non-whitespace.
            else if( text ) {

                // Where does the current function call begin?
                var functionTrimIndex = -1;
                var currEl = callStack.currentElement;
                if (currEl.type == PushPopType.Function) {
                    functionTrimIndex = currEl.functionStartInOuputStream;
                }

                // Do 2 things:
                //  - Find latest glue
                //  - Check whether we're in the middle of string evaluation
                // If we're in string eval within the current function, we
                // don't want to trim back further than the length of the current string.
                int glueTrimIndex = -1;
                for (int i = outputStream.Count - 1; i >= 0; i--) {
                    var o = outputStream [i];
                    var c = o as ControlCommand;
                    var g = o as Glue;

                    // Find latest glue
                    if (g) {
                        glueTrimIndex = i;
                        break;
                    } 

                    // Don't function-trim past the start of a string evaluation section
                    else if (c && c.commandType == ControlCommand.CommandType.BeginString) {
                        if (i >= functionTrimIndex) {
                            functionTrimIndex = -1;
                        }
                        break;
                    }
                }

                // Where is the most agressive (earliest) trim point?
                var trimIndex = -1;
                if (glueTrimIndex != -1 && functionTrimIndex != -1)
                    trimIndex = Math.Min (functionTrimIndex, glueTrimIndex);
                else if (glueTrimIndex != -1)
                    trimIndex = glueTrimIndex;
                else
                    trimIndex = functionTrimIndex;

                // So, are we trimming then?
                if (trimIndex != -1) {

                    // While trimming, we want to throw all newlines away,
                    // whether due to glue or the start of a function
                    if (text.isNewline) {
                        includeInOutput = false;
                    } 

                    // Able to completely reset when normal text is pushed
                    else if (text.isNonWhitespace) {

                        if( glueTrimIndex > -1 )
                            RemoveExistingGlue ();

                        // Tell all functions in callstack that we have seen proper text,
                        // so trimming whitespace at the start is done.
                        if (functionTrimIndex > -1) {
                            var callstackElements = callStack.elements;
                            for (int i = callstackElements.Count - 1; i >= 0; i--) {
                                var el = callstackElements [i];
                                if (el.type == PushPopType.Function) {
                                    el.functionStartInOuputStream = -1;
                                } else {
                                    break;
                                }
                            }
                        }
                    }
                } 

                // De-duplicate newlines, and don't ever lead with a newline
                else if (text.isNewline) {
                    if (outputStreamEndsInNewline || !outputStreamContainsContent)
                        includeInOutput = false;
                }
            }

            if (includeInOutput) {
                outputStream.Add (obj);
                OutputStreamDirty();
            }
        }

        void TrimNewlinesFromOutputStream()
        {
            int removeWhitespaceFrom = -1;

            // Work back from the end, and try to find the point where
            // we need to start removing content.
            //  - Simply work backwards to find the first newline in a string of whitespace
            // e.g. This is the content   \n   \n\n
            //                            ^---------^ whitespace to remove
            //                        ^--- first while loop stops here
            int i = outputStream.Count-1;
            while (i >= 0) {
                var obj = outputStream [i];
                var cmd = obj as ControlCommand;
                var txt = obj as StringValue;

                if (cmd || (txt && txt.isNonWhitespace)) {
                    break;
                } 
                else if (txt && txt.isNewline) {
                    removeWhitespaceFrom = i;
                }
                i--;
            }

            // Remove the whitespace
            if (removeWhitespaceFrom >= 0) {
                i=removeWhitespaceFrom;
                while(i < outputStream.Count) {
                    var text = outputStream [i] as StringValue;
                    if (text) {
                        outputStream.RemoveAt (i);
                    } else {
                        i++;
                    }
                }
            }

			OutputStreamDirty();
        }

        // Only called when non-whitespace is appended
        void RemoveExistingGlue()
        {
            for (int i = outputStream.Count - 1; i >= 0; i--) {
                var c = outputStream [i];
                if (c is Glue) {
                    outputStream.RemoveAt (i);
                } else if( c is ControlCommand ) { // e.g. BeginString
                    break;
                }
            }

			OutputStreamDirty();
        }

        public bool outputStreamEndsInNewline {
            get {
                if (outputStream.Count > 0) {

                    for (int i = outputStream.Count - 1; i >= 0; i--) {
                        var obj = outputStream [i];
                        if (obj is ControlCommand) // e.g. BeginString
                            break;
                        var text = outputStream [i] as StringValue;
                        if (text) {
                            if (text.isNewline)
                                return true;
                            else if (text.isNonWhitespace)
                                break;
                        }
                    }
                }

                return false;
            }
        }

        public bool outputStreamContainsContent {
            get {
                foreach (var content in outputStream) {
                    if (content is StringValue)
                        return true;
                }
                return false;
            }
        }

        public bool inStringEvaluation {
            get {
                for (int i = outputStream.Count - 1; i >= 0; i--) {
                    var cmd = outputStream [i] as ControlCommand;
                    if (cmd && cmd.commandType == ControlCommand.CommandType.BeginString) {
                        return true;
                    }
                }

                return false;
            }
        }

        public void PushEvaluationStack(Runtime.Object obj)
        {
            // Include metadata about the origin List for list values when
            // they're used, so that lower level functions can make use
            // of the origin list to get related items, or make comparisons
            // with the integer values etc.
            var listValue = obj as ListValue;
            if (listValue) {
                
                // Update origin when list is has something to indicate the list origin
                var rawList = listValue.value;
				if (rawList.originNames != null) {
					if( rawList.origins == null ) rawList.origins = new List<ListDefinition>();
					rawList.origins.Clear();

					foreach (var n in rawList.originNames) {
                        ListDefinition def = null;
                        story.listDefinitions.TryListGetDefinition (n, out def);
						if( !rawList.origins.Contains(def) )
							rawList.origins.Add (def);
                    }
                }
            }

            evaluationStack.Add(obj);
        }

        public Runtime.Object PopEvaluationStack()
        {
            var obj = evaluationStack [evaluationStack.Count - 1];
            evaluationStack.RemoveAt (evaluationStack.Count - 1);
            return obj;
        }

        public Runtime.Object PeekEvaluationStack()
        {
            return evaluationStack [evaluationStack.Count - 1];
        }

        public List<Runtime.Object> PopEvaluationStack(int numberOfObjects)
        {
            if(numberOfObjects > evaluationStack.Count) {
                throw new System.Exception ("trying to pop too many objects");
            }

            var popped = evaluationStack.GetRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
            evaluationStack.RemoveRange (evaluationStack.Count - numberOfObjects, numberOfObjects);
            return popped;
        }

        /// <summary>
        /// Ends the current ink flow, unwrapping the callstack but without
        /// affecting any variables. Useful if the ink is (say) in the middle
        /// a nested tunnel, and you want it to reset so that you can divert
        /// elsewhere using ChoosePathString(). Otherwise, after finishing
        /// the content you diverted to, it would continue where it left off.
        /// Calling this is equivalent to calling -> END in ink.
        /// </summary>
        public void ForceEnd()
        {
            callStack.Reset();

			_currentFlow.currentChoices.Clear();

            currentPointer = Pointer.Null;
            previousPointer = Pointer.Null;

            didSafeExit = true;
        }

        // Add the end of a function call, trim any whitespace from the end.
        // We always trim the start and end of the text that a function produces.
        // The start whitespace is discard as it is generated, and the end
        // whitespace is trimmed in one go here when we pop the function.
        void TrimWhitespaceFromFunctionEnd ()
        {
            Debug.Assert (callStack.currentElement.type == PushPopType.Function);

            var functionStartPoint = callStack.currentElement.functionStartInOuputStream;

            // If the start point has become -1, it means that some non-whitespace
            // text has been pushed, so it's safe to go as far back as we're able.
            if (functionStartPoint == -1) {
                functionStartPoint = 0;
            }

            // Trim whitespace from END of function call
            for (int i = outputStream.Count - 1; i >= functionStartPoint; i--) {
                var obj = outputStream [i];
                var txt = obj as StringValue;
                var cmd = obj as ControlCommand;
                if (!txt) continue;
                if (cmd) break;

                if (txt.isNewline || txt.isInlineWhitespace) {
                    outputStream.RemoveAt (i);
                    OutputStreamDirty ();
                } else {
                    break;
                }
            }
        }

        public void PopCallstack (PushPopType? popType = null)
        {
            // Add the end of a function call, trim any whitespace from the end.
            if (callStack.currentElement.type == PushPopType.Function)
                TrimWhitespaceFromFunctionEnd ();

            callStack.Pop (popType);
        }

        // Don't make public since the method need to be wrapped in Story for visit counting
        public void SetChosenPath(Path path, bool incrementingTurnIndex)
        {
            // Changing direction, assume we need to clear current set of choices
			_currentFlow.currentChoices.Clear ();

            var newPointer = story.PointerAtPath (path);
            if (!newPointer.isNull && newPointer.index == -1)
                newPointer.index = 0;

            currentPointer = newPointer;

            if( incrementingTurnIndex )
                currentTurnIndex++;
        }

        public void StartFunctionEvaluationFromGame (Container funcContainer, params object[] arguments)
        {
            callStack.Push (PushPopType.FunctionEvaluationFromGame, evaluationStack.Count);
            callStack.currentElement.currentPointer = Pointer.StartOf (funcContainer);

            PassArgumentsToEvaluationStack (arguments);
        }

        public void PassArgumentsToEvaluationStack (params object [] arguments)
        {
            // Pass arguments onto the evaluation stack
            if (arguments != null) {
                for (int i = 0; i < arguments.Length; i++) {
                    if (!(arguments [i] is int || arguments [i] is float || arguments [i] is string || arguments [i] is bool || arguments [i] is InkList)) {
                        throw new System.ArgumentException ("ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be int, float, string, bool or InkList. Argument was "+(arguments [i] == null ? "null" : arguments [i].GetType().Name));
                    }

                    PushEvaluationStack (Runtime.Value.Create (arguments [i]));
                }
            }
        }
            
        public bool TryExitFunctionEvaluationFromGame ()
        {
            if( callStack.currentElement.type == PushPopType.FunctionEvaluationFromGame ) {
                currentPointer = Pointer.Null;
                didSafeExit = true;
                return true;
            }

            return false;
        }

        public object CompleteFunctionEvaluationFromGame ()
        {
            if (callStack.currentElement.type != PushPopType.FunctionEvaluationFromGame) {
                throw new Exception ("Expected external function evaluation to be complete. Stack trace: "+callStack.callStackTrace);
            }

            int originalEvaluationStackHeight = callStack.currentElement.evaluationStackHeightWhenPushed;
            
            // Do we have a returned value?
            // Potentially pop multiple values off the stack, in case we need
            // to clean up after ourselves (e.g. caller of EvaluateFunction may 
            // have passed too many arguments, and we currently have no way to check for that)
            Runtime.Object returnedObj = null;
            while (evaluationStack.Count > originalEvaluationStackHeight) {
                var poppedObj = PopEvaluationStack ();
                if (returnedObj == null)
                    returnedObj = poppedObj;
            }

            // Finally, pop the external function evaluation
            PopCallstack (PushPopType.FunctionEvaluationFromGame);

            // What did we get back?
            if (returnedObj) {
                if (returnedObj is Runtime.Void)
                    return null;

                // Some kind of value, if not void
                var returnVal = returnedObj as Runtime.Value;

                // DivertTargets get returned as the string of components
                // (rather than a Path, which isn't public)
                if (returnVal.valueType == ValueType.DivertTarget) {
                    return returnVal.valueObject.ToString ();
                }

                // Other types can just have their exact object type:
                // int, float, string. VariablePointers get returned as strings.
                return returnVal.valueObject;
            }

            return null;
        }

        public void AddError(string message, bool isWarning)
        {
            if (!isWarning) {
                if (currentErrors == null) currentErrors = new List<string> ();
                currentErrors.Add (message);
            } else {
                if (currentWarnings == null) currentWarnings = new List<string> ();
                currentWarnings.Add (message);
            }
        }

		void OutputStreamDirty()
		{
			_outputStreamTextDirty = true;
			_outputStreamTagsDirty = true;
		}

        // REMEMBER! REMEMBER! REMEMBER!
        // When adding state, update the Copy method and serialisation
        // REMEMBER! REMEMBER! REMEMBER!


        Dictionary<string, int> _visitCounts;
        Dictionary<string, int> _turnIndices;
		bool _outputStreamTextDirty = true;
		bool _outputStreamTagsDirty = true;

        StatePatch _patch;

        Flow _currentFlow;
        Dictionary<string, Flow> _namedFlows;
        const string kDefaultFlowName = "DEFAULT_FLOW";
        bool _aliveFlowNamesDirty = true;
    }
}