Jump to content

R&D Tactical bug hunts (suppression and alien skipping turn)


johnnylump

Recommended Posts

sure, mind-controlled aliens won't mourn dead players, while players* will get crazy if a mind controlled alien dies... eheh it's funny

*[sic] (I mean, player-controlled units, xcom soldiers... but yeah, I get crazy sometimes as well if the alien die and my units panick)

 

I gave it another look and I think I've found the way to solve this bug, but I haven't actualy tried to code it.

It starts at XGUnit.OnDeath, which calls GetSquad().OnMoraleEvent(2, self); if the dying unit isn't a robot. That is pretty tight but maybe using Amineri's method of deleting unnecessary XComGameReplicationInfo's it can be done just tweaking this small portion of code:

 

kIsRobotic = XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kGameCore.CharacterHasProperty(GetCharacter().m_kChar.iType, 3);
    // End:0x637
    if(((SurvivingUnit != none) && !kIsRobotic) && !IsAlien_CheckByCharType())
    {
        SurvivingUnit.UnitSpeak(36);
    }
    // End:0x669
    if(!kIsRobotic)
    {
        GetSquad().OnMoraleEvent(2, self);
    }
    // End:0x682
    if(m_bForceVisible)
    {
        m_bForceVisible = false;
    }
    XComGameReplicationInfo(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kCameraManager.RemoveMovingUnit(self);
    //return;

... and thus stopping the whole thing right from the beginning, preventing aliens to trigger morale events on death

Edited by anUser
Link to comment
Share on other sites

  • Replies 89
  • Created
  • Last Reply

Top Posters In This Topic

It looks to me like the function OnMoraleEvent is called to update the OTHER unit(s) when a morale event occurs. If this is so, it means that the

 

 

if(XComTacticalGRI(class'Engine'.static.GetCurrentWorldInfo().GRI).m_kBattle.AllObjects('XGBattle_SP') && (isHuman()) && (iMoraleEvent == 2 || (iMoraleEvent == 3)))
{
++ m_iFallenComrades;
UpdateUnitBuffs();
}
implemenation implies that the ++m_iFallenComrades incrementation is only APPLIED to Human units when morale event 2 or 3 occurs. Nothing here is preventing a mind-controlled alien from triggering the morale event that causes a human soldier's m_iFallenComradres counter to increment.
It does appear that the statement in XGUnit.OnDeath:
if(!kIsRobotic && (GetSquad().GetPermanentIndex(self) != -1))
{
GetSquad().OnMoraleEvent(2, self);
}
is intended to prevent robotic units and nonpermanent members of the squad from triggering the XGSquad-based OnMoraleEvent. However, clearly that is not working as intended.
An alternate to (GetSquad().GetPermanentIndex(self) != -1) would be (!IsPossessed())
IsPossessed() is a native function prototyped in XGUnitNativeBase, so is an inherited local member of XGUnit and can be called from XGUnit.OnDeath. Since the call is smaller than the one it's replacing, shouldn't be too much trouble to put in as a quick check.
Link to comment
Share on other sites

@ Amineri: there's 2 function named the same, one XGUnit.OnMoraleEvent, the other XGSquad.OnMoraleEvent.

 

Mind-controlled aliens don't get the penalty, ... but they DO trigger the fallen comrades penalty, that's the trick

 

When a unit dies: XGUnit.OnDeath checks if it isn't a robot and calls the morale event for the whole squad

if(!kIsRobotic) {GetSquad().OnMoraleEvent(2, self);}

 

hich is XGSquad.OnMoraleEvent ... there's no need to reach that far is death unit were an alien, but in any case, in that function there's the call to kUnit.OnMoraleEvent(EEvent, iNumPanicked >= iNumReadyForPanic) which is XGUnit.OnMoraleEvent. Then inside the XGUnit.OnMoraleEvent those checks are to prevent non permanent units and robots from suffering the effect.

Edited by anUser
Link to comment
Share on other sites

Edit: AnUser sent me some pre-patch code, and it's changed!

 

This:

 

&& GetSquad().GetPermanentIndex(self) != -1)

 

was added in the recent patch. Suggests the MC bug is fixed.

 

I'm going to update the original post with information on these bugs.

Edited by johnnylump
Link to comment
Share on other sites

