It is a shared Meteor method.
Meteor.methods({
Inventory_Transfer: function (gameId, item, origin, destination, quantityToTransfer) {
if (! Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'User is not logged in.');
}
let game = Games.findOne(gameId);
if (_.isEmpty(game)) {
throw new Meteor.Error('invalid-input', 'Invalid gameId.');
}
if (Meteor.userId() != game._host && game._players.indexOf(Meteor.userId()) == -1) {
throw new Meteor.Error('not-authorized', 'User is not playing this game.');
}
item = utility.loadObject(item);
if (!(item instanceof app.classes.Item)) {
throw new Meteor.Error('invalid-input', 'Cannot add non-items to an Inventory.');
}
let originObject = game;
if (_.startsWith(origin, 'character.')) {
let characterId = origin.slice(10, origin.indexOf('.', 10));
originObject = Characters.findOne({_game: gameId, _id: characterId});
if (_.isEmpty(originObject)) {
throw new Meteor.Error('invalid-input', 'Invalid origin.');
}
} else if (_.startsWith(origin, 'party.')) {
originObject = game.party;
} else if (_.startsWith(origin, 'currentGameEventState.')) {
originObject = game.currentGameEventState;
}
origin = origin.slice(origin.lastIndexOf('.') + 1)
let destinationObject = game;
if (_.startsWith(destination, 'character.')) {
let characterId = destination.slice(10, destination.indexOf('.', 10));
destinationObject = Characters.findOne({_game: gameId, _id: characterId});
if (_.isEmpty(destinationObject)) {
throw new Meteor.Error('invalid-input', 'Invalid destination.');
}
} else if (_.startsWith(destination, 'party.')) {
destinationObject = game.party;
} else if (_.startsWith(destination, 'currentGameEventState.')) {
destinationObject = game.currentGameEventState;
}
destination = destination.slice(destination.lastIndexOf('.') + 1)
if (_.isEqual(originObject, destinationObject) && (origin == destination)) {
return; // ignore attempts to transfer to and from the same place
}
if (!(originObject[origin] instanceof app.classes.Inventory) || !(destinationObject[destination] instanceof app.classes.Inventory)) {
throw new Meteor.Error('invalid-input', 'Must transfer from an inventory, to an inventory.');
}
if (!originObject[origin].hasItem(item)) {
throw new Meteor.Error('invalid-input', 'Origin is missing item for transfer.');
}
quantityToTransfer = math.min(quantityToTransfer, item.quantity);
let itemToTransfer = utility.loadObject(_.cloneDeep(item));
itemToTransfer.quantity = quantityToTransfer;
originObject[origin].removeItem(itemToTransfer);
destinationObject[destination].addItem(itemToTransfer);
// On both client and server, these are never incorrect.
console.log(game.party.inventory);
console.log(game.currentGameEventState.inventory);
if (Meteor.isServer) {
if (originObject instanceof app.classes.Character) {
Characters.update({ _id: originObject._id }, {
$set: {
[origin]: originObject[origin]
}
});
}
if (destinationObject instanceof app.classes.Character) {
Characters.update({ _id: destinationObject._id }, {
$set: {
[destination]: destinationObject[destination]
}
});
}
if (!(originObject instanceof app.classes.Character) || !(destinationObject instanceof app.classes.Character)) {
Games.update(gameId, game);
}
}
}
});
If I run this without the final Games.update()
call, then the client-side simulation results in game.currentGameEventState.inventory
being an empty object instead of an object with an array .items
. The client template feeds a jQuery UI DataTable with that .items
array.
let oldData = templateInstance.data.tableData;
templateInstance.autorun(function () {
let templateInstance = Template.instance();
let templateData = Template.currentData();
if (_.isArray(templateData.tableData) && !_.isEqual(templateData.tableData, oldData)) {
if (_.isEmpty(templateData.tableData)) {
// This triggers once per transfer method call, even when nothing is supposed to be empty.
console.log(templateData.tableClass + ' data is empty.');
}
let activeItemRow = $('table.itemTable tbody tr.selected');
let activeItemTable = activeItemRow.parents('table.itemTable.js-isDataTable');
let thisTable = templateInstance.$('table.itemTable.js-isDataTable');
let reselectItem = false;
if (activeItemTable[0] == thisTable[0]) {
let itemData = activeItemTable.dataTable().api().row('.selected').data();
reselectItem = itemData;
}
thisTable.dataTable().api().clear().rows.add(templateData.tableData).draw();
if (reselectItem) {
thisTable.dataTable().api().row(function ( index, data, node ) {
return data.matches(reselectItem);
}).select();
}
oldData = templateData.tableData;
}
});
Without the isServer
protection, the reselection fails (because the new data might actually be empty), the table contents blink (because they are temporarily empty and the table is redrawn once empty and once full again), and I thought that there was a third visual artifact but I’m failing to remember or replicate it right now.
I don’t believe that this should require different functionality between the client and server. I have other methods which make collection document updates without trouble, although they are probably all simpler (e.g. updating a specific single string or number value rather than an array inside an object at a dynamic path location).