diff --git a/binaries/data/mods/public/globalscripts/Resources.js b/binaries/data/mods/public/globalscripts/Resources.js
new file mode 100644
index 0000000..04c3056
--- /dev/null
+++ b/binaries/data/mods/public/globalscripts/Resources.js
@@ -0,0 +1,164 @@
+
+/**
+ * Resources Global
+ *
+ * Engine.FindJSONFiles only exists within the session context
+ * Engine.BuildDirEntList only exists within the gui context
+ * The AI and test contexts have no access to any JSON file access functions
+ */
+function Resources()
+{
+	let jsonFiles = [];
+	if (Engine.FindJSONFiles)
+	{
+		jsonFiles = Engine.FindJSONFiles("resources", false);
+		for (let file in jsonFiles)
+			jsonFiles[file] = "resources/" + jsonFiles[file] + ".json";
+	}
+	else if (Engine.BuildDirEntList)
+		jsonFiles = Engine.BuildDirEntList("simulation/data/resources/", "*.json", false);
+	else
+	{
+		warn("Resources: No JSON access functions are unavailable");
+		return;
+	}
+
+	this.resourceData = [];
+	this.resourceCodes = [];
+
+	for (let filename of jsonFiles)
+	{
+		let data = Engine.ReadJSONFile(filename);
+		if (!data)
+			continue;
+
+		data.subtypeNames = data.subtypes;
+		data.subtypes = Object.keys(data.subtypes);
+
+		this.resourceData.push(data);
+		if (data.enabled)
+			this.resourceCodes.push(data.code);
+	}
+};
+
+Resources.prototype.GetData = function()
+{
+	return this.resourceData.filter(resource => resource.enabled);
+};
+
+Resources.prototype.GetResource = function(type)
+{
+	let lType = type.toLowerCase();
+	return this.GetData().find(resource => resource.code == lType);
+};
+
+Resources.prototype.GetCodes = function()
+{
+	return this.resourceCodes;
+};
+
+/**
+ * Returns an object containing untranslated resource names mapped to
+ * resource codes. Includes subtypes.
+ */
+Resources.prototype.GetNames = function()
+{
+	let names = {};
+	for (let res of this.GetData())
+	{
+		names[res.code] = res.name;
+		for (let subres of res.subtypes)
+			names[subres] = res.subtypeNames[subres]
+	}
+	return names;
+};
+
+/**
+ * Builds a RelaxRNG schema based on currently valid elements.
+ *
+ * To prevent validation errors, disabled resources are included in the schema.
+ *
+ * @param datatype The datatype of the element
+ * @param additional Array of additional data elements. Time, xp, treasure, etc.
+ * @param subtypes If true, resource subtypes will be included as well.
+ * @return RelaxNG schema string
+ */
+Resources.prototype.BuildSchema = function(datatype, additional = [], subtypes = false)
+{
+	if (!datatype)
+		return "";
+
+	switch (datatype)
+	{
+	case "decimal":
+	case "nonNegativeDecimal":
+	case "positiveDecimal":
+		datatype = "<ref name='" + datatype + "'/>";
+		break;
+
+	default:
+		datatype = "<data type='" + datatype + "'/>";
+	}
+
+	let resCodes = this.resourceData.map(resource => resource.code);
+	let schema = "<interleave>";
+	for (let res of resCodes.concat(additional))
+		schema +=
+			"<optional>" +
+				"<element name='" + res + "'>" +
+					datatype +
+				"</element>" +
+			"</optional>";
+
+	if (!subtypes)
+		return schema + "</interleave>";
+
+	for (let res of this.resourceData)
+		for (let subtype of res.subtypes)
+			schema +=
+				"<optional>" +
+					"<element name='" + res.code + "." + subtype + "'>" +
+						datatype +
+					"</element>" +
+				"</optional>";
+
+	if (additional.indexOf("treasure") !== -1)
+		for (let res of resCodes)
+			schema +=
+				"<optional>" +
+					"<element name='" + "treasure." + res + "'>" +
+						datatype +
+					"</element>" +
+				"</optional>";
+
+	return schema + "</interleave>";
+}
+
+/**
+ * Builds the value choices for a RelaxNG `<choice></choice>` object, based on currently valid resources.
+ *
+ * @oaram subtypes If set to true, the choices returned will be resource subtypes, rather than main types
+ * @param treasure If set to true, the pseudo resource 'treasure' (or its subtypes) will be included
+ * @return String of RelaxNG Schema `<choice/>` values.
+ */
+Resources.prototype.BuildChoicesSchema = function(subtypes = false, treasure = false)
+{
+	let schema = "<choice>";
+
+	if (!subtypes)
+	{
+		let resCodes = this.resourceData.map(resource => resource.code);
+		for (let res of resCodes.concat(treasure ? [ "treasure" ] : []))
+			schema += "<value>" + res + "</value>";
+	}
+	else
+		for (let res of this.resourceData)
+		{
+			for (let subtype of res.subtypes)
+				schema += "<value>" + res.code + "." + subtype + "</value>";
+			if (treasure)
+				schema += "<value>" + "treasure." + res.code + "</value>";
+		}
+
+	return schema + "</choice>";
+}
diff --git a/binaries/data/mods/public/gui/common/functions_utility.js b/binaries/data/mods/public/gui/common/functions_utility.js
index 9984cb0..9e89efd 100644
--- a/binaries/data/mods/public/gui/common/functions_utility.js
+++ b/binaries/data/mods/public/gui/common/functions_utility.js
@@ -416,3 +416,45 @@ function formatPlayerInfo(playerDataArray, playerStates)
 
 	return teamDescription.join("\n\n");
 }