Working on tracking down what is causing the AI to skip turns. I had to manually decompile (partially, anyhow) XGAIBehavior.DecideNextDestination. Even with the XCOM-specific table, the virtual sizes weren't right and so the jumps were showing all screwy.

 

There seem to be two primary AI modes:

1) Visually tracking enemy

2) Behavior 1, which tries to move toward the nearest visible enemy, or nearest enemy if none are visible

 

TCATT stands for Tracking Cue Audio Type. When your soldiers make noise (e.g. crashing through a door, breaking a window) it can trigger one of these, which can result in the alien (or alien pod) moving toward that location.

 

Specific behaviors are coded into the extensions of the base XGAIBehavior module.

 

Here is my result, for people to look at:

 

 

 

function Vector DecideNextDestination(optional out string strFail)
{
local XGUnit kEnemy;
local Vector vDestination, vCurrLoc, VDir;
local float DistSq;
local int iRand;
local bool bDone;
vCurrLoc = m_kUnit.GetLocation();
vDestination = vCurrLoc;
bDone = false;
if(m_kTrackingCue.bTracking)
{
if(m_kTrackingCue.bVisual)
{
if(CanSeeTrackingTarget())
{
if(m_kPlayer.HasUnitFired(m_iAIIndex))
{
DistSq = -1.0 - (class'XComEngine'.static.SyncFRand((string(Name) @ string(GetStateName())) @ string(GetFuncName())) * float(3));
if(!FindRunawayPosition(vDestination, m_kTrackingCue.kTarget, VDir, DistSq, strFail))
{
SwitchToAttack(strFail);
}
}
else
{
vDestination = FindFiringPosition(m_kTrackingCue.kTarget);
if(!IsValidPathDestination(vDestination))
{
strFail @= "UnitFired=0, FiringPos invalid.";
DistSq = -1.0 - (class'XComEngine'.static.SyncFRand((string(Name) @ string(GetStateName())) @ string(GetFuncName())) * float(3));
if(!FindRunawayPosition(vDestination, m_kTrackingCue.kTarget, VDir, DistSq, strFail))
{
SwitchToAttack(strFail);
}
}
}
}
else
{
if(m_kTrackingCue.eTCATType == 3)
{
vDestination = m_kPlayer.GetRandomValidPoint(m_kUnit, m_kTrackingCue.vLoc, 4.0 * float(64));
m_bMoveToActionPoint = false;
}
else
{
m_bMoveToActionPoint = false;
}
}
}
else
{
vDestination = m_kPlayer.GetRandomValidPoint(m_kUnit, m_kTrackingCue.vLoc, 3.0 * float(64));
m_bMoveToActionPoint = false;
}
}
else
{
if(m_eSpawnBehavior == 1)
{
kEnemy = GetNearestVisibleEnemy();
if(kEnemy == none)
{
kEnemy = m_kPlayer.GetNearestEnemy(m_kUnit.Location);
}
if(kEnemy != none)
{
if(m_kUnit.GetMoveDestTowardLocation(vDestination, kEnemy.GetLocation()))
{
bDone = true;
}
else
{
strFail @= "DND base, Hunt. m_kUnit.GetMoveDestToward fn failed.";
}
}
else
{
strFail @= "DND base, Hunt. No nearest enemy.";
}
if(!bDone)
{
iRand = class'XComEngine'.static.SyncRand(100, (string(Name) @ string(GetStateName())) @ string(GetFuncName()));
if(iRand < 60)
{
if(!FindRunawayPosition(vDestination, kEnemy, strFail))
{
SwitchToAttack(strFail);
}
}
else
{
strFail @= "PickedRandomValidPoint 300-1000 units.";
vDestination = m_kPlayer.GetRandomValidPoint(m_kUnit, vCurrLoc, 1000.0, 300.0);
m_bMoveToActionPoint = false;
}
}
}
else
{
if(m_eSpawnBehavior == 2)
{
strFail @= "DND base,SeekActor.";
vDestination = m_kPlayer.GetRandomValidPoint(m_kUnit, m_kSeekTarget.Location, m_fSeekRadius);
m_bMoveToActionPoint = false;
}
else
{
strFail @= "DND base,No behavior. Picking random location.";
vDestination = m_kPlayer.GetRandomValidPoint(m_kUnit, m_vLeashPoint, GetLeashRadius(true));
m_bMoveToActionPoint = false;
m_kPlayer.SnapToBorders(vDestination);
}
}
}
return vDestination;
//return ReturnValue, endofscript
}
Link to comment
Share on other sites

