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 @@ -924,13 +924,30 @@ }, "leave": function() { + this.StopTimer(); this.StopMoving(); }, "MovementUpdate": function(msg) { + if (msg.veryObstructed && !this.timer) + { + // It's possible that the controller (with large clearance) + // is stuck, but not the individual units. + // Ask them to move individually for a little while. + this.CallMemberFunction("MoveTo", [this.order.data]); + this.StartTimer(2000); + return; + } + else if (this.timer) + return; if (msg.likelyFailure || this.CheckRange(this.order.data)) this.FinishOrder(); }, + + "Timer": function() { + // Reenter to reset the pathfinder state. + this.SetNextState("WALKING"); + } }, "WALKINGANDFIGHTING": { @@ -1207,8 +1224,8 @@ "MEMBER": { "OrderTargetRenamed": function(msg) { // In general, don't react - we don't want to send spurious messages to members. - // This looks odd for hunting hwoever because we wait for all - // entities to ahve clumped around the dead resource before proceeding + // This looks odd for hunting however because we wait for all + // entities to have clumped around the dead resource before proceeding // so explicitly handle this case. if (this.order && this.order.data && this.order.data.hunting && this.order.data.target == msg.data.newentity && @@ -1584,7 +1601,7 @@ "MovementUpdate": function(msg) { // If it looks like the path is failing, and we are close enough stop anyways. // This avoids pathing for an unreachable goal and reduces lag considerably. - if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || + if (msg.likelyFailure || msg.veryObstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || this.CheckRange(this.order.data)) this.FinishOrder(); }, @@ -4185,6 +4202,8 @@ UnitAI.prototype.OnMotionUpdate = function(msg) { + if (msg.veryObstructed) + msg.obstructed = true; this.UnitFsm.ProcessMessage(this, Object.assign({ "type": "MovementUpdate" }, msg)); }; Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -327,7 +327,8 @@ enum UpdateType { LIKELY_SUCCESS, // UnitMotion considers it is arrived at destination. LIKELY_FAILURE, // UnitMotion says it cannot reach the destination. - OBSTRUCTED, // UitMotion was obstructed. This does not mean stuck, but can be a hint to run range checks. + OBSTRUCTED, // UnitMotion was obstructed. This does not mean stuck, but can be a hint to run range checks. + VERY_OBSTRUCTED, // Sent when obstructed several time in a row. LENGTH }; Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -102,6 +102,12 @@ */ static const u8 MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH = 3; +/** + * After this many failed computations, start sending "VERY_OBSTRUCTED" messages instead. + * Should probably be larger than MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH. + */ +static const u8 VERY_OBSTRUCTED_THRESHOLD = 8; + static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); @@ -555,7 +561,7 @@ if (IsFormationMember() && IsFormationControllerMoving()) return; - CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED); + CMessageMotionUpdate msg(m_FailedPathComputations >= VERY_OBSTRUCTED_THRESHOLD ? CMessageMotionUpdate::VERY_OBSTRUCTED : CMessageMotionUpdate::OBSTRUCTED); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } @@ -1065,8 +1071,12 @@ if (!cmpPosition || !cmpPosition->IsInWorld()) return false; - if (m_FailedPathComputations >= 1) - // Inform other components - we might be ordered to stop, and computeGoal will then fail and return early. + // Always increment the failed path computations. This acts as a failsafe + // in case the pathing returns an apparently valid path that isn't actually working. + // This can in particular happen if the entity is stuck inside other unit obstruction shapes, + // a common case for the formation controller. + // Also inform other components of an obstruction on the 2nd failure in a row. + if (!IncrementFailedPathComputationAndMaybeNotify() && m_FailedPathComputations >= 2) MoveObstructed(); PathGoal goal; Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -265,7 +265,7 @@ //////////////////////////////// const std::array CMessageMotionUpdate::UpdateTypeStr = { { - "likelySuccess", "likelyFailure", "obstructed" + "likelySuccess", "likelyFailure", "obstructed", "veryObstructed" } }; JS::Value CMessageMotionUpdate::ToJSVal(const ScriptInterface& scriptInterface) const @@ -290,6 +290,8 @@ return new CMessageMotionUpdate(CMessageMotionUpdate::LIKELY_FAILURE); if (updateString == L"obstructed") return new CMessageMotionUpdate(CMessageMotionUpdate::OBSTRUCTED); + if (updateString == L"veryObstructed") + return new CMessageMotionUpdate(CMessageMotionUpdate::VERY_OBSTRUCTED); LOGWARNING("CMessageMotionUpdate::FromJSVal passed wrong updateString"); return NULL;