Logo Platform
logo amplifiers simplified

Some coding bugs

Reply
Copied to clipboard!
7 years ago
Oct 16, 2018, 1:57:22 PM

Dear amplitude dev(s) who read this,


In my misadventures as the developer of the Endless Legend community patch, I stumbled over a bunch of relatively easy to fix (and sometimes quite impactful) bugs. I decided its maybe a good idea to share some of them, so everyone can enjoy these fixes.


XML:

File: AI/AIParameters[DiplomaticTerm_Military].xml

<AIParameter Name="DiplomacyMilitarySupportReceiver" Value="1.25 * (0.1 + $(WarTermAgentCriticity)) * (100 max $Link(Provider|Property|MilitaryPower)) / (100 max $Link(ThirdParty|Property|MilitaryPower))"/>

This red marked snipped leads to counterintuitive behavior: The AI assigns more value the stronger itself is in relation to the third empire. Offering the AI to attack a weak neighbour for them easily can net you a few techs this way. The correct formular would be:

<AIParameter Name="DiplomacyMilitarySupportReceiver" Value="1.25 * (0.1 + $(WarTermAgentCriticity)) * (100 max $Link(ThirdParty|Property|MilitaryPower)) / (100 max $Link(Provider|Property|MilitaryPower))"/>



File: AI\Personalities[GameDifficulty].xml

<MarketBanNullificationTermAgent>
    <Multiplier>1.1</Multiplier>
</MarketBanNullificationTermAgent>

The above is an example, for how the AI should be more likely to use market ban nullification on hard difficulty. However the class MarketBanNullificationTermAgent is different than other DiplomaticTermAgents in that it has no "multiplier" property, so the value just does nothing. The one property it does have is BonusPerMarketplaceTechnology, so you can probably just increase that instead:

<MarketBanNullificationTermAgent>
    <BonusPerMarketplaceTechnology>1.1</BonusPerMarketplaceTechnology>
</MarketBanNullificationTermAgent>

This obviously is wrong for all difficulty levels.




DLL:

Class: NavyBehavior_Blitz.cs

protected override BehaviorNode<BaseNavyArmy> InitializeRoot()
{

(...)
Condition<BaseNavyArmy> condition2 = new Condition<BaseNavyArmy>(new Func<BaseNavyArmy, bool>(base.IsMainTargetUnderBombardment));
(...)
OrderAction<BaseNavyArmy> orderAction = new OrderAction<BaseNavyArmy>(new Func<BaseNavyArmy, Amplitude.Unity.Game.Orders.Order>(base.Attack));

(...)
Selector<BaseNavyArmy> selector = new Selector<BaseNavyArmy>(new BehaviorNode<BaseNavyArmy>[]
{

condition2,
orderAction

});
(...)

}

As far as I can see, Blitz is meant to be used for ships that should bombard coastal cities. It even does check if it does so with "condition2", but instead issues an attack order if the check fails. Since ships cant attacks cities, they just idle infront of enemy cities in that case. Replacing "base.Attack" with "base.Bombard" fixes the issue.




Class: AILayer_Diplomacy.cs

private bool TryGenerateAskToDiplomaticTerm(Empire alliedEmpire, Empire commonEnemy, StaticString proposalTermName, out DiplomaticTermProposal proposal)
{

(...)

proposal = new DiplomaticTermProposal(diplomaticTermProposalDefinition, this.empire, this.empire, alliedEmpire);

(...)

These parameters have to switch places. If the AI wants to ask alliedEmpire if it declares war on someone, alliedEmpire is the provider, and this.empire is the receiver. Currently this function always returns false.  Correct code:

private bool TryGenerateAskToDiplomaticTerm(Empire alliedEmpire, Empire commonEnemy, StaticString proposalTermName, out DiplomaticTermProposal proposal)
{

(...)

proposal = new DiplomaticTermProposal(diplomaticTermProposalDefinition, this.empire, alliedEmpirethis.empire);

(...)



That's all for now, but there is more where that came from ;)

Updated 7 years ago.
0Send private message
0Send private message
7 years ago
Oct 16, 2018, 9:39:13 PM

I'll be watching these closely.

The description of the consequences of these mistakes is a very nice touch, some times these seemingly easy fixes get ignored for their danger factor (how likely the fix is to cause ripple effects in other areas of the game that might not be intuitive to foresee) vs the benefit of fixing it.

0Send private message
6 years ago
Dec 4, 2018, 12:18:03 AM

Dear Amplitude/NGD dev(s),


Missed me? ;) I have some new presents!


XML:

Files: AI\ersonalities[AffinityReplicants].xml, AI\Personalities[AffinitySeaDemons].xml, AI\Personalities[GameDifficulty].xml


In these three files the Attitudemodifiers of <AILayer_Attitude> are in the wrong path (in MajorEmpire instead of MajorEmpire/AIEntity_Empire) which means the game uses the default modifiers instead (which is especially problematic for difficulty settings). You can check Registry.xml or other personality files in the folder to see how its done.




File: AI\AIArmyMissionDefinition[SubTree].xml

<AIArmyMissionDefinition Name="TryToAttackEnemyInRangeOrFlee">

        (...)

                <Controller_Sequence>

                    <!-- Check if we ca defeat this target. -->

                    <Decorator_CanDefeatTarget Inverted="false" TargetVarName="$Target"/>

                    <Decorator_GetTargetPosition TargetVarName="$Target" Output_DestinationVarName="$AttackWorldPositionDestination"/>

                    <Include SubTreeName="OptimizeArmyAttackPosition"/>


                    <Controller_Selector Debug="TryToAttackEnemyInRange_EnemyInRange">

                        (...)                        

                    </Controller_Selector>

                </Controller_Sequence>


          (...)

    </AIArmyMissionDefinition>

The Behavior-Subtree "TryToAttackEnemyInRangeOrFlee" is a bit broken and instructs the unit to flee most of the time. The culprit is <Include SubTreeName="OptimizeArmyAttackPosition"/> which returns false if the unit is not next to the targeted unit which in turn lets the sequence fail. It needs to be put in the following selector (like it is done everywhere else).


<AIArmyMissionDefinition Name="TryToAttackEnemyInRangeOrFlee">

        (...)

                <Controller_Sequence>

                    <!-- Check if we ca defeat this target. -->

                    <Decorator_CanDefeatTarget Inverted="false" TargetVarName="$Target"/>

                    <Decorator_GetTargetPosition TargetVarName="$Target" Output_DestinationVarName="$AttackWorldPositionDestination"/>


                    <Controller_Selector Debug="TryToAttackEnemyInRange_EnemyInRange">

                        <Include SubTreeName="OptimizeArmyAttackPosition"/>

                        (...)                        

                    </Controller_Selector>

                </Controller_Sequence>


          (...)

    </AIArmyMissionDefinition>



DLL:

Class: AIBehaviorTreeNode_Action_ImmolateUnits.cs

protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)
{

      (...)

            IDownloadableContentService service = Services.GetService<IDownloadableContentService>();

            if (!service.IsShared(DownloadableContent13.ReadOnlyName))

            {

                return State.Success;

            }


            (...)


            if (this.EvaluateImmolationNeed(army, garrison))

            {

                GameEntityGUID[] immolatingUnitGuids = null;

                this.SelectImmolableUnits(army, out immolatingUnitGuids);

                OrderImmolateUnits order = new OrderImmolateUnits(army.Empire.Index, immolatingUnitGuids);

                this.orderExecuted = false;

                aiBehaviorTree.AICommander.Empire.PlayerControllers.AI.PostOrder(order, out this.orderTicket, new EventHandler<TicketRaisedEventArgs>(this.Order_TicketRaised));

            }

            return State.Running;

}

