From a79a47effe38a92d477b98c200a95bf346da4eb3 Mon Sep 17 00:00:00 2001 From: Freagarach Date: Tue, 29 Dec 2020 11:00:54 +0000 Subject: [PATCH] Allow to limit unit count per match. This allows to limit the number of times a specific template can be constructed/trained/created during a match. Part of: https://wildfiregames.com/forum/index.php?/topic/27214-borg-expansion-pack-mod-implementation-in-0ad-alpha-24-release/ Refs. https://wildfiregames.com/forum/topic/24682-champions-and-civilisations-balance-mod-for-a23/ Differential revision: D2411 Reviewed by: @wraitii Comments by: @Angen, @Stan This was SVN commit r24468. --- .../mods/public/globalscripts/Templates.js | 6 +- .../data/mods/public/gui/session/input.js | 28 ++++++- .../public/gui/session/selection_panels.js | 5 +- .../gui/session/selection_panels_helpers.js | 60 +++++++++++++++ .../components/BuildRestrictions.js | 5 ++ .../simulation/components/EntityLimits.js | 76 ++++++++++++------- .../simulation/components/GuiInterface.js | 1 + .../simulation/components/ProductionQueue.js | 4 + .../components/TrainingRestrictions.js | 7 +- .../components/tests/test_EntityLimits.js | 3 +- .../components/tests/test_GuiInterface.js | 10 ++- .../components/tests/test_ProductionQueue.js | 18 ++++- .../public/simulation/helpers/Commands.js | 2 +- 13 files changed, 185 insertions(+), 40 deletions(-) diff --git a/binaries/data/mods/public/globalscripts/Templates.js b/binaries/data/mods/public/globalscripts/Templates.js index 39ca7847f1..e4d81e507d 100644 --- a/binaries/data/mods/public/globalscripts/Templates.js +++ b/binaries/data/mods/public/globalscripts/Templates.js @@ -291,9 +291,13 @@ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {}) } if (template.TrainingRestrictions) + { ret.trainingRestrictions = { - "category": template.TrainingRestrictions.Category, + "category": template.TrainingRestrictions.Category }; + if (template.TrainingRestrictions.MatchLimit) + ret.trainingRestrictions.matchLimit = +template.TrainingRestrictions.MatchLimit; + } if (template.Cost) { diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index d5eeac778f..6dbef90bb1 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -1270,13 +1270,28 @@ function getEntityLimitAndCount(playerState, entType) "entLimit": undefined, "entCount": undefined, "entLimitChangers": undefined, - "canBeAddedCount": undefined + "canBeAddedCount": undefined, + "matchLimit": undefined, + "matchCount": undefined, + "type": undefined }; if (!playerState.entityLimits) return ret; let template = GetTemplateData(entType); - let entCategory = template.trainingRestrictions && template.trainingRestrictions.category || - template.buildRestrictions && template.buildRestrictions.category; + let entCategory; + let matchLimit; + if (template.trainingRestrictions) + { + entCategory = template.trainingRestrictions.category; + matchLimit = template.trainingRestrictions.matchLimit; + ret.type = "training"; + } + else if (template.buildRestrictions) + { + entCategory = template.buildRestrictions.category; + matchLimit = template.buildRestrictions.matchLimit; + ret.type = "build"; + } if (entCategory && playerState.entityLimits[entCategory] !== undefined) { @@ -1285,6 +1300,13 @@ function getEntityLimitAndCount(playerState, entType) ret.entLimitChangers = playerState.entityLimitChangers[entCategory]; ret.canBeAddedCount = Math.max(ret.entLimit - ret.entCount, 0); } + + if (matchLimit) + { + ret.matchLimit = matchLimit; + ret.matchCount = playerState.matchEntityCounts[entType] || 0; + ret.canBeAddedCount = Math.min(Math.max(ret.entLimit - ret.entCount, 0), Math.max(ret.matchLimit - ret.matchCount, 0)); + } return ret; } diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js index a0fe49895e..a471002e0d 100644 --- a/binaries/data/mods/public/gui/session/selection_panels.js +++ b/binaries/data/mods/public/gui/session/selection_panels.js @@ -257,6 +257,7 @@ g_SelectionPanels.Construction = { let limits = getEntityLimitAndCount(data.playerState, data.item); tooltips.push( formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), + formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources)); @@ -998,7 +999,8 @@ g_SelectionPanels.Training = { getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch) ]; let limits = getEntityLimitAndCount(data.playerState, data.item); - tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers)); + tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), + formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type)); if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true") tooltips = tooltips.concat([ @@ -1111,6 +1113,7 @@ g_SelectionPanels.Upgrade = { tooltips.push( getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length), formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers), + formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type), getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ), getNeededResourcesTooltip(neededResources), showTemplateViewerOnRightClickTooltip()); 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 54466f9446..55bec15696 100644 --- a/binaries/data/mods/public/gui/session/selection_panels_helpers.js +++ b/binaries/data/mods/public/gui/session/selection_panels_helpers.js @@ -115,6 +115,66 @@ function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers) return text; } +/** + * Format template match count/limit message for the tooltip. + * + * @param {number} matchEntLimit - The limit of the entity. + * @param {number} matchEntCount - The count of the entity. + * @param {string} type - The type of the action (i.e. "build" or "training"). + * + * @return {string} - The string to show the user with information regarding the limit of this template. + */ +function formatMatchLimitString(matchEntLimit, matchEntCount, type) +{ + if (matchEntLimit == undefined) + return ""; + + let passedLimit = matchEntCount >= matchEntLimit; + let count = matchEntLimit - matchEntCount; + let text; + if (type == "build") + { + if (passedLimit) + text = sprintf(translatePlural("Could be constructed merely once.", "Could be constructed merely %(limit)s times.", matchEntLimit), { + "limit": matchEntLimit + }); + else if (matchEntLimit == 1) + text = translate("Can be constructed only once."); + else + text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), { + "count": count + }); + } + else if (type == "training") + { + if (passedLimit) + text = sprintf(translatePlural("Could be trained merely once.", "Could be trained merely %(limit)s times.", matchEntLimit), { + "limit": matchEntLimit + }); + else if (matchEntLimit == 1) + text = translate("Can be trained only once."); + else + text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), { + "count": count + }); + } + else + { + if (passedLimit) + text = sprintf(translatePlural("Could be created merely once.", "Could be created merely %(limit)s times.", matchEntLimit), { + "limit": matchEntLimit + }); + else if (matchEntLimit == 1) + text = translate("Can be created only once."); + else + text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), { + "count": count + }); + } + + return passedLimit ? coloredText(text, "red") : text; +} + /** * Format batch training string for the tooltip * Examples: diff --git a/binaries/data/mods/public/simulation/components/BuildRestrictions.js b/binaries/data/mods/public/simulation/components/BuildRestrictions.js index a8cdd1c451..cf4eab0cfa 100644 --- a/binaries/data/mods/public/simulation/components/BuildRestrictions.js +++ b/binaries/data/mods/public/simulation/components/BuildRestrictions.js @@ -35,6 +35,11 @@ BuildRestrictions.prototype.Schema = "" + "" + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + diff --git a/binaries/data/mods/public/simulation/components/EntityLimits.js b/binaries/data/mods/public/simulation/components/EntityLimits.js index 82e8692764..8bc8da8cbd 100644 --- a/binaries/data/mods/public/simulation/components/EntityLimits.js +++ b/binaries/data/mods/public/simulation/components/EntityLimits.js @@ -72,6 +72,7 @@ EntityLimits.prototype.Init = function() this.removers = {}; this.classCount = {}; // counts entities with the given class, used in the limit removal this.removedLimit = {}; + this.matchTemplateCount = {}; for (var category in this.template.Limits) { this.limit[category] = +this.template.Limits[category]; @@ -103,6 +104,14 @@ EntityLimits.prototype.ChangeCount = function(category, value) this.count[category] += value; }; +EntityLimits.prototype.ChangeMatchCount = function(template, value) +{ + if (!this.matchTemplateCount[template]) + this.matchTemplateCount[template] = 0; + + this.matchTemplateCount[template] += value; +}; + EntityLimits.prototype.GetLimits = function() { return this.limit; @@ -113,6 +122,11 @@ EntityLimits.prototype.GetCounts = function() return this.count; }; +EntityLimits.prototype.GetMatchCounts = function() +{ + return this.matchTemplateCount; +}; + EntityLimits.prototype.GetLimitChangers = function() { return this.changers; @@ -145,40 +159,48 @@ EntityLimits.prototype.UpdateLimitRemoval = function() } }; -EntityLimits.prototype.AllowedToCreate = function(limitType, category, count) +EntityLimits.prototype.AllowedToCreate = function(limitType, category, count, templateName, matchLimit) { - // Allow unspecified categories and those with no limit - if (this.count[category] === undefined || this.limit[category] === undefined) - return true; - - if (this.count[category] + count > this.limit[category]) + if (this.count[category] !== undefined && this.limit[category] !== undefined && + this.count[category] + count > this.limit[category]) { - var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); - var notification = { - "players": [cmpPlayer.GetPlayerID()], - "translateMessage": true, - "translateParameters": ["category"], - "parameters": {"category": category, "limit": this.limit[category]}, - }; - - if (limitType == BUILD) - notification.message = markForTranslation("%(category)s build limit of %(limit)s reached"); - else if (limitType == TRAINING) - notification.message = markForTranslation("%(category)s training limit of %(limit)s reached"); - else - { - warn("EntityLimits.js: Unknown LimitType " + limitType); - notification.message = markForTranslation("%(category)s limit of %(limit)s reached"); - } - var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.PushNotification(notification); + this.NotifyLimit(limitType, category, this.limit[category]); + return false; + } + if (this.matchTemplateCount[templateName] !== undefined && matchLimit !== undefined && + this.matchTemplateCount[templateName] + count > matchLimit) + { + this.NotifyLimit(limitType, category, matchLimit); return false; } return true; }; +EntityLimits.prototype.NotifyLimit = function(limitType, category, limit) +{ + let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + let notification = { + "players": [cmpPlayer.GetPlayerID()], + "translateMessage": true, + "translateParameters": ["category"], + "parameters": { "category": category, "limit": limit }, + }; + + if (limitType == BUILD) + notification.message = markForTranslation("%(category)s build limit of %(limit)s reached"); + else if (limitType == TRAINING) + notification.message = markForTranslation("%(category)s training limit of %(limit)s reached"); + else + { + warn("EntityLimits.js: Unknown LimitType " + limitType); + notification.message = markForTranslation("%(category)s limit of %(limit)s reached"); + } + let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification(notification); +}; + EntityLimits.prototype.AllowedToBuild = function(category) { // We pass count 0 as the creation of the building has already taken place and @@ -186,9 +208,9 @@ EntityLimits.prototype.AllowedToBuild = function(category) return this.AllowedToCreate(BUILD, category, 0); }; -EntityLimits.prototype.AllowedToTrain = function(category, count) +EntityLimits.prototype.AllowedToTrain = function(category, count, templateName, matchLimit) { - return this.AllowedToCreate(TRAINING, category, count); + return this.AllowedToCreate(TRAINING, category, count, templateName, matchLimit); }; /** diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index 0bc4ada6d3..e315dadee7 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -119,6 +119,7 @@ GuiInterface.prototype.GetSimulationState = function() "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, + "matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null, diff --git a/binaries/data/mods/public/simulation/components/ProductionQueue.js b/binaries/data/mods/public/simulation/components/ProductionQueue.js index f9e05440c4..392c7d8e5a 100644 --- a/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -397,6 +397,8 @@ ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadat let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.ChangeCount(unitCategory, count); + if (template.TrainingRestrictions.MatchLimit) + cmpPlayerEntityLimits.ChangeMatchCount(templateName, count); } let buildTime = ApplyValueModificationsToTemplate( @@ -545,6 +547,8 @@ ProductionQueue.prototype.RemoveBatch = function(id) let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.count); + if (template.TrainingRestrictions.MatchLimit) + cmpPlayerEntityLimits.ChangeMatchCount(item.unitTemplate, -item.count); } } diff --git a/binaries/data/mods/public/simulation/components/TrainingRestrictions.js b/binaries/data/mods/public/simulation/components/TrainingRestrictions.js index 315d9c5081..129d284a43 100644 --- a/binaries/data/mods/public/simulation/components/TrainingRestrictions.js +++ b/binaries/data/mods/public/simulation/components/TrainingRestrictions.js @@ -9,7 +9,12 @@ TrainingRestrictions.prototype.Schema = "" + "" + "" + - ""; + "" + + "" + + "" + + "" + + "" + + ""; TrainingRestrictions.prototype.Init = function() { diff --git a/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js b/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js index c9543c824e..d2aac05774 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js +++ b/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js @@ -34,6 +34,7 @@ let cmpEntityLimits = ConstructComponent(10, "EntityLimits", template); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 }); TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimitChangers(), { "Tower": { "Monument": 1 } }); +TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetMatchCounts(), {}); // Test training restrictions TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero")); @@ -44,7 +45,7 @@ for (let ent = 60; ent < 63; ++ent) { AddMock(ent, IID_TrainingRestrictions, { "GetCategory": () => "Hero" -}); + }); } cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 60, "from": INVALID_PLAYER, "to": 1 }); 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 574097ae93..d964525aa1 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -124,7 +124,8 @@ AddMock(100, IID_Player, { AddMock(100, IID_EntityLimits, { "GetLimits": function() { return { "Foo": 10 }; }, "GetCounts": function() { return { "Foo": 5 }; }, - "GetLimitChangers": function() {return { "Foo": {} }; } + "GetLimitChangers": function() { return { "Foo": {} }; }, + "GetMatchCounts": function() { return { "Bar": 0 }; } }); AddMock(100, IID_TechnologyManager, { @@ -211,7 +212,8 @@ AddMock(101, IID_Player, { AddMock(101, IID_EntityLimits, { "GetLimits": function() { return { "Bar": 20 }; }, "GetCounts": function() { return { "Bar": 0 }; }, - "GetLimitChangers": function() {return { "Bar": {} }; } + "GetLimitChangers": function() { return { "Bar": {} }; }, + "GetMatchCounts": function() { return { "Foo": 0 }; } }); AddMock(101, IID_TechnologyManager, { @@ -299,6 +301,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, + "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), @@ -349,6 +352,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), { "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, + "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), @@ -409,6 +413,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "isEnemy": [true, true], "entityLimits": { "Foo": 10 }, "entityCounts": { "Foo": 5 }, + "matchEntityCounts": { "Bar": 0 }, "entityLimitChangers": { "Foo": {} }, "researchQueued": new Map(), "researchStarted": new Set(), @@ -482,6 +487,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), { "isEnemy": [false, false], "entityLimits": { "Bar": 20 }, "entityCounts": { "Bar": 0 }, + "matchEntityCounts": { "Foo": 0 }, "entityLimitChangers": { "Bar": {} }, "researchQueued": new Map(), "researchStarted": new Set(), diff --git a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js index 131b0961f6..652bac4b06 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -227,7 +227,8 @@ function regression_test_d1879() "Resources": {} }, "TrainingRestrictions": { - "Category": "some_limit" + "Category": "some_limit", + "MatchLimit": "7" } }) }); @@ -244,6 +245,7 @@ function regression_test_d1879() "UnBlockTraining": () => {}, "UnReservePopulationSlots": () => {}, "TrySubtractResources": () => true, + "AddResources": () => true, "TryReservePopulationSlots": () => false // Always have pop space. }); @@ -259,6 +261,8 @@ function regression_test_d1879() let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits); TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8)); TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9)); + TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8)); + TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8)); // Check that the entity limits do get updated if the spawn succeeds. AddMock(testEntity, IID_Footprint, { @@ -268,10 +272,12 @@ function regression_test_d1879() cmpProdQueue.AddBatch("some_template", "unit", 3); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); + TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); - Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout(); + cmpProdQueue.ProgressTimeout(); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); + TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); // Now check that it doesn't get updated when the spawn doesn't succeed. AddMock(testEntity, IID_Footprint, { @@ -283,10 +289,16 @@ function regression_test_d1879() }); cmpProdQueue.AddBatch("some_template", "unit", 3); - Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout(); + cmpProdQueue.ProgressTimeout(); TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6); + TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6); + + // Check that when the batch is removed the counts are subtracted again. + cmpProdQueue.RemoveBatch(cmpProdQueue.GetQueue()[0].id); + TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); + TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); } function test_batch_adding() diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 274d956331..73dc2222ed 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -296,7 +296,7 @@ var g_Commands = { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); - if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count)) + if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd));