mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
Add registerGlobalGuiPageHotkeys() to common/functions_utility.js to selectively register GUI page hotkeys Allow active GUI pages to close using their corresponding hotkey Move page_hotkeys.xml from gui/hotkeys/ to gui/ Update page_hotkeys.xml references in MainMenuItems and MenuButtons Add default tipScrolling fallback in TipsPage when no initData is provided
385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
/**
|
|
* Used for checking replay compatibility.
|
|
*/
|
|
const g_EngineInfo = Engine.GetEngineInfo();
|
|
|
|
/**
|
|
* Needed for formatPlayerInfo to show the player civs in the details.
|
|
*/
|
|
const g_CivData = loadCivData(false, false);
|
|
|
|
/**
|
|
* Used for creating the mapsize filter.
|
|
*/
|
|
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
|
|
|
|
/**
|
|
* All replays found in the directory.
|
|
*/
|
|
var g_Replays = [];
|
|
|
|
/**
|
|
* List of replays after applying the display filter.
|
|
*/
|
|
var g_ReplaysFiltered = [];
|
|
|
|
/**
|
|
* Array of unique usernames of all replays. Used for autocompleting usernames.
|
|
*/
|
|
var g_Playernames = [];
|
|
|
|
/**
|
|
* Sorted list of unique maptitles. Used by mapfilter.
|
|
*/
|
|
var g_MapNames = [];
|
|
|
|
/**
|
|
* Sorted list of the victory conditions occuring in the replays
|
|
*/
|
|
var g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
|
|
|
|
/**
|
|
* Directory name of the currently selected replay. Used to restore the selection after changing filters.
|
|
*/
|
|
var g_SelectedReplayDirectory = "";
|
|
|
|
/**
|
|
* Skip duplicate expensive GUI updates before init is complete.
|
|
*/
|
|
var g_ReplaysLoaded = false;
|
|
|
|
/**
|
|
* Remember last viewed summary panel and charts.
|
|
*/
|
|
var g_SummarySelection;
|
|
|
|
var g_MapCache = new MapCache();
|
|
|
|
/**
|
|
* Initializes globals, loads replays and displays the list.
|
|
*/
|
|
async function init(data)
|
|
{
|
|
if (!g_Settings)
|
|
{
|
|
return { [Engine.openRequest]: {
|
|
"page": "page_pregame.xml"
|
|
} };
|
|
}
|
|
|
|
g_SummarySelection = data && data.summarySelection;
|
|
|
|
loadReplays(data && data.replaySelectionData, false);
|
|
|
|
if (!g_Replays)
|
|
{
|
|
return { [Engine.openRequest]: {
|
|
"page": "page_pregame.xml"
|
|
} };
|
|
}
|
|
registerGlobalGuiPageHotkeys(["options", "hotkeys", "civinfo", "structree", "catafalque", "mapbrowser", "manual", "tips"]);
|
|
initHotkeyTooltips();
|
|
displayReplayList();
|
|
|
|
const closePromise = new Promise(closePageCallback =>
|
|
{
|
|
Engine.GetGUIObjectByName("mainMenu").onPress = closePageCallback.bind(undefined, {
|
|
"page": "page_pregame.xml"
|
|
});
|
|
});
|
|
|
|
const ret = await Promise.race([ closePromise, startReplayHandler(), showReplaySummary() ]);
|
|
return { [Engine.openRequest]: ret };
|
|
}
|
|
|
|
/**
|
|
* Store the list of replays loaded in C++ in g_Replays.
|
|
* Check timestamp and compatibility and extract g_Playernames, g_MapNames, g_VictoryConditions.
|
|
* Restore selected filters and item.
|
|
* @param replaySelectionData - Currently selected filters and item to be restored after the loading.
|
|
* @param compareFiles - If true, compares files briefly (which might be slow with optical harddrives),
|
|
* otherwise blindly trusts the replay cache.
|
|
*/
|
|
function loadReplays(replaySelectionData, compareFiles)
|
|
{
|
|
g_Replays = Engine.GetReplays(compareFiles);
|
|
|
|
if (!g_Replays)
|
|
return;
|
|
|
|
g_Playernames = [];
|
|
for (const replay of g_Replays)
|
|
{
|
|
let nonAIPlayers = 0;
|
|
|
|
// Check replay for compatibility
|
|
replay.isCompatible = isReplayCompatible(replay);
|
|
|
|
sanitizeInitAttributes(replay.attribs);
|
|
|
|
// Extract map names
|
|
if (g_MapNames.indexOf(replay.attribs.settings.mapName) == -1 && replay.attribs.settings.mapName != "")
|
|
g_MapNames.push(replay.attribs.settings.mapName);
|
|
|
|
// Extract playernames
|
|
for (const playerData of replay.attribs.settings.PlayerData)
|
|
{
|
|
if (!playerData || playerData.AI)
|
|
continue;
|
|
|
|
// Remove rating from nick
|
|
let playername = playerData.Name;
|
|
const ratingStart = playername.indexOf(" (");
|
|
if (ratingStart != -1)
|
|
playername = playername.substr(0, ratingStart);
|
|
|
|
if (g_Playernames.indexOf(playername) == -1)
|
|
g_Playernames.push(playername);
|
|
|
|
++nonAIPlayers;
|
|
}
|
|
|
|
replay.isMultiplayer = nonAIPlayers > 1;
|
|
if (replay.attribs.campaignData)
|
|
replay.isCampaign = true;
|
|
|
|
replay.isRated = nonAIPlayers == 2 &&
|
|
replay.attribs.settings.PlayerData.length == 2 &&
|
|
replay.attribs.settings.RatingEnabled;
|
|
}
|
|
|
|
g_MapNames.sort();
|
|
|
|
// Reload filters (since they depend on g_Replays and its derivatives)
|
|
initFilters(replaySelectionData && replaySelectionData.filters);
|
|
|
|
// Restore user selection
|
|
if (replaySelectionData)
|
|
{
|
|
if (replaySelectionData.directory)
|
|
g_SelectedReplayDirectory = replaySelectionData.directory;
|
|
|
|
const replaySelection = Engine.GetGUIObjectByName("replaySelection");
|
|
if (replaySelectionData.column)
|
|
replaySelection.selected_column = replaySelectionData.column;
|
|
if (replaySelectionData.columnOrder)
|
|
replaySelection.selected_column_order = replaySelectionData.columnOrder;
|
|
}
|
|
|
|
g_ReplaysLoaded = true;
|
|
}
|
|
|
|
/**
|
|
* We may encounter malformed replays.
|
|
*/
|
|
function sanitizeInitAttributes(attribs)
|
|
{
|
|
if (!attribs.settings)
|
|
attribs.settings = {};
|
|
|
|
if (!attribs.settings.Size)
|
|
attribs.settings.Size = -1;
|
|
|
|
if (!attribs.settings.mapName)
|
|
attribs.settings.mapName = "";
|
|
|
|
if (!attribs.settings.PlayerData)
|
|
attribs.settings.PlayerData = [];
|
|
|
|
attribs.settings.PopulationCap ??= Infinity;
|
|
|
|
if (!attribs.settings.PopulationCapType)
|
|
attribs.settings.PopulationCapType =
|
|
attribs.settings.WorldPopulation ?
|
|
"world" :
|
|
"player";
|
|
|
|
if (!attribs.mapType)
|
|
attribs.mapType = "skirmish";
|
|
|
|
// Remove gaia
|
|
if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null)
|
|
attribs.settings.PlayerData.shift();
|
|
|
|
attribs.settings.PlayerData.forEach((pData, index) =>
|
|
{
|
|
if (!pData.Name)
|
|
pData.Name = "";
|
|
});
|
|
}
|
|
|
|
function initHotkeyTooltips()
|
|
{
|
|
Engine.GetGUIObjectByName("playersFilter").tooltip =
|
|
translate("Filter replays by typing one or more, partial or complete player names.") +
|
|
" " + colorizeAutocompleteHotkey();
|
|
|
|
let deleteTooltip = colorizeHotkey(
|
|
translate("Delete the selected replay using %(hotkey)s."),
|
|
"session.savedgames.delete");
|
|
|
|
if (deleteTooltip)
|
|
deleteTooltip += colorizeHotkey(
|
|
"\n" + translate("Hold %(hotkey)s to skip the confirmation dialog while deleting."),
|
|
"session.savedgames.noconfirmation");
|
|
|
|
Engine.GetGUIObjectByName("deleteReplayButton").tooltip = deleteTooltip;
|
|
}
|
|
|
|
/**
|
|
* Filter g_Replays, fill the GUI list with that data and show the description of the current replay.
|
|
*/
|
|
function displayReplayList()
|
|
{
|
|
if (!g_ReplaysLoaded)
|
|
return;
|
|
|
|
// Remember previously selected replay
|
|
var replaySelection = Engine.GetGUIObjectByName("replaySelection");
|
|
if (replaySelection.selected != -1)
|
|
g_SelectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory;
|
|
|
|
filterReplays();
|
|
|
|
var list = g_ReplaysFiltered.map(replay =>
|
|
{
|
|
const works = replay.isCompatible;
|
|
return {
|
|
"directories": replay.directory,
|
|
"months": compatibilityColor(getReplayDateTime(replay), works),
|
|
"popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap, replay.attribs.settings.PopulationCapType), works),
|
|
"mapNames": compatibilityColor(getReplayMapName(replay), works),
|
|
"mapSizes": compatibilityColor(translateMapSize(replay.attribs.settings.Size), works),
|
|
"durations": compatibilityColor(getReplayDuration(replay), works),
|
|
"playerNames": compatibilityColor(getReplayPlayernames(replay), works)
|
|
};
|
|
});
|
|
|
|
if (list.length)
|
|
list = prepareForDropdown(list);
|
|
|
|
// Push to GUI
|
|
replaySelection.selected = -1;
|
|
replaySelection.list_months = list.months || [];
|
|
replaySelection.list_players = list.playerNames || [];
|
|
replaySelection.list_mapName = list.mapNames || [];
|
|
replaySelection.list_mapSize = list.mapSizes || [];
|
|
replaySelection.list_popCapacity = list.popCaps || [];
|
|
replaySelection.list_duration = list.durations || [];
|
|
|
|
// Change these last, otherwise crash
|
|
replaySelection.list = list.directories || [];
|
|
replaySelection.list_data = list.directories || [];
|
|
|
|
replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
|
|
|
|
displayReplayDetails();
|
|
}
|
|
|
|
/**
|
|
* Shows preview image, description and player text in the right panel.
|
|
*/
|
|
function displayReplayDetails()
|
|
{
|
|
const selected = Engine.GetGUIObjectByName("replaySelection").selected;
|
|
const replaySelected = selected > -1;
|
|
|
|
Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
|
|
Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
|
|
Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
|
|
Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
|
|
Engine.GetGUIObjectByName("replayFilename").hidden = !replaySelected;
|
|
Engine.GetGUIObjectByName("summaryButton").hidden = true;
|
|
|
|
if (!replaySelected)
|
|
return;
|
|
|
|
const replay = g_ReplaysFiltered[selected];
|
|
|
|
Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.mapName);
|
|
Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size);
|
|
Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.mapType);
|
|
Engine.GetGUIObjectByName("sgVictory").caption = replay.attribs.settings.VictoryConditions.map(victoryConditionName =>
|
|
translateVictoryCondition(victoryConditionName)).join(translate(", "));
|
|
Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"),
|
|
{ "numberOfPlayers": replay.attribs.settings.PlayerData.length });
|
|
Engine.GetGUIObjectByName("replayFilename").caption = Engine.GetReplayDirectoryName(replay.directory);
|
|
|
|
const metadata = Engine.GetReplayMetadata(replay.directory);
|
|
Engine.GetGUIObjectByName("sgPlayersNames").caption =
|
|
formatPlayerInfo(
|
|
replay.attribs.settings.PlayerData,
|
|
Engine.GetGUIObjectByName("showSpoiler").checked &&
|
|
metadata &&
|
|
metadata.playerStates &&
|
|
metadata.playerStates.map(pState => pState.state));
|
|
|
|
Engine.GetGUIObjectByName("sgMapPreview").sprite = g_MapCache.getMapPreview(replay.attribs.mapType, replay.attribs.map, replay.attribs?.mapPreview);
|
|
Engine.GetGUIObjectByName("sgMapDescription").caption = g_MapCache.getTranslatedMapDescription(replay.attribs.mapType, replay.attribs.map);
|
|
|
|
Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory);
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable version of the replay date.
|
|
*/
|
|
function getReplayDateTime(replay)
|
|
{
|
|
return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM-dd HH:mm"));
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable list of the playernames of that replay.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getReplayPlayernames(replay)
|
|
{
|
|
return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", ");
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the map of the given replay.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getReplayMapName(replay)
|
|
{
|
|
return translate(replay.attribs.settings.mapName);
|
|
}
|
|
|
|
/**
|
|
* Returns the month of the given replay in the format "yyyy-MM".
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getReplayMonth(replay)
|
|
{
|
|
return Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM"));
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable version of the time when the replay started.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
function getReplayDuration(replay)
|
|
{
|
|
return timeToString(replay.duration * 1000);
|
|
}
|
|
|
|
/**
|
|
* True if we can start the given replay with the currently loaded mods.
|
|
*/
|
|
function isReplayCompatible(replay)
|
|
{
|
|
return replayHasCompatibleEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods);
|
|
}
|
|
|
|
/**
|
|
* True if we can start the given replay with the currently loaded mods.
|
|
*/
|
|
function replayHasCompatibleEngineVersion(replay)
|
|
{
|
|
return replay.attribs.engine_serialization_version && replay.attribs.engine_serialization_version == g_EngineInfo.engine_serialization_version;
|
|
}
|