Files
0ad/binaries/data/mods/public/gui/session/messages.js
T
Atrik 0322d039f6 Fix player data not updating on diplomacy change
f39e71cca0 moved the player data refresh after a diplomacy change
into a diplomacy colors change handler, which only runs when
diplomacy colors are enabled. With them disabled, the diplomacy
dialog's stance buttons didn't reflect the new stances.

Refresh the player data directly in the diplomacy notification
handler, before the diplomacy colors are recomputed, and remove
the now redundant diplomacy colors handler registration.
2026-09-14 11:35:19 +02:00

584 lines
14 KiB
JavaScript

/**
* The number of seconds we monitor to rate limit flares.
*/
var g_FlareRateLimitScope = 6;
/**
* The maximum allowed number of flares within g_FlareRateLimitScope seconds.
* This should be a bit larger than the number of flares that can be sent in theory by using the GUI.
*/
var g_FlareRateLimitMaximumFlares = 16;
/**
* Contains the arrival timestamps the flares of the last g_FlareRateLimitScope seconds.
*/
var g_FlareRateLimitLastTimes = [];
/**
* These handlers are called everytime a client joins or disconnects.
*/
var g_PlayerAssignmentsChangeHandlers = new Set();
/**
* These handlers are called when the ceasefire time has run out.
*/
var g_CeasefireEndedHandlers = new Set();
/**
* These handlers are fired when the match is networked and
* the current client established the connection, authenticated,
* finished the loading screen, starts or finished synchronizing after a rejoin.
* The messages are constructed in NetClient.cpp.
*/
var g_NetworkStatusChangeHandlers = new Set();
/**
* These handlers are triggered whenever a client finishes the loading screen.
*/
var g_ClientsLoadingHandlers = new Set();
/**
* These handlers are fired if the server informed the players that the networked game is out of sync.
*/
var g_NetworkOutOfSyncHandlers = new Set();
/**
* Handle all netmessage types that can occur.
*/
var g_NetMessageTypes = {
"netstatus": msg =>
{
handleNetStatusMessage(msg);
},
"netwarn": msg =>
{
addNetworkWarning(msg);
},
"out-of-sync": msg =>
{
for (const handler of g_NetworkOutOfSyncHandlers)
handler(msg);
},
"players": msg =>
{
handlePlayerAssignmentsMessage(msg);
},
"paused": msg =>
{
g_PauseControl.setClientPauseState(msg.guid, msg.pause);
},
"clients-loading": msg =>
{
for (const handler of g_ClientsLoadingHandlers)
handler(msg.guids);
},
"rejoined": msg =>
{
addChatMessage({
"type": "rejoined",
"guid": msg.guid
});
},
"kicked": msg =>
{
addChatMessage({
"type": "kicked",
"username": msg.username,
"banned": msg.banned
});
},
"chat": msg =>
{
addChatMessage({
"type": "message",
"guid": msg.guid,
"text": msg.text
});
},
"flare": msg =>
{
handleFlare(msg);
},
"gamesetup": msg => {}, // Needed for autostart
"start": msg => {}
};
var g_PlayerStateMessages = {
"won": translate("You have won!"),
"defeated": translate("You have been defeated!")
};
var g_LastAttack;
/**
* Defines how the GUI reacts to notifications that are sent by the simulation.
* Don't open new pages (message boxes) here! Otherwise further notifications
* handled in the same turn can't access the GUI objects anymore.
*/
var g_NotificationsTypes =
{
"aichat": function(notification, player)
{
const message = {
"type": "message",
"text": notification.message,
"guid": findGuidForPlayerID(player) || -1,
"player": player,
"translate": true
};
if (notification.translateParameters)
{
message.translateParameters = notification.translateParameters;
message.parameters = notification.parameters;
colorizePlayernameParameters(notification.parameters);
}
addChatMessage(message);
},
"defeat": function(notification, player)
{
handlePlayersFinished(notification.allies, notification.message, false);
},
"won": function(notification, player)
{
handlePlayersFinished(notification.allies, notification.message, true);
},
"diplomacy": function(notification, player)
{
updatePlayerData();
g_DiplomacyColors.onDiplomacyChange();
addChatMessage({
"type": "diplomacy",
"sourcePlayer": player,
"targetPlayer": notification.targetPlayer,
"status": notification.status
});
},
"ceasefire-ended": function(notification, player)
{
for (const handler of g_CeasefireEndedHandlers)
handler();
},
"tutorial": function(notification, player)
{
g_TutorialManager.handleNotification(notification);
},
"tribute": function(notification, player)
{
addChatMessage({
"type": "tribute",
"sourcePlayer": notification.donator,
"targetPlayer": player,
"amounts": notification.amounts
});
},
"barter": function(notification, player)
{
addChatMessage({
"type": "barter",
"player": player,
"amountGiven": notification.amountGiven,
"amountGained": notification.amountGained,
"resourceGiven": notification.resourceGiven,
"resourceGained": notification.resourceGained
});
},
"spy-response": function(notification, player)
{
g_DiplomacyDialog.onSpyResponse(notification, player);
if (notification.entity && g_ViewedPlayer == player && (!g_IsObserver || g_FollowPlayer))
{
g_DiplomacyDialog.close();
setCameraFollow(notification.entity);
}
},
"attack": function(notification, player)
{
if (player != g_ViewedPlayer)
return;
// Focus camera on attacks
if (g_FollowPlayer)
{
setCameraFollow(notification.target);
g_Selection.reset();
if (notification.target)
g_Selection.addList([notification.target]);
}
g_LastAttack = { "target": notification.target, "position": notification.position };
if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true")
return;
addChatMessage({
"type": "attack",
"player": player,
"attacker": notification.attacker,
"target": notification.target,
"position": notification.position,
"targetIsDomesticAnimal": notification.targetIsDomesticAnimal
});
},
"phase": function(notification, player)
{
addChatMessage({
"type": "phase",
"player": player,
"phaseName": notification.phaseName,
"phaseState": notification.phaseState
});
},
"dialog": function(notification, player)
{
if (player == Engine.GetPlayerID())
openDialog(notification.dialogName, notification.data, player);
},
"playercommand": function(notification, player)
{
// For observers, focus the camera on units commanded by the selected player
if (!g_FollowPlayer || player != g_ViewedPlayer)
return;
const cmd = notification.cmd;
// Ignore commands executed because of units following rally points
if (cmd.fromRallyPoint)
return;
const entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]);
// Focus the structure to build.
if (cmd.type == "repair")
{
const targetState = GetEntityState(cmd.target);
if (targetState)
Engine.CameraMoveTo(targetState.position.x, targetState.position.z);
}
else if (cmd.type == "delete-entities" && notification.position)
Engine.CameraMoveTo(notification.position.x, notification.position.y);
// Focus commanded entities, but don't lose previous focus when training units
else if (cmd.type != "train" && cmd.type != "research" && entState)
setCameraFollow(cmd.entities[0]);
if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1)
DrawTargetMarker(cmd);
// Select units affected by that command
let selection = [];
if (cmd.entities)
selection = cmd.entities;
if (cmd.target)
selection.push(cmd.target);
// Allow gaia in selection when gathering
g_Selection.reset();
g_Selection.addList(selection, false, cmd.type == "gather");
},
"play-tracks": function(notification, player)
{
if (notification.lock)
{
global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track })));
global.music.setState(global.music.states.CUSTOM);
}
global.music.setLocked(notification.lock);
}
};
function registerPlayerAssignmentsChangeHandler(handler)
{
g_PlayerAssignmentsChangeHandlers.add(handler);
}
function registerCeasefireEndedHandler(handler)
{
g_CeasefireEndedHandlers.add(handler);
}
function registerNetworkOutOfSyncHandler(handler)
{
g_NetworkOutOfSyncHandlers.add(handler);
}
function registerNetworkStatusChangeHandler(handler)
{
g_NetworkStatusChangeHandlers.add(handler);
}
function registerClientsLoadingHandler(handler)
{
g_ClientsLoadingHandlers.add(handler);
}
function findGuidForPlayerID(playerID)
{
return Object.keys(g_PlayerAssignments).find(guid => g_PlayerAssignments[guid].player == playerID);
}
/**
* Processes all pending notifications sent from the GUIInterface simulation component.
*/
function handleNotifications()
{
for (const notification of Engine.GuiInterfaceCall("GetNotifications"))
{
if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type])
{
error("Invalid GUI notification: " + uneval(notification));
continue;
}
for (const player of notification.players)
g_NotificationsTypes[notification.type](notification, player);
}
}
function focusAttack(attack)
{
if (!attack)
return;
const entState = attack.target && GetEntityState(attack.target);
if (entState && hasClass(entState, "Unit"))
setCameraFollow(attack.target);
else
{
const position = attack.position;
Engine.SetCameraTarget(position.x, position.y, position.z);
}
}
/**
* Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
* Saves the received object to mainlog.html.
*/
async function handleNetMessages()
{
while (true)
{
const msg = await Engine.PollNetworkClient();
if (!msg)
return;
log("Net message: " + uneval(msg));
if (g_NetMessageTypes[msg.type])
g_NetMessageTypes[msg.type](msg);
else
error("Unrecognized net message type '" + msg.type + "'");
}
}
function handleNetStatusMessage(message)
{
if (g_Disconnected)
return;
g_IsNetworkedActive = message.status == "active";
if (message.status == "disconnected")
{
g_Disconnected = true;
closeOpenDialogs();
}
for (const handler of g_NetworkStatusChangeHandlers)
handler(message);
}
function handlePlayerAssignmentsMessage(message)
{
for (const guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
onClientLeave(guid);
const joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]);
g_PlayerAssignments = message.newAssignments;
joins.forEach(guid =>
{
onClientJoin(guid);
});
for (const handler of g_PlayerAssignmentsChangeHandlers)
handler();
// TODO: use subscription instead
updateGUIObjects();
}
function handlePlayersFinished(players, message, won)
{
addChatMessage({
"type": "playerstate",
"message": message,
"players": players
});
for (const handler of g_PlayerFinishedHandlers)
handler(players, won);
}
function onClientJoin(guid)
{
const playerID = g_PlayerAssignments[guid].player;
if (g_Players[playerID])
{
g_Players[playerID].guid = guid;
g_Players[playerID].name = g_PlayerAssignments[guid].name;
g_Players[playerID].offline = false;
}
addChatMessage({
"type": "connect",
"guid": guid
});
}
function onClientLeave(guid)
{
g_PauseControl.setClientPauseState(guid, false);
for (const id in g_Players)
if (g_Players[id].guid == guid)
g_Players[id].offline = true;
addChatMessage({
"type": "disconnect",
"guid": guid
});
}
function addChatMessage(msg)
{
g_Chat.ChatMessageHandler.handleMessage(msg);
}
function clearChatMessages()
{
g_Chat.ChatOverlay.clearChatMessages();
}
function handleFlare(data)
{
const playerID = g_PlayerAssignments[data.guid].player;
const shouldSeeFlare = g_IsObserver || g_Players[playerID]?.isMutualAlly[Engine.GetPlayerID()];
// Don't display for the player that sent the flare because they will see it immediately.
if (!shouldSeeFlare || data.guid == Engine.GetPlayerGUID())
return;
const now = Date.now();
if (g_FlareRateLimitLastTimes.length)
{
g_FlareRateLimitLastTimes = g_FlareRateLimitLastTimes.filter(t => now - t < g_FlareRateLimitScope * 1000);
if (g_FlareRateLimitLastTimes.length >= g_FlareRateLimitMaximumFlares)
{
warn("Received too many flares. Dropping a flare request by '" + g_PlayerAssignments[data.guid].name + "'.");
return;
}
}
g_FlareRateLimitLastTimes.push(now);
renderAndPlayFlare(data.position, data.guid);
}
/**
* This function is used for AIs, whose names don't exist in g_PlayerAssignments.
*/
function colorizePlayernameByID(playerID)
{
const username = g_Players[playerID] && escapeText(g_Players[playerID].name);
return colorizePlayernameHelper(username, playerID);
}
function colorizePlayernameByGUID(guid)
{
const username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : "";
const playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
return colorizePlayernameHelper(username, playerID);
}
function colorizePlayernameHelper(username, playerID)
{
const playerColor = playerID > -1 ? g_DiplomacyColors.getPlayerColor(playerID) : "white";
return coloredText(username || translate("Unknown Player"), playerColor);
}
/**
* Insert the colorized playername to chat messages sent by the AI and time notifications.
*/
function colorizePlayernameParameters(parameters)
{
for (const param in parameters)
if (param.startsWith("_player_"))
parameters[param] = colorizePlayernameByID(parameters[param]);
}
/**
* Custom dialog response handling, usable by trigger maps.
*/
function sendDialogAnswer(guiObject, dialogName)
{
Engine.GetGUIObjectByName(dialogName + "-dialog").hidden = true;
Engine.PostNetworkCommand({
"type": "dialog-answer",
"dialog": dialogName,
"answer": guiObject.name.split("-").pop(),
});
resumeGame();
}
/**
* Custom dialog opening, usable by trigger maps.
*/
function openDialog(dialogName, data, player)
{
const dialog = Engine.GetGUIObjectByName(dialogName + "-dialog");
if (!dialog)
{
warn("messages.js: Unknown dialog with name " + dialogName);
return;
}
dialog.hidden = false;
for (const objName in data)
{
const obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName);
if (!obj)
{
warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog.");
continue;
}
for (const key in data[objName])
{
const n = data[objName][key];
if (typeof n === "object" && n.message)
{
let message = n.message;
if (n.translateMessage)
message = translate(message);
const parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
obj[key] = sprintf(message, parameters);
}
else
obj[key] = n;
}
}
g_PauseControl.implicitPause();
}