Biting the bullet, started rewriting Action engine
This commit is contained in:
290
app/imports/api/engine/actions/Actions.ts
Normal file
290
app/imports/api/engine/actions/Actions.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { forEachRight, forEach, isEmpty } from 'lodash';
|
||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema';
|
||||
import { getPropertyChildren, getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations';
|
||||
|
||||
const Actions = new Mongo.Collection<Action>('actions');
|
||||
|
||||
type Action = {
|
||||
_id: string;
|
||||
creatureId: string;
|
||||
rootPropId: string;
|
||||
targetIds: string[];
|
||||
userInputNeeded: boolean;
|
||||
stepThrough: boolean;
|
||||
taskQueue: Task[];
|
||||
results: TaskResult[];
|
||||
}
|
||||
|
||||
type Task = {
|
||||
propId: string;
|
||||
targetIds: string[];
|
||||
}
|
||||
|
||||
type TaskResult = {
|
||||
propId: string;
|
||||
targetIds: string[];
|
||||
scope: any;
|
||||
mutations: Mutation[];
|
||||
}
|
||||
|
||||
type PartialTaskResult = {
|
||||
scope: any;
|
||||
mutations: Mutation[];
|
||||
}
|
||||
|
||||
type Mutation = {
|
||||
// Which creatures the mutation is applied to
|
||||
targetIds: string[];
|
||||
// What changes in the database
|
||||
updates?: {
|
||||
propId: string;
|
||||
set: any;
|
||||
}[];
|
||||
// Logged when this is applied
|
||||
contents?: LogContent[];
|
||||
}
|
||||
|
||||
type LogContent = {
|
||||
name?: string;
|
||||
value?: string;
|
||||
inline?: boolean;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
const ActionSchema = new SimpleSchema({
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
rootPropId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
userInputNeeded: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
stepThrough: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
// A stack of tasks to apply
|
||||
// Each task has a propId to apply and a targetId list
|
||||
taskQueue: {
|
||||
type: Array,
|
||||
},
|
||||
'taskQueue.$': {
|
||||
type: Object,
|
||||
},
|
||||
'taskQueue.$.propId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
'taskQueue.$.targetIds': {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'taskQueue.$.targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
|
||||
// Applied properties
|
||||
results: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'results.$': {
|
||||
type: Object,
|
||||
},
|
||||
// The property and target ids popped off the task stack
|
||||
// Pushing these to the top of the stack and deleting the results from this point onwards
|
||||
// Should re-run the action identically from this point
|
||||
'results.$.propId': {
|
||||
type: Object,
|
||||
},
|
||||
'result.$.targetIds': {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'result.$.targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
// Changes in local scope made by this result
|
||||
'results.$.scope': {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// database changes
|
||||
'results.$.mutations': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'results.$.mutations.$': {
|
||||
type: Object,
|
||||
},
|
||||
'results.$.mutations.$.propId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
'results.$.mutations.$.set': {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
'results.$.mutations.$.logContent': {
|
||||
type: LogContentSchema,
|
||||
},
|
||||
});
|
||||
|
||||
Actions.attachSchema(ActionSchema);
|
||||
|
||||
// Run an already created action
|
||||
export async function runAction(actionId: string, userInput) {
|
||||
const action = await Actions.findOneAsync(actionId);
|
||||
if (!action) throw new Meteor.Error('Not found', 'The action does not exist');
|
||||
const originalAction = EJSON.clone(action);
|
||||
do {
|
||||
// Get the next task
|
||||
const task = action.taskQueue.shift();
|
||||
// If there isn't one, stop
|
||||
if (!task) break;
|
||||
|
||||
// Apply the prop
|
||||
await applyProperty(task, action, userInput);
|
||||
} while (!action.userInputNeeded || !action.stepThrough)
|
||||
|
||||
// Persist changes to the action
|
||||
return writeChangedAction(originalAction, action);
|
||||
}
|
||||
|
||||
function writeChangedAction(original: Action, changed: Action) {
|
||||
const $set = {};
|
||||
for (const key of ActionSchema.objectKeys()) {
|
||||
if (!EJSON.equals(original[key], changed[key])) {
|
||||
$set[key] = changed[key];
|
||||
}
|
||||
}
|
||||
if (!isEmpty($set)) {
|
||||
return Actions.updateAsync(original._id, { $set });
|
||||
}
|
||||
}
|
||||
|
||||
async function applyProperty(task, action, userInput) {
|
||||
// Ensure the prop exists
|
||||
const prop = await getSingleProperty(action.creatureId, task.propId);
|
||||
if (!prop) throw new Meteor.Error('Not found', 'Property could not be found');
|
||||
if (prop.deactivatedByToggle) return;
|
||||
|
||||
// Apply the property, applyPropertyByType must have no side effects except to action.
|
||||
const { result } = await applyPropertyByType[prop.type]?.(prop, task, action, userInput);
|
||||
|
||||
// store the task's details and save the result
|
||||
result.propId = task.propId;
|
||||
result.targetIds = task.targetIds;
|
||||
action.results.push(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a prop and its before/after triggers to the task stack
|
||||
* Triggers will share the same targetIds as the prop task
|
||||
* @param action The action to add the task to
|
||||
* @param prop The property to make a task of
|
||||
* @param targetIds The targetIds the prop and triggers will apply to
|
||||
*/
|
||||
function pushPropAndTriggers(action: Action, prop, targetIds) {
|
||||
// Push the before triggers to the queue
|
||||
forEach(prop.triggerIds?.before, triggerId => {
|
||||
action.taskQueue.push({ propId: triggerId, targetIds });
|
||||
});
|
||||
|
||||
// Push the prop task to the queue
|
||||
action.taskQueue.push({ propId: prop._id, targetIds });
|
||||
|
||||
// Push the after triggers to the queue
|
||||
forEach(prop.triggerIds?.after, triggerId => {
|
||||
action.taskQueue.push({ propId: triggerId, targetIds });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Push all the children of a prop and all related triggers to the task stack
|
||||
* Triggers will share the same targetIds as the prop task
|
||||
* @param action The action to add the task to
|
||||
* @param prop The property to make a task of
|
||||
* @param targetIds The targetIds the prop and triggers will apply to
|
||||
*/
|
||||
function pushChildrenAndTriggers(action: Action, prop, targetIds) {
|
||||
const children = getPropertyChildren(action.creatureId, prop._id);
|
||||
|
||||
// Push the child tasks and related triggers to the stack
|
||||
forEach(children, childProp => {
|
||||
pushPropAndTriggers(action, childProp, targetIds)
|
||||
});
|
||||
|
||||
// After the children run, it must run 'afterChildren' triggers
|
||||
// Make sure they're on the bottom of the stack
|
||||
forEach(prop.triggerIds?.afterChildren, triggerId => {
|
||||
action.taskQueue.push({ propId: triggerId, targetIds });
|
||||
});
|
||||
}
|
||||
|
||||
function createResult(prop): PartialTaskResult {
|
||||
// Add the property to the action's local scope
|
||||
return {
|
||||
scope: {
|
||||
[`#${prop.type}`]: { _propId: prop._id }
|
||||
},
|
||||
mutations: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Return result object
|
||||
// No side effects except pushing to taskQueue
|
||||
const applyPropertyByType = {
|
||||
|
||||
async note(prop, task: Task, action) {
|
||||
const result = createResult(prop);
|
||||
|
||||
let contents: LogContent[] | undefined = undefined;
|
||||
const logContent = { name: prop.name, value: undefined };
|
||||
if (prop.summary?.text) {
|
||||
recalculateInlineCalculations(prop.summary, action);
|
||||
logContent.value = prop.summary.value;
|
||||
}
|
||||
|
||||
if (logContent.name || logContent.value) {
|
||||
contents = [logContent];
|
||||
}
|
||||
// Log description
|
||||
if (prop.description?.text) {
|
||||
recalculateInlineCalculations(prop.description, action);
|
||||
if (!contents) contents = [];
|
||||
contents.push({ value: prop.description.value });
|
||||
}
|
||||
if (contents) {
|
||||
result.mutations.push({
|
||||
contents,
|
||||
targetIds: task.targetIds,
|
||||
});
|
||||
}
|
||||
|
||||
pushChildrenAndTriggers(action, prop, task.targetIds);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
|
||||
import recalculateCalculation from './recalculateCalculation.js'
|
||||
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, action) {
|
||||
// Skip if there are no calculations
|
||||
if (!inlineCalcObj?.inlineCalculations?.length) return;
|
||||
// Recalculate each calculation with the current scope
|
||||
inlineCalcObj.inlineCalculations.forEach(calc => {
|
||||
recalculateCalculation(calc, actionContext);
|
||||
recalculateCalculation(calc, action);
|
||||
});
|
||||
// Embed the new calculated values
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
|
||||
@@ -189,6 +189,30 @@ export function getPropertyDecendants(creatureId, propertyId) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPropertyChildren(creatureId, propertyId) {
|
||||
const property = getSingleProperty(creatureId, propertyId);
|
||||
if (!property) return [];
|
||||
// This propertyId will always appear in the parent of the children
|
||||
if (loadedCreatures.has(creatureId)) {
|
||||
const creature = loadedCreatures.get(creatureId);
|
||||
const props = [];
|
||||
for (const prop of creature.properties.values()) {
|
||||
if (prop.parent?.id === propertyId) {
|
||||
props.push(prop);
|
||||
}
|
||||
}
|
||||
const cloneProps = EJSON.clone(props);
|
||||
return cloneProps.sort((a, b) => a.order - b.order);
|
||||
} else {
|
||||
return CreatureProperties.find({
|
||||
'parent.id': propertyId,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
}
|
||||
}
|
||||
|
||||
class LoadedCreature {
|
||||
constructor(sub, creatureId) {
|
||||
// This may be called from a subscription, but we don't want the observers
|
||||
|
||||
Reference in New Issue
Block a user