Files
GDevelop/Extensions/Multiplayer/multiplayerVariablesManager.ts

282 lines
9.6 KiB
TypeScript

namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
const debugLogger = new gdjs.Logger('Multiplayer - Debug');
/** @category Multiplayer */
export type MultiplayerVariablesManager = ReturnType<
typeof makeMultiplayerVariablesManager
>;
/**
* @category Multiplayer
*/
export const makeMultiplayerVariablesManager = () => {
const variableOwnershipChangesToSyncAtEndOfFrame: {
[variableNetworkId: string]: {
variableName: string;
sceneNetworkId?: string; // If not defined, the variable is global.
previousVariableOwner: number;
newVariableOwner: number;
};
} = {};
const addVariableOwnershipChangeToSync = function ({
variableNetworkId,
previousVariableOwner,
newVariableOwner,
}: {
variableNetworkId: string;
previousVariableOwner: number;
newVariableOwner: number;
}) {
// If the variable is already planned to be synchronized, update it with the new owner.
if (variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId]) {
variableOwnershipChangesToSyncAtEndOfFrame[
variableNetworkId
].newVariableOwner = newVariableOwner;
return;
}
variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId] = {
variableName: variableNetworkId,
previousVariableOwner: previousVariableOwner,
newVariableOwner: newVariableOwner,
};
};
const getVariableTypeAndNameFromNetworkId = function (
variableNetworkId: string
): { type: string; name: string; containerId: string } {
const parts = variableNetworkId.split('_');
if (parts.length < 2) {
throw new Error(
'Trying to get the variable type from a network id that is not a valid variable network id.'
);
}
return {
type: parts[0] === 'game' ? 'global' : 'scene',
name: parts.slice(1).join('_'),
containerId: parts[0],
};
};
// A variable network id is a combination of the scene network id if it's a scene variable,
// and the variable name, or "game" and the variable name if it's a global variable.
const _guessVariableNetworkIdFromSceneAndGame = function (
variable: gdjs.Variable,
currentScene: gdjs.RuntimeScene
): string | undefined {
const currentSceneVariables = currentScene.getVariables();
if (currentSceneVariables.hasVariable(variable)) {
// Scene variable.
const sceneNetworkId = currentScene.networkId;
if (!sceneNetworkId) {
// Variable is being synchronized but the scene has no networkId yet.
// It should have one assigned as soon as the scene is synchronized.
// Skipping.
debugLogger.info(
'Variable is being synchronized but the scene has no networkId yet.'
);
return;
}
const variableName =
currentSceneVariables.getVariableNameInContainerByLoopingThroughAllVariables(
variable
);
if (!variableName) {
logger.error('Variable is being synchronized but has no name.');
return;
}
return sceneNetworkId + '_' + variableName;
}
const runtimeGame = currentScene.getGame();
const runtimeGameVariables = runtimeGame.getVariables();
if (runtimeGameVariables.hasVariable(variable)) {
// Global variable.
// TODO: prevent returning a networkID if this is not a root variable.
const variableName =
runtimeGameVariables.getVariableNameInContainerByLoopingThroughAllVariables(
variable
);
if (!variableName) {
logger.error('Variable is being synchronized but has no name.');
return;
}
return 'game_' + variableName;
}
logger.error(
'Trying to modify synchronization of a variable that is not a scene or global variable.'
);
return;
};
const getPlayerVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
return variable.getPlayerOwnership();
};
const setPlayerVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable,
newVariablePlayerNumber: number
) {
debugLogger.info(
`Setting ownership of variable to player ${newVariablePlayerNumber}.`
);
if (newVariablePlayerNumber < 0) {
logger.error(
'Invalid player number (' +
newVariablePlayerNumber +
') when setting ownership of a variable.'
);
return;
}
const previousVariablePlayerNumber = variable.getPlayerOwnership();
if (previousVariablePlayerNumber === null) {
logger.error(
'Cannot change ownership of a variable that is not synchronized.'
);
return;
}
variable.setPlayerOwnership(newVariablePlayerNumber);
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
// If the lobby game is not running, do not try to update the ownership over the network,
// as the game may update variable ownerships before the lobby game starts.
if (!gdjs.multiplayer.isLobbyGameRunning()) {
return;
}
if (newVariablePlayerNumber !== currentPlayerNumber) {
// If we are not the new owner, we should not send a message to the host to change the ownership.
// Just return and wait to receive an update message to reconcile the variable.
return;
}
const variableNetworkId = _guessVariableNetworkIdFromSceneAndGame(
variable,
runtimeScene
);
const sceneNetworkId = runtimeScene.networkId;
if (!variableNetworkId || !sceneNetworkId) {
// An error was already logged.
return;
}
const { type: variableType } =
getVariableTypeAndNameFromNetworkId(variableNetworkId);
debugLogger.info(
`Adding variable to be synchronized: ${variableNetworkId} (type: ${variableType}) from owner ${previousVariablePlayerNumber} to ${newVariablePlayerNumber}.`
);
addVariableOwnershipChangeToSync({
variableNetworkId,
previousVariableOwner: previousVariablePlayerNumber,
newVariableOwner: newVariablePlayerNumber,
});
};
const takeVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
setPlayerVariableOwnership(runtimeScene, variable, currentPlayerNumber);
};
const removeVariableOwnership = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
setPlayerVariableOwnership(runtimeScene, variable, 0);
};
const disableVariableSynchronization = function (
runtimeScene: gdjs.RuntimeScene,
variable: gdjs.Variable
) {
variable.disableSynchronization();
};
const handleChangeVariableOwnerMessagesToSend = function () {
if (
!gdjs.multiplayer.isLobbyGameRunning() ||
!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()
) {
return;
}
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
for (const variableNetworkId in variableOwnershipChangesToSyncAtEndOfFrame) {
const variableData =
variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId];
const { messageName, messageData } =
gdjs.multiplayerMessageManager.createChangeVariableOwnerMessage({
variableNetworkId,
variableOwner: variableData.previousVariableOwner,
newVariableOwner: variableData.newVariableOwner,
});
// Before sending the change owner message, if we are becoming the new owner,
// we want to ensure this message is acknowledged, by everyone we're connected to.
if (variableData.newVariableOwner === currentPlayerNumber) {
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const variableOwnerChangedMessageName =
gdjs.multiplayerMessageManager.createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage(
messageName
);
gdjs.multiplayerMessageManager.addExpectedMessageAcknowledgement({
originalMessageName: messageName,
originalData: messageData,
expectedMessageName: variableOwnerChangedMessageName,
otherPeerIds,
// If we are not the host and don't receive an acknowledgement from the host, we should cancel the ownership change.
shouldCancelMessageIfTimesOut: currentPlayerNumber !== 1,
});
}
debugLogger.info('Sending change owner message', messageName);
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
gdjs.multiplayerMessageManager.sendDataTo(
connectedPeerIds,
messageName,
messageData
);
// Remove the variable from the list of variables ownership changes to sync.
delete variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId];
}
};
return {
getVariableTypeAndNameFromNetworkId,
getPlayerVariableOwnership,
setPlayerVariableOwnership,
takeVariableOwnership,
removeVariableOwnership,
disableVariableSynchronization,
handleChangeVariableOwnerMessagesToSend,
};
};
/**
* The MultiplayerVariablesManager used by the game.
* @category Multiplayer
*/
export let multiplayerVariablesManager = makeMultiplayerVariablesManager();
}