From 862320d686bce24f2b1c0f103e374f05f98b5e7a Mon Sep 17 00:00:00 2001 From: guerringuerrin Date: Wed, 18 Mar 2026 19:05:12 -0300 Subject: [PATCH] Prioritize least busy buildings for hotkey training commands getTotalQueueTime(queue) calculates total remaining queue time of a production building. getBuildingsSortedByQueueTime(entities), sorts building selection by their queued training time, filtering invalid entities Removes the resource availability guard from the Training selection panel onPress handler to align mouse interaction with hotkey behavior, --- .../data/mods/public/gui/session/input.js | 56 ++++++++++++++++--- .../public/gui/session/selection_panels.js | 3 +- 2 files changed, 49 insertions(+), 10 deletions(-) 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); };