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": "scenarios/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": "scenarios/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.\u000a\u000aWarning: 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,75 @@
+// ID of the current campaign. This is the name of the json file (not the "human readable" name)
+var g_CampaignID = null;
+
+// Campaign template data from the JSON file.
+var g_CampaignTemplate = null;
+
+// name of the file we're saving campaign data in
+var g_CampaignSaveFilename = null;
+
+// Current campaign state, to be saved in/loaded from the above file
+var g_CampaignState = null;
+
+function startLevel(level)
+{
+ let matchID = launchGame(g_CampaignTemplate.Levels[level], level);
+
+ g_CampaignState.currentlyPlaying = matchID;
+
+ saveCurrentCampaign();
+}
+
+// This function is called by session.js at the end of a game. It should save the campaign save state immediately.
+function campaignGameEnded(data)
+{
+ g_CampaignID = data.ID;
+ g_CampaignTemplate = data.template;
+ g_CampaignSaveFilename = data.save;
+ g_CampaignState = data.data;
+
+ // TODO: Deal with the endGameData
+
+ // if we're not active, we either lost or won, so we're no longer playing the game.
+ if (data.endGameData.status !== "active")
+ {
+ if (g_CampaignState.currentlyPlaying)
+ g_CampaignState.currentlyPlaying = undefined;
+ }
+ 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();
+}
+
+// @returns true if the level "level" is available.
+function hasRequirements(level)
+{
+ if (!level.Requires)
+ return true;
+
+ if (!g_CampaignState.completed)
+ return false;
+
+ // reuse class matching system, supporting "or", "+" and "!"
+ if (!MatchesClassList(g_CampaignState.completed, level.Requires))
+ return false;
+
+ return true;
+}
+
+// @returns true if the player has completed the level with ID "level"
+function hasCompleted(level)
+{
+ if (!g_CampaignState.completed)
+ return false;
+
+ if (g_CampaignState.completed.indexOf(level.ID) === -1)
+ return false;
+
+ return true;
+}
Index: binaries/data/mods/public/gui/campaign/campaign_io.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/campaign_io.js
@@ -0,0 +1,137 @@
+// This file provides helper functions related to loading/saving campaign save states and campaign templates
+// Most functions rely on loading campaign.js, but not all (such as canLoadCurrentCampaign)
+
+/*
+ * TODOs in this file:
+ * - Provide a way to check if the campaign state changed?
+ * Other things
+ */
+
+function LoadAvailableCampaigns()
+{
+ let campaigns = Engine.BuildDirEntList("campaigns/", "*.json", false);
+
+ let ret = {};
+
+ for (let filename of campaigns)
+ {
+ let data = Engine.ReadJSONFile(filename);
+ if (!data)
+ continue;
+
+ // TODO: sanity checks, probably in their own function?
+ if (!data.Name)
+ continue;
+
+ ret[filename.replace("campaigns/","").replace(".json","")] = data;
+ }
+
+ return ret;
+}
+
+function canLoadCurrentCampaign(verbose = false)
+{
+ let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
+
+ if (!campaign)
+ {
+ if (verbose)
+ warn("No campaign chosen, currentcampaign is not defined in user.cfg. Quitting campaign mode.")
+ return false;
+ }
+
+ if (!Engine.FileExists("campaignsaves/" + campaign + ".0adcampaign"))
+ {
+ if (verbose)
+ warn("Current campaign not found. Quitting campaign mode.");
+ return false;
+ }
+
+ // TODO: load up the file and do some checks?
+
+ return true;
+}
+
+function loadCurrentCampaignSave()
+{
+ let campaign = Engine.ConfigDB_GetValue("user", "currentcampaign");
+
+ if (!canLoadCurrentCampaign(true))
+ 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;
+
+ // actually fetch the menu
+ Engine.SwitchGuiPage("campaign/" + g_CampaignTemplate.Interface + "/page_menu.xml",
+ {
+ "ID" : g_CampaignID,
+ "template" : g_CampaignTemplate,
+ "save": g_CampaignSaveFilename,
+ "data" : g_CampaignState
+ });
+
+ return true;
+}
+
+function loadCampaignTemplate(name)
+{
+ let data = Engine.ReadJSONFile("campaigns/" + name + ".json");
+
+ if (!data)
+ {
+ warn("Could not parse campaign data.");
+ return false;
+ }
+
+ g_CampaignTemplate = data;
+
+ return true;
+}
+
+function saveCurrentCampaign()
+{
+ if (!g_CampaignSaveFilename)
+ {
+ warn("Cannot save current campaign, no campaign is currently loaded.");
+ return false;
+ }
+
+ return saveCampaign(g_CampaignSaveFilename, g_CampaignState);
+}
+
+function saveCampaign(filename, data)
+{
+ Engine.WriteJSONFile("campaignsaves/" + filename + ".0adcampaign", data);
+
+ return true;
+}
+
+function loadCampaignSave(filename)
+{
+ let campaignData = Engine.ReadJSONFile("campaignsaves/" + filename + ".0adcampaign");
+ if (!campaignData)
+ {
+ error("Campaign " + filename + " failed to load properly.");
+ return undefined;
+ }
+
+ return campaignData;
+}
Index: binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/gamesetup/gamesetup.js
@@ -0,0 +1,83 @@
+// Here should go functions to set up Campaign games.
+var g_GameAttributes = { "settings": {} };
+
+var g_DefaultPlayerData = [];
+
+function sanitizePlayerData(playerData)
+{
+ // Remove gaia
+ if (playerData.length && !playerData[0])
+ playerData.shift();
+
+ playerData.forEach((pData, index) => {
+ pData.Color = pData.Color;
+ pData.Civ = pData.Civ;
+
+ // Use default AI if the map doesn't specify any explicitly
+ if (!("AI" in pData))
+ pData.AI = g_DefaultPlayerData[index].AI;
+
+ if (!("AIDiff" in pData))
+ pData.AIDiff = g_DefaultPlayerData[index].AIDiff;
+ });
+}
+
+
+// TODO: this is a minimalist patchwork from gamesetup.Js and only barely attempts to work.
+
+function launchGame(level, levelID)
+{
+ if (!level.Map)
+ {
+ warn("cannot start scenario: no maps specified.");
+ return;
+ }
+
+ loadSettingsValues();
+
+ g_DefaultPlayerData = clone(g_Settings.PlayerDefaults);
+
+ let mapData = Engine.LoadMapSettings("maps/" + level.Map);
+ if (!mapData)
+ {
+ warn("Could not load map");
+ return;
+ }
+
+ let mapSettings = mapData && mapData.settings ? clone(mapData.settings) : {};
+
+ if (mapSettings.PlayerData)
+ sanitizePlayerData(mapSettings.PlayerData);
+
+ // Copy any new settings
+ g_GameAttributes.map = "maps/" + level.Map;
+ g_GameAttributes.script = mapSettings.Script;
+
+ for (let prop in mapSettings)
+ g_GameAttributes.settings[prop] = mapSettings[prop];
+
+ // TODO: support default victory conditions?
+ g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.TriggerScripts || [];
+
+ g_GameAttributes.settings.mapType = "scenario";
+ g_GameAttributes.mapType = "scenario";
+
+ // Seed used for both map generation and simulation
+ g_GameAttributes.settings.Seed = Math.floor(Math.random() * Math.pow(2, 32));
+ g_GameAttributes.settings.AISeed = Math.floor(Math.random() * Math.pow(2, 32));
+
+ g_GameAttributes.matchID = Engine.GetMatchID();
+
+ // TODO: player should be defined in the map or the campaign at the least.
+ let playerID = 1;
+
+ g_GameAttributes.campaignData = {"ID" : g_CampaignID, "template" : g_CampaignTemplate, "save": g_CampaignSaveFilename, "data" : g_CampaignState, "level" : levelID};
+
+ Engine.StartGame(g_GameAttributes, playerID);
+ Engine.SwitchGuiPage("page_loading.xml", {
+ "attribs": g_GameAttributes,
+ "isNetworked" : false,
+ "playerAssignments": {}
+ });
+ return g_GameAttributes.matchID;
+}
Index: binaries/data/mods/public/gui/campaign/load.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load.js
@@ -0,0 +1,112 @@
+var g_Campaigns = [];
+
+var g_CampaignTemplate = null;
+
+function init()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+
+ let campaigns = Engine.BuildDirEntList("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 campaignData.userDescription + " - " + 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("\"%(label)s\""), {
+ "label": 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.DeleteCampaignGame(name))
+ error("Could not delete campaign game " + name);
+
+ if (Engine.ConfigDB_GetValue("user", "currentcampaign") === name)
+ {
+ Engine.ConfigDB_RemoveValue("user", "currentcampaign");
+ Engine.ConfigDB_WriteFile("user", "config/user.cfg");
+
+ // TODO: this doesn't seem to work.
+ if (Engine.GetGUIObjectByName("subMenuContinueCampaignButton"))
+ Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = false;
+ }
+
+ // re-run init to refresh.
+ init();
+}
Index: binaries/data/mods/public/gui/campaign/load.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/newcampaign_modal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/newcampaign_modal.js
@@ -0,0 +1,76 @@
+var g_CampaignID = null;
+var g_CampaignState = null;
+
+// TODO: refuse empty names
+
+function init(data)
+{
+ // load existing campaigns
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+ gameSelection.selected = -1;
+
+ let campaigns = Engine.BuildDirEntList("campaignsaves/", "*.0adcampaign", false);
+ if (!campaigns.length)
+ gameSelection.list = [translate("No ongoing campaigns.")];
+ 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");
+ if (gameSelection.selected === -1)
+ return;
+
+ // TODO: do something?
+}
+
+function startCampaign()
+{
+ // TODO: handle overwrite and so on
+
+ // temp: prefill campaign name
+ realStartCampaign(Engine.GetGUIObjectByName("saveGameDesc").caption);
+}
+
+function realStartCampaign(desc)
+{
+ let name = g_CampaignID + "_1";
+ // if file already exists, pick the number above the existing ones. Don't bother making it dense.
+ if (Engine.FileExists("campaignsaves/" + name + ".0adcampaign"))
+ {
+ // get other campaigns following that template
+ let campaigns = Engine.BuildDirEntList("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);
+ // sanity check
+ if (Engine.FileExists("campaignsaves/" + name + ".0adcampaign"))
+ {
+ error("tell wraitii he can't code");
+ return;
+ }
+ }
+
+ saveCampaign(name, {"userDescription" : desc, "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,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ New Campaign
+
+
+
+ selectionChanged();
+
+
+
+ Name of this campaign run:
+
+
+
+ startCampaign();
+
+
+
+ Cancel
+ Engine.PopGuiPage();
+
+
+
+ Delete
+ deleteCampaign();
+
+
+
+ Start
+ startCampaign();
+
+
+
+
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 = null;
+
+var g_SavedGamesMetadata = [];
+
+function init(data)
+{
+ if (!data)
+ {
+ warn("Loading campaign menu without a campaign loaded")
+ return false;
+ }
+
+ 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()
+{
+ // TODO: remember old selection?
+ 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 && !hasRequirements(level))
+ continue;
+
+ let status = "";
+ let name = level.Name;
+ if (!hasRequirements(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));
+
+ // change array of object into object of array.
+ list = prepareForDropdown(list);
+
+ // Push to GUI
+ selection.selected = -1;
+ selection.list_name = list.name || [];
+ selection.list_status = list.status || [];
+
+ // Change these last, otherwise crash
+ // TODO: do we need both of those? I'm unsure.
+ selection.list = list.ID || [];
+ selection.list_data = list.ID || [];
+
+// replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
+
+// displayReplayDetails();
+
+}
+
+function displayLevelDetails(levelID)
+{
+ let level = g_CampaignTemplate.Levels[levelID];
+
+ // TODO: load from map file if not present
+ Engine.GetGUIObjectByName("scenarioName").caption = translate(level.Name);
+ Engine.GetGUIObjectByName("scenarioDesc").caption = translate(level.Description);
+
+ // todo: ibidem
+ if (level.Preview)
+ Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview;
+ else
+ Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ g_SelectedLevel = levelID;
+
+ if (!hasRequirements(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 (!hasSameSavegameVersion(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)
+{
+ // TODO: This should probably have a confirmation popup
+ 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", {});
+}
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,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaign Name
+
+
+
+
+
+
+ selectionChanged();
+
+
+
+ Scenario Name
+
+
+
+ Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No scenario selected
+
+
+
+
+
+
+
+
+
+
+
+ Saved Games
+
+
+
+ saveSelectionChanged();
+
+
+
+
+
+
+
+
+ Back to Main Menu
+ exitCampaignMode()
+
+
+
+ Exit Game
+ exitCampaignMode(true);
+
+
+
+ Start Scenario
+
+ startLevel(g_SelectedLevel);
+
+
+
+ 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,14 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaign/simple_campaign/sprites.xml
+ campaign/simple_campaign/styles.xml
+ campaign/simple_campaign/mainmenu.xml
+
Index: binaries/data/mods/public/gui/campaign/simple_campaign/sprites.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_campaign/sprites.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/simple_campaign/styles.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_campaign/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
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,69 @@
+var g_CampaignsAvailable = {}; // "name of JSON file/ID of campaign" : data as parsed JSON
+
+var g_SelectedCampaign = null;
+
+/*
+ * Initializes the campaign window.
+ * Loads all campaigns
+ * Allows you to start them.
+ */
+
+// TODO: Should we support mods?
+
+function init(data)
+{
+ g_CampaignsAvailable = LoadAvailableCampaigns();
+
+ Engine.GetGUIObjectByName("CampaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ GenerateCampaignList();
+}
+
+function GenerateCampaignList()
+{
+ let selection = Engine.GetGUIObjectByName("campaignSelection");
+ if (selection.selected !== -1)
+ displayCampaignDetails();
+
+ let list = [];
+ for (let key in g_CampaignsAvailable)
+ list.push({ "directories" : key, "name" : g_CampaignsAvailable[key].Name });
+
+ // change array of object into object of array.
+ list = prepareForDropdown(list);
+
+ // Push to GUI
+ selection.selected = -1;
+ selection.list_name = list.name || [];
+
+ // Change these last, otherwise crash
+ // TODO: do we need both of those? I'm unsure.
+ selection.list = list.directories || [];
+ selection.list_data = list.directories || [];
+}
+
+function displayCampaignDetails()
+{
+ let selection = Engine.GetGUIObjectByName("campaignSelection");
+ if (selection.selected === -1)
+ return;
+
+ g_SelectedCampaign = selection.list[selection.selected];
+
+ Engine.GetGUIObjectByName("startCampButton").enabled = true;
+ Engine.GetGUIObjectByName("campaignOptionsButton").enabled = true;
+
+ Engine.GetGUIObjectByName("CampaignTitle").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].Name);
+ Engine.GetGUIObjectByName("campaignDesc").caption = translate(g_CampaignsAvailable[g_SelectedCampaign].Description);
+
+ if (g_CampaignsAvailable[g_SelectedCampaign].Image)
+ Engine.GetGUIObjectByName("CampaignImage").sprite = "stretched:" + g_CampaignsAvailable[g_SelectedCampaign].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_SelectedCampaign, "campaignData" : g_CampaignsAvailable[g_SelectedCampaign] });
+}
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,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Campaigns
+
+
+
+
+
+
+ displayCampaignDetails();
+
+ startCampaign();
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+ No campaign selected
+
+
+
+
+
+
+ Options
+
+
+
+
+
+
+
+
+ Main Menu
+ Engine.SwitchGuiPage("page_pregame.xml");
+
+
+
+
+ Start Campaign
+ startCampaign();
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/simple_setup/sprites.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_setup/sprites.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaign/simple_setup/styles.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/simple_setup/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
Index: binaries/data/mods/public/gui/page_campaignsetup_simple.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/page_campaignsetup_simple.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/simple_setup/sprites.xml
+ campaign/simple_setup/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,14 @@
+
+
+
+ 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
@@ -38,6 +38,17 @@
guiObj.sprite = g_BackgroundLayerset[i].sprite;
guiObj.z = i;
}
+
+ // Enable campaign button if we have campaigns available
+ if (Object.keys(LoadAvailableCampaigns()).length !== 0)
+ {
+ Engine.GetGUIObjectByName("subMenuNewCampaignButton").enabled = true;
+ // TODO
+ // Engine.GetGUIObjectByName("subMenuLoadCampaignButton").enabled = true;
+ // Continue if it seems we would be able to.
+ if (canLoadCurrentCampaign())
+ Engine.GetGUIObjectByName("subMenuContinueCampaignButton").enabled = true;
+ }
}
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
@@ -4,6 +4,8 @@
+
+
@@ -222,34 +224,62 @@
-
- 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_loadgame.xml", { "type": "offline" });
+ Engine.PushGuiPage("page_campaignsetup_simple.xml", { type: "offline" });
+
+ Load Campaign
+ Click here to resume an existing campaign.
+
+ closeMenu();
+ Engine.PushGuiPage("page_loadcampaign.xml", { type: "offline" });
+
+
@@ -466,7 +496,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), 3);
+ openMenu("submenuSinglePlayer", (this.parent.size.top+this.size.top), (this.size.bottom-this.size.top), 5);
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
@@ -669,6 +669,14 @@
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = getReplayMetadata();
+ let campaignData = null;
+ if (!g_IsReplay && Engine.GetInitAttributes().campaignData)
+ {
+ campaignData = Engine.GetInitAttributes().campaignData;
+ campaignData.endGameData = Engine.GuiInterfaceCall("GetEndGameCampaignData");
+ campaignGameEnded(campaignData);
+ }
+
Engine.EndGame();
// After the replay file was closed in EndGame
@@ -687,7 +695,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
@@ -14,6 +14,10 @@
+
+
+
+
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
@@ -389,6 +389,8 @@
});
else if (Engine.HasXmppClient())
Engine.SwitchGuiPage("page_lobby.xml");
+ 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
@@ -15,6 +15,10 @@
+
+
+
+
.
+ */
+
+#ifndef INCLUDED_CAMPAIGNS
+#define INCLUDED_CAMPAIGNS
+
+#include "ps/Filesystem.h"
+#include "scriptinterface/ScriptInterface.h"
+
+
+class CSimulation2;
+class CGUIManager;
+
+/**
+ * @file
+ * Contains functions for deleting a campaign save
+ */
+
+namespace Campaigns
+{
+/**
+ * Permanently deletes the saved campaign run with the given name
+ *
+ * @param name filename of saved campaign (without path or extension)
+ * @return true if deletion was successful, or false on error
+ */
+bool DeleteGame(const std::wstring& name)
+{
+ const VfsPath basename(L"campaignsaves/" + name);
+ const VfsPath filename = basename.ChangeExtension(L".0adcampaign");
+ OsPath realpath;
+
+ // Make sure it exists in VFS and find its real path
+ if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK)
+ return false; // Error
+
+ // Remove from VFS
+ if (g_VFS->RemoveFile(filename) != INFO::OK)
+ return false; // Error
+
+ // Delete actual file
+ if (wunlink(realpath) != 0)
+ return false; // Error
+
+ // Successfully deleted file
+ return true;
+}
+}
+
+#endif // INCLUDED_CAMPAIGNS
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -502,6 +502,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_SavedGame.h
===================================================================
--- source/ps/scripting/JSInterface_SavedGame.h
+++ source/ps/scripting/JSInterface_SavedGame.h
@@ -31,6 +31,8 @@
void QuickLoad(ScriptInterface::CxPrivate* pCxPrivate);
JS::Value StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
+ bool DeleteCampaignGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
+
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
}
Index: source/ps/scripting/JSInterface_SavedGame.cpp
===================================================================
--- source/ps/scripting/JSInterface_SavedGame.cpp
+++ source/ps/scripting/JSInterface_SavedGame.cpp
@@ -21,6 +21,7 @@
#include "network/NetClient.h"
#include "network/NetServer.h"
+#include "ps/Campaigns.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/SavedGame.h"
@@ -109,6 +110,11 @@
return guiContextMetadata;
}
+bool JSI_SavedGame::DeleteCampaignGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
+{
+ return Campaigns::DeleteGame(name);
+}
+
void JSI_SavedGame::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("GetEngineInfo");
@@ -119,4 +125,5 @@
scriptInterface.RegisterFunction("QuickSave");
scriptInterface.RegisterFunction("QuickLoad");
scriptInterface.RegisterFunction("StartSavedGame");
+ scriptInterface.RegisterFunction("DeleteCampaignGame");
}