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" : "campaign/simple_campaign/page_menu.xml",
+ "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",
+ "Map" : "",
+ "Description" : "None",
+ "Requires" : "!Example_1"
+ }
+ },
+ "Order" : ["Example_1", "Example_2", "Example_3", "Example_4", "Example_5"],
+ "ShowUnavailable" : true
+}
\ No newline at end of file
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" : "campaign/simple_campaign/page_menu.xml",
+ "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
+}
\ No newline at end of file
Index: binaries/data/mods/public/gui/campaign/campaign.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/campaign.js
@@ -0,0 +1,76 @@
+// 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_CampaignSave = null;
+
+// Current campaign state, to be saved in/loaded from the above file
+var g_CampaignData = null;
+
+function startLevel(level)
+{
+ let matchID = launchGame(g_CampaignTemplate.Levels[level], level);
+
+ g_CampaignData.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_CampaignSave = data.save;
+ g_CampaignData = 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_CampaignData.currentlyPlaying)
+ g_CampaignData.currentlyPlaying = undefined;
+ }
+ if (data.endGameData.status === "won")
+ {
+ if (!g_CampaignData.completed)
+ g_CampaignData.completed = [];
+ if (g_CampaignData.completed.indexOf(data.level) == -1)
+ g_CampaignData.completed.push(data.level);
+ }
+
+ saveCurrentCampaign();
+}
+
+// returns true if the level "level" is available.
+function hasRequirements(level)
+{
+ if (!level.Requires)
+ return true;
+
+ if (!g_CampaignData.completed)
+ return false;
+
+ // reuse class matching system, supporting "or", "+" and "!"
+ if (!MatchesClassList(g_CampaignData.completed, level.Requires))
+ return false;
+
+ return true;
+}
+
+// return true if the player has completed the level with ID "level"
+function hasCompleted(level)
+{
+ if (!g_CampaignData.completed)
+ return false;
+
+ if (g_CampaignData.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,132 @@
+// 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_CampaignSave)
+ {
+ warn("Campaign already loaded");
+ return false;
+ }
+ g_CampaignSave = campaign;
+
+ let campaignData = loadCampaignSave(g_CampaignSave);
+ if (!campaignData)
+ {
+ warn("Campaign failed to load properly. Quitting campaign mode.")
+ return false;
+ }
+
+ g_CampaignData = campaignData;
+ g_CampaignID = g_CampaignData.campaign;
+
+ if (!loadCampaignTemplate(g_CampaignID))
+ return false;
+
+ // actually fetch the menu
+ Engine.SwitchGuiPage(g_CampaignTemplate.Interface, {"ID" : g_CampaignID, "template" : g_CampaignTemplate, "save": g_CampaignSave, "data" : g_CampaignData});
+
+ 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_CampaignSave)
+ {
+ warn("Cannot save current campaign, no campaign is currently loaded.");
+ return false;
+ }
+
+ return saveCampaign(g_CampaignSave, g_CampaignData);
+}
+
+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,84 @@
+// 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 = g_Settings.PlayerDefaults;
+ g_DefaultPlayerData.shift();
+
+ let mapData = Engine.LoadMapSettings("maps/" + level.Map);
+ if (!mapData)
+ {
+ warn("Could not load map");
+ return;
+ }
+
+ let mapSettings = mapData && mapData.settings ? deepcopy(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_CampaignSave, "data" : g_CampaignData, "level" : levelID};
+
+ Engine.StartGame(g_GameAttributes, playerID);
+ Engine.SwitchGuiPage("page_loading.xml", {
+ "attribs": g_GameAttributes,
+ "isNetworked" : false,
+ "playerAssignments": {}
+ });
+ return g_GameAttributes.matchID;
+}
\ No newline at end of file
Index: binaries/data/mods/public/gui/campaign/load.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaign/load.js
@@ -0,0 +1,169 @@
+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;
+ Engine.GetGUIObjectByName("invalidGame").hidden = !selectionEmpty;
+ Engine.GetGUIObjectByName("validGame").hidden = selectionEmpty;
+
+ if (selectionEmpty)
+ return;
+
+/*
+ Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name);
+ let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map);
+ setMapPreviewImage("savedInfoPreview", mapData.preview);
+
+ Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1;
+ Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0);
+ Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType);
+ Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size);
+ Engine.GetGUIObjectByName("savedVictory").caption = translateVictoryCondition(metadata.initAttributes.settings.GameType);
+
+ let caption = sprintf(translate("Mods: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) });
+ if (!hasSameMods(metadata, Engine.GetEngineInfo()))
+ caption = "[color=\"orange\"]" + caption + "[/color]";
+ Engine.GetGUIObjectByName("savedMods").caption = caption;
+
+ Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo(
+ metadata.initAttributes.settings.PlayerData,
+ metadata.gui.states
+ );
+ */
+}
+
+function loadCampaign()
+{
+ let gameSelection = Engine.GetGUIObjectByName("gameSelection");
+
+ let campaign = gameSelection.list_data[gameSelection.selected];
+ reallyLoadCampaign(campaign);
+
+ // TODO: compatibility checks cf saved games
+/*
+ // Check compatibility before really loading it
+ let sameMods = hasSameMods(metadata, engineInfo);
+
+ if (sameEngineVersion && sameSavegameVersion && sameMods)
+ {
+ reallyLoadGame(gameId);
+ return;
+ }
+
+ if (!sameMods)
+ {
+ if (!metadata.mods)
+ metadata.mods = [];
+
+ message += translate("The savegame needs a different set of mods:") + "\n" +
+ sprintf(translate("Required: %(mods)s"), {
+ "mods": metadata.mods.join(translate(", "))
+ }) + "\n" +
+ sprintf(translate("Active: %(mods)s"), {
+ "mods": engineInfo.mods.join(translate(", "))
+ });
+ }
+
+ message += "\n" + translate("Do you still want to proceed?");
+
+ messageBox(
+ 500, 250,
+ message,
+ translate("Warning"),
+ [translate("No"), translate("Yes")],
+ [init, function(){ reallyLoadGame(gameId); }]
+ );*/
+}
+
+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,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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_CampaignData = 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_CampaignData = 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();
+}
\ No newline at end of file
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,218 @@
+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_CampaignSave = data.save;
+ g_CampaignData = 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 = "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;
+/*
+ Engine.GetGUIObjectByName("savedMapName").caption = translate(metadata.initAttributes.settings.Name);
+ let mapData = getMapDescriptionAndPreview(metadata.initAttributes.mapType, metadata.initAttributes.map);
+ setMapPreviewImage("savedInfoPreview", mapData.preview);
+
+ Engine.GetGUIObjectByName("savedPlayers").caption = metadata.initAttributes.settings.PlayerData.length - 1;
+ Engine.GetGUIObjectByName("savedPlayedTime").caption = timeToString(metadata.gui.timeElapsed ? metadata.gui.timeElapsed : 0);
+ Engine.GetGUIObjectByName("savedMapType").caption = translateMapType(metadata.initAttributes.mapType);
+ Engine.GetGUIObjectByName("savedMapSize").caption = translateMapSize(metadata.initAttributes.settings.Size);
+ Engine.GetGUIObjectByName("savedVictory").caption = translateVictoryCondition(metadata.initAttributes.settings.GameType);
+
+ let caption = sprintf(translate("Mods: %(mods)s"), { "mods": metadata.mods.join(translate(", ")) });
+ if (!hasSameMods(metadata, Engine.GetEngineInfo()))
+ caption = "[color=\"orange\"]" + caption + "[/color]";
+ Engine.GetGUIObjectByName("savedMods").caption = caption;
+
+ Engine.GetGUIObjectByName("savedPlayersNames").caption = formatPlayerInfo(
+ metadata.initAttributes.settings.PlayerData,
+ metadata.gui.states
+ );*/
+}
+
+function generateCampaignLabel(metadata, engineInfo)
+{
+ let dateTimeString = Engine.FormatMillisecondsIntoDateString(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: should this be here?
+ 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,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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,15 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+ common/init.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,73 @@
+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 || [];
+
+// replaySelection.selected = replaySelection.list.findIndex(directory => directory == g_SelectedReplayDirectory);
+
+// displayReplayDetails();
+}
+
+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
@@ -35,6 +35,17 @@
guiObj.sprite = layerset[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
@@ -5,6 +5,8 @@
+
+
@@ -212,38 +214,62 @@
-
- Campaigns
- Relive history through historical military campaigns. \[NOT YET IMPLEMENTED]
+ Load Game
+ Click here to load a saved game.
- closeMenu();
-
+ 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" });
+
+
@@ -468,7 +494,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
@@ -581,6 +581,14 @@
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = getReplayMetadata();
+ let campaignData = null;
+ if (Engine.GetInitAttributes().campaignData)
+ {
+ campaignData = Engine.GetInitAttributes().campaignData;
+ campaignData.endGameData = Engine.GuiInterfaceCall("GetEndGameCampaignData");
+ campaignGameEnded(campaignData);
+ }
+
Engine.EndGame();
if (g_IsController && Engine.HasXmppClient())
@@ -594,7 +602,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
@@ -15,6 +15,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
@@ -170,6 +170,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
@@ -13,6 +13,10 @@
+
+
+
+
pScriptInterface));
}
+void SaveCampaign(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, JS::HandleValue metadata)
+{
+ shared_ptr metadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(metadata);
+ if (Campaigns::Save(*(pCxPrivate->pScriptInterface), filename, metadataClone) < 0)
+ LOGERROR("Failed to save campaign state");
+}
+
+JS::Value LoadCampaign(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& campaignName)
+{
+ JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue campaignData(cx);
+ Status err = Campaigns::Load(*(pCxPrivate->pScriptInterface), campaignName, &campaignData);
+ if (err < 0)
+ return JS::UndefinedValue();
+
+ return campaignData;
+}
+
+bool DeleteCampaignGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
+{
+ return Campaigns::DeleteGame(name);
+}
+
void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
@@ -334,6 +360,16 @@
LOGERROR("Failed to save game");
}
+JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
+}
+
+bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
+{
+ return SavedGames::DeleteSavedGame(name);
+}
+
void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1)
{
ENSURE(g_NetClient);
@@ -465,16 +501,6 @@
return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface));
}
-JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate)
-{
- return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface));
-}
-
-bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name)
-{
- return SavedGames::DeleteSavedGame(name);
-}
-
void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url)
{
sys_open_url(url);
@@ -1054,6 +1080,11 @@
scriptInterface.RegisterFunction("GetAIs");
scriptInterface.RegisterFunction("GetEngineInfo");
+ // Campaigns
+ scriptInterface.RegisterFunction("SaveCampaign");
+ scriptInterface.RegisterFunction("LoadCampaign");
+ scriptInterface.RegisterFunction("DeleteCampaignGame");
+
// Saved games
scriptInterface.RegisterFunction("StartSavedGame");
scriptInterface.RegisterFunction("GetSavedGames");
Index: source/ps/Campaigns.h
===================================================================
--- /dev/null
+++ source/ps/Campaigns.h
@@ -0,0 +1,66 @@
+/* Copyright (C) 2016 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_CAMPAIGNS
+#define INCLUDED_CAMPAIGNS
+
+#include "scriptinterface/ScriptInterface.h"
+class CSimulation2;
+class CGUIManager;
+
+/**
+ * @file
+ * Contains functions for managing campaign archives.
+ *
+ * A saved game is a simple *.0adcampaign file
+ * which is a binary JSON file containing the campaign metadata
+ */
+
+namespace Campaigns
+{
+
+/**
+ * Create new campaign file with given name and metadata
+ *
+ * @param scriptInterface
+ * @param name Name of the campaign
+ * @param MetadataClone Actual campaign Metadata
+ * @return INFO::OK if successfully saved, else an error Status
+ */
+Status Save(ScriptInterface& scriptInterface, const CStrW& name, const shared_ptr& metadataClone);
+
+/**
+ * Load campaign with the given name
+ *
+ * @param scriptInterface
+ * @param name filename of campaign game (without path or extension)
+ * @param[out] metadata object containing metadata associated with saved game,
+ * parsed from metadata.json inside the archive.
+ * @return INFO::OK if successfully loaded, else an error Status
+ */
+Status Load(ScriptInterface& scriptInterface, const std::wstring& name, JS::MutableHandleValue campaignData);
+
+/**
+ * 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);
+}
+
+#endif // INCLUDED_CAMPAIGNS
Index: source/ps/Campaigns.cpp
===================================================================
--- /dev/null
+++ source/ps/Campaigns.cpp
@@ -0,0 +1,136 @@
+/* Copyright (C) 2016 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "Campaigns.h"
+
+#include "graphics/GameView.h"
+#include "gui/GUIManager.h"
+#include "lib/allocators/shared_ptr.h"
+#include "i18n/L10n.h"
+#include "lib/utf8.h"
+#include "ps/CLogger.h"
+#include "ps/FileIo.h"
+#include "ps/Filesystem.h"
+#include "ps/Game.h"
+#include "ps/Mod.h"
+#include "ps/Pyrogenesis.h"
+#include "ps/XML/Xeromyces.h"
+#include "scriptinterface/ScriptInterface.h"
+#include "simulation2/Simulation2.h"
+
+static const int CAMPAIGN_VERSION = 1; // increment on incompatible changes to the format
+
+// TODO: we ought to check version numbers when loading files
+
+Status Campaigns::Save(ScriptInterface& scriptInterface, const CStrW& name, const shared_ptr& metadataClone)
+{
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ // Determine the filename to save under
+ const VfsPath basenameFormat(L"campaignsaves/" + name);
+ const VfsPath filename = basenameFormat.ChangeExtension(L".0adcampaign");
+
+ time_t now = time(NULL);
+
+ JS::RootedValue metadata(cx);
+ scriptInterface.Eval("({})", &metadata);
+ scriptInterface.SetProperty(metadata, "engine_version", std::string(engine_version));
+ scriptInterface.SetProperty(metadata, "mods", g_modsLoaded);
+ scriptInterface.SetProperty(metadata, "time", (double)now);
+
+ JS::RootedValue campaignState(cx);
+ scriptInterface.ReadStructuredClone(metadataClone, &campaignState);
+
+ scriptInterface.SetProperty(metadata, "campaign_state", campaignState);
+
+ std::string dataString = scriptInterface.StringifyJSON(&metadata, true);
+
+ // ensure we won't crash if the save somehow fails.
+ try
+ {
+ CFilePacker packer(CAMPAIGN_VERSION, "CSST");
+ packer.PackString(dataString);
+ packer.Write(filename);
+ }
+ catch (PSERROR_File_WriteFailed&)
+ {
+ LOGERROR("Failed to write campaign '%s'", filename.string8());
+ return ERR::FAIL;
+ }
+
+ LOGMESSAGERENDER(g_L10n.Translate("Saved campaign to '%s'"), filename.string8());
+ debug_printf("Saved campaign to '%s'\n", filename.string8().c_str());
+
+ return INFO::OK;
+}
+
+Status Campaigns::Load(ScriptInterface& scriptInterface, const std::wstring& name, JS::MutableHandleValue campaignData)
+{
+ // Determine the filename to load
+ const VfsPath basename(L"campaignsaves/" + name);
+ const VfsPath filename = basename.ChangeExtension(L".0adcampaign");
+
+ if (!VfsFileExists(filename))
+ return ERR::FILE_NOT_FOUND;
+
+ CFileUnpacker unpacker;
+ unpacker.Read(filename, "CSST");
+
+ if (unpacker.GetVersion() < CAMPAIGN_VERSION)
+ {
+ LOGWARNING("Campaign file is too old, version %i, current version %i", unpacker.GetVersion(), CAMPAIGN_VERSION);
+ return ERR::FAIL;
+ }
+
+ CStr datastream;
+ unpacker.UnpackString(datastream);
+
+
+ if (!scriptInterface.ParseJSON(datastream, campaignData))
+ {
+ LOGERROR("Error parsing campaign JSON.");
+ return ERR::FAIL;
+ }
+
+ return INFO::OK;
+}
+
+bool Campaigns::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;
+}
+
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -499,6 +499,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())