Ticket #3258: replay_menu_v8.1_16876.patch

File replay_menu_v8.1_16876.patch, 90.0 KB (added by elexis, 9 years ago)

Some cleanup & optimizations.

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

     
    11var g_AIs; // [ {"id": ..., "data": {"name": ..., "description": ..., ...} }, ... ]
     2var g_AIDiffs; // translated AI difficulties
    23var g_PlayerSlot;
    34
    45function init(settings)
    56{
    67    g_PlayerSlot = settings.playerSlot;
    78
    8     translateObjectKeys(settings.ais, ["name", "description"]);
    99    g_AIs = [
    1010        {id: "", data: {name: translateWithContext("ai", "None"), description: translate("AI will be disabled for this player.")}}
    1111    ].concat(settings.ais);
    1212
    1313    var aiSelection = Engine.GetGUIObjectByName("aiSelection");
     
    2222            break;
    2323        }
    2424    }
    2525    aiSelection.selected = selected;
    2626
     27    g_AIDiffs = initAIDifficulties(); // translated AI difficulties
    2728    var aiDiff = Engine.GetGUIObjectByName("aiDifficulty");
    28     // Translation: AI difficulty level.
    29     aiDiff.list = [translateWithContext("aiDiff", "Sandbox"), translateWithContext("aiDiff", "Very Easy"), translateWithContext("aiDiff", "Easy"), translateWithContext("aiDiff", "Medium"), translateWithContext("aiDiff", "Hard"), translateWithContext("aiDiff", "Very Hard")];
     29    aiDiff.list = g_AIDiffs.titles;
    3030    aiDiff.selected = settings.difficulty;
    3131}
    3232
    3333function selectAI(idx)
    3434{
  • binaries/data/mods/public/gui/aiconfig/aiconfig.xml

     
    11<?xml version="1.0" encoding="utf-8"?>
    22
    33<objects>
    44
    55    <script file="gui/aiconfig/aiconfig.js"/>
     6    <script file="gui/common/functions_utility.js"/>
    67
    78    <!-- Add a translucent black background to fade out the menu page -->
    89    <object type="image" sprite="ModernFade"/>
    910
    1011    <object type="image" style="ModernDialog" size="50%-230 50%-130 50%+230 50%+130">
  • binaries/data/mods/public/gui/common/functions_utility.js

     
    101101// ====================================================================
    102102
    103103// Load map size data
    104104function initMapSizes()
    105105{
    106     var sizes = {
     106    let sizes = {
    107107        "shortNames":[],
    108108        "names":[],
    109109        "tiles": [],
    110110        "default": 0
    111111    };
    112112
    113     var data = Engine.ReadJSONFile("simulation/data/map_sizes.json");
     113    let data = Engine.ReadJSONFile("simulation/data/map_sizes.json");
    114114    if (!data || !data.Sizes)
    115115    {
    116116        error("Failed to parse map sizes in map_sizes.json (check for valid JSON data)");
    117117        return sizes;
    118118    }
    119119
    120120    translateObjectKeys(data, ["Name", "LongName"]);
    121     for (var i = 0; i < data.Sizes.length; ++i)
     121    for (let i = 0; i < data.Sizes.length; ++i)
    122122    {
    123123        sizes.shortNames.push(data.Sizes[i].Name);
    124124        sizes.names.push(data.Sizes[i].LongName);
    125125        sizes.tiles.push(data.Sizes[i].Tiles);
    126126
    127127        if (data.Sizes[i].Default)
    128             sizes["default"] = i;
     128            sizes.default = i;
    129129    }
    130130
    131131    return sizes;
    132132}
    133133
     134// Returns translated map size for a given a numeric size. Requires g_MapSizes to be present.
     135function translateMapSize(mapSize)
     136{
     137    let index = g_MapSizes.tiles.indexOf(parseInt(mapSize))
     138    if (index > -1)
     139        return g_MapSizes.shortNames[index];
     140    else
     141        return translateWithContext("map size", "Default");
     142}
     143
     144// ====================================================================
     145
     146// Load map size data
     147function initPopulationCapacity()
     148{
     149    let population = {
     150        "population":[],
     151        "titles": [],
     152        "default": 0
     153    };
     154
     155    let data = Engine.ReadJSONFile("simulation/data/population_capacity.json");
     156    if (!data || !data.PopulationCapacity)
     157    {
     158        error("Failed to parse map sizes in population_capacity.json (check for valid JSON data)");
     159        return population;
     160    }
     161
     162    translateObjectKeys(data, ["Title"]);
     163    for (let i = 0; i < data.PopulationCapacity.length; ++i)
     164    {
     165        population.population.push(data.PopulationCapacity[i].Population);
     166        population.titles.push(data.PopulationCapacity[i].Title);
     167
     168        if (data.PopulationCapacity[i].Default)
     169            population.default = i;
     170    }
     171
     172    return population;
     173}
     174
     175// Returns the translation for a given numeric value. Requires g_PopCaps to be present.
     176function translatePopulationCapacity(popCap)
     177{
     178    let index = g_PopCaps.population.indexOf(parseInt(popCap))
     179    if (index > -1)
     180        return g_PopCaps.titles[index];
     181    else
     182        return translateWithContext("population capacity", "Unknown");
     183}
     184
     185// ====================================================================
     186
     187// Load map type data
     188function initMapTypes()
     189{
     190    let types = {
     191        "names":[],
     192        "titles":[],
     193        "default": 0
     194    };
     195
     196    let data = Engine.ReadJSONFile("simulation/data/map_types.json");
     197    if (!data || !data.Types)
     198    {
     199        error("Failed to parse map types in map_types.json (check for valid JSON data)");
     200        return types;
     201    }
     202
     203    translateObjectKeys(data, ["Title"]);
     204    for (let i = 0; i < data.Types.length; ++i)
     205    {
     206        types.names.push(data.Types[i].Name);
     207        types.titles.push(data.Types[i].Title);
     208
     209        if (data.Types[i].Default)
     210            types.default = i;
     211    }
     212
     213    return types;
     214}
     215
     216// Returns the translation of the given map type. Requires g_MapTypes to be present.
     217function translateMapType(mapType)
     218{
     219    let index = g_MapTypes.names.indexOf(mapType)
     220    if (index > -1)
     221        return g_MapTypes.titles[index];
     222    else
     223        return translateWithContext("map type", "Unknown");
     224}
     225
     226// ====================================================================
     227
     228function initAIDescription()
     229{
     230    let AIs = Engine.GetAIs();
     231    translateObjectKeys(AIs, ["name", "description"]);
     232    // Sort AIs by displayed name
     233    AIs.sort((a, b) => a.data.name < b.data.name ? -1 : b.data.name < a.data.name ? +1 : 0);
     234    return AIs;
     235}
     236
     237// Returns the translation of the given AI name (like "petra"). Requires g_AIs to be present.
     238function translateAIName(aiName)
     239{
     240    let AI_description = g_AIs.filter(ai => ai.id == aiName);
     241    if (AI_description.length > 0)
     242        return translate(AI_description[0].data.name);
     243    else
     244        return translateWithContext("AI name", "Unknown");
     245}
     246
     247// ====================================================================
     248
     249function initAIDifficulties()
     250{
     251    let difficulties = {
     252        "names":[],
     253        "titles":[],
     254        "default": 0
     255    };
     256
     257    let data = Engine.ReadJSONFile("simulation/ai/difficulties.json");
     258    if (!data || !data.Difficulties)
     259    {
     260        error("Failed to parse AI difficulties (check difficulties.json for valid data)");
     261        return ["Default"];
     262    }
     263
     264    translateObjectKeys(data, ["Title"]);
     265    for (let i = 0; i < data.Difficulties.length; ++i)
     266    {
     267        difficulties.names.push(data.Difficulties[i].Name);
     268        difficulties.titles.push(data.Difficulties[i].Title);
     269
     270        if (data.Difficulties[i].Default)
     271            difficulties.default = i;
     272    }
     273    return difficulties;
     274}
     275
     276// Returns the translation of the given AI difficulty. Requires g_AIDiffs to be present.
     277function translateAIDifficulty(index)
     278{
     279    if (0 <= index && index < g_AIDiffs.titles.length)
     280        return g_AIDiffs.titles[index];
     281    else
     282        return translateWithContext("aiDiff", "Unknown");
     283}
     284
     285// ====================================================================
     286
     287// Returns the translation of the given map type. Requires g_VictoryConditions to be present.
     288function translateGameType(gameType)
     289{
     290    if (g_VictoryConditions.hasOwnProperty(gameType))
     291        return g_VictoryConditions[gameType].name;
     292    else
     293        return translateWithContext("game type", "Unknown");
     294}
     295
    134296// ====================================================================
    135297
    136298// Load game speed data
    137299function initGameSpeeds()
    138300{
  • binaries/data/mods/public/gui/common/functions_utility_loadsave.js

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

     
    22// Constants
    33const DEFAULT_NETWORKED_MAP = "Acropolis 01";
    44const DEFAULT_OFFLINE_MAP = "Acropolis 01";
    55
    66const VICTORY_DEFAULTIDX = 1;
    7 
    87// TODO: Move these somewhere like simulation\data\game_types.json, Atlas needs them too
    9 // Translation: Type of victory condition.
    10 const POPULATION_CAP = ["50", "100", "150", "200", "250", "300", translate("Unlimited")];
    11 const POPULATION_CAP_DATA = [50, 100, 150, 200, 250, 300, 10000];
    12 const POPULATION_CAP_DEFAULTIDX = 5;
    138// Translation: Amount of starting resources.
    149const STARTING_RESOURCES = [translateWithContext("startingResources", "Very Low"), translateWithContext("startingResources", "Low"), translateWithContext("startingResources", "Medium"), translateWithContext("startingResources", "High"), translateWithContext("startingResources", "Very High"), translateWithContext("startingResources", "Deathmatch")];
    1510const STARTING_RESOURCES_DATA = [100, 300, 500, 1000, 3000, 50000];
    1611const STARTING_RESOURCES_DEFAULTIDX = 1;
    1712// Translation: Ceasefire.
     
    5550var g_DefaultPlayerData = [];
    5651var g_GameAttributes = {
    5752    settings: {}
    5853};
    5954
    60 var g_GameSpeeds = {};
    61 var g_MapSizes = {};
    62 
     55var g_GameSpeeds = [];
     56var g_MapSizes = [];
     57var g_MapTypes = [];
    6358var g_AIs = [];
    64 
     59var g_PopCaps = [];
    6560var g_ChatMessages = [];
    6661
    6762// Data caches
    6863var g_MapData = {};
    6964var g_CivData = {};
     
    116111
    117112// Called after the map data is loaded and cached
    118113function initMain()
    119114{
    120115    // Load AI list
    121     g_AIs = Engine.GetAIs();
    122 
    123     // Sort AIs by displayed name
    124     g_AIs.sort(function (a, b) {
    125         return a.data.name < b.data.name ? -1 : b.data.name < a.data.name ? +1 : 0;
    126     });
     116    g_AIs = initAIDescription();
    127117
    128118    // Get default player data - remove gaia
    129119    g_DefaultPlayerData = initPlayerDefaults();
    130120    g_DefaultPlayerData.shift();
    131121    for (var i = 0; i < g_DefaultPlayerData.length; ++i)
    132122        g_DefaultPlayerData[i].Civ = "random";
    133123
    134124    g_GameSpeeds = initGameSpeeds();
    135125    g_MapSizes = initMapSizes();
     126    g_MapTypes = initMapTypes();
     127    g_PopCaps = initPopulationCapacity();
    136128
    137129    // Init civs
    138130    initCivNameList();
    139131
    140132    // Init map types
    141133    var mapTypes = Engine.GetGUIObjectByName("mapTypeSelection");
    142     mapTypes.list = [translateWithContext("map", "Skirmish"), translateWithContext("map", "Random"), translate("Scenario")];
    143     mapTypes.list_data = ["skirmish","random","scenario"];
     134    mapTypes.list = g_MapTypes.titles;
     135    mapTypes.list_data = g_MapTypes.names;
    144136
    145137    // Setup map filters - will appear in order they are added
    146138    addFilter("default", translate("Default"), function(settings) { return settings && (settings.Keywords === undefined || !keywordTestOR(settings.Keywords, ["naval", "demo", "hidden"])); });
    147139    addFilter("naval", translate("Naval Maps"), function(settings) { return settings && settings.Keywords !== undefined && keywordTestAND(settings.Keywords, ["naval"]); });
    148140    addFilter("demo", translate("Demo Maps"), function(settings) { return settings && settings.Keywords !== undefined && keywordTestAND(settings.Keywords, ["demo"]); });
     
    186178            updateGameAttributes();
    187179        }
    188180        gameSpeed.selected = g_GameSpeeds["default"];
    189181
    190182        var populationCaps = Engine.GetGUIObjectByName("populationCap");
    191         populationCaps.list = POPULATION_CAP;
    192         populationCaps.list_data = POPULATION_CAP_DATA;
    193         populationCaps.selected = POPULATION_CAP_DEFAULTIDX;
     183        populationCaps.list = g_PopCaps.titles;
     184        populationCaps.list_data = g_PopCaps.population;
     185        populationCaps.selected = g_PopCaps.default;
    194186        populationCaps.onSelectionChange = function() {
    195187            if (this.selected != -1)
    196                 g_GameAttributes.settings.PopulationCap = POPULATION_CAP_DATA[this.selected];
     188                g_GameAttributes.settings.PopulationCap = g_PopCaps.population[this.selected];
    197189
    198190            updateGameAttributes();
    199191        }
    200192
    201193        var startingResourcesL = Engine.GetGUIObjectByName("startingResources");
     
    487479        }
    488480        Engine.SwitchGuiPage("page_loading.xml", {
    489481            "attribs": g_GameAttributes,
    490482            "isNetworked" : g_IsNetworked,
    491483            "playerAssignments": g_PlayerAssignments,
    492             "isController": g_IsController
     484            "isController": g_IsController,
     485            "g_IsReplay" : false
    493486        });
    494487        break;
    495488
    496489    case "chat":
    497490        addChatMessage({ "type": "message", "guid": message.guid, "text": message.text });
     
    12221215    }
    12231216    else
    12241217        enableRatingText.caption = "Unknown";
    12251218    gameSpeedText.caption = g_GameSpeeds.names[speedIdx];
    12261219    gameSpeedBox.selected = speedIdx;
    1227     populationCap.selected = (mapSettings.PopulationCap !== undefined && POPULATION_CAP_DATA.indexOf(mapSettings.PopulationCap) != -1 ? POPULATION_CAP_DATA.indexOf(mapSettings.PopulationCap) : POPULATION_CAP_DEFAULTIDX);
    1228     populationCapText.caption = POPULATION_CAP[populationCap.selected];
     1220    populationCap.selected = (mapSettings.PopulationCap !== undefined && g_PopCaps.population.indexOf(mapSettings.PopulationCap) != -1 ? g_PopCaps.population.indexOf(mapSettings.PopulationCap) : g_PopCaps.default);
     1221    populationCapText.caption = g_PopCaps.titles[populationCap.selected];
    12291222    startingResources.selected = (mapSettings.StartingResources !== undefined && STARTING_RESOURCES_DATA.indexOf(mapSettings.StartingResources) != -1 ? STARTING_RESOURCES_DATA.indexOf(mapSettings.StartingResources) : STARTING_RESOURCES_DEFAULTIDX);
    12301223    startingResourcesText.caption = STARTING_RESOURCES[startingResources.selected];
    12311224    ceasefire.selected = (mapSettings.Ceasefire !== undefined && CEASEFIRE_DATA.indexOf(mapSettings.Ceasefire) != -1 ? CEASEFIRE_DATA.indexOf(mapSettings.Ceasefire) : CEASEFIRE_DEFAULTIDX);
    12321225    ceasefireText.caption = CEASEFIRE[ceasefire.selected];
    12331226
  • binaries/data/mods/public/gui/gamesetup/gamesetup.xml

     
    11<?xml version="1.0" encoding="utf-8"?>
    22
    33<objects>
    44
     5    <script file="gui/aiconfig/aiconfig.js"/>
    56    <script file="gui/common/network.js"/>
    67    <script file="gui/common/functions_civinfo.js"/>
    78    <script file="gui/common/functions_global_object.js"/>
    89    <script file="gui/common/functions_utility.js"/>
    910    <script file="gui/gamesetup/gamesetup.js"/>
  • binaries/data/mods/public/gui/lobby/lobby.js

     
    77var g_PlayerListOrder = 1;
    88var g_specialKey = Math.random();
    99// This object looks like {"name":[numMessagesSinceReset, lastReset, timeBlocked]} when in use.
    1010var g_spamMonitor = {};
    1111var g_timestamp = Engine.ConfigDB_GetValue("user", "lobby.chattimestamp") == "true";
    12 var g_mapSizes = {};
    13 const g_mapTypesText = [translateWithContext("map", "Skirmish"), translateWithContext("map", "Random"), translate("Scenario")];
    14 const g_mapTypes = ["skirmish", "random", "scenario"];
     12var g_MapSizes = {};
     13var g_MapTypes = {};
    1514var g_userRating = ""; // Rating of user, defaults to Unrated
    1615var g_modPrefix = "@";
    1716var g_joined = false;
    1817// Block spammers for 30 seconds.
    1918var SPAM_BLOCK_LENGTH = 30;
     
    2625    initMusic();
    2726    global.music.setState(global.music.states.MENU);
    2827
    2928    g_Name = Engine.LobbyGetNick();
    3029
    31     g_mapSizes = initMapSizes();
    32     g_mapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
    33     g_mapSizes.tiles.splice(0, 0, "");
     30    g_MapSizes = initMapSizes();
     31    g_MapSizes.shortNames.splice(0, 0, translateWithContext("map size", "Any"));
     32    g_MapSizes.tiles.splice(0, 0, "");
    3433
    3534    var mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter");
    36     mapSizeFilter.list = g_mapSizes.shortNames;
    37     mapSizeFilter.list_data = g_mapSizes.tiles;
     35    mapSizeFilter.list = g_MapSizes.shortNames;
     36    mapSizeFilter.list_data = g_MapSizes.tiles;
    3837
    3938    var playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter");
    4039    playersNumberFilter.list = [translateWithContext("player number", "Any"),2,3,4,5,6,7,8];
    4140    playersNumberFilter.list_data = ["",2,3,4,5,6,7,8];
    4241
     42    g_MapTypes = initMapTypes();
     43    g_MapTypes.titles.splice(0, 0, translateWithContext("map type", "Any"));
     44    g_MapTypes.names.splice(0, 0, "");
     45
    4346    var mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter");
    44     mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_mapTypesText);
    45     mapTypeFilter.list_data = [""].concat(g_mapTypes);
     47    mapTypeFilter.list = g_MapTypes.titles;
     48    mapTypeFilter.list_data = g_MapTypes.names;
    4649
    4750    Engine.LobbySetPlayerPresence("available");
    4851    Engine.SendGetGameList();
    4952    Engine.SendGetBoardList();
    5053    Engine.SendGetRatingList();
     
    468471            else
    469472                name = '[color="255 0 0"]' + name + '[/color]';
    470473            list_name.push(name);
    471474            list_ip.push(g.ip);
    472475            list_mapName.push(translate(g.niceMapName));
    473             list_mapSize.push(translatedMapSize(g.mapSize));
    474             let idx = g_mapTypes.indexOf(g.mapType);
    475             list_mapType.push(idx != -1 ? g_mapTypesText[idx] : "");
     476            list_mapSize.push(translateMapSize(g.mapSize));
     477            list_mapType.push(translateMapType(g.mapType));
    476478            list_nPlayers.push(g.nbp + "/" +g.tnbp);
    477479            list.push(name);
    478480            list_data.push(c);
    479481        }
    480482        c++;
     
    546548    // Push this player's name and status onto the list
    547549    return [formattedName, formattedStatus, formattedRating];
    548550}
    549551
    550552/**
    551  * Given a map size, returns that map size translated into the current
    552  * language.
    553  */
    554 function translatedMapSize(mapSize)
    555 {
    556     if (+mapSize !== +mapSize) // NaN
    557         return translate(mapSize);
    558     else
    559         return g_mapSizes.shortNames[g_mapSizes.tiles.indexOf(+mapSize)];
    560 }
    561 
    562 /**
    563553 * Populate the game info area with information on the current game selection.
    564554 */
    565555function updateGameSelection()
    566556{
    567557    var selected = Engine.GetGUIObjectByName("gamesBox").selected;
     
    595585
    596586    // Display the map name, number of players, the names of the players, the map size and the map type.
    597587    Engine.GetGUIObjectByName("sgMapName").caption = translate(g_GameList[g].niceMapName);
    598588    Engine.GetGUIObjectByName("sgNbPlayers").caption = g_GameList[g].nbp + "/" + g_GameList[g].tnbp;
    599589    Engine.GetGUIObjectByName("sgPlayersNames").caption = g_GameList[g].players;
    600     Engine.GetGUIObjectByName("sgMapSize").caption = translatedMapSize(g_GameList[g].mapSize);
    601     let idx = g_mapTypes.indexOf(g_GameList[g].mapType);
    602     Engine.GetGUIObjectByName("sgMapType").caption = idx != -1 ? g_mapTypesText[idx] : "";
     590    Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(g_GameList[g].mapSize);
     591    Engine.GetGUIObjectByName("sgMapType").caption = translateMapType(g_GameList[g].mapType);
    603592
    604593    // Display map description if it exists, otherwise display a placeholder.
    605594    if (mapData && mapData.settings.Description)
    606595        var mapDescription = translate(mapData.settings.Description);
    607596    else
  • binaries/data/mods/public/gui/page_replaymenu.xml

     
     1<?xml version="1.0" encoding="utf-8"?>
     2<page>
     3    <include>common/modern/setup.xml</include>
     4    <include>common/modern/styles.xml</include>
     5    <include>common/modern/sprites.xml</include>
     6
     7    <include>common/setup.xml</include>
     8    <include>common/sprite1.xml</include>
     9    <include>common/styles.xml</include>
     10    <include>common/common_sprites.xml</include>
     11    <include>common/common_styles.xml</include>
     12
     13    <include>replaymenu/styles.xml</include>
     14    <include>replaymenu/replaymenu.xml</include>
     15</page>
  • binaries/data/mods/public/gui/pregame/mainmenu.xml

     
    344344                        Engine.PushGuiPage("page_locale.xml");
    345345                        ]]>
    346346                    </action>
    347347                </object>
    348348
     349                <object name="submenuReplayButton"
     350                    type="button"
     351                    style="StoneButtonFancy"
     352                    size="0 64 100% 92"
     353                    tooltip_style="pgToolTip"
     354                >
     355                    <translatableAttribute id="caption">Replay</translatableAttribute>
     356                    <translatableAttribute id="tooltip">Shows a replay of a past game.</translatableAttribute>
     357                    <action on="Press">
     358                        closeMenu();
     359                        Engine.SwitchGuiPage("page_replaymenu.xml");
     360                    </action>
     361                </object>
     362
    349363                <object name="submenuEditorButton"
    350364                    style="StoneButtonFancy"
    351365                    type="button"
    352                     size="0 64 100% 92"
     366                    size="0 96 100% 124"
    353367                    tooltip_style="pgToolTip"
    354368                >
    355369                    <translatableAttribute id="caption">Scenario Editor</translatableAttribute>
    356370                    <translatableAttribute id="tooltip">Open the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument &quot;-editor&quot;.</translatableAttribute>
    357371                    <action on="Press">
     
    360374                </object>
    361375
    362376                <object name="submenuWelcomeScreenButton"
    363377                    style="StoneButtonFancy"
    364378                    type="button"
    365                     size="0 96 100% 124"
     379                    size="0 128 100% 156"
    366380                    tooltip_style="pgToolTip"
    367381                >
    368382                    <translatableAttribute id="caption">Welcome Screen</translatableAttribute>
    369383                    <translatableAttribute id="tooltip">Show the Welcome Screen. Useful if you hid it by mistake.</translatableAttribute>
    370384                    <action on="Press">
     
    375389                    </action>
    376390                </object>
    377391                <object name="submenuModSelection"
    378392                    style="StoneButtonFancy"
    379393                    type="button"
    380                     size="0 128 100% 156"
     394                    size="0 156 100% 188"
    381395                    tooltip_style="pgToolTip"
    382396                >
    383397                    <translatableAttribute id="caption">Mod Selection</translatableAttribute>
    384398                    <translatableAttribute id="tooltip">Select mods to use.</translatableAttribute>
    385399                    <action on="Press">
     
    482496                >
    483497                    <translatableAttribute id="caption">Tools &amp; Options</translatableAttribute>
    484498                    <translatableAttribute id="tooltip">Game options and scenario design tools.</translatableAttribute>
    485499                    <action on="Press">
    486500                        closeMenu();
    487                         openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 5);
     501                        openMenu("submenuToolsAndOptions", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 6);
    488502                    </action>
    489503                </object>
    490504
    491505                <!-- EXIT BUTTON -->
    492506                <object name="menuExitButton"
  • binaries/data/mods/public/gui/replaymenu/replaymenu.js

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

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

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

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

     
    2828const INCOME_COLOR = '[color="201 255 200"]';
    2929const OUTCOME_COLOR = '[color="255 213 213"]';
    3030
    3131const DEFAULT_DECIMAL = "0.00";
    3232const INFINITE_SYMBOL = "\u221E";
     33var g_IsReplay = false;
    3334// Load data
    3435var g_CivData = loadCivData();
    3536var g_Teams = [];
    3637// TODO set g_MaxPlayers as playerCounters.length
    3738var g_MaxPlayers = 0;
     
    132133
    133134function init(data)
    134135{
    135136    updateObjectPlayerPosition();
    136137    g_GameData = data;
     138    g_IsReplay = data.isReplay;
    137139
    138140    // Map
    139141    var mapDisplayType = translate("Scenario");
    140142
    141143    Engine.GetGUIObjectByName("timeElapsed").caption = sprintf(translate("Game time elapsed: %(time)s"), { time: timeToString(data.timeElapsed) });
  • binaries/data/mods/public/gui/summary/summary.xml

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

     
    463463                }
    464464            },
    465465            {
    466466                "extractor": "json",
    467467                "filemasks": [
     468                    "simulation/data/map_types.json"
     469                ],
     470                "options": {
     471                    "keywords": [
     472                        "Title"
     473                    ]
     474                }
     475            },
     476            {
     477                "extractor": "json",
     478                "filemasks": [
    468479                    "simulation/data/map_sizes.json"
    469480                ],
    470481                "options": {
    471482                    "keywords": [
    472483                        "Name",
     
    475486                }
    476487            },
    477488            {
    478489                "extractor": "json",
    479490                "filemasks": [
     491                    "simulation/data/population_capacity.json"
     492                ],
     493                "options": {
     494                    "keywords": [
     495                        "Title"
     496                    ]
     497                }
     498            },
     499            {
     500                "extractor": "json",
     501                "filemasks": [
    480502                    "simulation/ai/**.json"
    481503                ],
    482504                "options": {
    483505                    "keywords": [
    484506                        "name",
    485507                        "description"
    486508                    ]
    487509                }
    488             }
     510            },
     511            {
     512                "extractor": "json",
     513                "filemasks": [
     514                    "simulation/ai/difficulties.json"
     515                ],
     516                "options": {
     517                    "keywords": [
     518                        "Title"
     519                    ]
     520                }
     521            },
    489522        ]
    490523    },
    491524    {
    492525        "output": "public-maps.pot",
    493526        "inputRoot": "..",
  • binaries/data/mods/public/simulation/ai/difficulties.json

     
     1{
     2    "Difficulties":
     3    [
     4        {
     5            "Name": "sandbox",
     6            "Title": "Sandbox"
     7        },
     8        {
     9            "Name": "very easy",
     10            "Title": "Very Easy"
     11        },
     12        {
     13            "Name": "easy",
     14            "Title": "Easy"
     15        },
     16        {
     17            "Name": "medium",
     18            "Title": "Medium",
     19            "Default": true
     20        },
     21        {
     22            "Name": "hard",
     23            "Title": "Hard"
     24        },
     25        {
     26            "Name": "very hard",
     27            "Title": "Very Hard"
     28        }
     29    ]
     30}
     31 No newline at end of file
  • binaries/data/mods/public/simulation/data/game_speeds.json

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

     
     1{
     2    "Types":
     3    [
     4        {
     5            "Name": "skirmish",
     6            "Title": "Skirmish",
     7            "Default": true
     8        },
     9        {
     10            "Name": "random",
     11            "Title": "Random"
     12        },
     13        {
     14            "Name": "scenario",
     15            "Title": "Scenario"
     16        }
     17    ]
     18}
     19 No newline at end of file
  • binaries/data/mods/public/simulation/data/population_capacity.json

     
     1{
     2    "PopulationCapacity":
     3    [
     4        {
     5            "Population": 50,
     6            "Title": "50"
     7        },
     8        {
     9            "Population": 100,
     10            "Title": "100"
     11        },
     12        {
     13            "Population": 150,
     14            "Title": "150"
     15        },
     16        {
     17            "Population": 200,
     18            "Title": "200"
     19        },
     20        {
     21            "Population": 250,
     22            "Title": "250"
     23        },
     24        {
     25            "Population": 300,
     26            "Title": "300",
     27            "Default": true
     28        },
     29        {
     30            "Population": 10000,
     31            "Title": "Unlimited"
     32        }
     33    ]
     34}
  • source/gui/scripting/ScriptFunctions.cpp

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

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

     
    533533    ENSURE(m_Simulation2.ComputeStateHash(hash, quickHash));
    534534    hash = Hexify(hash);
    535535
    536536    if (hash != expectedHash)
    537537        DisplayOOSError(turn, hash, expectedHash, true);
     538
     539    if (turn == m_FinalReplayTurn)
     540        g_GUI->SendEventToAll("ReplayFinished");
    538541}
    539542
    540543void CNetReplayTurnManager::DoTurn(u32 turn)
    541544{
    542545    // Save turn length
  • source/ps/EngineVersion.h

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

     
    134134        if (type == "turn")
    135135        {
    136136            u32 turn = 0;
    137137            u32 turnLength = 0;
    138138            *m_ReplayStream >> turn >> turnLength;
     139            if (turn == 0 && turn != currentTurn)
     140                LOGERROR("Looks like you tried to replay a commands.txt file of a rejoined client, which is not supported yet.\n");
    139141            ENSURE(turn == currentTurn);
    140142            replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength);
    141143        }
    142144        else if (type == "cmd")
    143145        {
     
    154156            std::string replayHash;
    155157            *m_ReplayStream >> replayHash;
    156158            replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick);
    157159        }
    158160        else if (type == "end")
    159         {
    160161            currentTurn++;
    161         }
    162162        else
    163         {
    164163            CancelLoad(L"Failed to load replay data (unrecognized content)");
    165         }
    166164    }
     165    m_ReplayStream->close();
    167166    m_FinalReplayTurn = currentTurn;
    168167    replayTurnMgr->StoreFinalReplayTurn(currentTurn);
    169168    return 0;
    170169}
    171170
     
    389388        {
    390389            {
    391390                PROFILE3("gui sim update");
    392391                g_GUI->SendEventToAll("SimulationUpdate");
    393392            }
    394             if (m_IsReplay && m_TurnManager->GetCurrentTurn() == m_FinalReplayTurn - 1)
    395                 g_GUI->SendEventToAll("ReplayFinished");
    396393
    397394            GetView()->GetLOSTexture().MakeDirty();
    398395        }
    399        
     396
    400397        if (CRenderer::IsInitialised())
    401398            g_Renderer.GetTimeManager().Update(deltaSimTime);
    402399    }
    403400
    404401    if (doInterpolate)
  • source/ps/Game.h

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

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

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

     
    2222#include "graphics/TerrainTextureManager.h"
    2323#include "lib/timer.h"
    2424#include "lib/file/file_system.h"
    2525#include "lib/res/h_mgr.h"
    2626#include "lib/tex/tex.h"
     27#include "ps/EngineVersion.h"
    2728#include "ps/Game.h"
    2829#include "ps/Loader.h"
    2930#include "ps/Profile.h"
     31#include "ps/Mod.h"
    3032#include "ps/ProfileViewer.h"
    3133#include "scriptinterface/ScriptInterface.h"
    3234#include "scriptinterface/ScriptStats.h"
    3335#include "simulation2/Simulation2.h"
    3436#include "simulation2/helpers/SimulationCommand.h"
     
    5052        str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i];
    5153    return str.str();
    5254}
    5355
    5456CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) :
    55     m_ScriptInterface(scriptInterface)
     57    m_ScriptInterface(scriptInterface), m_Stream(NULL)
     58{
     59}
     60
     61CReplayLogger::~CReplayLogger()
     62{
     63    delete m_Stream;
     64}
     65
     66void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
    5667{
    5768    // Construct the directory name based on the PID, to be relatively unique.
    5869    // Append "-1", "-2" etc if we run multiple matches in a single session,
    5970    // to avoid accidentally overwriting earlier logs.
    6071
     
    6374
    6475    static int run = -1;
    6576    if (++run)
    6677        name << "-" << run;
    6778
    68     OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt";
    69     CreateDirectories(path.Parent(), 0700);
    70     m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
    71 }
    72 
    73 CReplayLogger::~CReplayLogger()
    74 {
    75     delete m_Stream;
    76 }
    77 
    78 void CReplayLogger::StartGame(JS::MutableHandleValue attribs)
    79 {
     79    // Add timestamp and engine version to game attributes
     80    m_ScriptInterface.SetProperty(attribs, "timestamp", std::time(0));
     81    m_ScriptInterface.SetProperty(attribs, "engine_version", utf8_from_wstring(engine_version));
     82    m_ScriptInterface.SetProperty(attribs, "mods", g_modsLoaded);
     83
     84    // Remember the directory, so that we can save additional files to it
     85    m_Directory = psLogDir() / L"sim_log" / name.str();
     86    OsPath filepath = m_Directory / L"commands.txt";
     87
     88    // Open the file when starting the game to prevent empty files
     89    CreateDirectories(m_Directory, 0700);
     90    m_Stream = new std::ofstream(OsString(filepath).c_str(), std::ofstream::out | std::ofstream::trunc);
    8091    *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n";
    8192}
    8293
    8394void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector<SimulationCommand>& commands)
    8495{
     
    100111        *m_Stream << "hash-quick " << Hexify(hash) << "\n";
    101112    else
    102113        *m_Stream << "hash " << Hexify(hash) << "\n";
    103114}
    104115
     116const OsPath CReplayLogger::GetReplayDirectory()
     117{
     118    return m_Directory;
     119}
    105120////////////////////////////////////////////////////////////////
    106121
    107122CReplayPlayer::CReplayPlayer() :
    108123    m_Stream(NULL)
    109124{
     
    157172    JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext();
    158173    JSAutoRequest rq(cx);
    159174    std::string type;
    160175    while ((*m_Stream >> type).good())
    161176    {
    162 //      if (turn >= 1400) break;
    163 
    164177        if (type == "start")
    165178        {
    166179            std::string line;
    167180            std::getline(*m_Stream, line);
    168181            JS::RootedValue attribs(cx);
     
    198211            std::string replayHash;
    199212            *m_Stream >> replayHash;
    200213
    201214            bool quick = (type == "hash-quick");
    202215
    203 //          if (turn >= 1300)
    204 //          if (turn >= 0)
    205216            if (turn % 100 == 0)
    206217            {
    207218                std::string hash;
    208219                bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, quick);
    209220                ENSURE(ok);
     
    224235
    225236                g_Game->GetSimulation2()->Update(turnLength, commands);
    226237                commands.clear();
    227238            }
    228239
    229 //          std::string hash;
    230 //          bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, true);
    231 //          ENSURE(ok);
    232 //          debug_printf("%s\n", Hexify(hash).c_str());
    233 
    234240            g_Profiler.Frame();
    235241
    236 //          if (turn % 1000 == 0)
    237 //              JS_GC(g_Game->GetSimulation2()->GetScriptInterface().GetContext());
    238 
    239242            if (turn % 20 == 0)
    240243                g_ProfileViewer.SaveToFile();
    241244        }
    242245        else
    243246        {
    244247            debug_printf("Unrecognised replay token %s\n", type.c_str());
    245248        }
    246249    }
    247250    }
    248 
     251    m_Stream->close();
    249252    g_Profiler2.SaveToFile();
    250253
    251254    std::string hash;
    252255    bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false);
    253256    ENSURE(ok);
  • source/ps/Replay.h

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

     
    1 /* Copyright (C) 2014 Wildfire Games.
     1/* Copyright (C) 2015 Wildfire Games.
    22 * This file is part of 0 A.D.
    33 *
    44 * 0 A.D. is free software: you can redistribute it and/or modify
    55 * it under the terms of the GNU General Public License as published by
    66 * the Free Software Foundation, either version 2 of the License, or
     
    2424#include "lib/allocators/shared_ptr.h"
    2525#include "lib/file/archive/archive_zip.h"
    2626#include "i18n/L10n.h"
    2727#include "lib/utf8.h"
    2828#include "ps/CLogger.h"
     29#include "ps/EngineVersion.h"
    2930#include "ps/Filesystem.h"
    3031#include "ps/Game.h"
    3132#include "ps/Mod.h"
    3233#include "scriptinterface/ScriptInterface.h"
    3334#include "simulation2/Simulation2.h"
     
    8485    JS::RootedValue metadata(cx);
    8586    JS::RootedValue initAttributes(cx, simulation.GetInitAttributes());
    8687    simulation.GetScriptInterface().Eval("({})", &metadata);
    8788    simulation.GetScriptInterface().SetProperty(metadata, "version_major", SAVED_GAME_VERSION_MAJOR);
    8889    simulation.GetScriptInterface().SetProperty(metadata, "version_minor", SAVED_GAME_VERSION_MINOR);
     90    simulation.GetScriptInterface().SetProperty(metadata, "engine_version", utf8_from_wstring(engine_version));
    8991    simulation.GetScriptInterface().SetProperty(metadata, "mods", g_modsLoaded);
    9092    simulation.GetScriptInterface().SetProperty(metadata, "time", (double)now);
    9193    simulation.GetScriptInterface().SetProperty(metadata, "player", playerID);
    9294    simulation.GetScriptInterface().SetProperty(metadata, "initAttributes", initAttributes);
    9395
     
    298300   
    299301    JS::RootedValue metainfo(cx);
    300302    scriptInterface.Eval("({})", &metainfo);
    301303    scriptInterface.SetProperty(metainfo, "version_major", SAVED_GAME_VERSION_MAJOR);
    302304    scriptInterface.SetProperty(metainfo, "version_minor", SAVED_GAME_VERSION_MINOR);
    303     scriptInterface.SetProperty(metainfo, "mods"         , g_modsLoaded);
     305    scriptInterface.SetProperty(metainfo, "engine_version", utf8_from_wstring(engine_version));
     306    scriptInterface.SetProperty(metainfo, "mods", g_modsLoaded);
    304307    return metainfo;
    305308}
    306309
  • source/ps/VisualReplay.cpp

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

     
     1/* Copyright (C) 2015 Wildfire Games.
     2 * This file is part of 0 A.D.
     3 *
     4 * 0 A.D. is free software: you can redistribute it and/or modify
     5 * it under the terms of the GNU General Public License as published by
     6 * the Free Software Foundation, either version 2 of the License, or
     7 * (at your option) any later version.
     8 *
     9 * 0 A.D. is distributed in the hope that it will be useful,
     10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     12 * GNU General Public License for more details.
     13 *
     14 * You should have received a copy of the GNU General Public License
     15 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     16 */
     17
     18#ifndef INCLUDED_REPlAY
     19#define INCLUDED_REPlAY
     20
     21#include "scriptinterface/ScriptInterface.h"
     22class CSimulation2;
     23class CGUIManager;
     24
     25/**
     26 * Contains functions for visually replaying past games.
     27 */
     28
     29namespace VisualReplay
     30{
     31
     32/**
     33 * Returns the path to the sim-log directory (that contains the directories with the replay files.
     34 *
     35 * @param scriptInterface the ScriptInterface in which to create the return data.
     36 * @return OsPath the absolute file path
     37 */
     38OsPath GetDirectoryName();
     39
     40/**
     41 * Get a list of replays to display in the GUI.
     42 *
     43 * @param scriptInterface the ScriptInterface in which to create the return data.
     44 * @return array of objects containing replay data
     45 */
     46JS::Value GetReplays(ScriptInterface& scriptInterface);
     47
     48/**
     49 * Parses a commands.txt file and extracts metadata.
     50 * Works similarly to CGame::LoadReplayData().
     51 */
     52JS::Value LoadReplayData(ScriptInterface& scriptInterface, const OsPath& directory);
     53
     54/**
     55 * Permanently deletes the visual replay (including the parent directory)
     56 *
     57 * @param replayFile path to commands.txt, whose parent directory will be deleted
     58 * @return true if deletion was successful, false on error
     59 */
     60bool DeleteReplay(const CStrW& replayFile);
     61
     62/**
     63 * Returns the parsed header of the replay file (commands.txt).
     64 */
     65JS::Value GetReplayAttributes(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
     66
     67/**
     68 * Returns the metadata of a replay.
     69 */
     70JS::Value GetReplayMetadata(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& directoryName);
     71
     72/**
     73 * Saves the metadata from the session to metadata.json
     74 */
     75void SaveReplayMetadata(const CStrW& data);
     76
     77}
     78
     79#endif
  • source/simulation2/components/ICmpAIManager.cpp

     
    4747        m_AIs.set(JS_NewArrayObject(cx, 0));
    4848    }
    4949
    5050    void Run()
    5151    {
    52         vfs::ForEachFile(g_VFS, L"simulation/ai/", Callback, (uintptr_t)this, L"*.json", vfs::DIR_RECURSIVE);
     52        vfs::ForEachFile(g_VFS, L"simulation/ai/", Callback, (uintptr_t)this, L"data.json", vfs::DIR_RECURSIVE);
    5353    }
    5454
    5555    static Status Callback(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData)
    5656    {
    5757        GetAIsHelper* self = (GetAIsHelper*)cbData;