Ticket #3258: replay_menu_wip_v6_r16812.patch

File replay_menu_wip_v6_r16812.patch, 71.3 KB (added by elexis, 9 years ago)

Saves the data that is passed to the summary screen to the commands.txt directory. Adds a link to the visual replay menu that allows browsing the summary screen (without replaying the whole game), so that you can see who won and how. Translates some more strings TODO: the game should remember all defeated players, not only those that resign

  • binaries/data/mods/public/gui/common/functions_utility.js

     
    8181        return text;
    8282
    8383    return text.substr(0, 255).replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
    8484}
    8585
     86//====================================================================
     87
     88function capitalizeFirstLetter(text) {
     89    return text.charAt(0).toUpperCase() + text.slice(1);
     90}
     91
    8692// ====================================================================
    8793
    8894// Load default player data, for when it's not otherwise specified
    8995function initPlayerDefaults()
    9096{
  • binaries/data/mods/public/gui/common/functions_utility_loadsave.js

     
    2323/**
    2424 * Check the version compatibility between the saved game to be loaded and the engine
    2525 */
    2626function hasSameVersion(metadata, engineInfo)
    2727{
    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);
    2929}
    3030
    3131/**
    3232 * Check the mod compatibility between the saved game to be loaded and the engine
    3333 */
  • binaries/data/mods/public/gui/gamesetup/gamesetup.js

     
    487487        }
    488488        Engine.SwitchGuiPage("page_loading.xml", {
    489489            "attribs": g_GameAttributes,
    490490            "isNetworked" : g_IsNetworked,
    491491            "playerAssignments": g_PlayerAssignments,
    492             "isController": g_IsController
     492            "isController": g_IsController,
     493            "g_IsReplay" : false
    493494        });
    494495        break;
    495496
    496497    case "chat":
    497498        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

     
    344344                        Engine.PushGuiPage("page_locale.xml");
    345345                        ]]>
    346346                    </action>
    347347                </object>
    348348
     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
    349363                <object name="submenuEditorButton"
    350364                    style="StoneButtonFancy"
    351365                    type="button"
    352                     size="0 64 100% 92"
     366                    size="0 96 100% 124"
    353367                    tooltip_style="pgToolTip"
    354368                >
    355369                    <translatableAttribute id="caption">Scenario Editor</translatableAttribute>
    356370                    <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 &quot;-editor&quot;.</translatableAttribute>
    357371                    <action on="Press">
     
    360374                </object>
    361375
    362376                <object name="submenuWelcomeScreenButton"
    363377                    style="StoneButtonFancy"
    364378                    type="button"
    365                     size="0 96 100% 124"
     379                    size="0 128 100% 156"
    366380                    tooltip_style="pgToolTip"
    367381                >
    368382                    <translatableAttribute id="caption">Welcome Screen</translatableAttribute>
    369383                    <translatableAttribute id="tooltip">Show the Welcome Screen. Useful if you hid it by mistake.</translatableAttribute>
    370384                    <action on="Press">
     
    375389                    </action>
    376390                </object>
    377391                <object name="submenuModSelection"
    378392                    style="StoneButtonFancy"
    379393                    type="button"
    380                     size="0 128 100% 156"
     394                    size="0 156 100% 188"
    381395                    tooltip_style="pgToolTip"
    382396                >
    383397                    <translatableAttribute id="caption">Mod Selection</translatableAttribute>
    384398                    <translatableAttribute id="tooltip">Select mods to use.</translatableAttribute>
    385399                    <action on="Press">
     
    482496                >
    483497                    <translatableAttribute id="caption">Tools &amp; Options</translatableAttribute>
    484498                    <translatableAttribute id="tooltip">Game options and scenario design tools.</translatableAttribute>
    485499                    <action on="Press">
    486500                        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);
    488502                    </action>
    489503                </object>
    490504
    491505                <!-- EXIT BUTTON -->
    492506                <object name="menuExitButton"
  • binaries/data/mods/public/gui/replay/replay.js

     
     1var g_Replays = []; // All replays found including metadata
     2var g_ReplaysFiltered = []; // List of replays after filtering
     3var g_Playernames = []; // All playernames of all replays, used for autocomplete
     4var g_ReplayListSortBy = "name"; // Used for sorting
     5var g_ReplayListOrder = -1; // Used for sorting
     6var g_mapSizes = initMapSizes(); // already translated
     7var g_CivData = loadCivData();
     8var g_EngineInfo = Engine.GetEngineInfo();
     9var g_DefaultPlayerData = initPlayerDefaults();
     10var g_maxPlayerlistChars = 70;
     11var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     12var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     13
     14function init()
     15{
     16    loadReplays();
     17    updateReplayList();
     18}
     19
     20function 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
     52function updateReplayListOrderSelection()
     53{
     54    g_ReplayListSortBy = Engine.GetGUIObjectByName("replaySelection").selected_column;
     55    g_ReplayListOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
     56    applyFilters();
     57}
     58
     59function 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
     159function Greyout(replay, text)
     160{
     161    if (isReplayCompatible(replay))
     162        return text;
     163    else
     164        return '[color="128 128 128"]' + text + '[/color]';
     165}
     166
     167function getReplayDateTime(replay)
     168{
     169    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm"))
     170}
     171
     172function 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
     201function getReplayMapName(replay)
     202{
     203    return escapeText(translate(replay.settings.Name));
     204}
     205
     206function 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
     215function getReplayPopCap(replay)
     216{
     217    if (replay.settings.PopulationCap == 10000)
     218        return translateWithContext("population capacity", "Unlimited");
     219    else
     220        return replay.settings.PopulationCap;   
     221}
     222
     223function getReplayDuration(replay)
     224{
     225    return timeToString(replay.duration * 1000);
     226}
     227
     228function 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
     288function 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
     309function isReplayCompatible_EngineVersion(replay)
     310{
     311    return replay.attribs.hasOwnProperty("engine_version")
     312            && replay.attribs.engine_version == g_EngineInfo.engine_version;
     313}
     314
     315function initFilters()
     316{
     317    initDateFilter();
     318    initMapNameFilter();
     319    initMapSizeFilter();
     320    initPopCapFilter();
     321    initDurationFilter();
     322}
     323
     324function 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
     343function getDateTimeFilterVal(replay)
     344{
     345    var date = new Date(replay.timestamp * 1000);
     346    return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2)
     347}
     348
     349function 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
     378function 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
     400function 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
     420function 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
     448function 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.
     458function 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
     531function 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
     581function 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
     594function 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
     606function 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
     629function 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
     653function 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
     666function deleteReplayWithoutConfirmation()
     667{
     668    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     669    if (selected > -1)
     670        reallyDeleteReplay(g_ReplaysFiltered[selected].directory);
     671}
     672
     673function 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

     
    55var g_IsController;
    66// Match ID for tracking
    77var g_MatchID;
    88// Is this user an observer?
    99var g_IsObserver = false;
     10var g_IsReplay = false;
    1011
    1112// Cache the basic player data (name, civ, color)
    1213var g_Players = [];
    1314// Cache the useful civ data
    1415var g_CivData = {};
     
    149150{
    150151    if (initData)
    151152    {
    152153        g_IsNetworked = initData.isNetworked; // Set network mode
    153154        g_IsController = initData.isController; // Set controller mode
     155        g_IsReplay = initData.isReplay; // Set replay mode
    154156        g_PlayerAssignments = initData.playerAssignments;
    155157        g_MatchID = initData.attribs.matchID;
    156158
    157159        // Cache the player data
    158160        // (This may be updated at runtime by handleNetMessage)
     
    309311 * Leave the game
    310312 * @param willRejoin If player is going to be rejoining a networked game.
    311313 */
    312314function leaveGame(willRejoin)
    313315{
    314     var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
    315     var mapSettings = Engine.GetMapSettings();
     316    var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState")
     317   
    316318    var gameResult;
    317 
    318319    if (g_IsObserver)
    319320    {
    320321        // Observers don't win/lose.
    321322        gameResult = translate("You have left the game.");
    322323        global.music.setState(global.music.states.VICTORY);
     
    342343            }
    343344        }
    344345    }
    345346
    346347    stopAmbient();
    347     Engine.EndGame();
    348348
     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();
    349357    if (g_IsController && Engine.HasXmppClient())
    350358        Engine.SendUnregisterGame();
    351359
    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
     363function 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          };
    359372}
    360373
    361374// Return some data that we'll use when hotloading this file after changes
    362375function getHotloadData()
    363376{
  • binaries/data/mods/public/gui/summary/summary.js

     
    2828const INCOME_COLOR = '[color="201 255 200"]';
    2929const OUTCOME_COLOR = '[color="255 213 213"]';
    3030
    3131const DEFAULT_DECIMAL = "0.00";
    3232const INFINITE_SYMBOL = "\u221E";
     33var g_IsReplay = false;
    3334// Load data
    3435var g_CivData = loadCivData();
    3536var g_Teams = [ ];
    3637// TODO set g_MaxPlayers as playerCounters.length
    3738var g_MaxPlayers = 0;
     
    132133
    133134function init(data)
    134135{
    135136    updateObjectPlayerPosition();
    136137    g_GameData = data;
    137 
     138    g_IsReplay = data.isReplay;
     139   
    138140    // Map
    139141    var mapDisplayType = translate("Scenario");
    140142
    141143    Engine.GetGUIObjectByName("timeElapsed").caption = sprintf(translate("Game time elapsed: %(time)s"), { time: timeToString(data.timeElapsed) });
    142144
     
    165167    g_MaxPlayers = data.playerStates.length - 1;
    166168
    167169    if (data.mapSettings.LockTeams) // teams ARE locked
    168170    {
    169171        // count teams
    170         for(var t = 0; t < g_MaxPlayers; ++t)
     172        for (var t = 0; t < g_MaxPlayers; ++t)
    171173        {
    172174            if (!g_Teams[data.playerStates[t+1].team])
    173175            {
    174176                g_Teams[data.playerStates[t+1].team] = 1;
    175177                continue;
     
    184186        g_Teams = false;
    185187
    186188    // Erase teams data if teams are not displayed
    187189    if (!g_Teams)
    188190    {
    189         for(var p = 0; p < g_MaxPlayers; ++p)
     191        for (var p = 0; p < g_MaxPlayers; ++p)
    190192            data.playerStates[p+1].team = -1;
    191193    }
    192194
    193195    g_WithoutTeam = g_MaxPlayers;
    194196    if (g_Teams)
  • binaries/data/mods/public/gui/summary/summary.xml

     
    155155        </object>
    156156
    157157        <object type="button" style="ModernButtonRed" size="100%-160 100%-48 100%-20 100%-20">
    158158            <translatableAttribute id="caption">Continue</translatableAttribute>
    159159            <action on="Press"><![CDATA[
    160                 if (!Engine.HasXmppClient())
     160                if (Engine.HasXmppClient())
    161161                {
    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");
    163168                }
    164169                else
    165170                {
    166                     Engine.LobbySetPlayerPresence("available");
    167                     Engine.SwitchGuiPage("page_lobby.xml");
     171                    Engine.SwitchGuiPage("page_pregame.xml");
    168172                }
    169173                ]]>
    170174            </action>
    171175        </object>
    172176    </object>
  • binaries/data/mods/public/simulation/data/game_speeds.json

     
    3131            "Speed": 1.5
    3232        },
    3333        {
    3434            "Name": "Insane (2x)",
    3535            "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
    3648        }
    3749    ]
    3850}
  • source/gui/scripting/ScriptFunctions.cpp

     
    4848#include "ps/Globals.h" // g_frequencyFilter
    4949#include "ps/Hotkey.h"
    5050#include "ps/ProfileViewer.h"
    5151#include "ps/Pyrogenesis.h"
    5252#include "ps/SavedGame.h"
     53#include "ps/VisualReplay.h"
    5354#include "ps/UserReport.h"
    5455#include "ps/World.h"
    5556#include "ps/scripting/JSInterface_ConfigDB.h"
    5657#include "ps/scripting/JSInterface_Console.h"
    5758#include "ps/scripting/JSInterface_Mod.h"
     
    281282    shared_ptr<ScriptInterface::StructuredClone> GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
    282283    if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
    283284        LOGERROR("Failed to save game");
    284285}
    285286
     287void 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
     303JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     304{
     305    return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName);
     306}
     307
     308void SaveReplaySummary(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring data)
     309{
     310    VisualReplay::SaveReplaySummary(data);
     311}
     312
     313JS::Value GetReplaySummary(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     314{
     315    return VisualReplay::GetReplaySummary(pCxPrivate, directoryName);
     316}
     317
    286318void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
    287319{
    288320    ENSURE(g_NetServer);
    289321    //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere
    290322    // (with no obvious reason).
     
    415447bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name)
    416448{
    417449    return SavedGames::DeleteSavedGame(name);
    418450}
    419451
     452JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
     453{
     454    return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
     455}
     456
     457bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring replayFile)
     458{
     459    return VisualReplay::DeleteReplay(replayFile);
     460}
     461
    420462void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url)
    421463{
    422464    sys_open_url(url);
    423465}
    424466
     
    9741016    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGame>("SaveGame");
    9751017    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGamePrefix>("SaveGamePrefix");
    9761018    scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave");
    9771019    scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad");
    9781020
     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
    9791029    // Misc functions
    9801030    scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
    9811031    scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID");
    9821032    scriptInterface.RegisterFunction<void, int, &SetPlayerID>("SetPlayerID");
    9831033    scriptInterface.RegisterFunction<void, std::string, &OpenURL>("OpenURL");
  • source/lobby/XmppClient.cpp

     
    2222#include "glooxwrapper/glooxwrapper.h"
    2323#include "i18n/L10n.h"
    2424#include "lib/utf8.h"
    2525#include "ps/CLogger.h"
    2626#include "ps/ConfigDB.h"
     27#include "ps/EngineVersion.h"
    2728#include "scriptinterface/ScriptInterface.h"
    2829
    2930//debug
    3031#if 1
    3132#define DbgXMPP(x)
     
    9596    const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain;
    9697    m_client->setSASLMechanisms(mechs);
    9798
    9899    m_client->registerConnectionListener(this);
    99100    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));
    101102    m_client->disco()->setIdentity("client", "bot");
    102103    m_client->setCompression(false);
    103104
    104105    m_client->registerStanzaExtension(new GameListQuery());
    105106    m_client->registerIqHandler(this, EXTGAMELISTQUERY);
  • source/ps/EngineVersion.h

     
     1const wchar_t engine_version[] = L"0.0.19";
  • source/ps/Game.cpp

     
    134134        if (type == "turn")
    135135        {
    136136            u32 turn = 0;
    137137            u32 turnLength = 0;
    138138            *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
    139143            ENSURE(turn == currentTurn);
     144
    140145            replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength);
    141146        }
    142147        else if (type == "cmd")
    143148        {
    144149            player_id_t player;
     
    162167        else
    163168        {
    164169            CancelLoad(L"Failed to load replay data (unrecognized content)");
    165170        }
    166171    }
     172    m_ReplayStream->close();
    167173    m_FinalReplayTurn = currentTurn;
    168174    replayTurnMgr->StoreFinalReplayTurn(currentTurn);
    169175    return 0;
    170176}
    171177
     
    173179{
    174180    m_IsReplay = true;
    175181    ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface();
    176182
    177183    SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger()));
     184    SetPlayerID(-1);
    178185
    179186    m_ReplayPath = replayPath;
    180187    m_ReplayStream = new std::ifstream(m_ReplayPath.c_str());
    181188
    182189    std::string type;
  • source/ps/Game.h

     
    176176    bool m_IsSavedGame; // true if loading a saved game; false for a new game
    177177
    178178    int LoadReplayData();
    179179    std::string m_ReplayPath;
    180180    bool m_IsReplay;
    181     std::istream* m_ReplayStream;
     181    std::ifstream* m_ReplayStream;
    182182    u32 m_FinalReplayTurn;
    183183};
    184184
    185185extern CGame *g_Game;
    186186
  • source/ps/GameSetup/GameSetup.cpp

     
    879879    srand(time(NULL));  // NOTE: this rand should *not* be used for simulation!
    880880}
    881881
    882882bool Autostart(const CmdLineArgs& args);
    883883
    884 // Returns true if and only if the user has intended to replay a file
    885 bool VisualReplay(const std::string replayFile);
     884bool StartVisualReplay(const std::string replayFile);
    886885
    887886bool Init(const CmdLineArgs& args, int flags)
    888887{
    889888    h_mgr_init();
    890889
     
    10781077    if (VfsDirectoryExists(L"maps/"))
    10791078        CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng");
    10801079
    10811080    try
    10821081    {
    1083         if (!VisualReplay(args.Get("replay-visual")) && !Autostart(args))
     1082        if (!StartVisualReplay(args.Get("replay-visual")) && !Autostart(args))
    10841083        {
    10851084            const bool setup_gui = ((flags & INIT_NO_GUI) == 0);
    10861085            // We only want to display the splash screen at startup
    10871086            shared_ptr<ScriptInterface> scriptInterface = g_GUI->GetScriptInterface();
    10881087            JSContext* cx = scriptInterface->GetContext();
     
    14751474    }
    14761475
    14771476    return true;
    14781477}
    14791478
    1480 bool VisualReplay(const std::string replayFile)
     1479bool StartVisualReplay(const std::string replayFile)
    14811480{
    14821481    if (!FileExists(OsPath(replayFile)))
    14831482        return false;
    14841483
    14851484    g_Game = new CGame(false, false);
    1486     g_Game->SetPlayerID(-1);
    14871485    g_Game->StartReplay(replayFile);
    14881486
    14891487    // TODO: Non progressive load can fail - need a decent way to handle this
    14901488    LDR_NonprogressiveLoad();
    14911489
  • source/ps/Pyrogenesis.cpp

     
    1 /* Copyright (C) 2009 Wildfire Games.
     1/* Copyright (C) 2015 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
     
    1919
    2020#include <cstdio>
    2121
    2222#include "Pyrogenesis.h"
    2323
     24#include "ps/EngineVersion.h"
    2425#include "lib/sysdep/sysdep.h"
    2526#include "lib/svn_revision.h"
    2627
    2728static const wchar_t* translate_no_mem = L"(no mem)";
    2829
     
    7172
    7273// for user convenience, bundle all logs into this file:
    7374void psBundleLogs(FILE* f)
    7475{
    7576    fwprintf(f, L"SVN Revision: %ls\n\n", svn_revision);
     77    fwprintf(f, L"Engine Version: %ls\n\n", engine_version);
    7678
    7779    fwprintf(f, L"System info:\n\n");
    7880    OsPath path1 = psLogDir()/"system_info.txt";
    7981    AppendAsciiFile(f, path1);
    8082    fwprintf(f, L"\n\n====================================\n\n");
  • source/ps/Replay.cpp

     
    2222#include "graphics/TerrainTextureManager.h"
    2323#include "lib/timer.h"
    2424#include "lib/file/file_system.h"
    2525#include "lib/res/h_mgr.h"
    2626#include "lib/tex/tex.h"
     27#include "ps/EngineVersion.h"
    2728#include "ps/Game.h"
    2829#include "ps/Loader.h"
    2930#include "ps/Profile.h"
     31#include "ps/Mod.h"
    3032#include "ps/ProfileViewer.h"
    3133#include "scriptinterface/ScriptInterface.h"
    3234#include "scriptinterface/ScriptStats.h"
    3335#include "simulation2/Simulation2.h"
    3436#include "simulation2/helpers/SimulationCommand.h"
     
    5254}
    5355
    5456CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) :
    5557    m_ScriptInterface(scriptInterface)
    5658{
     59    m_Stream = NULL;
     60}
     61
     62CReplayLogger::~CReplayLogger()
     63{
     64    delete m_Stream;
     65}
     66
     67void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
     68{
    5769    // Construct the directory name based on the PID, to be relatively unique.
    5870    // Append "-1", "-2" etc if we run multiple matches in a single session,
    5971    // to avoid accidentally overwriting earlier logs.
    6072
    6173    std::wstringstream name;
     
    6375
    6476    static int run = -1;
    6577    if (++run)
    6678        name << "-" << run;
    6779
    68     OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";
    69     CreateDirectories(path.Parent(), 0700);
    70     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    // 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);
    8092    *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
    8193}
    8294
    8395void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
    8496{
     
    100112        *m_Stream << "hash-quick " << Hexify(hash) << "\n";
    101113    else
    102114        *m_Stream << "hash " << Hexify(hash) << "\n";
    103115}
    104116
     117OsPath CReplayLogger::GetReplayDirectory()
     118{
     119    return m_Directory;
     120}
    105121////////////////////////////////////////////////////////////////
    106122
    107123CReplayPlayer::CReplayPlayer() :
    108124    m_Stream(NULL)
    109125{
     
    157173    JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
    158174    JSAutoRequest rq(cx);
    159175    std::string type;
    160176    while ((*m_Stream >> type).good())
    161177    {
    162 //      if (turn >= 1400) break;
    163 
    164178        if (type == "start")
    165179        {
    166180            std::string line;
    167181            std::getline(*m_Stream, line);
    168182            JS::RootedValue attribs(cx);
     
    198212            std::string replayHash;
    199213            *m_Stream >> replayHash;
    200214
    201215            bool quick = (type == "hash-quick");
    202216
    203 //          if (turn >= 1300)
    204 //          if (turn >= 0)
    205217            if (turn % 100 == 0)
    206218            {
    207219                std::string hash;
    208220                bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick);
    209221                ENSURE(ok);
     
    224236
    225237                g_Game->GetSimulation2()->Update(turnLength, commands);
    226238                commands.clear();
    227239            }
    228240
    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 
    234241            g_Profiler.Frame();
    235242
    236 //          if (turn % 1000 == 0)
    237 //              JS_GC(g_Game->GetSimulation2()->GetScriptInterface().GetContext());
    238 
    239243            if (turn % 20 == 0)
    240244                g_ProfileViewer.SaveToFile();
    241245        }
    242246        else
    243247        {
    244248            debug_printf("Unrecognised replay token %s\n", type.c_str());
    245249        }
    246250    }
    247251    }
    248 
     252    m_Stream->close();
    249253    g_Profiler2.SaveToFile();
    250254
    251255    std::string hash;
    252256    bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
    253257    ENSURE(ok);
  • source/ps/Replay.h

     
    4545
    4646    /**
    4747     * Optional hash of simulation state (for sync checking).
    4848     */
    4949    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;
    5055};
    5156
    5257/**
    5358 * Implementation of IReplayLogger that simply throws away all data.
    5459 */
     
    5661{
    5762public:
    5863    virtual void StartGame(JS::MutableHandleValue UNUSED(attribs)) { }
    5964    virtual void Turn(u32 UNUSED(n), u32 UNUSED(turnLength), std::vector<SimulationCommand>& UNUSED(commands)) { }
    6065    virtual void Hash(const std::string& UNUSED(hash), bool UNUSED(quick)) { }
     66    virtual OsPath GetReplayDirectory() { return OsPath();}
    6167};
    6268
    6369/**
    6470 * Implementation of IReplayLogger that saves data to a file in the logs directory.
    6571 */
     
    7177    ~CReplayLogger();
    7278
    7379    virtual void StartGame(JS::MutableHandleValue attribs);
    7480    virtual void Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands);
    7581    virtual void Hash(const std::string& hash, bool quick);
     82    virtual OsPath GetReplayDirectory();
    7683
    7784private:
    7885    ScriptInterface& m_ScriptInterface;
    7986    std::ostream* m_Stream;
     87    OsPath m_Directory;
    8088};
    8189
    8290/**
    8391 * Replay log replayer. Runs the log with no graphics and dumps some info to stdout.
    8492 */
     
    9098
    9199    void Load(const std::string& path);
    92100    void Replay(bool serializationtest, bool ooslog);
    93101
    94102private:
    95     std::istream* m_Stream;
     103    std::ifstream* m_Stream;
    96104};
    97105
    98106#endif // INCLUDED_REPLAY
  • source/ps/SavedGame.cpp

     
    1 /* Copyright (C) 2014 Wildfire Games.
     1/* Copyright (C) 2015 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
     
    2424#include "lib/allocators/shared_ptr.h"
    2525#include "lib/file/archive/archive_zip.h"
    2626#include "i18n/L10n.h"
    2727#include "lib/utf8.h"
    2828#include "ps/CLogger.h"
     29#include "ps/EngineVersion.h"
    2930#include "ps/Filesystem.h"
    3031#include "ps/Game.h"
    3132#include "ps/Mod.h"
    3233#include "scriptinterface/ScriptInterface.h"
    3334#include "simulation2/Simulation2.h"
     
    8485    JS::RootedValue metadata(cx);
    8586    JS::RootedValue initAttributes(cx, simulation.GetInitAttributes());
    8687    simulation.GetScriptInterface().Eval("({})", &metadata);
    8788    simulation.GetScriptInterface().SetProperty(metadata, "version_major", SAVED_GAME_VERSION_MAJOR);
    8889    simulation.GetScriptInterface().SetProperty(metadata, "version_minor", SAVED_GAME_VERSION_MINOR);
     90    simulation.GetScriptInterface().SetProperty(metadata, "engine_version", utf8_from_wstring(engine_version));
     91
    8992    simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded);
    9093    simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now);
    9194    simulation.GetScriptInterface().SetProperty(metadata, "player", playerID);
    9295    simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes);
    9396
     
    296299    JSContext* cx = scriptInterface.GetContext();
    297300    JSAutoRequest rq(cx);
    298301   
    299302    JS::RootedValue metainfo(cx);
    300303    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);
    304309    return metainfo;
    305310}
    306311
  • 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
     32OsPath VisualReplay::GetDirectoryName()
     33{
     34    return OsPath(psLogDir() / L"sim_log");
     35}
     36
     37JS::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
     60JS::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
     192bool 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
     202JS::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
     227void 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
     248JS::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"
     22class CSimulation2;
     23class CGUIManager;
     24
     25/**
     26 * Contains functions for visually replaying past games.
     27 */
     28
     29namespace 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 */
     41Status 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 */
     49OsPath 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 */
     57JS::Value GetReplays(ScriptInterface& scriptInterface);
     58
     59/**
     60 * Parses a commands.txt file and extracts metadata.
     61 */
     62JS::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 */
     70bool DeleteReplay(const std::wstring replayFile);
     71
     72/**
     73 * Returns the parsed header of the replay file (commands.txt).
     74 */
     75JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName);
     76
     77/**
     78 * Returns the data for the summary screen of a replay.
     79 */
     80JS::Value GetReplaySummary(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName);
     81
     82/**
     83 * Saves the summary data from the session to summary.json
     84 */
     85void SaveReplaySummary(std::wstring data);
     86
     87}
     88
     89#endif