// // 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; } }