Index: ps/trunk/binaries/data/mods/public/gui/common/settings.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 17053)
+++ ps/trunk/binaries/data/mods/public/gui/common/settings.js (revision 17054)
@@ -1,258 +1,313 @@
/**
* The maximum number of players that the engine supports.
* TODO: Maybe we can support more than 8 players sometime.
*/
const g_MaxPlayers = 8;
/**
* The maximum number of teams allowed.
*/
const g_MaxTeams = 4;
/**
* Directory containing all editable settings.
*/
const g_SettingsDirectory = "simulation/data/settings/";
/**
* An object containing all values given by setting name.
* Used by lobby, gamesetup, session, summary screen and replay menu.
*/
const g_Settings = loadSettingsValues();
/**
* Loads and translates all values of all settings which
* can be configured by dropdowns in the gamesetup.
*
* @returns {Object|undefined}
*/
function loadSettingsValues()
{
// TODO: move PlayerDefaults and MapSizes from functions_utility.js here
var settings = {
"AIDescriptions": loadAIDescriptions(),
"AIDifficulties": loadAIDifficulties(),
"Ceasefire": loadCeasefire(),
"GameSpeeds": loadSettingValuesFile("game_speeds.json"),
"MapTypes": loadMapTypes(),
"PopulationCapacities": loadPopulationCapacities(),
"StartingResources": loadSettingValuesFile("starting_resources.json"),
"VictoryConditions": loadVictoryConditions()
};
if (Object.keys(settings).some(key => settings[key] === undefined))
return undefined;
return settings;
}
/**
* Returns an array of objects reflecting all possible values for a given setting.
*
* @param {string} filename
* @see simulation/data/settings/
* @returns {Array|undefined}
*/
function loadSettingValuesFile(filename)
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + filename);
if (!json || !json.Data)
{
error("Could not load " + filename + "!");
return undefined;
}
if (json.TranslatedKeys)
translateObjectKeys(json.Data, json.TranslatedKeys);
return json.Data;
}
/**
* Loads the descriptions as defined in simulation/ai/.../data.json and loaded by ICmpAIManager.cpp.
*
* @returns {Array}
*/
function loadAIDescriptions()
{
var ais = Engine.GetAIs();
translateObjectKeys(ais, ["name", "description"]);
return ais.sort((a, b) => a.data.name.localeCompare(b.data.name));
}
/**
* Hardcoded, as modding is not supported without major changes.
* Notice the AI code parses the difficulty level by the index, not by name.
*
* @returns {Array}
*/
function loadAIDifficulties()
{
return [
{
"Name": "sandbox",
"Title": translateWithContext("aiDiff", "Sandbox")
},
{
"Name": "very easy",
"Title": translateWithContext("aiDiff", "Very Easy")
},
{
"Name": "easy",
"Title": translateWithContext("aiDiff", "Easy")
},
{
"Name": "medium",
"Title": translateWithContext("aiDiff", "Medium"),
"Default": true
},
{
"Name": "hard",
"Title": translateWithContext("aiDiff", "Hard")
},
{
"Name": "very hard",
"Title": translateWithContext("aiDiff", "Very Hard")
}
];
}
/**
* Loads available ceasefire settings.
*
* @returns {Array|undefined}
*/
function loadCeasefire()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "ceasefire.json");
if (!json || json.Default === undefined || !json.Times || !Array.isArray(json.Times))
{
error("Could not load ceasefire.json");
return undefined;
}
return json.Times.map(timeout => ({
"Duration": timeout,
"Default": timeout == json.Default,
"Title": timeout == 0 ? translateWithContext("ceasefire", "No ceasefire") :
sprintf(translatePluralWithContext("ceasefire", "%(minutes)s minute", "%(minutes)s minutes", timeout), { "minutes": timeout })
}));
}
/**
* Hardcoded, as modding is not supported without major changes.
*
* @returns {Array}
*/
function loadMapTypes()
{
return [
{
"Name": "skirmish",
"Title": translateWithContext("map", "Skirmish"),
"Default": true
},
{
"Name": "random",
"Title": translateWithContext("map", "Random")
},
{
"Name": "scenario",
"Title": translate("Scenario") // TODO: update the translation (but not shortly before a release)
}
];
}
/**
* Loads available gametypes.
*
* @returns {Array|undefined}
*/
function loadVictoryConditions()
{
const subdir = "victory_conditions/";
const files = Engine.BuildDirEntList(g_SettingsDirectory + subdir, "*.json", false).map(
file => file.substr(g_SettingsDirectory.length));
var victoryConditions = files.map(file => {
let vc = loadSettingValuesFile(file);
if (vc)
vc.Name = file.substr(subdir.length, file.length - (subdir + ".json").length);
return vc;
});
if (victoryConditions.some(vc => vc == undefined))
return undefined;
// TODO: We might support enabling victory conditions separately sometime.
// Until then, we supplement the endless gametype here.
victoryConditions.push({
"Name": "endless",
"Title": translate("None"),
"Description": translate("Endless Game"),
"Scripts": []
});
return victoryConditions;
}
/**
* Loads available population capacities.
*
* @returns {Array|undefined}
*/
function loadPopulationCapacities()
{
var json = Engine.ReadJSONFile(g_SettingsDirectory + "population_capacities.json");
if (!json || json.Default === undefined || !json.PopulationCapacities || !Array.isArray(json.PopulationCapacities))
{
error("Could not load population_capacities.json");
return undefined;
}
return json.PopulationCapacities.map(population => ({
"Population": population,
"Default": population == json.Default,
"Title": population < 10000 ? population : translate("Unlimited")
}));
}
/**
* Creates an object with all values of that property of the given setting and
* finds the index of the default value.
*
* This allows easy copying of setting values to dropdown lists.
*
* @param settingValues {Array}
* @returns {Object|undefined}
*/
function prepareForDropdown(settingValues)
{
if (!settingValues)
return undefined;
var settings = { "Default": 0 };
for (let index in settingValues)
{
for (let property in settingValues[index])
{
if (property == "Default")
continue;
if (!settings[property])
settings[property] = [];
// Switch property and index
settings[property][index] = settingValues[index][property];
}
// Copy default value
if (settingValues[index].Default)
settings.Default = +index;
}
return settings;
}
+
+/**
+ * Returns title or placeholder.
+ *
+ * @param aiName {string} - for example "petra"
+ */
+function translateAIName(aiName)
+{
+ var description = g_Settings.AIDescriptions.find(ai => ai.id == aiName);
+ return description ? translate(description.data.name) : translate("Unknown");
+}
+
+/**
+ * Returns title or placeholder.
+ *
+ * @param index {Number} - index of AIDifficulties
+ */
+function translateAIDifficulty(index)
+{
+ var difficulty = g_Settings.AIDifficulties[index];
+ return difficulty ? difficulty.Title : translate("Unknown");
+}
+
+/**
+ * Returns title or placeholder.
+ *
+ * @param mapType {string} - for example "skirmish"
+ */
+function translateMapType(mapType)
+{
+ var type = g_Settings.MapTypes.find(t => t.Name == mapType);
+ return type ? type.Title : translate("Unknown");
+}
+
+/**
+ * Returns title or placeholder.
+ *
+ * @param population {Number} - for example 300
+ */
+function translatePopulationCapacity(population)
+{
+ var popCap = g_Settings.PopulationCapacities.find(p => p.Population == population);
+ return popCap ? popCap.Title : translate("Unknown");
+}
+
+/**
+ * Returns title or placeholder.
+ *
+ * @param gameType {string} - for example "conquest"
+ */
+function translateVictoryCondition(gameType)
+{
+ var vc = g_Settings.VictoryConditions.find(vc => vc.Name == gameType);
+ return vc ? vc.Title : translate("Unknown");
+}
Index: ps/trunk/binaries/data/mods/public/gui/page_replaymenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/page_replaymenu.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/page_replaymenu.xml (revision 17054)
@@ -0,0 +1,15 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprite1.xml
+ common/styles.xml
+ common/common_sprites.xml
+ common/common_styles.xml
+
+ replaymenu/styles.xml
+ replaymenu/replay_menu.xml
+
Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 17053)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 17054)
@@ -1,618 +1,632 @@
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_actions.js (revision 17054)
@@ -0,0 +1,137 @@
+/**
+ * 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": {},
+ "savedGUIData": "",
+ "isReplay" : true
+ });
+}
+
+/**
+ * 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 ? 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(", ") }) + "\n";
+ errMsg += sprintf(translate("Active: %(mods)s"), { "mods": g_EngineInfo.mods.join(", ") });
+ }
+ else
+ errMsg = translate("This replay is not compatible with your version of the game!");
+
+ messageBox(500, 200, errMsg, translate("Incompatible replay"), 0, [translate("Ok")], [null]);
+}
+
+/**
+ * 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
+ var summary = Engine.GetReplayMetadata(g_ReplaysFiltered[selected].directory);
+
+ if (!summary)
+ {
+ messageBox(500, 200, translate("No summary data available."), translate("Error"), 0, [translate("Ok")], [null]);
+ return;
+ }
+
+ // Open summary screen
+ summary.isReplay = true;
+ summary.gameResult = translate("Scores at the end of the game.");
+ Engine.SwitchGuiPage("page_summary.xml", summary);
+}
+
+/**
+ * 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];
+
+ // Show confirmation message
+ var btCaptions = [translate("Yes"), translate("No")];
+ var btCode = [function() { reallyDeleteReplay(replay.directory); }, null];
+
+ var title = translate("Delete replay");
+ var question = translate("Are you sure to delete this replay permanently?") + "\n" + escapeText(replay.file);
+
+ messageBox(500, 200, question, title, 0, btCaptions, btCode);
+}
+
+/**
+ * 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)
+{
+ if (!Engine.DeleteReplay(replayDirectory))
+ error(sprintf("Could not delete replay '%(id)s'", { "id": replayDirectory }));
+
+ // Refresh replay list
+ init();
+}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_filters.js (revision 17054)
@@ -0,0 +1,220 @@
+/**
+ * Allow to filter replays by duration in 15min / 30min intervals.
+ */
+const g_DurationFilterIntervals = [
+ { "min": -1, "max": -1 },
+ { "min": -1, "max": 15 },
+ { "min": 15, "max": 30 },
+ { "min": 30, "max": 45 },
+ { "min": 45, "max": 60 },
+ { "min": 60, "max": 90 },
+ { "min": 90, "max": 120 },
+ { "min": 120, "max": -1 }
+];
+
+/**
+ * Allow to filter by population capacity.
+ */
+const g_PopulationCapacities = prepareForDropdown(g_Settings ? g_Settings.PopulationCapacities : undefined);
+
+/**
+ * Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays
+ * (including its derivatives g_MapSizes, g_MapNames).
+ */
+function initFilters()
+{
+ initDateFilter();
+ initMapNameFilter();
+ initMapSizeFilter();
+ initPopCapFilter();
+ initDurationFilter();
+}
+
+/**
+ * Allow to filter by month. Uses g_Replays.
+ */
+function initDateFilter()
+{
+ var months = g_Replays.map(replay => getReplayMonth(replay));
+ months = months.filter((month, index) => months.indexOf(month) == index).sort();
+ months.unshift(translateWithContext("datetime", "Any"));
+
+ var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
+ dateTimeFilter.list = months;
+ dateTimeFilter.list_data = months;
+
+ if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length)
+ dateTimeFilter.selected = 0;
+}
+
+/**
+ * Allow to filter by mapsize. Uses g_MapSizes.
+ */
+function initMapSizeFilter()
+{
+ var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
+ mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_mapSizes.shortNames);
+ mapSizeFilter.list_data = [-1].concat(g_mapSizes.tiles);
+
+ if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length)
+ mapSizeFilter.selected = 0;
+}
+
+/**
+ * Allow to filter by mapname. Uses g_MapNames.
+ */
+function initMapNameFilter()
+{
+ var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
+ mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames);
+ mapNameFilter.list_data = [""].concat(g_MapNames.map(mapName => translate(mapName)));
+
+ if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length)
+ mapNameFilter.selected = 0;
+}
+
+/**
+ * Allow to filter by population capacity.
+ */
+function initPopCapFilter()
+{
+ var populationFilter = Engine.GetGUIObjectByName("populationFilter");
+ populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title);
+ populationFilter.list_data = [""].concat(g_PopulationCapacities.Population);
+
+ if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length)
+ populationFilter.selected = 0;
+}
+
+/**
+ * Allow to filter by game duration. Uses g_DurationFilterIntervals.
+ */
+function initDurationFilter()
+{
+ var durationFilter = Engine.GetGUIObjectByName("durationFilter");
+ durationFilter.list = g_DurationFilterIntervals.map((interval, index) => {
+
+ if (index == 0)
+ return translateWithContext("duration", "Any");
+
+ if (index == 1)
+ // Translation: Shorter duration than max minutes.
+ return sprintf(translateWithContext("duration filter", "< %(max)s min"), interval);
+
+ if (index == g_DurationFilterIntervals.length - 1)
+ // Translation: Longer duration than min minutes.
+ return sprintf(translateWithContext("duration filter", "> %(min)s min"), interval);
+
+ // Translation: Duration between min and max minutes.
+ return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval);
+ });
+ durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index);
+
+ if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length)
+ durationFilter.selected = 0;
+}
+
+/**
+ * Initializes g_ReplaysFiltered with replays that are not filtered out and sort it.
+ */
+function filterReplays()
+{
+ const sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column;
+ const sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
+
+ g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) =>
+ {
+ let cmpA, cmpB;
+ switch (sortKey)
+ {
+ case 'name':
+ cmpA = +a.timestamp;
+ cmpB = +b.timestamp;
+ break;
+ case 'duration':
+ cmpA = +a.duration;
+ cmpB = +b.duration;
+ break;
+ case 'players':
+ cmpA = +a.attribs.settings.PlayerData.length;
+ cmpB = +b.attribs.settings.PlayerData.length;
+ break;
+ case 'mapName':
+ cmpA = getReplayMapName(a);
+ cmpB = getReplayMapName(b);
+ break;
+ case 'mapSize':
+ cmpA = +a.attribs.settings.Size;
+ cmpB = +b.attribs.settings.Size;
+ break;
+ case 'popCapacity':
+ cmpA = +a.attribs.settings.PopulationCap;
+ cmpB = +b.attribs.settings.PopulationCap;
+ break;
+ }
+
+ if (cmpA < cmpB)
+ return -sortOrder;
+ else if (cmpA > cmpB)
+ return +sortOrder;
+
+ return 0;
+ });
+}
+
+/**
+ * Decides whether the replay should be listed.
+ *
+ * @returns {bool} - true if replay should be visible
+ */
+function filterReplay(replay)
+{
+ // Check for compability first (most likely to filter)
+ var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter");
+ if (compabilityFilter.checked && !isReplayCompatible(replay))
+ return false;
+
+ // Filter date/time (select a month)
+ var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
+ if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected])
+ return false;
+
+ // Filter by playernames
+ var playersFilter = Engine.GetGUIObjectByName("playersFilter");
+ var keywords = playersFilter.caption.toLowerCase().split(" ");
+ if (keywords.length)
+ {
+ // We just check if all typed words are somewhere in the playerlist of that replay
+ let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase();
+ if (!keywords.every(keyword => playerList.indexOf(keyword) != -1))
+ return false;
+ }
+
+ // Filter by map name
+ var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
+ if (mapNameFilter.selected > 0 && getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
+ return false;
+
+ // Filter by map size
+ var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
+ if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected])
+ return false;
+
+ // Filter by population capacity
+ var populationFilter = Engine.GetGUIObjectByName("populationFilter");
+ if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected])
+ return false;
+
+ // Filter by game duration
+ var durationFilter = Engine.GetGUIObjectByName("durationFilter");
+ if (durationFilter.selected > 0)
+ {
+ let interval = g_DurationFilterIntervals[durationFilter.selected];
+
+ if ((interval.min > -1 && replay.duration < interval.min * 60) ||
+ (interval.max > -1 && replay.duration > interval.max * 60))
+ return false;
+ }
+
+ return true;
+}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.js (revision 17054)
@@ -0,0 +1,342 @@
+const g_EngineInfo = Engine.GetEngineInfo();
+const g_CivData = loadCivData();
+const g_DefaultPlayerData = initPlayerDefaults();
+const g_mapSizes = initMapSizes();
+
+/**
+ * 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 = [];
+
+/**
+ * Directory name of the currently selected replay. Used to restore the selection after changing filters.
+ */
+var g_selectedReplayDirectory = "";
+
+/**
+ * Initializes globals, loads replays and displays the list.
+ */
+function init()
+{
+ if (!g_Settings)
+ {
+ Engine.SwitchGuiPage("page_pregame.xml");
+ return;
+ }
+
+ // By default, sort replays by date in descending order
+ Engine.GetGUIObjectByName("replaySelection").selected_column_order = -1;
+
+ loadReplays();
+ displayReplayList();
+}
+
+/**
+ * Store the list of replays loaded in C++ in g_Replays.
+ * Check timestamp and compatibility and extract g_Playernames, g_MapNames
+ */
+function loadReplays()
+{
+ g_Replays = Engine.GetReplays();
+
+ g_Playernames = [];
+ for (let replay of g_Replays)
+ {
+ // Use time saved in file, otherwise file mod date
+ replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp;
+
+ // Check replay for compability
+ 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 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);
+ }
+ }
+ g_MapNames.sort();
+
+ // Reload filters (since they depend on g_Replays and its derivatives)
+ initFilters();
+}
+
+/**
+ * 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 = "";
+ });
+}
+
+/**
+ * Filter g_Replays, 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_selectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory;
+
+ filterReplays();
+
+ // Create GUI list data
+ var list = g_ReplaysFiltered.map(replay => {
+ let works = replay.isCompatible;
+ return {
+ "directories": replay.directory,
+ "months": greyout(getReplayDateTime(replay), works),
+ "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works),
+ "mapNames": greyout(getReplayMapName(replay), works),
+ "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works),
+ "durations": greyout(getReplayDuration(replay), works),
+ "playerNames": greyout(getReplayPlayernames(replay), works)
+ };
+ });
+
+ // Extract arrays
+ if (list.length)
+ list = prepareForDropdown(list);
+
+ // Push to GUI
+ replaySelection.selected = -1;
+ replaySelection.list_name = 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 || [];
+
+ // Restore selection
+ replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_selectedReplayDirectory);
+
+ displayReplayDetails();
+}
+
+/**
+ * Shows preview image, description and player text in the right panel.
+ */
+function displayReplayDetails()
+{
+ var selected = Engine.GetGUIObjectByName("replaySelection").selected;
+ var 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("summaryButton").enabled = replaySelected;
+
+ if (!replaySelected)
+ return;
+
+ var replay = g_ReplaysFiltered[selected];
+ var mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map);
+
+ // Update GUI
+ 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 = replay.attribs.settings.PlayerData.length;
+ Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay);
+ Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
+ Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapData.preview;
+}
+
+/**
+ * Adds grey font if replay is not compatible.
+ */
+function greyout(text, isCompatible)
+{
+ return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]';
+}
+
+/**
+ * Returns a human-readable version of the replay date.
+ */
+function getReplayDateTime(replay)
+{
+ return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm"))
+}
+
+/**
+ * Returns a human-readable list of the playernames of that replay.
+ *
+ * @returns {string}
+ */
+function getReplayPlayernames(replay)
+{
+ // TODO: colorize playernames like in the lobby.
+ 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.FormatMillisecondsIntoDateString(replay.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;
+}
+
+/**
+ * Returns a description of the player assignments.
+ * Including civs, teams, AI settings and player colors.
+ *
+ * If the spoiler-checkbox is checked, it also shows defeated players.
+ *
+ * @returns {string}
+ */
+function getReplayTeamText(replay)
+{
+ // Load replay metadata
+ const metadata = Engine.GetReplayMetadata(replay.directory);
+ const spoiler = Engine.GetGUIObjectByName("showSpoiler").checked;
+
+ var playerDescriptions = {};
+ var playerIdx = 0;
+ for (let playerData of replay.attribs.settings.PlayerData)
+ {
+ // Get player info
+ ++playerIdx;
+ let teamIdx = playerData.Team;
+ let playerColor = playerData.Color ? playerData.Color : g_DefaultPlayerData[playerIdx].Color;
+ let showDefeated = spoiler && metadata && metadata.playerStates && metadata.playerStates[playerIdx].state == "defeated";
+ let isAI = playerData.AI;
+
+ // Create human-readable player description
+ let playerDetails = {
+ "playerName": '[color="' + rgbToGuiColor(playerColor) + '"]' + escapeText(playerData.Name) + "[/color]",
+ "civ": translate(g_CivData[playerData.Civ].Name),
+ "AIname": isAI ? translateAIName(playerData.AI) : "",
+ "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : ""
+ };
+
+ if (!isAI && !showDefeated)
+ playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s)"), playerDetails);
+ else if (!isAI && showDefeated)
+ playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, defeated)"), playerDetails);
+ else if (isAI && !showDefeated)
+ playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"), playerDetails);
+ else
+ playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, defeated)"), playerDetails);
+
+ // Sort player descriptions by team
+ if (!playerDescriptions[teamIdx])
+ playerDescriptions[teamIdx] = [];
+ playerDescriptions[teamIdx].push(playerDetails);
+ }
+
+ var teams = Object.keys(playerDescriptions);
+
+ // If there are no teams, merge all playersDescriptions
+ if (teams.length == 1)
+ return playerDescriptions[teams[0]].join("\n") + "\n";
+
+ // If there are teams, merge "Team N:" + playerDescriptions
+ return teams.map(team => {
+ let teamCaption = (team == -1) ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 });
+ return '[font="sans-bold-14"]' + teamCaption + "[/font]:\n" + playerDescriptions[team].join("\n");
+ }).join("\n");
+}
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/replay_menu.xml (revision 17054)
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Replay Games
+
+
+
+
+
+
+
+
+
+ displayReplayList();
+
+
+
+ displayReplayList();
+ autoCompleteNick("playersFilter", g_Playernames.map(name => ({ "name": name })));
+
+
+
+ displayReplayList();
+
+
+
+ displayReplayList();
+
+
+
+ displayReplayList();
+
+
+
+ displayReplayList();
+
+
+
+
+
+
+
+ displayReplayDetails();
+ displayReplayList();
+
+
+
+
+ Date / Time
+
+
+
+ Players
+
+
+
+ Map Name
+
+
+
+ Size
+
+
+
+ Population
+
+
+
+ Duration
+
+
+
+
+
+
+
+
+
+
+
+ displayReplayList();
+
+
+
+
+ Filter compatible replays
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Map Type:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Map Size:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Victory:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Players:
+
+
+
+
+
+
+
+
+
+
+
+ displayReplayDetails();
+
+
+
+
+ Spoiler
+
+
+
+
+
+
+
+
+
+
+
+ Main Menu
+ Engine.SwitchGuiPage("page_pregame.xml");
+
+
+
+
+ Delete
+ deleteReplayButtonPressed();
+
+
+
+
+ Summary
+ showReplaySummary();
+
+
+
+
+ Start Replay
+ startReplay();
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/gui/replaymenu/styles.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/replaymenu/styles.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/replaymenu/styles.xml (revision 17054)
@@ -0,0 +1,14 @@
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 17053)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 17054)
@@ -1,1081 +1,1087 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_GameSpeeds = prepareForDropdown(g_Settings ? g_Settings.GameSpeeds.filter(speed => !speed.ReplayOnly || g_IsReplay) : undefined);
// Network Mode
var g_IsNetworked = false;
// Is this user in control of game settings (i.e. is a network server, or offline player)
var g_IsController;
// Match ID for tracking
var g_MatchID;
// Is this user an observer?
var g_IsObserver = false;
// Cache the basic player data (name, civ, color)
var g_Players = [];
// Cache the useful civ data
var g_CivData = {};
var g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1 } };
// Cache dev-mode settings that are frequently or widely used
var g_DevSettings = {
controlAll: false
};
// Whether status bars should be shown for all of the player's units.
var g_ShowAllStatusBars = false;
// Indicate when one of the current player's training queues is blocked
// (this is used to support population counter blinking)
var g_IsTrainingBlocked = false;
// Cache simulation state (updated on every simulation update)
var g_SimState;
// Cache EntityStates
var g_EntityStates = {}; // {id:entState}
// Whether the player has lost/won and reached the end of their game
var g_GameEnded = false;
var g_Disconnected = false; // Lost connection to server
// Holds player states from the last tick
var g_CachedLastStates = "";
// Top coordinate of the research list
var g_ResearchListTop = 4;
// Colors to flash when pop limit reached
const DEFAULT_POPULATION_COLOR = "white";
const POPULATION_ALERT_COLOR = "orange";
// List of additional entities to highlight
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
// for saving the hitpoins of the hero (is there a better way to do that?)
// Should be possible with AttackDetection but might be an overkill because it would have to loop
// always through the list of all ongoing attacks...
var g_previousHeroHitPoints = undefined;
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;
}
// Cache TemplateData
var g_TemplateData = {}; // {id:template}
var g_TemplateDataWithoutLocalization = {};
function GetTemplateData(templateName)
{
if (!(templateName in g_TemplateData))
{
var 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))
{
var template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
g_TemplateDataWithoutLocalization[templateName] = template;
}
return g_TemplateDataWithoutLocalization[templateName];
}
// Cache TechnologyData
var g_TechnologyData = {}; // {id:template}
function GetTechnologyData(technologyName)
{
if (!(technologyName in g_TechnologyData))
{
var template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[technologyName] = template;
}
return g_TechnologyData[technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
if (initData)
{
g_IsNetworked = initData.isNetworked; // Set network mode
g_IsController = initData.isController; // Set controller mode
g_PlayerAssignments = initData.playerAssignments;
g_MatchID = initData.attribs.matchID;
// Cache the player data
// (This may be updated at runtime by handleNetMessage)
g_Players = getPlayerData(g_PlayerAssignments);
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
}
else // Needed for autostart loading option
{
g_Players = getPlayerData(null);
}
// Cache civ data
g_CivData = loadCivData();
g_CivData["gaia"] = { "Code": "gaia", "Name": translate("Gaia") };
if (Engine.GetPlayerID() <= 0)
g_IsObserver = true;
updateTopPanel();
var gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
var 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(); // set initial position
// Populate player selection dropdown
var playerNames = [];
var playerIDs = [];
for (var player in g_Players)
{
playerNames.push(g_Players[player].name);
playerIDs.push(player);
}
var viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
viewPlayerDropdown.list = playerNames;
viewPlayerDropdown.list_data = playerIDs;
viewPlayerDropdown.selected = Engine.GetPlayerID();
// If in Atlas editor, disable the exit button
if (Engine.IsAtlasRunning())
Engine.GetGUIObjectByName("menuExitButton").enabled = false;
if (hotloadData)
g_Selection.selected = hotloadData.selection;
// Starting for the first time:
initMusic();
if (!g_IsObserver)
{
var civMusic = g_CivData[g_Players[Engine.GetPlayerID()].civ].Music;
global.music.storeTracks(civMusic);
}
global.music.setState(global.music.states.PEACE);
playRandomAmbient("temperate");
onSimulationUpdate();
// 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 selectViewPlayer(playerID)
{
Engine.SetPlayerID(playerID);
updateTopPanel();
}
function updateTopPanel()
{
var playerID = Engine.GetPlayerID();
var isPlayer = playerID > 0;
if (isPlayer)
{
var civName = g_CivData[g_Players[playerID].civ].Name;
Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[playerID].civ].Emblem;
Engine.GetGUIObjectByName("civIconOverlay").tooltip = sprintf(translate("%(civ)s - Structure Tree"), {"civ": civName});
}
// Hide stuff gaia/observers don't use.
Engine.GetGUIObjectByName("food").hidden = !isPlayer;
Engine.GetGUIObjectByName("wood").hidden = !isPlayer;
Engine.GetGUIObjectByName("stone").hidden = !isPlayer;
Engine.GetGUIObjectByName("metal").hidden = !isPlayer;
Engine.GetGUIObjectByName("population").hidden = !isPlayer;
Engine.GetGUIObjectByName("civIcon").hidden = !isPlayer;
Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
Engine.GetGUIObjectByName("observerText").hidden = playerID >= 0;
// Disable stuff observers shouldn't use
var isActive = isPlayer && GetSimState().players[playerID].state == "active";
Engine.GetGUIObjectByName("pauseButton").enabled = isActive || !g_IsNetworked;
Engine.GetGUIObjectByName("menuResignButton").enabled = isActive;
}
function reportPerformance(time)
{
var settings = Engine.GetMapSettings();
var data = {
time: time,
map: settings.Name,
seed: settings.Seed, // only defined for random maps
size: settings.Size, // only defined for random maps
profiler: Engine.GetProfilerState()
};
Engine.SubmitUserReport("profile", 3, JSON.stringify(data));
}
/**
* Resign a player.
* @param leaveGameAfterResign If player is quitting after resignation.
*/
function resignGame(leaveGameAfterResign)
{
var simState = GetSimState();
// Players can't resign if they've already won or lost.
if (simState.players[Engine.GetPlayerID()].state != "active" || g_Disconnected)
return;
// Tell other players that we have given up and been defeated
Engine.PostNetworkCommand({
"type": "defeat-player",
"playerId": Engine.GetPlayerID()
});
updateTopPanel();
global.music.setState(global.music.states.DEFEAT);
// Resume the game if not resigning.
if (!leaveGameAfterResign)
resumeGame();
}
/**
* Leave the game
* @param willRejoin If player is going to be rejoining a networked game.
*/
function leaveGame(willRejoin)
{
var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
var mapSettings = Engine.GetMapSettings();
var gameResult;
if (g_IsObserver)
{
// Observers don't win/lose.
gameResult = translate("You have left the game.");
global.music.setState(global.music.states.VICTORY);
}
else
{
var playerState = extendedSimState.players[Engine.GetPlayerID()];
if (g_Disconnected)
gameResult = translate("You have been disconnected.");
else if (playerState.state == "won")
gameResult = translate("You have won the battle!");
else if (playerState.state == "defeated")
gameResult = translate("You have been defeated...");
else // "active"
{
global.music.setState(global.music.states.DEFEAT);
if (willRejoin)
gameResult = translate("You have left the game.");
else
{
gameResult = translate("You have abandoned the game.");
resignGame(true);
}
}
}
+ let summary = {
+ "timeElapsed" : extendedSimState.timeElapsed,
+ "playerStates": extendedSimState.players,
+ "players": g_Players,
+ "mapSettings": Engine.GetMapSettings(),
+ }
+
+ if (!g_IsReplay)
+ Engine.SaveReplayMetadata(JSON.stringify(summary));
+
stopAmbient();
Engine.EndGame();
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
- Engine.SwitchGuiPage("page_summary.xml", {
- "gameResult" : gameResult,
- "timeElapsed" : extendedSimState.timeElapsed,
- "playerStates": extendedSimState.players,
- "players": g_Players,
- "mapSettings": mapSettings
- });
+ summary.gameResult = gameResult;
+ summary.isReplay = g_IsReplay;
+ Engine.SwitchGuiPage("page_summary.xml", summary);
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return { selection: g_Selection.selected };
}
// Return some data that will be stored in saved game files
function getSavedGameData()
{
var data = {};
data.playerAssignments = g_PlayerAssignments;
data.groups = g_Groups.groups;
// TODO: any other gui state?
return data;
}
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 (var groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
var lastTickTime = new Date();
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
var now = new Date();
var tickLength = new Date() - lastTickTime;
lastTickTime = now;
checkPlayerState();
while (true)
{
var message = Engine.PollNetworkClient();
if (!message)
break;
handleNetMessage(message);
}
updateCursorAndTooltip();
// If the selection changed, we need to regenerate the sim display (the display depends on both the
// simulation state and the current selection).
if (g_Selection.dirty)
{
g_Selection.dirty = false;
onSimulationUpdate();
// Display rally points for selected buildings
if (!g_IsObserver)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
// Run timers
updateTimers();
// Animate menu
updateMenuPosition(tickLength);
// When training is blocked, flash population (alternates color every 500msec)
if (g_IsTrainingBlocked && (Date.now() % 1000) < 500)
Engine.GetGUIObjectByName("resourcePop").textcolor = POPULATION_ALERT_COLOR;
else
Engine.GetGUIObjectByName("resourcePop").textcolor = DEFAULT_POPULATION_COLOR;
// Clear renamed entities list
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function checkPlayerState()
{
// Once the game ends, we're done here.
if (g_GameEnded || g_IsObserver)
return;
// Send a game report for each player in this game.
var m_simState = GetSimState();
var playerState = m_simState.players[Engine.GetPlayerID()];
var tempStates = "";
for each (var player in m_simState.players) {tempStates += player.state + ",";}
if (g_CachedLastStates != tempStates)
{
g_CachedLastStates = tempStates;
reportGame(Engine.GuiInterfaceCall("GetExtendedSimulationState"));
}
// If the local player hasn't finished playing, we return here to avoid the victory/defeat messages.
if (playerState.state == "active")
return;
// Disable resign and pause buttons (we can't resign once the game is over)
updateTopPanel();
// Make sure nothing is open to avoid stacking.
closeMenu();
closeOpenDialogs();
// Make sure this doesn't run again.
g_GameEnded = true;
if (Engine.IsAtlasRunning())
{
// If we're in Atlas, we can't leave the game
var btCaptions = [translate("OK")];
var btCode = [null];
var message = translate("Press OK to continue");
}
else
{
var btCaptions = [translate("No"), translate("Yes")];
var btCode = [null, leaveGame];
var message = translate("Do you want to quit?");
}
if (playerState.state == "defeated")
{
global.music.setState(global.music.states.DEFEAT);
messageBox(400, 200, message, translate("DEFEATED!"), 0, btCaptions, btCode);
}
else if (playerState.state == "won")
{
global.music.setState(global.music.states.VICTORY);
// TODO: Reveal map directly instead of this silly proxy.
if (!Engine.GetGUIObjectByName("devCommandsRevealMap").checked)
Engine.GetGUIObjectByName("devCommandsRevealMap").checked = true;
messageBox(400, 200, message, translate("VICTORIOUS!"), 0, btCaptions, btCode);
}
}
function changeGameSpeed(speed)
{
// For non-networked games only
if (!g_IsNetworked)
Engine.SetSimRate(speed);
}
/**
* Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
* update (see session.xml), or from onTick when the selection has changed.
*/
function onSimulationUpdate()
{
g_EntityStates = {};
g_TemplateData = {};
g_TechnologyData = {};
g_SimState = Engine.GuiInterfaceCall("GetSimulationState");
// If we're called during init when the game is first loading, there will be no simulation yet, so do nothing
if (!g_SimState)
return;
handleNotifications();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateHero();
updateGroups();
updateDebug();
updatePlayerDisplay();
updateSelectionDetails();
updateBuildingPlacementPreview();
updateTimeNotifications();
if (!g_IsObserver)
updateResearchDisplay();
if (!g_IsObserver && !g_GameEnded)
{
// Update music state on basis of battle state.
var battleState = Engine.GuiInterfaceCall("GetBattleState", Engine.GetPlayerID());
if (battleState)
global.music.setState(global.music.states[battleState]);
}
}
function onReplayFinished()
{
closeMenu();
closeOpenDialogs();
pauseGame();
var btCaptions = [translateWithContext("replayFinished", "Yes"), translateWithContext("replayFinished", "No")];
var btCode = [leaveGame, resumeGame];
messageBox(400, 200, translateWithContext("replayFinished", "The replay has finished. Do you want to quit?"), translateWithContext("replayFinished","Confirmation"), 0, btCaptions, btCode);
}
/**
* 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
var statusBar = Engine.GetGUIObjectByName(nameOfBar);
if (!statusBar)
return;
var healthSize = statusBar.size;
var 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;
// update bar
statusBar.size = healthSize;
}
function updateHero()
{
var playerState = GetSimState().players[Engine.GetPlayerID()];
var unitHeroPanel = Engine.GetGUIObjectByName("unitHeroPanel");
var heroButton = Engine.GetGUIObjectByName("unitHeroButton");
if (!playerState || playerState.heroes.length <= 0)
{
g_previousHeroHitPoints = undefined;
unitHeroPanel.hidden = true;
return;
}
var heroImage = Engine.GetGUIObjectByName("unitHeroImage");
var heroState = GetExtendedEntityState(playerState.heroes[0]);
var template = GetTemplateData(heroState.template);
heroImage.sprite = "stretched:session/portraits/" + template.icon;
var hero = playerState.heroes[0];
heroButton.onpress = function()
{
if (!Engine.HotkeyIsPressed("selection.add"))
g_Selection.reset();
g_Selection.addList([hero]);
};
heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); };
unitHeroPanel.hidden = false;
// Setup tooltip
var tooltip = "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
var healthLabel = "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]";
tooltip += "\n" + sprintf(translate("%(label)s %(current)s / %(max)s"), { label: healthLabel, current: heroState.hitpoints, max: heroState.maxHitpoints });
if (heroState.attack)
tooltip += "\n" + getAttackTooltip(heroState);
tooltip += "\n" + getArmorTooltip(heroState.armour);
if (template.tooltip)
tooltip += "\n" + template.tooltip;
heroButton.tooltip = tooltip;
// update heros health bar
updateGUIStatusBar("heroHealthBar", heroState.hitpoints, heroState.maxHitpoints);
// define the hit points if not defined
if (!g_previousHeroHitPoints)
g_previousHeroHitPoints = heroState.hitpoints;
// if the health of the hero changed since the last update, trigger the animation
if (heroState.hitpoints < g_previousHeroHitPoints)
startColorFade("heroHitOverlay", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);
g_previousHeroHitPoints = heroState.hitpoints;
}
function updateGroups()
{
var guiName = "Group";
g_Groups.update();
for (var i = 0; i < 10; i++)
{
var button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
var label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i;
if (g_Groups.groups[i].getTotalCount() == 0)
button.hidden = true;
else
button.hidden = false;
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);
setPanelObjectPosition(button, i, 1);
}
}
function updateDebug()
{
let debug = Engine.GetGUIObjectByName("debug");
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 updatePlayerDisplay()
{
var playerState = GetSimState().players[Engine.GetPlayerID()];
if (!playerState)
return;
Engine.GetGUIObjectByName("resourceFood").caption = Math.floor(playerState.resourceCounts.food);
Engine.GetGUIObjectByName("resourceWood").caption = Math.floor(playerState.resourceCounts.wood);
Engine.GetGUIObjectByName("resourceStone").caption = Math.floor(playerState.resourceCounts.stone);
Engine.GetGUIObjectByName("resourceMetal").caption = Math.floor(playerState.resourceCounts.metal);
Engine.GetGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit;
Engine.GetGUIObjectByName("population").tooltip = translate("Population (current / limit)") + "\n" +
sprintf(translate("Maximum population: %(popCap)s"), { "popCap": playerState.popMax });
g_IsTrainingBlocked = playerState.trainingBlocked;
}
function selectAndMoveTo(ent)
{
var entState = GetEntityState(ent);
if (!entState || !entState.position)
return;
g_Selection.reset();
g_Selection.addList([ent]);
var position = entState.position;
Engine.CameraMoveTo(position.x, position.z);
}
function updateResearchDisplay()
{
var researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", Engine.GetPlayerID());
if (!researchStarted)
return;
// Set up initial positioning.
var buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
for (var i = 0; i < 10; ++i)
{
var button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
var size = button.size;
size.top = g_ResearchListTop + (4 + buttonSideLength) * i;
size.bottom = size.top + buttonSideLength;
button.size = size;
}
var numButtons = 0;
for (var tech in researchStarted)
{
// Show at most 10 in-progress techs.
if (numButtons >= 10)
break;
var template = GetTechnologyData(tech);
var button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
button.hidden = false;
button.tooltip = getEntityNames(template);
button.onpress = (function(e) { return function() { selectAndMoveTo(e); } })(researchStarted[tech].researcher);
var icon = "stretched:session/portraits/" + template.icon;
Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;
// Scale the progress indicator.
var 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 (var i = numButtons; i < 10; ++i)
Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}
// Toggles the display of status bars for all of the player's entities.
function recalculateStatusBarDisplay()
{
if (g_ShowAllStatusBars)
var entities = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID());
else
{
var selected = g_Selection.toList();
for each (var ent in g_Selection.highlighted)
selected.push(ent);
// Remove selected entities from the 'all entities' array, to avoid disabling their status bars.
var entities = Engine.GuiInterfaceCall("GetPlayerEntities").filter(
function(idx) { return (selected.indexOf(idx) == -1); }
);
}
Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
var entsAdd = []; // list of entities units to be highlighted
var entsRemove = [];
var highlighted = g_Selection.toList();
for each (var ent in g_Selection.highlighted)
highlighted.push(ent);
if (g_ShowGuarding)
{
// flag the guarding entities to add in this additional highlight
for each (var sel in g_Selection.selected)
{
var state = GetEntityState(sel);
if (!state.guard || !state.guard.entities.length)
continue;
for each (var ent in 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 each (var sel in g_Selection.selected)
{
var state = GetEntityState(sel);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
var 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 each (var ent in g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd , HIGHLIGHTED_ALPHA, true );
_setHighlight(entsRemove, 0 , false);
g_AdditionalHighlight = entsAdd;
}
// Temporarily adding this here
const AMBIENT_TEMPERATE = "temperate";
var currentAmbient;
function playRandomAmbient(type)
{
switch (type)
{
case AMBIENT_TEMPERATE:
const AMBIENT = "audio/ambient/dayscape/day_temperate_gen_03.ogg";
Engine.PlayAmbientSound(AMBIENT, true);
break;
default:
error("Unrecognized ambient type: '" + type + "'");
break;
}
}
// Temporarily adding this here
function stopAmbient()
{
if (currentAmbient)
{
currentAmbient.free();
currentAmbient = null;
}
}
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"), 2);
}
// Send a report on the game status to the lobby
function reportGame(extendedSimState)
{
if (!Engine.HasXmppClient() || !Engine.IsRankedGame())
return;
// units
var unitsClasses = [
"total",
"Infantry",
"Worker",
"Female",
"Cavalry",
"Champion",
"Hero",
"Ship",
"Trader"
];
var unitsCountersTypes = [
"unitsTrained",
"unitsLost",
"enemyUnitsKilled"
];
// buildings
var buildingsClasses = [
"total",
"CivCentre",
"House",
"Economic",
"Outpost",
"Military",
"Fortress",
"Wonder"
];
var buildingsCountersTypes = [
"buildingsConstructed",
"buildingsLost",
"enemyBuildingsDestroyed"
];
// resources
var resourcesTypes = [
"wood",
"food",
"stone",
"metal"
];
var resourcesCounterTypes = [
"resourcesGathered",
"resourcesUsed",
"resourcesSold",
"resourcesBought"
];
var playerStatistics = { };
// Unit Stats
for each (var unitCounterType in unitsCountersTypes)
{
if (!playerStatistics[unitCounterType])
playerStatistics[unitCounterType] = { };
for each (var unitsClass in unitsClasses)
playerStatistics[unitCounterType][unitsClass] = "";
}
playerStatistics.unitsLostValue = "";
playerStatistics.unitsKilledValue = "";
// Building stats
for each (var buildingCounterType in buildingsCountersTypes)
{
if (!playerStatistics[buildingCounterType])
playerStatistics[buildingCounterType] = { };
for each (var buildingsClass in buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] = "";
}
playerStatistics.buildingsLostValue = "";
playerStatistics.enemyBuildingsDestroyedValue = "";
// Resources
for each (var resourcesCounterType in resourcesCounterTypes)
{
if (!playerStatistics[resourcesCounterType])
playerStatistics[resourcesCounterType] = { };
for each (var resourcesType in resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] = "";
}
playerStatistics.resourcesGathered.vegetarianFood = "";
playerStatistics.tradeIncome = "";
// Tribute
playerStatistics.tributesSent = "";
playerStatistics.tributesReceived = "";
// Total
playerStatistics.economyScore = "";
playerStatistics.militaryScore = "";
playerStatistics.totalScore = "";
// Various
playerStatistics.treasuresCollected = "";
playerStatistics.lootCollected = "";
playerStatistics.feminisation = "";
playerStatistics.percentMapExplored = "";
var mapName = Engine.GetMapSettings().Name;
var playerStates = "";
var playerCivs = "";
var teams = "";
var 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];
playerStates += player.state + ",";
playerCivs += player.civ + ",";
teams += player.team + ",";
teamsLocked = teamsLocked && player.teamsLocked;
for each (var resourcesCounterType in resourcesCounterTypes)
for each (var resourcesType in resourcesTypes)
playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ",";
playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ",";
for each (var unitCounterType in unitsCountersTypes)
for each (var unitsClass in unitsClasses)
playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ",";
for each (var buildingCounterType in buildingsCountersTypes)
for each (var buildingsClass in buildingsClasses)
playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ",";
var total = 0;
for each (var res in player.statistics.resourcesGathered)
total += res;
playerStatistics.economyScore += total + ",";
playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10) + ",";
playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue +
player.statistics.enemyBuildingsDestroyedValue) / 10)) + ",";
playerStatistics.tradeIncome += player.statistics.tradeIncome + ",";
playerStatistics.tributesSent += player.statistics.tributesSent + ",";
playerStatistics.tributesReceived += player.statistics.tributesReceived + ",";
playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ",";
playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ",";
playerStatistics.lootCollected += player.statistics.lootCollected + ",";
}
// Send the report with serialized data
var reportObject = { };
reportObject.timeElapsed = extendedSimState.timeElapsed;
reportObject.playerStates = playerStates;
reportObject.playerID = Engine.GetPlayerID();
reportObject.matchID = g_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 each (var rct in resourcesCounterTypes)
{
for each (var rt in resourcesTypes)
reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt];
// eg. rt = food rct.substr = Gathered rct = resourcesGathered
}
reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
for each (var type in 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 each (var type in 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];
}
reportObject.tributesSent = playerStatistics.tributesSent;
reportObject.tributesReceived = playerStatistics.tributesReceived;
reportObject.percentMapExplored = playerStatistics.percentMapExplored;
reportObject.treasuresCollected = playerStatistics.treasuresCollected;
reportObject.lootCollected = playerStatistics.lootCollected;
reportObject.tradeIncome = playerStatistics.tradeIncome;
Engine.SendGameReport(reportObject);
}
Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/summary/summary.xml (revision 17053)
+++ ps/trunk/binaries/data/mods/public/gui/summary/summary.xml (revision 17054)
@@ -1,174 +1,178 @@
SummaryselectPanel(0);ScoreselectPanel(1);BuildingsselectPanel(2);UnitsselectPanel(3);ResourcesselectPanel(4);MarketselectPanel(5);MiscellaneousPlayer nameContinue
Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp
===================================================================
--- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 17053)
+++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 17054)
@@ -1,1037 +1,1039 @@
/* Copyright (C) 2015 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 "scriptinterface/ScriptInterface.h"
#include "graphics/Camera.h"
#include "graphics/GameView.h"
#include "graphics/MapReader.h"
#include "graphics/scripting/JSInterface_GameView.h"
#include "gui/GUI.h"
#include "gui/GUIManager.h"
#include "gui/IGUIObject.h"
#include "gui/scripting/JSInterface_GUITypes.h"
#include "i18n/L10n.h"
#include "i18n/scripting/JSInterface_L10n.h"
#include "lib/svn_revision.h"
#include "lib/sysdep/sysdep.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lobby/scripting/JSInterface_Lobby.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetTurnManager.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Errors.h"
#include "ps/GUID.h"
#include "ps/Game.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h" // g_frequencyFilter
#include "ps/Hotkey.h"
#include "ps/ProfileViewer.h"
#include "ps/Pyrogenesis.h"
#include "ps/SavedGame.h"
#include "ps/UserReport.h"
#include "ps/World.h"
#include "ps/scripting/JSInterface_ConfigDB.h"
#include "ps/scripting/JSInterface_Console.h"
#include "ps/scripting/JSInterface_Mod.h"
#include "ps/scripting/JSInterface_VFS.h"
+#include "ps/scripting/JSInterface_VisualReplay.h"
#include "renderer/scripting/JSInterface_Renderer.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpAIManager.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpGuiInterface.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/helpers/Selection.h"
#include "soundmanager/SoundManager.h"
#include "soundmanager/scripting/JSInterface_Sound.h"
#include "tools/atlas/GameInterface/GameLoop.h"
/*
* This file defines a set of functions that are available to GUI scripts, to allow
* interaction with the rest of the engine.
* Functions are exposed to scripts within the global object 'Engine', so
* scripts should call "Engine.FunctionName(...)" etc.
*/
extern void restart_mainloop_in_atlas(); // from main.cpp
extern void EndGame();
extern void kill_mainloop();
namespace {
// Note that the initData argument may only contain clonable data.
// Functions aren't supported for example!
// TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning.
void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, std::wstring name, JS::HandleValue initData)
{
g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData));
}
void SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, std::wstring name, JS::HandleValue initData)
{
g_GUI->SwitchPage(name, pCxPrivate->pScriptInterface, initData);
}
void PopGuiPage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_GUI->PopPage();
}
// Note that the args argument may only contain clonable data.
// Functions aren't supported for example!
// TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning.
void PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
{
g_GUI->PopPageCB(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
}
JS::Value GuiInterfaceCall(ScriptInterface::CxPrivate* pCxPrivate, std::wstring name, JS::HandleValue data)
{
if (!g_Game)
return JS::UndefinedValue();
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY);
if (!cmpGuiInterface)
return JS::UndefinedValue();
int player = g_Game->GetPlayerID();
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue arg(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), data));
JS::RootedValue ret(cxSim);
cmpGuiInterface->ScriptCall(player, name, arg, &ret);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(sim->GetScriptInterface(), ret);
}
void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue cmd)
{
if (!g_Game)
return;
CSimulation2* sim = g_Game->GetSimulation2();
ENSURE(sim);
CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY);
if (!cmpCommandQueue)
return;
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue cmd2(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), cmd));
cmpCommandQueue->PostNetworkCommand(cmd2);
}
entity_id_t PickEntityAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y)
{
return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false);
}
std::vector PickFriendlyEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player)
{
return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false);
}
std::vector PickFriendlyEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate, int player)
{
return PickFriendlyEntitiesInRect(pCxPrivate, 0, 0, g_xres, g_yres, player);
}
std::vector PickSimilarFriendlyEntities(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string templateName, bool includeOffScreen, bool matchRank, bool allowFoundations)
{
return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations);
}
CFixedVector3D GetTerrainAtScreenPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y)
{
CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true);
return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z));
}
std::wstring SetCursor(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name)
{
std::wstring old = g_CursorName;
g_CursorName = name;
return old;
}
bool IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game ? g_Game->IsVisualReplay() : false;
}
int GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game)
return g_Game->GetPlayerID();
return -1;
}
void SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id)
{
if (g_Game)
g_Game->SetPlayerID(id);
}
JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate)
{
return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface));
}
void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetServer);
g_NetServer->StartGame();
}
void StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID)
{
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
g_Game = new CGame();
// Convert from GUI script context to sim script context
CSimulation2* sim = g_Game->GetSimulation2();
JSContext* cxSim = sim->GetScriptInterface().GetContext();
JSAutoRequest rqSim(cxSim);
JS::RootedValue gameAttribs(cxSim,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs));
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameAttribs, "");
}
JS::Value StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, std::wstring name)
{
// We need to be careful with different compartments and contexts.
// The GUI calls this function from the GUI context and expects the return value in the same context.
// The game we start from here creates another context and expects data in this context.
JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cxGui);
ENSURE(!g_NetServer);
ENSURE(!g_NetClient);
ENSURE(!g_Game);
// Load the saved game data from disk
JS::RootedValue guiContextMetadata(cxGui);
std::string savedState;
Status err = SavedGames::Load(name, *(pCxPrivate->pScriptInterface), &guiContextMetadata, savedState);
if (err < 0)
return JS::UndefinedValue();
g_Game = new CGame();
{
CSimulation2* sim = g_Game->GetSimulation2();
JSContext* cxGame = sim->GetScriptInterface().GetContext();
JSAutoRequest rq(cxGame);
JS::RootedValue gameContextMetadata(cxGame,
sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata));
JS::RootedValue gameInitAttributes(cxGame);
sim->GetScriptInterface().GetProperty(gameContextMetadata, "initAttributes", &gameInitAttributes);
int playerID;
sim->GetScriptInterface().GetProperty(gameContextMetadata, "player", playerID);
// Start the game
g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameInitAttributes, savedState);
}
return guiContextMetadata;
}
void SaveGame(ScriptInterface::CxPrivate* pCxPrivate, std::wstring filename, std::wstring description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
LOGERROR("Failed to save game");
}
void SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, std::wstring prefix, std::wstring description, JS::HandleValue GUIMetadata)
{
shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
LOGERROR("Failed to save game");
}
void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
{
ENSURE(g_NetServer);
//TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere
// (with no obvious reason).
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue attribs(cx, attribs1);
g_NetServer->UpdateGameAttributes(&attribs, *(pCxPrivate->pScriptInterface));
}
void StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, std::wstring playerName)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_NetServer = new CNetServer();
if (!g_NetServer->SetupConnection())
{
pCxPrivate->pScriptInterface->ReportError("Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection("127.0.0.1"))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, std::wstring playerName, std::string serverAddress)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
if (!g_NetClient->SetupConnection(serverAddress))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void DisconnectNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
JS::Value PollNetworkClient(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_NetClient)
return JS::UndefinedValue();
// Convert from net client context to GUI script context
JSContext* cxNet = g_NetClient->GetScriptInterface().GetContext();
JSAutoRequest rqNet(cxNet);
JS::RootedValue pollNet(cxNet);
g_NetClient->GuiPoll(&pollNet);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), pollNet);
}
void AssignNetworkPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int playerID, std::string guid)
{
ENSURE(g_NetServer);
g_NetServer->AssignPlayer(playerID, guid);
}
void SetNetworkPlayerStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string guid, int ready)
{
ENSURE(g_NetServer);
g_NetServer->SetPlayerReady(guid, ready);
}
void ClearAllPlayerReady (ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetServer);
g_NetServer->ClearAllPlayerReady();
}
void SendNetworkChat(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
void SendNetworkReady(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int message)
{
ENSURE(g_NetClient);
g_NetClient->SendReadyMessage(message);
}
void SendNetworkRejoined(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
g_NetClient->SendRejoinedMessage();
}
JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate)
{
return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface));
}
JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
{
return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
}
bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name)
{
return SavedGames::DeleteSavedGame(name);
}
void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url)
{
sys_open_url(url);
}
std::wstring GetMatchID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ps_generate_guid().FromUTF8();
}
void RestartInAtlas(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
restart_mainloop_in_atlas();
}
bool AtlasIsAvailable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return ATLAS_IsAvailable();
}
bool IsAtlasRunning(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return (g_AtlasGameLoop && g_AtlasGameLoop->running);
}
JS::Value LoadMapSettings(ScriptInterface::CxPrivate* pCxPrivate, VfsPath pathname)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
CMapSummaryReader reader;
if (reader.LoadMap(pathname) != PSRETURN_OK)
return JS::UndefinedValue();
JS::RootedValue settings(cx);
reader.GetMapSettings(*(pCxPrivate->pScriptInterface), &settings);
return settings;
}
JS::Value GetMapSettings(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_Game)
return JS::UndefinedValue();
JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue mapSettings(cx);
g_Game->GetSimulation2()->GetMapSettings(&mapSettings);
return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(
g_Game->GetSimulation2()->GetScriptInterface(),
mapSettings);
}
/**
* Get the current X coordinate of the camera.
*/
float CameraGetX(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraX();
return -1;
}
/**
* Get the current Z coordinate of the camera.
*/
float CameraGetZ(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetCameraZ();
return -1;
}
/**
* Start / stop camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollow(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, false);
}
/**
* Start / stop first-person camera following mode
* @param entityid unit id to follow. If zero, stop following mode
*/
void CameraFollowFPS(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid)
{
if (g_Game && g_Game->GetView())
g_Game->GetView()->CameraFollow(entityid, true);
}
/**
* Set the data (position, orientation and zoom) of the camera
*/
void SetCameraData(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom)
{
// called from JS; must not fail
if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain()))
return;
CVector3D Pos = CVector3D(x.ToFloat(), y.ToFloat(), z.ToFloat());
float RotX = rotx.ToFloat();
float RotY = roty.ToFloat();
float Zoom = zoom.ToFloat();
g_Game->GetView()->SetCamera(Pos, RotX, RotY, Zoom);
}
/// Move camera to a 2D location
void CameraMoveTo(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t z)
{
// called from JS; must not fail
if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain()))
return;
CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
CVector3D target;
target.X = x.ToFloat();
target.Z = z.ToFloat();
target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
g_Game->GetView()->MoveCameraTarget(target);
}
entity_id_t GetFollowedEntity(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
if (g_Game && g_Game->GetView())
return g_Game->GetView()->GetFollowedEntity();
return INVALID_ENTITY;
}
bool HotkeyIsPressed_(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string hotkeyName)
{
return HotkeyIsPressed(hotkeyName);
}
void DisplayErrorDialog(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring msg)
{
debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL);
}
JS::Value GetProfilerState(ScriptInterface::CxPrivate* pCxPrivate)
{
return g_ProfileViewer.SaveToJS(*(pCxPrivate->pScriptInterface));
}
bool IsUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_UserReporter.IsReportingEnabled();
}
void SetUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled)
{
g_UserReporter.SetReportingEnabled(enabled);
}
std::string GetUserReportStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_UserReporter.GetStatus();
}
void SubmitUserReport(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string type, int version, std::wstring data)
{
g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data));
}
void SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate)
{
g_Game->SetSimRate(rate);
}
float GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return g_Game->GetSimRate();
}
void SetTurnLength(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR("Only network host can change turn length");
}
// Focus the game camera on a given position.
void SetCameraTarget(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float x, float y, float z)
{
g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z));
}
// Deliberately cause the game to crash.
// Currently implemented via access violation (read of address 0).
// Useful for testing the crashlog/stack trace code.
int Crash(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
debug_printf("Crashing at user's request.\n");
return *(volatile int*)0;
}
void DebugWarn(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
debug_warn(L"Warning at user's request.");
}
// Force a JS garbage collection cycle to take place immediately.
// Writes an indication of how long this took to the console.
void ForceGC(ScriptInterface::CxPrivate* pCxPrivate)
{
double time = timer_Time();
JS_GC(pCxPrivate->pScriptInterface->GetJSRuntime());
time = timer_Time() - time;
g_Console->InsertMessage(fmt::sprintf("Garbage collection completed in: %f", time));
}
void DumpSimState(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
OsPath path = psLogDir()/"sim_dump.txt";
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
g_Game->GetSimulation2()->DumpDebugState(file);
}
void DumpTerrainMipmap(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
VfsPath filename(L"screenshots/terrainmipmap.png");
g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename);
OsPath realPath;
g_VFS->GetRealPath(filename, realPath);
LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8());
}
void EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns)
{
g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns);
}
void RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->RewindTimeWarp();
}
void QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->QuickSave();
}
void QuickLoad(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
g_Game->GetTurnManager()->QuickLoad();
}
void SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled)
{
ICmpSelectable::ms_EnableDebugOverlays = enabled;
}
void Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
EndGame();
}
// Cause the game to exit gracefully.
// params:
// returns:
// notes:
// - Exit happens after the current main loop iteration ends
// (since this only sets a flag telling it to end)
void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
kill_mainloop();
}
// Is the game paused?
bool IsPaused(ScriptInterface::CxPrivate* pCxPrivate)
{
if (!g_Game)
{
JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started");
return false;
}
return g_Game->m_Paused;
}
// Pause/unpause the game
void SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause)
{
if (!g_Game)
{
JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started");
return;
}
g_Game->m_Paused = pause;
#if CONFIG2_AUDIO
if (g_SoundManager)
g_SoundManager->Pause(pause);
#endif
}
// Return the global frames-per-second value.
// params:
// returns: FPS [int]
// notes:
// - This value is recalculated once a frame. We take special care to
// filter it, so it is both accurate and free of jitter.
int GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
int freq = 0;
if (g_frequencyFilter)
freq = g_frequencyFilter->StableFrequency();
return freq;
}
JS::Value GetGUIObjectByName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStr name)
{
IGUIObject* guiObj = g_GUI->FindObjectByName(name);
if (guiObj)
return JS::ObjectValue(*guiObj->GetJSObject());
else
return JS::UndefinedValue();
}
// Return the date/time at which the current executable was compiled.
// params: mode OR an integer specifying
// what to display: -1 for "date time (svn revision)", 0 for date, 1 for time, 2 for svn revision
// returns: string with the requested timestamp info
// notes:
// - Displayed on main menu screen; tells non-programmers which auto-build
// they are running. Could also be determined via .EXE file properties,
// but that's a bit more trouble.
// - To be exact, the date/time returned is when scriptglue.cpp was
// last compiled, but the auto-build does full rebuilds.
// - svn revision is generated by calling svnversion and cached in
// lib/svn_revision.cpp. it is useful to know when attempting to
// reproduce bugs (the main EXE and PDB should be temporarily reverted to
// that revision so that they match user-submitted crashdumps).
std::wstring GetBuildTimestamp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int mode)
{
char buf[200];
if (mode == -1) // Date, time and revision.
{
UDate dateTime = g_L10n.ParseDateTime(__DATE__ " " __TIME__, "MMM d yyyy HH:mm:ss", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::DateTime, SimpleDateFormat::DATE_TIME);
char svnRevision[32];
sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision);
if (strcmp(svnRevision, "custom build") == 0)
{
// Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build.
sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (custom build)").c_str(), dateTimeString.c_str());
}
else
{
// Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build.
sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (%ls)").c_str(), dateTimeString.c_str(), svn_revision);
}
}
else if (mode == 0) // Date.
{
UDate dateTime = g_L10n.ParseDateTime(__DATE__, "MMM d yyyy", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Date, SimpleDateFormat::MEDIUM);
sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str());
}
else if (mode == 1) // Time.
{
UDate dateTime = g_L10n.ParseDateTime(__TIME__, "HH:mm:ss", Locale::getUS());
std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Time, SimpleDateFormat::MEDIUM);
sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str());
}
else if (mode == 2) // Revision.
{
char svnRevision[32];
sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision);
if (strcmp(svnRevision, "custom build") == 0)
{
sprintf_s(buf, ARRAY_SIZE(buf), "%s", g_L10n.Translate("custom build").c_str());
}
else
{
sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision);
}
}
return wstring_from_utf8(buf);
}
JS::Value ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, std::wstring filePath)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue out(cx);
pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out);
return out;
}
void WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, std::wstring filePath, JS::HandleValue val1)
{
JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
JSAutoRequest rq(cx);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(cx, val1);
std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
bool TemplateExists(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string templateName)
{
return g_GUI->TemplateExists(templateName);
}
CParamNode GetTemplate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string templateName)
{
return g_GUI->GetTemplate(templateName);
}
//-----------------------------------------------------------------------------
// Timer
//-----------------------------------------------------------------------------
// Script profiling functions: Begin timing a piece of code with StartJsTimer(num)
// and stop timing with StopJsTimer(num). The results will be printed to stdout
// when the game exits.
static const size_t MAX_JS_TIMERS = 20;
static TimerUnit js_start_times[MAX_JS_TIMERS];
static TimerUnit js_timer_overhead;
static TimerClient js_timer_clients[MAX_JS_TIMERS];
static wchar_t js_timer_descriptions_buf[MAX_JS_TIMERS * 12]; // depends on MAX_JS_TIMERS and format string below
static void InitJsTimers(ScriptInterface& scriptInterface)
{
wchar_t* pos = js_timer_descriptions_buf;
for(size_t i = 0; i < MAX_JS_TIMERS; i++)
{
const wchar_t* description = pos;
pos += swprintf_s(pos, 12, L"js_timer %d", (int)i)+1;
timer_AddClient(&js_timer_clients[i], description);
}
// call several times to get a good approximation of 'hot' performance.
// note: don't use a separate timer slot to warm up and then judge
// overhead from another: that causes worse results (probably some
// caching effects inside JS, but I don't entirely understand why).
std::wstring calibration_script =
L"Engine.StartXTimer(0);\n" \
L"Engine.StopXTimer (0);\n" \
L"\n";
scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script);
// slight hack: call LoadGlobalScript twice because we can't average several
// TimerUnit values because there's no operator/. this way is better anyway
// because it hopefully avoids the one-time JS init overhead.
js_timer_clients[0].sum.SetToZero();
scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script);
js_timer_clients[0].sum.SetToZero();
js_timer_clients[0].num_calls = 0;
}
void StartJsTimer(ScriptInterface::CxPrivate* pCxPrivate, unsigned int slot)
{
ONCE(InitJsTimers(*(pCxPrivate->pScriptInterface)));
if (slot >= MAX_JS_TIMERS)
LOGERROR("Exceeded the maximum number of timer slots for scripts!");
js_start_times[slot].SetFromTimer();
}
void StopJsTimer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int slot)
{
if (slot >= MAX_JS_TIMERS)
LOGERROR("Exceeded the maximum number of timer slots for scripts!");
TimerUnit now;
now.SetFromTimer();
now.Subtract(js_timer_overhead);
BillingPolicy_Default()(&js_timer_clients[slot], js_start_times[slot], now);
js_start_times[slot].SetToZero();
}
} // namespace
void GuiScriptingInit(ScriptInterface& scriptInterface)
{
JSI_IGUIObject::init(scriptInterface);
JSI_GUITypes::init(scriptInterface);
JSI_GameView::RegisterScriptFunctions(scriptInterface);
JSI_Renderer::RegisterScriptFunctions(scriptInterface);
JSI_Console::RegisterScriptFunctions(scriptInterface);
JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
JSI_Mod::RegisterScriptFunctions(scriptInterface);
JSI_Sound::RegisterScriptFunctions(scriptInterface);
JSI_L10n::RegisterScriptFunctions(scriptInterface);
JSI_Lobby::RegisterScriptFunctions(scriptInterface);
+ JSI_VisualReplay::RegisterScriptFunctions(scriptInterface);
// VFS (external)
scriptInterface.RegisterFunction("BuildDirEntList");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("GetFileMTime");
scriptInterface.RegisterFunction("GetFileSize");
scriptInterface.RegisterFunction("ReadFile");
scriptInterface.RegisterFunction("ReadFileLines");
// GUI manager functions:
scriptInterface.RegisterFunction("PushGuiPage");
scriptInterface.RegisterFunction("SwitchGuiPage");
scriptInterface.RegisterFunction("PopGuiPage");
scriptInterface.RegisterFunction("PopGuiPageCB");
scriptInterface.RegisterFunction("GetGUIObjectByName");
// Simulation<->GUI interface functions:
scriptInterface.RegisterFunction("GuiInterfaceCall");
scriptInterface.RegisterFunction("PostNetworkCommand");
// Entity picking
scriptInterface.RegisterFunction("PickEntityAtPoint");
scriptInterface.RegisterFunction, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect");
scriptInterface.RegisterFunction, int, &PickFriendlyEntitiesOnScreen>("PickFriendlyEntitiesOnScreen");
scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities");
scriptInterface.RegisterFunction("GetTerrainAtScreenPoint");
// Network / game setup functions
scriptInterface.RegisterFunction("StartNetworkGame");
scriptInterface.RegisterFunction("StartGame");
scriptInterface.RegisterFunction("EndGame");
scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("PollNetworkClient");
scriptInterface.RegisterFunction("SetNetworkGameAttributes");
scriptInterface.RegisterFunction("AssignNetworkPlayer");
scriptInterface.RegisterFunction("SetNetworkPlayerStatus");
scriptInterface.RegisterFunction("ClearAllPlayerReady");
scriptInterface.RegisterFunction("SendNetworkChat");
scriptInterface.RegisterFunction("SendNetworkReady");
scriptInterface.RegisterFunction("SendNetworkRejoined");
scriptInterface.RegisterFunction("GetAIs");
scriptInterface.RegisterFunction("GetEngineInfo");
// Saved games
scriptInterface.RegisterFunction("StartSavedGame");
scriptInterface.RegisterFunction("GetSavedGames");
scriptInterface.RegisterFunction("DeleteSavedGame");
scriptInterface.RegisterFunction("SaveGame");
scriptInterface.RegisterFunction("SaveGamePrefix");
scriptInterface.RegisterFunction("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
// Misc functions
scriptInterface.RegisterFunction("SetCursor");
scriptInterface.RegisterFunction("IsVisualReplay");
scriptInterface.RegisterFunction("GetPlayerID");
scriptInterface.RegisterFunction("SetPlayerID");
scriptInterface.RegisterFunction("OpenURL");
scriptInterface.RegisterFunction("GetMatchID");
scriptInterface.RegisterFunction("RestartInAtlas");
scriptInterface.RegisterFunction("AtlasIsAvailable");
scriptInterface.RegisterFunction("IsAtlasRunning");
scriptInterface.RegisterFunction("LoadMapSettings");
scriptInterface.RegisterFunction("GetMapSettings");
scriptInterface.RegisterFunction("CameraGetX");
scriptInterface.RegisterFunction("CameraGetZ");
scriptInterface.RegisterFunction("CameraFollow");
scriptInterface.RegisterFunction("CameraFollowFPS");
scriptInterface.RegisterFunction("SetCameraData");
scriptInterface.RegisterFunction("CameraMoveTo");
scriptInterface.RegisterFunction("GetFollowedEntity");
scriptInterface.RegisterFunction("HotkeyIsPressed");
scriptInterface.RegisterFunction("DisplayErrorDialog");
scriptInterface.RegisterFunction("GetProfilerState");
scriptInterface.RegisterFunction("Exit");
scriptInterface.RegisterFunction("IsPaused");
scriptInterface.RegisterFunction("SetPaused");
scriptInterface.RegisterFunction("GetFPS");
scriptInterface.RegisterFunction("GetBuildTimestamp");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
scriptInterface.RegisterFunction("TemplateExists");
scriptInterface.RegisterFunction("GetTemplate");
// User report functions
scriptInterface.RegisterFunction("IsUserReportEnabled");
scriptInterface.RegisterFunction("SetUserReportEnabled");
scriptInterface.RegisterFunction("GetUserReportStatus");
scriptInterface.RegisterFunction("SubmitUserReport");
// Development/debugging functions
scriptInterface.RegisterFunction("StartXTimer");
scriptInterface.RegisterFunction("StopXTimer");
scriptInterface.RegisterFunction("SetSimRate");
scriptInterface.RegisterFunction("GetSimRate");
scriptInterface.RegisterFunction("SetTurnLength");
scriptInterface.RegisterFunction("SetCameraTarget");
scriptInterface.RegisterFunction("Crash");
scriptInterface.RegisterFunction("DebugWarn");
scriptInterface.RegisterFunction("ForceGC");
scriptInterface.RegisterFunction("DumpSimState");
scriptInterface.RegisterFunction("DumpTerrainMipmap");
scriptInterface.RegisterFunction("EnableTimeWarpRecording");
scriptInterface.RegisterFunction("RewindTimeWarp");
scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay");
}
Index: ps/trunk/source/ps/Replay.cpp
===================================================================
--- ps/trunk/source/ps/Replay.cpp (revision 17053)
+++ ps/trunk/source/ps/Replay.cpp (revision 17054)
@@ -1,260 +1,273 @@
/* Copyright (C) 2015 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 "Replay.h"
#include "graphics/TerrainTextureManager.h"
#include "lib/timer.h"
#include "lib/file/file_system.h"
#include "lib/res/h_mgr.h"
#include "lib/tex/tex.h"
#include "ps/Game.h"
#include "ps/Loader.h"
+#include "ps/Mod.h"
#include "ps/Profile.h"
#include "ps/ProfileViewer.h"
+#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "simulation2/Simulation2.h"
#include "simulation2/helpers/SimulationCommand.h"
#include
#include
#include
#if MSC_VERSION
#include
#define getpid _getpid // use the non-deprecated function name
#endif
static std::string Hexify(const std::string& s)
{
std::stringstream str;
str << std::hex;
for (size_t i = 0; i < s.size(); ++i)
str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i];
return str.str();
}
CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) :
m_ScriptInterface(scriptInterface), m_Stream(NULL)
{
}
CReplayLogger::~CReplayLogger()
{
delete m_Stream;
}
void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
{
+ // Add timestamp, since the file-modification-date can change
+ m_ScriptInterface.SetProperty(attribs, "timestamp", std::to_string(std::time(nullptr)));
+
+ // Add engine version and currently loaded mods for sanity checks when replaying
+ m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version));
+ m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded);
+
// Construct the directory name based on the PID, to be relatively unique.
// Append "-1", "-2" etc if we run multiple matches in a single session,
// to avoid accidentally overwriting earlier logs.
-
std::wstringstream name;
name << getpid();
static int run = -1;
if (++run)
name << "-" << run;
- OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";
- CreateDirectories(path.Parent(), 0700);
- m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
+ m_Directory = psLogDir() / L"sim_log" / name.str();
+ CreateDirectories(m_Directory, 0700);
+ m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
*m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
}
void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
*m_Stream << "turn " << n << " " << turnLength << "\n";
for (size_t i = 0; i < commands.size(); ++i)
{
*m_Stream << "cmd " << commands[i].player << " " << m_ScriptInterface.StringifyJSON(&commands[i].data, false) << "\n";
}
*m_Stream << "end\n";
m_Stream->flush();
}
void CReplayLogger::Hash(const std::string& hash, bool quick)
{
if (quick)
*m_Stream << "hash-quick " << Hexify(hash) << "\n";
else
*m_Stream << "hash " << Hexify(hash) << "\n";
}
+OsPath CReplayLogger::GetDirectory() const
+{
+ return m_Directory;
+}
+
////////////////////////////////////////////////////////////////
CReplayPlayer::CReplayPlayer() :
m_Stream(NULL)
{
}
CReplayPlayer::~CReplayPlayer()
{
delete m_Stream;
}
void CReplayPlayer::Load(const std::string& path)
{
ENSURE(!m_Stream);
m_Stream = new std::ifstream(path.c_str());
ENSURE(m_Stream->good());
}
void CReplayPlayer::Replay(bool serializationtest, bool ooslog)
{
ENSURE(m_Stream);
new CProfileViewer;
new CProfileManager;
g_ScriptStatsTable = new CScriptStatsTable;
g_ProfileViewer.AddRootTable(g_ScriptStatsTable);
const int runtimeSize = 384 * 1024 * 1024;
const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024;
g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger);
g_Game = new CGame(true);
if (serializationtest)
g_Game->GetSimulation2()->EnableSerializationTest();
if (ooslog)
g_Game->GetSimulation2()->EnableOOSLog();
// Need some stuff for terrain movement costs:
// (TODO: this ought to be independent of any graphics code)
new CTerrainTextureManager;
g_TexMan.LoadTerrainTextures();
// Initialise h_mgr so it doesn't crash when emitting sounds
h_mgr_init();
std::vector commands;
u32 turn = 0;
u32 turnLength = 0;
{
JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
std::string type;
while ((*m_Stream >> type).good())
{
if (type == "start")
{
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue attribs(cx);
ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs));
g_Game->StartGame(&attribs, "");
// TODO: Non progressive load can fail - need a decent way to handle this
LDR_NonprogressiveLoad();
PSRETURN ret = g_Game->ReallyStartGame();
ENSURE(ret == PSRETURN_OK);
}
else if (type == "turn")
{
*m_Stream >> turn >> turnLength;
debug_printf("Turn %u (%u)...\n", turn, turnLength);
}
else if (type == "cmd")
{
player_id_t player;
*m_Stream >> player;
std::string line;
std::getline(*m_Stream, line);
JS::RootedValue data(cx);
g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data);
commands.emplace_back(SimulationCommand(player, cx, data));
}
else if (type == "hash" || type == "hash-quick")
{
std::string replayHash;
*m_Stream >> replayHash;
bool quick = (type == "hash-quick");
if (turn % 100 == 0)
{
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick);
ENSURE(ok);
std::string hexHash = Hexify(hash);
if (hexHash == replayHash)
debug_printf("hash ok (%s)\n", hexHash.c_str());
else
debug_printf("HASH MISMATCH (%s != %s)\n", hexHash.c_str(), replayHash.c_str());
}
}
else if (type == "end")
{
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
g_Game->GetSimulation2()->Update(turnLength, commands);
commands.clear();
}
g_Profiler.Frame();
if (turn % 20 == 0)
g_ProfileViewer.SaveToFile();
}
else
{
debug_printf("Unrecognised replay token %s\n", type.c_str());
}
}
}
SAFE_DELETE(m_Stream);
g_Profiler2.SaveToFile();
std::string hash;
bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
ENSURE(ok);
debug_printf("# Final state: %s\n", Hexify(hash).c_str());
timer_DisplayClientTotals();
SAFE_DELETE(g_Game);
// Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when
// it's already destructed.
g_ScriptRuntime.reset();
// Clean up
delete &g_TexMan;
delete &g_Profiler;
delete &g_ProfileViewer;
SAFE_DELETE(g_ScriptStatsTable);
}
Index: ps/trunk/source/ps/Replay.h
===================================================================
--- ps/trunk/source/ps/Replay.h (revision 17053)
+++ ps/trunk/source/ps/Replay.h (revision 17054)
@@ -1,98 +1,106 @@
/* Copyright (C) 2015 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/ScriptTypes.h"
struct SimulationCommand;
class ScriptInterface;
/**
* Replay log recorder interface.
* Call its methods at appropriate times during the game.
*/
class IReplayLogger
{
public:
IReplayLogger() { }
virtual ~IReplayLogger() { }
/**
* Started the game with the given game attributes.
*/
virtual void StartGame(JS::MutableHandleValue attribs) = 0;
/**
* Run the given turn with the given collection of player commands.
*/
virtual void Turn(u32 n, u32 turnLength, std::vector& commands) = 0;
/**
* Optional hash of simulation state (for sync checking).
*/
virtual void Hash(const std::string& hash, bool quick) = 0;
+
+ /**
+ * Remember the directory containing the commands.txt file, so that we can save additional files to it.
+ */
+ virtual OsPath GetDirectory() const = 0;
};
/**
* Implementation of IReplayLogger that simply throws away all data.
*/
class CDummyReplayLogger : public IReplayLogger
{
public:
virtual void StartGame(JS::MutableHandleValue UNUSED(attribs)) { }
virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector& UNUSED(commands)) { }
virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { }
+ virtual OsPath GetDirectory() const { return OsPath(); }
};
/**
* Implementation of IReplayLogger that saves data to a file in the logs directory.
*/
class CReplayLogger : public IReplayLogger
{
NONCOPYABLE(CReplayLogger);
public:
CReplayLogger(ScriptInterface& scriptInterface);
~CReplayLogger();
virtual void StartGame(JS::MutableHandleValue attribs);
virtual void Turn(u32 n, u32 turnLength, std::vector& commands);
virtual void Hash(const std::string& hash, bool quick);
+ virtual OsPath GetDirectory() const;
private:
ScriptInterface& m_ScriptInterface;
std::ostream* m_Stream;
+ OsPath m_Directory;
};
/**
* Replay log replayer. Runs the log with no graphics and dumps some info to stdout.
*/
class CReplayPlayer
{
public:
CReplayPlayer();
~CReplayPlayer();
void Load(const std::string& path);
void Replay(bool serializationtest, bool ooslog);
private:
std::istream* m_Stream;
};
#endif // INCLUDED_REPLAY
Index: ps/trunk/source/ps/VisualReplay.cpp
===================================================================
--- ps/trunk/source/ps/VisualReplay.cpp (nonexistent)
+++ ps/trunk/source/ps/VisualReplay.cpp (revision 17054)
@@ -0,0 +1,305 @@
+/* Copyright (C) 2015 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/utf8.h"
+#include "ps/CLogger.h"
+#include "ps/Filesystem.h"
+#include "ps/Game.h"
+#include "ps/Pyrogenesis.h"
+#include "ps/Replay.h"
+#include "scriptinterface/ScriptInterface.h"
+
+/**
+ * Filter too short replays (value in seconds).
+ */
+const u8 minimumReplayDuration = 3;
+
+/**
+ * Allows quick debugging of potential platform-dependent file-reading bugs.
+ */
+const bool debugParser = false;
+
+OsPath VisualReplay::GetDirectoryName()
+{
+ return OsPath(psLogDir() / L"sim_log");
+}
+
+JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
+{
+ TIMER(L"GetReplays");
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ u32 i = 0;
+ DirectoryNames directories;
+ JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
+ if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
+ for (OsPath& directory : directories)
+ {
+ JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
+ if (!replayData.isNull())
+ JS_SetElement(cx, replays, i++, replayData);
+ }
+ return JS::ObjectValue(*replays);
+}
+
+/**
+ * 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. 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 (debugParser)
+ debug_printf("At position %i of %lu after %i lines reads.\n", currentPosition, fileSize, linesRead);
+
+ if (!replayStream->good())
+ {
+ LOGERROR("Read error when determining replay duration at %i of %lu 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)
+{
+ // The directory argument must not be constant, otherwise concatenating will fail
+ const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
+
+ if (debugParser)
+ debug_printf("Opening %s\n", utf8_from_wstring(replayFile.string()).c_str());
+
+ if (!FileExists(replayFile))
+ return JSVAL_NULL;
+
+ // Get file size and modification date
+ CFileInfo fileInfo;
+ GetFileInfo(replayFile, &fileInfo);
+ const u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
+ const u64 fileSize = (u64)fileInfo.Size();
+
+ if (fileSize == 0)
+ return JSVAL_NULL;
+
+ // Open file
+ // TODO: enhancement: support unicode when OsString() is properly implemented for windows
+ const CStr fileName = utf8_from_wstring(replayFile.string());
+ std::ifstream* replayStream = new std::ifstream(fileName.c_str());
+
+ // File must begin with "start"
+ CStr type;
+ if (!(*replayStream >> type).good() || type != "start")
+ {
+ LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", 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, "filemod_timestamp", std::to_string(fileTime));
+ 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(utf8_from_wstring(replayFile.string()).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;
+}
+
+// TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp)
+void VisualReplay::SaveReplayMetadata(const CStrW& data)
+{
+ // TODO: enhancement: use JS::HandleValue similar to SaveGame
+ if (!g_Game)
+ 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 (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
+ stream << utf8_from_wstring(data);
+ stream.close();
+}
+
+JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
+{
+ const OsPath filePath = GetDirectoryName() / directoryName / L"metadata.json";
+
+ JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+ JS::RootedValue metadata(cx);
+
+ if (!FileExists(filePath))
+ return JSVAL_NULL;
+
+ std::ifstream* stream = new std::ifstream(OsString(filePath).c_str());
+ ENSURE(stream->good());
+ CStr line;
+ std::getline(*stream, line);
+ stream->close();
+ delete stream;
+ pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
+
+ return metadata;
+}
Index: ps/trunk/source/ps/VisualReplay.h
===================================================================
--- ps/trunk/source/ps/VisualReplay.h (nonexistent)
+++ ps/trunk/source/ps/VisualReplay.h (revision 17054)
@@ -0,0 +1,78 @@
+/* Copyright (C) 2015 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.
+ * @return OsPath the absolute file path
+ */
+OsPath GetDirectoryName();
+
+/**
+ * Get a list of replays to display in the GUI.
+ *
+ * @param scriptInterface the ScriptInterface in which to create the return data.
+ * @return array of objects containing replay data
+ */
+JS::Value GetReplays(ScriptInterface& scriptInterface);
+
+/**
+ * Parses a commands.txt file and extracts metadata.
+ * Works similarly to CGame::LoadReplayData().
+ */
+JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory);
+
+/**
+ * Permanently deletes the visual replay (including the parent directory)
+ *
+ * @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 the metadata of a replay.
+ */
+JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
+
+/**
+ * Saves the metadata from the session to metadata.json
+ */
+void SaveReplayMetadata(const CStrW& data);
+
+}
+
+#endif