This one is pretty bad. AIBehaviorTreeNode_Action_ImmolateUnits.cs ist the BT-Action-Class for the new "Soul Burn"-Ability of the Broken Lords. There are several issues here: First of all the it checks for the wrong DLC, meaning the Action wont be used when an inferno owner doesnt have the "Shifters"-DLC. Furthermore it always returns "Running" even when EvaluateImmolationNeed is false or worse, even when the immolatingUnitGuids-Array is empty. This leads to Armies often standing around during Eclipse because they want to attack something and try to use "Soul Burn", which they dont have, because they aren't even Broken Lords.


protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)
{

      (...)

            IDownloadableContentService service = Services.GetService<IDownloadableContentService>();

            if (!service.IsShared(DownloadableContent19.ReadOnlyName))

            {

                return State.Success;

            }


            (...)


            if (this.EvaluateImmolationNeed(army, garrison))

            {

                GameEntityGUID[] immolatingUnitGuids = null;

                this.SelectImmolableUnits(army, out immolatingUnitGuids);

                if (array.Length < 1)

                {

                    return State.Success;

                }

                OrderImmolateUnits order = new OrderImmolateUnits(army.Empire.Index, immolatingUnitGuids);

                this.orderExecuted = false;

                aiBehaviorTree.AICommander.Empire.PlayerControllers.AI.PostOrder(order, out this.orderTicket, new EventHandler<TicketRaisedEventArgs>(this.Order_TicketRaised))

                return State.Running;     

           }

            return State.Success;

}



Class: SeasonManager.cs

    public int GetExactInitialSummerStartTurn()

    {

        if (this.seasons != null)

        {

            for (int i = 0; i < this.seasons.Count; i++)

            {

                Season season = this.seasons[i];

                Season followingSeason = this.GetFollowingSeason(season, true);

                if (season.SeasonDefinition.SeasonType == Season.ReadOnlySummer && followingSeason != null && followingSeason.SeasonDefinition.SeasonType == Season.ReadOnlyHeatWave)

                {

                    return season.StartTurn;

                }

            }

        }

        return -1;

    }

This new function is intended to calculate the start of the Summer (since summer is split into three parts since inferno, a Summer-Start, an optional Eclipse, and a Summer continuation). However for some reasons it doesnt check for the existence of Eclipses/Inferno at all, which can lead to the function always returning -1. The returnvalue is assigned to the empire-property "NumberOfTurnsSinceSummerStart" that is used for calculating the orb cost of Altar-Prayers. This means: if eclipses are disabled or the player doesnt have inferno, prayer cost wont reset on summer start but will increase in cost indefinitely.


    public int GetExactInitialSummerStartTurn()

    {

        if (this.seasons != null)

        {

            for (int i = 0; i < this.seasons.Count; i++)

            {

                Season season = this.seasons[i];

                Season followingSeason = this.GetFollowingSeason(season, true);

                if (this.playWithMadSeason && this.downloadableContentService.IsShared(DownloadableContent19.ReadOnlyName))

                {

                    if (season.SeasonDefinition.SeasonType == Season.ReadOnlySummer && followingSeason != null && followingSeason.SeasonDefinition.SeasonType == Season.ReadOnlyHeatWave)

                    {

                        return season.StartTurn;

                    }

                }

                else if (season.SeasonDefinition.SeasonType == Season.ReadOnlySummer && followingSeason != null && followingSeason.SeasonDefinition.SeasonType == Season.ReadOnlyWinter)

                {

                    return season.StartTurn;

                }

            }

        }

        return -1;

    }



