Ticket #3258: replay_menu_wip_v2.patch
File replay_menu_wip_v2.patch, 50.2 KB (added by , 9 years ago) |
---|
-
binaries/data/mods/public/gui/page_replay.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>replay/styles.xml</include> 14 <include>replay/replay.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">Replay</translatableAttribute> 356 <translatableAttribute id="tooltip">Shows a replay of a past game.</translatableAttribute> 357 <action on="Press"> 358 closeMenu(); 359 Engine.SwitchGuiPage("page_replay.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/replay/replay.js
1 var g_Replays = []; 2 var g_ReplaysFiltered = []; 3 var g_Playernames = []; // All playernames of all replays, used for autocomplete 4 var g_mapSizes = initMapSizes(); 5 var g_maxNameChars = 70; 6 var g_DurationFilterMin = [ 0, 0, 15, 30, 45, 60, 90, 120]; 7 var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120, -1]; 8 9 function init() 10 { 11 loadReplays(); 12 updateReplayList(); 13 } 14 15 function loadReplays() 16 { 17 g_Playernames = []; 18 g_Replays = Engine.GetReplays(); 19 for(let replay of g_Replays) 20 { 21 // TODO: enhancement: ask Yves how to parse the json in c++ and send the object instead of the json string 22 replay.attribs = JSON.parse(replay.header); 23 delete replay.header; 24 25 // TODO: enhancement: remove those copies for better performance 26 replay.settings = replay.attribs.settings; 27 replay.playerData = replay.settings.PlayerData; 28 29 // Skirmish maps don't have that attribute 30 if (!replay.settings.hasOwnProperty("Size")) 31 replay.settings.Size = -1; 32 33 for(var i in replay.playerData) 34 { 35 // I've encountered a file where 'null' was added to the playerData array... 36 if (!replay.playerData[i]) 37 { 38 error("Replay " + replay.directory + " has bogus player data!"); 39 continue; 40 } 41 42 var name = replay.playerData[i].Name; 43 if (g_Playernames.indexOf(name) == -1) 44 g_Playernames.push({"name": name}); 45 } 46 } 47 } 48 49 function updateReplayList() 50 { 51 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 52 53 // Filters depend on the replay list 54 initFilters(); 55 56 if (g_Replays.length == 0) 57 replaySelection.selected = -1; 58 59 // TODO: enhancement: do better sorting after #2405 60 g_Replays.sort(function(a,b) 61 { 62 return a.timestamp < b.timestamp; 63 }); 64 65 // Get current game version and loaded mods 66 var engineInfo = Engine.GetEngineInfo(); 67 // TODO: requirement filter compatible games 68 69 // Fill lists with items 70 var replayListLabels = []; 71 var replayListDirectories = []; 72 var list_name = []; 73 var list_players = []; 74 var list_mapName = []; 75 var list_mapSize = []; 76 var list_popCapacity = []; 77 var list_duration = []; 78 g_ReplaysFiltered = []; 79 for (var replay of g_Replays) 80 { 81 if (filterReplay(replay)) 82 continue; 83 84 g_ReplaysFiltered.push(replay); 85 86 // TODO: enhancement: if (settings.GameType != "conquest") use other color 87 88 replayListLabels.push(replay.directory); 89 replayListDirectories.push(replay.directory); 90 91 list_name.push(getReplayDateTime(replay)); 92 list_players.push(getReplayPlayernames(replay, true)); 93 list_mapName.push(getReplayMapName(replay)); 94 list_mapSize.push(getReplayMapSizeText(replay.settings.Size)); 95 list_popCapacity.push(getReplayPopCap(replay)); 96 list_duration.push(getReplayDuration(replay)); 97 } 98 99 // TODO: enhancement: remember last selection, like #3244 100 if (replaySelection.selected >= list_name.length) 101 replaySelection.selected = -1; 102 103 // Update list 104 replaySelection.list_name = list_name; 105 replaySelection.list_players = list_players; 106 replaySelection.list_mapName = list_mapName; 107 replaySelection.list_mapSize = list_mapSize; 108 replaySelection.list_popCapacity = list_popCapacity; 109 replaySelection.list_duration = list_duration; 110 111 replaySelection.list = replayListLabels; 112 replaySelection.list_data = replayListDirectories; 113 } 114 115 function getReplayDateTime(replay) 116 { 117 var date = new Date(replay.timestamp * 1000); 118 var year = date.getFullYear(); 119 var month = ('0' + (date.getMonth() + 1)).slice(-2); 120 var day = ('0' + date.getDate()).slice(-2); 121 var hour = ('0' + date.getHours()).slice(-2); 122 var minute = ('0' + date.getMinutes()).slice(-2); 123 return year + "-" + month.slice(-2) + "-" + day.slice(-2) + " " + hour + ":" + minute.slice(-2); 124 } 125 126 function getReplayPlayernames(replay, shorten) 127 { 128 // Extract playernames 129 var playernames = []; 130 for(let i in replay.playerData) 131 { 132 // I've encountered a file where 'null' was added to the playerData array... 133 if (!replay.playerData[i]) 134 { 135 error("Replay " + replay.directory + " has bogus player data!"); 136 continue; 137 } 138 139 var playername = escapeText(replay.playerData[i].Name); 140 141 // TODO: enhancement: colorize playernames like in lobby using colorPlayerName 142 // #3205 moves the function to common/color.js 143 playernames.push(playername); 144 } 145 146 if (!shorten) 147 return playernames; 148 149 playernames = playernames.join(", "); 150 151 // Shorten if too long 152 if (playernames.length > g_maxNameChars) 153 return playernames.substr(0, g_maxNameChars) + "..."; 154 else 155 return playernames; 156 } 157 158 function getReplayMapName(replay) 159 { 160 return escapeText(replay.settings.Name); 161 } 162 163 function getReplayMapSizeText(tiles) 164 { 165 var index = g_mapSizes.tiles.indexOf(tiles); 166 if (index > -1) 167 return g_mapSizes.shortNames[index] 168 else 169 return translateWithContext("map size", "Default"); 170 } 171 172 function getReplayPopCap(replay) 173 { 174 if (replay.settings.PopulationCap == 10000) 175 return "Unlimited"; 176 else 177 return replay.settings.PopulationCap; 178 } 179 180 function getReplayDuration(replay) 181 { 182 var hours = Math.floor(replay.duration / 3600); 183 var min = Math.floor((replay.duration - hours * 3600) / 60); 184 var sec = replay.duration % 60; 185 186 if (hours > 0) 187 return sprintf(translateWithContext("replay duration", "%(hours)sh %(minutes)sm %(seconds)ss"), { "hours": hours, "minutes": min, "seconds": sec}); 188 else if (min > 0) 189 return sprintf(translateWithContext("replay duration", "%(minutes)sm %(seconds)ss"), { "minutes": min, "seconds": sec}); 190 else 191 return sprintf(translateWithContext("replay duration", "%(seconds)ss"), { "seconds": sec}); 192 } 193 194 function initFilters() 195 { 196 initDateFilter(); 197 initMapNameFilter(); 198 initMapSizeFilter(); 199 initPopCapFilter(); 200 initDurationFilter(); 201 } 202 203 function initDateFilter() 204 { 205 var months = ["Any"]; 206 for(var replay of g_Replays) 207 { 208 var month = getDateTimeFilterVal(replay); 209 210 if (months.indexOf(month) == -1) 211 months.push(month); 212 } 213 214 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 215 dateTimeFilter.list = months; 216 dateTimeFilter.list_data = months; 217 218 if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= months.length) 219 dateTimeFilter.selected = 0; 220 } 221 222 function getDateTimeFilterVal(replay) 223 { 224 var date = new Date(replay.timestamp * 1000); 225 return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2) 226 } 227 228 function initMapSizeFilter() 229 { 230 // Get tilecounts actually used by maps 231 var tiles = []; 232 for(let replay of g_Replays) 233 { 234 if (tiles.indexOf(replay.settings.Size) == -1) 235 tiles.push(replay.settings.Size); 236 } 237 tiles.sort(); 238 239 // Add translated names 240 var names = []; 241 for(var size of tiles) 242 names.push(getReplayMapSizeText(size)); 243 244 // Add "Any" 245 names.unshift(translateWithContext("map size", "Any")); 246 tiles.unshift(""); 247 248 // Save values to filter 249 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 250 mapSizeFilter.list = names; 251 mapSizeFilter.list_data = tiles; 252 253 if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= tiles.length) 254 mapSizeFilter.selected = 0; 255 } 256 257 function initMapNameFilter() 258 { 259 var mapNames = ["Any"]; 260 for(var replay of g_Replays) 261 { 262 var mapName = escapeText(replay.settings.Name); 263 264 if (mapNames.indexOf(mapName) == -1) 265 mapNames.push(mapName); 266 } 267 268 269 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 270 mapNameFilter.list = mapNames; 271 mapNameFilter.list_data = mapNames; 272 273 if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNames.length) 274 mapNameFilter.selected = 0; 275 } 276 277 function initPopCapFilter() 278 { 279 var popCaps = []; 280 for(var replay of g_Replays) 281 { 282 var popCap = getReplayPopCap(replay); 283 if (popCaps.indexOf(popCap) == -1) 284 popCaps.push(popCap); 285 } 286 popCaps.sort(); 287 288 popCaps.unshift("Any"); 289 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 290 populationFilter.list = popCaps; 291 populationFilter.list_data = popCaps; 292 293 if (populationFilter.selected == -1 || populationFilter.selected >= popCaps.length) 294 populationFilter.selected = 0; 295 } 296 297 function initDurationFilter() 298 { 299 var data = [-1]; 300 var labels = ["Any"]; 301 302 var g_DurationFilterMin = [0, 0, 15, 30, 45, 60, 90, 120]; 303 var g_DurationFilterMax = [0, 15, 30, 45, 60, 90, 120, -1]; 304 305 for(var i in g_DurationFilterMin) 306 { 307 if (i == 0) 308 continue; 309 310 data.push(i); 311 312 if (i == 1) 313 labels.push("<" + g_DurationFilterMax[i] + "min"); 314 else if (i == g_DurationFilterMin.length -1) 315 labels.push(">" + g_DurationFilterMin[i] + "min"); 316 else 317 labels.push(g_DurationFilterMin[i] + "-" + g_DurationFilterMax[i] + "min"); 318 } 319 320 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 321 durationFilter.list = labels; 322 durationFilter.list_data = data; 323 324 if (durationFilter.selected == -1 || durationFilter.selected >= data.length) 325 durationFilter.selected = 0; 326 } 327 328 function applyFilters() 329 { 330 // Update the list of replays 331 updateReplayList(); 332 333 // Update info box about the replay currently selected 334 updateReplaySelection(); 335 } 336 337 // Returns true if the replay should not be listed. 338 function filterReplay(replay) 339 { 340 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 341 var playersFilter = Engine.GetGUIObjectByName("playersFilter"); 342 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 343 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 344 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 345 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 346 347 // Filter date/time (select a month) 348 if (dateTimeFilter.selected > 0) 349 { 350 let selectedMonth = dateTimeFilter.list_data[dateTimeFilter.selected]; 351 if (getDateTimeFilterVal(replay) != selectedMonth) 352 return true; 353 } 354 355 // Filter selected players 356 let playerText = playersFilter.caption; 357 if (playerText.length) 358 { 359 // Player and botnames can contain spaces 360 // We just check if all words of all players are somewhere in the playerlist 361 playerText = playerText.toLowerCase().split(" "); 362 let replayPlayers = replay.playerData.map(function(player){ return player.Name.toLowerCase() }).join(" "); 363 for(let word of playerText) 364 { 365 if (replayPlayers.indexOf(word) == -1) 366 return true; 367 } 368 } 369 370 // Filter map name 371 if (mapNameFilter.selected > 0) 372 { 373 if (getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected]) 374 return true; 375 } 376 377 // Filter map size 378 if (mapSizeFilter.selected > 0) 379 { 380 let selectedMapSize = mapSizeFilter.list_data[mapSizeFilter.selected]; 381 if (replay.settings.Size != selectedMapSize) 382 return true; 383 } 384 385 // Filter population capacity 386 if (populationFilter.selected > 0 && 387 getReplayPopCap(replay) != populationFilter.list_data[populationFilter.selected]) 388 { 389 return true; 390 } 391 392 // Filter game duration 393 if (durationFilter.selected > 0) 394 { 395 var g_DurationFilterMin = [ 0, 0, 15, 30, 45, 60, 90, 120]; 396 var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120, -1]; 397 398 let minutes = replay.duration / 60; 399 let min = g_DurationFilterMin[durationFilter.selected]; 400 let max = g_DurationFilterMax[durationFilter.selected]; 401 402 if (minutes < min || (max > -1 && minutes > max)) 403 return true; 404 } 405 406 return false; 407 } 408 409 function updateReplaySelection() 410 { 411 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 412 var replaySelected = selected > -1; 413 414 Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; 415 Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; 416 Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; 417 Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; 418 419 if (!replaySelected) 420 return; 421 422 var replay = g_ReplaysFiltered[selected]; 423 var mapData; 424 425 // Load map data 426 if (replay.settings.mapType == "random" && replay.attribs.map == "random") 427 mapData = {"settings": {"Description": translate("A randomly selected map.")}}; 428 else if (replay.settings.mapType == "random" && Engine.FileExists(replay.attribs.map + ".json")) 429 mapData = Engine.ReadJSONFile(replay.attribs.map + ".json"); 430 else if (Engine.FileExists(replay.attribs.map + ".xml")) 431 mapData = Engine.LoadMapSettings(replay.attribs.map + ".xml"); 432 else 433 // Warn the player if we can't find the map. 434 warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: replay.attribs.map })); 435 436 var players = getReplayPlayernames(replay, false); 437 438 // Display the map name, number of players, the names of the players, the map size and the map type. 439 Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.settings.Name); 440 Engine.GetGUIObjectByName("sgNbPlayers").caption = players.length; 441 Engine.GetGUIObjectByName("sgPlayersNames").caption = players.join(", "); 442 Engine.GetGUIObjectByName("sgMapSize").caption = getReplayMapSizeText(replay.settings.Size); 443 Engine.GetGUIObjectByName("sgMapType").caption = replay.settings.mapType; 444 445 // Display map description if it exists, otherwise display a placeholder. 446 if (mapData && mapData.settings.Description) 447 var mapDescription = translate(mapData.settings.Description); 448 else 449 var mapDescription = translate("Sorry, no description available."); 450 451 // Display map preview if it exists, otherwise display a placeholder. 452 if (mapData && mapData.settings.Preview) 453 var mapPreview = mapData.settings.Preview; 454 else 455 var mapPreview = "nopreview.png"; 456 457 Engine.GetGUIObjectByName("sgMapDescription").caption = mapDescription; 458 Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapPreview; 459 } 460 461 function startReplay() 462 { 463 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 464 var replayDirectory = replaySelection.list_data[replaySelection.selected]; 465 reallyStartVisualReplay(replayDirectory); 466 } 467 468 function reallyStartVisualReplay(replayDirectory) 469 { 470 // TODO: requirement: check replay compatibility before really loading it (similar to savegames) 471 Engine.StartVisualReplay(replayDirectory); 472 Engine.SwitchGuiPage("page_loading.xml", { 473 "attribs": Engine.GetVisualReplayAttributes(replayDirectory), 474 "isNetworked" : false, 475 "playerAssignments": {}, 476 "savedGUIData": "" 477 }); 478 } 479 480 function deleteReplay() 481 { 482 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 483 484 if (replaySelection.selected == -1) 485 return; 486 487 var replayLabel = replaySelection.list[replaySelection.selected]; 488 var replayDirectory = replaySelection.list_data[replaySelection.selected]; 489 490 // Ask for confirmation 491 var btCaptions = [translate("Yes"), translate("No")]; 492 var btCode = [function(){ reallyDeleteReplay(replayDirectory); }, null]; 493 messageBox(500, 200, sprintf(translate("\"%(label)s\""), { label: replayLabel }) + "\n" + translate("Are you sure to delete this replay permanently?"), translate("DELETE"), 0, btCaptions, btCode); 494 } 495 496 function deleteReplayWithoutConfirmation() 497 { 498 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 499 var replayDirectory = replaySelection.list_data[replaySelection.selected]; 500 reallyDeleteReplay(replayDirectory); 501 } 502 503 function reallyDeleteReplay(replayDirectory) 504 { 505 if (!Engine.DeleteReplay(replayDirectory)) 506 error(sprintf("Could not delete replay '%(id)s'", { id: replayDirectory })); 507 508 // Refresh replay list 509 init(); 510 } 511 No newline at end of file -
binaries/data/mods/public/gui/replay/replay.xml
1 <?xml version="1.0" encoding="utf-8"?> 2 3 <objects> 4 5 <script file="gui/common/functions_global_object.js" /> 6 <script file="gui/common/functions_utility.js" /> 7 <script file="gui/replay/replay.js" /> 8 9 <object type="image" style="ModernWindow" size="0 0 100% 100%" name="replayWindow"> 10 11 <object style="ModernLabelText" type="text" size="50%-128 0%+4 50%+128 36"> 12 <translatableAttribute id="caption">Replay Games</translatableAttribute> 13 </object> 14 15 <!-- Left Panel: Filters & Replay List --> 16 <object name="leftPanel" size="3% 5% 100%-255 97%"> 17 18 <!-- Filters --> 19 <object name="filterPanel" size="0 0 100% 24"> 20 <object name="dateTimeFilter" 21 type="dropdown" 22 style="ModernDropDown" 23 size="5 0 110-10 100%" 24 font="sans-bold-13"> 25 <action on="SelectionChange">applyFilters();</action> 26 </object> 27 <object name="playersFilter" 28 type="input" 29 style="ModernInput" 30 size="110-5 0 110+400-10 100%" 31 font="sans-bold-13"> 32 <action on="Press">applyFilters();</action> 33 <action on="Tab">autoCompleteNick("playersFilter", g_Playernames);</action> 34 </object> 35 <object name="mapNameFilter" 36 type="dropdown" 37 style="ModernDropDown" 38 size="110+400-5 0 110+400+140-10 100%" 39 font="sans-bold-13"> 40 <action on="SelectionChange">applyFilters();</action> 41 </object> 42 <object name="mapSizeFilter" 43 type="dropdown" 44 style="ModernDropDown" 45 size="110+400+140-5 0 110+400+140+80-10 100%" 46 font="sans-bold-13"> 47 <action on="SelectionChange">applyFilters();</action> 48 </object> 49 <object name="populationFilter" 50 type="dropdown" 51 style="ModernDropDown" 52 size="110+400+140+80-5 0 110+400+140+80+80-10 100%" 53 font="sans-bold-13"> 54 <action on="SelectionChange">applyFilters();</action> 55 </object> 56 <object name="durationFilter" 57 type="dropdown" 58 style="ModernDropDown" 59 size="110+400+140+80+80-5 0 110+400+140+80+80+80-10 100%" 60 font="sans-bold-13"> 61 <action on="SelectionChange">applyFilters();</action> 62 </object> 63 </object> 64 65 <!-- Replay list --> 66 <object name="replaySelection" style="ModernList" type="olist" size="0 35 100% 100%-50" font="sans-stroke-13"> 67 <action on="SelectionChange">updateReplaySelection();</action> 68 <!-- Columns --> 69 <!-- 0ad crashes if there is no column with the id "name"! --> 70 <def id="name" color="128 128 128" width="110"> 71 <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute> 72 </def> 73 <def id="players" color="128 128 128" width="400"> 74 <translatableAttribute id="heading" context="replay">Players</translatableAttribute> 75 </def> 76 <def id="mapName" color="128 128 128" width="140"> 77 <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute> 78 </def> 79 <def id="mapSize" color="128 128 128" width="80"> 80 <translatableAttribute id="heading" context="replay">Size</translatableAttribute> 81 </def> 82 <def id="popCapacity" color="0 128 128" width="80"> 83 <translatableAttribute id="heading" context="replay">Population</translatableAttribute> 84 </def> 85 <def id="duration" color="128 128 128" width="80"> 86 <translatableAttribute id="heading" context="replay">Duration</translatableAttribute> 87 </def> 88 </object> 89 </object> 90 91 <!-- Right Panel: Replay Details --> 92 <object name="rightPanel" size="100%-250 30 100%-20 100%-20" > 93 94 <object name="replayInfoEmpty" size="0 0 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false"> 95 <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/> 96 <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%"> 97 <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/> 98 </object> 99 </object> 100 101 <object name="replayInfo" size="0 0 100% 100%-90" type="image" sprite="ModernDarkBoxGold" hidden="true"> 102 103 <!-- Map Name --> 104 <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/> 105 106 <!-- Map Preview --> 107 <object name="sgMapPreview" size="5 25 100%-5 190" type="image" sprite=""/> 108 109 <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/> 110 111 <!-- Map Type --> 112 <object size="5 195 50% 240" type="image" sprite="ModernItemBackShadeLeft"> 113 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 114 <translatableAttribute id="caption">Map Type:</translatableAttribute> 115 </object> 116 </object> 117 <object size="50% 195 100%-5 240" type="image" sprite="ModernItemBackShadeRight"> 118 <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 119 </object> 120 121 <object size="5 239 100%-5 240" type="image" sprite="ModernWhiteLine" z="25"/> 122 123 <!-- Map Size --> 124 <object size="5 240 50% 285" type="image" sprite="ModernItemBackShadeLeft"> 125 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 126 <translatableAttribute id="caption">Map Size:</translatableAttribute> 127 </object> 128 </object> 129 <object size="50% 240 100%-5 285" type="image" sprite="ModernItemBackShadeRight"> 130 <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 131 </object> 132 133 <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/> 134 135 <!-- Map Description --> 136 <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 75%"> 137 <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/> 138 </object> 139 140 <object type="image" sprite="ModernDarkBoxWhite" size="3% 75%+5 97% 99%"> 141 <!-- Number of Players --> 142 <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText"> 143 <translatableAttribute id="caption">Players:</translatableAttribute> 144 </object> 145 <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/> 146 147 <!-- Player Names --> 148 <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/> 149 </object> 150 151 </object> 152 </object> 153 154 <!-- Bottom Panel: Buttons. --> 155 <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" > 156 157 <object type="button" style="StoneButton" size="0%+25 0 17%+25 100%"> 158 <translatableAttribute id="caption">Main Menu</translatableAttribute> 159 <action on="Press"> 160 Engine.SwitchGuiPage("page_pregame.xml"); 161 </action> 162 </object> 163 164 <object name="deleteReplayButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%" hotkey="session.savedgames.delete"> 165 <translatableAttribute id="caption">Delete</translatableAttribute> 166 <action on="Press"> 167 if (!this.enabled) 168 return; 169 if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation")) 170 deleteReplayWithoutConfirmation(); 171 else 172 deleteReplay(); 173 </action> 174 </object> 175 176 <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%"> 177 <translatableAttribute id="caption">Start Replay</translatableAttribute> 178 <action on="Press"> 179 startReplay(); 180 </action> 181 </object> 182 183 </object> 184 </object> 185 </objects> 186 No newline at end of file -
binaries/data/mods/public/gui/replay/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> -
build/svn_revision/engine_version.txt
1 L"0.0.19" -
source/gui/scripting/ScriptFunctions.cpp
48 48 #include "ps/Globals.h" // g_frequencyFilter 49 49 #include "ps/Hotkey.h" 50 50 #include "ps/ProfileViewer.h" 51 51 #include "ps/Pyrogenesis.h" 52 52 #include "ps/SavedGame.h" 53 #include "ps/VisualReplay.h" 53 54 #include "ps/UserReport.h" 54 55 #include "ps/World.h" 55 56 #include "ps/scripting/JSInterface_ConfigDB.h" 56 57 #include "ps/scripting/JSInterface_Console.h" 57 58 #include "ps/scripting/JSInterface_Mod.h" … … 281 282 shared_ptr<ScriptInterface::StructuredClone> GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata); 282 283 if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0) 283 284 LOGERROR("Failed to save game"); 284 285 } 285 286 287 JS::Value StartVisualReplay(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 288 { 289 JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext(); 290 JSAutoRequest rq(cxGui); 291 JS::RootedValue guiContextMetadata(cxGui); 292 293 ENSURE(!g_NetServer); 294 ENSURE(!g_NetClient); 295 ENSURE(!g_Game); 296 297 CStrW replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / L"commands.txt").string(); 298 std::string replayFile( replayFilePath.begin(), replayFilePath.end() ); 299 300 if (FileExists(OsPath(replayFile))) 301 { 302 g_Game = new CGame(false, false); 303 g_Game->StartReplay(replayFile); 304 } 305 306 return guiContextMetadata; 307 } 308 309 JS::Value GetVisualReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 310 { 311 CStrW replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / "commands.txt").string(); 312 std::string replayFile( replayFilePath.begin(), replayFilePath.end() ); 313 314 if (!FileExists(OsPath(replayFile))) 315 return {}; 316 317 std::istream* replayStream = new std::ifstream(replayFile.c_str()); 318 std::string type, line; 319 ENSURE((*replayStream >> type).good() && type == "start"); 320 321 std::getline(*replayStream, line); 322 JS::RootedValue attribs(pCxPrivate->pScriptInterface->GetContext()); 323 pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); 324 delete replayStream; 325 return attribs; 326 } 327 286 328 void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1) 287 329 { 288 330 ENSURE(g_NetServer); 289 331 //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere 290 332 // (with no obvious reason). … … 415 457 bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name) 416 458 { 417 459 return SavedGames::DeleteSavedGame(name); 418 460 } 419 461 462 JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate) 463 { 464 return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface)); 465 } 466 467 bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring replayFile) 468 { 469 return VisualReplay::DeleteReplay(replayFile); 470 } 471 420 472 void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url) 421 473 { 422 474 sys_open_url(url); 423 475 } 424 476 … … 974 1026 scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGame>("SaveGame"); 975 1027 scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGamePrefix>("SaveGamePrefix"); 976 1028 scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave"); 977 1029 scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad"); 978 1030 1031 // Visual replay 1032 scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays"); 1033 scriptInterface.RegisterFunction<bool, std::wstring, &DeleteReplay>("DeleteReplay"); 1034 scriptInterface.RegisterFunction<JS::Value, std::wstring, &StartVisualReplay>("StartVisualReplay"); 1035 scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetVisualReplayAttributes>("GetVisualReplayAttributes"); 1036 979 1037 // Misc functions 980 1038 scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor"); 981 1039 scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID"); 982 1040 scriptInterface.RegisterFunction<void, int, &SetPlayerID>("SetPlayerID"); 983 1041 scriptInterface.RegisterFunction<void, std::string, &OpenURL>("OpenURL"); -
source/lib/svn_revision.cpp
1 /* Copyright (c) 201 0Wildfire Games1 /* Copyright (c) 2015 Wildfire Games 2 2 * 3 3 * Permission is hereby granted, free of charge, to any person obtaining 4 4 * a copy of this software and associated documentation files (the 5 5 * "Software"), to deal in the Software without restriction, including 6 6 * without limitation the rights to use, copy, modify, merge, publish, … … 23 23 #include "precompiled.h" 24 24 25 25 wchar_t svn_revision[] = 26 26 #include "../../build/svn_revision/svn_revision.txt" 27 27 ; 28 29 wchar_t engine_version[] = 30 #include "../../build/svn_revision/engine_version.txt" 31 ; -
source/lib/svn_revision.h
1 /* Copyright (c) 201 0Wildfire Games1 /* Copyright (c) 2015 Wildfire Games 2 2 * 3 3 * Permission is hereby granted, free of charge, to any person obtaining 4 4 * a copy of this software and associated documentation files (the 5 5 * "Software"), to deal in the Software without restriction, including 6 6 * without limitation the rights to use, copy, modify, merge, publish, … … 19 19 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 20 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 21 */ 22 22 23 23 extern wchar_t svn_revision[]; 24 extern wchar_t engine_version[]; -
source/lobby/XmppClient.cpp
1 /* Copyright (C) 201 4Wildfire Games.1 /* Copyright (C) 2015 Wildfire Games. 2 2 * This file is part of 0 A.D. 3 3 * 4 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 5 * it under the terms of the GNU General Public License as published by 6 6 * the Free Software Foundation, either version 2 of the License, or … … 16 16 */ 17 17 18 18 #include "precompiled.h" 19 19 #include "XmppClient.h" 20 20 #include "StanzaExtensions.h" 21 22 21 #include "glooxwrapper/glooxwrapper.h" 23 22 #include "i18n/L10n.h" 23 #include "lib/svn_revision.h" 24 24 #include "lib/utf8.h" 25 25 #include "ps/CLogger.h" 26 26 #include "ps/ConfigDB.h" 27 27 #include "scriptinterface/ScriptInterface.h" 28 28 … … 93 93 // if the server doesn't list any supported SASL mechanism or the response 94 94 // has been modified to exclude those. 95 95 const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain; 96 96 m_client->setSASLMechanisms(mechs); 97 97 98 // copy engine version string 99 std::wstring ev(engine_version); 100 std::string ev_str(ev.begin(), ev.end()); 98 101 m_client->registerConnectionListener(this); 99 102 m_client->setPresence(gloox::Presence::Available, -1); 100 m_client->disco()->setVersion("Pyrogenesis", "0.0.19");103 m_client->disco()->setVersion("Pyrogenesis", ev_str); 101 104 m_client->disco()->setIdentity("client", "bot"); 102 105 m_client->setCompression(false); 103 106 104 107 m_client->registerStanzaExtension(new GameListQuery()); 105 108 m_client->registerIqHandler(this, ExtGameListQuery); -
source/ps/Game.cpp
134 134 if (type == "turn") 135 135 { 136 136 u32 turn = 0; 137 137 u32 turnLength = 0; 138 138 *m_ReplayStream >> turn >> turnLength; 139 140 if (turn == 0 && turn != currentTurn) 141 LOGERROR("Looks like you tried to replay a commands.txt file of a rejoined client.\n"); 142 139 143 ENSURE(turn == currentTurn); 144 140 145 replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); 141 146 } 142 147 else if (type == "cmd") 143 148 { 144 149 player_id_t player; … … 173 178 { 174 179 m_IsReplay = true; 175 180 ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); 176 181 177 182 SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger())); 183 SetPlayerID(-1); 178 184 179 185 m_ReplayPath = replayPath; 180 186 m_ReplayStream = new std::ifstream(m_ReplayPath.c_str()); 181 187 182 188 std::string type; -
source/ps/GameSetup/GameSetup.cpp
879 879 srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! 880 880 } 881 881 882 882 bool Autostart(const CmdLineArgs& args); 883 883 884 // Returns true if and only if the user has intended to replay a file 885 bool VisualReplay(const std::string replayFile); 884 bool StartVisualReplay(const std::string replayFile); 886 885 887 886 bool Init(const CmdLineArgs& args, int flags) 888 887 { 889 888 h_mgr_init(); 890 889 … … 1078 1077 if (VfsDirectoryExists(L"maps/")) 1079 1078 CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); 1080 1079 1081 1080 try 1082 1081 { 1083 if (! VisualReplay(args.Get("replay-visual")) && !Autostart(args))1082 if (!StartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) 1084 1083 { 1085 1084 const bool setup_gui = ((flags & INIT_NO_GUI) == 0); 1086 1085 // We only want to display the splash screen at startup 1087 1086 shared_ptr<ScriptInterface> scriptInterface = g_GUI->GetScriptInterface(); 1088 1087 JSContext* cx = scriptInterface->GetContext(); … … 1475 1474 } 1476 1475 1477 1476 return true; 1478 1477 } 1479 1478 1480 bool VisualReplay(const std::string replayFile)1479 bool StartVisualReplay(const std::string replayFile) 1481 1480 { 1482 1481 if (!FileExists(OsPath(replayFile))) 1483 1482 return false; 1484 1483 1485 1484 g_Game = new CGame(false, false); 1486 g_Game->SetPlayerID(-1);1487 1485 g_Game->StartReplay(replayFile); 1488 1486 1489 1487 // TODO: Non progressive load can fail - need a decent way to handle this 1490 1488 LDR_NonprogressiveLoad(); 1491 1489 -
source/ps/Pyrogenesis.cpp
1 /* Copyright (C) 20 09Wildfire Games.1 /* Copyright (C) 2015 Wildfire Games. 2 2 * This file is part of 0 A.D. 3 3 * 4 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 5 * it under the terms of the GNU General Public License as published by 6 6 * the Free Software Foundation, either version 2 of the License, or … … 71 71 72 72 // for user convenience, bundle all logs into this file: 73 73 void psBundleLogs(FILE* f) 74 74 { 75 75 fwprintf(f, L"SVN Revision: %ls\n\n", svn_revision); 76 fwprintf(f, L"Engine Version: %ls\n\n", engine_version); 76 77 77 78 fwprintf(f, L"System info:\n\n"); 78 79 OsPath path1 = psLogDir()/"system_info.txt"; 79 80 AppendAsciiFile(f, path1); 80 81 fwprintf(f, L"\n\n====================================\n\n"); -
source/ps/Replay.cpp
52 52 } 53 53 54 54 CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) : 55 55 m_ScriptInterface(scriptInterface) 56 56 { 57 m_Stream = NULL; 58 } 59 60 CReplayLogger::~CReplayLogger() 61 { 62 delete m_Stream; 63 } 64 65 void CReplayLogger::StartGame(JS::MutableHandleValue attribs) 66 { 57 67 // Construct the directory name based on the PID, to be relatively unique. 58 68 // Append "-1", "-2" etc if we run multiple matches in a single session, 59 69 // to avoid accidentally overwriting earlier logs. 60 70 61 71 std::wstringstream name; … … 66 76 name << "-" << run; 67 77 68 78 OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt"; 69 79 CreateDirectories(path.Parent(), 0700); 70 80 m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); 71 }72 73 CReplayLogger::~CReplayLogger()74 {75 delete m_Stream;76 }77 78 void CReplayLogger::StartGame(JS::MutableHandleValue attribs)79 {80 81 *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; 81 82 } 82 83 83 84 void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands) 84 85 { … … 157 158 JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); 158 159 JSAutoRequest rq(cx); 159 160 std::string type; 160 161 while ((*m_Stream >> type).good()) 161 162 { 162 // if (turn >= 1400) break;163 164 163 if (type == "start") 165 164 { 166 165 std::string line; 167 166 std::getline(*m_Stream, line); 168 167 JS::RootedValue attribs(cx); -
source/ps/SavedGame.cpp
1 /* Copyright (C) 201 4Wildfire Games.1 /* Copyright (C) 2015 Wildfire Games. 2 2 * This file is part of 0 A.D. 3 3 * 4 4 * 0 A.D. is free software: you can redistribute it and/or modify 5 5 * it under the terms of the GNU General Public License as published by 6 6 * the Free Software Foundation, either version 2 of the License, or … … 29 29 #include "ps/Filesystem.h" 30 30 #include "ps/Game.h" 31 31 #include "ps/Mod.h" 32 32 #include "scriptinterface/ScriptInterface.h" 33 33 #include "simulation2/Simulation2.h" 34 #include "lib/svn_revision.h" 34 35 35 36 static const int SAVED_GAME_VERSION_MAJOR = 1; // increment on incompatible changes to the format 36 37 static const int SAVED_GAME_VERSION_MINOR = 0; // increment on compatible changes to the format 37 38 38 39 // TODO: we ought to check version numbers when loading files … … 296 297 JSContext* cx = scriptInterface.GetContext(); 297 298 JSAutoRequest rq(cx); 298 299 299 300 JS::RootedValue metainfo(cx); 300 301 scriptInterface.Eval("({})", &metainfo); 301 scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR); 302 scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR); 303 scriptInterface.SetProperty(metainfo, "mods" , g_modsLoaded); 302 scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR); 303 scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR); 304 305 std::wstring eng(engine_version); 306 scriptInterface.SetProperty(metainfo, "engine_version", eng); 307 308 scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded); 304 309 return metainfo; 305 310 } 306 311 -
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 22 #include "graphics/GameView.h" 23 #include "gui/GUIManager.h" 24 #include "lib/allocators/shared_ptr.h" 25 #include "lib/svn_revision.h" 26 #include "lib/utf8.h" 27 #include "ps/CLogger.h" 28 #include "ps/Filesystem.h" 29 #include "ps/Game.h" 30 #include "scriptinterface/ScriptInterface.h" 31 32 OsPath VisualReplay::GetDirectoryName() 33 { 34 return OsPath(psLogDir() / L"sim_log"); 35 } 36 37 JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface) 38 { 39 // TODO: enhancement: load from cache 40 41 TIMER(L"GetReplays"); 42 JSContext* cx = scriptInterface.GetContext(); 43 JSAutoRequest rq(cx); 44 45 OsPath root = GetDirectoryName(); 46 int i = 0; 47 DirectoryNames directories; 48 JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); 49 GetDirectoryEntries(root, NULL, &directories); 50 for (auto& directory : directories) 51 { 52 OsPath replayFile(root / directory / L"commands.txt"); 53 54 if (!FileExists(replayFile)) 55 continue; 56 57 // Get fileinfo (size, timestamp) 58 CFileInfo fileInfo; 59 GetFileInfo(replayFile, &fileInfo); 60 61 u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath) 62 u64 fileSize = (u64)fileInfo.Size(); 63 64 // Don't list empty files 65 if (fileSize == 0) 66 continue; 67 68 // Extract metadata 69 u64 duration = 0; 70 std::string header(""); 71 if (!LoadReplayData(scriptInterface, replayFile, header, duration)) 72 continue; 73 74 // Don't list too short replays 75 if (duration < 5) 76 continue; 77 78 JS::RootedValue replay(cx); 79 scriptInterface.Eval("({})", &replay); 80 //scriptInterface.SetProperty(replay, "file", replayFile); 81 scriptInterface.SetProperty(replay, "directory", directory); 82 scriptInterface.SetProperty(replay, "timestamp", fileTime); // TODO: enhancement: save the timestamp to the replayfile itself scriptInterface.SetProperty(replay, "mapName", "Unknown Nomad"); 83 scriptInterface.SetProperty(replay, "header", header); 84 scriptInterface.SetProperty(replay, "duration", duration); 85 86 JS_SetElement(cx, replays, i++, replay); 87 } 88 return JS::ObjectValue(*replays); 89 } 90 91 // Works similar to CGame::LoadReplayData() 92 // Extracts metadata from file header, loads commands and computes duration of the game 93 int VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile, std::string& header, u64& duration) 94 { 95 if (!FileExists(replayFile)) 96 return false; 97 98 // Open file 99 CStrW filenameW = replayFile.string(); 100 std::string filename( filenameW.begin(), filenameW.end() ); 101 std::ifstream* replayStream = new std::ifstream(filename.c_str()); 102 103 // TODO: (debug only) when hotloading replay.js, the stream is not good anymore somehow and it crashes 104 105 // File must begin with "start" 106 std::string type; 107 ENSURE((*replayStream >> type).good() && type == "start"); 108 109 // Save header to parse it later in JS 110 std::getline(*replayStream, header); 111 112 // Parse commands & turns 113 u32 currentTurn = 0; 114 u32 currentMinute = 0; 115 duration = 0; 116 std::vector<player_id_t> defeatedPlayers; 117 std::map<player_id_t, u16> currentCommandCount; 118 std::map<player_id_t, u16> commandsPerMinute; 119 while ((*replayStream >> type).good()) 120 { 121 if (type == "turn") 122 { 123 // Store turn & turn length 124 u32 turn = 0; 125 u32 turnLength = 0; 126 *replayStream >> turn >> turnLength; 127 if (turn != currentTurn) // happens for replays of rejoined clients 128 return false; 129 130 // Compute game duration 131 duration += turnLength; 132 133 // Compute commands per minute 134 u32 min = duration / 1000 / 60; 135 if (min > currentMinute) 136 { 137 //commandsPerMinute[player][currentMinute] = currentCommandCount[player]; 138 currentMinute = min; 139 } 140 } 141 else if (type == "cmd") 142 { 143 // Extract player id 144 player_id_t player; 145 *replayStream >> player; 146 147 // Increase command count 148 if (currentCommandCount.find(player) == currentCommandCount.end()) 149 currentCommandCount[player] = 1; 150 else 151 currentCommandCount[player]++; 152 153 // Store command 154 std::string command; 155 std::getline(*replayStream, command); 156 157 // Parse command 158 JS::RootedValue commandData(scriptInterface.GetContext()); 159 std::wstring commandType; 160 if (!scriptInterface.ParseJSON(command, &commandData)) 161 return false; 162 scriptInterface.GetProperty(commandData, "type", commandType); 163 164 // Save defeated players {"type":"defeat-player","playerId":2} 165 if (commandType == L"defeat-player") 166 { 167 JS::RootedValue defeatedPlayer(scriptInterface.GetContext()); 168 scriptInterface.GetProperty(commandData, "playerId", &defeatedPlayer); 169 //defeatedPlayers.push_back(defeatedPlayer); 170 } 171 } 172 else if (type == "hash" || type == "hash-quick") 173 { 174 // skip hash values 175 std::string replayHash; 176 *replayStream >> replayHash; 177 } 178 else if (type == "end") 179 { 180 currentTurn++; 181 } 182 else 183 { 184 LOGERROR("Unrecognized replay data '%s'", type); 185 } 186 } 187 duration = duration / 1000; 188 // finalReplayTurn = currentTurn; 189 // TODO: compute average turn time, if turn length varies 190 return true; 191 } 192 193 bool VisualReplay::DeleteReplay(const std::wstring replayDirectory) 194 { 195 if (replayDirectory == L"") 196 return false; 197 198 const OsPath directory = OsPath(GetDirectoryName() / replayDirectory); 199 return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; 200 } -
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 29 namespace VisualReplay 30 { 31 32 /** 33 * Start replaying the given commands.txt. 34 * 35 * @param name filename of the commands.txt file (including path) 36 * @param scriptInterface 37 * @param[out] savedState serialized simulation state stored as string of bytes, 38 * loaded from simulation.dat inside the archive. 39 * @return INFO::OK if successfully loaded, else an error Status 40 */ 41 Status Load(const std::wstring& name, ScriptInterface& scriptInterface, std::string& savedState); 42 43 /** 44 * Returns the path to the sim-log directory (that contains the directories with the replay files. 45 * 46 * @param scriptInterface the ScriptInterface in which to create the return data. 47 * @return OsPath with an absolte file path 48 */ 49 OsPath GetDirectoryName(); 50 51 /** 52 * Get a list of replays (filenames and timestamps [later more information like playernames]) for GUI script usage 53 * 54 * @param scriptInterface the ScriptInterface in which to create the return data. 55 * @return array of objects containing saved game data 56 */ 57 JS::Value GetReplays(ScriptInterface& scriptInterface); 58 59 /** 60 * Parses a commands.txt file and extracts metadata. 61 */ 62 int LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile, std::string& header, u64& duration); 63 64 /** 65 * Permanently deletes the visual replay (including the parent directory) 66 * 67 * @param replayFile path to commands.txt, whose parent directory will be deleted 68 * @return true if deletion was successful, or false on error 69 */ 70 bool DeleteReplay(const std::wstring replayFile); 71 } 72 73 #endif