Ticket #4387: campaign_V0.patch

File campaign_V0.patch, 66.7 KB (added by wraitii, 4 years ago)
  • new file inaries/data/mods/public/campaigns/example.json

    diff --git a/binaries/data/mods/public/campaigns/example.json b/binaries/data/mods/public/campaigns/example.json
    new file mode 100644
    index 0000000..f2a0d2f
    - +  
     1{
     2    "Name" : "Example Campaign",
     3    "Description" : "Lorem Ipsum and so on and so on",
     4    "Interface" : "campaign/simple_campaign/page_menu.xml",
     5    "Levels" : {
     6        "Example_1" : {
     7            "Name" : "Example 1",
     8            "Map" : "scenarios/Serengeti.xml",
     9            "Description" : "Whatever"
     10        },
     11        "Example_2" : {
     12            "Name" : "Example 2",
     13            "Map" : "",
     14            "Description" : "None",
     15            "Requires" : "Example_1"
     16        },
     17        "Example_3" : {
     18            "Name" : "This one requires 1 and 2",
     19            "Map" : "",
     20            "Description" : "None",
     21            "Requires" : "Example_1+Example_2"
     22        },
     23        "Example_4" : {
     24            "Name" : "This one requires 2 or 3",
     25            "Map" : "",
     26            "Description" : "None",
     27            "Requires" : "Example_2 Example_3"
     28        },
     29        "Example_5" : {
     30            "Name" : "This one unavailable if 1",
     31            "Map" : "",
     32            "Description" : "None",
     33            "Requires" : "!Example_1"
     34        }
     35    },
     36    "Order" : ["Example_1", "Example_2", "Example_3", "Example_4", "Example_5"],
     37    "ShowUnavailable" : true
     38}
     39 No newline at end of file
  • new file inaries/data/mods/public/campaigns/tutorial.json

    diff --git a/binaries/data/mods/public/campaigns/tutorial.json b/binaries/data/mods/public/campaigns/tutorial.json
    new file mode 100644
    index 0000000..955dc76
    - +  
     1{
     2    "Name" : "Tutorial",
     3    "Description" : "Learn how to play 0 A.D.",
     4    "Interface" : "campaign/simple_campaign/page_menu.xml",
     5    "Image" : "session/icons/mappreview/Introductory Tutorial.png",
     6    "Levels" : {
     7        "introduction" : {
     8            "Name" : "Introductory Tutorial",
     9            "Map" : "scenarios/Introductory Tutorial.xml",
     10            "Description" : "This is a basic tutorial to get you started playing 0 A.D.",
     11            "Preview" : "session/icons/mappreview/Introductory Tutorial.png"
     12        },
     13        "eco_walkthrough" : {
     14            "Name" : "Economy Walkthrough",
     15            "Map" : "scenarios/starting_economy_walkthrough.xml",
     16            "Description" : "This map will give a rough guide for starting the game effectively.  Early in the game the most important thing is to gather resources as fast as possible so you are able to build enough troops later.\u000a\u000aWarning: This is very fast at the start, be prepared to run through the initial bit several times.",
     17            "Requires" : "introduction"
     18        }
     19    },
     20    "Order" : ["introduction", "eco_walkthrough"],
     21    "ShowUnavailable" : true
     22}
     23 No newline at end of file
  • new file inaries/data/mods/public/gui/campaign/campaign.js

    diff --git a/binaries/data/mods/public/gui/campaign/campaign.js b/binaries/data/mods/public/gui/campaign/campaign.js
    new file mode 100644
    index 0000000..2050027
    - +  
     1// ID of the current campaign. This is the name of the json file (not the "human readable" name)
     2var g_CampaignID = null;
     3
     4// Campaign template data from the JSON file.
     5var g_CampaignTemplate = null;
     6
     7// name of the file we're saving campaign data in
     8var g_CampaignSave = null;
     9
     10// Current campaign state, to be saved in/loaded from the above file
     11var g_CampaignData = null;
     12
     13function startLevel(level)
     14{
     15    let matchID = launchGame(g_CampaignTemplate.Levels[level], level);
     16
     17    g_CampaignData.currentlyPlaying = matchID;
     18
     19    saveCurrentCampaign();
     20}
     21
     22// this function is called by session.js at the end of a game. It should save the campaign save state immediately.
     23function campaignGameEnded(data)
     24{
     25    g_CampaignID = data.ID;
     26    g_CampaignTemplate = data.template;
     27    g_CampaignSave = data.save;
     28    g_CampaignData = data.data;
     29
     30    // TODO: Deal with the endGameData
     31
     32    // if we're not active, we either lost or won, so we're no longer playing the game.
     33    if (data.endGameData.status !== "active")
     34    {
     35        if (g_CampaignData.currentlyPlaying)
     36            g_CampaignData.currentlyPlaying = undefined;
     37    }
     38    if (data.endGameData.status === "won")
     39    {
     40        if (!g_CampaignData.completed)
     41            g_CampaignData.completed = [];
     42        if (g_CampaignData.completed.indexOf(data.level) == -1)
     43            g_CampaignData.completed.push(data.level);
     44    }
     45
     46    saveCurrentCampaign();
     47}
     48
     49// returns true if the level "level" is available.
     50function hasRequirements(level)
     51{
     52    if (!level.Requires)
     53        return true;
     54
     55    if (!g_CampaignData.completed)
     56        return false;
     57
     58    // reuse class matching system, supporting "or", "+" and "!"
     59    if (!MatchesClassList(g_CampaignData.completed, level.Requires))
     60        return false;
     61
     62    return true;
     63}
     64
     65// return true if the player has completed the level with ID "level"
     66function hasCompleted(level)
     67{
     68    if (!g_CampaignData.completed)
     69        return false;
     70
     71    if (g_CampaignData.completed.indexOf(level.ID) === -1)
     72        return false;
     73
     74    return true;
     75}
     76
  • new file inaries/data/mods/public/gui/campaign/campaign_io.js

    diff --git a/binaries/data/mods/public/gui/campaign/campaign_io.js b/binaries/data/mods/public/gui/campaign/campaign_io.js
    new file mode 100644
    index 0000000..9497176
    - +  
     1// This file provides helper functions related to loading/saving campaign save states and campaign templates
     2// Most functions rely on loading campaign.js, but not all (such as canLoadCurrentCampaign)
     3
     4/*
     5 * TODOs in this file:
     6 * - Provide a way to check if the campaign state changed?
     7 * Other things
     8 */
     9
     10function LoadAvailableCampaigns()
     11{
     12    let campaigns = Engine.BuildDirEntList("campaigns/", "*.json", false);
     13
     14    let ret = {};
     15
     16    for (let filename of campaigns)
     17    {
     18        let data = Engine.ReadJSONFile(filename);
     19        if (!data)
     20            continue;
     21
     22        // TODO: sanity checks, probably in their own function?
     23        if (!data.Name)
     24            continue;
     25
     26        ret[filename.replace("campaigns/","").replace(".json","")] = data;
     27    }
     28
     29    return ret;
     30}
     31
     32function canLoadCurrentCampaign(verbose = false)
     33{
     34    let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
     35
     36    if (!campaign)
     37    {
     38        if (verbose)
     39            warn("No campaign chosen, currentcampaign is not defined in user.cfg. Quitting campaign mode.")
     40        return false;
     41    }
     42
     43    if (!Engine.FileExists("campaignsaves/" + campaign + ".0adcampaign"))
     44    {
     45        if (verbose)
     46            warn("Current campaign not found. Quitting campaign mode.");
     47        return false;
     48    }
     49
     50    // TODO: load up the file and do some checks?
     51
     52    return true;
     53}
     54
     55function loadCurrentCampaignSave()
     56{
     57    let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
     58
     59    if (!canLoadCurrentCampaign(true))
     60        return false;
     61
     62    if (g_CampaignSave)
     63    {
     64        warn("Campaign already loaded");
     65        return false;
     66    }
     67    g_CampaignSave = campaign;
     68
     69    let campaignData = loadCampaignSave(g_CampaignSave);
     70    if (!campaignData)
     71    {
     72        warn("Campaign failed to load properly. Quitting campaign mode.")
     73        return false;
     74    }
     75
     76    g_CampaignData = campaignData;
     77    g_CampaignID = g_CampaignData.campaign;
     78
     79    if (!loadCampaignTemplate(g_CampaignID))
     80        return false;
     81
     82    // actually fetch the menu
     83    Engine.SwitchGuiPage(g_CampaignTemplate.Interface, {"ID" : g_CampaignID, "template" : g_CampaignTemplate, "save": g_CampaignSave, "data" : g_CampaignData});
     84
     85    return true;
     86}
     87
     88function loadCampaignTemplate(name)
     89{
     90    let data = Engine.ReadJSONFile("campaigns/" + name + ".json");
     91
     92    if (!data)
     93    {
     94        warn("Could not parse campaign data.");
     95        return false;
     96    }
     97   
     98    g_CampaignTemplate = data;
     99
     100    return true;
     101}
     102
     103function saveCurrentCampaign()
     104{
     105    if (!g_CampaignSave)
     106    {
     107        warn("Cannot save current campaign, no campaign is currently loaded.");
     108        return false;
     109    }
     110
     111    return saveCampaign(g_CampaignSave, g_CampaignData);
     112}
     113
     114function saveCampaign(filename, data)
     115{
     116    Engine.WriteJSONFile("campaignsaves/" + filename + ".0adcampaign", data);
     117
     118    return true;
     119}
     120
     121function loadCampaignSave(filename)
     122{
     123    let campaignData = Engine.ReadJSONFile("campaignsaves/" + filename + ".0adcampaign");
     124    if (!campaignData)
     125    {
     126        error("Campaign " + filename + " failed to load properly.");
     127        return undefined;
     128    }
     129
     130    return campaignData;
     131}
     132
  • new file inaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js

    diff --git a/binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js b/binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js
    new file mode 100644
    index 0000000..c06100d
    - +  
     1// Here should go functions to set up Campaign games.
     2var g_GameAttributes = { "settings": {} };
     3
     4var g_DefaultPlayerData = [];
     5
     6function sanitizePlayerData(playerData)
     7{
     8    // Remove gaia
     9    if (playerData.length && !playerData[0])
     10        playerData.shift();
     11
     12    playerData.forEach((pData, index) => {
     13        pData.Color = pData.Color;
     14        pData.Civ = pData.Civ;
     15
     16        // Use default AI if the map doesn't specify any explicitly
     17        if (!("AI" in pData))
     18            pData.AI = g_DefaultPlayerData[index].AI;
     19
     20        if (!("AIDiff" in pData))
     21            pData.AIDiff = g_DefaultPlayerData[index].AIDiff;
     22    });
     23}
     24
     25
     26// TODO: this is a minimalist patchwork from gamesetup.Js and only barely attempts to work.
     27
     28function launchGame(level, levelID)
     29{
     30    if (!level.Map)
     31    {
     32        warn("cannot start scenario: no maps specified.");
     33        return;
     34    }
     35
     36    loadSettingsValues();
     37
     38    g_DefaultPlayerData = g_Settings.PlayerDefaults;
     39    g_DefaultPlayerData.shift();
     40
     41    let mapData = Engine.LoadMapSettings("maps/" + level.Map);
     42    if (!mapData)
     43    {
     44        warn("Could not load map");
     45        return;
     46    }
     47
     48    let mapSettings = mapData && mapData.settings ? deepcopy(mapData.settings) : {};
     49
     50    if (mapSettings.PlayerData)
     51        sanitizePlayerData(mapSettings.PlayerData);
     52
     53    // Copy any new settings
     54    g_GameAttributes.map = "maps/" + level.Map;
     55    g_GameAttributes.script = mapSettings.Script;
     56
     57    for (let prop in mapSettings)
     58        g_GameAttributes.settings[prop] = mapSettings[prop];
     59
     60    // TODO: support default victory conditions?
     61    g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.TriggerScripts || [];
     62
     63    g_GameAttributes.settings.mapType = "scenario";
     64    g_GameAttributes.mapType = "scenario";
     65
     66    // Seed used for both map generation and simulation
     67    g_GameAttributes.settings.Seed = Math.floor(Math.random() * Math.pow(2, 32));
     68    g_GameAttributes.settings.AISeed = Math.floor(Math.random() * Math.pow(2, 32));
     69
     70    g_GameAttributes.matchID = Engine.GetMatchID();
     71
     72    // TODO: player should be defined in the map or the campaign at the least.
     73    let playerID = 1;
     74
     75    g_GameAttributes.campaignData = {"ID" : g_CampaignID, "template" : g_CampaignTemplate, "save": g_CampaignSave, "data" : g_CampaignData, "level" : levelID};
     76
     77    Engine.StartGame(g_GameAttributes, playerID);
     78    Engine.SwitchGuiPage("page_loading.xml", {
     79        "attribs": g_GameAttributes,
     80        "isNetworked" : false,
     81        "playerAssignments": {}
     82    });
     83    return g_GameAttributes.matchID;
     84}
     85 No newline at end of file
  • new file inaries/data/mods/public/gui/campaign/load.js

    diff --git a/binaries/data/mods/public/gui/campaign/load.js b/binaries/data/mods/public/gui/campaign/load.js
    new file mode 100644
    index 0000000..81752d6
    - +  
     1var g_Campaigns = [];
     2
     3var g_CampaignTemplate = null;
     4
     5function init()
     6{
     7    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     8
     9    let campaigns = Engine.BuildDirEntList("campaignsaves/", "*.0adcampaign", false);
     10
     11    if (!campaigns.length)
     12    {
     13        gameSelection.list = [translate("No ongoing campaigns found")];
     14        gameSelection.selected = -1;
     15        selectionChanged();
     16        Engine.GetGUIObjectByName("loadGameButton").enabled = false;
     17        Engine.GetGUIObjectByName("deleteGameButton").enabled = false;
     18        return;
     19    }
     20
     21    gameSelection.list = campaigns.map(path => generateLabel(pathToGame(path)));
     22    gameSelection.list_data = campaigns.map(path => pathToGame(path));
     23
     24    if (gameSelection.selected == -1)
     25        gameSelection.selected = 0;
     26    else if (gameSelection.selected >= campaigns.length) // happens when deleting the last saved game
     27        gameSelection.selected = campaigns.length - 1;
     28    else
     29        selectionChanged();
     30}
     31
     32function pathToGame(path)
     33{
     34    return path.replace("campaignsaves/","").replace(".0adcampaign","");
     35}
     36
     37function generateLabel(game)
     38{
     39    let campaignData = loadCampaignSave(game);
     40    if (!campaignData)
     41        return "Incompatible - " + game;
     42
     43    if (!loadCampaignTemplate(campaignData.campaign))
     44        return "Incompatible - " + game;
     45
     46    return campaignData.userDescription + " - " + g_CampaignTemplate.Name;
     47}
     48
     49function selectionChanged()
     50{
     51    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     52    let selectionEmpty = gameSelection.selected == -1;
     53    Engine.GetGUIObjectByName("invalidGame").hidden = !selectionEmpty;
     54    Engine.GetGUIObjectByName("validGame").hidden = selectionEmpty;
     55
     56    if (selectionEmpty)
     57        return;
     58
     59/*
     60    Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name);
     61    let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map);
     62    setMapPreviewImage("savedInfoPreview", mapData.preview);
     63
     64    Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1;
     65    Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0);
     66    Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType);
     67    Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size);
     68    Engine.GetGUIObjectByName("savedVictory").caption = translateVictoryCondition(metadata.initAttributes.settings.GameType);
     69
     70    let caption = sprintf(translate("Mods: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) });
     71    if (!hasSameMods(metadata, Engine.GetEngineInfo()))
     72        caption = "[color=\"orange\"]" + caption + "[/color]";
     73    Engine.GetGUIObjectByName("savedMods").caption = caption;
     74
     75    Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo(
     76        metadata.initAttributes.settings.PlayerData,
     77        metadata.gui.states
     78    );
     79    */
     80}
     81
     82function loadCampaign()
     83{
     84    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     85
     86    let campaign = gameSelection.list_data[gameSelection.selected];
     87    reallyLoadCampaign(campaign);
     88
     89    // TODO: compatibility checks cf saved games
     90/*
     91    // Check compatibility before really loading it
     92    let sameMods = hasSameMods(metadata, engineInfo);
     93
     94    if (sameEngineVersion && sameSavegameVersion && sameMods)
     95    {
     96        reallyLoadGame(gameId);
     97        return;
     98    }
     99
     100    if (!sameMods)
     101    {
     102        if (!metadata.mods)
     103            metadata.mods = [];
     104
     105        message += translate("The savegame needs a different set of mods:") + "\n" +
     106            sprintf(translate("Required: %(mods)s"), {
     107                "mods": metadata.mods.join(translate(", "))
     108            }) + "\n" +
     109            sprintf(translate("Active: %(mods)s"), {
     110                "mods": engineInfo.mods.join(translate(", "))
     111            });
     112    }
     113
     114    message += "\n" + translate("Do you still want to proceed?");
     115
     116    messageBox(
     117        500, 250,
     118        message,
     119        translate("Warning"),
     120        [translate("No"), translate("Yes")],
     121        [init, function(){ reallyLoadGame(gameId); }]
     122    );*/
     123}
     124
     125function reallyLoadCampaign(name)
     126{
     127    Engine.ConfigDB_CreateValue("user", "currentcampaign", name);
     128    Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", name, "config/user.cfg");
     129
     130    loadCurrentCampaignSave();
     131}
     132
     133function deleteCampaign()
     134{
     135    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     136    let campaign = gameSelection.list_data[gameSelection.selected];
     137
     138    if (!campaign)
     139        return;
     140
     141    messageBox(
     142        500, 200,
     143        sprintf(translate("\"%(label)s\""), {
     144            "label": gameSelection.list[gameSelection.selected]
     145        }) + "\n" + translate("Campaign will be permanently deleted, are you sure?"),
     146        translate("DELETE"),
     147        [translate("No"), translate("Yes")],
     148        [null, function(){ reallyDeleteCampaign(campaign); }]
     149    );
     150}
     151
     152function reallyDeleteCampaign(name)
     153{
     154    if (!Engine.DeleteCampaignGame(name))
     155        error("Could not delete campaign game " + name);
     156
     157    if (Engine.ConfigDB_GetValue("user", "currentcampaign") === name)
     158    {
     159        Engine.ConfigDB_RemoveValue("user", "currentcampaign");
     160        Engine.ConfigDB_WriteFile("user", "config/user.cfg");
     161
     162        // TODO: this doesn't seem to work.
     163        if (Engine.GetGUIObjectByName("subMenuContinueCampaignButton"))
     164            Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false;
     165    }
     166
     167    // re-run init to refresh.
     168    init();
     169}
  • new file inaries/data/mods/public/gui/campaign/load.xml

    diff --git a/binaries/data/mods/public/gui/campaign/load.xml b/binaries/data/mods/public/gui/campaign/load.xml
    new file mode 100644
    index 0000000..380f83d
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4
     5    <script file="gui/common/color.js" />
     6    <script file="gui/common/functions_global_object.js" />
     7    <script file="gui/common/functions_utility.js" />
     8
     9    <script file="gui/campaign/campaign.js" />
     10    <script file="gui/campaign/campaign_io.js" />
     11    <script file="gui/campaign/load.js" />
     12
     13    <!-- Add a translucent black background to fade out the menu page -->
     14    <object type="image" z="0" sprite="BackgroundTranslucent"/>
     15
     16    <object type="image" style="ModernDialog" size="50%-420 50%-325 50%+420 50%+325">
     17
     18        <object type="text" style="TitleText" size="50%-128 -18 50%+128 14">
     19            <translatableAttribute id="caption">Load Campaign</translatableAttribute>
     20        </object>
     21
     22        <object type="image" size="0 20 550 100%">
     23            <object name="gameSelection" style="ModernList" type="list" size="24 12 100%-24 100%-90">
     24                <action on="SelectionChange">selectionChanged();</action>
     25            </object>
     26
     27            <object type="button" size="0%+25 100%-60 33%+10 100%-32" style="StoneButton" hotkey="cancel">
     28                <translatableAttribute id="caption">Cancel</translatableAttribute>
     29                <action on="Press">Engine.PopGuiPage();</action>
     30            </object>
     31
     32            <object name="deleteGameButton" type="button" size="33%+20 100%-60 66%-15 100%-32" style="StoneButton" hotkey="session.savedgames.delete">
     33                <translatableAttribute id="caption">Delete</translatableAttribute>
     34                <action on="Press">deleteCampaign();</action>
     35            </object>
     36
     37            <object name="loadGameButton" type="button" style="StoneButton" size="66%-5 100%-60 100%-25 100%-32">
     38                <translatableAttribute id="caption">Load</translatableAttribute>
     39                <action on="Press">loadCampaign();</action>
     40            </object>
     41
     42        </object>
     43
     44        <object name="validGame" type="image" size="550 20 100%-20 100%">
     45            <object name="savedMapName" size="0 0 100% 20" type="text" style="ModernLabelText" />
     46            <object name="savedInfoPreview" size="20 20 240 240" type="image" sprite="" />
     47
     48            <object size="0 250 50% 270" type="text" style="ModernLabelText" text_align="left">
     49                <translatableAttribute id="caption">TODO:</translatableAttribute>
     50            </object>
     51            <object name="savedPlayers" size="50% 250 100%-15 270" type="text" style="ModernLabelText" text_align="left" />
     52            <object size="0 270 50% 290" type="text" style="ModernLabelText" text_align="left">
     53                <translatableAttribute id="caption">TODO:</translatableAttribute>
     54            </object>
     55            <object name="savedPlayedTime" size="50% 270 100%-15 290" type="text" style="ModernLabelText" text_align="left" />
     56            <object size="0 290 50% 310" type="text" style="ModernLabelText" text_align="left">
     57                <translatableAttribute id="caption">TODO:</translatableAttribute>
     58            </object>
     59            <object name="savedMapType" size="50% 290 100%-15 310" type="text" style="ModernLabelText" text_align="left" />
     60            <object size="0 310 50% 330" type="text" style="ModernLabelText" text_align="left">
     61                <translatableAttribute id="caption">TODO:</translatableAttribute>
     62            </object>
     63            <object name="savedMapSize" size="50% 310 100%-15 330" type="text" style="ModernLabelText" text_align="left" />
     64            <object size="0 330 50% 350" type="text" style="ModernLabelText" text_align="left">
     65                <translatableAttribute id="caption">TODO:</translatableAttribute>
     66            </object>
     67            <object name="savedVictory" size="50% 330 100%-15 350" type="text" style="ModernLabelText" text_align="left" />
     68
     69            <object size="0 352 100%-15 353" type="image" sprite="ModernWhiteLine" z="25" />
     70
     71            <object name="savedMods" size="0 355 100%-15 395" type="text" style="ModernLabelText" text_align="left" />
     72
     73            <object size="0 397 100%-15 398" type="image" sprite="ModernWhiteLine" z="25" />
     74            <object name="savedPlayersNames" size="0 400 100%-10 100%-32" type="text" style="MapPlayerList" />
     75            <object size="0 100%-32 100%-15 100%-31" type="image" sprite="ModernWhiteLine" z="25" />
     76        </object>
     77
     78        <object name="invalidGame" size="570 55 790 155" type="image" sprite="logo" />
     79    </object>
     80
     81</objects>
  • new file inaries/data/mods/public/gui/campaign/newcampaign_modal.js

    diff --git a/binaries/data/mods/public/gui/campaign/newcampaign_modal.js b/binaries/data/mods/public/gui/campaign/newcampaign_modal.js
    new file mode 100644
    index 0000000..fac7bcc
    - +  
     1var g_CampaignID = null;
     2var g_CampaignData = null;
     3
     4// TODO: refuse empty names
     5
     6function init(data)
     7{
     8    // load existing campaigns
     9    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     10    gameSelection.selected = -1;
     11
     12    let campaigns = Engine.BuildDirEntList("campaignsaves/", "*.0adcampaign", false);
     13    if (!campaigns.length)
     14        gameSelection.list = [translate("No ongoing campaigns.")];
     15    else
     16    {
     17        gameSelection.list = campaigns.map(path => generateLabel(pathToGame(path)));
     18        gameSelection.list_data = campaigns.map(path => pathToGame(path));
     19    }
     20
     21    if (data)
     22    {
     23        g_CampaignID = data.campaignID;
     24        g_CampaignData = data.campaignData;
     25    }
     26}
     27
     28function selectionChanged()
     29{
     30    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     31    if (gameSelection.selected === -1)
     32        return;
     33
     34    // TODO: do something?
     35}
     36
     37function startCampaign()
     38{
     39    // TODO: handle overwrite and so on
     40
     41    // temp: prefill campaign name
     42    realStartCampaign(Engine.GetGUIObjectByName("saveGameDesc").caption);
     43}
     44
     45function realStartCampaign(desc)
     46{
     47    let name = g_CampaignID + "_1";
     48    // if file already exists, pick the number above the existing ones. Don't bother making it dense.
     49    if (Engine.FileExists("campaignsaves/" + name + ".0adcampaign"))
     50    {
     51        // get other campaigns following that template
     52        let campaigns = Engine.BuildDirEntList("campaignsaves/", g_CampaignID + "_*.0adcampaign", false);
     53        let max = 1;
     54        for (let camp of campaigns)
     55        {
     56            let nb = camp.replace("campaignsaves/" + g_CampaignID + "_","").replace(".0adcampaign","");
     57            if (+nb > max)
     58                max = +nb;
     59        }
     60        name = g_CampaignID + "_" + (max+1);
     61        // sanity check
     62        if (Engine.FileExists("campaignsaves/" + name + ".0adcampaign"))
     63        {
     64            error("tell wraitii he can't code");
     65            return;
     66        }
     67    }
     68
     69    saveCampaign(name, {"userDescription" : desc, "campaign" : g_CampaignID})
     70
     71    // inform user config that we are playing this campaign
     72    Engine.ConfigDB_CreateValue("user", "currentcampaign", name);
     73    Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", name, "config/user.cfg");
     74
     75    loadCurrentCampaignSave();
     76}
     77 No newline at end of file
  • new file inaries/data/mods/public/gui/campaign/newcampaign_modal.xml

    diff --git a/binaries/data/mods/public/gui/campaign/newcampaign_modal.xml b/binaries/data/mods/public/gui/campaign/newcampaign_modal.xml
    new file mode 100644
    index 0000000..6380442
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4
     5    <!-- Used for engine + mod version checks and deleteTooltip. -->
     6    <script file="gui/common/functions_utility_loadsave.js" />
     7    <script file="gui/common/functions_global_object.js" />
     8
     9    <script file="gui/campaign/campaign.js" />
     10    <script file="gui/campaign/campaign_io.js" />
     11    <script file="gui/campaign/load.js" />
     12    <script file="gui/campaign/newcampaign_modal.js" />
     13
     14    <!-- Add a translucent black background to fade out the page -->
     15    <object type="image" z="0" sprite="ModernFade"/>
     16
     17    <object type="image" style="ModernDialog" size="50%-300 50%-200 50%+300 50%+200">
     18        <object type="image" z="0" sprite="ModernFade"/>
     19        <object type="text" style="TitleText" size="50%-128 -18 50%+128 14">
     20            <translatableAttribute id="caption">New Campaign</translatableAttribute>
     21        </object>
     22
     23        <object name="gameSelection" style="ModernList" type="list" size="24 22 100%-24 100%-130">
     24            <action on="SelectionChange">selectionChanged();</action>
     25        </object>
     26
     27        <object size="24 100%-124 100%-24 100%-100" name="descLabel" type="text" style="ModernLeftLabelText">
     28            <translatableAttribute id="caption">Name of this campaign run:</translatableAttribute>
     29        </object>
     30
     31        <object name="saveGameDesc" size="24 100%-96 100%-24 100%-72" type="input" style="ModernInput">
     32            <action on="Press">startCampaign();</action>
     33        </object>
     34
     35        <object name="saveButton" type="button" size="0%+25 100%-60 33%+10 100%-32" style="StoneButton" hotkey="cancel">
     36            <translatableAttribute id="caption">Cancel</translatableAttribute>
     37            <action on="Press">Engine.PopGuiPage();</action>
     38        </object>
     39
     40        <object name="deleteGameButton" type="button" size="33%+20 100%-60 66%-15 100%-32" style="StoneButton">
     41            <translatableAttribute id="caption">Delete</translatableAttribute>
     42            <action on="Press">deleteCampaign();</action>
     43        </object>
     44
     45        <object type="button" style="StoneButton" size="66%-5 100%-60 100%-25 100%-32">
     46            <translatableAttribute id="caption">Start</translatableAttribute>
     47            <action on="Press">startCampaign();</action>
     48        </object>
     49
     50    </object>
     51</objects>
  • new file inaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.js

    diff --git a/binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.js b/binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.js
    new file mode 100644
    index 0000000..9d31b4c
    - +  
     1var g_SelectedLevel = null;
     2
     3var g_SavedGamesMetadata = [];
     4
     5function init(data)
     6{
     7    if (!data)
     8    {
     9        warn("Loading campaign menu without a campaign loaded")
     10        return false;
     11    }
     12
     13    g_CampaignID = data.ID;
     14    g_CampaignTemplate = data.template;
     15    g_CampaignSave = data.save;
     16    g_CampaignData = data.data;
     17
     18    generateLevelList();
     19    selectionChanged();
     20
     21    Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
     22
     23    let gameSelection = Engine.GetGUIObjectByName("gameSelection");
     24    let savedGames = Engine.GetSavedGames().sort(sortDecreasingDate).filter(game => game.metadata.initAttributes && game.metadata.initAttributes.campaignData && game.metadata.initAttributes.campaignData.ID == g_CampaignID);
     25    gameSelection.enabled = !!savedGames.length;
     26    if (!savedGames.length)
     27    {
     28        gameSelection.list = [translate("No saved games found")];
     29        gameSelection.selected = -1;
     30        return;
     31    }
     32
     33    // Get current game version and loaded mods
     34    let engineInfo = Engine.GetEngineInfo();
     35
     36    g_SavedGamesMetadata = savedGames.map(game => game.metadata);
     37
     38    gameSelection.list = savedGames.map(game => generateCampaignLabel(game.metadata, engineInfo));
     39    gameSelection.list_data = savedGames.map(game => game.id);
     40
     41    saveSelectionChanged();
     42}
     43
     44function generateLevelList()
     45{
     46    // TODO: remember old selection?
     47    let selection = Engine.GetGUIObjectByName("levelSelection");
     48
     49    let list = [];
     50    for (let key in g_CampaignTemplate.Levels)
     51    {
     52        let level = g_CampaignTemplate.Levels[key];
     53
     54        if (!("ShowUnavailable" in g_CampaignTemplate) || !g_CampaignTemplate.ShowUnavailable && !hasRequirements(level))
     55            continue;
     56
     57        let status = "";
     58        let name = level.Name;
     59        if (!hasRequirements(level))
     60        {
     61            status = "not unlocked yet";
     62            name = "[color=\"gray\"]" + name + "[/color]";
     63        }
     64        list.push({ "ID" : key, "name" : name, "status" : status });
     65    }
     66    list.sort((a, b) => g_CampaignTemplate.Order.indexOf(a.ID) - g_CampaignTemplate.Order.indexOf(b.ID));
     67
     68    // change array of object into object of array.
     69    list = prepareForDropdown(list);
     70
     71    // Push to GUI
     72    selection.selected = -1;
     73    selection.list_name = list.name || [];
     74    selection.list_status = list.status || [];
     75
     76    // Change these last, otherwise crash
     77    // TODO: do we need both of those? I'm unsure.
     78    selection.list = list.ID || [];
     79    selection.list_data = list.ID || [];
     80
     81//  replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
     82
     83//  displayReplayDetails();
     84
     85}
     86
     87function displayLevelDetails(levelID)
     88{
     89    let level = g_CampaignTemplate.Levels[levelID];
     90
     91    // TODO: load from map file if not present
     92    Engine.GetGUIObjectByName("scenarioName").caption = translate(level.Name);
     93    Engine.GetGUIObjectByName("scenarioDesc").caption = translate(level.Description);
     94
     95    // todo: ibidem
     96    if (level.Preview)
     97        Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview;
     98    else
     99        Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
     100
     101    g_SelectedLevel = levelID;
     102
     103    if (!hasRequirements(level))
     104    {
     105        Engine.GetGUIObjectByName("startButton").enabled = false;
     106        return;
     107    }
     108
     109    Engine.GetGUIObjectByName("startButton").enabled = true;
     110}
     111
     112function selectionChanged()
     113{
     114    let selection = Engine.GetGUIObjectByName("levelSelection");
     115
     116    if (selection.selected === -1)
     117    {
     118        Engine.GetGUIObjectByName("startButton").enabled = false;
     119        Engine.GetGUIObjectByName("startButton").hidden = false;
     120        Engine.GetGUIObjectByName("loadSavedButton").hidden = true;
     121        return;
     122    }
     123
     124    Engine.GetGUIObjectByName("loadSavedButton").hidden = true;
     125    Engine.GetGUIObjectByName("startButton").hidden = false;
     126
     127    let selec = Engine.GetGUIObjectByName("gameSelection");
     128    selec.selected = -1;
     129
     130    displayLevelDetails(selection.list[selection.selected]);
     131}
     132
     133function saveSelectionChanged()
     134{
     135    let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected];
     136
     137    if (!metadata)
     138        return;
     139
     140    // fetch campaign scenario metadata if present.
     141    let scenarioID = metadata.initAttributes.campaignData.level;
     142
     143    let selection = Engine.GetGUIObjectByName("levelSelection");
     144    selection.selected = -1;
     145    for (let level of selection.list_data)
     146        if (level == scenarioID)
     147        {
     148            displayLevelDetails(level);
     149            break;
     150        }
     151
     152    Engine.GetGUIObjectByName("loadSavedButton").hidden = false;
     153    Engine.GetGUIObjectByName("startButton").hidden = true;
     154/*
     155    Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name);
     156    let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map);
     157    setMapPreviewImage("savedInfoPreview", mapData.preview);
     158
     159    Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1;
     160    Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0);
     161    Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType);
     162    Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size);
     163    Engine.GetGUIObjectByName("savedVictory").caption = translateVictoryCondition(metadata.initAttributes.settings.GameType);
     164
     165    let caption = sprintf(translate("Mods: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) });
     166    if (!hasSameMods(metadata, Engine.GetEngineInfo()))
     167        caption = "[color=\"orange\"]" + caption + "[/color]";
     168    Engine.GetGUIObjectByName("savedMods").caption = caption;
     169
     170    Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo(
     171        metadata.initAttributes.settings.PlayerData,
     172        metadata.gui.states
     173    );*/
     174}
     175
     176function generateCampaignLabel(metadata, engineInfo)
     177{
     178    let dateTimeString = Engine.FormatMillisecondsIntoDateString(metadata.time*1000, translate("yyyy-MM-dd HH:mm:ss"));
     179    let dateString = sprintf(translate("\\[%(date)s]"), { "date": dateTimeString });
     180
     181    if (engineInfo)
     182    {
     183        if (!hasSameSavegameVersion(metadata, engineInfo) || !hasSameEngineVersion(metadata, engineInfo))
     184            dateString = "[color=\"red\"]" + dateString + "[/color]";
     185        else if (!hasSameMods(metadata, engineInfo))
     186            dateString = "[color=\"orange\"]" + dateString + "[/color]";
     187    }
     188
     189    return sprintf(
     190        metadata.description ?
     191            translate("%(dateString)s %(level)s - %(description)s") :
     192            translate("%(dateString)s %(level)s"),
     193        {
     194            "dateString": dateString,
     195            "level": metadata.initAttributes.campaignData.template.Levels[metadata.initAttributes.campaignData.level].Name,
     196            "description": metadata.description || ""
     197        }
     198    );
     199}
     200
     201function exitCampaignMode(exitGame = false)
     202{
     203    // TODO: should this be here?
     204    saveCurrentCampaign();
     205
     206    if (exitGame)
     207    {
     208        messageBox(
     209        400, 200,
     210        translate("Are you sure you want to quit 0 A.D.?"),
     211        translate("Confirmation"),
     212        [translate("No"), translate("Yes")],
     213        [null, Engine.Exit]
     214        );
     215        return;
     216    }
     217    Engine.SwitchGuiPage("page_pregame.xml", {});
     218}
  • new file inaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.xml b/binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.xml
    new file mode 100644
    index 0000000..785c7b1
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4    <script file="gui/common/music.js"/>
     5    <script file="gui/common/functions_civinfo.js"/>
     6    <script file="gui/common/functions_global_object.js"/>
     7    <script file="gui/common/functions_utility_error.js"/>
     8    <script file="gui/common/functions_utility_loadsave.js"/>
     9    <script file="gui/common/settings.js"/>
     10
     11    <script file="gui/savedgames/load.js"/>
     12
     13    <script file="gui/campaign/campaign.js"/>
     14    <script file="gui/campaign/campaign_io.js"/>
     15    <script file="gui/campaign/gamesetup/gamesetup.js"/>
     16
     17    <script file="gui/campaign/simple_campaign/mainmenu.js"/>
     18
     19    <object type="image" style="ModernWindow" size="0 0 100% 100%" name="campaignMenuWindow">
     20
     21        <!-- Title -->
     22        <object style="ModernLabelText" name="CampaignTitle" type="text" size="50%-128 4 50%+128 36">
     23            <translatableAttribute id="caption">Campaign Name</translatableAttribute>
     24        </object>
     25
     26        <object name="leftPanel" size="30 50 100%-447 100%-340">
     27            <!-- List of available levels -->
     28            <object name="levelSelection"
     29                size="0 0 100% 100%"
     30                style="ModernList"
     31                type="olist"
     32                font="sans-stroke-16"
     33                >
     34
     35                <action on="SelectionChange">selectionChanged();</action>
     36                <!--<action on="SelectionColumnChange">displayReplayList();</action>
     37                <action on="mouseleftdoubleclickitem">startCampaign();</action>-->
     38
     39            <!-- Columns -->
     40            <column id="name" color="172 172 212" width="80%">
     41                <translatableAttribute id="heading" context="campaignLevelList">Scenario Name</translatableAttribute>
     42            </column>
     43
     44            <column id="status" color="192 192 192" width="20%">
     45                <translatableAttribute id="heading" context="campaignLevelList">Status</translatableAttribute>
     46            </column>
     47            </object>
     48        </object>
     49
     50        <object name="rightPanel" size="100%-432 50 100%-30 100%-340">
     51            <!-- Scenario Image -->
     52            <object type="image" sprite="ModernDarkBoxGold" name="levelPreviewBox" size="0 0 100% 302">
     53                <object type="image" sprite="snLevelPreview" size="1 1 100%-1 301" name="mapPreview"/>
     54            </object>
     55
     56            <!-- Name and description -->
     57            <!-- TODO: figure out if we want an options button here too. -->
     58            <object size="0 315 100% 100%">
     59                <object name="scenarioName" type="text" style="ScenarioNameText" size="0 0 100% 30">
     60                    <translatableAttribute id="caption">No scenario selected</translatableAttribute>
     61                </object>
     62                <object type="image" sprite="ModernDarkBoxGold" size="0 30 100% 100%">
     63                    <object name="scenarioDesc" type="text" style="ModernText" size="10 10 100%-10 100%-10"/>
     64                </object>
     65            </object>
     66
     67        </object>
     68
     69        <object name="savePanel" size="30 100%-330 100%-30 100%-85">
     70           
     71            <object style="ModernLabelText" type="text" size="50%-128 4 50%+128 36">
     72                <translatableAttribute id="caption">Saved Games</translatableAttribute>
     73            </object>
     74
     75            <object name="gameSelection" style="ModernList" type="list" size="0 50 100% 100%">
     76                <action on="SelectionChange">saveSelectionChanged();</action>
     77            </object>
     78   
     79        </object>
     80
     81        <object name="bottomPanel" size="30 100%-55 100%-5 100%-25" >
     82
     83            <!-- Exit & Back to Main Menu -->
     84            <object type="button" style="StoneButton" size="0 0 17% 100%">
     85                <translatableAttribute id="caption">Back to Main Menu</translatableAttribute>
     86                <action on="Press">exitCampaignMode()</action>
     87            </object>
     88
     89            <object type="button" style="StoneButton" size="17%+20 0 34%+20 100%">
     90                <translatableAttribute id="caption">Exit Game</translatableAttribute>
     91                <action on="Press">exitCampaignMode(true);</action>
     92            </object>
     93
     94            <object name="startButton" type="button" style="StoneButtonFancy" size="83%-25 0 100%-25 100%" enabled="false">
     95                <translatableAttribute id="caption">Start Scenario</translatableAttribute>
     96                <action on="Press">
     97                    startLevel(g_SelectedLevel);
     98                </action>
     99            </object>
     100            <object name="loadSavedButton" type="button" style="StoneButtonFancy" size="83%-25 0 100%-25 100%" hidden="true">
     101                <translatableAttribute id="caption">Resume Saved Game</translatableAttribute>
     102                <action on="Press">
     103                    loadGame();
     104                </action>
     105            </object>
     106
     107        </object>
     108    </object>
     109
     110</objects>
  • new file inaries/data/mods/public/gui/campaign/simple_campaign/page_menu.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_campaign/page_menu.xml b/binaries/data/mods/public/gui/campaign/simple_campaign/page_menu.xml
    new file mode 100644
    index 0000000..d9a1909
    - +  
     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/sprites.xml</include>
     9    <include>common/styles.xml</include>
     10    <include>common/init.xml</include>
     11
     12    <include>campaign/simple_campaign/sprites.xml</include>
     13    <include>campaign/simple_campaign/styles.xml</include>
     14    <include>campaign/simple_campaign/mainmenu.xml</include>
     15</page>
  • new file inaries/data/mods/public/gui/campaign/simple_campaign/sprites.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_campaign/sprites.xml b/binaries/data/mods/public/gui/campaign/simple_campaign/sprites.xml
    new file mode 100644
    index 0000000..184f35f
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<sprites>
     4
     5    <sprite name="snLevelPreview">
     6        <image  texture="session/icons/mappreview/nopreview.png"
     7            size="0 0 100% 100%"
     8        />
     9    </sprite>
     10
     11</sprites>
  • new file inaries/data/mods/public/gui/campaign/simple_campaign/styles.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_campaign/styles.xml b/binaries/data/mods/public/gui/campaign/simple_campaign/styles.xml
    new file mode 100644
    index 0000000..89fe270
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<styles>
     4    <style name="ScenarioNameText"
     5        font="sans-bold-18"
     6        textcolor="white"
     7        text_align="center"
     8        text_valign="top"
     9    />
     10</styles>
  • new file inaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.js

    diff --git a/binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.js b/binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.js
    new file mode 100644
    index 0000000..477f750
    - +  
     1var g_CampaignsAvailable = {}; // "name of JSON file/ID of campaign" : data as parsed JSON
     2
     3var g_SelectedCampaign = null;
     4
     5/*
     6 * Initializes the campaign window.
     7 * Loads all campaigns
     8 * Allows you to start them.
     9 */
     10
     11// TODO: Should we support mods?
     12
     13function init(data)
     14{
     15    g_CampaignsAvailable = LoadAvailableCampaigns();
     16
     17    Engine.GetGUIObjectByName("CampaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
     18
     19    GenerateCampaignList();
     20}
     21
     22function GenerateCampaignList()
     23{
     24    let selection = Engine.GetGUIObjectByName("campaignSelection");
     25    if (selection.selected !== -1)
     26     displayCampaignDetails();
     27
     28    let list = [];
     29    for (let key in g_CampaignsAvailable)
     30        list.push({ "directories" : key, "name" : g_CampaignsAvailable[key].Name });
     31
     32    // change array of object into object of array.
     33    list = prepareForDropdown(list);
     34
     35    // Push to GUI
     36    selection.selected = -1;
     37    selection.list_name = list.name || [];
     38
     39    // Change these last, otherwise crash
     40    // TODO: do we need both of those? I'm unsure.
     41    selection.list = list.directories || [];
     42    selection.list_data = list.directories || [];
     43
     44//  replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
     45
     46//  displayReplayDetails();
     47}
     48
     49function displayCampaignDetails()
     50{
     51    let selection = Engine.GetGUIObjectByName("campaignSelection");
     52    if (selection.selected === -1)
     53        return;
     54
     55    g_SelectedCampaign = selection.list[selection.selected];
     56
     57    Engine.GetGUIObjectByName("startCampButton").enabled = true;
     58    Engine.GetGUIObjectByName("campaignOptionsButton").enabled = true;
     59
     60    Engine.GetGUIObjectByName("CampaignTitle").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].Name); 
     61    Engine.GetGUIObjectByName("campaignDesc").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].Description);   
     62
     63    if (g_CampaignsAvailable[g_SelectedCampaign].Image)
     64        Engine.GetGUIObjectByName("CampaignImage").sprite = "stretched:" + g_CampaignsAvailable[g_SelectedCampaign].Image;
     65    else
     66        Engine.GetGUIObjectByName("CampaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
     67
     68}
     69
     70function startCampaign()
     71{
     72    Engine.PushGuiPage("page_newcampaign.xml", { "campaignID" : g_SelectedCampaign, "campaignData" : g_CampaignsAvailable[g_SelectedCampaign] });
     73}
  • new file inaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.xml b/binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.xml
    new file mode 100644
    index 0000000..a738040
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<objects>
     4
     5    <!-- Used to display game info. -->
     6    <script file="gui/common/color.js" />
     7    <script file="gui/common/functions_civinfo.js" />
     8    <script file="gui/common/functions_utility.js" />
     9    <script file="gui/common/gamedescription.js"/>
     10    <script file="gui/common/settings.js" />
     11
     12    <!-- Used to display message boxes. -->
     13    <script file="gui/common/functions_global_object.js" />
     14
     15    <!-- Used for engine + mod version checks and deleteTooltip. -->
     16    <script file="gui/common/functions_utility_loadsave.js" />
     17
     18    <script file="gui/campaign/campaign_io.js" />
     19    <script file="gui/campaign/simple_setup/campaignsetup.js" />
     20
     21    <!-- Everything displayed in the campaign menu. -->
     22    <object type="image" style="ModernWindow" size="0 0 100% 100%" name="campaignWindow">
     23
     24        <!-- Title -->
     25        <object style="ModernLabelText" type="text" size="50%-128 4 50%+128 36">
     26            <translatableAttribute id="caption">Campaigns</translatableAttribute>
     27        </object>
     28
     29        <object name="leftPanel" size="30 50 100%-515 100%-80">
     30            <!-- List of campaigns -->
     31            <object name="campaignSelection"
     32                size="0 0 100% 100%"
     33                style="ModernList"
     34                type="olist"
     35                sortable="true"
     36                selected_column="name"
     37                selected_column_order="-1"
     38                sprite_asc="ModernArrowDown"
     39                sprite_desc="ModernArrowUp"
     40                sprite_not_sorted="ModernNotSorted"
     41                font="sans-stroke-16"
     42                >
     43
     44                <action on="SelectionChange">displayCampaignDetails();</action>
     45                <!--<action on="SelectionColumnChange">displayReplayList();</action>-->
     46                <action on="mouseleftdoubleclickitem">startCampaign();</action>
     47
     48            <column id="name" color="172 172 212" width="99%">
     49                <translatableAttribute id="heading" context="campaignsetup">Name</translatableAttribute>
     50            </column>
     51            </object>
     52        </object>
     53
     54        <object name="rightPanel" size="100%-485 50 100%-30 100%-80">
     55            <object type="image" sprite="ModernDarkBoxGold" size="0 0 100% 342">
     56                <object name="CampaignImage" type="image" sprite="snCampaignImage" size="1 1 100%-1 100%-1">
     57                </object>
     58            </object>
     59            <object type="text" name="CampaignTitle" style="CampaignNameText" size="0 360 100% 390">
     60                <translatableAttribute id="caption">No campaign selected</translatableAttribute>
     61            </object>
     62            <object type="image" sprite="ModernDarkBoxGold" name="campaignDescBox" size="0 400 100% 100%-50" hidden="false">
     63                <object type="text" name="campaignDesc" style="LeftButtonText" font="sans-16" size="10 10 100%-10 100%-10">
     64                </object>
     65            </object>
     66            <object type="button" name="campaignOptionsButton" style="ModernButtonRed" size="100%-140 100%-30 100% 100%-0" enabled="false">
     67                <translatableAttribute id="caption">Options</translatableAttribute>
     68            </object>
     69        </object>
     70    </object>
     71
     72    <object name="bottomPanel" size="25 100%-55 100%-5 100%-25" >
     73
     74        <!-- Main Menu Button -->
     75        <object type="button" style="StoneButton" size="25 0 17%+25 100%">
     76            <translatableAttribute id="caption">Main Menu</translatableAttribute>
     77            <action on="Press">Engine.SwitchGuiPage("page_pregame.xml");</action>
     78        </object>
     79
     80        <!-- Start Campaign Button -->
     81        <object name="startCampButton" type="button" style="StoneButtonFancy" size="83%-25 0 100%-25 100%" enabled="false">
     82            <translatableAttribute id="caption">Start Campaign</translatableAttribute>
     83            <action on="Press">startCampaign();</action>
     84        </object>
     85
     86    </object>
     87</objects>
  • new file inaries/data/mods/public/gui/campaign/simple_setup/sprites.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_setup/sprites.xml b/binaries/data/mods/public/gui/campaign/simple_setup/sprites.xml
    new file mode 100644
    index 0000000..8a7e819
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<sprites>
     4
     5    <sprite name="snCampaignImage">
     6        <image  texture="session/icons/mappreview/nopreview.png"
     7            size="0 0 100% 100%"
     8        />
     9    </sprite>
     10
     11</sprites>
  • new file inaries/data/mods/public/gui/campaign/simple_setup/styles.xml

    diff --git a/binaries/data/mods/public/gui/campaign/simple_setup/styles.xml b/binaries/data/mods/public/gui/campaign/simple_setup/styles.xml
    new file mode 100644
    index 0000000..1be4e97
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2
     3<styles>
     4    <style name="CampaignNameText"
     5        font="sans-bold-18"
     6        textcolor="white"
     7        text_align="center"
     8        text_valign="top"
     9    />
     10</styles>
  • new file inaries/data/mods/public/gui/page_campaignsetup_simple.xml

    diff --git a/binaries/data/mods/public/gui/page_campaignsetup_simple.xml b/binaries/data/mods/public/gui/page_campaignsetup_simple.xml
    new file mode 100644
    index 0000000..56630b2
    - +  
     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/sprites.xml</include>
     9    <include>common/styles.xml</include>
     10
     11    <include>campaign/simple_setup/sprites.xml</include>
     12    <include>campaign/simple_setup/styles.xml</include>
     13    <include>campaign/simple_setup/campaignsetup.xml</include>
     14</page>
  • new file inaries/data/mods/public/gui/page_loadcampaign.xml

    diff --git a/binaries/data/mods/public/gui/page_loadcampaign.xml b/binaries/data/mods/public/gui/page_loadcampaign.xml
    new file mode 100644
    index 0000000..4d6770a
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<page>
     3    <!-- Window to load/delete existing campaign runs-->
     4    <include>common/modern/setup.xml</include>
     5    <include>common/modern/styles.xml</include>
     6    <include>common/modern/sprites.xml</include>
     7
     8    <include>common/setup.xml</include>
     9    <include>common/sprites.xml</include>
     10    <include>common/styles.xml</include>
     11
     12    <include>campaign/load.xml</include>
     13   
     14</page>
  • new file inaries/data/mods/public/gui/page_newcampaign.xml

    diff --git a/binaries/data/mods/public/gui/page_newcampaign.xml b/binaries/data/mods/public/gui/page_newcampaign.xml
    new file mode 100644
    index 0000000..9ece28d
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<page>
     3    <!-- Screen that pops up when you start a new campaign, asking you to name it. Will then create the file with the according name and start everything up. -->
     4    <include>common/modern/setup.xml</include>
     5    <include>common/modern/styles.xml</include>
     6    <include>common/modern/sprites.xml</include>
     7
     8    <include>common/setup.xml</include>
     9    <include>common/sprites.xml</include>
     10    <include>common/styles.xml</include>
     11
     12    <include>campaign/newcampaign_modal.xml</include>
     13   
     14</page>
  • binaries/data/mods/public/gui/pregame/mainmenu.js

    diff --git a/binaries/data/mods/public/gui/pregame/mainmenu.js b/binaries/data/mods/public/gui/pregame/mainmenu.js
    index aa93c71..7706d73 100644
    a b function init(initData, hotloadData)  
    3535        guiObj.sprite = layerset[i].sprite;
    3636        guiObj.z = i;
    3737    }
     38
     39    // Enable campaign button if we have campaigns available
     40    if (Object.keys(LoadAvailableCampaigns()).length !== 0)
     41    {
     42        Engine.GetGUIObjectByName("subMenuNewCampaignButton").enabled = true;
     43        // TODO
     44        // Engine.GetGUIObjectByName("subMenuLoadCampaignButton").enabled = true;
     45        // Continue if it seems we would be able to.
     46        if (canLoadCurrentCampaign())
     47            Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = true;
     48    }
    3849}
    3950
    4051function getHotloadData()
  • binaries/data/mods/public/gui/pregame/mainmenu.xml

    diff --git a/binaries/data/mods/public/gui/pregame/mainmenu.xml b/binaries/data/mods/public/gui/pregame/mainmenu.xml
    index f040bd1..ce0d181 100644
    a b  
    55    <script file="gui/common/functions_global_object.js"/>
    66    <script file="gui/common/functions_utility_error.js"/>
    77    <script file="gui/pregame/mainmenu.js"/>
     8    <script file="gui/campaign/campaign.js"/>
     9    <script file="gui/campaign/campaign_io.js"/>
    810    <script directory="gui/pregame/backgrounds/"/>
    911
    1012
     
    212214                    </action>
    213215                </object>
    214216
    215                 <object name="subMenuCampaignButton"
     217                <object name="subMenuLoadButton"
    216218                    type="button"
    217219                    style="StoneButtonFancy"
    218220                    size="0 32 100% 60"
    219221                    tooltip_style="pgToolTip"
    220                     enabled="false"
    221222                >
    222                     <translatableAttribute id="caption">Campaigns</translatableAttribute>
    223                     <translatableAttribute id="tooltip">Relive history through historical military campaigns. \[NOT YET IMPLEMENTED]</translatableAttribute>
     223                    <translatableAttribute id="caption">Load Game</translatableAttribute>
     224                    <translatableAttribute id="tooltip">Click here to load a saved game.</translatableAttribute>
    224225                    <action on="Press">
    225                     closeMenu();
    226                         <![CDATA[
    227                             // Open Campaigns window.
    228                             // NOT IMPLEMENTED YET
    229                         ]]>
     226                        closeMenu();
     227                        Engine.PushGuiPage("page_loadgame.xml", { type: "offline" });
    230228                    </action>
    231229                </object>
    232230
    233                 <object name="subMenuLoadButton"
     231                <object name="subMenuContinueCampaignButton"
    234232                    type="button"
    235233                    style="StoneButtonFancy"
    236234                    size="0 64 100% 92"
    237235                    tooltip_style="pgToolTip"
     236                    enabled="false"
    238237                >
    239                     <translatableAttribute id="caption">Load Game</translatableAttribute>
    240                     <translatableAttribute id="tooltip">Click here to load a saved game.</translatableAttribute>
     238                    <translatableAttribute id="caption">Continue Campaign</translatableAttribute>
     239                    <translatableAttribute id="tooltip">Click here to load your latest campaign.</translatableAttribute>
     240                    <action on="Press">
     241                        loadCurrentCampaignSave();
     242                    </action>
     243                </object>
     244
     245                <object name="subMenuNewCampaignButton"
     246                    type="button"
     247                    style="StoneButtonFancy"
     248                    size="0 96 100% 124"
     249                    tooltip_style="pgToolTip"
     250                    enabled="false"
     251                >
     252                    <translatableAttribute id="caption">New Campaign</translatableAttribute>
     253                    <translatableAttribute id="tooltip">Relive history through historical military campaigns. \[WIP]</translatableAttribute>
    241254                    <action on="Press">
    242255                        closeMenu();
    243                         Engine.PushGuiPage("page_loadgame.xml", { type: "offline" });
     256                        Engine.PushGuiPage("page_campaignsetup_simple.xml", { type: "offline" });
    244257                    </action>
    245258                </object>
    246259
     260                <object name="subMenuLoadCampaignButton"
     261                    type="button"
     262                    style="StoneButtonFancy"
     263                    size="0 128 100% 156"
     264                    tooltip_style="pgToolTip"
     265                >
     266                    <translatableAttribute id="caption">Load Campaign</translatableAttribute>
     267                    <translatableAttribute id="tooltip">Click here to resume an existing campaign.</translatableAttribute>
     268                    <action on="Press">
     269                        closeMenu();
     270                        Engine.PushGuiPage("page_loadcampaign.xml", { type: "offline" });
     271                    </action>
     272                </object>
    247273            </object>
    248274
    249275            <!-- submenuMultiplayer -->
     
    468494                    <translatableAttribute id="tooltip">Challenge the computer player to a single player match.</translatableAttribute>
    469495                    <action on="Press">
    470496                        closeMenu();
    471                         openMenu("submenuSinglePlayer", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 3);
     497                        openMenu("submenuSinglePlayer", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 5);
    472498                    </action>
    473499                </object>
    474500
  • binaries/data/mods/public/gui/session/session.js

    diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js
    index ada8705..d7ff7b8 100644
    a b function leaveGame(willRejoin)  
    581581    let replayDirectory = Engine.GetCurrentReplayDirectory();
    582582    let simData = getReplayMetadata();
    583583
     584    let campaignData = null;
     585    if (Engine.GetInitAttributes().campaignData)
     586    {
     587        campaignData = Engine.GetInitAttributes().campaignData;
     588        campaignData.endGameData = Engine.GuiInterfaceCall("GetEndGameCampaignData");
     589        campaignGameEnded(campaignData);
     590    }
     591
    584592    Engine.EndGame();
    585593
    586594    if (g_IsController && Engine.HasXmppClient())
    function leaveGame(willRejoin)  
    594602            "isReplay": g_IsReplay,
    595603            "replayDirectory": !g_HasRejoined && replayDirectory,
    596604            "replaySelectionData": g_ReplaySelectionData
    597         }
     605        },
     606        "campaignData" : campaignData
    598607    });
    599608}
    600609
  • binaries/data/mods/public/gui/session/session.xml

    diff --git a/binaries/data/mods/public/gui/session/session.xml b/binaries/data/mods/public/gui/session/session.xml
    index 510d4c6..871633f 100644
    a b  
    1515<script file="gui/common/timer.js"/>
    1616<script file="gui/common/tooltips.js"/>
    1717
     18<!-- If this was a campaign game, we need to tell the campaign -->
     19<script file="gui/campaign/campaign.js"/>
     20<script file="gui/campaign/campaign_io.js"/>
     21
    1822<script directory="gui/session/"/>
    1923
    2024<object name="sn" hotkey="session.gui.toggle">
  • binaries/data/mods/public/gui/summary/summary.js

    diff --git a/binaries/data/mods/public/gui/summary/summary.js b/binaries/data/mods/public/gui/summary/summary.js
    index c3cf891..9c2b869 100644
    a b function continueButton()  
    170170        });
    171171    else if (Engine.HasXmppClient())
    172172        Engine.SwitchGuiPage("page_lobby.xml");
     173    else if (g_GameData.campaignData)
     174        loadCurrentCampaignSave();
    173175    else
    174176        Engine.SwitchGuiPage("page_pregame.xml");
    175177}
  • binaries/data/mods/public/gui/summary/summary.xml

    diff --git a/binaries/data/mods/public/gui/summary/summary.xml b/binaries/data/mods/public/gui/summary/summary.xml
    index 137d1c4..72f28c6 100644
    a b  
    1313    <!-- Needs the colors from summary.js -->
    1414    <script file="gui/summary/layout.js"/>
    1515
     16    <!-- Load the campaign screen if this was a campaign -->
     17    <script file="gui/campaign/campaign.js"/>
     18    <script file="gui/campaign/campaign_io.js"/>
     19
    1620    <object name="summaryWindow"
    1721        type="image"
    1822        style="ModernWindow"
  • binaries/data/mods/public/simulation/components/GuiInterface.js

    diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
    index 932c8cd..1cacbe0 100644
    a b GuiInterface.prototype.GetExtendedSimulationState = function()  
    199199    return ret;
    200200};
    201201
     202GuiInterface.prototype.GetEndGameCampaignData = function(player)
     203{
     204    // TODO here: return some relevant information, possibly based on initial attributes + some trigger data?
     205
     206    // return whether player lost or won at the very least.
     207    let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
     208    let status = cmpPlayer.GetState();
     209
     210    return {"status" : status};
     211}
     212
    202213GuiInterface.prototype.GetRenamedEntities = function(player)
    203214{
    204215    if (this.miragedEntities[player])
    let exposedFunctions = {  
    19681979
    19691980    "GetSimulationState": 1,
    19701981    "GetExtendedSimulationState": 1,
     1982    "GetEndGameCampaignData": 1,
    19711983    "GetRenamedEntities": 1,
    19721984    "ClearRenamedEntities": 1,
    19731985    "GetEntityState": 1,
  • source/gui/scripting/ScriptFunctions.cpp

    diff --git a/source/gui/scripting/ScriptFunctions.cpp b/source/gui/scripting/ScriptFunctions.cpp
    index a366c90..baf3382 100644
    a b  
    3939#include "network/NetClient.h"
    4040#include "network/NetServer.h"
    4141#include "network/NetTurnManager.h"
     42#include "ps/Campaigns.h"
    4243#include "ps/CConsole.h"
    4344#include "ps/CLogger.h"
    4445#include "ps/Errors.h"
    JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate)  
    250251    return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface));
    251252}
    252253
     254void SaveCampaign(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, JS::HandleValue metadata)
     255{
     256    shared_ptr<ScriptInterface::StructuredClone> metadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(metadata);
     257    if (Campaigns::Save(*(pCxPrivate->pScriptInterface), filename, metadataClone) < 0)
     258        LOGERROR("Failed to save campaign state");
     259}
     260
     261JS::Value LoadCampaign(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& campaignName)
     262{
     263    JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
     264    JSAutoRequest rq(cx);
     265
     266    JS::RootedValue campaignData(cx);
     267    Status err = Campaigns::Load(*(pCxPrivate->pScriptInterface), campaignName, &campaignData);
     268    if (err < 0)
     269        return JS::UndefinedValue();
     270
     271    return campaignData;
     272}
     273
     274bool DeleteCampaignGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
     275{
     276    return Campaigns::DeleteGame(name);
     277}
     278
    253279void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
    254280{
    255281    ENSURE(g_NetClient);
    void SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring&  
    334360        LOGERROR("Failed to save game");
    335361}
    336362
     363JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
     364{
     365    return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
     366}
     367
     368bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
     369{
     370    return SavedGames::DeleteSavedGame(name);
     371}
     372
    337373void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
    338374{
    339375    ENSURE(g_NetClient);
    JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate)  
    465501    return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface));
    466502}
    467503
    468 JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
    469 {
    470     return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
    471 }
    472 
    473 bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
    474 {
    475     return SavedGames::DeleteSavedGame(name);
    476 }
    477 
    478504void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url)
    479505{
    480506    sys_open_url(url);
    void GuiScriptingInit(ScriptInterface& scriptInterface)  
    10541080    scriptInterface.RegisterFunction<JS::Value, &GetAIs>("GetAIs");
    10551081    scriptInterface.RegisterFunction<JS::Value, &GetEngineInfo>("GetEngineInfo");
    10561082
     1083    // Campaigns
     1084    scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, &SaveCampaign>("SaveCampaign");
     1085    scriptInterface.RegisterFunction<JS::Value, std::wstring, &LoadCampaign>("LoadCampaign");
     1086    scriptInterface.RegisterFunction<bool, std::wstring, &DeleteCampaignGame>("DeleteCampaignGame");
     1087
    10571088    // Saved games
    10581089    scriptInterface.RegisterFunction<JS::Value, std::wstring, &StartSavedGame>("StartSavedGame");
    10591090    scriptInterface.RegisterFunction<JS::Value, &GetSavedGames>("GetSavedGames");
  • new file source/ps/Campaigns.cpp

    diff --git a/source/ps/Campaigns.cpp b/source/ps/Campaigns.cpp
    new file mode 100644
    index 0000000..397e28c
    - +  
     1/* Copyright (C) 2016 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 "Campaigns.h"
     21
     22#include "graphics/GameView.h"
     23#include "gui/GUIManager.h"
     24#include "lib/allocators/shared_ptr.h"
     25#include "i18n/L10n.h"
     26#include "lib/utf8.h"
     27#include "ps/CLogger.h"
     28#include "ps/FileIo.h"
     29#include "ps/Filesystem.h"
     30#include "ps/Game.h"
     31#include "ps/Mod.h"
     32#include "ps/Pyrogenesis.h"
     33#include "ps/XML/Xeromyces.h"
     34#include "scriptinterface/ScriptInterface.h"
     35#include "simulation2/Simulation2.h"
     36
     37static const int CAMPAIGN_VERSION = 1; // increment on incompatible changes to the format
     38
     39// TODO: we ought to check version numbers when loading files
     40
     41Status Campaigns::Save(ScriptInterface& scriptInterface, const CStrW& name, const shared_ptr<ScriptInterface::StructuredClone>& metadataClone)
     42{
     43    JSContext* cx = scriptInterface.GetContext();
     44    JSAutoRequest rq(cx);
     45
     46    // Determine the filename to save under
     47    const VfsPath basenameFormat(L"campaignsaves/" + name);
     48    const VfsPath filename = basenameFormat.ChangeExtension(L".0adcampaign");
     49
     50    time_t now = time(NULL);
     51
     52    JS::RootedValue metadata(cx);
     53    scriptInterface.Eval("({})", &metadata);
     54    scriptInterface.SetProperty(metadata, "engine_version", std::string(engine_version));
     55    scriptInterface.SetProperty(metadata, "mods", g_modsLoaded);
     56    scriptInterface.SetProperty(metadata, "time", (double)now);
     57
     58    JS::RootedValue campaignState(cx);
     59    scriptInterface.ReadStructuredClone(metadataClone, &campaignState);
     60
     61    scriptInterface.SetProperty(metadata, "campaign_state", campaignState);
     62
     63    std::string dataString = scriptInterface.StringifyJSON(&metadata, true);
     64
     65    // ensure we won't crash if the save somehow fails.
     66    try
     67    {
     68        CFilePacker packer(CAMPAIGN_VERSION, "CSST");
     69        packer.PackString(dataString);
     70        packer.Write(filename);
     71    }
     72    catch (PSERROR_File_WriteFailed&)
     73    {
     74        LOGERROR("Failed to write campaign '%s'", filename.string8());
     75        return ERR::FAIL;
     76    }
     77
     78    LOGMESSAGERENDER(g_L10n.Translate("Saved campaign to '%s'"), filename.string8());
     79    debug_printf("Saved campaign to '%s'\n", filename.string8().c_str());
     80
     81    return INFO::OK;
     82}
     83
     84Status Campaigns::Load(ScriptInterface& scriptInterface, const std::wstring& name, JS::MutableHandleValue campaignData)
     85{
     86    // Determine the filename to load
     87    const VfsPath basename(L"campaignsaves/" + name);
     88    const VfsPath filename = basename.ChangeExtension(L".0adcampaign");
     89
     90    if (!VfsFileExists(filename))
     91        return ERR::FILE_NOT_FOUND;
     92
     93    CFileUnpacker unpacker;
     94    unpacker.Read(filename, "CSST");
     95
     96    if (unpacker.GetVersion() < CAMPAIGN_VERSION)
     97    {
     98        LOGWARNING("Campaign file is too old, version %i, current version %i", unpacker.GetVersion(), CAMPAIGN_VERSION);
     99        return ERR::FAIL;
     100    }
     101
     102    CStr datastream;
     103    unpacker.UnpackString(datastream);
     104
     105
     106    if (!scriptInterface.ParseJSON(datastream, campaignData))
     107    {
     108        LOGERROR("Error parsing campaign JSON.");
     109        return ERR::FAIL;
     110    }
     111
     112    return INFO::OK;
     113}
     114
     115bool Campaigns::DeleteGame(const std::wstring& name)
     116{
     117    const VfsPath basename(L"campaignsaves/" + name);
     118    const VfsPath filename = basename.ChangeExtension(L".0adcampaign");
     119    OsPath realpath;
     120
     121    // Make sure it exists in VFS and find its real path
     122    if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK)
     123        return false; // Error
     124
     125    // Remove from VFS
     126    if (g_VFS->RemoveFile(filename) != INFO::OK)
     127        return false; // Error
     128
     129    // Delete actual file
     130    if (wunlink(realpath) != 0)
     131        return false; // Error
     132
     133    // Successfully deleted file
     134    return true;
     135}
     136
  • new file source/ps/Campaigns.h

    diff --git a/source/ps/Campaigns.h b/source/ps/Campaigns.h
    new file mode 100644
    index 0000000..8e0f67a
    - +  
     1/* Copyright (C) 2016 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_CAMPAIGNS
     19#define INCLUDED_CAMPAIGNS
     20
     21#include "scriptinterface/ScriptInterface.h"
     22class CSimulation2;
     23class CGUIManager;
     24
     25/**
     26 * @file
     27 * Contains functions for managing campaign archives.
     28 *
     29 * A saved game is a simple *.0adcampaign file
     30 * which is a binary JSON file containing the campaign metadata
     31 */
     32
     33namespace Campaigns
     34{
     35
     36/**
     37 * Create new campaign file with given name and metadata
     38 *
     39 * @param scriptInterface
     40 * @param name Name of the campaign
     41 * @param MetadataClone Actual campaign Metadata
     42 * @return INFO::OK if successfully saved, else an error Status
     43 */
     44Status Save(ScriptInterface& scriptInterface, const CStrW& name, const shared_ptr<ScriptInterface::StructuredClone>& metadataClone);
     45
     46/**
     47 * Load campaign with the given name
     48 *
     49 * @param scriptInterface
     50 * @param name filename of campaign game (without path or extension)
     51 * @param[out] metadata object containing metadata associated with saved game,
     52 *  parsed from metadata.json inside the archive.
     53 * @return INFO::OK if successfully loaded, else an error Status
     54 */
     55Status Load(ScriptInterface& scriptInterface, const std::wstring& name, JS::MutableHandleValue campaignData);
     56
     57/**
     58 * Permanently deletes the saved campaign run with the given name
     59 *
     60 * @param name filename of saved campaign (without path or extension)
     61 * @return true if deletion was successful, or false on error
     62 */
     63bool DeleteGame(const std::wstring& name);
     64}
     65
     66#endif // INCLUDED_CAMPAIGNS
  • source/ps/GameSetup/GameSetup.cpp

    diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp
    index b834b63..b789a4a 100644
    a b static void InitVfs(const CmdLineArgs& args, int flags)  
    499499    // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
    500500    g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
    501501    g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
     502    g_VFS->Mount(L"campaignsaves/", paths.UserData()/"campaignsaves"/"");
     503
    502504    // Mounting with highest priority, so that a mod supplied user.cfg is harmless
    503505    g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
    504506    if(readonlyConfig != paths.Config())