Class: PanelFeatureOrbCost.cs

    protected override IEnumerator OnShow(params object[] parameters)

    {

            (...)

            float orbCostFromPastWinters = simulationObject.GetPropertyValue("PrayerCostByPastWinter") * simulationObject.GetPropertyValue("NumberOfPastWinters");

            float orbCostFromTurns = simulationObject.GetPropertyValue("PrayerCostByTurnsSinceSeasonStart") * (1f + simulationObject.GetPropertyValue("NumberOfTurnsSinceSeasonStart"));

            (...)

    }

Speaking of Orb-Costs for prayers. The two float values in this example are used to display how the prayer cost is calculated in the tooltip. However the values are calculated in a way that no longer fits with the actualy PrayerOrbCost-Formula (in Public\Registry.xml). The tooltip therefore displays wrong, nonsensical values. 


    protected override IEnumerator OnShow(params object[] parameters)

    {

            (...)

            float orbCostFromPastWinters = simulationObject.GetPropertyValue("NumberOfPastWinters");

            float orbCostFromTurns = simulationObject.GetPropertyValue("PrayerCostByTurnsSinceSeasonStart") * simulationObject.GetPropertyValue("NumberOfTurnsSinceSummerStart");

            (...)

    }



That's all for now! It would be nice to know, if another patch is even planned. If not I can stop posting these and just fix everything for ELCP ;).

0Send private message
6 years ago
Jan 16, 2019, 8:57:07 PM

Amazing, thanks for the report!

0Send private message
0Send private message
6 years ago
Jan 20, 2019, 7:59:39 AM

Dear Amplitude/NGD dev(s),


As a thank you for the upcoming Surprise-DLC, I gift you a new load o' bugs ;).


XML:



File: Simulation\PathfindingRules.xml

<!-- Rule on the empire district tiles. -->

<PathfindingRule Name="DistrictTile" DefaultCost="0.5" OverrideTerrainCost="true">

</PathfindingRule>

There is an issue with the Pathfinding of Ships (Seafaring Units), namely that the pathfinding for some reason considers land based districts as valid tiles to move through. At first glance this has no negative consequences ingame. Sure the generated path is wrong, but when giving the order, the ship correctly "recognizes" the impossibility to move through land and stops dead in its tracks. Human players can just manually steer the ship around obstacles such as depicted in the screenshot above. However AIs fully rely on the paths the game generates them. This can therefore lead to ships getting unnecessarily trapped in spots where they could easily get out (the blue player in the example above is an AI player, that ship army is sitting there for 10 turns, spamming moveorders and clogging up the log-file. Luckily the fix is rather simple:


<!-- Rule on the empire district tiles. -->

<PathfindingRule Name="DistrictTile" DefaultCost="0.5" OverrideTerrainCost="true">

    <RuleOverride MovementCapacity="Ground" Cost="0.5" />

    <RuleOverride MovementCapacity="Air"    Cost="0.5" />

    <RuleOverride MovementCapacity="Water" Cost="Infinity"/>

</PathfindingRule>

The overrides for Ground and Air are added to prevent weird pathing issues for landbased Armies (trust me, I tried!), the last rule override ofcourse disallows ships from generating paths through districts, which in turn frees the poor trapped Army. The only side effect is that the movement cost for Cargo docks is now displayed as "Impassable" in the hover-over-tooltip for ships, however seafaring units can still path through them just fine.





File: Quests\QuestDefinitions[Vaulters].xml

<QuestDefinition Name="MainQuestVaulters-Chapter2Alt" Category="MainQuest" SubCategory="Main" Cooldown="0" SkipLockedQuestTarget="false" NumberOfConcurrentInstances="0" NumberOfOccurencesPerEmpire="1" NumberOfOccurencesPerGame="0">

      (...)

      <!--Step 3-->

      <Action_UpdateStep StepName="MainQuestVaulters-Chapter2Alt-Step3" State="InProgress" />

      <Action_SpawnArmy SpawnLocationVarName="$LocationsToSpawnArmyID1" ArmyDroplist="DroplistArmyDefinitionMainQuestVaultersChapter2AltForbiddenSpawnLocationVarName="$ForbiddenSpawnLocation" Output_EnemyArmyGUIDVarName="$ArmyID1"/>

      <Action_UpdateArmyObjective ArmyGUIDVarName="$ArmyID1" BehaviourName="Roaming" TargetEmpireVarName="$InstigatorEmpire"/>

      <Action_SpawnArmy SpawnLocationVarName="$LocationsToSpawnArmyID2" ArmyDroplist="DroplistArmyDefinitionMainQuestVaultersChapter2Alt" ForbiddenSpawnLocationVarName="$ForbiddenSpawnLocation" Output_EnemyArmyGUIDVarName="$ArmyID2"/>

      <Action_UpdateArmyObjective ArmyGUIDVarName="$ArmyID2" BehaviourName="Roaming" TargetEmpireVarName="$InstigatorEmpire"/>

      <Controller_Parallel CompletionPolicy="All">

      (...)

</QuestDefinition>

This one has quite a history. Bug reports concerning missing armies in Chapter 2 of the Vaulter/Mezari questline are found aplenty. Well I discovered the reason I think. The issue only occurs when the alternative 3-step branch of the second chapter is triggered, which can happen if the player has three strategic extractors by the time chapter 2 starts. The spawned army is selected from an ArmyDroplist called "DroplistArmyDefinitionMainQuestVaultersChapter2Alt". Problem is, that Droplist doesnt exist. Using the normal Chapter2-Droplist fixes the issue.


<QuestDefinition Name="MainQuestVaulters-Chapter2Alt" Category="MainQuest" SubCategory="Main" Cooldown="0" SkipLockedQuestTarget="false" NumberOfConcurrentInstances="0" NumberOfOccurencesPerEmpire="1" NumberOfOccurencesPerGame="0">

      (...)

      <!--Step 3-->

      <Action_UpdateStep StepName="MainQuestVaulters-Chapter2Alt-Step3" State="InProgress" />

      <Action_SpawnArmy SpawnLocationVarName="$LocationsToSpawnArmyID1" ArmyDroplist="DroplistArmyDefinitionMainQuestVaultersChapter2ForbiddenSpawnLocationVarName="$ForbiddenSpawnLocation" Output_EnemyArmyGUIDVarName="$ArmyID1"/>

      <Action_UpdateArmyObjective ArmyGUIDVarName="$ArmyID1" BehaviourName="Roaming" TargetEmpireVarName="$InstigatorEmpire"/>

      <Action_SpawnArmy SpawnLocationVarName="$LocationsToSpawnArmyID2" ArmyDroplist="DroplistArmyDefinitionMainQuestVaultersChapter2" ForbiddenSpawnLocationVarName="$ForbiddenSpawnLocation" Output_EnemyArmyGUIDVarName="$ArmyID2"/>

      <Action_UpdateArmyObjective ArmyGUIDVarName="$ArmyID2" BehaviourName="Roaming" TargetEmpireVarName="$InstigatorEmpire"/>

      <Controller_Parallel CompletionPolicy="All">

      (...)

</QuestDefinition>



DLL:


Class: AIBehaviorTreeNode_Decorator_SelectMapBoostSpawnTarget.cs

protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)

{

    (...)


    MapBoostSpawnInfo mapBoostSpawnInfo = null;

    float num = 0f;

    foreach (MapBoost mapBoost in this.mapBoostService.MapBoosts.Values)

    {

        float num2 = 1f;

        if (!this.ArmyHasBoost(mapBoost.MapBoostDefinition, army))

        {

              (...)

        }

    }


   (...)

}

This class is responsible for finding mapboosts (i.e. Confluxes during Eclipse) and the AI using them. There are two problems, a small one, and a rather big one. 

The small one: The Behaviour-subtree that uses this class (called: "MapBoostOpportunity") is expecting a XmlAttribute-Property with the name "OpportunityMaximumTurnName" to assign its customizable "$OpportunityMaximumTurn"-value  (just like it is done in AIBehaviorTreeNode_Decorator_SelectOrbSpawnTarget.cs). However that property is nowhere to be found in the class, so I added it and some necessary checks. Its not a big deal, because the relevant value will just default to -1, which means unlimited turns.

The big one: Mapboosts remain active internally even after they have been collected. This can lead to armies trying to collect a mapboost that already has been collected. They will then lock up on that position for the remainder of the eclipse because they think, it's still there. Luckily, checking whether a mapboost has been collected is very easy.


[XmlAttribute]

public string OpportunityMaximumTurnName { get; set; }


protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)

{

    (...)

     if (!string.IsNullOrEmpty(this.OpportunityMaximumTurnName) && aiBehaviorTree.Variables.ContainsKey(this.OpportunityMaximumTurnName))

     {

         this.OpportunityMaximumTurn = (float)aiBehaviorTree.Variables[this.OpportunityMaximumTurnName];

     }

    MapBoostSpawnInfo mapBoostSpawnInfo = null;

    float num = 0f;

    foreach (MapBoost mapBoost in this.mapBoostService.MapBoosts.Values)

    {

        float num2 = 1f;

        if (!this.ArmyHasBoost(mapBoost.MapBoostDefinition, army) && mapBoost.IsBuffAvailable)

        {

              (...)

        }

    }


   (...)

}



Class: AIBehaviorTreeNode_Decorator_SelectTarget.cs


This class governs AI-Army target selections for many things like ruins, villages, enemies etc. . There are several issues, so I'll split them up.


Issue 1:

protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)

{

    (...)


    if (list2 != null && list2.Count != 0)

    {

        IWorldPositionningService worldPositionService = service.Game.Services.GetService<IWorldPositionningService>();

        Diagnostics.Assert(worldPositionService != null);

        bool allowWaterTile = false;

        if (this.TypeOfTarget == AIBehaviorTreeNode_Decorator_SelectTarget.TargetType.Ruin)

        {

            allowWaterTile = army.SimulationObject.Tags.Contains("MovementCapacitySail");

        }

        else

        {

            allowWaterTile = army.IsSeafaring;

        }

        IWorldPositionable value = list2.FindLowest((IWorldPositionable element) => (float)worldPositionService.GetDistance(element.WorldPosition, army.WorldPosition), (IWorldPositionable element) => allowWaterTile == worldPositionService.IsWaterTile(element.WorldPosition));

        if (aiBehaviorTree.Variables.ContainsKey(this.Output_TargetVarName))

        {

            aiBehaviorTree.Variables[this.Output_TargetVarName] = value;

        }

        else

        {

            aiBehaviorTree.Variables.Add(this.Output_TargetVarName, value);

        }

    }  

   else if (aiBehaviorTree.Variables.ContainsKey(this.Output_TargetVarName))

   {

       aiBehaviorTree.Variables.Remove(this.Output_TargetVarName);

   }


   (...)  

}

First of all something that got introduced with inferno (or one of the inferno patches). The snipped above was a well intended change to allow land armies to actively search out sea ruins (very important for Skyfins). However the way it's written, it prevents land based armies from ever searching land based ruins again as soon as shipyard (MovementCapacitySail) is researched! This is one reason, why Morgawr and Allayi-AIs tend to fall behind hard. The fix I wrote below is a bit more complicated then it needs to be, but it works. Rewriting the predicate in list2.FindLowest would probably be nicer to look at, but anyway:


protected override State Execute(AIBehaviorTree aiBehaviorTree, params object[] parameters)

