Index: README.md
===================================================================
--- /dev/null
+++ README.md
@@ -0,0 +1,13 @@
+# 0 A.D.
+
+This is a fork for various work I do on 0 A.D.
+The `master_up` branch will (sometimes) contain upstream patches. `master` is just following 0 A.D.'s master branch with some tweaks for my own setup.
+
+----
+
+0 A.D. (pronounced "zero ey-dee") is a free, open-source, cross-platform
+real-time strategy game of ancient warfare.
+
+This is currently an incomplete, under-development version of the game.
+
+Visit the web site http://play0ad.com/ for more information.
Index: binaries/data/mods/mod/gui/common/modern/styles.xml
===================================================================
--- binaries/data/mods/mod/gui/common/modern/styles.xml
+++ binaries/data/mods/mod/gui/common/modern/styles.xml
@@ -30,7 +30,6 @@
scrollbar_style="ModernScrollBar"
sprite="ModernDarkBoxGoldNoTop"
sprite_selectarea="ModernDarkBoxWhite"
- sprite_heading="ModernDarkBoxGoldNoBottom"
textcolor="white"
textcolor_selected="white"
text_align="left"
Index: binaries/data/mods/public/campaigns/example.json
===================================================================
--- /dev/null
+++ binaries/data/mods/public/campaigns/example.json
@@ -0,0 +1,43 @@
+{
+ "Name": "Example Campaign",
+ "Description": "Lorem Ipsum and so on and so on",
+ "Interface": "default_menu",
+ "Levels": {
+ "Example_1": {
+ "Name": "Example 1",
+ "Map": "scenarios/Serengeti.xml",
+ "MapType": "scenario",
+ "Description": "Whatever"
+ },
+ "Example_2": {
+ "Name": "Example 2",
+ "Map": "scenarios/Serengeti.xml",
+ "MapType": "scenario",
+ "Description": "None",
+ "Requires": "Example_1"
+ },
+ "Example_3": {
+ "Name": "This one requires 1 and 2",
+ "Map": "scenarios/Serengeti.xml",
+ "MapType": "scenario",
+ "Description": "None",
+ "Requires": "Example_1+Example_2"
+ },
+ "Example_4": {
+ "Name": "This one requires 2 or 3",
+ "Map": "scenarios/Serengeti.xml",
+ "MapType": "scenario",
+ "Description": "None",
+ "Requires": "Example_2 Example_3"
+ },
+ "Example_5": {
+ "Name": "This one unavailable if 1 isn't completed",
+ "Map": "scenarios/Serengeti.xml",
+ "MapType": "scenario",
+ "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,23 @@
+{
+ "Name": "Tutorial",
+ "Description": "Learn how to play 0 A.D.",
+ "Image": "session/icons/mappreview/Introductory_Tutorial.png",
+ "Levels": {
+ "introduction": {
+ "Name": "Introductory Tutorial",
+ "Map": "tutorials/Introductory_Tutorial.xml",
+ "MapType": "scenario",
+ "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": "tutorials/starting_economy_walkthrough.xml",
+ "MapType": "scenario",
+ "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/campaigns/common_scripts.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/common_scripts.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js
@@ -0,0 +1,140 @@
+/**
+ * This is the main menu screen of the campaign.
+ * It shows you the currently available scenarios, scenarios you've already completed, etc.
+ * This particular variant is extremely simple and shows a list similar to Age 1's campaigns,
+ * but conceptually nothing really prevents more complex systems.
+ */
+class CampaignMenu extends DefaultPage
+{
+ constructor(campaignRun, finishedLevel, won)
+ {
+ super();
+
+ this.run = campaignRun;
+
+ this.selectedLevel = -1;
+ this.levelSelection = _("levelSelection");
+ this.levelSelection.onSelectionChange = () => { this.selectedLevel = this.levelSelection.selected; };
+
+ this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario();
+ _('startButton').onPress = () => this.startScenario();
+ _('backToMain').onPress = () => this.goBackToMainMenu();
+ _('savedGamesButton').onPress = () => Engine.PushGuiPage('page_loadgame.xml', {
+ 'campaignRun': this.run.filename
+ });
+ this._ready = true;
+ }
+
+ goBackToMainMenu()
+ {
+ this.run.save();
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+ }
+
+ startScenario()
+ {
+ let level = this.getSelectedLevelData();
+ if (!this.run.meetsRequirements(level))
+ return;
+ Engine.SwitchGuiPage("page_gamesetup.xml", {
+ "mapType": level.MapType,
+ "map": "maps/" + level.Map,
+ "autostart": true,
+ "campaignData": {
+ "run": this.run.filename,
+ "levelID": this.levelSelection.list_data[this.selectedLevel],
+ "data": this.run.data
+ }
+ });
+ }
+
+ getSelectedLevelData()
+ {
+ if (this.selectedLevel === -1)
+ return undefined;
+ return this.run.template.Levels[this.levelSelection.list_data[this.selectedLevel]];
+ }
+
+ shouldShowLevel(levelData)
+ {
+ if (this.run.template.ShowUnavailable)
+ return true;
+
+ return this.run.meetsRequirements(levelData);
+ }
+
+ displayLevelsList()
+ {
+ let list = [];
+ for (let key in this.run.template.Levels)
+ {
+ let level = this.run.template.Levels[key];
+
+ if (!this.shouldShowLevel(level))
+ continue;
+
+ let status = "";
+ let name = translate(level.Name);
+ if (!this.run.meetsRequirements(level))
+ {
+ status = translate("not unlocked yet");
+ name = "[color=\"gray\"]" + name + "[/color]";
+ }
+ else if (this.run.isCompleted(key))
+ {
+ status = translateWithContext("campaign status", "Won");
+ }
+ list.push({ "ID": key, "name": name, "status": status });
+ }
+
+ list.sort((a, b) => this.run.template.Order.indexOf(a.ID) - this.run.template.Order.indexOf(b.ID));
+
+ list = prepareForDropdown(list);
+
+ this.levelSelection.list_name = list.name || [];
+ this.levelSelection.list_status = list.status || [];
+
+ // These must be changed last or things crash.
+ this.levelSelection.list = list.ID || [];
+ this.levelSelection.list_data = list.ID || [];
+ }
+
+ displayLevelDetails()
+ {
+ if (this.selectedLevel === -1)
+ {
+ _("startButton").enabled = false;
+ _("startButton").hidden = false;
+ return;
+ }
+
+ let level = this.getSelectedLevelData();
+
+ _("scenarioName").caption = translate(level.Name);
+ _("scenarioDesc").caption = translate(level.Description);
+ if (level.Preview)
+ _('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview;
+ else
+ _('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ _("startButton").enabled = this.run.meetsRequirements(level);
+ _("startButton").hidden = false;
+ _("loadSavedButton").hidden = true;
+ }
+
+ render()
+ {
+ _("campaignTitle").caption = this.run.generateLabel();
+ this.displayLevelDetails();
+ this.displayLevelsList();
+ }
+}
+
+
+var g_CampaignMenu;
+
+function init(initData)
+{
+ let run = new CampaignRun(initData.filename).load();
+ g_CampaignMenu = new CampaignMenu(run, initData.finishedLevel || null, initData.won || null);
+}
Index: binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
Index: binaries/data/mods/public/gui/campaigns/default_menu/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/default_menu/page.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
+
+ campaigns/common_scripts.xml
+ campaigns/default_menu/CampaignMenu.xml
+
Index: binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js
@@ -0,0 +1,90 @@
+/**
+ * Lets you load/delete/look at existing campaign runs in your user folder.
+ */
+class LoadModal extends DefaultPage
+{
+ constructor(campaignTemplate)
+ {
+ super();
+
+ this.currentRuns = this._watch(CampaignRun.getCurrentRuns());
+
+ _('cancelButton').onPress = () => Engine.PopGuiPage();
+ _('deleteGameButton').onPress = () => this.deleteSelectedRun();
+ _('startButton').onPress = () => this.loadCampaign();
+
+ this.selectedRun = -1;
+ this.runSelection = _("runSelection");
+ this.runSelection.onSelectionChange = () => {
+ this.selectedRun = this.runSelection.selected;
+ if (this.selectedRun === -1)
+ _('runDescription').caption = "";
+ else
+ _('runDescription').caption = this.currentRuns[this.selectedRun].generateLabel();
+ };
+
+ this.runSelection.onMouseLeftDoubleClickItem = () => this.loadCampaign();
+
+ this._ready = true;
+ }
+
+ loadCampaign()
+ {
+ let filename = this.currentRuns[this.selectedRun].filename;
+ let run = new CampaignRun(filename)
+ .load()
+ .setCurrent();
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+
+ deleteSelectedRun()
+ {
+ if (this.selectedRun === -1)
+ return;
+
+ let run = this.currentRuns[this.selectedRun];
+
+ messageBox(
+ 400, 200,
+ translate("Are you sure you want to delete run " + run.generateLabel() + "? This can't be undone."),
+ translate("Confirmation"),
+ [translate("Yes"), translate("No")],
+ [() => {
+ run.destroy();
+ this.currentRuns.splice(this.selectedRun, 1);
+ }, null]
+ );
+ }
+
+ displayCurrentRuns()
+ {
+ if (!this.currentRuns.length)
+ {
+ this.runSelection.list = [translate("No ongoing campaigns.")];
+ this.runSelection.list_data = [""];
+ }
+ else
+ {
+ this.runSelection.list = this.currentRuns.map(run => run.generateLabel());
+ this.runSelection.list_data = this.currentRuns.map(run => run.filename);
+ }
+ }
+
+ render()
+ {
+ _('deleteGameButton').enabled = this.selectedRun !== -1;
+ _('startButton').enabled = this.selectedRun !== -1;
+ this.displayCurrentRuns();
+ }
+}
+
+
+var g_LoadModal;
+
+function init()
+{
+ g_LoadModal = new LoadModal();
+}
Index: binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Load Campaign
+
+
+
+
+
+
+ Name of this campaign run:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Delete
+
+
+
+ Load Campaign
+
+
+
+
Index: binaries/data/mods/public/gui/campaigns/load_modal/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/load_modal/page.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
+
+ campaigns/common_scripts.xml
+ campaigns/load_modal/LoadModal.xml
+
Index: binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js
@@ -0,0 +1,41 @@
+/**
+ * Modal screen that pops up when you start a new campaign from the setup screen.
+ * asking you to name it.
+ * Will then create the file with the according name and start everything up.
+ */
+class NewCampaignModal extends DefaultPage
+{
+ constructor(campaignTemplate)
+ {
+ super();
+
+ this.template = campaignTemplate;
+
+ _('cancelButton').onPress = () => Engine.PopGuiPage();
+ _('startButton').onPress = () => this.createAndStartCampaign();
+
+ this._ready = true;
+ }
+
+ createAndStartCampaign()
+ {
+ let filename = this.template.identifier + "_" + Date.now() + "_" + Math.floor(Math.random()*100000);
+ let run = new CampaignRun(filename)
+ .setTemplate(this.template)
+ .setMeta(_('runDescription').caption)
+ .save()
+ .setCurrent();
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+}
+
+
+var g_NewCampaignModal;
+
+function init(campaign_template_data)
+{
+ g_NewCampaignModal = new NewCampaignModal(campaign_template_data);
+}
Index: binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+ Start a campaign
+
+
+
+ Please enter the name of your new campaign run:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Start Campaign
+
+
+
+
Index: binaries/data/mods/public/gui/campaigns/new_modal/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/new_modal/page.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
+
+ campaigns/common_scripts.xml
+ campaigns/new_modal/NewCampaignModal.xml
+
Index: binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js
@@ -0,0 +1,70 @@
+/**
+ * The campaign setup page shows you the list of available campaigns,
+ * some information about them, and lets you start a new one.
+ */
+class CampaignSetupPage extends DefaultPage
+{
+ constructor()
+ {
+ super();
+ this.selectedIndex = -1;
+ this.templates = CampaignTemplate.getAvailableTemplates();
+
+ _("mainMenuButton").onPress = () => Engine.SwitchGuiPage("page_pregame.xml");
+ _("startCampButton").onPress = () => Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate);
+
+ this.campaignSelection = _("campaignSelection");
+ this.campaignSelection.onMouseLeftDoubleClickItem = () => {
+ if (this.selectedIndex === -1)
+ return;
+ Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate);
+ };
+ this.campaignSelection.onSelectionChange = () => {
+ this.selectedIndex = this.campaignSelection.selected;
+ if (this.selectedIndex !== -1)
+ this.selectedTemplate = this.templates[this.selectedIndex];
+ else
+ this.selectedTemplate = null;
+ };
+
+ this._ready = true;
+ }
+
+ displayCampaignDetails()
+ {
+ _("startCampButton").enabled = this.selectedIndex !== -1;
+
+ if (!this.selectedTemplate)
+ {
+ _("campaignTitle").caption = translate("No campaign selected.");
+ _("campaignDesc").caption = "";
+ _("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ return;
+ }
+
+ _("campaignTitle").caption = translate(this.selectedTemplate.Name);
+ _("campaignDesc").caption = translate(this.selectedTemplate.Description);
+ if ('Image' in this.selectedTemplate)
+ _("campaignImage").sprite = "stretched:" + this.selectedTemplate.Image;
+ else
+ _("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ }
+
+ render()
+ {
+ this.displayCampaignDetails();
+
+ _("campaignSelection").list_name = this.templates.map((camp) => camp.Name);
+ // These must be changed last or things crash.
+ _("campaignSelection").list = this.templates.map((camp) => camp.identifier) || [];
+ _("campaignSelection").list_data = this.templates.map((camp) => camp.identifier) || [];
+ }
+}
+
+
+var g_CampaignSetupPage;
+
+function init()
+{
+ g_CampaignSetupPage = new CampaignSetupPage();
+}
Index: binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+ Campaigns
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+ No Campaign selected
+
+
+
+
+
+
+
+
+
+
+
+
+ Main Menu
+
+
+
+
+ Start Campaign
+
+
+
+
Index: binaries/data/mods/public/gui/campaigns/setup/page.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/campaigns/setup/page.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
+
+ campaigns/common_scripts.xml
+ campaigns/setup/CampaignSetupPage.xml
+
Index: binaries/data/mods/public/gui/common/campaigns/CampaignRun.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/common/campaigns/CampaignRun.js
@@ -0,0 +1,156 @@
+// Cached run for CampaignRun.getCurrentRun()
+// TODO: Move this to a static member once we upgrade to SM75
+var g_CurrentCampaignRun;
+
+/**
+ * A campaign "Run" saves metadata on a campaign progession.
+ * It is equivalent to a saved game for a game.
+ * It is named a "run" in an attempt to disambiguate with saved games from campaign runs,
+ * campaign templates, and the actual concept of a campaign at large.
+ */
+class CampaignRun
+{
+ static getCurrentRuns()
+ {
+ let names = Engine.ListDirectoryFiles("campaignsaves/", "*.0adcampaign", false);
+ return names.map(path => {
+ let filename = path.replace("campaignsaves/", "").replace(".0adcampaign", "");
+ return new CampaignRun(filename).load();
+ });
+ }
+
+ static getCurrentRun()
+ {
+ let current = Engine.ConfigDB_GetValue("user", "currentcampaign");
+ if (g_CurrentCampaignRun && g_CurrentCampaignRun.ID == current)
+ return g_CurrentCampaignRun.run;
+ try
+ {
+ let run = new CampaignRun(current).load();
+ g_CurrentCampaignRun = {
+ "run": run,
+ "ID": current
+ };
+ return run;
+ }
+ catch(error)
+ {
+ return undefined;
+ }
+ }
+
+ constructor(name = "")
+ {
+ this.filename = name;
+ this.completedLevels = [];
+ // Metadata on the run, such as its description.
+ this.meta = {};
+ // 'User' data
+ this.data = {};
+ // ID of the campaign templates.
+ this.template = null;
+ }
+
+ setData(data)
+ {
+ if (!data)
+ {
+ warn("Invalid campaign scenario end data. Nothing will be saved.");
+ return this;
+ }
+
+ this.data = data;
+ this.save();
+ return this;
+ }
+
+ setTemplate(template)
+ {
+ this.template = template;
+ this.save();
+ return this;
+ }
+
+ setMeta(description)
+ {
+ this.meta.userDescription = description;
+ this.save();
+ return this;
+ }
+
+ setCurrent()
+ {
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", this.filename);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", this.filename, "config/user.cfg");
+ return this;
+ }
+
+ markLevelComplete(levelID)
+ {
+ if (!this.isCompleted(levelID))
+ {
+ this.completedLevels.push(levelID);
+ this.save();
+ }
+ }
+
+ isCompleted(levelID)
+ {
+ return this.completedLevels.indexOf(levelID) !== -1;
+ }
+
+ meetsRequirements(levelData)
+ {
+ if (!levelData.Requires)
+ return true;
+
+ if (!this.completedLevels)
+ return false;
+
+ return MatchesClassList(this.completedLevels, levelData.Requires);
+ }
+
+ getMenuPath()
+ {
+ return "campaigns/" + this.template.interface + "/page.xml";
+ }
+
+ generateLabel()
+ {
+ return sprintf(translate("%(userDesc)s - %(templateName)s"), {
+ "userDesc": this.meta.userDescription,
+ "templateName": this.template.Name
+ });
+ }
+
+
+ load()
+ {
+ if (!Engine.FileExists("campaignsaves/" + this.filename + ".0adcampaign"))
+ throw ("Campaign file does not exist");
+ let data = Engine.ReadJSONFile("campaignsaves/" + this.filename + ".0adcampaign");
+ this.data = data.data;
+ this.meta = data.meta;
+ this.completedLevels = data.completedLevels;
+ this.template = CampaignTemplate.getTemplate(data.template_identifier);
+ return this;
+ }
+
+ save()
+ {
+ let data = {
+ "data": this.data,
+ "meta": this.meta,
+ "completedLevels": this.completedLevels,
+ "template_identifier": this.template.identifier
+ };
+ Engine.WriteJSONFile("campaignsaves/" + this.filename + ".0adcampaign", data);
+ return this;
+ }
+
+ destroy()
+ {
+ Engine.DeleteFile("campaignsaves/" + this.filename + ".0adcampaign");
+ // hang in memory for a while.
+ }
+}
Index: binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js
@@ -0,0 +1,54 @@
+// TODO: replace this with a static member once we hit SM 75.
+var g_CachedTemplates;
+
+class CampaignTemplate
+{
+ /**
+ * @returns a dictionary of campaign templates, as [ { 'identifier': id, 'data': data }, ... ]
+ */
+ static getAvailableTemplates()
+ {
+ if (g_CachedTemplates)
+ return g_CachedTemplates;
+
+ let campaigns = Engine.ListDirectoryFiles("campaigns/", "*.json", false);
+
+ g_CachedTemplates = [];
+
+ for (let filename of campaigns)
+ // Use file name as identifier to guarantee unicity.
+ g_CachedTemplates.push(new CampaignTemplate(filename.slice("campaigns/".length, -".json".length)));
+
+ return g_CachedTemplates;
+ }
+
+ static getTemplate(identifier)
+ {
+ if (!g_CachedTemplates)
+ CampaignTemplate.getAvailableTemplates();
+ let temp = g_CachedTemplates.filter(t => t.identifier == identifier);
+ if (!temp.length)
+ return null;
+ return temp[0];
+ }
+
+ constructor(identifier)
+ {
+ Object.assign(this, Engine.ReadJSONFile("campaigns/" + identifier + ".json"));
+
+ this.identifier = identifier;
+
+ if (this.Interface)
+ this.interface = this.Interface;
+ else
+ this.interface = "default_menu";
+
+ if (!this.isValid())
+ throw ("Campaign template " + this.identifier + ".json is not a valid campaign template.");
+ }
+
+ isValid()
+ {
+ return this.Name;
+ }
+}
Index: binaries/data/mods/public/gui/common/campaigns/utils.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/common/campaigns/utils.js
@@ -0,0 +1,41 @@
+function _(obj)
+{
+ return Engine.GetGUIObjectByName(obj);
+}
+
+function _watch(object, callback)
+{
+ return new Proxy(object, {
+ "get": (obj, key) => {
+ return obj[key];
+ },
+ "set": (obj, key, value) => {
+ obj[key] = value;
+ callback();
+ return true;
+ }
+ });
+}
+
+class DefaultPage
+{
+ _watch(object, lambda = null)
+ {
+ if (lambda == null)
+ lambda = () => {
+ if (this._ready)
+ this.render();
+ };
+ return _watch(object, lambda);
+ }
+
+ constructor()
+ {
+ this._ready = false;
+ return this._watch(this);
+ }
+
+ render()
+ {
+ }
+}
Index: binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
===================================================================
--- binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
+++ binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
@@ -104,20 +104,18 @@
{
if (initData && initData.map && initData.mapType)
{
- Object.defineProperty(this, "autostart", {
- "value": true,
- "writable": false,
- "configurable": false
- });
+ if (initData.autostart)
+ Object.defineProperty(this, "autostart", {
+ "value": true,
+ "writable": false,
+ "configurable": false
+ });
// TODO: Fix g_GameAttributes, g_GameAttributes.settings,
// g_GameAttributes.settings.PlayerData object references and
// copy over each attribute individually when receiving
// settings from the server or the local file.
- g_GameAttributes = {
- "mapType": initData.mapType,
- "map": initData.map
- };
+ g_GameAttributes = initData;
this.updateGameAttributes();
// Don't launchGame before all Load handlers finished
Index: binaries/data/mods/public/gui/loadgame/SavegameList.js
===================================================================
--- binaries/data/mods/public/gui/loadgame/SavegameList.js
+++ binaries/data/mods/public/gui/loadgame/SavegameList.js
@@ -8,11 +8,15 @@
*/
class SavegameList
{
- constructor()
+ constructor(campaignRun)
{
this.savedGamesMetadata = [];
this.selectionChangeHandlers = [];
+ // If not null, only show games for the following campaign run
+ // (campaign save-games are not shown by default).
+ this.campaignRun = campaignRun;
+
this.gameSelection = Engine.GetGUIObjectByName("gameSelection");
this.gameSelectionFeedback = Engine.GetGUIObjectByName("gameSelectionFeedback");
this.confirmButton = Engine.GetGUIObjectByName("confirmButton");
@@ -67,7 +71,13 @@
let engineInfo = Engine.GetEngineInfo();
if (this.compatibilityFilter.checked)
- savedGames = savedGames.filter(game => this.isCompatibleSavegame(game.metadata, engineInfo));
+ savedGames = savedGames.filter(game => {
+ return this.isCompatibleSavegame(game.metadata, engineInfo) &&
+ this.campaignFilter(game.metadata, this.campaignRun);
+ });
+ else if (this.campaignRun)
+ savedGames = savedGames.filter(game => this.campaignFilter(game.metadata, this.campaignRun));
+
this.gameSelection.enabled = !!savedGames.length;
this.gameSelectionFeedback.hidden = !!savedGames.length;
@@ -114,7 +124,8 @@
});
let list = this.savedGamesMetadata.map(metadata => {
- let isCompatible = this.isCompatibleSavegame(metadata, engineInfo);
+ let isCompatible = this.isCompatibleSavegame(metadata, engineInfo) &&
+ this.campaignFilter(metadata, this.campaignRun);
return {
"date": this.generateSavegameDateString(metadata, engineInfo),
"mapName": compatibilityColor(translate(metadata.initAttributes.settings.Name), isCompatible),
@@ -144,6 +155,15 @@
this.gameSelection.selected = this.savedGamesMetadata.length - 1;
}
+ campaignFilter(metadata, campaignRun)
+ {
+ if (!campaignRun)
+ return !metadata.initAttributes.campaignData;
+ if (metadata.initAttributes.campaignData)
+ return metadata.initAttributes.campaignData.run == campaignRun;
+ return false;
+ }
+
isCompatibleSavegame(metadata, engineInfo)
{
return engineInfo &&
Index: binaries/data/mods/public/gui/loadgame/SavegamePage.js
===================================================================
--- binaries/data/mods/public/gui/loadgame/SavegamePage.js
+++ binaries/data/mods/public/gui/loadgame/SavegamePage.js
@@ -17,7 +17,7 @@
{
constructor(data)
{
- this.savegameList = new SavegameList();
+ this.savegameList = new SavegameList(data && data.campaignRun || null);
this.savegameDetails = new SavegameDetails();
this.savegameList.registerSelectionChangeHandler(this.savegameDetails);
Index: binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js
===================================================================
--- binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js
+++ binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js
@@ -38,7 +38,7 @@
0, 0, 100, 0);
button.caption = item.caption;
button.tooltip = item.tooltip;
- button.enabled = item.enabled === undefined || item.enabled;
+ button.enabled = item.enabled === undefined || item.enabled();
button.onPress = this.pressButton.bind(this, item, i);
button.hidden = false;
});
Index: binaries/data/mods/public/gui/pregame/MainMenuItems.js
===================================================================
--- binaries/data/mods/public/gui/pregame/MainMenuItems.js
+++ binaries/data/mods/public/gui/pregame/MainMenuItems.js
@@ -57,11 +57,6 @@
Engine.SwitchGuiPage("page_gamesetup.xml");
}
},
- {
- "caption": translate("Campaigns"),
- "tooltip": translate("Relive history through historical military campaigns. \\[NOT YET IMPLEMENTED]"),
- "enabled": false
- },
{
"caption": translate("Load Game"),
"tooltip": translate("Load a saved game."),
@@ -69,6 +64,30 @@
Engine.PushGuiPage("page_loadgame.xml");
}
},
+ {
+ "caption": translate("Continue Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage(CampaignRun.getCurrentRun().getMenuPath(), {
+ "filename": CampaignRun.getCurrentRun().filename
+ });
+ },
+ "enabled": () => !!CampaignRun.getCurrentRun()
+ },
+ {
+ "caption": translate("New Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage("campaigns/setup/page.xml");
+ }
+ },
+ {
+ "caption": translate("Load Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.PushGuiPage("campaigns/load_modal/page.xml");
+ }
+ },
{
"caption": translate("Replays"),
"tooltip": translate("Playback previous games."),
@@ -112,7 +131,7 @@
"tooltip":
colorizeHotkey(translate("%(hotkey)s: Launch the multiplayer lobby to join and host publicly visible games and chat with other players."), "lobby") +
(Engine.StartXmppClient ? "" : translate("Launch the multiplayer lobby. \\[DISABLED BY BUILD]")),
- "enabled": !!Engine.StartXmppClient,
+ "enabled": () => !!Engine.StartXmppClient,
"hotkey": "lobby",
"onPress": () => {
if (Engine.StartXmppClient)
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,7 @@
+
Index: binaries/data/mods/public/gui/session/MenuButtons.js
===================================================================
--- binaries/data/mods/public/gui/session/MenuButtons.js
+++ binaries/data/mods/public/gui/session/MenuButtons.js
@@ -61,7 +61,10 @@
Engine.PushGuiPage(
"page_loadgame.xml",
- { "savedGameData": getSavedGameData() },
+ {
+ "savedGameData": getSavedGameData(),
+ "campaignRun": g_CampaignSession ? g_CampaignSession.run.filename : null
+ },
resumeGame);
}
};
Index: binaries/data/mods/public/gui/session/campaigns/CampaignSession.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/session/campaigns/CampaignSession.js
@@ -0,0 +1,42 @@
+class CampaignSession
+{
+ constructor(data)
+ {
+ this.run = new CampaignRun(data.run).load();
+ this.levelID = data.levelID;
+ registerPlayersFinishedHandler(this.onFinish.bind(this));
+ this.won = false;
+ }
+
+ onFinish(players, won)
+ {
+ let playerID = Engine.GetPlayerID();
+ if (players.indexOf(playerID) === -1)
+ return;
+
+ this.won = won;
+ if (this.won)
+ this.run.markLevelComplete(this.levelID);
+
+ let endData = Engine.GuiInterfaceCall("GetCampaignGameEndData", {
+ "player": playerID
+ });
+ this.run.setData(endData);
+ }
+
+ getMenu()
+ {
+ return this.run.getMenuPath();
+ }
+
+ getInitData()
+ {
+ return {
+ "filename": this.run.filename,
+ "finishedLevel": this.levelID,
+ "won": this.won,
+ };
+ }
+}
+
+var g_CampaignSession;
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
@@ -264,6 +264,9 @@
restoreSavedGameData(initData.savedGUIData);
}
+ if (g_GameAttributes.campaignData)
+ g_CampaignSession = new CampaignSession(g_GameAttributes.campaignData);
+
let mapCache = new MapCache();
g_Cheats = new Cheats();
g_DiplomacyColors = new DiplomacyColors();
@@ -521,7 +524,7 @@
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
- Engine.SwitchGuiPage("page_summary.xml", {
+ let summaryData = {
"sim": simData,
"gui": {
"dialog": false,
@@ -531,7 +534,22 @@
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
- });
+ };
+
+ if (g_GameAttributes.campaignData)
+ {
+ let menu = g_CampaignSession.getMenu();
+ let initData = g_CampaignSession.getInitData();
+ if (g_GameAttributes.campaignData.skipSummary)
+ {
+ Engine.SwitchGuiPage(menu, initData);
+ return;
+ }
+ summaryData.campaignData = initData;
+ summaryData.nextPage = menu;
+ }
+
+ Engine.SwitchGuiPage("page_summary.xml", summaryData);
}
// Return some data that we'll use when hotloading this file after changes
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
@@ -3,7 +3,9 @@
+
+
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
@@ -144,6 +144,7 @@
g_ScorePanelsData = getScorePanelsData();
g_PanelButtons = Object.keys(g_ScorePanelsData).concat(["charts"]).map(panel => panel + "PanelButton");
+
g_SelectedPanel = g_PanelButtons[0];
if (data && data.selectedData)
{
@@ -455,6 +456,8 @@
"replaySelectionData": g_GameData.gui.replaySelectionData,
"summarySelectedData": summarySelectedData
});
+ else if (g_GameData.campaignData)
+ Engine.SwitchGuiPage(g_GameData.nextPage, g_GameData.campaignData);
else
Engine.SwitchGuiPage("page_pregame.xml");
}
Index: binaries/data/mods/public/l10n/.tx/config
===================================================================
--- binaries/data/mods/public/l10n/.tx/config
+++ binaries/data/mods/public/l10n/.tx/config
@@ -31,6 +31,11 @@
source_file = public-gui-other.pot
source_lang = en
+[0ad.public-gui-campaigns]
+file_filter = .public-gui-campaigns.po
+source_file = public-gui-campaigns.pot
+source_lang = en
+
[0ad.public-gui-userreport]
file_filter = .public-gui-userreport.po
source_file = public-gui-userreport.pot
Index: binaries/data/mods/public/l10n/messages.json
===================================================================
--- binaries/data/mods/public/l10n/messages.json
+++ binaries/data/mods/public/l10n/messages.json
@@ -240,6 +240,71 @@
}
]
},
+ {
+ "output": "public-gui-campaigns.pot",
+ "inputRoot": "..",
+ "project": "0 A.D. — Empires Ascendant",
+ "copyrightHolder": "Wildfire Games",
+ "rules": [
+ {
+ "extractor": "javascript",
+ "filemasks": [
+ "gui/campaigns/**.js",
+ "gui/campaigns/default_menu/**.js",
+ "gui/campaigns/load_modal/**.js",
+ "gui/campaigns/new_modal/**.js",
+ "gui/campaigns/setup/**.js",
+ "gui/common/campaigns/**.js"
+ ],
+ "options": {
+ "format": "javascript-format",
+ "keywords": {
+ "translate": [1],
+ "translatePlural": [1, 2],
+ "translateWithContext": [[1], 2],
+ "translatePluralWithContext": [[1], 2, 3],
+ "markForTranslation": [1],
+ "markForTranslationWithContext": [[1], 2],
+ "markForPluralTranslation": [1, 2]
+ },
+ "commentTags": [
+ "Translation:"
+ ]
+ }
+ },
+ {
+ "extractor": "xml",
+ "filemasks": [
+ "gui/campaigns/**.xml",
+ "gui/campaigns/default_menu/**.xml",
+ "gui/campaigns/load_modal/**.xml",
+ "gui/campaigns/new_modal/**.xml",
+ "gui/campaigns/setup/**.xml",
+ "gui/common/campaigns/**.xml"
+ ],
+ "options": {
+ "keywords": {
+ "translatableAttribute": {
+ "locationAttributes": ["id"]
+ },
+ "translate": {}
+ }
+ }
+ },
+ {
+ "extractor": "json",
+ "filemasks": [
+ "campaigns/**.json"
+ ],
+ "options": {
+ "keywords": [
+ "Name",
+ "Description"
+ ]
+ }
+ }
+ ]
+ },
{
"output": "public-gui-other.pot",
"inputRoot": "..",
Index: binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
===================================================================
--- binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
+++ binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.js
@@ -6,6 +6,10 @@
"instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."),
"OnPlayerCommand": function(msg)
{
+ let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
+ cmpEndGameManager.MarkPlayerAndAlliesAsWon(1, () => "", () => "");
+ this.NextGoal();
+
if (msg.cmd.type == "gather" && msg.cmd.target &&
TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit")
this.NextGoal();
@@ -407,6 +411,13 @@
}
};
+Trigger.prototype.OnCampaignGameEnd = function(campaignData)
+{
+ return Object.assign(campaignData, {
+ "somearbitrarydata": 4
+ });
+};
+
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.playerID = 1;
Index: binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
===================================================================
--- binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
+++ binaries/data/mods/public/maps/tutorials/Introductory_Tutorial.xml
@@ -40,7 +40,7 @@
{
"CircularMap": true,
"Description": "This is a basic tutorial to get you started playing 0 A.D.",
- "Keywords": ["demo"],
+ "Keywords": ["trigger"],
"LockTeams": false,
"Name": "Introductory Tutorial",
"PlayerData": [
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
@@ -205,6 +205,17 @@
};
};
+/**
+ *
+ */
+GuiInterface.prototype.GetCampaignGameEndData = function(player)
+{
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ if (Trigger.prototype.OnCampaignGameEnd)
+ return Trigger.prototype.OnCampaignGameEnd(clone(InitAttributes.campaignData.data));
+ return InitAttributes.campaignData.data;
+};
+
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
@@ -1938,6 +1949,7 @@
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
+ "GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
Index: build/premake/extern_libs5.lua
===================================================================
--- build/premake/extern_libs5.lua
+++ build/premake/extern_libs5.lua
@@ -282,6 +282,7 @@
-- Manually add gnutls dependencies, those are not present in gloox's pkg-config
add_default_lib_paths("nettle")
add_default_lib_paths("gmp")
+ add_default_lib_paths("gnutls")
add_default_links({
osx_names = { "nettle", "hogweed", "gmp" },
})
Index: build/workspaces/update-workspaces.sh
===================================================================
--- build/workspaces/update-workspaces.sh
+++ build/workspaces/update-workspaces.sh
@@ -66,6 +66,9 @@
cd "$(dirname $0)"
# Now in build/workspaces/ (where we assume this script resides)
+# remove resource-fork files.
+dot_clean -mn ../../
+
if [ "`uname -s`" = "Darwin" ]; then
# Set minimal SDK version
export MIN_OSX_VERSION=${MIN_OSX_VERSION:="10.9"}
Index: libraries/osx/build-osx-libs.sh
===================================================================
--- libraries/osx/build-osx-libs.sh
+++ libraries/osx/build-osx-libs.sh
@@ -37,7 +37,7 @@
GMP_VERSION="gmp-6.1.2"
NETTLE_VERSION="nettle-3.5.1"
# NOTE: remember to also update LIB_URL below when changing version
-GNUTLS_VERSION="gnutls-3.6.8"
+GNUTLS_VERSION="gnutls-3.6.13"
GLOOX_VERSION="gloox-1.0.22"
# OS X only includes part of ICU, and only the dylib
# NOTE: remember to also update LIB_URL below when changing version
@@ -663,8 +663,13 @@
LIB_DIRECTORY="$LIB_VERSION"
LIB_URL="https://www.gnupg.org/ftp/gcrypt/gnutls/v3.6/"
-mkdir -p gnutls
-pushd gnutls > /dev/null
+
+PATCH_DIR="$(pwd)/patches"
+
+[[ -e gnutls ]] && mv gnutls /tmp/
+
+mkdir -p /tmp/gnutls
+pushd /tmp/gnutls > /dev/null
GNUTLS_DIR="$(pwd)"
@@ -682,7 +687,7 @@
# GnuTLS 3.6.8 added the TCP Fast Open feature, which requires connectx
# but that's only available on OS X 10.11+ (GnuTLS doesn't support SDK based builds yet)
# So we disable that functionality
- (patch -Np0 -i ../../patches/gnutls-disable-tcpfastopen.diff \
+ (patch -Np0 -i "$PATCH_DIR/gnutls-disable-tcpfastopen.diff" \
&& ./configure CFLAGS="$CFLAGS" \
CXXFLAGS="$CXXFLAGS" \
LDFLAGS="$LDFLAGS" \
@@ -700,8 +705,9 @@
--with-included-libtasn1 \
--without-p11-kit \
--disable-tests \
+ --disable-guile \
--disable-nls \
- && make ${JOBS} && make install) || die "GnuTLS build failed"
+ && make ${JOBS} LDFLAGS= install) || die "GnuTLS build failed"
popd
echo "$LIB_VERSION" > .already-built
else
@@ -709,6 +715,8 @@
fi
popd > /dev/null
+mv /tmp/gnutls .
+
# --------------------------------------------------------------
echo -e "Building gloox..."
@@ -874,8 +882,10 @@
LIB_DIRECTORY="$LIB_VERSION"
LIB_URL="https://download.libsodium.org/libsodium/releases/"
-mkdir -p libsodium
-pushd libsodium > /dev/null
+[[ -e libsodium ]] && mv libsodium /tmp/
+
+mkdir -p /tmp/libsodium
+pushd /tmp/libsodium > /dev/null
if [[ "$force_rebuild" = "true" ]] || [[ ! -e .already-built ]] || [[ "$(<.already-built)" != "$LIB_VERSION" ]]
then
@@ -904,6 +914,8 @@
fi
popd > /dev/null
+mv /tmp/libsodium .
+
# --------------------------------------------------------------------
# The following libraries are shared on different OSes and may
# be customized, so we build and install them from bundled sources
Index: libraries/source/nvtt/README.txt
===================================================================
--- libraries/source/nvtt/README.txt
+++ libraries/source/nvtt/README.txt
@@ -4,6 +4,8 @@
cmake-freebsd.patch (fixes build on FreeBSD)
issue188.patch (fixes http://code.google.com/p/nvidia-texture-tools/issues/detail?id=188)
issue261.patch (fixes https://github.com/castano/nvidia-texture-tools/issues/261)
+ pr270.patch (from https://github.com/castano/nvidia-texture-tools/pull/270)
rpath.patch (fixes .so file search paths for bundled copy)
win-shared-build.patch (adapted from https://github.com/castano/nvidia-texture-tools/pull/285)
musl-build.patch (fixes build on musl linux; contributed by voroskoi, with a part by leper, see https://code.wildfiregames.com/D2491)
+ arm-build.patch (fixes build on non-aarch64 arm, TODO send upstream after CI tests it)
Index: libraries/source/nvtt/src/src/nvconfig.h.in
===================================================================
--- libraries/source/nvtt/src/src/nvconfig.h.in
+++ libraries/source/nvtt/src/src/nvconfig.h.in
@@ -1,7 +1,11 @@
#ifndef NV_CONFIG
#define NV_CONFIG
+#if NV_OS_DARWIN && !NV_OS_IOS
+#cmakedefine01 HAVE_UNISTD_H
+#else
#cmakedefine HAVE_UNISTD_H
+#endif
#cmakedefine HAVE_STDARG_H
#cmakedefine HAVE_SIGNAL_H
#cmakedefine HAVE_EXECINFO_H
Index: libraries/source/nvtt/src/src/nvcore/Debug.cpp
===================================================================
--- libraries/source/nvtt/src/src/nvcore/Debug.cpp
+++ libraries/source/nvtt/src/src/nvcore/Debug.cpp
@@ -665,6 +665,9 @@
# elif NV_CPU_PPC
ucontext_t * ucp = (ucontext_t *)secret;
return (void *) ucp->uc_mcontext.regs->nip;
+# elif NV_CPU_ARM
+ ucontext_t * ucp = (ucontext_t *)secret;
+ return (void *) ucp->uc_mcontext.arm_pc;
# elif NV_CPU_AARCH64
ucontext_t * ucp = (ucontext_t *)secret;
return (void *) ucp->uc_mcontext.pc;
@@ -1021,7 +1024,8 @@
writeStackTrace(trace, size, callstackLevelsToSkip + 1, lines); // + 1 to skip the call to dumpCallstack
for (uint i = 0; i < lines.count(); i++) {
- messageHandler->log(lines[i], NULL);
+ va_list empty_va_list = {};
+ messageHandler->log(lines[i], empty_va_list);
delete lines[i];
}
}
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -462,6 +462,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
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -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
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -213,6 +213,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)
@@ -264,6 +273,7 @@
scriptInterface.RegisterFunction("ReadFileLines");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
+ scriptInterface.RegisterFunction("DeleteFile");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
@@ -278,4 +288,5 @@
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
+ scriptInterface.RegisterFunction("WriteJSONFile");
}