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));