//
// 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 non-player characters (e.g. merchants) within the specified range of a point.
///
/// The coordinates.
/// The range.
/// The non-player characters in range of the specified coordinate.
public IList GetNpcsInRange(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);
}
}
}