Index: ps/trunk/binaries/data/mods/public/gui/loading/loading.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loading/loading.js (revision 22514)
+++ ps/trunk/binaries/data/mods/public/gui/loading/loading.js (revision 22515)
@@ -1,103 +1,106 @@
/**
* Path to a file containing quotes of historical figures.
*/
var g_QuotesFile = "gui/text/quotes.txt";
/**
* Directory storing txt files containing the gameplay tips.
*/
var g_TipsTextPath = "gui/text/tips/";
/**
* Directory storing the PNG images with filenames corresponding to the tip text files.
*/
var g_TipsImagePath = "loading/tips/";
var g_Data;
var g_EndPieceWidth = 16;
function init(data)
{
g_Data = data;
Engine.SetCursor("cursor-wait");
let tipFile = pickRandom(listFiles(g_TipsTextPath, ".txt", false));
if (tipFile)
{
let tipText = Engine.TranslateLines(Engine.ReadFile(g_TipsTextPath + tipFile + ".txt")).split("\n");
Engine.GetGUIObjectByName("tipTitle").caption = tipText.shift();
Engine.GetGUIObjectByName("tipText").caption = tipText.map(
// Translation: A bullet point used before every item of list of tips displayed on loading screen
text => text && sprintf(translate("• %(tiptext)s"), { "tiptext": text })
).join("\n\n");
Engine.GetGUIObjectByName("tipImage").sprite = "stretched:" + g_TipsImagePath + tipFile + ".png";
}
else
error("Failed to find any matching tips for the loading screen.");
// janwas: main loop now sets progress / description, but that won't
// happen until the first timeslice completes, so set initial values.
let loadingMapName = Engine.GetGUIObjectByName("loadingMapName");
if (data)
{
let mapName = translate(data.attribs.settings.Name);
switch (data.attribs.mapType)
{
case "skirmish":
case "scenario":
loadingMapName.caption = sprintf(translate("Loading “%(map)s”"), { "map": mapName });
break;
case "random":
loadingMapName.caption = sprintf(translate("Generating “%(map)s”"), { "map": mapName });
break;
}
}
Engine.GetGUIObjectByName("progressText").caption = "";
- Engine.GetGUIObjectByName("progressbar").caption = 0;
+
+ let progressbar = Engine.GetGUIObjectByName("progressbar");
+ progressbar.caption = 0;
+ progressbar.onGameLoadProgress = displayProgress;
Engine.GetGUIObjectByName("quoteText").caption = translate(pickRandom(Engine.ReadFileLines(g_QuotesFile).filter(line => line)));
}
-function displayProgress()
+function displayProgress(progression, description)
{
// Make the progessbar finish a little early so that the user can actually see it finish
- if (g_Progress >= 100)
+ if (progression >= 100)
return;
// Show 100 when it is really 99
- let progress = g_Progress + 1;
+ let progress = progression + 1;
Engine.GetGUIObjectByName("progressbar").caption = progress; // display current progress
Engine.GetGUIObjectByName("progressText").caption = progress + "%";
// Displays detailed loading info rather than a percent
- // Engine.GetGUIObjectByName("progressText").caption = g_LoadDescription; // display current progess details
+ // Engine.GetGUIObjectByName("progressText").caption = description;
// Keep curved right edge of progress bar in sync with the rest of the progress bar
let middle = Engine.GetGUIObjectByName("progressbar");
let rightSide = Engine.GetGUIObjectByName("progressbar_right");
let middleLength = (middle.size.right - middle.size.left) - (g_EndPieceWidth / 2);
let increment = Math.round(progress * middleLength / 100);
let size = rightSide.size;
size.left = increment;
size.right = increment + g_EndPieceWidth;
rightSide.size = size;
}
/**
* This is a reserved function name that is executed by the engine when it is ready
* to start the game (i.e. loading progress has reached 100%).
*/
function reallyStartGame()
{
Engine.SwitchGuiPage("page_session.xml", g_Data);
Engine.ResetCursor();
}
Index: ps/trunk/binaries/data/mods/public/gui/loading/loading.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loading/loading.xml (revision 22514)
+++ ps/trunk/binaries/data/mods/public/gui/loading/loading.xml (revision 22515)
@@ -1,50 +1,49 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/messages.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 22514)
+++ ps/trunk/binaries/data/mods/public/gui/session/messages.js (revision 22515)
@@ -1,1293 +1,1295 @@
/**
* All known cheat commands.
*/
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 = [];
/**
* GUI tags applied to the most recent tutorial message.
*/
var g_TutorialNewMessageTags = { "color": "yellow" };
/**
* 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-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.
*/
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" +
getDisconnectReason(msg.reason, true),
"waiting_for_players": msg => translate("Waiting for players to connect:"),
"join_syncing": msg => translate("Synchronizing 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)
{
playersFinished(notification.allies, notification.message, false);
},
"won": function(notification, player)
{
playersFinished(notification.allies, notification.message, true);
},
"diplomacy": function(notification, player)
{
updatePlayerData();
if (g_DiplomacyColorsToggle)
updateDisplayedPlayerColors();
addChatMessage({
"type": "diplomacy",
"sourcePlayer": player,
"targetPlayer": notification.targetPlayer,
"status": notification.status
});
},
"ceasefire-ended": function(notification, player)
{
updatePlayerData();
if (g_DiplomacyColorsToggle)
updateDisplayedPlayerColors();
},
"tutorial": function(notification, player)
{
updateTutorial(notification);
},
"tribute": function(notification, player)
{
addChatMessage({
"type": "tribute",
"sourcePlayer": notification.donator,
"targetPlayer": player,
"amounts": notification.amounts
});
},
"barter": function(notification, player)
{
addChatMessage({
"type": "barter",
"player": player,
"amountsSold": notification.amountsSold,
"amountsBought": notification.amountsBought,
"resourceSold": notification.resourceSold,
"resourceBought": notification.resourceBought
});
},
"spy-response": function(notification, player)
{
if (g_BribeButtonsWaiting[player])
g_BribeButtonsWaiting[player] = g_BribeButtonsWaiting[player].filter(p => p != notification.target);
if (notification.entity && g_ViewedPlayer == player)
{
closeDiplomacy();
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 rallypoint commands of trained animals
let entState = cmd.entities && cmd.entities[0] && GetEntityState(cmd.entities[0]);
if (g_ViewedPlayer != 0 &&
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);
}
else if (cmd.type == "delete-entities" && notification.position)
Engine.CameraMoveTo(notification.position.x, notification.position.y);
// Focus commanded entities, but don't lose previous focus when training units
else if (cmd.type != "train" && cmd.type != "research" && entState)
setCameraFollow(cmd.entities[0]);
if (["walk", "attack-walk", "patrol"].indexOf(cmd.type) != -1)
DrawTargetMarker(cmd);
// Select units affected by that command
let selection = [];
if (cmd.entities)
selection = cmd.entities;
if (cmd.target)
selection.push(cmd.target);
// Allow gaia in selection when gathering
g_Selection.reset();
g_Selection.addList(selection, false, cmd.type == "gather");
},
"play-tracks": function (notification, player)
{
if (notification.lock)
{
global.music.storeTracks(notification.tracks.map(track => ({ "Type": "custom", "File": track })));
global.music.setState(global.music.states.CUSTOM);
}
global.music.setLocked(notification.lock);
}
};
/**
* Loads all known cheat commands.
*/
function getCheatsData()
{
let cheats = {};
for (let fileName of Engine.ListDirectoryFiles("simulation/data/cheats/", "*.json", false))
{
let currentCheat = Engine.ReadJSONFile(fileName);
if (cheats[currentCheat.Name])
warn("Cheat name '" + currentCheat.Name + "' is already present");
else
cheats[currentCheat.Name] = currentCheat.Data;
}
return deepfreeze(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(code => text.indexOf(code) == 0);
if (!cheatCode)
return false;
let cheat = g_Cheats[cheatCode];
let parameter = text.substr(cheatCode.length + 1);
if (cheat.isNumeric)
parameter = +parameter;
if (cheat.DefaultParameter && !parameter)
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);
}
}
/**
* Updates the tutorial panel when a new goal.
*/
function updateTutorial(notification)
{
// Show the tutorial panel if not yet done
Engine.GetGUIObjectByName("tutorialPanel").hidden = false;
if (notification.warning)
{
Engine.GetGUIObjectByName("tutorialWarning").caption = coloredText(translate(notification.warning), "orange");
return;
}
let notificationText =
notification.instructions.reduce((instructions, item) =>
instructions + (typeof item == "string" ? translate(item) : colorizeHotkey(translate(item.text), item.hotkey)),
"");
Engine.GetGUIObjectByName("tutorialText").caption = g_TutorialMessages.concat(setStringTags(notificationText, g_TutorialNewMessageTags)).join("\n");
g_TutorialMessages.push(notificationText);
if (notification.readyButton)
{
Engine.GetGUIObjectByName("tutorialReady").hidden = false;
if (notification.leave)
{
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click to quit this tutorial.");
Engine.GetGUIObjectByName("tutorialReady").caption = translate("Quit");
Engine.GetGUIObjectByName("tutorialReady").onPress = leaveGame;
}
else
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Click when ready.");
}
else
{
Engine.GetGUIObjectByName("tutorialWarning").caption = translate("Follow the instructions.");
Engine.GetGUIObjectByName("tutorialReady").hidden = true;
}
}
/**
* 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 - GetSimState().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;
updateCinemaPath();
closeOpenDialogs();
}
}
function handleClientsLoadingMessage(guids)
{
let loadingClientsText = Engine.GetGUIObjectByName("loadingClientsText");
loadingClientsText.caption = guids.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of client loading messages", ", "));
}
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(translateWithContext("Separator for a list of players", ", "))
}),
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 a Bug" 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()
+function onReplayOutOfSync(turn, hash, expectedHash)
{
messageBox(
500, 140,
- translate("Out-Of-Sync error!") + "\n" +
+ sprintf(translate("Out-Of-Sync error on turn %(turn)s."), {
+ "turn": turn
+ }) + "\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(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();
}
/**
* 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_DisplayedPlayerColors[playerID]) : "white";
return coloredText(username || translate("Unknown Player"), playerColor);
}
/**
* Insert the colorized playername to chat messages sent by the AI and time notifications.
*/
function colorizePlayernameParameters(parameters)
{
for (let param in parameters)
if (param.startsWith("_player_"))
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.
*/
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 22514)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 22515)
@@ -1,158 +1,158 @@
onTick();
onWindowResized();
onSimulationUpdate();
onReplayFinished();
- onReplayOutOfSync();
+ onReplayOutOfSync(arguments[0], arguments[1], arguments[2]);
Engine.ConfigDB_CreateValue("user", "gui.session.timeelapsedcounter", String(Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") != "true"));
Engine.ConfigDB_CreateValue("user", "gui.session.ceasefirecounter", String(Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") != "true"));
ExitleaveGame();Game PausedClick to Resume GametogglePause();
Index: ps/trunk/source/gui/CGUI.cpp
===================================================================
--- ps/trunk/source/gui/CGUI.cpp (revision 22514)
+++ ps/trunk/source/gui/CGUI.cpp (revision 22515)
@@ -1,1757 +1,1761 @@
/* Copyright (C) 2019 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
#include
#include "GUI.h"
// Types - when including them into the engine.
#include "CButton.h"
#include "CChart.h"
#include "CCheckBox.h"
#include "CDropDown.h"
#include "CImage.h"
#include "CInput.h"
#include "CList.h"
#include "COList.h"
#include "CProgressBar.h"
#include "CRadioButton.h"
#include "CSlider.h"
#include "CText.h"
#include "CTooltip.h"
#include "MiniMap.h"
#include "graphics/FontMetrics.h"
#include "graphics/ShaderManager.h"
#include "graphics/TextRenderer.h"
#include "i18n/L10n.h"
#include "lib/bits.h"
#include "lib/input.h"
#include "lib/sysdep/sysdep.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/Renderer.h"
#include "scripting/ScriptFunctions.h"
#include "scriptinterface/ScriptInterface.h"
extern int g_yres;
const double SELECT_DBLCLICK_RATE = 0.5;
const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion
InReaction CGUI::HandleEvent(const SDL_Event_* ev)
{
InReaction ret = IN_PASS;
if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYUP)
{
const char* hotkey = static_cast(ev->ev.user.data1);
std::map >::iterator it = m_HotkeyObjects.find(hotkey);
if (it != m_HotkeyObjects.end())
for (IGUIObject* const& obj : it->second)
{
// Update hotkey status before sending the event,
// else the status will be outdated when processing the GUI event.
HotkeyInputHandler(ev);
ret = IN_HANDLED;
if (ev->ev.type == SDL_HOTKEYDOWN)
obj->SendEvent(GUIM_PRESSED, "press");
else
obj->SendEvent(GUIM_RELEASED, "release");
}
}
else if (ev->ev.type == SDL_MOUSEMOTION)
{
// Yes the mouse position is stored as float to avoid
// constant conversions when operating in a
// float-based environment.
m_MousePos = CPos((float)ev->ev.motion.x / g_GuiScale, (float)ev->ev.motion.y / g_GuiScale);
SGUIMessage msg(GUIM_MOUSE_MOTION);
GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject,
&IGUIObject::HandleMessage,
msg);
}
// Update m_MouseButtons. (BUTTONUP is handled later.)
else if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
case SDL_BUTTON_RIGHT:
case SDL_BUTTON_MIDDLE:
m_MouseButtons |= Bit(ev->ev.button.button);
break;
default:
break;
}
}
// Update m_MousePos (for delayed mouse button events)
CPos oldMousePos = m_MousePos;
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
{
m_MousePos = CPos((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale);
}
// Only one object can be hovered
IGUIObject* pNearest = NULL;
// TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress!
try
{
PROFILE("mouse events");
// TODO Gee: Optimizations needed!
// these two recursive function are quite overhead heavy.
// pNearest will after this point at the hovered object, possibly NULL
pNearest = FindObjectUnderMouse();
// Now we'll call UpdateMouseOver on *all* objects,
// we'll input the one hovered, and they will each
// update their own data and send messages accordingly
GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST,
m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest);
if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
// Focus the clicked object (or focus none if nothing clicked on)
SetFocusedObject(pNearest);
if (pNearest)
ret = pNearest->SendEvent(GUIM_MOUSE_PRESS_LEFT, "mouseleftpress");
break;
case SDL_BUTTON_RIGHT:
if (pNearest)
ret = pNearest->SendEvent(GUIM_MOUSE_PRESS_RIGHT, "mouserightpress");
break;
default:
break;
}
}
else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest)
{
if (ev->ev.wheel.y < 0)
ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_DOWN, "mousewheeldown");
else if (ev->ev.wheel.y > 0)
ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_UP, "mousewheelup");
}
else if (ev->ev.type == SDL_MOUSEBUTTONUP)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
if (pNearest)
{
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT];
pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time();
if (timeElapsed < SELECT_DBLCLICK_RATE)
ret = pNearest->SendEvent(GUIM_MOUSE_DBLCLICK_LEFT, "mouseleftdoubleclick");
else
ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_LEFT, "mouseleftrelease");
}
break;
case SDL_BUTTON_RIGHT:
if (pNearest)
{
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT];
pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time();
if (timeElapsed < SELECT_DBLCLICK_RATE)
ret = pNearest->SendEvent(GUIM_MOUSE_DBLCLICK_RIGHT, "mouserightdoubleclick");
else
ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_RIGHT, "mouserightrelease");
}
break;
}
// Reset all states on all visible objects
GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject,
&IGUIObject::ResetStates);
// Since the hover state will have been reset, we reload it.
GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST,
m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest);
}
}
catch (PSERROR_GUI& e)
{
UNUSED2(e);
debug_warn(L"CGUI::HandleEvent error");
// TODO Gee: Handle
}
// BUTTONUP's effect on m_MouseButtons is handled after
// everything else, so that e.g. 'press' handlers (activated
// on button up) see which mouse button had been pressed.
if (ev->ev.type == SDL_MOUSEBUTTONUP)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
case SDL_BUTTON_RIGHT:
case SDL_BUTTON_MIDDLE:
m_MouseButtons &= ~Bit(ev->ev.button.button);
break;
default:
break;
}
}
// Restore m_MousePos (for delayed mouse button events)
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
m_MousePos = oldMousePos;
// Handle keys for input boxes
if (GetFocusedObject())
{
if ((ev->ev.type == SDL_KEYDOWN &&
ev->ev.key.keysym.sym != SDLK_ESCAPE &&
!g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] &&
!g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) ||
ev->ev.type == SDL_HOTKEYDOWN ||
ev->ev.type == SDL_TEXTINPUT ||
ev->ev.type == SDL_TEXTEDITING)
{
ret = GetFocusedObject()->ManuallyHandleEvent(ev);
}
// else will return IN_PASS because we never used the button.
}
return ret;
}
void CGUI::TickObjects()
{
CStr action = "tick";
GUI::RecurseObject(0, m_BaseObject,
&IGUIObject::ScriptEvent, action);
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, this);
}
void CGUI::SendEventToAll(const CStr& EventName)
{
// janwas 2006-03-03: spoke with Ykkrosh about EventName case.
// when registering, case is converted to lower - this avoids surprise
// if someone were to get the case wrong and then not notice their
// handler is never called. however, until now, the other end
// (sending events here) wasn't converting to lower case,
// leading to a similar problem.
// now fixed; case is irrelevant since all are converted to lower.
- GUI::RecurseObject(0, m_BaseObject,
- &IGUIObject::ScriptEvent, EventName.LowerCase());
+ GUI::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, EventName.LowerCase());
+}
+
+void CGUI::SendEventToAll(const CStr& EventName, JS::HandleValueArray paramData)
+{
+ GUI::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, EventName.LowerCase(), paramData);
}
CGUI::CGUI(const shared_ptr& runtime)
: m_MouseButtons(0), m_FocusedObject(NULL), m_InternalNameNumber(0)
{
m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", runtime));
m_ScriptInterface->SetCallbackData(this);
GuiScriptingInit(*m_ScriptInterface);
m_ScriptInterface->LoadGlobalScripts();
m_BaseObject = new CGUIDummyObject;
m_BaseObject->SetGUI(this);
}
CGUI::~CGUI()
{
Destroy();
if (m_BaseObject)
delete m_BaseObject;
}
IGUIObject* CGUI::ConstructObject(const CStr& str)
{
if (m_ObjectTypes.count(str) > 0)
return (*m_ObjectTypes[str])();
else
{
// Error reporting will be handled with the NULL return.
return NULL;
}
}
void CGUI::Initialize()
{
// Add base types!
// You can also add types outside the GUI to extend the flexibility of the GUI.
// Pyrogenesis though will have all the object types inserted from here.
AddObjectType("empty", &CGUIDummyObject::ConstructObject);
AddObjectType("button", &CButton::ConstructObject);
AddObjectType("image", &CImage::ConstructObject);
AddObjectType("text", &CText::ConstructObject);
AddObjectType("checkbox", &CCheckBox::ConstructObject);
AddObjectType("radiobutton", &CRadioButton::ConstructObject);
AddObjectType("progressbar", &CProgressBar::ConstructObject);
AddObjectType("minimap", &CMiniMap::ConstructObject);
AddObjectType("input", &CInput::ConstructObject);
AddObjectType("list", &CList::ConstructObject);
AddObjectType("olist", &COList::ConstructObject);
AddObjectType("dropdown", &CDropDown::ConstructObject);
AddObjectType("tooltip", &CTooltip::ConstructObject);
AddObjectType("chart", &CChart::ConstructObject);
AddObjectType("slider", &CSlider::ConstructObject);
}
void CGUI::Draw()
{
// Clear the depth buffer, so the GUI is
// drawn on top of everything else
glClear(GL_DEPTH_BUFFER_BIT);
try
{
// Recurse IGUIObject::Draw() with restriction: hidden
// meaning all hidden objects won't call Draw (nor will it recurse its children)
GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::Draw);
}
catch (PSERROR_GUI& e)
{
LOGERROR("GUI draw error: %s", e.what());
}
}
void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, int CellID, const float& Z, const CRect& Rect, const CRect& UNUSED(Clipping))
{
// If the sprite doesn't exist (name == ""), don't bother drawing anything
if (Sprite.IsEmpty())
return;
// TODO: Clipping?
Sprite.Draw(Rect, CellID, m_Sprites, Z);
}
void CGUI::Destroy()
{
// We can use the map to delete all
// now we don't want to cancel all if one Destroy fails
for (const std::pair& p : m_pAllObjects)
{
try
{
p.second->Destroy();
}
catch (PSERROR_GUI& e)
{
UNUSED2(e);
debug_warn(L"CGUI::Destroy error");
// TODO Gee: Handle
}
delete p.second;
}
m_pAllObjects.clear();
for (const std::pair& p : m_Sprites)
delete p.second;
m_Sprites.clear();
m_Icons.clear();
}
void CGUI::UpdateResolution()
{
// Update ALL cached
GUI<>::RecurseObject(0, m_BaseObject, &IGUIObject::UpdateCachedSize);
}
void CGUI::AddObject(IGUIObject* pObject)
{
try
{
// Add CGUI pointer
GUI::RecurseObject(0, pObject, &IGUIObject::SetGUI, this);
m_BaseObject->AddChild(pObject);
// Cache tree
GUI<>::RecurseObject(0, pObject, &IGUIObject::UpdateCachedSize);
SGUIMessage msg(GUIM_LOAD);
GUI::RecurseObject(0, pObject, &IGUIObject::HandleMessage, msg);
}
catch (PSERROR_GUI&)
{
throw;
}
}
void CGUI::UpdateObjects()
{
// We'll fill a temporary map until we know everything succeeded
map_pObjects AllObjects;
try
{
// Fill freshly
GUI::RecurseObject(0, m_BaseObject, &IGUIObject::AddToPointersMap, AllObjects);
}
catch (PSERROR_GUI&)
{
throw;
}
// Else actually update the real one
m_pAllObjects.swap(AllObjects);
}
bool CGUI::ObjectExists(const CStr& Name) const
{
return m_pAllObjects.count(Name) != 0;
}
IGUIObject* CGUI::FindObjectByName(const CStr& Name) const
{
map_pObjects::const_iterator it = m_pAllObjects.find(Name);
if (it == m_pAllObjects.end())
return NULL;
else
return it->second;
}
IGUIObject* CGUI::FindObjectUnderMouse() const
{
IGUIObject* pNearest = NULL;
GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject,
&IGUIObject::ChooseMouseOverAndClosest, pNearest);
return pNearest;
}
void CGUI::SetFocusedObject(IGUIObject* pObject)
{
if (pObject == m_FocusedObject)
return;
if (m_FocusedObject)
{
SGUIMessage msg(GUIM_LOST_FOCUS);
m_FocusedObject->HandleMessage(msg);
}
m_FocusedObject = pObject;
if (m_FocusedObject)
{
SGUIMessage msg(GUIM_GOT_FOCUS);
m_FocusedObject->HandleMessage(msg);
}
}
const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const
{
std::map::const_iterator it = m_ScrollBarStyles.find(style);
if (it == m_ScrollBarStyles.end())
return nullptr;
return &it->second;
}
// private struct used only in GenerateText(...)
struct SGenerateTextImage
{
float m_YFrom, // The image's starting location in Y
m_YTo, // The image's end location in Y
m_Indentation; // The image width in other words
// Some help functions
// TODO Gee: CRect => CPoint ?
void SetupSpriteCall(const bool Left, SGUIText::SSpriteCall& SpriteCall,
const float width, const float y,
const CSize& Size, const CStr& TextureName,
const float BufferZone, const int CellID)
{
// TODO Gee: Temp hardcoded values
SpriteCall.m_Area.top = y+BufferZone;
SpriteCall.m_Area.bottom = y+BufferZone + Size.cy;
if (Left)
{
SpriteCall.m_Area.left = BufferZone;
SpriteCall.m_Area.right = Size.cx+BufferZone;
}
else
{
SpriteCall.m_Area.left = width-BufferZone - Size.cx;
SpriteCall.m_Area.right = width-BufferZone;
}
SpriteCall.m_CellID = CellID;
SpriteCall.m_Sprite = TextureName;
m_YFrom = SpriteCall.m_Area.top-BufferZone;
m_YTo = SpriteCall.m_Area.bottom+BufferZone;
m_Indentation = Size.cx+BufferZone*2;
}
};
SGUIText CGUI::GenerateText(const CGUIString& string, const CStrW& FontW, const float& Width, const float& BufferZone, const IGUIObject* pObject)
{
SGUIText Text;
CStrIntern Font(FontW.ToUTF8());
if (string.m_Words.empty())
return Text;
float x = BufferZone, y = BufferZone; // drawing pointer
int from = 0;
bool done = false;
bool FirstLine = true; // Necessary because text in the first line is shorter
// (it doesn't count the line spacing)
// Images on the left or the right side.
std::vector Images[2];
int pos_last_img = -1; // Position in the string where last img (either left or right) were encountered.
// in order to avoid duplicate processing.
// Easier to read.
bool WordWrapping = (Width != 0);
// get the alignment type for the control we are computing the text for since
// we are computing the horizontal alignment in this method in order to not have
// to run through the TextCalls a second time in the CalculateTextPosition method again
EAlign align = EAlign_Left;
if (pObject->SettingExists("text_align"))
GUI::GetSetting(pObject, "text_align", align);
// Go through string word by word
for (int i = 0; i < (int)string.m_Words.size()-1 && !done; ++i)
{
// Pre-process each line one time, so we know which floating images
// will be added for that line.
// Generated stuff is stored in Feedback.
CGUIString::SFeedback Feedback;
// Preliminary line_height, used for word-wrapping with floating images.
float prelim_line_height = 0.f;
// Width and height of all text calls generated.
string.GenerateTextCall(this, Feedback, Font,
string.m_Words[i], string.m_Words[i+1],
FirstLine);
// Loop through our images queues, to see if images has been added.
// Check if this has already been processed.
// Also, floating images are only applicable if Word-Wrapping is on
if (WordWrapping && i > pos_last_img)
{
// Loop left/right
for (int j = 0; j < 2; ++j)
{
for (const CStr& imgname : Feedback.m_Images[j])
{
SGUIText::SSpriteCall SpriteCall;
SGenerateTextImage Image;
// Y is if no other floating images is above, y. Else it is placed
// after the last image, like a stack downwards.
float _y;
if (!Images[j].empty())
_y = std::max(y, Images[j].back().m_YTo);
else
_y = y;
// Get Size from Icon database
SGUIIcon icon = GetIcon(imgname);
CSize size = icon.m_Size;
Image.SetupSpriteCall((j == CGUIString::SFeedback::Left), SpriteCall, Width, _y, size, icon.m_SpriteName, BufferZone, icon.m_CellID);
// Check if image is the lowest thing.
Text.m_Size.cy = std::max(Text.m_Size.cy, Image.m_YTo);
Images[j].push_back(Image);
Text.m_SpriteCalls.push_back(SpriteCall);
}
}
}
pos_last_img = std::max(pos_last_img, i);
x += Feedback.m_Size.cx;
prelim_line_height = std::max(prelim_line_height, Feedback.m_Size.cy);
// If Width is 0, then there's no word-wrapping, disable NewLine.
if ((WordWrapping && (x > Width-BufferZone || Feedback.m_NewLine)) || i == (int)string.m_Words.size()-2)
{
// Change 'from' to 'i', but first keep a copy of its value.
int temp_from = from;
from = i;
static const int From = 0, To = 1;
//int width_from=0, width_to=width;
float width_range[2];
width_range[From] = BufferZone;
width_range[To] = Width - BufferZone;
// Floating images are only applicable if word-wrapping is enabled.
if (WordWrapping)
{
// Decide width of the line. We need to iterate our floating images.
// this won't be exact because we're assuming the line_height
// will be as our preliminary calculation said. But that may change,
// although we'd have to add a couple of more loops to try straightening
// this problem out, and it is very unlikely to happen noticeably if one
// structures his text in a stylistically pure fashion. Even if not, it
// is still quite unlikely it will happen.
// Loop through left and right side, from and to.
for (int j = 0; j < 2; ++j)
{
for (const SGenerateTextImage& img : Images[j])
{
// We're working with two intervals here, the image's and the line height's.
// let's find the union of these two.
float union_from, union_to;
union_from = std::max(y, img.m_YFrom);
union_to = std::min(y+prelim_line_height, img.m_YTo);
// The union is not empty
if (union_to > union_from)
{
if (j == From)
width_range[From] = std::max(width_range[From], img.m_Indentation);
else
width_range[To] = std::min(width_range[To], Width - img.m_Indentation);
}
}
}
}
// Reset X for the next loop
x = width_range[From];
// Now we'll do another loop to figure out the height and width of
// the line (the height of the largest character and the width is
// the sum of all of the individual widths). This
// couldn't be determined in the first loop (main loop)
// because it didn't regard images, so we don't know
// if all characters processed, will actually be involved
// in that line.
float line_height = 0.f;
float line_width = 0.f;
for (int j = temp_from; j <= i; ++j)
{
// We don't want to use Feedback now, so we'll have to use
// another.
CGUIString::SFeedback Feedback2;
// Don't attach object, it'll suppress the errors
// we want them to be reported in the final GenerateTextCall()
// so that we don't get duplicates.
string.GenerateTextCall(this, Feedback2, Font,
string.m_Words[j], string.m_Words[j+1],
FirstLine);
// Append X value.
x += Feedback2.m_Size.cx;
if (WordWrapping && x > width_range[To] && j!=temp_from && !Feedback2.m_NewLine)
{
// The calculated width of each word includes the space between the current
// word and the next. When we're wrapping, we need subtract the width of the
// space after the last word on the line before the wrap.
CFontMetrics currentFont(Font);
line_width -= currentFont.GetCharacterWidth(*L" ");
break;
}
// Let line_height be the maximum m_Height we encounter.
line_height = std::max(line_height, Feedback2.m_Size.cy);
// If the current word is an explicit new line ("\n"),
// break now before adding the width of this character.
// ("\n" doesn't have a glyph, thus is given the same width as
// the "missing glyph" character by CFont::GetCharacterWidth().)
if (WordWrapping && Feedback2.m_NewLine)
break;
line_width += Feedback2.m_Size.cx;
}
float dx = 0.f;
// compute offset based on what kind of alignment
switch (align)
{
case EAlign_Left:
// don't add an offset
dx = 0.f;
break;
case EAlign_Center:
dx = ((width_range[To] - width_range[From]) - line_width) / 2;
break;
case EAlign_Right:
dx = width_range[To] - line_width;
break;
default:
debug_warn(L"Broken EAlign in CGUI::GenerateText()");
break;
}
// Reset x once more
x = width_range[From];
// Move down, because font drawing starts from the baseline
y += line_height;
// Do the real processing now
for (int j = temp_from; j <= i; ++j)
{
// We don't want to use Feedback now, so we'll have to use
// another one.
CGUIString::SFeedback Feedback2;
// Defaults
string.GenerateTextCall(this, Feedback2, Font,
string.m_Words[j], string.m_Words[j+1],
FirstLine, pObject);
// Iterate all and set X/Y values
// Since X values are not set, we need to make an internal
// iteration with an increment that will append the internal
// x, that is what x_pointer is for.
float x_pointer = 0.f;
for (SGUIText::STextCall& tc : Feedback2.m_TextCalls)
{
tc.m_Pos = CPos(dx + x + x_pointer, y);
x_pointer += tc.m_Size.cx;
if (tc.m_pSpriteCall)
tc.m_pSpriteCall->m_Area += tc.m_Pos - CSize(0, tc.m_pSpriteCall->m_Area.GetHeight());
}
// Append X value.
x += Feedback2.m_Size.cx;
// The first word overrides the width limit, what we
// do, in those cases, are just drawing that word even
// though it'll extend the object.
if (WordWrapping) // only if word-wrapping is applicable
{
if (Feedback2.m_NewLine)
{
from = j+1;
// Sprite call can exist within only a newline segment,
// therefore we need this.
Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end());
break;
}
else if (x > width_range[To] && j == temp_from)
{
from = j+1;
// do not break, since we want it to be added to m_TextCalls
}
else if (x > width_range[To])
{
from = j;
break;
}
}
// Add the whole Feedback2.m_TextCalls to our m_TextCalls.
Text.m_TextCalls.insert(Text.m_TextCalls.end(), Feedback2.m_TextCalls.begin(), Feedback2.m_TextCalls.end());
Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end());
if (j == (int)string.m_Words.size()-2)
done = true;
}
// Reset X
x = BufferZone;
// Update dimensions
Text.m_Size.cx = std::max(Text.m_Size.cx, line_width + BufferZone * 2);
Text.m_Size.cy = std::max(Text.m_Size.cy, y + BufferZone);
FirstLine = false;
// Now if we entered as from = i, then we want
// i being one minus that, so that it will become
// the same i in the next loop. The difference is that
// we're on a new line now.
i = from-1;
}
}
return Text;
}
void CGUI::DrawText(SGUIText& Text, const CColor& DefaultColor, const CPos& pos, const float& z, const CRect& clipping)
{
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text);
tech->BeginPass();
bool isClipped = (clipping != CRect());
if (isClipped)
{
glEnable(GL_SCISSOR_TEST);
glScissor(
clipping.left * g_GuiScale,
g_yres - clipping.bottom * g_GuiScale,
clipping.GetWidth() * g_GuiScale,
clipping.GetHeight() * g_GuiScale);
}
CTextRenderer textRenderer(tech->GetShader());
textRenderer.SetClippingRect(clipping);
textRenderer.Translate(0.0f, 0.0f, z);
for (const SGUIText::STextCall& tc : Text.m_TextCalls)
{
// If this is just a placeholder for a sprite call, continue
if (tc.m_pSpriteCall)
continue;
CColor color = tc.m_UseCustomColor ? tc.m_Color : DefaultColor;
textRenderer.Color(color);
textRenderer.Font(tc.m_Font);
textRenderer.Put((float)(int)(pos.x + tc.m_Pos.x), (float)(int)(pos.y + tc.m_Pos.y), &tc.m_String);
}
textRenderer.Render();
for (const SGUIText::SSpriteCall& sc : Text.m_SpriteCalls)
DrawSprite(sc.m_Sprite, sc.m_CellID, z, sc.m_Area + pos);
if (isClipped)
glDisable(GL_SCISSOR_TEST);
tech->EndPass();
}
bool CGUI::GetPreDefinedColor(const CStr& name, CColor& Output) const
{
std::map::const_iterator cit = m_PreDefinedColors.find(name);
if (cit == m_PreDefinedColors.end())
return false;
Output = cit->second;
return true;
}
/**
* @callgraph
*/
void CGUI::LoadXmlFile(const VfsPath& Filename, boost::unordered_set& Paths)
{
Paths.insert(Filename);
CXeromyces XeroFile;
if (XeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK)
return;
XMBElement node = XeroFile.GetRoot();
CStr root_name(XeroFile.GetElementString(node.GetNodeName()));
try
{
if (root_name == "objects")
{
Xeromyces_ReadRootObjects(node, &XeroFile, Paths);
// Re-cache all values so these gets cached too.
//UpdateResolution();
}
else if (root_name == "sprites")
Xeromyces_ReadRootSprites(node, &XeroFile);
else if (root_name == "styles")
Xeromyces_ReadRootStyles(node, &XeroFile);
else if (root_name == "setup")
Xeromyces_ReadRootSetup(node, &XeroFile);
else
debug_warn(L"CGUI::LoadXmlFile error");
}
catch (PSERROR_GUI& e)
{
LOGERROR("Errors loading GUI file %s (%u)", Filename.string8(), e.getCode());
return;
}
}
//===================================================================
// XML Reading Xeromyces Specific Sub-Routines
//===================================================================
void CGUI::Xeromyces_ReadRootObjects(XMBElement Element, CXeromyces* pFile, boost::unordered_set& Paths)
{
int el_script = pFile->GetElementID("script");
std::vector > subst;
// Iterate main children
// they should all be or