I think I've traced the root cause of the alien turn-skipping.

 

It is a combination of 'interesting' design (using a lot of discontinuities in the weighting functions) and a floating point round-off error.

 

First off, it seems very likely (from what I've looked at so far) that the new XCOM uses a deterministic AI, which always tries to figure out the "optimal" choice. This optimal choice is based upon a variety of heuristics, so it's not really optimal in any true sense. For comparison, the original X-COM used a stochastic AI, which computed relative choices and then applied fuzzy logic to randomize across the possible options. It sometimes chooses less optimal results, but has the benefit of being capable of surprising the player. It is also a lot harder to debug, which I assume is why it wasn't included.

 

----------------------------------------------

 

Ability Scoring

 

The game has a list of hard requirements and restrictions on abilities, based on a variety of conditions. Abilities that meet these are allowed to be 'scored'.

Scoring consists of computing an Offense, Defense, and Intangible value. What I am presenting here is a simplified version for a lone sectoid, in cover, subjected to overwatch and potentially flanking.

 

ShotStandard Scoring:

 

1) Offense - if the hit chance is 30% or less, and no other conditions apply, then Offense = hit chance. If hit chance is above 30%, it is doubled.

If all XCOM units are in high cover and at long range, then a sectoid's hit chance in Long War is 25%, so offense score = 25. In vanilla classic/impossible, the hit chance is 35%, so offense score = 70.

2) Defense - The defense score is 100 if flanking target, -100 if flanked by anyone, 75 if suppressed by target, 0 otherwise. This explains why it is so much easier to force the alien to skip its turn by flanking. It does not check to see if the flanking unit is overwatching -- just if it is being flanked.

3) Intangibles - Intangibles score, in this situation, is = critical chance. For basic non-mindmerged sectoids, this is 10.

The combined score = 25 -100 + 10 = -65. An ability is only eligible to be added to the "possible abilities" array if it's score is > 0, so the standard shot is disallowed -- because it is has low to-hit and it being flanked. This negative score has nothing to do with overwatch, however. Simply flanking the unit will prevent it from shooting.

The -100 penalty is in there (I think) to try and force the unit to move.

 

Move Action

 

If the unit is overwatched and there is no better cover nearby, the move action will fail as well. There is a condition in the move code:

