Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 19490)
+++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 19491)
@@ -1,1179 +1,1234 @@
/**
* All known cheat commands.
* @type {Object}
*/
const g_Cheats = getCheatsData();
/**
* Number of seconds after which chatmessages will disappear.
*/
const g_ChatTimeout = 30;
/**
* Maximum number of lines to display simultaneously.
*/
const g_ChatLines = 20;
/**
* The currently displayed strings, limited by the given timeframe and limit above.
*/
var g_ChatMessages = [];
/**
* All unparsed chat messages received since connect, including timestamp.
*/
var g_ChatHistory = [];
/**
* Holds the timer-IDs used for hiding the chat after g_ChatTimeout seconds.
*/
var g_ChatTimers = [];
/**
* Command to send to the previously selected private chat partner.
*/
var g_LastChatAddressee = "";
/**
* Handle all netmessage types that can occur.
*/
var g_NetMessageTypes = {
"netstatus": msg => {
handleNetStatusMessage(msg);
},
"netwarn": msg => {
addNetworkWarning(msg);
},
+ "out-of-sync": msg => {
+ onNetworkOutOfSync(msg);
+ },
"players": msg => {
handlePlayerAssignmentsMessage(msg);
},
"paused": msg => {
setClientPauseState(msg.guid, msg.pause);
},
"clients-loading": msg => {
handleClientsLoadingMessage(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
});
},
"aichat": msg => {
addChatMessage({
"type": "message",
"guid": msg.guid,
"text": msg.text,
"translate": true
});
},
"gamesetup": msg => {}, // Needed for autostart
"start": msg => {}
};
var g_FormatChatMessage = {
"system": msg => msg.text,
"connect": msg =>
sprintf(
g_PlayerAssignments[msg.guid].player != -1 ?
// Translation: A player that left the game joins again
translate("%(player)s is starting to rejoin the game.") :
// Translation: A player joins the game for the first time
translate("%(player)s is starting to join the game."),
{ "player": colorizePlayernameByGUID(msg.guid) }
),
"disconnect": msg =>
sprintf(translate("%(player)s has left the game."), {
"player": colorizePlayernameByGUID(msg.guid)
}),
"rejoined": msg =>
sprintf(
g_PlayerAssignments[msg.guid].player != -1 ?
// Translation: A player that left the game joins again
translate("%(player)s has rejoined the game.") :
// Translation: A player joins the game for the first time
translate("%(player)s has joined the game."),
{ "player": colorizePlayernameByGUID(msg.guid) }
),
"kicked": msg =>
sprintf(
msg.banned ?
translate("%(username)s has been banned") :
translate("%(username)s has been kicked"),
{
"username": colorizePlayernameHelper(
msg.username,
g_Players.findIndex(p => p.name == msg.username)
)
}
),
"clientlist": msg => getUsernameList(),
"message": msg => formatChatCommand(msg),
"defeat": msg => formatDefeatMessage(msg),
"won": msg => formatWinMessage(msg),
"diplomacy": msg => formatDiplomacyMessage(msg),
"tribute": msg => formatTributeMessage(msg),
"barter": msg => formatBarterMessage(msg),
"attack": msg => formatAttackMessage(msg),
"phase": msg => formatPhaseMessage(msg)
};
/**
* Show a label and grey overlay or hide both on connection change.
*/
var g_StatusMessageTypes = {
"authenticated": msg => translate("Connection to the server has been authenticated."),
"connected": msg => translate("Connected to the server."),
"disconnected": msg => translate("Connection to the server has been lost.") + "\n" +
// Translation: States the reason why the client disconnected from the server.
sprintf(translate("Reason: %(reason)s."), {
"reason": getDisconnectReason(msg.reason, true)
}),
"waiting_for_players": msg => translate("Waiting for players to connect:"),
"join_syncing": msg => translate("Synchronising gameplay with other players..."),
"active": msg => ""
};
/**
* Chatmessage shown after commands like /me or /enemies.
*/
var g_ChatCommands = {
"regular": {
"context": translate("(%(context)s) %(userTag)s %(message)s"),
"no-context": translate("%(userTag)s %(message)s")
},
"me": {
"context": translate("(%(context)s) * %(user)s %(message)s"),
"no-context": translate("* %(user)s %(message)s")
}
};
var g_ChatAddresseeContext = {
"/team": translate("Team"),
"/allies": translate("Ally"),
"/enemies": translate("Enemy"),
"/observers": translate("Observer"),
"/msg": translate("Private")
};
/**
* Returns true if the current player is an addressee, given the chat message type and sender.
*/
var g_IsChatAddressee = {
"/team": senderID =>
g_Players[senderID] &&
g_Players[Engine.GetPlayerID()] &&
g_Players[Engine.GetPlayerID()].team != -1 &&
g_Players[Engine.GetPlayerID()].team == g_Players[senderID].team,
"/allies": senderID =>
g_Players[senderID] &&
g_Players[Engine.GetPlayerID()] &&
g_Players[senderID].isMutualAlly[Engine.GetPlayerID()],
"/enemies": senderID =>
g_Players[senderID] &&
g_Players[Engine.GetPlayerID()] &&
g_Players[senderID].isEnemy[Engine.GetPlayerID()],
"/observers": senderID =>
g_IsObserver,
"/msg": (senderID, addresseeGUID) =>
addresseeGUID == Engine.GetPlayerGUID()
};
/**
* Notice only messages will be filtered that are visible to the player in the first place.
*/
var g_ChatHistoryFilters = [
{
"key": "all",
"text": translateWithContext("chat history filter", "Chat and notifications"),
"filter": (msg, senderID) => true
},
{
"key": "chat",
"text": translateWithContext("chat history filter", "Chat messages"),
"filter": (msg, senderID) => msg.type == "message"
},
{
"key": "player",
"text": translateWithContext("chat history filter", "Players chat"),
"filter": (msg, senderID) =>
msg.type == "message" &&
senderID > 0 && !isPlayerObserver(senderID)
},
{
"key": "ally",
"text": translateWithContext("chat history filter", "Ally chat"),
"filter": (msg, senderID) =>
msg.type == "message" &&
msg.cmd && msg.cmd == "/allies"
},
{
"key": "enemy",
"text": translateWithContext("chat history filter", "Enemy chat"),
"filter": (msg, senderID) =>
msg.type == "message" &&
msg.cmd && msg.cmd == "/enemies"
},
{
"key": "observer",
"text": translateWithContext("chat history filter", "Observer chat"),
"filter": (msg, senderID) =>
msg.type == "message" &&
msg.cmd && msg.cmd == "/observers"
},
{
"key": "private",
"text": translateWithContext("chat history filter", "Private chat"),
"filter": (msg, senderID) => !!msg.isVisiblePM
}
];
var g_PlayerStateMessages = {
"won": translate("You have won!"),
"defeated": translate("You have been defeated!")
};
/**
* Chatmessage shown on diplomacy change.
*/
var g_DiplomacyMessages = {
"active": {
"ally": translate("You are now allied with %(player)s."),
"enemy": translate("You are now at war with %(player)s."),
"neutral": translate("You are now neutral with %(player)s.")
},
"passive": {
"ally": translate("%(player)s is now allied with you."),
"enemy": translate("%(player)s is now at war with you."),
"neutral": translate("%(player)s is now neutral with you.")
},
"observer": {
"ally": translate("%(player)s is now allied with %(player2)s."),
"enemy": translate("%(player)s is now at war with %(player2)s."),
"neutral": translate("%(player)s is now neutral with %(player2)s.")
}
};
/**
* 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 =
{
"chat": function(notification, player)
{
let message = {
"type": "message",
"guid": findGuidForPlayerID(player) || -1,
"text": notification.message
};
if (message.guid == -1)
message.player = player;
addChatMessage(message);
},
"aichat": function(notification, player)
{
let 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)
{
addChatMessage({
"type": "defeat",
"guid": findGuidForPlayerID(player),
"player": player,
"resign": !!notification.resign
});
playerFinished(player, false);
sendLobbyPlayerlistUpdate();
},
"won": function(notification, player)
{
addChatMessage({
"type": "won",
"guid": findGuidForPlayerID(player),
"player": player
});
playerFinished(player, true);
sendLobbyPlayerlistUpdate();
},
"diplomacy": function(notification, player)
{
addChatMessage({
"type": "diplomacy",
"sourcePlayer": player,
"targetPlayer": notification.targetPlayer,
"status": notification.status
});
updatePlayerData();
},
"ceasefire-ended": function(notification, player)
{
updatePlayerData();
},
"tribute": function(notification, player)
{
addChatMessage({
"type": "tribute",
"sourcePlayer": notification.donator,
"targetPlayer": player,
"amounts": notification.amounts
});
},
"barter": function(notification, player)
{
addChatMessage({
"type": "barter",
"player": player,
"amountsSold": notification.amountsSold,
"amountsBought": notification.amountsBought,
"resourceSold": notification.resourceSold,
"resourceBought": notification.resourceBought
});
},
"spy-response": function(notification, player)
{
if (g_ViewedPlayer == player)
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]);
}
if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.attack") !== "true")
return;
addChatMessage({
"type": "attack",
"player": player,
"attacker": notification.attacker,
"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);
},
"resetselectionpannel": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
g_Selection.rebuildSelection({});
},
"playercommand": function(notification, player)
{
// For observers, focus the camera on units commanded by the selected player
if (!g_FollowPlayer || player != g_ViewedPlayer)
return;
let cmd = notification.cmd;
// Ignore boring animals
let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]);
if (entState && entState.identity && entState.identity.classes &&
entState.identity.classes.indexOf("Animal") != -1)
return;
// Focus the building to construct
if (cmd.type == "repair")
{
let targetState = GetEntityState(cmd.target);
if (targetState)
Engine.CameraMoveTo(targetState.position.x, targetState.position.z);
}
// 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]);
// 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");
}
};
/**
* Loads all known cheat commands.
*
* @returns {Object}
*/
function getCheatsData()
{
let cheats = {};
for (let fileName of getJSONFileList("simulation/data/cheats/"))
{
let currentCheat = Engine.ReadJSONFile("simulation/data/cheats/"+fileName+".json");
if (!currentCheat)
continue;
if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1)
warn("Cheat name '" + currentCheat.Name + "' is already present");
else
cheats[currentCheat.Name] = currentCheat.Data;
}
return cheats;
}
/**
* Reads userinput from the chat and sends a simulation command in case it is a known cheat.
*
* @returns {boolean} - True if a cheat was executed.
*/
function executeCheat(text)
{
if (!controlsPlayer(Engine.GetPlayerID()) ||
!g_Players[Engine.GetPlayerID()].cheatsEnabled)
return false;
// Find the cheat code that is a prefix of the user input
let cheatCode = Object.keys(g_Cheats).find(cheatCode => text.indexOf(cheatCode) == 0);
if (!cheatCode)
return false;
let cheat = g_Cheats[cheatCode];
let parameter = text.substr(cheatCode.length);
if (cheat.isNumeric)
parameter = +parameter;
if (cheat.DefaultParameter && (isNaN(parameter) || parameter <= 0))
parameter = cheat.DefaultParameter;
Engine.PostNetworkCommand({
"type": "cheat",
"action": cheat.Action,
"text": cheat.Type,
"player": Engine.GetPlayerID(),
"parameter": parameter,
"templates": cheat.Templates,
"selected": g_Selection.toList()
});
return true;
}
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 (let notification of Engine.GuiInterfaceCall("GetNotifications"))
{
if (!notification.players || !notification.type || !g_NotificationsTypes[notification.type])
{
error("Invalid GUI notification: " + uneval(notification));
continue;
}
for (let player of notification.players)
g_NotificationsTypes[notification.type](notification, player);
}
}
/**
* Displays all active counters (messages showing the remaining time) for wonder-victory, ceasefire etc.
*/
function updateTimeNotifications()
{
let notifications = Engine.GuiInterfaceCall("GetTimeNotifications", g_ViewedPlayer);
let notificationText = "";
for (let n of notifications)
{
let message = n.message;
if (n.translateMessage)
message = translate(message);
let parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
parameters.time = timeToString(n.endTime - g_SimState.timeElapsed);
colorizePlayernameParameters(parameters);
notificationText += sprintf(message, parameters) + "\n";
}
Engine.GetGUIObjectByName("notificationText").caption = notificationText;
}
/**
* Process every CNetMessage (see NetMessage.h, NetMessages.h) sent by the CNetServer.
* Saves the received object to mainlog.html.
*/
function handleNetMessages()
{
while (true)
{
let msg = Engine.PollNetworkClient();
if (!msg)
return;
log("Net message: " + uneval(msg));
if (g_NetMessageTypes[msg.type])
g_NetMessageTypes[msg.type](msg);
else
error("Unrecognised net message type '" + msg.type + "'");
}
}
/**
* @param {Object} message
*/
function handleNetStatusMessage(message)
{
if (g_Disconnected)
return;
if (!g_StatusMessageTypes[message.status])
{
error("Unrecognised netstatus type '" + message.status + "'");
return;
}
g_IsNetworkedActive = message.status == "active";
let netStatus = Engine.GetGUIObjectByName("netStatus");
let statusMessage = g_StatusMessageTypes[message.status](message);
netStatus.caption = statusMessage;
netStatus.hidden = !statusMessage;
let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText");
loadingClientsText.hidden = message.status != "waiting_for_players";
if (message.status == "disconnected")
{
// Hide the pause overlay, and pause animations.
Engine.GetGUIObjectByName("pauseOverlay").hidden = true;
Engine.SetPaused(true, false);
g_Disconnected = true;
closeOpenDialogs();
}
}
function handleClientsLoadingMessage(guids)
{
let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText");
loadingClientsText.caption = guids.map(guid => colorizePlayernameByGUID(guid)).join(translate(", "));
}
+function onNetworkOutOfSync(msg)
+{
+ let txt = [
+ sprintf(translate("Out-Of-Sync error on turn %(turn)s."), {
+ "turn": msg.turn
+ }),
+
+ sprintf(translateWithContext("Out-Of-Sync", "Players: %(players)s"), {
+ "players": msg.players.join(translate(", "))
+ }),
+
+ msg.hash == msg.expectedHash ?
+ translateWithContext("Out-Of-Sync", "Your game state is identical to the hosts game state.") :
+ translateWithContext("Out-Of-Sync", "Your game state differs from the hosts game state."),
+
+ ""
+ ];
+
+ if (msg.turn > 1 && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.AI))
+ txt.push(translateWithContext("Out-Of-Sync", "Rejoining Multiplayer games with AIs is not supported yet!"));
+ else
+ {
+ txt.push(
+ translateWithContext("Out-Of-Sync", "Ensure all players use the same mods."),
+ translateWithContext("Out-Of-Sync", 'Click on "Report Bugs" in the main menu to help fix this.'),
+ sprintf(translateWithContext("Out-Of-Sync", "Replay saved to %(filepath)s"), {
+ "filepath": escapeText(msg.path_replay)
+ }),
+ sprintf(translateWithContext("Out-Of-Sync", "Dumping current state to %(filepath)s"), {
+ "filepath": escapeText(msg.path_oos_dump)
+ })
+ );
+ }
+
+ messageBox(
+ 600, 280,
+ txt.join("\n"),
+ translate("Out of Sync")
+ );
+}
+
+function onReplayOutOfSync()
+{
+ messageBox(
+ 500, 140,
+ translate("Out-Of-Sync error!") + "\n" +
+ // Translation: This is shown if replay is out of sync
+ translateWithContext("Out-Of-Sync", "The current game state is different from the original game state."),
+ translate("Out Of Sync")
+ );
+}
+
function handlePlayerAssignmentsMessage(message)
{
for (let guid in g_PlayerAssignments)
if (!message.newAssignments[guid])
onClientLeave(guid);
let joins = Object.keys(message.newAssignments).filter(guid => !g_PlayerAssignments[guid]);
g_PlayerAssignments = message.newAssignments;
joins.forEach(guid => {
onClientJoin(guid);
});
updateGUIObjects();
updateChatAddressees();
sendLobbyPlayerlistUpdate();
}
function onClientJoin(guid)
{
let 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)
{
setClientPauseState(guid, false);
for (let id in g_Players)
if (g_Players[id].guid == guid)
g_Players[id].offline = true;
addChatMessage({
"type": "disconnect",
"guid": guid
});
}
function updateChatAddressees()
{
// Remember previously selected item
let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
let selectedName = chatAddressee.list_data[chatAddressee.selected] || "";
selectedName = selectedName.substr(0, 4) == "/msg" && selectedName.substr(5);
let addressees = [
{
"label": translateWithContext("chat addressee", "Everyone"),
"cmd": ""
}
];
if (!g_IsObserver)
{
addressees.push({
"label": translateWithContext("chat addressee", "Allies"),
"cmd": "/allies"
});
addressees.push({
"label": translateWithContext("chat addressee", "Enemies"),
"cmd": "/enemies"
});
}
addressees.push({
"label": translateWithContext("chat addressee", "Observers"),
"cmd": "/observers"
});
// Add playernames for private messages
let guids = sortGUIDsByPlayerID();
for (let guid of guids)
{
if (guid == Engine.GetPlayerGUID())
continue;
let playerID = g_PlayerAssignments[guid].player;
// Don't provide option for PM from observer to player
if (g_IsObserver && !isPlayerObserver(playerID))
continue;
let colorBox = isPlayerObserver(playerID) ? "" : colorizePlayernameHelper("■", playerID) + " ";
addressees.push({
"cmd": "/msg " + g_PlayerAssignments[guid].name,
"label": colorBox + g_PlayerAssignments[guid].name
});
}
// Select mock item if the selected addressee went offline
if (selectedName && guids.every(guid => g_PlayerAssignments[guid].name != selectedName))
addressees.push({
"cmd": "/msg " + selectedName,
"label": sprintf(translate("\\[OFFLINE] %(player)s"), { "player": selectedName })
});
let oldChatAddressee = chatAddressee.list_data[chatAddressee.selected];
chatAddressee.list = addressees.map(adressee => adressee.label);
chatAddressee.list_data = addressees.map(adressee => adressee.cmd);
chatAddressee.selected = Math.max(0, chatAddressee.list_data.indexOf(oldChatAddressee));
}
/**
* Send text as chat. Don't look for commands.
*
* @param {string} text
*/
function submitChatDirectly(text)
{
if (!text.length)
return;
if (g_IsNetworked)
Engine.SendNetworkChat(text);
else
addChatMessage({ "type": "message", "guid": "local", "text": text });
}
/**
* Loads the text from the GUI window, checks if it is a local command
* or cheat and executes it. Otherwise sends it as chat.
*/
function submitChatInput()
{
let text = Engine.GetGUIObjectByName("chatInput").caption;
closeChat();
if (!text.length)
return;
if (executeNetworkCommand(text))
return;
if (executeCheat(text))
return;
let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
if (chatAddressee.selected > 0 && (text.indexOf("/") != 0 || text.indexOf("/me ") == 0))
text = chatAddressee.list_data[chatAddressee.selected] + " " + text;
let selectedChat = chatAddressee.list_data[chatAddressee.selected]
if (selectedChat.startsWith("/msg"))
g_LastChatAddressee = selectedChat;
submitChatDirectly(text);
}
/**
* Displays the prepared chatmessage.
*
* @param msg {Object}
*/
function addChatMessage(msg)
{
if (!g_FormatChatMessage[msg.type])
return;
let formatted = g_FormatChatMessage[msg.type](msg);
if (!formatted)
return;
// Update chat overlay
g_ChatMessages.push(formatted);
g_ChatTimers.push(setTimeout(removeOldChatMessage, g_ChatTimeout * 1000));
if (g_ChatMessages.length > g_ChatLines)
removeOldChatMessage();
else
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
// Save to chat history
let historical = {
"txt": formatted,
"timePrefix": sprintf(translate("\\[%(time)s]"), {
"time": Engine.FormatMillisecondsIntoDateStringLocal(new Date().getTime(), translate("HH:mm"))
}),
"filter": {}
};
// Apply the filters now before diplomacies or playerstates change
let senderID = msg.guid && g_PlayerAssignments[msg.guid] ? g_PlayerAssignments[msg.guid].player : 0;
for (let filter of g_ChatHistoryFilters)
historical.filter[filter.key] = filter.filter(msg, senderID);
g_ChatHistory.push(historical);
updateChatHistory();
}
/**
* Called when the timer has run out for the oldest chatmessage or when the message limit is reached.
*/
function removeOldChatMessage()
{
clearTimeout(g_ChatTimers[0]);
g_ChatTimers.shift();
g_ChatMessages.shift();
Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}
/**
* This function is used for AIs, whose names don't exist in g_PlayerAssignments.
*/
function colorizePlayernameByID(playerID)
{
let username = g_Players[playerID] && escapeText(g_Players[playerID].name);
return colorizePlayernameHelper(username, playerID);
}
function colorizePlayernameByGUID(guid)
{
let username = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].name : "";
let playerID = g_PlayerAssignments[guid] ? g_PlayerAssignments[guid].player : -1;
return colorizePlayernameHelper(username, playerID);
}
function colorizePlayernameHelper(username, playerID)
{
let playerColor = playerID > -1 ? rgbToGuiColor(g_Players[playerID].color) : "white";
return '[color="' + playerColor + '"]' + (username || translate("Unknown Player")) + "[/color]";
}
/**
* Insert the colorized playername to chat messages sent by the AI and time notifications.
*/
function colorizePlayernameParameters(parameters)
{
for (let param in parameters)
if (param.startsWith("_player_"))
parameters[param] = colorizePlayernameByID(parameters[param]);
}
function formatDefeatMessage(msg)
{
return sprintf(
msg.resign ?
translate("%(player)s has resigned.") :
translate("%(player)s has been defeated."),
{ "player": colorizePlayernameByID(msg.player) }
);
}
function formatWinMessage(msg)
{
return sprintf(translate("%(player)s has won."), {
"player": colorizePlayernameByID(msg.player)
});
}
function formatDiplomacyMessage(msg)
{
let messageType;
if (g_IsObserver)
messageType = "observer";
else if (Engine.GetPlayerID() == msg.sourcePlayer)
messageType = "active";
else if (Engine.GetPlayerID() == msg.targetPlayer)
messageType = "passive";
else
return "";
return sprintf(g_DiplomacyMessages[messageType][msg.status], {
"player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer),
"player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer)
});
}
/**
* Optionally show all tributes sent in observer mode and tributes sent between allied players.
* Otherwise, only show tributes sent directly to us, and tributes that we send.
*/
function formatTributeMessage(msg)
{
let message = "";
if (msg.targetPlayer == Engine.GetPlayerID())
message = translate("%(player)s has sent you %(amounts)s.");
else if (msg.sourcePlayer == Engine.GetPlayerID())
message = translate("You have sent %(player2)s %(amounts)s.");
else if (Engine.ConfigDB_GetValue("user", "gui.session.notifications.tribute") == "true" &&
(g_IsObserver || g_GameAttributes.settings.LockTeams &&
g_Players[msg.sourcePlayer].isMutualAlly[Engine.GetPlayerID()] &&
g_Players[msg.targetPlayer].isMutualAlly[Engine.GetPlayerID()]))
message = translate("%(player)s has sent %(player2)s %(amounts)s.");
return sprintf(message, {
"player": colorizePlayernameByID(msg.sourcePlayer),
"player2": colorizePlayernameByID(msg.targetPlayer),
"amounts": getLocalizedResourceAmounts(msg.amounts)
});
}
function formatBarterMessage(msg)
{
if (!g_IsObserver || Engine.ConfigDB_GetValue("user", "gui.session.notifications.barter") != "true")
return "";
let amountsSold = {};
amountsSold[msg.resourceSold] = msg.amountsSold;
let amountsBought = {};
amountsBought[msg.resourceBought] = msg.amountsBought;
return sprintf(translate("%(player)s bartered %(amountsBought)s for %(amountsSold)s."), {
"player": colorizePlayernameByID(msg.player),
"amountsBought": getLocalizedResourceAmounts(amountsBought),
"amountsSold": getLocalizedResourceAmounts(amountsSold)
});
}
function formatAttackMessage(msg)
{
if (msg.player != g_ViewedPlayer)
return "";
let message = msg.targetIsDomesticAnimal ?
translate("Your livestock has been attacked by %(attacker)s!") :
translate("You have been attacked by %(attacker)s!");
return sprintf(message, {
"attacker": colorizePlayernameByID(msg.attacker)
});
}
function formatPhaseMessage(msg)
{
let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase");
if (notifyPhase == 0 || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer])
return "";
let message = "";
if (notifyPhase == 2)
{
if (msg.phaseState == "started")
message = translate("%(player)s is advancing to the %(phaseName)s.");
else if (msg.phaseState == "aborted")
message = translate("The %(phaseName)s of %(player)s has been aborted.");
}
if (msg.phaseState == "completed")
message = translate("%(player)s has reached the %(phaseName)s.");
return sprintf(message, {
"player": colorizePlayernameByID(msg.player),
"phaseName": getEntityNames(GetTechnologyData(msg.phaseName, g_Players[msg.player].civ))
});
}
function formatChatCommand(msg)
{
if (!msg.text)
return "";
let isMe = msg.text.indexOf("/me ") == 0;
if (!isMe && !parseChatAddressee(msg))
return "";
isMe = msg.text.indexOf("/me ") == 0;
if (isMe)
msg.text = msg.text.substr("/me ".length);
// Translate or escape text
if (!msg.text)
return "";
if (msg.translate)
{
msg.text = translate(msg.text);
if (msg.translateParameters)
{
let parameters = msg.parameters || {};
translateObjectKeys(parameters, msg.translateParameters);
msg.text = sprintf(msg.text, parameters);
}
}
else
{
msg.text = escapeText(msg.text);
let userName = g_PlayerAssignments[Engine.GetPlayerGUID()].name;
if (userName != g_PlayerAssignments[msg.guid].name)
notifyUser(userName, msg.text);
}
// GUID for players, playerID for AIs
let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player);
return sprintf(g_ChatCommands[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"], {
"message": msg.text,
"context": msg.context || undefined,
"user": coloredUsername,
"userTag": sprintf(translate("<%(user)s>"), { "user": coloredUsername })
});
}
/**
* Checks if the current user is an addressee of the chatmessage sent by another player.
* Sets the context and potentially addresseeGUID of that message.
* Returns true if the message should be displayed.
*
* @param {Object} msg
*/
function parseChatAddressee(msg)
{
if (msg.text[0] != '/')
return true;
// Split addressee command and message-text
msg.cmd = msg.text.split(/\s/)[0];
msg.text = msg.text.substr(msg.cmd.length + 1);
// GUID is "local" in singleplayer, some string in multiplayer.
// Chat messages sent by the simulation (AI) come with the playerID.
let senderID = msg.player ? msg.player : (g_PlayerAssignments[msg.guid] || msg).player;
let isSender = msg.guid ?
msg.guid == Engine.GetPlayerGUID() :
senderID == Engine.GetPlayerID();
// Parse private message
let isPM = msg.cmd == "/msg";
let addresseeGUID;
let addresseeIndex;
if (isPM)
{
addresseeGUID = matchUsername(msg.text);
let addressee = g_PlayerAssignments[addresseeGUID];
if (!addressee)
{
if (isSender)
warn("Couldn't match username: " + msg.text);
return false;
}
// Prohibit PM if addressee and sender are identical
if (isSender && addresseeGUID == Engine.GetPlayerGUID())
return false;
msg.text = msg.text.substr(addressee.name.length + 1);
addresseeIndex = addressee.player;
}
// Set context string
if (!g_ChatAddresseeContext[msg.cmd])
{
if (isSender)
warn("Unknown chat command: " + msg.cmd);
return false;
}
msg.context = g_ChatAddresseeContext[msg.cmd];
// For observers only permit public- and observer-chat and PM to observers
if (isPlayerObserver(senderID) &&
(isPM && !isPlayerObserver(addresseeIndex) || !isPM && msg.cmd != "/observers"))
return false;
let visible = isSender || g_IsChatAddressee[msg.cmd](senderID, addresseeGUID);
msg.isVisiblePM = isPM && visible;
return visible;
}
/**
* Returns the guid of the user with the longest name that is a prefix of the given string.
*/
function matchUsername(text)
{
if (!text)
return "";
let match = "";
let playerGUID = "";
for (let guid in g_PlayerAssignments)
{
let pName = g_PlayerAssignments[guid].name;
if (text.indexOf(pName + " ") == 0 && pName.length > match.length)
{
match = pName;
playerGUID = guid;
}
}
return playerGUID;
}
/**
* 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)
{
let dialog = Engine.GetGUIObjectByName(dialogName + "-dialog");
if (!dialog)
{
warn("messages.js: Unknow dialog with name " + dialogName);
return;
}
dialog.hidden = false;
for (let objName in data)
{
let obj = Engine.GetGUIObjectByName(dialogName + "-dialog-" + objName);
if (!obj)
{
warn("messages.js: Key '" + objName + "' not found in '" + dialogName + "' dialog.");
continue;
}
for (let key in data[objName])
{
let n = data[objName][key];
if (typeof n == "object" && n.message)
{
let message = n.message;
if (n.translateMessage)
message = translate(message);
let parameters = n.parameters || {};
if (n.translateParameters)
translateObjectKeys(parameters, n.translateParameters);
obj[key] = sprintf(message, parameters);
}
else
obj[key] = n;
}
}
pauseGame();
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 19490)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 19491)
@@ -1,165 +1,169 @@
ExitleaveGame();
Index: ps/trunk/source/network/NetClientTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetClientTurnManager.cpp (revision 19490)
+++ ps/trunk/source/network/NetClientTurnManager.cpp (revision 19491)
@@ -1,139 +1,150 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetClientTurnManager.h"
#include "NetClient.h"
#include "gui/GUIManager.h"
#include "ps/CLogger.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "simulation2/Simulation2.h"
#if 0
#define NETCLIENTTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETCLIENTTURN_LOG(...)
#endif
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay)
: CTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
{
}
void CNetClientTurnManager::PostCommand(JS::HandleValue data)
{
NETCLIENTTURN_LOG("PostCommand()\n");
// Transmit command to server
CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data);
m_NetClient.SendMessage(&msg);
// Add to our local queue
//AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY);
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
CEndCommandBatchMessage msg;
msg.m_Turn = turn;
// The turn-length field of the CEndCommandBatchMessage is currently only relevant
// when sending it from the server to the clients.
// It could be used to verify that the client simulated the correct turn length.
msg.m_TurnLength = 0;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
}
NETCLIENTTURN_LOG("NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str());
m_Replay.Hash(hash, quick);
// Don't send the hash if OOS
if (m_HasSyncError)
return;
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::OnDestroyConnection()
{
NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
}
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector& playerNames)
{
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, Hexify(expectedHash).c_str());
// Only complain the first time
if (m_HasSyncError)
return;
m_HasSyncError = true;
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, !TurnNeedsFullHash(turn)));
OsPath path = psLogDir() / "oos_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
hash = Hexify(hash);
- std::stringstream msg;
- msg << "Out of sync on turn " << turn;
-
+ std::stringstream playerNamesString;
+ std::vector playerNamesStrings;
+ playerNamesStrings.reserve(playerNames.size());
for (size_t i = 0; i < playerNames.size(); ++i)
- msg << (i == 0 ? "\nPlayers: " : ", ") << utf8_from_wstring(playerNames[i].m_Name);
-
- msg << "\n\n" << "Your game state is " << (expectedHash == hash ? "identical to" : "different from") << " the hosts game state.";
-
- msg << "\n\n" << "Dumping current state to " << CStr(path.string8()).EscapeToPrintableASCII();
+ {
+ CStr name = utf8_from_wstring(playerNames[i].m_Name);
+ playerNamesString << (i == 0 ? "" : ", ") << name;
+ playerNamesStrings.push_back(name);
+ }
- LOGERROR("%s", msg.str());
+ LOGERROR("Out-Of-Sync on turn %d\nPlayers: %s\nDumping state to %s", turn, playerNamesString.str().c_str(), path.string8());
- if (g_GUI)
- g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
+ ScriptInterface& scriptInterface = m_NetClient.GetScriptInterface();
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue msg(cx);
+ scriptInterface.Eval("({ 'type':'out-of-sync' })", &msg);
+ scriptInterface.SetProperty(msg, "turn", turn);
+ scriptInterface.SetProperty(msg, "players", playerNamesStrings);
+ scriptInterface.SetProperty(msg, "expectedHash", expectedHash);
+ scriptInterface.SetProperty(msg, "hash", hash);
+ scriptInterface.SetProperty(msg, "path_oos_dump", path.string8());
+ scriptInterface.SetProperty(msg, "path_replay", m_Replay.GetDirectory().string8());
+ m_NetClient.PushGuiMessage(msg);
}
Index: ps/trunk/source/simulation2/system/ReplayTurnManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/ReplayTurnManager.cpp (revision 19490)
+++ ps/trunk/source/simulation2/system/ReplayTurnManager.cpp (revision 19491)
@@ -1,115 +1,106 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "ReplayTurnManager.h"
#include "gui/GUIManager.h"
#include "ps/Util.h"
#include "simulation2/Simulation2.h"
CReplayTurnManager::CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay)
: CLocalTurnManager(simulation, replay)
{
}
void CReplayTurnManager::StoreReplayCommand(u32 turn, int player, const std::string& command)
{
// Using the pair we make sure that commands per turn will be processed in the correct order
m_ReplayCommands[turn].emplace_back(player, command);
}
void CReplayTurnManager::StoreReplayHash(u32 turn, const std::string& hash, bool quick)
{
m_ReplayHash[turn] = std::make_pair(hash, quick);
}
void CReplayTurnManager::StoreReplayTurnLength(u32 turn, u32 turnLength)
{
m_ReplayTurnLengths[turn] = turnLength;
// Initialize turn length
if (turn == 0)
m_TurnLength = m_ReplayTurnLengths[0];
}
void CReplayTurnManager::StoreFinalReplayTurn(u32 turn)
{
m_FinalTurn = turn;
}
void CReplayTurnManager::NotifyFinishedUpdate(u32 turn)
{
if (turn == 1 && m_FinalTurn == 0)
g_GUI->SendEventToAll("ReplayFinished");
if (turn > m_FinalTurn)
return;
DoTurn(turn);
// Compare hash if it exists in the replay and if we didn't have an OOS already
std::map>::iterator turnHashIt = m_ReplayHash.find(turn);
if (m_HasSyncError || turnHashIt == m_ReplayHash.end())
return;
std::string expectedHash = turnHashIt->second.first;
bool quickHash = turnHashIt->second.second;
// Compute hash
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash));
hash = Hexify(hash);
if (hash != expectedHash)
- OnSyncError(turn);
+ {
+ m_HasSyncError = true;
+ LOGERROR("Replay out of sync on turn %d", turn);
+ g_GUI->SendEventToAll("ReplayOutOfSync");
+ }
}
void CReplayTurnManager::DoTurn(u32 turn)
{
debug_printf("Executing turn %u of %u\n", turn, m_FinalTurn);
m_TurnLength = m_ReplayTurnLengths[turn];
JSContext* cx = m_Simulation2.GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
// Simulate commands for that turn
for (const std::pair& p : m_ReplayCommands[turn])
{
JS::RootedValue command(cx);
m_Simulation2.GetScriptInterface().ParseJSON(p.second, &command);
AddCommand(m_ClientId, p.first, command, m_CurrentTurn + 1);
}
if (turn == m_FinalTurn)
g_GUI->SendEventToAll("ReplayFinished");
}
-
-void CReplayTurnManager::OnSyncError(u32 turn)
-{
- m_HasSyncError = true;
-
- std::stringstream msg;
- msg << "Out of sync on turn " << turn << "\n\n" << "The current game state is different from the original game state.";
-
- LOGERROR("%s", msg.str());
-
- if (g_GUI)
- g_GUI->DisplayMessageBox(600, 350, L"Sync error", wstring_from_utf8(msg.str()));
-}
Index: ps/trunk/source/simulation2/system/ReplayTurnManager.h
===================================================================
--- ps/trunk/source/simulation2/system/ReplayTurnManager.h (revision 19490)
+++ ps/trunk/source/simulation2/system/ReplayTurnManager.h (revision 19491)
@@ -1,56 +1,54 @@
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef INCLUDED_REPLAYTURNMANAGER
#define INCLUDED_REPLAYTURNMANAGER
#include "LocalTurnManager.h"
/**
* Implementation of CLocalTurnManager for replay games.
*/
class CReplayTurnManager : public CLocalTurnManager
{
public:
CReplayTurnManager(CSimulation2& simulation, IReplayLogger& replay);
void StoreReplayCommand(u32 turn, int player, const std::string& command);
void StoreReplayTurnLength(u32 turn, u32 turnLength);
void StoreReplayHash(u32 turn, const std::string& hash, bool quick);
void StoreFinalReplayTurn(u32 turn);
private:
void NotifyFinishedUpdate(u32 turn) override;
void DoTurn(u32 turn);
- void OnSyncError(u32 turn);
-
// Contains the commands of every player on each turn
std::map>> m_ReplayCommands;
// Contains the length of every turn
std::map m_ReplayTurnLengths;
// Contains all replay hash values and weather or not the quick hash method was used
std::map> m_ReplayHash;
};
#endif // INCLUDED_REPLAYTURNMANAGER