Skip to content

Commit

Permalink
Multi Threading?? And tasks on main thread?? Firearm extensions???
Browse files Browse the repository at this point in the history
  • Loading branch information
marchellc committed Jan 6, 2025
1 parent 4908901 commit 69a8c12
Show file tree
Hide file tree
Showing 16 changed files with 1,035 additions and 31 deletions.
2 changes: 2 additions & 0 deletions LabExtended.sln.DotSettings.user
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FLabApi_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FMirror_002Dpublicized_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FMirror_002EComponents_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002Fmscorlib_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FNorthwoodLib_002Dpublicized_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FPluginAPI_002Dpublicized_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=_002Fhome_002Fmcx_002Frefs_002FPooling_002Edll/@EntryIndexedValue">True</s:Boolean>
Expand Down Expand Up @@ -35,6 +36,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e08f021e5c743e4ac8f9175cd432d545c000_003Fca_003F0335a6d5_003FServer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASpawnableWaveBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F99d0d3ec5df64fbeb8a2a74b5700add7301200_003Ff8_003F77be23aa_003FSpawnableWaveBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASSPrimitiveSpawnerExample_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F99d0d3ec5df64fbeb8a2a74b5700add7301200_003F49_003Fbba875dd_003FSSPrimitiveSpawnerExample_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATask_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6b1444477c1d41c692ef6c02a838308529ac30_003F17_003F73c55847_003FTask_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWarhead_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8e08f021e5c743e4ac8f9175cd432d545c000_003F42_003F3cd43293_003FWarhead_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWaveManager_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F99d0d3ec5df64fbeb8a2a74b5700add7301200_003Ff1_003F63a02a2e_003FWaveManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWaveTimer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F99d0d3ec5df64fbeb8a2a74b5700add7301200_003Fac_003F1982fa7b_003FWaveTimer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
Expand Down
12 changes: 9 additions & 3 deletions LabExtended/API/Containers/AmmoContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,17 @@ public void SubstractAmmo(ItemType ammoType, ushort amount)
/// <summary>
/// Gets a value indicating whether the player has the required amount of ammo.
/// </summary>
/// <param name="itemType">The type of ammo to count.</param>
/// <param name="ammoType">The type of ammo to count.</param>
/// <param name="minAmount">The minimum required amount of ammo.</param>
/// <returns><see langword="true"/> if the player has at least <see cref="minAmount"/> of ammo, otherwise <see langword="false"/></returns>
public bool HasAmmo(ItemType itemType, ushort minAmount = 1)
=> GetAmmo(itemType) >= minAmount;
public bool HasAmmo(ItemType ammoType, ushort minAmount = 1)
=> GetAmmo(ammoType) >= minAmount;

public bool TryGetReserveAmmo(ItemType ammoType, out int reserveAmmo)
=> ReserveAmmoSync.TryGet(Inventory._hub, ammoType, out reserveAmmo);

public void SetReserveAmmo(ItemType ammoType, int reserveAmmo)
=> ReserveAmmoSync.Set(Inventory._hub, ammoType, reserveAmmo);

/// <summary>
/// Removes all the player's ammo.
Expand Down
63 changes: 60 additions & 3 deletions LabExtended/API/Containers/RoleContainer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using PlayerRoles;
using LabExtended.Extensions;
using LabExtended.Utilities.Values;

using PlayerRoles;
using PlayerRoles.Voice;
using PlayerRoles.Spectating;
using PlayerRoles.FirstPersonControl;
Expand All @@ -15,9 +18,9 @@
using PlayerRoles.PlayableScps.Scp3114;
using PlayerRoles.PlayableScps.Scp049.Zombies;

using UnityEngine;
using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers;

using LabExtended.Utilities.Values;
using UnityEngine;

namespace LabExtended.API.Containers
{
Expand Down Expand Up @@ -47,6 +50,18 @@ public RoleTypeId Type
set => Manager.ServerSetRole(value, RoleChangeReason.RemoteAdmin, RoleSpawnFlags.All);
}

public EmotionPresetType Emotion
{
get => EmotionSync.GetEmotionPreset(Manager._hub);
set => EmotionSync.ServerSetEmotionPreset(Manager._hub, value);
}

public WearableElements WearableElements
{
get => WearableSync.GetWearables(Manager._hub);
set => WearableSync.OverrideWearables(Manager._hub, value);
}

public Team Team => Type.GetTeam();
public Faction Faction => Team.GetFaction();

Expand All @@ -73,6 +88,48 @@ public RoleTypeId Type
public bool IsTutorial => Type is RoleTypeId.Tutorial;
public bool IsNone => Type is RoleTypeId.None;

