From feda708d5d0d724da403f4e1defd34f65efebd95 Mon Sep 17 00:00:00 2001 From: wraitii Date: Sat, 6 Jun 2020 16:07:01 +0000 Subject: [PATCH] Fix formations not queueing orders properly. Formation controllers were not always waiting on their members correctly because the "finishedOrder" check was being reset in the wrong place. This happened particularly with queued orders. This merges the "finishedOrder" and the "InPosition" logic. Reported by: elexis Based on a patch by: Freagarach Tested by: Freagarach Reviewed By: Angen Fixes #3274 Differential Revision: https://code.wildfiregames.com/D2702 This was SVN commit r23744. --- .../public/simulation/components/Formation.js | 81 +++++++++---------- .../public/simulation/components/UnitAI.js | 59 ++++---------- .../components/tests/test_UnitAI.js | 8 +- 3 files changed, 58 insertions(+), 90 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/Formation.js b/binaries/data/mods/public/simulation/components/Formation.js index 6730fcebd7..0159ff9d4b 100644 --- a/binaries/data/mods/public/simulation/components/Formation.js +++ b/binaries/data/mods/public/simulation/components/Formation.js @@ -78,7 +78,7 @@ Formation.prototype.variablesToSerialize = [ "memberPositions", "maxRowsUsed", "maxColumnsUsed", - "inPosition", + "waitingOnController", "columnar", "rearrange", "formationMembersWithAura", @@ -135,7 +135,7 @@ Formation.prototype.Init = function(deserialized = false) this.memberPositions = {}; this.maxRowsUsed = 0; this.maxColumnsUsed = []; - this.inPosition = []; // entities that have reached their final position + this.waitingOnController = []; // entities that are waiting on the controller. this.columnar = false; // whether we're travelling in column (vs box) formation this.rearrange = true; // whether we should rearrange all formation members this.formationMembersWithAura = []; // Members with a formation aura @@ -281,10 +281,7 @@ Formation.prototype.GetFormationAnimationVariant = function(entity) return undefined; }; -/** - * Permits formation members to register that they've reached their destination. - */ -Formation.prototype.SetInPosition = function(ent) +Formation.prototype.SetWaitingOnController = function(ent) { // Rotate the entity to the right angle. let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); @@ -292,19 +289,26 @@ Formation.prototype.SetInPosition = function(ent) if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld()) cmpEntPosition.TurnTo(cmpPosition.GetRotation().y); - if (this.inPosition.indexOf(ent) != -1) + if (this.waitingOnController.indexOf(ent) !== -1) return; - this.inPosition.push(ent); + this.waitingOnController.push(ent); }; -/** - * Called by formation members upon entering non-walking states. - */ -Formation.prototype.UnsetInPosition = function(ent) +Formation.prototype.UnsetWaitingOnController = function(ent) { - var ind = this.inPosition.indexOf(ent); - if (ind != -1) - this.inPosition.splice(ind, 1); + let ind = this.waitingOnController.indexOf(ent); + if (ind !== -1) + this.waitingOnController.splice(ind, 1); +}; + +Formation.prototype.ResetWaitingEntities = function() +{ + this.waitingOnController = []; +}; + +Formation.prototype.AreAllMembersWaiting = function() +{ + return this.waitingOnController.length === this.members.length; }; /** @@ -355,8 +359,8 @@ Formation.prototype.SetMembers = function(ents) Formation.prototype.RemoveMembers = function(ents, renamed = false) { this.offsets = undefined; - this.members = this.members.filter(function(e) { return ents.indexOf(e) == -1; }); - this.inPosition = this.inPosition.filter(function(e) { return ents.indexOf(e) == -1; }); + this.members = this.members.filter(ent => ents.indexOf(ent) === -1); + this.waitingOnController = this.waitingOnController.filter(ent => ents.indexOf(ent) === -1); for (let ent of ents) { @@ -397,7 +401,6 @@ Formation.prototype.RemoveMembers = function(ents, renamed = false) Formation.prototype.AddMembers = function(ents) { this.offsets = undefined; - this.inPosition = []; for (let ent of this.formationMembersWithAura) { @@ -430,25 +433,6 @@ Formation.prototype.AddMembers = function(ents) this.MoveMembersIntoFormation(true, true, this.lastOrderVariant); }; -/** - * Called when the formation stops moving in order to detect - * units that have already reached their final positions. - */ -Formation.prototype.FindInPosition = function() -{ - for (var i = 0; i < this.members.length; ++i) - { - var cmpUnitMotion = Engine.QueryInterface(this.members[i], IID_UnitMotion); - if (!cmpUnitMotion.IsMoveRequested()) - { - // Verify that members are stopped in FORMATIONMEMBER.WALKING - var cmpUnitAI = Engine.QueryInterface(this.members[i], IID_UnitAI); - if (cmpUnitAI.IsWalking()) - this.SetInPosition(this.members[i]); - } - } -}; - /** * Remove all members and destroy the formation. */ @@ -468,7 +452,7 @@ Formation.prototype.Disband = function() this.members = []; - this.inPosition = []; + this.waitingOnController = []; this.formationMembersWithAura = []; this.offsets = undefined; @@ -549,15 +533,22 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, varia var xMin = 0; var yMin = 0; - for (var i = 0; i < this.offsets.length; ++i) + if (force) + // Reset waitingOnController as FormationWalk is called. + this.ResetWaitingEntities(); + + for (let i = 0; i < this.offsets.length; ++i) { - var offset = this.offsets[i]; + let offset = this.offsets[i]; - var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI); + let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI); if (!cmpUnitAI) + { + warn("Entities without UnitAI in formation are not supported."); continue; + } - var data = + let data = { "target": this.entity, "x": offset.x, @@ -978,9 +969,13 @@ Formation.prototype.OnGlobalOwnershipChanged = function(msg) Formation.prototype.OnGlobalEntityRenamed = function(msg) { - if (this.members.indexOf(msg.entity) == -1) + if (this.members.indexOf(msg.entity) === -1) return; + let waitingIndex = this.waitingOnController.indexOf(msg.entity); + if (waitingIndex !== -1) + this.waitingOnController.splice(waitingIndex, 1, msg.newentity); + // Save rearranging to temporarily set it to false. let temp = this.rearrange; this.rearrange = false; diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 63012a6feb..3bae86b88e 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -962,10 +962,7 @@ UnitAI.prototype.UnitFsmSpec = { "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data)) - { this.FinishOrder(); - this.CallMemberFunction("ResetFinishOrder", []); - } }, }, @@ -995,10 +992,7 @@ UnitAI.prototype.UnitFsmSpec = { "MovementUpdate": function(msg) { if (msg.likelyFailure || this.CheckRange(this.order.data)) - { this.FinishOrder(); - this.CallMemberFunction("ResetFinishOrder", []); - } }, }, @@ -1132,14 +1126,7 @@ UnitAI.prototype.UnitFsmSpec = { if (!msg.likelyFailure && !this.CheckRange(this.order.data)) return; - if (this.FinishOrder()) - { - this.CallMemberFunction("ResetFinishOrder", []); - return; - } - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - - cmpFormation.FindInPosition(); + this.FinishOrder(); } }, @@ -1239,11 +1226,10 @@ UnitAI.prototype.UnitFsmSpec = { "Timer": function(msg) { // Have all members finished the task? - if (!this.TestAllMemberFunction("HasFinishedOrder", [])) + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + if (cmpFormation && !cmpFormation.AreAllMembersWaiting()) return; - this.CallMemberFunction("ResetFinishOrder", []); - // Execute the next order if (this.FinishOrder()) { @@ -1267,9 +1253,6 @@ UnitAI.prototype.UnitFsmSpec = { // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { - // We're not in a formation anymore, so no need to track this. - this.finishedOrder = false; - // Stop moving as soon as the formation disbands this.StopMoving(); @@ -1377,10 +1360,6 @@ UnitAI.prototype.UnitFsmSpec = { if (this.FinishOrder()) return; - let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - cmpFormation.SetInPosition(this.entity); - delete this.formationOffset; }, }, @@ -1388,10 +1367,6 @@ UnitAI.prototype.UnitFsmSpec = { // Special case used by Order.LeaveFoundation "WALKINGTOPOINT": { "enter": function() { - var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); - if (cmpFormation) - cmpFormation.UnsetInPosition(this.entity); - if (!this.MoveTo(this.order.data)) { this.FinishOrder(); @@ -3377,7 +3352,6 @@ UnitAI.prototype.Init = function() this.isGarrisoned = false; this.isIdle = false; this.isImmobile = false; // True if the unit is currently unable to move (garrisoned,...) - this.finishedOrder = false; // used to find if all formation members finished the order this.heldPosition = undefined; @@ -3413,16 +3387,6 @@ UnitAI.prototype.IsFormationMember = function() return (this.formationController != INVALID_ENTITY); }; -UnitAI.prototype.HasFinishedOrder = function() -{ - return this.finishedOrder; -}; - -UnitAI.prototype.ResetFinishOrder = function() -{ - this.finishedOrder = false; -}; - UnitAI.prototype.IsAnimal = function() { return (this.template.NaturalBehaviour ? true : false); @@ -3785,7 +3749,8 @@ UnitAI.prototype.FinishOrder = function() if (cmpUnitAI) { // Inform the formation controller that we finished this task - this.finishedOrder = true; + let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); + cmpFormation.SetWaitingOnController(this.entity); // We don't want to carry out the default order // if there are still queued formation orders left if (cmpUnitAI.GetOrders().length > 1) @@ -6389,16 +6354,24 @@ UnitAI.prototype.AttackEntitiesByPreference = function(ents) }; /** - * Call obj.funcname(args) on UnitAI components of all formation members. + * Call UnitAI.funcname(args) on all formation members. + * @param resetWaitingEntities - If true, call ResetWaitingEntities first. + * If the controller wants to wait on its members to finish their order, + * this needs to be reset before sending new orders (in case they instafail) + * so it makes sense to do it here. + * Only set this to false if you're sure it's safe. */ -UnitAI.prototype.CallMemberFunction = function(funcname, args) +UnitAI.prototype.CallMemberFunction = function(funcname, args, resetWaitingEntities = true) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); if (!cmpFormation) return; + if (resetWaitingEntities) + cmpFormation.ResetWaitingEntities(); + cmpFormation.GetMembers().forEach(ent => { - var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI[funcname].apply(cmpUnitAI, args); }); }; diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js index 1774b5b15e..91f70025b0 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -394,16 +394,16 @@ function TestMoveIntoFormationWhileAttacking() controllerAI.Attack(enemy, []); - for (var ent of unitAIs) + for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerAI.MoveIntoFormation({"name": "Circle"}); // let all units be in position - for (var ent of unitAIs) - controllerFormation.SetInPosition(ent); + for (let ent of unitAIs) + controllerFormation.SetWaitingOnController(ent); - for (var ent of unitAIs) + for (let ent of unitAIs) TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); controllerFormation.Disband();