Began rebuilding computation engine to be dependency graph centric
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { get } from 'lodash';
|
||||
|
||||
export function applyFnToKey(doc, key, fn){
|
||||
if (key.includes('$.')){
|
||||
applyToArrayKey(doc, key, fn);
|
||||
} else {
|
||||
applyToSingleKey(doc, key, fn);
|
||||
}
|
||||
}
|
||||
|
||||
function applyToSingleKey(doc, key, fn){
|
||||
// call the function with the current value and document for context
|
||||
fn(doc, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function to all instances in a document key
|
||||
* key.$.with.$.subdocs will apply to all key[i...n].with[j...m].subdocs
|
||||
*/
|
||||
function applyToArrayKey(doc, key, fn){
|
||||
const keySplit = key.split('.$');
|
||||
|
||||
// Stack based depth first traversal of arrays
|
||||
const stack = [{
|
||||
array: get(doc, keySplit[0]),
|
||||
paths: keySplit.slice(1),
|
||||
currentPath: keySplit[0],
|
||||
indices: [],
|
||||
}];
|
||||
while(stack.length){
|
||||
const state = stack.pop();
|
||||
for (let index in state.array.length){
|
||||
const currentPath = `${state.currentPath}[${index}]${state.paths[0]}`
|
||||
if (state.paths.length == 1){
|
||||
applyToSingleKey(doc, currentPath, fn);
|
||||
} else {
|
||||
stack.push({
|
||||
array: get(doc, currentPath),
|
||||
paths: state.paths.slice(1),
|
||||
currentPath,
|
||||
indices: [...state.indices, index],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import CreatureProperties,
|
||||
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
|
||||
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||
import applyFnToKey from '/imports/api/creature/computation/newEngine/applyFnToKey.js';
|
||||
import { cloneDeep, unset } from 'lodash';
|
||||
import { prettifyParseError, parse } from '/imports/parser/parser.js';
|
||||
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
|
||||
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
|
||||
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
|
||||
import createGraph from 'ngraph.graph';
|
||||
import findAncestorByType from 'imports/api/creature/computation/newEngine/findAncestorByType.js';
|
||||
|
||||
/**
|
||||
* Store index of properties
|
||||
* recompute static tree-based enabled/disabled status
|
||||
* Build a dependency graph
|
||||
* id -> id dependencies for docs that rely on other docs directly
|
||||
* id -> variable deps for docs that rely on a variable's value
|
||||
* TODO:
|
||||
* variable -> id deps for variables that are impacted by docs
|
||||
* Depth first traversal or dependency graph to:
|
||||
* Find loops in the dependency graph
|
||||
* resolve variables in dependency order
|
||||
*/
|
||||
|
||||
export default function computeCreature(creatureId){
|
||||
let properties = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
'removed': {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
|
||||
const originalPropsById = {};
|
||||
const propsById = {};
|
||||
const propsByType = {};
|
||||
|
||||
// Process the properties one by one
|
||||
properties.forEach(prop => {
|
||||
// Store the prop by Id and Type
|
||||
originalPropsById[prop._id] = cloneDeep(prop);
|
||||
propsById[prop._id] = prop;
|
||||
if (!propsByType[prop.type]) propsByType[prop.type] = [];
|
||||
propsByType[prop.type].push(prop);
|
||||
|
||||
// Store the prop in the dependency graph
|
||||
dependencyGraph.addNode(prop._id, prop);
|
||||
|
||||
// Remove all computed only fields
|
||||
computedOnlySchemas[prop.type]._schemaKeys.forEach(key =>
|
||||
applyFnToKey(prop, key, unset)
|
||||
);
|
||||
|
||||
// Remove all denormalised fields
|
||||
denormSchema._schemaKeys.forEach(key =>
|
||||
applyFnToKey(prop, key, unset)
|
||||
);
|
||||
|
||||
// Add a place to store all the computation details
|
||||
prop._computationDetails = {
|
||||
calculations: [],
|
||||
toggleAncestors: [],
|
||||
};
|
||||
|
||||
// parse every calculation field
|
||||
computedSchemas[prop.type]._schemaKeys.forEach( key => {
|
||||
if (key.slice(-11) !== 'calculation') return;
|
||||
const calcKey = key.sclice(0, -11);
|
||||
applyFnToKey(prop, calcKey, calcObj => {
|
||||
// Store a reference to all the calculations
|
||||
prop._computationDetails.calculations.push(calcObj);
|
||||
// Parse the calculation
|
||||
parseCalculation(calcObj);
|
||||
return calcObj;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Dependency graph where edge(a, b) means a depends on b
|
||||
const dependencyGraph = createGraph();
|
||||
// Build graph now that all props are stored
|
||||
properties.forEach(prop => {
|
||||
linkDependencies(dependencyGraph, prop, propsById);
|
||||
});
|
||||
|
||||
// Process the properties in tree format
|
||||
let creatureTree = nodeArrayToTree(properties);
|
||||
walkDown(creatureTree, node => {
|
||||
denormaliseInactiveStatus(node);
|
||||
inheritToggleDependencies(node);
|
||||
});
|
||||
}
|
||||
|
||||
function walkDown(tree, callback){
|
||||
let stack = [...tree];
|
||||
while(stack.length){
|
||||
let node = stack.pop();
|
||||
callback(node);
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
|
||||
function denormaliseInactiveStatus(node){
|
||||
const prop = node.node;
|
||||
if (isActive(prop)) return;
|
||||
prop.inactive = true;
|
||||
prop.deactivatedBySelf = true;
|
||||
// Mark children as inactive due to ancestor
|
||||
walkDown(node.children, child => {
|
||||
child.node.inactive = true;
|
||||
child.node.deactivatedByAncestor = true;
|
||||
});
|
||||
}
|
||||
|
||||
function isActive(prop){
|
||||
if (prop.disabled) return false;
|
||||
switch (prop.type){
|
||||
case 'buff': return !!prop.applied;
|
||||
case 'item': return !!prop.equipped;
|
||||
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
function inheritToggleDependencies(node, dependencyGraph){
|
||||
const prop = node.node;
|
||||
// Only for toggles that aren't inactive and aren't set to enabled or disabled
|
||||
if (
|
||||
prop.inactive ||
|
||||
prop.type !== 'toggle' ||
|
||||
prop.disabled ||
|
||||
prop.enabled
|
||||
) return;
|
||||
walkDown(node.children, child => {
|
||||
child.node._computationDetails.toggleAncestors.push(prop._id);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, prop.condition);
|
||||
});
|
||||
}
|
||||
|
||||
function parseCalculation(calcObj){
|
||||
let calculation = calcObj.calculation || '';
|
||||
try {
|
||||
calcObj._parsedCalculation = parse(calculation);
|
||||
} catch (e) {
|
||||
let error = prettifyParseError(e);
|
||||
calcObj.errors ?
|
||||
calcObj.errors.push(error) :
|
||||
calcObj.errors = [error];
|
||||
calcObj._parsedCalculation = new ErrorNode({error});
|
||||
}
|
||||
}
|
||||
|
||||
function linkDependencies(dependencyGraph, prop, propsById){
|
||||
let variableNames = [];
|
||||
prop._computationDetails.calculations.forEach(calcObj => {
|
||||
calcObj._parsedCalculation.travese(node => {
|
||||
if (node instanceof SymbolNode || node instanceof AccessorNode){
|
||||
if (node.name[0] !== '#'){
|
||||
dependencyGraph.addLink(prop._id, node.name, calcObj);
|
||||
} else {
|
||||
let ancestorProp = findAncestorByType(
|
||||
prop, node.name.slice(1), propsById
|
||||
);
|
||||
if (!ancestorProp) return;
|
||||
dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return variableNames;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function findAncestorByType(prop, type, propsById){
|
||||
if (!prop || !prop.ancestors) return;
|
||||
let ancestor;
|
||||
for (let i = prop.ancestors.length - 1; i >= 0; i--){
|
||||
ancestor = propsById[prop.ancestors[i].id];
|
||||
if (ancestor && ancestor.type === type){
|
||||
return ancestor;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user