accelerator-core
Version:
[](https://travis-ci.org/furkleindustries/accelerator-core)
611 lines (528 loc) • 21.7 kB
text/typescript
import {
Container,
} from './inkjs/src/Container';
import {
ControlCommand,
} from './inkjs/src/ControlCommand';
import {
Divert,
} from './inkjs/src/Divert';
import {
NativeFunctionCall,
} from './inkjs/src/NativeFunctionCall';
import {
throwNullException,
} from './inkjs/src/NullException';
import {
InkList,
InkListItem,
} from './inkjs/src/InkList';
import {
InkObject,
} from './inkjs/src/Object';
import {
Pointer,
} from './inkjs/src/Pointer';
import {
PRNG,
} from './inkjs/src/PRNG';
import {
PushPopType,
} from './inkjs/src/PushPop';
import {
Story,
} from './inkjs/src/Story';
import {
StoryException,
} from './inkjs/src/StoryException';
import {
StringBuilder,
} from './inkjs/src/StringBuilder';
import {
assertValid,
} from 'ts-assertions';
import {
asOrNull,
asOrThrows,
} from './inkjs/src/TypeAssertion';
import {
DivertTargetValue,
IntValue,
ListValue,
StringValue,
Value,
} from './inkjs/src/Value';
import {
VariableAssignment,
} from './inkjs/src/VariableAssignment';
import {
VariableReference,
} from './inkjs/src/VariableReference';
import {
Void,
} from './inkjs/src/Void';
export class StoryWithDoneEvent extends Story {
public readonly PerformLogicAndFlowControl = (contentObj: InkObject | null) => {
if (contentObj === null) {
return false;
}
if (contentObj instanceof Divert) {
return this.__handleDivert(contentObj);
} else if (contentObj instanceof ControlCommand) {
return this.__handleControlCommand(contentObj);
} else if (contentObj instanceof VariableAssignment) {
return this.__handleVariableAssignment(contentObj);
} else if (contentObj instanceof VariableReference) {
return this.__handleVariableReference(contentObj);
} else if (contentObj instanceof NativeFunctionCall) {
return this.__handleNativeFunctionCall(contentObj);
}
/* No control content, must be ordinary content. */
return false;
};
public readonly __handleDivert = (currentDivert: Divert) => {
if (currentDivert.isConditional) {
const conditionValue = this.state.PopEvaluationStack();
// False conditional? Cancel divert
if (!this.IsTruthy(conditionValue)) {
return true;
}
}
if (currentDivert.hasVariableTarget) {
const varName = currentDivert.variableDivertName;
const varContents = this.state.variablesState.GetVariableWithName(varName);
if (varContents == null) {
this.Error(
`Tried to divert using a target from a variable that could not be ` +
`found (${varName})`,
);
} else if (!(varContents instanceof DivertTargetValue)) {
// var intContent = varContents as IntValue;
const intContent = asOrNull(varContents, IntValue);
let errorMessage = `Tried to divert to a target from a variable, ` +
`but the variable (${varName}) didn't contain a divert target, it `;
if (intContent instanceof IntValue && intContent.value == 0) {
errorMessage += 'was empty/null (the value 0).';
} else {
errorMessage += `contained "${varContents}".`;
}
this.Error(errorMessage);
}
let target = asOrThrows(varContents, DivertTargetValue);
this.state.divertedPointer = this.PointerAtPath(target.targetPath);
} else if (currentDivert.isExternal) {
this.CallExternalFunction(currentDivert.targetPathString, currentDivert.externalArgs);
return true;
} else {
this.state.divertedPointer = currentDivert.targetPointer.copy();
}
if (currentDivert.pushesToStack) {
this.state.callStack.Push(
currentDivert.stackPushType,
undefined,
this.state.outputStream.length,
);
}
if (this.state.divertedPointer.isNull && !currentDivert.isExternal) {
if (currentDivert && currentDivert.debugMetadata && currentDivert.debugMetadata.sourceName != null) {
this.Error(
`Divert target doesn't exist: ` +
`${currentDivert.debugMetadata.sourceName}.`,
);
} else {
this.Error(`Divert resolution failed: ${currentDivert}.`);
}
}
return true;
};
public readonly __handleControlCommand = (evalCommand: ControlCommand) => {
// Start/end an expression evaluation? Or print out the result?
const commandType = evalCommand.commandType;
if (commandType === ControlCommand.CommandType.EvalStart) {
this.Assert(this.state.inExpressionEvaluation === false, 'Already in expression evaluation?');
this.state.inExpressionEvaluation = true;
} else if (commandType === ControlCommand.CommandType.EvalEnd) {
this.Assert(this.state.inExpressionEvaluation === true, 'Not in expression evaluation mode');
this.state.inExpressionEvaluation = false;
} else if (commandType === ControlCommand.CommandType.EvalOutput &&
this.state.evaluationStack.length)
{
// If the expression turned out to be empty, there may not be anything on the stack
const output = this.state.PopEvaluationStack();
// Functions may evaluate to Void, in which if (we skip output
if (!(output instanceof Void)) {
// TODO: Should we really always blanket convert to string?
// It would be okay to have numbers in the output stream the
// only problem is when exporting text for viewing, it skips over numbers etc.
const text = new StringValue(output.toString());
this.state.PushToOutputStream(text);
}
} else if (commandType === ControlCommand.CommandType.NoOp) {
return true;
} else if (commandType === ControlCommand.CommandType.Duplicate) {
this.state.PushEvaluationStack(this.state.PeekEvaluationStack());
} else if (commandType === ControlCommand.CommandType.PopEvaluatedValue) {
this.state.PopEvaluationStack();
} else if (commandType === ControlCommand.CommandType.PopFunction ||
commandType === ControlCommand.CommandType.PopTunnel)
{
const popFuncType = ControlCommand.CommandType.PopFunction;
const popType = evalCommand.commandType === popFuncType ?
PushPopType.Function :
PushPopType.Tunnel;
let overrideTunnelReturnTarget: DivertTargetValue | null = null;
if (popType == PushPopType.Tunnel) {
const popped = this.state.PopEvaluationStack();
// overrideTunnelReturnTarget = popped as DivertTargetValue;
overrideTunnelReturnTarget = asOrNull(popped, DivertTargetValue);
if (overrideTunnelReturnTarget === null) {
this.Assert(
popped instanceof Void,
`Expected void if ->-> doesn't override target.`,
);
}
}
if (this.state.TryExitFunctionEvaluationFromGame()) {
return true;
} else if (this.state.callStack.currentElement.type !== popType ||
!this.state.callStack.canPop)
{
const names: Map<PushPopType, string> = new Map();
names.set(PushPopType.Function, 'function return statement (~ return)');
names.set(PushPopType.Tunnel, 'tunnel onwards statement (->->)');
const expected = this.state.callStack.canPop ?
names.get(this.state.callStack.currentElement.type) :
'end of flow (-> END or choice)';
const errorMsg = `Found ${names.get(popType)} when expecting ` +
`${expected}.`;
this.Error(errorMsg);
} else {
this.state.PopCallStack();
if (overrideTunnelReturnTarget) {
this.state.divertedPointer = this.PointerAtPath(overrideTunnelReturnTarget.targetPath);
}
}
} else if (commandType === ControlCommand.CommandType.BeginString) {
this.state.PushToOutputStream(evalCommand);
this.Assert(
this.state.inExpressionEvaluation === true,
'Expected to be in an expression when evaluating a string.',
);
this.state.inExpressionEvaluation = false;
} else if (commandType === ControlCommand.CommandType.EndString) {
let contentStackForString: InkObject[] = [];
let outputCountConsumed = 0;
for (let ii = this.state.outputStream.length - 1; ii >= 0; ii -= 1) {
const obj = this.state.outputStream[ii];
outputCountConsumed += 1;
/* var command = obj as ControlCommand; */
const command = asOrNull(obj, ControlCommand);
if (command && command.commandType === ControlCommand.CommandType.BeginString) {
break;
}
if (obj instanceof StringValue) {
contentStackForString.push(obj);
}
}
/* Consume the content that was produced for this string. */
this.state.PopFromOutputStream(outputCountConsumed);
/* The C# version uses a Stack for contentStackForString, but we're
* using a simple array, so we need to reverse it before using it. */
contentStackForString = contentStackForString.reverse();
// Build string out of the content we collected
const sb = new StringBuilder();
for (const c of contentStackForString) {
sb.Append(c.toString());
}
// Return to expression evaluation (from content mode)
this.state.inExpressionEvaluation = true;
this.state.PushEvaluationStack(new StringValue(sb.toString()));
} else if (commandType === ControlCommand.CommandType.ChoiceCount) {
const choiceCount = this.state.generatedChoices.length;
this.state.PushEvaluationStack(new IntValue(choiceCount));
} else if (commandType === ControlCommand.CommandType.Turns) {
const intVal = new IntValue(this.state.currentTurnIndex + 1);
this.state.PushEvaluationStack(intVal);
} else if (commandType === ControlCommand.CommandType.TurnsSince ||
commandType === ControlCommand.CommandType.ReadCount)
{
const target = this.state.PopEvaluationStack();
if (!(target instanceof DivertTargetValue)) {
const extraNote = target instanceof IntValue ?
'' :
`. Did you accidentally pass a read count ('knot_name') instead ` +
`of a target ('-> knot_name')?`;
this.Error(
'TURNS_SINCE / READ_COUNT expected a divert target (knot, ' +
`stitch, label name), but saw ${target}${extraNote}`,
);
}
/* var divertTarget = target as DivertTargetValue; */
const divertTarget = asOrThrows(target, DivertTargetValue);
/* var container = ContentAtPath(divertTarget.targetPath).correctObj as Container; */
const { correctObj } = this.ContentAtPath(divertTarget.targetPath);
const container = asOrNull(correctObj, Container);
let eitherCount;
if (container !== null) {
eitherCount = commandType === ControlCommand.CommandType.TurnsSince ?
this.TurnsSinceForContainer(container) :
this.VisitCountForContainer(container);
} else {
eitherCount = commandType === ControlCommand.CommandType.TurnsSince ?
-1 :
0;
this.Warning(
`Failed to find container for ${evalCommand.toString()} lookup ` +
`at ${divertTarget.targetPath.toString()}.`,
);
}
this.state.PushEvaluationStack(new IntValue(eitherCount));
} else if (commandType === ControlCommand.CommandType.Random) {
const maxInt = asOrNull(this.state.PopEvaluationStack(), IntValue);
const minInt = asOrNull(this.state.PopEvaluationStack(), IntValue);
if (minInt === null || !(minInt instanceof IntValue)) {
return this.Error('Invalid value for minimum parameter of ' +
'RANDOM(min, max).');
}
if (maxInt === null || !(maxInt instanceof IntValue)) {
return this.Error('Invalid value for maximum parameter of RANDOM(min, max).');
} else if (maxInt.value === null) {
/* Originally a primitive type, but here, can be null.
* TODO: Replace by default value? */
return throwNullException('maxInt.value');
} else if (minInt.value === null) {
return throwNullException('minInt.value');
}
const randomRange = maxInt.value - minInt.value + 1;
if (randomRange <= 0) {
this.Error(`RANDOM was called with minimum as ${minInt.value} and ` +
`maximum as ${maxInt.value}. The maximum must be larger.`,
);
}
const resultSeed = this.state.storySeed + this.state.previousRandom;
const random = new PRNG(resultSeed);
const nextRandom = random.next();
const chosenValue = (nextRandom % randomRange) + minInt.value;
this.state.PushEvaluationStack(new IntValue(chosenValue));
/* Next random number (rather than keeping the Random object
* around). */
this.state.previousRandom = nextRandom;
} else if (commandType === ControlCommand.CommandType.SeedRandom) {
let seed = asOrNull(this.state.PopEvaluationStack(), IntValue);
if (seed === null || !(seed instanceof IntValue)) {
return this.Error('Invalid value passed to SEED_RANDOM.');
}
/* Originally a primitive type, but here, can be null.
* TODO: Replace by default value? */
if (seed.value === null) {
return throwNullException('minInt.value');
}
this.state.storySeed = seed.value;
this.state.previousRandom = 0;
this.state.PushEvaluationStack(new Void());
} else if (commandType === ControlCommand.CommandType.VisitIndex) {
const ptrContainer = this.state.currentPointer.container;
const count = this.VisitCountForContainer(ptrContainer) - 1;
this.state.PushEvaluationStack(new IntValue(count));
} else if (commandType === ControlCommand.CommandType.SequenceShuffleIndex) {
const shuffleIndex = this.NextSequenceShuffleIndex();
this.state.PushEvaluationStack(new IntValue(shuffleIndex));
} else if (commandType === ControlCommand.CommandType.StartThread) {
/* Handled in main step function. */
return true;
} else if (commandType === ControlCommand.CommandType.Done) {
/* We may exist in the context of the initial
* act of creating the thread, or in the context of
* evaluating the content. */
if (this.state.callStack.canPopThread) {
this.state.callStack.PopThread();
} else {
/* In normal flow - allow safe exit without warning. */
this.state.didSafeExit = true;
/* Stop flow in current thread. */
this.state.currentPointer = Pointer.Null;
/* Fire all registered callbacks. */
this.__performDoneCallbacks();
}
} else if (commandType === ControlCommand.CommandType.End) {
/* Force flow to end completely. */
this.state.ForceEnd();
/* Fire all registered callbacks. */
this.__performDoneCallbacks();
} else if (commandType === ControlCommand.CommandType.ListFromInt) {
// var intVal = state.PopEvaluationStack () as IntValue;
const intVal = asOrNull(this.state.PopEvaluationStack(), IntValue);
// var listNameVal = state.PopEvaluationStack () as StringValue;
let listNameVal = asOrThrows(this.state.PopEvaluationStack(), StringValue);
if (intVal === null) {
throw new StoryException('Passed non-integer when creating a list element from a numerical value.');
}
let generatedListValue = null;
if (this.listDefinitions === null) {
return throwNullException('this.listDefinitions');
}
const {
exists,
result,
} = this.listDefinitions.TryListGetDefinition(listNameVal.value, null);
if (exists) {
// Originally a primitive type, but here, can be null.
// TODO: Replace by default value?
if (intVal.value === null) {
return throwNullException('minInt.value');
}
const {
exists,
result: foundResult,
} = result!.TryGetItemWithValue(
intVal.value,
InkListItem.Null,
);
if (exists) {
generatedListValue = new ListValue(foundResult, intVal.value);
}
} else {
throw new StoryException('Failed to find LIST called ' + listNameVal.value);
}
if (generatedListValue === null) {
generatedListValue = new ListValue();
}
this.state.PushEvaluationStack(generatedListValue);
} else if (commandType === ControlCommand.CommandType.ListRange) {
let max = asOrNull(this.state.PopEvaluationStack(), Value);
let min = asOrNull(this.state.PopEvaluationStack(), Value);
/* var targetList = state.PopEvaluationStack () as ListValue; */
const targetList = asOrNull(
this.state.PopEvaluationStack(),
ListValue,
);
if (targetList === null || min === null || max === null) {
throw new StoryException(
'Expected list, minimum and maximum for LIST_RANGE',
);
}
if (targetList.value === null) {
return throwNullException('targetList.value');
}
const result = targetList.value.ListWithSubRange(
min.valueObject,
max.valueObject,
);
this.state.PushEvaluationStack(new ListValue(result));
} else if (commandType === ControlCommand.CommandType.ListRandom) {
let listVal = this.state.PopEvaluationStack() as ListValue;
if (listVal === null) {
throw new StoryException('Expected list for LIST_RANDOM');
}
const { value: list } = listVal;
let newList: InkList | null = null;
if (list === null) {
throw throwNullException('list');
} else if (!list.Count) {
newList = new InkList();
} else {
/* Generate a random index for the element to take. */
const resultSeed = this.state.storySeed + this.state.previousRandom;
const random = new PRNG(resultSeed);
const nextRandom = random.next();
const listItemIndex = nextRandom % list.Count;
/* This bit is a little different from the original
* C# code, since iterators do not work in the same way.
* First, we iterate listItemIndex - 1 times, calling next().
* The listItemIndex-th time is made outside of the loop,
* in order to retrieve the value. */
const listEnumerator = list.entries();
for (let ii = 0; ii <= listItemIndex - 1; ii += 1) {
listEnumerator.next();
}
const [
serializedKey,
value,
] = listEnumerator.next().value;
const key = InkListItem.fromSerializedKey(serializedKey);
/* Origin list is simply the origin of the one element */
if (key.originName === null) {
return throwNullException('randomItem.Key.originName');
}
newList = new InkList(key.originName, this);
newList.Add(key, value);
this.state.previousRandom = nextRandom;
}
this.state.PushEvaluationStack(new ListValue(newList));
} else {
this.Error(`Unhandled ControlCommand: ${evalCommand}`);
}
return true;
};
public readonly __handleVariableAssignment = (
varAssignment: VariableAssignment,
) => {
this.state.variablesState.Assign(
varAssignment,
this.state.PopEvaluationStack(),
);
return true;
};
public readonly __handleVariableReference = (varRef: VariableReference) => {
let foundValue = null;
if (varRef.pathForCount !== null) {
// Explicit read count value
let container = varRef.containerForCount;
let count = this.VisitCountForContainer(container);
foundValue = new IntValue(count);
} else {
// Normal variable reference
foundValue = this.state.variablesState.GetVariableWithName(varRef.name);
if (foundValue == null) {
let defaultVal = this.state.variablesState.TryGetDefaultVariableValue (varRef.name);
if (defaultVal != null) {
this.Warning(
`Variable not found in save state: "${varRef.name}", but seems ` +
`to have been newly created. Assigning value from latest ` +
`ink's declaration: ${defaultVal}.`,
);
foundValue = defaultVal;
/* Save for future usage, preventing future errors. Only do this for
* variables that are known to be globals, not those that may be
* missing temps. */
this.state.variablesState.SetGlobal(varRef.name, foundValue);
} else {
this.Warning(
`Variable not found: "${varRef.name}". Using default value of 0 ` +
`(false). This can happen with temporary variables if the ` +
`declaration hasn't yet been hit.`,
);
foundValue = new IntValue(0);
}
}
}
this.state.PushEvaluationStack(foundValue);
return true;
};
public readonly __handleNativeFunctionCall = ({
Call,
numberOfParameters,
}: NativeFunctionCall) => {
const funcParams = this.state.PopEvaluationStack(numberOfParameters);
const result = Call(funcParams);
this.state.PushEvaluationStack(result);
return true;
};
private readonly __registeredDoneCallbacks: Array<
(story: this) => void
> = [];
public readonly __registerDoneCallback = (
callback: (story: this, ...args: any[]) => void,
) => {
this.__registeredDoneCallbacks.push(
assertValid(
callback,
'The value passed to StoryWithDoneEvent was not a function.',
(func) => typeof func === 'function',
)
);
};
public readonly __performDoneCallbacks = () => (
this.__registeredDoneCallbacks.forEach((callback) => callback(this))
);
}