//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Pathfinding;
using Nito.AsyncEx;
///
/// A area of interest manager which works with a bucket map.
///
internal class BucketAreaOfInterestManager : IAreaOfInterestManager
{
///
/// Initializes a new instance of the class.
///
/// Size of the chunk.
public BucketAreaOfInterestManager(int chunkSize)
{
this.Map = new BucketMap(0x100, true, chunkSize / 2, chunkSize);
}
///
/// Gets the underlying bucket map.
///
protected BucketMap Map { get; }
///
public async ValueTask AddObjectAsync(ILocateable obj)
{
var newBucket = this.Map[obj.Position];
if (obj is IHasBucketInformation bucketInfo)
{
bucketInfo.OldBucket = null;
bucketInfo.NewBucket = newBucket;
}
await newBucket.AddAsync(obj).ConfigureAwait(false);
if (obj is IBucketMapObserver observingPlayer)
{
await this.UpdateObservingBucketsAsync(obj.Position, observingPlayer).ConfigureAwait(false);
}
}
///
///
/// This needs to take into account, that the might change the ,
/// but not updating the buckets. So accessing the bucket of the current coordinates might not be the current bucket!.
///
public async ValueTask RemoveObjectAsync(ILocateable obj)
{
if (obj is IHasBucketInformation bucketInfo)
{
// we remove the object from all known buckets and set old/new bucket to null
var oldBucket = bucketInfo.OldBucket;
var newBucket = bucketInfo.NewBucket;
bucketInfo.OldBucket = newBucket;
bucketInfo.NewBucket = null;
newBucket?.RemoveAsync(obj);
bucketInfo.OldBucket = newBucket;
oldBucket?.RemoveAsync(obj);
bucketInfo.OldBucket = null;
}
else
{
await this.Map[obj.Position].RemoveAsync(obj).ConfigureAwait(false);
}
if (obj is IBucketMapObserver observingPlayer)
{
foreach (var bucket in observingPlayer.ObservingBuckets)
{
bucket.ItemAdded -= observingPlayer.LocateableAddedAsync;
bucket.ItemRemoved -= observingPlayer.LocateableRemovedAsync;
}
if (observingPlayer.ObservingBuckets.Any(b => b.Count > 0))
{
await observingPlayer.LocateablesOutOfScopeAsync(observingPlayer.ObservingBuckets.SelectMany(o => o)).ConfigureAwait(false);
}
observingPlayer.ObservingBuckets.Clear();
}
}
///
public async ValueTask MoveObjectAsync(ILocateable obj, Point target, AsyncLock moveLock, MoveType moveType)
{
var differentBucket = await this.MoveObjectOnMapAsync(obj, target, moveLock, moveType).ConfigureAwait(false);
if (obj is IObservable observable)
{
await observable.ForEachWorldObserverAsync(p => p.ObjectMovedAsync(obj, moveType), true).ConfigureAwait(false);
}
var observingPlayer = obj as IBucketMapObserver;
if (differentBucket && observingPlayer != null)
{
await this.UpdateObservingBucketsAsync(target, observingPlayer).ConfigureAwait(false);
}
}
///
public IEnumerable GetInRange(Point point, int range)
{
return this.Map.GetInRange(point, range);
}
///
/// Updates the observing buckets, and notifies the about new objects which are in scope, and old objects which are out of scope.
///
/// The new coordinates on the map.
/// The player.
protected async ValueTask UpdateObservingBucketsAsync(Point newPoint, IBucketMapObserver player)
{
var curbuckets = this.Map.GetBucketsInRange(newPoint, player.InfoRange).ToList(); // All buckets in range
var oldbuckets = player.ObservingBuckets.Where(i => !curbuckets.Contains(i)).ToList(); // Buckets which are not meant to be observed anymore
var newbuckets = curbuckets.Where(i => !player.ObservingBuckets.Contains(i)).ToList(); // New buckets for observation
oldbuckets.ForEach(i =>
{
i.ItemAdded -= player.LocateableAddedAsync;
i.ItemRemoved -= player.LocateableRemovedAsync;
});
newbuckets.ForEach(i =>
{
i.ItemAdded += player.LocateableAddedAsync;
i.ItemRemoved += player.LocateableRemovedAsync;
});
oldbuckets.ForEach(b => player.ObservingBuckets.Remove(b));
newbuckets.ForEach(player.ObservingBuckets.Add);
if (oldbuckets.Any(b => b.Count > 0))
{
await player.LocateablesOutOfScopeAsync(oldbuckets.SelectMany(o => o)).ConfigureAwait(false);
}
if (newbuckets.Any(b => b.Count > 0))
{
await player.NewLocateablesInScopeAsync(newbuckets.SelectMany(o => o)).ConfigureAwait(false);
}
}
private async ValueTask MoveObjectOnMapAsync(ILocateable obj, Point target, AsyncLock moveLock, MoveType moveType)
{
if (moveType == MoveType.Walk)
{
if (obj is ISupportWalk supportWalk)
{
target = supportWalk.WalkTarget;
}
}
var differentBucket = obj.Position.X / this.Map.BucketSideLength != target.X / this.Map.BucketSideLength
|| obj.Position.Y / this.Map.BucketSideLength != target.Y / this.Map.BucketSideLength;
if (!differentBucket)
{
if (moveType != MoveType.Walk)
{
obj.Position = target;
}
return false;
}
using (await moveLock.LockAsync())
{
var oldPosition = obj.Position;
Bucket? oldBucket;
Bucket newBucket = this.Map[target];
if (obj is IHasBucketInformation bucketInfo)
{
oldBucket = bucketInfo.NewBucket;
bucketInfo.NewBucket = newBucket;
bucketInfo.OldBucket = oldBucket;
}
else
{
oldBucket = this.Map[oldPosition];
}
// only set x and y of the object if it's not walking - the Walker sets these coordinates!
if (moveType != MoveType.Walk)
{
obj.Position = target;
}
if (oldBucket is not null)
{
await oldBucket.RemoveAsync(obj).ConfigureAwait(false);
}
await newBucket.AddAsync(obj).ConfigureAwait(false);
}
return true;
}
}