public bool IsWearingScp268
{
get => WearableElements.Any(WearableElements.Scp268Hat);
set
{
if (value)
{
if (IsWearingScp268)
return;

WearableElements |= WearableElements.Scp268Hat;
return;
}

if (!IsWearingScp268)
return;

WearableElements &= ~WearableElements.Scp268Hat;
}
}

public bool IsWearingScp1344
{
get => WearableElements.Any(WearableElements.Scp1344Goggles);
set
{
if (value)
{
if (IsWearingScp268)
return;

WearableElements |= WearableElements.Scp1344Goggles;
return;
}

if (!IsWearingScp268)
return;

WearableElements &= ~WearableElements.Scp1344Goggles;
}
}

public IFpcRole FpcRole => Role as IFpcRole;
public IVoiceRole VoiceRole => Role as IVoiceRole;
public ISubroutinedRole SubroutinedRole => Role as ISubroutinedRole;
Expand Down
3 changes: 3 additions & 0 deletions LabExtended/Core/Configs/ApiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class ApiConfig
[Description("Pooling API configuration.")]
public PoolSection PoolSection { get; set; } = new PoolSection();

[Description("Multi Threading configuration.")]
public MultiThreadSection MultiThreadSection { get; set; } = new MultiThreadSection();

[Description("Voice chat threading configuration.")]
public ThreadedVoiceSection ThreadedVoiceSection { get; set; } = new ThreadedVoiceSection();

Expand Down
15 changes: 15 additions & 0 deletions LabExtended/Core/Configs/Sections/MultiThreadSection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel;

namespace LabExtended.Core.Configs.Sections;

public class MultiThreadSection
{
[Description("The maximum allowed amount of waiting jobs in a single thread handle before starting a new one.")]
public int MultiThreadHandleMaxSize { get; set; } = 10;

[Description("How many pending operations can be executed on main thread per tick.")]
public int MainThreadMaxQueueSize { get; set; } = 50;

[Description("How long can the queue update take per tick (in milliseconds).")]
public int MainThreadMaxQueueTime { get; set; } = 200;
}
2 changes: 1 addition & 1 deletion LabExtended/Core/Pooling/Pools/DictionaryPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class DictionaryPool<TKey, TValue> : PoolBase<Dictionary<TKey, TValue>>

public static DictionaryPool<TKey, TValue> Shared => _shared ??= new DictionaryPool<TKey, TValue>();

public override string Name { get; } = $"DictionaryPool<{typeof(TKey).FullName}, {typeof(TValue).FullName}>";
public override string Name { get; } = $"DictionaryPool<{typeof(TKey).Name}, {typeof(TValue).Name}>";

public Dictionary<TKey, TValue> Rent()
=> base.Rent(null, () => new Dictionary<TKey, TValue>());
Expand Down
2 changes: 1 addition & 1 deletion LabExtended/Core/Pooling/Pools/ObjectPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class ObjectPool<T> : PoolBase<T>

public static ObjectPool<T> Shared => _shared ??= new ObjectPool<T>();

public override string Name { get; } = $"ObjectPool<{typeof(T).FullName}>";
public override string Name { get; } = $"ObjectPool<{typeof(T).Name}>";

