forked from 0ad/0ad
655 lines
18 KiB
JavaScript
655 lines
18 KiB
JavaScript
// Chat data
|
|
const CHAT_TIMEOUT = 30000;
|
|
const MAX_NUM_CHAT_LINES = 20;
|
|
var chatMessages = [];
|
|
var chatTimers = [];
|
|
|
|
// Notification Data
|
|
const NOTIFICATION_TIMEOUT = 10000;
|
|
const MAX_NUM_NOTIFICATION_LINES = 3;
|
|
var notifications = [];
|
|
var notificationsTimers = [];
|
|
var cheats = getCheatsData();
|
|
|
|
function getCheatsData()
|
|
{
|
|
var cheats = {};
|
|
var cheatFileList = getJSONFileList("simulation/data/cheats/");
|
|
for each (var fileName in cheatFileList)
|
|
{
|
|
var currentCheat = parseJSONData("simulation/data/cheats/"+fileName+".json");
|
|
if (Object.keys(cheats).indexOf(currentCheat.Name) !== -1)
|
|
warn(sprintf("Cheat name '%(name)s' is already present", { name: currentCheat.Name }));
|
|
else
|
|
cheats[currentCheat.Name] = currentCheat.Data;
|
|
}
|
|
return cheats;
|
|
}
|
|
|
|
// Notifications
|
|
function handleNotifications()
|
|
{
|
|
var notification = Engine.GuiInterfaceCall("GetNextNotification");
|
|
|
|
if (!notification)
|
|
return;
|
|
|
|
if (notification.type === undefined)
|
|
notification.type = "text";
|
|
|
|
// Handle chat notifications specially
|
|
if (notification.type == "chat" ||
|
|
notification.type == "aichat")
|
|
{
|
|
var message = {
|
|
"type": "message",
|
|
"text": notification.message
|
|
}
|
|
if (notification.type == "aichat")
|
|
message["translate"] = true;
|
|
var guid = findGuidForPlayerID(g_PlayerAssignments, notification.player);
|
|
if (guid == undefined)
|
|
{
|
|
message["guid"] = -1;
|
|
message["player"] = notification.player;
|
|
} else {
|
|
message["guid"] = findGuidForPlayerID(g_PlayerAssignments, notification.player);
|
|
}
|
|
addChatMessage(message);
|
|
}
|
|
else if (notification.type == "defeat")
|
|
{
|
|
addChatMessage({
|
|
"type": "defeat",
|
|
"guid": findGuidForPlayerID(g_PlayerAssignments, notification.player),
|
|
"player": notification.player
|
|
});
|
|
|
|
// If the diplomacy panel is open refresh it.
|
|
if (isDiplomacyOpen)
|
|
openDiplomacy();
|
|
}
|
|
else if (notification.type == "diplomacy")
|
|
{
|
|
addChatMessage({
|
|
"type": "diplomacy",
|
|
"player": notification.player,
|
|
"player1": notification.player1,
|
|
"status": notification.status
|
|
});
|
|
|
|
// If the diplomacy panel is open refresh it.
|
|
if (isDiplomacyOpen)
|
|
openDiplomacy();
|
|
}
|
|
else if (notification.type == "quit")
|
|
{
|
|
// Used for AI testing
|
|
exit();
|
|
}
|
|
else if (notification.type == "tribute")
|
|
{
|
|
addChatMessage({
|
|
"type": "tribute",
|
|
"player": notification.player,
|
|
"player1": notification.player1,
|
|
"amounts": notification.amounts
|
|
});
|
|
}
|
|
else if (notification.type == "attack")
|
|
{
|
|
if (notification.player == Engine.GetPlayerID())
|
|
{
|
|
if (Engine.ConfigDB_GetValue("user", "gui.session.attacknotificationmessage") === "true")
|
|
{
|
|
addChatMessage({
|
|
"type": "attack",
|
|
"player": notification.player,
|
|
"attacker": notification.attacker
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else if (notification.type == "text")
|
|
{
|
|
// Only display notifications directed to this player
|
|
if (notification.player == Engine.GetPlayerID())
|
|
{
|
|
notifications.push(notification);
|
|
notificationsTimers.push(setTimeout(removeOldNotifications, NOTIFICATION_TIMEOUT));
|
|
|
|
if (notifications.length > MAX_NUM_NOTIFICATION_LINES)
|
|
removeOldNotifications();
|
|
else
|
|
displayNotifications();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
warn("notification of unknown type!");
|
|
}
|
|
}
|
|
|
|
function removeOldNotifications()
|
|
{
|
|
clearTimeout(notificationsTimers[0]); // The timer only needs to be cleared when new notifications bump old notifications off
|
|
notificationsTimers.shift();
|
|
notifications.shift();
|
|
displayNotifications();
|
|
}
|
|
|
|
function displayNotifications()
|
|
{
|
|
var messages = [];
|
|
for each (var n in notifications)
|
|
{
|
|
var parameters = n.parameters || {};
|
|
if (n.translateParameters)
|
|
translateObjectKeys(parameters, n.translateParameters);
|
|
var message = n.message;
|
|
if (n.translateMessage)
|
|
message = translate(message);
|
|
messages.push(sprintf(message, parameters));
|
|
}
|
|
Engine.GetGUIObjectByName("notificationText").caption = messages.join("\n");
|
|
}
|
|
|
|
function updateTimeNotifications()
|
|
{
|
|
var notifications = Engine.GuiInterfaceCall("GetTimeNotifications");
|
|
var notificationText = "";
|
|
for (var n of notifications)
|
|
{
|
|
var message = n.message;
|
|
if (n.translateMessage)
|
|
message = translate(message);
|
|
var parameters = n.parameters || {};
|
|
if (n.translateParameters)
|
|
translateObjectKeys(parameters, n.translateParameters);
|
|
parameters.time = timeToString(n.time);
|
|
notificationText += sprintf(message, parameters) + "\n";
|
|
}
|
|
Engine.GetGUIObjectByName("timeNotificationText").caption = notificationText;
|
|
}
|
|
|
|
// Returns [username, playercolor] for the given player
|
|
function getUsernameAndColor(player)
|
|
{
|
|
// This case is hit for AIs, whose names don't exist in playerAssignments.
|
|
var color = g_Players[player].color;
|
|
return [
|
|
escapeText(g_Players[player].name),
|
|
color.r + " " + color.g + " " + color.b,
|
|
];
|
|
}
|
|
|
|
// Messages
|
|
function handleNetMessage(message)
|
|
{
|
|
log(sprintf(translate("Net message: %(message)s"), { message: uneval(message) }));
|
|
|
|
switch (message.type)
|
|
{
|
|
case "netstatus":
|
|
// If we lost connection, further netstatus messages are useless
|
|
if (g_Disconnected)
|
|
return;
|
|
|
|
var obj = Engine.GetGUIObjectByName("netStatus");
|
|
switch (message.status)
|
|
{
|
|
case "waiting_for_players":
|
|
obj.caption = translate("Waiting for other players to connect...");
|
|
obj.hidden = false;
|
|
break;
|
|
case "join_syncing":
|
|
obj.caption = translate("Synchronising gameplay with other players...");
|
|
obj.hidden = false;
|
|
break;
|
|
case "active":
|
|
obj.caption = "";
|
|
obj.hidden = true;
|
|
break;
|
|
case "connected":
|
|
obj.caption = translate("Connected to the server.");
|
|
obj.hidden = false;
|
|
break;
|
|
case "authenticated":
|
|
obj.caption = translate("Connection to the server has been authenticated.");
|
|
obj.hidden = false;
|
|
break;
|
|
case "disconnected":
|
|
g_Disconnected = true;
|
|
obj.caption = translate("Connection to the server has been lost.") + "\n\n" + translate("The game has ended.");
|
|
obj.hidden = false;
|
|
break;
|
|
default:
|
|
error(sprintf("Unrecognised netstatus type %(netType)s", { netType: message.status }));
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case "players":
|
|
// Find and report all leavings
|
|
for (var host in g_PlayerAssignments)
|
|
{
|
|
if (! message.hosts[host])
|
|
{
|
|
// Tell the user about the disconnection
|
|
addChatMessage({ "type": "disconnect", "guid": host });
|
|
|
|
// Update the cached player data, so we can display the disconnection status
|
|
updatePlayerDataRemove(g_Players, host);
|
|
}
|
|
}
|
|
|
|
// Find and report all joinings
|
|
for (var host in message.hosts)
|
|
{
|
|
if (! g_PlayerAssignments[host])
|
|
{
|
|
// Update the cached player data, so we can display the correct name
|
|
updatePlayerDataAdd(g_Players, host, message.hosts[host]);
|
|
|
|
// Tell the user about the connection
|
|
addChatMessage({ "type": "connect", "guid": host }, message.hosts);
|
|
}
|
|
}
|
|
|
|
g_PlayerAssignments = message.hosts;
|
|
|
|
if (g_IsController)
|
|
{
|
|
var players = [ assignment.name for each (assignment in g_PlayerAssignments) ]
|
|
Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", "));
|
|
}
|
|
|
|
break;
|
|
|
|
case "chat":
|
|
addChatMessage({ "type": "message", "guid": message.guid, "text": message.text });
|
|
break;
|
|
|
|
case "aichat":
|
|
addChatMessage({ "type": "message", "guid": message.guid, "text": message.text, "translate": true });
|
|
break;
|
|
|
|
// To prevent errors, ignore these message types that occur during autostart
|
|
case "gamesetup":
|
|
case "start":
|
|
break;
|
|
|
|
default:
|
|
error(sprintf("Unrecognised net message type %(messageType)s", { messageType: message.type }));
|
|
}
|
|
}
|
|
|
|
function submitChatDirectly(text)
|
|
{
|
|
if (text.length)
|
|
{
|
|
if (g_IsNetworked)
|
|
Engine.SendNetworkChat(text);
|
|
else
|
|
addChatMessage({ "type": "message", "guid": "local", "text": text });
|
|
}
|
|
}
|
|
|
|
function submitChatInput()
|
|
{
|
|
var input = Engine.GetGUIObjectByName("chatInput");
|
|
var text = input.caption;
|
|
var isCheat = false;
|
|
if (text.length)
|
|
{
|
|
if (!g_IsObserver && g_Players[Engine.GetPlayerID()].cheatsEnabled)
|
|
{
|
|
for each (var cheat in Object.keys(cheats))
|
|
{
|
|
// Line must start with the cheat.
|
|
if (text.indexOf(cheat) !== 0)
|
|
continue;
|
|
|
|
// test for additional parameter which is the rest of the string after the cheat
|
|
var parameter = "";
|
|
if (cheats[cheat].DefaultParameter !== undefined)
|
|
{
|
|
var par = text.substr(cheat.length);
|
|
par = par.replace(/^\W+/, '').replace(/\W+$/, ''); // remove whitespaces at start and end
|
|
|
|
// check, if the isNumeric flag is set
|
|
if (cheats[cheat].isNumeric)
|
|
{
|
|
// Match the first word in the substring.
|
|
var match = par.match(/\S+/);
|
|
if (match && match[0])
|
|
par = Math.floor(match[0]);
|
|
// check, if valid number could be parsed
|
|
if (par <= 0 || isNaN(par))
|
|
par = "";
|
|
}
|
|
|
|
// replace default parameter, if not empty or number
|
|
if (par.length > 0 || parseFloat(par) === par)
|
|
parameter = par;
|
|
else
|
|
parameter = cheats[cheat].DefaultParameter;
|
|
}
|
|
|
|
Engine.PostNetworkCommand({
|
|
"type": "cheat",
|
|
"action": cheats[cheat].Action,
|
|
"parameter": parameter,
|
|
"text": cheats[cheat].Type,
|
|
"selected": g_Selection.toList(),
|
|
"templates": cheats[cheat].Templates,
|
|
"player": Engine.GetPlayerID()});
|
|
isCheat = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isCheat)
|
|
{
|
|
if (Engine.GetGUIObjectByName("toggleTeamChat").checked)
|
|
text = "/team " + text;
|
|
|
|
if (g_IsNetworked)
|
|
Engine.SendNetworkChat(text);
|
|
else
|
|
addChatMessage({ "type": "message", "guid": "local", "text": text });
|
|
}
|
|
input.caption = ""; // Clear chat input
|
|
}
|
|
|
|
input.blur(); // Remove focus
|
|
|
|
toggleChatWindow();
|
|
}
|
|
|
|
function addChatMessage(msg, playerAssignments)
|
|
{
|
|
// Default to global assignments, but allow overriding for when reporting
|
|
// new players joining
|
|
if (!playerAssignments)
|
|
playerAssignments = g_PlayerAssignments;
|
|
|
|
var playerColor, username;
|
|
|
|
// No context by default. May be set by parseChatCommands().
|
|
msg.context = "";
|
|
|
|
if ("guid" in msg && playerAssignments[msg.guid])
|
|
{
|
|
var n = playerAssignments[msg.guid].player;
|
|
// Observers have an ID of -1 which is not a valid index.
|
|
if (n < 0)
|
|
n = 0;
|
|
playerColor = g_Players[n].color.r + " " + g_Players[n].color.g + " " + g_Players[n].color.b;
|
|
username = escapeText(playerAssignments[msg.guid].name);
|
|
|
|
// Parse in-line commands in regular messages.
|
|
if (msg.type == "message")
|
|
parseChatCommands(msg, playerAssignments);
|
|
}
|
|
else if (msg.type == "defeat" && msg.player)
|
|
{
|
|
[username, playerColor] = getUsernameAndColor(msg.player);
|
|
}
|
|
else if (msg.type == "message")
|
|
{
|
|
[username, playerColor] = getUsernameAndColor(msg.player);
|
|
parseChatCommands(msg, playerAssignments);
|
|
}
|
|
else
|
|
{
|
|
playerColor = "255 255 255";
|
|
username = translate("Unknown player");
|
|
}
|
|
|
|
var formatted;
|
|
|
|
switch (msg.type)
|
|
{
|
|
case "connect":
|
|
formatted = sprintf(translate("%(player)s has joined the game."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
break;
|
|
case "disconnect":
|
|
formatted = sprintf(translate("%(player)s has left the game."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
break;
|
|
case "defeat":
|
|
// In singleplayer, the local player is "You". "You has" is incorrect.
|
|
if (!g_IsNetworked && msg.player == Engine.GetPlayerID())
|
|
formatted = translate("You have been defeated.");
|
|
else
|
|
formatted = sprintf(translate("%(player)s has been defeated."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
break;
|
|
case "diplomacy":
|
|
var status = (msg.status == "ally" ? "allied" : (msg.status == "enemy" ? "at war" : "neutral"));
|
|
if (msg.player == Engine.GetPlayerID())
|
|
{
|
|
[username, playerColor] = getUsernameAndColor(msg.player1);
|
|
if (msg.status == "ally")
|
|
formatted = sprintf(translate("You are now allied with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
else if (msg.status == "enemy")
|
|
formatted = sprintf(translate("You are now at war with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
else // (msg.status == "neutral")
|
|
formatted = sprintf(translate("You are now neutral with %(player)s."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
}
|
|
else if (msg.player1 == Engine.GetPlayerID())
|
|
{
|
|
[username, playerColor] = getUsernameAndColor(msg.player);
|
|
if (msg.status == "ally")
|
|
formatted = sprintf(translate("%(player)s is now allied with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
else if (msg.status == "enemy")
|
|
formatted = sprintf(translate("%(player)s is now at war with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
else // (msg.status == "neutral")
|
|
formatted = sprintf(translate("%(player)s is now neutral with you."), { player: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
}
|
|
else // No need for other players to know of this.
|
|
return;
|
|
break;
|
|
case "tribute":
|
|
if (msg.player != Engine.GetPlayerID())
|
|
return;
|
|
|
|
[username, playerColor] = getUsernameAndColor(msg.player1);
|
|
|
|
// Format the amounts to proper English: 200 food, 100 wood, and 300 metal; 100 food; 400 wood and 200 stone
|
|
var amounts = Object.keys(msg.amounts)
|
|
.filter(function (type) { return msg.amounts[type] > 0; })
|
|
.map(function (type) { return msg.amounts[type] + " " + type; });
|
|
|
|
if (amounts.length > 1)
|
|
{
|
|
var lastAmount = amounts.pop();
|
|
amounts = sprintf(translate("%(previousAmounts)s and %(lastAmount)s"), {
|
|
previousAmounts: amounts.join(translate(", ")),
|
|
lastAmount: lastAmount
|
|
});
|
|
}
|
|
|
|
formatted = sprintf(translate("%(player)s has sent you %(amounts)s."), {
|
|
player: "[color=\"" + playerColor + "\"]" + username + "[/color]",
|
|
amounts: amounts
|
|
});
|
|
break;
|
|
case "attack":
|
|
if (msg.player != Engine.GetPlayerID())
|
|
return;
|
|
|
|
[username, playerColor] = getUsernameAndColor(msg.attacker);
|
|
formatted = sprintf(translate("You have been attacked by %(attacker)s!"), { attacker: "[color=\"" + playerColor + "\"]" + username + "[/color]" });
|
|
break;
|
|
case "message":
|
|
// May have been hidden by the 'team' command.
|
|
if (msg.hide)
|
|
return;
|
|
|
|
var message;
|
|
if ("translate" in msg && msg.translate)
|
|
message = translate(msg.text); // No need to escape, not a use message.
|
|
else
|
|
message = escapeText(msg.text)
|
|
|
|
if (msg.action)
|
|
{
|
|
if (msg.context !== "")
|
|
{
|
|
Engine.Console_Write(sprintf(translate("(%(context)s) * %(user)s %(message)s"), {
|
|
context: msg.context,
|
|
user: username,
|
|
message: message
|
|
}));
|
|
formatted = sprintf(translate("(%(context)s) * %(user)s %(message)s"), {
|
|
context: msg.context,
|
|
user: "[color=\"" + playerColor + "\"]" + username + "[/color]",
|
|
message: message
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Engine.Console_Write(sprintf(translate("* %(user)s %(message)s"), {
|
|
user: username,
|
|
message: message
|
|
}));
|
|
formatted = sprintf(translate("* %(user)s %(message)s"), {
|
|
user: "[color=\"" + playerColor + "\"]" + username + "[/color]",
|
|
message: message
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var userTag = sprintf(translate("<%(user)s>"), { user: username })
|
|
var formattedUserTag = sprintf(translate("<%(user)s>"), { user: "[color=\"" + playerColor + "\"]" + username + "[/color]" })
|
|
if (msg.context !== "")
|
|
{
|
|
Engine.Console_Write(sprintf(translate("(%(context)s) %(userTag)s %(message)s"), {
|
|
context: msg.context,
|
|
userTag: userTag,
|
|
message: message
|
|
}));
|
|
formatted = sprintf(translate("(%(context)s) %(userTag)s %(message)s"), {
|
|
context: msg.context,
|
|
userTag: formattedUserTag,
|
|
message: message
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Engine.Console_Write(sprintf(translate("%(userTag)s %(message)s"), { userTag: userTag, message: message}));
|
|
formatted = sprintf(translate("%(userTag)s %(message)s"), { userTag: formattedUserTag, message: message});
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
error(sprintf("Invalid chat message '%(message)s'", { message: uneval(msg) }));
|
|
return;
|
|
}
|
|
|
|
chatMessages.push(formatted);
|
|
chatTimers.push(setTimeout(removeOldChatMessages, CHAT_TIMEOUT));
|
|
|
|
if (chatMessages.length > MAX_NUM_CHAT_LINES)
|
|
removeOldChatMessages();
|
|
else
|
|
Engine.GetGUIObjectByName("chatText").caption = chatMessages.join("\n");
|
|
}
|
|
|
|
function removeOldChatMessages()
|
|
{
|
|
clearTimeout(chatTimers[0]); // The timer only needs to be cleared when new messages bump old messages off
|
|
chatTimers.shift();
|
|
chatMessages.shift();
|
|
Engine.GetGUIObjectByName("chatText").caption = chatMessages.join("\n");
|
|
}
|
|
|
|
// Parses chat messages for commands.
|
|
function parseChatCommands(msg, playerAssignments)
|
|
{
|
|
// Only interested in messages that start with '/'.
|
|
if (!msg.text || msg.text[0] != '/')
|
|
return;
|
|
|
|
var sender;
|
|
if (playerAssignments[msg.guid])
|
|
sender = playerAssignments[msg.guid].player;
|
|
else
|
|
sender = msg.player;
|
|
|
|
var recurse = false;
|
|
var split = msg.text.split(/\s/);
|
|
|
|
// Parse commands embedded in the message.
|
|
switch (split[0])
|
|
{
|
|
case "/all":
|
|
// Resets values that 'team' or 'enemy' may have set.
|
|
msg.context = "";
|
|
msg.hide = false;
|
|
recurse = true;
|
|
break;
|
|
case "/team":
|
|
// Check if we are in a team.
|
|
if (g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1)
|
|
{
|
|
if (g_Players[Engine.GetPlayerID()].team != g_Players[sender].team)
|
|
msg.hide = true;
|
|
else
|
|
msg.context = translate("Team");
|
|
}
|
|
else
|
|
msg.hide = true;
|
|
recurse = true;
|
|
break;
|
|
case "/enemy":
|
|
// Check if we are in a team.
|
|
if (g_Players[Engine.GetPlayerID()] && g_Players[Engine.GetPlayerID()].team != -1)
|
|
{
|
|
if (g_Players[Engine.GetPlayerID()].team == g_Players[sender].team && sender != Engine.GetPlayerID())
|
|
msg.hide = true;
|
|
else
|
|
msg.context = translate("Enemy");
|
|
}
|
|
recurse = true;
|
|
break;
|
|
case "/me":
|
|
msg.action = true;
|
|
break;
|
|
case "/msg":
|
|
var trimmed = msg.text.substr(split[0].length + 1);
|
|
var matched = "";
|
|
|
|
// Reject names which don't match or are a superset of the intended name.
|
|
for each (var player in playerAssignments)
|
|
if (trimmed.indexOf(player.name + " ") == 0 && player.name.length > matched.length)
|
|
matched = player.name;
|
|
|
|
// If the local player's name was the longest one matched, show the message.
|
|
var playerName = g_Players[Engine.GetPlayerID()].name;
|
|
if (matched.length && (matched == playerName || sender == Engine.GetPlayerID()))
|
|
{
|
|
msg.context = translate("Private");
|
|
msg.text = trimmed.substr(matched.length + 1);
|
|
msg.hide = false; // Might override team message hiding.
|
|
return;
|
|
}
|
|
else
|
|
msg.hide = true;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
msg.text = msg.text.substr(split[0].length + 1);
|
|
|
|
// Hide the message if parsing commands left it empty.
|
|
if (!msg.text.length)
|
|
msg.hide = true;
|
|
|
|
// Attempt to parse more commands if the current command allows it.
|
|
if (recurse)
|
|
parseChatCommands(msg, playerAssignments);
|
|
}
|