Index: binaries/data/mods/public/campaigns/example.json
===================================================================
--- /dev/null
+++ binaries/data/mods/public/campaigns/example.json
@@ -0,0 +1,38 @@
+{
+ "Name": "Example Campaign",
+ "Description": "Lorem Ipsum and so on and so on",
+ "Interface": "simple_campaign",
+ "Levels": {
+ "Example_1": {
+ "Name": "Example 1",
+ "Map": "scenarios/Serengeti.xml",
+ "Description": "Whatever"
+ },
+ "Example_2": {
+ "Name": "Example 2",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_1"
+ },
+ "Example_3": {
+ "Name": "This one requires 1 and 2",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_1+Example_2"
+ },
+ "Example_4": {
+ "Name": "This one requires 2 or 3",
+ "Map": "",
+ "Description": "None",
+ "Requires": "Example_2 Example_3"
+ },
+ "Example_5": {
+ "Name": "This one unavailable if 1 isn't completed",
+ "Map": "",
+ "Description": "None",
+ "Requires": "!Example_1"
+ }
+ },
+ "Order": ["Example_1", "Example_2", "Example_3", "Example_4", "Example_5"],
+ "ShowUnavailable": true
+}
Index: binaries/data/mods/public/campaigns/tutorial.json
===================================================================
--- /dev/null
+++ binaries/data/mods/public/campaigns/tutorial.json
@@ -0,0 +1,22 @@
+{
+ "Name": "Tutorial",
+ "Description": "Learn how to play 0 A.D.",
+ "Interface": "simple_campaign",
+ "Image": "session/icons/mappreview/Introductory Tutorial.png",
+ "Levels": {
+ "introduction": {
+ "Name": "Introductory Tutorial",
+ "Map": "maps/tutorials/Introductory_Tutorial.xml",
+ "Description": "This is a basic tutorial to get you started playing 0 A.D.",
+ "Preview": "session/icons/mappreview/Introductory Tutorial.png"
+ },
+ "eco_walkthrough": {
+ "Name": "Economy Walkthrough",
+ "Map": "maps/tutorials/starting_economy_walkthrough.xml",
+ "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. Warning: This is very fast at the start, be prepared to run through the initial bit several times.",
+ "Requires": "introduction"
+ }
+ },
+ "Order": ["introduction", "eco_walkthrough"],
+ "ShowUnavailable": true
+}
Index: binaries/data/mods/public/gui/campaign/campaign.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/campaign.js
@@ -0,0 +1,58 @@
+/**
+ * ID of the current campaign. This is the name of the json file (not the "human readable" name)
+ */
+var g_CampaignID;
+
+/**
+ * Campaign template data from the JSON file.
+ */
+var g_CampaignTemplate;
+
+/**
+ * name of the file we're saving campaign data in
+ */
+var g_CampaignSaveFilename;
+
+/**
+ * Current campaign state, to be saved in/loaded from the above file
+ */
+var g_CampaignState;
+
+/**
+ * This function is called by session.js at the end of a game. It should save the campaign save state immediately.
+ */
+function onCampaignGameEnded(data)
+{
+ g_CampaignID = data.ID;
+ g_CampaignTemplate = data.template;
+ g_CampaignSaveFilename = data.save;
+ g_CampaignState = data.data;
+
+ if (data.endGameData.status === "won")
+ {
+ if (!g_CampaignState.completed)
+ g_CampaignState.completed = [];
+ if (g_CampaignState.completed.indexOf(data.level) == -1)
+ g_CampaignState.completed.push(data.level);
+ }
+
+ saveCurrentCampaign();
+}
+
+/**
+ * @param level the ID of the level.
+ * @returns true if the level is available.
+ */
+function meetsRequirements(level)
+{
+ return !level.Requires && g_CampaignState.completed && MatchesClassList(g_CampaignState.completed, level.Requires);
+}
+
+/**
+ * @param level the ID of the level.
+ * @returns true if the player has completed the level
+ */
+function hasCompleted(level)
+{
+ return g_CampaignState.completed && g_CampaignState.completed.indexOf(level.ID) != -1;
+}
Index: binaries/data/mods/public/gui/campaign/campaign_io.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/campaign_io.js
@@ -0,0 +1,125 @@
+/**
+ * This file provides helper functions related to loading/saving campaign save states and campaign templates
+ * Most functions rely on loading campaign.js
+ */
+
+/**
+ * @returns a dictionary of campaign templates, as [ { 'identifier': id, 'data': data }, ... ]
+ */
+function loadAvailableCampaignTemplates()
+{
+ let campaigns = Engine.ListDirectoryFiles("campaigns/", "*.json", false);
+
+ let ret = [];
+
+ for (let filename of campaigns)
+ {
+ let data = Engine.ReadJSONFile(filename);
+ if (!isValidCampaignTemplate(data))
+ continue;
+
+ // Use file name rather than a particular identifier to guarantee unicity.
+ ret.push({
+ "identifier": filename.slice("campaigns/".length, -".json".length),
+ "data": data
+ });
+ }
+
+ return ret;
+}
+
+/**
+ * @returns true if data (a JSON object) is a valid campaign template.
+ */
+function isValidCampaignTemplate(data)
+{
+ return data && data.Name;
+}
+
+function loadCampaignTemplate(name)
+{
+ let data = Engine.ReadJSONFile("campaigns/" + name + ".json");
+
+ if (!isValidCampaignTemplate(data))
+ {
+ warn("Campaign template " + name + " is not a valid campaign template.");
+ return false;
+ }
+
+ g_CampaignTemplate = data;
+
+ return true;
+}
+
+/**
+ * Check whether loading the current campaign can be loaded.
+ * @returns true if it can be loaded
+ */
+function canLoadCurrentCampaignSave()
+{
+ let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
+
+ if (!campaign)
+ return false;
+
+ if (!Engine.FileExists("campaignsaves/" + campaign + ".0adcampaign"))
+ return false;
+
+ // TODO: load up the file and run other checks, such as mod compatibility perhaps.
+ return true;
+}
+
+function loadCurrentCampaignSave()
+{
+ let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
+
+ if (!canLoadCurrentCampaignSave())
+ return false;
+
+ if (g_CampaignSaveFilename)
+ {
+ warn("Campaign already loaded");
+ return false;
+ }
+ g_CampaignSaveFilename = campaign;
+
+ let campaignData = loadCampaignSave(g_CampaignSaveFilename);
+ if (!campaignData)
+ {
+ warn("Campaign failed to load properly. Quitting campaign mode.")
+ return false;
+ }
+
+ g_CampaignState = campaignData;
+ g_CampaignID = g_CampaignState.campaign;
+
+ if (!loadCampaignTemplate(g_CampaignID))
+ return false;
+
+ Engine.SwitchGuiPage("campaign/" + g_CampaignTemplate.Interface + "/page_menu.xml", {
+ "ID" : g_CampaignID,
+ "template" : g_CampaignTemplate,
+ "save": g_CampaignSaveFilename,
+ "data" : g_CampaignState
+ });
+
+ return true;
+}
+
+function saveCurrentCampaign()
+{
+ if (!g_CampaignSaveFilename)
+ return false;
+
+ return saveCampaign(g_CampaignSaveFilename, g_CampaignState);
+}
+
+function saveCampaign(filename, data)
+{
+ Engine.WriteJSONFile("campaignsaves/" + filename + ".0adcampaign", data);
+}
+
+function loadCampaignSave(filename)
+{
+ return Engine.ReadJSONFile("campaignsaves/" + filename + ".0adcampaign");;
+}
Index: binaries/data/mods/public/gui/campaign/load.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load.js
@@ -0,0 +1,108 @@
+function init()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+
+ let campaigns = Engine.ListDirectoryFiles("campaignsaves/", "*.0adcampaign", false);
+
+ if (!campaigns.length)
+ {
+ gameSelection.list = [translate("No ongoing campaigns found")];
+ gameSelection.selected = -1;
+ selectionChanged();
+ Engine.GetGUIObjectByName("loadGameButton").enabled = false;
+ Engine.GetGUIObjectByName("deleteGameButton").enabled = false;
+ return;
+ }
+
+ gameSelection.list = campaigns.map(path => generateLabel(pathToGame(path)));
+ gameSelection.list_data = campaigns.map(path => pathToGame(path));
+
+ if (gameSelection.selected == -1)
+ gameSelection.selected = 0;
+ else if (gameSelection.selected >= campaigns.length) // happens when deleting the last saved game
+ gameSelection.selected = campaigns.length - 1;
+ else
+ selectionChanged();
+}
+
+function pathToGame(path)
+{
+ return path.replace("campaignsaves/", "").replace(".0adcampaign", "");
+}
+
+function generateLabel(game)
+{
+ let campaignData = loadCampaignSave(game);
+ if (!campaignData)
+ return "Incompatible - " + game;
+
+ if (!loadCampaignTemplate(campaignData.campaign))
+ return "Incompatible - " + game;
+
+ return sprintf(translate("%(userDesc)s - %(campaignName)s"), {
+ "userDesc": campaignData.userDescription,
+ "campaignName": g_CampaignTemplate.Name
+ });
+}
+
+function selectionChanged()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+ let selectionEmpty = gameSelection.selected == -1;
+
+ if (selectionEmpty)
+ return;
+}
+
+function loadCampaign()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+
+ let campaign = gameSelection.list_data[gameSelection.selected];
+ reallyLoadCampaign(campaign);
+
+ // TODO: compatibility checks of saved games
+}
+
+function reallyLoadCampaign(name)
+{
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", name);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", name, "config/user.cfg");
+
+ loadCurrentCampaignSave();
+}
+
+function deleteCampaign()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+ let campaign = gameSelection.list_data[gameSelection.selected];
+
+ if (!campaign)
+ return;
+
+ messageBox(
+ 500, 200,
+ sprintf(translate("\"%(filename)s\""), {
+ "filename": gameSelection.list[gameSelection.selected]
+ }) + "\n" + translate("Campaign will be permanently deleted, are you sure?"),
+ translate("DELETE"),
+ [translate("No"), translate("Yes")],
+ [null, function(){ reallyDeleteCampaign(campaign); }]
+ );
+}
+
+function reallyDeleteCampaign(name)
+{
+ if (!Engine.DeleteJSONFile("campaignsaves/" + name + ".0adcampaign"))
+ return;
+
+ if (Engine.ConfigDB_GetValue("user", "currentcampaign") === name)
+ {
+ Engine.ConfigDB_RemoveValue("user", "currentcampaign");
+ Engine.ConfigDB_WriteFile("user", "config/user.cfg");
+
+ Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false;
+ }
+
+ init();
+}
Index: binaries/data/mods/public/gui/campaign/load.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/newcampaign_modal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/newcampaign_modal.js
@@ -0,0 +1,61 @@
+var g_CampaignID = null;
+var g_CampaignState = null;
+
+function init(data)
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+ gameSelection.selected = -1;
+
+ let campaigns = Engine.ListDirectoryFiles("campaignsaves/", "*.0adcampaign", false);
+ if (!campaigns.length)
+ {
+ gameSelection.list = [translate("No ongoing campaigns.")];
+ gameSelection.list_data = [""];
+ }
+ else
+ {
+ gameSelection.list = campaigns.map(path => generateLabel(pathToGame(path)));
+ gameSelection.list_data = campaigns.map(path => pathToGame(path));
+ }
+
+ if (data)
+ {
+ g_CampaignID = data.campaignID;
+ g_CampaignState = data.campaignData;
+ }
+}
+
+function selectionChanged()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+
+ Engine.GetGUIObjectByName("deleteGameButton").enabled = gameSelection.selected !== -1;
+}
+
+function createAndStartCampaign()
+{
+ let name = g_CampaignID + "_1";
+
+ // TODO: use VFS::NextNumberedFilename like save games instead.
+ if (Engine.FileExists("campaignsaves/" + name + ".0adcampaign"))
+ {
+ // get other campaigns following that template
+ let campaigns = Engine.ListDirectoryFiles("campaignsaves/", g_CampaignID + "_*.0adcampaign", false);
+ let max = 1;
+ for (let camp of campaigns)
+ {
+ let nb = camp.replace("campaignsaves/" + g_CampaignID + "_", "").replace(".0adcampaign", "");
+ if (+nb > max)
+ max = +nb;
+ }
+ name = g_CampaignID + "_" + (max+1);
+ }
+
+ saveCampaign(name, { "userDescription": Engine.GetGUIObjectByName("saveGameDesc").caption, "campaign": g_CampaignID });
+
+ // inform user config that we are playing this campaign
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", name);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", name, "config/user.cfg");
+
+ loadCurrentCampaignSave();
+}
Index: binaries/data/mods/public/gui/campaign/newcampaign_modal.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/newcampaign_modal.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New Campaign
+
+
+
+ selectionChanged();
+
+
+
+ Name of this campaign run:
+
+
+
+ createAndStartCampaign();
+
+
+
+ Cancel
+ Engine.PopGuiPage();
+
+
+
+ Delete
+ deleteCampaign();
+
+
+
+ Start
+ createAndStartCampaign();
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.js
@@ -0,0 +1,198 @@
+var g_SelectedLevel;
+
+var g_SavedGamesMetadata = [];
+
+function init(data)
+{
+ if (!data)
+ {
+ warn("Loading campaign menu without a campaign loaded");
+ return;
+ }
+
+ g_CampaignID = data.ID;
+ g_CampaignTemplate = data.template;
+ g_CampaignSaveFilename = data.save;
+ g_CampaignState = data.data;
+
+ generateLevelList();
+ selectionChanged();
+
+ Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+ let savedGames = Engine.GetSavedGames().sort(sortDecreasingDate).filter(game => game.metadata.initAttributes && game.metadata.initAttributes.campaignData && game.metadata.initAttributes.campaignData.ID == g_CampaignID);
+ gameSelection.enabled = !!savedGames.length;
+ if (!savedGames.length)
+ {
+ gameSelection.list = [translate("No saved games found")];
+ gameSelection.selected = -1;
+ return;
+ }
+
+ // Get current game version and loaded mods
+ let engineInfo = Engine.GetEngineInfo();
+
+ g_SavedGamesMetadata = savedGames.map(game => game.metadata);
+
+ gameSelection.list = savedGames.map(game => generateCampaignLabel(game.metadata, engineInfo));
+ gameSelection.list_data = savedGames.map(game => game.id);
+
+ saveSelectionChanged();
+}
+
+function generateLevelList()
+{
+ let selection = Engine.GetGUIObjectByName("levelSelection");
+
+ let list = [];
+ for (let key in g_CampaignTemplate.Levels)
+ {
+ let level = g_CampaignTemplate.Levels[key];
+
+ if (!("ShowUnavailable" in g_CampaignTemplate) || !g_CampaignTemplate.ShowUnavailable && !meetsRequirements(level))
+ continue;
+
+ let status = "";
+ let name = level.Name;
+ if (!meetsRequirements(level))
+ {
+ status = translate("not unlocked yet");
+ name = "[color=\"gray\"]" + name + "[/color]";
+ }
+ list.push({ "ID": key, "name": name, "status": status });
+ }
+ list.sort((a, b) => g_CampaignTemplate.Order.indexOf(a.ID) - g_CampaignTemplate.Order.indexOf(b.ID));
+
+ list = prepareForDropdown(list);
+
+ selection.selected = -1;
+ selection.list_name = list.name || [];
+ selection.list_status = list.status || [];
+
+ // These must be changed last or things crash.
+ selection.list = list.ID || [];
+ selection.list_data = list.ID || [];
+}
+
+function displayLevelDetails(levelID)
+{
+ let level = g_CampaignTemplate.Levels[levelID];
+
+ Engine.GetGUIObjectByName("scenarioName").caption = translate(level.Name);
+ Engine.GetGUIObjectByName("scenarioDesc").caption = translate(level.Description);
+
+ setMapPreviewImage("mapPreview", level.Preview);
+
+ g_SelectedLevel = levelID;
+
+ if (!meetsRequirements(level))
+ {
+ Engine.GetGUIObjectByName("startButton").enabled = false;
+ return;
+ }
+
+ Engine.GetGUIObjectByName("startButton").enabled = true;
+}
+
+function selectionChanged()
+{
+ let selection = Engine.GetGUIObjectByName("levelSelection");
+
+ if (selection.selected === -1)
+ {
+ Engine.GetGUIObjectByName("startButton").enabled = false;
+ Engine.GetGUIObjectByName("startButton").hidden = false;
+ Engine.GetGUIObjectByName("loadSavedButton").hidden = true;
+ return;
+ }
+
+ Engine.GetGUIObjectByName("loadSavedButton").hidden = true;
+ Engine.GetGUIObjectByName("startButton").hidden = false;
+
+ let selec = Engine.GetGUIObjectByName("gameSelection");
+ selec.selected = -1;
+
+ displayLevelDetails(selection.list[selection.selected]);
+}
+
+function saveSelectionChanged()
+{
+ let metadata = g_SavedGamesMetadata[Engine.GetGUIObjectByName("gameSelection").selected];
+
+ if (!metadata)
+ return;
+
+ // fetch campaign scenario metadata if present.
+ let scenarioID = metadata.initAttributes.campaignData.level;
+
+ let selection = Engine.GetGUIObjectByName("levelSelection");
+ selection.selected = -1;
+ for (let level of selection.list_data)
+ if (level == scenarioID)
+ {
+ displayLevelDetails(level);
+ break;
+ }
+
+ Engine.GetGUIObjectByName("loadSavedButton").hidden = false;
+ Engine.GetGUIObjectByName("startButton").hidden = true;
+}
+
+function generateCampaignLabel(metadata, engineInfo)
+{
+ let dateTimeString = Engine.FormatMillisecondsIntoDateStringLocal(metadata.time*1000, translate("yyyy-MM-dd HH:mm:ss"));
+ let dateString = sprintf(translate("\\[%(date)s]"), { "date": dateTimeString });
+
+ if (engineInfo)
+ {
+ if (!isCompatibleSavegame(metadata, engineInfo) || !hasSameEngineVersion(metadata, engineInfo))
+ dateString = "[color=\"red\"]" + dateString + "[/color]";
+ else if (!hasSameMods(metadata, engineInfo))
+ dateString = "[color=\"orange\"]" + dateString + "[/color]";
+ }
+
+ return sprintf(
+ metadata.description ?
+ translate("%(dateString)s %(level)s - %(description)s") :
+ translate("%(dateString)s %(level)s"),
+ {
+ "dateString": dateString,
+ "level": metadata.initAttributes.campaignData.template.Levels[metadata.initAttributes.campaignData.level].Name,
+ "description": metadata.description || ""
+ }
+ );
+}
+
+function exitCampaignMode(exitGame = false)
+{
+ saveCurrentCampaign();
+
+ if (exitGame)
+ {
+ messageBox(
+ 400, 200,
+ translate("Are you sure you want to quit 0 A.D.?"),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [null, Engine.Exit]
+ );
+ return;
+ }
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+}
+
+function startSelectedLevel()
+{
+ Engine.SwitchGuiPage("page_gamesetup.xml", {
+ "type": "offline",
+ "autostart": g_CampaignTemplate.Levels[g_SelectedLevel].Map,
+ "campaignData": {
+ "ID": g_CampaignID,
+ "template": g_CampaignTemplate,
+ "save": g_CampaignSaveFilename,
+ "data": g_CampaignState,
+ "level": g_SelectedLevel
+ }
+ });
+}
Index: binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_campaign/mainmenu.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaign Name
+
+
+
+
+
+
+ selectionChanged();
+
+
+
+ Scenario Name
+
+
+
+ Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No scenario selected
+
+
+
+
+
+
+
+
+
+
+
+ Saved Games
+
+
+
+ saveSelectionChanged();
+
+
+
+
+
+
+
+
+ Back to Main Menu
+ exitCampaignMode();
+
+
+
+ Start Scenario
+
+ startSelectedLevel();
+
+
+
+ Resume Saved Game
+
+ loadGame();
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/simple_campaign/page_menu.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_campaign/page_menu.xml
@@ -0,0 +1,12 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/simple_campaign/mainmenu.xml
+
Index: binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.js
@@ -0,0 +1,60 @@
+// "name of JSON file/ID of campaign" : data as parsed JSON
+var g_CampaignsAvailable = {};
+
+var g_SelectedCampaign = null;
+
+/**
+ * Initializes the campaign window.
+ * Loads all campaigns
+ * Allows you to start them.
+ */
+
+function init(data)
+{
+ g_CampaignsAvailable = loadAvailableCampaignTemplates();
+
+ generateCampaignList();
+}
+
+function generateCampaignList()
+{
+ let selection = Engine.GetGUIObjectByName("campaignSelection");
+ if (selection.selected !== -1)
+ displayCampaignDetails();
+
+
+ selection.selected = -1;
+ selection.list_name = g_CampaignsAvailable.map((camp) => camp.data.Name);
+
+ // These must be changed last or things crash.
+ selection.list = g_CampaignsAvailable.map((camp) => camp.identifier) || [];
+ selection.list_data = g_CampaignsAvailable.map((camp) => camp.identifier) || [];
+}
+
+function displayCampaignDetails()
+{
+ let selection = Engine.GetGUIObjectByName("campaignSelection");
+ if (selection.selected === -1)
+ return;
+
+ g_SelectedCampaign = selection.selected;
+
+ Engine.GetGUIObjectByName("startCampButton").enabled = true;
+
+ Engine.GetGUIObjectByName("campaignTitle").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].data.Name);
+ Engine.GetGUIObjectByName("campaignDesc").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].data.Description);
+
+ if (g_CampaignsAvailable[g_SelectedCampaign].data.Image)
+ Engine.GetGUIObjectByName("campaignImage").sprite = "stretched:" + g_CampaignsAvailable[g_SelectedCampaign].data.Image;
+ else
+ Engine.GetGUIObjectByName("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+}
+
+function startCampaign()
+{
+ Engine.PushGuiPage("page_newcampaign.xml", {
+ "campaignID": g_CampaignsAvailable[g_SelectedCampaign].identifier,
+ "campaignData": g_CampaignsAvailable[g_SelectedCampaign].data
+ });
+}
Index: binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_setup/campaignsetup.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaigns
+
+
+
+
+
+
+ displayCampaignDetails();
+ startCampaign();
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+ No Campaign selected
+
+
+
+
+
+
+
+
+
+
+
+
+ Main Menu
+ Engine.SwitchGuiPage("page_pregame.xml");
+
+
+
+
+ Start Campaign
+ startCampaign();
+
+
+
+
Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- binaries/data/mods/public/gui/gamesetup/gamesetup.js
+++ binaries/data/mods/public/gui/gamesetup/gamesetup.js
@@ -233,9 +233,9 @@
const g_IsController = !g_IsNetworked || Engine.HasNetServer();
/**
- * Whether this is a tutorial.
+ * If non-null, autostart (without showing the GUI page) the scenario.
*/
-var g_IsTutorial;
+var g_AutostartScenario = null;
/**
* To report the game to the lobby bot.
@@ -1113,7 +1113,7 @@
/**
* Initializes some globals without touching the GUI.
*
- * @param {Object} attribs - context data sent by the lobby / mainmenu
+ * @param {Object} attribs - context data sent by the lobby / mainmenu / campaign setup screen
*/
function init(attribs)
{
@@ -1123,11 +1123,14 @@
return;
}
- g_IsTutorial = !!attribs.tutorial;
+ g_AutostartScenario = attribs.autostart || undefined;
g_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
g_StunEndpoint = attribs.stunEndpoint;
+ if (attribs.campaignData)
+ g_GameAttributes.campaignData = attribs.campaignData || undefined;
+
if (!g_IsNetworked)
g_PlayerAssignments = {
"local": {
@@ -1201,9 +1204,9 @@
updateGameAttributes();
sendRegisterGameStanzaImmediate();
- if (g_IsTutorial)
+ if (g_AutostartScenario)
{
- launchTutorial();
+ autostartScenario();
return;
}
@@ -1851,7 +1854,7 @@
*/
function loadPersistMatchSettings()
{
- if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_IsTutorial)
+ if (!g_IsController || Engine.ConfigDB_GetValue("user", "persistmatchsettings") != "true" || g_AutostartScenario)
return;
let settingsFile = g_IsNetworked ? g_MatchSettings_MP : g_MatchSettings_SP;
@@ -1911,7 +1914,7 @@
function savePersistMatchSettings()
{
- if (g_IsTutorial)
+ if (g_AutostartScenario)
return;
Engine.WriteJSONFile(
@@ -2293,10 +2296,10 @@
}
}
-function launchTutorial()
+function autostartScenario()
{
g_GameAttributes.mapType = "scenario";
- selectMap("maps/tutorials/starting_economy_walkthrough");
+ selectMap(g_AutostartScenario);
launchGame();
}
Index: binaries/data/mods/public/gui/page_campaignsetup_simple.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/page_campaignsetup_simple.xml
@@ -0,0 +1,12 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/simple_setup/campaignsetup.xml
+
Index: binaries/data/mods/public/gui/page_loadcampaign.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/page_loadcampaign.xml
@@ -0,0 +1,13 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/load.xml
+
+
Index: binaries/data/mods/public/gui/page_newcampaign.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/page_newcampaign.xml
@@ -0,0 +1,14 @@
+
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/newcampaign_modal.xml
+
+
Index: binaries/data/mods/public/gui/pregame/mainmenu.js
===================================================================
--- binaries/data/mods/public/gui/pregame/mainmenu.js
+++ binaries/data/mods/public/gui/pregame/mainmenu.js
@@ -48,6 +48,13 @@
Engine.GetGUIObjectByName("lobbyButton").tooltip = colorizeHotkey(
translate("%(hotkey)s: Launch the multiplayer lobby to join and host publicly visible games and chat with other players."),
"lobby");
+
+ Engine.GetGUIObjectByName("subMenuNewCampaignButton").enabled = false;
+ Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false;
+
+ Engine.GetGUIObjectByName("subMenuNewCampaignButton").enabled = loadAvailableCampaignTemplates().length;
+ Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = Engine.GetGUIObjectByName("subMenuNewCampaignButton").enabled &&
+ canLoadCurrentCampaignSave();
}
function getHotloadData()
Index: binaries/data/mods/public/gui/pregame/mainmenu.xml
===================================================================
--- binaries/data/mods/public/gui/pregame/mainmenu.xml
+++ binaries/data/mods/public/gui/pregame/mainmenu.xml
@@ -2,6 +2,8 @@
+
+
@@ -166,38 +168,67 @@
-
- Campaigns
- Relive history through historical military campaigns. \[NOT YET IMPLEMENTED]
+ Load Game
+ Click here to load a saved game.
closeMenu();
+ Engine.PushGuiPage("page_loadgame.xml", { type: "offline" });
-
- Load Game
- Click here to load a saved game.
+ Continue Campaign
+ Click here to load your latest campaign.
+
+ loadCurrentCampaignSave();
+
+
+
+
+ New Campaign
+ Relive history through historical military campaigns. \[WIP]
+
+ closeMenu();
+ Engine.PushGuiPage("page_campaignsetup_simple.xml", { type: "offline" });
+
+
+
+
+ Load Campaign
+ Click here to resume an existing campaign.
closeMenu();
- Engine.PushGuiPage("page_loadgame.xml", { "type": "offline" });
+ Engine.PushGuiPage("page_loadcampaign.xml", { type: "offline" });
Replays
@@ -425,7 +456,7 @@
Challenge the computer player to a single player match.
closeMenu();
- openMenu("submenuSinglePlayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 4);
+ openMenu("submenuSinglePlayer", this.parent.size.top + this.size.top, this.size.bottom - this.size.top, 6);
Index: binaries/data/mods/public/gui/session/session.js
===================================================================
--- binaries/data/mods/public/gui/session/session.js
+++ binaries/data/mods/public/gui/session/session.js
@@ -734,6 +734,14 @@
let simData = getReplayMetadata();
let playerID = Engine.GetPlayerID();
+ let campaignData = null;
+ if (!g_IsReplay && Engine.GetInitAttributes().campaignData)
+ {
+ campaignData = Engine.GetInitAttributes().campaignData;
+ campaignData.endGameData = Engine.GuiInterfaceCall("GetEndGameCampaignData");
+ onCampaignGameEnded(campaignData);
+ }
+
Engine.EndGame();
// After the replay file was closed in EndGame
@@ -753,7 +761,8 @@
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
- }
+ },
+ "campaignData" : campaignData
});
}
Index: binaries/data/mods/public/gui/session/session.xml
===================================================================
--- binaries/data/mods/public/gui/session/session.xml
+++ binaries/data/mods/public/gui/session/session.xml
@@ -5,6 +5,8 @@
+
+
Index: binaries/data/mods/public/gui/summary/summary.js
===================================================================
--- binaries/data/mods/public/gui/summary/summary.js
+++ binaries/data/mods/public/gui/summary/summary.js
@@ -456,6 +456,8 @@
"replaySelectionData": g_GameData.gui.replaySelectionData,
"summarySelectedData": summarySelectedData
});
+ else if (g_GameData.campaignData)
+ loadCurrentCampaignSave();
else
Engine.SwitchGuiPage("page_pregame.xml");
}
Index: binaries/data/mods/public/gui/summary/summary.xml
===================================================================
--- binaries/data/mods/public/gui/summary/summary.xml
+++ binaries/data/mods/public/gui/summary/summary.xml
@@ -5,6 +5,10 @@
+
+
+
+
continueButton();
Index: binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- binaries/data/mods/public/simulation/components/GuiInterface.js
+++ binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -191,6 +191,13 @@
return ret;
};
+GuiInterface.prototype.GetEndGameCampaignData = function(player)
+{
+ return {
+ "status": QueryPlayerIDInterface(player, IID_Player).GetState()
+ };
+}
+
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
@@ -1908,6 +1915,7 @@
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
+ "GetEndGameCampaignData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -446,6 +446,8 @@
// We mount these dirs last as otherwise writing could result in files being placed in a mod's dir.
g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"");
g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH);
+ g_VFS->Mount(L"campaignsaves/", paths.UserData()/"campaignsaves"/"");
+
// Mounting with highest priority, so that a mod supplied user.cfg is harmless
g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1);
if(readonlyConfig != paths.Config())
Index: source/ps/scripting/JSInterface_VFS.h
===================================================================
--- source/ps/scripting/JSInterface_VFS.h
+++ source/ps/scripting/JSInterface_VFS.h
@@ -47,6 +47,9 @@
// Save given JS Object to a JSON file
void WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1);
+ // Delete given file.
+ bool DeleteFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath);
+
// Tests whether the current script context is allowed to read from the given directory
bool PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath);
Index: source/ps/scripting/JSInterface_VFS.cpp
===================================================================
--- source/ps/scripting/JSInterface_VFS.cpp
+++ source/ps/scripting/JSInterface_VFS.cpp
@@ -209,6 +209,15 @@
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
+bool JSI_VFS::DeleteFile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filePath)
+{
+ OsPath realPath;
+ return VfsFileExists(filePath) &&
+ g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
+ g_VFS->RemoveFile(filePath) == INFO::OK &&
+ wunlink(realPath) == 0;
+}
+
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
@@ -261,6 +270,7 @@
scriptInterface.RegisterFunction("ReadFileLines");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
+ scriptInterface.RegisterFunction("DeleteFile");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
@@ -275,4 +285,5 @@
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
+ scriptInterface.RegisterFunction("WriteJSONFile");
}