Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 19673)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 19674)
@@ -1,191 +1,197 @@
/**
* Creates the data for restoring selection, order and filters when returning to the replay menu.
*/
function createReplaySelectionData(selectedDirectory)
{
let replaySelection = Engine.GetGUIObjectByName("replaySelection");
let dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
let playersFilter = Engine.GetGUIObjectByName("playersFilter");
let mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
let populationFilter = Engine.GetGUIObjectByName("populationFilter");
let durationFilter = Engine.GetGUIObjectByName("durationFilter");
let compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter");
let singleplayerFilter = Engine.GetGUIObjectByName("singleplayerFilter");
let victoryConFilter = Engine.GetGUIObjectByName("victoryConditionFilter");
let ratedGamesFilter = Engine.GetGUIObjectByName("ratedGamesFilter");
return {
"directory": selectedDirectory,
"column": replaySelection.selected_column,
"columnOrder": replaySelection.selected_column_order,
"filters": {
"date": dateTimeFilter.list_data[dateTimeFilter.selected],
"playernames": playersFilter.caption,
"mapName": mapNameFilter.list_data[mapNameFilter.selected],
"mapSize": mapSizeFilter.list_data[mapSizeFilter.selected],
"popCap": populationFilter.list_data[populationFilter.selected],
"duration": durationFilter.list_data[durationFilter.selected],
"compatibility": compatibilityFilter.checked,
"singleplayer": singleplayerFilter.list_data[singleplayerFilter.selected],
"victoryCondition": victoryConFilter.list_data[victoryConFilter.selected],
"ratedGames": ratedGamesFilter.selected
}
};
}
/**
* Starts the selected visual replay, or shows an error message in case of incompatibility.
*/
function startReplay()
{
var selected = Engine.GetGUIObjectByName("replaySelection").selected;
if (selected == -1)
return;
var replay = g_ReplaysFiltered[selected];
if (isReplayCompatible(replay))
reallyStartVisualReplay(replay.directory);
else
displayReplayCompatibilityError(replay);
}
/**
* Attempts the visual replay, regardless of the compatibility.
*
* @param replayDirectory {string}
*/
function reallyStartVisualReplay(replayDirectory)
{
// TODO: enhancement: restore filter settings and selected replay when returning from the summary screen.
Engine.StartVisualReplay(replayDirectory);
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": Engine.GetReplayAttributes(replayDirectory),
"isNetworked": false,
"playerAssignments": {
"local":{
"name": singleplayerName(),
"player": -1
}
},
"savedGUIData": "",
"isReplay": true,
"replaySelectionData": createReplaySelectionData(replayDirectory)
});
}
/**
* Shows an error message stating why the replay is not compatible.
*
* @param replay {Object}
*/
function displayReplayCompatibilityError(replay)
{
var errMsg;
if (replayHasSameEngineVersion(replay))
{
let gameMods = replay.attribs.mods || [];
errMsg = translate("You don't have the same mods active as the replay does.") + "\n";
errMsg += sprintf(translate("Required: %(mods)s"), { "mods": gameMods.join(translate(", ")) }) + "\n";
errMsg += sprintf(translate("Active: %(mods)s"), { "mods": g_EngineInfo.mods.join(translate(", ")) });
}
else
{
errMsg = translate("This replay is not compatible with your version of the game!") + "\n";
errMsg += sprintf(translate("Your version: %(version)s"), { "version": g_EngineInfo.engine_version }) + "\n";
errMsg += sprintf(translate("Required version: %(version)s"), { "version": replay.attribs.engine_version });
}
messageBox(500, 200, errMsg, translate("Incompatible replay"));
}
/**
* Opens the summary screen of the given replay, if its data was found in that directory.
*/
function showReplaySummary()
{
var selected = Engine.GetGUIObjectByName("replaySelection").selected;
if (selected == -1)
return;
// Load summary screen data from the selected replay directory
let simData = Engine.GetReplayMetadata(g_ReplaysFiltered[selected].directory);
if (!simData)
{
messageBox(500, 200, translate("No summary data available."), translate("Error"));
return;
}
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"isReplay": true,
"replayDirectory": g_ReplaysFiltered[selected].directory,
"replaySelectionData": createReplaySelectionData(g_ReplaysFiltered[selected].directory)
}
});
}
+function reloadCache()
+{
+ let selected = Engine.GetGUIObjectByName("replaySelection").selected;
+ loadReplays(selected > -1 ? createReplaySelectionData(g_ReplaysFiltered[selected].directory) : "", true);
+}
+
/**
* Callback.
*/
function deleteReplayButtonPressed()
{
if (!Engine.GetGUIObjectByName("deleteReplayButton").enabled)
return;
if (Engine.HotkeyIsPressed("session.savedgames.noconfirmation"))
deleteReplayWithoutConfirmation();
else
deleteReplay();
}
/**
* Shows a confirmation dialog and deletes the selected replay from the disk in case.
*/
function deleteReplay()
{
// Get selected replay
var selected = Engine.GetGUIObjectByName("replaySelection").selected;
if (selected == -1)
return;
var replay = g_ReplaysFiltered[selected];
messageBox(
500, 200,
translate("Are you sure you want to delete this replay permanently?") + "\n" + escapeText(replay.file),
translate("Delete replay"),
[translate("No"), translate("Yes")],
[null, function() { reallyDeleteReplay(replay.directory); }]
);
}
/**
* Attempts to delete the selected replay from the disk.
*/
function deleteReplayWithoutConfirmation()
{
var selected = Engine.GetGUIObjectByName("replaySelection").selected;
if (selected > -1)
reallyDeleteReplay(g_ReplaysFiltered[selected].directory);
}
/**
* Attempts to delete the given replay directory from the disk.
*
* @param replayDirectory {string}
*/
function reallyDeleteReplay(replayDirectory)
{
var replaySelection = Engine.GetGUIObjectByName("replaySelection");
var selectedIndex = replaySelection.selected;
if (!Engine.DeleteReplay(replayDirectory))
error(sprintf("Could not delete replay '%(id)s'", { "id": replayDirectory }));
// Refresh replay list
init();
replaySelection.selected = Math.min(selectedIndex, g_ReplaysFiltered.length - 1);
}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19673)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 19674)
@@ -1,353 +1,356 @@
/**
* 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();
/**
* 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 = [];
/**
* 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;
/**
* Initializes globals, loads replays and displays the list.
*/
function init(data)
{
if (!g_Settings)
{
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
- loadReplays(data && data.replaySelectionData);
+ loadReplays(data && data.replaySelectionData, false);
if (!g_Replays)
{
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
initHotkeyTooltips();
displayReplayList();
}
/**
* 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)
+function loadReplays(replaySelectionData, compareFiles)
{
- g_Replays = Engine.GetReplays();
+ g_Replays = Engine.GetReplays(compareFiles);
if (!g_Replays)
return;
g_Playernames = [];
for (let replay of g_Replays)
{
let nonAIPlayers = 0;
// Check replay for compatibility
replay.isCompatible = isReplayCompatible(replay);
sanitizeGameAttributes(replay.attribs);
// Extract map names
if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "")
g_MapNames.push(replay.attribs.settings.Name);
// Extract victory conditions
if (replay.attribs.settings.GameType && g_VictoryConditions.indexOf(replay.attribs.settings.GameType) == -1)
g_VictoryConditions.push(replay.attribs.settings.GameType);
// Extract playernames
for (let playerData of replay.attribs.settings.PlayerData)
{
if (!playerData || playerData.AI)
continue;
// Remove rating from nick
let playername = playerData.Name;
let 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;
replay.isRated = nonAIPlayers == 2 &&
replay.attribs.settings.PlayerData.length == 2 &&
replay.attribs.settings.RatingEnabled;
}
g_MapNames.sort();
g_VictoryConditions.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;
let 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 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";
if (!attribs.settings.GameType)
attribs.settings.GameType = "conquest";
// 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.") +
" " + colorizeAutocompleteHotkey();
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 => {
let works = replay.isCompatible;
return {
"directories": replay.directory,
"months": compatibilityColor(getReplayDateTime(replay), works),
"popCaps": compatibilityColor(translatePopulationCapacity(replay.attribs.settings.PopulationCap), 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()
{
let selected = Engine.GetGUIObjectByName("replaySelection").selected;
let 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;
let replay = g_ReplaysFiltered[selected];
Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name);
Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size);
Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType);
Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType);
Engine.GetGUIObjectByName("sgNbPlayers").caption = sprintf(translate("Players: %(numberOfPlayers)s"),
{ "numberOfPlayers": replay.attribs.settings.PlayerData.length });
Engine.GetGUIObjectByName("replayFilename").caption = escapeText(Engine.GetReplayDirectoryName(replay.directory));
let 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)
);
let mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map);
Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
Engine.GetGUIObjectByName("summaryButton").hidden = !Engine.HasReplayMetadata(replay.directory);
setMapPreviewImage("sgMapPreview", mapData.preview);
}
/**
* 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.Name);
}
/**
* 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 replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo);
}
/**
* True if we can start the given replay with the currently loaded mods.
*/
function replayHasSameEngineVersion(replay)
{
return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version;
}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 19673)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 19674)
@@ -1,266 +1,273 @@
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19673)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 19674)
@@ -1,1642 +1,1647 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_Ceasefire = prepareForDropdown(g_Settings && g_Settings.Ceasefire);
const g_GameSpeeds = prepareForDropdown(g_Settings && g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay));
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = prepareForDropdown(g_Settings && g_Settings.VictoryConditions);
const g_VictoryDurations = prepareForDropdown(g_Settings && g_Settings.VictoryDurations);
/**
* Colors to flash when pop limit reached.
*/
var g_DefaultPopulationColor = "white";
var g_PopulationAlertColor = "orange";
/**
* Seen in the tooltip of the top panel.
*/
var g_ResourceTitleFont = "sans-bold-16";
/**
* A random file will be played. TODO: more variety
*/
const g_Ambient = [ "audio/ambient/dayscape/day_temperate_gen_03.ogg" ];
/**
* Map, player and match settings set in gamesetup.
*/
const g_GameAttributes = Object.freeze(Engine.GetInitAttributes());
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController;
/**
* True if this is a multiplayer game.
*/
var g_IsNetworked = false;
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* Shows a message box asking the user to leave if "won" or "defeated".
*/
var g_ConfirmExit = false;
/**
* True if the current player has paused the game explicitly.
*/
var g_Paused = false;
/**
* The list of GUIDs of players who have currently paused the game, if the game is networked.
*/
var g_PausingClients = [];
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var lastTickTime = new Date();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
const g_StatusBarUpdate = 200;
/**
* Not constant as we add "gaia".
*/
var g_CivData = {};
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
var g_PlayerAssignments = {
"local": {
"name": singleplayerName(),
"player": 1
}
};
/**
* Cache dev-mode settings that are frequently or widely used.
*/
var g_DevSettings = {
"changePerspective": false,
"controlAll": false
};
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Blink the population counter if the player can't train more units.
*/
var g_IsTrainingBlocked = false;
/**
* Cache simulation state (updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TemplateDataWithoutLocalization = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* Top coordinate of the research list.
* Changes depending on the number of displayed counters.
*/
var g_ResearchListTop = 4;
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Display data of the current players entities shown in the top panel.
*/
var g_PanelEntities = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "CitizenSoldier"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
/**
* Cache the idle worker status.
*/
var g_HasIdleWorker = false;
function GetSimState()
{
if (!g_SimState)
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
return g_SimState;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
g_EntityStates[entId] = Engine.GuiInterfaceCall("GetEntityState", entId);
return g_EntityStates[entId];
}
function GetExtendedEntityState(entId)
{
let entState = GetEntityState(entId);
if (!entState || entState.extended)
return entState;
let extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
for (let prop in extension)
entState[prop] = extension[prop];
entState.extended = true;
g_EntityStates[entId] = entState;
return entState;
}
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = template;
}
return g_TemplateData[templateName];
}
function GetTemplateDataWithoutLocalization(templateName)
{
if (!(templateName in g_TemplateDataWithoutLocalization))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
g_TemplateDataWithoutLocalization[templateName] = template;
}
return g_TemplateDataWithoutLocalization[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = Engine.GuiInterfaceCall("GetTechnologyData", { "name": technologyName, "civ": civ });
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = template;
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
if (initData)
{
g_IsNetworked = initData.isNetworked;
g_IsController = initData.isController;
g_PlayerAssignments = initData.playerAssignments;
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else // Needed for autostart loading option
{
if (g_IsReplay)
g_PlayerAssignments.local.player = -1;
}
updatePlayerData();
g_CivData = loadCivData();
g_CivData.gaia = { "Code": "gaia", "Name": translate("Gaia") };
g_BarterSell = g_ResourceData.GetCodes()[0];
initializeMusic(); // before changing the perspective
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(Engine.GetSimRate());
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); };
initMenuPosition();
resizeDiplomacyDialog();
resizeTradeDialog();
for (let slot in Engine.GetGUIObjectByName("panelEntityPanel").children)
initPanelEntities(slot);
// Populate player selection dropdown
let playerNames = [translate("Observer")];
let playerIDs = [-1];
for (let player in g_Players)
{
playerIDs.push(player);
playerNames.push(colorizePlayernameHelper("■", player) + " " + g_Players[player].name);
}
// Select "observer" item when rejoining as a defeated player
let viewedPlayer = g_Players[Engine.GetPlayerID()];
let viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
viewPlayerDropdown.list = playerNames;
viewPlayerDropdown.list_data = playerIDs;
viewPlayerDropdown.selected = viewedPlayer && viewedPlayer.state == "defeated" ? 0 : Engine.GetPlayerID() + 1;
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
Engine.GetGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
g_Selection.selected = hotloadData.selection;
initChatWindow();
sendLobbyPlayerlistUpdate();
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
// Report the performance after 5 seconds (when we're still near
// the initial camera view) and a minute (when the profiler will
// have settled down if framerates as very low), to give some
// extremely rough indications of performance
//
// DISABLED: this information isn't currently useful for anything much,
// and it generates a massive amount of data to transmit and store
//setTimeout(function() { reportPerformance(5); }, 5000);
//setTimeout(function() { reportPerformance(60); }, 60000);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
/**
* Depends on the current player (g_IsObserver).
*/
function updateHotkeyTooltips()
{
Engine.GetGUIObjectByName("chatInput").tooltip =
translateWithContext("chat input", "Type the message to send.") + "\n" +
colorizeAutocompleteHotkey() +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the public chat."), "chat") +
colorizeHotkey(
"\n" + (g_IsObserver ?
translate("Press %(hotkey)s to open the observer chat.") :
translate("Press %(hotkey)s to open the ally chat.")),
"teamchat") +
colorizeHotkey("\n" + translate("Press %(hotkey)s to open the previously selected private chat."), "privatechat");
Engine.GetGUIObjectByName("idleWorkerButton").tooltip =
colorizeHotkey("%(hotkey)s" + " ", "selection.idleworker") +
translate("Find idle worker");
Engine.GetGUIObjectByName("tradeHelp").tooltip = colorizeHotkey(
translate("Select one type of goods you want to modify by clicking on it (Pressing %(hotkey)s while selecting will also bring its share to 100%%) and then use the arrows of the other types to modify their shares."),
"session.fulltradeswap");
Engine.GetGUIObjectByName("barterHelp").tooltip = sprintf(
translate("Start by selecting the resource from the upper row that you wish to sell. Upon each press on one of the lower buttons, %(quantity)s of the upper resource will be sold for the displayed quantity of the lower. Press and hold %(hotkey)s to temporarily multiply all quantities by %(multiplier)s."), {
"quantity": g_BarterResourceSellQuantity,
"hotkey": colorizeHotkey("%(hotkey)s", "session.massbarter"),
"multiplier": g_BarterMultiplier
});
}
function initPanelEntities(slot)
{
let button = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
button.onPress = function() {
let panelEnt = g_PanelEntities.find(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot);
if (!panelEnt)
return;
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([panelEnt.ent]);
};
button.onDoublePress = function() {
let panelEnt = g_PanelEntities.find(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot);
if (panelEnt)
selectAndMoveTo(getEntityOrHolder(panelEnt.ent));
};
}
/**
* Returns the entity itself except when garrisoned where it returns its garrisonHolder
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length &&
(entState.unitAI.orders[0].type == "Garrison" || entState.unitAI.orders[0].type == "Autogarrison"))
return getEntityOrHolder(entState.unitAI.orders[0].data.target);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
playAmbient();
}
function toggleChangePerspective(enabled)
{
g_DevSettings.changePerspective = enabled;
selectViewPlayer(g_ViewedPlayer);
}
/**
* Change perspective tool.
* Shown to observers or when enabling the developers option.
*/
function selectViewPlayer(playerID)
{
if (playerID < -1 || playerID > g_Players.length - 1)
return;
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
g_IsObserver = isPlayerObserver(Engine.GetPlayerID());
if (g_IsObserver || g_DevSettings.changePerspective)
{
if (g_ViewedPlayer != playerID)
clearSelection();
g_ViewedPlayer = playerID;
}
if (g_DevSettings.changePerspective)
{
Engine.SetPlayerID(g_ViewedPlayer);
g_IsObserver = isPlayerObserver(g_ViewedPlayer);
}
Engine.SetViewedPlayer(g_ViewedPlayer);
updateTopPanel();
updateChatAddressees();
updateHotkeyTooltips();
// Update GUI and clear player-dependent cache
onSimulationUpdate();
if (g_IsDiplomacyOpen)
openDiplomacy();
if (g_IsTradeOpen)
openTrade();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when a player has won or was defeated.
*/
function playerFinished(player, won)
{
if (player == Engine.GetPlayerID())
reportGame();
updatePlayerData();
updateChatAddressees();
if (player != g_ViewedPlayer)
return;
// Select "observer" item on loss. On win enable observermode without changing perspective
Engine.GetGUIObjectByName("viewPlayer").selected = won ? g_ViewedPlayer + 1 : 0;
if (player != Engine.GetPlayerID() || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
g_ConfirmExit = won ? "won" : "defeated";
}
/**
* Sets civ icon for the currently viewed player.
* Hides most gui objects for observers.
*/
function updateTopPanel()
{
let isPlayer = g_ViewedPlayer > 0;
let civIcon = Engine.GetGUIObjectByName("civIcon");
civIcon.hidden = !isPlayer;
if (isPlayer)
{
civIcon.sprite = "stretched:" + g_CivData[g_Players[g_ViewedPlayer].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), {
"civ": g_CivData[g_Players[g_ViewedPlayer].civ].Name
});
}
Engine.GetGUIObjectByName("optionFollowPlayer").hidden = !g_IsObserver || !isPlayer;
let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective;
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let res of resCodes)
{
if (!Engine.GetGUIObjectByName("resource["+r+"]"))
{
warn("Current GUI limits prevent displaying more than " + r + " resources in the top panel!");
break;
}
Engine.GetGUIObjectByName("resource["+r+"]_icon").sprite = "stretched:session/icons/resources/" + res + ".png";
Engine.GetGUIObjectByName("resource["+r+"]").hidden = !isPlayer;
++r;
}
horizontallySpaceObjects("resourceCounts", 5);
hideRemaining("resourceCounts", r);
let resPop = Engine.GetGUIObjectByName("population");
let resPopSize = resPop.size;
resPopSize.left = Engine.GetGUIObjectByName("resource["+ (r-1) +"]").size.right;
resPop.size = resPopSize;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("observerText").hidden = isPlayer;
let alphaLabel = Engine.GetGUIObjectByName("alphaLabel");
alphaLabel.hidden = isPlayer && !viewPlayer.hidden;
alphaLabel.size = isPlayer ? "50%+20 0 100%-226 100%" : "200 0 100%-475 100%";
Engine.GetGUIObjectByName("pauseButton").enabled = !g_IsObserver || !g_IsNetworked || g_IsController;
Engine.GetGUIObjectByName("menuResignButton").enabled = !g_IsObserver;
}
function reportPerformance(time)
{
let settings = g_GameAttributes.settings;
Engine.SubmitUserReport("profile", 3, JSON.stringify({
"time": time,
"map": settings.Name,
"seed": settings.Seed, // only defined for random maps
"size": settings.Size, // only defined for random maps
"profiler": Engine.GetProfilerState()
}));
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
if (g_IsObserver || g_Disconnected)
return;
Engine.PostNetworkCommand({
"type": "defeat-player",
"playerId": Engine.GetPlayerID(),
"resign": true
});
if (!leaveGameAfterResign)
resumeGame(true);
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
if (!willRejoin && !g_IsObserver)
resignGame(true);
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = getReplayMetadata();
Engine.EndGame();
+ // After the replay file was closed in EndGame
+ // Done here to keep EndGame small
+ if (!g_IsReplay)
+ Engine.AddReplayToCache(replayDirectory);
+
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
Engine.SwitchGuiPage("page_summary.xml", {
"sim": simData,
"gui": {
"assignedPlayer": Engine.GetPlayerID(),
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
});
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { "selection": g_Selection.selected };
}
function getSavedGameData()
{
return {
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = new Date();
let tickLength = now - lastTickTime;
lastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
updateGUIObjects();
// Display rally points for selected buildings
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
Engine.GetGUIObjectByName("resourcePop").textcolor = g_IsTrainingBlocked && now % 1000 < 500 ? g_PopulationAlertColor : g_DefaultPopulationColor;
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function changeGameSpeed(speed)
{
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
function hasIdleWorker()
{
return Engine.GuiInterfaceCall("HasIdleUnits", {
"viewedPlayer": g_ViewedPlayer,
"idleClasses": g_WorkerTypes,
"excludeUnits": []
});
}
function updateIdleWorkerButton()
{
g_HasIdleWorker = hasIdleWorker();
let idleWorkerButton = Engine.GetGUIObjectByName("idleOverlay");
let prefix = "stretched:session/";
if (!g_HasIdleWorker)
idleWorkerButton.sprite = prefix + "minimap-idle-disabled.png";
else if (idleWorkerButton.sprite != prefix + "minimap-idle-highlight.png")
idleWorkerButton.sprite = prefix + "minimap-idle.png";
}
function onSimulationUpdate()
{
g_EntityStates = {};
g_TemplateData = {};
g_TechnologyData = {};
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
if (!g_SimState)
return;
handleNotifications();
updateGUIObjects();
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": "Aura",
"enabled": Engine.ConfigDB_GetValue("user", "gui.session.aurarange") == "true"
});
if (g_ConfirmExit)
confirmExit();
}
/**
* Don't show the message box before all playerstate changes are processed.
*/
function confirmExit()
{
if (g_IsNetworked && !g_IsNetworkedActive)
return;
closeOpenDialogs();
// Don't ask for exit if other humans are still playing
let isHost = g_IsController && g_IsNetworked;
let askExit = !isHost || isHost && g_Players.every((player, i) =>
i == 0 ||
player.state != "active" ||
g_GameAttributes.settings.PlayerData[i].AI != "");
let subject = g_PlayerStateMessages[g_ConfirmExit];
if (askExit)
subject += "\n" + translate("Do you want to quit?");
messageBox(
400, 200,
subject,
g_ConfirmExit == "won" ?
translate("VICTORIOUS!") :
translate("DEFEATED!"),
askExit ? [translate("No"), translate("Yes")] : [translate("Ok")],
askExit ? [resumeGame, leaveGame] : [resumeGame]
);
g_ConfirmExit = false;
}
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updatePanelEntities();
displayPanelEntities();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateResearchDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
updateIdleWorkerButton();
if (g_IsTradeOpen)
{
updateTraderTexts();
updateBarterButtons();
}
if (g_ViewedPlayer > 0)
{
let playerState = GetSimState().players[g_ViewedPlayer];
g_DevSettings.controlAll = playerState && playerState.controlsAll;
Engine.GetGUIObjectByName("devControlAll").checked = g_DevSettings.controlAll;
}
if (!g_IsObserver)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
updateDiplomacy();
}
function onReplayFinished()
{
closeOpenDialogs();
pauseGame();
messageBox(400, 200,
translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"),
translateWithContext("replayFinished", "Confirmation"),
[translateWithContext("replayFinished", "No"), translateWithContext("replayFinished", "Yes")],
[resumeGame, leaveGame]);
}
/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction)
{
// check, if optional direction parameter is valid.
if (!direction || !(direction >= 0 && direction < 4))
direction = 0;
// get the bar and update it
let statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
let healthSize = statusBar.size;
let value = 100*Math.max(0, Math.min(1, points / maxPoints));
// inverse bar
if (direction == 2 || direction == 3)
value = 100 - value;
if (direction == 0)
healthSize.rright = value;
else if (direction == 1)
healthSize.rbottom = value;
else if (direction == 2)
healthSize.rleft = value;
else if (direction == 3)
healthSize.rtop = value;
statusBar.size = healthSize;
}
function updatePanelEntities()
{
let playerState = GetSimState().players[g_ViewedPlayer];
let panelEnts = playerState ? playerState.panelEntities : [];
g_PanelEntities = g_PanelEntities.filter(panelEnt => panelEnts.find(ent => ent == panelEnt.ent));
for (let ent of panelEnts)
{
let panelEntState = GetExtendedEntityState(ent);
let template = GetTemplateData(panelEntState.template);
let panelEnt = g_PanelEntities.find(panelEnt => ent == panelEnt.ent);
if (!panelEnt)
{
panelEnt = {
"ent": ent,
"tooltip": undefined,
"sprite": "stretched:session/portraits/" + template.icon,
"maxHitpoints": undefined,
"currentHitpoints": panelEntState.hitpoints,
"previousHitpoints": undefined
};
g_PanelEntities.push(panelEnt);
}
panelEnt.tooltip = createPanelEntityTooltip(panelEntState, template);
panelEnt.previousHitpoints = panelEnt.currentHitpoints;
panelEnt.currentHitpoints = panelEntState.hitpoints;
panelEnt.maxHitpoints = panelEntState.maxHitpoints;
}
let panelEntIndex = ent => g_PanelEntityOrder.findIndex(entClass =>
GetEntityState(ent).identity.classes.indexOf(entClass) != -1);
g_PanelEntities = g_PanelEntities.sort((panelEntA, panelEntB) => panelEntIndex(panelEntA.ent) - panelEntIndex(panelEntB.ent))
}
function createPanelEntityTooltip(panelEntState, template)
{
let getPanelEntNameTooltip = panelEntState => "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
return [
getPanelEntNameTooltip,
getCurrentHealthTooltip,
getAttackTooltip,
getArmorTooltip,
getEntityTooltip,
getAurasTooltip
].map(tooltip => tooltip(panelEntState)).filter(tip => tip).join("\n");
}
function displayPanelEntities()
{
let buttons = Engine.GetGUIObjectByName("panelEntityPanel").children;
buttons.forEach((button, slot) => {
if (button.hidden || g_PanelEntities.some(panelEnt => panelEnt.slot !== undefined && panelEnt.slot == slot))
return;
button.hidden = true;
stopColorFade("panelEntityHitOverlay[" + slot + "]");
});
// The slot identifies the button, displayIndex determines its position.
for (let displayIndex = 0; displayIndex < Math.min(g_PanelEntities.length, buttons.length); ++displayIndex)
{
let panelEnt = g_PanelEntities[displayIndex];
// Find the first unused slot if new, otherwise reuse previous.
let slot = panelEnt.slot === undefined ?
buttons.findIndex(button => button.hidden) :
panelEnt.slot;
let panelEntButton = Engine.GetGUIObjectByName("panelEntityButton[" + slot + "]");
panelEntButton.tooltip = panelEnt.tooltip;
updateGUIStatusBar("panelEntityHealthBar[" + slot + "]", panelEnt.currentHitpoints, panelEnt.maxHitpoints);
if (panelEnt.slot === undefined)
{
let panelEntImage = Engine.GetGUIObjectByName("panelEntityImage[" + slot + "]");
panelEntImage.sprite = panelEnt.sprite;
panelEntButton.hidden = false;
panelEnt.slot = slot;
}
// If the health of the panelEnt changed since the last update, trigger the animation.
if (panelEnt.previousHitpoints > panelEnt.currentHitpoints)
startColorFade("panelEntityHitOverlay[" + slot + "]", 100, 0,
colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
// TODO: Instead of instant position changes, animate button movement.
setPanelObjectPosition(panelEntButton, displayIndex, buttons.length);
}
}
function updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) =>
{
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton["+i+"]");
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.ondoublepress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onpressright = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
// Chose icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
function updateDebug()
{
let debug = Engine.GetGUIObjectByName("debugEntityState");
if (!Engine.GetGUIObjectByName("devDisplayState").checked)
{
debug.hidden = true;
return;
}
debug.hidden = false;
let conciseSimState = deepcopy(GetSimState());
conciseSimState.players = "<<>>";
let text = "simulation: " + uneval(conciseSimState);
let selection = g_Selection.toList();
if (selection.length)
{
let entState = GetExtendedEntityState(selection[0]);
if (entState)
{
let template = GetTemplateData(entState.template);
text += "\n\nentity: {\n";
for (let k in entState)
text += " "+k+":"+uneval(entState[k])+"\n";
text += "}\n\ntemplate: " + uneval(template);
}
}
debug.caption = text.replace(/\[/g, "\\[");
}
function getAllyStatTooltip(resource)
{
let playersState = GetSimState().players;
let ret = "";
for (let player in playersState)
{
if (player != 0 &&
player != g_ViewedPlayer &&
g_Players[player].state != "defeated" &&
(g_IsObserver ||
playersState[g_ViewedPlayer].hasSharedLos &&
g_Players[player].isMutualAlly[g_ViewedPlayer]))
{
ret += "\n" + sprintf(translate("%(playername)s: %(statValue)s"),{
"playername": colorizePlayernameHelper("■", player) + " " + g_Players[player].name,
"statValue": resource == "pop" ?
sprintf(translate("%(popCount)s/%(popLimit)s/%(popMax)s"), playersState[player]) :
Math.round(playersState[player].resourceCounts[resource])
});
}
}
return ret;
}
function updatePlayerDisplay()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let resCodes = g_ResourceData.GetCodes();
let resNames = g_ResourceData.GetNames();
for (let r = 0; r < resCodes.length; ++r)
{
let resourceObj = Engine.GetGUIObjectByName("resource[" + r + "]");
if (!resourceObj)
break;
let res = resCodes[r];
let tooltip = '[font="' + g_ResourceTitleFont + '"]' +
getLocalizedResourceName(resNames[res], "firstWord") + '[/font]';
let descr = g_ResourceData.GetResource(res).description;
if (descr)
tooltip += "\n" + translate(descr);
tooltip += getAllyStatTooltip(res);
resourceObj.tooltip = tooltip;
Engine.GetGUIObjectByName("resource["+r+"]_count").caption = Math.floor(playerState.resourceCounts[res]);
}
Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState);
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax }) +
getAllyStatTooltip("pop");
g_IsTrainingBlocked = playerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
let entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
let position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
let researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", g_ViewedPlayer);
// Set up initial positioning.
let buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (let i = 0; i < 10; ++i)
{
let button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
let size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
let numButtons = 0;
for (let tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
let template = GetTechnologyData(tech);
let button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); }; })(researchStarted[tech].researcher);
let icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
let size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;
++numButtons;
}
// Hide unused buttons.
for (let i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {Boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array,
// to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove
});
}
/**
* Toggles the display of range overlays of selected entities for the given range type.
* @param {string} type - for example "Aura"
*/
function toggleRangeOverlay(type, currentValue)
{
let configString = "gui.session." + type.toLowerCase() + "range";
let enabled = Engine.ConfigDB_GetValue("user", configString) != "true";
Engine.ConfigDB_CreateValue("user", configString, String(enabled));
Engine.ConfigDB_WriteValueToFile("user", configString, String(enabled), "config/user.cfg");
Engine.GuiInterfaceCall("EnableVisualRangeOverlayType", {
"type": type,
"enabled": enabled
});
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
Engine.GuiInterfaceCall("SetRangeOverlays", {
"entities": selected,
"enabled": enabled
});
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
{
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
if (g_ShowGuarded)
{
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
function playAmbient()
{
Engine.PlayAmbientSound(pickRandom(g_Ambient), true);
}
function getBuildString()
{
return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), {
"buildDate": Engine.GetBuildTimestamp(0),
"revision": Engine.GetBuildTimestamp(2)
});
}
function showTimeWarpMessageBox()
{
messageBox(
500, 250,
translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."),
translate("Time warp mode")
);
}
/**
* Adds the ingame time and ceasefire counter to the global FPS and
* realtime counters shown in the top right corner.
*/
function appendSessionCounters(counters)
{
let simState = GetSimState();
if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true")
{
let currentSpeed = Engine.GetSimRate();
if (currentSpeed != 1.0)
// Translation: The "x" means "times", with the mathematical meaning of multiplication.
counters.push(sprintf(translate("%(time)s (%(speed)sx)"), {
"time": timeToString(simState.timeElapsed),
"speed": Engine.FormatDecimalNumberIntoString(currentSpeed)
}));
else
counters.push(timeToString(simState.timeElapsed));
}
if (simState.ceasefireActive && Engine.ConfigDB_GetValue("user", "gui.session.ceasefirecounter") === "true")
counters.push(timeToString(simState.ceasefireTimeRemaining));
g_ResearchListTop = 4 + 14 * counters.length;
}
/**
* Send the current list of players, teams, AIs, observers and defeated/won and offline states to the lobby.
* The playerData format from g_GameAttributes is kept to reuse the GUI function presenting the data.
*/
function sendLobbyPlayerlistUpdate()
{
if (!g_IsController || !Engine.HasXmppClient())
return;
// Extract the relevant player data and minimize packet load
let minPlayerData = [];
for (let playerID in g_GameAttributes.settings.PlayerData)
{
if (+playerID == 0)
continue;
let pData = g_GameAttributes.settings.PlayerData[playerID];
let minPData = { "Name": pData.Name };
if (g_GameAttributes.settings.LockTeams)
minPData.Team = pData.Team;
if (pData.AI)
{
minPData.AI = pData.AI;
minPData.AIDiff = pData.AIDiff;
}
if (g_Players[playerID].offline)
minPData.Offline = true;
// Whether the player has won or was defeated
let state = g_Players[playerID].state;
if (state != "active")
minPData.State = state;
minPlayerData.push(minPData);
}
// Add observers
let connectedPlayers = 0;
for (let guid in g_PlayerAssignments)
{
let pData = g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player];
if (pData)
++connectedPlayers;
else
minPlayerData.push({
"Name": g_PlayerAssignments[guid].name,
"Team": "observer"
});
}
Engine.SendChangeStateGame(connectedPlayers, playerDataToStringifiedTeamList(minPlayerData));
}
/**
* Send a report on the gamestatus to the lobby.
*/
function reportGame()
{
// Only 1v1 games are rated (and Gaia is part of g_Players)
if (!Engine.HasXmppClient() || !Engine.IsRankedGame() ||
g_Players.length != 3 || Engine.GetPlayerID() == -1)
return;
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
let unitsClasses = [
"total",
"Infantry",
"Worker",
"FemaleCitizen",
"Cavalry",
"Champion",
"Hero",
"Siege",
"Ship",
"Trader"
];
let unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
let buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
let buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
let resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
let resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
let misc = [
"tradeIncome",
"tributesSent",
"tributesReceived",
"treasuresCollected",
"lootCollected",
"percentMapExplored"
];
let playerStatistics = {};
// Unit Stats
for (let unitCounterType of unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for (let buildingCounterType of buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for (let resourcesCounterType of resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
for (let type of misc)
playerStatistics[type] = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
let mapName = g_GameAttributes.settings.Name;
let playerStates = "";
let playerCivs = "";
let teams = "";
let teamsLocked = true;
// Serialize the statistics for each player into a comma-separated list.
// Ignore gaia
for (let i = 1; i < extendedSimState.players.length; ++i)
{
let player = extendedSimState.players[i];
let maxIndex = player.sequences.time.length - 1;
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for (let resourcesCounterType of resourcesCounterTypes)
for (let resourcesType of resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.sequences[resourcesCounterType][resourcesType][maxIndex] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.sequences.resourcesGathered.vegetarianFood[maxIndex] + ",";
for (let unitCounterType of unitsCountersTypes)
for (let unitsClass of unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.sequences[unitCounterType][unitsClass][maxIndex] + ",";
for (let buildingCounterType of buildingsCountersTypes)
for (let buildingsClass of buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.sequences[buildingCounterType][buildingsClass][maxIndex] + ",";
let total = 0;
for (let type in player.sequences.resourcesGathered)
total += player.sequences.resourcesGathered[type][maxIndex];
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.sequences.enemyUnitsKilledValue[maxIndex] +
player.sequences.enemyBuildingsDestroyedValue[maxIndex]) / 10)) + ",";
for (let type of misc)
playerStatistics[type] += player.sequences[type][maxIndex] + ",";
}
// Send the report with serialized data
let reportObject = {};
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_GameAttributes.matchID;
reportObject.civs = playerCivs;
reportObject.teams = teams;
reportObject.teamsLocked = String(teamsLocked);
reportObject.ceasefireActive = String(extendedSimState.ceasefireActive);
reportObject.ceasefireTimeRemaining = String(extendedSimState.ceasefireTimeRemaining);
reportObject.mapName = mapName;
reportObject.economyScore = playerStatistics.economyScore;
reportObject.militaryScore = playerStatistics.militaryScore;
reportObject.totalScore = playerStatistics.totalScore;
for (let rct of resourcesCounterTypes)
{
for (let rt of resourcesTypes)
reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
}
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for (let type of unitsClasses)
{
// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type];
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type];
reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
}
for (let type of buildingsClasses)
{
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type];
reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
}
for (let type of misc)
reportObject[type] = playerStatistics[type];
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/source/ps/VisualReplay.cpp
===================================================================
--- ps/trunk/source/ps/VisualReplay.cpp (revision 19673)
+++ ps/trunk/source/ps/VisualReplay.cpp (revision 19674)
@@ -1,348 +1,510 @@
/* Copyright (C) 2017 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 "VisualReplay.h"
#include "graphics/GameView.h"
#include "gui/GUIManager.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/utf8.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Mod.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptInterface.h"
/**
* Filter too short replays (value in seconds).
*/
const u8 minimumReplayDuration = 3;
+static const OsPath tempCacheFileName = VisualReplay::GetDirectoryName() / L"replayCache_temp.json";
+static const OsPath cacheFileName = VisualReplay::GetDirectoryName() / L"replayCache.json";
+
OsPath VisualReplay::GetDirectoryName()
{
const Paths paths(g_args);
return OsPath(paths.UserData() / "replays" / engine_version);
}
void VisualReplay::StartVisualReplay(const CStrW& directory)
{
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt";
if (!FileExists(replayFile))
return;
g_Game = new CGame(false, false);
g_Game->StartVisualReplay(replayFile.string8());
}
-/**
- * Load all replays found in the directory.
- *
- * Since files are spread across the harddisk,
- * loading hundreds of them can consume a lot of time.
- */
-JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
+bool VisualReplay::ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject)
{
- TIMER(L"GetReplays");
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
- u32 i = 0;
- DirectoryNames directories;
+ if (!FileExists(cacheFileName))
+ return false;
+
+ std::ifstream cacheStream(cacheFileName.string8().c_str());
+ CStr cacheStr((std::istreambuf_iterator(cacheStream)), std::istreambuf_iterator());
+ cacheStream.close();
+
+ JS::RootedValue cachedReplays(cx);
+ if (scriptInterface.ParseJSON(cacheStr, &cachedReplays))
+ {
+ cachedReplaysObject.set(&cachedReplays.toObject());
+ if (JS_IsArrayObject(cx, cachedReplaysObject))
+ return true;
+ }
+
+ LOGWARNING("The replay cache file is corrupted, it will be deleted");
+ wunlink(cacheFileName);
+ return false;
+}
+
+void VisualReplay::StoreCacheFile(ScriptInterface& scriptInterface, JS::HandleObject replays)
+{
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue replaysRooted(cx, JS::ObjectValue(*replays));
+ std::ofstream cacheStream(tempCacheFileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc);
+ cacheStream << scriptInterface.StringifyJSON(&replaysRooted);
+ cacheStream.close();
+
+ wunlink(cacheFileName);
+ if (wrename(tempCacheFileName, cacheFileName))
+ LOGERROR("Could not store the replay cache");
+}
+
+JS::HandleObject VisualReplay::ReloadReplayCache(ScriptInterface& scriptInterface, bool compareFiles)
+{
+ TIMER(L"ReloadReplayCache");
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ // Maps the filename onto the index and size
+ typedef std::map> replayCacheMap;
+
+ replayCacheMap fileList;
+
+ JS::RootedObject cachedReplaysObject(cx);
+ if (ReadCacheFile(scriptInterface, &cachedReplaysObject))
+ {
+ // Create list of files included in the cache
+ u32 cacheLength = 0;
+ JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength);
+ for (u32 j = 0; j < cacheLength; ++j)
+ {
+ JS::RootedValue replay(cx);
+ JS_GetElement(cx, cachedReplaysObject, j, &replay);
+
+ JS::RootedValue file(cx);
+ CStr fileName;
+ double fileSize;
+ scriptInterface.GetProperty(replay, "directory", fileName);
+ scriptInterface.GetProperty(replay, "fileSize", fileSize);
+
+ fileList[fileName] = std::make_pair(j, fileSize);
+ }
+ }
+
JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
+ DirectoryNames directories;
+
+ if (GetDirectoryEntries(GetDirectoryName(), nullptr, &directories) != INFO::OK)
+ return replays;
- if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
- for (OsPath& directory : directories)
+ bool newReplays = false;
+ std::vector copyFromOldCache;
+ // Specifies where the next replay should be kept
+ u32 i = 0;
+
+ for (const OsPath& directory : directories)
+ {
+ if (SDL_QuitRequested())
+ // We want to save our progress in searching through the replays
+ break;
+
+ bool isNew = true;
+ replayCacheMap::iterator it = fileList.find(directory.string8());
+ if (it != fileList.end())
{
- if (SDL_QuitRequested())
- return JSVAL_NULL;
+ if (compareFiles)
+ {
+ CFileInfo fileInfo;
+ GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo);
+ if (fileInfo.Size() == it->second.second)
+ isNew = false;
+ }
+ else
+ isNew = false;
+ }
+ if (isNew)
+ {
JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
- if (!replayData.isNull())
- JS_SetElement(cx, replays, i++, replayData);
+ if (replayData.isNull())
+ {
+ CFileInfo fileInfo;
+ GetFileInfo(GetDirectoryName() / directory / L"commands.txt", &fileInfo);
+ scriptInterface.Eval("({})", &replayData);
+ scriptInterface.SetProperty(replayData, "directory", directory);
+ scriptInterface.SetProperty(replayData, "fileSize", (double)fileInfo.Size());
+ }
+ JS_SetElement(cx, replays, i++, replayData);
+ newReplays = true;
}
- return JS::ObjectValue(*replays);
+ else
+ copyFromOldCache.push_back(it->second.first);
+ }
+
+ debug_printf(
+ "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n",
+ (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i);
+
+ if (!newReplays && fileList.empty())
+ return replays;
+
+ // No replay was changed, so just return the cache
+ if (!newReplays && fileList.size() == copyFromOldCache.size())
+ return cachedReplaysObject;
+
+ {
+ // Copy the replays from the old cache that are not deleted
+ if (!copyFromOldCache.empty())
+ for (u32 j : copyFromOldCache)
+ {
+ JS::RootedValue replay(cx);
+ JS_GetElement(cx, cachedReplaysObject, j, &replay);
+ JS_SetElement(cx, replays, i++, replay);
+ }
+ }
+ StoreCacheFile(scriptInterface, replays);
+ return replays;
+}
+
+JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface, bool compareFiles)
+{
+ TIMER(L"GetReplays");
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+ JS::RootedObject replays(cx, ReloadReplayCache(scriptInterface, compareFiles));
+ // Only take entries with data
+ JS::RootedObject replaysWithoutNullEntries(cx, JS_NewArrayObject(cx, 0));
+ u32 replaysLength = 0;
+ JS_GetArrayLength(cx, replays, &replaysLength);
+ for (u32 j = 0, i = 0; j < replaysLength; ++j)
+ {
+ JS::RootedValue replay(cx);
+ JS_GetElement(cx, replays, j, &replay);
+ if (scriptInterface.HasProperty(replay, "attribs"))
+ JS_SetElement(cx, replaysWithoutNullEntries, i++, replay);
+ }
+ return JS::ObjectValue(*replaysWithoutNullEntries);
}
/**
* Move the cursor backwards until a newline was read or the beginning of the file was found.
* Either way the cursor points to the beginning of a newline.
*
* @return The current cursor position or -1 on error.
*/
inline int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64& fileSize)
{
int currentPos;
char character;
for (int characters = 0; characters < 10000; ++characters)
{
currentPos = (int) replayStream->tellg();
// Stop when reached the beginning of the file
if (currentPos == 0)
return currentPos;
if (!replayStream->good())
{
LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str());
return -1;
}
// Stop when reached newline
replayStream->get(character);
if (character == '\n')
return currentPos;
// Otherwise go back one character.
// Notice: -1 will set the cursor back to the most recently read character.
replayStream->seekg(-2, std::ios_base::cur);
}
LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str());
return -1;
}
/**
* Compute game duration in seconds. Assume constant turn length.
* Find the last line that starts with "turn" by reading the file backwards.
*
* @return seconds or -1 on error
*/
inline int getReplayDuration(std::istream* replayStream, const CStr& fileName, const u64& fileSize)
{
CStr type;
// Move one character before the file-end
replayStream->seekg(-2, std::ios_base::end);
// Infinite loop protection, should never occur.
// There should be about 5 lines to read until a turn is found.
for (int linesRead = 1; linesRead < 1000; ++linesRead)
{
int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize);
// Read error or reached file beginning. No turns exist.
if (currentPosition < 1)
return -1;
if (!replayStream->good())
{
LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.c_str());
return -1;
}
// Found last turn, compute duration.
if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn")
{
u32 turn = 0, turnLength = 0;
*replayStream >> turn >> turnLength;
return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0
}
// Otherwise move cursor back to the character before the last newline
replayStream->seekg(currentPosition - 2, std::ios_base::beg);
}
LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str());
return -1;
}
-JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory)
+JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory)
{
// The directory argument must not be constant, otherwise concatenating will fail
const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
if (!FileExists(replayFile))
return JSVAL_NULL;
// Get file size and modification date
CFileInfo fileInfo;
GetFileInfo(replayFile, &fileInfo);
const u64 fileSize = (u64)fileInfo.Size();
if (fileSize == 0)
return JSVAL_NULL;
// Open file
const CStr fileName = replayFile.string8();
std::ifstream* replayStream = new std::ifstream(fileName.c_str());
// File must begin with "start"
CStr type;
if (!(*replayStream >> type).good())
{
LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str());
SAFE_DELETE(replayStream);
return JSVAL_NULL;
}
if (type != "start")
{
LOGWARNING("The replay %s is broken!", fileName.c_str());
SAFE_DELETE(replayStream);
return JSVAL_NULL;
}
// Parse header / first line
CStr header;
std::getline(*replayStream, header);
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attribs(cx);
if (!scriptInterface.ParseJSON(header, &attribs))
{
LOGERROR("Couldn't parse replay header of %s", fileName.c_str());
SAFE_DELETE(replayStream);
return JSVAL_NULL;
}
// Ensure "turn" after header
if (!(*replayStream >> type).good() || type != "turn")
{
SAFE_DELETE(replayStream);
return JSVAL_NULL; // there are no turns at all
}
// Don't process files of rejoined clients
u32 turn = 1;
*replayStream >> turn;
if (turn != 0)
{
SAFE_DELETE(replayStream);
return JSVAL_NULL;
}
int duration = getReplayDuration(replayStream, fileName, fileSize);
SAFE_DELETE(replayStream);
// Ensure minimum duration
if (duration < minimumReplayDuration)
return JSVAL_NULL;
// Return the actual data
JS::RootedValue replayData(cx);
scriptInterface.Eval("({})", &replayData);
scriptInterface.SetProperty(replayData, "file", replayFile);
scriptInterface.SetProperty(replayData, "directory", directory);
+ scriptInterface.SetProperty(replayData, "fileSize", (double)fileSize);
scriptInterface.SetProperty(replayData, "attribs", attribs);
scriptInterface.SetProperty(replayData, "duration", duration);
return replayData;
}
bool VisualReplay::DeleteReplay(const CStrW& replayDirectory)
{
if (replayDirectory.empty())
return false;
const OsPath directory = GetDirectoryName() / replayDirectory;
return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
}
-
JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
{
// Create empty JS object
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attribs(cx);
pCxPrivate->pScriptInterface->Eval("({})", &attribs);
// Return empty object if file doesn't exist
const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt";
if (!FileExists(replayFile))
return attribs;
// Open file
std::istream* replayStream = new std::ifstream(replayFile.string8().c_str());
CStr type, line;
ENSURE((*replayStream >> type).good() && type == "start");
// Read and return first line
std::getline(*replayStream, line);
pCxPrivate->pScriptInterface->ParseJSON(line, &attribs);
SAFE_DELETE(replayStream);;
return attribs;
}
+void VisualReplay::AddReplayToCache(ScriptInterface& scriptInterface, const CStrW& directoryName)
+{
+ TIMER(L"AddReplayToCache");
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, OsPath(directoryName)));
+ if (replayData.isNull())
+ return;
+
+ JS::RootedObject cachedReplaysObject(cx);
+ if (!ReadCacheFile(scriptInterface, &cachedReplaysObject))
+ cachedReplaysObject = JS_NewArrayObject(cx, 0);
+
+ u32 cacheLength = 0;
+ JS_GetArrayLength(cx, cachedReplaysObject, &cacheLength);
+ JS_SetElement(cx, cachedReplaysObject, cacheLength, replayData);
+
+ StoreCacheFile(scriptInterface, cachedReplaysObject);
+}
+
void VisualReplay::SaveReplayMetadata(ScriptInterface* scriptInterface)
{
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue metadata(cx);
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
if (!scriptInterface->CallFunction(global, "getReplayMetadata", &metadata))
{
LOGERROR("Could not save replay metadata!");
return;
}
// Get the directory of the currently active replay
const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
CreateDirectories(fileName.Parent(), 0700);
std::ofstream stream (fileName.string8().c_str(), std::ofstream::out | std::ofstream::trunc);
stream << scriptInterface->StringifyJSON(&metadata, false);
stream.close();
debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str());
}
bool VisualReplay::HasReplayMetadata(const CStrW& directoryName)
{
const OsPath filePath(GetDirectoryName() / directoryName / L"metadata.json");
if (!FileExists(filePath))
return false;
CFileInfo fileInfo;
GetFileInfo(filePath, &fileInfo);
return fileInfo.Size() > 0;
}
JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
{
if (!HasReplayMetadata(directoryName))
return JSVAL_NULL;
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue metadata(cx);
std::ifstream* stream = new std::ifstream(OsPath(GetDirectoryName() / directoryName / L"metadata.json").string8());
ENSURE(stream->good());
CStr line;
std::getline(*stream, line);
stream->close();
SAFE_DELETE(stream);
pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
return metadata;
}
Index: ps/trunk/source/ps/VisualReplay.h
===================================================================
--- ps/trunk/source/ps/VisualReplay.h (revision 19673)
+++ ps/trunk/source/ps/VisualReplay.h (revision 19674)
@@ -1,88 +1,120 @@
-/* Copyright (C) 2016 Wildfire Games.
+/* Copyright (C) 2017 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 .
*/
#ifndef INCLUDED_REPlAY
#define INCLUDED_REPlAY
#include "scriptinterface/ScriptInterface.h"
class CSimulation2;
class CGUIManager;
/**
* Contains functions for visually replaying past games.
*/
namespace VisualReplay
{
/**
* Returns the path to the sim-log directory (that contains the directories with the replay files.
*
- * @param scriptInterface the ScriptInterface in which to create the return data.
+ * @param scriptInterface - the ScriptInterface in which to create the return data.
* @return OsPath the absolute file path
*/
OsPath GetDirectoryName();
/**
* Replays the commands.txt file in the given subdirectory visually.
*/
void StartVisualReplay(const CStrW& directory);
/**
+ * Reads the replay Cache file and parses it into a jsObject
+ *
+ * @param scriptInterface - the ScriptInterface in which to create the return data.
+ * @param cachedReplaysObject - the cached replays.
+ * @return true on succes
+ */
+bool ReadCacheFile(ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject);
+
+/**
+ * Stores the replay list in the replay cache file
+ *
+ * @param scriptInterface - the ScriptInterface in which to create the return data.
+ * @param replays - the replay list to store.
+ */
+void StoreCacheFile(ScriptInterface& scriptInterface, JS::HandleObject replays);
+
+/**
+ * Load the replay cache and check if there are new/deleted replays. If so, update the cache.
+ *
+ * @param scriptInterface - the ScriptInterface in which to create the return data.
+ * @param compareFiles - compare the directory name and the FileSize of the replays and the cache.
+ * @return cache entries
+ */
+JS::HandleObject ReloadReplayCache(ScriptInterface& scriptInterface, bool compareFiles);
+
+/**
* Get a list of replays to display in the GUI.
*
- * @param scriptInterface the ScriptInterface in which to create the return data.
+ * @param scriptInterface - the ScriptInterface in which to create the return data.
+ * @param compareFiles - reload the cache, which takes more time,
+ * but nearly ensures, that no changed replay is missed.
* @return array of objects containing replay data
*/
-JS::Value GetReplays(ScriptInterface& scriptInterface);
+JS::Value GetReplays(ScriptInterface& scriptInterface, bool compareFiles);
/**
* Parses a commands.txt file and extracts metadata.
* Works similarly to CGame::LoadReplayData().
*/
-JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory);
+JS::Value LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory);
/**
* Permanently deletes the visual replay (including the parent directory)
*
- * @param replayFile path to commands.txt, whose parent directory will be deleted
+ * @param replayFile - path to commands.txt, whose parent directory will be deleted.
* @return true if deletion was successful, false on error
*/
bool DeleteReplay(const CStrW& replayFile);
/**
* Returns the parsed header of the replay file (commands.txt).
*/
JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
/**
* Returns whether or not the metadata / summary screen data has been saved properly when the game ended.
*/
bool HasReplayMetadata(const CStrW& directoryName);
/**
* Returns the metadata of a replay.
*/
JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
/**
- * Saves the metadata from the session to metadata.json
+ * Saves the metadata from the session to metadata.json.
*/
void SaveReplayMetadata(ScriptInterface* scriptInterface);
+/**
+* Adds a replay to the replayCache.
+*/
+void AddReplayToCache(ScriptInterface& scriptInterface, const CStrW& directoryName);
}
#endif
Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 19673)
+++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 19674)
@@ -1,70 +1,76 @@
/* Copyright (C) 2017 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 "ps/scripting/JSInterface_VisualReplay.h"
#include "ps/CStr.h"
#include "ps/Profile.h"
#include "ps/VisualReplay.h"
void JSI_VisualReplay::StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directory)
{
VisualReplay::StartVisualReplay(directory);
}
bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& replayFile)
{
return VisualReplay::DeleteReplay(replayFile);
}
-JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
+JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate, bool compareFiles)
{
- return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
+ return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface), compareFiles);
}
JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
{
return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName);
}
bool JSI_VisualReplay::HasReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName)
{
return VisualReplay::HasReplayMetadata(directoryName);
}
JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
{
return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName);
}
+void JSI_VisualReplay::AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
+{
+ VisualReplay::AddReplayToCache(*(pCxPrivate->pScriptInterface), directoryName);
+}
+
CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& directoryName)
{
return OsPath(VisualReplay::GetDirectoryName() / directoryName).string();
}
void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface)
{
- scriptInterface.RegisterFunction("GetReplays");
+ scriptInterface.RegisterFunction("GetReplays");
scriptInterface.RegisterFunction("DeleteReplay");
scriptInterface.RegisterFunction("StartVisualReplay");
scriptInterface.RegisterFunction("GetReplayAttributes");
scriptInterface.RegisterFunction("GetReplayMetadata");
scriptInterface.RegisterFunction("HasReplayMetadata");
+ scriptInterface.RegisterFunction("AddReplayToCache");
scriptInterface.RegisterFunction("GetReplayDirectoryName");
}
Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 19673)
+++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 19674)
@@ -1,36 +1,37 @@
/* Copyright (C) 2017 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 .
*/
#ifndef INCLUDED_JSI_VISUALREPLAY
#define INCLUDED_JSI_VISUALREPLAY
#include "ps/VisualReplay.h"
#include "scriptinterface/ScriptInterface.h"
namespace JSI_VisualReplay
{
void StartVisualReplay(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directory);
bool DeleteReplay(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& replayFile);
- JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate);
+ JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate, bool compareFiles);
JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
bool HasReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
+ void AddReplayToCache(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
void RegisterScriptFunctions(ScriptInterface& scriptInterface);
CStrW GetReplayDirectoryName(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
}
#endif