Skip to content

Commit

Permalink
DistanceMatrix fixes and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
artifixer committed May 14, 2024
1 parent 8c4f576 commit 2584f61
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 74 deletions.
4 changes: 3 additions & 1 deletion docs/articles/CampaignIdentifier/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
> This SubSystem is obsolete!
>
[``CampaignIdentifier``](xref:Bannerlord.ButterLib.CampaignIdentifier) associates unique string ID with every campaign basing on the initial character.
CampaignIdentifier used to associate unique string ID with every campaign basing on the initial character.
This feature is obsolete and no longer supported.

```csharp
// Get current campaign ID
string campaignID = Camapaign.Current.GetCampaignId();
Expand Down
43 changes: 43 additions & 0 deletions docs/articles/DistanceMatrix/Overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Distance Matrix
Distance Matrix is a subsystem that provides a means for handling distances between various `MBObjectBase` objects in the game. You can use it to create your own implementations for the types you need.
Additionally, there are built-in implementations for `Settlement`, `Clan`, and `Kingdom` types, along with a behavior to keep the distances updated.

## Usage
Use [``DistanceMatrix``](xref:Bannerlord.ButterLib.DistanceMatrix.DistanceMatrix-1.html) class to work with your custom distance matrix.
Use [``CampaignExtensions``](xref:Bannerlord.ButterLib.Common.Extensions.CampaignExtensions) to access the built-in implementations.

If you plan to use built-in implementations and behavior, don't forget to enable the SubSystem in your `SubModule` class:
```csharp
if (this.GetServiceProvider() is { } serviceProvider)
{
var distanceMatrixSubSystem = serviceProvider.GetSubSystem("Distance Matrix");
distanceMatrixSubSystem?.Enable();
}
```

Example usage of built-in `DistanceMatrix` for `Clan` type:
```csharp
var clanDistanceMatrix = Campaign.Current.GetDefaultClanDistanceMatrix();

var playerClan = Clan.PlayerClan;
var playerNeighbors = clanDistanceMatrix.GetNearestNeighbors(playerClan, 10);

