View Issue Details

IDProjectCategoryLast Update
0022352AI War 2SuggestionDec 11, 2019 6:01 pm
ReporterAstilious Assigned ToBadgerBadger  
Severityminor 
Status resolvedResolutionfixed 
Product Version1.014 High AIP Playstyle Viability 
Fixed in VersionBETA 1.015 Testing The Outguard Waters 
Summary0022352: Allow Mods to Discourage Pathing Through Planets
DescriptionI spent quite a while attempting to determine how pathfinding might be altered for player units via an external dll when making my Astilious Interface Tweaks mod (https://forums.arcengames.com/ai-war-ii-modding/astilious-interface-tweaks/). I was unable to find a way to do so. Sorry if I missed something.

The use case for "Astilious Interface Tweaks" is allowing the player to set planets to be avoided during pathing, something I use frequently in my play since completing that part of the mod. When I was talking about making the mod others also seemed interested in the feature, but none have used it (possibly because the mod now needs to be updated manually with every version due to changing AIWarExternalCode.dll, possibly just because the forums have been down frequently). I'd appreciate if this limitation could be fixed.

I've recently neatened up the portion of the mod that changes AIWar External Code files so that they are generalised to allow mods to add and remove pathing penalties for any planet and any faction seperately. This is in the hope it might be able to be integrated into the game. The relevant files are attached.
TagsNo tags attached.

Activities

Astilious

Dec 11, 2019 4:54 am

reporter  

KDL_VanillaEntries.xml (4,162 bytes)   
<?xml version="1.0" encoding="utf-8"?>
<root>
	<item name="DoomData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.DoomData" />
	<item name="ExternalData_GroupTargetSorting" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.ExternalData_GroupTargetSorting" />
    <item name="ExternalData_AI" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AI_PlannedWave_Data" />
    <item name="ExternalData_MinorFactionCommon" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.ExternalData_MinorFactionCommon" />
    <item name="ExternalData_AIFactionCommon" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.ExternalData_AIFactionCommon" />
    <item name="ExternalData_FactionCommon" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.ExternalData_FactionCommon" />
    <item name="HRFDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.HRFDataExt" />
    <item name="AIReservesDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AIReservesDataExt" />
    <item name="MarauderDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MarauderDataExt" />
    <item name="SpawnAnimationData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.SpawnAnimationData" />
    <item name="DespawnAnimationData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.DespawnAnimationData" />
    <item name="AutosaveDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AutosaveDataExt" />
    <item name="RiskAnalyzerData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.RiskAnalyzerData" />
    <item name="DarkSpireData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.DarkSpireExternalData" />
    <item name="AstroTrainGlobalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AstroTrainsGlobalExternalData" />
    <item name="AstroTrainPerDepotData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AstroTrainsPerDepotExternalData" />
    <item name="AstroTrainPerTrainData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AstroTrainsPerTrainExternalData" />
    <item name="MacrophageGlobalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MacrophageGlobalExternalData" />
    <item name="MacrophagePerTeliumData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MacrophagePerTeliumExternalData" />
    <item name="MacrophagePerSporeData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MacrophagePerSporeExternalData" />
    <item name="MacrophagePerHarvesterData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MacrophagePerHarvesterExternalData" />
    <item name="DysonExternalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.DysonExternalData" />
    <item name="MercenaryUnitExternalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MercenaryUnitExternalData" />
    <item name="MercenaryGlobalExternalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MercenaryGlobalExternalData" />
    <item name="MarauderOutpostRaiderSpawnDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MarauderOutpostRaiderSpawnDataExt" />
    <item name="ExoDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.ExoDataExt" />
    <item name="MDCExoDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.MDCExoDataExt" />
    <item name="MinorFactionWaveDataExt" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.AntiMinorFactionWaveDataExt" />
    <item name="InstigatorDataExternal" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.InstigatorDataExternal" />
    <item name="InstigatorPerUnitDataExternal" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.InstigatorPerUnitDataExternal" />
    <item name="WormholeInvasionDataExternal" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.WormholeInvasionDataExternal" />
    <item name="PlanetPathfindingExternalData" dll_name="AIWarExternalCode" type_name="Arcen.AIW2.External.PlanetPathfindingExternalData" />
</root>
KDL_VanillaEntries.xml (4,162 bytes)   
ExternalData_Pathfinding.cs (3,740 bytes)   
using Arcen.AIW2.Core;
using Arcen.Universal;
using System;
using System.Collections.Generic;
using System.Text;

namespace Arcen.AIW2.External
{
    // Tracks the AIT data that should be attached to each planet.
    public class PlanetPathfindingExternalData : IArcenExternalDataPatternImplementation
    {
        public List<int> PathingPenalties;

        public static int PatternIndex;

        public static string RelatedParentTypeName = "Planet";

        public void ReceivePatternIndex( int Index )
        {
            PatternIndex = Index;
        }
        public int GetNumberOfItems()
        {
            return 1;
        }
        public bool GetShouldInitializeOn( string ParentTypeName )
        {
            return ArcenStrings.Equals( ParentTypeName, RelatedParentTypeName );
        }

        public void InitializeData( object ParentObject, object[] Target )
        {
            this.PathingPenalties = new List<int>( World_AIW2.Instance.Factions.Count );
            for ( int i = 0; i < World_AIW2.Instance.Factions.Count; i++ )
                this.PathingPenalties.Add(0);
            Target[0] = this.PathingPenalties;
        }
        public void SerializeExternalData( object[] Source, ArcenSerializationBuffer Buffer )
        {
            // For saving to disk, translate this object into the buffer"
            List<int> pathingPenalties = (List<int>)Source[0];

            // There is one list per faction.
            for ( int i = 0; i < World_AIW2.Instance.Factions.Count; i++ )
                Buffer.AddItem( pathingPenalties[i] );
        }
        public void DeserializeExternalData( object ParentObject, object[] Target, int ItemsToExpect, ArcenDeserializationBuffer Buffer )
        {
            //reverses SerializeData; gets the data out of the buffer and populates the variables
            List<int> pathingPenalties = new List<int>( World_AIW2.Instance.Factions.Count );

            // There is one list per faction.
            for ( int i = 0; i < World_AIW2.Instance.Factions.Count; i++ )
                pathingPenalties.Add( Buffer.ReadInt32() );

            Target[0] = pathingPenalties;
        }
    }

    // The following is a halper function to the above, designed to allow us to save and load data on demand.
    public static class ExtensionMethodsFor_PlanetPathfindingExternalData
    {
        // Get the list of pathing penalties for this planet.
        public static int GetPathingPenalty( this Planet ParentObject, Faction faction )
        {
            return ((List<int>)ParentObject.ExternalData.GetCollectionByPatternIndex( PlanetPathfindingExternalData.PatternIndex ).Data[0])[faction.FactionIndex];
        }

        // Adds a penalty to pathing through this planet.
        public static void AddPathingPenalty( this Planet ParentObject, Faction faction, int penalty )
        {
            List<int> pathingPenalties = (List<int>)ParentObject.ExternalData.GetCollectionByPatternIndex( PlanetPathfindingExternalData.PatternIndex ).Data[0];
            pathingPenalties[faction.FactionIndex] += penalty;
        }

        // Removes a penalty of the specified size from this planet.
        public static void RemovePathingPenalty( this Planet ParentObject, Faction faction, int penalty )
        {
            List<int> pathingPenalties = (List<int>)ParentObject.ExternalData.GetCollectionByPatternIndex( PlanetPathfindingExternalData.PatternIndex ).Data[0];
            pathingPenalties[faction.FactionIndex] -= penalty;
            // Perhaps there should be an error if the penalty becomes negative? Can the algorithm cope with negatives?
        }
    }
}
ExternalData_Pathfinding.cs (3,740 bytes)   
Pathfinding.cs (8,073 bytes)   
using Arcen.Universal;
using System;
using System.Collections.Generic;
using System.Text;
using Arcen.AIW2.Core;

namespace Arcen.AIW2.External
{
    public class PlanetPathfinder : ArcenPathfinder<Planet>
    {
        protected Faction Faction;

        public PlanetPathfinder( Faction Faction )
        {
            this.Faction = Faction;
        }

        protected override int GetAccurateCrowFliesDistanceBetweenNodes( Planet left, Planet right )
        {
            return left.GetHopsTo( right );
        }

        protected override string GetDebugLogString( Planet node )
        {
            return "#" + node.PlanetIndex + "(" + node.Name + ")";
        }

        protected override NodePassability GetIsNodePassable( Planet node )
        {
            PlanetFaction planetFaction = node.GetPlanetFactionForFaction( this.Faction );
            //this untested code should hopefully prevent the player from routing ships through an unexplored planet
            if ( planetFaction.Faction.Type == FactionType.Player && node.IntelLevel <= PlanetIntelLevel.Unexplored)
                return NodePassability.NEVER_PASSABLE;
            if ( planetFaction.BooleanFlags[PlanetFactionBooleanFlag.DoNotPathThrough] )
                return NodePassability.ONLY_PASSABLE_FOR_ORIGIN;
            return NodePassability.ALWAYS_PASSABLE;
        }

        protected override bool GetMatch( Planet Origin, Planet Target )
        {
            return Origin.PlanetIndex == Target.PlanetIndex;
        }

        protected override int GetNeighborCount( Planet node )
        {
            return node.GetLinkedNeighborCount();
        }

        protected override Planet GetNeighborOfNodeByIndex( Planet node, int neighborIndex )
        {
            Planet result = null;
            node.DoForLinkedNeighbors( delegate ( Planet neighbor )
            {
                if ( neighborIndex <= 0 )
                {
                    result = neighbor;
                    return DelReturn.Break;
                }
                neighborIndex--;
                return DelReturn.Continue;
            } );
            return result;
        }

        protected override int HeuristicCostEstimate( Planet Origin, Planet Target )
        {
            int hops = Origin.GetHopsTo( Target );
            hops += Origin.GetPathingPenalty( Faction );
            return hops;
        }

        protected override void SetDebugText( Planet node, string CostToGetHere, string GuessCostToGetToTarget )
        {
            node.DebugText = CostToGetHere + "|" + GuessCostToGetToTarget;
        }
    }

    public abstract class PlanetConservativePathfinder : PlanetPathfinder
    {
        public PlanetConservativePathfinder( Faction faction ) : base( faction ) { }

        protected abstract Base_StrengthData_PlanetFaction_Stance GetStanceData( Planet node, FactionStance stance );

        protected override int HeuristicCostEstimate( Planet Origin, Planet Target )
        {
            Base_StrengthData_PlanetFaction_Stance selfData = GetStanceData( Origin, FactionStance.Self );
            Base_StrengthData_PlanetFaction_Stance friendlyData = GetStanceData( Origin, FactionStance.Friendly );
            Base_StrengthData_PlanetFaction_Stance hostileData = GetStanceData( Origin, FactionStance.Hostile );
            int friendlyStrength = selfData.TotalStrength + friendlyData.TotalStrength;
            int hostileStrength = hostileData.TotalStrength;

            int uncounteredHostileStrength = Math.Max( 0, hostileStrength - friendlyStrength );

            return Origin.GetHopsTo( Target ) + uncounteredHostileStrength + Origin.GetPathingPenalty( Faction ); // so generally the hostile strength will drown out distance considerations
        }
    }

    public class PlanetConservativePathfinder_ShortTermPlanning : PlanetConservativePathfinder
    {
        public PlanetConservativePathfinder_ShortTermPlanning( Faction faction ) : base( faction ) { }

        protected override Base_StrengthData_PlanetFaction_Stance GetStanceData( Planet node, FactionStance stance )
        {
            return node.GetPlanetFactionForFaction( this.Faction ).DataByStance[stance];
        }
    }

    public class PlanetConservativePathfinder_LongTermPlanning : PlanetConservativePathfinder
    {
        public PlanetConservativePathfinder_LongTermPlanning( Faction faction ) : base( faction ) { }

        protected override Base_StrengthData_PlanetFaction_Stance GetStanceData( Planet node, FactionStance stance )
        {
            return node.LongRangePlanningData.PlanetFactionDataByIndex[this.Faction.FactionIndex].DataByStance[stance];
        }
    }

    public abstract class PlanetSpecialForcesPathfinder : PlanetConservativePathfinder
    {
        public PlanetSpecialForcesPathfinder( Faction faction ) : base( faction ) { }

        protected abstract bool GetIsPlanetControllerOrInfluencerHostileToMe(Planet planet );
        protected override Base_StrengthData_PlanetFaction_Stance GetStanceData( Planet node, FactionStance stance )
        {
            return node.LongRangePlanningData.PlanetFactionDataByIndex[this.Faction.FactionIndex].DataByStance[stance];
        }

        protected override NodePassability GetIsNodePassable( Planet node )
        {
            NodePassability result = base.GetIsNodePassable( node );
            if ( result > NodePassability.ONLY_PASSABLE_FOR_ORIGIN && this.GetIsPlanetControllerOrInfluencerHostileToMe( node  ) )
                result = NodePassability.ONLY_PASSABLE_FOR_ORIGIN;
            return result;
        }
    }

    public class PlanetSpecialForcesPathfinder_ShortTermPlanning : PlanetSpecialForcesPathfinder
    {
        public PlanetSpecialForcesPathfinder_ShortTermPlanning( Faction faction ) : base( faction ) { }

        protected override bool GetIsPlanetControllerOrInfluencerHostileToMe(Planet planet)
        {
            return planet.GetIsEitherControllerOrInfluencerHostileTo( this.Faction );
        }
    }

    public class PlanetSpecialForcesPathfinder_LongTermPlanning : PlanetSpecialForcesPathfinder
    {
        public PlanetSpecialForcesPathfinder_LongTermPlanning( Faction faction ) : base( faction ) { }

        protected override bool GetIsPlanetControllerOrInfluencerHostileToMe( Planet planet )
        {
            return planet.LongRangePlanningData?.GetIsEitherControllerOrInfluencerHostileTo( this.Faction ) ?? false;
        }
        protected override int HeuristicCostEstimate( Planet Origin, Planet Target )
        {
            Base_StrengthData_PlanetFaction_Stance selfData = GetStanceData( Origin, FactionStance.Self );
            Base_StrengthData_PlanetFaction_Stance friendlyData = GetStanceData( Origin, FactionStance.Friendly );
            Base_StrengthData_PlanetFaction_Stance hostileData = GetStanceData( Origin, FactionStance.Hostile );
            int friendlyStrength = selfData.TotalStrength + friendlyData.TotalStrength;
            int hostileStrength = hostileData.TotalStrength;

            int uncounteredHostileStrength = Math.Max( 0, hostileStrength - friendlyStrength );

            return Origin.GetHopsTo( Target ) + uncounteredHostileStrength / 10;
        }

    }

    public class DevourerLongRangePlanningPathfinder : PlanetPathfinder
    {
        public DevourerLongRangePlanningPathfinder( Faction faction ) : base( faction ) { }

        protected override NodePassability GetIsNodePassable( Planet node )
        {
            NodePassability result = base.GetIsNodePassable( node );
            if ( result > NodePassability.ONLY_PASSABLE_FOR_ORIGIN && node.LongRangePlanningData.PlanetFactionDataByIndex[this.Faction.FactionIndex].DataByStance[FactionStance.Hostile].HasKingUnitPresent )
                result = NodePassability.ONLY_PASSABLE_FOR_ORIGIN;
            return result;
        }
    }
}
Pathfinding.cs (8,073 bytes)   

BadgerBadger

Dec 11, 2019 11:12 am

manager   ~0054933

This shouldn't be done in a mod, this should be just done via the base game. The code to prevent pathing through a planet is all there, there's just no UI interface for it.

BadgerBadger

Dec 11, 2019 1:20 pm

manager   ~0054936

I've implemented this feature. The player can toggle whether planets should be pathed through by Right Clicking the "Planet" name in the top left of the screen. The UI feedback for this isn't amazing (I think changing the Planet icon in some fashion, or putting flair on it would be best), but I've put some feedback in the tooltip for hovering over a planet.

I think I suggested that this was the easiest mechanism to implement this a few weeks ago.

Astilious

Dec 11, 2019 6:01 pm

reporter   ~0054952

That's great. Thanks. I look forward to seeing exactly what you've done in the next patch.

I remember the conversation you mentioned, but I prefer the hold key and click planet method I've implemented in my mod. Hopefully it will be possible to call whatever the "mark to avoid" function you have is from there.

The feedback I put in the mod was just in the hover tooltip too, I'm not a graphics person...

Issue History

Date Modified Username Field Change
Dec 11, 2019 4:54 am Astilious New Issue
Dec 11, 2019 4:54 am Astilious File Added: KDL_VanillaEntries.xml
Dec 11, 2019 4:54 am Astilious File Added: ExternalData_Pathfinding.cs
Dec 11, 2019 4:54 am Astilious File Added: Pathfinding.cs
Dec 11, 2019 11:12 am BadgerBadger Note Added: 0054933
Dec 11, 2019 1:20 pm BadgerBadger Assigned To => BadgerBadger
Dec 11, 2019 1:20 pm BadgerBadger Status new => resolved
Dec 11, 2019 1:20 pm BadgerBadger Resolution open => fixed
Dec 11, 2019 1:20 pm BadgerBadger Fixed in Version => BETA 1.015 Testing The Outguard Waters
Dec 11, 2019 1:20 pm BadgerBadger Note Added: 0054936
Dec 11, 2019 6:01 pm Astilious Note Added: 0054952