accelerator-core
Version:
[](https://travis-ci.org/furkleindustries/accelerator-core)
861 lines (683 loc) • 24 kB
text/typescript
import {CallStack} from './CallStack';
import {VariablesState} from './VariablesState';
import {ValueType, Value, StringValue, ListValue} from './Value';
import {PushPopType} from './PushPop';
import {Tag} from './Tag';
import {Glue} from './Glue';
import {Path} from './Path';
import {ControlCommand} from './ControlCommand';
import {StoryException} from './StoryException';
import {StringBuilder} from './StringBuilder';
import {JsonSerialisation} from './JsonSerialisation';
import {PRNG} from './PRNG';
import {Void} from './Void';
import {Pointer} from './Pointer';
import {tryGetValueFromMap} from './TryGetResult';
import {Choice} from './Choice';
import {asOrNull, asOrThrows, nullIfUndefined} from './TypeAssertion';
import {JObject} from './JObject';
import {Debug} from './Debug';
import {Container} from './Container';
import {InkObject} from './Object';
import { throwNullException } from './NullException';
import { Story } from './Story';
export class StoryState{
public readonly kInkSaveStateVersion = 8;
public readonly kMinCompatibleLoadVersion = 8;
public ToJson(indented: boolean = false){
return JSON.stringify(this.jsonToken, null, (indented) ? 2 : 0);
}
public toJson(indented: boolean = false){
return this.ToJson(indented);
}
public LoadJson(json: string){
this.jsonToken = JSON.parse(json);
}
public VisitCountAtPathString(pathString: string){
let visitCountOut = tryGetValueFromMap(this.visitCounts, pathString, null);
if (visitCountOut.exists)
return visitCountOut.result;
return 0;
}
get callstackDepth(){
return this.callStack.depth;
}
get outputStream(){
return this._outputStream;
}
get currentChoices(){
// 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 ( this.canContinue ) return [];
return this._currentChoices;
}
get generatedChoices(){
return this._currentChoices;
}
get currentErrors(){
return this._currentErrors;
}
private _currentErrors: string[] | null = null;
get currentWarnings(){
return this._currentWarnings;
}
private _currentWarnings: string[] | null = null;
get variablesState(){
return this._variablesState;
}
private _variablesState: VariablesState;
public callStack: CallStack;
get evaluationStack(){
return this._evaluationStack;
}
private _evaluationStack: InkObject[];
public divertedPointer: Pointer = Pointer.Null;
get visitCounts(){
return this._visitCounts;
}
private _visitCounts: Map<string, number>;
get turnIndices(){
return this._turnIndices;
}
private _turnIndices: Map<string, number>;
get currentTurnIndex(){
return this._currentTurnIndex;
}
private _currentTurnIndex: number = 0;
public storySeed: number = 0;
public previousRandom: number = 0;
public didSafeExit: boolean = false;
public story: Story;
get currentPathString(){
let pointer = this.currentPointer;
if (pointer.isNull) {
return null;
} else {
if (pointer.path === null) { return throwNullException('pointer.path'); }
return pointer.path.toString();
}
}
get currentPointer(){
return this.callStack.currentElement.currentPointer.copy();
}
set currentPointer(value){
this.callStack.currentElement.currentPointer = value.copy();
}
get previousPointer(){
return this.callStack.currentThread.previousPointer.copy();
}
set previousPointer(value){
this.callStack.currentThread.previousPointer = value.copy();
}
get canContinue(){
return !this.currentPointer.isNull && !this.hasError;
}
get hasError(){
return this.currentErrors != null && this.currentErrors.length > 0;
}
get hasWarning(){
return this.currentWarnings != null && this.currentWarnings.length > 0;
}
get currentText(){
if( this._outputStreamTextDirty ) {
let sb = new StringBuilder();
for (let outputObj of this._outputStream) {
// var textContent = outputObj as StringValue;
let textContent = asOrNull(outputObj, StringValue);
if (textContent !== null) {
sb.Append(textContent.value);
}
}
this._currentText = this.CleanOutputWhitespace(sb.toString());
this._outputStreamTextDirty = false;
}
return this._currentText;
}
private _currentText: string | null = null;
public CleanOutputWhitespace(str: string){
let sb = new StringBuilder();
let currentWhitespaceStart = -1;
let startOfLine = 0;
for (let i = 0; i < str.length; i++) {
let c = str.charAt(i);
let 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();
}
get currentTags(){
if( this._outputStreamTagsDirty ) {
this._currentTags = [];
for(let outputObj of this._outputStream) {
// var tag = outputObj as Tag;
let tag = asOrNull(outputObj, Tag);
if (tag !== null) {
this._currentTags.push(tag.text);
}
}
this._outputStreamTagsDirty = false;
}
return this._currentTags;
}
private _currentTags: string[] | null = null;
get inExpressionEvaluation(){
return this.callStack.currentElement.inExpressionEvaluation;
}
set inExpressionEvaluation(value){
this.callStack.currentElement.inExpressionEvaluation = value;
}
constructor(story: Story){
this.story = story;
this._outputStream = [];
this.OutputStreamDirty();
this._evaluationStack = [];
this.callStack = new CallStack(story);
this._variablesState = new VariablesState(this.callStack, story.listDefinitions);
this._visitCounts = new Map();
this._turnIndices = new Map();
this._currentTurnIndex = -1;
let timeSeed = (new Date()).getTime();
this.storySeed = (new PRNG(timeSeed)).next() % 100;
this.previousRandom = 0;
this._currentChoices = [];
this.GoToStart();
}
public GoToStart(){
this.callStack.currentElement.currentPointer = Pointer.StartOf(this.story.mainContentContainer);
}
public Copy(){
let copy = new StoryState(this.story);
copy.outputStream.push.apply(copy.outputStream, this._outputStream);
this.OutputStreamDirty();
copy._currentChoices.push.apply(copy._currentChoices, this._currentChoices);
if (this.hasError) {
copy._currentErrors = [];
copy._currentErrors.push.apply(copy._currentErrors, this.currentErrors || []);
}
if (this.hasWarning) {
copy._currentWarnings = [];
copy._currentWarnings.push.apply(copy._currentWarnings, this.currentWarnings || []);
}
copy.callStack = new CallStack(this.callStack);
copy._variablesState = new VariablesState(copy.callStack, this.story.listDefinitions);
copy.variablesState.CopyFrom(this.variablesState);
copy.evaluationStack.push.apply(copy.evaluationStack, this.evaluationStack);
if (!this.divertedPointer.isNull)
copy.divertedPointer = this.divertedPointer.copy();
copy.previousPointer = this.previousPointer.copy();
copy._visitCounts = new Map(this.visitCounts);
copy._turnIndices = new Map(this.turnIndices);
copy._currentTurnIndex = this.currentTurnIndex;
copy.storySeed = this.storySeed;
copy.previousRandom = this.previousRandom;
copy.didSafeExit = this.didSafeExit;
return copy;
}
get jsonToken(){
let obj: JObject = {};
let choiceThreads: JObject | undefined;
for (let c of this._currentChoices) {
if (c.threadAtGeneration === null) { return throwNullException('c.threadAtGeneration'); }
c.originalThreadIndex = c.threadAtGeneration.threadIndex;
if( this.callStack.ThreadWithIndex(c.originalThreadIndex) == null ) {
if( choiceThreads == null )
choiceThreads = new Map();
choiceThreads[c.originalThreadIndex.toString()] = c.threadAtGeneration.jsonToken;
}
}
if (choiceThreads != null)
obj['choiceThreads'] = choiceThreads;
obj['callstackThreads'] = this.callStack.GetJsonToken();
obj['variablesState'] = this.variablesState.jsonToken;
obj['evalStack'] = JsonSerialisation.ListToJArray(this.evaluationStack);
obj['outputStream'] = JsonSerialisation.ListToJArray(this._outputStream);
obj['currentChoices'] = JsonSerialisation.ListToJArray(this._currentChoices);
if(!this.divertedPointer.isNull) {
if (this.divertedPointer.path === null) { return throwNullException('this.divertedPointer.path'); }
obj['currentDivertTarget'] = this.divertedPointer.path.componentsString;
}
obj['visitCounts'] = JsonSerialisation.IntDictionaryToJObject(this.visitCounts);
obj['turnIndices'] = JsonSerialisation.IntDictionaryToJObject(this.turnIndices);
obj['turnIdx'] = this.currentTurnIndex;
obj['storySeed'] = this.storySeed;
obj['previousRandom'] = this.previousRandom;
obj['inkSaveVersion'] = this.kInkSaveStateVersion;
// Not using this right now, but could do in future.
obj['inkFormatVersion'] = this.story.inkVersionCurrent;
return obj;
}
set jsonToken(value: JObject){
let jObject = value;
let jSaveVersion = jObject['inkSaveVersion'];
if (jSaveVersion == null) {
throw new StoryException("ink save format incorrect, can't load.");
}
else if (parseInt(jSaveVersion) < this.kMinCompatibleLoadVersion) {
throw new StoryException("Ink save format isn't compatible with the current version (saw '"+jSaveVersion+"', but minimum is "+this.kMinCompatibleLoadVersion+"), so can't load.");
}
this.callStack.SetJsonToken(jObject['callstackThreads'], this.story);
this.variablesState.jsonToken = jObject['variablesState'];
this._evaluationStack = JsonSerialisation.JArrayToRuntimeObjList(jObject['evalStack']);
this._outputStream = JsonSerialisation.JArrayToRuntimeObjList(jObject['outputStream']);
this.OutputStreamDirty();
// currentChoices = Json.JArrayToRuntimeObjList<Choice>((JArray)jObject ["currentChoices"]);
this._currentChoices = JsonSerialisation.JArrayToRuntimeObjList(jObject['currentChoices']) as Choice[];
let currentDivertTargetPath = jObject['currentDivertTarget'];
if (currentDivertTargetPath != null) {
let divertPath = new Path(currentDivertTargetPath.toString());
this.divertedPointer = this.story.PointerAtPath(divertPath);
}
this._visitCounts = JsonSerialisation.JObjectToIntDictionary(jObject['visitCounts']) as Map<string, number>;
this._turnIndices = JsonSerialisation.JObjectToIntDictionary(jObject['turnIndices']) as Map<string, number>;
this._currentTurnIndex = parseInt(jObject['turnIdx']);
this.storySeed = parseInt(jObject['storySeed']);
this.previousRandom = parseInt(jObject['previousRandom']);
// var jChoiceThreads = jObject["choiceThreads"] as JObject;
let jChoiceThreads = jObject['choiceThreads'];
for(let c of this._currentChoices) {
let foundActiveThread = this.callStack.ThreadWithIndex(c.originalThreadIndex);
if( foundActiveThread != null ) {
c.threadAtGeneration = foundActiveThread.Copy();
} else {
let jSavedChoiceThread = jChoiceThreads[c.originalThreadIndex.toString()];
c.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, this.story);
}
}
}
public ResetErrors(){
this._currentErrors = null;
this._currentWarnings = null;
}
public ResetOutput(objs: InkObject[] | null = null){
this._outputStream.length = 0;
if (objs !== null) this._outputStream.push.apply(this._outputStream, objs);
this.OutputStreamDirty();
}
public PushToOutputStream(obj: InkObject | null){
// var text = obj as StringValue;
let text = asOrNull(obj, StringValue);
if (text !== null) {
let listText = this.TrySplittingHeadTailWhitespace(text);
if (listText !== null) {
for(let textObj of listText) {
this.PushToOutputStreamIndividual(textObj);
}
this.OutputStreamDirty();
return;
}
}
this.PushToOutputStreamIndividual(obj);
this.OutputStreamDirty();
}
public PopFromOutputStream(count: number){
this.outputStream.splice(this.outputStream.length - count, count);
this.OutputStreamDirty();
}
public TrySplittingHeadTailWhitespace(single: StringValue) {
let str = single.value;
if (str === null) { return throwNullException('single.value'); }
let headFirstNewlineIdx = -1;
let headLastNewlineIdx = -1;
for (let i = 0; i < str.length; ++i) {
let c = str[i];
if (c == '\n') {
if (headFirstNewlineIdx == -1)
headFirstNewlineIdx = i;
headLastNewlineIdx = i;
}
else if (c == ' ' || c == '\t')
continue;
else
break;
}
let tailLastNewlineIdx = -1;
let tailFirstNewlineIdx = -1;
for (let i = 0; i < str.length; ++i) {
let 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;
let listTexts: StringValue[] = [];
let innerStrStart = 0;
let innerStrEnd = str.length;
if (headFirstNewlineIdx != -1) {
if (headFirstNewlineIdx > 0) {
let leadingSpaces = new StringValue(str.substring(0, headFirstNewlineIdx));
listTexts.push(leadingSpaces);
}
listTexts.push(new StringValue('\n'));
innerStrStart = headLastNewlineIdx + 1;
}
if (tailLastNewlineIdx != -1) {
innerStrEnd = tailFirstNewlineIdx;
}
if (innerStrEnd > innerStrStart) {
let innerStrText = str.substring(innerStrStart, innerStrEnd - innerStrStart);
listTexts.push(new StringValue(innerStrText));
}
if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) {
listTexts.push(new StringValue('\n'));
if (tailLastNewlineIdx < str.length - 1) {
let numSpaces = (str.length - tailLastNewlineIdx) - 1;
let trailingSpaces = new StringValue(str.substring(tailLastNewlineIdx + 1, numSpaces));
listTexts.push(trailingSpaces);
}
}
return listTexts;
}
// @ts-ignore
public PushToOutputStreamIndividual(obj: InkObject | null){
let glue = asOrNull(obj, Glue);
let text = asOrNull(obj, StringValue);
let includeInOutput = true;
if (glue) {
this.TrimNewlinesFromOutputStream();
includeInOutput = true;
}
else if( text ) {
let functionTrimIndex = -1;
let currEl = this.callStack.currentElement;
if (currEl.type == PushPopType.Function) {
functionTrimIndex = currEl.functionStartInOutputStream;
}
let glueTrimIndex = -1;
for (let i = this._outputStream.length - 1; i >= 0; i--) {
let o = this._outputStream[i];
let c = (o instanceof ControlCommand) ? o : null;
let g = (o instanceof Glue) ? o : null;
if (g != null) {
glueTrimIndex = i;
break;
}
else if (c != null && c.commandType == ControlCommand.CommandType.BeginString) {
if (i >= functionTrimIndex) {
functionTrimIndex = -1;
}
break;
}
}
let trimIndex = -1;
if (glueTrimIndex != -1 && functionTrimIndex != -1)
trimIndex = Math.min(functionTrimIndex, glueTrimIndex);
else if (glueTrimIndex != -1)
trimIndex = glueTrimIndex;
else
trimIndex = functionTrimIndex;
if (trimIndex != -1) {
if (text.isNewline) {
includeInOutput = false;
}
else if (text.isNonWhitespace) {
if (glueTrimIndex > -1)
this.RemoveExistingGlue();
if (functionTrimIndex > -1) {
let callStackElements = this.callStack.elements;
for (let i = callStackElements.length - 1; i >= 0; i--) {
let el = callStackElements[i];
if (el.type == PushPopType.Function) {
el.functionStartInOutputStream = -1;
} else {
break;
}
}
}
}
}
else if (text.isNewline) {
if (this.outputStreamEndsInNewline || !this.outputStreamContainsContent)
includeInOutput = false;
}
}
if (includeInOutput) {
if (obj === null) { return throwNullException('obj'); }
this._outputStream.push(obj);
this.OutputStreamDirty();
}
}
public TrimNewlinesFromOutputStream(){
let removeWhitespaceFrom = -1;
let i = this._outputStream.length-1;
while (i >= 0) {
let obj = this._outputStream[i];
let cmd = asOrNull(obj, ControlCommand);
let txt = asOrNull(obj, StringValue);
if (cmd != null || (txt != null && txt.isNonWhitespace)) {
break;
} else if (txt != null && txt.isNewline) {
removeWhitespaceFrom = i;
}
i--;
}
// Remove the whitespace
if (removeWhitespaceFrom >= 0) {
i=removeWhitespaceFrom;
while(i < this._outputStream.length) {
let text = asOrNull(this._outputStream[i], StringValue);
if (text) {
this._outputStream.splice(i, 1);
} else {
i++;
}
}
}
this.OutputStreamDirty();
}
public RemoveExistingGlue(){
for (let i = this._outputStream.length - 1; i >= 0; i--) {
let c = this._outputStream[i];
if (c instanceof Glue) {
this._outputStream.splice(i, 1);
} else if( c instanceof ControlCommand ) {
break;
}
}
this.OutputStreamDirty();
}
get outputStreamEndsInNewline(){
if (this._outputStream.length > 0) {
for (let i = this._outputStream.length - 1; i >= 0; i--) {
let obj = this._outputStream[i];
if (obj instanceof ControlCommand)
break;
let text = this._outputStream[i];
if (text instanceof StringValue) {
if (text.isNewline)
return true;
else if (text.isNonWhitespace)
break;
}
}
}
return false;
}
get outputStreamContainsContent(){
for (let i = 0; i < this._outputStream.length; i++){
if (this._outputStream[i] instanceof StringValue)
return true;
}
return false;
}
get inStringEvaluation(){
for (let i = this._outputStream.length - 1; i >= 0; i--) {
// var cmd = this._outputStream[i] as ControlCommand;
let cmd = asOrNull(this._outputStream[i], ControlCommand);
if (cmd instanceof ControlCommand && cmd.commandType == ControlCommand.CommandType.BeginString) {
return true;
}
}
return false;
}
// @ts-ignore
public PushEvaluationStack(obj: InkObject | null){
// var listValue = obj as ListValue;
let listValue = asOrNull(obj, ListValue);
if (listValue) {
// Update origin when list is has something to indicate the list origin
let rawList = listValue.value;
if (rawList === null) { return throwNullException('rawList'); }
if (rawList.originNames != null) {
if (!rawList.origins) rawList.origins = [];
rawList.origins.length = 0;
for (let n of rawList.originNames) {
if (this.story.listDefinitions === null) return throwNullException('StoryState.story.listDefinitions');
let def = this.story.listDefinitions.TryListGetDefinition(n, null);
if (def.result === null) return throwNullException('StoryState def.result');
if (rawList.origins.indexOf(def.result) < 0) rawList.origins.push(def.result);
}
}
}
if (obj === null) { return throwNullException('obj'); }
this.evaluationStack.push(obj);
}
public PopEvaluationStack(): InkObject;
public PopEvaluationStack(numberOfObjects: number): InkObject[];
public PopEvaluationStack(numberOfObjects?: number){
if (typeof numberOfObjects === 'undefined'){
let obj = this.evaluationStack.pop();
return nullIfUndefined(obj);
} else {
if(numberOfObjects > this.evaluationStack.length) {
throw new Error('trying to pop too many objects');
}
let popped = this.evaluationStack.splice(this.evaluationStack.length - numberOfObjects, numberOfObjects);
return nullIfUndefined(popped);
}
}
public PeekEvaluationStack(){
return this.evaluationStack[this.evaluationStack.length - 1];
}
public ForceEnd(){
this.callStack.Reset();
this._currentChoices.length = 0;
this.currentPointer = Pointer.Null;
this.previousPointer = Pointer.Null;
this.didSafeExit = true;
}
public TrimWhitespaceFromFunctionEnd(){
Debug.Assert (this.callStack.currentElement.type == PushPopType.Function);
let functionStartPoint = this.callStack.currentElement.functionStartInOutputStream;
if (functionStartPoint == -1) {
functionStartPoint = 0;
}
for (let i = this._outputStream.length - 1; i >= functionStartPoint; i--) {
let obj = this._outputStream[i];
let txt = asOrNull(obj, StringValue);
let cmd = asOrNull(obj, ControlCommand);
if (txt == null) continue;
if (cmd) break;
if (txt.isNewline || txt.isInlineWhitespace) {
this._outputStream.splice(i, 1);
this.OutputStreamDirty();
} else {
break;
}
}
}
public PopCallStack(popType: PushPopType | null = null) {
if (this.callStack.currentElement.type == PushPopType.Function)
this.TrimWhitespaceFromFunctionEnd();
this.callStack.Pop(popType);
}
public SetChosenPath(path: Path, incrementingTurnIndex: boolean){
// Changing direction, assume we need to clear current set of choices
this._currentChoices.length = 0;
let newPointer = this.story.PointerAtPath(path);
if (!newPointer.isNull && newPointer.index == -1)
newPointer.index = 0;
this.currentPointer = newPointer;
if (incrementingTurnIndex)
this._currentTurnIndex++;
}
public StartFunctionEvaluationFromGame(funcContainer: Container, args: any[]){
this.callStack.Push(PushPopType.FunctionEvaluationFromGame, this.evaluationStack.length);
this.callStack.currentElement.currentPointer = Pointer.StartOf(funcContainer);
this.PassArgumentsToEvaluationStack(args);
}
public PassArgumentsToEvaluationStack(args: any[]){
// Pass arguments onto the evaluation stack
if (args != null) {
for (let i = 0; i < args.length; i++) {
if (!(typeof args[i] === 'number' || typeof args[i] === 'string')) {
throw new Error('ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be int, float or string');
}
this.PushEvaluationStack(Value.Create(args[i]));
}
}
}
public TryExitFunctionEvaluationFromGame(){
if (this.callStack.currentElement.type == PushPopType.FunctionEvaluationFromGame) {
this.currentPointer = Pointer.Null;
this.didSafeExit = true;
return true;
}
return false;
}
public CompleteFunctionEvaluationFromGame(){
if (this.callStack.currentElement.type != PushPopType.FunctionEvaluationFromGame) {
throw new StoryException('Expected external function evaluation to be complete. Stack trace: '+this.callStack.callStackTrace);
}
let originalEvaluationStackHeight = this.callStack.currentElement.evaluationStackHeightWhenPushed;
let returnedObj: InkObject | null = null;
while (this.evaluationStack.length > originalEvaluationStackHeight) {
let poppedObj = this.PopEvaluationStack();
if (returnedObj === null)
returnedObj = poppedObj;
}
this.PopCallStack(PushPopType.FunctionEvaluationFromGame);
if (returnedObj) {
if (returnedObj instanceof Void)
return null;
// Some kind of value, if not void
// var returnVal = returnedObj as Runtime.Value;
let returnVal = asOrThrows(returnedObj, 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 AddError(message: string, isWarning: boolean){
if (!isWarning) {
if (this._currentErrors == null) this._currentErrors = [];
this._currentErrors.push(message);
} else {
if (this._currentWarnings == null) this._currentWarnings = [];
this._currentWarnings.push(message);
}
}
public OutputStreamDirty(){
this._outputStreamTextDirty = true;
this._outputStreamTagsDirty = true;
}
private _outputStream: InkObject[];
private _outputStreamTextDirty = true;
private _outputStreamTagsDirty = true;
private _currentChoices: Choice[];
}