diff --git a/src/Perpetuum.Bootstrapper/Modules/ZonesModule.cs b/src/Perpetuum.Bootstrapper/Modules/ZonesModule.cs index c0e1a29c..36ccd98d 100644 --- a/src/Perpetuum.Bootstrapper/Modules/ZonesModule.cs +++ b/src/Perpetuum.Bootstrapper/Modules/ZonesModule.cs @@ -8,6 +8,7 @@ using Perpetuum.Services.EventServices.EventProcessors; using Perpetuum.Services.EventServices.EventProcessors.NpcSpawnEventHandlers; using Perpetuum.Services.HighScores; +using Perpetuum.Services.PathFind; using Perpetuum.Services.Relics; using Perpetuum.Services.RiftSystem; using Perpetuum.Services.Strongholds; @@ -58,6 +59,11 @@ protected override void Load(ContainerBuilder builder) e.Context.Resolve().AddProcess(e.Instance.ToAsync().AsTimed(TimeSpan.FromMinutes(5))); }).As(); + _ = builder.RegisterType().OnActivated(e => + { + e.Context.Resolve().AddProcess(e.Instance.ToAsync().AsTimed(TimeSpan.FromMinutes(5))); + }).As().SingleInstance(); + _ = builder.RegisterType(); _ = builder.RegisterType(); _ = builder.Register>(x => @@ -160,6 +166,7 @@ void RegisterZone(ZoneType type) where T : Zone zone.SafeSpawnPoints = ctx.Resolve(new TypedParameter(typeof(IZone), zone)); zone.ZoneSessionFactory = ctx.Resolve(); zone.RelicManager = ctx.Resolve>().Invoke(zone); + zone.PathFindService = ctx.Resolve(); if (configuration.Terraformable) { diff --git a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs index 2cf07914..01cca5ad 100644 --- a/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs +++ b/src/Perpetuum.Bootstrapper/PerpetuumBootstrapper.cs @@ -67,6 +67,7 @@ using System.Transactions; using IContainer = Autofac.IContainer; using LogEvent = Perpetuum.Log.LogEvent; +using Perpetuum.Zones.NpcSystem.Presences.PathFinders; [assembly: SupportedOSPlatform("windows")] namespace Perpetuum.Bootstrapper @@ -357,6 +358,7 @@ private void InitContainer(string gameRoot) string settingsFile = fileManager.ReadAllText("perpetuum.ini"); GlobalConfiguration configuration = JsonConvert.DeserializeObject(settingsFile); configuration.GameRoot = gameRoot; + RoamingState.Mode = configuration.RoamingMode; return configuration; }).SingleInstance(); diff --git a/src/Perpetuum/GlobalConfiguration.cs b/src/Perpetuum/GlobalConfiguration.cs index 9fd5157a..99ae45f5 100644 --- a/src/Perpetuum/GlobalConfiguration.cs +++ b/src/Perpetuum/GlobalConfiguration.cs @@ -1,4 +1,6 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Perpetuum.Zones.NpcSystem.Presences.PathFinders; using System.ComponentModel; namespace Perpetuum @@ -50,5 +52,17 @@ public class GlobalConfiguration public string DiscordBotToken { get; set; } public string OpHelpChannelId { get; set; } + + // Default roaming mode. + [DefaultValue(RoamingState.RoamingMode.Default)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + [JsonConverter(typeof(StringEnumConverter))] + public RoamingState.RoamingMode RoamingMode { get; set; } + + /// + /// Gets or sets the maximum number of concurrent pathfinding operations allowed. + /// + [DefaultValue(0), JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int ConcurrentPathFinds { get; set; } } } diff --git a/src/Perpetuum/Services/Channels/ChatCommands/AdminCommandHandlers.cs b/src/Perpetuum/Services/Channels/ChatCommands/AdminCommandHandlers.cs index 8448d7b5..7a87d2bc 100644 --- a/src/Perpetuum/Services/Channels/ChatCommands/AdminCommandHandlers.cs +++ b/src/Perpetuum/Services/Channels/ChatCommands/AdminCommandHandlers.cs @@ -5,6 +5,7 @@ using Perpetuum.Services.Weather; using Perpetuum.Zones; using Perpetuum.Zones.Locking.Locks; +using Perpetuum.Zones.NpcSystem.Presences.PathFinders; using Perpetuum.Zones.Teleporting.Strategies; using Perpetuum.Zones.Terrains; using Perpetuum.Zones.Terrains.Materials.Plants; @@ -695,6 +696,28 @@ public static void GetWeather(AdminCommandData data) IZone zone = data.Request.Session.ZoneMgr.GetZone(zoneId); SendMessageToAll(data, $"Weather set {zone.Weather.GetCurrentWeather()}"); } + [ChatCommand("GetRoamingMode")] + public static void GetRoamingMode(AdminCommandData data) + { + SendMessageToAll(data, $"RoamingMode is {RoamingState.Mode}"); + } + [ChatCommand("SetRoamingMode")] + public static void SetRoamingMode(AdminCommandData data) + { + RoamingState.RoamingMode mode = RoamingState.RoamingMode.Default; + bool err = false; + if (!data.Command.Args.IsNullOrEmpty()) + { + err = !RoamingState.RoamingMode.TryParse(data.Command.Args[0], out mode); + } + if (err) + { + SendMessageToAll(data, $"Bad RoamingMode!"); + throw PerpetuumException.Create(ErrorCodes.RequiredArgumentIsNotSpecified); + } + RoamingState.Mode = mode; + SendMessageToAll(data, $"RoamingMode set {RoamingState.Mode}"); + } #endregion #region DevCommands [ChatCommand("ZoneCleanObstacleBlocking")] diff --git a/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs b/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs new file mode 100644 index 00000000..594175a0 --- /dev/null +++ b/src/Perpetuum/Services/PathFind/AStarFinderReusable.cs @@ -0,0 +1,174 @@ +using Perpetuum.Services.PathFind; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Threading; + +namespace Perpetuum.PathFinders +{ + public class AStarFinderReusable + { + public const int W_H = 2048; + + private const int WEIGHT = 10; + private const int WEIGHT_DIAG = (int)(1.41421356237 * WEIGHT); // sqrt(2) * WEIGHT + private readonly PriorityQueue _openList = new PriorityQueue(); + private readonly uint[] _closedList = new uint[W_H * W_H]; + + private uint _currentStamp = 0; + + /// + /// Finds a path using the A* algorithm based on the provided pathfinding information. + /// + public Point[]? FindPathHome(IPathFindInfo pathFindInfo, CancellationToken cancellationToken) + { + var start = pathFindInfo.Start; + var end = pathFindInfo.End; + var heuristic = pathFindInfo.Heuristic; + var passable = pathFindInfo.Passable; + + // basic validation + if (start.X < 0 || start.Y < 0 || end.X < 0 || end.Y < 0 || start.X >= W_H || start.Y >= W_H || end.X >= W_H || end.Y >= W_H) + { + return null; + } + + if (!passable(end.X, end.Y)) + { + return null; + } + + if (start == end) + { + return Array.Empty(); + } + + _openList.Clear(); + if (++_currentStamp >= 1024) + { + _currentStamp = 1; + Array.Clear(_closedList); + } + + var startNode = new Node((uint)start.X, (uint)start.Y, 0); + _openList.Enqueue(startNode, heuristic.Calculate(start.X, start.Y, end.X, end.Y) * WEIGHT); + _closedList[(int)startNode.HashCode] = (_currentStamp << 22) | startNode.HashCode; + + while (_openList.TryDequeue(out Node node, out _) && !cancellationToken.IsCancellationRequested) + { + if (node.X == end.X && node.Y == end.Y) + { + var result = Backtrace(node, _closedList); + TestPath(result, start, end); + return result; + } + + foreach (var neighbor in GetNeighbors(node, passable)) + { + int idx = (int)neighbor.HashCode; + var stamp = _closedList[idx] >> 22; + + // already processed this stamp -> skip + if (stamp == _currentStamp) + continue; + + // set parent and mark with current stamp + _closedList[idx] = (_currentStamp << 22) | node.HashCode; + var priority = neighbor.G + heuristic.Calculate((int)neighbor.X, (int)neighbor.Y, end.X, end.Y) * WEIGHT; + _openList.Enqueue(neighbor, priority); + } + } + + return null; + } + + [Conditional("DEBUG")] + private static void TestPath(Point[] path, Point start, Point end) + { + if (path.Length < 2) + { + throw new InvalidOperationException($"[AStarFinderReusable] Pathfinding logic error: less than 2 nodes in path"); + } + if (path[0] != start || path[^1] != end) + { + throw new InvalidOperationException($"[AStarFinderReusable] Pathfinding logic error: path does not connect start and end"); + } + for (int i = 0; i < path.Length - 1; i++) + { + var dx = Math.Abs(path[i].X - path[i + 1].X); + var dy = Math.Abs(path[i].Y - path[i + 1].Y); + if (dx > 1 || dy > 1) + { + throw new InvalidOperationException($"[AStarFinderReusable] Pathfinding logic error: move bigger than 1 cell"); + } + } + } + + private static Point[] Backtrace(Node node, uint[] list) + { + var stack = new Stack(); + + int hash = (int)(node.HashCode & 0x3FFFFF); + while (true) + { + int x = hash & 0x7FF; + int y = (hash >> 11) & 0x7FF; + stack.Push(new Point(x, y)); + + uint parent = list[hash] & 0x3FFFFF; + if (parent == (uint)hash) + break; + + hash = (int)parent; + } + + return stack.ToArray(); + } + + protected static readonly sbyte[,] _n = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { -1, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 } }; + + private static IEnumerable GetNeighbors(Node node, Func passable) + { + for (var i = 0; i < 8; i++) + { + var dx = _n[i, 0]; + var dy = _n[i, 1]; + + int nx = (int)node.X + dx; + int ny = (int)node.Y + dy; + + if (nx < 0 || ny < 0 || nx >= W_H || ny >= W_H) + continue; + + if (!passable(nx, ny)) + continue; + + int cost = node.G + ((dx == 0 || dy == 0) ? WEIGHT : WEIGHT_DIAG); + yield return new Node((uint)nx, (uint)ny, cost); + } + } + + protected class Node + { + private readonly uint _hashCode; + private readonly int _g; + + public uint HashCode => _hashCode; + public uint X => _hashCode & 0x7FF; + public uint Y => (_hashCode >> 11) & 0x7FF; + public int G => _g; + + public Node(uint x, uint y, int g) + { + _hashCode = (y << 11) | x; + _g = g; + } + + public override string ToString() + { + return $"({X}, {Y}), G: {G}"; + } + } + } +} diff --git a/src/Perpetuum/Services/PathFind/IPathFindInfo.cs b/src/Perpetuum/Services/PathFind/IPathFindInfo.cs new file mode 100644 index 00000000..101c52da --- /dev/null +++ b/src/Perpetuum/Services/PathFind/IPathFindInfo.cs @@ -0,0 +1,31 @@ +using Perpetuum.PathFinders; +using System.Drawing; + +namespace Perpetuum.Services.PathFind +{ + /// + /// Interface for providing pathfinding information to the A* algorithm. + /// + public interface IPathFindInfo + { + /// + /// Gets the starting point of the path. + /// + Point Start { get; } + /// + /// Gets the ending point of the path. + /// + Point End { get; } + /// + /// Gets the heuristic function for pathfinding. + /// + Heuristic Heuristic { get; } + + /// + /// Gets the function that determines if a cell is passable. + /// + Func Passable { get; } + + int ZoneId { get; } + } +} diff --git a/src/Perpetuum/Services/PathFind/IPathFindService.cs b/src/Perpetuum/Services/PathFind/IPathFindService.cs new file mode 100644 index 00000000..986781e4 --- /dev/null +++ b/src/Perpetuum/Services/PathFind/IPathFindService.cs @@ -0,0 +1,7 @@ +namespace Perpetuum.Services.PathFind +{ + public interface IPathFindService + { + void EnqueuePathFinding(IPathFindInfo pathFindInfo); + } +} diff --git a/src/Perpetuum/Services/PathFind/PathFindInfo.cs b/src/Perpetuum/Services/PathFind/PathFindInfo.cs new file mode 100644 index 00000000..32c8acd9 --- /dev/null +++ b/src/Perpetuum/Services/PathFind/PathFindInfo.cs @@ -0,0 +1,47 @@ +using Perpetuum.PathFinders; +using System; +using System.Drawing; + +namespace Perpetuum.Services.PathFind +{ + public class PathFindInfo : IPathFindInfo + { + private readonly Point _start; + private readonly Point _end; + private readonly Heuristic _heuristic; + private readonly Func _passable; + private readonly int _zoneId; + + public PathFindInfo(Point start, Point end, Heuristic heuristic, Func passable, int zoneId) + { + _start = start; + _end = end; + _heuristic = heuristic; + _passable = passable; + _zoneId = zoneId; + } + + public Point Start => _start; + + public Point End => _end; + + public Heuristic Heuristic => _heuristic; + + public Func Passable => _passable; + + public int ZoneId => _zoneId; + } + + public class HomePathFindInfo : PathFindInfo + { + private readonly Action _onPathFound; + + public HomePathFindInfo(Point start, Point end, Heuristic heuristic, Func passable, int zoneId, Action onPathFound) + : base(start, end, heuristic, passable, zoneId) + { + _onPathFound = onPathFound ?? (_ => { }); + } + + public Action OnPathFound => _onPathFound; + } +} diff --git a/src/Perpetuum/Services/PathFind/PathFindService.cs b/src/Perpetuum/Services/PathFind/PathFindService.cs new file mode 100644 index 00000000..c4a7c060 --- /dev/null +++ b/src/Perpetuum/Services/PathFind/PathFindService.cs @@ -0,0 +1,193 @@ +using Perpetuum.Log; +using Perpetuum.PathFinders; +using Perpetuum.Threading.Process; +using Perpetuum.Zones.NpcSystem; +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Perpetuum.Services.PathFind +{ + public class PathFindService : Process, IPathFindService + { + private CancellationTokenSource _cancellationTokenSource; // Token source for managing cancellation of pathfinding tasks + private readonly int _maxConcurrentPathFinds; // Maximum number of concurrent pathfinding operations + private Task[] _pathFindingTasks; // Array to hold the pathfinding tasks + private int _currentWorkers; // Counter for the current number of concurrent pathfinding operations + + private readonly Channel _pathFindingQueue = Channel.CreateBounded(10000); + + + // approximate queued items counter because ChannelReader does not expose Count in all runtimes + private readonly object _lock = new object(); + private int _count; + + private int _depthMax; + private int _canceledCount; + private int _enqueuedCount; + private int _safeCount; + + public PathFindService(GlobalConfiguration globalConfiguration) + { + if (globalConfiguration.ConcurrentPathFinds <= 0) + { + _maxConcurrentPathFinds = Math.Max(1, Environment.ProcessorCount / 2 - 1); // Default to the number of logical processors if not set + } + else + { + _maxConcurrentPathFinds = globalConfiguration.ConcurrentPathFinds; + } + } + + private int _start = 0; + + public override void Start() + { + if (Interlocked.CompareExchange(ref _start, 1, 0) != 0) + { + return; // Already started + } + + base.Start(); + + _cancellationTokenSource = new CancellationTokenSource(); + _pathFindingTasks = new Task[_maxConcurrentPathFinds]; + for (var i = 0; i < _maxConcurrentPathFinds; i++) + { + var index = i; // Capture the current index for the task + _pathFindingTasks[i] = ProcessPathFindingQueue(index, _cancellationTokenSource.Token); + } + } + + public override void Stop() + { + if (Interlocked.CompareExchange(ref _start, 0, 1) != 1) + { + Logger.Warning("[PathFindService] PathFindService is not running."); + return; // Already stopped + } + + _pathFindingQueue.Writer.Complete(); // Signal that no more items will be written to the queue + _cancellationTokenSource.Cancel(); + Task.WaitAll(_pathFindingTasks, CancellationToken.None); // Wait for all pathfinding tasks to complete + _cancellationTokenSource.Dispose(); + + base.Stop(); + } + + public void EnqueuePathFinding(IPathFindInfo pathFindInfo) + { + if (_pathFindingQueue.Writer.TryWrite(pathFindInfo)) + { + lock (_lock) + { + _enqueuedCount++; + if (++_count > _depthMax) + { + _depthMax = _count; + } + } + } + else + { + lock (_lock) + { + _canceledCount++; + } + } + } + + private async Task ProcessPathFindingQueue(int index, CancellationToken cancellationToken) + { + Logger.Info($"[PathFindService] Pathfinding worker started. Index: {index}"); + Interlocked.Increment(ref _currentWorkers); + var finder = new AStarFinderReusable(); + + try + { + while (await _pathFindingQueue.Reader.WaitToReadAsync(cancellationToken)) + { + while (_pathFindingQueue.Reader.TryRead(out var pathFindInfo)) + { + // decrement queued counter for each consumed item + lock(_lock) + { + _count--; + } + + if (pathFindInfo is HomePathFindInfo homePathFindInfo) + { + var result = finder.FindPathHome(homePathFindInfo, cancellationToken); + + if (result != null) + { + homePathFindInfo.OnPathFound?.Invoke(result); + continue; + } + + Logger.DebugInfo($"[PathFindService] Path not found for HomePathFindInfo. ({homePathFindInfo.Start}) => ({homePathFindInfo.End}) Zone: {pathFindInfo.ZoneId}"); + + lock(_lock) + { + _safeCount++; + } + + var directPathFindInfo = new PathFindInfo( + homePathFindInfo.Start, + homePathFindInfo.End, + homePathFindInfo.Heuristic, + (x, y) => true, + homePathFindInfo.ZoneId + ); + + result = finder.FindPathHome(directPathFindInfo, cancellationToken); + if (result == null) + { + Logger.DebugWarning($"[PathFindService] Safe path not found for HomePathFindInfo. ({homePathFindInfo.Start}) => ({homePathFindInfo.End}) Zone: {pathFindInfo.ZoneId}"); + } + else + { + homePathFindInfo.OnPathFound?.Invoke(result); + } + } + else + { + Logger.Warning($"[PathFindService] Unknown pathfinding info type. Zone: {pathFindInfo.ZoneId}"); + } + } + } + } + catch (OperationCanceledException) + { + Logger.Info($"[PathFindService] Pathfinding worker canceled. Index: {index}"); + } + catch (Exception ex) + { + Logger.Exception(ex); + } + finally + { + Logger.Info($"[PathFindService] Pathfinding worker stopped. Index: {index}"); + Interlocked.Decrement(ref _currentWorkers); + } + } + + public override void Update(TimeSpan time) + { + lock(_lock) + { + if (_enqueuedCount > 0 || _depthMax > 0 || _canceledCount > 0 || _safeCount > 0) + { + var status = _currentWorkers == _maxConcurrentPathFinds ? "OK" : "WARNING"; + + Logger.Info($"[PathFindService] {status}:{_currentWorkers}/{_maxConcurrentPathFinds} Total:{_enqueuedCount} Safe:{_safeCount} Depth:{_depthMax} Canceled:{_canceledCount}"); + _enqueuedCount = 0; + _safeCount = 0; + _depthMax = 0; + _canceledCount = 0; + } + } + } + } +} diff --git a/src/Perpetuum/Zones/IZone.cs b/src/Perpetuum/Zones/IZone.cs index 4a40afd9..f413ef5f 100644 --- a/src/Perpetuum/Zones/IZone.cs +++ b/src/Perpetuum/Zones/IZone.cs @@ -5,6 +5,7 @@ using Perpetuum.Groups.Corporations; using Perpetuum.Log; using Perpetuum.Players; +using Perpetuum.Services.PathFind; using Perpetuum.Services.Relics; using Perpetuum.Services.Strongholds; using Perpetuum.Services.Weather; @@ -33,6 +34,7 @@ public interface IZone IEnumerable Units { get; } IEnumerable Players { get; } + bool FreeFromPlayers { get; } [CanBeNull] Unit GetUnit(long eid); @@ -61,6 +63,8 @@ public interface IZone ILogger ChatLogger { get; } + IPathFindService PathFindService { get; } + void AddUnit(Unit unit); void RemoveUnit(Unit unit); void SetGang(Player player); diff --git a/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs b/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs index 63a2dcd1..6351daed 100644 --- a/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs +++ b/src/Perpetuum/Zones/NpcSystem/AI/HomingAI.cs @@ -1,5 +1,6 @@ using Perpetuum.Modules; using Perpetuum.PathFinders; +using Perpetuum.Services.PathFind; using Perpetuum.Zones.Movements; using System; using System.Collections.Generic; @@ -11,12 +12,10 @@ public class HomingAI : BaseAI { private PathMovement movement; private readonly double maxReturnHomeRadius; - private readonly PathFinder pathFinder; public HomingAI(SmartCreature smartCreature) : base(smartCreature) { maxReturnHomeRadius = (smartCreature.HomeRange * 0.4).Clamp(3, 20); - pathFinder = new AStarFinder(Heuristic.Manhattan, smartCreature.IsWalkable); } public override void Enter() @@ -29,29 +28,19 @@ public override void Enter() randomHome = smartCreature.HomePosition; } - _ = pathFinder - .FindPathAsync(smartCreature.CurrentPosition, randomHome) - .ContinueWith(t => + smartCreature.Zone.PathFindService.EnqueuePathFinding(new HomePathFindInfo + ( + smartCreature.CurrentPosition, + randomHome, + Heuristic.Manhattan, + smartCreature.IsWalkable, + smartCreature.Zone.Id, + path => { - System.Drawing.Point[] path = t.Result; - - if (path == null) - { - WriteLog("Path not found! (" + smartCreature.CurrentPosition + " => " + smartCreature.HomePosition + ")"); - - AStarFinder f = new AStarFinder(Heuristic.Manhattan, (x, y) => true); - - path = f.FindPath(smartCreature.CurrentPosition, smartCreature.HomePosition); - - if (path == null) - { - WriteLog("Safe path not found! (" + smartCreature.CurrentPosition + " => " + smartCreature.HomePosition + ")"); - } - } - movement = new PathMovement(path); movement.Start(smartCreature); - }); + } + )); base.Enter(); } diff --git a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs index 9f053396..c9378515 100644 --- a/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs +++ b/src/Perpetuum/Zones/NpcSystem/Presences/PathFinders/RoamingState.cs @@ -7,6 +7,16 @@ namespace Perpetuum.Zones.NpcSystem.Presences.PathFinders { public class RoamingState : NullRoamingState { + public enum RoamingMode + { + Default = 0, + Off, + Player, + Timer + } + + public static RoamingMode Mode = RoamingMode.Default; + public RoamingState(IRoamingPresence presence) : base(presence) { } @@ -30,6 +40,26 @@ public override void Update(TimeSpan time) return; } + switch (Mode) + { + case RoamingMode.Off: + return; + case RoamingMode.Player: + if (_presence.Zone.Players.IsNullOrEmpty()) + { + return; + } + break; + case RoamingMode.Timer: + if (_presence.Zone.FreeFromPlayers) + { + return; + } + break; + default: + break; + } + RunTask(() => FindNextRoamingPosition(), t => { }); } diff --git a/src/Perpetuum/Zones/Zone.cs b/src/Perpetuum/Zones/Zone.cs index 099e66ab..eb85a930 100644 --- a/src/Perpetuum/Zones/Zone.cs +++ b/src/Perpetuum/Zones/Zone.cs @@ -7,6 +7,7 @@ using Perpetuum.Network; using Perpetuum.Players; using Perpetuum.Services.HighScores; +using Perpetuum.Services.PathFind; using Perpetuum.Services.Relics; using Perpetuum.Services.RiftSystem; using Perpetuum.Services.Sessions; @@ -64,6 +65,7 @@ public abstract class Zone : Threading.Process.Process, IZone public HarvestLogHandler HarvestLogHandler { get; set; } public ZoneSession.Factory ZoneSessionFactory { get; set; } public IZoneEffectHandler ZoneEffectHandler { get; set; } + public IPathFindService PathFindService { get; set; } [CanBeNull] public IRiftManager RiftManager { private get; set; } @@ -315,6 +317,9 @@ private void OnUnitUpdated(Unit unit, UnitUpdatedEventArgs e) public IEnumerable Players => _players.Values; + private readonly IntervalTimer _timerLastPlayer = new IntervalTimer(TimeSpan.FromMinutes(15), false); + public bool FreeFromPlayers { get; set; } = true; + public Unit GetUnit(long eid) { return _units.GetValueOrDefault(eid); @@ -342,6 +347,7 @@ private void MeasureUpdate(TimeSpan time) public override void Update(TimeSpan time) { + UpdatePlayerPresence(time); UpdateSessions(time); _updateUnitsTimer.Update(time).IsPassed(ProcessUpdatedUnits); @@ -355,6 +361,42 @@ public override void Update(TimeSpan time) MeasureUpdate(time); } + /// + /// Checks for the presence of players on the field. + /// + /// + private void UpdatePlayerPresence(TimeSpan time) + { + if (FreeFromPlayers) + { + // There are no players, but if someone comes out, we’ll change the flag. + if (!Players.IsNullOrEmpty()) + { + _timerLastPlayer.Reset(); + Logger.Info($"Zone: {Id} - player enter o/"); + FreeFromPlayers = false; + } + } + else + { + // We get here if the flag indicates that there are players. + if (Players.IsNullOrEmpty()) + { + // If no one is around, time is ticking away. + _timerLastPlayer.Update(time).IsPassed(() => + { + Logger.Info($"Zone: {Id} - free from players."); + FreeFromPlayers = true; + }); + } + else + { + // If someone is on the field, we reset the timer. + _timerLastPlayer.Reset(); + } + } + } + private void UpdateUnits(TimeSpan time) { foreach (KeyValuePair kvp in _units)