Ticket #3258: replay_menu_wip_v6_r16812.patch
File replay_menu_wip_v6_r16812.patch, 71.3 KB (added by , 9 years ago) |
---|
-
binaries/data/mods/public/gui/common/functions_utility.js
81 81 return text; 82 82 83 83 return text.substr(0, 255).replace(/\\/g, "\\\\").replace(/\[/g, "\\["); 84 84 } 85 85 86 //==================================================================== 87 88 function capitalizeFirstLetter(text) { 89 return text.charAt(0).toUpperCase() + text.slice(1); 90 } 91 86 92 // ==================================================================== 87 93 88 94 // Load default player data, for when it's not otherwise specified 89 95 function initPlayerDefaults() 90 96 { -
binaries/data/mods/public/gui/common/functions_utility_loadsave.js
23 23 /** 24 24 * Check the version compatibility between the saved game to be loaded and the engine 25 25 */ 26 26 function hasSameVersion(metadata, engineInfo) 27 27 { 28 return (metadata.version_major == engineInfo.version_major );28 return (metadata.version_major == engineInfo.version_major && metadata.hasOwnProperty("engine_version") && metadata.engine_version == engineInfo.engine_version); 29 29 } 30 30 31 31 /** 32 32 * Check the mod compatibility between the saved game to be loaded and the engine 33 33 */ -
binaries/data/mods/public/gui/gamesetup/gamesetup.js
487 487 } 488 488 Engine.SwitchGuiPage("page_loading.xml", { 489 489 "attribs": g_GameAttributes, 490 490 "isNetworked" : g_IsNetworked, 491 491 "playerAssignments": g_PlayerAssignments, 492 "isController": g_IsController 492 "isController": g_IsController, 493 "g_IsReplay" : false 493 494 }); 494 495 break; 495 496 496 497 case "chat": 497 498 addChatMessage({ "type": "message", "guid": message.guid, "text": message.text }); -
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 = []; // All replays found including metadata 2 var g_ReplaysFiltered = []; // List of replays after filtering 3 var g_Playernames = []; // All playernames of all replays, used for autocomplete 4 var g_ReplayListSortBy = "name"; // Used for sorting 5 var g_ReplayListOrder = -1; // Used for sorting 6 var g_mapSizes = initMapSizes(); // already translated 7 var g_CivData = loadCivData(); 8 var g_EngineInfo = Engine.GetEngineInfo(); 9 var g_DefaultPlayerData = initPlayerDefaults(); 10 var g_maxPlayerlistChars = 70; 11 var g_DurationFilterMin = [ 0, 0, 15, 30, 45, 60, 90, 120]; 12 var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120, -1]; 13 14 function init() 15 { 16 loadReplays(); 17 updateReplayList(); 18 } 19 20 function loadReplays() 21 { 22 g_Playernames = []; 23 g_Replays = Engine.GetReplays(); 24 for (let replay of g_Replays) 25 { 26 // TODO: enhancement: remove those copies for better performance 27 replay.settings = replay.attribs.settings; 28 replay.playerData = replay.settings.PlayerData; 29 30 // Use time saved in file, otherwise file mod date 31 replay.timestamp = replay.attribs.hasOwnProperty("timestamp") ? replay.attribs.timestamp : replay.filemod_timestamp; 32 33 // Skirmish maps don't have that attribute 34 if (!replay.settings.hasOwnProperty("Size")) 35 replay.settings.Size = -1; 36 37 for (var i in replay.playerData) 38 { 39 // I've encountered a file where 'null' was added to the playerData array... 40 if (!replay.playerData[i]) 41 { 42 error("Replay " + replay.file + " has bogus player data!"); 43 continue; 44 } 45 var name = replay.playerData[i].Name; 46 if (g_Playernames.indexOf(name) == -1) 47 g_Playernames.push({"name": name}); 48 } 49 } 50 } 51 52 function updateReplayListOrderSelection() 53 { 54 g_ReplayListSortBy = Engine.GetGUIObjectByName("replaySelection").selected_column; 55 g_ReplayListOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order; 56 applyFilters(); 57 } 58 59 function updateReplayList() 60 { 61 var replaySelection = Engine.GetGUIObjectByName("replaySelection"); 62 63 // Filters depend on the replay list 64 initFilters(); 65 66 if (g_Replays.length == 0) 67 replaySelection.selected = -1; 68 69 // Sort replays 70 g_Replays.sort(function(a,b) 71 { 72 var cmpA, cmpB; 73 switch (g_ReplayListSortBy) 74 { 75 case 'name': 76 cmpA = a.timestamp; 77 cmpB = b.timestamp; 78 break; 79 case 'duration': 80 cmpA = a.duration; 81 cmpB = b.duration; 82 break; 83 case 'players': 84 cmpA = a.playerData.length; 85 cmpB = b.playerData.length; 86 break; 87 case 'mapName': 88 cmpA = getReplayMapName(a); 89 cmpB = getReplayMapName(b); 90 break; 91 case 'mapSize': 92 cmpA = a.settings.Size; 93 cmpB = b.settings.Size; 94 break; 95 case 'popCapacity': 96 cmpA = a.settings.PopulationCap; 97 cmpB = b.settings.PopulationCap; 98 break; 99 } 100 101 // Sort by selected column 102 if (cmpA < cmpB) 103 return -g_ReplayListOrder; 104 else if (cmpA > cmpB) 105 return g_ReplayListOrder; 106 107 // Sort by date/time as a tiebreaker and keep most recent replays at the top 108 if (a.timestamp < b.timestamp) 109 return 1; 110 else if (a.timestamp > b.timestamp) 111 return -1; 112 113 return 0; 114 }); 115 116 // Filter replays and create GUI list data 117 var replayListLabels = []; 118 var replayListDirectories = []; 119 var list_name = []; 120 var list_players = []; 121 var list_mapName = []; 122 var list_mapSize = []; 123 var list_popCapacity = []; 124 var list_duration = []; 125 g_ReplaysFiltered = []; 126 for (var replay of g_Replays) 127 { 128 if (filterReplay(replay)) 129 continue; 130 131 g_ReplaysFiltered.push(replay); 132 replayListLabels.push(replay.directory); 133 replayListDirectories.push(replay.directory); 134 135 list_name.push(Greyout(replay, getReplayDateTime(replay))); 136 list_players.push(Greyout(replay, getReplayPlayernames(replay, true))); 137 list_mapName.push(Greyout(replay, getReplayMapName(replay))); 138 list_mapSize.push(Greyout(replay, getReplayMapSizeText(replay.settings.Size))); 139 list_popCapacity.push(Greyout(replay, getReplayPopCap(replay))); 140 list_duration.push(Greyout(replay, getReplayDuration(replay))); 141 } 142 143 // TODO: enhancement: remember last selection, like #3244 144 if (replaySelection.selected >= list_name.length) 145 replaySelection.selected = -1; 146 147 // Update list 148 replaySelection.list_name = list_name; 149 replaySelection.list_players = list_players; 150 replaySelection.list_mapName = list_mapName; 151 replaySelection.list_mapSize = list_mapSize; 152 replaySelection.list_popCapacity = list_popCapacity; 153 replaySelection.list_duration = list_duration; 154 155 replaySelection.list = replayListLabels; 156 replaySelection.list_data = replayListDirectories; 157 } 158 159 function Greyout(replay, text) 160 { 161 if (isReplayCompatible(replay)) 162 return text; 163 else 164 return '[color="128 128 128"]' + text + '[/color]'; 165 } 166 167 function getReplayDateTime(replay) 168 { 169 return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm")) 170 } 171 172 function getReplayPlayernames(replay, shorten) 173 { 174 // Extract playernames 175 var playernames = []; 176 for (let i in replay.playerData) 177 { 178 // I've encountered a file where 'null' was added to the playerData array... 179 if (!replay.playerData[i]) 180 continue; 181 182 var playername = escapeText(replay.playerData[i].Name); 183 184 // TODO: enhancement: colorize playernames like in lobby using colorPlayerName 185 // #3205 moves the function to common/color.js 186 playernames.push(playername); 187 } 188 189 if (!shorten) 190 return playernames; 191 192 playernames = playernames.join(", "); 193 194 // Shorten if too long 195 if (playernames.length > g_maxPlayerlistChars) 196 return playernames.substr(0, g_maxPlayerlistChars) + "..."; 197 else 198 return playernames; 199 } 200 201 function getReplayMapName(replay) 202 { 203 return escapeText(translate(replay.settings.Name)); 204 } 205 206 function getReplayMapSizeText(tiles) 207 { 208 var index = g_mapSizes.tiles.indexOf(tiles); 209 if (index > -1) 210 return g_mapSizes.shortNames[index] 211 else 212 return translateWithContext("map size", "Default"); 213 } 214 215 function getReplayPopCap(replay) 216 { 217 if (replay.settings.PopulationCap == 10000) 218 return translateWithContext("population capacity", "Unlimited"); 219 else 220 return replay.settings.PopulationCap; 221 } 222 223 function getReplayDuration(replay) 224 { 225 return timeToString(replay.duration * 1000); 226 } 227 228 function getReplayTeamText(replay) 229 { 230 var spoiler = Engine.GetGUIObjectByName("showSpoiler").checked; 231 232 // Extract players and civs by team 233 var teams = {}; 234 var teamCount = 0; 235 for (var i in replay.playerData) 236 { 237 var playerData = replay.playerData[i]; 238 239 // I've encountered a file where 'null' was added to the playerData array... 240 if (!playerData) 241 continue; 242 243 var team = playerData.Team; 244 // TODO: enhancement: too few contrast, see #3205 245 var col = playerData.Color ? playerData.Color : g_DefaultPlayerData[parseInt(i)+1].Color; 246 var civ = g_CivData[playerData.Civ]; 247 // TODO: requirement: refactoring with aiconfig.js 248 var aiDiff = [translateWithContext("aiDiff", "Sandbox"), translateWithContext("aiDiff", "Very Easy"), translateWithContext("aiDiff", "Easy"), translateWithContext("aiDiff", "Medium"), translateWithContext("aiDiff", "Hard"), translateWithContext("aiDiff", "Very Hard")]; 249 250 // Create player text 251 var playername = '[color="' + col.r + " " + col.g + " " + col.b + '"]' + escapeText(playerData.Name) + "[/color]"; 252 var playerDetails = translate(civ.Name); 253 if (playerData.AI != "") 254 playerDetails += ", " + sprintf(translateWithContext("replay AI text", "%(difficulty)s %(name)s AI"), { "difficulty": aiDiff[playerData.AIDiff], "name": capitalizeFirstLetter(playerData.AI) }); 255 if (spoiler && replay.resignedPlayers.indexOf(parseInt(i) + 1) > -1) 256 playerDetails += ", resigned"; 257 258 // Add player text to playerlist 259 var player = playername + escapeText(" (" + playerDetails + ")"); 260 if (teams.hasOwnProperty(team)) 261 { 262 teams[team].push(player); 263 } 264 else 265 { 266 teams[team] = [player]; 267 teamCount++; 268 } 269 } 270 271 // Create player info text sorted by team 272 var teamString = ""; 273 for(var team in teams) 274 { 275 if (teamCount > 1) 276 { 277 if (team == -1) 278 teamString += '[font="sans-bold-14"]No Team:[/font]\n'; 279 else 280 teamString += '[font="sans-bold-14"]Team ' + (parseInt(team) + 1) + "[/font]:\n"; 281 } 282 teamString += teams[team].join("\n") + "\n"; 283 } 284 285 return teamString; 286 } 287 288 function isReplayCompatible(replay) 289 { 290 if (!isReplayCompatible_EngineVersion(replay)) 291 return false; 292 293 // Now check mods 294 if (replay.attribs.mods) 295 var gameMods = replay.attribs.mods; 296 else 297 var gameMods = []; 298 299 if (gameMods.length != g_EngineInfo.mods.length) 300 return false; 301 302 for (var i = 0; i < gameMods.length; ++i) 303 if (gameMods[i] != g_EngineInfo.mods[i]) 304 return false; 305 306 return true; 307 } 308 309 function isReplayCompatible_EngineVersion(replay) 310 { 311 return replay.attribs.hasOwnProperty("engine_version") 312 && replay.attribs.engine_version == g_EngineInfo.engine_version; 313 } 314 315 function initFilters() 316 { 317 initDateFilter(); 318 initMapNameFilter(); 319 initMapSizeFilter(); 320 initPopCapFilter(); 321 initDurationFilter(); 322 } 323 324 function initDateFilter() 325 { 326 var months = ["Any"]; 327 for (var replay of g_Replays) 328 { 329 var month = getDateTimeFilterVal(replay); 330 331 if (months.indexOf(month) == -1) 332 months.push(month); 333 } 334 335 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 336 dateTimeFilter.list = months; 337 dateTimeFilter.list_data = months; 338 339 if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= months.length) 340 dateTimeFilter.selected = 0; 341 } 342 343 function getDateTimeFilterVal(replay) 344 { 345 var date = new Date(replay.timestamp * 1000); 346 return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2) 347 } 348 349 function initMapSizeFilter() 350 { 351 // Get tilecounts actually used by maps 352 var tiles = []; 353 for (let replay of g_Replays) 354 { 355 if (tiles.indexOf(replay.settings.Size) == -1) 356 tiles.push(replay.settings.Size); 357 } 358 tiles.sort(); 359 360 // Add translated names 361 var names = []; 362 for (var size of tiles) 363 names.push(getReplayMapSizeText(size)); 364 365 // Add "Any" 366 names.unshift(translateWithContext("map size", "Any")); 367 tiles.unshift(""); 368 369 // Save values to filter 370 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 371 mapSizeFilter.list = names; 372 mapSizeFilter.list_data = tiles; 373 374 if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= tiles.length) 375 mapSizeFilter.selected = 0; 376 } 377 378 function initMapNameFilter() 379 { 380 var mapNames = []; 381 for (var replay of g_Replays) 382 { 383 var mapName = escapeText(replay.settings.Name); 384 385 if (mapNames.indexOf(mapName) == -1) 386 mapNames.push(mapName); 387 } 388 389 mapNames.sort(); 390 mapNames.unshift("Any"); 391 392 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 393 mapNameFilter.list = mapNames; 394 mapNameFilter.list_data = mapNames; 395 396 if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNames.length) 397 mapNameFilter.selected = 0; 398 } 399 400 function initPopCapFilter() 401 { 402 var popCaps = []; 403 for (var replay of g_Replays) 404 { 405 var popCap = getReplayPopCap(replay); 406 if (popCaps.indexOf(popCap) == -1) 407 popCaps.push(popCap); 408 } 409 popCaps.sort(); 410 411 popCaps.unshift("Any"); 412 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 413 populationFilter.list = popCaps; 414 populationFilter.list_data = popCaps; 415 416 if (populationFilter.selected == -1 || populationFilter.selected >= popCaps.length) 417 populationFilter.selected = 0; 418 } 419 420 function initDurationFilter() 421 { 422 var data = [-1]; 423 var labels = ["Any"]; 424 425 for (var i in g_DurationFilterMin) 426 { 427 if (i == 0) 428 continue; 429 else if (i == 1) 430 var label = sprintf(translateWithContext("duration filter", "< %(min)s min"), { "min": g_DurationFilterMax[i] }); 431 else if (i == g_DurationFilterMin.length -1) 432 var label = sprintf(translateWithContext("duration filter", "> %(min)s min"), { "min": g_DurationFilterMin[i] }); 433 else 434 var label = sprintf(translateWithContext("duration filter", "%(min1)s - %(min2)s min"), { "min1": g_DurationFilterMin[i], "min2": g_DurationFilterMax[i] }); 435 436 data.push(i); 437 labels.push(label); 438 } 439 440 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 441 durationFilter.list = labels; 442 durationFilter.list_data = data; 443 444 if (durationFilter.selected == -1 || durationFilter.selected >= data.length) 445 durationFilter.selected = 0; 446 } 447 448 function applyFilters() 449 { 450 // Update the list of replays 451 updateReplayList(); 452 453 // Update info box about the replay currently selected 454 updateReplaySelection(); 455 } 456 457 // Returns true if the replay should not be listed. 458 function filterReplay(replay) 459 { 460 var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter"); 461 var playersFilter = Engine.GetGUIObjectByName("playersFilter"); 462 var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter"); 463 var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); 464 var populationFilter = Engine.GetGUIObjectByName("populationFilter"); 465 var durationFilter = Engine.GetGUIObjectByName("durationFilter"); 466 var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter"); 467 468 // Check for compability first (most likely to filter) 469 if (compabilityFilter.checked && !isReplayCompatible(replay)) 470 return true; 471 472 // Filter date/time (select a month) 473 if (dateTimeFilter.selected > 0) 474 { 475 let selectedMonth = dateTimeFilter.list_data[dateTimeFilter.selected]; 476 if (getDateTimeFilterVal(replay) != selectedMonth) 477 return true; 478 } 479 480 // Filter selected players 481 let playerText = playersFilter.caption; 482 if (playerText.length) 483 { 484 // Player and botnames can contain spaces 485 // We just check if all words of all players are somewhere in the playerlist 486 playerText = playerText.toLowerCase().split(" "); 487 let replayPlayers = replay.playerData.map(function(player){ return player.Name.toLowerCase() }).join(" "); 488 for (let word of playerText) 489 { 490 if (replayPlayers.indexOf(word) == -1) 491 return true; 492 } 493 } 494 495 // Filter map name 496 if (mapNameFilter.selected > 0) 497 { 498 if (getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected]) 499 return true; 500 } 501 502 // Filter map size 503 if (mapSizeFilter.selected > 0) 504 { 505 let selectedMapSize = mapSizeFilter.list_data[mapSizeFilter.selected]; 506 if (replay.settings.Size != selectedMapSize) 507 return true; 508 } 509 510 // Filter population capacity 511 if (populationFilter.selected > 0 && 512 getReplayPopCap(replay) != populationFilter.list_data[populationFilter.selected]) 513 { 514 return true; 515 } 516 517 // Filter game duration 518 if (durationFilter.selected > 0) 519 { 520 let minutes = replay.duration / 60; 521 let min = g_DurationFilterMin[durationFilter.selected]; 522 let max = g_DurationFilterMax[durationFilter.selected]; 523 524 if (minutes < min || (max > -1 && minutes > max)) 525 return true; 526 } 527 528 return false; 529 } 530 531 function updateReplaySelection() 532 { 533 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 534 var replaySelected = selected > -1; 535 536 Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected; 537 Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected; 538 Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected; 539 Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected; 540 Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected; 541 542 if (!replaySelected) 543 return; 544 545 var replay = g_ReplaysFiltered[selected]; 546 547 // Load map data 548 if (replay.settings.mapType == "random" && replay.attribs.map == "random") 549 var mapData = {"settings": {"Description": translate("A randomly selected map.")}}; 550 else if (replay.settings.mapType == "random" && Engine.FileExists(replay.attribs.map + ".json")) 551 var mapData = Engine.ReadJSONFile(replay.attribs.map + ".json"); 552 else if (Engine.FileExists(replay.attribs.map + ".xml")) 553 var mapData = Engine.LoadMapSettings(replay.attribs.map + ".xml"); 554 else 555 // Warn the player if we can't find the map. 556 warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: replay.attribs.map })); 557 558 // Load map description 559 if (mapData && mapData.settings.Description) 560 var mapDescription = translate(mapData.settings.Description); 561 else 562 var mapDescription = translate("Sorry, no description available."); 563 564 // Load map preview image 565 if (mapData && mapData.settings.Preview) 566 var mapPreview = mapData.settings.Preview; 567 else 568 var mapPreview = "nopreview.png"; 569 570 // Update GUI, compare with gamesetup.js for translations 571 Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.settings.Name); 572 Engine.GetGUIObjectByName("sgMapSize").caption = getReplayMapSizeText(replay.settings.Size); 573 Engine.GetGUIObjectByName("sgMapType").caption = translateWithContext("map",capitalizeFirstLetter(replay.settings.mapType)); 574 Engine.GetGUIObjectByName("sgVictory").caption = translate(capitalizeFirstLetter(replay.settings.GameType)); 575 Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.playerData.length; 576 Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay); 577 Engine.GetGUIObjectByName("sgMapDescription").caption = mapDescription; 578 Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapPreview; 579 } 580 581 function startReplay() 582 { 583 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 584 if (selected == -1) 585 return; 586 587 var replay = g_ReplaysFiltered[selected]; 588 if (isReplayCompatible(replay)) 589 reallyStartVisualReplay(replay.directory); 590 else 591 displayReplayCompatibilityError(replay); 592 } 593 594 function reallyStartVisualReplay(replayDirectory) 595 { 596 Engine.StartVisualReplay(replayDirectory); 597 Engine.SwitchGuiPage("page_loading.xml", { 598 "attribs": Engine.GetReplayAttributes(replayDirectory), 599 "isNetworked" : false, 600 "playerAssignments": {}, 601 "savedGUIData": "", 602 "isReplay" : true 603 }); 604 } 605 606 function displayReplayCompatibilityError(replay) 607 { 608 if (isReplayCompatible_EngineVersion(replay)) 609 { 610 if (replay.attribs.mods) 611 var gameMods = replay.attribs.mods; 612 else 613 var gameMods = []; 614 615 var errMsg = translate("The replay is compatible with your version of the game, but you don't have the same mods as in the replay.") + "\n"; 616 errMsg += sprintf(translate("Mods enabled in the replay: %(mods)s"), { "mods": gameMods.join(",") }) + "\n"; 617 errMsg += sprintf(translate("Currently enabled mods: %(mods)s"), { "mods": g_EngineInfo.mods.join(",") }); 618 } 619 else 620 { 621 var errMsg = translate("This replay is not compatible with your version of the game!"); 622 } 623 624 var btCaptions = [translate("Ok")]; 625 var btCode = [null]; 626 messageBox(500, 200, errMsg, translate("REPLAY INCOMPATIBLE"), 0, btCaptions, btCode); 627 } 628 629 function showReplaySummary() 630 { 631 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 632 if (selected == -1) 633 return; 634 635 var replay = g_ReplaysFiltered[selected]; 636 var summary = Engine.GetReplaySummary(replay.directory); 637 638 if (Object.keys(summary) == 0) 639 { 640 var btCaptions = [translate("Ok")]; 641 var btCode = [null]; 642 messageBox(500, 200, translateWithContext("replay", "No summary data found!"), translate("ERROR"), 0, btCaptions, btCode); 643 return; 644 } 645 646 summary.isReplay = true; 647 summary.gameResult = translateWithContext("replay", "Scores at the end of the game."); 648 649 // TODO: use Engine.PushGuiPage instead? 650 Engine.SwitchGuiPage("page_summary.xml", summary); 651 } 652 653 function deleteReplay() 654 { 655 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 656 if (selected == -1) 657 return; 658 659 // Ask for confirmation 660 var replay = g_ReplaysFiltered[selected]; 661 var btCaptions = [translate("Yes"), translate("No")]; 662 var btCode = [function(){ reallyDeleteReplay(replay.directory); }, null]; 663 messageBox(500, 200, translate("Are you sure to delete this replay permanently?") + "\n" + replay.file, translate("DELETE"), 0, btCaptions, btCode); 664 } 665 666 function deleteReplayWithoutConfirmation() 667 { 668 var selected = Engine.GetGUIObjectByName("replaySelection").selected; 669 if (selected > -1) 670 reallyDeleteReplay(g_ReplaysFiltered[selected].directory); 671 } 672 673 function reallyDeleteReplay(replayDirectory) 674 { 675 if (!Engine.DeleteReplay(replayDirectory)) 676 error(sprintf("Could not delete replay '%(id)s'", { id: replayDirectory })); 677 678 // Refresh replay list 679 init(); 680 } 681 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_civinfo.js" /> 6 <script file="gui/common/functions_global_object.js" /> 7 <script file="gui/common/functions_utility.js" /> 8 <script file="gui/replay/replay.js" /> 9 10 <object type="image" style="ModernWindow" size="0 0 100% 100%" name="replayWindow"> 11 12 <object style="ModernLabelText" type="text" size="50%-128 0%+4 50%+128 36"> 13 <translatableAttribute id="caption">Replay Games</translatableAttribute> 14 </object> 15 16 <!-- Left Panel: Filters & Replay List --> 17 <object name="leftPanel" size="3% 5% 100%-255 100%-80"> 18 19 <!-- Filters --> 20 <object name="filterPanel" size="0 0 100% 24"> 21 <object name="dateTimeFilter" 22 type="dropdown" 23 style="ModernDropDown" 24 size="5 0 110-10 100%" 25 font="sans-bold-13"> 26 <action on="SelectionChange">applyFilters();</action> 27 </object> 28 <object name="playersFilter" 29 type="input" 30 style="ModernInput" 31 size="110-5 0 110+400-10 100%" 32 font="sans-bold-13"> 33 <action on="Press">applyFilters();</action> 34 <action on="Tab">autoCompleteNick("playersFilter", g_Playernames);</action> 35 </object> 36 <object name="mapNameFilter" 37 type="dropdown" 38 style="ModernDropDown" 39 size="110+400-5 0 110+400+140-10 100%" 40 font="sans-bold-13"> 41 <action on="SelectionChange">applyFilters();</action> 42 </object> 43 <object name="mapSizeFilter" 44 type="dropdown" 45 style="ModernDropDown" 46 size="110+400+140-5 0 110+400+140+80-10 100%" 47 font="sans-bold-13"> 48 <action on="SelectionChange">applyFilters();</action> 49 </object> 50 <object name="populationFilter" 51 type="dropdown" 52 style="ModernDropDown" 53 size="110+400+140+80-5 0 110+400+140+80+80-10 100%" 54 font="sans-bold-13"> 55 <action on="SelectionChange">applyFilters();</action> 56 </object> 57 <object name="durationFilter" 58 type="dropdown" 59 style="ModernDropDown" 60 size="110+400+140+80+80-5 0 110+400+140+80+80+80-10 100%" 61 font="sans-bold-13"> 62 <action on="SelectionChange">applyFilters();</action> 63 </object> 64 <object name="compabilityFilter" 65 type="checkbox" 66 checked="true" 67 style="ModernTickBox" 68 size="110+400+140+80+80+73 4 110+400+140+80+80+70+20 100%" 69 font="sans-bold-13"> 70 <action on="Press">applyFilters();</action> 71 </object> 72 <object type="text" size="110+400+140+80+80+80+10 2 100% 100%" text_align="left" textcolor="white"> 73 <translatableAttribute id="caption">Compatible</translatableAttribute> 74 </object> 75 </object> 76 77 <!-- Replay list --> 78 <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"> 79 <action on="SelectionChange">updateReplaySelection();</action> 80 <action on="SelectionColumnChange">updateReplayListOrderSelection();</action> 81 <!-- Columns --> 82 <!-- 0ad crashes if there is no column with the id "name"! --> 83 <def id="name" color="0 128 128" width="110"> 84 <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute> 85 </def> 86 <def id="players" color="0 128 128" width="400"> 87 <translatableAttribute id="heading" context="replay">Players</translatableAttribute> 88 </def> 89 <def id="mapName" color="0 128 128" width="140"> 90 <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute> 91 </def> 92 <def id="mapSize" color="0 128 128" width="80"> 93 <translatableAttribute id="heading" context="replay">Size</translatableAttribute> 94 </def> 95 <def id="popCapacity" color="0 128 128" width="80"> 96 <translatableAttribute id="heading" context="replay">Population</translatableAttribute> 97 </def> 98 <def id="duration" color="0 128 128" width="80"> 99 <translatableAttribute id="heading" context="replay">Duration</translatableAttribute> 100 </def> 101 </object> 102 </object> 103 104 <!-- Right Panel: Replay Details --> 105 <object name="rightPanel" size="100%-250 30 100%-20 100%-20" > 106 107 <object name="replayInfoEmpty" size="0 0 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false"> 108 <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/> 109 <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%"> 110 <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/> 111 </object> 112 </object> 113 114 <object name="replayInfo" size="0 0 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="true"> 115 116 <!-- Map Name --> 117 <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/> 118 119 <!-- Map Preview --> 120 <object name="sgMapPreview" size="5 25 100%-5 190" type="image" sprite=""/> 121 122 <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/> 123 124 <!-- Map Type --> 125 <object size="5 195 50% 225" type="image" sprite="ModernItemBackShadeLeft"> 126 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 127 <translatableAttribute id="caption">Map Type:</translatableAttribute> 128 </object> 129 </object> 130 <object size="50% 195 100%-5 225" type="image" sprite="ModernItemBackShadeRight"> 131 <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 132 </object> 133 134 <object size="5 224 100%-5 225" type="image" sprite="ModernWhiteLine" z="25"/> 135 136 <!-- Map Size --> 137 <object size="5 225 50% 255" type="image" sprite="ModernItemBackShadeLeft"> 138 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 139 <translatableAttribute id="caption">Map Size:</translatableAttribute> 140 </object> 141 </object> 142 <object size="50% 225 100%-5 255" type="image" sprite="ModernItemBackShadeRight"> 143 <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 144 </object> 145 146 <object size="5 254 100%-5 255" type="image" sprite="ModernWhiteLine" z="25"/> 147 148 <!-- Victory Condition --> 149 <object size="5 255 50% 285" type="image" sprite="ModernItemBackShadeLeft"> 150 <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right"> 151 <translatableAttribute id="caption">Victory:</translatableAttribute> 152 </object> 153 </object> 154 <object size="50% 255 100%-5 285" type="image" sprite="ModernItemBackShadeRight"> 155 <object name="sgVictory" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/> 156 </object> 157 158 <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/> 159 160 <!-- Map Description --> 161 <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 70%"> 162 <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/> 163 </object> 164 165 <object type="image" sprite="ModernDarkBoxWhite" size="3% 70%+5 97% 100%-30"> 166 <!-- Number of Players --> 167 <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText"> 168 <translatableAttribute id="caption">Players:</translatableAttribute> 169 </object> 170 <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/> 171 172 <!-- Player Names --> 173 <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/> 174 </object> 175 176 <object name="showSpoiler" 177 type="checkbox" 178 checked="false" 179 style="ModernTickBox" 180 size="10 100%-27 30 100%" 181 font="sans-bold-13"> 182 <action on="Press">updateReplaySelection();</action> 183 </object> 184 <object type="text" size="30 100%-28 100% 100%" text_align="left" textcolor="white"> 185 <translatableAttribute id="caption">Spoiler</translatableAttribute> 186 </object> 187 188 </object> 189 </object> 190 191 <!-- Bottom Panel: Buttons. --> 192 <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" > 193 194 <object type="button" style="StoneButton" size="0%+25 0 17%+25 100%"> 195 <translatableAttribute id="caption">Main Menu</translatableAttribute> 196 <action on="Press"> 197 Engine.SwitchGuiPage("page_pregame.xml"); 198 </action> 199 </object> 200 201 <object name="deleteReplayButton" type="button" style="StoneButton" size="20%+25 0 37%+25 100%" hotkey="session.savedgames.delete"> 202 <translatableAttribute id="caption">Delete</translatableAttribute> 203 <action on="Press"> 204 if (!this.enabled) 205 return; 206 if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation")) 207 deleteReplayWithoutConfirmation(); 208 else 209 deleteReplay(); 210 </action> 211 </object> 212 213 <object name="summaryButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%"> 214 <translatableAttribute id="caption">Summary</translatableAttribute> 215 <action on="Press"> 216 showReplaySummary(); 217 </action> 218 </object> 219 220 <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%"> 221 <translatableAttribute id="caption">Start Replay</translatableAttribute> 222 <action on="Press"> 223 startReplay(); 224 </action> 225 </object> 226 227 </object> 228 </object> 229 </objects> 230 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> -
binaries/data/mods/public/gui/session/session.js
5 5 var g_IsController; 6 6 // Match ID for tracking 7 7 var g_MatchID; 8 8 // Is this user an observer? 9 9 var g_IsObserver = false; 10 var g_IsReplay = false; 10 11 11 12 // Cache the basic player data (name, civ, color) 12 13 var g_Players = []; 13 14 // Cache the useful civ data 14 15 var g_CivData = {}; … … 149 150 { 150 151 if (initData) 151 152 { 152 153 g_IsNetworked = initData.isNetworked; // Set network mode 153 154 g_IsController = initData.isController; // Set controller mode 155 g_IsReplay = initData.isReplay; // Set replay mode 154 156 g_PlayerAssignments = initData.playerAssignments; 155 157 g_MatchID = initData.attribs.matchID; 156 158 157 159 // Cache the player data 158 160 // (This may be updated at runtime by handleNetMessage) … … 309 311 * Leave the game 310 312 * @param willRejoin If player is going to be rejoining a networked game. 311 313 */ 312 314 function leaveGame(willRejoin) 313 315 { 314 var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState") ;315 var mapSettings = Engine.GetMapSettings();316 var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState") 317 316 318 var gameResult; 317 318 319 if (g_IsObserver) 319 320 { 320 321 // Observers don't win/lose. 321 322 gameResult = translate("You have left the game."); 322 323 global.music.setState(global.music.states.VICTORY); … … 342 343 } 343 344 } 344 345 } 345 346 346 347 stopAmbient(); 347 Engine.EndGame();348 348 349 // Save summary screen data before ending the game 350 var summary = getSummary(extendedSimState); 351 if (!g_IsReplay) 352 Engine.SaveReplaySummary(JSON.stringify(summary)); 353 summary.isReplay = g_IsReplay; 354 summary.gameResult = gameResult; 355 356 Engine.EndGame(); 349 357 if (g_IsController && Engine.HasXmppClient()) 350 358 Engine.SendUnregisterGame(); 351 359 352 Engine.SwitchGuiPage("page_summary.xml", { 353 "gameResult" : gameResult, 354 "timeElapsed" : extendedSimState.timeElapsed, 355 "playerStates": extendedSimState.players, 356 "players": g_Players, 357 "mapSettings": mapSettings 358 }); 360 Engine.SwitchGuiPage("page_summary.xml", summary); 361 } 362 363 function getSummary(extendedSimState) 364 { 365 var simState = extendedSimState ? extendedSimState : Engine.GuiInterfaceCall("GetExtendedSimulationState"); 366 return { 367 "timeElapsed" : simState.timeElapsed, 368 "playerStates": simState.players, 369 "players": g_Players, 370 "mapSettings": Engine.GetMapSettings() 371 }; 359 372 } 360 373 361 374 // Return some data that we'll use when hotloading this file after changes 362 375 function getHotloadData() 363 376 { -
binaries/data/mods/public/gui/summary/summary.js
28 28 const INCOME_COLOR = '[color="201 255 200"]'; 29 29 const OUTCOME_COLOR = '[color="255 213 213"]'; 30 30 31 31 const DEFAULT_DECIMAL = "0.00"; 32 32 const INFINITE_SYMBOL = "\u221E"; 33 var g_IsReplay = false; 33 34 // Load data 34 35 var g_CivData = loadCivData(); 35 36 var g_Teams = [ ]; 36 37 // TODO set g_MaxPlayers as playerCounters.length 37 38 var g_MaxPlayers = 0; … … 132 133 133 134 function init(data) 134 135 { 135 136 updateObjectPlayerPosition(); 136 137 g_GameData = data; 137 138 g_IsReplay = data.isReplay; 139 138 140 // Map 139 141 var mapDisplayType = translate("Scenario"); 140 142 141 143 Engine.GetGUIObjectByName("timeElapsed").caption = sprintf(translate("Game time elapsed: %(time)s"), { time: timeToString(data.timeElapsed) }); 142 144 … … 165 167 g_MaxPlayers = data.playerStates.length - 1; 166 168 167 169 if (data.mapSettings.LockTeams) // teams ARE locked 168 170 { 169 171 // count teams 170 for (var t = 0; t < g_MaxPlayers; ++t)172 for (var t = 0; t < g_MaxPlayers; ++t) 171 173 { 172 174 if (!g_Teams[data.playerStates[t+1].team]) 173 175 { 174 176 g_Teams[data.playerStates[t+1].team] = 1; 175 177 continue; … … 184 186 g_Teams = false; 185 187 186 188 // Erase teams data if teams are not displayed 187 189 if (!g_Teams) 188 190 { 189 for (var p = 0; p < g_MaxPlayers; ++p)191 for (var p = 0; p < g_MaxPlayers; ++p) 190 192 data.playerStates[p+1].team = -1; 191 193 } 192 194 193 195 g_WithoutTeam = g_MaxPlayers; 194 196 if (g_Teams) -
binaries/data/mods/public/gui/summary/summary.xml
155 155 </object> 156 156 157 157 <object type="button" style="ModernButtonRed" size="100%-160 100%-48 100%-20 100%-20"> 158 158 <translatableAttribute id="caption">Continue</translatableAttribute> 159 159 <action on="Press"><![CDATA[ 160 if ( !Engine.HasXmppClient())160 if (Engine.HasXmppClient()) 161 161 { 162 Engine.SwitchGuiPage("page_pregame.xml"); 162 Engine.LobbySetPlayerPresence("available"); 163 Engine.SwitchGuiPage("page_lobby.xml"); 164 } 165 else if (g_IsReplay) 166 { 167 Engine.SwitchGuiPage("page_replay.xml"); 163 168 } 164 169 else 165 170 { 166 Engine.LobbySetPlayerPresence("available"); 167 Engine.SwitchGuiPage("page_lobby.xml"); 171 Engine.SwitchGuiPage("page_pregame.xml"); 168 172 } 169 173 ]]> 170 174 </action> 171 175 </object> 172 176 </object> -
binaries/data/mods/public/simulation/data/game_speeds.json
31 31 "Speed": 1.5 32 32 }, 33 33 { 34 34 "Name": "Insane (2x)", 35 35 "Speed": 2.0 36 }, 37 { 38 "Name": "Fast forward (5x)", 39 "Speed": 5.0 40 }, 41 { 42 "Name": "Faster forward (10x)", 43 "Speed": 10.0 44 }, 45 { 46 "Name": "Very fast forward (20x)", 47 "Speed": 20.0 36 48 } 37 49 ] 38 50 } -
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 void StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring directoryName) 288 { 289 ENSURE(!g_NetServer); 290 ENSURE(!g_NetClient); 291 ENSURE(!g_Game); 292 293 std::wstring replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / L"commands.txt").string(); 294 std::string replayFile( replayFilePath.begin(), replayFilePath.end() ); 295 296 if (FileExists(OsPath(replayFile))) 297 { 298 g_Game = new CGame(false, false); 299 g_Game->StartReplay(replayFile); 300 } 301 } 302 303 JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 304 { 305 return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName); 306 } 307 308 void SaveReplaySummary(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring data) 309 { 310 VisualReplay::SaveReplaySummary(data); 311 } 312 313 JS::Value GetReplaySummary(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 314 { 315 return VisualReplay::GetReplaySummary(pCxPrivate, directoryName); 316 } 317 286 318 void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1) 287 319 { 288 320 ENSURE(g_NetServer); 289 321 //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere 290 322 // (with no obvious reason). … … 415 447 bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name) 416 448 { 417 449 return SavedGames::DeleteSavedGame(name); 418 450 } 419 451 452 JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate) 453 { 454 return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface)); 455 } 456 457 bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring replayFile) 458 { 459 return VisualReplay::DeleteReplay(replayFile); 460 } 461 420 462 void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url) 421 463 { 422 464 sys_open_url(url); 423 465 } 424 466 … … 974 1016 scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGame>("SaveGame"); 975 1017 scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGamePrefix>("SaveGamePrefix"); 976 1018 scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave"); 977 1019 scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad"); 978 1020 1021 // Visual replay 1022 scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays"); 1023 scriptInterface.RegisterFunction<bool, std::wstring, &DeleteReplay>("DeleteReplay"); 1024 scriptInterface.RegisterFunction<void, std::wstring, &StartVisualReplay>("StartVisualReplay"); 1025 scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetReplayAttributes>("GetReplayAttributes"); 1026 scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetReplaySummary>("GetReplaySummary"); 1027 scriptInterface.RegisterFunction<void, std::wstring, &SaveReplaySummary>("SaveReplaySummary"); 1028 979 1029 // Misc functions 980 1030 scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor"); 981 1031 scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID"); 982 1032 scriptInterface.RegisterFunction<void, int, &SetPlayerID>("SetPlayerID"); 983 1033 scriptInterface.RegisterFunction<void, std::string, &OpenURL>("OpenURL"); -
source/lobby/XmppClient.cpp
22 22 #include "glooxwrapper/glooxwrapper.h" 23 23 #include "i18n/L10n.h" 24 24 #include "lib/utf8.h" 25 25 #include "ps/CLogger.h" 26 26 #include "ps/ConfigDB.h" 27 #include "ps/EngineVersion.h" 27 28 #include "scriptinterface/ScriptInterface.h" 28 29 29 30 //debug 30 31 #if 1 31 32 #define DbgXMPP(x) … … 95 96 const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain; 96 97 m_client->setSASLMechanisms(mechs); 97 98 98 99 m_client->registerConnectionListener(this); 99 100 m_client->setPresence(gloox::Presence::Available, -1); 100 m_client->disco()->setVersion("Pyrogenesis", "0.0.19");101 m_client->disco()->setVersion("Pyrogenesis", utf8_from_wstring(engine_version)); 101 102 m_client->disco()->setIdentity("client", "bot"); 102 103 m_client->setCompression(false); 103 104 104 105 m_client->registerStanzaExtension(new GameListQuery()); 105 106 m_client->registerIqHandler(this, EXTGAMELISTQUERY); -
source/ps/EngineVersion.h
1 const wchar_t engine_version[] = L"0.0.19"; -
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; … … 162 167 else 163 168 { 164 169 CancelLoad(L"Failed to load replay data (unrecognized content)"); 165 170 } 166 171 } 172 m_ReplayStream->close(); 167 173 m_FinalReplayTurn = currentTurn; 168 174 replayTurnMgr->StoreFinalReplayTurn(currentTurn); 169 175 return 0; 170 176 } 171 177 … … 173 179 { 174 180 m_IsReplay = true; 175 181 ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); 176 182 177 183 SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger())); 184 SetPlayerID(-1); 178 185 179 186 m_ReplayPath = replayPath; 180 187 m_ReplayStream = new std::ifstream(m_ReplayPath.c_str()); 181 188 182 189 std::string type; -
source/ps/Game.h
176 176 bool m_IsSavedGame; // true if loading a saved game; false for a new game 177 177 178 178 int LoadReplayData(); 179 179 std::string m_ReplayPath; 180 180 bool m_IsReplay; 181 std::i stream* m_ReplayStream;181 std::ifstream* m_ReplayStream; 182 182 u32 m_FinalReplayTurn; 183 183 }; 184 184 185 185 extern CGame *g_Game; 186 186 -
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 … … 19 19 20 20 #include <cstdio> 21 21 22 22 #include "Pyrogenesis.h" 23 23 24 #include "ps/EngineVersion.h" 24 25 #include "lib/sysdep/sysdep.h" 25 26 #include "lib/svn_revision.h" 26 27 27 28 static const wchar_t* translate_no_mem = L"(no mem)"; 28 29 … … 71 72 72 73 // for user convenience, bundle all logs into this file: 73 74 void psBundleLogs(FILE* f) 74 75 { 75 76 fwprintf(f, L"SVN Revision: %ls\n\n", svn_revision); 77 fwprintf(f, L"Engine Version: %ls\n\n", engine_version); 76 78 77 79 fwprintf(f, L"System info:\n\n"); 78 80 OsPath path1 = psLogDir()/"system_info.txt"; 79 81 AppendAsciiFile(f, path1); 80 82 fwprintf(f, L"\n\n====================================\n\n"); -
source/ps/Replay.cpp
22 22 #include "graphics/TerrainTextureManager.h" 23 23 #include "lib/timer.h" 24 24 #include "lib/file/file_system.h" 25 25 #include "lib/res/h_mgr.h" 26 26 #include "lib/tex/tex.h" 27 #include "ps/EngineVersion.h" 27 28 #include "ps/Game.h" 28 29 #include "ps/Loader.h" 29 30 #include "ps/Profile.h" 31 #include "ps/Mod.h" 30 32 #include "ps/ProfileViewer.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" … … 52 54 } 53 55 54 56 CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) : 55 57 m_ScriptInterface(scriptInterface) 56 58 { 59 m_Stream = NULL; 60 } 61 62 CReplayLogger::~CReplayLogger() 63 { 64 delete m_Stream; 65 } 66 67 void CReplayLogger::StartGame(JS::MutableHandleValue attribs) 68 { 57 69 // Construct the directory name based on the PID, to be relatively unique. 58 70 // Append "-1", "-2" etc if we run multiple matches in a single session, 59 71 // to avoid accidentally overwriting earlier logs. 60 72 61 73 std::wstringstream name; … … 63 75 64 76 static int run = -1; 65 77 if (++run) 66 78 name << "-" << run; 67 79 68 OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";69 CreateDirectories(path.Parent(), 0700);70 m_S tream = 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 // Add timestamp and engine version to game attributes 81 m_ScriptInterface.SetProperty(attribs, "timestamp", std::time(0)); 82 m_ScriptInterface.SetProperty(attribs, "engine_version", utf8_from_wstring(engine_version)); 83 m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded); 84 85 // Remember the directory, so that we can save additional files to it 86 m_Directory = psLogDir() / L"sim_log" / name.str(); 87 OsPath filepath = m_Directory / L"commands.txt"; 88 89 // Open the file when starting the game to prevent empty files 90 CreateDirectories(m_Directory, 0700); 91 m_Stream = new std::ofstream(OsString(filepath).c_str(), std::ofstream::out | std::ofstream::trunc); 80 92 *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; 81 93 } 82 94 83 95 void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands) 84 96 { … … 100 112 *m_Stream << "hash-quick " << Hexify(hash) << "\n"; 101 113 else 102 114 *m_Stream << "hash " << Hexify(hash) << "\n"; 103 115 } 104 116 117 OsPath CReplayLogger::GetReplayDirectory() 118 { 119 return m_Directory; 120 } 105 121 //////////////////////////////////////////////////////////////// 106 122 107 123 CReplayPlayer::CReplayPlayer() : 108 124 m_Stream(NULL) 109 125 { … … 157 173 JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); 158 174 JSAutoRequest rq(cx); 159 175 std::string type; 160 176 while ((*m_Stream >> type).good()) 161 177 { 162 // if (turn >= 1400) break;163 164 178 if (type == "start") 165 179 { 166 180 std::string line; 167 181 std::getline(*m_Stream, line); 168 182 JS::RootedValue attribs(cx); … … 198 212 std::string replayHash; 199 213 *m_Stream >> replayHash; 200 214 201 215 bool quick = (type == "hash-quick"); 202 216 203 // if (turn >= 1300)204 // if (turn >= 0)205 217 if (turn % 100 == 0) 206 218 { 207 219 std::string hash; 208 220 bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick); 209 221 ENSURE(ok); … … 224 236 225 237 g_Game->GetSimulation2()->Update(turnLength, commands); 226 238 commands.clear(); 227 239 } 228 240 229 // std::string hash;230 // bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, true);231 // ENSURE(ok);232 // debug_printf("%s\n", Hexify(hash).c_str());233 234 241 g_Profiler.Frame(); 235 242 236 // if (turn % 1000 == 0)237 // JS_GC(g_Game->GetSimulation2()->GetScriptInterface().GetContext());238 239 243 if (turn % 20 == 0) 240 244 g_ProfileViewer.SaveToFile(); 241 245 } 242 246 else 243 247 { 244 248 debug_printf("Unrecognised replay token %s\n", type.c_str()); 245 249 } 246 250 } 247 251 } 248 252 m_Stream->close(); 249 253 g_Profiler2.SaveToFile(); 250 254 251 255 std::string hash; 252 256 bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false); 253 257 ENSURE(ok); -
source/ps/Replay.h
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 * Returns the directory that contains the replay file. 53 */ 54 virtual OsPath GetReplayDirectory() = 0; 50 55 }; 51 56 52 57 /** 53 58 * Implementation of IReplayLogger that simply throws away all data. 54 59 */ … … 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 GetReplayDirectory() { 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 */ … … 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 GetReplayDirectory(); 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 */ … … 90 98 91 99 void Load(const std::string& path); 92 100 void Replay(bool serializationtest, bool ooslog); 93 101 94 102 private: 95 std::i stream* m_Stream;103 std::ifstream* m_Stream; 96 104 }; 97 105 98 106 #endif // INCLUDED_REPLAY -
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 … … 24 24 #include "lib/allocators/shared_ptr.h" 25 25 #include "lib/file/archive/archive_zip.h" 26 26 #include "i18n/L10n.h" 27 27 #include "lib/utf8.h" 28 28 #include "ps/CLogger.h" 29 #include "ps/EngineVersion.h" 29 30 #include "ps/Filesystem.h" 30 31 #include "ps/Game.h" 31 32 #include "ps/Mod.h" 32 33 #include "scriptinterface/ScriptInterface.h" 33 34 #include "simulation2/Simulation2.h" … … 84 85 JS::RootedValue metadata(cx); 85 86 JS::RootedValue initAttributes(cx, simulation.GetInitAttributes()); 86 87 simulation.GetScriptInterface().Eval("({})", &metadata); 87 88 simulation.GetScriptInterface().SetProperty(metadata, "version_major", SAVED_GAME_VERSION_MAJOR); 88 89 simulation.GetScriptInterface().SetProperty(metadata, "version_minor", SAVED_GAME_VERSION_MINOR); 90 simulation.GetScriptInterface().SetProperty(metadata, "engine_version", utf8_from_wstring(engine_version)); 91 89 92 simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded); 90 93 simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now); 91 94 simulation.GetScriptInterface().SetProperty(metadata, "player", playerID); 92 95 simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes); 93 96 … … 296 299 JSContext* cx = scriptInterface.GetContext(); 297 300 JSAutoRequest rq(cx); 298 301 299 302 JS::RootedValue metainfo(cx); 300 303 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); 304 scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR); 305 scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR); 306 scriptInterface.SetProperty(metainfo, "engine_version", utf8_from_wstring(engine_version)); 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 #include "graphics/GameView.h" 22 #include "gui/GUIManager.h" 23 #include "lib/allocators/shared_ptr.h" 24 #include "lib/utf8.h" 25 #include "ps/CLogger.h" 26 #include "ps/EngineVersion.h" 27 #include "ps/Filesystem.h" 28 #include "ps/Game.h" 29 #include "ps/Replay.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 u32 i = 0; 46 DirectoryNames directories; 47 JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0)); 48 GetDirectoryEntries(GetDirectoryName(), NULL, &directories); 49 for (auto& directory : directories) 50 { 51 JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory)); 52 if (!replayData.isNull()) 53 JS_SetElement(cx, replays, i++, replayData); 54 } 55 return JS::ObjectValue(*replays); 56 } 57 58 // Works similar to CGame::LoadReplayData() 59 // Extracts metadata from file header, loads commands and computes duration of the game 60 JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath directory) 61 { 62 OsPath replayFile(GetDirectoryName() / directory / L"commands.txt"); 63 64 if (!FileExists(replayFile)) 65 return JSVAL_NULL; 66 67 // Get fileinfo (size, timestamp) 68 CFileInfo fileInfo; 69 GetFileInfo(replayFile, &fileInfo); 70 u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath) 71 u64 fileSize = (u64)fileInfo.Size(); 72 73 if (fileSize == 0) 74 return JSVAL_NULL; 75 76 // Open file 77 std::wstring filenameW = replayFile.string(); 78 std::string filename( filenameW.begin(), filenameW.end() ); 79 std::ifstream* replayStream = new std::ifstream(filename.c_str()); 80 81 // File must begin with "start" 82 std::string type; 83 ENSURE((*replayStream >> type).good() && type == "start"); 84 85 // Parse header 86 std::string header; 87 std::getline(*replayStream, header); 88 JSContext* cx = scriptInterface.GetContext(); 89 JSAutoRequest rq(cx); 90 JS::RootedValue attribs(cx); 91 if (!scriptInterface.ParseJSON(header, &attribs)) 92 return JSVAL_NULL; 93 94 // Parse commands & turns 95 u32 currentTurn = 0; 96 u32 currentMinute = 0; 97 u64 duration = 0; 98 std::vector<player_id_t> resignedPlayers; 99 std::map<player_id_t, u16> currentCommandCount; 100 std::map<player_id_t, u16> commandsPerMinute; 101 while ((*replayStream >> type).good()) 102 { 103 if (type == "turn") 104 { 105 // Store turn & turn length 106 u32 turn = 0; 107 u32 turnLength = 0; 108 *replayStream >> turn >> turnLength; 109 if (turn != currentTurn) // Happens for replays of rejoined clients 110 return JSVAL_NULL; 111 112 // Compute game duration 113 duration += turnLength; 114 115 // Compute commands per minute 116 u32 min = duration / 1000 / 60; 117 if (min > currentMinute) 118 { 119 //commandsPerMinute[player][currentMinute] = currentCommandCount[player]; 120 currentMinute = min; 121 } 122 } 123 else if (type == "cmd") 124 { 125 // Extract player id 126 player_id_t player; 127 *replayStream >> player; 128 129 // Increase command count 130 if (currentCommandCount.find(player) == currentCommandCount.end()) 131 currentCommandCount[player] = 1; 132 else 133 currentCommandCount[player]++; 134 135 // Store command 136 std::string command; 137 std::getline(*replayStream, command); 138 139 // Parse command 140 JS::RootedValue commandData(scriptInterface.GetContext()); 141 std::wstring commandType; 142 if (!scriptInterface.ParseJSON(command, &commandData)) 143 { 144 LOGERROR("Corrupted replay command '%s' in file '%s'", command, filename.c_str()); 145 return JSVAL_NULL; 146 } 147 scriptInterface.GetProperty(commandData, "type", commandType); 148 149 // Save defeated players {"type":"defeat-player","playerId":2} 150 if (commandType == L"defeat-player") 151 { 152 player_id_t defeatedPlayer; 153 scriptInterface.GetProperty(commandData, "playerId", defeatedPlayer); 154 resignedPlayers.push_back(defeatedPlayer); 155 } 156 } 157 else if (type == "hash" || type == "hash-quick") 158 { 159 // Skip hash values 160 std::string replayHash; 161 *replayStream >> replayHash; 162 } 163 else if (type == "end") 164 { 165 currentTurn++; 166 } 167 else 168 { 169 LOGERROR("Unrecognized replay data '%s' in file %s", type, filename.c_str()); 170 } 171 } 172 replayStream->close(); 173 174 duration = duration / 1000; 175 if (duration < 5) // Don't list too short replays 176 return JSVAL_NULL; 177 178 // finalReplayTurn = currentTurn; 179 // TODO: compute average turn time, if turn length varies 180 181 JS::RootedValue replayData(cx); 182 scriptInterface.Eval("({})", &replayData); 183 scriptInterface.SetProperty(replayData, "file", replayFile); 184 scriptInterface.SetProperty(replayData, "directory", directory); 185 scriptInterface.SetProperty(replayData, "filemod_timestamp", fileTime); 186 scriptInterface.SetProperty(replayData, "attribs", attribs); 187 scriptInterface.SetProperty(replayData, "duration", duration); 188 scriptInterface.SetProperty(replayData, "resignedPlayers", resignedPlayers); 189 return replayData; 190 } 191 192 bool VisualReplay::DeleteReplay(const std::wstring replayDirectory) 193 { 194 if (replayDirectory == L"") 195 return false; 196 197 const OsPath directory = OsPath(GetDirectoryName() / replayDirectory); 198 return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; 199 } 200 201 202 JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 203 { 204 // TODO: move to VisualReplay.cpp 205 // TODO: Add description, say that it loads the header and parses of commands.txt 206 std::wstring replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / "commands.txt").string(); 207 std::string replayFile( replayFilePath.begin(), replayFilePath.end() ); 208 209 JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); 210 JSAutoRequest rq(cx); 211 JS::RootedValue attribs(cx); 212 pCxPrivate->pScriptInterface->Eval("({})", &attribs); 213 214 if (!FileExists(OsPath(replayFile))) 215 return attribs; 216 217 std::ifstream* replayStream = new std::ifstream(replayFile.c_str()); 218 std::string type, line; 219 ENSURE((*replayStream >> type).good() && type == "start"); 220 221 std::getline(*replayStream, line); 222 pCxPrivate->pScriptInterface->ParseJSON(line, &attribs); 223 replayStream->close(); 224 return attribs; 225 } 226 227 void VisualReplay::SaveReplaySummary(std::wstring data) 228 { 229 // Returns directory of the currently active replay 230 // TODO: move to VisualReplay.cpp 231 // TODO: use JS::HandleValue similar to SaveGame 232 if (!g_Game) 233 return; 234 235 OsPath directory = g_Game->GetReplayLogger().GetReplayDirectory(); 236 CreateDirectories(directory, 0700); 237 238 OsPath filepath = OsPath(directory / L"summary.json"); 239 240 std::wstring filenameW = filepath.string(); 241 std::string filename( filenameW.begin(), filenameW.end() ); 242 243 std::ofstream stream (filename.c_str(), std::ofstream::out | std::ofstream::trunc); 244 stream << utf8_from_wstring(data); 245 stream.close(); 246 } 247 248 JS::Value VisualReplay::GetReplaySummary(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName) 249 { 250 std::wstring filePathW = OsPath(VisualReplay::GetDirectoryName() / directoryName / "summary.json").string(); 251 std::string filePath( filePathW.begin(), filePathW.end() ); 252 253 JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); 254 JSAutoRequest rq(cx); 255 JS::RootedValue summary(cx); 256 257 if (FileExists(OsPath(filePathW))) 258 { 259 std::ifstream* stream = new std::ifstream(filePath.c_str()); 260 ENSURE(stream->good()); 261 262 std::string type, line; 263 std::getline(*stream, line); 264 pCxPrivate->pScriptInterface->ParseJSON(line, &summary); 265 stream->close(); 266 } 267 else 268 { 269 pCxPrivate->pScriptInterface->Eval("({})", &summary); 270 } 271 272 return summary; 273 } -
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 JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile); 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 * Returns the parsed header of the replay file (commands.txt). 74 */ 75 JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName); 76 77 /** 78 * Returns the data for the summary screen of a replay. 79 */ 80 JS::Value GetReplaySummary(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName); 81 82 /** 83 * Saves the summary data from the session to summary.json 84 */ 85 void SaveReplaySummary(std::wstring data); 86 87 } 88 89 #endif