mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Up to now `eslint-plugin-brace-rules` was used to enforce a common brace style for JavaScript code. This plugin was however updated the last time over 9 years ago and will be incompatible with ESLint v10, as that [removes `context.getSourceCode()`][1], the plugin relies on. To keep the eslint config working with ESLint v10, this replaces `eslint-plugin-brace-rules` with the [`@stylistic/brace-style`][2] rule from `@stylistic/eslint-plugin`, a package we already use. While `@stylistic/brace-style` doesn't offer an option to format braces in exactly the same way as before, the "allman" style seems to be the one closest to the existing code. [1]: https://eslint.org/blog/2025/11/eslint-v10.0.0-alpha.0-released/#removed-deprecated-rule-context-members [2]: https://eslint.style/rules/brace-style
350 lines
9.1 KiB
JavaScript
350 lines
9.1 KiB
JavaScript
/**
|
|
* This class represents a multiplayer match hosted by a player in the lobby.
|
|
* Having this represented as a class allows to leverage significant performance
|
|
* gains by caching computed, escaped, translated strings and sorting keys.
|
|
*
|
|
* Additionally class representation allows implementation of events such as
|
|
* a new match being hosted, a match having ended, or a buddy having joined a match.
|
|
*
|
|
* Ensure that escapeText is applied to player controlled data for display.
|
|
*
|
|
* Users of the properties of this class:
|
|
* GameList, GameDetails, MapFilters, JoinButton, any user of GameList.selectedGame()
|
|
*/
|
|
class Game
|
|
{
|
|
constructor(mapCache)
|
|
{
|
|
this.mapCache = mapCache;
|
|
|
|
// Stanza data, object with exclusively string values
|
|
// Used to compare which part of the stanza data changed,
|
|
// perform partial updates and trigger event notifications.
|
|
this.stanza = {};
|
|
for (const name of this.StanzaKeys)
|
|
this.stanza[name] = "";
|
|
|
|
// This will be displayed in the GameList and GameDetails
|
|
// Important: Player input must be processed with escapeText
|
|
this.displayData = {
|
|
"tags": {}
|
|
};
|
|
|
|
// Cache the values used for sorting
|
|
this.sortValues = {
|
|
"state": "",
|
|
"compatibility": "",
|
|
"hasBuddyString": ""
|
|
};
|
|
|
|
// Array of objects, result of stringifiedTeamListToPlayerData
|
|
this.players = [];
|
|
|
|
// Whether the current player has the same mods launched as the host of this game
|
|
this.isCompatible = undefined;
|
|
|
|
// Used to display which mods are missing if the player attempts a join
|
|
this.mods = undefined;
|
|
|
|
// Used by the rating column and rating filer
|
|
this.gameRating = undefined;
|
|
|
|
// 'Persistent temporary' sprintf arguments object to avoid repeated object construction
|
|
this.playerCountArgs = {};
|
|
}
|
|
|
|
/**
|
|
* Called from GameList to ensure call order.
|
|
*/
|
|
onBuddyChange()
|
|
{
|
|
this.updatePlayers(this.stanza);
|
|
}
|
|
|
|
/**
|
|
* This function computes values that will either certainly or
|
|
* most likely be used later (i.e. by filtering, sorting and gamelist display).
|
|
*
|
|
* The performance benefit arises from the fact that for a new gamelist stanza
|
|
* many if not most games and game properties did not change.
|
|
*/
|
|
update(newStanza, sortKey)
|
|
{
|
|
const oldStanza = this.stanza;
|
|
const displayData = this.displayData;
|
|
const sortValues = this.sortValues;
|
|
|
|
if (oldStanza.name != newStanza.name)
|
|
{
|
|
Engine.ProfileStart("gameName");
|
|
sortValues.gameName = newStanza.name.toLowerCase();
|
|
this.updateGameName(newStanza);
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.state != newStanza.state)
|
|
{
|
|
Engine.ProfileStart("gameState");
|
|
this.updateGameTags(newStanza);
|
|
sortValues.state = this.GameStatusOrder.indexOf(newStanza.state);
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.niceMapName != newStanza.niceMapName)
|
|
{
|
|
Engine.ProfileStart("niceMapName");
|
|
if (this.mapCache.checkIfExists(newStanza.mapType, newStanza.mapName))
|
|
{
|
|
displayData.mapName = escapeText(this.mapCache.translateMapName(newStanza.niceMapName));
|
|
displayData.mapDescription = this.mapCache.getTranslatedMapDescription(newStanza.mapType, newStanza.mapName);
|
|
}
|
|
else
|
|
{
|
|
displayData.mapName = escapeText(newStanza.niceMapName);
|
|
displayData.mapDescription = "";
|
|
}
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.mapName != newStanza.mapName)
|
|
{
|
|
Engine.ProfileStart("mapName");
|
|
sortValues.mapName = displayData.mapName;
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.mapType != newStanza.mapType)
|
|
{
|
|
Engine.ProfileStart("mapType");
|
|
displayData.mapType = g_MapTypes.Title[g_MapTypes.Name.indexOf(newStanza.mapType)] || "";
|
|
sortValues.mapType = newStanza.mapType;
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.mapSize != newStanza.mapSize)
|
|
{
|
|
Engine.ProfileStart("mapSize");
|
|
displayData.mapSize = translateMapSize(newStanza.mapSize);
|
|
sortValues.mapSize = newStanza.mapSize;
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
const playersChanged = oldStanza.players != newStanza.players;
|
|
if (playersChanged)
|
|
{
|
|
Engine.ProfileStart("playerData");
|
|
this.updatePlayers(newStanza);
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.nbp != newStanza.nbp ||
|
|
oldStanza.maxnbp != newStanza.maxnbp ||
|
|
playersChanged)
|
|
{
|
|
Engine.ProfileStart("playerCount");
|
|
displayData.playerCount = this.getTranslatedPlayerCount(newStanza);
|
|
sortValues.maxnbp = newStanza.maxnbp;
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
if (oldStanza.mods != newStanza.mods)
|
|
{
|
|
Engine.ProfileStart("mods");
|
|
this.updateMods(newStanza);
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
sortValues.private = newStanza.hasPassword;
|
|
displayData.private = newStanza.hasPassword ? '[icon="icon_private"]' : '';
|
|
|
|
this.stanza = newStanza;
|
|
this.sortValue = this.sortValues[sortKey];
|
|
}
|
|
|
|
updatePlayers(newStanza)
|
|
{
|
|
let players;
|
|
{
|
|
Engine.ProfileStart("stringifiedTeamListToPlayerData");
|
|
players = stringifiedTeamListToPlayerData(newStanza.players);
|
|
this.players = players;
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
{
|
|
Engine.ProfileStart("parsePlayers");
|
|
let observerCount = 0;
|
|
let hasBuddies = 0;
|
|
|
|
let playerRatingTotal = 0;
|
|
for (const player of players)
|
|
{
|
|
const playerNickRating = splitRatingFromNick(player.Name);
|
|
|
|
if (player.Team == "observer")
|
|
++observerCount;
|
|
else
|
|
playerRatingTotal += playerNickRating.rating || g_DefaultLobbyRating;
|
|
|
|
// Sort games with playing buddies above games with spectating buddies
|
|
if (hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1)
|
|
hasBuddies = player.Team == "observer" ? 1 : 2;
|
|
}
|
|
|
|
this.observerCount = observerCount;
|
|
this.hasBuddies = hasBuddies;
|
|
|
|
const displayData = this.displayData;
|
|
const sortValues = this.sortValues;
|
|
displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
|
|
sortValues.hasBuddyString = String(hasBuddies);
|
|
sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
|
|
|
|
const playerCount = players.length - observerCount;
|
|
const gameRating =
|
|
playerCount ?
|
|
Math.round(playerRatingTotal / playerCount) :
|
|
g_DefaultLobbyRating;
|
|
this.gameRating = gameRating;
|
|
sortValues.gameRating = gameRating;
|
|
Engine.ProfileStop();
|
|
}
|
|
}
|
|
|
|
updateMods(newStanza)
|
|
{
|
|
{
|
|
Engine.ProfileStart("JSON.parse");
|
|
try
|
|
{
|
|
this.mods = JSON.parse(newStanza.mods);
|
|
}
|
|
catch(e)
|
|
{
|
|
this.mods = [];
|
|
}
|
|
Engine.ProfileStop();
|
|
}
|
|
|
|
{
|
|
Engine.ProfileStart("hasSameMods");
|
|
const isCompatible = this.mods && hasSameMods(this.mods, Engine.GetEngineInfo().mods);
|
|
if (this.isCompatible != isCompatible)
|
|
{
|
|
this.isCompatible = isCompatible;
|
|
this.updateGameTags(newStanza);
|
|
this.sortValues.compatibility = String(isCompatible);
|
|
}
|
|
Engine.ProfileStop();
|
|
}
|
|
}
|
|
|
|
updateGameTags(newStanza)
|
|
{
|
|
const displayData = this.displayData;
|
|
displayData.tags = this.isCompatible ? this.StateTags[newStanza.state] : this.IncompatibleTags;
|
|
displayData.buddy = this.hasBuddies ? setStringTags(g_BuddySymbol, displayData.tags) : "";
|
|
this.updateGameName(newStanza);
|
|
}
|
|
|
|
updateGameName(newStanza)
|
|
{
|
|
const displayData = this.displayData;
|
|
displayData.gameName = setStringTags(escapeText(newStanza.name), displayData.tags);
|
|
|
|
const sortValues = this.sortValues;
|
|
sortValues.gameName = sortValues.compatibility + sortValues.state + sortValues.gameName;
|
|
sortValues.buddy = sortValues.hasBuddyString + sortValues.gameName;
|
|
}
|
|
|
|
getTranslatedPlayerCount(newStanza)
|
|
{
|
|
const playerCountArgs = this.playerCountArgs;
|
|
playerCountArgs.current = setStringTags(escapeText(newStanza.nbp), this.PlayerCountTags.CurrentPlayers);
|
|
playerCountArgs.max = setStringTags(escapeText(newStanza.maxnbp), this.PlayerCountTags.MaxPlayers);
|
|
|
|
let txt;
|
|
if (this.observerCount)
|
|
{
|
|
playerCountArgs.observercount = setStringTags(this.observerCount, this.PlayerCountTags.Observers);
|
|
txt = this.PlayerCountObservers;
|
|
}
|
|
else
|
|
txt = this.PlayerCountNoObservers;
|
|
|
|
return sprintf(txt, playerCountArgs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* These are all keys that occur in a gamelist stanza sent by XPartaMupp.
|
|
*/
|
|
Game.prototype.StanzaKeys = [
|
|
"name",
|
|
"hasPassword",
|
|
"hostUsername",
|
|
"hostJID",
|
|
"state",
|
|
"nbp",
|
|
"maxnbp",
|
|
"players",
|
|
"mapName",
|
|
"niceMapName",
|
|
"mapSize",
|
|
"mapType",
|
|
"victoryConditions",
|
|
"startTime",
|
|
"mods"
|
|
];
|
|
|
|
/**
|
|
* Initial sorting order of the gamelist.
|
|
*/
|
|
Game.prototype.GameStatusOrder = [
|
|
"init",
|
|
"waiting",
|
|
"running"
|
|
];
|
|
|
|
// Translation: The number of players and observers in this game
|
|
Game.prototype.PlayerCountObservers = translate("%(current)s/%(max)s +%(observercount)s");
|
|
|
|
// Translation: The number of players in this game
|
|
Game.prototype.PlayerCountNoObservers = translate("%(current)s/%(max)s");
|
|
|
|
/**
|
|
* Compatible games will be listed in these colors.
|
|
*/
|
|
Game.prototype.StateTags = {
|
|
"init": {
|
|
"color": "0 219 0"
|
|
},
|
|
"waiting": {
|
|
"color": "255 127 0"
|
|
},
|
|
"running": {
|
|
"color": "219 0 0"
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Games that require different mods than the ones launched by the current player are grayed out.
|
|
*/
|
|
Game.prototype.IncompatibleTags = {
|
|
"color": "gray"
|
|
};
|
|
|
|
/**
|
|
* Color for the player count number in the games list.
|
|
*/
|
|
Game.prototype.PlayerCountTags = {
|
|
"CurrentPlayers": {
|
|
"color": "0 160 160"
|
|
},
|
|
"MaxPlayers": {
|
|
"color": "0 160 160"
|
|
},
|
|
"Observers": {
|
|
"color": "0 128 128"
|
|
}
|
|
};
|