Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -1637,14 +1637,12 @@ } } -var lastIdleUnit = 0; -var currIdleClassIndex = 0; +var lastIdleUnit = INVALID_ENTITY; var lastIdleClasses = []; function resetIdleUnit() { - lastIdleUnit = 0; - currIdleClassIndex = 0; + lastIdleUnit = INVALID_ENTITY; lastIdleClasses = []; } @@ -1658,19 +1656,13 @@ resetIdleUnit(); lastIdleClasses = classes; - let data = { + let idleUnits = [].concat(...Engine.GuiInterfaceCall("GetIdleUnitsByClass", { "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) - { - data.limit = 1; - data.prevUnit = lastIdleUnit; - } - - let idleUnits = Engine.GuiInterfaceCall("FindIdleUnits", data); + "idleClasses": classes + })); + if(append) + idleUnits = idleUnits.filter(e => !g_Selection.selected.has(e)); + if (!idleUnits.length) { // TODO: display a message to indicate no more idle units, or something @@ -1683,19 +1675,19 @@ if (!append) g_Selection.reset(); - g_Selection.addList(idleUnits); - if (selectall) + if (selectall) { + g_Selection.addList(idleUnits); return; + } + + let idleUnit = idleUnits[(idleUnits.indexOf(lastIdleUnit) + 1) % idleUnits.length]; + g_Selection.addList([idleUnit]); + lastIdleUnit = idleUnit; - lastIdleUnit = idleUnits[0]; - let entityState = GetEntityState(lastIdleUnit); + let entityState = GetEntityState(idleUnit); if (entityState.position) Engine.CameraMoveTo(entityState.position.x, entityState.position.z); - - // Move the idle class index to the first class an idle unit was found for. - let indexChange = data.idleClasses.findIndex(elem => MatchesClassList(entityState.identity.classes, elem)); - currIdleClassIndex = (currIdleClassIndex + indexChange) % classes.length; } function clearSelection() Index: binaries/data/mods/public/gui/session/minimap/MiniMapIdleWorkerButton.js =================================================================== --- binaries/data/mods/public/gui/session/minimap/MiniMapIdleWorkerButton.js +++ binaries/data/mods/public/gui/session/minimap/MiniMapIdleWorkerButton.js @@ -24,11 +24,7 @@ rebuild() { - this.idleWorkerButton.enabled = Engine.GuiInterfaceCall("HasIdleUnits", { - "viewedPlayer": g_ViewedPlayer, - "idleClasses": this.idleClasses, - "excludeUnits": [] - }); + this.idleWorkerButton.enabled = Engine.GuiInterfaceCall("HasIdleUnits", {viewedPlayer: g_ViewedPlayer, idleClasses: this.idleClasses }); } onKeyDown() Index: binaries/data/mods/public/gui/summary/counters.js =================================================================== --- binaries/data/mods/public/gui/summary/counters.js +++ binaries/data/mods/public/gui/summary/counters.js @@ -334,6 +334,18 @@ return { "population": playerState.sequences.populationCount[index] }; } +function calculateIdleTime(playerState, index) +{ + print(JSON.stringify(Object.keys(playerState.sequences.idleTime))+"\n") + let idleTime = {}; + for(let cls of [ + "citizen", "femaleCitizen", "trader", "fishingBoat", "soldier" + ]){ + idleTime["idleTime_" + cls] = playerState.sequences.idleTime[cls][index]/1000; + } + return idleTime; +} + function calculateMapExploration(playerState, index) { return { "percent": playerState.sequences.percentMapExplored[index] }; @@ -360,7 +372,7 @@ if (type == "killDeath") return calculateRatio(g_TeamHelperData[team].enemyUnitsKilled[index], g_TeamHelperData[team].unitsLost[index]); - if (type == "bribes" || type == "population") + if (type == "bribes" || type == "population" || type == "idleTime") return summaryArraySum(getPlayerValuesPerTeam(team, index, type, counters, headings)); return { "percent": g_TeamHelperData[team][type][index] }; Index: binaries/data/mods/public/gui/summary/layout.js =================================================================== --- binaries/data/mods/public/gui/summary/layout.js +++ binaries/data/mods/public/gui/summary/layout.js @@ -194,6 +194,7 @@ { "identifier": "playername", "caption": translate("Player name"), "yStart": 26, "width": 200 }, { "identifier": "killDeath", "caption": translate("Kill / Death ratio"), "yStart": 16, "width": 100, "format": "DECIMAL2" }, { "identifier": "population", "caption": translate("Population"), "yStart": 16, "width": 100, "hideInSummary": true }, + { "identifier": "idleTime", "caption": translate("Idle time"), "yStart": 16, "width": 100, "hideInSummary": true, "format": "DURATION_SHORT"}, { "identifier": "mapControlPeak", "caption": translate("Map control (peak)"), "yStart": 16, "width": 100, "format": "PERCENTAGE" }, { "identifier": "mapControl", "caption": translate("Map control (finish)"), "yStart": 16, "width": 100, "format": "PERCENTAGE" }, { "identifier": "mapExploration", "caption": translate("Map exploration"), "yStart": 16, "width": 100, "format": "PERCENTAGE" }, @@ -215,6 +216,7 @@ "counters": [ { "width": 100, "fn": calculateKillDeathRatio, "verticalOffset": 12 }, { "width": 100, "fn": calculatePopulationCount, "verticalOffset": 12, "hideInSummary": true }, + { "width": 100, "fn": calculateIdleTime, "verticalOffset": 12, "hideInSummary": true }, { "width": 100, "fn": calculateMapPeakControl, "verticalOffset": 12 }, { "width": 100, "fn": calculateMapFinalControl, "verticalOffset": 12 }, { "width": 100, "fn": calculateMapExploration, "verticalOffset": 12 }, Index: binaries/data/mods/public/gui/summary/summary.js =================================================================== --- binaries/data/mods/public/gui/summary/summary.js +++ binaries/data/mods/public/gui/summary/summary.js @@ -94,6 +94,21 @@ "caption": translate("Received"), "postfix": "" }, + "idleTime_citizen": { + "caption": translate("Citizens") + }, + "idleTime_femaleCitizen": { + "caption": translate("Female citizens") + }, + "idleTime_soldier": { + "caption": translate("Soldiers") + }, + "idleTime_trader": { + "caption": translate("Trader") + }, + "idleTime_fishingBoat": { + "caption": translate("Fishing boat") + }, "population": { "color": g_TypeColors.red, "caption": translate("Population"), Index: binaries/data/mods/public/simulation/components/Garrisonable.js =================================================================== --- binaries/data/mods/public/simulation/components/Garrisonable.js +++ binaries/data/mods/public/simulation/components/Garrisonable.js @@ -100,6 +100,7 @@ cmpPosition.MoveOutOfWorld(); Engine.PostMessage(this.entity, MT_GarrisonedStateChanged, { + "entity": this.entity, "oldHolder": INVALID_ENTITY, "holderID": target }); @@ -143,6 +144,7 @@ } Engine.PostMessage(this.entity, MT_GarrisonedStateChanged, { + "entity": this.entity, "oldHolder": this.holder, "holderID": INVALID_ENTITY }); Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -1817,99 +1817,29 @@ }; /** - * Find any idle units. + * Get any 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. + * Returns an array. For each of the supplied classes this array will contain all units satisfying that class. + * A unit will only be present in the first matching class. */ -GuiInterface.prototype.FindIdleUnits = function(player, data) -{ - let idleUnits = []; - // 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(player)) - { - let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); - if (!filtered.idle) - continue; - - // 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. - let 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]; - } - - 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.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(player).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()) - return { "idle": false }; - - let cmpGarrisonable = Engine.QueryInterface(unit, IID_Garrisonable); - if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) - return { "idle": false }; - - const cmpTurretable = Engine.QueryInterface(unit, IID_Turretable); - if (cmpTurretable && cmpTurretable.IsTurreted()) - return { "idle": false }; - - let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); - if (!cmpIdentity) - return { "idle": false }; - - let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); - if (bucket == -1 || excludeUnits.indexOf(unit) > -1) - return { "idle": false }; - - return { "idle": true, "bucket": bucket }; -}; + GuiInterface.prototype.GetIdleUnitsByClass = function(player, data) + { + let cmpPlayer = QueryPlayerIDInterface(player); + return cmpPlayer ? cmpPlayer.GetIdleUnitsByClass(data.idleClasses) : data.idleClasses.map(() => []); + }; + + /** + * Returns if the current player has any idle units. + * + * @param data.idleClasses Array of class names to include. + */ + GuiInterface.prototype.HasIdleUnits = function(player, data) + { + let cmpPlayer = QueryPlayerIDInterface(player); + return cmpPlayer ? cmpPlayer.HasIdleUnits(data.idleClasses) : false; + }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { @@ -2111,7 +2041,7 @@ "GetFoundationSnapData": 1, "PlaySound": 1, "PlaySoundForPlayer": 1, - "FindIdleUnits": 1, + "GetIdleUnitsByClass": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -82,6 +82,7 @@ "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; + this.idleUnits = new Map(); // {ClassList: {"units": [Unit], "lastUpdate": 0, "idleTime": 0}; // Initial resources. let resCodes = Resources.GetCodes(); @@ -781,8 +782,11 @@ if (msg.from == this.playerID) { - if (cmpCost) + this.IdleUnitChanged(msg.entity, false); + + if (cmpCost) { this.popUsed -= cmpCost.GetPopCost(); + } let panelIndex = this.panelEntities.indexOf(msg.entity); if (panelIndex >= 0) @@ -794,6 +798,8 @@ } if (msg.to == this.playerID) { + this.IdleUnitChanged(msg.entity); + if (cmpCost) this.popUsed += cmpCost.GetPopCost(); @@ -809,6 +815,132 @@ } }; + +Player.prototype.IsOwnedIdleUnit = function(entity) { + let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() != this.GetPlayerID()) + return false; + + let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); + if (!cmpUnitAI || !cmpUnitAI.IsIdle()) + return false; + + let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable); + if (cmpGarrisonable && cmpGarrisonable.IsGarrisoned()) + return false; + + const cmpTurretable = Engine.QueryInterface(entity, IID_Turretable); + if (cmpTurretable && cmpTurretable.IsTurreted()) + return false; + + return true; +}; + +Player.prototype.GetIdleUnitsByClass = function(matchClasses) +{ + // Transform the string to an array + if (typeof matchClasses === "string") + matchClasses = matchClasses.split(/\s+/); + + let units = matchClasses.map(() => []); + + for(let unitType of this.idleUnits.values()) + { + for(let i = 0; i < matchClasses.length; ++i) + if(MatchesClassList(unitType.classes, [matchClasses[i]])) + { + units[i].push(...unitType.units); + break; + } + } + + return units; +}; + +Player.prototype.HasIdleUnits = function(matchClasses) +{ + for(let unitType of this.idleUnits.values()) + { + if(MatchesClassList(unitType.classes, matchClasses) && unitType.units.size > 0) + return true; + } + + return false; +}; + +function GetTime() { + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + return cmpTimer.GetTime(); +} + +function IdleUnitUpdate(data, time) { + data.idleTime += (time - data.lastUpdate)*data.units.size; + data.lastUpdate = time; +}; + + +Player.prototype.GetIdleTime = function(matchClasses) +{ + let time = GetTime(); + let idleTime = 0; + for(let unitType of this.idleUnits.values()) { + if(MatchesClassList(unitType.classes, matchClasses)) + { + IdleUnitUpdate(unitType, time); + idleTime += unitType.idleTime; + } + } + + return idleTime; +}; + +Player.prototype.IdleUnitChanged = function(entity) +{ + let time = GetTime(); + + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + if (!cmpIdentity) + return; + let classesList = cmpIdentity.GetClassesList(); + let key = classesList.join(" "); + let idleUnitData = this.idleUnits.get(key); + + if(this.IsOwnedIdleUnit(entity)) { + if(idleUnitData === undefined) + this.idleUnits.set(key, { + units: new Set([entity]), + lastUpdate: time, + classes: classesList, + idleTime: 0 + }); + else + { + IdleUnitUpdate(idleUnitData, time); + idleUnitData.units.add(entity); + } + } + else if(idleUnitData !== undefined && idleUnitData.units.has(entity)) + { + IdleUnitUpdate(idleUnitData, time); + idleUnitData.units.delete(entity); + } +}; + +Player.prototype.OnGlobalUnitIdleChanged = function(msg) +{ + this.IdleUnitChanged(msg.entity); +}; + +Player.prototype.OnGlobalGarrisonedStateChanged = function(msg) +{ + this.IdleUnitChanged(msg.entity); +}; + +Player.prototype.OnGlobalTurretedStateChanged= function(msg) +{ + this.IdleUnitChanged(msg.entity); +}; + Player.prototype.OnResearchFinished = function(msg) { if (msg.tech == this.template.SharedLosTech) Index: binaries/data/mods/public/simulation/components/StatisticsTracker.js =================================================================== --- binaries/data/mods/public/simulation/components/StatisticsTracker.js +++ binaries/data/mods/public/simulation/components/StatisticsTracker.js @@ -149,7 +149,8 @@ "peakPercentMapControlled": this.peakPercentMapControlled, "teamPeakPercentMapControlled": this.teamPeakPercentMapControlled, "successfulBribes": this.successfulBribes, - "failedBribes": this.failedBribes + "failedBribes": this.failedBribes, + "idleTime": this.GetIdleTime() }; }; @@ -522,6 +523,18 @@ this.teamPeakPercentMapControlled = Math.max(this.teamPeakPercentMapControlled, this.GetTeamPercentMapControlled()); }; +StatisticsTracker.prototype.GetIdleTime = function() +{ + let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + return { + citizen: cmpPlayer.GetIdleTime(["FemaleCitizen", "Citizen"]), + femaleCitizen: cmpPlayer.GetIdleTime(["FemaleCitizen"]), + trader: cmpPlayer.GetIdleTime(["Trader"]), + fishingBoat: cmpPlayer.GetIdleTime(["FishingBoat"]), + soldier: cmpPlayer.GetIdleTime(["Soldier"]), + }; +}; + /** * Adds the values of fromData to the end of the arrays of toData. * If toData misses the needed array, one will be created. Index: binaries/data/mods/public/simulation/components/Turretable.js =================================================================== --- binaries/data/mods/public/simulation/components/Turretable.js +++ binaries/data/mods/public/simulation/components/Turretable.js @@ -91,6 +91,7 @@ cmpObstruction.SetActive(false); Engine.PostMessage(this.entity, MT_TurretedStateChanged, { + "entity": this.entity, "oldHolder": INVALID_ENTITY, "holderID": target }); @@ -148,6 +149,7 @@ cmpObstruction.SetActive(true); Engine.PostMessage(this.entity, MT_TurretedStateChanged, { + "entity": this.entity, "oldHolder": this.holder, "holderID": INVALID_ENTITY }); Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -1592,7 +1592,7 @@ if (this.isIdle) { this.isIdle = false; - Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); + Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle, "entity": this.entity }); } }, @@ -1662,7 +1662,7 @@ } this.isIdle = true; - Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); + Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle, "entity": this.entity }); } // Go linger first to prevent all roaming entities @@ -3483,7 +3483,7 @@ if (this.isIdle == shouldBeIdle) return; this.isIdle = shouldBeIdle; - Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle }); + Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle, "entity": this.entity }); }; UnitAI.prototype.SetGarrisoned = function()