Ticket #3258: replay_menu_wip_v2.patch

File replay_menu_wip_v2.patch, 50.2 KB (added by elexis, 9 years ago)

Works for r16777. Implements a nice list, filter capabilities and preview data. Remaining Todo: requires a check so that replays of old versions will be greyed out and not replayed. Some more data like players that resigned will be displayed if a spoiler button is pressed.

  • 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 = [];
     2var g_ReplaysFiltered = [];
     3var g_Playernames = []; // All playernames of all replays, used for autocomplete
     4var g_mapSizes = initMapSizes();
     5var g_maxNameChars = 70;
     6var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     7var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     8
     9function init()
     10{
     11    loadReplays();
     12    updateReplayList();
     13}
     14
     15function loadReplays()
     16{
     17    g_Playernames = [];
     18    g_Replays = Engine.GetReplays();
     19    for(let replay of g_Replays)
     20    {
     21        // TODO: enhancement: ask Yves how to parse the json in c++ and send the object instead of the json string
     22        replay.attribs = JSON.parse(replay.header);
     23        delete replay.header;
     24       
     25        // TODO: enhancement: remove those copies for better performance
     26        replay.settings = replay.attribs.settings;
     27        replay.playerData = replay.settings.PlayerData;
     28       
     29        // Skirmish maps don't have that attribute
     30        if (!replay.settings.hasOwnProperty("Size"))
     31            replay.settings.Size = -1;
     32
     33        for(var i in replay.playerData)
     34        {
     35            // I've encountered a file where 'null' was added to the playerData array...
     36            if (!replay.playerData[i])
     37            {
     38                error("Replay " + replay.directory + " has bogus player data!");
     39                continue;
     40            }
     41           
     42            var name = replay.playerData[i].Name;
     43            if (g_Playernames.indexOf(name) == -1)
     44                g_Playernames.push({"name": name});
     45        }
     46    }
     47}
     48
     49function updateReplayList()
     50{
     51    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     52   
     53    // Filters depend on the replay list
     54    initFilters();
     55
     56    if (g_Replays.length == 0)
     57        replaySelection.selected = -1;
     58
     59    // TODO: enhancement: do better sorting after #2405
     60    g_Replays.sort(function(a,b)
     61    {
     62        return a.timestamp < b.timestamp;
     63    });
     64
     65    // Get current game version and loaded mods
     66    var engineInfo = Engine.GetEngineInfo();
     67    // TODO: requirement filter compatible games
     68   
     69    // Fill lists with items
     70    var replayListLabels = [];
     71    var replayListDirectories = [];
     72    var list_name = [];
     73    var list_players = [];
     74    var list_mapName = [];
     75    var list_mapSize = [];
     76    var list_popCapacity = [];
     77    var list_duration = [];
     78    g_ReplaysFiltered = [];
     79    for (var replay of g_Replays)
     80    {
     81        if (filterReplay(replay))
     82            continue;
     83
     84        g_ReplaysFiltered.push(replay);
     85       
     86        // TODO: enhancement: if (settings.GameType != "conquest") use other color
     87
     88        replayListLabels.push(replay.directory);
     89        replayListDirectories.push(replay.directory);
     90
     91        list_name.push(getReplayDateTime(replay));
     92        list_players.push(getReplayPlayernames(replay, true));
     93        list_mapName.push(getReplayMapName(replay));
     94        list_mapSize.push(getReplayMapSizeText(replay.settings.Size));
     95        list_popCapacity.push(getReplayPopCap(replay));
     96        list_duration.push(getReplayDuration(replay));
     97    }
     98
     99    // TODO: enhancement: remember last selection, like #3244
     100    if (replaySelection.selected >= list_name.length)
     101        replaySelection.selected = -1;
     102   
     103    // Update list
     104    replaySelection.list_name = list_name;
     105    replaySelection.list_players = list_players;
     106    replaySelection.list_mapName = list_mapName;
     107    replaySelection.list_mapSize = list_mapSize;
     108    replaySelection.list_popCapacity = list_popCapacity;
     109    replaySelection.list_duration = list_duration;
     110
     111    replaySelection.list = replayListLabels;
     112    replaySelection.list_data = replayListDirectories;
     113}
     114
     115function getReplayDateTime(replay)
     116{
     117    var date = new Date(replay.timestamp * 1000);
     118    var year = date.getFullYear();
     119    var month = ('0' + (date.getMonth() + 1)).slice(-2);
     120    var day = ('0' + date.getDate()).slice(-2);
     121    var hour = ('0' + date.getHours()).slice(-2);
     122    var minute = ('0' + date.getMinutes()).slice(-2);
     123    return year + "-" + month.slice(-2) + "-" + day.slice(-2) + "   " + hour + ":" + minute.slice(-2);
     124}
     125
     126function getReplayPlayernames(replay, shorten)
     127{
     128    // Extract playernames
     129    var playernames = [];
     130    for(let i in replay.playerData)
     131    {
     132        // I've encountered a file where 'null' was added to the playerData array...
     133        if (!replay.playerData[i])
     134        {
     135            error("Replay " + replay.directory + " has bogus player data!");
     136            continue;
     137        }
     138       
     139        var playername = escapeText(replay.playerData[i].Name);
     140       
     141        // TODO: enhancement: colorize playernames like in lobby using colorPlayerName
     142        // #3205 moves the function to common/color.js
     143        playernames.push(playername);
     144    }
     145   
     146    if (!shorten)
     147        return playernames;
     148   
     149    playernames = playernames.join(", ");
     150   
     151    // Shorten if too long
     152    if (playernames.length > g_maxNameChars)
     153        return playernames.substr(0, g_maxNameChars) + "...";
     154    else
     155        return playernames;
     156}
     157
     158function getReplayMapName(replay)
     159{
     160    return escapeText(replay.settings.Name);
     161}
     162
     163function getReplayMapSizeText(tiles)
     164{
     165    var index = g_mapSizes.tiles.indexOf(tiles);
     166    if (index > -1)
     167        return g_mapSizes.shortNames[index]
     168    else
     169        return translateWithContext("map size", "Default");
     170}
     171
     172function getReplayPopCap(replay)
     173{
     174    if (replay.settings.PopulationCap == 10000)
     175        return "Unlimited";
     176    else
     177        return replay.settings.PopulationCap;   
     178}
     179
     180function getReplayDuration(replay)
     181{
     182    var hours = Math.floor(replay.duration / 3600);
     183    var min = Math.floor((replay.duration - hours * 3600) / 60);
     184    var sec = replay.duration % 60;
     185   
     186    if (hours > 0)
     187        return sprintf(translateWithContext("replay duration", "%(hours)sh %(minutes)sm %(seconds)ss"), { "hours": hours, "minutes": min, "seconds": sec});
     188    else if (min > 0)
     189        return sprintf(translateWithContext("replay duration", "%(minutes)sm %(seconds)ss"), { "minutes": min, "seconds": sec});
     190    else
     191        return sprintf(translateWithContext("replay duration", "%(seconds)ss"), { "seconds": sec});
     192}
     193
     194function initFilters()
     195{
     196    initDateFilter();
     197    initMapNameFilter();
     198    initMapSizeFilter();
     199    initPopCapFilter();
     200    initDurationFilter();
     201}
     202
     203function initDateFilter()
     204{
     205    var months = ["Any"];
     206    for(var replay of g_Replays)
     207    {
     208        var month = getDateTimeFilterVal(replay);
     209           
     210        if (months.indexOf(month) == -1)
     211            months.push(month);
     212    }
     213   
     214    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     215    dateTimeFilter.list = months;
     216    dateTimeFilter.list_data = months;
     217   
     218    if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= months.length)
     219        dateTimeFilter.selected = 0;
     220}
     221
     222function getDateTimeFilterVal(replay)
     223{
     224    var date = new Date(replay.timestamp * 1000);
     225    return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2)
     226}
     227
     228function initMapSizeFilter()
     229{
     230    // Get tilecounts actually used by maps
     231    var tiles = [];
     232    for(let replay of g_Replays)
     233    {
     234        if (tiles.indexOf(replay.settings.Size) == -1)
     235            tiles.push(replay.settings.Size);
     236    }
     237    tiles.sort();
     238
     239    // Add translated names
     240    var names = [];
     241    for(var size of tiles)
     242        names.push(getReplayMapSizeText(size));
     243   
     244    // Add "Any"
     245    names.unshift(translateWithContext("map size", "Any"));
     246    tiles.unshift("");
     247
     248    // Save values to filter
     249    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     250    mapSizeFilter.list = names;
     251    mapSizeFilter.list_data = tiles;
     252   
     253    if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= tiles.length)
     254        mapSizeFilter.selected = 0;
     255}
     256
     257function initMapNameFilter()
     258{
     259    var mapNames = ["Any"];
     260    for(var replay of g_Replays)
     261    {
     262        var mapName = escapeText(replay.settings.Name);
     263       
     264        if (mapNames.indexOf(mapName) == -1)
     265            mapNames.push(mapName);
     266    }
     267
     268   
     269    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     270    mapNameFilter.list = mapNames;
     271    mapNameFilter.list_data = mapNames;
     272   
     273    if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNames.length)
     274        mapNameFilter.selected = 0;
     275}
     276
     277function initPopCapFilter()
     278{
     279    var popCaps = [];
     280    for(var replay of g_Replays)
     281    {
     282        var popCap = getReplayPopCap(replay);
     283        if (popCaps.indexOf(popCap) == -1)
     284            popCaps.push(popCap);
     285    }
     286    popCaps.sort();
     287   
     288    popCaps.unshift("Any");
     289    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     290    populationFilter.list = popCaps;
     291    populationFilter.list_data = popCaps;
     292   
     293    if (populationFilter.selected == -1 || populationFilter.selected >= popCaps.length)
     294        populationFilter.selected = 0;
     295}
     296
     297function initDurationFilter()
     298{
     299    var data = [-1];
     300    var labels = ["Any"];
     301
     302    var g_DurationFilterMin = [0,  0, 15, 30, 45, 60,  90, 120];
     303    var g_DurationFilterMax = [0, 15, 30, 45, 60, 90, 120,  -1];
     304
     305    for(var i in g_DurationFilterMin)
     306    {
     307        if (i == 0)
     308            continue;
     309       
     310        data.push(i);
     311
     312        if (i == 1)
     313            labels.push("<" + g_DurationFilterMax[i] + "min");
     314        else if (i == g_DurationFilterMin.length -1)
     315            labels.push(">" + g_DurationFilterMin[i] + "min");
     316        else
     317            labels.push(g_DurationFilterMin[i] + "-" + g_DurationFilterMax[i] + "min");
     318    }
     319   
     320    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     321    durationFilter.list = labels;
     322    durationFilter.list_data = data;
     323   
     324    if (durationFilter.selected == -1 || durationFilter.selected >= data.length)
     325        durationFilter.selected = 0;
     326}
     327
     328function applyFilters()
     329{
     330    // Update the list of replays
     331    updateReplayList();
     332
     333    // Update info box about the replay currently selected
     334    updateReplaySelection();
     335}
     336
     337// Returns true if the replay should not be listed.
     338function filterReplay(replay)
     339{
     340    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     341    var playersFilter = Engine.GetGUIObjectByName("playersFilter");
     342    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     343    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     344    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     345    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     346
     347    // Filter date/time (select a month)
     348    if (dateTimeFilter.selected > 0)
     349    {
     350        let selectedMonth = dateTimeFilter.list_data[dateTimeFilter.selected];
     351        if (getDateTimeFilterVal(replay) != selectedMonth) 
     352            return true;
     353    }
     354
     355    // Filter selected players
     356    let playerText = playersFilter.caption;
     357    if (playerText.length)
     358    {
     359        // Player and botnames can contain spaces
     360        // We just check if all words of all players are somewhere in the playerlist
     361        playerText = playerText.toLowerCase().split(" ");
     362        let replayPlayers = replay.playerData.map(function(player){ return player.Name.toLowerCase() }).join(" ");
     363        for(let word of playerText)
     364        {
     365            if (replayPlayers.indexOf(word) == -1)
     366                return true;
     367        }
     368    }
     369
     370    // Filter map name
     371    if (mapNameFilter.selected > 0)
     372    {
     373        if (getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
     374            return true;
     375    }
     376
     377    // Filter map size
     378    if (mapSizeFilter.selected > 0)
     379    {
     380        let selectedMapSize = mapSizeFilter.list_data[mapSizeFilter.selected];
     381        if (replay.settings.Size != selectedMapSize)
     382            return true;
     383    }
     384
     385    // Filter population capacity
     386    if (populationFilter.selected > 0 &&
     387            getReplayPopCap(replay) != populationFilter.list_data[populationFilter.selected])
     388    {
     389        return true;
     390    }
     391
     392    // Filter game duration
     393    if (durationFilter.selected > 0)
     394    {
     395        var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     396        var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     397
     398        let minutes = replay.duration / 60;
     399        let min = g_DurationFilterMin[durationFilter.selected];
     400        let max = g_DurationFilterMax[durationFilter.selected];
     401       
     402        if (minutes < min || (max > -1 && minutes > max))
     403            return true;
     404    }
     405
     406    return false;
     407}
     408
     409function updateReplaySelection()
     410{
     411    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     412    var replaySelected = selected > -1;
     413   
     414    Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
     415    Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
     416    Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
     417    Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
     418   
     419    if (!replaySelected)
     420        return;
     421   
     422    var replay = g_ReplaysFiltered[selected];
     423    var mapData;
     424
     425    // Load map data
     426    if (replay.settings.mapType == "random" && replay.attribs.map == "random")
     427        mapData = {"settings": {"Description": translate("A randomly selected map.")}};
     428    else if (replay.settings.mapType == "random" && Engine.FileExists(replay.attribs.map + ".json"))
     429        mapData = Engine.ReadJSONFile(replay.attribs.map + ".json");
     430    else if (Engine.FileExists(replay.attribs.map + ".xml"))
     431        mapData = Engine.LoadMapSettings(replay.attribs.map + ".xml");
     432    else
     433        // Warn the player if we can't find the map.
     434        warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: replay.attribs.map }));
     435
     436    var players = getReplayPlayernames(replay, false);
     437   
     438    // Display the map name, number of players, the names of the players, the map size and the map type.
     439    Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.settings.Name);
     440    Engine.GetGUIObjectByName("sgNbPlayers").caption = players.length;
     441    Engine.GetGUIObjectByName("sgPlayersNames").caption = players.join(", ");
     442    Engine.GetGUIObjectByName("sgMapSize").caption = getReplayMapSizeText(replay.settings.Size);
     443    Engine.GetGUIObjectByName("sgMapType").caption = replay.settings.mapType;
     444
     445    // Display map description if it exists, otherwise display a placeholder.
     446    if (mapData && mapData.settings.Description)
     447        var mapDescription = translate(mapData.settings.Description);
     448    else
     449        var mapDescription = translate("Sorry, no description available.");
     450
     451    // Display map preview if it exists, otherwise display a placeholder.
     452    if (mapData && mapData.settings.Preview)
     453        var mapPreview = mapData.settings.Preview;
     454    else
     455        var mapPreview = "nopreview.png";
     456
     457    Engine.GetGUIObjectByName("sgMapDescription").caption = mapDescription;
     458    Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapPreview;
     459}
     460
     461function startReplay()
     462{
     463    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     464    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     465    reallyStartVisualReplay(replayDirectory);
     466}
     467
     468function reallyStartVisualReplay(replayDirectory)
     469{
     470    // TODO: requirement: check replay compatibility before really loading it (similar to savegames)
     471    Engine.StartVisualReplay(replayDirectory);
     472    Engine.SwitchGuiPage("page_loading.xml", {
     473        "attribs": Engine.GetVisualReplayAttributes(replayDirectory),
     474        "isNetworked" : false,
     475        "playerAssignments": {},
     476        "savedGUIData": ""
     477    });
     478}
     479
     480function deleteReplay()
     481{
     482    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     483   
     484    if (replaySelection.selected == -1)
     485        return;
     486   
     487    var replayLabel = replaySelection.list[replaySelection.selected];
     488    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     489
     490    // Ask for confirmation
     491    var btCaptions = [translate("Yes"), translate("No")];
     492    var btCode = [function(){ reallyDeleteReplay(replayDirectory); }, null];
     493    messageBox(500, 200, sprintf(translate("\"%(label)s\""), { label: replayLabel }) + "\n" + translate("Are you sure to delete this replay permanently?"), translate("DELETE"), 0, btCaptions, btCode);
     494}
     495
     496function deleteReplayWithoutConfirmation()
     497{
     498    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     499    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     500    reallyDeleteReplay(replayDirectory);
     501}
     502
     503function reallyDeleteReplay(replayDirectory)
     504{
     505    if (!Engine.DeleteReplay(replayDirectory))
     506        error(sprintf("Could not delete replay '%(id)s'", { id: replayDirectory }));
     507
     508    // Refresh replay list
     509    init();
     510}
     511 No newline at end of file
  • binaries/data/mods/public/gui/replay/replay.xml

     
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4
     5    <script file="gui/common/functions_global_object.js" />
     6    <script file="gui/common/functions_utility.js" />
     7    <script file="gui/replay/replay.js" />
     8
     9    <object type="image" style="ModernWindow" size="0 0 100% 100%" name="replayWindow">
     10
     11        <object style="ModernLabelText" type="text" size="50%-128 0%+4 50%+128 36">
     12            <translatableAttribute id="caption">Replay Games</translatableAttribute>
     13        </object>
     14       
     15        <!-- Left Panel: Filters & Replay List -->
     16        <object name="leftPanel" size="3% 5% 100%-255 97%">
     17
     18            <!-- Filters -->
     19            <object name="filterPanel" size="0 0 100% 24">
     20                <object name="dateTimeFilter"
     21                    type="dropdown"
     22                    style="ModernDropDown"
     23                    size="5 0 110-10 100%"
     24                    font="sans-bold-13">
     25                    <action on="SelectionChange">applyFilters();</action>
     26                </object>
     27                <object name="playersFilter"
     28                    type="input"
     29                    style="ModernInput"
     30                    size="110-5 0 110+400-10 100%"
     31                    font="sans-bold-13">
     32                    <action on="Press">applyFilters();</action>
     33                    <action on="Tab">autoCompleteNick("playersFilter", g_Playernames);</action>
     34                </object>
     35                <object name="mapNameFilter"
     36                    type="dropdown"
     37                    style="ModernDropDown"
     38                    size="110+400-5 0 110+400+140-10 100%"
     39                    font="sans-bold-13">
     40                    <action on="SelectionChange">applyFilters();</action>
     41                </object>
     42                <object name="mapSizeFilter"
     43                    type="dropdown"
     44                    style="ModernDropDown"
     45                    size="110+400+140-5 0 110+400+140+80-10 100%"
     46                    font="sans-bold-13">
     47                    <action on="SelectionChange">applyFilters();</action>
     48                </object>
     49                <object name="populationFilter"
     50                    type="dropdown"
     51                    style="ModernDropDown"
     52                    size="110+400+140+80-5 0 110+400+140+80+80-10 100%"
     53                    font="sans-bold-13">
     54                    <action on="SelectionChange">applyFilters();</action>
     55                </object>
     56                <object name="durationFilter"
     57                    type="dropdown"
     58                    style="ModernDropDown"
     59                    size="110+400+140+80+80-5 0 110+400+140+80+80+80-10 100%"
     60                    font="sans-bold-13">
     61                    <action on="SelectionChange">applyFilters();</action>
     62                </object>
     63            </object>
     64
     65            <!-- Replay list -->
     66            <object name="replaySelection" style="ModernList" type="olist" size="0 35 100% 100%-50" font="sans-stroke-13">
     67                <action on="SelectionChange">updateReplaySelection();</action>
     68                <!-- Columns -->
     69                <!-- 0ad crashes if there is no column with the id "name"! -->
     70                <def id="name" color="128 128 128" width="110">
     71                    <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute>
     72                </def>
     73                <def id="players" color="128 128 128" width="400">
     74                    <translatableAttribute id="heading" context="replay">Players</translatableAttribute>
     75                </def>
     76                <def id="mapName" color="128 128 128" width="140">
     77                    <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute>
     78                </def>
     79                <def id="mapSize" color="128 128 128" width="80">
     80                    <translatableAttribute id="heading" context="replay">Size</translatableAttribute>
     81                </def>
     82                <def id="popCapacity" color="0 128 128" width="80">
     83                    <translatableAttribute id="heading" context="replay">Population</translatableAttribute>
     84                </def>
     85                <def id="duration" color="128 128 128" width="80">
     86                    <translatableAttribute id="heading" context="replay">Duration</translatableAttribute>
     87                </def>
     88            </object>
     89        </object>
     90       
     91        <!-- Right Panel: Replay Details -->
     92        <object name="rightPanel" size="100%-250 30 100%-20 100%-20" >
     93           
     94            <object name="replayInfoEmpty" size="0 0 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false">
     95                <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/>
     96                <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%">
     97                    <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/>
     98                </object>
     99            </object>
     100           
     101            <object name="replayInfo" size="0 0 100% 100%-90" type="image" sprite="ModernDarkBoxGold" hidden="true">
     102
     103                <!-- Map Name -->
     104                <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/>
     105
     106                <!-- Map Preview -->
     107                <object name="sgMapPreview" size="5 25 100%-5 190"  type="image" sprite=""/>
     108
     109                <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/>
     110
     111                <!-- Map Type -->
     112                <object size="5 195 50% 240" type="image" sprite="ModernItemBackShadeLeft">
     113                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     114                        <translatableAttribute id="caption">Map Type:</translatableAttribute>
     115                    </object>
     116                </object>
     117                <object size="50% 195 100%-5 240" type="image" sprite="ModernItemBackShadeRight">
     118                    <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     119                </object>
     120
     121                <object size="5 239 100%-5 240" type="image" sprite="ModernWhiteLine" z="25"/>
     122
     123                <!-- Map Size -->
     124                <object size="5 240 50% 285" type="image" sprite="ModernItemBackShadeLeft">
     125                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     126                        <translatableAttribute id="caption">Map Size:</translatableAttribute>
     127                    </object>
     128                </object>
     129                <object size="50% 240 100%-5 285" type="image" sprite="ModernItemBackShadeRight">
     130                    <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     131                </object>
     132
     133                <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/>
     134
     135                <!-- Map Description -->
     136                <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 75%">
     137                    <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/>
     138                </object>
     139
     140                <object type="image" sprite="ModernDarkBoxWhite" size="3% 75%+5 97% 99%">
     141                    <!-- Number of Players -->
     142                    <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText">
     143                        <translatableAttribute id="caption">Players:</translatableAttribute>
     144                    </object>
     145                    <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/>
     146
     147                    <!-- Player Names -->
     148                    <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/>
     149                </object>
     150               
     151            </object>
     152        </object>
     153
     154        <!-- Bottom Panel: Buttons. -->
     155        <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" >
     156       
     157            <object type="button" style="StoneButton" size="0%+25 0 17%+25 100%">
     158                <translatableAttribute id="caption">Main Menu</translatableAttribute>
     159                <action on="Press">
     160                    Engine.SwitchGuiPage("page_pregame.xml");
     161                </action>
     162            </object>
     163
     164            <object name="deleteReplayButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%" hotkey="session.savedgames.delete">
     165                <translatableAttribute id="caption">Delete</translatableAttribute>
     166                <action on="Press">
     167                    if (!this.enabled)
     168                        return;
     169                    if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation"))
     170                        deleteReplayWithoutConfirmation();
     171                    else
     172                        deleteReplay();
     173                </action>
     174            </object>
     175
     176            <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%">
     177                <translatableAttribute id="caption">Start Replay</translatableAttribute>
     178                <action on="Press">
     179                    startReplay();
     180                </action>
     181            </object>
     182           
     183        </object>
     184    </object>
     185</objects>
     186 No newline at end of file
  • binaries/data/mods/public/gui/replay/styles.xml

     
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<styles>
     4    <style name="MapPlayerList"
     5        buffer_zone="8"
     6        font="sans-14"
     7        scrollbar="true"
     8        scrollbar_style="ModernScrollBar"
     9        scroll_bottom="true"
     10        textcolor="white"
     11        text_align="left"
     12        text_valign="top"
     13    />
     14</styles>
  • build/svn_revision/engine_version.txt

     
     1L"0.0.19"
  • 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
     287JS::Value StartVisualReplay(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     288{
     289    JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext();
     290    JSAutoRequest rq(cxGui);
     291    JS::RootedValue guiContextMetadata(cxGui);
     292
     293    ENSURE(!g_NetServer);
     294    ENSURE(!g_NetClient);
     295    ENSURE(!g_Game);
     296
     297    CStrW replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / L"commands.txt").string();
     298    std::string replayFile( replayFilePath.begin(), replayFilePath.end() );
     299
     300    if (FileExists(OsPath(replayFile)))
     301    {
     302        g_Game = new CGame(false, false);
     303        g_Game->StartReplay(replayFile);
     304    }
     305
     306    return guiContextMetadata;
     307}
     308
     309JS::Value GetVisualReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     310{
     311    CStrW replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / "commands.txt").string();
     312    std::string replayFile( replayFilePath.begin(), replayFilePath.end() );
     313
     314    if (!FileExists(OsPath(replayFile)))
     315        return {};
     316
     317    std::istream* replayStream = new std::ifstream(replayFile.c_str());
     318    std::string type, line;
     319    ENSURE((*replayStream >> type).good() && type == "start");
     320
     321    std::getline(*replayStream, line);
     322    JS::RootedValue attribs(pCxPrivate->pScriptInterface->GetContext());
     323    pCxPrivate->pScriptInterface->ParseJSON(line, &attribs);
     324    delete replayStream;
     325    return attribs;
     326}
     327
    286328void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
    287329{
    288330    ENSURE(g_NetServer);
    289331    //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere
    290332    // (with no obvious reason).
     
    415457bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name)
    416458{
    417459    return SavedGames::DeleteSavedGame(name);
    418460}
    419461
     462JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
     463{
     464    return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
     465}
     466
     467bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring replayFile)
     468{
     469    return VisualReplay::DeleteReplay(replayFile);
     470}
     471
    420472void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url)
    421473{
    422474    sys_open_url(url);
    423475}
    424476
     
    9741026    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGame>("SaveGame");
    9751027    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGamePrefix>("SaveGamePrefix");
    9761028    scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave");
    9771029    scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad");
    9781030
     1031    // Visual replay
     1032    scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays");
     1033    scriptInterface.RegisterFunction<bool, std::wstring, &DeleteReplay>("DeleteReplay");
     1034    scriptInterface.RegisterFunction<JS::Value, std::wstring, &StartVisualReplay>("StartVisualReplay");
     1035    scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetVisualReplayAttributes>("GetVisualReplayAttributes");
     1036
    9791037    // Misc functions
    9801038    scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
    9811039    scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID");
    9821040    scriptInterface.RegisterFunction<void, int, &SetPlayerID>("SetPlayerID");
    9831041    scriptInterface.RegisterFunction<void, std::string, &OpenURL>("OpenURL");
  • source/lib/svn_revision.cpp

     
    1 /* Copyright (c) 2010 Wildfire Games
     1/* Copyright (c) 2015 Wildfire Games
    22 *
    33 * Permission is hereby granted, free of charge, to any person obtaining
    44 * a copy of this software and associated documentation files (the
    55 * "Software"), to deal in the Software without restriction, including
    66 * without limitation the rights to use, copy, modify, merge, publish,
     
    2323#include "precompiled.h"
    2424
    2525wchar_t svn_revision[] =
    2626#include "../../build/svn_revision/svn_revision.txt"
    2727;
     28
     29wchar_t engine_version[] =
     30#include "../../build/svn_revision/engine_version.txt"
     31;
  • source/lib/svn_revision.h

     
    1 /* Copyright (c) 2010 Wildfire Games
     1/* Copyright (c) 2015 Wildfire Games
    22 *
    33 * Permission is hereby granted, free of charge, to any person obtaining
    44 * a copy of this software and associated documentation files (the
    55 * "Software"), to deal in the Software without restriction, including
    66 * without limitation the rights to use, copy, modify, merge, publish,
     
    1919 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    2020 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    2121 */
    2222
    2323extern wchar_t svn_revision[];
     24extern wchar_t engine_version[];
  • source/lobby/XmppClient.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
     
    1616 */
    1717
    1818#include "precompiled.h"
    1919#include "XmppClient.h"
    2020#include "StanzaExtensions.h"
    21 
    2221#include "glooxwrapper/glooxwrapper.h"
    2322#include "i18n/L10n.h"
     23#include "lib/svn_revision.h"
    2424#include "lib/utf8.h"
    2525#include "ps/CLogger.h"
    2626#include "ps/ConfigDB.h"
    2727#include "scriptinterface/ScriptInterface.h"
    2828
     
    9393    // if the server doesn't list any supported SASL mechanism or the response
    9494    // has been modified to exclude those.
    9595    const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain;
    9696    m_client->setSASLMechanisms(mechs);
    9797
     98    // copy engine version string
     99    std::wstring ev(engine_version);
     100    std::string ev_str(ev.begin(), ev.end());
    98101    m_client->registerConnectionListener(this);
    99102    m_client->setPresence(gloox::Presence::Available, -1);
    100     m_client->disco()->setVersion("Pyrogenesis", "0.0.19");
     103    m_client->disco()->setVersion("Pyrogenesis", ev_str);
    101104    m_client->disco()->setIdentity("client", "bot");
    102105    m_client->setCompression(false);
    103106
    104107    m_client->registerStanzaExtension(new GameListQuery());
    105108    m_client->registerIqHandler(this, ExtGameListQuery);
  • 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;
     
    173178{
    174179    m_IsReplay = true;
    175180    ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface();
    176181
    177182    SetTurnManager(new CNetReplayTurnManager(*m_Simulation2, GetReplayLogger()));
     183    SetPlayerID(-1);
    178184
    179185    m_ReplayPath = replayPath;
    180186    m_ReplayStream = new std::ifstream(m_ReplayPath.c_str());
    181187
    182188    std::string type;
  • 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
     
    7171
    7272// for user convenience, bundle all logs into this file:
    7373void psBundleLogs(FILE* f)
    7474{
    7575    fwprintf(f, L"SVN Revision: %ls\n\n", svn_revision);
     76    fwprintf(f, L"Engine Version: %ls\n\n", engine_version);
    7677
    7778    fwprintf(f, L"System info:\n\n");
    7879    OsPath path1 = psLogDir()/"system_info.txt";
    7980    AppendAsciiFile(f, path1);
    8081    fwprintf(f, L"\n\n====================================\n\n");
  • source/ps/Replay.cpp

     
    5252}
    5353
    5454CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) :
    5555    m_ScriptInterface(scriptInterface)
    5656{
     57    m_Stream = NULL;
     58}
     59
     60CReplayLogger::~CReplayLogger()
     61{
     62    delete m_Stream;
     63}
     64
     65void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
     66{
    5767    // Construct the directory name based on the PID, to be relatively unique.
    5868    // Append "-1", "-2" etc if we run multiple matches in a single session,
    5969    // to avoid accidentally overwriting earlier logs.
    6070
    6171    std::wstringstream name;
     
    6676        name << "-" << run;
    6777
    6878    OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";
    6979    CreateDirectories(path.Parent(), 0700);
    7080    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 {
    8081    *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
    8182}
    8283
    8384void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
    8485{
     
    157158    JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
    158159    JSAutoRequest rq(cx);
    159160    std::string type;
    160161    while ((*m_Stream >> type).good())
    161162    {
    162 //      if (turn >= 1400) break;
    163 
    164163        if (type == "start")
    165164        {
    166165            std::string line;
    167166            std::getline(*m_Stream, line);
    168167            JS::RootedValue attribs(cx);
  • 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
     
    2929#include "ps/Filesystem.h"
    3030#include "ps/Game.h"
    3131#include "ps/Mod.h"
    3232#include "scriptinterface/ScriptInterface.h"
    3333#include "simulation2/Simulation2.h"
     34#include "lib/svn_revision.h"
    3435
    3536static const int SAVED_GAME_VERSION_MAJOR = 1; // increment on incompatible changes to the format
    3637static const int SAVED_GAME_VERSION_MINOR = 0; // increment on compatible changes to the format
    3738
    3839// TODO: we ought to check version numbers when loading files
     
    296297    JSContext* cx = scriptInterface.GetContext();
    297298    JSAutoRequest rq(cx);
    298299   
    299300    JS::RootedValue metainfo(cx);
    300301    scriptInterface.Eval("({})", &metainfo);
    301     scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR);
    302     scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR);
    303     scriptInterface.SetProperty(metainfo, "mods"         , g_modsLoaded);
     302    scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR);
     303    scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR);
     304
     305    std::wstring eng(engine_version);
     306    scriptInterface.SetProperty(metainfo, "engine_version", eng);
     307
     308    scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded);
    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
     22#include "graphics/GameView.h"
     23#include "gui/GUIManager.h"
     24#include "lib/allocators/shared_ptr.h"
     25#include "lib/svn_revision.h"
     26#include "lib/utf8.h"
     27#include "ps/CLogger.h"
     28#include "ps/Filesystem.h"
     29#include "ps/Game.h"
     30#include "scriptinterface/ScriptInterface.h"
     31
     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    OsPath root = GetDirectoryName();
     46    int i = 0;
     47    DirectoryNames directories;
     48    JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
     49    GetDirectoryEntries(root, NULL, &directories);
     50    for (auto& directory : directories)
     51    {
     52        OsPath replayFile(root / directory / L"commands.txt");
     53
     54        if (!FileExists(replayFile))
     55            continue;
     56
     57        // Get fileinfo (size, timestamp)
     58        CFileInfo fileInfo;
     59        GetFileInfo(replayFile, &fileInfo);
     60
     61        u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
     62        u64 fileSize = (u64)fileInfo.Size();
     63
     64        // Don't list empty files
     65        if (fileSize == 0)
     66            continue;
     67
     68        // Extract metadata
     69        u64 duration = 0;
     70        std::string header("");
     71        if (!LoadReplayData(scriptInterface, replayFile, header, duration))
     72            continue;
     73
     74        // Don't list too short replays
     75        if (duration < 5)
     76            continue;
     77
     78        JS::RootedValue replay(cx);
     79        scriptInterface.Eval("({})", &replay);
     80        //scriptInterface.SetProperty(replay, "file", replayFile);
     81        scriptInterface.SetProperty(replay, "directory", directory);
     82        scriptInterface.SetProperty(replay, "timestamp", fileTime); // TODO: enhancement: save the timestamp to the replayfile itself       scriptInterface.SetProperty(replay, "mapName", "Unknown Nomad");
     83        scriptInterface.SetProperty(replay, "header", header);
     84        scriptInterface.SetProperty(replay, "duration", duration);
     85
     86        JS_SetElement(cx, replays, i++, replay);
     87    }
     88    return JS::ObjectValue(*replays);
     89}
     90
     91// Works similar to CGame::LoadReplayData()
     92// Extracts metadata from file header, loads commands and computes duration of the game
     93int VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile, std::string& header, u64& duration)
     94{
     95    if (!FileExists(replayFile))
     96        return false;
     97
     98    // Open file
     99    CStrW filenameW = replayFile.string();
     100    std::string filename( filenameW.begin(), filenameW.end() );
     101    std::ifstream* replayStream = new std::ifstream(filename.c_str());
     102
     103    // TODO: (debug only) when hotloading replay.js, the stream is not good anymore somehow and it crashes
     104
     105    // File must begin with "start"
     106    std::string type;
     107    ENSURE((*replayStream >> type).good() && type == "start");
     108
     109    // Save header to parse it later in JS
     110    std::getline(*replayStream, header);
     111
     112    // Parse commands & turns
     113    u32 currentTurn = 0;
     114    u32 currentMinute = 0;
     115    duration = 0;
     116    std::vector<player_id_t> defeatedPlayers;
     117    std::map<player_id_t, u16> currentCommandCount;
     118    std::map<player_id_t, u16> commandsPerMinute;
     119    while ((*replayStream >> type).good())
     120    {
     121        if (type == "turn")
     122        {
     123            // Store turn & turn length
     124            u32 turn = 0;
     125            u32 turnLength = 0;
     126            *replayStream >> turn >> turnLength;
     127            if (turn != currentTurn) // happens for replays of rejoined clients
     128                return false;
     129
     130            // Compute game duration
     131            duration += turnLength;
     132
     133            // Compute commands per minute
     134            u32 min = duration / 1000 / 60;
     135            if (min > currentMinute)
     136            {
     137                //commandsPerMinute[player][currentMinute] = currentCommandCount[player];
     138                currentMinute = min;
     139            }
     140        }
     141        else if (type == "cmd")
     142        {
     143            // Extract player id
     144            player_id_t player;
     145            *replayStream >> player;
     146
     147            // Increase command count
     148            if (currentCommandCount.find(player) == currentCommandCount.end())
     149                currentCommandCount[player] = 1;
     150            else
     151                currentCommandCount[player]++;
     152
     153            // Store command
     154            std::string command;
     155            std::getline(*replayStream, command);
     156
     157            // Parse command
     158            JS::RootedValue commandData(scriptInterface.GetContext());
     159            std::wstring commandType;
     160            if (!scriptInterface.ParseJSON(command, &commandData))
     161                return false;
     162            scriptInterface.GetProperty(commandData, "type", commandType);
     163
     164            // Save defeated players {"type":"defeat-player","playerId":2}
     165            if (commandType == L"defeat-player")
     166            {
     167                JS::RootedValue defeatedPlayer(scriptInterface.GetContext());
     168                scriptInterface.GetProperty(commandData, "playerId", &defeatedPlayer);
     169                //defeatedPlayers.push_back(defeatedPlayer);
     170            }
     171        }
     172        else if (type == "hash" || type == "hash-quick")
     173        {
     174            // skip hash values
     175            std::string replayHash;
     176            *replayStream >> replayHash;
     177        }
     178        else if (type == "end")
     179        {
     180            currentTurn++;
     181        }
     182        else
     183        {
     184            LOGERROR("Unrecognized replay data '%s'", type);
     185        }
     186    }
     187    duration = duration / 1000;
     188    // finalReplayTurn = currentTurn;
     189    // TODO: compute average turn time, if turn length varies
     190    return true;
     191}
     192
     193bool VisualReplay::DeleteReplay(const std::wstring replayDirectory)
     194{
     195    if (replayDirectory == L"")
     196        return false;
     197
     198    const OsPath directory = OsPath(GetDirectoryName() / replayDirectory);
     199    return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     200}
  • source/ps/VisualReplay.h

     
     1/* Copyright (C) 2015 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#ifndef INCLUDED_REPlAY
     19#define INCLUDED_REPlAY
     20
     21#include "scriptinterface/ScriptInterface.h"
     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 */
     62int LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile, std::string& header, u64& duration);
     63
     64/**
     65 * Permanently deletes the visual replay (including the parent directory)
     66 *
     67 * @param replayFile path to commands.txt, whose parent directory will be deleted
     68 * @return true if deletion was successful, or false on error
     69 */
     70bool DeleteReplay(const std::wstring replayFile);
     71}
     72
     73#endif