Index: binaries/data/mods/public/gui/session/chat/Chat.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/Chat.js
@@ -0,0 +1,91 @@
+/**
+ * This class instantiates the various subclasses and links them.
+ */
+class Chat
+{
+ constructor()
+ {
+ this.ChatWindow = new ChatWindow();
+ this.ChatHistory = new ChatHistory();
+ this.ChatOverlay = new ChatOverlay();
+
+ this.ChatSender = new ChatSender();
+ this.ChatSender.registerChatSubmitHandler(executeNetworkCommand);
+ this.ChatSender.registerChatSubmitHandler(executeCheat);
+ this.ChatSender.registerChatSubmitHandler(this.submitChat.bind(this));
+ this.ChatSender.registerChatSubmittedHandler(this.closePage.bind(this));
+
+ this.ChatAddressees = new ChatAddressees();
+ this.ChatAddressees.registerSelectionChangeHandler(
+ command => { this.ChatSender.onSelectionChange(command); });
+
+ this.ChatMessageFormat = new ChatMessageFormat();
+ this.ChatMessageFormat.registerMessageHandlers(ChatMessageFormatNetwork);
+ this.ChatMessageFormat.registerMessageHandlers(ChatMessageFormatSimulation);
+ this.ChatMessageFormatPlayer = new ChatMessageFormatPlayer();
+ this.ChatMessageFormatPlayer.registerAddresseeTypes(this.ChatAddressees.AddresseeTypes);
+ this.ChatMessageFormat.registerMessageHandler("message", this.ChatMessageFormatPlayer);
+
+ Engine.SetGlobalHotkey("chat", () => this.openPage());
+ Engine.SetGlobalHotkey("teamchat", () => this.openPage(g_IsObserver ? "/observers" : "/allies"));
+ Engine.SetGlobalHotkey("privatechat", () => this.openPage());
+ }
+
+ /**
+ * Called by the owner whenever g_PlayerAssignments or g_Players changed.
+ */
+ onUpdatePlayers()
+ {
+ this.ChatAddressees.updateChatAddressees();
+ }
+
+ openPage(command = "")
+ {
+ if (g_Disconnected)
+ return;
+
+ closeOpenDialogs();
+
+ this.ChatAddressees.select(command);
+ this.ChatHistory.displayChatHistory();
+ this.ChatWindow.openPage(command);
+ }
+
+ closePage()
+ {
+ this.ChatWindow.closePage();
+ }
+
+ /**
+ * Send the given chat message.
+ */
+ submitChat(text, command = "")
+ {
+ if (command.startsWith("/msg "))
+ Engine.SetGlobalHotkey("privatechat", () => { this.openPage(command); });
+
+ let msg = command + " " + text;
+
+ if (Engine.HasNetClient())
+ Engine.SendNetworkChat(msg);
+ else
+ this.addMessage({
+ "type": "message",
+ "guid": "local",
+ "text": msg
+ });
+ }
+
+ addMessage(msg)
+ {
+ let formatted = this.ChatMessageFormat.parseMessage(msg);
+ if (!formatted)
+ return;
+
+ this.ChatOverlay.onChatMessage(msg, formatted);
+ this.ChatHistory.onChatMessage(msg, formatted);
+
+ if (this.ChatWindow.isOpen() && this.ChatWindow.isExtended())
+ this.ChatHistory.displayChatHistory();
+ }
+}
Index: binaries/data/mods/public/gui/session/chat/ChatAddressees.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatAddressees.js
@@ -0,0 +1,124 @@
+class ChatAddressees
+{
+ constructor()
+ {
+ this.selectionChangeHandlers = [];
+
+ this.chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
+ this.chatAddressee.onSelectionChange = this.onSelectionChange.bind(this);
+ }
+
+ getSelection()
+ {
+ return this.chatAddressee.list_data[this.chatAddressee.selected] || "";
+ }
+
+ select(command)
+ {
+ this.chatAddressee.selected = this.chatAddressee.list_data.indexOf(command);
+ }
+
+ registerSelectionChangeHandler(handler)
+ {
+ this.selectionChangeHandlers.push(handler);
+ }
+
+ onSelectionChange()
+ {
+ let selection = this.getSelection();
+ for (let handler of this.selectionChangeHandlers)
+ handler(selection);
+ }
+
+ updateChatAddressees()
+ {
+ // Remember previously selected item
+ let selectedName = this.getSelection();
+ selectedName = selectedName.startsWith("/msg") && selectedName.substr(5);
+
+ let addressees = this.AddresseeTypes.filter(
+ addresseeType => addresseeType.isSelectable()).map(
+ addresseeType => ({
+ "label": translateWithContext("chat addressee", addresseeType.label),
+ "cmd": addresseeType.command
+ }));
+
+ // 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 = this.getSelection();
+ this.chatAddressee.list = addressees.map(adressee => adressee.label);
+ this.chatAddressee.list_data = addressees.map(adressee => adressee.cmd);
+ this.chatAddressee.selected = Math.max(0, this.chatAddressee.list_data.indexOf(oldChatAddressee));
+ }
+}
+
+ChatAddressees.prototype.AddresseeTypes = [
+ {
+ "command": "",
+ "isSelectable": () => true,
+ "label": markForTranslationWithContext("chat addressee", "Everyone"),
+ "isAddressee": () => true
+ },
+ {
+ "command": "/allies",
+ "isSelectable": () => !g_IsObserver,
+ "label": markForTranslationWithContext("chat addressee", "Allies"),
+ "context": markForTranslationWithContext("chat message context", "Ally"),
+ "isAddressee":
+ senderID =>
+ g_Players[senderID] &&
+ g_Players[Engine.GetPlayerID()] &&
+ g_Players[senderID].isMutualAlly[Engine.GetPlayerID()],
+ },
+ {
+ "command": "/enemies",
+ "isSelectable": () => !g_IsObserver,
+ "label": markForTranslationWithContext("chat addressee", "Enemies"),
+ "context": markForTranslationWithContext("chat message context", "Enemy"),
+ "isAddressee":
+ senderID =>
+ g_Players[senderID] &&
+ g_Players[Engine.GetPlayerID()] &&
+ g_Players[senderID].isEnemy[Engine.GetPlayerID()],
+ },
+ {
+ "command": "/observers",
+ "isSelectable": () => true,
+ "label": markForTranslationWithContext("chat addressee", "Observers"),
+ "context": markForTranslationWithContext("chat message context", "Observer"),
+ "isAddressee": senderID => g_IsObserver
+ },
+ {
+ "command": "/msg",
+ "isSelectable": () => false,
+ "label": undefined,
+ "context": markForTranslationWithContext("chat message context", "Private"),
+ "isAddressee": (senderID, addresseeGUID) => addresseeGUID == Engine.GetPlayerGUID()
+ }
+];
Index: binaries/data/mods/public/gui/session/chat/ChatHistory.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatHistory.js
@@ -0,0 +1,117 @@
+class ChatHistory
+{
+ constructor()
+ {
+ /**
+ * All unparsed chat messages received since connect, including timestamp.
+ */
+ this.chatMessages = [];
+
+ this.chatHistoryText = Engine.GetGUIObjectByName("chatHistoryText");
+ this.chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
+
+ this.initChatHistoryFilter();
+ }
+
+ initChatHistoryFilter()
+ {
+ let filters = prepareForDropdown(this.Filters.filter(chatFilter => !chatFilter.hidden));
+ this.chatHistoryFilter.onSelectionChange = this.displayChatHistory.bind(this); // TODO: Focus chat input
+ this.chatHistoryFilter.list = filters.text.map(text => translateWithContext("chat history filter", text));
+ this.chatHistoryFilter.list_data = filters.key;
+ this.chatHistoryFilter.selected = 0;
+ }
+
+ displayChatHistory()
+ {
+ let selected = this.chatHistoryFilter.list_data[this.chatHistoryFilter.selected];
+
+ this.chatHistoryText.caption =
+ this.chatMessages.filter(msg => msg.filter[selected]).map(msg =>
+ Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
+ sprintf(translate("%(time)s %(message)s"), {
+ "time": msg.timePrefix,
+ "message": msg.txt
+ }) :
+ msg.txt).join("\n");
+ }
+
+ onChatMessage(msg, formatted)
+ {
+ // Save to chat history
+ let historical = {
+ "txt": formatted,
+ "timePrefix": sprintf(translate("\\[%(time)s]"), {
+ "time": Engine.FormatMillisecondsIntoDateStringLocal(Date.now(), 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 this.Filters)
+ historical.filter[filter.key] = filter.filter(msg, senderID);
+
+ this.chatMessages.push(historical);
+ }
+}
+
+/**
+ * Notice only messages will be filtered that are visible to the player in the first place.
+ */
+ChatHistory.prototype.Filters = [
+ {
+ "key": "all",
+ "text": markForTranslationWithContext("chat history filter", "Chat and notifications"),
+ "filter": (msg, senderID) => true
+ },
+ {
+ "key": "chat",
+ "text": markForTranslationWithContext("chat history filter", "Chat messages"),
+ "filter": (msg, senderID) => msg.type == "message"
+ },
+ {
+ "key": "player",
+ "text": markForTranslationWithContext("chat history filter", "Players chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ senderID > 0 && !isPlayerObserver(senderID)
+ },
+ {
+ "key": "ally",
+ "text": markForTranslationWithContext("chat history filter", "Ally chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/allies"
+ },
+ {
+ "key": "enemy",
+ "text": markForTranslationWithContext("chat history filter", "Enemy chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/enemies"
+ },
+ {
+ "key": "observer",
+ "text": markForTranslationWithContext("chat history filter", "Observer chat"),
+ "filter": (msg, senderID) =>
+ msg.type == "message" &&
+ msg.cmd && msg.cmd == "/observers"
+ },
+ {
+ "key": "private",
+ "text": markForTranslationWithContext("chat history filter", "Private chat"),
+ "filter": (msg, senderID) => !!msg.isVisiblePM
+ },
+ {
+ "key": "gamenotifications",
+ "text": markForTranslationWithContext("chat history filter", "Game notifications"),
+ "filter": (msg, senderID) => msg.type != "message" && msg.guid === undefined
+ },
+ {
+ "key": "chatnotifications",
+ "text": markForTranslationWithContext("chat history filter", "Network notifications"),
+ "filter": (msg, senderID) => msg.type != "message" && msg.guid !== undefined,
+ "hidden": !Engine.HasNetClient()
+ }
+];
Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormat.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatMessageFormat.js
@@ -0,0 +1,49 @@
+class ChatMessageFormat
+{
+ constructor()
+ {
+ this.messageTypes = {};
+
+ this.registerMessageHandler("system", new ChatMessageFormat.System());
+ }
+
+ registerMessageHandler(type, handler)
+ {
+ if (!this.messageTypes[type])
+ this.messageTypes[type] = [];
+
+ this.messageTypes[type].push(handler);
+ }
+
+ registerMessageHandlers(handlerTypes)
+ {
+ for (let type in handlerTypes)
+ this.registerMessageHandler(type, new handlerTypes[type]());
+ }
+
+ parseMessage(msg)
+ {
+ if (!this.messageTypes[msg.type])
+ {
+ error("Unknown chat message type: " + uneval(msg));
+ return undefined;
+ }
+
+ for (let handler of this.messageTypes[msg.type])
+ {
+ let txt = handler.parse(msg);
+ if (txt)
+ return txt;
+ }
+
+ return undefined;
+ }
+}
+
+ChatMessageFormat.System = class
+{
+ parse(msg)
+ {
+ return msg.txt;
+ }
+};
Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatNetwork.js
@@ -0,0 +1,66 @@
+class ChatMessageFormatNetwork
+{
+}
+
+ChatMessageFormatNetwork.clientlist = class
+{
+ parse()
+ {
+ return getUsernameList();
+ }
+};
+
+ChatMessageFormatNetwork.connect = class
+{
+ parse(msg)
+ {
+ return 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) });
+ }
+};
+
+ChatMessageFormatNetwork.disconnect = class
+{
+ parse(msg)
+ {
+ return sprintf(translate("%(player)s has left the game."), {
+ "player": colorizePlayernameByGUID(msg.guid)
+ });
+ }
+};
+
+ChatMessageFormatNetwork.kicked = class
+{
+ parse(msg)
+ {
+ return 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)
+ )
+ });
+ }
+};
+
+ChatMessageFormatNetwork.rejoined = class
+{
+ parse(msg)
+ {
+ return 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) });
+ }
+};
Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatPlayer.js
@@ -0,0 +1,162 @@
+class ChatMessageFormatPlayer
+{
+ constructor()
+ {
+ this.AddresseeTypes = [];
+ }
+
+ registerAddresseeTypes(types)
+ {
+ this.AddresseeTypes = this.AddresseeTypes.concat(types);
+ }
+
+ parse(msg)
+ {
+ if (!msg.text)
+ return "";
+
+ let isMe = msg.text.startsWith("/me ");
+ if (!isMe && !this.parseAddressedMessage(msg))
+ return "";
+
+ isMe = msg.text.startsWith("/me ");
+ 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 &&
+ msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
+ soundNotification("nick");
+ }
+
+ // GUID for players, playerID for AIs
+ let coloredUsername = msg.guid != -1 ? colorizePlayernameByGUID(msg.guid) : colorizePlayernameByID(msg.player);
+
+ return sprintf(translate(this.strings[isMe ? "me" : "regular"][msg.context ? "context" : "no-context"]), {
+ "message": msg.text,
+ "context": msg.context ? translateWithContext("chat message context", msg.context) : "",
+ "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.
+ */
+ parseAddressedMessage(msg)
+ {
+ if (!msg.text.startsWith('/'))
+ 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 = this.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
+ let addresseeType = this.AddresseeTypes.find(type => type.command == msg.cmd);
+ if (!addresseeType)
+ {
+ if (isSender)
+ warn("Unknown chat command: " + msg.cmd);
+ return false;
+ }
+ msg.context = addresseeType.context;
+
+ // 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 || addresseeType.isAddressee(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.
+ */
+ 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;
+ }
+}
+
+/**
+ * Chatmessage shown after commands like /me or /enemies.
+ */
+ChatMessageFormatPlayer.prototype.strings = {
+ "regular": {
+ "context": markForTranslation("(%(context)s) %(userTag)s %(message)s"),
+ "no-context": markForTranslation("%(userTag)s %(message)s")
+ },
+ "me": {
+ "context": markForTranslation("(%(context)s) * %(user)s %(message)s"),
+ "no-context": markForTranslation("* %(user)s %(message)s")
+ }
+};
Index: binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatMessageFormatSimulation.js
@@ -0,0 +1,154 @@
+class ChatMessageFormatSimulation
+{
+}
+
+ChatMessageFormatSimulation.attack = class
+{
+ parse(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)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.barter = class
+{
+ parse(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)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.playerstate = class
+{
+ parse(msg)
+ {
+ if (!msg.message.pluralMessage)
+ return sprintf(translate(msg.message), {
+ "player": colorizePlayernameByID(msg.players[0])
+ });
+
+ let mPlayers = msg.players.map(playerID => colorizePlayernameByID(playerID));
+ let lastPlayer = mPlayers.pop();
+
+ return sprintf(translatePlural(msg.message.message, msg.message.pluralMessage, msg.message.pluralCount), {
+ // Translation: This comma is used for separating first to penultimate elements in an enumeration.
+ "players": mPlayers.join(translate(", ")),
+ "lastPlayer": lastPlayer
+ });
+ }
+};
+
+ChatMessageFormatSimulation.diplomacy = class
+{
+ parse(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(translate(this.strings[messageType][msg.status]), {
+ "player": colorizePlayernameByID(messageType == "active" ? msg.targetPlayer : msg.sourcePlayer),
+ "player2": colorizePlayernameByID(messageType == "active" ? msg.sourcePlayer : msg.targetPlayer)
+ });
+ }
+};
+
+ChatMessageFormatSimulation.diplomacy.prototype.strings = {
+ "active": {
+ "ally": markForTranslation("You are now allied with %(player)s."),
+ "enemy": markForTranslation("You are now at war with %(player)s."),
+ "neutral": markForTranslation("You are now neutral with %(player)s.")
+ },
+ "passive": {
+ "ally": markForTranslation("%(player)s is now allied with you."),
+ "enemy": markForTranslation("%(player)s is now at war with you."),
+ "neutral": markForTranslation("%(player)s is now neutral with you.")
+ },
+ "observer": {
+ "ally": markForTranslation("%(player)s is now allied with %(player2)s."),
+ "enemy": markForTranslation("%(player)s is now at war with %(player2)s."),
+ "neutral": markForTranslation("%(player)s is now neutral with %(player2)s.")
+ }
+};
+
+ChatMessageFormatSimulation.phase = class
+{
+ parse(msg)
+ {
+ let notifyPhase = Engine.ConfigDB_GetValue("user", "gui.session.notifications.phase");
+ if (notifyPhase == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer])
+ return "";
+
+ let message = "";
+ if (notifyPhase == "all")
+ {
+ 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))
+ });
+ }
+};
+
+/**
+ * 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.
+ */
+ChatMessageFormatSimulation.tribute = class
+{
+ parse(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)
+ });
+ }
+};
Index: binaries/data/mods/public/gui/session/chat/ChatOverlay.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatOverlay.js
@@ -0,0 +1,66 @@
+class ChatOverlay
+{
+ constructor()
+ {
+ this.chatText = Engine.GetGUIObjectByName("chatText");
+
+ /**
+ * Maximum number of lines to display simultaneously.
+ */
+ this.chatLines = 20;
+
+ /**
+ * Number of seconds after which chatmessages will disappear.
+ */
+ this.chatTimeout = 30;
+
+ /**
+ * Holds the timer-IDs used for hiding the chat after chatTimeout seconds.
+ */
+ this.chatTimers = [];
+
+ /**
+ * The currently displayed strings, limited by the given timeframe and limit above.
+ */
+ this.chatMessages = [];
+ }
+
+ /**
+ * Displays this message in the chat overlay and sets up the timer to remove it after a while.
+ */
+ onChatMessage(msg, chatMessage)
+ {
+ this.chatMessages.push(chatMessage);
+ this.chatTimers.push(setTimeout(this.removeOldChatMessage.bind(this), this.chatTimeout * 1000));
+
+ if (this.chatMessages.length > this.chatLines)
+ this.removeOldChatMessage();
+ else
+ this.chatText.caption = this.chatMessages.join("\n");
+ }
+
+ /**
+ * Empty all messages currently displayed in the chat overlay.
+ */
+ clearChatMessages()
+ {
+ this.chatMessages = [];
+ this.chatText.caption = "";
+
+ for (let timer of this.chatTimers)
+ clearTimeout(timer);
+
+ this.chatTimers = [];
+ }
+
+ /**
+ * Called when the timer has run out for the oldest chatmessage or when the message limit is reached.
+ */
+ removeOldChatMessage()
+ {
+ clearTimeout(this.chatTimers[0]);
+ this.chatTimers.shift();
+ this.chatMessages.shift();
+ this.chatText.caption = this.chatMessages.join("\n");
+ }
+}
Index: binaries/data/mods/public/gui/session/chat/ChatSender.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatSender.js
@@ -0,0 +1,60 @@
+class ChatSender
+{
+ constructor()
+ {
+ this.selectedCommand = "";
+ this.chatSubmitHandlers = [];
+ this.chatSubmittedHandlers = [];
+
+ this.chatInput = Engine.GetGUIObjectByName("chatInput");
+ this.chatInput.onPress = this.submitChatInput.bind(this);
+ this.chatInput.onTab = this.autoComplete.bind(this);
+
+ Engine.GetGUIObjectByName("sendChat").onPress = this.submitChatInput.bind(this);
+ }
+
+ /**
+ * The functions registered using this function will be called sequentially
+ * when the user submits chat, until one of them returns true.
+ */
+ registerChatSubmitHandler(submitChatHandler)
+ {
+ this.chatSubmitHandlers.push(submitChatHandler);
+ }
+
+ /**
+ * The functions registered using this function will be called after the user submitted chat input.
+ */
+ registerChatSubmittedHandler(submittedChatHandler)
+ {
+ this.chatSubmittedHandlers.push(submittedChatHandler);
+ }
+
+ /**
+ * Called each time the addressee dropdown changes selection.
+ */
+ onSelectionChange(command)
+ {
+ this.selectedCommand = command;
+ this.chatInput.focus();
+ }
+
+ autoComplete()
+ {
+ let playernames = [];
+ for (let player in g_PlayerAssignments)
+ playernames.push(g_PlayerAssignments[player].name);
+ autoCompleteText(this.chatInput, playernames);
+ }
+
+ submitChatInput()
+ {
+ let text = this.chatInput.caption;
+
+ if (text.length)
+ this.chatSubmitHandlers.some(handler => handler(text, this.selectedCommand));
+
+ for (let handler of this.chatSubmittedHandlers)
+ handler();
+ }
+}
Index: binaries/data/mods/public/gui/session/chat/ChatWindow.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/chat/ChatWindow.js
@@ -0,0 +1,85 @@
+/**
+ * This class is concerned with opening, closing the chat page, and
+ * resizing it depending on whether the chat history is shown.
+ */
+class ChatWindow
+{
+ constructor()
+ {
+ this.chatInput = Engine.GetGUIObjectByName("chatInput");
+ this.closeChat = Engine.GetGUIObjectByName("closeChat");
+
+ this.extendedChat = Engine.GetGUIObjectByName("extendedChat");
+ this.chatHistoryText = Engine.GetGUIObjectByName("chatHistoryText");
+ this.chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
+
+ this.chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
+ this.chatDialogPanelSmallSize = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
+ this.chatDialogPanelLargeSize = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
+
+ // Adjust the width so that the chat history is in the golden ratio
+ this.aspectRatio = (1 + Math.sqrt(5)) / 2;
+
+ this.initPage();
+ }
+
+ initPage()
+ {
+ this.closeChat.onPress = this.closePage.bind(this);
+
+ this.extendedChat.onPress = () => {
+ Engine.ConfigDB_CreateAndWriteValueToFile("user", "chat.session.extended", String(this.isExtended()), "config/user.cfg");
+ this.resizeChatWindow();
+ this.chatInput.focus();
+ };
+
+ this.extendedChat.checked = Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
+
+ this.resizeChatWindow();
+ }
+
+ isOpen()
+ {
+ return !this.chatDialogPanel.hidden;
+ }
+
+ isExtended()
+ {
+ return this.extendedChat.checked;
+ }
+
+ openPage(command)
+ {
+ this.chatInput.focus();
+ this.chatDialogPanel.hidden = false;
+ }
+
+ closePage()
+ {
+ this.chatInput.caption = "";
+ this.chatInput.blur();
+ this.chatDialogPanel.hidden = true;
+ }
+
+ resizeChatWindow()
+ {
+ // Hide/show the panel
+ this.chatHistoryPage.hidden = !this.isExtended();
+
+ // Resize the window
+ if (this.isExtended())
+ {
+ this.chatDialogPanel.size = this.chatDialogPanelLargeSize;
+
+ let chatHistoryTextSize = this.chatHistoryText.getComputedSize();
+ let width = this.aspectRatio * (chatHistoryTextSize.bottom - chatHistoryTextSize.top);
+
+ let size = this.chatDialogPanel.size;
+ size.left = -width / 2 - this.chatHistoryText.size.left;
+ size.right = width / 2 + this.chatHistoryText.size.left;
+ this.chatDialogPanel.size = size;
+ }
+ else
+ this.chatDialogPanel.size = this.chatDialogPanelSmallSize;
+ }
+}
Index: binaries/data/mods/public/gui/session/chat/chat_window.xml
===================================================================
--- binaries/data/mods/public/gui/session/chat/chat_window.xml
+++ binaries/data/mods/public/gui/session/chat/chat_window.xml
@@ -27,12 +27,11 @@
tooltip_style="sessionToolTipBold"
>
Filter the chat history.
- updateChatHistory();
+ />
-
+ />
@@ -98,9 +86,8 @@
-
+
Send
- submitChatInput();
Index: binaries/data/mods/public/gui/session/chat_window.xml
===================================================================
--- binaries/data/mods/public/gui/session/chat_window.xml
+++ binaries/data/mods/public/gui/session/chat_window.xml
@@ -1,107 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Filter:
-
-
-
- Filter the chat history.
- updateChatHistory();
-
-
-
-
-
-
-
-
-
-
- To:
-
-
- Select chat addressee.
-
-
-
-
- Text:
-
-
- submitChatInput();
-
- let playernames = [];
- for (let player in g_PlayerAssignments)
- playernames.push(g_PlayerAssignments[player].name);
- autoCompleteText(this, playernames);
-
-
-
-
-
- Cancel
- closeChat();
-
-
-
-
- onToggleChatWindowExtended();
-
-
-
-
- History
-
-
-
-
- Send
- submitChatInput();
-
-
-
-
Index: binaries/data/mods/public/gui/session/developer_overlay.js
===================================================================
--- binaries/data/mods/public/gui/session/developer_overlay.js
+++ binaries/data/mods/public/gui/session/developer_overlay.js
@@ -202,7 +202,7 @@
// Only players can send the simulation chat command
if (Engine.GetPlayerID() == -1)
- submitChatDirectly(message);
+ g_Chat.submitChat(message);
else
Engine.PostNetworkCommand({
"type": "aichat",
Index: binaries/data/mods/public/gui/session/hotkeys/misc.xml
===================================================================
--- binaries/data/mods/public/gui/session/hotkeys/misc.xml
+++ binaries/data/mods/public/gui/session/hotkeys/misc.xml
@@ -4,18 +4,6 @@
closeOpenDialogs();
-
- openChat();
-
-
-
- openChat(g_IsObserver ? "/observers" : "/allies");
-
-
-
- openChat(g_LastChatAddressee);
-
-
toggleGUI();
Index: binaries/data/mods/public/gui/session/menu.js
===================================================================
--- binaries/data/mods/public/gui/session/menu.js
+++ binaries/data/mods/public/gui/session/menu.js
@@ -136,8 +136,7 @@
function chatMenuButton()
{
- closeOpenDialogs();
- openChat();
+ g_Chat.openPage();
}
function resignMenuButton()
@@ -248,29 +247,6 @@
});
}
-function openChat(command = "")
-{
- if (g_Disconnected)
- return;
-
- closeOpenDialogs();
-
- let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
- chatAddressee.selected = chatAddressee.list_data.indexOf(command);
-
- Engine.GetGUIObjectByName("chatInput").focus();
- Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
-
- updateChatHistory();
-}
-
-function closeChat()
-{
- Engine.GetGUIObjectByName("chatInput").caption = "";
- Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
- Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
-}
-
function resizeDiplomacyDialog()
{
let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel");
@@ -295,74 +271,6 @@
dialog.size = size;
}
-function initChatWindow()
-{
- let filters = prepareForDropdown(g_ChatHistoryFilters.filter(chatFilter => !chatFilter.hidden));
- let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
- chatHistoryFilter.list = filters.text;
- chatHistoryFilter.list_data = filters.key;
- chatHistoryFilter.selected = 0;
-
- Engine.GetGUIObjectByName("extendedChat").checked =
- Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
-
- resizeChatWindow();
-}
-
-function resizeChatWindow()
-{
- // Hide/show the panel
- let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
- let extended = Engine.GetGUIObjectByName("extendedChat").checked;
- chatHistoryPage.hidden = !extended;
-
- // Resize the window
- let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
- if (extended)
- {
- chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
- // Adjust the width so that the chat history is in the golden ratio
- let chatHistory = Engine.GetGUIObjectByName("chatHistory");
- let height = chatHistory.getComputedSize().bottom - chatHistory.getComputedSize().top;
- let width = (1 + Math.sqrt(5)) / 2 * height;
- let size = chatDialogPanel.size;
- size.left = -width / 2 - chatHistory.size.left;
- size.right = width / 2 + chatHistory.size.left;
- chatDialogPanel.size = size;
- }
- else
- chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
-}
-
-function updateChatHistory()
-{
- if (Engine.GetGUIObjectByName("chatDialogPanel").hidden ||
- !Engine.GetGUIObjectByName("extendedChat").checked)
- return;
-
- let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
- let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected];
-
- Engine.GetGUIObjectByName("chatHistory").caption =
- g_ChatHistory.filter(msg => msg.filter[selected]).map(msg =>
- Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
- sprintf(translate("%(time)s %(message)s"), {
- "time": msg.timePrefix,
- "message": msg.txt
- }) :
- msg.txt
- ).join("\n");
-}
-
-function onToggleChatWindowExtended()
-{
- Engine.ConfigDB_CreateAndWriteValueToFile("user", "chat.session.extended", String(Engine.GetGUIObjectByName("extendedChat").checked), "config/user.cfg");
-
- resizeChatWindow();
-
- Engine.GetGUIObjectByName("chatInput").focus();
-}
-
function openDiplomacy()
{
closeOpenDialogs();
@@ -1250,10 +1158,11 @@
function closeOpenDialogs()
{
closeMenu();
- closeChat();
closeDiplomacy();
closeTrade();
closeObjectives();
+
+ g_Chat.closePage();
}
function formatTributeTooltip(playerID, resourceCode, amount)
Index: binaries/data/mods/public/gui/session/messages.js
===================================================================
--- binaries/data/mods/public/gui/session/messages.js
+++ binaries/data/mods/public/gui/session/messages.js
@@ -4,36 +4,6 @@
const g_Cheats = getCheatsData();
/**
- * Number of seconds after which chatmessages will disappear.
- */
-var g_ChatTimeout = 30;
-
-/**
- * Maximum number of lines to display simultaneously.
- */
-var 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 = "";
-
-/**
* All tutorial messages received so far.
*/
var g_TutorialMessages = [];
@@ -97,52 +67,6 @@
"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-victory": msg => formatDefeatVictoryMessage(msg.message, msg.players),
- "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.
*/
@@ -156,142 +80,12 @@
"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
- },
- {
- "key": "gamenotifications",
- "text": translateWithContext("chat history filter", "Game notifications"),
- "filter": (msg, senderID) => msg.type != "message" && msg.guid === undefined
- },
- {
- "key": "chatnotifications",
- "text": translateWithContext("chat history filter", "Network notifications"),
- "filter": (msg, senderID) => msg.type != "message" && msg.guid !== undefined,
- "hidden": !Engine.HasNetClient()
- }
-];
-
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.
@@ -478,7 +272,7 @@
g_Selection.reset();
g_Selection.addList(selection, false, cmd.type == "gather");
},
- "play-tracks": function (notification, player)
+ "play-tracks": function(notification, player)
{
if (notification.lock)
{
@@ -765,7 +559,7 @@
});
updateGUIObjects();
- updateChatAddressees();
+ g_Chat.onUpdatePlayers();
sendLobbyPlayerlistUpdate();
}
@@ -800,178 +594,17 @@
});
}
-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}
+ * TODO: Remove Placeholder/proxy function required by gui/common/network.js
*/
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(Date.now(), 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();
+ g_Chat.addMessage(msg);
}
function clearChatMessages()
{
- g_ChatMessages.length = 0;
- Engine.GetGUIObjectByName("chatText").caption = "";
-
- for (let timer of g_ChatTimers)
- clearTimeout(timer);
-
- g_ChatTimers.length = 0;
-}
-
-/**
- * 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");
+ g_Chat.ChatOverlay.clearMessages();
}
/**
@@ -1006,257 +639,6 @@
parameters[param] = colorizePlayernameByID(parameters[param]);
}
-function formatDefeatVictoryMessage(message, players)
-{
- if (!message.pluralMessage)
- return sprintf(translate(message), {
- "player": colorizePlayernameByID(players[0])
- });
-
- let mPlayers = players.map(playerID => colorizePlayernameByID(playerID));
- let lastPlayer = mPlayers.pop();
-
- return sprintf(translatePlural(message.message, message.pluralMessage, message.pluralCount), {
- // Translation: This comma is used for separating first to penultimate elements in an enumeration.
- "players": mPlayers.join(translate(", ")),
- "lastPlayer": lastPlayer
- });
-}
-
-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 == "none" || msg.player != g_ViewedPlayer && !g_IsObserver && !g_Players[msg.player].isMutualAlly[g_ViewedPlayer])
- return "";
-
- let message = "";
- if (notifyPhase == "all")
- {
- 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 &&
- msg.text.toLowerCase().indexOf(splitRatingFromNick(userName).nick.toLowerCase()) != -1)
- soundNotification("nick");
- }
-
- // 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.
*/
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js
+++ binaries/data/mods/public/gui/session/session.js
@@ -10,6 +10,8 @@
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
+var g_Chat;
+
var g_GameSpeeds;
/**
@@ -271,13 +273,19 @@
}
g_DeveloperOverlay = new DeveloperOverlay();
+ g_Chat = new Chat();
+
LoadModificationTemplates();
updatePlayerData();
initializeMusic(); // before changing the perspective
initGUIObjects();
if (hotloadData)
+ {
g_Selection.selected = hotloadData.selection;
+ g_PlayerAssignments = hotloadData.playerAssignments;
+ g_Players = hotloadData.player;
+ }
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
@@ -293,7 +301,6 @@
initBarterButtons();
initPanelEntities();
initViewedPlayerDropdown();
- initChatWindow();
Engine.SetBoundingBoxDebugOverlay(false);
updateEnabledRangeOverlayTypes();
}
@@ -557,7 +564,7 @@
Engine.SetViewedPlayer(g_ViewedPlayer);
updateDisplayedPlayerColors();
updateTopPanel();
- updateChatAddressees();
+ g_Chat.onUpdatePlayers();
updateHotkeyTooltips();
// Update GUI and clear player-dependent cache
@@ -605,7 +612,7 @@
function playersFinished(players, victoryString, won)
{
addChatMessage({
- "type": "defeat-victory",
+ "type": "playerstate",
"message": victoryString,
"players": players
});
@@ -616,7 +623,7 @@
sendLobbyPlayerlistUpdate();
updatePlayerData();
- updateChatAddressees();
+ g_Chat.onUpdatePlayers();
updateGameSpeedControl();
if (players.indexOf(g_ViewedPlayer) == -1)
@@ -762,7 +769,11 @@
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
- return { "selection": g_Selection.selected };
+ return {
+ "selection": g_Selection.selected,
+ "playerAssignments": g_PlayerAssignments,
+ "player": g_Players,
+ };
}
function getSavedGameData()
@@ -837,7 +848,7 @@
// Update followPlayerLabel
updateTopPanel();
- resizeChatWindow();
+ g_Chat.ChatWindow.resizeChatWindow();
}
function changeGameSpeed(speed)
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml
+++ binaries/data/mods/public/gui/session/session.xml
@@ -4,6 +4,7 @@
+
@@ -94,8 +95,8 @@
font="mono-stroke-10"
/>
+
-