{

    (...)

  

    if (list2 != null && list2.Count != 0)

    {

         bool allowWaterTile = false;

        if (this.TypeOfTarget == AIBehaviorTreeNode_Decorator_SelectTarget.TargetType.Ruin)

        {

            allowWaterTile = army.SimulationObject.Tags.Contains("MovementCapacitySail");

        }

        else

        {

            allowWaterTile = army.HasSeafaringUnits();

        }

        Diagnostics.Assert(worldPositionService != null);

        if (!allowWaterTile)

        {

            list2.RemoveAll((IWorldPositionable element) => worldPositionService.IsWaterTile(element.WorldPosition));

        }

        if (army.IsSeafaring)

        {

            list2.RemoveAll((IWorldPositionable element) => !worldPositionService.IsWaterTile(element.WorldPosition));

            list2.RemoveAll((IWorldPositionable element) => worldPositionService.IsFrozenWaterTile(element.WorldPosition));

        }

    }

    if (list2 != null && list2.Count != 0)

    {

        if (list2.Count > 0)

        {

            list2.Sort((IWorldPositionable left, IWorldPositionable right) => worldPositionService.GetDistance(left.WorldPosition, army.WorldPosition).CompareTo(worldPositionService.GetDistance(right.WorldPosition, army.WorldPosition)));

        }

        if (aiBehaviorTree.Variables.ContainsKey(this.Output_TargetVarName))

        {

            aiBehaviorTree.Variables[this.Output_TargetVarName] = list2[0];

        }

        else

        {

            aiBehaviorTree.Variables.Add(this.Output_TargetVarName, list2[0]);

        }

    }

    else if (aiBehaviorTree.Variables.ContainsKey(this.Output_TargetVarName))

    {

        aiBehaviorTree.Variables.Remove(this.Output_TargetVarName);

    }



    (...)  

}



Issue 2:

private static bool ValidateTarget(Army myArmy, IGameEntity gameEntity, DepartmentOfForeignAffairs departmentOfForeignAffairs, bool canAttack, IGameEntityRepositoryService gameEntityRepositoryService, IWorldPositionningService worldPositionningService)

{

    (...)


    if (departmentOfForeignAffairs != null)

    {

        IGarrison garrison = gameEntity as IGarrison;

        if (garrison == null)

        {

            return false;

        }

        if (canAttack)

        {

            if (!departmentOfForeignAffairs.CanAttack(gameEntity))

            {

                return false;

            }

        }

        else if (!departmentOfForeignAffairs.IsEnnemy(garrison.Empire))

        {

            return false;

        }


        (...)

    }

    (...)

}

The next issue can occur, if the AI-Player is under the influence of blackspot. departmentOfForeignAffairs.CanAttack always returns true for armies that are affected by blackspot, since apparently the function doesnt expect to be handed over gameEntities that belong to its owner. Since Decorator_GetTargetInRange collects everything and the kitchen sink, and Decorator_SelectTarget doesnt necessarily rule out friendly armies until this point, it may happen that an AI-Army will try to attack its own kind and predictably fail at that, locking it up. 


private static bool ValidateTarget(Army myArmy, IGameEntity gameEntity, DepartmentOfForeignAffairs departmentOfForeignAffairs, bool canAttack, IGameEntityRepositoryService gameEntityRepositoryService, IWorldPositionningService worldPositionningService)

{

    (...)


    if (departmentOfForeignAffairs != null)

    {

        IGarrison garrison = gameEntity as IGarrison;

        if (garrison == null)

        {

            return false;

        }

        if (canAttack)

        {

            if (!departmentOfForeignAffairs.CanAttack(gameEntity) || garrison.Empire == myArmy.Empire)

            {

                return false;

            }

        }

        else if (!departmentOfForeignAffairs.IsEnnemy(garrison.Empire))

        {

            return false;

        }


        (...)

    }

    (...)

}



Issue 3:

private bool CanSearch(Army army, IWorldPositionable item, IQuestManagementService questManagementService)

{

    (...)

    

    if ((pointOfInterest.Interaction.Bits & army.Empire.Bits) == army.Empire.Bits)

    {

        return false;

    }


    (...)

}


This functions determines, whether an AI is allowed to search a ruin. It is there to prevent the AI from repeatedly trying to search empty ruins, or ruins that are active for a quest that it cant solve anyway. However with the arrival of Inferno it should be updated to account for dust ecplise ruins. Currently it ignores all ruins that it considers "searched", even if the dust eclipse is active and there are riches to be gained. I'm pretty sure the AI mostly ignoring eclipse ruins isnt intended. Also racing to ruins before the enemy gets them is fun ;).


private bool CanSearch(Army army, IWorldPositionable item, IQuestManagementService questManagementService)

{

    (...)

      

    if ((pointOfInterest.Interaction.Bits & army.Empire.Bits) == army.Empire.Bits && !SimulationGlobal.GlobalTagsContains(SeasonManager.RuinDustDepositsTag))

    {

        return false;

    }

    if (SimulationGlobal.GlobalTagsContains(SeasonManager.RuinDustDepositsTag) && !pointOfInterest.UntappedDustDeposits && (pointOfInterest.Interaction.Bits & army.Empire.Bits) == army.Empire.Bits)

    {

        return false;

    }


    (...)

}



Phew, I think thats most of the easy to fix issues I have found so far. I'll let you know if I stumble upon new ones (or remember others I currently dont think of). Until then:



*** Happy Endless Day! ***

0Send private message
6 years ago
Jan 21, 2019, 12:13:38 PM

This is awesome stuff boss, I can tell you we already went through a bunch of it (some of it we had spotted ourselves as well) and its going through the integration pipeline, I'm not sure when it will make it into a public build yet, but it'll get there.

0Send private message
6 years ago
Jan 29, 2019, 5:32:07 AM

