// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic; using System.Collections.Concurrent; using System.Threading; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlayerActions; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.World; using MUnique.OpenMU.Pathfinding; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.PlugIns; using Nito.AsyncEx; /// /// The game map which contains instances of players, npcs, drops, and more. /// public class GameMap { private readonly IDictionary _objectsInMap = new ConcurrentDictionary(); private readonly IAreaOfInterestManager _areaOfInterestManager; private readonly IdGenerator _objectIdGenerator; private readonly IdGenerator _dropIdGenerator; private readonly ExitGate? _safezoneSpawnGate; private int _playerCount; /// /// Initializes a new instance of the class. /// /// The map definition. /// Duration of the item drop. /// Size of the chunk. public GameMap(GameMapDefinition mapDefinition, TimeSpan itemDropDuration, byte chunkSize) { this.Id = Guid.NewGuid(); this.Definition = mapDefinition; this.ItemDropDuration = itemDropDuration; this.Terrain = new GameMapTerrain(this.Definition); this._areaOfInterestManager = new BucketAreaOfInterestManager(chunkSize); this._objectIdGenerator = new IdGenerator(ViewExtensions.ConstantPlayerId + 1, 0x7FFF); this._dropIdGenerator = new IdGenerator(0, ViewExtensions.ConstantPlayerId - 1); this._safezoneSpawnGate = this.Definition.GetSafezoneGate(this.Terrain); } /// /// Occurs when an object was added to the map. /// public event AsyncEventHandler<(GameMap Map, ILocateable Object)>? ObjectAdded; /// /// Occurs when an object was removed from the map. /// public event AsyncEventHandler<(GameMap Map, ILocateable Object)>? ObjectRemoved; /// /// Gets the map identifier. /// public ushort MapId => this.Definition.Number.ToUnsigned(); /// /// Gets the terrain of the map. /// public GameMapTerrain Terrain { get; } /// /// Gets the safe zone spawn gate. /// public ExitGate? SafeZoneSpawnGate => this._safezoneSpawnGate; /// /// Gets the duration about how long drops are laying on the ground until they are disappearing. /// public TimeSpan ItemDropDuration { get; } /// /// Gets the definition of the map. /// public GameMapDefinition Definition { get; } /// /// Gets the unique identifier of this map instance. /// public Guid Id { get; } /// /// Gets the object with the specified identifier. /// /// The identifier. /// The object with the specified identifier. public ILocateable? GetObject(ushort id) { this._objectsInMap.TryGetValue(id, out var result); return result; } // ADAMU-CUSTOM: remote NPC ("NPC list" mobile feature) — spawn olmuş NPC'yi definition number ile bul. /// /// Gets a spawned non-player-character on this map by its definition number. /// Used to open an NPC's window remotely (mobile "NPC list" feature), independent /// of the player's position. /// /// The . /// The first matching non-player-character, or null. public NonPlayerCharacter? GetNpcByNumber(short number) { return this._objectsInMap.Values .OfType() .FirstOrDefault(npc => npc.Definition.Number == number); } // ADAMU-CUSTOM end /// /// Gets the attackables in range of the specified coordinates. /// /// The coordinates. /// The range. /// The attackables in range of the specified coordinate. public IList GetAttackablesInRange(Point point, int range) { return this._areaOfInterestManager.GetInRange(point, range).OfType().ToList(); } /// /// Gets all dropped items and money within the specified range of a point. /// /// The coordinates. /// The range. /// Dropped items and money in range. public IList GetDropsInRange(Point point, int range) { return this._areaOfInterestManager.GetInRange(point, range) .Where(l => l is DroppedItem or DroppedMoney) .ToList(); } /// /// Gets the drop by id. /// /// The drop identifier. /// The dropped item. public ILocateable? GetDrop(ushort dropId) { this._objectsInMap.TryGetValue(dropId, out var item); return item; } /// /// Removes the locateable from the map. /// /// The locateable. public async ValueTask RemoveAsync(ILocateable locateable) { await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false); if (this._objectsInMap.Remove(locateable.Id) && locateable.Id != 0) { if (locateable is DroppedItem || locateable is DroppedMoney) { this._dropIdGenerator.GiveBack(locateable.Id); } else { this._objectIdGenerator.GiveBack(locateable.Id); } if (locateable is Player player) { player.Id = 0; Interlocked.Decrement(ref this._playerCount); } if (this.ObjectRemoved is { } eventHandler) { await eventHandler((this, locateable)).ConfigureAwait(false); } } } /// /// Adds the locateable to the map. /// /// The locateable object. public async ValueTask AddAsync(ILocateable locateable) { if (!this._objectsInMap.TryGetValue(locateable.Id, out var existing) || existing != locateable) { switch (locateable) { case DroppedItem droppedItem: droppedItem.Id = (ushort)this._dropIdGenerator.GenerateId(); break; case DroppedMoney droppedMoney: droppedMoney.Id = (ushort)this._dropIdGenerator.GenerateId(); break; case Player player: player.Id = (ushort)this._objectIdGenerator.GenerateId(); Interlocked.Increment(ref this._playerCount); break; case NonPlayerCharacter npc: npc.Id = (ushort)this._objectIdGenerator.GenerateId(); break; case ISupportIdUpdate idUpdate: idUpdate.Id = (ushort)this._objectIdGenerator.GenerateId(); break; default: throw new ArgumentException($"Adding an object of type {locateable.GetType()} is not supported."); } this._objectsInMap.Add(locateable.Id, locateable); } await this._areaOfInterestManager.AddObjectAsync(locateable).ConfigureAwait(false); if (this.ObjectAdded is { } eventHandler) { await eventHandler((this, locateable)).ConfigureAwait(false); } } /// /// Moves the locatable on the map. /// /// The monster. /// The new coordinates. /// The move lock. /// Type of the move. public ValueTask MoveAsync(ILocateable locatable, Point target, AsyncLock moveLock, MoveType moveType) { return this._areaOfInterestManager.MoveObjectAsync(locatable, target, moveLock, moveType); } /// /// Initializes a respawn for the specified locateable. /// /// The locateable. public async ValueTask InitRespawnAsync(ILocateable locateable) { await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false); } /// /// Respawns the specified locateable. /// /// The locateable. public async ValueTask RespawnAsync(ILocateable locateable) { await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false); await this._areaOfInterestManager.AddObjectAsync(locateable).ConfigureAwait(false); } /// /// Clears event NPCs. /// public async ValueTask ClearEventSpawnedNpcsAsync() { var eventMonsters = this._objectsInMap.Values .OfType() .Where(n => n.SpawnArea.SpawnTrigger is not SpawnTrigger.Automatic) .ToList(); foreach (var monster in eventMonsters) { await monster.CurrentMap.RemoveAsync(monster).ConfigureAwait(false); monster.Dispose(); } } /// /// Clears the drops on invalid terrain. /// public async ValueTask ClearDropsOnInvalidTerrainAsync() { var drops = this._objectsInMap.Values .OfType() .Where(d => !this.Terrain.WalkMap[d.Position.X, d.Position.Y]) .ToList(); foreach (var drop in drops) { await drop.DisposeAsync().ConfigureAwait(false); } } }