if(!IsBetterLocation(vDestination, kCover))
{
OnMoveFailure();
SwitchToAttack("Destination is worse than initial location!");
return false;
}
that prevents moving to a location that is worse than the original, and instead switches the unit to the attack state. However, StandardShot is not an available action because its priority is negative.
Overwatch scoring:
This should leave overwatch as the ability of last resort.
For overwatch:
1) Offense always returns 0
2) Defense is 0 if the unit has a "strong attack", 10 if it is mindmerged or being overwatched (this is one place it comes in) and 30 otherwise. Overwatching the unit makes this 10.
3) Intangible always returns 0
This means the base priority for overwatch if the unit is being overwatched is 10.
Special Conditions on scoring:
However, there is another factor that comes into play. There is a score modifier multiplier on this value. This multipler takes a few things into account, but is always one of the set {1.0, 0.3, 0.2, 0.1}.
If no special conditions apply, it is 1.0.
If the unit is being overwatched, a special m_iOverwatchDangerZone value is computed. This value ranges from 1 to 3, depending on the number of enemies and their range from the unit. The game counts the number of "close" units, and "far" units that are in overwatch, and within sight range of the unit. What is "close" and "far" depends on the game difficulty, but is always a % of sight range. For easy it is 66%, normal 75%, and classic/impossible it is 90%. This means that for classic / impossible, nearly all overwatchers will count as being in "close" range.
If ANY overwatchers are in close range, then m_iOverwatchDangerZone is clamped to the max value of 3. Thus in classic/impossible, this value is nearly always maximum if any XCOM units are overwatching.
This sets the score multiplier to 0.30 / m_iOverwatchDangerZone. I present it like this because it is not EXACTLY 0.10. This number is not an "even" fractional part in base 2 binary. It is like 1/3 = 0.333333 ... there is a little bit of round-off error. This is what causes the issue. The value is just slightly less than 0.10.
Application of special condition to Overwatch:
Recall from above that the base overwatch priority (if the unit was being overwatched) was 10, an integer. To determine the final priority, it takes 10 * (0.30 / 3), and casts the result as an integer. I believe that due to round off error, this result is coming out as 0.
Since the priority is 0, it is not considered a valid ability. The function AddAbilityOption performs a hard check if(kAbilityData.iPriority > 0) before allowing the ability to be added.
This results in the following conditions:
1) The unit cannot shoot because the ShotStandard ability was not added because its priority was negative
2) The unit cannot overwatch because the Overwatch ability priority is 0 because of roundoff error
3) The unit cannot move because every possible place to move to is worse than its current location
The unit has no valid action, times out and skips its turn.
This is MORE likely to occur on classic/impossible because of the increased chances of m_iOverwatchDangerZone becoming 3. On easy you would have to have three units overwatching, or a unit with 18 tiles overwatching to make overwatch not possible.
This likely occurs less frequently in vanilla because of the 30% aim cutoff to the offense score. In vanilla hit chance is 35% against units in high cover, compared to 25% in the Long War mod. However, even with 35%, the ShotStandard priority can still be negative. Once hit % reaches over 45%, then 45*2 + 10 = 100, which balances out the -100 from being flanked. This will allow ShotStandard to be a valid ability even when being flanked.
---------------------------------------------------------------------------------
I can think of a few possible ways to remedy this.
1) Clamp the ShotStandard priority to be no less than 1. This leaves the basic shot as always being an option (assuming the unit can shoot)
2) Adjust the defense priority value on Overwatch to 11 instead of 10. This will ensure that Overwatch priority is always at least 1, so it will also be an option (if two actions have the same priority, the game does randomize which action to take).
3) Change the cutoff to the offense score to be less than 30%. This by itself will not make the ShotStandard an available option in many cases, however.
4) Change the flanking penalty against moving. More on this at the end.
There are probably a few others, but I think ensuring that Overwatch and ShotStandard always remain on the ability list (even if at the absolutely minimum possible priority) should prevent the AI from skipping aliens' turns).
The AddAbilityOption does allow for an optional bool bForcePriority, which sets the priority to 1000.0 prior to making the check. However, this is only invoked for what are called "Predetermined Abilities". For Sectoids, the only predetermined abilities are Move and Mind Merge. Move can fail as described, and a lone sectoid cannot mind merge.
I think the preferred option is to clamp abilities 8 (ShotStandard) and 33 (Overwatch) to be no less than priority 1.
I think there is room for this in the XGAIAbilityDM.AI_GetScore function:

 

function int AI_GetScore(XGAbility kAbility, out string strAbilities)
{
local float fOS, fDS, fIS;
local int iOS, Ids, iIS;

// End:0x11
if(kAbility == none)
{
return 0;
}
fOS = AI_OffenseScore(kAbility);
fDS = AI_DefenseScore(kAbility);
fIS = AI_IntangiblesScore(kAbility, fOS, fDS);
iOS = int(float(100) * fOS);
Ids = int(float(100) * fDS);
iIS = int(float(100) * fIS);
strAbilities = ((((((AI_GetDbgTxt(kAbility)) @ "Off=") $ string(iOS)) @ "Def=") $ string(Ids)) @ "Int=") $ string(iIS);
return int(float((iOS + Ids) + iIS) * (AI_GetScoreModifier(kAbility)));
//return ReturnValue;
}
Removing the debug text string will free up 73 bytes, and should allow for the conditional

 

if(kAbility.iType == 8 || kAbility.iType == 33)
{
return Max (int((iOS + Ids + iIS) * AI_GetScoreModifier(kAbility)), 1);
}
else
{
return int((iOS + Ids + iIS) * AI_GetScoreModifier(kAbility));
}

 

----------------------------------------------------

 

