Ticket #3258: t3258_visual_replay_menu_v11_PROFILING.patch

File t3258_visual_replay_menu_v11_PROFILING.patch, 65.2 KB (added by elexis, 9 years ago)

Same patch above, but with profiling output. Notice that showing the output also consumes performance.

  • 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    var time = {
     27        "date": 0,
     28        "mapnames": 0,
     29        "mapsizes": 0,
     30        "popcap": 0,
     31        "duration": 0
     32    };
     33
     34    var start = Date.now();
     35    initDateFilter();
     36    time["date"] = Date.now() - start;
     37
     38    start = Date.now();
     39    initMapNameFilter();
     40    time["mapname"] = Date.now() - start;
     41
     42    start = Date.now();
     43    initMapSizeFilter();
     44    time["mapsize"] = Date.now() - start;
     45
     46    start = Date.now();
     47    initPopCapFilter();
     48    time["popcap"] = Date.now() - start;
     49
     50    start = Date.now();
     51    initDurationFilter();
     52    time["duration"] = Date.now() - start;
     53   
     54    error("initFilters: " + JSON.stringify(time));
     55}
     56
     57/**
     58 * Allow to filter by month. Uses g_Replays.
     59 */
     60function initDateFilter()
     61{
     62    var months = g_Replays.map(replay => getReplayMonth(replay));
     63    months = months.filter((month, index) => months.indexOf(month) == index).sort();
     64    months.unshift(translateWithContext("datetime", "Any"));
     65
     66    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     67    dateTimeFilter.list = months;
     68    dateTimeFilter.list_data = months;
     69
     70    if (dateTimeFilter.selected == -1 || dateTimeFilter.selected >= dateTimeFilter.list.length)
     71        dateTimeFilter.selected = 0;
     72}
     73
     74/**
     75 * Allow to filter by mapsize. Uses g_MapSizes.
     76 */
     77function initMapSizeFilter()
     78{
     79    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     80    mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_mapSizes.shortNames);
     81    mapSizeFilter.list_data = [-1].concat(g_mapSizes.tiles);
     82
     83    if (mapSizeFilter.selected == -1 || mapSizeFilter.selected >= mapSizeFilter.list.length)
     84        mapSizeFilter.selected = 0;
     85}
     86
     87/**
     88 * Allow to filter by mapname. Uses g_MapNames.
     89 */
     90function initMapNameFilter()
     91{
     92    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     93    mapNameFilter.list = [translateWithContext("map name", "Any")].concat(g_MapNames);
     94    mapNameFilter.list_data = [""].concat(g_MapNames.map(mapName => translate(mapName)));
     95
     96    if (mapNameFilter.selected == -1 || mapNameFilter.selected >= mapNameFilter.list.length)
     97        mapNameFilter.selected = 0;
     98}
     99
     100/**
     101 * Allow to filter by population capacity.
     102 */
     103function initPopCapFilter()
     104{
     105    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     106    populationFilter.list = [translateWithContext("population capacity", "Any")].concat(g_PopulationCapacities.Title);
     107    populationFilter.list_data = [""].concat(g_PopulationCapacities.Population);
     108
     109    if (populationFilter.selected == -1 || populationFilter.selected >= populationFilter.list.length)
     110        populationFilter.selected = 0;
     111}
     112
     113/**
     114 * Allow to filter by game duration. Uses g_DurationFilterIntervals.
     115 */
     116function initDurationFilter()
     117{
     118    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     119    durationFilter.list = g_DurationFilterIntervals.map((interval, index) => {
     120
     121        if (index == 0)
     122            return translateWithContext("duration", "Any");
     123
     124        if (index == 1)
     125            // Translation: Shorter duration than max minutes.
     126            return sprintf(translateWithContext("duration filter", "< %(max)s min"), interval);
     127
     128        if (index == g_DurationFilterIntervals.length - 1)
     129            // Translation: Longer duration than min minutes.
     130            return sprintf(translateWithContext("duration filter", "> %(min)s min"), interval);
     131
     132        // Translation: Duration between min and max minutes.
     133        return sprintf(translateWithContext("duration filter", "%(min)s - %(max)s min"), interval);
     134    });
     135    durationFilter.list_data = g_DurationFilterIntervals.map((interval, index) => index);
     136
     137    if (durationFilter.selected == -1 || durationFilter.selected >= g_DurationFilterIntervals.length)
     138        durationFilter.selected = 0;
     139}
     140
     141/**
     142 * Initializes g_ReplaysFiltered with replays that are not filtered out and sort it.
     143 */
     144function filterReplays()
     145{
     146    const sortKey = Engine.GetGUIObjectByName("replaySelection").selected_column;
     147    const sortOrder = Engine.GetGUIObjectByName("replaySelection").selected_column_order;
     148
     149    g_ReplaysFiltered = g_Replays.filter(replay => filterReplay(replay)).sort((a, b) =>
     150    {
     151        let cmpA, cmpB;
     152        switch (sortKey)
     153        {
     154        case 'name':
     155            cmpA = +a.timestamp;
     156            cmpB = +b.timestamp;
     157            break;
     158        case 'duration':
     159            cmpA = +a.duration;
     160            cmpB = +b.duration;
     161            break;
     162        case 'players':
     163            cmpA = +a.attribs.settings.PlayerData.length;
     164            cmpB = +b.attribs.settings.PlayerData.length;
     165            break;
     166        case 'mapName':
     167            cmpA = getReplayMapName(a);
     168            cmpB = getReplayMapName(b);
     169            break;
     170        case 'mapSize':
     171            cmpA = +a.attribs.settings.Size;
     172            cmpB = +b.attribs.settings.Size;
     173            break;
     174        case 'popCapacity':
     175            cmpA = +a.attribs.settings.PopulationCap;
     176            cmpB = +b.attribs.settings.PopulationCap;
     177            break;
     178        }
     179
     180        if (cmpA < cmpB)
     181            return -sortOrder;
     182        else if (cmpA > cmpB)
     183            return +sortOrder;
     184
     185        return 0;
     186    });
     187}
     188
     189/**
     190 * Decides whether the replay should be listed.
     191 *
     192 * @returns {bool} - true if replay should be visible
     193 */
     194function filterReplay(replay)
     195{
     196    // Check for compability first (most likely to filter)
     197    var compabilityFilter = Engine.GetGUIObjectByName("compabilityFilter");
     198    if (compabilityFilter.checked && !isReplayCompatible(replay))
     199        return false;
     200
     201    // Filter date/time (select a month)
     202    var dateTimeFilter = Engine.GetGUIObjectByName("dateTimeFilter");
     203    if (dateTimeFilter.selected > 0 && getReplayMonth(replay) != dateTimeFilter.list_data[dateTimeFilter.selected])
     204        return false;
     205
     206    // Filter by playernames
     207    var playersFilter = Engine.GetGUIObjectByName("playersFilter");
     208    var keywords = playersFilter.caption.toLowerCase().split(" ");
     209    if (keywords.length)
     210    {
     211        // We just check if all typed words are somewhere in the playerlist of that replay
     212        let playerList = replay.attribs.settings.PlayerData.map(player => player ? player.Name : "").join(" ").toLowerCase();
     213        if (!keywords.every(keyword => playerList.indexOf(keyword) != -1))
     214            return false;
     215    }
     216
     217    // Filter by map name
     218    var mapNameFilter = Engine.GetGUIObjectByName("mapNameFilter");
     219    if (mapNameFilter.selected > 0 && getReplayMapName(replay) != mapNameFilter.list_data[mapNameFilter.selected])
     220        return false;
     221
     222    // Filter by map size
     223    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
     224    if (mapSizeFilter.selected > 0 && replay.attribs.settings.Size != mapSizeFilter.list_data[mapSizeFilter.selected])
     225        return false;
     226
     227    // Filter by population capacity
     228    var populationFilter = Engine.GetGUIObjectByName("populationFilter");
     229    if (populationFilter.selected > 0 && replay.attribs.settings.PopulationCap != populationFilter.list_data[populationFilter.selected])
     230        return false;
     231
     232    // Filter by game duration
     233    var durationFilter = Engine.GetGUIObjectByName("durationFilter");
     234    if (durationFilter.selected > 0)
     235    {
     236        let interval = g_DurationFilterIntervals[durationFilter.selected];
     237
     238        if ((interval.min > -1 && replay.duration < interval.min * 60) ||
     239            (interval.max > -1 && replay.duration > interval.max * 60))
     240            return false;
     241    }
     242
     243    return true;
     244}
  • 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    var start = Date.now();
     46    var time = {};
     47    loadReplays();
     48    time["loadReplays"] = Date.now() - start;
     49
     50    start = Date.now();
     51    displayReplayList();
     52    time["displayReplayList"] = Date.now() - start;
     53   
     54    error("init():" + JSON.stringify(time));
     55}
     56
     57/**
     58 * Store the list of replays loaded in C++ in g_Replays.
     59 * Check timestamp and compatibility and extract g_Playernames, g_MapNames
     60 */
     61function loadReplays()
     62{
     63    g_Replays = Engine.GetReplays();
     64
     65    var start;
     66    var time = {
     67        "replay compability check": 0,
     68        "sanitize attributes": 0,
     69        "parse playernames": 0,
     70        "extract maps": 0
     71    };
     72
     73    // TODO: enhancement: Move logic to C++
     74    g_Playernames = [];
     75    for (let replay of g_Replays)
     76    {
     77        // Use time saved in file, otherwise file mod date
     78        replay.timestamp = replay.attribs.timestamp ? +replay.attribs.timestamp : +replay.filemod_timestamp;
     79
     80        // Check replay for compability
     81        start = Date.now();
     82        replay.isCompatible = isReplayCompatible(replay);
     83        time["replay compability check"] += Date.now() - start;
     84
     85        start = Date.now();
     86        sanitizeGameAttributes(replay.attribs);
     87        time["sanitize attributes"] += Date.now() - start;
     88
     89        // Extract map names
     90        start = Date.now();
     91        if (g_MapNames.indexOf(replay.attribs.settings.Name) == -1 && replay.attribs.settings.Name != "")
     92            g_MapNames.push(replay.attribs.settings.Name);
     93        time["extract maps"] += Date.now() - start;
     94
     95        start = Date.now();
     96        // Extract playernames
     97        for (let playerData of replay.attribs.settings.PlayerData)
     98        {
     99            if (!playerData || playerData.AI)
     100                continue;
     101
     102            // Remove rating from nick
     103            let playername = playerData.Name;
     104            let ratingStart = playername.indexOf(" (");
     105            if (ratingStart != -1)
     106                playername = playername.substr(0, ratingStart);
     107
     108            if (g_Playernames.indexOf(playername) == -1)
     109                g_Playernames.push(playername);
     110        }
     111        time["parse playernames"] += Date.now() - start;
     112    }
     113
     114    start = Date.now();
     115    g_MapNames.sort();
     116    time["sort playernames"] = Date.now() - start;
     117
     118    // Reload filters (since they depend on g_Replays and its derivatives)
     119    start = Date.now();
     120    initFilters();
     121    time["reload filters"] = Date.now() - start;
     122
     123    error("loadReplays: " + JSON.stringify(time));
     124}
     125
     126/**
     127 * We may encounter malformed replays.
     128 */
     129function sanitizeGameAttributes(attribs)
     130{
     131    if (!attribs.settings)
     132        attribs.settings = {};
     133
     134    if (!attribs.settings.Size)
     135        attribs.settings.Size = -1;
     136
     137    if (!attribs.settings.Name)
     138        attribs.settings.Name = "";
     139
     140    if (!attribs.settings.PlayerData)
     141        attribs.settings.PlayerData = [];
     142
     143    if (!attribs.settings.PopulationCap)
     144        attribs.settings.PopulationCap = 300;
     145
     146    if (!attribs.settings.mapType)
     147        attribs.settings.mapType = "skirmish";
     148
     149    if (!attribs.settings.GameType)
     150        attribs.settings.GameType = "conquest";
     151
     152    // Remove gaia
     153    if (attribs.settings.PlayerData.length && attribs.settings.PlayerData[0] == null)
     154        attribs.settings.PlayerData.shift();
     155
     156    attribs.settings.PlayerData.forEach((pData, index) => {
     157        if (!pData.Name)
     158            pData.Name = "";
     159    });
     160}
     161
     162/**
     163 * Filter g_Replays, fill the GUI list with that data and show the description of the current replay.
     164 */
     165function displayReplayList()
     166{
     167    // Remember previously selected replay
     168    var replaySelection = Engine.GetGUIObjectByName("replaySelection");
     169    if (replaySelection.selected != -1)
     170        g_selectedReplayDirectory = g_ReplaysFiltered[replaySelection.selected].directory;
     171
     172    var time = {};
     173    var start = Date.now();
     174    filterReplays();
     175    time["filter replays"] = Date.now() - start;
     176
     177    // Create GUI list data
     178    start = Date.now();
     179    var list = g_ReplaysFiltered.map(replay => {
     180        let works = replay.isCompatible;
     181        return {
     182            "directories": replay.directory,
     183            "months": greyout(getReplayDateTime(replay), works),
     184            "popCaps": greyout(translatePopulationCapacity(replay.attribs.settings.PopulationCap), works),
     185            "mapNames": greyout(getReplayMapName(replay), works),
     186            "mapSizes": greyout(translateMapSize(replay.attribs.settings.Size), works),
     187            "durations": greyout(getReplayDuration(replay), works),
     188            "playerNames": greyout(getReplayPlayernames(replay), works)
     189        };
     190    });
     191    time["create list data"] = Date.now() - start;
     192
     193    // Extract arrays
     194    start = Date.now();
     195    if (list.length)
     196        list = prepareForDropdown(list);
     197    time["prepareForDropdown"] = Date.now() - start;
     198
     199    // Push to GUI
     200    replaySelection.selected = -1;
     201    replaySelection.list_name = list.months || [];
     202    replaySelection.list_players = list.playerNames || [];
     203    replaySelection.list_mapName = list.mapNames || [];
     204    replaySelection.list_mapSize = list.mapSizes || [];
     205    replaySelection.list_popCapacity = list.popCaps || [];
     206    replaySelection.list_duration = list.durations || [];
     207
     208    // Change these last, otherwise crash
     209    replaySelection.list = list.directories || [];
     210    replaySelection.list_data = list.directories || [];
     211    time["push gui data"] = Date.now() - start;
     212
     213    // Restore selection
     214    replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_selectedReplayDirectory);
     215
     216    start = Date.now();
     217    displayReplayDetails();
     218    time["displayReplayDetails"] = Date.now() - start;
     219   
     220    error("displayReplayList: " + JSON.stringify(time));
     221}
     222
     223/**
     224 * Shows preview image, description and player text in the right panel.
     225 */
     226function displayReplayDetails()
     227{
     228    var selected = Engine.GetGUIObjectByName("replaySelection").selected;
     229    var replaySelected = selected > -1;
     230
     231    Engine.GetGUIObjectByName("replayInfo").hidden = !replaySelected;
     232    Engine.GetGUIObjectByName("replayInfoEmpty").hidden = replaySelected;
     233    Engine.GetGUIObjectByName("startReplayButton").enabled = replaySelected;
     234    Engine.GetGUIObjectByName("deleteReplayButton").enabled = replaySelected;
     235    Engine.GetGUIObjectByName("summaryButton").enabled = replaySelected;
     236
     237    if (!replaySelected)
     238        return;
     239
     240    var replay = g_ReplaysFiltered[selected];
     241    var mapData = getMapDescriptionAndPreview(replay.attribs.settings.mapType, replay.attribs.map);
     242
     243    // Update GUI
     244    Engine.GetGUIObjectByName("sgMapName").caption = translate(replay.attribs.settings.Name);
     245    Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(replay.attribs.settings.Size);
     246    Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(replay.attribs.settings.mapType);
     247    Engine.GetGUIObjectByName("sgVictory").caption = translateVictoryCondition(replay.attribs.settings.GameType);
     248    Engine.GetGUIObjectByName("sgNbPlayers").caption = replay.attribs.settings.PlayerData.length;
     249    Engine.GetGUIObjectByName("sgPlayersNames").caption = getReplayTeamText(replay);
     250    Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description;
     251    Engine.GetGUIObjectByName("sgMapPreview").sprite = "cropped:(0.7812,0.5859)session/icons/mappreview/" + mapData.preview;
     252}
     253
     254/**
     255 * Adds grey font if replay is not compatible.
     256 */
     257function greyout(text, isCompatible)
     258{
     259    return isCompatible ? text : '[color="96 96 96"]' + text + '[/color]';
     260}
     261
     262/**
     263 * Returns a human-readable version of the replay date.
     264 */
     265function getReplayDateTime(replay)
     266{
     267    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM-dd HH:mm"))
     268}
     269
     270/**
     271 * Returns a human-readable list of the playernames of that replay.
     272 *
     273 * @returns {string}
     274 */
     275function getReplayPlayernames(replay)
     276{
     277    // TODO: colorize playernames like in the lobby.
     278    return replay.attribs.settings.PlayerData.map(pData => pData.Name).join(", ");
     279}
     280
     281/**
     282 * Returns the name of the map of the given replay.
     283 *
     284 * @returns {string}
     285 */
     286function getReplayMapName(replay)
     287{
     288    return translate(replay.attribs.settings.Name);
     289}
     290
     291/**
     292 * Returns the month of the given replay in the format "yyyy-MM".
     293 *
     294 * @returns {string}
     295 */
     296function getReplayMonth(replay)
     297{
     298    return Engine.FormatMillisecondsIntoDateString(replay.timestamp * 1000, translate("yyyy-MM"));
     299}
     300
     301/**
     302 * Returns a human-readable version of the time when the replay started.
     303 *
     304 * @returns {string}
     305 */
     306function getReplayDuration(replay)
     307{
     308    return timeToString(replay.duration * 1000);
     309}
     310
     311/**
     312 * True if we can start the given replay with the currently loaded mods.
     313 */
     314function isReplayCompatible(replay)
     315{
     316    return replayHasSameEngineVersion(replay) && hasSameMods(replay.attribs, g_EngineInfo);
     317}
     318
     319/**
     320 * True if we can start the given replay with the currently loaded mods.
     321 */
     322function replayHasSameEngineVersion(replay)
     323{
     324    return replay.attribs.engine_version && replay.attribs.engine_version == g_EngineInfo.engine_version;
     325}
     326
     327/**
     328 * Returns a description of the player assignments.
     329 * Including civs, teams, AI settings and player colors.
     330 *
     331 * If the spoiler-checkbox is checked, it also shows defeated players.
     332 *
     333 * @returns {string}
     334 */
     335function getReplayTeamText(replay)
     336{
     337    // Load replay metadata
     338    const metadata = Engine.GetReplayMetadata(replay.directory);
     339    const spoiler = Engine.GetGUIObjectByName("showSpoiler").checked;
     340
     341    var playerDescriptions = {};
     342    var playerIdx = 0;
     343    for (let playerData of replay.attribs.settings.PlayerData)
     344    {
     345        // Get player info
     346        ++playerIdx;
     347        let teamIdx = playerData.Team;
     348        let playerColor = playerData.Color ? playerData.Color : g_DefaultPlayerData[playerIdx].Color;
     349        let showDefeated = spoiler && metadata && metadata.playerStates && metadata.playerStates[playerIdx].state == "defeated";
     350        let isAI = playerData.AI;
     351
     352        // Create human-readable player description
     353        let playerDetails = {
     354            "playerName": '[color="' + rgbToGuiColor(playerColor) + '"]' + escapeText(playerData.Name) + "[/color]",
     355            "civ": translate(g_CivData[playerData.Civ].Name),
     356            "AIname": isAI ? translateAIName(playerData.AI) : "",
     357            "AIdifficulty": isAI ? translateAIDifficulty(playerData.AIDiff) : ""
     358        };
     359
     360        if (!isAI && !showDefeated)
     361            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s)"), playerDetails);
     362        else if (!isAI && showDefeated)
     363            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, defeated)"), playerDetails);
     364        else if (isAI && !showDefeated)
     365            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s)"), playerDetails);
     366        else
     367            playerDetails = sprintf(translateWithContext("replay", "%(playerName)s (%(civ)s, %(AIdifficulty)s %(AIname)s, defeated)"), playerDetails);
     368
     369        // Sort player descriptions by team
     370        if (!playerDescriptions[teamIdx])
     371            playerDescriptions[teamIdx] = [];
     372        playerDescriptions[teamIdx].push(playerDetails);
     373    }
     374
     375    var teams = Object.keys(playerDescriptions);
     376
     377    // If there are no teams, merge all playersDescriptions
     378    if (teams.length == 1)
     379        return playerDescriptions[teams[0]].join("\n") + "\n";
     380
     381    // If there are teams, merge "Team N:" + playerDescriptions
     382    return teams.map(team => {
     383        let teamCaption = (team == -1) ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 });
     384        return '[font="sans-bold-14"]' + teamCaption + "[/font]:\n" + playerDescriptions[team].join("\n");
     385    }).join("\n");
     386}
  • 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/external_libraries/libsdl.h"
     25#include "lib/utf8.h"
     26#include "ps/CLogger.h"
     27#include "ps/Filesystem.h"
     28#include "ps/Game.h"
     29#include "ps/Pyrogenesis.h"
     30#include "ps/Replay.h"
     31#include "scriptinterface/ScriptInterface.h"
     32
     33/**
     34 * Filter too short replays (value in seconds).
     35 */
     36const u8 minimumReplayDuration = 3;
     37
     38/**
     39 * Allows quick debugging of potential platform-dependent file-reading bugs.
     40 */
     41const bool debugParser = false;
     42
     43OsPath VisualReplay::GetDirectoryName()
     44{
     45    return OsPath(psLogDir() / L"sim_log");
     46}
     47
     48JS::Value VisualReplay::GetReplays(ScriptInterface& scriptInterface)
     49{
     50    TIMER(L"GetReplays");
     51    JSContext* cx = scriptInterface.GetContext();
     52    JSAutoRequest rq(cx);
     53
     54    u32 i = 0;
     55    DirectoryNames directories;
     56    JS::RootedObject replays(cx, JS_NewArrayObject(cx, 0));
     57    if (GetDirectoryEntries(GetDirectoryName(), NULL, &directories) == INFO::OK)
     58        for (OsPath& directory : directories)
     59        {
     60            // Load & store replay data
     61            JS::RootedValue replayData(cx, LoadReplayData(scriptInterface, directory));
     62            if (!replayData.isNull())
     63                JS_SetElement(cx, replays, i++, replayData);
     64
     65            // Wait a bit, so that we can quit the process with SIGTERM (Ctrl+c or Alt+F4)
     66case SDL_HOTKEYDOWN:
     67    std::string hotkey = static_cast<const char*>(ev->ev.user.data1);
     68    if (hotkey == "exit")
     69    {
     70        kill_mainloop();
     71        return IN_HANDLED;
     72    }
     73case SDL_QUIT:
     74    kill_mainloop();
     75    break;      }
     76    return JS::ObjectValue(*replays);
     77}
     78
     79/**
     80 * Move the cursor backwards until a newline was read or the beginning of the file was found.
     81 * Either way the cursor points to the beginning of a newline.
     82 *
     83 * @return The current cursor position or -1 on error.
     84 */
     85int goBackToLineBeginning(std::istream* replayStream, const CStr& fileName, const u64 fileSize)
     86{
     87    int currentPos;
     88    char character;
     89    for (int characters = 0; characters < 10000; ++characters)
     90    {
     91        currentPos = (int) replayStream->tellg();
     92
     93        // Stop when reached the beginning of the file
     94        if (currentPos == 0)
     95            return currentPos;
     96
     97        if (!replayStream->good())
     98        {
     99            LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.c_str());
     100            return -1;
     101        }
     102
     103        // Stop when reached newline
     104        replayStream->get(character);
     105        if (character == '\n')
     106            return currentPos;
     107
     108        // Otherwise go back one character.
     109        // Notice: -1 will set the cursor back to the most recently read character.
     110        replayStream->seekg(-2, std::ios_base::cur);
     111    }
     112
     113    LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.c_str());
     114    return -1;
     115}
     116
     117/**
     118 * Compute game duration. Assume constant turn length.
     119 * Find the last line that starts with "turn" by reading the file backwards.
     120 *
     121 * @return seconds or -1 on error
     122 */
     123int getReplayDuration(std::istream *replayStream, const CStr& fileName, const u64 fileSize)
     124{
     125    CStr type;
     126
     127    // Move one character before the file-end
     128    replayStream->seekg(-2, std::ios_base::end);
     129
     130    // Infinite loop protection, should never occur.
     131    // There should be about 5 lines to read until a turn is found.
     132    for (int linesRead = 1; linesRead < 1000; ++linesRead)
     133    {
     134        int currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize);
     135
     136        // Read error or reached file beginning. No turns exist.
     137        if (currentPosition < 1)
     138            return -1;
     139
     140        if (debugParser)
     141            debug_printf("At position %i of %lu after %i lines reads.\n", currentPosition, fileSize, linesRead);
     142
     143        if (!replayStream->good())
     144        {
     145            LOGERROR("Read error when determining replay duration at %i of %lu in %s", currentPosition - 2, fileSize, fileName.c_str());
     146            return -1;
     147        }
     148
     149        // Found last turn, compute duration.
     150        if ((u64) currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn")
     151        {
     152            u32 turn = 0, turnLength = 0;
     153            *replayStream >> turn >> turnLength;
     154            return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0
     155        }
     156
     157        // Otherwise move cursor back to the character before the last newline
     158        replayStream->seekg(currentPosition - 2, std::ios_base::beg);
     159    }
     160
     161    LOGERROR("Infinite loop when determining replay duration for %s", fileName.c_str());
     162    return -1;
     163}
     164
     165JS::Value VisualReplay::LoadReplayData(ScriptInterface& scriptInterface, OsPath& directory)
     166{
     167    TIMER(L"LoadReplayData");
     168
     169    // The directory argument must not be constant, otherwise concatenating will fail
     170    const OsPath replayFile = GetDirectoryName() / directory / L"commands.txt";
     171
     172    //if (debugParser)
     173        debug_printf("Opening %s\n", utf8_from_wstring(replayFile.string()).c_str());
     174
     175    if (!FileExists(replayFile))
     176        return JSVAL_NULL;
     177
     178    // Get file size and modification date
     179    CFileInfo fileInfo;
     180    GetFileInfo(replayFile, &fileInfo);
     181    const i32 fileTime = (i32) fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it (according to CCacheLoader::LooseCachePath)
     182    const u64 fileSize = (u64)fileInfo.Size();
     183
     184    if (fileSize == 0)
     185        return JSVAL_NULL;
     186
     187    // Open file
     188    // TODO: enhancement: support unicode when OsString() is properly implemented for windows
     189    const CStr fileName = utf8_from_wstring(replayFile.string());
     190    std::ifstream* replayStream = new std::ifstream(fileName.c_str());
     191
     192    // File must begin with "start"
     193    CStr type;
     194    if (!(*replayStream >> type).good() || type != "start")
     195    {
     196        LOGERROR("Couldn't open %s. Non-latin characters are not supported yet.", fileName.c_str());
     197        SAFE_DELETE(replayStream);
     198        return JSVAL_NULL;
     199    }
     200
     201    // Parse header / first line
     202    CStr header;
     203    std::getline(*replayStream, header);
     204    JSContext* cx = scriptInterface.GetContext();
     205    JSAutoRequest rq(cx);
     206    JS::RootedValue attribs(cx);
     207    if (!scriptInterface.ParseJSON(header, &attribs))
     208    {
     209        LOGERROR("Couldn't parse replay header of %s", fileName.c_str());
     210        SAFE_DELETE(replayStream);
     211        return JSVAL_NULL;
     212    }
     213
     214    // Ensure "turn" after header
     215    if (!(*replayStream >> type).good() || type != "turn")
     216    {
     217        SAFE_DELETE(replayStream);
     218        return JSVAL_NULL; // there are no turns at all
     219    }
     220
     221    // Don't process files of rejoined clients
     222    u32 turn = 1;
     223    *replayStream >> turn;
     224    if (turn != 0)
     225    {
     226        SAFE_DELETE(replayStream);
     227        return JSVAL_NULL;
     228    }
     229
     230    int duration = getReplayDuration(replayStream, fileName, fileSize);
     231
     232    SAFE_DELETE(replayStream);
     233
     234    // Ensure minimum duration
     235    if (duration < minimumReplayDuration)
     236        return JSVAL_NULL;
     237
     238    // Return the actual data
     239    JS::RootedValue replayData(cx);
     240    scriptInterface.Eval("({})", &replayData);
     241    scriptInterface.SetProperty(replayData, "file", replayFile);
     242    scriptInterface.SetProperty(replayData, "directory", directory);
     243    scriptInterface.SetProperty(replayData, "filemod_timestamp", fileTime);
     244    scriptInterface.SetProperty(replayData, "attribs", attribs);
     245    scriptInterface.SetProperty(replayData, "duration", duration);
     246    return replayData;
     247}
     248
     249bool VisualReplay::DeleteReplay(const CStrW& replayDirectory)
     250{
     251    if (replayDirectory.empty())
     252        return false;
     253
     254    const OsPath directory = GetDirectoryName() / replayDirectory;
     255    return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK;
     256}
     257
     258
     259JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
     260{
     261    // Create empty JS object
     262    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     263    JSAutoRequest rq(cx);
     264    JS::RootedValue attribs(cx);
     265    pCxPrivate->pScriptInterface->Eval("({})", &attribs);
     266
     267    // Return empty object if file doesn't exist
     268    const OsPath replayFile = GetDirectoryName() / directoryName / L"commands.txt";
     269    if (!FileExists(replayFile))
     270        return attribs;
     271
     272    // Open file
     273    std::istream* replayStream = new std::ifstream(utf8_from_wstring(replayFile.string()).c_str());
     274    CStr type, line;
     275    ENSURE((*replayStream >> type).good() && type == "start");
     276
     277    // Read and return first line
     278    std::getline(*replayStream, line);
     279    pCxPrivate->pScriptInterface->ParseJSON(line, &attribs);
     280    SAFE_DELETE(replayStream);;
     281    return attribs;
     282}
     283
     284// TODO: enhancement: how to save the data if the process is killed? (case SDL_QUIT in main.cpp)
     285void VisualReplay::SaveReplayMetadata(const CStrW& data)
     286{
     287    // TODO: enhancement: use JS::HandleValue similar to SaveGame
     288    if (!g_Game)
     289        return;
     290
     291    // Get the directory of the currently active replay
     292    const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json";
     293    CreateDirectories(fileName.Parent(), 0700);
     294
     295    std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc);
     296    stream << utf8_from_wstring(data);
     297    stream.close();
     298}
     299
     300JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName)
     301{
     302    const OsPath filePath = GetDirectoryName() / directoryName / L"metadata.json";
     303
     304    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     305    JSAutoRequest rq(cx);
     306    JS::RootedValue metadata(cx);
     307
     308    if (!FileExists(filePath))
     309        return JSVAL_NULL;
     310
     311    std::ifstream* stream = new std::ifstream(OsString(filePath).c_str());
     312    ENSURE(stream->good());
     313    CStr line;
     314    std::getline(*stream, line);
     315    stream->close();
     316    delete stream;
     317    pCxPrivate->pScriptInterface->ParseJSON(line, &metadata);
     318
     319    return metadata;
     320}
  • 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