Changeset View
Changeset View
Standalone View
Standalone View
binaries/data/mods/public/gui/rdb/replay_menu.js
/** | |||||
* 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 pages loaded from the server for this query | |||||
*/ | |||||
var g_Pages = []; | |||||
/** | |||||
* All replays loaded from the server. | |||||
*/ | |||||
var g_Replays = []; | |||||
/** | |||||
* ID of the currently selected replay. Used to restore the selection after changing filters. | |||||
*/ | |||||
var g_SelectedReplayID = -1; | |||||
/** | |||||
* Page number | |||||
*/ | |||||
var g_PageNumber = 0; | |||||
/** | |||||
* Number to query per page | |||||
*/ | |||||
const g_QueryNumb = 100; | |||||
/** | |||||
* Wait one tick before initializing stuff. | |||||
*/ | |||||
var g_LoadingState = 0; | |||||
/** | |||||
* Page we are wating for list for. -1 for none. | |||||
*/ | |||||
var g_WaitingForQueryListResponce = -1; | |||||
/** | |||||
* If we should load this page when done waiting. | |||||
*/ | |||||
var g_ToLoadAfterQueryListResponce = false; | |||||
/** | |||||
* What part we are querying for. | |||||
*/ | |||||
var g_QueryPartForQueryDatas = []; | |||||
/** | |||||
* Cache containing the mapsettings. Just-in-time loading. | |||||
*/ | |||||
var g_MapData = {}; | |||||
const SortMethod = { | |||||
"months": 0x00, // Time | |||||
"players": 0x01, // Players | |||||
"mapName": 0x02, // MapName | |||||
"mapSize": 0x03, // Size | |||||
"popCapacity": 0x04, // Population | |||||
"duration": 0x05, // Duration | |||||
"id": 0x06, // ID | |||||
"submitter": 0x07, // Submitter | |||||
"title": 0x08, // Title | |||||
"submissionDate": 0x09, // Submission Date | |||||
}; | |||||
/** | |||||
* Initializes globals, loads replays and displays the list. | |||||
*/ | |||||
function init(data) { | |||||
Engine.SetCursor("cursor-wait"); | |||||
if (!g_Settings) { | |||||
Engine.SwitchGuiPage("page_pregame.xml"); | |||||
return; | |||||
} | |||||
initHotkeyTooltips(); | |||||
initDropdowns(); | |||||
} | |||||
function RdbQueryForReplayList() { | |||||
if (g_WaitingForQueryListResponce != -1 || g_LoadingState < 1) { | |||||
return; | |||||
} | |||||
// Parses ints, but better than `parseInt` | |||||
let parseIntU = function(str, forgive) { | |||||
if (/^\+?\d+$/.test(str)) { | |||||
return Number(str); | |||||
} | |||||
if (str == "") { | |||||
return 0; | |||||
} | |||||
if (!(forgive || false)) { | |||||
error("Invalid number: " + ret); | |||||
return undefined; | |||||
} | |||||
return 0; | |||||
}; | |||||
let rgFilter = Engine.GetGUIObjectByName("ratedGamesFilter"); | |||||
Stan: No curly braces. | |||||
let srgFilter = rgFilter.list_data[rgFilter.selected] || ""; | |||||
let ratedGame = srgFilter != "Unrated games"; | |||||
let unratedGame = srgFilter != "Rated games"; | |||||
let spgFilter = Engine.GetGUIObjectByName("singleplayerFilter"); | |||||
let sspgFilter = spgFilter.list_data[spgFilter.selected] || ""; | |||||
let singleplayer = sspgFilter != "Multiplayer"; | |||||
let multiplayer = sspgFilter != "Singleplayer"; | |||||
let sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column; | |||||
let sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order; | |||||
let sidFilter = Engine.GetGUIObjectByName("replayIdFilter"); | |||||
let replayId = parseIntU(sidFilter.caption, true); | |||||
let subFilter = Engine.GetGUIObjectByName("submitterFilter"); | |||||
let submitter = subFilter.caption || ""; | |||||
let dtFilter = Engine.GetGUIObjectByName("dateTimeFilter"); | |||||
let timestampDay = dtFilter.caption || ""; | |||||
let pFilter = Engine.GetGUIObjectByName("playersFilter"); | |||||
let players = pFilter.caption.toLowerCase() || ""; | |||||
Done Inline ActionsWeird indent seem inneficient to have a variable and it's contrary below. Stan: Weird indent seem inneficient to have a variable and it's contrary below. | |||||
let mnFilter = Engine.GetGUIObjectByName("mapNameFilter"); | |||||
let mapName = mnFilter.list_data[mnFilter.selected] || ""; | |||||
let sFilter = Engine.GetGUIObjectByName("mapSizeFilter"); | |||||
let sizeMin = sFilter.list_data[sFilter.selected] || "0"; | |||||
let sizeMax; | |||||
if (sFilter.selected == 0 || (sFilter.selected + 1) == sFilter.list.length) { | |||||
sizeMax = "0"; | |||||
} else { | |||||
sizeMax = sFilter.list_data[sFilter.selected + 1] || "0"; | |||||
} | |||||
let popFilter = Engine.GetGUIObjectByName("populationFilter"); | |||||
let populationMin = popFilter.list_data[popFilter.selected] || "0"; | |||||
let populationMax; | |||||
if (popFilter.selected == 0 || (popFilter.selected + 1) == popFilter.list.length) { | |||||
populationMax = "0"; | |||||
} else { | |||||
populationMax = popFilter.list_data[popFilter.selected + 1] || "0"; | |||||
} | |||||
let dmnFilter = Engine.GetGUIObjectByName("durationMinFilter"); | |||||
let durationMin = dmnFilter.caption || "0"; | |||||
let dmxFilter = Engine.GetGUIObjectByName("durationMaxFilter"); | |||||
let durationMax = dmxFilter.caption || "0"; | |||||
let vcFilter = Engine.GetGUIObjectByName("victoryConditionFilter"); | |||||
let victoryCondition = vcFilter.list_data[vcFilter.selected] || ""; | |||||
let obj = { | |||||
"offset": g_PageNumber * g_QueryNumb, | |||||
"numb": g_QueryNumb, | |||||
"timestampDay": timestampDay, | |||||
"sortMethod": SortMethod[sortKey], | |||||
"dirAccending": sortOrder > 0, | |||||
"singleplayer": singleplayer, | |||||
"multiplayer": multiplayer, | |||||
"ratedGame": ratedGame, | |||||
"unratedGame": unratedGame, | |||||
"submitter": submitter, | |||||
"replayId": replayId, | |||||
"players": players, | |||||
"mapName": mapName, | |||||
"sizeMin": parseIntU(sizeMin), | |||||
"sizeMax": parseIntU(sizeMax), | |||||
"populationMin": parseIntU(populationMin), | |||||
"populationMax": parseIntU(populationMax), | |||||
"durationMin": parseIntU(durationMin, true) * 60, | |||||
"durationMax": parseIntU(durationMax, true) * 60, | |||||
"victoryCondition": victoryCondition, | |||||
"engineVersion": g_EngineInfo.engine_version, | |||||
"mods": g_EngineInfo.mods, | |||||
"getReplayTitle": true, | |||||
"getReplayMsg": false, | |||||
"getReplayDataEdgeLines": false, | |||||
"getReplayDataFull": false, | |||||
"getReplaySubmissionDate": true, | |||||
"getMetadata": false | |||||
} | |||||
Engine.RdbQueryForReplayList(obj); | |||||
g_WaitingForQueryListResponce = g_PageNumber; | |||||
} | |||||
function RdbQueryForReplayDatas(toLoad, getReplayDataEdgeLines) { | |||||
let obj = { | |||||
"replayIds": toLoad, | |||||
"getReplayTitle": false, | |||||
"getReplayMsg": false, | |||||
Not Done Inline ActionsDon't use Parseint use + instead I believe parse int is slower Stan: Don't use Parseint use + instead I believe parse int is slower | |||||
Not Done Inline ActionsParseIntU uses Number(str), not ParseInt. I realize the name is partially misleading, but it did use to use it. gentz: `ParseIntU` uses `Number(str)`, not `ParseInt`. I realize the name is partially misleading, but… | |||||
"getMetadata": false, | |||||
"getReplayDataFull": false, | |||||
"getReplayDataEdgeLines": getReplayDataEdgeLines, | |||||
"getReplaySubmissionDate": false, | |||||
}; | |||||
Engine.RdbQueryForReplayDatas(obj); | |||||
g_QueryPartForQueryDatas.push(getReplayDataEdgeLines ? 1 : 0); | |||||
} | |||||
var g_rdbSystemMessages = { | |||||
"disconnected": message => { | |||||
setFeedback(message.reason); | |||||
Engine.StopRdbClient(); | |||||
} | |||||
}; | |||||
var g_rdbMessages = { | |||||
"QueryForReplayListResponce": message => { | |||||
loadReplays(message.replays, g_WaitingForQueryListResponce); | |||||
g_WaitingForQueryListResponce = -1; | |||||
setFeedback("Done."); | |||||
if (g_PageNumber > 0 && message.replays.length == 0) { | |||||
FlipPage(-1); | |||||
} | |||||
}, | |||||
"QueryForReplayDatasResponce": message => { | |||||
for (let replay of message.replays) { | |||||
if (!g_Replays[replay.id]) { | |||||
g_Replays[replay.id] = replay; | |||||
} | |||||
} | |||||
let part = g_QueryPartForQueryDatas.shift(); | |||||
if ((part & 1) == 1) { | |||||
for (let replay of message.replays) { | |||||
addReplayAttribsToCache(replay); | |||||
} | |||||
} | |||||
displayReplayList(); | |||||
setFeedback("Done."); | |||||
}, | |||||
}; | |||||
function onTick() { | |||||
if (g_LoadingState == 0) { | |||||
++g_LoadingState; | |||||
return; | |||||
} else if (g_LoadingState == 1) { | |||||
refresh(); | |||||
++g_LoadingState; | |||||
return; | |||||
} | |||||
while (true) { | |||||
let message = Engine.RdbGuiPollNewMessage(); | |||||
if (!message) | |||||
break; | |||||
if (message.type == "system" && message.level) { | |||||
g_rdbSystemMessages[message.level](message); | |||||
} else if (message.type == "rdb" && message.level) { | |||||
g_rdbMessages[message.level](message); | |||||
} else { | |||||
error("Unknown rdb message: " + uneval(message)); | |||||
} | |||||
} | |||||
if (g_WaitingForQueryListResponce == -1 && g_ToLoadAfterQueryListResponce) { | |||||
RdbQueryForReplayList(); | |||||
g_ToLoadAfterQueryListResponce = false; | |||||
} | |||||
} | |||||
function setFeedback(msg) { | |||||
Engine.GetGUIObjectByName("feedback").caption = msg; | |||||
} | |||||
Done Inline ActionsRemove if and for braces. Same for above.e. Stan: Remove if and for braces. Same for above.e. | |||||
function addReplayAttribsToCache(replay) { | |||||
g_Replays[replay.id].attribs = replay.attribs; | |||||
g_Replays[replay.id].duration = replay.duration; | |||||
let nonAIPlayers = 0; | |||||
g_Replays[replay.id].isCompatible = isReplayCompatible(g_Replays[replay.id]); | |||||
sanitizeGameAttributes(g_Replays[replay.id].attribs); | |||||
for (let playerData of g_Replays[replay.id].attribs.settings.PlayerData) { | |||||
if (!playerData || playerData.AI) | |||||
continue; | |||||
let playername = playerData.Name; | |||||
let ratingStart = playername.indexOf(" ("); | |||||
if (ratingStart != -1) | |||||
playername = playername.substr(0, ratingStart); | |||||
++nonAIPlayers; | |||||
} | |||||
g_Replays[replay.id].isMultiplayer = nonAIPlayers > 1; | |||||
g_Replays[replay.id].isRated = nonAIPlayers == 2 && | |||||
g_Replays[replay.id].attribs.settings.PlayerData.length == 2 && | |||||
g_Replays[replay.id].attribs.settings.RatingEnabled; | |||||
} | |||||
Not Done Inline ActionsCould be simplified. Stan: Could be simplified. | |||||
Not Done Inline ActionsHow? gentz: How? | |||||
function addReplayTitleToCache(replay) { | |||||
g_Replays[replay.id].title = replay.title; | |||||
} | |||||
/** | |||||
* Set g_Pages[page] to replays. | |||||
* Check timestamp and compatibility | |||||
* Restore selected filters and item. | |||||
*/ | |||||
function loadReplays(replays, page) { | |||||
g_Pages[page] = replays; | |||||
let toFullLoad = []; | |||||
let i = 0; | |||||
for (let replay of g_Pages[page]) { | |||||
//let id = replay.id; | |||||
if (!( | |||||
g_Replays[replay.id] | |||||
&& g_Replays[replay.id].attribs | |||||
&& g_Replays[replay.id].duration | |||||
)) { | |||||
if (!g_Replays[replay.id]) { | |||||
g_Replays[replay.id] = replay; | |||||
} | |||||
toFullLoad.push(replay.id); | |||||
} else { | |||||
g_Pages[page][i] = g_Replays[replay.id]; | |||||
} | |||||
++i; | |||||
} | |||||
if (page == g_PageNumber) { | |||||
RdbQueryForReplayDatas(toFullLoad, true); | |||||
} | |||||
var replaySelection = Engine.GetGUIObjectByName("replaySelection"); | |||||
if (replaySelection.selected >= g_Pages[g_PageNumber].length) | |||||
replaySelection.selected = -1; | |||||
if (page == g_PageNumber) { | |||||
displayReplayList(); | |||||
} | |||||
} | |||||
/** | |||||
* We may encounter malformed replays. | |||||
*/ | |||||
function sanitizeGameAttributes(attribs) { | |||||
if (!attribs.settings) | |||||
attribs.settings = {}; | |||||
if (!attribs.settings.Size) | |||||
attribs.settings.Size = -1; | |||||
if (!attribs.settings.Name) | |||||
attribs.settings.Name = ""; | |||||
if (!attribs.settings.PlayerData) | |||||
attribs.settings.PlayerData = []; | |||||
if (!attribs.settings.PopulationCap) | |||||
attribs.settings.PopulationCap = 300; | |||||
if (!attribs.settings.mapType) | |||||
attribs.settings.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 playernames. Separate playernames by commas."); | |||||
} | |||||
/** | |||||
* Filter g_Pages, fill the GUI list with that data and show the description of the current replay. | |||||
*/ | |||||
function displayReplayList() { | |||||
// Remember previously selected replay | |||||
var replaySelection = Engine.GetGUIObjectByName("replaySelection"); | |||||
if (replaySelection.selected != -1) | |||||
g_SelectedReplayID = g_Pages[g_PageNumber][replaySelection.selected].id; | |||||
var list = g_Pages[g_PageNumber].map(replay => { | |||||
let works = replay.isCompatible; | |||||
return { | |||||
"ids": "" + replay.id, | |||||
"submitters": "" + replay.submitterId, | |||||
"months": compatibilityColor(getReplayDateTime(replay), works), | |||||
"popCaps": compatibilityColor(getReplayPopulationCap(replay), works), | |||||
"mapSizes": compatibilityColor(getReplayMapSize(replay), works), | |||||
"mapNames": compatibilityColor(getReplayMapName(replay), works), | |||||
"durations": compatibilityColor(getReplayDuration(replay), works), | |||||
"playerNames": replay.attribs ? compatibilityColor(getReplayPlayernames(replay), works) : "Loading...", | |||||
"titles": replay.title ? replay.title : "Loading...", | |||||
"submissionDates": getReplaySubmissionDate(replay), | |||||
}; | |||||
}); | |||||
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 || []; | |||||
replaySelection.list_id = list.ids || []; | |||||
replaySelection.list_submitter = list.submitters || []; | |||||
replaySelection.list_title = list.titles || []; | |||||
replaySelection.list_submissionDate = list.submissionDates || []; | |||||
// Change these last, otherwise crash | |||||
replaySelection.list = list.ids || []; | |||||
replaySelection.list_data = list.ids || []; | |||||
replaySelection.selected = replaySelection.list.findIndex(id => id == g_SelectedReplayID); | |||||
} | |||||
function getReplayPopulationCap(replay) { | |||||
return replay.attribs | |||||
? translatePopulationCapacity(replay.attribs.settings.PopulationCap) | |||||
: ""; | |||||
} | |||||
function getReplayMapSize(replay) { | |||||
return replay.attribs | |||||
? translateMapSize(replay.attribs.settings.Size) | |||||
: ""; | |||||
} | |||||
/** | |||||
* Returns a human-readable version of the replay date. | |||||
*/ | |||||
function getReplayDateTime(replay) { | |||||
return replay.attribs | |||||
? Engine.FormatMillisecondsIntoDateStringLocal(replay.attribs.timestamp * 1000, translate("yyyy-MM-dd HH:mm")) | |||||
: ""; | |||||
} | |||||
function getReplaySubmissionDate(replay) { | |||||
return replay.submissionDate | |||||
? Engine.FormatMillisecondsIntoDateStringLocal(replay.submissionDate * 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 | |||||
? 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 replay.attribs ? translate(replay.attribs.settings.Name) : ""; | |||||
} | |||||
/** | |||||
* Returns the month of the given replay in the format "yyyy-MM". | |||||
* | |||||
* @returns {string} | |||||
*/ | |||||
function getReplayMonth(replay) { | |||||
return replay.attribs | |||||
? 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 replay.duration ? timeToString(replay.duration * 1000) : ""; | |||||
} | |||||
/** | |||||
* True if we can start the given replay with the currently loaded mods. | |||||
*/ | |||||
function isReplayCompatible(replay) { | |||||
return replay.attribs && replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs.mods, g_EngineInfo.mods); | |||||
} | |||||
/** | |||||
* True if we can start the given replay with the currently loaded mods. | |||||
*/ | |||||
function replayHasSameEngineVersion(replay) { | |||||
return replay.attribs && replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version; | |||||
} | |||||
/** | |||||
* Inits the dropdowns. | |||||
*/ | |||||
function initDropdowns() { | |||||
let maps = getMaps(); | |||||
populateDropdown("mapNameFilter", "map name", maps, "name", "name", ""); | |||||
populateDropdown("mapSizeFilter", "map size", g_Settings.MapSizes, "Name", "Tiles", 0); | |||||
populateDropdown("populationFilter", "population capacity", g_Settings.PopulationCapacities, "Title", "Population", 0); | |||||
populateDropdown("victoryConditionFilter", undefined, g_Settings.VictoryConditions, "Title", "Title", ""); | |||||
populateDropdown("singleplayerFilter", undefined, [["Multiplayer"], ["Singleplayer"]], 0, 0, "", "Both"); | |||||
populateDropdown("ratedGamesFilter", undefined, [["Rated games"], ["Unrated games"]], 0, 0, "", "Both"); | |||||
} | |||||
function populateDropdown(filterName, translateContext, data, label, value, any, anyShown) { | |||||
var filter = Engine.GetGUIObjectByName(filterName); | |||||
let translateFunc = translateContext | |||||
? function(anyShown) { return translateWithContext(translateContext, anyShown); } | |||||
: function(anyShown) { return translate(anyShown); }; | |||||
filter.list = [translateFunc(anyShown || "Any")].concat(data.map(data => translate(data[label]))); | |||||
filter.list_data = [any].concat(data.map(data => data[value])); | |||||
if (filter.selected == -1 || filter.selected >= filter.list.length) | |||||
filter.selected = 0; | |||||
} | |||||
function loadMapData(name, mapEnd) { | |||||
if (name == "random") { | |||||
return { "settings": { "Name": "", "Description": "" } }; | |||||
} | |||||
if (!g_MapData[name]) { | |||||
g_MapData[name] = | |||||
mapEnd == ".json" ? | |||||
Engine.ReadJSONFile(name + mapEnd) : | |||||
Engine.LoadMapSettings(name); | |||||
} | |||||
return g_MapData[name]; | |||||
} | |||||
/** | |||||
* Doesn't translate, so that lobby clients can do that locally | |||||
* (even if they don't have that map). | |||||
*/ | |||||
function getMapDisplayName(map, mapEnd) { | |||||
if (map == "random") | |||||
return map; | |||||
let mapData = loadMapData(map, mapEnd); | |||||
if (!mapData || !mapData.settings || !mapData.settings.Name) | |||||
return map; | |||||
return mapData.settings.Name; | |||||
} | |||||
/** | |||||
* Get a list of maps | |||||
* | |||||
* @return {Array} all maps | |||||
*/ | |||||
function getMaps() { | |||||
let mapPaths = ["maps/random/", "maps/scenarios/", "maps/skirmishes/"]; | |||||
let mapEnds = [".json", ".xml", ".xml"]; | |||||
let maps = []; | |||||
// TODO: Should verify these are valid maps before adding to list | |||||
for (let i = 0; i < mapPaths.length; ++i) { | |||||
let mapPath = mapPaths[i]; | |||||
let mapEnd = mapEnds[i]; | |||||
for (let mapFile of listFiles(mapPath, mapEnd, false)) { | |||||
if (mapFile.startsWith("_")) | |||||
continue; | |||||
let file = mapPath + mapFile; | |||||
maps.push({ | |||||
//"file": file, | |||||
"name": translate(getMapDisplayName(file, mapEnd)), | |||||
}); | |||||
} | |||||
} | |||||
return maps; | |||||
} | |||||
function showReplay() { | |||||
} |
Wildfire Games · Phabricator
No curly braces.