Ticket #3258: t3258_visual_replay_menu_v11.patch

File t3258_visual_replay_menu_v11.patch, 63.0 KB (added by elexis, 9 years ago)

Compiles on both windows and linux and doesn't freeze.

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

    function prepareForDropdown(settingValue  
    254254        if (settingValues[index].Default)
    255255            settings.Default = +index;
    256256    }
    257257    return settings;
    258258}
     259
     260/**
     261 * Returns title or placeholder.
     262 *
     263 * @param aiName {string} - for example "petra"
     264 */
     265function translateAIName(aiName)
     266{
     267    var description = g_Settings.AIDescriptions.find(ai => ai.id == aiName);
     268    return description ? translate(description.data.name) : translate("Unknown");
     269}
     270
     271/**
     272 * Returns title or placeholder.
     273 *
     274 * @param index {Number} - index of AIDifficulties
     275 */
     276function translateAIDifficulty(index)
     277{
     278    var difficulty = g_Settings.AIDifficulties[index];
     279    return difficulty ? difficulty.Title : translate("Unknown");
     280}
     281
     282/**
     283 * Returns title or placeholder.
     284 *
     285 * @param mapType {string} - for example "skirmish"
     286 */
     287function translateMapType(mapType)
     288{
     289    var type = g_Settings.MapTypes.find(t => t.Name == mapType);
     290    return type ? type.Title : translate("Unknown");
     291}
     292
     293/**
     294 * Returns title or placeholder.
     295 *
     296 * @param population {Number} - for example 300
     297 */
     298function translatePopulationCapacity(population)
     299{
     300    var popCap = g_Settings.PopulationCapacities.find(p => p.Population == population);
     301    return popCap ? popCap.Title : translate("Unknown");
     302}
     303
     304/**
     305 * Returns title or placeholder.
     306 *
     307 * @param gameType {string} - for example "conquest"
     308 */
     309function translateVictoryCondition(gameType)
     310{
     311    var vc = g_Settings.VictoryConditions.find(vc => vc.Name == gameType);
     312    return vc ? vc.Title : translate("Unknown");
     313}
  • 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/replay_menu.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">Replays</translatableAttribute>
     356                    <translatableAttribute id="tooltip">Playback previous games.</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/replay_actions.js

     
     1/**
     2 * Starts the selected visual replay, or shows an error message in case of incompatibility.
     3 */
     4function startReplay()
     5{
     6    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     7    if (selected == -1)
     8        return;
     9
     10    var replay = g_ReplaysFiltered[selected];
     11    if (isReplayCompatible(replay))
     12        reallyStartVisualReplay(replay.directory);
     13    else
     14        displayReplayCompatibilityError(replay);
     15}
     16
     17/**
     18 * Attempts the visual replay, regardless of the compatibility.
     19 *
     20 * @param replayDirectory {string}
     21 */
     22function reallyStartVisualReplay(replayDirectory)
     23{
     24    // TODO: enhancement: restore filter settings and selected replay when returning from the summary screen.
     25    Engine.StartVisualReplay(replayDirectory);
     26    Engine.SwitchGuiPage("page_loading.xml", {
     27        "attribs": Engine.GetReplayAttributes(replayDirectory),
     28        "isNetworked" : false,
     29        "playerAssignments": {},
     30        "savedGUIData": "",
     31        "isReplay" : true
     32    });
     33}
     34
     35/**
     36 * Shows an error message stating why the replay is not compatible.
     37 *
     38 * @param replay {Object}
     39 */
     40function displayReplayCompatibilityError(replay)
     41{
     42    var errMsg;
     43    if (replayHasSameEngineVersion(replay))
     44    {
     45        let gameMods = replay.attribs.mods ? replay.attribs.mods : [];
     46        errMsg = translate("You don't have the same mods active as the replay.") + "\n";
     47        errMsg += sprintf(translate("Required: %(mods)s"), { "mods": gameMods.join(", ") }) + "\n";
     48        errMsg += sprintf(translate("Active: %(mods)s"), { "mods": g_EngineInfo.mods.join(", ") });
     49    }
     50    else
     51        errMsg = translate("This replay is not compatible with your version of the game!");
     52
     53    messageBox(500, 200, errMsg, translate("REPLAY INCOMPATIBLE"), 0, [translate("Ok")], [null]);
     54}
     55
     56/**
     57 * Opens the summary screen of the given replay, if its data was found in that directory.
     58 */
     59function showReplaySummary()
     60{
     61    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     62    if (selected == -1)
     63        return;
     64
     65    // Load summary screen data from the selected replay directory
     66    var summary = Engine.GetReplayMetadata(g_ReplaysFiltered[selected].directory);
     67
     68    if (!summary)
     69    {
     70        messageBox(500, 200, translate("No summary data available."), translate("ERROR"), 0, [translate("Ok")], [null]);
     71        return;
     72    }
     73
     74    // Open summary screen
     75    summary.isReplay = true;
     76    summary.gameResult = translate("Scores at the end of the game.");
     77    Engine.SwitchGuiPage("page_summary.xml", summary);
     78}
     79
     80/**
     81 * Callback.
     82 */
     83function deleteReplayButtonPressed()
     84{
     85    if (!Engine.GetGUIObjectByName("deleteReplayButton").enabled)
     86        return;
     87
     88    if (Engine.HotkeyIsPressed("session.savedgames.noConfirmation"))
     89        deleteReplayWithoutConfirmation();
     90    else
     91        deleteReplay();
     92}
     93/**
     94 * Shows a confirmation dialog and deletes the selected replay from the disk in case.
     95 */
     96function deleteReplay()
     97{
     98    // Get selected replay
     99    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     100    if (selected == -1)
     101        return;
     102
     103    var replay = g_ReplaysFiltered[selected];
     104
     105    // Show confirmation message
     106    var btCaptions = [translate("Yes"), translate("No")];
     107    var btCode = [function() { reallyDeleteReplay(replay.directory); }, null];
     108
     109    var title = translate("DELETE");
     110    var question = translate("Are you sure to delete this replay permanently?") + "\n" + replay.file;
     111
     112    messageBox(500, 200, question, title, 0, btCaptions, btCode);
     113}
     114
     115/**
     116 * Attempts to delete the selected replay from the disk.
     117 */
     118function deleteReplayWithoutConfirmation()
     119{
     120    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     121    if (selected > -1)
     122        reallyDeleteReplay(g_ReplaysFiltered[selected].directory);
     123}
     124
     125/**
     126 * Attempts to delete the given replay directory from the disk.
     127 *
     128 * @param replayDirectory {string}
     129 */
     130function reallyDeleteReplay(replayDirectory)
     131{
     132    if (!Engine.DeleteReplay(replayDirectory))
     133        error(sprintf("Could not delete replay '%(id)s'", { "id": replayDirectory }));
     134
     135    // Refresh replay list
     136    init();
     137}
  • binaries/data/mods/public/gui/replaymenu/replay_filters.js

     
     1/**
     2 * Allow to filter replays by duration in 15min / 30min intervals.
     3 */
     4const g_DurationFilterIntervals = [
     5    { "min":  -1, "max":  -1 },
     6    { "min":  -1, "max":  15 },
     7    { "min":  15, "max":  30 },
     8    { "min":  30, "max":  45 },
     9    { "min":  45, "max":  60 },
     10    { "min":  60, "max":  90 },
     11    { "min":  90, "max": 120 },
     12    { "min": 120, "max":  -1 }
     13];
     14
     15/**
     16 * Allow to filter by population capacity.
     17 */
     18const g_PopulationCapacities = prepareForDropdown(g_Settings ? g_Settings.PopulationCapacities : undefined);
     19
     20/**
     21 * Reloads the selectable values in the filters. The filters depend on g_Settings and g_Replays
     22 * (including its derivatives g_MapSizes, g_MapNames).
     23 */
     24function initFilters()
     25{
     26    initDateFilter();
     27    initMapNameFilter();
     28    initMapSizeFilter();
     29    initPopCapFilter();
     30    initDurationFilter();
     31}
     32
     33/**
     34 * Allow to filter by month. Uses g_Replays.
     35 */
     36function initDateFilter()
     37{
     38    var months = g_Replays.map(replay => getReplayMonth(replay));
     39    months = months.filter((month, index) => months.indexOf(month) == index).sort();
     40    months.unshift(translateWithContext("datetime", "Any"));
     41
     42    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     43    dateTimeFilter.list = months;
     44    dateTimeFilter.list_data = months;
     45
     46    if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length)
     47        dateTimeFilter.selected = 0;
     48}
     49
     50/**
     51 * Allow to filter by mapsize. Uses g_MapSizes.
     52 */
     53function initMapSizeFilter()
     54{
     55    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     56    mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_mapSizes.shortNames);
     57    mapSizeFilter.list_data = [-1].concat(g_mapSizes.tiles);
     58
     59    if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length)
     60        mapSizeFilter.selected = 0;
     61}
     62
     63/**
     64 * Allow to filter by mapname. Uses g_MapNames.
     65 */
     66function initMapNameFilter()
     67{
     68    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     69    mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames);
     70    mapNameFilter.list_data = [""].concat(g_MapNames.map(mapName => translate(mapName)));
     71
     72    if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length)
     73        mapNameFilter.selected = 0;
     74}
     75
     76/**
     77 * Allow to filter by population capacity.
     78 */
     79function initPopCapFilter()
     80{
     81    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     82    populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title);
     83    populationFilter.list_data = [""].concat(g_PopulationCapacities.Population);
     84
     85    if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length)
     86        populationFilter.selected = 0;
     87}
     88
     89/**
     90 * Allow to filter by game duration. Uses g_DurationFilterIntervals.
     91 */
     92function initDurationFilter()
     93{
     94    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     95    durationFilter.list = g_DurationFilterIntervals.map((interval, index) => {
     96
     97        if (index == 0)
     98            return translateWithContext("duration", "Any");
     99
     100        if (index == 1)
     101            // Translation: Shorter duration than max minutes.
     102            return sprintf(translateWithContext("duration filter", "< %(max)s min"), interval);
     103
     104        if (index == g_DurationFilterIntervals.length - 1)
     105            // Translation: Longer duration than min minutes.
     106            return sprintf(translateWithContext("duration filter", "> %(min)s min"), interval);
     107
     108        // Translation: Duration between min and max minutes.
     109        return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval);
     110    });
     111    durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index);
     112
     113    if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length)
     114        durationFilter.selected = 0;
     115}
     116
     117/**
     118 * Initializes g_ReplaysFiltered with replays that are not filtered out and sort it.
     119 */
     120function filterReplays()
     121{
     122    const sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column;
     123    const sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
     124
     125    g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) =>
     126    {
     127        let cmpA, cmpB;
     128        switch (sortKey)
     129        {
     130        case 'name':
     131            cmpA = +a.timestamp;
     132            cmpB = +b.timestamp;
     133            break;
     134        case 'duration':
     135            cmpA = +a.duration;
     136            cmpB = +b.duration;
     137            break;
     138        case 'players':
     139            cmpA = +a.attribs.settings.PlayerData.length;
     140            cmpB = +b.attribs.settings.PlayerData.length;
     141            break;
     142        case 'mapName':
     143            cmpA = getReplayMapName(a);
     144            cmpB = getReplayMapName(b);
     145            break;
     146        case 'mapSize':
     147            cmpA = +a.attribs.settings.Size;
     148            cmpB = +b.attribs.settings.Size;
     149            break;
     150        case 'popCapacity':
     151            cmpA = +a.attribs.settings.PopulationCap;
     152            cmpB = +b.attribs.settings.PopulationCap;
     153            break;
     154        }
     155
     156        if (cmpA < cmpB)
     157            return -sortOrder;
     158        else if (cmpA > cmpB)
     159            return +sortOrder;
     160
     161        return 0;
     162    });
     163}
     164
     165/**
     166 * Decides whether the replay should be listed.
     167 *
     168 * @returns {bool} - true if replay should be visible
     169 */
     170function filterReplay(replay)
     171{
     172    // Check for compability first (most likely to filter)
     173    var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter");
     174    if (compabilityFilter.checked && !isReplayCompatible(replay))
     175        return false;
     176
     177    // Filter date/time (select a month)
     178    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     179    if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected])
     180        return false;
     181
     182    // Filter by playernames
     183    var playersFilter = Engine.GetGUIObjectByName("playersFilter");
     184    var keywords = playersFilter.caption.toLowerCase().split(" ");
     185    if (keywords.length)
     186    {
     187        // We just check if all typed words are somewhere in the playerlist of that replay
     188        let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase();
     189        if (!keywords.every(keyword => playerList.indexOf(keyword) != -1))
     190            return false;
     191    }
     192
     193    // Filter by map name
     194    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     195    if (mapNameFilter.selected > 0 && getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
     196        return false;
     197
     198    // Filter by map size
     199    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     200    if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected])
     201        return false;
     202
     203    // Filter by population capacity
     204    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     205    if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected])
     206        return false;
     207
     208    // Filter by game duration
     209    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     210    if (durationFilter.selected > 0)
     211    {
     212        let interval = g_DurationFilterIntervals[durationFilter.selected];
     213
     214        if ((interval.min > -1 && replay.duration < interval.min * 60) ||
     215            (interval.max > -1 && replay.duration > interval.max * 60))
     216            return false;
     217    }
     218
     219    return true;
     220}
  • binaries/data/mods/public/gui/replaymenu/replay_menu.js

     
     1const g_EngineInfo = Engine.GetEngineInfo();
     2const g_CivData = loadCivData();
     3const g_DefaultPlayerData = initPlayerDefaults();
     4const g_mapSizes = initMapSizes();
     5
     6/**
     7 * All replays found in the directory.
     8 */
     9var g_Replays = [];
     10
     11/**
     12 * List of replays after applying the display filter.
     13 */
     14var g_ReplaysFiltered = [];
     15
     16/**
     17 * Array of unique usernames of all replays. Used for autocompleting usernames.
     18 */
     19var g_Playernames = [];
     20
     21/**
     22 * Sorted list of unique maptitles. Used by mapfilter.
     23 */
     24var g_MapNames = [];
     25
     26/**
     27 * Directory name of the currently selected replay. Used to restore the selection after changing filters.
     28 */
     29var g_selectedReplayDirectory = "";
     30
     31/**
     32 * Initializes globals, loads replays and displays the list.
     33 */
     34function init()
     35{
     36    if (!g_Settings)
     37    {
     38        Engine.SwitchGuiPage("page_pregame.xml");
     39        return;
     40    }
     41
     42    // By default, sort replays by date in descending order
     43    Engine.GetGUIObjectByName("replaySelection").selected_column_order = -1;
     44
     45    loadReplays();
     46    displayReplayList();
     47}
     48
     49/**
     50 * Store the list of replays loaded in C++ in g_Replays.
     51 * Check timestamp and compatibility and extract g_Playernames, g_MapNames
     52 */
     53function loadReplays()
     54{
     55    g_Replays = Engine.GetReplays();
     56
     57    g_Playernames = [];
     58    for (let replay of g_Replays)
     59    {
     60        // Use time saved in file, otherwise file mod date
     61        replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp;
     62
     63        // Check replay for compability
     64        replay.isCompatible = isReplayCompatible(replay);
     65
     66        sanitizeGameAttributes(replay.attribs);
     67
     68        // Extract map names
     69        if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "")
     70            g_MapNames.push(replay.attribs.settings.Name);
     71
     72        // Extract playernames
     73        for (let playerData of replay.attribs.settings.PlayerData)
     74        {
     75            if (!playerData || playerData.AI)
     76                continue;
     77
     78            // Remove rating from nick
     79            let playername = playerData.Name;
     80            let ratingStart = playername.indexOf(" (");
     81            if (ratingStart != -1)
     82                playername = playername.substr(0, ratingStart);
     83
     84            if (g_Playernames.indexOf(playername) == -1)
     85                g_Playernames.push(playername);
     86        }
     87    }
     88    g_MapNames.sort();
     89
     90    // Reload filters (since they depend on g_Replays and its derivatives)
     91    initFilters();
     92}
     93
     94/**
     95 * We may encounter malformed replays.
     96 */
     97function sanitizeGameAttributes(attribs)
     98{
     99    if (!attribs.settings)
     100        attribs.settings = {};
     101
     102    if (!attribs.settings.Size)
     103        attribs.settings.Size = -1;
     104
     105    if (!attribs.settings.Name)
     106        attribs.settings.Name = "";
     107
     108    if (!attribs.settings.PlayerData)
     109        attribs.settings.PlayerData = [];
     110
     111    if (!attribs.settings.PopulationCap)
     112        attribs.settings.PopulationCap = 300;
     113
     114    if (!attribs.settings.mapType)
     115        attribs.settings.mapType = "skirmish";
     116
     117    if (!attribs.settings.GameType)
     118        attribs.settings.GameType = "conquest";
     119
     120    // Remove gaia
     121    if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null)
     122        attribs.settings.PlayerData.shift();
     123
     124    attribs.settings.PlayerData.forEach((pData, index) => {
     125        if (!pData.Name)
     126            pData.Name = "";
     127    });
     128}
     129
     130/**
     131 * Filter g_Replays, fill the GUI list with that data and show the description of the current replay.
     132 */
     133function displayReplayList()
     134{
     135    // Remember previously selected replay
     136    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     137    if (replaySelection.selected != -1)
     138        g_selectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory;
     139
     140    filterReplays();
     141
     142    // Create GUI list data
     143    var list = g_ReplaysFiltered.map(replay => {
     144        let works = replay.isCompatible;
     145        return {
     146            "directories": replay.directory,
     147            "months": greyout(getReplayDateTime(replay), works),
     148            "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works),
     149            "mapNames": greyout(getReplayMapName(replay), works),
     150            "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works),
     151            "durations": greyout(getReplayDuration(replay), works),
     152            "playerNames": greyout(getReplayPlayernames(replay), works)
     153        };
     154    });
     155
     156    // Extract arrays
     157    if (list.length)
     158        list = prepareForDropdown(list);
     159
     160    // Push to GUI
     161    replaySelection.selected = -1;
     162    replaySelection.list_name = list.months || [];
     163    replaySelection.list_players = list.playerNames || [];
     164    replaySelection.list_mapName = list.mapNames || [];
     165    replaySelection.list_mapSize = list.mapSizes || [];
     166    replaySelection.list_popCapacity = list.popCaps || [];
     167    replaySelection.list_duration = list.durations || [];
     168
     169    // Change these last, otherwise crash
     170    replaySelection.list = list.directories || [];
     171    replaySelection.list_data = list.directories || [];
     172
     173    // Restore selection
     174    replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_selectedReplayDirectory);
     175
     176    displayReplayDetails();
     177}
     178
     179/**
     180 * Shows preview image, description and player text in the right panel.
     181 */
     182function displayReplayDetails()
     183{
     184    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     185    var replaySelected = selected > -1;
     186
     187    Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
     188    Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
     189    Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
     190    Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
     191    Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected;
     192
     193    if (!replaySelected)
     194        return;
     195
     196    var replay = g_ReplaysFiltered[selected];
     197    var mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map);
     198
     199    // Update GUI
     200    Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name);
     201    Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size);
     202    Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType);
     203    Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType);
     204    Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.attribs.settings.PlayerData.length;
     205    Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay);
     206    Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
     207    Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapData.preview;
     208}
     209
     210/**
     211 * Adds grey font if replay is not compatible.
     212 */
     213function greyout(text, isCompatible)
     214{
     215    return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]';
     216}
     217
     218/**
     219 * Returns a human-readable version of the replay date.
     220 */
     221function getReplayDateTime(replay)
     222{
     223    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm"))
     224}
     225
     226/**
     227 * Returns a human-readable list of the playernames of that replay.
     228 *
     229 * @returns {string}
     230 */
     231function getReplayPlayernames(replay)
     232{
     233    // TODO: colorize playernames like in the lobby.
     234    return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", ");
     235}
     236
     237/**
     238 * Returns the name of the map of the given replay.
     239 *
     240 * @returns {string}
     241 */
     242function getReplayMapName(replay)
     243{
     244    return translate(replay.attribs.settings.Name);
     245}
     246
     247/**
     248 * Returns the month of the given replay in the format "yyyy-MM".
     249 *
     250 * @returns {string}
     251 */
     252function getReplayMonth(replay)
     253{
     254    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM"));
     255}
     256
     257/**
     258 * Returns a human-readable version of the time when the replay started.
     259 *
     260 * @returns {string}
     261 */
     262function getReplayDuration(replay)
     263{
     264    return timeToString(replay.duration * 1000);
     265}
     266
     267/**
     268 * True if we can start the given replay with the currently loaded mods.
     269 */
     270function isReplayCompatible(replay)
     271{
     272    return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo);
     273}
     274
     275/**
     276 * True if we can start the given replay with the currently loaded mods.
     277 */
     278function replayHasSameEngineVersion(replay)
     279{
     280    return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version;
     281}
     282
     283/**
     284 * Returns a description of the player assignments.
     285 * Including civs, teams, AI settings and player colors.
     286 *
     287 * If the spoiler-checkbox is checked, it also shows defeated players.
     288 *
     289 * @returns {string}
     290 */
     291function getReplayTeamText(replay)
     292{
     293    // Load replay metadata
     294    const metadata = Engine.GetReplayMetadata(replay.directory);
     295    const spoiler = Engine.GetGUIObjectByName("showSpoiler").checked;
     296
     297    var playerDescriptions = {};
     298    var playerIdx = 0;
     299    for (let playerData of replay.attribs.settings.PlayerData)
     300    {
     301        // Get player info
     302        ++playerIdx;
     303        let teamIdx = playerData.Team;
     304        let playerColor = playerData.Color ? playerData.Color : g_DefaultPlayerData[playerIdx].Color;
     305        let showDefeated = spoiler && metadata && metadata.playerStates && metadata.playerStates[playerIdx].state == "defeated";
     306        let isAI = playerData.AI;
     307
     308        // Create human-readable player description
     309        let playerDetails = {
     310            "playerName": '[color="' + rgbToGuiColor(playerColor) + '"]' + escapeText(playerData.Name) + "[/color]",
     311            "civ": translate(g_CivData[playerData.Civ].Name),
     312            "AIname": isAI ? translateAIName(playerData.AI) : "",
     313            "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : ""
     314        };
     315
     316        if (!isAI && !showDefeated)
     317            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s)"), playerDetails);
     318        else if (!isAI && showDefeated)
     319            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, defeated)"), playerDetails);
     320        else if (isAI && !showDefeated)
     321            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"), playerDetails);
     322        else
     323            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, defeated)"), playerDetails);
     324
     325        // Sort player descriptions by team
     326        if (!playerDescriptions[teamIdx])
     327            playerDescriptions[teamIdx] = [];
     328        playerDescriptions[teamIdx].push(playerDetails);
     329    }
     330
     331    var teams = Object.keys(playerDescriptions);
     332
     333    // If there are no teams, merge all playersDescriptions
     334    if (teams.length == 1)
     335        return playerDescriptions[teams[0]].join("\n") + "\n";
     336
     337    // If there are teams, merge "Team N:" + playerDescriptions
     338    return teams.map(team => {
     339        let teamCaption = (team == -1) ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 });
     340        return '[font="sans-bold-14"]' + teamCaption + "[/font]:\n" + playerDescriptions[team].join("\n");
     341    }).join("\n");
     342}
  • binaries/data/mods/public/gui/replaymenu/replay_menu.xml

     
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4
     5    <!-- Used to display game info. -->
     6    <script file="gui/common/functions_civinfo.js" />
     7    <script file="gui/common/functions_utility.js" />
     8    <script file="gui/common/settings.js" />
     9
     10    <!-- Used to display message boxes. -->
     11    <script file="gui/common/functions_global_object.js" />
     12
     13    <!-- Used for engine + mod version checks. -->
     14    <script file="gui/common/functions_utility_loadsave.js" />
     15
     16    <!-- Actual replay scripts after settings.js, as it initializes g_Settings. -->
     17    <script file="gui/replaymenu/replay_menu.js" />
     18    <script file="gui/replaymenu/replay_actions.js" />
     19    <script file="gui/replaymenu/replay_filters.js" />
     20
     21    <!-- Everything displayed in the replay menu. -->
     22    <object type="image" style="ModernWindow" size="0 0 100% 100%" name="replayWindow">
     23
     24        <!-- Title -->
     25        <object style="ModernLabelText" type="text" size="50%-128 4 50%+128 36">
     26            <translatableAttribute id="caption">Replay Games</translatableAttribute>
     27        </object>
     28
     29        <!-- Left Panel: Filters & Replay List -->
     30        <object name="leftPanel" size="3% 5% 100%-255 100%-80">
     31
     32            <!-- Filters -->
     33            <object name="filterPanel" size="0 0 100% 24">
     34
     35                <object name="dateTimeFilter" type="dropdown" style="ModernDropDown" size="5 0 12%-10 100%" font="sans-bold-13">
     36                    <action on="SelectionChange">displayReplayList();</action>
     37                </object>
     38
     39                <object name="playersFilter" type="input" style="ModernInput" size="12%-5 0 56%-10 100%" font="sans-bold-13">
     40                    <action on="Press">displayReplayList();</action>
     41                    <action on="Tab">autoCompleteNick("playersFilter", g_Playernames.map(name => ({ "name": name })));</action>
     42                </object>
     43
     44                <object name="mapNameFilter" type="dropdown" style="ModernDropDown" size="56%-5 0 70%-10 100%" font="sans-bold-13">
     45                    <action on="SelectionChange">displayReplayList();</action>
     46                </object>
     47
     48                <object name="mapSizeFilter" type="dropdown" style="ModernDropDown" size="70%-5 0 80%-10 100%" font="sans-bold-13">
     49                    <action on="SelectionChange">displayReplayList();</action>
     50                </object>
     51
     52                <object name="populationFilter" type="dropdown" style="ModernDropDown" size="80%-5 0 90%-10 100%" font="sans-bold-13">
     53                    <action on="SelectionChange">displayReplayList();</action>
     54                </object>
     55
     56                <object name="durationFilter" type="dropdown" style="ModernDropDown" size="90%-5 0 100%-10 100%" font="sans-bold-13">
     57                    <action on="SelectionChange">displayReplayList();</action>
     58                </object>
     59
     60            </object>
     61
     62            <!-- Replay List in that left panel -->
     63            <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">
     64
     65                <action on="SelectionChange">displayReplayDetails();</action>
     66                <action on="SelectionColumnChange">displayReplayList();</action>
     67
     68                <!-- Columns -->
     69                <!-- We have to call one "name" as the GUI expects one. -->
     70                <def id="name" color="172 172 212" width="12%">
     71                    <translatableAttribute id="heading" context="replay">Date / Time</translatableAttribute>
     72                </def>
     73
     74                <def id="players" color="192 192 192" width="44%">
     75                    <translatableAttribute id="heading" context="replay">Players</translatableAttribute>
     76                </def>
     77
     78                <def id="mapName" color="192 192 192" width="14%">
     79                    <translatableAttribute id="heading" context="replay">Map Name</translatableAttribute>
     80                </def>
     81
     82                <def id="mapSize" color="192 192 192" width="10%">
     83                    <translatableAttribute id="heading" context="replay">Size</translatableAttribute>
     84                </def>
     85
     86                <def id="popCapacity" color="192 192 192" width="10%">
     87                    <translatableAttribute id="heading" context="replay">Population</translatableAttribute>
     88                </def>
     89
     90                <def id="duration" color="192 192 192" width="10%">
     91                    <translatableAttribute id="heading" context="replay">Duration</translatableAttribute>
     92                </def>
     93
     94            </object>
     95
     96        </object>
     97
     98        <!-- Right Panel: Compability Filter & Replay Details -->
     99        <object name="rightPanel" size="100%-250 30 100%-20 100%-20" >
     100
     101            <!-- Compability Filter Checkbox -->
     102            <object name="compabilityFilter" type="checkbox" checked="true" style="ModernTickBox" size="0 4 20 100%" font="sans-bold-13">
     103                    <action on="Press">displayReplayList();</action>
     104            </object>
     105
     106            <!-- Compability Filter Label -->
     107            <object type="text" size="20 2 100% 100%" text_align="left" textcolor="white">
     108                <translatableAttribute id="caption">Filter compatible replays</translatableAttribute>
     109            </object>
     110
     111            <!-- Placeholder to show if no replay is selected -->
     112            <object name="replayInfoEmpty" size="0 30 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="false">
     113                <object name="logo" size="50%-110 40 50%+110 140" type="image" sprite="logo"/>
     114                <object name="subjectBox" type="image" sprite="ModernDarkBoxWhite" size="3% 180 97% 99%">
     115                    <object name="subject" size="5 5 100%-5 100%-5" type="text" style="ModernText" text_align="center"/>
     116                </object>
     117            </object>
     118
     119            <!--  -->
     120            <object name="replayInfo" size="0 30 100% 100%-60" type="image" sprite="ModernDarkBoxGold" hidden="true">
     121
     122                <!-- Map Name Label -->
     123                <object name="sgMapName" size="0 5 100% 20" type="text" style="ModernLabelText"/>
     124
     125                <!-- Map Preview Image -->
     126                <object name="sgMapPreview" size="5 25 100%-5 190"  type="image" sprite=""/>
     127
     128                <!-- Separator Line -->
     129                <object size="5 194 100%-5 195" type="image" sprite="ModernWhiteLine" z="25"/>
     130
     131                <!-- Map Type Caption -->
     132                <object size="5 195 50% 225" type="image" sprite="ModernItemBackShadeLeft">
     133                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     134                        <translatableAttribute id="caption">Map Type:</translatableAttribute>
     135                    </object>
     136                </object>
     137
     138                <!-- Map Type Label -->
     139                <object size="50% 195 100%-5 225" type="image" sprite="ModernItemBackShadeRight">
     140                    <object name="sgMapType" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     141                </object>
     142
     143                <!-- Separator Line -->
     144                <object size="5 224 100%-5 225" type="image" sprite="ModernWhiteLine" z="25"/>
     145
     146                <!-- Map Size Caption -->
     147                <object size="5 225 50% 255" type="image" sprite="ModernItemBackShadeLeft">
     148                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     149                        <translatableAttribute id="caption">Map Size:</translatableAttribute>
     150                    </object>
     151                </object>
     152
     153                <!-- Map Size Label -->
     154                <object size="50% 225 100%-5 255" type="image" sprite="ModernItemBackShadeRight">
     155                    <object name="sgMapSize" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     156                </object>
     157
     158                <!-- Separator Line -->
     159                <object size="5 254 100%-5 255" type="image" sprite="ModernWhiteLine" z="25"/>
     160
     161                <!-- Victory Condition Caption -->
     162                <object size="5 255 50% 285" type="image" sprite="ModernItemBackShadeLeft">
     163                    <object size="0 0 100%-10 100%" type="text" style="ModernLabelText" text_align="right">
     164                        <translatableAttribute id="caption">Victory:</translatableAttribute>
     165                    </object>
     166                </object>
     167
     168                <!-- Victory Condition Label -->
     169                <object size="50% 255 100%-5 285" type="image" sprite="ModernItemBackShadeRight">
     170                    <object name="sgVictory" size="0 0 100% 100%" type="text" style="ModernLabelText" text_align="left"/>
     171                </object>
     172
     173                <!-- Separator Line -->
     174                <object size="5 284 100%-5 285" type="image" sprite="ModernWhiteLine" z="25"/>
     175
     176                <!-- Map Description Text -->
     177                <object type="image" sprite="ModernDarkBoxWhite" size="3% 290 97% 60%">
     178                    <object name="sgMapDescription" size="0 0 100% 100%" type="text" style="ModernText" font="sans-12"/>
     179                </object>
     180
     181                <object type="image" sprite="ModernDarkBoxWhite" size="3% 60%+5 97% 100%-30">
     182
     183                    <!-- Number of Players Caption-->
     184                    <object size="0% 3% 57% 12%" type="text" style="ModernRightLabelText">
     185                        <translatableAttribute id="caption">Players:</translatableAttribute>
     186                    </object>
     187
     188                    <!-- Number of Players Label-->
     189                    <object name="sgNbPlayers" size="58% 3% 70% 12%" type="text" style="ModernLeftLabelText" text_align="left"/>
     190
     191                    <!-- Player Names -->
     192                    <object name="sgPlayersNames" size="0 15% 100% 100%" type="text" style="MapPlayerList"/>
     193                </object>
     194
     195                <!-- "Show Spoiler" Checkbox -->
     196                <object name="showSpoiler" type="checkbox" checked="false" style="ModernTickBox" size="10 100%-27 30 100%" font="sans-bold-13">
     197                    <action on="Press">displayReplayDetails();</action>
     198                </object>
     199
     200                <!-- "Show Spoiler" Label -->
     201                <object type="text" size="30 100%-28 100% 100%" text_align="left" textcolor="white">
     202                    <translatableAttribute id="caption">Spoiler</translatableAttribute>
     203                </object>
     204
     205            </object>
     206        </object>
     207
     208
     209        <!-- Bottom Panel: Buttons. -->
     210        <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" >
     211
     212            <!-- Main Menu Button -->
     213            <object type="button" style="StoneButton" size="25 0 17%+25 100%">
     214                <translatableAttribute id="caption">Main Menu</translatableAttribute>
     215                <action on="Press">Engine.SwitchGuiPage("page_pregame.xml");</action>
     216            </object>
     217
     218            <!-- Delete Button -->
     219            <object name="deleteReplayButton" type="button" style="StoneButton" size="20%+25 0 37%+25 100%" hotkey="session.savedgames.delete">
     220                <translatableAttribute id="caption">Delete</translatableAttribute>
     221                <action on="Press">deleteReplayButtonPressed();</action>
     222            </object>
     223
     224            <!-- Summary Button -->
     225            <object name="summaryButton" type="button" style="StoneButton" size="65%-50 0 82%-50 100%">
     226                <translatableAttribute id="caption">Summary</translatableAttribute>
     227                <action on="Press">showReplaySummary();</action>
     228            </object>
     229
     230            <!-- Start Replay Button -->
     231            <object name="startReplayButton" type="button" style="StoneButton" size="83%-25 0 100%-25 100%">
     232                <translatableAttribute id="caption">Start Replay</translatableAttribute>
     233                <action on="Press">startReplay();</action>
     234            </object>
     235
     236        </object>
     237    </object>
     238</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

    function leaveGame(willRejoin)  
    335335                resignGame(true);
    336336            }
    337337        }
    338338    }
    339339
     340    let summary = {
     341        "timeElapsed" : extendedSimState.timeElapsed,
     342        "playerStates": extendedSimState.players,
     343        "players": g_Players,
     344        "mapSettings": Engine.GetMapSettings(),
     345    }
     346
     347    if (!g_IsReplay)
     348        Engine.SaveReplayMetadata(JSON.stringify(summary));
     349
    340350    stopAmbient();
    341351    Engine.EndGame();
    342352
    343353    if (g_IsController && Engine.HasXmppClient())
    344354        Engine.SendUnregisterGame();
    345355
    346     Engine.SwitchGuiPage("page_summary.xml", {
    347                             "gameResult"  : gameResult,
    348                             "timeElapsed" : extendedSimState.timeElapsed,
    349                             "playerStates": extendedSimState.players,
    350                             "players": g_Players,
    351                             "mapSettings": mapSettings
    352                          });
     356    summary.gameResult = gameResult;
     357    summary.isReplay = g_IsReplay;
     358    Engine.SwitchGuiPage("page_summary.xml", summary);
    353359}
    354360
    355361// Return some data that we'll use when hotloading this file after changes
    356362function getHotloadData()
    357363{
  • binaries/data/mods/public/gui/summary/summary.xml

     
    156156        </object>
    157157
    158158        <object type="button" style="ModernButtonRed" size="100%-160 100%-48 100%-20 100%-20">
    159159            <translatableAttribute id="caption">Continue</translatableAttribute>
    160160            <action on="Press"><![CDATA[
    161                 if (!Engine.HasXmppClient())
     161                if (g_GameData.isReplay)
     162                {
     163                    Engine.SwitchGuiPage("page_replaymenu.xml");
     164                }
     165                else if (!Engine.HasXmppClient())
    162166                {
    163167                    Engine.SwitchGuiPage("page_pregame.xml");
    164168                }
    165169                else
    166170                {
  • source/gui/scripting/ScriptFunctions.cpp

     
    5454#include "ps/World.h"
    5555#include "ps/scripting/JSInterface_ConfigDB.h"
    5656#include "ps/scripting/JSInterface_Console.h"
    5757#include "ps/scripting/JSInterface_Mod.h"
    5858#include "ps/scripting/JSInterface_VFS.h"
     59#include "ps/scripting/JSInterface_VisualReplay.h"
    5960#include "renderer/scripting/JSInterface_Renderer.h"
    6061#include "simulation2/Simulation2.h"
    6162#include "simulation2/components/ICmpAIManager.h"
    6263#include "simulation2/components/ICmpCommandQueue.h"
    6364#include "simulation2/components/ICmpGuiInterface.h"
    void GuiScriptingInit(ScriptInterface& s  
    926927    JSI_ConfigDB::RegisterScriptFunctions(scriptInterface);
    927928    JSI_Mod::RegisterScriptFunctions(scriptInterface);
    928929    JSI_Sound::RegisterScriptFunctions(scriptInterface);
    929930    JSI_L10n::RegisterScriptFunctions(scriptInterface);
    930931    JSI_Lobby::RegisterScriptFunctions(scriptInterface);
     932    JSI_VisualReplay::RegisterScriptFunctions(scriptInterface);
    931933
    932934    // VFS (external)
    933935    scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &JSI_VFS::BuildDirEntList>("BuildDirEntList");
    934936    scriptInterface.RegisterFunction<bool, CStrW, JSI_VFS::FileExists>("FileExists");
    935937    scriptInterface.RegisterFunction<double, std::wstring, &JSI_VFS::GetFileMTime>("GetFileMTime");
  • source/ps/Replay.cpp

     
    2424#include "lib/file/file_system.h"
    2525#include "lib/res/h_mgr.h"
    2626#include "lib/tex/tex.h"
    2727#include "ps/Game.h"
    2828#include "ps/Loader.h"
     29#include "ps/Mod.h"
    2930#include "ps/Profile.h"
    3031#include "ps/ProfileViewer.h"
     32#include "ps/Pyrogenesis.h"
    3133#include "scriptinterface/ScriptInterface.h"
    3234#include "scriptinterface/ScriptStats.h"
    3335#include "simulation2/Simulation2.h"
    3436#include "simulation2/helpers/SimulationCommand.h"
    3537
    CReplayLogger::~CReplayLogger()  
    6163    delete m_Stream;
    6264}
    6365
    6466void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
    6567{
     68    // Add timestamp, since the file-modification-date can change
     69    m_ScriptInterface.SetProperty(attribs, "timestamp", (i32) std::time(nullptr));
     70
     71    // Add engine version and currently loaded mods for sanity checks when replaying
     72    m_ScriptInterface.SetProperty(attribs, "engine_version", CStr(engine_version));
     73    m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded);
     74
    6675    // Construct the directory name based on the PID, to be relatively unique.
    6776    // Append "-1", "-2" etc if we run multiple matches in a single session,
    6877    // to avoid accidentally overwriting earlier logs.
    69 
    7078    std::wstringstream name;
    7179    name << getpid();
    7280
    7381    static int run = -1;
    7482    if (++run)
    7583        name << "-" << run;
    7684
    77     OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";
    78     CreateDirectories(path.Parent(), 0700);
    79     m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
     85    m_Directory = psLogDir() / L"sim_log" / name.str();
     86    CreateDirectories(m_Directory, 0700);
    8087
     88    m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc);
    8189    *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
    8290}
    8391
    8492void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
    8593{
    void CReplayLogger::Hash(const std::stri  
    101109        *m_Stream << "hash-quick " << Hexify(hash) << "\n";
    102110    else
    103111        *m_Stream << "hash " << Hexify(hash) << "\n";
    104112}
    105113
     114OsPath CReplayLogger::GetDirectory() const
     115{
     116    return m_Directory;
     117}
     118
    106119////////////////////////////////////////////////////////////////
    107120
    108121CReplayPlayer::CReplayPlayer() :
    109122    m_Stream(NULL)
    110123{
  • source/ps/Replay.h

    public:  
    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     * Remember the directory containing the commands.txt file, so that we can save additional files to it.
     53     */
     54    virtual OsPath GetDirectory() const = 0;
    5055};
    5156
    5257/**
    5358 * Implementation of IReplayLogger that simply throws away all data.
    5459 */
    class CDummyReplayLogger : public IRepla  
    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 GetDirectory() const { return OsPath(); }
    6167};
    6268
    6369/**
    6470 * Implementation of IReplayLogger that saves data to a file in the logs directory.
    6571 */
    public:  
    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 GetDirectory() const;
    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 */
  • 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/Filesystem.h"
     27#include "ps/Game.h"
     28#include "ps/Pyrogenesis.h"
     29#include "ps/Replay.h"
     30#include "scriptinterface/ScriptInterface.h"
     31
     32/**
     33 * Filter too short replays (value in seconds).
     34 */
     35const u8 minimumReplayDuration = 3;
     36
     37/**
     38 * Allows quick debugging of potential platform-dependent file-reading bugs.
     39 */
     40const bool debugParser = false;
     41
     42OsPath VisualReplay::GetDirectoryName()
     43{
     44    return OsPath(psLogDir() / L"sim_log");
     45}
     46
     47JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
     48{
     49    TIMER(L"GetReplays");
     50    JSContext* cx = scriptInterface.GetContext();
     51    JSAutoRequest rq(cx);
     52
     53    u32 i = 0;
     54    DirectoryNames directories;
     55    JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
     56    if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
     57        for (OsPath& directory : directories)
     58        {
     59            JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
     60            if (!replayData.isNull())
     61                JS_SetElement(cx, replays, i++, replayData);
     62        }
     63    return JS::ObjectValue(*replays);
     64}
     65
     66/**
     67 * Move the cursor backwards until a newline was read or the beginning of the file was found.
     68 * Either way the cursor points to the beginning of a newline.
     69 *
     70 * @return The current cursor position or -1 on error.
     71 */
     72int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64 fileSize)
     73{
     74    int currentPos;
     75    char character;
     76    for (int characters = 0; characters < 10000; ++characters)
     77    {
     78        currentPos = (int) replayStream->tellg();
     79
     80        // Stop when reached the beginning of the file
     81        if (currentPos == 0)
     82            return currentPos;
     83
     84        if (!replayStream->good())
     85        {
     86            LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str());
     87            return -1;
     88        }
     89
     90        // Stop when reached newline
     91        replayStream->get(character);
     92        if (character == '\n')
     93            return currentPos;
     94
     95        // Otherwise go back one character.
     96        // Notice: -1 will set the cursor back to the most recently read character.
     97        replayStream->seekg(-2, std::ios_base::cur);
     98    }
     99
     100    LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str());
     101    return -1;
     102}
     103
     104/**
     105 * Compute game duration. Assume constant turn length.
     106 * Find the last line that starts with "turn" by reading the file backwards.
     107 *
     108 * @return seconds or -1 on error
     109 */
     110int getReplayDuration(std::istream *replayStream, const CStr& fileName, const u64 fileSize)
     111{
     112    CStr type;
     113
     114    // Move one character before the file-end
     115    replayStream->seekg(-2, std::ios_base::end);
     116
     117    // Infinite loop protection, should never occur.
     118    // There should be about 5 lines to read until a turn is found.
     119    for (int linesRead = 1; linesRead < 1000; ++linesRead)
     120    {
     121        int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize);
     122
     123        // Read error or reached file beginning. No turns exist.
     124        if (currentPosition < 1)
     125            return -1;
     126
     127        if (debugParser)
     128            debug_printf("At position %i of %lu after %i lines reads.\n", currentPosition, fileSize, linesRead);
     129
     130        if (!replayStream->good())
     131        {
     132            LOGERROR("Read error when determining replay duration at %i of %lu in %s", currentPosition - 2, fileSize, fileName.c_str());
     133            return -1;
     134        }
     135
     136        // Found last turn, compute duration.
     137        if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn")
     138        {
     139            u32 turn = 0, turnLength = 0;
     140            *replayStream >> turn >> turnLength;
     141            return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0
     142        }
     143
     144        // Otherwise move cursor back to the character before the last newline
     145        replayStream->seekg(currentPosition - 2, std::ios_base::beg);
     146    }
     147
     148    LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str());
     149    return -1;
     150}
     151
     152JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory)
     153{
     154    // The directory argument must not be constant, otherwise concatenating will fail
     155    const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
     156
     157    if (debugParser)
     158        debug_printf("Opening %s\n", utf8_from_wstring(replayFile.string()).c_str());
     159
     160    if (!FileExists(replayFile))
     161        return JSVAL_NULL;
     162
     163    // Get file size and modification date
     164    CFileInfo fileInfo;
     165    GetFileInfo(replayFile, &fileInfo);
     166    const u64 fileTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
     167    const u64 fileSize = (u64)fileInfo.Size();
     168
     169    if (fileSize == 0)
     170        return JSVAL_NULL;
     171
     172    // Open file
     173    // TODO: enhancement: support unicode when OsString() is properly implemented for windows
     174    const CStr fileName = utf8_from_wstring(replayFile.string());
     175    std::ifstream* replayStream = new std::ifstream(fileName.c_str());
     176
     177    // File must begin with "start"
     178    CStr type;
     179    if (!(*replayStream >> type).good() || type != "start")
     180    {
     181        LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str());
     182        SAFE_DELETE(replayStream);
     183        return JSVAL_NULL;
     184    }
     185
     186    // Parse header / first line
     187    CStr header;
     188    std::getline(*replayStream, header);
     189    JSContext* cx = scriptInterface.GetContext();
     190    JSAutoRequest rq(cx);
     191    JS::RootedValue attribs(cx);
     192    if (!scriptInterface.ParseJSON(header, &attribs))
     193    {
     194        LOGERROR("Couldn't parse replay header of %s", fileName.c_str());
     195        SAFE_DELETE(replayStream);
     196        return JSVAL_NULL;
     197    }
     198
     199    // Ensure "turn" after header
     200    if (!(*replayStream >> type).good() || type != "turn")
     201    {
     202        SAFE_DELETE(replayStream);
     203        return JSVAL_NULL; // there are no turns at all
     204    }
     205
     206    // Don't process files of rejoined clients
     207    u32 turn = 1;
     208    *replayStream >> turn;
     209    if (turn != 0)
     210    {
     211        SAFE_DELETE(replayStream);
     212        return JSVAL_NULL;
     213    }
     214
     215    int duration = getReplayDuration(replayStream, fileName, fileSize);
     216
     217    SAFE_DELETE(replayStream);
     218
     219    // Ensure minimum duration
     220    if (duration < minimumReplayDuration)
     221        return JSVAL_NULL;
     222
     223    // Return the actual data
     224    JS::RootedValue replayData(cx);
     225    scriptInterface.Eval("({})", &replayData);
     226    scriptInterface.SetProperty(replayData, "file", replayFile);
     227    scriptInterface.SetProperty(replayData, "directory", directory);
     228    scriptInterface.SetProperty(replayData, "filemod_timestamp", std::to_string(fileTime));
     229    scriptInterface.SetProperty(replayData, "attribs", attribs);
     230    scriptInterface.SetProperty(replayData, "duration", duration);
     231    return replayData;
     232}
     233
     234bool VisualReplay::DeleteReplay(const CStrW& replayDirectory)
     235{
     236    if (replayDirectory.empty())
     237        return false;
     238
     239    const OsPath directory = GetDirectoryName() / replayDirectory;
     240    return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     241}
     242
     243
     244JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
     245{
     246    // Create empty JS object
     247    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     248    JSAutoRequest rq(cx);
     249    JS::RootedValue attribs(cx);
     250    pCxPrivate->pScriptInterface->Eval("({})", &attribs);
     251
     252    // Return empty object if file doesn't exist
     253    const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt";
     254    if (!FileExists(replayFile))
     255        return attribs;
     256
     257    // Open file
     258    std::istream* replayStream = new std::ifstream(utf8_from_wstring(replayFile.string()).c_str());
     259    CStr type, line;
     260    ENSURE((*replayStream >> type).good() && type == "start");
     261
     262    // Read and return first line
     263    std::getline(*replayStream, line);
     264    pCxPrivate->pScriptInterface->ParseJSON(line, &attribs);
     265    SAFE_DELETE(replayStream);;
     266    return attribs;
     267}
     268
     269// TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp)
     270void VisualReplay::SaveReplayMetadata(const CStrW& data)
     271{
     272    // TODO: enhancement: use JS::HandleValue similar to SaveGame
     273    if (!g_Game)
     274        return;
     275
     276    // Get the directory of the currently active replay
     277    const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
     278    CreateDirectories(fileName.Parent(), 0700);
     279
     280    std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
     281    stream << utf8_from_wstring(data);
     282    stream.close();
     283}
     284
     285JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
     286{
     287    const OsPath filePath = GetDirectoryName() / directoryName / L"metadata.json";
     288
     289    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     290    JSAutoRequest rq(cx);
     291    JS::RootedValue metadata(cx);
     292
     293    if (!FileExists(filePath))
     294        return JSVAL_NULL;
     295
     296    std::ifstream* stream = new std::ifstream(OsString(filePath).c_str());
     297    ENSURE(stream->good());
     298    CStr line;
     299    std::getline(*stream, line);
     300    stream->close();
     301    delete stream;
     302    pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
     303
     304    return metadata;
     305}
  • 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 */
     28namespace VisualReplay
     29{
     30
     31/**
     32 * Returns the path to the sim-log directory (that contains the directories with the replay files.
     33 *
     34 * @param scriptInterface the ScriptInterface in which to create the return data.
     35 * @return OsPath the absolute file path
     36 */
     37OsPath GetDirectoryName();
     38
     39/**
     40 * Get a list of replays to display in the GUI.
     41 *
     42 * @param scriptInterface the ScriptInterface in which to create the return data.
     43 * @return array of objects containing replay data
     44 */
     45JS::Value GetReplays(ScriptInterface& scriptInterface);
     46
     47/**
     48 * Parses a commands.txt file and extracts metadata.
     49 * Works similarly to CGame::LoadReplayData().
     50 */
     51JS::Value LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory);
     52
     53/**
     54 * Permanently deletes the visual replay (including the parent directory)
     55 *
     56 * @param replayFile path to commands.txt, whose parent directory will be deleted
     57 * @return true if deletion was successful, false on error
     58 */
     59bool DeleteReplay(const CStrW& replayFile);
     60
     61/**
     62 * Returns the parsed header of the replay file (commands.txt).
     63 */
     64JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
     65
     66/**
     67 * Returns the metadata of a replay.
     68 */
     69JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
     70
     71/**
     72 * Saves the metadata from the session to metadata.json
     73 */
     74void SaveReplayMetadata(const CStrW& data);
     75
     76}
     77
     78#endif
  • source/ps/scripting/JSInterface_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 "network/NetClient.h"
     21#include "network/NetServer.h"
     22#include "ps/Filesystem.h"
     23#include "ps/Game.h"
     24#include "ps/VisualReplay.h"
     25#include "ps/scripting/JSInterface_VisualReplay.h"
     26
     27void JSI_VisualReplay::StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory)
     28{
     29    ENSURE(!g_NetServer);
     30    ENSURE(!g_NetClient);
     31    ENSURE(!g_Game);
     32
     33    const OsPath replayFile = VisualReplay::GetDirectoryName() / directory / L"commands.txt";
     34    if (FileExists(replayFile))
     35    {
     36        g_Game = new CGame(false, false);
     37        // TODO: support unicode when OsString() is implemented for windows
     38        g_Game->StartVisualReplay(utf8_from_wstring(replayFile.string()));
     39    }
     40}
     41
     42bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile)
     43{
     44    return VisualReplay::DeleteReplay(replayFile);
     45}
     46
     47JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CxPrivate* pCxPrivate)
     48{
     49    return VisualReplay::GetReplays(*(pCxPrivate->pScriptInterface));
     50}
     51
     52JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName)
     53{
     54    return VisualReplay::GetReplayAttributes(pCxPrivate, directoryName);
     55}
     56
     57JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName)
     58{
     59    return VisualReplay::GetReplayMetadata(pCxPrivate, directoryName);
     60}
     61
     62void JSI_VisualReplay::SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data)
     63{
     64    VisualReplay::SaveReplayMetadata(data);
     65}
     66
     67void JSI_VisualReplay::RegisterScriptFunctions(ScriptInterface& scriptInterface)
     68{
     69    scriptInterface.RegisterFunction<JS::Value, &GetReplays>("GetReplays");
     70    scriptInterface.RegisterFunction<bool, CStrW, &DeleteReplay>("DeleteReplay");
     71    scriptInterface.RegisterFunction<void, CStrW, &StartVisualReplay>("StartVisualReplay");
     72    scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayAttributes>("GetReplayAttributes");
     73    scriptInterface.RegisterFunction<JS::Value, CStrW, &GetReplayMetadata>("GetReplayMetadata");
     74    scriptInterface.RegisterFunction<void, CStrW, &SaveReplayMetadata>("SaveReplayMetadata");
     75}
  • source/ps/scripting/JSInterface_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_JSI_VISUALREPLAY
     19#define INCLUDED_JSI_VISUALREPLAY
     20
     21#include "ps/VisualReplay.h"
     22#include "scriptinterface/ScriptInterface.h"
     23
     24namespace JSI_VisualReplay
     25{
     26    void StartVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW directory);
     27    bool DeleteReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW replayFile);
     28    JS::Value GetReplays(ScriptInterface::CxPrivate* pCxPrivate);
     29    JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName);
     30    JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, CStrW directoryName);
     31    void SaveReplayMetadata(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), CStrW data);
     32    void RegisterScriptFunctions(ScriptInterface& scriptInterface);
     33}
     34
     35#endif