diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index 2585b80c16..6e7347f6b3 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -1596,6 +1596,34 @@ function updateDefaultBatchSize() g_BatchSize = getDefaultBatchTrainingSize(); } +/** + * Calculates the total remaining production queue time of a building. + */ +function getTotalQueueTime(queue) +{ + return queue.reduce((sum, item) => sum + (item.timeRemaining ?? item.timeTotal ?? 0), 0); +} + +/** + * Returns the given production building selection sorted by their current + * queued training time, from lowest to highest. + * Invalid entities or entities without a valid production queue are filtered out. + */ +function getBuildingsSortedByQueueTime(entities) +{ + return entities.map(entity => + { + const state = GetEntityState(entity); + const queue = state?.production?.queue; + if (!Array.isArray(queue)) + return null; + return { + "entity": entity, + "totalQueueTime": getTotalQueueTime(queue) + }; + }).filter(data => data !== null).sort((a, b) => a.totalQueueTime - b.totalQueueTime).map(building => building.entity); +} + /** * Add the unit shown at position to the training queue for all entities in the selection. * @param {number} position - The position of the template to train. @@ -1628,6 +1656,12 @@ function addTrainingToQueue(selection, trainEntType, playerState) if (!decrement) template = GetTemplateData(trainEntType); + + // Sort buildings by queued training time so that, if some training + // commands fail due to resource or entity limits, units are assigned + // to the least busy buildings first. + + const sortedBuildings = getBuildingsSortedByQueueTime(appropriateBuildings); // Batch training only possible if we can train at least 2 units. if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { @@ -1638,8 +1672,8 @@ function addTrainingToQueue(selection, trainEntType, playerState) // If the order changed, we have a new selection and we should create a new batch. // If we're already creating a batch of this unit (in the same structure(s)), then just extend it // (if training limits allow). - if (g_BatchTrainingEntities.length == selection.length && - g_BatchTrainingEntities.every((ent, i) => ent == selection[i]) && + if (g_BatchTrainingEntities.length == sortedBuildings.length && + g_BatchTrainingEntities.every((ent, i) => ent == sortedBuildings[i]) && g_BatchTrainingType == trainEntType) { if (decrement) @@ -1649,7 +1683,7 @@ function addTrainingToQueue(selection, trainEntType, playerState) inputState = INPUT_NORMAL; } else if (canBeAddedCount == undefined || - canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * appropriateBuildings.length) + canBeAddedCount > g_NumberOfBatches * getBatchTrainingSize() * sortedBuildings.length) { if (Engine.GuiInterfaceCall("GetNeededResources", { "cost": multiplyEntityCosts(template, (g_NumberOfBatches + 1) * getBatchTrainingSize()) @@ -1671,14 +1705,14 @@ function addTrainingToQueue(selection, trainEntType, playerState) return; inputState = INPUT_BATCHTRAINING; - g_BatchTrainingEntities = selection; + g_BatchTrainingEntities = sortedBuildings; g_BatchTrainingType = trainEntType; g_BatchTrainingEntityAllowedCount = canBeAddedCount; g_NumberOfBatches = 1; } else { - let buildingsForTraining = appropriateBuildings; + let buildingsForTraining = sortedBuildings; if (canBeAddedCount !== undefined) buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); Engine.PostNetworkCommand({ @@ -1734,6 +1768,12 @@ function flushTrainingBatch() { const batchedSize = g_NumberOfBatches * getBatchTrainingSize(); const appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); + + // Sort buildings by queued training time so that, if some training + // commands fail due to resource or entity limits, units are assigned + // to the least busy buildings first. + + const sortedBuildings = getBuildingsSortedByQueueTime(appropriateBuildings); // If training limits don't allow us to train batchedSize in each appropriate structure. if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) @@ -1742,7 +1782,7 @@ function flushTrainingBatch() const buildingsCountToTrainFullBatch = Math.floor(g_BatchTrainingEntityAllowedCount / batchedSize); Engine.PostNetworkCommand({ "type": "train", - "entities": appropriateBuildings.slice(0, buildingsCountToTrainFullBatch), + "entities": sortedBuildings.slice(0, buildingsCountToTrainFullBatch), "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") @@ -1753,7 +1793,7 @@ function flushTrainingBatch() if (remainer) Engine.PostNetworkCommand({ "type": "train", - "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], + "entities": [sortedBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") @@ -1762,7 +1802,7 @@ function flushTrainingBatch() else Engine.PostNetworkCommand({ "type": "train", - "entities": appropriateBuildings, + "entities": sortedBuildings, "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js index 4ff9444444..d8ee139c9d 100644 --- a/binaries/data/mods/public/gui/session/selection_panels.js +++ b/binaries/data/mods/public/gui/session/selection_panels.js @@ -1035,8 +1035,7 @@ g_SelectionPanels.Training = { data.button.onPress = function() { - if (!neededResources) - addTrainingToQueue(unitIds, data.item, data.playerState); + addTrainingToQueue(unitIds, data.item, data.playerState); }; const showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };