Ticket #3258: t3258_visual_replay_menu_v11_PROFILING.patch
File t3258_visual_replay_menu_v11_PROFILING.patch, 65.2 KB (added by , 9 years ago) |
---|
-
binaries/data/mods/public/gui/common/settings.js
function prepareForDropdown(settingValue 254 254 if (settingValues[index].Default) 255 255 settings.Default = +index; 256 256 } 257 257 return settings; 258 258 } 259 260 /** 261 * Returns title or placeholder. 262 * 263 * @param aiName {string} - for example "petra" 264 */ 265 function translateAIName(aiName) 266 { 267 var description = g_Settings.AIDescriptions.find(ai => ai.id == aiName); 268 return description ? translate(description.data.name) : translate("Unknown"); 269 } 270 271 /** 272 * Returns title or placeholder. 273 * 274 * @param index {Number} - index of AIDifficulties 275 */ 276 function translateAIDifficulty(index) 277 { 278 var difficulty = g_Settings.AIDifficulties[index]; 279 return difficulty ? difficulty.Title : translate("Unknown"); 280 } 281 282 /** 283 * Returns title or placeholder. 284 * 285 * @param mapType {string} - for example "skirmish" 286 */ 287 function translateMapType(mapType) 288 { 289 var type = g_Settings.MapTypes.find(t => t.Name == mapType); 290 return type ? type.Title : translate("Unknown"); 291 } 292 293 /** 294 * Returns title or placeholder. 295 * 296 * @param population {Number} - for example 300 297 */ 298 function translatePopulationCapacity(population) 299 { 300 var popCap = g_Settings.PopulationCapacities.find(p => p.Population == population); 301 return popCap ? popCap.Title : translate("Unknown"); 302 } 303 304 /** 305 * Returns title or placeholder. 306 * 307 * @param gameType {string} - for example "conquest" 308 */ 309 function translateVictoryCondition(gameType) 310 { 311 var vc = g_Settings.VictoryConditions.find(vc => vc.Name == gameType); 312 return vc ? vc.Title : translate("Unknown"); 313 } -
binaries/data/mods/public/gui/page_replaymenu.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 <page> 3 <include>common/modern/setup.xml</include> 4 <include>common/modern/styles.xml</include> 5 <include>common/modern/sprites.xml</include> 6 7 <include>common/setup.xml</include> 8 <include>common/sprite1.xml</include> 9 <include>common/styles.xml</include> 10 <include>common/common_sprites.xml</include> 11 <include>common/common_styles.xml</include> 12 13 <include>replaymenu/styles.xml</include> 14 <include>replaymenu/replay_menu.xml</include> 15 </page> -
binaries/data/mods/public/gui/pregame/mainmenu.xml
344 344 Engine.PushGuiPage("page_locale.xml"); 345 345 ]]> 346 346 </action> 347 347 </object> 348 348 349 <object name="submenuReplayButton" 350 type="button" 351 style="StoneButtonFancy" 352 size="0 64 100% 92" 353 tooltip_style="pgToolTip" 354 > 355 <translatableAttribute id="caption">Replays</translatableAttribute> 356 <translatableAttribute id="tooltip">Playback previous games.</translatableAttribute> 357 <action on="Press"> 358 closeMenu(); 359 Engine.SwitchGuiPage("page_replaymenu.xml"); 360 </action> 361 </object> 362 349 363 <object name="submenuEditorButton" 350 364 style="StoneButtonFancy" 351 365 type="button" 352 size="0 64 100% 92"366 size="0 96 100% 124" 353 367 tooltip_style="pgToolTip" 354 368 > 355 369 <translatableAttribute id="caption">Scenario Editor</translatableAttribute> 356 370 <translatableAttribute id="tooltip">Open the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument "-editor".</translatableAttribute> 357 371 <action on="Press"> … … 360 374 </object> 361 375 362 376 <object name="submenuWelcomeScreenButton" 363 377 style="StoneButtonFancy" 364 378 type="button" 365 size="0 96 100% 124"379 size="0 128 100% 156" 366 380 tooltip_style="pgToolTip" 367 381 > 368 382 <translatableAttribute id="caption">Welcome Screen</translatableAttribute> 369 383 <translatableAttribute id="tooltip">Show the Welcome Screen. Useful if you hid it by mistake.</translatableAttribute> 370 384 <action on="Press"> … … 375 389 </action> 376 390 </object> 377 391 <object name="submenuModSelection" 378 392 style="StoneButtonFancy" 379 393 type="button" 380 size="0 1 28 100% 156"394 size="0 156 100% 188" 381 395 tooltip_style="pgToolTip" 382 396 > 383 397 <translatableAttribute id="caption">Mod Selection</translatableAttribute> 384 398 <translatableAttribute id="tooltip">Select mods to use.</translatableAttribute> 385 399 <action on="Press"> … … 482 496 > 483 497 <translatableAttribute id="caption">Tools & Options</translatableAttribute> 484 498 <translatableAttribute id="tooltip">Game options and scenario design tools.</translatableAttribute> 485 499 <action on="Press"> 486 500 closeMenu(); 487 openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 5);501 openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 6); 488 502 </action> 489 503 </object> 490 504 491 505 <!-- EXIT BUTTON --> 492 506 <object name="menuExitButton" -
binaries/data/mods/public/gui/replaymenu/replay_actions.js
1 /** 2 * Starts the selected visual replay, or shows an error message in case of incompatibility. 3 */ 4 function startReplay() 5 { 6 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 7 if (selected == -1) 8 return; 9 10 var replay = g_ReplaysFiltered[selected]; 11 if (isReplayCompatible(replay)) 12 reallyStartVisualReplay(replay.directory); 13 else 14 displayReplayCompatibilityError(replay); 15 } 16 17 /** 18 * Attempts the visual replay, regardless of the compatibility. 19 * 20 * @param replayDirectory {string} 21 */ 22 function reallyStartVisualReplay(replayDirectory) 23 { 24 // TODO: enhancement: restore filter settings and selected replay when returning from the summary screen. 25 Engine.StartVisualReplay(replayDirectory); 26 Engine.SwitchGuiPage("page_loading.xml", { 27 "attribs": Engine.GetReplayAttributes(replayDirectory), 28 "isNetworked" : false, 29 "playerAssignments": {}, 30 "savedGUIData": "", 31 "isReplay" : true 32 }); 33 } 34 35 /** 36 * Shows an error message stating why the replay is not compatible. 37 * 38 * @param replay {Object} 39 */ 40 function displayReplayCompatibilityError(replay) 41 { 42 var errMsg; 43 if (replayHasSameEngineVersion(replay)) 44 { 45 let gameMods = replay.attribs.mods ? replay.attribs.mods : []; 46 errMsg = translate("You don't have the same mods active as the replay.") + "\n"; 47 errMsg += sprintf(translate("Required: %(mods)s"), { "mods": gameMods.join(", ") }) + "\n"; 48 errMsg += sprintf(translate("Active: %(mods)s"), { "mods": g_EngineInfo.mods.join(", ") }); 49 } 50 else 51 errMsg = translate("This replay is not compatible with your version of the game!"); 52 53 messageBox(500, 200, errMsg, translate("REPLAY INCOMPATIBLE"), 0, [translate("Ok")], [null]); 54 } 55 56 /** 57 * Opens the summary screen of the given replay, if its data was found in that directory. 58 */ 59 function showReplaySummary() 60 { 61 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 62 if (selected == -1) 63 return; 64 65 // Load summary screen data from the selected replay directory 66 var summary = Engine.GetReplayMetadata(g_ReplaysFiltered[selected].directory); 67 68 if (!summary) 69 { 70 messageBox(500, 200, translate("No summary data available."), translate("ERROR"), 0, [translate("Ok")], [null]); 71 return; 72 } 73 74 // Open summary screen 75 summary.isReplay = true; 76 summary.gameResult = translate("Scores at the end of the game."); 77 Engine.SwitchGuiPage("page_summary.xml", summary); 78 } 79 80 /** 81 * Callback. 82 */ 83 function deleteReplayButtonPressed() 84 { 85 if (!Engine.GetGUIObjectByName("deleteReplayButton").enabled) 86 return; 87 88 if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation")) 89 deleteReplayWithoutConfirmation(); 90 else 91 deleteReplay(); 92 } 93 /** 94 * Shows a confirmation dialog and deletes the selected replay from the disk in case. 95 */ 96 function deleteReplay() 97 { 98 // Get selected replay 99 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 100 if (selected == -1) 101 return; 102 103 var replay = g_ReplaysFiltered[selected]; 104 105 // Show confirmation message 106 var btCaptions = [translate("Yes"), translate("No")]; 107 var btCode = [function() { reallyDeleteReplay(replay.directory); }, null]; 108 109 var title = translate("DELETE"); 110 var question = translate("Are you sure to delete this replay permanently?") + "\n" + replay.file; 111 112 messageBox(500, 200, question, title, 0, btCaptions, btCode); 113 } 114 115 /** 116 * Attempts to delete the selected replay from the disk. 117 */ 118 function deleteReplayWithoutConfirmation() 119 { 120 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 121 if (selected > -1) 122 reallyDeleteReplay(g_ReplaysFiltered[selected].directory); 123 } 124 125 /** 126 * Attempts to delete the given replay directory from the disk. 127 * 128 * @param replayDirectory {string} 129 */ 130 function reallyDeleteReplay(replayDirectory) 131 { 132 if (!Engine.DeleteReplay(replayDirectory)) 133 error(sprintf("Could not delete replay '%(id)s'", { "id": replayDirectory })); 134 135 // Refresh replay list 136 init(); 137 } -
binaries/data/mods/public/gui/replaymenu/replay_filters.js
1 /** 2 * Allow to filter replays by duration in 15min / 30min intervals. 3 */ 4 const g_DurationFilterIntervals = [ 5 { "min": -1, "max": -1 }, 6 { "min": -1, "max": 15 }, 7 { "min": 15, "max": 30 }, 8 { "min": 30, "max": 45 }, 9 { "min": 45, "max": 60 }, 10 { "min": 60, "max": 90 }, 11 { "min": 90, "max": 120 }, 12 { "min": 120, "max": -1 } 13 ]; 14 15 /** 16 * Allow to filter by population capacity. 17 */ 18 const g_PopulationCapacities = prepareForDropdown(g_Settings ? g_Settings.PopulationCapacities : undefined); 19 20 /** 21 * Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays 22 * (including its derivatives g_MapSizes, g_MapNames). 23 */ 24 function initFilters() 25 { 26 var time = { 27 "date": 0, 28 "mapnames": 0, 29 "mapsizes": 0, 30 "popcap": 0, 31 "duration": 0 32 }; 33 34 var start = Date.now(); 35 initDateFilter(); 36 time["date"] = Date.now() - start; 37 38 start = Date.now(); 39 initMapNameFilter(); 40 time["mapname"] = Date.now() - start; 41 42 start = Date.now(); 43 initMapSizeFilter(); 44 time["mapsize"] = Date.now() - start; 45 46 start = Date.now(); 47 initPopCapFilter(); 48 time["popcap"] = Date.now() - start; 49 50 start = Date.now(); 51 initDurationFilter(); 52 time["duration"] = Date.now() - start; 53 54 error("initFilters: " + JSON.stringify(time)); 55 } 56 57 /** 58 * Allow to filter by month. Uses g_Replays. 59 */ 60 function initDateFilter() 61 { 62 var months = g_Replays.map(replay => getReplayMonth(replay)); 63 months = months.filter((month, index) => months.indexOf(month) == index).sort(); 64 months.unshift(translateWithContext("datetime", "Any")); 65 66 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 67 dateTimeFilter.list = months; 68 dateTimeFilter.list_data = months; 69 70 if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length) 71 dateTimeFilter.selected = 0; 72 } 73 74 /** 75 * Allow to filter by mapsize. Uses g_MapSizes. 76 */ 77 function initMapSizeFilter() 78 { 79 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 80 mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_mapSizes.shortNames); 81 mapSizeFilter.list_data = [-1].concat(g_mapSizes.tiles); 82 83 if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length) 84 mapSizeFilter.selected = 0; 85 } 86 87 /** 88 * Allow to filter by mapname. Uses g_MapNames. 89 */ 90 function initMapNameFilter() 91 { 92 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 93 mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames); 94 mapNameFilter.list_data = [""].concat(g_MapNames.map(mapName => translate(mapName))); 95 96 if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length) 97 mapNameFilter.selected = 0; 98 } 99 100 /** 101 * Allow to filter by population capacity. 102 */ 103 function initPopCapFilter() 104 { 105 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 106 populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title); 107 populationFilter.list_data = [""].concat(g_PopulationCapacities.Population); 108 109 if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length) 110 populationFilter.selected = 0; 111 } 112 113 /** 114 * Allow to filter by game duration. Uses g_DurationFilterIntervals. 115 */ 116 function initDurationFilter() 117 { 118 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 119 durationFilter.list = g_DurationFilterIntervals.map((interval, index) => { 120 121 if (index == 0) 122 return translateWithContext("duration", "Any"); 123 124 if (index == 1) 125 // Translation: Shorter duration than max minutes. 126 return sprintf(translateWithContext("duration filter", "< %(max)s min"), interval); 127 128 if (index == g_DurationFilterIntervals.length - 1) 129 // Translation: Longer duration than min minutes. 130 return sprintf(translateWithContext("duration filter", "> %(min)s min"), interval); 131 132 // Translation: Duration between min and max minutes. 133 return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval); 134 }); 135 durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index); 136 137 if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length) 138 durationFilter.selected = 0; 139 } 140 141 /** 142 * Initializes g_ReplaysFiltered with replays that are not filtered out and sort it. 143 */ 144 function filterReplays() 145 { 146 const sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column; 147 const sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order; 148 149 g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) => 150 { 151 let cmpA, cmpB; 152 switch (sortKey) 153 { 154 case 'name': 155 cmpA = +a.timestamp; 156 cmpB = +b.timestamp; 157 break; 158 case 'duration': 159 cmpA = +a.duration; 160 cmpB = +b.duration; 161 break; 162 case 'players': 163 cmpA = +a.attribs.settings.PlayerData.length; 164 cmpB = +b.attribs.settings.PlayerData.length; 165 break; 166 case 'mapName': 167 cmpA = getReplayMapName(a); 168 cmpB = getReplayMapName(b); 169 break; 170 case 'mapSize': 171 cmpA = +a.attribs.settings.Size; 172 cmpB = +b.attribs.settings.Size; 173 break; 174 case 'popCapacity': 175 cmpA = +a.attribs.settings.PopulationCap; 176 cmpB = +b.attribs.settings.PopulationCap; 177 break; 178 } 179 180 if (cmpA < cmpB) 181 return -sortOrder; 182 else if (cmpA > cmpB) 183 return +sortOrder; 184 185 return 0; 186 }); 187 } 188 189 /** 190 * Decides whether the replay should be listed. 191 * 192 * @returns {bool} - true if replay should be visible 193 */ 194 function filterReplay(replay) 195 { 196 // Check for compability first (most likely to filter) 197 var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter"); 198 if (compabilityFilter.checked && !isReplayCompatible(replay)) 199 return false; 200 201 // Filter date/time (select a month) 202 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 203 if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected]) 204 return false; 205 206 // Filter by playernames 207 var playersFilter = Engine.GetGUIObjectByName("playersFilter"); 208 var keywords = playersFilter.caption.toLowerCase().split(" "); 209 if (keywords.length) 210 { 211 // We just check if all typed words are somewhere in the playerlist of that replay 212 let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase(); 213 if (!keywords.every(keyword => playerList.indexOf(keyword) != -1)) 214 return false; 215 } 216 217 // Filter by map name 218 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 219 if (mapNameFilter.selected > 0 && getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected]) 220 return false; 221 222 // Filter by map size 223 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 224 if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected]) 225 return false; 226 227 // Filter by population capacity 228 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 229 if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected]) 230 return false; 231 232 // Filter by game duration 233 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 234 if (durationFilter.selected > 0) 235 { 236 let interval = g_DurationFilterIntervals[durationFilter.selected]; 237 238 if ((interval.min > -1 && replay.duration < interval.min * 60) || 239 (interval.max > -1 && replay.duration > interval.max * 60)) 240 return false; 241 } 242 243 return true; 244 } -
binaries/data/mods/public/gui/replaymenu/replay_menu.js
1 const g_EngineInfo = Engine.GetEngineInfo(); 2 const g_CivData = loadCivData(); 3 const g_DefaultPlayerData = initPlayerDefaults(); 4 const g_mapSizes = initMapSizes(); 5 6 /** 7 * All replays found in the directory. 8 */ 9 var g_Replays = []; 10 11 /** 12 * List of replays after applying the display filter. 13 */ 14 var g_ReplaysFiltered = []; 15 16 /** 17 * Array of unique usernames of all replays. Used for autocompleting usernames. 18 */ 19 var g_Playernames = []; 20 21 /** 22 * Sorted list of unique maptitles. Used by mapfilter. 23 */ 24 var g_MapNames = []; 25 26 /** 27 * Directory name of the currently selected replay. Used to restore the selection after changing filters. 28 */ 29 var g_selectedReplayDirectory = ""; 30 31 /** 32 * Initializes globals, loads replays and displays the list. 33 */ 34 function init() 35 { 36 if (!g_Settings) 37 { 38 Engine.SwitchGuiPage("page_pregame.xml"); 39 return; 40 } 41 42 // By default, sort replays by date in descending order 43 Engine.GetGUIObjectByName("replaySelection").selected_column_order = -1; 44 45 var start = Date.now(); 46 var time = {}; 47 loadReplays(); 48 time["loadReplays"] = Date.now() - start; 49 50 start = Date.now(); 51 displayReplayList(); 52 time["displayReplayList"] = Date.now() - start; 53 54 error("init():" + JSON.stringify(time)); 55 } 56 57 /** 58 * Store the list of replays loaded in C++ in g_Replays. 59 * Check timestamp and compatibility and extract g_Playernames, g_MapNames 60 */ 61 function loadReplays() 62 { 63 g_Replays = Engine.GetReplays(); 64 65 var start; 66 var time = { 67 "replay compability check": 0, 68 "sanitize attributes": 0, 69 "parse playernames": 0, 70 "extract maps": 0 71 }; 72 73 // TODO: enhancement: Move logic to C++ 74 g_Playernames = []; 75 for (let replay of g_Replays) 76 { 77 // Use time saved in file, otherwise file mod date 78 replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp; 79 80 // Check replay for compability 81 start = Date.now(); 82 replay.isCompatible = isReplayCompatible(replay); 83 time["replay compability check"] += Date.now() - start; 84 85 start = Date.now(); 86 sanitizeGameAttributes(replay.attribs); 87 time["sanitize attributes"] += Date.now() - start; 88 89 // Extract map names 90 start = Date.now(); 91 if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "") 92 g_MapNames.push(replay.attribs.settings.Name); 93 time["extract maps"] += Date.now() - start; 94 95 start = Date.now(); 96 // Extract playernames 97 for (let playerData of replay.attribs.settings.PlayerData) 98 { 99 if (!playerData || playerData.AI) 100 continue; 101 102 // Remove rating from nick 103 let playername = playerData.Name; 104 let ratingStart = playername.indexOf(" ("); 105 if (ratingStart != -1) 106 playername = playername.substr(0, ratingStart); 107 108 if (g_Playernames.indexOf(playername) == -1) 109 g_Playernames.push(playername); 110 } 111 time["parse playernames"] += Date.now() - start; 112 } 113 114 start = Date.now(); 115 g_MapNames.sort(); 116 time["sort playernames"] = Date.now() - start; 117 118 // Reload filters (since they depend on g_Replays and its derivatives) 119 start = Date.now(); 120 initFilters(); 121 time["reload filters"] = Date.now() - start; 122 123 error("loadReplays: " + JSON.stringify(time)); 124 } 125 126 /** 127 * We may encounter malformed replays. 128 */ 129 function sanitizeGameAttributes(attribs) 130 { 131 if (!attribs.settings) 132 attribs.settings = {}; 133 134 if (!attribs.settings.Size) 135 attribs.settings.Size = -1; 136 137 if (!attribs.settings.Name) 138 attribs.settings.Name = ""; 139 140 if (!attribs.settings.PlayerData) 141 attribs.settings.PlayerData = []; 142 143 if (!attribs.settings.PopulationCap) 144 attribs.settings.PopulationCap = 300; 145 146 if (!attribs.settings.mapType) 147 attribs.settings.mapType = "skirmish"; 148 149 if (!attribs.settings.GameType) 150 attribs.settings.GameType = "conquest"; 151 152 // Remove gaia 153 if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null) 154 attribs.settings.PlayerData.shift(); 155 156 attribs.settings.PlayerData.forEach((pData, index) => { 157 if (!pData.Name) 158 pData.Name = ""; 159 }); 160 } 161 162 /** 163 * Filter g_Replays, fill the GUI list with that data and show the description of the current replay. 164 */ 165 function displayReplayList() 166 { 167 // Remember previously selected replay 168 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 169 if (replaySelection.selected != -1) 170 g_selectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory; 171 172 var time = {}; 173 var start = Date.now(); 174 filterReplays(); 175 time["filter replays"] = Date.now() - start; 176 177 // Create GUI list data 178 start = Date.now(); 179 var list = g_ReplaysFiltered.map(replay => { 180 let works = replay.isCompatible; 181 return { 182 "directories": replay.directory, 183 "months": greyout(getReplayDateTime(replay), works), 184 "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works), 185 "mapNames": greyout(getReplayMapName(replay), works), 186 "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works), 187 "durations": greyout(getReplayDuration(replay), works), 188 "playerNames": greyout(getReplayPlayernames(replay), works) 189 }; 190 }); 191 time["create list data"] = Date.now() - start; 192 193 // Extract arrays 194 start = Date.now(); 195 if (list.length) 196 list = prepareForDropdown(list); 197 time["prepareForDropdown"] = Date.now() - start; 198 199 // Push to GUI 200 replaySelection.selected = -1; 201 replaySelection.list_name = list.months || []; 202 replaySelection.list_players = list.playerNames || []; 203 replaySelection.list_mapName = list.mapNames || []; 204 replaySelection.list_mapSize = list.mapSizes || []; 205 replaySelection.list_popCapacity = list.popCaps || []; 206 replaySelection.list_duration = list.durations || []; 207 208 // Change these last, otherwise crash 209 replaySelection.list = list.directories || []; 210 replaySelection.list_data = list.directories || []; 211 time["push gui data"] = Date.now() - start; 212 213 // Restore selection 214 replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_selectedReplayDirectory); 215 216 start = Date.now(); 217 displayReplayDetails(); 218 time["displayReplayDetails"] = Date.now() - start; 219 220 error("displayReplayList: " + JSON.stringify(time)); 221 } 222 223 /** 224 * Shows preview image, description and player text in the right panel. 225 */ 226 function displayReplayDetails() 227 { 228 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 229 var replaySelected = selected > -1; 230 231 Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; 232 Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; 233 Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; 234 Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; 235 Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected; 236 237 if (!replaySelected) 238 return; 239 240 var replay = g_ReplaysFiltered[selected]; 241 var mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map); 242 243 // Update GUI 244 Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name); 245 Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size); 246 Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType); 247 Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType); 248 Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.attribs.settings.PlayerData.length; 249 Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay); 250 Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description; 251 Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapData.preview; 252 } 253 254 /** 255 * Adds grey font if replay is not compatible. 256 */ 257 function greyout(text, isCompatible) 258 { 259 return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]'; 260 } 261 262 /** 263 * Returns a human-readable version of the replay date. 264 */ 265 function getReplayDateTime(replay) 266 { 267 return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm")) 268 } 269 270 /** 271 * Returns a human-readable list of the playernames of that replay. 272 * 273 * @returns {string} 274 */ 275 function getReplayPlayernames(replay) 276 { 277 // TODO: colorize playernames like in the lobby. 278 return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", "); 279 } 280 281 /** 282 * Returns the name of the map of the given replay. 283 * 284 * @returns {string} 285 */ 286 function getReplayMapName(replay) 287 { 288 return translate(replay.attribs.settings.Name); 289 } 290 291 /** 292 * Returns the month of the given replay in the format "yyyy-MM". 293 * 294 * @returns {string} 295 */ 296 function getReplayMonth(replay) 297 { 298 return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM")); 299 } 300 301 /** 302 * Returns a human-readable version of the time when the replay started. 303 * 304 * @returns {string} 305 */ 306 function getReplayDuration(replay) 307 { 308 return timeToString(replay.duration * 1000); 309 } 310 311 /** 312 * True if we can start the given replay with the currently loaded mods. 313 */ 314 function isReplayCompatible(replay) 315 { 316 return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo); 317 } 318 319 /** 320 * True if we can start the given replay with the currently loaded mods. 321 */ 322 function replayHasSameEngineVersion(replay) 323 { 324 return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version; 325 } 326 327 /** 328 * Returns a description of the player assignments. 329 * Including civs, teams, AI settings and player colors. 330 * 331 * If the spoiler-checkbox is checked, it also shows defeated players. 332 * 333 * @returns {string} 334 */ 335 function getReplayTeamText(replay) 336 { 337 // Load replay metadata 338 const metadata = Engine.GetReplayMetadata(replay.directory); 339 const spoiler = Engine.GetGUIObjectByName("showSpoiler").checked; 340 341 var playerDescriptions = {}; 342 var playerIdx = 0; 343 for (let playerData of replay.attribs.settings.PlayerData) 344 { 345 // Get player info 346 ++playerIdx; 347 let teamIdx = playerData.Team; 348 let playerColor = playerData.Color ? playerData.Color : g_DefaultPlayerData[playerIdx].Color; 349 let showDefeated = spoiler && metadata && metadata.playerStates && metadata.playerStates[playerIdx].state == "defeated"; 350 let isAI = playerData.AI; 351 352 // Create human-readable player description 353 let playerDetails = { 354 "playerName": '[color="' + rgbToGuiColor(playerColor) + '"]' + escapeText(playerData.Name) + "[/color]", 355 "civ": translate(g_CivData[playerData.Civ].Name), 356 "AIname": isAI ? translateAIName(playerData.AI) : "", 357 "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : "" 358 }; 359 360 if (!isAI && !showDefeated) 361 playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s)"), playerDetails); 362 else if (!isAI && showDefeated) 363 playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, defeated)"), playerDetails); 364 else if (isAI && !showDefeated) 365 playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"), playerDetails); 366 else 367 playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, defeated)"), playerDetails); 368 369 // Sort player descriptions by team 370 if (!playerDescriptions[teamIdx]) 371 playerDescriptions[teamIdx] = []; 372 playerDescriptions[teamIdx].push(playerDetails); 373 } 374 375 var teams = Object.keys(playerDescriptions); 376 377 // If there are no teams, merge all playersDescriptions 378 if (teams.length == 1) 379 return playerDescriptions[teams[0]].join("\n") + "\n"; 380 381 // If there are teams, merge "Team N:" + playerDescriptions 382 return teams.map(team => { 383 let teamCaption = (team == -1) ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); 384 return '[font="sans-bold-14"]' + teamCaption + "[/font]:\n" + playerDescriptions[team].join("\n"); 385 }).join("\n"); 386 } -
binaries/data/mods/public/gui/replaymenu/replay_menu.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 3 <objects> 4 5 <!-- Used to display game info. --> 6 <script file="gui/common/functions_civinfo.js" /> 7 <script file="gui/common/functions_utility.js" /> 8 <script file="gui/common/settings.js" /> 9 10 <!-- Used to display message boxes. --> 11 <script file="gui/common/functions_global_object.js" /> 12 13 <!-- Used for engine + mod version checks. --> 14 <script file="gui/common/functions_utility_loadsave.js" /> 15 16 <!-- Actual replay scripts after settings.js, as it initializes g_Settings. --> 17 <script file="gui/replaymenu/replay_menu.js" /> 18 <script file="gui/replaymenu/replay_actions.js" /> 19 <script file="gui/replaymenu/replay_filters.js" /> 20 21 <!-- Everything displayed in the replay menu. --> 22 <object type="image" style="ModernWindow" size="0 0 100% 100%" name="replayWindow"> 23 24 <!-- Title --> 25 <object style="ModernLabelText" type="text" size="50%-128 4 50%+128 36"> 26 <translatableAttribute id="caption">Replay Games</translatableAttribute> 27 </object> 28 29 <!-- Left Panel: Filters & Replay List --> 30 <object name="leftPanel" size="3% 5% 100%-255 100%-80"> 31 32 <!-- Filters --> 33 <object name="filterPanel" size="0 0 100% 24"> 34 35 <object name="dateTimeFilter" type="dropdown" style="ModernDropDown" size="5 0 12%-10 100%" font="sans-bold-13"> 36 <action on="SelectionChange">displayReplayList();</action> 37 </object> 38 39 <object name="playersFilter" type="input" style="ModernInput" size="12%-5 0 56%-10 100%" font="sans-bold-13"> 40 <action on="Press">displayReplayList();</action> 41 <action on="Tab">autoCompleteNick("playersFilter", g_Playernames.map(name => ({ "name": name })));</action> 42 </object> 43 44 <object name="mapNameFilter" type="dropdown" style="ModernDropDown" size="56%-5 0 70%-10 100%" font="sans-bold-13"> 45 <action on="SelectionChange">displayReplayList();</action> 46 </object> 47 48 <object name="mapSizeFilter" type="dropdown" style="ModernDropDown" size="70%-5 0 80%-10 100%" font="sans-bold-13"> 49 <action on="SelectionChange">displayReplayList();</action> 50 </object> 51 52 <object name="populationFilter" type="dropdown" style="ModernDropDown" size="80%-5 0 90%-10 100%" font="sans-bold-13"> 53 <action on="SelectionChange">displayReplayList();</action> 54 </object> 55 56 <object name="durationFilter" type="dropdown" style="ModernDropDown" size="90%-5 0 100%-10 100%" font="sans-bold-13"> 57 <action on="SelectionChange">displayReplayList();</action> 58 </object> 59 60 </object> 61 62 <!-- Replay List in that left panel --> 63 <object name="replaySelection" size="0 35 100% 100%" style="ModernList" type="olist" sortable="true" default_column="name" sprite_asc="ModernArrowDown" sprite_desc="ModernArrowUp" sprite_not_sorted="ModernNotSorted" font="sans-stroke-13"> 64 65 <action on="SelectionChange">displayReplayDetails();</action> 66 <action on="SelectionColumnChange">displayReplayList();</action> 67 68 <!-- Columns --> 69 <!-- We have to call one "name" as the GUI expects one. --> 70 <def id="name" color="172 172 212" width="12%"> 71 <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute> 72 </def> 73 74 <def id="players" color="192 192 192" width="44%"> 75 <translatableAttribute id="heading" context="replay">Players</translatableAttribute> 76 </def> 77 78 <def id="mapName" color="192 192 192" width="14%"> 79 <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute> 80 </def> 81 82 <def id="mapSize" color="192 192 192" width="10%"> 83 <translatableAttribute id="heading" context="replay">Size</translatableAttribute> 84 </def> 85 86 <def id="popCapacity" color="192 192 192" width="10%"> 87 <translatableAttribute id="heading" context="replay">Population</translatableAttribute> 88 </def> 89 90 <def id="duration" color="192 192 192" width="10%"> 91 <translatableAttribute id="heading" context="replay">Duration</translatableAttribute> 92 </def> 93 94 </object> 95 96 </object> 97 98 <!-- Right Panel: Compability Filter & Replay Details --> 99 <object name="rightPanel" size="100%-250 30 100%-20 100%-20" > 100 101 <!-- Compability Filter Checkbox --> 102 <object name="compabilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%" font="sans-bold-13"> 103 <action on="Press">displayReplayList();</action> 104 </object> 105 106 <!-- Compability Filter Label --> 107 <object type="text" size="20 2 100% 100%" text_align="left" textcolor="white"> 108 <translatableAttribute id="caption">Filter compatible replays</translatableAttribute> 109 </object> 110 111 <!-- Placeholder to show if no replay is selected --> 112 <object name="replayInfoEmpty" size="0 30 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false"> 113 <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/> 114 <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%"> 115 <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/> 116 </object> 117 </object> 118 119 <!-- --> 120 <object name="replayInfo" size="0 30 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="true"> 121 122 <!-- Map Name Label --> 123 <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/> 124 125 <!-- Map Preview Image --> 126 <object name="sgMapPreview" size="5 25 100%-5 190" type="image" sprite=""/> 127 128 <!-- Separator Line --> 129 <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/> 130 131 <!-- Map Type Caption --> 132 <object size="5 195 50% 225" type="image" sprite="ModernItemBackShadeLeft"> 133 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 134 <translatableAttribute id="caption">Map Type:</translatableAttribute> 135 </object> 136 </object> 137 138 <!-- Map Type Label --> 139 <object size="50% 195 100%-5 225" type="image" sprite="ModernItemBackShadeRight"> 140 <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 141 </object> 142 143 <!-- Separator Line --> 144 <object size="5 224 100%-5 225" type="image" sprite="ModernWhiteLine" z="25"/> 145 146 <!-- Map Size Caption --> 147 <object size="5 225 50% 255" type="image" sprite="ModernItemBackShadeLeft"> 148 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 149 <translatableAttribute id="caption">Map Size:</translatableAttribute> 150 </object> 151 </object> 152 153 <!-- Map Size Label --> 154 <object size="50% 225 100%-5 255" type="image" sprite="ModernItemBackShadeRight"> 155 <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 156 </object> 157 158 <!-- Separator Line --> 159 <object size="5 254 100%-5 255" type="image" sprite="ModernWhiteLine" z="25"/> 160 161 <!-- Victory Condition Caption --> 162 <object size="5 255 50% 285" type="image" sprite="ModernItemBackShadeLeft"> 163 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 164 <translatableAttribute id="caption">Victory:</translatableAttribute> 165 </object> 166 </object> 167 168 <!-- Victory Condition Label --> 169 <object size="50% 255 100%-5 285" type="image" sprite="ModernItemBackShadeRight"> 170 <object name="sgVictory" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 171 </object> 172 173 <!-- Separator Line --> 174 <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/> 175 176 <!-- Map Description Text --> 177 <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 60%"> 178 <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/> 179 </object> 180 181 <object type="image" sprite="ModernDarkBoxWhite" size="3% 60%+5 97% 100%-30"> 182 183 <!-- Number of Players Caption--> 184 <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText"> 185 <translatableAttribute id="caption">Players:</translatableAttribute> 186 </object> 187 188 <!-- Number of Players Label--> 189 <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/> 190 191 <!-- Player Names --> 192 <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/> 193 </object> 194 195 <!-- "Show Spoiler" Checkbox --> 196 <object name="showSpoiler" type="checkbox" checked="false" style="ModernTickBox" size="10 100%-27 30 100%" font="sans-bold-13"> 197 <action on="Press">displayReplayDetails();</action> 198 </object> 199 200 <!-- "Show Spoiler" Label --> 201 <object type="text" size="30 100%-28 100% 100%" text_align="left" textcolor="white"> 202 <translatableAttribute id="caption">Spoiler</translatableAttribute> 203 </object> 204 205 </object> 206 </object> 207 208 209 <!-- Bottom Panel: Buttons. --> 210 <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" > 211 212 <!-- Main Menu Button --> 213 <object type="button" style="StoneButton" size="25 0 17%+25 100%"> 214 <translatableAttribute id="caption">Main Menu</translatableAttribute> 215 <action on="Press">Engine.SwitchGuiPage("page_pregame.xml");</action> 216 </object> 217 218 <!-- Delete Button --> 219 <object name="deleteReplayButton" type="button" style="StoneButton" size="20%+25 0 37%+25 100%" hotkey="session.savedgames.delete"> 220 <translatableAttribute id="caption">Delete</translatableAttribute> 221 <action on="Press">deleteReplayButtonPressed();</action> 222 </object> 223 224 <!-- Summary Button --> 225 <object name="summaryButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%"> 226 <translatableAttribute id="caption">Summary</translatableAttribute> 227 <action on="Press">showReplaySummary();</action> 228 </object> 229 230 <!-- Start Replay Button --> 231 <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%"> 232 <translatableAttribute id="caption">Start Replay</translatableAttribute> 233 <action on="Press">startReplay();</action> 234 </object> 235 236 </object> 237 </object> 238 </objects> -
binaries/data/mods/public/gui/replaymenu/styles.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 3 <styles> 4 <style name="MapPlayerList" 5 buffer_zone="8" 6 font="sans-14" 7 scrollbar="true" 8 scrollbar_style="ModernScrollBar" 9 scroll_bottom="true" 10 textcolor="white" 11 text_align="left" 12 text_valign="top" 13 /> 14 </styles> -
binaries/data/mods/public/gui/session/session.js
function leaveGame(willRejoin) 335 335 resignGame(true); 336 336 } 337 337 } 338 338 } 339 339 340 let summary = { 341 "timeElapsed" : extendedSimState.timeElapsed, 342 "playerStates": extendedSimState.players, 343 "players": g_Players, 344 "mapSettings": Engine.GetMapSettings(), 345 } 346 347 if (!g_IsReplay) 348 Engine.SaveReplayMetadata(JSON.stringify(summary)); 349 340 350 stopAmbient(); 341 351 Engine.EndGame(); 342 352 343 353 if (g_IsController && Engine.HasXmppClient()) 344 354 Engine.SendUnregisterGame(); 345 355 346 Engine.SwitchGuiPage("page_summary.xml", { 347 "gameResult" : gameResult, 348 "timeElapsed" : extendedSimState.timeElapsed, 349 "playerStates": extendedSimState.players, 350 "players": g_Players, 351 "mapSettings": mapSettings 352 }); 356 summary.gameResult = gameResult; 357 summary.isReplay = g_IsReplay; 358 Engine.SwitchGuiPage("page_summary.xml", summary); 353 359 } 354 360 355 361 // Return some data that we'll use when hotloading this file after changes 356 362 function getHotloadData() 357 363 { -
binaries/data/mods/public/gui/summary/summary.xml
156 156 </object> 157 157 158 158 <object type="button" style="ModernButtonRed" size="100%-160 100%-48 100%-20 100%-20"> 159 159 <translatableAttribute id="caption">Continue</translatableAttribute> 160 160 <action on="Press"><![CDATA[ 161 if (!Engine.HasXmppClient()) 161 if (g_GameData.isReplay) 162 { 163 Engine.SwitchGuiPage("page_replaymenu.xml"); 164 } 165 else if (!Engine.HasXmppClient()) 162 166 { 163 167 Engine.SwitchGuiPage("page_pregame.xml"); 164 168 } 165 169 else 166 170 { -
source/gui/scripting/ScriptFunctions.cpp
54 54 #include "ps/World.h" 55 55 #include "ps/scripting/JSInterface_ConfigDB.h" 56 56 #include "ps/scripting/JSInterface_Console.h" 57 57 #include "ps/scripting/JSInterface_Mod.h" 58 58 #include "ps/scripting/JSInterface_VFS.h" 59 #include "ps/scripting/JSInterface_VisualReplay.h" 59 60 #include "renderer/scripting/JSInterface_Renderer.h" 60 61 #include "simulation2/Simulation2.h" 61 62 #include "simulation2/components/ICmpAIManager.h" 62 63 #include "simulation2/components/ICmpCommandQueue.h" 63 64 #include "simulation2/components/ICmpGuiInterface.h" … … void GuiScriptingInit(ScriptInterface& s 926 927 JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); 927 928 JSI_Mod::RegisterScriptFunctions(scriptInterface); 928 929 JSI_Sound::RegisterScriptFunctions(scriptInterface); 929 930 JSI_L10n::RegisterScriptFunctions(scriptInterface); 930 931 JSI_Lobby::RegisterScriptFunctions(scriptInterface); 932 JSI_VisualReplay::RegisterScriptFunctions(scriptInterface); 931 933 932 934 // VFS (external) 933 935 scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &JSI_VFS::BuildDirEntList>("BuildDirEntList"); 934 936 scriptInterface.RegisterFunction<bool, CStrW, JSI_VFS::FileExists>("FileExists"); 935 937 scriptInterface.RegisterFunction<double, std::wstring, &JSI_VFS::GetFileMTime>("GetFileMTime"); -
source/ps/Replay.cpp
24 24 #include "lib/file/file_system.h" 25 25 #include "lib/res/h_mgr.h" 26 26 #include "lib/tex/tex.h" 27 27 #include "ps/Game.h" 28 28 #include "ps/Loader.h" 29 #include "ps/Mod.h" 29 30 #include "ps/Profile.h" 30 31 #include "ps/ProfileViewer.h" 32 #include "ps/Pyrogenesis.h" 31 33 #include "scriptinterface/ScriptInterface.h" 32 34 #include "scriptinterface/ScriptStats.h" 33 35 #include "simulation2/Simulation2.h" 34 36 #include "simulation2/helpers/SimulationCommand.h" 35 37 … … CReplayLogger::~CReplayLogger() 61 63 delete m_Stream; 62 64 } 63 65 64 66 void CReplayLogger::StartGame(JS::MutableHandleValue attribs) 65 67 { 68 // Add timestamp, since the file-modification-date can change 69 m_ScriptInterface.SetProperty(attribs, "timestamp", (i32) std::time(nullptr)); 70 71 // Add engine version and currently loaded mods for sanity checks when replaying 72 m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version)); 73 m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded); 74 66 75 // Construct the directory name based on the PID, to be relatively unique. 67 76 // Append "-1", "-2" etc if we run multiple matches in a single session, 68 77 // to avoid accidentally overwriting earlier logs. 69 70 78 std::wstringstream name; 71 79 name << getpid(); 72 80 73 81 static int run = -1; 74 82 if (++run) 75 83 name << "-" << run; 76 84 77 OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt"; 78 CreateDirectories(path.Parent(), 0700); 79 m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); 85 m_Directory = psLogDir() / L"sim_log" / name.str(); 86 CreateDirectories(m_Directory, 0700); 80 87 88 m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc); 81 89 *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; 82 90 } 83 91 84 92 void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands) 85 93 { … … void CReplayLogger::Hash(const std::stri 101 109 *m_Stream << "hash-quick " << Hexify(hash) << "\n"; 102 110 else 103 111 *m_Stream << "hash " << Hexify(hash) << "\n"; 104 112 } 105 113 114 OsPath CReplayLogger::GetDirectory() const 115 { 116 return m_Directory; 117 } 118 106 119 //////////////////////////////////////////////////////////////// 107 120 108 121 CReplayPlayer::CReplayPlayer() : 109 122 m_Stream(NULL) 110 123 { -
source/ps/Replay.h
public: 45 45 46 46 /** 47 47 * Optional hash of simulation state (for sync checking). 48 48 */ 49 49 virtual void Hash(const std::string& hash, bool quick) = 0; 50 51 /** 52 * Remember the directory containing the commands.txt file, so that we can save additional files to it. 53 */ 54 virtual OsPath GetDirectory() const = 0; 50 55 }; 51 56 52 57 /** 53 58 * Implementation of IReplayLogger that simply throws away all data. 54 59 */ … … class CDummyReplayLogger : public IRepla 56 61 { 57 62 public: 58 63 virtual void StartGame(JS::MutableHandleValue UNUSED(attribs)) { } 59 64 virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector<SimulationCommand>& UNUSED(commands)) { } 60 65 virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { } 66 virtual OsPath GetDirectory() const { return OsPath(); } 61 67 }; 62 68 63 69 /** 64 70 * Implementation of IReplayLogger that saves data to a file in the logs directory. 65 71 */ … … public: 71 77 ~CReplayLogger(); 72 78 73 79 virtual void StartGame(JS::MutableHandleValue attribs); 74 80 virtual void Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands); 75 81 virtual void Hash(const std::string& hash, bool quick); 82 virtual OsPath GetDirectory() const; 76 83 77 84 private: 78 85 ScriptInterface& m_ScriptInterface; 79 86 std::ostream* m_Stream; 87 OsPath m_Directory; 80 88 }; 81 89 82 90 /** 83 91 * Replay log replayer. Runs the log with no graphics and dumps some info to stdout. 84 92 */ -
source/ps/VisualReplay.cpp
1 /* Copyright (C) 2015 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "precompiled.h" 19 20 #include "VisualReplay.h" 21 #include "graphics/GameView.h" 22 #include "gui/GUIManager.h" 23 #include "lib/allocators/shared_ptr.h" 24 #include "lib/external_libraries/libsdl.h" 25 #include "lib/utf8.h" 26 #include "ps/CLogger.h" 27 #include "ps/Filesystem.h" 28 #include "ps/Game.h" 29 #include "ps/Pyrogenesis.h" 30 #include "ps/Replay.h" 31 #include "scriptinterface/ScriptInterface.h" 32 33 /** 34 * Filter too short replays (value in seconds). 35 */ 36 const u8 minimumReplayDuration = 3; 37 38 /** 39 * Allows quick debugging of potential platform-dependent file-reading bugs. 40 */ 41 const bool debugParser = false; 42 43 OsPath VisualReplay::GetDirectoryName() 44 { 45 return OsPath(psLogDir() / L"sim_log"); 46 } 47 48 JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface) 49 { 50 TIMER(L"GetReplays"); 51 JSContext* cx = scriptInterface.GetContext(); 52 JSAutoRequest rq(cx); 53 54 u32 i = 0; 55 DirectoryNames directories; 56 JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); 57 if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK) 58 for (OsPath& directory : directories) 59 { 60 // Load & store replay data 61 JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); 62 if (!replayData.isNull()) 63 JS_SetElement(cx, replays, i++, replayData); 64 65 // Wait a bit, so that we can quit the process with SIGTERM (Ctrl+c or Alt+F4) 66 case SDL_HOTKEYDOWN: 67 std::string hotkey = static_cast<const char*>(ev->ev.user.data1); 68 if (hotkey == "exit") 69 { 70 kill_mainloop(); 71 return IN_HANDLED; 72 } 73 case SDL_QUIT: 74 kill_mainloop(); 75 break; } 76 return JS::ObjectValue(*replays); 77 } 78 79 /** 80 * Move the cursor backwards until a newline was read or the beginning of the file was found. 81 * Either way the cursor points to the beginning of a newline. 82 * 83 * @return The current cursor position or -1 on error. 84 */ 85 int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64 fileSize) 86 { 87 int currentPos; 88 char character; 89 for (int characters = 0; characters < 10000; ++characters) 90 { 91 currentPos = (int) replayStream->tellg(); 92 93 // Stop when reached the beginning of the file 94 if (currentPos == 0) 95 return currentPos; 96 97 if (!replayStream->good()) 98 { 99 LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str()); 100 return -1; 101 } 102 103 // Stop when reached newline 104 replayStream->get(character); 105 if (character == '\n') 106 return currentPos; 107 108 // Otherwise go back one character. 109 // Notice: -1 will set the cursor back to the most recently read character. 110 replayStream->seekg(-2, std::ios_base::cur); 111 } 112 113 LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str()); 114 return -1; 115 } 116 117 /** 118 * Compute game duration. Assume constant turn length. 119 * Find the last line that starts with "turn" by reading the file backwards. 120 * 121 * @return seconds or -1 on error 122 */ 123 int getReplayDuration(std::istream *replayStream, const CStr& fileName, const u64 fileSize) 124 { 125 CStr type; 126 127 // Move one character before the file-end 128 replayStream->seekg(-2, std::ios_base::end); 129 130 // Infinite loop protection, should never occur. 131 // There should be about 5 lines to read until a turn is found. 132 for (int linesRead = 1; linesRead < 1000; ++linesRead) 133 { 134 int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); 135 136 // Read error or reached file beginning. No turns exist. 137 if (currentPosition < 1) 138 return -1; 139 140 if (debugParser) 141 debug_printf("At position %i of %lu after %i lines reads.\n", currentPosition, fileSize, linesRead); 142 143 if (!replayStream->good()) 144 { 145 LOGERROR("Read error when determining replay duration at %i of %lu in %s", currentPosition - 2, fileSize, fileName.c_str()); 146 return -1; 147 } 148 149 // Found last turn, compute duration. 150 if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") 151 { 152 u32 turn = 0, turnLength = 0; 153 *replayStream >> turn >> turnLength; 154 return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0 155 } 156 157 // Otherwise move cursor back to the character before the last newline 158 replayStream->seekg(currentPosition - 2, std::ios_base::beg); 159 } 160 161 LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str()); 162 return -1; 163 } 164 165 JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory) 166 { 167 TIMER(L"LoadReplayData"); 168 169 // The directory argument must not be constant, otherwise concatenating will fail 170 const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt"; 171 172 //if (debugParser) 173 debug_printf("Opening %s\n", utf8_from_wstring(replayFile.string()).c_str()); 174 175 if (!FileExists(replayFile)) 176 return JSVAL_NULL; 177 178 // Get file size and modification date 179 CFileInfo fileInfo; 180 GetFileInfo(replayFile, &fileInfo); 181 const i32 fileTime = (i32) fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath) 182 const u64 fileSize = (u64)fileInfo.Size(); 183 184 if (fileSize == 0) 185 return JSVAL_NULL; 186 187 // Open file 188 // TODO: enhancement: support unicode when OsString() is properly implemented for windows 189 const CStr fileName = utf8_from_wstring(replayFile.string()); 190 std::ifstream* replayStream = new std::ifstream(fileName.c_str()); 191 192 // File must begin with "start" 193 CStr type; 194 if (!(*replayStream >> type).good() || type != "start") 195 { 196 LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str()); 197 SAFE_DELETE(replayStream); 198 return JSVAL_NULL; 199 } 200 201 // Parse header / first line 202 CStr header; 203 std::getline(*replayStream, header); 204 JSContext* cx = scriptInterface.GetContext(); 205 JSAutoRequest rq(cx); 206 JS::RootedValue attribs(cx); 207 if (!scriptInterface.ParseJSON(header, &attribs)) 208 { 209 LOGERROR("Couldn't parse replay header of %s", fileName.c_str()); 210 SAFE_DELETE(replayStream); 211 return JSVAL_NULL; 212 } 213 214 // Ensure "turn" after header 215 if (!(*replayStream >> type).good() || type != "turn") 216 { 217 SAFE_DELETE(replayStream); 218 return JSVAL_NULL; // there are no turns at all 219 } 220 221 // Don't process files of rejoined clients 222 u32 turn = 1; 223 *replayStream >> turn; 224 if (turn != 0) 225 { 226 SAFE_DELETE(replayStream); 227 return JSVAL_NULL; 228 } 229 230 int duration = getReplayDuration(replayStream, fileName, fileSize); 231 232 SAFE_DELETE(replayStream); 233 234 // Ensure minimum duration 235 if (duration < minimumReplayDuration) 236 return JSVAL_NULL; 237 238 // Return the actual data 239 JS::RootedValue replayData(cx); 240 scriptInterface.Eval("({})", &replayData); 241 scriptInterface.SetProperty(replayData, "file", replayFile); 242 scriptInterface.SetProperty(replayData, "directory", directory); 243 scriptInterface.SetProperty(replayData, "filemod_timestamp", fileTime); 244 scriptInterface.SetProperty(replayData, "attribs", attribs); 245 scriptInterface.SetProperty(replayData, "duration", duration); 246 return replayData; 247 } 248 249 bool VisualReplay::DeleteReplay(const CStrW& replayDirectory) 250 { 251 if (replayDirectory.empty()) 252 return false; 253 254 const OsPath directory = GetDirectoryName() / replayDirectory; 255 return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; 256 } 257 258 259 JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) 260 { 261 // Create empty JS object 262 JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); 263 JSAutoRequest rq(cx); 264 JS::RootedValue attribs(cx); 265 pCxPrivate->pScriptInterface->Eval("({})", &attribs); 266 267 // Return empty object if file doesn't exist 268 const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt"; 269 if (!FileExists(replayFile)) 270 return attribs; 271 272 // Open file 273 std::istream* replayStream = new std::ifstream(utf8_from_wstring(replayFile.string()).c_str()); 274 CStr type, line; 275 ENSURE((*replayStream >> type).good() && type == "start"); 276 277 // Read and return first line 278 std::getline(*replayStream, line); 279 pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); 280 SAFE_DELETE(replayStream);; 281 return attribs; 282 } 283 284 // TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp) 285 void VisualReplay::SaveReplayMetadata(const CStrW& data) 286 { 287 // TODO: enhancement: use JS::HandleValue similar to SaveGame 288 if (!g_Game) 289 return; 290 291 // Get the directory of the currently active replay 292 const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"; 293 CreateDirectories(fileName.Parent(), 0700); 294 295 std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc); 296 stream << utf8_from_wstring(data); 297 stream.close(); 298 } 299 300 JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName) 301 { 302 const OsPath filePath = GetDirectoryName() / directoryName / L"metadata.json"; 303 304 JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); 305 JSAutoRequest rq(cx); 306 JS::RootedValue metadata(cx); 307 308 if (!FileExists(filePath)) 309 return JSVAL_NULL; 310 311 std::ifstream* stream = new std::ifstream(OsString(filePath).c_str()); 312 ENSURE(stream->good()); 313 CStr line; 314 std::getline(*stream, line); 315 stream->close(); 316 delete stream; 317 pCxPrivate->pScriptInterface->ParseJSON(line, &metadata); 318 319 return metadata; 320 } -
source/ps/VisualReplay.h
1 /* Copyright (C) 2015 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #ifndef INCLUDED_REPlAY 19 #define INCLUDED_REPlAY 20 21 #include "scriptinterface/ScriptInterface.h" 22 class CSimulation2; 23 class CGUIManager; 24 25 /** 26 * Contains functions for visually replaying past games. 27 */ 28 namespace VisualReplay 29 { 30 31 /** 32 * Returns the path to the sim-log directory (that contains the directories with the replay files. 33 * 34 * @param scriptInterface the ScriptInterface in which to create the return data. 35 * @return OsPath the absolute file path 36 */ 37 OsPath GetDirectoryName(); 38 39 /** 40 * Get a list of replays to display in the GUI. 41 * 42 * @param scriptInterface the ScriptInterface in which to create the return data. 43 * @return array of objects containing replay data 44 */ 45 JS::Value GetReplays(ScriptInterface& scriptInterface); 46 47 /** 48 * Parses a commands.txt file and extracts metadata. 49 * Works similarly to CGame::LoadReplayData(). 50 */ 51 JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory); 52 53 /** 54 * Permanently deletes the visual replay (including the parent directory) 55 * 56 * @param replayFile path to commands.txt, whose parent directory will be deleted 57 * @return true if deletion was successful, false on error 58 */ 59 bool DeleteReplay(const CStrW& replayFile); 60 61 /** 62 * Returns the parsed header of the replay file (commands.txt). 63 */ 64 JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); 65 66 /** 67 * Returns the metadata of a replay. 68 */ 69 JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName); 70 71 /** 72 * Saves the metadata from the session to metadata.json 73 */ 74 void SaveReplayMetadata(const CStrW& data); 75 76 } 77 78 #endif -
source/ps/scripting/JSInterface_VisualReplay.cpp
1 /* Copyright (C) 2015 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #include "precompiled.h" 19 20 #include "network/NetClient.h" 21 #include "network/NetServer.h" 22 #include "ps/Filesystem.h" 23 #include "ps/Game.h" 24 #include "ps/VisualReplay.h" 25 #include "ps/scripting/JSInterface_VisualReplay.h" 26 27 void JSI_VisualReplay::StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory) 28 { 29 ENSURE(!g_NetServer); 30 ENSURE(!g_NetClient); 31 ENSURE(!g_Game); 32 33 const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt"; 34 if (FileExists(replayFile)) 35 { 36 g_Game = new CGame(false, false); 37 // TODO: support unicode when OsString() is implemented for windows 38 g_Game->StartVisualReplay(utf8_from_wstring(replayFile.string())); 39 } 40 } 41 42 bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile) 43 { 44 return VisualReplay::DeleteReplay(replayFile); 45 } 46 47 JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate) 48 { 49 return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface)); 50 } 51 52 JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName) 53 { 54 return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName); 55 } 56 57 JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName) 58 { 59 return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName); 60 } 61 62 void JSI_VisualReplay::SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data) 63 { 64 VisualReplay::SaveReplayMetadata(data); 65 } 66 67 void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface) 68 { 69 scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays"); 70 scriptInterface.RegisterFunction<bool, CStrW, &DeleteReplay>("DeleteReplay"); 71 scriptInterface.RegisterFunction<void, CStrW, &StartVisualReplay>("StartVisualReplay"); 72 scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayAttributes>("GetReplayAttributes"); 73 scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayMetadata>("GetReplayMetadata"); 74 scriptInterface.RegisterFunction<void, CStrW, &SaveReplayMetadata>("SaveReplayMetadata"); 75 } -
source/ps/scripting/JSInterface_VisualReplay.h
1 /* Copyright (C) 2015 Wildfire Games. 2 * This file is part of 0 A.D. 3 * 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * 0 A.D. is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 #ifndef INCLUDED_JSI_VISUALREPLAY 19 #define INCLUDED_JSI_VISUALREPLAY 20 21 #include "ps/VisualReplay.h" 22 #include "scriptinterface/ScriptInterface.h" 23 24 namespace JSI_VisualReplay 25 { 26 void StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory); 27 bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile); 28 JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate); 29 JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName); 30 JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName); 31 void SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data); 32 void RegisterScriptFunctions(ScriptInterface& scriptInterface); 33 } 34 35 #endif