diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json b/binaries/data/mods/public/gui/credits/texts/programming.json index 16de2e2b66..062e62e29d 100644 --- a/binaries/data/mods/public/gui/credits/texts/programming.json +++ b/binaries/data/mods/public/gui/credits/texts/programming.json @@ -39,6 +39,7 @@ {"nick": "Calvinh", "name": "Carl-Johan Höiby"}, {"name": "Cédric Houbart"}, {"nick": "Chakakhan", "name": "Kenny Long"}, + {"nick": "Clockwork-Muse", "name": "Stephen A. Imhoff"}, {"nick": "Cracker78", "name": "Chad Heim"}, {"nick": "Crynux", "name": "Stephen J. Fewer"}, {"nick": "cwprogger"}, diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index 9ef4ee6f16..9357ca02bc 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -1683,14 +1683,14 @@ function setCameraFollow(entity) } var lastIdleUnit = 0; -var currIdleClass = 0; -var lastIdleType = undefined; +var currIdleClassIndex = 0; +var lastIdleClasses = []; function resetIdleUnit() { lastIdleUnit = 0; - currIdleClass = 0; - lastIdleType = undefined; + currIdleClassIndex = 0; + lastIdleClasses = []; } function findIdleUnit(classes) @@ -1699,61 +1699,46 @@ function findIdleUnit(classes) var selectall = Engine.HotkeyIsPressed("selection.offscreen"); // Reset the last idle unit, etc., if the selection type has changed. - var type = classes.join(); - if (selectall || type != lastIdleType) + if (selectall || classes.length != lastIdleClasses.length || !classes.every((v,i) => v === lastIdleClasses[i])) resetIdleUnit(); - lastIdleType = type; + lastIdleClasses = classes; - // If selectall is true, there is no limit and it's necessary to iterate - // over all of the classes, resetting only when the first match is found. - var matched = false; - - for (var i = 0; i < classes.length; ++i) + var data = { + "viewedPlayer": g_ViewedPlayer, + "excludeUnits": append ? g_Selection.toList() : [], + // If the current idle class index is not 0, put the class at that index first. + "idleClasses": classes.slice(currIdleClassIndex, classes.length).concat(classes.slice(0, currIdleClassIndex)) + }; + if (!selectall) { - var data = { - "idleClass": classes[currIdleClass], - "prevUnit": lastIdleUnit, - "limit": 1, - "excludeUnits": [] - }; - - if (append) - data.excludeUnits = g_Selection.toList(); - - if (selectall) - data = { "idleClass": classes[currIdleClass] }; - - data.viewedPlayer = g_ViewedPlayer; - - // Check if we have new valid entity - var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); - if (idleUnits.length && idleUnits[0] != lastIdleUnit) - { - lastIdleUnit = idleUnits[0]; - if (!append && (!selectall || selectall && !matched)) - g_Selection.reset(); - - if (selectall) - g_Selection.addList(idleUnits); - else - { - g_Selection.addList([lastIdleUnit]); - var position = GetEntityState(lastIdleUnit).position; - if (position) - Engine.CameraMoveTo(position.x, position.z); - return; - } - - matched = true; - } - - lastIdleUnit = 0; - currIdleClass = (currIdleClass + 1) % classes.length; + data.limit = 1; + data.prevUnit = lastIdleUnit; } - // TODO: display a message or play a sound to indicate no more idle units, or something - // Reset for next cycle - resetIdleUnit(); + var idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); + if (!idleUnits.length) + { + // TODO: display a message or play a sound to indicate no more idle units, or something + // Reset for next cycle + resetIdleUnit(); + return; + } + + if (!append) + g_Selection.reset(); + g_Selection.addList(idleUnits); + + if (selectall) + return; + + lastIdleUnit = idleUnits[0]; + var entityState = GetEntityState(lastIdleUnit); + var position = entityState.position; + if (position) + Engine.CameraMoveTo(position.x, position.z); + // Move the idle class index to the first class an idle unit was found for. + var indexChange = data.idleClasses.findIndex(elem => hasClass(entityState, elem)); + currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function stopUnits(entities) diff --git a/binaries/data/mods/public/gui/session/session.js b/binaries/data/mods/public/gui/session/session.js index 8b23cb38c3..5d6cb18142 100644 --- a/binaries/data/mods/public/gui/session/session.js +++ b/binaries/data/mods/public/gui/session/session.js @@ -650,20 +650,11 @@ function changeGameSpeed(speed) function hasIdleWorker() { - for (let workerType of g_WorkerTypes) - { - let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", { + return Engine.GuiInterfaceCall("HasIdleUnits", { "viewedPlayer": g_ViewedPlayer, - "idleClass": workerType, - "prevUnit": undefined, - "limit": 1, + "idleClasses": g_WorkerTypes, "excludeUnits": [] - }); - - if (idleUnits.length > 0) - return true; - } - return false; + }); } function updateIdleWorkerButton() diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index e4b73009ed..db0bb85e55 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -1645,34 +1645,93 @@ GuiInterface.prototype.PlaySound = function(player, data) PlaySound(data.name, data.entity); }; +/** + * Find any idle units. + * + * @param data.viewedPlayer The player for which to find idle units. + * @param data.idleClasses Array of class names to include. + * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. + * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). + * @param data.excludeUnits Array of units to exclude. + * + * Returns an array of idle units. + * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. + */ GuiInterface.prototype.FindIdleUnits = function(player, data) { - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let playerEntities = cmpRangeManager.GetEntitiesByPlayer(data.viewedPlayer).filter(entity => { - - let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); - if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) - return false; - - let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); - if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass)) - return false; - - return true; - }); - let idleUnits = []; - - for (let ent of playerEntities) + // The general case is that only the 'first' idle unit is required; filtering would examine every unit. + // This loop imitates a grouping/aggregation on the first matching idle class. + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + for (let entity of cmpRangeManager.GetEntitiesByPlayer(data.viewedPlayer)) { - if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1) + let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); + if (!filtered.idle) continue; - idleUnits.push(ent); - if (data.limit && idleUnits.length >= data.limit) - break; + + // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. + // By adding to the 'end', there is no pause if the series of units loops. + var bucket = filtered.bucket; + if(bucket == 0 && data.prevUnit && entity <= data.prevUnit) + bucket = data.idleClasses.length; + + if (!idleUnits[bucket]) + idleUnits[bucket] = []; + idleUnits[bucket].push(entity); + + // If enough units have been collected in the first bucket, go ahead and return them. + if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) + return idleUnits[0]; } - return idleUnits; + let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); + if (data.limit && reduced.length > data.limit) + return reduced.slice(0, data.limit); + + return reduced; +}; + +/** + * Discover if the player has idle units. + * + * @param data.viewedPlayer The player for which to find idle units. + * @param data.idleClasses Array of class names to include. + * @param data.excludeUnits Array of units to exclude. + * + * Returns a boolean of whether the player has any idle units + */ +GuiInterface.prototype.HasIdleUnits = function(player, data) +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + return cmpRangeManager.GetEntitiesByPlayer(data.viewedPlayer).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); +}; + +/** + * Whether to filter an idle unit + * + * @param unit The unit to filter. + * @param idleclasses Array of class names to include. + * @param excludeUnits Array of units to exclude. + * + * Returns an object with the following fields: + * - idle - true if the unit is considered idle by the filter, false otherwise. + * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. + */ +GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) +{ + let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); + if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) + return { "idle": false }; + + let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); + if(!cmpIdentity) + return { "idle": false }; + + let bucket = idleClasses.findIndex(elem => cmpIdentity.HasClass(elem)); + if (bucket == -1 || excludeUnits.indexOf(unit) > -1) + return { "idle": false }; + + return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) @@ -1899,6 +1958,7 @@ let exposedFunctions = { "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, + "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanCapture": 1,