There is a further set of modifiers that are applied to possible move actions. These take into account flanking, nearness of friendly units (and particularly the unit's pod), as well as things such as range imbalances and overwatch.

 

If any enemies are in Overwatch, the m_iOverwatchDangerZone is computed as above (for classic/hard, this almost always means a value of 3).

 

If the DangerZone level == 1, then a BONUS to movement of +400 is applied. However, if the DangerZone level > 1, then a PENALTY is applied to movement. The penalty is -250 for level 2, and -500 for level 3. This -500 overwhelms nearly any other factor in determining whether a spot is "good" to move to.

 

It is not clear to me why there is a large bonus to move if there is only one overwatch, but an drastic penalty if over one. For classic / impossible, since the dangerzone level is nearly always 3, this means aliens will nearly never move if there are visible units in overwatch range.

 

((to have a lower level, the XCOM unit has to be in visible range but over 90% of visible range))

 

Altering this penalty could allow units to make moves even when overwatched, if the unit is flanked, but the flanking unit(s) are not overwatching.

Link to comment
Share on other sites

One other reason that this bug would crop up less in classic/impossible.

 

The offense priority for standard shots also gets a boost of +7 for each point of hp the target has below 5 (a hard-coded value of 5).

 

In vanilla impossible, starting soldiers have 3 HP, I believe, which would provide sectoids a +14 bonus to offense priority. With a 35% aim chance, 35*2 + 14 = 84, against an uninjured XCOM soldier. In Long War, soldiers have 3 base HP and +2 from Kevlar, making the HP 5, and providing zero bonus OFFENSE priority.


I think the AI could be made a little smarter if it used a value based upon the current weapon damage. This would allow the AI in later stages of the game to take into account chances to "one shot" a soldier with a powerful weapon such as a Heavy Plasma.

 

Currently, the AI would place no higher priority on a 7 HP soldier than a 20 HP soldier when armed with a weapon that dealt 9 damage.

Link to comment
Share on other sites

Please tell me that i'm not the only one to feel really stupid, like a caveman, after an Amineri post.

 

Edit: In my opinion being flanked should give the alien like +200 incentive to move to get the f out of there if it isn't already flanking enemy.

Edited by Yzaxtol
Link to comment
Share on other sites

Amazing discoveries, Amineri!!

 

As I suspected it seems the AI makes little use of team strategy, all this actions seem based on individual conditions. Anyway just fixing the skipt turn would be great, although it seems here we have the possibility to improve combat AI a bit. I won't comment that because I still haven't got a clear idea of how it works, I'll just say that I'd love if aliens could try to seek for cover when flanked, and that better overwatch than shooting as default action (provided the shot % had been considered low enough).

If the DangerZone level == 1, then a BONUS to movement of +400 is applied. However, if the DangerZone level > 1, then a PENALTY is applied to movement. The penalty is -250 for level 2, and -500 for level 3. This -500 overwhelms nearly any other factor in determining whether a spot is "good" to move to.

(...)

Altering this penalty could allow units to make moves even when overwatched, if the unit is flanked, but the flanking unit(s) are not overwatching.

Could it be possible swapping this values to make that if many xcom units are in overwatch the first alien taking action (and in range of xcom units) would move to trigger all reaction fire? I do that, but I'm smart and I use an assault soldier with lightning reflexes and using run and gun, but only the alien were to perform a dashing move (if very overwatched) that could turn into a major team tactic, and since each alien seems to compute their own actions, only the first alien overwatched by 2 units or more and with no good shot chance would take the initiative to unleash xcom reaction fire on him, so following aliens in that turn would no longer be 'overwatched', would they?

 

btw, aliens only take 2 moves per turn one by one right?, I mean, they don't take dash moves at all or do they?

 

another random thought.. do aliens consider enemy having the "Covering Fire" perk when pondering shooting them? I guess they don't but just to know

One other reason that this bug would crop up less in classic/impossible.

 

The offense priority for standard shots also gets a boost of +7 for each point of hp the target has below 5 (a hard-coded value of 5).

 

In vanilla impossible, starting soldiers have 3 HP, I believe, which would provide sectoids a +14 bonus to offense priority. With a 35% aim chance, 35*2 + 14 = 84, against an uninjured XCOM soldier. In Long War, soldiers have 3 base HP and +2 from Kevlar, making the HP 5, and providing zero bonus OFFENSE priority.


I think the AI could be made a little smarter if it used a value based upon the current weapon damage. This would allow the AI in later stages of the game to take into account chances to "one shot" a soldier with a powerful weapon such as a Heavy Plasma.

 

Currently, the AI would place no higher priority on a 7 HP soldier than a 20 HP soldier when armed with a weapon that dealt 9 damage.

All shall hunt the weak! Brutal... Well, again, I do take more risk if there's the chance to kill an alien, so it's good if they do it as well :)

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.

×
×
  • Create New...