Hello again, Symbiosis is out and I'm sure you have your hands full with all the bug reports (this is not meant as a slight, even as a hobby programmer I know its just normal with big feature changes). Here are some things I found:


XML:


File: AI\AIParameters[UnitSkill].xml

  <!-- Inspirational Leader - FIDS Boost -->

  <AIParameterDatatableElement Name="HeroSkillGovernor29" >

    <AIParameter Name="GovernorCity"   Value="0.8"/>

    <AIParameter Name="GovernorEmpire" Value="0.8"/>

    <AIParameter Name="ArmySupport"    Value="0"/>

    <AIParameter Name="ArmyHero"       Value="0"/>

    <AIParameter Name="Spy"            Value="0"/>

  </AIParameterDatatableElement>

This first one is actually way older than Symbiosis. With the Shadows-DLC, the "Inspirational Leader"-Skill (HeroSkillGovernor29) was replaced with an almost identical skill (HeroSkillGovernor29ReplicantsPack) that just has different prerequisites due to changes to the hero skill tree. Sadly the AI was never taught to actually use that skill which is why the AI always ignores it when "Shadows" is active.


  <!-- Inspirational Leader - FIDS Boost -->

  <AIParameterDatatableElement Name="HeroSkillGovernor29" >

    <AIParameter Name="GovernorCity"   Value="0.8"/>

    <AIParameter Name="GovernorEmpire" Value="0.8"/>

    <AIParameter Name="ArmySupport"    Value="0"/>

    <AIParameter Name="ArmyHero"       Value="0"/>

    <AIParameter Name="Spy"            Value="0"/>

  </AIParameterDatatableElement>

  

  <AIParameterDatatableElement Name="HeroSkillGovernor29ReplicantsPack" >

    <AIParameter Name="GovernorCity"   Value="0.8"/>

    <AIParameter Name="GovernorEmpire" Value="0.8"/>

    <AIParameter Name="ArmySupport"    Value="0"/>

    <AIParameter Name="ArmyHero"       Value="0"/>

    <AIParameter Name="Spy"            Value="0"/>

  </AIParameterDatatableElement>



File: AI\AIArmyMissionDefinition[SubTree].xml

  <AIArmyMissionDefinition Name="OpportunityDestroyCreepingNode">

    (...)

          <Controller_Sequence Debug="Opportunity_DestroyNode_AlreadyAtDestination">

            <Decorator_DestinationReached DestinationVarName="$CreepingNodeTargetWorldPositionDestination" TypeOfCheck="Attack"/>

            <Action_ToggleDismantleCreepingNode TargetVarName="$CreepingNodeTarget"/>

          </Controller_Sequence>


          <!-- Wait until we have ended -->

          <Decorator_DestinationReached DestinationVarName="$CreepingNodeTargetWorldPositionDestination" TypeOfCheck="Attack"/>

     (...)

  </AIArmyMissionDefinition>

The new subtree for destroying Fungal Blooms has a small but fatal flaw: Decorator_DestinationReached does an "Attack"-TypeOfCheck, which checks whether the Army is on or next to the target tile. The problem is, Decorator_GetTargetPosition already returns a Tile next of the Target for PointOfInterest. This can often lead to Armies standing a tile next to where they could dispose a fungal Bloom, trying and invariably failing to do it. They then stand in place as long as they dont get any new mission that lets them ignore the Fungal Bloom.


<AIArmyMissionDefinition Name="OpportunityDestroyCreepingNode">

    (...)

          <Controller_Sequence Debug="Opportunity_DestroyNode_AlreadyAtDestination">

            <Decorator_DestinationReached DestinationVarName="$CreepingNodeTargetWorldPositionDestination" TypeOfCheck="Regular"/>

            <Action_ToggleDismantleCreepingNode TargetVarName="$CreepingNodeTarget"/>

          </Controller_Sequence>


          <!-- Wait until we have ended -->

          <Decorator_DestinationReached DestinationVarName="$CreepingNodeTargetWorldPositionDestination" TypeOfCheck="Regular"/>

     (...)

  </AIArmyMissionDefinition>



File: AI\AIArmyMissionDefinition[Exploration].xml

<AIArmyMissionDefinition Name="ExploreAt">

    (...)

            <!--IF nothing else to do THEN Move to Destination-->

            <Controller_Sequence Debug="Exploration_TryToFollowTheBorder">

                <!-- Ask for a destination. -->

                <Controller_Selector>

                    <Controller_Sequence Debug="Exploration_TryToFollowTheBorder_ChangeTarget">

                        <Decorator_VariableCheck VarName="$BorderDestination" CheckOperation="Exists"/>

                        <Decorator_DestinationReached DestinationVarName="$BorderDestination" TypeOfCheck="Regular"/>

                        <Decorator_GetNextRegionBorderPosition TargetRegionVarName="$RegionIndex" Output_DestinationVarName="$BorderDestination"/>

                        <Action_GeneratePath DestinationVarName="$BorderDestination" TypeOfPath="Regular" Output_PathVarName="$Path"/>

                    </Controller_Sequence>


                    <Controller_Sequence Debug="Exploration_TryToFollowTheBorder_GoToPreviousTarget">

                        <Decorator_VariableCheck VarName="$BorderDestination" CheckOperation="Exists"/>

                        <Action_GeneratePath DestinationVarName="$BorderDestination" TypeOfPath="Regular" Output_PathVarName="$Path"/>

                    </Controller_Sequence>


                    <Controller_Sequence Debug="Exploration_TryToFollowTheBorder_FirstTime">

                        <Decorator_GetNextRegionBorderPosition TargetRegionVarName="$RegionIndex" Output_DestinationVarName="$BorderDestination"/>

                        <Action_GeneratePath DestinationVarName="$BorderDestination" TypeOfPath="Regular" Output_PathVarName="$Path"/>

                    </Controller_Sequence>

                    

                    <!--ELCP: If no valid position was found, just roam around (prevents lag that can occure in regions with unreachable positions)-->

                    <Controller_Sequence Debug="LastResortRoaming">

                        <Decorator_GetNextRoamingPosition TargetRegionVarName="$RegionIndex" Output_DestinationVarName="$BorderDestination"/>

                        <Action_GeneratePath DestinationVarName="$BorderDestination" TypeOfPath="Regular" Output_PathVarName="$Path"/>

                    </Controller_Sequence>                    

                </Controller_Selector>


                <Action_Move PathVarName="$Path" TypeOfMove="Regular"/>

                <Decorator_MoveEnded PathVarName="$Path"/>

                <Decorator_DestinationReached DestinationVarName="$BorderDestination" TypeOfCheck="Regular"/>

            </Controller_Sequence>

    (...)

