/** * 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) { 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(); }