Ticket #3258: replay_menu_v7_r16841.patch

File replay_menu_v7_r16841.patch, 71.1 KB (added by elexis, 9 years ago)

Now displays defeated players if spoiler checkbox is checked. Some refactoring (most notably moved ai difficulty names to a separate file). Some performance optimization. Renamed replay.js to replaymenu.js (and other related files).

  • binaries/data/mods/public/gui/aiconfig/aiconfig.js

     
    2323        }
    2424    }
    2525    aiSelection.selected = selected;
    2626
    2727    var aiDiff = Engine.GetGUIObjectByName("aiDifficulty");
    28     // Translation: AI difficulty level.
    29     aiDiff.list = [translateWithContext("aiDiff", "Sandbox"), translateWithContext("aiDiff", "Very Easy"), translateWithContext("aiDiff", "Easy"), translateWithContext("aiDiff", "Medium"), translateWithContext("aiDiff", "Hard"), translateWithContext("aiDiff", "Very Hard")];
     28    aiDiff.list = initAIDifficulties();
    3029    aiDiff.selected = settings.difficulty;
    3130}
    3231
     32function initAIDifficulties()
     33{
     34    var difficulties = Engine.ReadJSONFile("gui/aiconfig/difficulties.json");
     35    if (!difficulties)
     36    {
     37        error("Failed to parse AI difficulties (check difficulties.json for valid data)");
     38        return ["Default"];
     39    }
     40
     41    for (let i in difficulties)
     42        difficulties[i] = translateWithContext("aiDiff", difficulties[i]);
     43
     44    return difficulties;
     45}
     46
    3347function selectAI(idx)
    3448{
    3549    var id = g_AIs[idx].id;
    3650    var name = g_AIs[idx].data.name;
    3751    var description = g_AIs[idx].data.description;
  • binaries/data/mods/public/gui/aiconfig/difficulties.json

     
     1[
     2    "Sandbox",
     3    "Very Easy",
     4    "Easy",
     5    "Medium",
     6    "Hard",
     7    "Very Hard"
     8]
  • binaries/data/mods/public/gui/common/functions_utility.js

     
    8383    return text.substr(0, 255).replace(/\\/g, "\\\\").replace(/\[/g, "\\[");
    8484}
    8585
    8686// ====================================================================
    8787
     88function capitalizeFirstLetter(text) {
     89    return text.charAt(0).toUpperCase() + text.slice(1);
     90}
     91
     92// ====================================================================
     93
    8894// Load default player data, for when it's not otherwise specified
    8995function initPlayerDefaults()
    9096{
    9197    var data = Engine.ReadJSONFile("simulation/data/player_defaults.json");
    9298    if (!data || !data.PlayerData)
  • binaries/data/mods/public/gui/common/functions_utility_loadsave.js

     
    2323/**
    2424 * Check the version compatibility between the saved game to be loaded and the engine
    2525 */
    2626function hasSameVersion(metadata, engineInfo)
    2727{
    28     return (metadata.version_major == engineInfo.version_major);
     28    return (metadata.version_major == engineInfo.version_major && metadata.hasOwnProperty("engine_version") && metadata.engine_version == engineInfo.engine_version);
    2929}
    3030
    3131/**
    3232 * Check the mod compatibility between the saved game to be loaded and the engine
    3333 */
  • binaries/data/mods/public/gui/gamesetup/gamesetup.js

     
    487487        }
    488488        Engine.SwitchGuiPage("page_loading.xml", {
    489489            "attribs": g_GameAttributes,
    490490            "isNetworked" : g_IsNetworked,
    491491            "playerAssignments": g_PlayerAssignments,
    492             "isController": g_IsController
     492            "isController": g_IsController,
     493            "g_IsReplay" : false
    493494        });
    494495        break;
    495496
    496497    case "chat":
    497498        addChatMessage({ "type": "message", "guid": message.guid, "text": message.text });
  • binaries/data/mods/public/gui/page_replaymenu.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>replaymenu/styles.xml</include>
     14    <include>replaymenu/replaymenu.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_replaymenu.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/replaymenu/replaymenu.js

     
     1var g_Replays = []; // All replays found including metadata
     2var g_ReplaysFiltered = []; // List of replays after filtering
     3var g_Playernames = []; // All playernames of all replays, used for autocomplete
     4var g_ReplayListSortBy = "name"; // Used for sorting
     5var g_ReplayListOrder = -1; // Used for sorting
     6var g_mapSizes = initMapSizes(); // already translated
     7var g_CivData = loadCivData();
     8var g_EngineInfo = Engine.GetEngineInfo();
     9var g_DefaultPlayerData = initPlayerDefaults();
     10var g_maxPlayerlistChars = 70;
     11var g_DurationFilterMin = [ 0,  0, 15, 30, 45, 60,  90, 120];
     12var g_DurationFilterMax = [-1, 15, 30, 45, 60, 90, 120,  -1];
     13
     14function init()
     15{
     16    loadReplays();
     17    updateReplayList();
     18}
     19
     20function loadReplays()
     21{
     22    g_Playernames = [];
     23    g_Replays = Engine.GetReplays();
     24    for (let replay of g_Replays)
     25    {
     26        // TODO: enhancement: remove those copies for better performance
     27        replay.settings = replay.attribs.settings;
     28        replay.playerData = replay.settings.PlayerData;
     29
     30        // Use time saved in file, otherwise file mod date
     31        replay.timestamp = replay.attribs.hasOwnProperty("timestamp") ? replay.attribs.timestamp : replay.filemod_timestamp;
     32       
     33        // Skirmish maps don't have that attribute
     34        if (!replay.settings.hasOwnProperty("Size"))
     35            replay.settings.Size = -1;
     36
     37        for (var i in replay.playerData)
     38        {
     39            // I've encountered a file where 'null' was added to the playerData array...
     40            if (!replay.playerData[i])
     41            {
     42                error("Replay " + replay.file + " has bogus player data!");
     43                continue;
     44            }
     45            var name = replay.playerData[i].Name;
     46            if (g_Playernames.indexOf(name) == -1)
     47                g_Playernames.push({"name": name});
     48        }
     49    }
     50}
     51
     52function updateReplayListOrderSelection()
     53{
     54    g_ReplayListSortBy = Engine.GetGUIObjectByName("replaySelection").selected_column;
     55    g_ReplayListOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
     56    applyFilters();
     57}
     58
     59function updateReplayList()
     60{
     61    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     62   
     63    // Filters depend on the replay list
     64    initFilters();
     65
     66    if (g_Replays.length == 0)
     67        replaySelection.selected = -1;
     68
     69    // Sort replays
     70    g_Replays.sort(function(a,b)
     71    {
     72        var cmpA, cmpB;
     73        switch (g_ReplayListSortBy)
     74        {
     75        case 'name':
     76            cmpA = a.timestamp;
     77            cmpB = b.timestamp;
     78            break;
     79        case 'duration':
     80            cmpA = a.duration;
     81            cmpB = b.duration;
     82            break;
     83        case 'players':
     84            cmpA = a.playerData.length;
     85            cmpB = b.playerData.length;
     86            break;
     87        case 'mapName':
     88            cmpA = getReplayMapName(a);
     89            cmpB = getReplayMapName(b);
     90            break;
     91        case 'mapSize':
     92            cmpA = a.settings.Size;
     93            cmpB = b.settings.Size;
     94            break;
     95        case 'popCapacity':
     96            cmpA = a.settings.PopulationCap;
     97            cmpB = b.settings.PopulationCap;
     98            break;
     99        }
     100
     101        // Sort by selected column
     102        if (cmpA < cmpB)
     103            return -g_ReplayListOrder;
     104        else if (cmpA > cmpB)
     105            return g_ReplayListOrder;
     106
     107        // Sort by date/time as a tiebreaker and keep most recent replays at the top
     108        if (a.timestamp < b.timestamp)
     109            return 1;
     110        else if (a.timestamp > b.timestamp)
     111            return -1;
     112
     113        return 0;
     114    });
     115   
     116    // Filter replays and create GUI list data
     117    var replayListLabels = [];
     118    var replayListDirectories = [];
     119    var list_name = [];
     120    var list_players = [];
     121    var list_mapName = [];
     122    var list_mapSize = [];
     123    var list_popCapacity = [];
     124    var list_duration = [];
     125    g_ReplaysFiltered = [];
     126    for (var replay of g_Replays)
     127    {
     128        if (filterReplay(replay))
     129            continue;
     130
     131        g_ReplaysFiltered.push(replay);
     132        replayListLabels.push(replay.directory);
     133        replayListDirectories.push(replay.directory);
     134
     135        list_name.push(Greyout(replay, getReplayDateTime(replay)));
     136        list_players.push(Greyout(replay, getReplayPlayernames(replay, true)));
     137        list_mapName.push(Greyout(replay, getReplayMapName(replay)));
     138        list_mapSize.push(Greyout(replay, getReplayMapSizeText(replay.settings.Size)));
     139        list_popCapacity.push(Greyout(replay, getReplayPopCap(replay)));
     140        list_duration.push(Greyout(replay, getReplayDuration(replay)));
     141    }
     142
     143    // TODO: enhancement: remember last selection, like #3244
     144    if (replaySelection.selected >= list_name.length)
     145        replaySelection.selected = -1;
     146   
     147    // Update list
     148    replaySelection.list_name = list_name;
     149    replaySelection.list_players = list_players;
     150    replaySelection.list_mapName = list_mapName;
     151    replaySelection.list_mapSize = list_mapSize;
     152    replaySelection.list_popCapacity = list_popCapacity;
     153    replaySelection.list_duration = list_duration;
     154
     155    replaySelection.list = replayListLabels;
     156    replaySelection.list_data = replayListDirectories;
     157}
     158
     159function Greyout(replay, text)
     160{
     161    if (isReplayCompatible(replay))
     162        return text;
     163    else
     164        return '[color="128 128 128"]' + text + '[/color]';
     165}
     166
     167function getReplayDateTime(replay)
     168{
     169    // TODO: enhancement: use local time instead of UTC
     170    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm"))
     171}
     172
     173function getReplayPlayernames(replay, shorten)
     174{
     175    // Extract playernames
     176    var playernames = [];
     177    for (let i in replay.playerData)
     178    {
     179        // I've encountered a file where 'null' was added to the playerData array...
     180        if (!replay.playerData[i])
     181            continue;
     182       
     183        var playername = escapeText(replay.playerData[i].Name);
     184       
     185        // TODO: enhancement: colorize playernames like in lobby using colorPlayerName
     186        // #3205 moves the function to common/color.js
     187        playernames.push(playername);
     188    }
     189   
     190    if (!shorten)
     191        return playernames;
     192   
     193    playernames = playernames.join(", ");
     194   
     195    // Shorten if too long
     196    if (playernames.length > g_maxPlayerlistChars)
     197        return playernames.substr(0, g_maxPlayerlistChars) + "...";
     198    else
     199        return playernames;
     200}
     201
     202function getReplayMapName(replay)
     203{
     204    return escapeText(translate(replay.settings.Name));
     205}
     206
     207function getReplayMapSizeText(tiles)
     208{
     209    var index = g_mapSizes.tiles.indexOf(tiles);
     210    if (index > -1)
     211        return g_mapSizes.shortNames[index]
     212    else
     213        return translateWithContext("map size", "Default");
     214}
     215
     216function getReplayPopCap(replay)
     217{
     218    if (replay.settings.PopulationCap == 10000)
     219        return translateWithContext("population capacity", "Unlimited");
     220    else
     221        return replay.settings.PopulationCap;   
     222}
     223
     224function getReplayDuration(replay)
     225{
     226    return timeToString(replay.duration * 1000);
     227}
     228
     229function getReplayTeamText(replay)
     230{
     231    var metadata = Engine.GetReplayMetadata(replay.directory);
     232    var spoiler = Engine.GetGUIObjectByName("showSpoiler").checked;
     233
     234    // Extract players and civs by team
     235    var teams = {};
     236    var teamCount = 0;
     237    for (var i in replay.playerData)
     238    {
     239        var playerData = replay.playerData[i];
     240       
     241        // I've encountered a file where 'null' was added to the playerData array...
     242        if (!playerData)
     243            return "Error!";
     244
     245        var team = playerData.Team;
     246        // TODO: enhancement: too few contrast, see #3205
     247        var col = playerData.Color ? playerData.Color : g_DefaultPlayerData[parseInt(i)+1].Color;
     248        var civ = g_CivData[playerData.Civ];
     249        var aiDiff = initAIDifficulties();
     250       
     251        // Create player text
     252        var playername = '[color="' + col.r + " " + col.g + " " + col.b + '"]' + escapeText(playerData.Name) + "[/color]";
     253        var playerDetails = translate(civ.Name);
     254        if (playerData.AI != "")
     255            playerDetails += ", " + sprintf(translateWithContext("replay AI text", "%(difficulty)s %(name)s AI"), { "difficulty": aiDiff[playerData.AIDiff], "name": capitalizeFirstLetter(playerData.AI) });
     256        if (spoiler && metadata.playerStates && metadata.playerStates[parseInt(i) + 1].state == "defeated")
     257            playerDetails += ", defeated";
     258
     259        // Add player text to playerlist
     260        var player = playername + escapeText(" (" + playerDetails + ")");
     261        if (teams.hasOwnProperty(team))
     262        {
     263            teams[team].push(player);
     264        }
     265        else
     266        {
     267            teams[team] = [player];
     268            teamCount++;
     269        }
     270    }
     271   
     272    // Create player info text sorted by team
     273    var teamString = "";
     274    for(var team in teams)
     275    {
     276        if (teamCount > 1)
     277        {
     278            if (team == -1)
     279                teamString += '[font="sans-bold-14"]No Team:[/font]\n';
     280            else
     281                teamString += '[font="sans-bold-14"]Team ' + (parseInt(team) + 1) + "[/font]:\n";
     282        }
     283        teamString += teams[team].join("\n") + "\n";
     284    }
     285   
     286    return teamString;
     287}
     288
     289function isReplayCompatible(replay)
     290{
     291    if (!isReplayCompatible_EngineVersion(replay))
     292        return false;
     293   
     294    // Now check mods
     295    if (replay.attribs.mods)
     296        var gameMods = replay.attribs.mods;
     297    else
     298        var gameMods = [];
     299
     300    if (gameMods.length != g_EngineInfo.mods.length)
     301        return false;
     302   
     303    for (var i = 0; i < gameMods.length; ++i)
     304        if (gameMods[i] != g_EngineInfo.mods[i])
     305            return false;
     306   
     307    return true;
     308}
     309
     310function isReplayCompatible_EngineVersion(replay)
     311{
     312    return replay.attribs.hasOwnProperty("engine_version")
     313            && replay.attribs.engine_version == g_EngineInfo.engine_version;
     314}
     315
     316function initFilters()
     317{
     318    initDateFilter();
     319    initMapNameFilter();
     320    initMapSizeFilter();
     321    initPopCapFilter();
     322    initDurationFilter();
     323}
     324
     325function initDateFilter()
     326{
     327    var months = ["Any"];
     328    for (var replay of g_Replays)
     329    {
     330        var month = getDateTimeFilterVal(replay);
     331           
     332        if (months.indexOf(month) == -1)
     333            months.push(month);
     334    }
     335   
     336    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     337    dateTimeFilter.list = months;
     338    dateTimeFilter.list_data = months;
     339   
     340    if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= months.length)
     341        dateTimeFilter.selected = 0;
     342}
     343
     344function getDateTimeFilterVal(replay)
     345{
     346    var date = new Date(replay.timestamp * 1000);
     347    return date.getFullYear() + "-" + ('0' + (date.getMonth() + 1)).slice(-2)
     348}
     349
     350function initMapSizeFilter()
     351{
     352    // Get tilecounts actually used by maps
     353    var tiles = [];
     354    for (let replay of g_Replays)
     355    {
     356        if (tiles.indexOf(replay.settings.Size) == -1)
     357            tiles.push(replay.settings.Size);
     358    }
     359    tiles.sort();
     360
     361    // Add translated names
     362    var names = [];
     363    for (var size of tiles)
     364        names.push(getReplayMapSizeText(size));
     365   
     366    // Add "Any"
     367    names.unshift(translateWithContext("map size", "Any"));
     368    tiles.unshift("");
     369
     370    // Save values to filter
     371    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     372    mapSizeFilter.list = names;
     373    mapSizeFilter.list_data = tiles;
     374   
     375    if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= tiles.length)
     376        mapSizeFilter.selected = 0;
     377}
     378
     379function initMapNameFilter()
     380{
     381    var mapNames = [];
     382    for (var replay of g_Replays)
     383    {
     384        var mapName = escapeText(replay.settings.Name);
     385       
     386        if (mapNames.indexOf(mapName) == -1)
     387            mapNames.push(mapName);
     388    }
     389
     390    mapNames.sort();
     391    mapNames.unshift("Any");
     392   
     393    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     394    mapNameFilter.list = mapNames;
     395    mapNameFilter.list_data = mapNames;
     396   
     397    if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNames.length)
     398        mapNameFilter.selected = 0;
     399}
     400
     401function initPopCapFilter()
     402{
     403    var popCaps = [];
     404    for (var replay of g_Replays)
     405    {
     406        var popCap = getReplayPopCap(replay);
     407        if (popCaps.indexOf(popCap) == -1)
     408            popCaps.push(popCap);
     409    }
     410    popCaps.sort();
     411   
     412    popCaps.unshift("Any");
     413    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     414    populationFilter.list = popCaps;
     415    populationFilter.list_data = popCaps;
     416   
     417    if (populationFilter.selected == -1 || populationFilter.selected >= popCaps.length)
     418        populationFilter.selected = 0;
     419}
     420
     421function initDurationFilter()
     422{
     423    var data = [-1];
     424    var labels = ["Any"];
     425
     426    for (var i in g_DurationFilterMin)
     427    {
     428        if (i == 0)
     429            continue;
     430        else if (i == 1)
     431            var label = sprintf(translateWithContext("duration filter", "< %(min)s min"), { "min": g_DurationFilterMax[i] });
     432        else if (i == g_DurationFilterMin.length -1)
     433            var label = sprintf(translateWithContext("duration filter", "> %(min)s min"), { "min": g_DurationFilterMin[i] });
     434        else
     435            var label = sprintf(translateWithContext("duration filter", "%(min1)s - %(min2)s min"), { "min1": g_DurationFilterMin[i], "min2": g_DurationFilterMax[i] });
     436       
     437        data.push(i);
     438        labels.push(label);
     439    }
     440   
     441    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     442    durationFilter.list = labels;
     443    durationFilter.list_data = data;
     444   
     445    if (durationFilter.selected == -1 || durationFilter.selected >= data.length)
     446        durationFilter.selected = 0;
     447}
     448
     449function applyFilters()
     450{
     451    // Update the list of replays
     452    updateReplayList();
     453
     454    // Update info box about the replay currently selected
     455    updateReplaySelection();
     456}
     457
     458// Returns true if the replay should not be listed.
     459function filterReplay(replay)
     460{
     461    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     462    var playersFilter = Engine.GetGUIObjectByName("playersFilter");
     463    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     464    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     465    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     466    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     467    var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter");
     468
     469    // Check for compability first (most likely to filter)
     470    if (compabilityFilter.checked && !isReplayCompatible(replay))
     471        return true;
     472   
     473    // Filter date/time (select a month)
     474    if (dateTimeFilter.selected > 0)
     475    {
     476        let selectedMonth = dateTimeFilter.list_data[dateTimeFilter.selected];
     477        if (getDateTimeFilterVal(replay) != selectedMonth) 
     478            return true;
     479    }
     480
     481    // Filter selected players
     482    let playerText = playersFilter.caption;
     483    if (playerText.length)
     484    {
     485        // Player and botnames can contain spaces
     486        // We just check if all words of all players are somewhere in the playerlist
     487        playerText = playerText.toLowerCase().split(" ");
     488        let replayPlayers = replay.playerData.map(function(player){ return player ? player.Name.toLowerCase() : "" }).join(" ");
     489        for (let word of playerText)
     490        {
     491            if (replayPlayers.indexOf(word) == -1)
     492                return true;
     493        }
     494    }
     495
     496    // Filter map name
     497    if (mapNameFilter.selected > 0)
     498    {
     499        if (getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
     500            return true;
     501    }
     502
     503    // Filter map size
     504    if (mapSizeFilter.selected > 0)
     505    {
     506        let selectedMapSize = mapSizeFilter.list_data[mapSizeFilter.selected];
     507        if (replay.settings.Size != selectedMapSize)
     508            return true;
     509    }
     510
     511    // Filter population capacity
     512    if (populationFilter.selected > 0 &&
     513            getReplayPopCap(replay) != populationFilter.list_data[populationFilter.selected])
     514    {
     515        return true;
     516    }
     517
     518    // Filter game duration
     519    if (durationFilter.selected > 0)
     520    {
     521        let minutes = replay.duration / 60;
     522        let min = g_DurationFilterMin[durationFilter.selected];
     523        let max = g_DurationFilterMax[durationFilter.selected];
     524       
     525        if (minutes < min || (max > -1 && minutes > max))
     526            return true;
     527    }
     528
     529    return false;
     530}
     531
     532function updateReplaySelection()
     533{
     534    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     535    var replaySelected = selected > -1;
     536   
     537    Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
     538    Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
     539    Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
     540    Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
     541    Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected;
     542   
     543    if (!replaySelected)
     544        return;
     545   
     546    var replay = g_ReplaysFiltered[selected];
     547
     548    // Load map data
     549    if (replay.settings.mapType == "random" && replay.attribs.map == "random")
     550        var mapData = {"settings": {"Description": translate("A randomly selected map.")}};
     551    else if (replay.settings.mapType == "random" && Engine.FileExists(replay.attribs.map + ".json"))
     552        var mapData = Engine.ReadJSONFile(replay.attribs.map + ".json");
     553    else if (Engine.FileExists(replay.attribs.map + ".xml"))
     554        var mapData = Engine.LoadMapSettings(replay.attribs.map + ".xml");
     555    else
     556        // Warn the player if we can't find the map.
     557        warn(sprintf("Map '%(mapName)s' not found locally.", { mapName: replay.attribs.map }));
     558   
     559    // Load map description
     560    if (mapData && mapData.settings.Description)
     561        var mapDescription = translate(mapData.settings.Description);
     562    else
     563        var mapDescription = translate("Sorry, no description available.");
     564
     565    // Load map preview image
     566    if (mapData && mapData.settings.Preview)
     567        var mapPreview = mapData.settings.Preview;
     568    else
     569        var mapPreview = "nopreview.png";
     570
     571    // Update GUI, compare with gamesetup.js for translations
     572    Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.settings.Name);
     573    Engine.GetGUIObjectByName("sgMapSize").caption = getReplayMapSizeText(replay.settings.Size);
     574    Engine.GetGUIObjectByName("sgMapType").caption = translateWithContext("map",capitalizeFirstLetter(replay.settings.mapType));
     575    Engine.GetGUIObjectByName("sgVictory").caption = translate(capitalizeFirstLetter(replay.settings.GameType));
     576    Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.playerData.length;
     577    Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay);
     578    Engine.GetGUIObjectByName("sgMapDescription").caption = mapDescription;
     579    Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapPreview;
     580}
     581
     582function startReplay()
     583{
     584    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     585    if (selected == -1)
     586        return;
     587   
     588    var replay = g_ReplaysFiltered[selected];
     589    if (isReplayCompatible(replay))
     590        reallyStartVisualReplay(replay.directory);
     591    else
     592        displayReplayCompatibilityError(replay);
     593}
     594
     595function reallyStartVisualReplay(replayDirectory)
     596{
     597    Engine.StartVisualReplay(replayDirectory);
     598    Engine.SwitchGuiPage("page_loading.xml", {
     599        "attribs": Engine.GetReplayAttributes(replayDirectory),
     600        "isNetworked" : false,
     601        "playerAssignments": {},
     602        "savedGUIData": "",
     603        "isReplay" : true
     604    });
     605}
     606
     607function displayReplayCompatibilityError(replay)
     608{
     609    if (isReplayCompatible_EngineVersion(replay))
     610    {
     611        if (replay.attribs.mods)
     612            var gameMods = replay.attribs.mods;
     613        else
     614            var gameMods = [];
     615
     616        var errMsg = translate("The replay is compatible with your version of the game, but you don't have the same mods as in the replay.") + "\n";
     617        errMsg += sprintf(translate("Mods enabled in the replay: %(mods)s"), { "mods": gameMods.join(",") }) + "\n";
     618        errMsg += sprintf(translate("Currently enabled mods: %(mods)s"), { "mods": g_EngineInfo.mods.join(",") });
     619    }
     620    else
     621    {
     622        var errMsg = translate("This replay is not compatible with your version of the game!");
     623    }
     624
     625    var btCaptions = [translate("Ok")];
     626    var btCode = [null];
     627    messageBox(500, 200, errMsg, translate("REPLAY INCOMPATIBLE"), 0, btCaptions, btCode);
     628}
     629
     630function showReplaySummary()
     631{
     632    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     633    if (selected == -1)
     634        return;
     635
     636    var replay = g_ReplaysFiltered[selected];
     637    var summary = Engine.GetReplayMetadata(replay.directory);
     638   
     639    if (Object.keys(summary) == 0)
     640    {
     641        var btCaptions = [translate("Ok")];
     642        var btCode = [null];
     643        messageBox(500, 200, translateWithContext("replay", "No summary data found!"), translate("ERROR"), 0, btCaptions, btCode);
     644        return;
     645    }
     646
     647    summary.isReplay = true;
     648    summary.gameResult = translateWithContext("replay", "Scores at the end of the game.");
     649
     650    Engine.SwitchGuiPage("page_summary.xml", summary);
     651}
     652
     653function deleteReplay()
     654{
     655    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     656    if (selected == -1)
     657        return;
     658
     659    // Ask for confirmation
     660    var replay = g_ReplaysFiltered[selected];
     661    var btCaptions = [translate("Yes"), translate("No")];
     662    var btCode = [function(){ reallyDeleteReplay(replay.directory); }, null];
     663    messageBox(500, 200, translate("Are you sure to delete this replay permanently?") + "\n" +  replay.file, translate("DELETE"), 0, btCaptions, btCode);
     664}
     665
     666function deleteReplayWithoutConfirmation()
     667{
     668    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     669    if (selected > -1)
     670        reallyDeleteReplay(g_ReplaysFiltered[selected].directory);
     671}
     672
     673function reallyDeleteReplay(replayDirectory)
     674{
     675    if (!Engine.DeleteReplay(replayDirectory))
     676        error(sprintf("Could not delete replay '%(id)s'", { id: replayDirectory }));
     677
     678    // Refresh replay list
     679    init();
     680}
  • binaries/data/mods/public/gui/replaymenu/replaymenu.xml

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

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

     
    55var g_IsController;
    66// Match ID for tracking
    77var g_MatchID;
    88// Is this user an observer?
    99var g_IsObserver = false;
     10var g_IsReplay = false;
    1011
    1112// Cache the basic player data (name, civ, color)
    1213var g_Players = [];
    1314// Cache the useful civ data
    1415var g_CivData = {};
     
    149150{
    150151    if (initData)
    151152    {
    152153        g_IsNetworked = initData.isNetworked; // Set network mode
    153154        g_IsController = initData.isController; // Set controller mode
     155        g_IsReplay = initData.isReplay; // Set replay mode
    154156        g_PlayerAssignments = initData.playerAssignments;
    155157        g_MatchID = initData.attribs.matchID;
    156158
    157159        // Cache the player data
    158160        // (This may be updated at runtime by handleNetMessage)
     
    309311 * Leave the game
    310312 * @param willRejoin If player is going to be rejoining a networked game.
    311313 */
    312314function leaveGame(willRejoin)
    313315{
    314     var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
    315     var mapSettings = Engine.GetMapSettings();
     316    var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState")
     317   
    316318    var gameResult;
    317 
    318319    if (g_IsObserver)
    319320    {
    320321        // Observers don't win/lose.
    321322        gameResult = translate("You have left the game.");
    322323        global.music.setState(global.music.states.VICTORY);
     
    342343            }
    343344        }
    344345    }
    345346
    346347    stopAmbient();
    347     Engine.EndGame();
    348348
     349    // Save summary screen data before ending the game
     350    let summary = {
     351            "timeElapsed" : extendedSimState.timeElapsed,
     352            "playerStates": extendedSimState.players,
     353            "players": g_Players,
     354            "mapSettings": Engine.GetMapSettings()
     355    };
     356    if (!g_IsReplay)
     357        Engine.SaveReplayMetadata(JSON.stringify(summary));
     358    summary.isReplay = g_IsReplay;
     359    summary.gameResult = gameResult;
     360   
     361    Engine.EndGame();
    349362    if (g_IsController && Engine.HasXmppClient())
    350363        Engine.SendUnregisterGame();
    351364
    352     Engine.SwitchGuiPage("page_summary.xml", {
    353                             "gameResult"  : gameResult,
    354                             "timeElapsed" : extendedSimState.timeElapsed,
    355                             "playerStates": extendedSimState.players,
    356                             "players": g_Players,
    357                             "mapSettings": mapSettings
    358                          });
     365    Engine.SwitchGuiPage("page_summary.xml", summary);
    359366}
    360367
    361368// Return some data that we'll use when hotloading this file after changes
    362369function getHotloadData()
    363370{
  • binaries/data/mods/public/gui/summary/summary.js

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

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

     
    3131            "Speed": 1.5
    3232        },
    3333        {
    3434            "Name": "Insane (2x)",
    3535            "Speed": 2.0
     36        },
     37        {
     38            "Name": "Fast forward (5x)",
     39            "Speed": 5.0
     40        },
     41        {
     42            "Name": "Faster forward (10x)",
     43            "Speed": 10.0
     44        },
     45        {
     46            "Name": "Very fast forward (20x)",
     47            "Speed": 20.0
    3648        }
    3749    ]
    3850}
  • source/gui/scripting/ScriptFunctions.cpp

     
    4848#include "ps/Globals.h" // g_frequencyFilter
    4949#include "ps/Hotkey.h"
    5050#include "ps/ProfileViewer.h"
    5151#include "ps/Pyrogenesis.h"
    5252#include "ps/SavedGame.h"
     53#include "ps/VisualReplay.h"
    5354#include "ps/UserReport.h"
    5455#include "ps/World.h"
    5556#include "ps/scripting/JSInterface_ConfigDB.h"
    5657#include "ps/scripting/JSInterface_Console.h"
    5758#include "ps/scripting/JSInterface_Mod.h"
     
    281282    shared_ptr<ScriptInterface::StructuredClone> GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata);
    282283    if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0)
    283284        LOGERROR("Failed to save game");
    284285}
    285286
     287void StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring directoryName)
     288{
     289    ENSURE(!g_NetServer);
     290    ENSURE(!g_NetClient);
     291    ENSURE(!g_Game);
     292
     293    std::wstring replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / L"commands.txt").string();
     294    std::string replayFile( replayFilePath.begin(), replayFilePath.end() );
     295
     296    if (FileExists(OsPath(replayFile)))
     297    {
     298        g_Game = new CGame(false, false);
     299        g_Game->StartReplay(replayFile);
     300    }
     301}
     302
     303JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     304{
     305    return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName);
     306}
     307
     308void SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring data)
     309{
     310    VisualReplay::SaveReplayMetadata(data);
     311}
     312
     313JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     314{
     315    return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName);
     316}
     317
    286318void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
    287319{
    288320    ENSURE(g_NetServer);
    289321    //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere
    290322    // (with no obvious reason).
     
    415447bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name)
    416448{
    417449    return SavedGames::DeleteSavedGame(name);
    418450}
    419451
     452JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
     453{
     454    return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
     455}
     456
     457bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring replayFile)
     458{
     459    return VisualReplay::DeleteReplay(replayFile);
     460}
     461
    420462void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::string url)
    421463{
    422464    sys_open_url(url);
    423465}
    424466
     
    9741016    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGame>("SaveGame");
    9751017    scriptInterface.RegisterFunction<void, std::wstring, std::wstring, JS::HandleValue, &SaveGamePrefix>("SaveGamePrefix");
    9761018    scriptInterface.RegisterFunction<void, &QuickSave>("QuickSave");
    9771019    scriptInterface.RegisterFunction<void, &QuickLoad>("QuickLoad");
    9781020
     1021    // Visual replay
     1022    scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays");
     1023    scriptInterface.RegisterFunction<bool, std::wstring, &DeleteReplay>("DeleteReplay");
     1024    scriptInterface.RegisterFunction<void, std::wstring, &StartVisualReplay>("StartVisualReplay");
     1025    scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetReplayAttributes>("GetReplayAttributes");
     1026    scriptInterface.RegisterFunction<JS::Value, std::wstring, &GetReplayMetadata>("GetReplayMetadata");
     1027    scriptInterface.RegisterFunction<void, std::wstring, &SaveReplayMetadata>("SaveReplayMetadata");
     1028
    9791029    // Misc functions
    9801030    scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
    9811031    scriptInterface.RegisterFunction<int, &GetPlayerID>("GetPlayerID");
    9821032    scriptInterface.RegisterFunction<void, int, &SetPlayerID>("SetPlayerID");
    9831033    scriptInterface.RegisterFunction<void, std::string, &OpenURL>("OpenURL");
  • source/lobby/XmppClient.cpp

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

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

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

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

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

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

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

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

     
    1 /* Copyright (C) 2014 Wildfire Games.
     1/* Copyright (C) 2015 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
     
    2424#include "lib/allocators/shared_ptr.h"
    2525#include "lib/file/archive/archive_zip.h"
    2626#include "i18n/L10n.h"
    2727#include "lib/utf8.h"
    2828#include "ps/CLogger.h"
     29#include "ps/EngineVersion.h"
    2930#include "ps/Filesystem.h"
    3031#include "ps/Game.h"
    3132#include "ps/Mod.h"
    3233#include "scriptinterface/ScriptInterface.h"
    3334#include "simulation2/Simulation2.h"
     
    8485    JS::RootedValue metadata(cx);
    8586    JS::RootedValue initAttributes(cx, simulation.GetInitAttributes());
    8687    simulation.GetScriptInterface().Eval("({})", &metadata);
    8788    simulation.GetScriptInterface().SetProperty(metadata, "version_major", SAVED_GAME_VERSION_MAJOR);
    8889    simulation.GetScriptInterface().SetProperty(metadata, "version_minor", SAVED_GAME_VERSION_MINOR);
     90    simulation.GetScriptInterface().SetProperty(metadata, "engine_version", utf8_from_wstring(engine_version));
     91
    8992    simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded);
    9093    simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now);
    9194    simulation.GetScriptInterface().SetProperty(metadata, "player", playerID);
    9295    simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes);
    9396
     
    296299    JSContext* cx = scriptInterface.GetContext();
    297300    JSAutoRequest rq(cx);
    298301   
    299302    JS::RootedValue metainfo(cx);
    300303    scriptInterface.Eval("({})", &metainfo);
    301     scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR);
    302     scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR);
    303     scriptInterface.SetProperty(metainfo, "mods"         , g_modsLoaded);
     304    scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR);
     305    scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR);
     306    scriptInterface.SetProperty(metainfo, "engine_version", utf8_from_wstring(engine_version));
     307
     308    scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded);
    304309    return metainfo;
    305310}
    306311
  • source/ps/VisualReplay.cpp

     
     1/* Copyright (C) 2015 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#include "precompiled.h"
     19
     20#include "VisualReplay.h"
     21#include "graphics/GameView.h"
     22#include "gui/GUIManager.h"
     23#include "lib/allocators/shared_ptr.h"
     24#include "lib/utf8.h"
     25#include "ps/CLogger.h"
     26#include "ps/EngineVersion.h"
     27#include "ps/Filesystem.h"
     28#include "ps/Game.h"
     29#include "ps/Replay.h"
     30#include "scriptinterface/ScriptInterface.h"
     31
     32OsPath VisualReplay::GetDirectoryName()
     33{
     34    return OsPath(psLogDir() / L"sim_log");
     35}
     36
     37JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
     38{
     39    // TODO: enhancement: make a cache file, so that only one file needs to be loaded
     40    TIMER(L"GetReplays");
     41    JSContext* cx = scriptInterface.GetContext();
     42    JSAutoRequest rq(cx);
     43   
     44    u32 i = 0;
     45    DirectoryNames directories;
     46    JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
     47    GetDirectoryEntries(GetDirectoryName(), NULL, &directories);
     48    for (auto& directory : directories)
     49    {
     50        JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
     51        if (!replayData.isNull())
     52            JS_SetElement(cx, replays, i++, replayData);
     53    }
     54    return JS::ObjectValue(*replays);
     55}
     56
     57JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath directory)
     58{
     59    OsPath replayFile(GetDirectoryName() / directory / L"commands.txt");
     60
     61    if (!FileExists(replayFile))
     62        return JSVAL_NULL;
     63
     64    // Get fileinfo (size, timestamp)
     65    CFileInfo fileInfo;
     66    GetFileInfo(replayFile, &fileInfo);
     67    u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
     68    u64 fileSize = (u64)fileInfo.Size();
     69
     70    if (fileSize == 0)
     71        return JSVAL_NULL;
     72
     73    // Open file
     74    std::wstring filenameW = replayFile.string();
     75    std::string filename( filenameW.begin(), filenameW.end() );
     76    std::ifstream* replayStream = new std::ifstream(filename.c_str());
     77
     78    // File must begin with "start"
     79    std::string type;
     80    ENSURE((*replayStream >> type).good() && type == "start");
     81
     82    // Parse header
     83    std::string header;
     84    std::getline(*replayStream, header);
     85    JSContext* cx = scriptInterface.GetContext();
     86    JSAutoRequest rq(cx);
     87    JS::RootedValue attribs(cx);
     88    if (!scriptInterface.ParseJSON(header, &attribs))
     89        return JSVAL_NULL;
     90
     91    // Parse commands & turns
     92    u32 currentTurn = 0;
     93    u64 duration = 0;
     94    while ((*replayStream >> type).good())
     95    {
     96        if (type == "turn")
     97        {
     98            // Store turn & turn length
     99            u32 turn = 0;
     100            u32 turnLength = 0;
     101            *replayStream >> turn >> turnLength;
     102            if (turn != currentTurn) // Happens for replays of rejoined clients
     103                return JSVAL_NULL;
     104
     105            // Compute game duration
     106            duration += turnLength;
     107        }
     108        else if (type == "cmd")
     109        {
     110            // Read command
     111            std::string command;
     112            std::getline(*replayStream, command);
     113        }
     114        else if (type == "hash" || type == "hash-quick")
     115        {
     116            // Skip hash values
     117            std::string replayHash;
     118            *replayStream >> replayHash;
     119        }
     120        else if (type == "end")
     121        {
     122            currentTurn++;
     123        }
     124        else
     125        {
     126            LOGERROR("Unrecognized replay data '%s' in file %s", type, filename.c_str());
     127        }
     128    }
     129    replayStream->close();
     130
     131    duration = duration / 1000;
     132    if (duration < 5) // Don't list too short replays
     133        return JSVAL_NULL;
     134
     135    JS::RootedValue replayData(cx);
     136    scriptInterface.Eval("({})", &replayData);
     137    scriptInterface.SetProperty(replayData, "file", replayFile);
     138    scriptInterface.SetProperty(replayData, "directory", directory);
     139    scriptInterface.SetProperty(replayData, "filemod_timestamp", fileTime);
     140    scriptInterface.SetProperty(replayData, "attribs", attribs);
     141    scriptInterface.SetProperty(replayData, "duration", duration);
     142    return replayData;
     143}
     144
     145bool VisualReplay::DeleteReplay(const std::wstring replayDirectory)
     146{
     147    if (replayDirectory == L"")
     148        return false;
     149
     150    const OsPath directory = OsPath(GetDirectoryName() / replayDirectory);
     151    return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     152}
     153
     154
     155JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     156{
     157    std::wstring replayFilePath = OsPath(VisualReplay::GetDirectoryName() / directoryName / "commands.txt").string();
     158    std::string replayFile( replayFilePath.begin(), replayFilePath.end() );
     159
     160    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     161    JSAutoRequest rq(cx);
     162    JS::RootedValue attribs(cx);
     163    pCxPrivate->pScriptInterface->Eval("({})", &attribs);
     164
     165    if (!FileExists(OsPath(replayFile)))
     166        return attribs;
     167
     168    std::ifstream* replayStream = new std::ifstream(replayFile.c_str());
     169    std::string type, line;
     170    ENSURE((*replayStream >> type).good() && type == "start");
     171
     172    std::getline(*replayStream, line);
     173    pCxPrivate->pScriptInterface->ParseJSON(line, &attribs);
     174    replayStream->close();
     175    return attribs;
     176}
     177
     178void VisualReplay::SaveReplayMetadata(std::wstring data)
     179{
     180    // Returns directory of the currently active replay
     181    // TODO: enhancement: use JS::HandleValue similar to SaveGame
     182    if (!g_Game)
     183        return;
     184
     185    OsPath directory = g_Game->GetReplayLogger().GetReplayDirectory();
     186    CreateDirectories(directory, 0700);
     187
     188    OsPath filepath = OsPath(directory / L"metadata.json");
     189
     190    std::wstring filenameW = filepath.string();
     191    std::string filename( filenameW.begin(), filenameW.end() );
     192
     193    std::ofstream stream (filename.c_str(), std::ofstream::out | std::ofstream::trunc);
     194    stream << utf8_from_wstring(data);
     195    stream.close();
     196}
     197
     198JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName)
     199{
     200    std::wstring filePathW = OsPath(VisualReplay::GetDirectoryName() / directoryName / "metadata.json").string();
     201    std::string filePath( filePathW.begin(), filePathW.end() );
     202
     203    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     204    JSAutoRequest rq(cx);
     205    JS::RootedValue metadata(cx);
     206
     207    if (FileExists(OsPath(filePathW)))
     208    {
     209        std::ifstream* stream = new std::ifstream(filePath.c_str());
     210        ENSURE(stream->good());
     211
     212        std::string type, line;
     213        std::getline(*stream, line);
     214        pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
     215        stream->close();
     216    }
     217    else
     218    {
     219        pCxPrivate->pScriptInterface->Eval("({})", &metadata);
     220    }
     221
     222    return metadata;
     223}
  • 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 * Works similarly to CGame::LoadReplayData().
     62 */
     63JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath replayFile);
     64
     65/**
     66 * Permanently deletes the visual replay (including the parent directory)
     67 *
     68 * @param replayFile path to commands.txt, whose parent directory will be deleted
     69 * @return true if deletion was successful, or false on error
     70 */
     71bool DeleteReplay(const std::wstring replayFile);
     72
     73/**
     74 * Returns the parsed header of the replay file (commands.txt).
     75 */
     76JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName);
     77
     78/**
     79 * Returns the metadata of a replay.
     80 */
     81JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, std::wstring directoryName);
     82
     83/**
     84 * Saves the metadata from the session to metadata.json
     85 */
     86void SaveReplayMetadata(std::wstring data);
     87
     88}
     89
     90#endif