public override void HandleRent(T item)
{
Expand Down
164 changes: 164 additions & 0 deletions LabExtended/Core/Threading/MultiThreadDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using LabExtended.Attributes;
using LabExtended.Extensions;
using LabExtended.Utilities.Values;

using LabExtended.Core.Configs.Sections;
using LabExtended.Core.Pooling.Pools;

namespace LabExtended.Core.Threading;

public static class MultiThreadDispatcher
{
private static volatile LockedValue<List<MultiThreadHandle>> _threadHandlers = new LockedValue<List<MultiThreadHandle>>(new List<MultiThreadHandle>());

private static volatile int _threadIdClock = 0;
private static volatile int _operationIdClock = 0;

public static MultiThreadSection Config => ApiLoader.ApiConfig.MultiThreadSection;

public static void DispatchOnMainThread(this Action target, Action<Exception> callback = null)
=> DispatchOnMainThread(
() =>
{
target();
return null;
},
(ex, _) =>
{
callback.InvokeSafe(ex);
});

public static void DispatchOnMainThread<T>(this Func<T> target, Action<Exception, T> callback = null)
=> DispatchOnMainThread(
() => target(),
(ex, resultObj) =>
{
if (ex != null || resultObj is null || resultObj is not T result)
{
callback.InvokeSafe(ex, default);
return;
}

callback.InvokeSafe(ex, result);
});

public static void DispatchOnSideThread(this Action target, Action<Exception> callback = null)
=> DispatchOnSideThread(
() =>
{
target();
return null;
},
(ex, _) =>
{
callback.InvokeSafe(ex);
});

public static void DispatchOnSideThread<T>(this Func<T> target, Action<Exception, T> callback = null)
=> DispatchOnSideThread(
() => target(),
(ex, resultObj) =>
{
if (ex != null || resultObj is null || resultObj is not T result)
{
callback.InvokeSafe(ex, default);
return;
}

callback.InvokeSafe(ex, result);
});

public static void DispatchOnSideThread(this Func<object> target, Action<Exception, object> callback = null)
{
if (target is null)
throw new ArgumentNullException(nameof(target));

var operation = ObjectPool<MultiThreadOperation>.Shared.Rent(null, () => new MultiThreadOperation());

operation.Target = target;
operation.Callback = callback;
}

public static void DispatchOnMainThread(this Func<object> target, Action<Exception, object> callback = null)
{
if (target is null)
throw new ArgumentNullException(nameof(target));

var operation = ObjectPool<MultiThreadOperation>.Shared.Rent(null, () => new MultiThreadOperation());

operation.Target = target;
operation.Callback = callback;
operation.IsMainThread = true;
}

public static void Dispatch(MultiThreadOperation threadOperation)
{
if (threadOperation is null)
throw new ArgumentNullException(nameof(threadOperation));

if (_operationIdClock + 1 >= int.MaxValue)
_operationIdClock = 0;

threadOperation.Id = _operationIdClock++;
StartOnAvailableHandle(threadOperation);
}

private static void StartOnAvailableHandle(MultiThreadOperation threadOperation)
{
if (threadOperation is null)
return;

if (threadOperation.IsMainThread)
{
MultiThreadMainThread.ProcessOperation(threadOperation);
return;
}

_threadHandlers.Access((_, list) =>
{
var targetHandle = GetAvailableHandle(list);

if (targetHandle is null)
return;

targetHandle.Queue.Enqueue(threadOperation);

ApiLog.Debug("MultiThread", $"Dispatched operation (ID: &6{threadOperation.Id}&r) on thread ID &6{targetHandle.Id}&r (&3{threadOperation.Target.Method.GetMemberName()}&r)");
});
}

private static MultiThreadHandle GetAvailableHandle(List<MultiThreadHandle> handles)
{
if (handles.TryGetFirst(
x => x.IsRunning && (Config.MultiThreadHandleMaxSize < 1 ||
x.Queue.Count < Config.MultiThreadHandleMaxSize), out var freeHandle))
return freeHandle;

freeHandle = StartNewHandle();

handles.Add(freeHandle);
return freeHandle;
}

private static MultiThreadHandle StartNewHandle()
{
var handle = new MultiThreadHandle();

handle.Id = _threadIdClock++;
handle.IsRunning = true;

handle.Thread = new Thread(handle.RunQueue);
handle.Thread.IsBackground = true;
handle.Thread.Priority = ThreadPriority.Lowest;
handle.Thread.Start();

ApiLog.Debug("MultiThread", $"Dispatched a new worker thread (ID: &6{handle.Id}&r)");
return handle;
}

[LoaderInitialize(1)]
private static void InitDispatch()
{
MultiThreadMainThread.InitMainThread();
}
}
51 changes: 51 additions & 0 deletions LabExtended/Core/Threading/MultiThreadHandle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Collections.Concurrent;

using LabExtended.Extensions;

namespace LabExtended.Core.Threading;

public class MultiThreadHandle
{
public volatile ConcurrentQueue<MultiThreadOperation> Queue = new ConcurrentQueue<MultiThreadOperation>();
public volatile Thread Thread;

public volatile bool IsRunning;
public volatile int Id;

internal void RunQueue()
{
while (IsRunning)
{
try
{
while (Queue.TryDequeue(out var next))
{
RunOperation(next);
}
}
catch { }
}
}

private void RunOperation(MultiThreadOperation operation)
{
try
{
operation.Result = operation.Target();
operation.IsFinished = true;

operation.Callback.InvokeSafe(null, operation.Result);
operation.ReturnToPool<MultiThreadOperation>();
}
catch (Exception ex)
{
operation.Exception = ex;
operation.IsFinished = true;

operation.Callback.InvokeSafe(ex, null);
operation.ReturnToPool<MultiThreadOperation>();

ApiLog.Error("MultiThread", $"An error was caught while executing job on side thread (ID: &6{Id}&r) &6{operation.Id}&r:\n{ex.ToColoredString()}");
}
}
}
Loading

0 comments on commit 69a8c12

Please sign in to comment.