</AIArmyMissionDefinition>

This one is not a specific bugfix, it's more of a demonstration, how I try to work around some ELs AI-related performance problems. Sometimes it happens, that the game (in this case Decorator_GetNextRegionBorderPosition) calculates a desired target position, that is actually not reachable. In my game, due to the map having many cliffs, there was a region, where parts of it where actually unreachable for the AI-Empire due to a city being located right at a choke point. So the AI wanted to explore the whole region, but it couldnt. Decorator_GetNextRegionBorderPosition calculated a position, that was not reachable again and again. And the AI tried to generate a path to that position again and again, several times per second. This actually had a very (!) noticeable performance impact. My workaround was to simply add a "last resort" roaming behavior to that army for that specific edge case. So instead of spamming Action_GeneratePath for the rest of the turn (and maybe the ones after that), the army just roams around in that region a bit, until it has no movement left. Its not necessarily the smartes behavior, but better than standing around and tanking performance.


The same principle can be applied to other AI-Behaviors aswell. For example the Colonization-Tree can sometimes target a colonization position that is blocked for a turn or more (roaming armies...) which can lead to a similar performance impact and can also causes the settler to just stand around. Introducing a "last resort" roaming behavior there makes the settler stop spamming Action_GeneratePath and as an additional benefit, it keeps moving towards the target region (It requires some dll changes aswell in that case, thats why I didnt post it for now. Contact me for more Information ;)).




DLL:


Class: AILayer_KaijuAdquisition.cs

private bool IsKaijuValidForObjective(Kaiju kaiju)

{

    bool flag = false;

    DepartmentOfForeignAffairs agency = base.AIEntity.Empire.GetAgency<DepartmentOfForeignAffairs>();

    if (kaiju != null && kaiju.MajorEmpire != null)

    {

        flag = agency.IsFriend(kaiju.MajorEmpire);

    }

    return kaiju != null && !flag && kaiju.Empire.Index != base.AIEntity.Empire.Index;

}

This class is responsible for sending AI-Armies to fight and tame Urkans. The problem is, that it doesnt consider at all, if the AI-Empire is even able to reach their target. This can lead to several armies being assigned to "Urkan-Duty", which may be unreachable due to not having researched Shipyard or closed borders being in the way. The armies then proceed to stand around, doing nothing. So I introduced a small pathfinding check. I take the first non-seafaring army, and look, if it is able to generate a valid path from one of the empire's cities to the urkan. If yes, the Urkan is a valid target. If not, those armies are better doing something they can actually reach. There may be edge cases, where this can still result in armies not being able to reach the targeted Urkan, like an empire being split in half by another empire that has closed its borders, but these should be rare. Note: You dont want to check all cities or armies, because pathfinding is expensive, and this can lead to serious performance issues in the later game stages. I've tried, believe me!


private bool IsKaijuValidForObjective(Kaiju kaiju)

{

    bool flag = false;

    DepartmentOfForeignAffairs agency = base.AIEntity.Empire.GetAgency<DepartmentOfForeignAffairs>();

    if (kaiju != null && kaiju.MajorEmpire != null)

    {

        flag = agency.IsFriend(kaiju.MajorEmpire);

    }

    if (kaiju != null && !flag && kaiju.Empire.Index != base.AIEntity.Empire.Index && this.departmentOfTheInterior.Cities.Count > 0)

    {

        foreach (Army army in this.departmentOfDefense.Armies)

        {

            if (!army.IsSeafaring)

            {

                if (this.pathfindingService.FindPath(army, this.departmentOfTheInterior.Cities[0].WorldPosition, kaiju.WorldPosition, PathfindingManager.RequestMode.Default, null, PathfindingFlags.IgnoreArmies | PathfindingFlags.IgnoreFogOfWar | PathfindingFlags.IgnoreSieges | PathfindingFlags.IgnoreKaijuGarrisons, null) == null)

                {

                    return false;

                }

                return true;

            }

        }

    }

    return false;

}


private DepartmentOfDefense departmentOfDefense;


private IPathfindingService pathfindingService;

Updated 6 years ago.
0Send private message
6 years ago
Jan 30, 2019, 8:36:19 AM

Oh no! I actually forgot probably the most important thing in my post from yesterday!


DLL:


Class: Intelligence.cs

public void ComputeMPBasedOnBattleArea(IGarrison firstGarrison, List<IGarrison> reinforcements, int availableTile, ref float militaryPower)

