From 89359ce6d1b2791a0df3c2471e5b4d1fb8346ef2 Mon Sep 17 00:00:00 2001 From: guerringuerrin Date: Wed, 18 Mar 2026 19:05:12 -0300 Subject: [PATCH] Prioritize least busy buildings and enforce max training queue limit Use reusable helpers for training metadata extraction, queue time calculation and sorting. Introduced: getTotalQueueTime(queue) getBuildingTrainingMetadata(entities) sortBuildingsByQueueTime(buildings) getBuildingsSortedByQueueTime(entities) --- .../data/mods/public/gui/session/input.js | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index 2585b80c16..91642b5158 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -1617,17 +1617,17 @@ function addTrainingByPosition(position) } // Called by GUI when user clicks training button -function addTrainingToQueue(selection, trainEntType, playerState) +function addTrainingToQueue(unitIds, trainEntType, playerState) { - const appropriateBuildings = getBuildingsWhichCanTrainEntity(selection, trainEntType); - const canBeAddedCount = getEntityLimitAndCount(playerState, trainEntType).canBeAddedCount; const decrement = Engine.HotkeyIsPressed("selection.remove"); let template; if (!decrement) template = GetTemplateData(trainEntType); - + // Sort the buildings by their current queue length (time-based) before training + const sortedBuildingsMetadata = getBuildingsSortedByQueueTime(unitIds); + const sortedBuildings = sortedBuildingsMetadata.map(building => building.entity); // Batch training only possible if we can train at least 2 units. if (Engine.HotkeyIsPressed("session.batchtrain") && (canBeAddedCount == undefined || canBeAddedCount > 1)) { @@ -1638,8 +1638,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 +1649,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,16 +1671,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; - if (canBeAddedCount !== undefined) - buildingsForTraining = buildingsForTraining.slice(0, canBeAddedCount); + const buildingsForTraining = sortedBuildings.slice(0, canBeAddedCount || sortedBuildings.length); Engine.PostNetworkCommand({ "type": "train", "template": trainEntType, @@ -1733,40 +1731,79 @@ function getTrainingStatus(selection, trainEntType, playerState) function flushTrainingBatch() { const batchedSize = g_NumberOfBatches * getBatchTrainingSize(); + + // Get the buildings that can train the current batch type const appropriateBuildings = getBuildingsWhichCanTrainEntity(g_BatchTrainingEntities, g_BatchTrainingType); - // If training limits don't allow us to train batchedSize in each appropriate structure. + + // Sort buildings by queue length + const sortedBuildingsMetadata = getBuildingsSortedByQueueTime(appropriateBuildings); + const sortedBuildings = sortedBuildingsMetadata.map(building => building.entity); + + // If training limits don't allow us to train batchedSize in each appropriate structure if (g_BatchTrainingEntityAllowedCount !== undefined && g_BatchTrainingEntityAllowedCount < batchedSize * appropriateBuildings.length) { - // Train as many full batches as we can. + // Train as many full batches as we can 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") }); - // Train remainer in one more structure. + // Train remainder in one more structure const remainer = g_BatchTrainingEntityAllowedCount % batchedSize; if (remainer) Engine.PostNetworkCommand({ "type": "train", - "entities": [appropriateBuildings[buildingsCountToTrainFullBatch]], + "entities": [sortedBuildings[buildingsCountToTrainFullBatch]], "template": g_BatchTrainingType, "count": remainer, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); } else + { + // Train full batch in all selected buildings Engine.PostNetworkCommand({ "type": "train", - "entities": appropriateBuildings, + "entities": sortedBuildings, "template": g_BatchTrainingType, "count": batchedSize, "pushFront": Engine.HotkeyIsPressed("session.pushorderfront") }); + } +} + +function getTotalQueueTime(queue) +{ + return queue.reduce((sum, item) => sum + (item.timeRemaining ?? item.timeTotal ?? 0), 0); +} + +function getBuildingTrainingMetadata(entities) +{ + return entities.map(entity => + { + const queue = GetEntityState(entity)?.production?.queue || []; + + return { + "entity": entity, + "queueLength": queue.length, + "totalQueueTime": getTotalQueueTime(queue) + }; + }); +} + +function sortBuildingsByQueueTime(buildings) +{ + return buildings.sort((a, b) => a.totalQueueTime - b.totalQueueTime); +} + +function getBuildingsSortedByQueueTime(entities) +{ + return sortBuildingsByQueueTime(getBuildingTrainingMetadata(entities)); } function performGroup(action, groupId)