diff --git a/GameServer/ECS-Components/EffectListComponent.cs b/GameServer/ECS-Components/EffectListComponent.cs index 3dfb6cb85..e35b78d61 100644 --- a/GameServer/ECS-Components/EffectListComponent.cs +++ b/GameServer/ECS-Components/EffectListComponent.cs @@ -479,7 +479,9 @@ static void OnEffectAddedOrEnabled(EffectListComponent component, AddEffectResul if (start) effect.OnStartEffect(); - ServiceObjectStore.Add(effect); + if (effect.Duration > 0) + ServiceObjectStore.Schedule(effect, effect.GetNextTick()); + effect.Owner.effectListComponent.RequestPlayerUpdate(EffectHelper.GetPlayerUpdateFromEffect(effect.EffectType)); // Animations must be sent after calling `OnStartEffect` to prevent interrupts from interfering with them. @@ -670,7 +672,10 @@ private AddEffectResult AddOrEnableEffectInternal(ECSGameEffect effect) { ServiceObjectStore.Remove(existingEffect); existingEffect.IsBeingReplaced = true; // Will be checked by the parent pulse effect so that it doesn't call `Stop` on it. - ServiceObjectStore.Add(effect); + + if (effect.Duration > 0) + ServiceObjectStore.Schedule(effect, effect.GetNextTick()); + existingEffects[i] = effect; result = existingEffect.IsActive ? AddEffectResult.RenewedActive : AddEffectResult.RenewedDisabled; } diff --git a/GameServer/ECS-Effects/CrowdControlECSEffect.cs b/GameServer/ECS-Effects/CrowdControlECSEffect.cs index 11024aee6..a13fc376c 100644 --- a/GameServer/ECS-Effects/CrowdControlECSEffect.cs +++ b/GameServer/ECS-Effects/CrowdControlECSEffect.cs @@ -42,7 +42,7 @@ protected void OnHardCCStop() // Re-schedule the next think so that the NPC can resume its attack immediately for example. if (Owner is GameNPC npc && npc.Brain is ABrain brain) - brain.NextThinkTick = GameLoop.GameLoopTime; + brain.Schedule(GameLoop.GameLoopTime); if (SpellHandler.Caster.Realm == 0 || Owner.Realm == 0) Owner.LastAttackedByEnemyTickPvE = GameLoop.GameLoopTime; diff --git a/GameServer/ECS-Effects/ECSGameAbilityEffect.cs b/GameServer/ECS-Effects/ECSGameAbilityEffect.cs index 3e074aee1..c69959ce7 100644 --- a/GameServer/ECS-Effects/ECSGameAbilityEffect.cs +++ b/GameServer/ECS-Effects/ECSGameAbilityEffect.cs @@ -8,5 +8,10 @@ public class ECSGameAbilityEffect : ECSGameEffect, IPooledList 0 ? NextTick : base.GetNextTick(); + } } -} \ No newline at end of file +} diff --git a/GameServer/ECS-Effects/ECSGameEffect.cs b/GameServer/ECS-Effects/ECSGameEffect.cs index ca64da96e..5139dc628 100644 --- a/GameServer/ECS-Effects/ECSGameEffect.cs +++ b/GameServer/ECS-Effects/ECSGameEffect.cs @@ -8,7 +8,7 @@ namespace DOL.GS { - public abstract class ECSGameEffect : IServiceObject, IPooledList + public abstract class ECSGameEffect : IShardedServiceObject, IPooledList { private State _state; private TransitionalState _transitionalState; @@ -34,7 +34,6 @@ public abstract class ECSGameEffect : IServiceObject, IPooledList public bool TriggersImmunity { get; set; } public int ImmunityDuration { get; protected set; } = 60000; public bool IsBeingReplaced { get; set; } // Used externally to force an effect to be silent (no message, no immunity) when being refreshed. - public ServiceObjectId ServiceObjectId { get; } = new(ServiceObjectType.Effect); // State properties. public bool IsActive => _state is State.Active; @@ -54,6 +53,12 @@ public abstract class ECSGameEffect : IServiceObject, IPooledList public bool CanBeEnabled => IsDisabled && CanChangeState; public bool CanBeEnded => (IsActive || IsDisabled) && CanChangeState; + // IServiceObject implementation. + private readonly ShardedServiceObjectId _serviceObjectId = new(ServiceObjectType.Effect); + public ServiceObjectId ServiceObjectId => _serviceObjectId; + SchedulableServiceObjectId ISchedulableServiceObject.ServiceObjectId => _serviceObjectId; + ShardedServiceObjectId IShardedServiceObject.ServiceObjectId => _serviceObjectId; + public ECSGameEffect(in ECSGameEffectInitParams initParams) { Owner = initParams.Target; @@ -66,6 +71,11 @@ public ECSGameEffect(in ECSGameEffectInitParams initParams) SpellHandler = initParams.SpellHandler; } + public virtual long GetNextTick() + { + return ExpireTick; + } + public bool Start() { if (!CanStart || !Owner.IsAlive) diff --git a/GameServer/ECS-Effects/ECSGameSpellEffect.cs b/GameServer/ECS-Effects/ECSGameSpellEffect.cs index 6c27ada98..898bb2796 100644 --- a/GameServer/ECS-Effects/ECSGameSpellEffect.cs +++ b/GameServer/ECS-Effects/ECSGameSpellEffect.cs @@ -44,6 +44,11 @@ public ECSGameSpellEffect(in ECSGameEffectInitParams initParams) : base(initPara NextTick = StartTick; } + public override long GetNextTick() + { + return NextTick > 0 && (IsAllowedToPulse || IsConcentrationEffect()) ? NextTick : base.GetNextTick(); + } + public override bool IsConcentrationEffect() { return SpellHandler.Spell.IsConcentration; diff --git a/GameServer/ECS-Services/AttackService.cs b/GameServer/ECS-Services/AttackService.cs index c6c3c3200..6e1cdaf60 100644 --- a/GameServer/ECS-Services/AttackService.cs +++ b/GameServer/ECS-Services/AttackService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -12,7 +11,7 @@ public class AttackService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static AttackService Instance { get; } @@ -24,24 +23,23 @@ static AttackService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.AttackComponent, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.AttackComponent); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(AttackComponent attackComponent) diff --git a/GameServer/ECS-Services/CastingService.cs b/GameServer/ECS-Services/CastingService.cs index 2c57dac37..83fda0f79 100644 --- a/GameServer/ECS-Services/CastingService.cs +++ b/GameServer/ECS-Services/CastingService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -12,7 +11,7 @@ public sealed class CastingService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static CastingService Instance { get; } @@ -24,24 +23,23 @@ static CastingService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.CastingComponent, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.CastingComponent); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(CastingComponent castingComponent) diff --git a/GameServer/ECS-Services/ClientService.cs b/GameServer/ECS-Services/ClientService.cs index 9df91c466..649a1f0e4 100644 --- a/GameServer/ECS-Services/ClientService.cs +++ b/GameServer/ECS-Services/ClientService.cs @@ -19,9 +19,8 @@ public sealed class ClientService : GameServiceBase private const int HARD_TIMEOUT = 150000; private const int STATIC_OBJECT_UPDATE_MIN_DISTANCE = 4000; - private List _clients = new(); + private ServiceObjectView _view; private SimpleDisposableLock _lock = new(LockRecursionPolicy.SupportsRecursion); - private int _lastValidIndex = -1; private int _clientCount; private GameClient[] _clientsBySessionId = new GameClient[ushort.MaxValue]; private Trie _playerNameTrie = new(); @@ -44,27 +43,26 @@ public override void BeginTick() try { - _clients = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Client, out _lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Client); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); - _lastValidIndex = -1; return; } } - GameLoop.ExecuteForEach(_clients, _lastValidIndex + 1, BeginTickInternal); + _view.ExecuteForEach(BeginTickInternal); } public override void EndTick() { - GameLoop.ExecuteForEach(_clients, _lastValidIndex + 1, EndTickInternal); + _view.ExecuteForEach(EndTickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _clients.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void BeginTickInternal(GameClient client) @@ -193,9 +191,6 @@ public void OnClientConnect(GameClient client) ServiceObjectId serviceObjectId = client.ServiceObjectId; log.Warn($"{nameof(OnClientConnect)} was called but the client couldn't be added to the entity manager." + $"(Client: {client})" + - $"(IsIdSet: {serviceObjectId.IsSet})" + - $"(IsPendingAddition: {serviceObjectId.IsPendingAddition})" + - $"(IsPendingRemoval: {serviceObjectId.IsPendingAddition})" + $"\n{Environment.StackTrace}"); } } @@ -216,9 +211,6 @@ public void OnClientDisconnect(GameClient client) ServiceObjectId serviceObjectId = client.ServiceObjectId; log.Warn($"{nameof(OnClientDisconnect)} was called but the client couldn't be removed from the entity manager." + $"(Client: {client})" + - $"(IsIdSet: {serviceObjectId.IsSet})" + - $"(IsPendingAddition: {serviceObjectId.IsPendingAddition})" + - $"(IsPendingRemoval: {serviceObjectId.IsPendingAddition})" + $"\n{Environment.StackTrace}"); } } @@ -244,9 +236,9 @@ public GamePlayer GetPlayer(CheckPlayerAction action, T actionArgument) { _lock.EnterReadLock(); - for (int i = 0; i <= _lastValidIndex; i++) + for (int i = 0; i < _view.TotalValidCount; i++) { - GameClient client = _clients[i]; + GameClient client = _view.Items[i]; if (client == null || !client.IsPlaying) continue; @@ -287,9 +279,9 @@ public List GetPlayers(CheckPlayerAction action, T actionArgum { _lock.EnterReadLock(); - for (int i = 0; i <= _lastValidIndex; i++) + for (int i = 0; i < _view.TotalValidCount; i++) { - GameClient client = _clients[i]; + GameClient client = _view.Items[i]; if (!client.IsPlaying) continue; @@ -323,9 +315,9 @@ public GameClient GetClient(CheckClientAction action, T actionArgument) { _lock.EnterReadLock(); - for (int i = 0; i <= _lastValidIndex; i++) + for (int i = 0; i < _view.TotalValidCount; i++) { - GameClient client = _clients[i]; + GameClient client = _view.Items[i]; // Most code assumes clients have an account for privilege checks. if (client.Account == null) @@ -357,9 +349,9 @@ public List GetClients(CheckClientAction action, T actionArgum { _lock.EnterReadLock(); - for (int i = 0; i <= _lastValidIndex; i++) + for (int i = 0; i < _view.TotalValidCount; i++) { - GameClient client = _clients[i]; + GameClient client = _view.Items[i]; // Most code assumes clients have an account for privilege checks. if (client.Account == null) diff --git a/GameServer/ECS-Services/CraftingService.cs b/GameServer/ECS-Services/CraftingService.cs index 1f4dad30d..c86b8485a 100644 --- a/GameServer/ECS-Services/CraftingService.cs +++ b/GameServer/ECS-Services/CraftingService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -11,7 +10,7 @@ public sealed class CraftingService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static CraftingService Instance { get; } @@ -23,24 +22,23 @@ static CraftingService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.CraftComponent, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.CraftComponent); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(CraftComponent craftComponent) diff --git a/GameServer/ECS-Services/DailyQuestService.cs b/GameServer/ECS-Services/DailyQuestService.cs index ce3ebce5d..b7db49250 100644 --- a/GameServer/ECS-Services/DailyQuestService.cs +++ b/GameServer/ECS-Services/DailyQuestService.cs @@ -53,26 +53,25 @@ public override void Tick() GameServer.Database.AddObject(newTime); } - List clients; - int lastValidIndex; + ServiceObjectView view; try { - clients = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Client, out lastValidIndex); + view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Client); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } _lastDailyRollover = DateTime.Now; - for (int i = 0; i < lastValidIndex + 1; i++) + for (int i = 0; i < view.TotalValidCount; i++) { - GameClient client = clients[i]; + GameClient client = view.Items[i]; client.Player?.RemoveFinishedQuests(x => x is Quests.DailyQuest); } diff --git a/GameServer/ECS-Services/EffectListService.cs b/GameServer/ECS-Services/EffectListService.cs index 2243b363c..297444854 100644 --- a/GameServer/ECS-Services/EffectListService.cs +++ b/GameServer/ECS-Services/EffectListService.cs @@ -13,8 +13,7 @@ public sealed class EffectListService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; - private int _lastValidIndex; + private ServiceObjectView _view; public static EffectListService Instance { get; } @@ -29,21 +28,20 @@ public override void BeginTick() try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.EffectListComponent, out _lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.EffectListComponent); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); - _lastValidIndex = -1; return; } - GameLoop.ExecuteForEach(_list, _lastValidIndex + 1, BeginTickInternal); + _view.ExecuteForEach(BeginTickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void BeginTickInternal(EffectListComponent effectListComponent) @@ -68,7 +66,7 @@ private static void BeginTickInternal(EffectListComponent effectListComponent) public override void EndTick() { - GameLoop.ExecuteForEach(_list, _lastValidIndex + 1, EndTickInternal); + _view.ExecuteForEach(EndTickInternal); } private static void EndTickInternal(EffectListComponent effectListComponent) diff --git a/GameServer/ECS-Services/EffectService.cs b/GameServer/ECS-Services/EffectService.cs index 2fac8b534..57cef6605 100644 --- a/GameServer/ECS-Services/EffectService.cs +++ b/GameServer/ECS-Services/EffectService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.GS.PacketHandler; @@ -14,8 +13,7 @@ public sealed class EffectService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; - private int _lastValidIndex; + private ServiceObjectView _view; public static EffectService Instance { get; } @@ -30,21 +28,20 @@ public override void Tick() try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Effect, out _lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Effect); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); - _lastValidIndex = -1; return; } - GameLoop.ExecuteForEach(_list, _lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(ECSGameEffect effect) @@ -73,6 +70,9 @@ private static void TickEffect(ECSGameEffect effect) TickAbilityEffect(abilityEffect); else if (effect is ECSGameSpellEffect spellEffect) TickSpellEffect(spellEffect); + + if (effect.Duration > 0 && !effect.IsEnded) + ServiceObjectStore.Schedule(effect, effect.GetNextTick()); } static void TickAbilityEffect(ECSGameAbilityEffect abilityEffect) diff --git a/GameServer/ECS-Services/MonthlyQuestService.cs b/GameServer/ECS-Services/MonthlyQuestService.cs index b267d1101..5498abff3 100644 --- a/GameServer/ECS-Services/MonthlyQuestService.cs +++ b/GameServer/ECS-Services/MonthlyQuestService.cs @@ -52,26 +52,25 @@ public override void Tick() GameServer.Database.AddObject(newTime); } - List clients; - int lastValidIndex; + ServiceObjectView view; try { - clients = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Client, out lastValidIndex); + view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Client); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } _lastMonthlyRollover = DateTime.Now; - for (int i = 0; i < lastValidIndex + 1; i++) + for (int i = 0; i < view.TotalValidCount; i++) { - GameClient client = clients[i]; + GameClient client = view.Items[i]; client.Player?.RemoveFinishedQuests(x => x is Quests.MonthlyQuest); } diff --git a/GameServer/ECS-Services/MovementService.cs b/GameServer/ECS-Services/MovementService.cs index 6325e46f9..ad64be5b3 100644 --- a/GameServer/ECS-Services/MovementService.cs +++ b/GameServer/ECS-Services/MovementService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -12,7 +11,7 @@ public sealed class MovementService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static MovementService Instance { get; } @@ -24,24 +23,23 @@ static MovementService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.MovementComponent, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.MovementComponent); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(MovementComponent movementComponent) diff --git a/GameServer/ECS-Services/NpcService.cs b/GameServer/ECS-Services/NpcService.cs index 5a167a479..d0f52cfb6 100644 --- a/GameServer/ECS-Services/NpcService.cs +++ b/GameServer/ECS-Services/NpcService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.AI; @@ -13,7 +12,7 @@ public sealed class NpcService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static NpcService Instance { get; } @@ -25,24 +24,23 @@ static NpcService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Brain, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Brain); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(ABrain brain) @@ -52,24 +50,23 @@ private static void TickInternal(ABrain brain) if (Diagnostics.CheckServiceObjectCount) Interlocked.Increment(ref Instance.EntityCount); - GameNPC npc = brain.Body; + if (!GameServiceUtils.ShouldTick(brain.NextThinkTick)) + return; - if (GameServiceUtils.ShouldTick(brain.NextThinkTick)) + if (!brain.IsActive) { - if (!brain.IsActive) - { - brain.Stop(); - return; - } - - long startTick = MonotonicTime.NowMs; - brain.Think(); - long stopTick = MonotonicTime.NowMs; + brain.Stop(); + return; + } - if (stopTick - startTick > Diagnostics.LongTickThreshold) - log.Warn($"Long {Instance.ServiceName}.{nameof(Tick)} for {npc.Name}({npc.ObjectID}) Interval: {brain.ThinkInterval} BrainType: {brain.GetType()} Time: {stopTick - startTick}ms"); + long startTick = MonotonicTime.NowMs; + brain.Think(); + long stopTick = MonotonicTime.NowMs; - brain.NextThinkTick = GameLoop.GameLoopTime + brain.ThinkInterval; + if (stopTick - startTick > Diagnostics.LongTickThreshold) + { + GameNPC npc = brain.Body; + log.Warn($"Long {Instance.ServiceName}.{nameof(Tick)} for {npc.Name}({npc.ObjectID}) Interval: {brain.ThinkInterval} BrainType: {brain.GetType()} Time: {stopTick - startTick}ms"); } } catch (Exception e) diff --git a/GameServer/ECS-Services/ReaperService.cs b/GameServer/ECS-Services/ReaperService.cs index c4e64dadd..507ae3c9c 100644 --- a/GameServer/ECS-Services/ReaperService.cs +++ b/GameServer/ECS-Services/ReaperService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -12,7 +11,7 @@ public sealed class ReaperService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static ReaperService Instance { get; } @@ -24,24 +23,23 @@ static ReaperService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.LivingBeingKilled, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.LivingBeingKilled); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(LivingBeingKilled livingBeingKilled) diff --git a/GameServer/ECS-Services/TimerService.cs b/GameServer/ECS-Services/TimerService.cs index 8b4eba59c..a0971ff6b 100644 --- a/GameServer/ECS-Services/TimerService.cs +++ b/GameServer/ECS-Services/TimerService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Threading; using DOL.Logging; @@ -12,7 +11,7 @@ public sealed class TimerService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static TimerService Instance { get; } @@ -24,24 +23,23 @@ static TimerService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Timer, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Timer); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(ECSGameTimer timer) @@ -51,15 +49,15 @@ private static void TickInternal(ECSGameTimer timer) if (Diagnostics.CheckServiceObjectCount) Interlocked.Increment(ref Instance.EntityCount); - if (GameServiceUtils.ShouldTick(timer.NextTick)) - { - long startTick = MonotonicTime.NowMs; - timer.Tick(); - long stopTick = MonotonicTime.NowMs; + if (!GameServiceUtils.ShouldTick(timer.NextTick)) + return; - if (stopTick - startTick > Diagnostics.LongTickThreshold) - log.Warn($"Long {Instance.ServiceName}.{nameof(Tick)} for Timer Callback: {timer.CallbackInfo?.DeclaringType}:{timer.CallbackInfo?.Name} Owner: {timer.Owner?.Name} Time: {stopTick - startTick}ms"); - } + long startTick = MonotonicTime.NowMs; + timer.Tick(); + long stopTick = MonotonicTime.NowMs; + + if (stopTick - startTick > Diagnostics.LongTickThreshold) + log.Warn($"Long {Instance.ServiceName}.{nameof(Tick)} for Timer Callback: {timer.CallbackInfo?.DeclaringType}:{timer.CallbackInfo?.Name} Owner: {timer.Owner?.Name} Time: {stopTick - startTick}ms"); } catch (Exception e) { @@ -68,7 +66,7 @@ private static void TickInternal(ECSGameTimer timer) } } - public class ECSGameTimer : IServiceObject + public class ECSGameTimer : IShardedServiceObject { public delegate int ECSTimerCallback(ECSGameTimer timer); @@ -79,8 +77,12 @@ public class ECSGameTimer : IServiceObject public long NextTick { get; protected set; } public bool IsAlive { get; private set; } public int TimeUntilElapsed => (int) (NextTick - GameLoop.GameLoopTime); - public ServiceObjectId ServiceObjectId { get; } = new(ServiceObjectType.Timer); - private PropertyCollection _properties; + + // IServiceObject implementation. + private readonly ShardedServiceObjectId _serviceObjectId = new(ServiceObjectType.Timer); + public ServiceObjectId ServiceObjectId => _serviceObjectId; + SchedulableServiceObjectId ISchedulableServiceObject.ServiceObjectId => _serviceObjectId; + ShardedServiceObjectId IShardedServiceObject.ServiceObjectId => _serviceObjectId; public ECSGameTimer(GameObject timerOwner) { @@ -112,7 +114,7 @@ public void Start(int interval) Interval = interval; NextTick = GameLoop.GameLoopTime + interval; - if (ServiceObjectStore.Add(this)) + if (ServiceObjectStore.Schedule(this, NextTick)) IsAlive = true; } @@ -134,21 +136,22 @@ public void Tick() } NextTick += Interval; + ServiceObjectStore.Schedule(this, NextTick); } public PropertyCollection Properties { get { - if (_properties == null) + if (field == null) { lock (this) { - _properties ??= new(); + field ??= new(); } } - return _properties; + return field; } } } diff --git a/GameServer/ECS-Services/WeeklyQuestService.cs b/GameServer/ECS-Services/WeeklyQuestService.cs index 9d88cbd14..5e45f6336 100644 --- a/GameServer/ECS-Services/WeeklyQuestService.cs +++ b/GameServer/ECS-Services/WeeklyQuestService.cs @@ -54,26 +54,25 @@ public override void Tick() GameServer.Database.AddObject(newTime); } - List clients; - int lastValidIndex; + ServiceObjectView view; try { - clients = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.Client, out lastValidIndex); + view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.Client); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } _lastWeeklyRollover = DateTime.Now; - for (int i = 0; i < lastValidIndex + 1; i++) + for (int i = 0; i < view.TotalValidCount; i++) { - GameClient client = clients[i]; + GameClient client = view.Items[i]; client.Player?.RemoveFinishedQuests(x => x is Quests.WeeklyQuest); } diff --git a/GameServer/ECS-Services/ZoneService.cs b/GameServer/ECS-Services/ZoneService.cs index 8c5f7a19b..eb027d422 100644 --- a/GameServer/ECS-Services/ZoneService.cs +++ b/GameServer/ECS-Services/ZoneService.cs @@ -11,7 +11,7 @@ public sealed class ZoneService : GameServiceBase { private static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); - private List _list; + private ServiceObjectView _view; public static ZoneService Instance { get; } @@ -23,24 +23,23 @@ static ZoneService() public override void Tick() { ProcessPostedActionsParallel(); - int lastValidIndex; try { - _list = ServiceObjectStore.UpdateAndGetAll(ServiceObjectType.SubZoneObject, out lastValidIndex); + _view = ServiceObjectStore.UpdateAndGetView(ServiceObjectType.SubZoneObject); } catch (Exception e) { if (log.IsErrorEnabled) - log.Error($"{nameof(ServiceObjectStore.UpdateAndGetAll)} failed. Skipping this tick.", e); + log.Error($"{nameof(ServiceObjectStore.UpdateAndGetView)} failed. Skipping this tick.", e); return; } - GameLoop.ExecuteForEach(_list, lastValidIndex + 1, TickInternal); + _view.ExecuteForEach(TickInternal); if (Diagnostics.CheckServiceObjectCount) - Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _list.Count); + Diagnostics.PrintServiceObjectCount(ServiceName, ref EntityCount, _view.TotalValidCount); } private static void TickInternal(SubZoneObject subZoneObject) diff --git a/GameServer/Managers/GameLoop/GameLoop.cs b/GameServer/Managers/GameLoop/GameLoop.cs index ecafcf993..a561cdc45 100644 --- a/GameServer/Managers/GameLoop/GameLoop.cs +++ b/GameServer/Managers/GameLoop/GameLoop.cs @@ -20,6 +20,7 @@ public static class GameLoop private static bool _running; private static List _tickSequence; + public static int DegreeOfParallelism { get; } = Environment.ProcessorCount; public static double TickDuration { get; private set; } public static long GameLoopTime { get; private set; } public static string ActiveService { get; set; } @@ -60,11 +61,16 @@ public static void Exit() return _tickPacer.Stats.GetAverageTicks(); } - public static void ExecuteForEach(List items, int toExclusive, Action action) + public static void ExecuteForEach(IReadOnlyList items, int toExclusive, Action action) { _threadPool.ExecuteForEach(items, toExclusive, action); } + public static void ExecuteForEachSharded(IReadOnlyList> shards, int[] shardStartIndices, int totalCount, Action action) + { + _threadPool.ExecuteForEachSharded(shards, shardStartIndices, totalCount, action); + } + public static T GetObjectForTick() where T : IPooledObject, new() { return _threadPool != null ? _threadPool.GetObjectForTick() : new(); @@ -77,10 +83,10 @@ public static List GetListForTick() where T : IPooledList private static void Run() { - if (Environment.ProcessorCount == 1) + if (DegreeOfParallelism == 1) _threadPool = new GameLoopThreadPoolSingleThreaded(); else - _threadPool = new GameLoopThreadPoolMultiThreaded(Environment.ProcessorCount); + _threadPool = new GameLoopThreadPoolMultiThreaded(DegreeOfParallelism); _threadPool.Init(); // Must be done from the game loop thread. BuildTickSequence(); diff --git a/GameServer/Managers/GameLoop/GameLoopThreadPool.cs b/GameServer/Managers/GameLoop/GameLoopThreadPool.cs index 5c1a597a5..0e967566a 100644 --- a/GameServer/Managers/GameLoop/GameLoopThreadPool.cs +++ b/GameServer/Managers/GameLoop/GameLoopThreadPool.cs @@ -16,8 +16,8 @@ public virtual void Init() InitThreadStatics(); } - public abstract void ExecuteForEach(List items, int toExclusive, Action action); - + public abstract void ExecuteForEach(IReadOnlyList items, int toExclusive, Action action); + public abstract void ExecuteForEachSharded(IReadOnlyList> shards, int[] shardStartIndices, int totalCount, Action action); public abstract void Dispose(); public T GetObjectForTick() where T : IPooledObject, new() diff --git a/GameServer/Managers/GameLoop/GameLoopThreadPoolMultiThreaded.cs b/GameServer/Managers/GameLoop/GameLoopThreadPoolMultiThreaded.cs index e6527f1f0..a49dcc068 100644 --- a/GameServer/Managers/GameLoop/GameLoopThreadPoolMultiThreaded.cs +++ b/GameServer/Managers/GameLoop/GameLoopThreadPoolMultiThreaded.cs @@ -110,7 +110,7 @@ void StartWatchdog() } } - public override void ExecuteForEach(List items, int toExclusive, Action action) + public override void ExecuteForEach(IReadOnlyList items, int toExclusive, Action action) { try { @@ -119,26 +119,31 @@ public override void ExecuteForEach(List items, int toExclusive, Action if (count <= 0) return; - _workContext = ExecutionContext.Capture(); _workProcessor = WorkProcessorCache.Instance.Set(items, action); - _workState.RemainingWork = count; - _workState.CompletedWorkerCount = 0; - - // If the count is less than the degree of parallelism, only signal the required number of workers. - // The caller thread will also be used, so in this case we need to subtract one from the amount of workers to start. - int workersToStart = count < _degreeOfParallelism ? count - 1 : _workerCount; + ExecuteForEachInternal(count); + } + catch (Exception e) + { + if (log.IsFatalEnabled) + log.Fatal($"Critical error encountered in \"{nameof(GameLoopThreadPoolMultiThreaded)}\"", e); - for (int i = 0; i < workersToStart; i++) - _workReady[i].Set(); + GameServer.Instance.Stop(); + } + finally + { + (_workProcessor as WorkProcessor)?.Clear(); + } + } - ProcessWorkActions(); - Interlocked.Increment(ref _workState.CompletedWorkerCount); + public override void ExecuteForEachSharded(IReadOnlyList> shards, int[] shardStartIndices, int totalCount, Action action) + { + try + { + if (totalCount <= 0) + return; - // Spin very tightly until all the workers have completed their work. - // We could adjust the spin wait time if we get here early, but this is hard to predict. - // However we really don't want to yield the CPU here, as this could delay the return by a lot. - while (Volatile.Read(ref _workState.CompletedWorkerCount) < workersToStart + 1) - Thread.SpinWait(1); + _workProcessor = ShardedWorkProcessorCache.Instance.Set(shards, shardStartIndices, totalCount, action); + ExecuteForEachInternal(totalCount); } catch (Exception e) { @@ -149,10 +154,33 @@ public override void ExecuteForEach(List items, int toExclusive, Action } finally { - (_workProcessor as WorkProcessor)?.Clear(); + (_workProcessor as ShardedWorkProcessor)?.Clear(); } } + private void ExecuteForEachInternal(int count) + { + _workContext = ExecutionContext.Capture(); + _workState.RemainingWork = count; + _workState.CompletedWorkerCount = 0; + + // If the count is less than the degree of parallelism, only signal the required number of workers. + // The caller thread will also be used, so in this case we need to subtract one from the amount of workers to start. + int workersToStart = count < _degreeOfParallelism ? count - 1 : _workerCount; + + for (int i = 0; i < workersToStart; i++) + _workReady[i].Set(); + + ProcessWorkActions(); + Interlocked.Increment(ref _workState.CompletedWorkerCount); + + // Spin very tightly until all the workers have completed their work. + // We could adjust the spin wait time if we get here early, but this is hard to predict. + // However we really don't want to yield the CPU here, as this could delay the return by a lot. + while (Volatile.Read(ref _workState.CompletedWorkerCount) < workersToStart + 1) + Thread.SpinWait(1); + } + public override void Dispose() { if (!Interlocked.CompareExchange(ref _running, false, true)) @@ -173,7 +201,7 @@ public override void Dispose() protected override void InitWorker(object obj) { - (int Id, bool Restart) = ((int, bool)) obj; + (int Id, _) = ((int, bool)) obj; _workers[Id] = Thread.CurrentThread; _workerCycle[Id] = GameLoopThreadPoolWatchdog.IDLE_CYCLE; base.InitWorker(obj); @@ -267,9 +295,7 @@ private void ProcessWorkActions() if (start < 0) start = 0; - for (int i = start; i < end; i++) - _workProcessor.Process(i); - + _workProcessor.Process(start, end); remainingWork = start - 1; } } @@ -279,35 +305,110 @@ private static class WorkProcessorCache public static readonly WorkProcessor Instance = new(); } + private static class ShardedWorkProcessorCache + { + public static readonly ShardedWorkProcessor Instance = new(); + } + private sealed class WorkProcessor : WorkProcessor { - private List _items; + private IReadOnlyList _items; private Action _action; public WorkProcessor() { } - public WorkProcessor Set(List items, Action action) + public WorkProcessor Set(IReadOnlyList items, Action action) { _items = items; _action = action; return this; } - public override void Process(int index) + public override void Process(int start, int end) { - _action(_items[index]); + for (int i = start; i < end; i++) + _action(_items[i]); + } + + public void Clear() + { + _items = null; + _action = null; + } + } + + private sealed class ShardedWorkProcessor : WorkProcessor + { + private IReadOnlyList> _shards; + private int[] _shardStartIndices; + private int _totalCount; + private Action _action; + + public ShardedWorkProcessor Set( + IReadOnlyList> shards, + int[] shardStartIndices, + int totalCount, + Action action) + { + _shards = shards; + _shardStartIndices = shardStartIndices; + _totalCount = totalCount; + _action = action; + return this; + } + + public override void Process(int start, int end) + { + int currentShardIdx = _shardStartIndices.Length - 1; + + while (currentShardIdx > 0 && start < _shardStartIndices[currentShardIdx]) + currentShardIdx--; + + int itemsLeft = end - start; + int localIdx = start - _shardStartIndices[currentShardIdx]; + + while (itemsLeft > 0) + { + int shardValidCount; + + // Derive the exact number of valid items in this shard using the boundaries. + if (currentShardIdx < _shardStartIndices.Length - 1) + shardValidCount = _shardStartIndices[currentShardIdx + 1] - _shardStartIndices[currentShardIdx]; + else + shardValidCount = _totalCount - _shardStartIndices[currentShardIdx]; + + int availableInShard = shardValidCount - localIdx; + int itemsToProcess = Math.Min(itemsLeft, availableInShard); + + if (itemsToProcess <= 0) + { + currentShardIdx++; + localIdx = 0; + continue; + } + + IReadOnlyList currentShard = _shards[currentShardIdx]; + + for (int i = 0; i < itemsToProcess; i++) + _action(currentShard[localIdx + i]); + + itemsLeft -= itemsToProcess; + currentShardIdx++; + localIdx = 0; + } } public void Clear() { - _items = default; + _shards = null; + _shardStartIndices = null; _action = null; } } private abstract class WorkProcessor { - public abstract void Process(int index); + public abstract void Process(int start, int end); } } } diff --git a/GameServer/Managers/GameLoop/GameLoopThreadPoolSingleThreaded.cs b/GameServer/Managers/GameLoop/GameLoopThreadPoolSingleThreaded.cs index af0493b30..cee5a1f67 100644 --- a/GameServer/Managers/GameLoop/GameLoopThreadPoolSingleThreaded.cs +++ b/GameServer/Managers/GameLoop/GameLoopThreadPoolSingleThreaded.cs @@ -7,7 +7,7 @@ public sealed class GameLoopThreadPoolSingleThreaded : GameLoopThreadPool { public GameLoopThreadPoolSingleThreaded() { } - public override void ExecuteForEach(List items, int toExclusive, Action action) + public override void ExecuteForEach(IReadOnlyList items, int toExclusive, Action action) { CheckResetTick(); @@ -15,6 +15,32 @@ public override void ExecuteForEach(List items, int toExclusive, Action action(items[i]); } + public override void ExecuteForEachSharded(IReadOnlyList> shards, int[] shardStartIndices, int totalCount, Action action) + { + CheckResetTick(); + + if (totalCount <= 0) + return; + + for (int i = 0; i < shards.Count; i++) + { + int shardValidCount; + + if (i < shards.Count - 1) + shardValidCount = shardStartIndices[i + 1] - shardStartIndices[i]; + else + shardValidCount = totalCount - shardStartIndices[i]; + + if (shardValidCount <= 0) + continue; + + IReadOnlyList currentShard = shards[i]; + + for (int j = 0; j < shardValidCount; j++) + action(currentShard[j]); + } + } + public override void Dispose() { } } } diff --git a/GameServer/Managers/ServiceObject/IServiceObject.cs b/GameServer/Managers/ServiceObject/IServiceObject.cs index ccd82084e..369a33878 100644 --- a/GameServer/Managers/ServiceObject/IServiceObject.cs +++ b/GameServer/Managers/ServiceObject/IServiceObject.cs @@ -1,8 +1,17 @@ namespace DOL.GS { - // Interface to be implemented by classes that are to be handled by `ServiceObjectStore`. public interface IServiceObject { - public ServiceObjectId ServiceObjectId { get; } + ServiceObjectId ServiceObjectId { get; } + } + + public interface ISchedulableServiceObject : IServiceObject + { + new SchedulableServiceObjectId ServiceObjectId { get; } + } + + public interface IShardedServiceObject : ISchedulableServiceObject + { + new ShardedServiceObjectId ServiceObjectId { get; } } } diff --git a/GameServer/Managers/ServiceObject/IServiceObjectArray.cs b/GameServer/Managers/ServiceObject/IServiceObjectArray.cs new file mode 100644 index 000000000..33fa8d313 --- /dev/null +++ b/GameServer/Managers/ServiceObject/IServiceObjectArray.cs @@ -0,0 +1,4 @@ +namespace DOL.GS +{ + public interface IServiceObjectArray { } +} diff --git a/GameServer/Managers/ServiceObject/SchedulableServiceObjectArray.cs b/GameServer/Managers/ServiceObject/SchedulableServiceObjectArray.cs new file mode 100644 index 000000000..b18f077ba --- /dev/null +++ b/GameServer/Managers/ServiceObject/SchedulableServiceObjectArray.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; + +namespace DOL.GS +{ + public class SchedulableServiceObjectArray : ServiceObjectArray + where T : class, ISchedulableServiceObject + { + private readonly DrainArray _itemsToSchedule = new(); + + // Hierarchical timing wheel for O(1) suspended item scheduling. + // Items sleeping longer than WHEEL_SIZE are parked in _farFuture. + // 1 << 16 = 65,536 buckets. + // 65,536 buckets * 50ms = 3,276,800ms = ~54.6 minutes. + // If the game loop ticks faster than WHEEL_RESOLUTION_MS, items may be woken up + private const int WHEEL_RESOLUTION_MS = 50; + private const int WHEEL_BITS = 16; + private const int WHEEL_SIZE = 1 << WHEEL_BITS; + private const int WHEEL_MASK = WHEEL_SIZE - 1; + + private readonly List[] _sleepWheel; + private readonly Stack> _listPool = new(); + private readonly PriorityQueue _farFuture = new(); + private long _lastProcessedWheelTick = -1; + private long _currentUpdateTick; + + public SchedulableServiceObjectArray(int capacity) : base(capacity) + { + _sleepWheel = new List[WHEEL_SIZE]; + + for (int i = 0; i < WHEEL_SIZE; i++) + _sleepWheel[i] = new(); + } + + public override void Schedule(T item, long wakeUpTimeMs) + { + SchedulableServiceObjectId id = item.ServiceObjectId; + long currentTick = GameLoop.GameLoopTime / WHEEL_RESOLUTION_MS; + long targetTick = Math.Max(currentTick, wakeUpTimeMs / WHEEL_RESOLUTION_MS); + id.ExpectedWakeTick = targetTick; + _itemsToSchedule.Add(item); + } + + protected override void ProcessItemsToRemove(long now) + { + _currentUpdateTick = now / WHEEL_RESOLUTION_MS; + base.ProcessItemsToRemove(now); + } + + protected override void ProcessItemsToAdd(long now) + { + WakeUpItems(); + + if (_itemsToSchedule.Any) + _itemsToSchedule.DrainTo(static (item, ctx) => ctx.ScheduleInternal(item), this); + + base.ProcessItemsToAdd(now); + } + + protected override void AddToList(T item) + { + // Invalidate any sleep ticket. Handles both early manual re-adds and wake-ups. + item.ServiceObjectId.ExpectedWakeTick = -1; + base.AddToList(item); + } + + private void ScheduleInternal(T item) + { + SchedulableServiceObjectId id = item.ServiceObjectId; + + if (!id.TryConsumeAction(ServiceObjectId.PendingAction.Schedule)) + return; + + long targetTick = Math.Max(id.ExpectedWakeTick, _lastProcessedWheelTick); + + if (targetTick - _lastProcessedWheelTick >= WHEEL_SIZE) + _farFuture.Enqueue(item, targetTick); + else + _sleepWheel[(int) (targetTick & WHEEL_MASK)].Add(item); + + RemoveFromList(id); + id.ExpectedWakeTick = targetTick; + id.MoveTo(SchedulableServiceObjectId.SLEEPING_ID); + } + + private void WakeUpItems() + { + if (_lastProcessedWheelTick == -1) + _lastProcessedWheelTick = _currentUpdateTick; + + List currentSwapList = _listPool.Count > 0 ? _listPool.Pop() : new(); + + // Catch up on all missed ticks. + while (_lastProcessedWheelTick <= _currentUpdateTick) + { + int bucketIndex = (int) (_lastProcessedWheelTick & WHEEL_MASK); + List bucket = _sleepWheel[bucketIndex]; + + if (bucket.Count > 0) + { + _sleepWheel[bucketIndex] = currentSwapList; + + for (int i = 0; i < bucket.Count; i++) + { + T item = bucket[i]; + SchedulableServiceObjectId id = item.ServiceObjectId; + + // Do not wake if it's no longer sleeping or has a pending action. + if (id.ExpectedWakeTick != _lastProcessedWheelTick || + !id.IsSleeping || + id.PeekAction() is not ServiceObjectId.PendingAction.None) + { + continue; + } + + AddToList(item); + id.MoveTo(LastValidIndex); + } + + bucket.Clear(); + currentSwapList = bucket; + } + + _lastProcessedWheelTick++; + + // If the lowest bits are 0, we just completed a full cycle of the wheel. + if ((_lastProcessedWheelTick & WHEEL_MASK) == 0) + { + while (_farFuture.TryPeek(out T item, out long targetTick)) + { + SchedulableServiceObjectId id = item.ServiceObjectId; + + // Lazy deletion. Discard items that were removed, unset, or rescheduled. + if (id.PeekAction() is ServiceObjectId.PendingAction.Remove || + id.Value == ServiceObjectId.UNSET_ID || + id.ExpectedWakeTick != targetTick) + { + _farFuture.Dequeue(); + continue; + } + + // Check if remaining items are too far in the future. + if (targetTick - _lastProcessedWheelTick >= WHEEL_SIZE) + break; + + _sleepWheel[(int) (targetTick & WHEEL_MASK)].Add(item); + _farFuture.Dequeue(); + } + } + } + + _listPool.Push(currentSwapList); + } + } +} diff --git a/GameServer/Managers/ServiceObject/SchedulableServiceObjectId.cs b/GameServer/Managers/ServiceObject/SchedulableServiceObjectId.cs new file mode 100644 index 000000000..619e88c79 --- /dev/null +++ b/GameServer/Managers/ServiceObject/SchedulableServiceObjectId.cs @@ -0,0 +1,18 @@ +namespace DOL.GS +{ + public class SchedulableServiceObjectId : ServiceObjectId + { + public const int SLEEPING_ID = -2; + + public bool IsSleeping => Value == SLEEPING_ID; + public long ExpectedWakeTick { get; set; } = -1; + + public SchedulableServiceObjectId(ServiceObjectType type) : base(type) { } + + public override void Unset() + { + base.Unset(); + ExpectedWakeTick = -1; + } + } +} diff --git a/GameServer/Managers/ServiceObject/ServiceObjectArray.cs b/GameServer/Managers/ServiceObject/ServiceObjectArray.cs new file mode 100644 index 000000000..e7009b419 --- /dev/null +++ b/GameServer/Managers/ServiceObject/ServiceObjectArray.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace DOL.GS +{ + // Manages a contiguous array of active items. + public class ServiceObjectArray : ServiceObjectArrayBase + where T : class, IServiceObject + { + private readonly DrainArray _itemsToAdd = new(); + private readonly DrainArray _itemsToRemove = new(); + private bool _updating; + private int _lastValidIndex = -1; + + public override bool IsSharded => false; + public override List Items { get; } + public override int LastValidIndex => _lastValidIndex; + public override IReadOnlyList> Shards => null; + public override int[] ShardStartIndices => null; + public override int TotalValidCount => LastValidIndex + 1; + + public ServiceObjectArray(int capacity) + { + Items = new(capacity); + } + + public override void Add(T item) + { + _itemsToAdd.Add(item); + } + + public override void Schedule(T item, long wakeUpTimeMs) { } + + public override void Remove(T item) + { + _itemsToRemove.Add(item); + } + + public override void Update(long now) + { + if (Interlocked.Exchange(ref _updating, true) != false) + throw new InvalidOperationException($"{typeof(T)} is already being updated."); + + try + { + ProcessItemsToRemove(now); + ProcessItemsToAdd(now); + } + finally + { + _updating = false; + } + } + + protected virtual void ProcessItemsToRemove(long now) + { + if (_itemsToRemove.Any) + _itemsToRemove.DrainTo(static (item, ctx) => ctx.RemoveInternal(item), this); + } + + protected virtual void ProcessItemsToAdd(long now) + { + if (_itemsToAdd.Any) + _itemsToAdd.DrainTo(static (item, ctx) => ctx.AddInternal(item), this); + } + + private void RemoveInternal(T item) + { + ServiceObjectId id = item.ServiceObjectId; + + if (!id.TryConsumeAction(ServiceObjectId.PendingAction.Remove)) + return; + + RemoveFromList(id); + } + + private void AddInternal(T item) + { + ServiceObjectId id = item.ServiceObjectId; + + if (!id.TryConsumeAction(ServiceObjectId.PendingAction.Add)) + return; + + AddToList(item); + } + + protected void RemoveFromList(ServiceObjectId id) + { + int idValue = id.Value; + + if (idValue >= 0 && idValue <= LastValidIndex) + { + // Swap the item being removed with the item at the end. + if (idValue == LastValidIndex) + Items[LastValidIndex] = null; + else + { + T lastItem = Items[LastValidIndex]; + Items[idValue] = lastItem; + lastItem.ServiceObjectId.MoveTo(idValue); + Items[LastValidIndex] = null; + } + + _lastValidIndex--; + } + + id.Unset(); + } + + protected virtual void AddToList(T item) + { + ServiceObjectId id = item.ServiceObjectId; + + if (id.IsActive) + return; + + if (++_lastValidIndex >= Items.Capacity) + { + int newCapacity = (int) (Items.Capacity * 1.2); + + if (Items.Capacity == newCapacity) + newCapacity++; + + Items.Resize(newCapacity); + } + + if (LastValidIndex >= Items.Count) + Items.Add(item); + else + Items[LastValidIndex] = item; + + id.MoveTo(LastValidIndex); + } + } +} diff --git a/GameServer/Managers/ServiceObject/ServiceObjectArrayBase.cs b/GameServer/Managers/ServiceObject/ServiceObjectArrayBase.cs new file mode 100644 index 000000000..1502791e1 --- /dev/null +++ b/GameServer/Managers/ServiceObject/ServiceObjectArrayBase.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace DOL.GS +{ + public abstract class ServiceObjectArrayBase : IServiceObjectArray + where T : class, IServiceObject + { + // Indicates how the caller should iterate over this collection. + public abstract bool IsSharded { get; } + + // Flat array API. + public abstract IReadOnlyList Items { get; } + public abstract int LastValidIndex { get; } + + // Sharded array API. + public abstract IReadOnlyList> Shards { get; } + public abstract int[] ShardStartIndices { get; } + public abstract int TotalValidCount { get; } + + public abstract void Add(T item); + public abstract void Schedule(T item, long wakeUpTimeMs); + public abstract void Remove(T item); + public abstract void Update(long now); + } + + public readonly struct ServiceObjectView where T : class, IServiceObject + { + public readonly bool IsSharded; + public readonly int TotalValidCount; + public readonly IReadOnlyList Items; + public readonly IReadOnlyList> Shards; + public readonly int[] ShardStartIndices; + + public ServiceObjectView(IReadOnlyList items, int totalValidCount) + { + IsSharded = false; + Items = items; + TotalValidCount = totalValidCount; + Shards = null; + ShardStartIndices = null; + } + + public ServiceObjectView(IReadOnlyList> shards, int[] shardStartIndices, int totalValidCount) + { + IsSharded = true; + Shards = shards; + ShardStartIndices = shardStartIndices; + TotalValidCount = totalValidCount; + Items = null; + } + + public void ExecuteForEach(Action action) + { + if (TotalValidCount <= 0) + return; + + if (IsSharded) + GameLoop.ExecuteForEachSharded(Shards, ShardStartIndices, TotalValidCount, action); + else + GameLoop.ExecuteForEach(Items, TotalValidCount, action); + } + } +} diff --git a/GameServer/Managers/ServiceObject/ServiceObjectId.cs b/GameServer/Managers/ServiceObject/ServiceObjectId.cs index d98dc20d7..28e75f101 100644 --- a/GameServer/Managers/ServiceObject/ServiceObjectId.cs +++ b/GameServer/Managers/ServiceObject/ServiceObjectId.cs @@ -4,49 +4,58 @@ public class ServiceObjectId { public const int UNSET_ID = -1; - private int _value = UNSET_ID; - private PendingState _pendingState = PendingState.None; - - public int Value - { - get => _value; - set - { - _value = value; - _pendingState = PendingState.None; - } - } + private PendingAction _action = PendingAction.None; + public int Value { get; private set; } = UNSET_ID; public ServiceObjectType Type { get; } - public bool IsSet => _value > UNSET_ID; - public bool IsPendingAddition => _pendingState == PendingState.Adding; - public bool IsPendingRemoval => _pendingState == PendingState.Removing; + + public bool IsActive => Value >= 0; public ServiceObjectId(ServiceObjectType type) { Type = type; } - public void OnPreAdd() + public bool TrySetAction(PendingAction action) + { + if (_action == action) + return false; + + _action = action; + return true; + } + + public bool TryConsumeAction(PendingAction expectedAction) + { + if (_action != expectedAction) + return false; + + _action = PendingAction.None; + return true; + } + + public PendingAction PeekAction() { - _pendingState = PendingState.Adding; + return _action; } - public void OnPreRemove() + public void MoveTo(int index) { - _pendingState = PendingState.Removing; + Value = index; } - public void Unset() + public virtual void Unset() { + _action = PendingAction.None; Value = UNSET_ID; } - private enum PendingState + public enum PendingAction { None, - Adding, - Removing + Add, + Schedule, + Remove } } } diff --git a/GameServer/Managers/ServiceObject/ServiceObjectStore.cs b/GameServer/Managers/ServiceObject/ServiceObjectStore.cs index 7f1dd7ee7..ae99dc67a 100644 --- a/GameServer/Managers/ServiceObject/ServiceObjectStore.cs +++ b/GameServer/Managers/ServiceObject/ServiceObjectStore.cs @@ -1,225 +1,111 @@ -using System; using System.Collections.Frozen; using System.Collections.Generic; -using System.Reflection; -using System.Threading; using DOL.AI; +using DOL.GS.ServerProperties; namespace DOL.GS { public static class ServiceObjectStore { - private static readonly Logging.Logger log = Logging.LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); + /* 3 types of array are currently available: + * + * ServiceObjectArray: + * Simple array with deferred additions and removals. + * Single threaded update. + * Doesn't support scheduling. + * Scales somewhat poorly with object count (O(n)). + * Memory efficient. + * Best for objects meant to be processed only once, or on every game loop tick. + * + * SchedulableServiceObjectArray: + * Inherits ServiceObjectArray, controlled by a timing wheel. + * Single threaded update. + * Supports scheduling. + * Memory efficiency depends on its bucket count. + * Scales well with object count, but has slow updates if objects need to be rescheduled frequently. + * No use case for now. + * + * ShardedServiceObjectArray: + * Backed by an array of SchedulableServiceObjectArray. + * Multithreaded update. + * Supports scheduling. + * Scales well with object count and has fairly fast updates (depending on CPU count). + * Memory efficiency depends on SchedulableServiceObjectArray's bucket count and CPU count. + * Best for everything that ticks much slower than the game loop, if object count is high enough to justify the overhead. + */ private static FrozenDictionary _serviceObjectArrays = new Dictionary() { - { ServiceObjectType.Client, new ServiceObjectArray(ServerProperties.Properties.MAX_PLAYERS) }, - { ServiceObjectType.Brain, new ServiceObjectArray(ServerProperties.Properties.MAX_ENTITIES) }, + { ServiceObjectType.Client, new ServiceObjectArray(Properties.MAX_PLAYERS) }, + { ServiceObjectType.Brain, new ShardedServiceObjectArray(Properties.MAX_ENTITIES) }, { ServiceObjectType.AttackComponent, new ServiceObjectArray(1250) }, { ServiceObjectType.CastingComponent, new ServiceObjectArray(1250) }, - { ServiceObjectType.Effect, new ServiceObjectArray(10000) }, + { ServiceObjectType.Effect, new ShardedServiceObjectArray(10000) }, { ServiceObjectType.EffectListComponent, new ServiceObjectArray(1250) }, { ServiceObjectType.MovementComponent, new ServiceObjectArray(1250) }, { ServiceObjectType.CraftComponent, new ServiceObjectArray(100) }, - { ServiceObjectType.SubZoneObject, new ServiceObjectArray(ServerProperties.Properties.MAX_ENTITIES) }, + { ServiceObjectType.SubZoneObject, new ServiceObjectArray(Properties.MAX_ENTITIES) }, { ServiceObjectType.LivingBeingKilled, new ServiceObjectArray(200) }, - { ServiceObjectType.Timer, new ServiceObjectArray(500) } + { ServiceObjectType.Timer, new ShardedServiceObjectArray(500) } }.ToFrozenDictionary(); public static bool Add(T serviceObject) where T : class, IServiceObject { ServiceObjectId id = serviceObject.ServiceObjectId; - // Return false if the service object is already present and not being removed. - if (id.IsSet && !id.IsPendingRemoval) + // Prevent re-entry if the object is already added, but not being removed or scheduled. + if (id.IsActive && id.PeekAction() is not ServiceObjectId.PendingAction.Remove and not ServiceObjectId.PendingAction.Schedule) return false; - (_serviceObjectArrays[serviceObject.ServiceObjectId.Type] as ServiceObjectArray).Add(serviceObject); + // Prevent re-entry if the object is already being added. + if (!id.TrySetAction(ServiceObjectId.PendingAction.Add)) + return false; + + (_serviceObjectArrays[serviceObject.ServiceObjectId.Type] as ServiceObjectArrayBase).Add(serviceObject); return true; } - public static bool Remove(T serviceObject) where T : class, IServiceObject + // Schedule an object to be returned at a later time. + // Scheduling in the past is allowed (the item will be returned immediately), and it's the caller's responsibility to ensure this makes sense. + public static bool Schedule(T serviceObject, long nextTickMs) where T : class, ISchedulableServiceObject { - ServiceObjectId id = serviceObject.ServiceObjectId; + SchedulableServiceObjectId id = serviceObject.ServiceObjectId; - // Return false if the service object is absent and not being added. - if (!id.IsSet && !id.IsPendingAddition) + // Prevent re-entry if the object is already being scheduled. + if (!id.TrySetAction(ServiceObjectId.PendingAction.Schedule)) return false; - (_serviceObjectArrays[serviceObject.ServiceObjectId.Type] as ServiceObjectArray).Remove(serviceObject); + (_serviceObjectArrays[id.Type] as ServiceObjectArrayBase).Schedule(serviceObject, nextTickMs); return true; } - // Applies pending additions and removals then returns the list alongside the last valid index. - // Thread unsafe. The returned list should not be modified. - // Elements should be null checked alongside the value returned by `ServiceObjectId.IsSet`. - public static List UpdateAndGetAll(ServiceObjectType type, out int lastValidIndex) where T : class, IServiceObject - { - ServiceObjectArray array = _serviceObjectArrays[type] as ServiceObjectArray; - lastValidIndex = array.Update(); - return array.Items; - } - - private class ServiceObjectArray : IServiceObjectArray where T : class, IServiceObject + public static bool Remove(T serviceObject) where T : class, IServiceObject { - private PriorityQueue _invalidIndexes = new(); - private DrainArray _itemsToAdd = new(); - private DrainArray _itemsToRemove = new(); - private int _updating = new(); - private int _lastValidIndex = -1; - - public List Items { get; } - - public ServiceObjectArray(int capacity) - { - Items = new List(capacity); - } - - public void Add(T item) - { - item.ServiceObjectId.OnPreAdd(); - _itemsToAdd.Add(item); - } - - public void Remove(T item) - { - item.ServiceObjectId.OnPreRemove(); - _itemsToRemove.Add(item); - } - - public int Update() - { - if (Interlocked.Exchange(ref _updating, 1) != 0) - throw new InvalidOperationException($"{typeof(T)} is already being updated."); - - try - { - if (_itemsToRemove.Any) - { - DrainItemsToRemove(); - UpdateLastValidIndexAfterRemoval(); - OptimizeIndexes(); - } - - if (_itemsToAdd.Any) - DrainItemsToAdd(); - } - finally - { - _updating = 0; - } - - return _lastValidIndex; - } - - private void DrainItemsToRemove() - { - _itemsToRemove.DrainTo(static (item, array) => array.RemoveInternal(item), this); - } - - private void RemoveInternal(T item) - { - ServiceObjectId id = item.ServiceObjectId; - - if (!id.IsPendingRemoval || !id.IsSet) - return; - - int idValue = id.Value; - - if (idValue == Items.Count - 1) - _lastValidIndex--; - - _invalidIndexes.Enqueue(idValue, idValue); - Items[idValue] = null; - id.Unset(); - } - - private void DrainItemsToAdd() - { - _itemsToAdd.DrainTo(static (item, array) => array.AddInternal(item), this); - } - - private void AddInternal(T item) - { - ServiceObjectId id = item.ServiceObjectId; - - if (!id.IsPendingAddition || id.IsSet) - return; - - if (_invalidIndexes.Count > 0) - { - int index = _invalidIndexes.Peek(); - _invalidIndexes.Dequeue(); - Items[index] = item; - - if (index > _lastValidIndex) - _lastValidIndex = index; - - id.Value = index; - return; - } - - // Increase the capacity of the list in the event that it's too small. This is a costly operation. - // 'Add' already does it, but we nay want to know when it happens and control by how much it grows ('Add' would double it). - if (++_lastValidIndex >= Items.Capacity) - { - int newCapacity = (int) (Items.Capacity * 1.2); - - if (log.IsWarnEnabled) - log.Warn($"Array for type '{typeof(T)}' is too short. Resizing it to {newCapacity}."); - - Items.Resize(newCapacity); - } - - Items.Add(item); - id.Value = _lastValidIndex; - } - - private void UpdateLastValidIndexAfterRemoval() - { - while (_lastValidIndex > -1 && Items[_lastValidIndex]?.ServiceObjectId.IsSet != true) - _lastValidIndex--; - } - - private void OptimizeIndexes() - { - // Only compact if there are invalid indexes and at least one valid item above the lowest invalid index. - while (_invalidIndexes.Count > 0) - { - int lowestInvalidIndex = _invalidIndexes.Peek(); - bool foundItemToMove = false; + ServiceObjectId id = serviceObject.ServiceObjectId; - for (int i = _lastValidIndex; i > lowestInvalidIndex; i--) - { - if (Items[i]?.ServiceObjectId.IsSet != true) - continue; + // Prevent re-entry if the object is already removed, but not being added or scheduled. + if (!id.IsActive && id.PeekAction() is not ServiceObjectId.PendingAction.Add and not ServiceObjectId.PendingAction.Schedule) + return false; - _invalidIndexes.Dequeue(); - T item = Items[i]; - Items[lowestInvalidIndex] = item; - Items[i] = null; - _invalidIndexes.Enqueue(i, i); - item.ServiceObjectId.Value = lowestInvalidIndex; + // Prevent re-entry if the object is already being removed. + if (!id.TrySetAction(ServiceObjectId.PendingAction.Remove)) + return false; - // Update last valid index if we just moved the last item. - if (i == _lastValidIndex) - { - do - _lastValidIndex--; - while (_lastValidIndex > -1 && Items[_lastValidIndex]?.ServiceObjectId.IsSet != true); - } + (_serviceObjectArrays[serviceObject.ServiceObjectId.Type] as ServiceObjectArrayBase).Remove(serviceObject); + return true; + } - foundItemToMove = true; - break; - } + // Applies pending additions and removals then returns the list alongside the last valid index. + public static ServiceObjectView UpdateAndGetView(ServiceObjectType type) where T : class, IServiceObject + { + ServiceObjectArrayBase array = _serviceObjectArrays[type] as ServiceObjectArrayBase; + array.Update(GameLoop.GameLoopTime); - if (!foundItemToMove) - break; - } - } + if (array.IsSharded) + return new(array.Shards, array.ShardStartIndices, array.TotalValidCount); + else + return new(array.Items, array.LastValidIndex + 1); } - - private interface IServiceObjectArray { } } } diff --git a/GameServer/Managers/ServiceObject/ShardedServiceObjectArray.cs b/GameServer/Managers/ServiceObject/ShardedServiceObjectArray.cs new file mode 100644 index 000000000..d50095d62 --- /dev/null +++ b/GameServer/Managers/ServiceObject/ShardedServiceObjectArray.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Threading; + +namespace DOL.GS +{ + public class ShardedServiceObjectArray : ServiceObjectArrayBase + where T : class, IShardedServiceObject + { + private readonly SchedulableServiceObjectArray[] _shards; + private readonly List[] _shardsView; + private readonly int[] _shardStartIndices; + private int _roundRobinCounter; + private int _totalValidCount; + + public override bool IsSharded => true; + public override List Items => null; + public override int LastValidIndex => TotalValidCount - 1; + public override IReadOnlyList> Shards => _shardsView; + public override int[] ShardStartIndices => _shardStartIndices; + public override int TotalValidCount => _totalValidCount; + + public ShardedServiceObjectArray(int initialCapacity) + { + int shardCount = GameLoop.DegreeOfParallelism; + _shards = new SchedulableServiceObjectArray[shardCount]; + _shardsView = new List[shardCount]; + + int capacityPerShard = initialCapacity / shardCount; + + for (int i = 0; i < shardCount; i++) + { + _shards[i] = new(capacityPerShard); + _shardsView[i] = _shards[i].Items; + } + + _shardStartIndices = new int[shardCount]; + } + + private SchedulableServiceObjectArray RouteItem(T item) + { + ShardedServiceObjectId id = item.ServiceObjectId; + + if (id.ShardIndex == -1) + id.ShardIndex = (int) ((uint) Interlocked.Increment(ref _roundRobinCounter) % _shards.Length); + + return _shards[id.ShardIndex]; + } + + public override void Add(T item) + { + RouteItem(item).Add(item); + } + + public override void Schedule(T item, long wakeUpTimeMs) + { + RouteItem(item).Schedule(item, wakeUpTimeMs); + } + + public override void Remove(T item) + { + if (item.ServiceObjectId.ShardIndex == -1) + return; + + RouteItem(item).Remove(item); + } + + public override void Update(long now) + { + GameLoop.ExecuteForEach(_shards, _shards.Length, static shard => shard.Update(GameLoop.GameLoopTime)); + + int currentTotal = 0; + + for (int i = 0; i < Shards.Count; i++) + { + _shardStartIndices[i] = currentTotal; + currentTotal += _shards[i].LastValidIndex + 1; + } + + _totalValidCount = currentTotal; + } + } +} diff --git a/GameServer/Managers/ServiceObject/ShardedServiceObjectId.cs b/GameServer/Managers/ServiceObject/ShardedServiceObjectId.cs new file mode 100644 index 000000000..d311a7362 --- /dev/null +++ b/GameServer/Managers/ServiceObject/ShardedServiceObjectId.cs @@ -0,0 +1,9 @@ +namespace DOL.GS +{ + public class ShardedServiceObjectId : SchedulableServiceObjectId + { + public int ShardIndex { get; set; } = -1; + + public ShardedServiceObjectId(ServiceObjectType type) : base(type) { } + } +} diff --git a/GameServer/ai/ABrain.cs b/GameServer/ai/ABrain.cs index d84e4b3ef..37b99b6d7 100644 --- a/GameServer/ai/ABrain.cs +++ b/GameServer/ai/ABrain.cs @@ -8,16 +8,21 @@ namespace DOL.AI /// /// This class is the base of all artificial intelligence in game objects /// - public abstract class ABrain : IServiceObject + public abstract class ABrain : IShardedServiceObject { public FSM FSM { get; set; } - public ServiceObjectId ServiceObjectId { get; } = new(ServiceObjectType.Brain); public virtual GameNPC Body { get; set; } public virtual int ThinkInterval { get; set; } = 2500; + public long NextThinkTick { get; private set; } public bool IsActive => Body != null && Body.IsAlive && Body.ObjectState is GameObject.eObjectState.Active && Body.IsVisibleToPlayers; - public long NextThinkTick { get; set; } protected virtual int ThinkOffsetOnStart => Util.Random(750, 3000); + // IServiceObject implementation. + private readonly ShardedServiceObjectId _serviceObjectId = new(ServiceObjectType.Brain); + public ServiceObjectId ServiceObjectId => _serviceObjectId; + SchedulableServiceObjectId ISchedulableServiceObject.ServiceObjectId => _serviceObjectId; + ShardedServiceObjectId IShardedServiceObject.ServiceObjectId => _serviceObjectId; + /// /// Returns the string representation of the ABrain /// @@ -32,21 +37,21 @@ public override string ToString() .ToString(); } + public bool Schedule(long nextThinkTick) + { + NextThinkTick = nextThinkTick; + return ServiceObjectStore.Schedule(this, NextThinkTick); + } + /// /// Starts the brain thinking /// /// true if started public virtual bool Start() { - if (ServiceObjectStore.Add(this)) - { - // Offset the first think tick by a random amount so that not too many are grouped in one server tick. - // We also delay the first think tick a bit because clients tend to send positive LoS checks when they shouldn't. - NextThinkTick = GameLoop.GameLoopTime + ThinkOffsetOnStart; - return true; - } - - return false; + // Offset the first think tick by a random amount so that not too many are grouped in one server tick. + // We also delay the first think tick a bit because clients tend to send positive LoS checks when they shouldn't. + return Schedule(GameLoop.GameLoopTime + ThinkOffsetOnStart); } /// @@ -55,7 +60,7 @@ public virtual bool Start() /// true if stopped public virtual bool Stop() { - if (ServiceObjectId.IsPendingRemoval) + if (_serviceObjectId.PeekAction() is ServiceObjectId.PendingAction.Remove) return false; // Prevents overrides from doing any redundant work. Maybe counter intuitive. // Without `IsActive` check, charming a NPC that's returning to spawn would teleport it. diff --git a/GameServer/ai/brain/StandardMob/StandardMobBrain.cs b/GameServer/ai/brain/StandardMob/StandardMobBrain.cs index fc82d1650..02c3b49cc 100644 --- a/GameServer/ai/brain/StandardMob/StandardMobBrain.cs +++ b/GameServer/ai/brain/StandardMob/StandardMobBrain.cs @@ -344,7 +344,7 @@ public void ForceAddToAggroList(GameLiving living, long aggroAmount = 0) if (FSM.GetCurrentState() != FSM.GetState(eFSMStateType.AGGRO) && HasAggro) { FSM.SetCurrentState(eFSMStateType.AGGRO); - NextThinkTick = GameLoop.GameLoopTime; + Schedule(GameLoop.GameLoopTime); } static AggroAmount Add(GameLiving key, long arg) @@ -438,7 +438,7 @@ public bool UnsetTemporaryAggroList() if (FSM.GetCurrentState() != FSM.GetState(eFSMStateType.AGGRO)) FSM.SetCurrentState(eFSMStateType.AGGRO); - NextThinkTick = GameLoop.GameLoopTime; + Schedule(GameLoop.GameLoopTime); } return true; diff --git a/GameServer/ai/brain/StandardMob/StandardMobState.cs b/GameServer/ai/brain/StandardMob/StandardMobState.cs index 51143ecde..cd53f45a6 100644 --- a/GameServer/ai/brain/StandardMob/StandardMobState.cs +++ b/GameServer/ai/brain/StandardMob/StandardMobState.cs @@ -1,12 +1,13 @@ using System.Reflection; using DOL.GS; using DOL.GS.ServerProperties; +using DOL.Logging; namespace DOL.AI.Brain { public class StandardMobState : FSMState { - protected static readonly Logging.Logger log = Logging.LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); + protected static readonly Logger log = LoggerManager.Create(MethodBase.GetCurrentMethod().DeclaringType); protected StandardMobBrain _brain = null; @@ -59,7 +60,7 @@ public StandardMobState_IDLE(StandardMobBrain brain) : base(brain) public override void Enter() { _brain.Body.StopMoving(); - _brain.NextThinkTick -= _brain.ThinkInterval; // Don't stay in IDLE for a full think cycle. + _brain.Schedule(GameLoop.GameLoopTime); // Don't stay in IDLE for a full think cycle. base.Enter(); } @@ -78,7 +79,7 @@ public override void Think() _brain.FSM.SetCurrentState(eFSMStateType.ROAMING); if (_brain.FSM.GetCurrentState() != this) - _brain.NextThinkTick -= _brain.ThinkInterval; // Don't stay in IDLE for a full think cycle. + _brain.Schedule(GameLoop.GameLoopTime); // Don't stay in IDLE for a full think cycle. else base.Think(); } diff --git a/GameServer/gameobjects/GameNPC.cs b/GameServer/gameobjects/GameNPC.cs index 738d6bff0..710fc2076 100644 --- a/GameServer/gameobjects/GameNPC.cs +++ b/GameServer/gameobjects/GameNPC.cs @@ -1938,7 +1938,7 @@ public override void OnUpdateOrCreateForPlayer() { m_lastVisibleToPlayerTick = GameLoop.GameLoopTime; - if (Brain != null && !Brain.ServiceObjectId.IsSet) + if (Brain != null && !Brain.ServiceObjectId.IsActive) Brain.Start(); }