{

    int num = (int)firstGarrison.GetPropertyValue(SimulationProperties.ReinforcementPointCount);

    float additionalHealthPoint = 0f;

    City city = firstGarrison as City;

    if (city == null)

    {

        IWorldPositionable worldPositionable = firstGarrison as IWorldPositionable;

        District district = this.worldPositionningService.GetDistrict(worldPositionable.WorldPosition);

        if (district != null && district.City.Empire == firstGarrison.Empire)

        {

            city = district.City;

        }

    }

    if (city != null)

    {

        additionalHealthPoint = city.GetPropertyValue(SimulationProperties.CityDefensePoint);

    }

    (...)

    while (num7 < this.numberOfBattleRound && reinforcements.Count > num5)

    {

        (...)

        for (int i = 0; i < num; i++)

        {

            if (availableTile <= 0)

            {

                break;

            }

            if (num6 >= reinforcements[num5].StandardUnits.Count)

            {

                num5++;

                if (num5 >= reinforcements.Count)

                {

                    break;

                }

                num6 = 0;

                if (reinforcements[num5].Hero != null)

                {

                    num6 = -1;

                }

            }

            if (num6 == -1)

            {

                num8 += this.EvaluateMilitaryPowerOfAllyUnit(reinforcements[num5].Hero, additionalHealthPoint);

            }

            else

            {

                num8 += this.EvaluateMilitaryPowerOfAllyUnit(reinforcements[num5].StandardUnits[num6], additionalHealthPoint);

            }

            num6++;

            num2++;

            availableTile--;

        }

        (...)

    }

}

This function is responsible for calculating the perceived military strength of each combatant in a Battle. It's used by the AI to calculate the odds of winning a battle and then do decisions based on this (like running away, or how many reinforcements to bring). There is a small and a severe (!) issue in this code.


The small issue

As you can see in the the first red marked line, the function decides, whether the battle is a city battle, and if so, it adds takes fortification into account for the city owner. The problem here is, that the function does not make a distinction between attacker and defender. Under the vanilla ruleset, only when defending a battle, an army can get fortification hp. This can lead to the AI being overly aggressive when standing on city tiles, because they think, they'll get the fortification bonus. I added an additional paramter to the function, so the calling function can determine, whether the calculation is made for a defending or an attacking empire.


The big issue

The second issue is much more severe. In the blue marked loop, Units and their perceived strength are added one after another to calculate the total perceived strength of the combatant. The red marked code makes sure, that, if all units of a reinforcement garrison have been added, the next one gets chosen and the process continues. The Problem here is: If the next chosen garrison has no Standard Units and no Hero, the following green marked code will throw an exception! This leads to the battle never starting properly, which in turn leads the two combatants being locked in an indefinite battle. The player can no longer progress in the game! This issue has been present since forever, but due to another factor, it now manifests way more often than it previously did.

One user of my mod sent me a savegame, where the bug happened. In this specific game, an AI-Cultist player (Empire 5) battled with a bunch of neutral Lice (they belonged to the neutral Empire 7 internally). As discribed above, the battle locked up and the game did not progress further. While investigating the issue, I noticed, that for some reason the function ComputeMPBasedOnBattleArea considered one of the fungal bloomed minor villages in the area, that belonged to the human player (Empire 0), as one of the reinforcements for Empire 7! And indeed, when checking the Empire variable of the Village-Object, it was still set to Empire 7. It seems that Fungal Blooming a village doesnt change its internal Owner. While I dont know the exact code location where this should be happening, I hope this is a good hint where you may want to investigate further.


public void ComputeMPBasedOnBattleArea(IGarrison firstGarrison, List<IGarrison> reinforcements, int availableTile, ref float militaryPower, bool isDefender = false)

{

    int num = (int)firstGarrison.GetPropertyValue(SimulationProperties.ReinforcementPointCount);

    float additionalHealthPoint = 0f;

    City city = firstGarrison as City;

    if (city == null)

    {

        IWorldPositionable worldPositionable = firstGarrison as IWorldPositionable;

        District district = this.worldPositionningService.GetDistrict(worldPositionable.WorldPosition);

        if (district != null && district.City.Empire == firstGarrison.Empire)

        {

            city = district.City;

        }

    }

    if (city != null && isDefender)

    {

        additionalHealthPoint = city.GetPropertyValue(SimulationProperties.CityDefensePoint);

    }

    (...)

    while (num7 < this.numberOfBattleRound && reinforcements.Count > num5)

    {

        (...)

        for (int i = 0; i < num; i++)

        {

            if (availableTile <= 0)

            {

                break;

            }

            next:

            if (num6 >= reinforcements[num5].StandardUnits.Count)

            {

                num5++;

                if (num5 >= reinforcements.Count)

                {

                    break;

                }

                num6 = 0;

                if (reinforcements[num5].Hero != null)

                {

                    num6 = -1;

                }

                if (num6 >= reinforcements[num5].StandardUnits.Count)

                {

                    goto next;

                }

            }

            if (num6 == -1)

            {

                num8 += this.EvaluateMilitaryPowerOfAllyUnit(reinforcements[num5].Hero, additionalHealthPoint);

            }

            else

            {

                num8 += this.EvaluateMilitaryPowerOfAllyUnit(reinforcements[num5].StandardUnits[num6], additionalHealthPoint);

            }

            num6++;

            num2++;

            availableTile--;

        }

        (...)

    }

}

I could in theory give you the aforementioned save. However it is an ELCP-Save, and these typically dont work in vanilla. Since the currently released version of ELCP has already fixed the bug, it would be of no use here. However if you are interested, I can give you a "downgraded version" of ELCP that + the save so you can reproduce the issue. 

That save also demonstrates a bunch of other issues: first it demonstrates the buggy Fungal Bloom Disposal subtree (the aforementioned Cultist-Army that battles with the lice is standing around passively because they thing they can start disposing blooms when they cant). It also displays the stuttering issue caused by the exploration AI, if you progress the game one turn (after fixing the battle-bug ofcourse).

Updated 6 years ago.
0Send private message
?

Click here to login

Reply
Comment

Characters : 0
No results
0Send private message