Clan inquiredClan = Clan.All.FirstOrDefault(clan => clan.Fiefs.Count > 0 && Clan.All.Any(x => x.Fiefs.Count > 0 && clan.MapFaction.IsAtWarWith(x.MapFaction)));
var unfriendlyNeighbors = clanDistanceMatrix.GetNearestNeighbors(inquiredObject: inquiredClan, 20, x => !float.IsNaN(x.Distance) && x.OtherObject != inquiredClan && x.OtherObject.MapFaction.IsAtWarWith(inquiredClan.MapFaction)).ToList();
var unfriendlyNeighborsN = clanDistanceMatrix.GetNearestNeighborsNormalized(inquiredObject: inquiredClan, 20, x => !float.IsNaN(x.Distance) && x.OtherObject != inquiredClan && x.OtherObject.MapFaction.IsAtWarWith(inquiredClan.MapFaction)).ToList();
```

Example usage of Distance Matrix with custom selector and distance calculator:
```csharp
//Gives same result as Campaign.Current.GetDefaultClanDistanceMatrix();
//...or Campaign.Current.GetCampaignBehavior<GeopoliticsBehavior>().ClanDistanceMatrix;
var settlementDistanceMatrix = Campaign.Current.GetCampaignBehavior<GeopoliticsBehavior>().SettlementDistanceMatrix ?? new DistanceMatrixImplementation<Settlement>();
var clanDistanceMatrix = DistanceMatrix<Clan>.Create(() => Clan.All.Where(x => !x.IsEliminated && !x.IsBanditFaction), (clan, otherClan, args) =>
{
if (args != null && args.Length == 1 && args[0] is Dictionary<ulong, WeightedDistance> lst)
{
return ButterLib.DistanceMatrix.DistanceMatrix.CalculateDistanceBetweenClans(clan, otherClan, lst).GetValueOrDefault();
}
return float.NaN;
}, [ButterLib.DistanceMatrix.DistanceMatrix.GetSettlementOwnersPairedList(settlementDistanceMatrix!)!]);
```
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ internal sealed class DistanceMatrixImplementation<T> : DistanceMatrix<T> where
private readonly Dictionary<T, SortedSet<(T OtherObject, float Distance)>> _flatenedDictionary;

private readonly Func<IEnumerable<T>>? _entityListGetter;
private readonly Func<T, T, float>? _distanceCalculator;
private readonly Func<T, T, object[]?, float>? _distanceCalculator;
private readonly object[]? _distanceCalculatorArgs;
private Dictionary<MBGUID, MBObjectBase> _cachedMapping = new();

//properties
/// <inheritdoc/>
public override Dictionary<ulong, float> AsDictionary => _distanceMatrix;

/// <inheritdoc/>
public override Dictionary<(T Object1, T Object2), float> AsTypedDictionary => _typedDistanceMatrix;

/// <inheritdoc/>
public override Dictionary<T, SortedSet<(T OtherObject, float Distance)>> AsFlatenedDictionary => _flatenedDictionary;

//Constructors
Expand All @@ -38,19 +38,23 @@ public DistanceMatrixImplementation()
{
_entityListGetter = null;
_distanceCalculator = null;
_distanceCalculatorArgs = null;

_distanceMatrix = CalculateDistanceMatrix();
_typedDistanceMatrix = GetTypedDistanceMatrix();
_flatenedDictionary = new();//GetFlatenedDictionary();
_flatenedDictionary = GetFlatenedDictionary();
}

/// <inheritdoc/>
public DistanceMatrixImplementation(Func<IEnumerable<T>> customListGetter, Func<T, T, float> customDistanceCalculator)
public DistanceMatrixImplementation(Func<IEnumerable<T>> customListGetter, Func<T, T, object[]?, float> customDistanceCalculator, object[]? distanceCalculatorArgs = null)
{
_entityListGetter = customListGetter;
_distanceCalculator = customDistanceCalculator;
_distanceCalculatorArgs = distanceCalculatorArgs;

_distanceMatrix = CalculateDistanceMatrix();
_typedDistanceMatrix = GetTypedDistanceMatrix();
_flatenedDictionary = new();//GetFlatenedDictionary();
_flatenedDictionary = GetFlatenedDictionary();
}

//Public methods
Expand All @@ -64,15 +68,55 @@ public override void SetDistance(T object1, T object2, float distance)
{
_distanceMatrix[object1.Id > object2.Id ? ElegantPairHelper.Pair(object2.Id, object1.Id) : ElegantPairHelper.Pair(object1.Id, object2.Id)] = distance;
_typedDistanceMatrix[object1.Id > object2.Id ? (object2, object1) : (object1, object2)] = distance;

_flatenedDictionary[object1].RemoveWhere(x => x.OtherObject == object2);
_flatenedDictionary[object1].Add((object2, distance));
_flatenedDictionary[object2].RemoveWhere(x => x.OtherObject == object1);
_flatenedDictionary[object2].Add((object1, distance));
}

/// <inheritdoc/>
public override IEnumerable<(T OtherObject, float Distance)> GetNearestNeighbors(T inquiredObject, int count) => GetNearestNeighbors(inquiredObject, count, IsNotNaN());

/// <inheritdoc/>
public override IEnumerable<(T OtherObject, float Distance)> GetNearestNeighbors(T inquiredObject, int count, Func<(T OtherObject, float Distance), bool> searchPredicate) =>
_flatenedDictionary.TryGetValue(inquiredObject, out var nearestNeighbors) ? nearestNeighbors.Where(searchPredicate).Take(count) : [];

/// <inheritdoc/>
public override IEnumerable<(T OtherObject, float Distance)> GetNearestNeighborsNormalized(T inquiredObject, int count, float scaleMin = 0f, float scaleMax = 100f) =>
GetNearestNeighborsNormalized(inquiredObject, count, IsNotNaN(), scaleMin, scaleMax);

/// <inheritdoc/>
public override IEnumerable<(T OtherObject, float Distance)> GetNearestNeighborsNormalized(T inquiredObject, int count, Func<(T OtherObject, float Distance), bool> searchPredicate, float scaleMin = 0f, float scaleMax = 100f)
{
if (_flatenedDictionary.TryGetValue(inquiredObject, out var nearestNeighbors))
{
var sourceList = nearestNeighbors.Where(searchPredicate).ToList();
GetRanges(scaleMin, scaleMax, sourceList, out var value, out var scale);
return sourceList.Select(i => (i.OtherObject, Distance: float.IsNaN(i.Distance) ? scale.Min : value.Range == 0f ? scale.Max : (scale.Range * (i.Distance - value.Min) / value.Range) + scale.Min)).Take(count);
}
return [];
}

//Private methods

private static Func<(T OtherObject, float Distance), bool> IsNotNaN() => x => !float.IsNaN(x.Distance);

private static void GetRanges(float scaleMin, float scaleMax, ICollection<(T OtherObject, float Distance)> nearestNeighbors, out (float Min, float Max, float Range) value, out (float Min, float Max, float Range) scale)
{
var numericList = nearestNeighbors.Where(IsNotNaN()).Select(x => x.Distance).ToList();

value = numericList.Count > 0
? numericList.GroupBy(g => 1).Select(g =>
{
float minValue = g.Min(x => x);
float maxValue = g.Max(x => x);
return (Min: minValue, Max: maxValue, Range: maxValue - minValue);
}).FirstOrDefault()
: (Min: float.NaN, Max: float.NaN, Range: float.NaN);
scale = (Min: scaleMin, Max: scaleMax, Range: scaleMax - scaleMin);
}

private T GetObject(MBGUID id) => _cachedMapping.TryGetValue(id, out var obj) && obj is T objT
? objT
: throw new ArgumentException($"Id '{id}' was not found!", nameof(id));
Expand All @@ -81,9 +125,6 @@ private T GetObject(MBGUID id) => _cachedMapping.TryGetValue(id, out var obj) &&
/// <exception cref="T:System.ArgumentException"></exception>
private Dictionary<ulong, float> CalculateDistanceMatrix()
{
if (Campaign.Current?.GetCampaignBehavior<GeopoliticsBehavior>() is null)
return new Dictionary<ulong, float>();

if (_entityListGetter is not null && _distanceCalculator is not null)
{
var entities = _entityListGetter().ToList();
Expand All @@ -94,13 +135,12 @@ private Dictionary<ulong, float> CalculateDistanceMatrix()
.Where(tuple => tuple.X.Id < tuple.Y.Id)
.ToDictionary(
key => ElegantPairHelper.Pair(key.X.Id, key.Y.Id),
value => _distanceCalculator(value.X, value.Y));
value => _distanceCalculator(value.X, value.Y, _distanceCalculatorArgs));
}

if (typeof(Hero).IsAssignableFrom(typeof(T)))
{
var activeHeroes = Hero.AllAliveHeroes
.Where(h => !h.IsNotSpawned && !h.IsDisabled && !h.IsDead && !h.IsChild && !h.IsNotable).ToList();
var activeHeroes = Hero.AllAliveHeroes.Where(h => !h.IsNotSpawned && !h.IsDisabled && !h.IsDead && !h.IsChild && !h.IsNotable).ToList();
_cachedMapping = activeHeroes.ToDictionary(key => key.Id, value => value as MBObjectBase);

return activeHeroes
Expand All @@ -113,19 +153,21 @@ private Dictionary<ulong, float> CalculateDistanceMatrix()

if (typeof(Settlement).IsAssignableFrom(typeof(T)))
{
var settlements = Settlement.All.Where(s => s.IsFortification || s.IsVillage).ToList();
bool considerVillages = DistanceMatrixSubSystem.Instance!.ConsiderVillages;
var settlements = Settlement.All.Where(s => s.IsFortification || (considerVillages && s.IsVillage)).ToList();
_cachedMapping = settlements.ToDictionary(key => key.Id, value => value as MBObjectBase);

return settlements
.SelectMany(_ => settlements, (X, Y) => (X, Y))
.Where(tuple => tuple.X.Id < tuple.Y.Id)
.ToDictionary(
key => ElegantPairHelper.Pair(key.X.Id, key.Y.Id),
value => Campaign.Current.Models.MapDistanceModel.GetDistance(value.X, value.Y));
}

if (typeof(Clan).IsAssignableFrom(typeof(T)))
{
var clans = Clan.All.Where(c => !c.IsEliminated && c.Fiefs.Any()).ToList();
var clans = Clan.All.Where(c => !c.IsEliminated && !c.IsBanditFaction).ToList();
_cachedMapping = clans.ToDictionary(key => key.Id, value => value as MBObjectBase);

var settlementDistanceMatrix = Campaign.Current.GetCampaignBehavior<GeopoliticsBehavior>().SettlementDistanceMatrix ?? new DistanceMatrixImplementation<Settlement>();
Expand All @@ -141,7 +183,7 @@ private Dictionary<ulong, float> CalculateDistanceMatrix()

if (typeof(Kingdom).IsAssignableFrom(typeof(T)))
{
var kingdoms = Kingdom.All.Where(k => !k.IsEliminated && k.Fiefs.Any()).ToList();
var kingdoms = Kingdom.All.Where(k => !k.IsEliminated).ToList();
_cachedMapping = kingdoms.ToDictionary(key => key.Id, value => value as MBObjectBase);

var claDistanceMatrix = Campaign.Current.GetCampaignBehavior<GeopoliticsBehavior>().ClanDistanceMatrix ?? new DistanceMatrixImplementation<Clan>();
Expand All @@ -161,11 +203,12 @@ private Dictionary<ulong, float> CalculateDistanceMatrix()
{
var list = _typedDistanceMatrix.ToList();
var keyList = list.SelectMany(kvp => new[] { kvp.Key.Object1, kvp.Key.Object2 }).Distinct().ToList();

var result = new Dictionary<T, SortedSet<(T OtherObject, float Distance)>>();
keyList.ForEach(key =>
{
var valueList = list.Where(kvp => kvp.Key.Object1 == key || kvp.Key.Object2 == key).Select(kvp => (OtherObject: kvp.Key.Object1 == key ? kvp.Key.Object2 : kvp.Key.Object1, Distance: kvp.Value)).Distinct().ToList();
SortedSet<(T OtherObject, float Distance)> valueSet = new(valueList, new TupleComparer());
SortedSet<(T OtherObject, float Distance)> valueSet = new(valueList, new TupleComparer());
result.Add(key, valueSet);
});
return result;
Expand All @@ -175,7 +218,8 @@ private class TupleComparer : IComparer<(T OtherObject, float Distance)>
{
public int Compare((T OtherObject, float Distance) x, (T OtherObject, float Distance) y)
{
return Comparer<float>.Default.Compare(x.Distance, y.Distance);
int distanceComparison = Comparer<float>.Default.Compare(x.Distance, y.Distance);
return distanceComparison == 0 ? Comparer<uint>.Default.Compare(x.OtherObject.Id.InternalValue, y.OtherObject.Id.InternalValue) : distanceComparison;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ namespace Bannerlord.ButterLib.Implementation.DistanceMatrix;
/// </summary>
internal sealed class DistanceMatrixStaticImplementation : IDistanceMatrixStatic
{
private record DistanceMatrixResultUnpaired(MBGUID OwnerId1, MBGUID OwnerId2, float Distance, float Weight);
public record OwnersDistancePaired(ulong Owners, float Distance, float Weight);
private record OwnersDistanceUnpaired(MBGUID OwnerId1, MBGUID OwnerId2, float Distance, float Weight);

/// <inheritdoc/>
public DistanceMatrix<T> Create<T>() where T : MBObjectBase => new DistanceMatrixImplementation<T>();

/// <inheritdoc/>
public DistanceMatrix<T> Create<T>(Func<IEnumerable<T>> customListGetter, Func<T, T, float> customDistanceCalculator) where T : MBObjectBase =>
new DistanceMatrixImplementation<T>(customListGetter, customDistanceCalculator);
public DistanceMatrix<T> Create<T>(Func<IEnumerable<T>> customListGetter, Func<T, T, object[]?, float> customDistanceCalculator, object[]? distanceCalculatorArgs = null) where T : MBObjectBase =>
new DistanceMatrixImplementation<T>(customListGetter, customDistanceCalculator, distanceCalculatorArgs);

/// <inheritdoc/>
public float CalculateDistanceBetweenHeroes(Hero hero1, Hero hero2)
Expand Down Expand Up @@ -72,13 +73,16 @@ static WeightedDistance WeightedSettlementSelector(KeyValuePair<(Clan object1, C
/// <inheritdoc/>
public Dictionary<ulong, WeightedDistance> GetSettlementOwnersPairedList(DistanceMatrix<Settlement> settlementDistanceMatrix)
{
static DistanceMatrixResultUnpaired FirstSelector(KeyValuePair<(Settlement Object1, Settlement Object2), float> kvp) =>
static OwnersDistanceUnpaired FirstSelector(KeyValuePair<(Settlement Object1, Settlement Object2), float> kvp) =>
new(OwnerId1: kvp.Key.Object1.OwnerClan.Id, OwnerId2: kvp.Key.Object2.OwnerClan.Id, Distance: kvp.Value, Weight: GetSettlementWeight(kvp.Key.Object1) + GetSettlementWeight(kvp.Key.Object2));

static DistanceMatrixResult SecondSelector(DistanceMatrixResultUnpaired x) =>
static OwnersDistancePaired SecondSelector(OwnersDistanceUnpaired x) =>
new(x.OwnerId1 > x.OwnerId2 ? ElegantPairHelper.Pair(x.OwnerId2, x.OwnerId1) : ElegantPairHelper.Pair(x.OwnerId1, x.OwnerId2), x.Distance, x.Weight);

return settlementDistanceMatrix.AsTypedDictionary.Select(FirstSelector).Select(SecondSelector).GroupBy(g => g.Owners).Select(g => new DistanceMatrixResult(g.Key, g.Sum(x => x.Distance * x.Weight), g.Sum(x => x.Weight))).ToDictionary(key => key.Owners, value => new WeightedDistance(value.Distance, value.Weight));
return settlementDistanceMatrix.AsTypedDictionary
.Select(FirstSelector).Select(SecondSelector).GroupBy(g => g.Owners)
.Select(g => new OwnersDistancePaired(g.Key, g.Sum(x => x.Distance * x.Weight), g.Sum(x => x.Weight)))
.ToDictionary(key => key.Owners, value => new WeightedDistance(value.Distance, value.Weight));
}

private static (MobileParty? mobileParty, Settlement? settlement) GetMapPosition(Hero hero) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Bannerlord.ButterLib.SubSystems;
using Bannerlord.ButterLib.SubSystems.Settings;

using System;
using System.Collections.Generic;

namespace Bannerlord.ButterLib.Implementation.DistanceMatrix;

internal class DistanceMatrixSubSystem : ISubSystem
internal class DistanceMatrixSubSystem : ISubSystem, ISubSystemSettings<DistanceMatrixSubSystem>
{
public static DistanceMatrixSubSystem? Instance { get; private set; }

Expand All @@ -15,6 +16,7 @@ internal class DistanceMatrixSubSystem : ISubSystem
public bool CanBeDisabled => true;
public bool CanBeSwitchedAtRuntime => false;
internal bool GameInitialized { get; set; } = false;
public bool ConsiderVillages { get; set; } = true;

public DistanceMatrixSubSystem()
{
Expand All @@ -36,4 +38,12 @@ public void Disable()

IsEnabled = false;
}

public IReadOnlyCollection<SubSystemSettingsDeclaration<DistanceMatrixSubSystem>> Declarations { get; } = new SubSystemSettingsDeclaration<DistanceMatrixSubSystem>[]
{
new SubSystemSettingsPropertyBool<DistanceMatrixSubSystem>(
"{=} Consider Villages",
"{=} Allow villages to be used for built-in distance matrix calculations. Negatively affects performance.",
x => x.ConsiderVillages),
};
}
Loading

0 comments on commit 2584f61

Please sign in to comment.