From 7353985062975bfead0aa70713273cd06d31209c Mon Sep 17 00:00:00 2001
From: James Sherratt <jdsherratt3@gmail.com>
Date: Thu, 3 Jun 2021 23:57:39 +0100
Subject: [PATCH] Add support for 0AD A25 (#7)

* Add support for 0AD A25

* AllyMap -> AllyView + bump version.

* All "Ally Map" changed to "Ally View". Also changed code names for
consistency.
* Bump version number

* Add missing newlines (to meet unix file spec).

* Remove pointless comments

* Reduce code + more mod-friendly

Rather than re-defining entire classes/ functions, just extend the code
we want to extend (by e.g. saving game code temporarily an "old"
variable and re-defining). Reduces code duplication + more friendly
towards other mods that may want to modify the same classes/ functions.
---
 a25/README.md                                 |  10 +
 .../common/gamedescription_CartographyMode.js | 254 ++++++++++++++++++
 .../MapExploration_CartographyMode.js         |  37 +++
 .../GameSettingsLayout_CartographyMode.js     |   1 +
 .../Single/Checkboxes/AlliedMap.js            |  28 ++
 .../Panels/GameDescription_CartographyMode.js |   8 +
 ...GB.public-gui-gamesetup-CartographyMode.po |  11 +
 a25/mod.json                                  |   7 +
 a25/simulation/helpers/InitGame.js            | 103 +++++++
 9 files changed, 459 insertions(+)
 create mode 100644 a25/README.md
 create mode 100644 a25/gui/common/gamedescription_CartographyMode.js
 create mode 100644 a25/gui/gamesettings/attributes/MapExploration_CartographyMode.js
 create mode 100644 a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout_CartographyMode.js
 create mode 100644 a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/AlliedMap.js
 create mode 100644 a25/gui/gamesetup/Pages/GameSetupPage/Panels/GameDescription_CartographyMode.js
 create mode 100644 a25/l10n/en_GB.public-gui-gamesetup-CartographyMode.po
 create mode 100644 a25/mod.json
 create mode 100644 a25/simulation/helpers/InitGame.js

diff --git a/a25/README.md b/a25/README.md
new file mode 100644
index 0000000..2d6299d
--- /dev/null
+++ b/a25/README.md
@@ -0,0 +1,10 @@
+# CartographyMode
+
+A [0 A.D.](https://play0ad.com/) mod; adds an "ally view" option to the
+game set-up menu, which allows allies to share vision at game start.
+
+[Forum post](https://wildfiregames.com/forum/index.php?/topic/27265-theres-any-mod-so-you-can-see-what-your-allies-see-from-the-start/&do=findComment&comment=397504)
+
+This mod was created by
+[Jammyjamjamman](https://github.com/jammyjamjamman) and maintained by
+him and [Andy Alt](https://github.com/andy5995).
diff --git a/a25/gui/common/gamedescription_CartographyMode.js b/a25/gui/common/gamedescription_CartographyMode.js
new file mode 100644
index 0000000..bc64832
--- /dev/null
+++ b/a25/gui/common/gamedescription_CartographyMode.js
@@ -0,0 +1,254 @@
+/**
+ * Sets an additional map label, map preview image and describes the chosen gamesettings more closely.
+ *
+ * Requires g_GameAttributes and g_VictoryConditions.
+ */
+function getGameDescriptionList(initAttributes, mapCache)
+{
+	let titles = [];
+	if (!initAttributes.settings.VictoryConditions.length)
+		titles.push({
+			"label": translateWithContext("victory condition", "Endless Game"),
+			"value": translate("No winner will be determined, even if everyone is defeated.")
+		});
+
+	for (let victoryCondition of g_VictoryConditions)
+	{
+		if (initAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) == -1)
+			continue;
+
+		let title = translateVictoryCondition(victoryCondition.Name);
+		if (victoryCondition.Name == "wonder")
+		{
+			let wonderDuration = Math.round(initAttributes.settings.WonderDuration);
+			title = sprintf(
+				translatePluralWithContext(
+					"victory condition",
+					"Wonder (%(min)s minute)",
+					"Wonder (%(min)s minutes)",
+					wonderDuration
+				),
+				{ "min": wonderDuration });
+		}
+
+		let isCaptureTheRelic = victoryCondition.Name == "capture_the_relic";
+		if (isCaptureTheRelic)
+		{
+			let relicDuration = Math.round(initAttributes.settings.RelicDuration);
+			title = sprintf(
+				translatePluralWithContext(
+					"victory condition",
+					"Capture the Relic (%(min)s minute)",
+					"Capture the Relic (%(min)s minutes)",
+					relicDuration
+				),
+				{ "min": relicDuration });
+		}
+
+		titles.push({
+			"label": title,
+			"value": victoryCondition.Description
+		});
+
+		if (isCaptureTheRelic)
+			titles.push({
+				"label": translate("Relic Count"),
+				"value": Math.round(initAttributes.settings.RelicCount)
+			});
+
+		if (victoryCondition.Name == "regicide")
+			if (initAttributes.settings.RegicideGarrison)
+				titles.push({
+					"label": translate("Hero Garrison"),
+					"value": translate("Heroes can be garrisoned.")
+				});
+			else
+				titles.push({
+					"label": translate("Exposed Heroes"),
+					"value": translate("Heroes cannot be garrisoned and they are vulnerable to raids.")
+				});
+	}
+
+	if (initAttributes.settings.RatingEnabled &&
+	    initAttributes.settings.PlayerData.length == 2)
+		titles.push({
+			"label": translate("Rated game"),
+			"value": translate("When the winner of this match is determined, the lobby score will be adapted.")
+		});
+
+	if (initAttributes.settings.LockTeams)
+		titles.push({
+			"label": translate("Locked Teams"),
+			"value": translate("Players can't change the initial teams.")
+		});
+	else
+		titles.push({
+			"label": translate("Diplomacy"),
+			"value": translate("Players can make alliances and declare war on allies.")
+		});
+
+	if (initAttributes.settings.LastManStanding)
+		titles.push({
+			"label": translate("Last Man Standing"),
+			"value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.")
+		});
+	else
+		titles.push({
+			"label": translate("Allied Victory"),
+			"value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.")
+		});
+
+	let ceasefire = Math.round(initAttributes.settings.Ceasefire);
+	titles.push({
+		"label": translate("Ceasefire"),
+		"value":
+			!ceasefire ?
+				translate("disabled") :
+				sprintf(translatePlural(
+					"For the first minute, other players will stay neutral.",
+					"For the first %(min)s minutes, other players will stay neutral.",
+					ceasefire),
+				{ "min": ceasefire })
+	});
+
+	if (initAttributes.map == "random")
+		titles.push({
+			"label": translateWithContext("Map Selection", "Random Map"),
+			"value": translate("Randomly select a map from the list.")
+		});
+	else
+	{
+		titles.push({
+			"label": translate("Map Name"),
+			"value": mapCache.translateMapName(
+				mapCache.getTranslatableMapName(initAttributes.mapType, initAttributes.map, initAttributes))
+		});
+
+		titles.push({
+			"label": translate("Map Description"),
+			"value": mapCache.getTranslatedMapDescription(initAttributes.mapType, initAttributes.map)
+		});
+	}
+
+	titles.push({
+		"label": translate("Map Type"),
+		"value": g_MapTypes.Title[g_MapTypes.Name.indexOf(initAttributes.mapType)]
+	});
+
+	if (initAttributes.mapType == "random")
+	{
+		let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(initAttributes.settings.Size)];
+		if (mapSize)
+			titles.push({
+				"label": translate("Map Size"),
+				"value": mapSize
+			});
+	}
+
+	if (initAttributes.settings.Biome)
+	{
+		let biome = g_Settings.Biomes.find(b => b.Id == initAttributes.settings.Biome);
+		titles.push({
+			"label": biome ? biome.Title : translateWithContext("biome", "Random Biome"),
+			"value": biome ? biome.Description : translate("Randomly select a biome from the list.")
+		});
+	}
+
+	if (initAttributes.settings.TriggerDifficulty !== undefined)
+	{
+		let triggerDifficulty = g_Settings.TriggerDifficulties.find(difficulty => difficulty.Difficulty == initAttributes.settings.TriggerDifficulty);
+		titles.push({
+			"label": triggerDifficulty.Title,
+			"value": triggerDifficulty.Tooltip
+		});
+	}
+
+	if (initAttributes.settings.Nomad !== undefined)
+		titles.push({
+			"label": initAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"),
+			"value":
+				initAttributes.settings.Nomad ?
+					translate("Players start with only few units and have to find a suitable place to build their city.") :
+					translate("Players start with a Civic Center.")
+		});
+
+	if (initAttributes.settings.StartingResources !== undefined)
+		titles.push({
+			"label": translate("Starting Resources"),
+			"value":
+				initAttributes.settings.PlayerData &&
+				initAttributes.settings.PlayerData.some(pData => pData && pData.Resources !== undefined) ?
+					translateWithContext("starting resources", "Per Player") :
+					sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), {
+						"startingResourcesTitle":
+							g_StartingResources.Title[
+								g_StartingResources.Resources.indexOf(
+									initAttributes.settings.StartingResources)],
+						"amount": initAttributes.settings.StartingResources
+					})
+		});
+
+	if (initAttributes.settings.PopulationCap !== undefined)
+		titles.push({
+			"label": translate("Population Limit"),
+			"value":
+				initAttributes.settings.PlayerData &&
+				initAttributes.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined) ?
+					translateWithContext("population limit", "Per Player") :
+					g_PopulationCapacities.Title[
+						g_PopulationCapacities.Population.indexOf(
+							initAttributes.settings.PopulationCap)]
+		});
+
+	if (initAttributes.settings.WorldPopulationCap !== undefined)
+		titles.push({
+			"label": translate("World Population Cap"),
+			"value":
+				g_WorldPopulationCapacities.Title[
+					g_WorldPopulationCapacities.Population.indexOf(
+						initAttributes.settings.WorldPopulationCap)]
+		});
+
+	titles.push({
+		"label": translate("Treasures"),
+		"value": initAttributes.settings.DisableTreasures ?
+			translateWithContext("treasures", "Disabled") :
+			translateWithContext("treasures", "As defined by the map.")
+	});
+
+	titles.push({
+		"label": translate("Explored Map"),
+		"value": initAttributes.settings.ExploreMap
+	});
+
+	titles.push({
+		"label": translate("Revealed Map"),
+		"value": initAttributes.settings.RevealMap
+	});
+
+	titles.push({
+		"label": translate("Cheats"),
+		"value": initAttributes.settings.CheatsEnabled
+	});
+
+	return titles;
+}
+
+function modDescriptions(initAttributes, titles) {
+	titles.push({
+		"label": translate("Allied View"),
+		"value": initAttributes.settings.AllyView
+	});
+    return titles;
+}
+
+function getGameDescription(initAttributes, mapCache) {
+	let titles = getGameDescriptionList(initAttributes, mapCache);
+    titles = modDescriptions(initAttributes, titles);
+	return titles.map(title => sprintf(translate("%(label)s %(details)s"), {
+		"label": coloredText(title.label, g_DescriptionHighlight),
+		"details":
+			title.value === true ? translateWithContext("game setup option", "enabled") :
+				title.value || translateWithContext("game setup option", "disabled")
+	})).join("\n");
+}
diff --git a/a25/gui/gamesettings/attributes/MapExploration_CartographyMode.js b/a25/gui/gamesettings/attributes/MapExploration_CartographyMode.js
new file mode 100644
index 0000000..606362a
--- /dev/null
+++ b/a25/gui/gamesettings/attributes/MapExploration_CartographyMode.js
@@ -0,0 +1,37 @@
+{
+	let initOld = GameSettings.prototype.Attributes.MapExploration.prototype.init;
+	GameSettings.prototype.Attributes.MapExploration.prototype.init = function () {
+		initOld.call(this);
+		this.allied = false;
+	}
+
+	let toInitAttributesOld = GameSettings.prototype.Attributes.MapExploration.prototype.toInitAttributes;
+	GameSettings.prototype.Attributes.MapExploration.prototype.toInitAttributes = function (attribs) {
+		toInitAttributesOld.call(this, attribs);
+		attribs.settings.AllyView = this.allied;
+	}
+
+	let fromInitAttributesOld = GameSettings.prototype.Attributes.MapExploration.prototype.fromInitAttributes;
+	GameSettings.prototype.Attributes.MapExploration.prototype.fromInitAttributes = function (attribs) {
+		fromInitAttributesOld.call(this, attribs);
+		this.allied = !!this.getLegacySetting(attribs, "AllyView");
+	}
+
+	let onMapChangeOld = GameSettings.prototype.Attributes.MapExploration.prototype.onMapChange;
+	GameSettings.prototype.Attributes.MapExploration.prototype.onMapChange = function (mapData) {
+		onMapChangeOld.call(this, mapData);
+		this.setRevealed(this.getMapSetting("AllyView"));
+	}
+
+	let setRevealedOld = GameSettings.prototype.Attributes.MapExploration.prototype.setRevealed;
+	GameSettings.prototype.Attributes.MapExploration.prototype.setRevealed = function (enabled) {
+		setRevealedOld.call(this, enabled);
+		this.allied = this.allied || enabled;
+	}
+
+	GameSettings.prototype.Attributes.MapExploration.prototype.setAllied = function (enabled) {
+		this.allied = enabled;
+		this.revealed = this.revealed && this.allied;
+	}
+
+}
diff --git a/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout_CartographyMode.js b/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout_CartographyMode.js
new file mode 100644
index 0000000..808245c
--- /dev/null
+++ b/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingsLayout_CartographyMode.js
@@ -0,0 +1 @@
+g_GameSettingsLayout[0]["settings"].push("AlliedView"); 
diff --git a/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/AlliedMap.js b/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/AlliedMap.js
new file mode 100644
index 0000000..736be78
--- /dev/null
+++ b/a25/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Checkboxes/AlliedMap.js
@@ -0,0 +1,28 @@
+GameSettingControls.AlliedView = class AlliedView extends GameSettingControlCheckbox
+{
+	constructor(...args)
+	{
+		super(...args);
+		g_GameSettings.mapExploration.watch(() => this.render(), ["allied"]);
+		g_GameSettings.map.watch(() => this.render(), ["type"]);
+		this.render();
+	}
+
+	render()
+	{
+		this.setEnabled(g_GameSettings.map.type != "scenario");
+		this.setChecked(g_GameSettings.mapExploration.allied);
+	}
+
+	onPress(checked)
+	{
+		g_GameSettings.mapExploration.setAllied(checked);
+		this.gameSettingsController.setNetworkInitAttributes();
+	}
+};
+
+GameSettingControls.AlliedView.prototype.TitleCaption =
+	translate("Allied View");
+
+GameSettingControls.AlliedView.prototype.Tooltip =
+	translate("Toggle allied view (see what your allies see).");
diff --git a/a25/gui/gamesetup/Pages/GameSetupPage/Panels/GameDescription_CartographyMode.js b/a25/gui/gamesetup/Pages/GameSetupPage/Panels/GameDescription_CartographyMode.js
new file mode 100644
index 0000000..a97145b
--- /dev/null
+++ b/a25/gui/gamesetup/Pages/GameSetupPage/Panels/GameDescription_CartographyMode.js
@@ -0,0 +1,8 @@
+{
+	let registerWatchersOld = GameDescription.prototype.registerWatchers;
+	GameDescription.prototype.registerWatchers = function () {
+		registerWatchersOld.call(this);
+		let update = () => this.updateGameDescription();
+		g_GameSettings.mapExploration.watch(update, ["allied"]);
+	}
+}
diff --git a/a25/l10n/en_GB.public-gui-gamesetup-CartographyMode.po b/a25/l10n/en_GB.public-gui-gamesetup-CartographyMode.po
new file mode 100644
index 0000000..c7dbdee
--- /dev/null
+++ b/a25/l10n/en_GB.public-gui-gamesetup-CartographyMode.po
@@ -0,0 +1,11 @@
+#. Translation: Make sure to differentiate between the revealed map and
+#. ally view settings!
+#: gui/gamesetup/gamesetup_CartographMode.js
+msgid "Ally View"
+msgstr "Ally View"
+
+#. Translation: Make sure to differentiate between the revealed map and
+#. ally view settings!
+#: gui/gamesetup/gamesetup_CarographyMode.js
+msgid "Toggle allied view (see everything you + allies can see)."
+msgstr "Toggle allied view (see everything you + allies can see)."
diff --git a/a25/mod.json b/a25/mod.json
new file mode 100644
index 0000000..9df9b6c
--- /dev/null
+++ b/a25/mod.json
@@ -0,0 +1,7 @@
+{
+    "name": "CartographyMode",
+    "version": "2.0.0",
+    "label": "Cartography Mode",
+    "description": "Start a game with \"cartography mode\" researched for all players. Everyone sees what their allies can see from the start of the game.",
+    "dependencies": ["0ad=0.0.25"]
+}
diff --git a/a25/simulation/helpers/InitGame.js b/a25/simulation/helpers/InitGame.js
new file mode 100644
index 0000000..918f6a9
--- /dev/null
+++ b/a25/simulation/helpers/InitGame.js
@@ -0,0 +1,103 @@
+/**
+ * Called when the map has been loaded, but before the simulation has started.
+ * Only called when a new game is started, not when loading a saved game.
+ * 
+ * This is pretty ugly and (I think) overwrites InitGame.js in "Public" mod (aka the 0AD game).
+ * It's the only way I could get this to work because you can only "Engine.RegisterGlobal()"" once :/.
+ */
+ function PreInitGame()
+ {
+	 // We need to replace skirmish "default" entities with real ones.
+	 // This needs to happen before AI initialization (in InitGame).
+	 // And we need to flush destroyed entities otherwise the AI gets the wrong game state in
+	 // the beginning and a bunch of "destroy" messages on turn 0, which just shouldn't happen.
+	 Engine.BroadcastMessage(MT_SkirmishReplace, {});
+	 Engine.FlushDestroyedEntities();
+ 
+	 let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
+	 for (let i = 1; i < numPlayers; ++i) // ignore gaia
+	 {
+		 let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
+		 if (cmpTechnologyManager)
+			 cmpTechnologyManager.UpdateAutoResearch();
+	 }
+ 
+	 // Explore the map inside the players' territory borders
+	 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+	 cmpRangeManager.ExploreTerritories();
+ }
+ 
+ function InitGame(settings)
+ {
+	 // No settings when loading a map in Atlas, so do nothing
+	 if (!settings)
+	 {
+		 // Map dependent initialisations of components (i.e. garrisoned units)
+		 Engine.BroadcastMessage(MT_InitGame, {});
+		 return;
+	 }
+ 
+	 if (settings.ExploreMap)
+	 {
+		 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+		 for (let i = 1; i < settings.PlayerData.length; ++i)
+			 cmpRangeManager.ExploreMap(i);
+	 }
+ 
+	 if (settings.AllyView)
+	 {
+		 for (let i = 1; i < settings.PlayerData.length; ++i)
+		 {
+			 let cmpPlayer = QueryPlayerIDInterface(i);
+			 let cmpTechnologyManager = Engine.QueryInterface(cmpPlayer.entity, IID_TechnologyManager);
+			 if (cmpTechnologyManager)
+			 {
+				 cmpTechnologyManager.ResearchTechnology(cmpPlayer.template.SharedLosTech);
+				 cmpPlayer.UpdateSharedLos();
+			 }
+		 }
+	 }
+	 
+	 // Sandbox, Very Easy, Easy, Medium, Hard, Very Hard
+	 // rate apply on resource stockpiling as gathering and trading
+	 // time apply on building, upgrading, packing, training and technologies
+	 let rate = [ 0.42, 0.56, 0.75, 1.00, 1.25, 1.56 ];
+	 let time = [ 1.40, 1.25, 1.10, 1.00, 1.00, 1.00 ];
+	 let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
+	 let cmpAIManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIManager);
+	 for (let i = 0; i < settings.PlayerData.length; ++i)
+	 {
+		 let cmpPlayer = QueryPlayerIDInterface(i);
+		 cmpPlayer.SetCheatsEnabled(!!settings.CheatsEnabled);
+ 
+		 if (settings.PlayerData[i] && !!settings.PlayerData[i].AI)
+		 {
+			 let AIDiff = +settings.PlayerData[i].AIDiff;
+			 cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i, AIDiff, settings.PlayerData[i].AIBehavior || "random");
+			 cmpPlayer.SetAI(true);
+			 AIDiff = Math.min(AIDiff, rate.length - 1);
+			 cmpModifiersManager.AddModifiers("AI Bonus", {
+				 "ResourceGatherer/BaseSpeed": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
+				 "Trader/GainMultiplier": [{ "affects": ["Unit", "Structure"], "multiply": rate[AIDiff] }],
+				 "Cost/BuildTime": [{ "affects": ["Unit", "Structure"], "multiply": time[AIDiff] }],
+			 }, cmpPlayer.entity);
+		 }
+ 
+		 if (settings.PopulationCap)
+			 cmpPlayer.SetMaxPopulation(settings.PopulationCap);
+	 }
+	 if (settings.WorldPopulationCap)
+		 Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).SetMaxWorldPopulation(settings.WorldPopulationCap);
+ 
+	 // Update the grid with all entities created for the map init.
+	 Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).UpdateGrid();
+ 
+	 // Map or player data (handicap...) dependent initialisations of components (i.e. garrisoned units).
+	 Engine.BroadcastMessage(MT_InitGame, {});
+ 
+	 cmpAIManager.TryLoadSharedComponent();
+	 cmpAIManager.RunGamestateInit();
+ }
+ 
+ Engine.RegisterGlobal("PreInitGame", PreInitGame);
+ Engine.RegisterGlobal("InitGame", InitGame);