From 6162f2fe900d7a7642f776101440f13594949766 Mon Sep 17 00:00:00 2001
From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com>
Date: Tue, 14 Nov 2023 13:55:17 +0200
Subject: [PATCH] Failed attempt at using method calls to manage awaited method
---
.../api/creature/actions/ActiveActions.js | 108 ------------------
.../api/engine/actions/ActionContext.js | 7 +-
.../applyPropertyByType/applyDamage.js | 2 +-
app/imports/api/engine/actions/doAction.js | 34 +++---
.../api/engine/actions/getUserInput.js | 108 ++++++++++++++----
.../ui/creature/actions/ActionDialog.vue | 79 +++++++++++++
.../ui/dialogStack/DialogComponentIndex.js | 2 +
.../ui/properties/viewers/ActionViewer.vue | 9 ++
app/imports/parser/parseTree/roll.js | 44 +++----
.../server/publications/singleCharacter.js | 6 -
10 files changed, 221 insertions(+), 178 deletions(-)
delete mode 100644 app/imports/api/creature/actions/ActiveActions.js
diff --git a/app/imports/api/creature/actions/ActiveActions.js b/app/imports/api/creature/actions/ActiveActions.js
deleted file mode 100644
index bbfe4731..00000000
--- a/app/imports/api/creature/actions/ActiveActions.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import SimpleSchema from 'simpl-schema';
-import { ValidatedMethod } from 'meteor/mdg:validated-method';
-import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
-
-// Actions are creature actions that have been partially executed and not yet resolved
-// They require some user input to progress
-let ActiveActions = new Mongo.Collection('activeActions');
-
-let ActiveActionSchema = new SimpleSchema({
- _id: {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- },
- // Which creature is taking the action
- creatureId: {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- },
- // The user who began taking the action
- userId: {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- },
- // Requests for user input
- questions: {
- type: Object,
- blackbox: true,
- optional: true,
- },
- // User responses
- answers: {
- type: Object,
- blackbox: true,
- optional: true,
- },
-});
-
-ActiveActions.attachSchema(ActiveActionSchema);
-
-export default ActiveActions;
-
-export const answerAction = new ValidatedMethod({
- name: 'activeActions.answer',
- validate: null /*new SimpleSchema({
- activeActionId: SimpleSchema.RegEx.Id,
- answers: {
- type: Object,
- blackbox: true,
- },
- }).validator()*/,
- applyOptions: {
- throwStubExceptions: false,
- },
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({ activeActionId, answers }) {
- return ActiveActions.update({}, {
- $set: { answers },
- $unset: { questions: 1 },
- });
- const action = ActiveActions.findOne(activeActionId);
- // Permissions
- if (action.userId !== this.userId) {
- throw new Meteor.Error('Permission denied', 'You do not own this action');
- }
- return ActiveActions.update(activeActionId, {
- $set: { answers },
- $unset: { questions: 1 },
- });
- },
-});
-
-export const removeAction = new ValidatedMethod({
- name: 'activeActions.remove',
- validate: new SimpleSchema({
- activeActionId: SimpleSchema.RegEx.Id,
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({ activeActionId }) {
- const action = ActiveActions.findOne(activeActionId);
- // Permissions
- if (action.userId !== this.userId) {
- throw new Meteor.Error('Permission denied', 'You do not own this action');
- }
- return ActiveActions.remove(activeActionId);
- },
-});
-
-// TODO remove this
-export const removeAllActions = new ValidatedMethod({
- name: 'activeActions.removeAll',
- validate: null,
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run() {
- return ActiveActions.remove({});
- },
-});
diff --git a/app/imports/api/engine/actions/ActionContext.js b/app/imports/api/engine/actions/ActionContext.js
index 81eb250e..7de72dc7 100644
--- a/app/imports/api/engine/actions/ActionContext.js
+++ b/app/imports/api/engine/actions/ActionContext.js
@@ -5,11 +5,12 @@ import {
import { groupBy, remove } from 'lodash';
export default class ActionContext {
- constructor(creatureId, targetIds = [], method, activeActionId) {
+ constructor(creatureId, targetIds = [], method, invocationId) {
// Get the creature
this.creature = getCreature(creatureId)
- // Store an active action ID for pausing/resuming this action
- this.activeActionId = activeActionId
+ // Store the details for pausing for user interaction
+ this.invocationId = invocationId;
+ this.userInputStep = 0;
if (!this.creature) {
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
index 58385f1e..9bda5d10 100644
--- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
+++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js
@@ -37,7 +37,7 @@ export default function applyDamage(node, actionContext) {
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string
- recalculateCalculation(prop.amount, actionContext, undefined, 'compile');
+ recalculateCalculation(prop.amount, actionContext, 'compile');
const { result: rolled } = resolve('roll', prop.amount.valueNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js
index 5cbd3cc4..74db6ef4 100644
--- a/app/imports/api/engine/actions/doAction.js
+++ b/app/imports/api/engine/actions/doAction.js
@@ -10,7 +10,6 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import applyProperty from './applyProperty.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
-import ActiveActions from '/imports/api/creature/actions/ActiveActions';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -31,24 +30,27 @@ const doAction = new ValidatedMethod({
blackbox: true,
optional: true,
},
+ invocationId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ optional: true,
+ }
}).validator(),
+ applyOptions: {
+ throwStubExceptions: false,
+ },
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
- async run({ actionId, targetIds = [], scope }) {
+ async run({ actionId, targetIds = [], scope, invocationId }) {
+ console.log('do Action running');
// Get action context
let action = CreatureProperties.findOne(actionId);
const creatureId = action.ancestors[0].id;
- // TODO remove this
- // For testing, remove all other active actions before inserting this one
- ActiveActions.remove({});
- const activeActionId = await ActiveActions.insertAsync({
- creatureId,
- userId: this.userId,
- });
- const actionContext = new ActionContext(creatureId, targetIds, this, activeActionId);
+
+ const actionContext = new ActionContext(creatureId, targetIds, this, invocationId);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
@@ -67,11 +69,13 @@ const doAction = new ValidatedMethod({
await doActionWork({ properties, ancestors, actionContext, methodScope: scope });
// Recompute all involved creatures
- Creatures.update({
- _id: { $in: [creatureId, ...targetIds] }
- }, {
- $set: { dirty: true },
- });
+ if (Meteor.isServer) {
+ Creatures.updateAsync({
+ _id: { $in: [creatureId, ...targetIds] }
+ }, {
+ $set: { dirty: true },
+ });
+ }
},
});
diff --git a/app/imports/api/engine/actions/getUserInput.js b/app/imports/api/engine/actions/getUserInput.js
index 0ee4bca5..c9a7134c 100644
--- a/app/imports/api/engine/actions/getUserInput.js
+++ b/app/imports/api/engine/actions/getUserInput.js
@@ -1,28 +1,90 @@
-import ActiveActions from '/imports/api/creature/actions/ActiveActions';
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import { set } from 'lodash';
+
+// Reminder: throwStubExceptions: true is the default, and only
+// possible when run() is not async
+// For async run() stub exceptions never stop the client from sending
+// the call to the server
+
+// Dict of invocationId: {steps: {earlyAnswers, resolve, reject}}
+// either resolve functions waiting for the user's input or early answers that were provided
+// before the resolves could be set up
+let userInputRequests = {};
+let provideUserInput;
+
+if (Meteor.isClient) {
+ provideUserInput = function (invocationId, step, answers, callback) {
+ Meteor.call('answerUserInputRequest', { invocationId, step, answers }, callback);
+ // Do the same work on the client without using a stub
+ answerInputRequestWork({ invocationId, step, answers });
+ }
+}
+
+export { userInputRequests, provideUserInput };
export default async function getUserInput(questions, actionContext) {
- const activeActionId = actionContext.activeActionId;
- // Set the questions on the active action
- ActiveActions.update(activeActionId, {
- $set: { questions },
- $unset: { answers: 1 },
- });
- // Wait for answers
+ // get the invocation details from the action context
+ const invocationId = actionContext.invocationId;
+ const step = actionContext.userInputStep;
+ actionContext.userInputStep += 1; // increment userInput step every time
+
+ // If the answers are already waiting, just return them
+ if (userInputRequests[invocationId]?.[step]?.earlyAnswers) {
+ return userInputRequests[invocationId][step].earlyAnswers;
+ }
+ // On the client, store the questions to be answered
+ if (Meteor.isClient) {
+ set(userInputRequests, `${invocationId}[${step}]`, { questions });
+ }
+ // Create a place for the answers to go when they are provided
return new Promise((resolve, reject) => {
- const observerHandle = ActiveActions.find({
- _id: activeActionId
- }).observeChanges({
- changed(id, fields) {
- // Only watch for answers
- if (!fields.answers) return;
- // Stop watching
- observerHandle.stop();
- // Give answers
- resolve(fields.answers);
- },
- removed() {
- reject('Active action was deleted')
- },
- });
+ set(userInputRequests, `${invocationId}[${step}]`, { resolve, reject });
+ });
+}
+
+function answerInputRequestWork({ invocationId, step, answers }) {
+ console.log('running answerUserInputRequest');
+ const invocation = userInputRequests[invocationId];
+ if (!invocation) {
+ // Call order on the server is guaranteed, so the invocation must have been created
+ // Before we can update it
+ throw new Meteor.Error('Not found', 'The method this answer is updating does not exist');
+ }
+ if (invocation[step]?.resolve) {
+ // If there is a resolve waiting for this response, provide it
+ invocation[step].resolve(answers);
+ } else {
+ // Otherwise just store the response as early answers
+ invocation[step] = {
+ earlyAnswers: answers
+ };
+ }
+}
+
+if (Meteor.isServer) {
+ // This function is not defined on the client so that it has no stub function
+ // This allows it to be called while still simulating an awaited async method
+ // See https://guide.meteor.com/2.8-migration.html#the-limitations
+ new ValidatedMethod({
+ name: 'answerUserInputRequest',
+ validate: new SimpleSchema({
+ invocationId: SimpleSchema.RegEx.Id,
+ step: SimpleSchema.Integer,
+ answers: {
+ type: Object,
+ blackbox: true,
+ },
+ }).validator(),
+ applyOptions: {
+ throwStubExceptions: false,
+ },
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 20,
+ timeInterval: 5000,
+ },
+ run: answerInputRequestWork,
});
}
diff --git a/app/imports/client/ui/creature/actions/ActionDialog.vue b/app/imports/client/ui/creature/actions/ActionDialog.vue
index e69de29b..ea0e30d6 100644
--- a/app/imports/client/ui/creature/actions/ActionDialog.vue
+++ b/app/imports/client/ui/creature/actions/ActionDialog.vue
@@ -0,0 +1,79 @@
+
+
+
+
+ Action
+
+
+
+ content
+
+
+ Cancel
+
+
+
+ Step
+
+
+ Apply all
+
+
+
+
+
diff --git a/app/imports/client/ui/dialogStack/DialogComponentIndex.js b/app/imports/client/ui/dialogStack/DialogComponentIndex.js
index 471d3b6a..acfe8c22 100644
--- a/app/imports/client/ui/dialogStack/DialogComponentIndex.js
+++ b/app/imports/client/ui/dialogStack/DialogComponentIndex.js
@@ -1,4 +1,5 @@
// Load commonly used dialogs immediately
+import ActionDialog from '/imports/client/ui/creature/actions/ActionDialog.vue';
import InsertPropertyDialog from '/imports/client/ui/properties/InsertPropertyDialog.vue';
import CharacterCreationDialog from '/imports/client/ui/creature/character/CharacterCreationDialog.vue';
import CastSpellWithSlotDialog from '/imports/client/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
@@ -33,6 +34,7 @@ const ShareDialog = () => import('/imports/client/ui/sharing/ShareDialog.vue');
const UsernameDialog = () => import('/imports/client/ui/user/UsernameDialog.vue');
export default {
+ ActionDialog,
InsertPropertyDialog,
ArchiveDialog,
CastSpellWithSlotDialog,
diff --git a/app/imports/client/ui/properties/viewers/ActionViewer.vue b/app/imports/client/ui/properties/viewers/ActionViewer.vue
index de20f2fe..feec1e52 100644
--- a/app/imports/client/ui/properties/viewers/ActionViewer.vue
+++ b/app/imports/client/ui/properties/viewers/ActionViewer.vue
@@ -187,7 +187,16 @@ export default {
},
},
methods: {
+
doAction() {
+ this.$store.commit('pushDialogStack', {
+ component: 'action-dialog',
+ elementId: 'do-action-button',
+ data: {
+ propId: this.model._id,
+ },
+ });
+ return;
if (this.model.type === 'action') {
this.doActionLoading = true;
doAction.call({ actionId: this.model._id }, error => {
diff --git a/app/imports/parser/parseTree/roll.js b/app/imports/parser/parseTree/roll.js
index c4e028c4..f2d7c491 100644
--- a/app/imports/parser/parseTree/roll.js
+++ b/app/imports/parser/parseTree/roll.js
@@ -5,51 +5,51 @@ import rollDice from '/imports/parser/rollDice.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const rollNode = {
- create({left, right}) {
+ create({ left, right }) {
return {
parseType: 'roll',
left,
right,
};
},
- compile(node, scope, context){
- const {result: left} = resolve('compile', node.left, scope, context);
- const {result: right} = resolve('compile', node.right, scope, context);
+ compile(node, scope, context) {
+ const { result: left } = resolve('compile', node.left, scope, context);
+ const { result: right } = resolve('compile', node.right, scope, context);
return {
- result: rollNode.create({left, right}),
+ result: rollNode.create({ left, right }),
context,
};
},
- toString(node){
+ toString(node) {
if (
node.left.valueType === 'number' && node.left.value === 1
- ){
+ ) {
return `d${toString(node.right)}`;
} else {
return `${toString(node.left)}d${toString(node.right)}`;
}
},
- roll(node, scope, context){
- const {result: left} = resolve('reduce', node.left, scope, context);
- const {result: right} = resolve('reduce', node.right, scope, context);
- if (left.valueType !== 'number' && !Number.isInteger(left.value)){
+ roll(node, scope, context) {
+ const { result: left } = resolve('reduce', node.left, scope, context);
+ const { result: right } = resolve('reduce', node.right, scope, context);
+ if (left.valueType !== 'number' && !Number.isInteger(left.value)) {
return errorResult('Number of dice is not an integer', node, context);
}
- if (right.valueType !== 'number' && !Number.isInteger(right.value)){
+ if (right.valueType !== 'number' && !Number.isInteger(right.value)) {
return errorResult('Dice size is not an integer', node, context);
}
let number = left.value;
- if (context.options.doubleRolls){
+ if (context.options.doubleRolls) {
number *= 2;
}
- if (number > STORAGE_LIMITS.diceRollValuesCount){
+ if (number > STORAGE_LIMITS.diceRollValuesCount) {
const message = `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`;
return errorResult(message, node, context);
}
let diceSize = right.value;
let values = rollDice(number, diceSize);
- if (context){
- context.rolls.push({number, diceSize, values});
+ if (context) {
+ context.rolls.push({ number, diceSize, values });
}
return {
result: rollArray.create({
@@ -60,18 +60,18 @@ const rollNode = {
context
};
},
- reduce(node, scope, context){
- const {result} = rollNode.roll(node, scope, context);
+ reduce(node, scope, context) {
+ const { result } = rollNode.roll(node, scope, context);
return resolve('reduce', result, scope, context);
},
- traverse(node, fn){
+ traverse(node, fn) {
fn(node);
traverse(node.left, fn);
traverse(node.right, fn);
},
- map(node, fn){
+ map(node, fn) {
const resultingNode = fn(node);
- if (resultingNode === node){
+ if (resultingNode === node) {
node.left = map(node.left, fn);
node.right = map(node.right, fn);
}
@@ -79,7 +79,7 @@ const rollNode = {
},
}
-function errorResult(message, node, context){
+function errorResult(message, node, context) {
context.error(message);
return {
result: error.create({ node, error: message }),
diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js
index b4ff4dc2..b41c9dcd 100644
--- a/app/imports/server/publications/singleCharacter.js
+++ b/app/imports/server/publications/singleCharacter.js
@@ -6,7 +6,6 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
-import ActiveActions from '/imports/api/creature/actions/ActiveActions';
import { loadCreature } from '/imports/api/engine/loadCreatures.js';
let schema = new SimpleSchema({
@@ -55,11 +54,6 @@ Meteor.publish('singleCharacter', function (creatureId) {
limit: 20,
sort: { date: -1 },
}),
- ActiveActions.find({
- creatureId,
- }, {
- limit: 10,
- }),
// Also publish the owner's username
Meteor.users.find(permissionCreature.owner, {
fields: {