+
+/**
+ * Horizontally fit objects within a parent.
+ *
+ * @param margin - The gap, in px, between the repeated objects
+ * @param limit - The number of elements to fit
+ */
+function horizontallyDistributeObjects(parentName, margin = 0, limit = undefined)
+{
+	let objects = Engine.GetGUIObjectByName(parentName).children;
+	if (limit !== undefined)
+		objects = objects.splice(0, limit);
+
+	for (let i in objects)
+	{
+		i = +i;
+		let size = objects[i].size;
+		size.rleft = 100 / objects.length * i;
+		size.rright = 100 / objects.length * (i + 1);
+		size.right = -margin;
+		objects[i].size = size;
+	}
+}
+
+/**
+ * Hide all children after a certain index
+ *
+ * @param prefix - The part of the element name preceeding the index
+ * @param idx - The index from which to start
+ * @param prefix - The part of the element name after the index
+ */
+function hideRemaining(prefix, idx, suffix)
+{
+	while (true)
+	{
+		let obj = Engine.GetGUIObjectByName(prefix + idx + suffix);
+		if (!obj)
+			return;
+		obj.hidden = true;
+		++idx;
+	}
+}
diff --git a/binaries/data/mods/public/gui/common/l10n.js b/binaries/data/mods/public/gui/common/l10n.js
index 53c16c2..3fd4570 100644
--- a/binaries/data/mods/public/gui/common/l10n.js
+++ b/binaries/data/mods/public/gui/common/l10n.js
@@ -1,75 +1,7 @@
-const localisedResourceNames = {
-	"firstWord": {
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"food": translateWithContext("firstWord", "Food"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"meat": translateWithContext("firstWord", "Meat"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"metal": translateWithContext("firstWord", "Metal"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"ore": translateWithContext("firstWord", "Ore"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"rock": translateWithContext("firstWord", "Rock"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"ruins": translateWithContext("firstWord", "Ruins"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"stone": translateWithContext("firstWord", "Stone"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"treasure": translateWithContext("firstWord", "Treasure"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"tree": translateWithContext("firstWord", "Tree"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"wood": translateWithContext("firstWord", "Wood"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"fruit": translateWithContext("firstWord", "Fruit"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"grain": translateWithContext("firstWord", "Grain"),
-		// Translation: Word as used at the beginning of a sentence or as a single-word sentence.
-		"fish": translateWithContext("firstWord", "Fish"),
-	},
-	"withinSentence": {
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"food": translateWithContext("withinSentence", "Food"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"meat": translateWithContext("withinSentence", "Meat"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"metal": translateWithContext("withinSentence", "Metal"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"ore": translateWithContext("withinSentence", "Ore"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"rock": translateWithContext("withinSentence", "Rock"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"ruins": translateWithContext("withinSentence", "Ruins"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"stone": translateWithContext("withinSentence", "Stone"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"treasure": translateWithContext("withinSentence", "Treasure"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"tree": translateWithContext("withinSentence", "Tree"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"wood": translateWithContext("withinSentence", "Wood"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"fruit": translateWithContext("withinSentence", "Fruit"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"grain": translateWithContext("withinSentence", "Grain"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"fish": translateWithContext("withinSentence", "Fish"),
-	}
-};
 
-function getLocalizedResourceName(resourceCode, context)
+function getLocalizedResourceName(resourceName, context)
 {
-	if (!localisedResourceNames[context])
-	{
-		warn("Internationalization: Unexpected context for resource type localization found: ‘" + context + "’. This context is not supported.");
-		return resourceCode;
-	}
-	if (!localisedResourceNames[context][resourceCode])
-	{
-		warn("Internationalization: Unexpected resource type found with code ‘" + resourceCode + ". This resource type must be internationalized.");
-		return resourceCode;
-	}
-	return localisedResourceNames[context][resourceCode];
+	return translateWithContext(context, resourceName);
 }
 
 /**
@@ -81,7 +13,7 @@ function getLocalizedResourceAmounts(resources)
 		.filter(type => resources[type] > 0)
 		.map(type => sprintf(translate("%(amount)s %(resourceType)s"), {
 			"amount": resources[type],
-			"resourceType": getLocalizedResourceName(type, "withinSentence")
+			"resourceType": translateWithContext("withinSentence", type)
 		}));
 
 	if (amounts.length > 1)
diff --git a/binaries/data/mods/public/gui/common/tooltips.js b/binaries/data/mods/public/gui/common/tooltips.js
index ada0ca7..0fe4e0b 100644
--- a/binaries/data/mods/public/gui/common/tooltips.js
+++ b/binaries/data/mods/public/gui/common/tooltips.js
@@ -342,6 +342,7 @@ function getEntityCostComponentsTooltipString(template, trainNum, entity)
 
 	return costs;
 }
+
 function getGatherTooltip(template)
 {
 	if (!template.gather)
diff --git a/binaries/data/mods/public/gui/session/diplomacy_window.xml b/binaries/data/mods/public/gui/session/diplomacy_window.xml
index 1708159..9a531f6 100644
--- a/binaries/data/mods/public/gui/session/diplomacy_window.xml
+++ b/binaries/data/mods/public/gui/session/diplomacy_window.xml
@@ -11,7 +11,7 @@
 	</object>
 
 	<object name="diplomacyHeader" size="32 32 100%-32 64">
-		<object name="diplomacyHeaderName" size="0 0 150 100%" type="text" style="chatPanel" ghost="true">
+		<object name="diplomacyHeaderName" size="0 0 140 100%" type="text" style="chatPanel" ghost="true" text_align="center">
 			<translatableAttribute id="caption">Name</translatableAttribute>
 		</object>
 		<object name="diplomacyHeaderCiv" size="150 0 250 100%" type="text" style="chatPanel" ghost="true">
@@ -23,19 +23,19 @@
 		<object name="diplomacyHeaderTheirs" size="300 0 360 100%" type="text" style="chatPanel" ghost="true">
 			<translatableAttribute id="caption">Theirs</translatableAttribute>
 		</object>
-		<object name="diplomacyHeaderAlly" size="100%-180 0 100%-160 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
+		<object name="diplomacyHeaderAlly" size="360 0 380 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
 			<translatableAttribute id="caption">A</translatableAttribute>
 			<translatableAttribute id="tooltip">Ally</translatableAttribute>
 		</object>
-		<object name="diplomacyHeaderNeutral" size="100%-160 0 100%-140 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
+		<object name="diplomacyHeaderNeutral" size="380 0 400 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
 			<translatableAttribute id="caption">N</translatableAttribute>
 			<translatableAttribute id="tooltip">Neutral</translatableAttribute>
 		</object>
-		<object name="diplomacyHeaderEnemy" size="100%-140 0 100%-120 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
+		<object name="diplomacyHeaderEnemy" size="400 0 420 100%" type="text" style="chatPanel" tooltip_style="sessionToolTipBold">
 			<translatableAttribute id="caption">E</translatableAttribute>
 			<translatableAttribute id="tooltip">Enemy</translatableAttribute>
 		</object>
-		<object name="diplomacyHeaderTribute" size="100%-110 0 100% 100%" type="text" style="chatPanel">
+		<object name="diplomacyHeaderTribute" size="430 0 100%-30 100%" type="text" style="chatPanel" text_align="center">
 			<translatableAttribute id="caption">Tribute</translatableAttribute>
 		</object>
 	</object>
@@ -48,22 +48,17 @@
 				<object name="diplomacyPlayerTheirs[n]" size="300 0 360 100%" type="text" style="chatPanel" ghost="true"/>
 
 				<!-- Diplomatic stance - selection -->
-				<object name="diplomacyPlayerAlly[n]" size="100%-180 0 100%-160 100%" type="button" style="StoneButton" hidden="true"/>
-				<object name="diplomacyPlayerNeutral[n]" size="100%-160 0 100%-140 100%" type="button" style="StoneButton" hidden="true"/>
-				<object name="diplomacyPlayerEnemy[n]" size="100%-140 0 100%-120 100%" type="button" style="StoneButton" hidden="true"/>
+				<object name="diplomacyPlayerAlly[n]" size="360 0 380 100%" type="button" style="StoneButton" hidden="true"/>
+				<object name="diplomacyPlayerNeutral[n]" size="380 0 400 100%" type="button" style="StoneButton" hidden="true"/>
+				<object name="diplomacyPlayerEnemy[n]" size="400 0 420 100%" type="button" style="StoneButton" hidden="true"/>
 
 				<!-- Tribute -->
-				<object name="diplomacyPlayerTributeFood[n]" size="100%-110 0 100%-90 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
-					<object name="diplomacyPlayerTributeFoodImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/resources/food.png" ghost="true"/>
-				</object>
-				<object name="diplomacyPlayerTributeWood[n]" size="100%-90 0 100%-70 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
-					<object name="diplomacyPlayerTributeWoodImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/resources/wood.png" ghost="true"/>
-				</object>
-				<object name="diplomacyPlayerTributeStone[n]" size="100%-70 0 100%-50 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
-					<object name="diplomacyPlayerTributeStoneImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/resources/stone.png" ghost="true"/>
-				</object>
-				<object name="diplomacyPlayerTributeMetal[n]" size="100%-50 0 100%-30 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
-					<object name="diplomacyPlayerTributeMetalImage[n]" type="image" size="0 0 100% 100%" sprite="stretched:session/icons/resources/metal.png" ghost="true"/>
+				<object size="430 0 100%-40 100%">
+					<repeat count="8" var="r">
+						<object name="diplomacyPlayer[n]_tribute[r]" size="0 0 20 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
+							<object name="diplomacyPlayer[n]_tribute[r]_image" type="image" size="0 0 100% 100%" ghost="true"/>
+						</object>
+					</repeat>
 				</object>
 
 				<object name="diplomacyAttackRequest[n]" size="100%-20 0 100% 100%" type="button" style="iconButton" tooltip_style="sessionToolTipBold" hidden="true">
diff --git a/binaries/data/mods/public/gui/session/menu.js b/binaries/data/mods/public/gui/session/menu.js
index 494bd14..99bfd5e 100644
--- a/binaries/data/mods/public/gui/session/menu.js
+++ b/binaries/data/mods/public/gui/session/menu.js
@@ -22,9 +22,6 @@ const INITIAL_MENU_POSITION = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
 // Number of pixels per millisecond to move
 const MENU_SPEED = 1.2;
 
-// Available resources in trade and tribute menu
-const RESOURCES = ["food", "wood", "stone", "metal"];
-
 // Trade menu: step for probability changes
 const STEP = 5;
 
@@ -237,6 +234,7 @@ function openDiplomacy()
 	g_IsDiplomacyOpen = true;
 
 	let isCeasefireActive = GetSimState().ceasefireActive;
+	let resCodes = g_ResourceData.GetCodes();
 
 	// Get offset for one line
 	let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
@@ -255,7 +253,13 @@ function openDiplomacy()
 		diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]);
 	}
 
-	Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
+	let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel");
+	let size = dialog.size;
+	let width = 260 + resCodes.length * 10;
+	size.left = -width;
+	size.right = width;
+	dialog.size = size;
+	dialog.hidden = false;
 }
 
 function diplomacySetupTexts(i, rowsize)
@@ -305,16 +309,22 @@ function diplomacyFormatStanceButtons(i, hidden)
 
 function diplomacyFormatTributeButtons(i, hidden)
 {
-	for (let resource of RESOURCES)
+	let resNames = g_ResourceData.GetNames();
+	let resCodes = g_ResourceData.GetCodes();
+	let r = 0;
+	for (let resCode of resCodes)
 	{
-		let button = Engine.GetGUIObjectByName("diplomacyPlayerTribute"+resource[0].toUpperCase()+resource.substring(1)+"["+(i-1)+"]");
+		let button = Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]_tribute["+r+"]");
+		Engine.GetGUIObjectByName("diplomacyPlayer["+(i-1)+"]_tribute["+r+"]_image").sprite = "stretched:session/icons/resources/"+resCode+".png";
 		button.hidden = hidden;
+		setPanelObjectPosition(button, r, 8, 0);
+		++r;
 		if (hidden)
 			continue;
 
 		button.enabled = controlsPlayer(g_ViewedPlayer);
-		button.tooltip = formatTributeTooltip(i, resource, 100);
-		button.onpress = (function(i, resource, button) {
+		button.tooltip = formatTributeTooltip(i, resNames[resCode], 100);
+		button.onpress = (function(i, resCode, button) {
 			// Shift+click to send 500, shift+click+click to send 1000, etc.
 			// See INPUT_MASSTRIBUTING in input.js
 			let multiplier = 1;
@@ -327,24 +337,24 @@ function diplomacyFormatTributeButtons(i, hidden)
 				}
 
 				let amounts = {};
-				for (let type of RESOURCES)
-					amounts[type] = 0;
-				amounts[resource] = 100 * multiplier;
+				for (let res of resCodes)
+					amounts[res] = 0;
+				amounts[resCode] = 100 * multiplier,
 
-				button.tooltip = formatTributeTooltip(i, resource, amounts[resource]);
+				button.tooltip = formatTributeTooltip(i, resNames[resCode], amounts[resCode]);
 
 				// This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some
 				// evil global variable hackery.
 				g_FlushTributing = function() {
 					Engine.PostNetworkCommand({ "type": "tribute", "player": i, "amounts":  amounts });
 					multiplier = 1;
-					button.tooltip = formatTributeTooltip(i, resource, 100);
+					button.tooltip = formatTributeTooltip(i, resNames[resCode], 100);
 				};
 
 				if (!isBatchTrainPressed)
 					g_FlushTributing();
 			};
-		})(i, resource, button);
+		})(i, resCode, button);
 	}
 }
 
@@ -398,37 +408,33 @@ function openTrade()
 		}
 	};
 
-	var proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer);
-	var button = {};
-	var selec = RESOURCES[0];
-	for (var i = 0; i < RESOURCES.length; ++i)
+	let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer);
+	let button = {};
+	let resCodes = g_ResourceData.GetCodes();
+	let selec = resCodes[0];
+	hideRemaining("tradeResource[", resCodes.length, "]");
+
+	for (let i = 0; i < resCodes.length; ++i)
 	{
-		var buttonResource = Engine.GetGUIObjectByName("tradeResource["+i+"]");
-		if (i > 0)
-		{
-			var size = Engine.GetGUIObjectByName("tradeResource["+(i-1)+"]").size;
-			var width = size.right - size.left;
-			size.left += width;
-			size.right += width;
-			Engine.GetGUIObjectByName("tradeResource["+i+"]").size = size;
-		}
-		var resource = RESOURCES[i];
-		proba[resource] = (proba[resource] ? proba[resource] : 0);
-		var buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]");
-		var icon = Engine.GetGUIObjectByName("tradeResourceIcon["+i+"]");
-		icon.sprite = "stretched:session/icons/resources/" + resource + ".png";
-		var label = Engine.GetGUIObjectByName("tradeResourceText["+i+"]");
-		var buttonUp = Engine.GetGUIObjectByName("tradeArrowUp["+i+"]");
-		var buttonDn = Engine.GetGUIObjectByName("tradeArrowDn["+i+"]");
-		var iconSel = Engine.GetGUIObjectByName("tradeResourceSelection["+i+"]");
-		button[resource] = { "up": buttonUp, "dn": buttonDn, "label": label, "sel": iconSel };
+		let buttonResource = Engine.GetGUIObjectByName("tradeResource["+i+"]");
+		setPanelObjectPosition(buttonResource, i, 8);
+		let resCode = resCodes[i];
+		proba[resCode] = (proba[resCode] ? proba[resCode] : 0);
+		buttonResource = Engine.GetGUIObjectByName("tradeResourceButton["+i+"]");
+		let icon = Engine.GetGUIObjectByName("tradeResourceIcon["+i+"]");
+		icon.sprite = "stretched:session/icons/resources/" + resCode + ".png";
+		let label = Engine.GetGUIObjectByName("tradeResourceText["+i+"]");
+		let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp["+i+"]");
+		let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn["+i+"]");
+		let iconSel = Engine.GetGUIObjectByName("tradeResourceSelection["+i+"]");
+		button[resCode] = { "up": buttonUp, "dn": buttonDn, "label": label, "sel": iconSel };
 
 		buttonResource.enabled = controlsPlayer(g_ViewedPlayer);
 		buttonResource.onpress = (function(resource){
 			return function() {
 				if (Engine.HotkeyIsPressed("session.fulltradeswap"))
 				{
-					for (var ress of RESOURCES)
+					for (let ress of resCodes)
 						proba[ress] = 0;
 					proba[resource] = 100;
 					Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
@@ -436,7 +442,7 @@ function openTrade()
 				selec = resource;
 				updateButtons();
 			};
-		})(resource);
+		})(resCode);
 
 		buttonUp.enabled = controlsPlayer(g_ViewedPlayer);
 		buttonUp.onpress = (function(resource){
@@ -446,7 +452,7 @@ function openTrade()
 				Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
 				updateButtons();
 			};
-		})(resource);
+		})(resCode);
 
 		buttonDn.enabled = controlsPlayer(g_ViewedPlayer);
 		buttonDn.onpress = (function(resource){
@@ -456,7 +462,7 @@ function openTrade()
 				Engine.PostNetworkCommand({"type": "set-trading-goods", "tradingGoods": proba});
 				updateButtons();
 			};
-		})(resource);
+		})(resCode);
 	}
 	updateButtons();
 
@@ -464,7 +470,13 @@ function openTrade()
 	Engine.GetGUIObjectByName("landTraders").caption = getIdleLandTradersText(traderNumber);
 	Engine.GetGUIObjectByName("shipTraders").caption = getIdleShipTradersText(traderNumber);
 
-	Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false;
+	let dialog = Engine.GetGUIObjectByName("tradeDialogPanel");
+	let size = dialog.size;
+	let wid = resCodes.length * (58/2);
+	size.left = -(134 + wid);
+	size.right = (134 + wid);
+	dialog.size = size;
+	dialog.hidden = false;
 }
 
 function getIdleLandTradersText(traderNumber)
diff --git a/binaries/data/mods/public/gui/session/selection_details.js b/binaries/data/mods/public/gui/session/selection_details.js
index 29acc09..36fa2a6 100644
--- a/binaries/data/mods/public/gui/session/selection_details.js
+++ b/binaries/data/mods/public/gui/session/selection_details.js
@@ -13,10 +13,8 @@ function layoutSelectionMultiple()
 function getResourceTypeDisplayName(resourceType)
 {
 	let resourceCode = resourceType.generic;
-	if (resourceCode == "treasure")
-		return getLocalizedResourceName(resourceType.specific, "firstWord");
-	else
-		return getLocalizedResourceName(resourceCode, "firstWord");
+	let resourceName = g_ResourceData.GetNames()[(resourceCode == "treasure" ? resourceType.specific : resourceCode)]
+	return getLocalizedResourceName(resourceName, "firstWord");
 }
 
 // Updates the health bar of garrisoned units
diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js
index c21fc4f..ceae7a9 100644
--- a/binaries/data/mods/public/gui/session/selection_panels.js
+++ b/binaries/data/mods/public/gui/session/selection_panels.js
@@ -34,6 +34,8 @@ let g_FormationsInfo = new Map();
 
 let g_SelectionPanels = {};
 
+let g_BarterSell;
+
 g_SelectionPanels.Alert = {
 	"getMaxNumberOfItems": function()
 	{
@@ -87,15 +89,14 @@ g_SelectionPanels.Alert = {
 g_SelectionPanels.Barter = {
 	"getMaxNumberOfItems": function()
 	{
-		return 4;
+		return 8;
 	},
 	"rowLength": 4,
 	"getItems": function(unitEntState, selection)
 	{
 		if (!unitEntState.barterMarket)
 			return [];
-		// ["food", "wood", "stone", "metal"]
-		return BARTER_RESOURCES;
+		return g_ResourceData.GetCodes();
 	},
 	"setupButton": function(data)
 	{
@@ -115,11 +116,14 @@ g_SelectionPanels.Barter = {
 		if (Engine.HotkeyIsPressed("session.massbarter"))
 			amountToSell *= BARTER_BUNCH_MULTIPLIER;
 
+		if (!g_BarterSell)
+			g_BarterSell = g_ResourceData.GetCodes()[0];
+
 		amount.Sell.caption = "-" + amountToSell;
 		let prices = data.unitEntState.barterMarket.prices;
 		amount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[data.item] * amountToSell);
 
-		let resource = getLocalizedResourceName(data.item, "withinSentence");
+		let resource = getLocalizedResourceName(g_ResourceData.GetNames()[data.item], "firstWord");
 		button.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
 		button.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
 
@@ -164,8 +168,10 @@ g_SelectionPanels.Barter = {
 		button.Sell.hidden = false;
 		selectionIcon.hidden = !isSelected;
 
-		setPanelObjectPosition(button.Sell, data.i, data.rowLength);
-		setPanelObjectPosition(button.Buy, data.i + data.rowLength, data.rowLength);
+		let sellPos = data.i + (data.i >= data.rowLength ? data.rowLength : 0);
+		let buyPos = data.i + data.rowLength * (data.i >= data.rowLength ? 2 : 1);
+		setPanelObjectPosition(button.Sell, sellPos, data.rowLength);
+		setPanelObjectPosition(button.Buy, buyPos, data.rowLength);
 		return true;
 	}
 };
diff --git a/binaries/data/mods/public/gui/session/selection_panels_helpers.js b/binaries/data/mods/public/gui/session/selection_panels_helpers.js
index fc0e7a6..46976f9 100644
--- a/binaries/data/mods/public/gui/session/selection_panels_helpers.js
+++ b/binaries/data/mods/public/gui/session/selection_panels_helpers.js
@@ -1,18 +1,11 @@
 const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
 const BARTER_BUNCH_MULTIPLIER = 5;
-const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
 const BARTER_ACTIONS = ["Sell", "Buy"];
 const GATE_ACTIONS = ["lock", "unlock"];
 
-// upgrade constants
 const UPGRADING_NOT_STARTED = -2;
 const UPGRADING_CHOSEN_OTHER = -1;
 
-// ==============================================
-// BARTER HELPERS
-// Resources to sell on barter panel
-var g_BarterSell = "food";
-
 function canMoveSelectionIntoFormation(formationTemplate)
 {
 	if (!(formationTemplate in g_canMoveIntoFormation))
diff --git a/binaries/data/mods/public/gui/session/selection_panels_left/barter_panel.xml b/binaries/data/mods/public/gui/session/selection_panels_left/barter_panel.xml
index f32e117..717efce 100644
--- a/binaries/data/mods/public/gui/session/selection_panels_left/barter_panel.xml
+++ b/binaries/data/mods/public/gui/session/selection_panels_left/barter_panel.xml
@@ -1,24 +1,24 @@
 <?xml version="1.0" encoding="utf-8"?>
 <object name="unitBarterPanel" 
-	size="6 36 100% 100%"
+	size="24 12 100% 100%"
 	hidden="true"
 >
-	<object ghost="true" style="resourceText" type="text" size="0 0 100% 20">
-		<translatableAttribute id="tooltip">Exchange resources:</translatableAttribute>
-	</object>
-	<object size="0 32 100% 124">
-		<repeat count="4">
-			<!-- sell -->
-			<object name="unitBarterSellButton[n]" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottomBold">
-				<object name="unitBarterSellIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
-				<object name="unitBarterSellAmount[n]" ghost="true" style="resourceText" type="text" size="0 0 100% 50%"/>
-				<object name="unitBarterSellSelection[n]" hidden="true" type="image" ghost="true" size="3 3 43 43" sprite="stretched:session/icons/corners.png"/>
-			</object>
-			<!-- buy -->
-			<object name="unitBarterBuyButton[n]" style="iconButton" type="button" size="0 0 46 46" tooltip_style="sessionToolTipBottomBold">
-				<object name="unitBarterBuyIcon[n]" type="image" ghost="true" size="3 3 43 43"/>
-				<object name="unitBarterBuyAmount[n]" ghost="true" style="resourceText" type="text" size="0 0 100% 50%"/>
-			</object>
-		</repeat>
-	</object>
+
+	<repeat count="8">
+
+		<!-- Sell -->
+		<object name="unitBarterSellButton[n]" style="iconButton" type="button" size="0 0 36 36" tooltip_style="sessionToolTipBottomBold" hidden="true">
+			<object name="unitBarterSellIcon[n]" type="image" ghost="true" size="3 3 33 33"/>
+			<object name="unitBarterSellAmount[n]" ghost="true" style="resourceText" type="text" size="0 0 100% 50%"/>
+			<object name="unitBarterSellSelection[n]" hidden="true" type="image" ghost="true" size="3 3 33 33" sprite="stretched:session/icons/corners.png"/>
+		</object>
+
+		<!-- Buy -->
+		<object name="unitBarterBuyButton[n]" style="iconButton" type="button" size="0 0 36 36" tooltip_style="sessionToolTipBottomBold" hidden="true">
+			<object name="unitBarterBuyIcon[n]" type="image" ghost="true" size="3 3 33 33"/>
+			<object name="unitBarterBuyAmount[n]" ghost="true" style="resourceText" type="text" size="0 0 100% 50%"/>
+		</object>
+
+	</repeat>
+
 </object>
diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js
index 35af7a7..af267cb 100644
--- a/binaries/data/mods/public/gui/session/session.js
+++ b/binaries/data/mods/public/gui/session/session.js
@@ -122,6 +122,7 @@ var g_EntityStates = {};
 var g_TemplateData = {};
 var g_TemplateDataWithoutLocalization = {};
 var g_TechnologyData = {};
+var g_ResourceData = new Resources();
 
 /**
  * Top coordinate of the research list.
@@ -463,10 +464,18 @@ function updateTopPanel()
 	let viewPlayer = Engine.GetGUIObjectByName("viewPlayer");
 	viewPlayer.hidden = !g_IsObserver && !g_DevSettings.changePerspective;
 
-	Engine.GetGUIObjectByName("food").hidden = !isPlayer;
-	Engine.GetGUIObjectByName("wood").hidden = !isPlayer;
-	Engine.GetGUIObjectByName("stone").hidden = !isPlayer;
-	Engine.GetGUIObjectByName("metal").hidden = !isPlayer;
+	let resCodes = g_ResourceData.GetCodes();
+	let resNames = g_ResourceData.GetNames();
+	let r = 0;
+	for (let res of resCodes)
+	{
+		Engine.GetGUIObjectByName("resource["+r+"]_icon").sprite = "stretched:session/icons/resources/" + res + ".png";
+		Engine.GetGUIObjectByName("resource["+r+"]").hidden = !isPlayer;
+		++r;
+	}
+	horizontallyDistributeObjects("resourceCounts", 0, r);
+	hideRemaining("resource[", r, "]");
+
 	Engine.GetGUIObjectByName("population").hidden = !isPlayer;
 	Engine.GetGUIObjectByName("diplomacyButton1").hidden = !isPlayer;
 	Engine.GetGUIObjectByName("tradeButton1").hidden = !isPlayer;
@@ -547,7 +556,8 @@ function leaveGame(willRejoin)
 			"disconnected": g_Disconnected,
 			"isReplay": g_IsReplay,
 			"replayDirectory": !g_HasRejoined && replayDirectory,
-			"replaySelectionData": g_ReplaySelectionData
+			"replaySelectionData": g_ReplaySelectionData,
+			"resources": GetSimState().resources
 		}
 	});
 }
@@ -947,14 +957,18 @@ function getAllyStatTooltip(resource)
 
 function updatePlayerDisplay()
 {
-	let playerState = GetSimState().players[g_ViewedPlayer];
+	let simState = GetSimState();
+	let playerState = simState.players[g_ViewedPlayer];
 	if (!playerState)
 		return;
 
-	for (let res of RESOURCES)
+	let resCodes = g_ResourceData.GetCodes();
+	let resNames = g_ResourceData.GetNames();
+	for (let r = 0; r < resCodes.length; ++r)
 	{
-		Engine.GetGUIObjectByName("resource_" + res).caption = Math.floor(playerState.resourceCounts[res]);
-		Engine.GetGUIObjectByName(res).tooltip = getLocalizedResourceName(res, "firstWord") + getAllyStatTooltip(res);
+		let res = resCodes[r];
+		Engine.GetGUIObjectByName("resource["+r+"]").tooltip = getLocalizedResourceName(resNames[res], "firstWord") + getAllyStatTooltip(res);
+		Engine.GetGUIObjectByName("resource["+r+"]_count").caption = Math.floor(playerState.resourceCounts[res]);
 	}
 
 	Engine.GetGUIObjectByName("resourcePop").caption = sprintf(translate("%(popCount)s/%(popLimit)s"), playerState);
diff --git a/binaries/data/mods/public/gui/session/top_panel/resource_food.xml b/binaries/data/mods/public/gui/session/top_panel/resource_food.xml
deleted file mode 100644
index 4d84ca7..0000000
--- a/binaries/data/mods/public/gui/session/top_panel/resource_food.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<object name="food" size="10 0 100 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
-		<object size="0 -4 40 36" type="image" sprite="stretched:session/icons/resources/food.png" ghost="true"/>
-		<object size="32 0 100% 100%-2" type="text" style="resourceText" name="resource_food"/>
-</object>
diff --git a/binaries/data/mods/public/gui/session/top_panel/resource_metal.xml b/binaries/data/mods/public/gui/session/top_panel/resource_metal.xml
deleted file mode 100644
index 4edba79..0000000
--- a/binaries/data/mods/public/gui/session/top_panel/resource_metal.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<object name="metal" size="280 0 370 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
-		<object size="0 -4 40 36" type="image" sprite="stretched:session/icons/resources/metal.png" ghost="true"/>
-		<object size="32 0 100% 100%-2" type="text" style="resourceText" name="resource_metal"/>
-</object>
diff --git a/binaries/data/mods/public/gui/session/top_panel/resource_population.xml b/binaries/data/mods/public/gui/session/top_panel/resource_population.xml
index 9c9dcc2..9d66e40 100644
--- a/binaries/data/mods/public/gui/session/top_panel/resource_population.xml
+++ b/binaries/data/mods/public/gui/session/top_panel/resource_population.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<object name="population" size="370 0 460 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
+<object name="population" size="50%-90-52 0 50%-52 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
 	<object size="0 -4 40 34" type="image" sprite="stretched:session/icons/resources/population.png" ghost="true"/>
 	<object size="32 0 100% 100%-2" type="text" style="resourceText" name="resourcePop"/>
 </object>
diff --git a/binaries/data/mods/public/gui/session/top_panel/resource_stone.xml b/binaries/data/mods/public/gui/session/top_panel/resource_stone.xml
deleted file mode 100644
index 6133acc..0000000
--- a/binaries/data/mods/public/gui/session/top_panel/resource_stone.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<object name="stone" size="190 0 280 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
-		<object size="0 -4 40 36" type="image" sprite="stretched:session/icons/resources/stone.png" ghost="true"/>
-		<object size="32 0 100% 100%-2" type="text" style="resourceText" name="resource_stone"/>
-</object>
diff --git a/binaries/data/mods/public/gui/session/top_panel/resource_wood.xml b/binaries/data/mods/public/gui/session/top_panel/resource_wood.xml
deleted file mode 100644
index f020979..0000000
--- a/binaries/data/mods/public/gui/session/top_panel/resource_wood.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<object name="wood" size="100 0 190 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
-		<object size="0 -4 40 36" type="image" sprite="stretched:session/icons/resources/wood.png" ghost="true"/>
-		<object size="32 0 100% 100%-2" type="text" style="resourceText" name="resource_wood"/>
-</object>
diff --git a/binaries/data/mods/public/gui/session/top_panel/resources.xml b/binaries/data/mods/public/gui/session/top_panel/resources.xml
new file mode 100644
index 0000000..520aa35
--- /dev/null
+++ b/binaries/data/mods/public/gui/session/top_panel/resources.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<object size="10 0 50%-90-52 100%" name="resourceCounts">
+  <repeat count="8">
+    <object name="resource[n]" size="0 0 90 100%" type="image" style="resourceCounter" tooltip_style="sessionToolTipBold">
+      <object size="0 -4 40 36" type="image" name="resource[n]_icon" ghost="true"/>
+      <object size="32 0 100% 100%-2" type="text" style="resourceText" name="resource[n]_count"/>
+    </object>
+  </repeat>
+</object>
diff --git a/binaries/data/mods/public/gui/session/trade_window.xml b/binaries/data/mods/public/gui/session/trade_window.xml
index 80b226c..0862bc1 100644
--- a/binaries/data/mods/public/gui/session/trade_window.xml
+++ b/binaries/data/mods/public/gui/session/trade_window.xml
@@ -16,7 +16,7 @@
 		</object>
 
 		<object size="180 0 100% 100%">
-			<repeat count="4">
+			<repeat count="8">
 				<object name="tradeResource[n]" size="0 0 58 32">
 					<object name="tradeResourceButton[n]" size="4 0 36 100%" type="button" style="StoneButton">
 						<object name="tradeResourceIcon[n]" type="image" ghost="true"/>
diff --git a/binaries/data/mods/public/gui/structree/draw.js b/binaries/data/mods/public/gui/structree/draw.js
index 92c60ce..ee15fb3 100644
--- a/binaries/data/mods/public/gui/structree/draw.js
+++ b/binaries/data/mods/public/gui/structree/draw.js
@@ -235,18 +235,6 @@ function getPositionOffset(idx)
 	return size;
 }
 
-function hideRemaining(prefix, idx, suffix)
-{
-	let obj = Engine.GetGUIObjectByName(prefix + idx + suffix);
-	while (obj)
-	{
-		obj.hidden = true;
-		++idx;
-		obj = Engine.GetGUIObjectByName(prefix + idx + suffix);
-	}
-}
-
-
 /**
  * Positions certain elements that only need to be positioned once
  * (as <repeat> does not reposition automatically).
diff --git a/binaries/data/mods/public/gui/structree/load.js b/binaries/data/mods/public/gui/structree/load.js
index 534747b..006de1b 100644
--- a/binaries/data/mods/public/gui/structree/load.js
+++ b/binaries/data/mods/public/gui/structree/load.js
@@ -5,28 +5,24 @@
  */
 function getGatherRates(templateName)
 {
-	// TODO: It would be nice to use the gather rates present in the templates
-	// instead of hard-coding the possible rates here.
-
-	// We ignore ruins here, as those are not that common and would skew the results
-	var types = {
-		"food": ["food", "food.fish", "food.fruit", "food.grain", "food.meat", "food.milk"],
-		"wood": ["wood", "wood.tree"],
-		"stone": ["stone", "stone.rock"],
-		"metal": ["metal", "metal.ore"]
-	};
-	var rates = {};
+	let rates = {};
 
-	for (let type in types)
+	for (let resource of g_ResourceData.GetData())
 	{
+		let types = [resource.code];
+		for (let subtype of resource.subtypes)
+			// We ignore ruins as those are not that common and skew the results
+			if (subtype !== "ruins")
+				types.push(resource.code + "." + subtype);
+
 		let count, rate;
-		[rate, count] = types[type].reduce(function(sum, t) {
+		[rate, count] = types.reduce(function(sum, t) {
 				let r = +fetchValue(templateName, "ResourceGatherer/Rates/"+t);
 				return [sum[0] + (r > 0 ? r : 0), sum[1] + (r > 0 ? 1 : 0)];
 			}, [0, 0]);
 
 		if (rate > 0)
-			rates[type] = Math.round(rate / count * 100) / 100;
+			rates[resource.code] = Math.round(rate / count * 100) / 100;
 	}
 
 	if (!Object.keys(rates).length)
diff --git a/binaries/data/mods/public/gui/structree/structree.js b/binaries/data/mods/public/gui/structree/structree.js
index 65bfd9e..fcb6a9c 100644
--- a/binaries/data/mods/public/gui/structree/structree.js
+++ b/binaries/data/mods/public/gui/structree/structree.js
@@ -9,6 +9,7 @@ var g_Lists = {};
 var g_CivData = {};
 var g_SelectedCiv = "";
 var g_CallbackSet = false;
+var g_ResourceData = new Resources();
 
 /**
  * Initialize the dropdown containing all the available civs
diff --git a/binaries/data/mods/public/gui/summary/counters.js b/binaries/data/mods/public/gui/summary/counters.js
index 562e3bb..1823b67 100644
--- a/binaries/data/mods/public/gui/summary/counters.js
+++ b/binaries/data/mods/public/gui/summary/counters.js
@@ -250,7 +250,7 @@ function calculateUnits(playerState, position)
 
 function calculateResources(playerState, position)
 {
-	let type = g_ResourcesTypes[position];
+	let type = g_ResourceData.GetCodes()[position];
 
 	return formatIncome(
 		playerState.statistics.resourcesGathered[type],
@@ -262,7 +262,7 @@ function calculateTotalResources(playerState)
 	let totalGathered = 0;
 	let totalUsed = 0;
 
-	for (let type of g_ResourcesTypes)
+	for (let type of g_ResourceData.GetCodes())
 	{
 		totalGathered += playerState.statistics.resourcesGathered[type];
 		totalUsed += playerState.statistics.resourcesUsed[type] - playerState.statistics.resourcesSold[type];
@@ -330,7 +330,7 @@ function calculateResourcesTeam(counters)
 
 function calculateResourceExchanged(playerState, position)
 {
-	let type = g_ResourcesTypes[position];
+	let type = g_ResourceData.GetCodes()[position];
 
 	return formatIncome(
 		playerState.statistics.resourcesBought[type],
diff --git a/binaries/data/mods/public/gui/summary/layout.js b/binaries/data/mods/public/gui/summary/layout.js
index 7feeb45..bd9962c 100644
--- a/binaries/data/mods/public/gui/summary/layout.js
+++ b/binaries/data/mods/public/gui/summary/layout.js
@@ -92,10 +92,6 @@ var g_ScorePanelsData = {
 	"resources": {
 		"headings": [
 			{ "caption": translate("Player name"), "yStart": 26, "width": 200 },
-			{ "caption": translate("Food"), "yStart": 34, "width": 100 },
-			{ "caption": translate("Wood"), "yStart": 34, "width": 100 },
-			{ "caption": translate("Stone"), "yStart": 34, "width": 100 },
-			{ "caption": translate("Metal"), "yStart": 34, "width": 100 },
 			{ "caption": translate("Total"), "yStart": 34, "width": 110 },
 			{
 				"caption": sprintf(translate("Tributes \n(%(sent)s / %(received)s)"),
@@ -121,10 +117,6 @@ var g_ScorePanelsData = {
 			}, // width = 510
 		],
 		"counters": [
-			{ "width": 100, "fn": calculateResources, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResources, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResources, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResources, "verticalOffset": 12 },
 			{ "width": 110, "fn": calculateTotalResources, "verticalOffset": 12 },
 			{ "width": 121, "fn": calculateTributeSent, "verticalOffset": 12 },
 			{ "width": 100, "fn": calculateTreasureCollected, "verticalOffset": 12 },
@@ -135,19 +127,11 @@ var g_ScorePanelsData = {
 	"market": {
 		"headings": [
 			{ "caption": translate("Player name"), "yStart": 26, "width": 200 },
-			{ "caption": translate("Food exchanged"), "yStart": 16, "width": 100 },
-			{ "caption": translate("Wood exchanged"), "yStart": 16, "width": 100 },
-			{ "caption": translate("Stone exchanged"), "yStart": 16, "width": 100 },
-			{ "caption": translate("Metal exchanged"), "yStart": 16, "width": 100 },
 			{ "caption": translate("Barter efficiency"), "yStart": 16, "width": 100 },
 			{ "caption": translate("Trade income"), "yStart": 16, "width": 100 }
 		],
 		"titleHeadings": [],
 		"counters": [
-			{ "width": 100, "fn": calculateResourceExchanged, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResourceExchanged, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResourceExchanged, "verticalOffset": 12 },
-			{ "width": 100, "fn": calculateResourceExchanged, "verticalOffset": 12 },
 			{ "width": 100, "fn": calculateBarterEfficiency, "verticalOffset": 12 },
 			{ "width": 100, "fn": calculateTradeIncome, "verticalOffset": 12 }
 		],
diff --git a/binaries/data/mods/public/gui/summary/summary.js b/binaries/data/mods/public/gui/summary/summary.js
index 51e5577..f4da805 100644
--- a/binaries/data/mods/public/gui/summary/summary.js
+++ b/binaries/data/mods/public/gui/summary/summary.js
@@ -1,4 +1,4 @@
-const g_MaxHeadingTitle= 8;
+const g_MaxHeadingTitle= 12;
 
 // const for filtering long collective headings
 const g_LongHeadingWidth = 250;
@@ -17,7 +17,6 @@ const g_CapturedColor = '[color="255 255 157"]';
 
 const g_BuildingsTypes = [ "total", "House", "Economic", "Outpost", "Military", "Fortress", "CivCentre", "Wonder" ];
 const g_UnitsTypes = [ "total", "Infantry", "Worker", "Cavalry", "Champion", "Hero", "Ship", "Trader" ];
-const g_ResourcesTypes = [ "food", "wood", "stone", "metal" ];
 
 // Colors used for gathered and traded resources
 const g_IncomeColor = '[color="201 255 200"]';
@@ -34,6 +33,7 @@ var g_PlayerCount = 0;
 // Count players without team (or all if teams are not displayed)
 var g_WithoutTeam = 0;
 var g_GameData;
+var g_ResourceData = new Resources();
 
 function selectPanel(panel)
 {
@@ -242,6 +242,46 @@ function init(data)
 	else
 		g_Teams = false;
 
+	// Resource names and counters
+	let resHeads = [];
+	let tradeHeads = [];
+	let resPanel = g_ScorePanelsData.resources;
+	let tradePanel = g_ScorePanelsData.market;
+	let resNames = g_ResourceData.GetNames();
+	let resCodes = g_ResourceData.GetCodes();
+	for (let code of resCodes)
+	{
+		resHeads.push({
+			"caption": translateWithContext("firstWord", resNames[code]),
+			"yStart": 34,
+			"width": 100
+		});
+
+		resPanel.counters.unshift({
+			"width": 100,
+			"fn": calculateResources,
+			"verticalOffset": 12
+		});
+
+		tradeHeads.push({
+			"caption": sprintf(
+				translate("%(resource)s exchanged"), {
+					"resource": translateWithContext("withinSentence", resNames[code])
+				}),
+			"yStart": 16,
+			"width": 100
+		});
+
+		tradePanel.counters.unshift({
+			"width": 100,
+			"fn": calculateResourceExchanged,
+			"verticalOffset": 12
+		});
+	}
+	resPanel.headings.splice.apply(resPanel.headings, [1, 0].concat(resHeads));
+	resPanel.titleHeadings[0].width = (100 * resCodes.length) + 110;
+	tradePanel.headings.splice.apply(tradePanel.headings, [1, 0].concat(tradeHeads));
+
 	// Erase teams data if teams are not displayed
 	if (!g_Teams)
 	{
diff --git a/binaries/data/mods/public/gui/summary/summary.xml b/binaries/data/mods/public/gui/summary/summary.xml
index 2c47f16..015638f 100644
--- a/binaries/data/mods/public/gui/summary/summary.xml
+++ b/binaries/data/mods/public/gui/summary/summary.xml
@@ -103,11 +103,11 @@
 				<object name="playerNameHeading" type="text" style="ModernLeftTabLabelText">
 					<translatableAttribute id="caption">Player name</translatableAttribute>
 				</object>
-				<repeat var="x" count="8">
+				<repeat var="x" count="12">
 					<object name="titleHeading[x]" type="text" style="ModernTabLabelText">
 					</object>
 				</repeat>
-				<repeat var="x" count="8">
+				<repeat var="x" count="12">
 					<object name="Heading[x]" type="text" style="ModernTabLabelText">
 					</object>
 				</repeat>
@@ -124,7 +124,7 @@
 								</object>
 								<object name="playerNamet[i][n]" type="text" size="40 2 208 100%" style="ModernLeftLabelText"/>
 								<object name="civIcont[i][n]" type="image" size="208 5 240 37" />
-								<repeat var="x" count="8">
+								<repeat var="x" count="12">
 									<object name="valueDataTeam[i][n][x]" type="text" style="ModernTabLabelText">
 									</object>
 								</repeat>
@@ -132,7 +132,7 @@
 						</repeat>
 					</object>
 					<object name="teamHeadingt[i]" type="text" style="ModernLeftTabLabelText"/>
-					<repeat var="x" count="8">
+					<repeat var="x" count="12">
 						<object name="valueDataTeam[i][x]" type="text" style="ModernTabLabelText">
 						</object>
 					</repeat>
@@ -147,7 +147,7 @@
 						</object>
 						<object name="playerName[n]" type="text"  size="40 2 208 100%" style="ModernLeftLabelText"/>
 						<object name="civIcon[n]" type="image" size="208 5 240 37"/>
-						<repeat var="x" count="8">
+						<repeat var="x" count="12">
 							<object name="valueData[n][x]" type="text" style="ModernTabLabelText">
 							</object>
 						</repeat>
diff --git a/binaries/data/mods/public/l10n/messages.json b/binaries/data/mods/public/l10n/messages.json
index 188e3cc..34bd751 100644
--- a/binaries/data/mods/public/l10n/messages.json
+++ b/binaries/data/mods/public/l10n/messages.json
@@ -289,7 +289,7 @@
                     }
                 }
             },
-			{
+            {
                 "extractor": "json",
                 "filemasks": [
                     "gui/credits/texts/**.json"
@@ -562,6 +562,32 @@
                         "description"
                     ]
                 }
+            },
+            {
+                "extractor": "json",
+                "filemasks": [
+                    "simulation/data/resources/**.json"
+                ],
+                "options": {
+                    "keywords": [
+                        "name",
+                        "subtypes"
+                    ],
+                    "context": "firstWord"
+                }
+            },
+            {
+                "extractor": "json",
+                "filemasks": [
+                    "simulation/data/resources/**.json"
+                ],
+                "options": {
+                    "keywords": [
+                        "name",
+                        "subtypes"
+                    ],
+                    "context": "withinSentence"
+                }
             }
         ]
     },
diff --git a/binaries/data/mods/public/simulation/ai/common-api/resources.js b/binaries/data/mods/public/simulation/ai/common-api/resources.js
index 8130676..b6fe84b 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/resources.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/resources.js
@@ -9,7 +9,8 @@ m.Resources = function(amounts = {}, population = 0)
 	this.population = population > 0 ? population : 0;
 };
 
-m.Resources.prototype.types = []; // This array will be filled in SharedScript.init
+// This array will be filled in SharedScript.init
+m.Resources.prototype.types = [];
 
 m.Resources.prototype.reset = function()
 {
diff --git a/binaries/data/mods/public/simulation/ai/common-api/shared.js b/binaries/data/mods/public/simulation/ai/common-api/shared.js
index 68ab45d..0e7b6c5 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/shared.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/shared.js
@@ -180,13 +180,10 @@ m.SharedScript.prototype.init = function(state, deserialization)
 	this.accessibility.init(state, this.terrainAnalyzer);
 
 	// Setup resources
-	this.resourceTypes = { "food": 0, "wood": 1, "stone": 2, "metal": 2 };
-	this.resourceList = [];
-	for (let res in this.resourceTypes)
-		this.resourceList.push(res);
-	m.Resources.prototype.types = this.resourceList;
+	this.resourceInfo = state.resources;
+	m.Resources.prototype.types = state.resources.codes;
 	// Resource types: 0 = not used for resource maps
-	//                 1 = abondant resource with small amount each
+	//                 1 = abundant resource with small amount each
 	//                 2 = spare resource, but huge amount each
 	// The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites)
 	// They are updated by checking for create and destroy events for all resources
@@ -197,18 +194,6 @@ m.SharedScript.prototype.init = function(state, deserialization)
 	this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement.
 	this.createResourceMaps();
 
-	/** Keep in sync with gui/common/l10n.js */
-	this.resourceNames = {
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"food": markForTranslationWithContext("withinSentence", "Food"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"wood": markForTranslationWithContext("withinSentence", "Wood"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"metal": markForTranslationWithContext("withinSentence", "Metal"),
-		// Translation: Word as used in the middle of a sentence (which may require using lowercase for your language).
-		"stone": markForTranslationWithContext("withinSentence", "Stone"),
-	};
-
 	this.gameState = {};
 	for (let i in this._players)
 	{
diff --git a/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js
index 04633dc..7d79317 100644
--- a/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js
+++ b/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js
@@ -383,9 +383,9 @@ m.Accessibility.prototype.floodFill = function(startIndex, value, onWater)
 /** creates a map of resource density */
 m.SharedScript.prototype.createResourceMaps = function()
 {
-	for (let resource of this.resourceList)
+	for (let resource of this.resourceInfo.codes)
 	{
-		if (this.resourceTypes[resource] !== 1 && this.resourceTypes[resource] !== 2)
+		if (this.resourceInfo.aiInfluenceGroups[resource] === 0)
 			continue;
 		// if there is no resourceMap create one with an influence for everything with that resource
 		if (this.resourceMaps[resource])
@@ -405,11 +405,11 @@ m.SharedScript.prototype.createResourceMaps = function()
 		let cellSize = this.resourceMaps[resource].cellSize;
 		let x = Math.floor(ent.position()[0] / cellSize);
 		let z = Math.floor(ent.position()[1] / cellSize);
-		let type = this.resourceTypes[resource];
-		let strength = Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[type]);
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2, "constant");
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2);
-		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[type]/cellSize, strength, "constant");
+		let grp = this.resourceInfo.aiInfluenceGroups[resource];
+		let strength = Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[grp]);
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2, "constant");
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2);
+		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp]/cellSize, strength, "constant");
 	}
 };
 
@@ -420,9 +420,9 @@ m.SharedScript.prototype.createResourceMaps = function()
  */
 m.SharedScript.prototype.updateResourceMaps = function(events)
 {
-	for (let resource of this.resourceList)
+	for (let resource of this.resourceInfo.codes)
 	{
-		if (this.resourceTypes[resource] !== 1 && this.resourceTypes[resource] !== 2)
+		if (this.resourceInfo.aiInfluenceGroups[resource] === 0)
 			continue;
 		// if there is no resourceMap create one with an influence for everything with that resource
 		if (this.resourceMaps[resource])
@@ -447,11 +447,11 @@ m.SharedScript.prototype.updateResourceMaps = function(events)
 		let cellSize = this.resourceMaps[resource].cellSize;
 		let x = Math.floor(ent.position()[0] / cellSize);
 		let z = Math.floor(ent.position()[1] / cellSize);
-		let type = this.resourceTypes[resource];
-		let strength = -Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[type]);
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2, "constant");
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2);
-		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[type]/cellSize, strength, "constant");
+		let grp = this.resourceInfo.aiInfluenceGroups[resource];
+		let strength = -Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[grp]);
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2, "constant");
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2);
+		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp]/cellSize, strength, "constant");
 	}
 	for (let e of events.Create)
 	{
@@ -466,11 +466,11 @@ m.SharedScript.prototype.updateResourceMaps = function(events)
 		let cellSize = this.resourceMaps[resource].cellSize;
 		let x = Math.floor(ent.position()[0] / cellSize);
 		let z = Math.floor(ent.position()[1] / cellSize);
-		let type = this.resourceTypes[resource];
-		let strength = Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[type]);
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2, "constant");
-		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[type]/cellSize, strength/2);
-		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[type]/cellSize, strength, "constant");
+		let grp = this.resourceInfo.aiInfluenceGroups[resource];
+		let strength = Math.floor(ent.resourceSupplyMax()/this.normalizationFactor[grp]);
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2, "constant");
+		this.resourceMaps[resource].addInfluence(x, z, this.influenceRadius[grp]/cellSize, strength/2);
+		this.ccResourceMaps[resource].addInfluence(x, z, this.ccInfluenceRadius[grp]/cellSize, strength, "constant");
 	}
 };
 
diff --git a/binaries/data/mods/public/simulation/ai/petra/baseManager.js b/binaries/data/mods/public/simulation/ai/petra/baseManager.js
index f1b338f..1c823c7 100644
--- a/binaries/data/mods/public/simulation/ai/petra/baseManager.js
+++ b/binaries/data/mods/public/simulation/ai/petra/baseManager.js
@@ -55,7 +55,7 @@ m.BaseManager.prototype.init = function(gameState, state)
 	this.dropsites = {};
 	this.dropsiteSupplies = {};
 	this.gatherers = {};
-	for (let res of gameState.sharedScript.resourceList)
+	for (let res of gameState.sharedScript.resourceInfo.codes)
 	{
 		this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
 		this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
@@ -434,7 +434,7 @@ m.BaseManager.prototype.getResourceLevel = function (gameState, type, nearbyOnly
 /** check our resource levels and react accordingly */
 m.BaseManager.prototype.checkResourceLevels = function (gameState, queues)
 {
-	for (let type of gameState.sharedScript.resourceList)
+	for (let type of gameState.sharedScript.resourceInfo.codes)
 	{
 		if (type === "food")
 		{
diff --git a/binaries/data/mods/public/simulation/ai/petra/chatHelper.js b/binaries/data/mods/public/simulation/ai/petra/chatHelper.js
index 60999e8..d50232f 100644
--- a/binaries/data/mods/public/simulation/ai/petra/chatHelper.js
+++ b/binaries/data/mods/public/simulation/ai/petra/chatHelper.js
@@ -91,7 +91,7 @@ m.chatRequestTribute = function(gameState, resource)
 		"message": message,
 		"translateMessage": true,
 		"translateParameters": {"resource": "withinSentence"},
-		"parameters": {"resource": gameState.sharedScript.resourceNames[resource]}
+		"parameters": {"resource": gameState.sharedScript.resourceInfo.names[resource]}
 	});
 };
 
diff --git a/binaries/data/mods/public/simulation/ai/petra/config.js b/binaries/data/mods/public/simulation/ai/petra/config.js
index d5dc6b3..b0f5e7f 100644
--- a/binaries/data/mods/public/simulation/ai/petra/config.js
+++ b/binaries/data/mods/public/simulation/ai/petra/config.js
@@ -103,7 +103,26 @@ m.Config = function(difficulty)
 		"defensive": 0.5
 	};
 
-	this.resources = ["food", "wood", "stone", "metal"];
+	// See m.QueueManager.prototype.wantedGatherRates()
+	this.queues =
+	{
+		"firstTurn": {
+			"food": 10,
+			"wood": 10,
+			"default": 0
+		},
+		"short": {
+			"food": 200,
+			"wood": 200,
+			"default": 100
+		},
+		"medium": {
+			"default": 0
+		},
+		"long": {
+			"default": 0
+		}
+	};
 };
 
 m.Config.prototype.setConfig = function(gameState)
diff --git a/binaries/data/mods/public/simulation/ai/petra/headquarters.js b/binaries/data/mods/public/simulation/ai/petra/headquarters.js
index 3e67da9..871035f 100644
--- a/binaries/data/mods/public/simulation/ai/petra/headquarters.js
+++ b/binaries/data/mods/public/simulation/ai/petra/headquarters.js
@@ -69,7 +69,7 @@ m.HQ.prototype.init = function(gameState, queues)
 	this.navalMap = false;
 	this.navalRegions = {};
 
-	for (let res of gameState.sharedScript.resourceList)
+	for (let res of gameState.sharedScript.resourceInfo.codes)
 	{
 		this.wantedRates[res] = 0;
 		this.currentRates[res] = 0;
@@ -653,7 +653,7 @@ m.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number)
 m.HQ.prototype.getTotalResourceLevel = function(gameState)
 {
 	let total = {};
-	for (let res of gameState.sharedScript.resourceList)
+	for (let res of gameState.sharedScript.resourceInfo.codes)
 		total[res] = 0;
 	for (let base of this.baseManagers)
 		for (let res in total)
diff --git a/binaries/data/mods/public/simulation/ai/petra/queueManager.js b/binaries/data/mods/public/simulation/ai/petra/queueManager.js
index 4691d11..671131d 100644
--- a/binaries/data/mods/public/simulation/ai/petra/queueManager.js
+++ b/binaries/data/mods/public/simulation/ai/petra/queueManager.js
@@ -84,8 +84,8 @@ m.QueueManager.prototype.wantedGatherRates = function(gameState)
 	if (gameState.ai.playedTurn === 0)
 	{
 		let ret = {};
-		for (let res of gameState.sharedScript.resourceList)
-			ret[res] = (res === "food" || res === "wood" ) ? 10 : 0;
+		for (let res of gameState.sharedScript.resourceInfo.codes)
+			ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default;
 		return ret;
 	}
 
@@ -97,11 +97,11 @@ m.QueueManager.prototype.wantedGatherRates = function(gameState)
 	let totalShort = {};
 	let totalMedium = {};
 	let totalLong = {};
-	for (let res of gameState.sharedScript.resourceList)
+	for (let res of gameState.sharedScript.resourceInfo.codes)
 	{
-		totalShort[res] = (res === "food" || res === "wood" ) ? 200 : 100;
-		totalMedium[res] = 0;
-		totalLong[res] = 0;
+		totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default;
+		totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default;
+		totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default;
 	}
 	let total;
 	//queueArrays because it's faster.
@@ -133,7 +133,7 @@ m.QueueManager.prototype.wantedGatherRates = function(gameState)
 	// global rates
 	let rates = {};
 	let diff;
-	for (let res of gameState.sharedScript.resourceList)
+	for (let res of gameState.sharedScript.resourceInfo.codes)
 	{
 		if (current[res] > 0)
 		{
diff --git a/binaries/data/mods/public/simulation/ai/petra/researchManager.js b/binaries/data/mods/public/simulation/ai/petra/researchManager.js
index f169b39..8e409a1 100644
--- a/binaries/data/mods/public/simulation/ai/petra/researchManager.js
+++ b/binaries/data/mods/public/simulation/ai/petra/researchManager.js
@@ -100,7 +100,8 @@ m.ResearchManager.prototype.researchWantedTechs = function(gameState, techs)
 			let cost = template.cost;
 			let costMax = 0;
 			for (let res in cost)
-				costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
+				if (gameState.sharedScript.resourceInfo.codes.indexOf(res))
+					costMax = Math.max(costMax, Math.max(cost[res]-available[res], 0));
 			if (10*numWorkers < costMax)
 				continue;
 		}
diff --git a/binaries/data/mods/public/simulation/components/Barter.js b/binaries/data/mods/public/simulation/components/Barter.js
index 24c39a4..106d1e3 100644
--- a/binaries/data/mods/public/simulation/components/Barter.js
+++ b/binaries/data/mods/public/simulation/components/Barter.js
@@ -1,9 +1,8 @@
-// True price of 100 units of resource (for case if some resource is more worth).
+// The "true price" is a base price of 100 units of resource (for the case of some resources being of more worth than others).
 // With current bartering system only relative values makes sense
 // so if for example stone is two times more expensive than wood,
 // there will 2:1 exchange rate.
-const TRUE_PRICES = { "food": 100, "wood": 100, "stone": 100, "metal": 100 };
-
+//
 // Constant part of price difference between true price and buy/sell price.
 // In percents.
 // Buy price equal to true price plus constant difference.
@@ -21,9 +20,6 @@ const DIFFERENCE_RESTORE = 0.5;
 // Interval of timer which slowly restore prices after deals
 const RESTORE_TIMER_INTERVAL = 5000;
 
-// Array of resource names
-const RESOURCES = ["food", "wood", "stone", "metal"];
-
 function Barter() {}
 
 Barter.prototype.Schema =
@@ -32,7 +28,7 @@ Barter.prototype.Schema =
 Barter.prototype.Init = function()
 {
 	this.priceDifferences = {};
-	for (var resource of RESOURCES)
+	for (let resource of Resources.GetCodes())
 		this.priceDifferences[resource] = 0;
 	this.restoreTimer = undefined;
 };
@@ -40,10 +36,11 @@ Barter.prototype.Init = function()
 Barter.prototype.GetPrices = function()
 {
 	var prices = { "buy": {}, "sell": {} };
-	for (var resource of RESOURCES)
+	for (let resource of Resources.GetCodes())
 	{
-		prices["buy"][resource] = TRUE_PRICES[resource] * (100 + CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
-		prices["sell"][resource] = TRUE_PRICES[resource] * (100 - CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
+		let truePrice = Resources.GetResource(resource).truePrice;
+		prices.buy[resource] = truePrice * (100 + CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
+		prices.sell[resource] = truePrice * (100 - CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
 	}
 	return prices;
 };
@@ -71,12 +68,13 @@ Barter.prototype.ExchangeResources = function(playerEntity, resourceToSell, reso
 		warn("ExchangeResources: incorrect amount: " + uneval(amount));
 		return;
 	}
-	if (RESOURCES.indexOf(resourceToSell) == -1)
+	let availResources = Resources.GetCodes();
+	if (availResources.indexOf(resourceToSell) == -1)
 	{
 		warn("ExchangeResources: incorrect resource to sell: " + uneval(resourceToSell));
 		return;
 	}
-	if (RESOURCES.indexOf(resourceToBuy) == -1)
+	if (availResources.indexOf(resourceToBuy) == -1)
 	{
 		warn("ExchangeResources: incorrect resource to buy: " + uneval(resourceToBuy));
 		return;
@@ -123,7 +121,7 @@ Barter.prototype.ExchangeResources = function(playerEntity, resourceToSell, reso
 Barter.prototype.ProgressTimeout = function(data)
 {
 	var needRestore = false;
-	for (var resource of RESOURCES)
+	for (let resource of Resources.GetCodes())
 	{
 		// Calculate value to restore, it should be limited to [-DIFFERENCE_RESTORE; DIFFERENCE_RESTORE] interval
 		var differenceRestore = Math.min(DIFFERENCE_RESTORE, Math.max(-DIFFERENCE_RESTORE, this.priceDifferences[resource]));
diff --git a/binaries/data/mods/public/simulation/components/Cost.js b/binaries/data/mods/public/simulation/components/Cost.js
index b3196a6..bf33b60 100644
--- a/binaries/data/mods/public/simulation/components/Cost.js
+++ b/binaries/data/mods/public/simulation/components/Cost.js
@@ -1,5 +1,7 @@
 function Cost() {}
 
+Cost.prototype.ResourcesSchema = Resources.BuildSchema("nonNegativeDecimal");
+
 Cost.prototype.Schema =
 	"<a:help>Specifies the construction/training costs of this entity.</a:help>" +
 	"<a:example>" +
@@ -19,16 +21,11 @@ Cost.prototype.Schema =
 	"<element name='PopulationBonus' a:help='Population cap increase while this entity exists'>" +
 		"<data type='nonNegativeInteger'/>" +
 	"</element>" +
-	"<element name='BuildTime' a:help='Time taken to construct/train this unit (in seconds)'>" +
+	"<element name='BuildTime' a:help='Time taken to construct/train this entity (in seconds)'>" +
 		"<ref name='nonNegativeDecimal'/>" +
 	"</element>" +
-	"<element name='Resources' a:help='Resource costs to construct/train this unit'>" +
-		"<interleave>" +
-			"<element name='food'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='wood'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='stone'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='metal'><ref name='nonNegativeDecimal'/></element>" +
-		"</interleave>" +
+	"<element name='Resources' a:help='Resource costs to construct/train this entity'>" +
+		Cost.prototype.ResourcesSchema +
 	"</element>";
 
 Cost.prototype.Init = function()
@@ -70,8 +67,16 @@ Cost.prototype.GetResourceCosts = function(owner)
 	let entityTemplate = cmpTemplateManager.GetTemplate(entityTemplateName);
 
 	let costs = {};
-	for (let r in this.template.Resources)
-		costs[r] = ApplyValueModificationsToTemplate("Cost/Resources/"+r, +this.template.Resources[r], owner, entityTemplate);
+	let resCodes = Resources.GetCodes();
+
+	for (let res in this.template.Resources)
+	{
+		let cost = +this.template.Resources[res];
+		if (resCodes.indexOf(res) < 0)
+			continue;
+		costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, cost, owner, entityTemplate);
+	}
+
 	return costs;
 };
 
diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js
index aaa7855..1b89b40 100644
--- a/binaries/data/mods/public/simulation/components/GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/GuiInterface.js
@@ -152,6 +152,15 @@ GuiInterface.prototype.GetSimulationState = function()
 	let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
 	ret.barterPrices = cmpBarter.GetPrices();
 
+	// Add Resource Codes, untranslated names and AI Analysis
+	ret.resources = {
+		"codes": Resources.GetCodes(),
+		"names": Resources.GetNames(),
+		"aiInfluenceGroups": {}
+	};
+	for (let res of ret.resources.codes)
+		ret.resources.aiInfluenceGroups[res] = Resources.GetResource(res).aiAnalysisInfluenceGroup || 0;
+
 	// Add basic statistics to each player
 	for (let i = 0; i < numPlayers; ++i)
 	{
@@ -1269,8 +1278,10 @@ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
 
 	let result = {
 		"pieces": [],
-		"cost": { "food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0 },
+		"cost": { "population": 0, "populationBonus": 0, "time": 0 },
 	};
+	for (let res of Resources.GetCodes())
+		result.cost[res] = 0;
 
 	let previewEntities = [];
 	if (end.pos)
@@ -1545,13 +1556,9 @@ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
 			// copied over, so we need to fetch it from the template instead).
 			// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
 			// boilerplate that's probably duplicated in tons of places.
-			result.cost.food += tplData.cost.food;
-			result.cost.wood += tplData.cost.wood;
-			result.cost.stone += tplData.cost.stone;
-			result.cost.metal += tplData.cost.metal;
-			result.cost.population += tplData.cost.population;
-			result.cost.populationBonus += tplData.cost.populationBonus;
-			result.cost.time += tplData.cost.time;
+			let entries = Resources.GetCodes().concat("population", "populationBonus", "time");
+			for (let res of entries)
+				result.cost[res] = tplData.cost[res];
 		}
 
 		let canAfford = true;
diff --git a/binaries/data/mods/public/simulation/components/Loot.js b/binaries/data/mods/public/simulation/components/Loot.js
index 3161340..2d0939c 100644
--- a/binaries/data/mods/public/simulation/components/Loot.js
+++ b/binaries/data/mods/public/simulation/components/Loot.js
@@ -1,21 +1,14 @@
 function Loot() {}
 
+Loot.prototype.ResourcesSchema = Resources.BuildSchema("nonNegativeInteger", [ "xp" ]);
+
 Loot.prototype.Schema =
-	"<optional>" +
-		"<element name='xp'><data type='nonNegativeInteger'/></element>" +
-	"</optional>" +
-	"<optional>" +
-		"<element name='food'><data type='nonNegativeInteger'/></element>" +
-	"</optional>" +
-	"<optional>" +
-		"<element name='wood'><data type='nonNegativeInteger'/></element>" +
-	"</optional>" +
-	"<optional>" +
-		"<element name='stone'><data type='nonNegativeInteger'/></element>" +
-	"</optional>" +
-	"<optional>" +
-		"<element name='metal'><data type='nonNegativeInteger'/></element>" +
-	"</optional>";
+	"<a:help>Specifies the loot credited when this entity is killed.</a:help>" +
+	"<a:example>" +
+		"<xp>35</xp>" +
+		"<metal>10</metal>" +
+	"</a:example>" +
+	Loot.prototype.ResourcesSchema;
 
 Loot.prototype.Serialize = null; // we have no dynamic state to save
 
@@ -26,12 +19,11 @@ Loot.prototype.GetXp = function()
 
 Loot.prototype.GetResources = function()
 {
-	return {
-		"food": +(this.template.food || 0),
-		"wood": +(this.template.wood || 0),
-		"metal": +(this.template.metal || 0),
-		"stone": +(this.template.stone || 0)
-	};
+	let ret = {};
+	for (let res of Resources.GetCodes())
+		ret[res] = +(this.template[res] || 0);
+
+	return ret;
 };
 
 Engine.RegisterComponentType(IID_Loot, "Loot", Loot);
diff --git a/binaries/data/mods/public/simulation/components/Player.js b/binaries/data/mods/public/simulation/components/Player.js
index cb137dd..51236ce 100644
--- a/binaries/data/mods/public/simulation/components/Player.js
+++ b/binaries/data/mods/public/simulation/components/Player.js
@@ -18,18 +18,8 @@ Player.prototype.Init = function()
 	this.popBonuses = 0; // sum of population bonuses of player's entities
 	this.maxPop = 300; // maximum population
 	this.trainingBlocked = false; // indicates whether any training queue is currently blocked
-	this.resourceCount = {
-		"food": 300,
-		"wood": 300,
-		"metal": 300,
-		"stone": 300
-	};
-	// goods for next trade-route and its proba in % (the sum of probas must be 100)
-	this.tradingGoods = [
-		{ "goods":  "wood", "proba": 30 },
-		{ "goods": "stone", "proba": 35 },
-		{ "goods": "metal", "proba": 35 },
-	];
+	this.resourceCount = {};
+	this.tradingGoods = []; // goods for next trade-route and its proba in % (the sum of probas must be 100)
 	this.team = -1;	// team number of the player, players on the same team will always have ally diplomatic status - also this is useful for team emblems, scoring, etc.
 	this.teamsLocked = false;
 	this.state = "active"; // game state - one of "active", "defeated", "won"
@@ -44,15 +34,25 @@ Player.prototype.Init = function()
 	this.cheatsEnabled = false;
 	this.cheatTimeMultiplier = 1;
 	this.heroes = [];
-	this.resourceNames = {
-		"food": markForTranslation("Food"),
-		"wood": markForTranslation("Wood"),
-		"metal": markForTranslation("Metal"),
-		"stone": markForTranslation("Stone"),
-	};
+	this.resourceNames = {};
 	this.disabledTemplates = {};
 	this.disabledTechnologies = {};
 	this.startingTechnologies = [];
+
+	// Initial resources and trading goods probability in steps of 5
+	let resCodes = Resources.GetCodes();
+	let quotient = Math.floor(20 / resCodes.length);
+	let remainder = 20 % resCodes.length;
+	for (let i in resCodes)
+	{
+		let res = resCodes[i];
+		this.resourceCount[res] = 300;
+		this.resourceNames[res] = Resources.GetResource(res).name;
+		this.tradingGoods.push({
+			"goods":  res,
+			"proba": 5 * (quotient + (+i < remainder ? 1 : 0))
+		});
+	}
 };
 
 Player.prototype.SetPlayerID = function(id)
@@ -197,14 +197,9 @@ Player.prototype.UnBlockTraining = function()
 
 Player.prototype.SetResourceCounts = function(resources)
 {
-	if (resources.food !== undefined)
-		this.resourceCount.food = resources.food;
-	if (resources.wood !== undefined)
-		this.resourceCount.wood = resources.wood;
-	if (resources.stone !== undefined)
-		this.resourceCount.stone = resources.stone;
-	if (resources.metal !== undefined)
-		this.resourceCount.metal = resources.metal;
+	for (let res in resources)
+		if (this.resourceCount[res])
+			this.resourceCount[res] = resources[res];
 };
 
 Player.prototype.GetResourceCounts = function()
@@ -297,7 +292,8 @@ Player.prototype.SubtractResourcesOrNotify = function(amounts)
 
 	// Subtract the resources
 	for (var type in amounts)
-		this.resourceCount[type] -= amounts[type];
+		if (this.resourceCount[type])
+			this.resourceCount[type] -= amounts[type];
 
 	return true;
 };
@@ -346,7 +342,15 @@ Player.prototype.SetTradingGoods = function(tradingGoods)
 	if (sumProba != 100)	// consistency check
 	{
 		error("Player.js SetTradingGoods: " + uneval(tradingGoods));
-		tradingGoods = { "food": 20, "wood":20, "stone":30, "metal":30 };
+		let first = true;
+		for (let res of Resources.GetCodes())
+			if (first)
+			{
+				tradingGoods[res] = 100;
+				first = false;
+			}
+			else
+				tradingGoods[res] = 0;
 	}
 
 	this.tradingGoods = [];
diff --git a/binaries/data/mods/public/simulation/components/ProductionQueue.js b/binaries/data/mods/public/simulation/components/ProductionQueue.js
index 6c72202..447b0ff 100644
--- a/binaries/data/mods/public/simulation/components/ProductionQueue.js
+++ b/binaries/data/mods/public/simulation/components/ProductionQueue.js
@@ -3,6 +3,8 @@ const MAX_QUEUE_SIZE = 16;
 
 function ProductionQueue() {}
 
+ProductionQueue.prototype.ResourceSchema = Resources.BuildSchema("nonNegativeDecimal", [ "time" ]);
+
 ProductionQueue.prototype.Schema =
 	"<a:help>Allows the building to train new units and research technologies</a:help>" +
 	"<a:example>" +
@@ -31,13 +33,7 @@ ProductionQueue.prototype.Schema =
 		"</element>" +
 	"</optional>" +
 	"<element name='TechCostMultiplier' a:help='Multiplier to modify ressources cost and research time of technologies searched in this building.'>" +
-		"<interleave>" +
-			"<element name='food'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='wood'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='stone'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='metal'><ref name='nonNegativeDecimal'/></element>" +
-			"<element name='time'><ref name='nonNegativeDecimal'/></element>" +
-		"</interleave>" +
+		ProductionQueue.prototype.ResourceSchema +
 	"</element>";
 
 ProductionQueue.prototype.Init = function()
@@ -260,7 +256,8 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
 	// TODO: there should probably be a limit on the number of queued batches
 	// TODO: there should be a way for the GUI to determine whether it's going
 	// to be possible to add a batch (based on resource costs and length limits)
-	var cmpPlayer = QueryOwnerInterface(this.entity);
+	let cmpPlayer = QueryOwnerInterface(this.entity);
+	let resCodes = Resources.GetCodes();
 
 	if (this.queue.length < MAX_QUEUE_SIZE)
 	{
@@ -293,10 +290,13 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
 			var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template);
 			var time = timeMult * buildTime;
 
-			for (var r in template.Cost.Resources)
+			for (let res in template.Cost.Resources)
 			{
-				costs[r] = ApplyValueModificationsToTemplate("Cost/Resources/"+r, +template.Cost.Resources[r], cmpPlayer.GetPlayerID(), template);
-				totalCosts[r] = Math.floor(count * costs[r]);
+				let cost = +template.Cost.Resources[res];
+				if (resCodes.indexOf(res) < 0)
+					continue;
+				costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, cost, cmpPlayer.GetPlayerID(), template);
+				totalCosts[res] = Math.floor(count * costs[res]);
 			}
 
 			var population = ApplyValueModificationsToTemplate("Cost/Population",  +template.Cost.Population, cmpPlayer.GetPlayerID(), template);
@@ -341,9 +341,13 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
 			let techCostMultiplier = this.GetTechCostMultiplier();
 			let time =  techCostMultiplier.time * template.researchTime * cmpPlayer.GetCheatTimeMultiplier();
 
-			var cost = {};
+			let cost = {};
 			for (let res in template.cost)
-				cost[res] = Math.floor(techCostMultiplier[res] * template.cost[res]);
+			{
+				if (resCodes.indexOf(res) < 0)
+					continue;
+				cost[res] = Math.floor((techCostMultiplier[res] ? techCostMultiplier[res] : 1) * template.cost[res]);
+			}
 
 			// TrySubtractResources should report error to player (they ran out of resources)
 			if (!cmpPlayer.TrySubtractResources(cost))
@@ -361,7 +365,7 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat
 				"player": cmpPlayer.GetPlayerID(),
 				"count": 1,
 				"technologyTemplate": templateName,
-				"resources": deepcopy(template.cost), // need to copy to avoid serialization problems
+				"resources": cost,
 				"productionStarted": false,
 				"timeTotal": time*1000,
 				"timeRemaining": time*1000,
@@ -433,8 +437,10 @@ ProductionQueue.prototype.RemoveBatch = function(id)
 		// Refund the resource cost for this batch
 		var totalCosts = {};
 		var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
-		for each (var r in ["food", "wood", "stone", "metal"])
+		for (let r of Resources.GetCodes())
 		{
+			if (!item.resources[r])
+				continue;
 			totalCosts[r] = Math.floor(item.count * item.resources[r]);
 			if (cmpStatisticsTracker)
 				cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]);
diff --git a/binaries/data/mods/public/simulation/components/ResourceDropsite.js b/binaries/data/mods/public/simulation/components/ResourceDropsite.js
index 819807c..a14ddaf 100644
--- a/binaries/data/mods/public/simulation/components/ResourceDropsite.js
+++ b/binaries/data/mods/public/simulation/components/ResourceDropsite.js
@@ -1,15 +1,12 @@
 function ResourceDropsite() {}
 
+ResourceDropsite.prototype.ResourceChoiceSchema = Resources.BuildChoicesSchema();
+
 ResourceDropsite.prototype.Schema =
 	"<element name='Types'>" +
 		"<list>" +
 			"<zeroOrMore>" +
-				"<choice>" +
-					"<value>food</value>" +
-					"<value>wood</value>" +
-					"<value>stone</value>" +
-					"<value>metal</value>" +
-				"</choice>" +
+				ResourceDropsite.prototype.ResourceChoiceSchema +
 			"</zeroOrMore>" +
 		"</list>" +
 	"</element>" +
@@ -24,12 +21,14 @@ ResourceDropsite.prototype.Init = function()
 };
 
 /**
- * Returns the list of resource types accepted by this dropsite.
+ * Returns the list of resource types accepted by this dropsite,
+ * as defined by it being referred to in the template and the resource being enabled.
  */
 ResourceDropsite.prototype.GetTypes = function()
 {
 	let types = ApplyValueModificationsToEntity("ResourceDropsite/Types", this.template.Types, this.entity);
-	return types ? types.split(/\s+/) : [];
+	let resources = Resources.GetCodes();
+	return types.split(/\s+/).filter(type => resources.indexOf(type.toLowerCase()) > -1);
 };
 
 /**
diff --git a/binaries/data/mods/public/simulation/components/ResourceGatherer.js b/binaries/data/mods/public/simulation/components/ResourceGatherer.js
index acd5fbd..52f0b44 100644
--- a/binaries/data/mods/public/simulation/components/ResourceGatherer.js
+++ b/binaries/data/mods/public/simulation/components/ResourceGatherer.js
@@ -1,5 +1,8 @@
 function ResourceGatherer() {}
 
+ResourceGatherer.prototype.ResourcesSchema = Resources.BuildSchema("positiveDecimal", [ "treasure" ], true);
+ResourceGatherer.prototype.CapacitiesSchema = Resources.BuildSchema("positiveDecimal");
+
 ResourceGatherer.prototype.Schema =
 	"<a:help>Lets the unit gather resources from entities that have the ResourceSupply component.</a:help>" +
 	"<a:example>" +
@@ -25,35 +28,10 @@ ResourceGatherer.prototype.Schema =
 		"<ref name='positiveDecimal'/>" +
 	"</element>" +
 	"<element name='Rates' a:help='Per-resource-type gather rate multipliers. If a resource type is not specified then it cannot be gathered by this unit'>" +
-		"<interleave>" +
-			"<optional><element name='food' a:help='Food gather rate (may be overridden by \"food.*\" subtypes)'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='wood' a:help='Wood gather rate'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='stone' a:help='Stone gather rate'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='metal' a:help='Metal gather rate'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='treasure' a:help='Treasure gather rate (only presense on value makes sense, size is only used to determine the delay before gathering, so it should be set to 1)'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='food.fish' a:help='Fish gather rate (overrides \"food\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='food.fruit' a:help='Fruit gather rate (overrides \"food\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='food.grain' a:help='Grain gather rate (overrides \"food\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='food.meat' a:help='Meat gather rate (overrides \"food\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='food.milk' a:help='Milk gather rate (overrides \"food\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='wood.tree' a:help='Tree gather rate (overrides \"wood\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='wood.ruins' a:help='Tree gather rate (overrides \"wood\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='stone.rock' a:help='Rock gather rate (overrides \"stone\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='stone.ruins' a:help='Rock gather rate (overrides \"stone\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='metal.ore' a:help='Ore gather rate (overrides \"metal\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='treasure.food' a:help='Food treasure gather rate (overrides \"treasure\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='treasure.wood' a:help='Wood treasure gather rate (overrides \"treasure\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='treasure.stone' a:help='Stone treasure gather rate (overrides \"treasure\")'><ref name='positiveDecimal'/></element></optional>" +
-			"<optional><element name='treasure.metal' a:help='Metal treasure gather rate (overrides \"treasure\")'><ref name='positiveDecimal'/></element></optional>" +
-		"</interleave>" +
+		ResourceGatherer.prototype.ResourcesSchema +
 	"</element>" +
 	"<element name='Capacities' a:help='Per-resource-type maximum carrying capacity'>" +
-		"<interleave>" +
-			"<element name='food' a:help='Food capacity'><ref name='positiveDecimal'/></element>" +
-			"<element name='wood' a:help='Wood capacity'><ref name='positiveDecimal'/></element>" +
-			"<element name='stone' a:help='Stone capacity'><ref name='positiveDecimal'/></element>" +
-			"<element name='metal' a:help='Metal capacity'><ref name='positiveDecimal'/></element>" +
-		"</interleave>" +
+		ResourceGatherer.prototype.CapacitiesSchema +
 	"</element>";
 
 ResourceGatherer.prototype.Init = function()
@@ -137,6 +115,12 @@ ResourceGatherer.prototype.RecalculateGatherRatesAndCapacities = function()
 	this.rates = {};
 	for (let r in this.template.Rates)
 	{
+		let type = r.split(".");
+		let res = Resources.GetResource(type[0]);
+
+		if (!res && type[0] !== "treasure" || (type.length > 1 && res.subtypes.indexOf(type[1]) < 0))
+			continue;
+
 		let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity);
 		this.rates[r] = rate * this.baseSpeed;
 	}
@@ -174,7 +158,7 @@ ResourceGatherer.prototype.GetRange = function()
 
 /**
  * Try to gather treasure
- * @return 'true' if treasure is successfully gathered and 'false' in the other case
+ * @return 'true' if treasure is successfully gathered and 'false' if not
  */
 ResourceGatherer.prototype.TryInstantGather = function(target)
 {
diff --git a/binaries/data/mods/public/simulation/components/ResourceSupply.js b/binaries/data/mods/public/simulation/components/ResourceSupply.js
index 04e95da..7cc580b 100644
--- a/binaries/data/mods/public/simulation/components/ResourceSupply.js
+++ b/binaries/data/mods/public/simulation/components/ResourceSupply.js
@@ -1,5 +1,7 @@
 function ResourceSupply() {}
 
+ResourceSupply.prototype.ResourceChoiceSchema = Resources.BuildChoicesSchema(true, true);
+
 ResourceSupply.prototype.Schema =
 	"<a:help>Provides a supply of one particular type of resource.</a:help>" +
 	"<a:example>" +
@@ -12,23 +14,8 @@ ResourceSupply.prototype.Schema =
 	"<element name='Amount' a:help='Amount of resources available from this entity'>" +
 		"<choice><data type='nonNegativeInteger'/><value>Infinity</value></choice>" +
 	"</element>" +
-	"<element name='Type' a:help='Type of resources'>" +
-		"<choice>" +
-			"<value>wood.tree</value>" +
-			"<value>wood.ruins</value>" +
-			"<value>stone.rock</value>" +
-			"<value>stone.ruins</value>" +
-			"<value>metal.ore</value>" +
-			"<value>food.fish</value>" +
-			"<value>food.fruit</value>" +
-			"<value>food.grain</value>" +
-			"<value>food.meat</value>" +
-			"<value>food.milk</value>" +
-			"<value>treasure.wood</value>" +
-			"<value>treasure.stone</value>" +
-			"<value>treasure.metal</value>" +
-			"<value>treasure.food</value>" +
-		"</choice>" +
+	"<element name='Type' a:help='Type and Subtype of resource available from this entity'>" +
+		ResourceSupply.prototype.ResourceChoiceSchema +
 	"</element>" +
 	"<element name='MaxGatherers' a:help='Amount of gatherers who can gather resources from this entity at the same time'>" +
 		"<data type='nonNegativeInteger'/>" +
@@ -45,16 +32,23 @@ ResourceSupply.prototype.Init = function()
 	this.amount = this.GetMaxAmount();
 
 	this.gatherers = [];	// list of IDs for each players
-	var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);	// system component so that's safe.
-	var numPlayers = cmpPlayerManager.GetNumPlayers();
-	for (var i = 0; i <= numPlayers; ++i)	// use "<=" because we want Gaia too.
+	let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);	// system component so that's safe.
+	let numPlayers = cmpPlayerManager.GetNumPlayers();
+	for (let i = 0; i <= numPlayers; ++i)	// use "<=" because we want Gaia too.
 		this.gatherers.push([]);
 
 	this.infinite = !isFinite(+this.template.Amount);
 
-	[this.type,this.subType] = this.template.Type.split('.');
-	this.cachedType = { "generic" : this.type, "specific" : this.subType };
+	[this.type, this.subtype] = this.template.Type.split('.');
+	let resData = Resources.GetResource(this.type);
+	if (this.type === "treasure")
+		resData = { "subtypes": Resources.GetCodes() };
+
+	// Remove entity from gameworld if the resource supplied by this entity is disabled or not valid.
+	if (!resData || resData.subtypes.indexOf(this.subtype) === -1)
+		Engine.DestroyEntity(this.entity);
 
+	this.cachedType = { "generic" : this.type, "specific" : this.subtype };
 };
 
 ResourceSupply.prototype.IsInfinite = function()
diff --git a/binaries/data/mods/public/simulation/components/ResourceTrickle.js b/binaries/data/mods/public/simulation/components/ResourceTrickle.js
index 5c554e7..7bed918 100644
--- a/binaries/data/mods/public/simulation/components/ResourceTrickle.js
+++ b/binaries/data/mods/public/simulation/components/ResourceTrickle.js
@@ -1,30 +1,11 @@
 function ResourceTrickle() {}
 
+ResourceTrickle.prototype.ResourcesSchema = Resources.BuildSchema("nonNegativeDecimal");
+
 ResourceTrickle.prototype.Schema = 
 	"<a:help>Controls the resource trickle ability of the unit.</a:help>" +
 	"<element name='Rates' a:help='Trickle Rates'>" +
-		"<interleave>" +
-			"<optional>" +
-				"<element name='food' a:help='Food given to the player every interval'>" +
-					"<ref name='nonNegativeDecimal'/>" +
-				"</element>" +
-			"</optional>" +
-			"<optional>" +
-				"<element name='wood' a:help='Wood given to the player every interval'>" +
-					"<ref name='nonNegativeDecimal'/>" +
-				"</element>" +
-			"</optional>" +
-			"<optional>" +
-				"<element name='stone' a:help='Stone given to the player every interval'>" +
-					"<ref name='nonNegativeDecimal'/>" +
-				"</element>" +
-			"</optional>" +
-			"<optional>" +
-				"<element name='metal' a:help='Metal given to the player every interval'>" +
-					"<ref name='nonNegativeDecimal'/>" +
-				"</element>" +
-			"</optional>" +
-		"</interleave>" +
+		ResourceTrickle.prototype.ResourcesSchema +
 	"</element>" +
 	"<element name='Interval' a:help='Number of miliseconds must pass for the player to gain the next trickle.'>" +
 		"<ref name='nonNegativeDecimal'/>" +
@@ -45,9 +26,14 @@ ResourceTrickle.prototype.GetTimer = function()
 
 ResourceTrickle.prototype.GetRates = function()
 {
-	var rates = {};
-	for (var resource in this.template.Rates)
+	let rates = {};
+	let resCodes = Resources.GetCodes();
+	for (let resource in this.template.Rates)
+	{
+		if (resCodes.indexOf(resource) < 0)
+			continue;
 		rates[resource] = ApplyValueModificationsToEntity("ResourceTrickle/Rates/"+resource, +this.template.Rates[resource], this.entity);
+	}
 
 	return rates;
 };
@@ -55,13 +41,9 @@ ResourceTrickle.prototype.GetRates = function()
 // Do the actual work here
 ResourceTrickle.prototype.Trickle = function(data, lateness)
 {
-	var cmpPlayer = QueryOwnerInterface(this.entity);
-	if (!cmpPlayer)
-		return;
-
-	var rates = this.GetRates();
-	for (var resource in rates)
-		cmpPlayer.AddResource(resource, rates[resource]);
+	let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
+	if (cmpPlayer)
+		cmpPlayer.AddResources(this.GetRates());
 };
 
 Engine.RegisterComponentType(IID_ResourceTrickle, "ResourceTrickle", ResourceTrickle);
diff --git a/binaries/data/mods/public/simulation/components/StatisticsTracker.js b/binaries/data/mods/public/simulation/components/StatisticsTracker.js
index 3bc1f79..482502d 100644
--- a/binaries/data/mods/public/simulation/components/StatisticsTracker.js
+++ b/binaries/data/mods/public/simulation/components/StatisticsTracker.js
@@ -105,30 +105,18 @@ StatisticsTracker.prototype.Init = function()
 	this.buildingsCapturedValue = 0;
 
 	this.resourcesGathered = {
-		"food": 0,
-		"wood": 0,
-		"metal": 0,
-		"stone": 0,
 		"vegetarianFood": 0
 	};
-	this.resourcesUsed = {
-		"food": 0,
-		"wood": 0,
-		"metal": 0,
-		"stone": 0
-	};
-	this.resourcesSold = {
-		"food": 0,
-		"wood": 0,
-		"metal": 0,
-		"stone": 0
-	};
-	this.resourcesBought = {
-		"food": 0,
-		"wood": 0,
-		"metal": 0,
-		"stone": 0
-	};
+	this.resourcesUsed = {};
+	this.resourcesSold = {};
+	this.resourcesBought = {};
+	for (let res of Resources.GetCodes())
+	{
+		this.resourcesGathered[res] = 0;
+		this.resourcesUsed[res] = 0;
+		this.resourcesSold[res] = 0;
+		this.resourcesBought[res] = 0;
+	}
 
 	this.tributesSent = 0;
 	this.tributesReceived = 0;
@@ -347,7 +335,8 @@ StatisticsTracker.prototype.IncreaseResourceGatheredCounter = function(type, amo
  */
 StatisticsTracker.prototype.IncreaseResourceUsedCounter = function(type, amount)
 {
-	this.resourcesUsed[type] += amount;
+	if (typeof this.resourcesUsed[type] === "number")
+		this.resourcesUsed[type] += amount;
 };
 
 StatisticsTracker.prototype.IncreaseTreasuresCollectedCounter = function()
diff --git a/binaries/data/mods/public/simulation/components/Trader.js b/binaries/data/mods/public/simulation/components/Trader.js
index 735778d..771a814 100644
--- a/binaries/data/mods/public/simulation/components/Trader.js
+++ b/binaries/data/mods/public/simulation/components/Trader.js
@@ -4,9 +4,6 @@
 // Additional gain for ships for each garrisoned trader, in percents
 const GARRISONED_TRADER_ADDITION = 20;
 
-// Array of resource names
-const RESOURCES = ["food", "wood", "stone", "metal"];
-
 function Trader() {}
 
 Trader.prototype.Schema =
diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
index 6ae026e..7e7f2d2 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
@@ -33,6 +33,12 @@ Engine.LoadComponentScript("interfaces/Upgrade.js");
 Engine.LoadComponentScript("interfaces/BuildingAI.js");
 Engine.LoadComponentScript("GuiInterface.js");
 
+Resources = {
+	"GetCodes": function() { return [ "food", "metal", "stone", "wood" ] },
+	"GetNames": function() { return { "food": "Food", "metal": "Metal", "stone": "Stone", "wood": "Wood" } },
+	"GetResource": function() { return {}; },
+};
+
 var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
 
 
@@ -330,7 +336,22 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
 	circularMap: false,
 	timeElapsed: 0,
 	gameType: "conquest",
-	barterPrices: {buy: {food: 150}, sell: {food: 25}}
+	barterPrices: {buy: {food: 150}, sell: {food: 25}},
+	resources: {
+		codes: [ "food", "metal", "stone", "wood" ],
+		names: {
+			"food": "Food",
+			"metal": "Metal",
+			"stone": "Stone",
+			"wood": "Wood",
+		},
+		aiInfluenceGroups: {
+			"food": 0,
+			"metal": 0,
+			"stone": 0,
+			"wood": 0,
+		}
+	},
 });
 
 TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
@@ -449,7 +470,22 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
 	circularMap: false,
 	timeElapsed: 0,
 	gameType: "conquest",
-	barterPrices: {buy: {food: 150}, sell: {food: 25}}
+	barterPrices: {buy: {food: 150}, sell: {food: 25}},
+	resources: {
+		codes: [ "food", "metal", "stone", "wood" ],
+		names: {
+			"food": "Food",
+			"metal": "Metal",
+			"stone": "Stone",
+			"wood": "Wood",
+		},
+		aiInfluenceGroups: {
+			"food": 0,
+			"metal": 0,
+			"stone": 0,
+			"wood": 0,
+		}
+	},
 });
 
 
diff --git a/binaries/data/mods/public/simulation/components/tests/test_Player.js b/binaries/data/mods/public/simulation/components/tests/test_Player.js
index 4191a68..7cba52a 100644
--- a/binaries/data/mods/public/simulation/components/tests/test_Player.js
+++ b/binaries/data/mods/public/simulation/components/tests/test_Player.js
@@ -9,6 +9,11 @@ Engine.LoadComponentScript("Timer.js")
 ConstructComponent(SYSTEM_ENTITY, "EndGameManager");
 ConstructComponent(SYSTEM_ENTITY, "Timer");
 
+Resources = {
+	"GetCodes": function() { return [ "food", "metal", "stone", "wood" ] },
+	"GetResource": function() { return {}; },
+};
+
 var cmpPlayer = ConstructComponent(10, "Player");
 
 TS_ASSERT_EQUALS(cmpPlayer.GetPopulationCount(), 0);
diff --git a/binaries/data/mods/public/simulation/data/resources/food.json b/binaries/data/mods/public/simulation/data/resources/food.json
new file mode 100644
index 0000000..67c85e7
--- /dev/null
+++ b/binaries/data/mods/public/simulation/data/resources/food.json
@@ -0,0 +1,14 @@
+{
+	"code": "food",
+	"name": "Food",
+	"subtypes": {
+		"fish": "Fish",
+		"fruit": "Fruit",
+		"grain": "Grain",
+		"meat": "Meat",
+		"milk": "Milk"
+	},
+	"truePrice": 100,
+	"aiAnalysisInfluenceGroup": 0,
+	"enabled": true
+}
diff --git a/binaries/data/mods/public/simulation/data/resources/metal.json b/binaries/data/mods/public/simulation/data/resources/metal.json
new file mode 100644
index 0000000..2a8b590
--- /dev/null
+++ b/binaries/data/mods/public/simulation/data/resources/metal.json
@@ -0,0 +1,10 @@
+{
+	"code": "metal",
+	"name": "Metal",
+	"subtypes": {
+		"ore": "Ore"
+	},
+	"truePrice": 100,
+	"aiAnalysisInfluenceGroup": 2,
+	"enabled": true
+}
diff --git a/binaries/data/mods/public/simulation/data/resources/stone.json b/binaries/data/mods/public/simulation/data/resources/stone.json
new file mode 100644
index 0000000..034783e
--- /dev/null
+++ b/binaries/data/mods/public/simulation/data/resources/stone.json
@@ -0,0 +1,11 @@
+{
+	"code": "stone",
+	"name": "Stone",
+	"subtypes": {
+		"rock": "Rock",
+		"ruins": "Ruins"
+	},
+	"truePrice": 100,
+	"aiAnalysisInfluenceGroup": 2,
+	"enabled": true
+}
diff --git a/binaries/data/mods/public/simulation/data/resources/wood.json b/binaries/data/mods/public/simulation/data/resources/wood.json
new file mode 100644
index 0000000..9e5a904
--- /dev/null
+++ b/binaries/data/mods/public/simulation/data/resources/wood.json
@@ -0,0 +1,11 @@
+{
+	"code": "wood",
+	"name": "Wood",
+	"subtypes": {
+		"tree": "Tree",
+		"ruins": "Ruins"
+	},
+	"truePrice": 100,
+	"aiAnalysisInfluenceGroup": 1,
+	"enabled": true
+}
diff --git a/binaries/data/mods/public/simulation/helpers/Resources.js b/binaries/data/mods/public/simulation/helpers/Resources.js
new file mode 100644
index 0000000..af6f747
--- /dev/null
+++ b/binaries/data/mods/public/simulation/helpers/Resources.js
@@ -0,0 +1,2 @@
+
+Resources = new Resources();
