Ticket #3258: replay_menu_wip_v3.patch

File replay_menu_wip_v3.patch, 52.0 KB (added by elexis, 9 years ago)

Now lists are sortable. Thanks to vlad and sander for making #2405 happen. Added error message for corrupted files. Found out that 0ad crashes (because the the ifstream opening the commands.txt files is not good) if you open the menu the second time and I don't know why yet... Screenshot: https://i.imgur.com/4ImuMrQ.jpg

  • 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();
     7var g_maxNameChars = 70;
     8var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     9var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     10
     11function init()
     12{
     13    loadReplays();
     14    updateReplayList();
     15}
     16
     17function loadReplays()
     18{
     19    g_Playernames = [];
     20    g_Replays = Engine.GetReplays();
     21    for(let replay of g_Replays)
     22    {
     23        // TODO: enhancement: ask Yves how to parse the json in c++ and send the object instead of the json string
     24        replay.attribs = JSON.parse(replay.header);
     25        delete replay.header;
     26       
     27        // TODO: enhancement: remove those copies for better performance
     28        replay.settings = replay.attribs.settings;
     29        replay.playerData = replay.settings.PlayerData;
     30       
     31        // Skirmish maps don't have that attribute
     32        if (!replay.settings.hasOwnProperty("Size"))
     33            replay.settings.Size = -1;
     34
     35        for(var i in replay.playerData)
     36        {
     37            // I've encountered a file where 'null' was added to the playerData array...
     38            if (!replay.playerData[i])
     39            {
     40                error("Replay " + replay.directory + " has bogus player data!");
     41                continue;
     42            }
     43           
     44            var name = replay.playerData[i].Name;
     45            if (g_Playernames.indexOf(name) == -1)
     46                g_Playernames.push({"name": name});
     47        }
     48    }
     49}
     50
     51function updateReplayListOrderSelection()
     52{
     53    g_ReplayListSortBy = Engine.GetGUIObjectByName("replaySelection").selected_column;
     54    g_ReplayListOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
     55
     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    // Get current game version and loaded mods
     117    var engineInfo = Engine.GetEngineInfo();
     118    // TODO: requirement filter compatible games
     119   
     120    // Filter replays and create GUI list data
     121    var replayListLabels = [];
     122    var replayListDirectories = [];
     123    var list_name = [];
     124    var list_players = [];
     125    var list_mapName = [];
     126    var list_mapSize = [];
     127    var list_popCapacity = [];
     128    var list_duration = [];
     129    g_ReplaysFiltered = [];
     130    for (var replay of g_Replays)
     131    {
     132        if (filterReplay(replay))
     133            continue;
     134
     135        g_ReplaysFiltered.push(replay);
     136       
     137        // TODO: enhancement: if (settings.GameType != "conquest") use other color
     138
     139        replayListLabels.push(replay.directory);
     140        replayListDirectories.push(replay.directory);
     141
     142        list_name.push(getReplayDateTime(replay));
     143        list_players.push(getReplayPlayernames(replay, true));
     144        list_mapName.push(getReplayMapName(replay));
     145        list_mapSize.push(getReplayMapSizeText(replay.settings.Size));
     146        list_popCapacity.push(getReplayPopCap(replay));
     147        list_duration.push(getReplayDuration(replay));
     148    }
     149
     150    // TODO: enhancement: remember last selection, like #3244
     151    if (replaySelection.selected >= list_name.length)
     152        replaySelection.selected = -1;
     153   
     154    // Update list
     155    replaySelection.list_name = list_name;
     156    replaySelection.list_players = list_players;
     157    replaySelection.list_mapName = list_mapName;
     158    replaySelection.list_mapSize = list_mapSize;
     159    replaySelection.list_popCapacity = list_popCapacity;
     160    replaySelection.list_duration = list_duration;
     161
     162    replaySelection.list = replayListLabels;
     163    replaySelection.list_data = replayListDirectories;
     164}
     165
     166function getReplayDateTime(replay)
     167{
     168    var date = new Date(replay.timestamp * 1000);
     169    var year = date.getFullYear();
     170    var month = ('0' + (date.getMonth() + 1)).slice(-2);
     171    var day = ('0' + date.getDate()).slice(-2);
     172    var hour = ('0' + date.getHours()).slice(-2);
     173    var minute = ('0' + date.getMinutes()).slice(-2);
     174    return year + "-" + month.slice(-2) + "-" + day.slice(-2) + "   " + hour + ":" + minute.slice(-2);
     175}
     176
     177function getReplayPlayernames(replay, shorten)
     178{
     179    // Extract playernames
     180    var playernames = [];
     181    for(let i in replay.playerData)
     182    {
     183        // I've encountered a file where 'null' was added to the playerData array...
     184        if (!replay.playerData[i])
     185        {
     186            error("Replay " + replay.directory + " has bogus player data!");
     187            continue;
     188        }
     189       
     190        var playername = escapeText(replay.playerData[i].Name);
     191       
     192        // TODO: enhancement: colorize playernames like in lobby using colorPlayerName
     193        // #3205 moves the function to common/color.js
     194        playernames.push(playername);
     195    }
     196   
     197    if (!shorten)
     198        return playernames;
     199   
     200    playernames = playernames.join(", ");
     201   
     202    // Shorten if too long
     203    if (playernames.length > g_maxNameChars)
     204        return playernames.substr(0, g_maxNameChars) + "...";
     205    else
     206        return playernames;
     207}
     208
     209function getReplayMapName(replay)
     210{
     211    return escapeText(translate(replay.settings.Name));
     212}
     213
     214function getReplayMapSizeText(tiles)
     215{
     216    var index = g_mapSizes.tiles.indexOf(tiles);
     217    if (index > -1)
     218        return g_mapSizes.shortNames[index]
     219    else
     220        return translateWithContext("map size", "Default");
     221}
     222
     223function getReplayPopCap(replay)
     224{
     225    if (replay.settings.PopulationCap == 10000)
     226        return translateWithContext("population capacity", "Unlimited");
     227    else
     228        return replay.settings.PopulationCap;   
     229}
     230
     231function getReplayDuration(replay)
     232{
     233    var hours = Math.floor(replay.duration / 3600);
     234    var min = Math.floor((replay.duration - hours * 3600) / 60);
     235    var sec = replay.duration % 60;
     236   
     237    if (hours > 0)
     238        return sprintf(translateWithContext("replay duration", "%(hours)sh %(minutes)sm %(seconds)ss"), { "hours": hours, "minutes": min, "seconds": sec});
     239    else if (min > 0)
     240        return sprintf(translateWithContext("replay duration", "%(minutes)sm %(seconds)ss"), { "minutes": min, "seconds": sec});
     241    else
     242        return sprintf(translateWithContext("replay duration", "%(seconds)ss"), { "seconds": sec});
     243}
     244
     245function initFilters()
     246{
     247    initDateFilter();
     248    initMapNameFilter();
     249    initMapSizeFilter();
     250    initPopCapFilter();
     251    initDurationFilter();
     252}
     253
     254function initDateFilter()
     255{
     256    var months = ["Any"];
     257    for(var replay of g_Replays)
     258    {
     259        var month = getDateTimeFilterVal(replay);
     260           
     261        if (months.indexOf(month) == -1)
     262            months.push(month);
     263    }
     264   
     265    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     266    dateTimeFilter.list = months;
     267    dateTimeFilter.list_data = months;
     268   
     269    if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= months.length)
     270        dateTimeFilter.selected = 0;
     271}
     272
     273function getDateTimeFilterVal(replay)
     274{
     275    var date = new Date(replay.timestamp * 1000);
     276    return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2)
     277}
     278
     279function initMapSizeFilter()
     280{
     281    // Get tilecounts actually used by maps
     282    var tiles = [];
     283    for(let replay of g_Replays)
     284    {
     285        if (tiles.indexOf(replay.settings.Size) == -1)
     286            tiles.push(replay.settings.Size);
     287    }
     288    tiles.sort();
     289
     290    // Add translated names
     291    var names = [];
     292    for(var size of tiles)
     293        names.push(getReplayMapSizeText(size));
     294   
     295    // Add "Any"
     296    names.unshift(translateWithContext("map size", "Any"));
     297    tiles.unshift("");
     298
     299    // Save values to filter
     300    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     301    mapSizeFilter.list = names;
     302    mapSizeFilter.list_data = tiles;
     303   
     304    if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= tiles.length)
     305        mapSizeFilter.selected = 0;
     306}
     307
     308function initMapNameFilter()
     309{
     310    var mapNames = [];
     311    for(var replay of g_Replays)
     312    {
     313        var mapName = escapeText(replay.settings.Name);
     314       
     315        if (mapNames.indexOf(mapName) == -1)
     316            mapNames.push(mapName);
     317    }
     318
     319    mapNames.sort();
     320    mapNames.unshift("Any");
     321   
     322    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     323    mapNameFilter.list = mapNames;
     324    mapNameFilter.list_data = mapNames;
     325   
     326    if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNames.length)
     327        mapNameFilter.selected = 0;
     328}
     329
     330function initPopCapFilter()
     331{
     332    var popCaps = [];
     333    for(var replay of g_Replays)
     334    {
     335        var popCap = getReplayPopCap(replay);
     336        if (popCaps.indexOf(popCap) == -1)
     337            popCaps.push(popCap);
     338    }
     339    popCaps.sort();
     340   
     341    popCaps.unshift("Any");
     342    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     343    populationFilter.list = popCaps;
     344    populationFilter.list_data = popCaps;
     345   
     346    if (populationFilter.selected == -1 || populationFilter.selected >= popCaps.length)
     347        populationFilter.selected = 0;
     348}
     349
     350function initDurationFilter()
     351{
     352    var data = [-1];
     353    var labels = ["Any"];
     354
     355    var g_DurationFilterMin = [0,  0, 15, 30, 45, 60,  90, 120];
     356    var g_DurationFilterMax = [0, 15, 30, 45, 60, 90, 120,  -1];
     357
     358    for(var i in g_DurationFilterMin)
     359    {
     360        if (i == 0)
     361            continue;
     362       
     363        data.push(i);
     364
     365        if (i == 1)
     366            labels.push("<" + g_DurationFilterMax[i] + "min");
     367        else if (i == g_DurationFilterMin.length -1)
     368            labels.push(">" + g_DurationFilterMin[i] + "min");
     369        else
     370            labels.push(g_DurationFilterMin[i] + "-" + g_DurationFilterMax[i] + "min");
     371    }
     372   
     373    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     374    durationFilter.list = labels;
     375    durationFilter.list_data = data;
     376   
     377    if (durationFilter.selected == -1 || durationFilter.selected >= data.length)
     378        durationFilter.selected = 0;
     379}
     380
     381function applyFilters()
     382{
     383    // Update the list of replays
     384    updateReplayList();
     385
     386    // Update info box about the replay currently selected
     387    updateReplaySelection();
     388}
     389
     390// Returns true if the replay should not be listed.
     391function filterReplay(replay)
     392{
     393    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     394    var playersFilter = Engine.GetGUIObjectByName("playersFilter");
     395    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     396    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     397    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     398    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     399
     400    // Filter date/time (select a month)
     401    if (dateTimeFilter.selected > 0)
     402    {
     403        let selectedMonth = dateTimeFilter.list_data[dateTimeFilter.selected];
     404        if (getDateTimeFilterVal(replay) != selectedMonth) 
     405            return true;
     406    }
     407
     408    // Filter selected players
     409    let playerText = playersFilter.caption;
     410    if (playerText.length)
     411    {
     412        // Player and botnames can contain spaces
     413        // We just check if all words of all players are somewhere in the playerlist
     414        playerText = playerText.toLowerCase().split(" ");
     415        let replayPlayers = replay.playerData.map(function(player){ return player.Name.toLowerCase() }).join(" ");
     416        for(let word of playerText)
     417        {
     418            if (replayPlayers.indexOf(word) == -1)
     419                return true;
     420        }
     421    }
     422
     423    // Filter map name
     424    if (mapNameFilter.selected > 0)
     425    {
     426        if (getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
     427            return true;
     428    }
     429
     430    // Filter map size
     431    if (mapSizeFilter.selected > 0)
     432    {
     433        let selectedMapSize = mapSizeFilter.list_data[mapSizeFilter.selected];
     434        if (replay.settings.Size != selectedMapSize)
     435            return true;
     436    }
     437
     438    // Filter population capacity
     439    if (populationFilter.selected > 0 &&
     440            getReplayPopCap(replay) != populationFilter.list_data[populationFilter.selected])
     441    {
     442        return true;
     443    }
     444
     445    // Filter game duration
     446    if (durationFilter.selected > 0)
     447    {
     448        var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     449        var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     450
     451        let minutes = replay.duration / 60;
     452        let min = g_DurationFilterMin[durationFilter.selected];
     453        let max = g_DurationFilterMax[durationFilter.selected];
     454       
     455        if (minutes < min || (max > -1 && minutes > max))
     456            return true;
     457    }
     458
     459    return false;
     460}
     461
     462function updateReplaySelection()
     463{
     464    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     465    var replaySelected = selected > -1;
     466   
     467    Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
     468    Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
     469    Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
     470    Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
     471   
     472    if (!replaySelected)
     473        return;
     474   
     475    var replay = g_ReplaysFiltered[selected];
     476    var mapData;
     477
     478    // Load map data
     479    if (replay.settings.mapType == "random" && replay.attribs.map == "random")
     480        mapData = {"settings": {"Description": translate("A randomly selected map.")}};
     481    else if (replay.settings.mapType == "random" && Engine.FileExists(replay.attribs.map + ".json"))
     482        mapData = Engine.ReadJSONFile(replay.attribs.map + ".json");
     483    else if (Engine.FileExists(replay.attribs.map + ".xml"))
     484        mapData = Engine.LoadMapSettings(replay.attribs.map + ".xml");
     485    else
     486        // Warn the player if we can't find the map.
     487        warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: replay.attribs.map }));
     488
     489    var players = getReplayPlayernames(replay, false);
     490   
     491    // Display the map name, number of players, the names of the players, the map size and the map type.
     492    Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.settings.Name);
     493    Engine.GetGUIObjectByName("sgNbPlayers").caption = players.length;
     494    Engine.GetGUIObjectByName("sgPlayersNames").caption = players.join(", ");
     495    Engine.GetGUIObjectByName("sgMapSize").caption = getReplayMapSizeText(replay.settings.Size);
     496    Engine.GetGUIObjectByName("sgMapType").caption = replay.settings.mapType;
     497
     498    // Display map description if it exists, otherwise display a placeholder.
     499    if (mapData && mapData.settings.Description)
     500        var mapDescription = translate(mapData.settings.Description);
     501    else
     502        var mapDescription = translate("Sorry, no description available.");
     503
     504    // Display map preview if it exists, otherwise display a placeholder.
     505    if (mapData && mapData.settings.Preview)
     506        var mapPreview = mapData.settings.Preview;
     507    else
     508        var mapPreview = "nopreview.png";
     509
     510    Engine.GetGUIObjectByName("sgMapDescription").caption = mapDescription;
     511    Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapPreview;
     512}
     513
     514function startReplay()
     515{
     516    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     517    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     518    reallyStartVisualReplay(replayDirectory);
     519}
     520
     521function reallyStartVisualReplay(replayDirectory)
     522{
     523    // TODO: requirement: check replay compatibility before really loading it (similar to savegames)
     524    Engine.StartVisualReplay(replayDirectory);
     525    Engine.SwitchGuiPage("page_loading.xml", {
     526        "attribs": Engine.GetVisualReplayAttributes(replayDirectory),
     527        "isNetworked" : false,
     528        "playerAssignments": {},
     529        "savedGUIData": ""
     530    });
     531}
     532
     533function deleteReplay()
     534{
     535    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     536   
     537    if (replaySelection.selected == -1)
     538        return;
     539   
     540    var replayLabel = replaySelection.list[replaySelection.selected];
     541    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     542
     543    // Ask for confirmation
     544    var btCaptions = [translate("Yes"), translate("No")];
     545    var btCode = [function(){ reallyDeleteReplay(replayDirectory); }, null];
     546    messageBox(500, 200, sprintf(translate("\"%(label)s\""), { label: replayLabel }) + "\n" + translate("Are you sure to delete this replay permanently?"), translate("DELETE"), 0, btCaptions, btCode);
     547}
     548
     549function deleteReplayWithoutConfirmation()
     550{
     551    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     552    var replayDirectory = replaySelection.list_data[replaySelection.selected];
     553    reallyDeleteReplay(replayDirectory);
     554}
     555
     556function reallyDeleteReplay(replayDirectory)
     557{
     558    if (!Engine.DeleteReplay(replayDirectory))
     559        error(sprintf("Could not delete replay '%(id)s'", { id: replayDirectory }));
     560
     561    // Refresh replay list
     562    init();
     563}
     564 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" sortable="true" default_column="name" sprite_asc="ModernArrowDown" sprite_desc="ModernArrowUp" sprite_not_sorted="ModernNotSorted" size="0 35 100% 100%-50" font="sans-stroke-13">
     67                <action on="SelectionChange">updateReplaySelection();</action>
     68                <action on="SelectionColumnChange">updateReplayListOrderSelection();</action>
     69                <!-- Columns -->
     70                <!-- 0ad crashes if there is no column with the id "name"! -->
     71                <def id="name" color="128 128 128" width="110">
     72                    <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute>
     73                </def>
     74                <def id="players" color="128 128 128" width="400">
     75                    <translatableAttribute id="heading" context="replay">Players</translatableAttribute>
     76                </def>
     77                <def id="mapName" color="128 128 128" width="140">
     78                    <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute>
     79                </def>
     80                <def id="mapSize" color="128 128 128" width="80">
     81                    <translatableAttribute id="heading" context="replay">Size</translatableAttribute>
     82                </def>
     83                <def id="popCapacity" color="0 128 128" width="80">
     84                    <translatableAttribute id="heading" context="replay">Population</translatableAttribute>
     85                </def>
     86                <def id="duration" color="128 128 128" width="80">
     87                    <translatableAttribute id="heading" context="replay">Duration</translatableAttribute>
     88                </def>
     89            </object>
     90        </object>
     91       
     92        <!-- Right Panel: Replay Details -->
     93        <object name="rightPanel" size="100%-250 30 100%-20 100%-20" >
     94           
     95            <object name="replayInfoEmpty" size="0 0 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false">
     96                <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/>
     97                <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%">
     98                    <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/>
     99                </object>
     100            </object>
     101           
     102            <object name="replayInfo" size="0 0 100% 100%-90" type="image" sprite="ModernDarkBoxGold" hidden="true">
     103
     104                <!-- Map Name -->
     105                <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/>
     106
     107                <!-- Map Preview -->
     108                <object name="sgMapPreview" size="5 25 100%-5 190"  type="image" sprite=""/>
     109
     110                <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/>
     111
     112                <!-- Map Type -->
     113                <object size="5 195 50% 240" type="image" sprite="ModernItemBackShadeLeft">
     114                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     115                        <translatableAttribute id="caption">Map Type:</translatableAttribute>
     116                    </object>
     117                </object>
     118                <object size="50% 195 100%-5 240" type="image" sprite="ModernItemBackShadeRight">
     119                    <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     120                </object>
     121
     122                <object size="5 239 100%-5 240" type="image" sprite="ModernWhiteLine" z="25"/>
     123
     124                <!-- Map Size -->
     125                <object size="5 240 50% 285" type="image" sprite="ModernItemBackShadeLeft">
     126                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     127                        <translatableAttribute id="caption">Map Size:</translatableAttribute>
     128                    </object>
     129                </object>
     130                <object size="50% 240 100%-5 285" type="image" sprite="ModernItemBackShadeRight">
     131                    <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     132                </object>
     133
     134                <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/>
     135
     136                <!-- Map Description -->
     137                <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 75%">
     138                    <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/>
     139                </object>
     140
     141                <object type="image" sprite="ModernDarkBoxWhite" size="3% 75%+5 97% 99%">
     142                    <!-- Number of Players -->
     143                    <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText">
     144                        <translatableAttribute id="caption">Players:</translatableAttribute>
     145                    </object>
     146                    <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/>
     147
     148                    <!-- Player Names -->
     149                    <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/>
     150                </object>
     151               
     152            </object>
     153        </object>
     154
     155        <!-- Bottom Panel: Buttons. -->
     156        <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" >
     157       
     158            <object type="button" style="StoneButton" size="0%+25 0 17%+25 100%">
     159                <translatableAttribute id="caption">Main Menu</translatableAttribute>
     160                <action on="Press">
     161                    Engine.SwitchGuiPage("page_pregame.xml");
     162                </action>
     163            </object>
     164
     165            <object name="deleteReplayButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%" hotkey="session.savedgames.delete">
     166                <translatableAttribute id="caption">Delete</translatableAttribute>
     167                <action on="Press">
     168                    if (!this.enabled)
     169                        return;
     170                    if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation"))
     171                        deleteReplayWithoutConfirmation();
     172                    else
     173                        deleteReplay();
     174                </action>
     175            </object>
     176
     177            <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%">
     178                <translatableAttribute id="caption">Start Replay</translatableAttribute>
     179                <action on="Press">
     180                    startReplay();
     181                </action>
     182            </object>
     183           
     184        </object>
     185    </object>
     186</objects>
     187 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: When getting into the menu the second time, the stream is not good anymore somehow and it crashes.
     104    // If we just return here if the stream is not good, 0ad can still not other files like map preview images
     105
     106    // File must begin with "start"
     107    std::string type;
     108    ENSURE((*replayStream >> type).good() && type == "start");
     109
     110    // Save header to parse it later in JS
     111    std::getline(*replayStream, header);
     112
     113    // Parse commands & turns
     114    u32 currentTurn = 0;
     115    u32 currentMinute = 0;
     116    duration = 0;
     117    std::vector<player_id_t> defeatedPlayers;
     118    std::map<player_id_t, u16> currentCommandCount;
     119    std::map<player_id_t, u16> commandsPerMinute;
     120    while ((*replayStream >> type).good())
     121    {
     122        if (type == "turn")
     123        {
     124            // Store turn & turn length
     125            u32 turn = 0;
     126            u32 turnLength = 0;
     127            *replayStream >> turn >> turnLength;
     128            if (turn != currentTurn) // happens for replays of rejoined clients
     129                return false;
     130
     131            // Compute game duration
     132            duration += turnLength;
     133
     134            // Compute commands per minute
     135            u32 min = duration / 1000 / 60;
     136            if (min > currentMinute)
     137            {
     138                //commandsPerMinute[player][currentMinute] = currentCommandCount[player];
     139                currentMinute = min;
     140            }
     141        }
     142        else if (type == "cmd")
     143        {
     144            // Extract player id
     145            player_id_t player;
     146            *replayStream >> player;
     147
     148            // Increase command count
     149            if (currentCommandCount.find(player) == currentCommandCount.end())
     150                currentCommandCount[player] = 1;
     151            else
     152                currentCommandCount[player]++;
     153
     154            // Store command
     155            std::string command;
     156            std::getline(*replayStream, command);
     157
     158            // Parse command
     159            JS::RootedValue commandData(scriptInterface.GetContext());
     160            std::wstring commandType;
     161            if (!scriptInterface.ParseJSON(command, &commandData))
     162            {
     163                LOGERROR("Corrupted replay command '%s' in file '%s'", command, filename.c_str());
     164                return false;
     165            }
     166            scriptInterface.GetProperty(commandData, "type", commandType);
     167
     168            // Save defeated players {"type":"defeat-player","playerId":2}
     169            if (commandType == L"defeat-player")
     170            {
     171                JS::RootedValue defeatedPlayer(scriptInterface.GetContext());
     172                scriptInterface.GetProperty(commandData, "playerId", &defeatedPlayer);
     173                //defeatedPlayers.push_back(defeatedPlayer);
     174            }
     175        }
     176        else if (type == "hash" || type == "hash-quick")
     177        {
     178            // skip hash values
     179            std::string replayHash;
     180            *replayStream >> replayHash;
     181        }
     182        else if (type == "end")
     183        {
     184            currentTurn++;
     185        }
     186        else
     187        {
     188            LOGERROR("Unrecognized replay data '%s' in file %s", type, filename.c_str());
     189        }
     190    }
     191    duration = duration / 1000;
     192    // finalReplayTurn = currentTurn;
     193    // TODO: compute average turn time, if turn length varies
     194    return true;
     195}
     196
     197bool VisualReplay::DeleteReplay(const std::wstring replayDirectory)
     198{
     199    if (replayDirectory == L"")
     200        return false;
     201
     202    const OsPath directory = OsPath(GetDirectoryName() / replayDirectory);
     203    return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     204}
  • 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