diff --git a/EXILED/Exiled.API/Enums/AuthenticationType.cs b/EXILED/Exiled.API/Enums/AuthenticationType.cs
index 9de49f902..0590ea097 100644
--- a/EXILED/Exiled.API/Enums/AuthenticationType.cs
+++ b/EXILED/Exiled.API/Enums/AuthenticationType.cs
@@ -42,5 +42,10 @@ public enum AuthenticationType
/// Indicates that the player has been authenticated as DedicatedServer.
///
DedicatedServer,
+
+ ///
+ /// Indicates that the player has been authenticated during Offline mode.
+ ///
+ Offline,
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs
index 2dd66766c..e85b54ed6 100644
--- a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs
+++ b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs
@@ -435,12 +435,11 @@ public static void SendFakeTargetRpc(Player target, NetworkIdentity behaviorOwne
///
/// EffectOnlySCP207.
///
- /// MirrorExtensions.SendCustomSync(player, player.ReferenceHub.networkIdentity, typeof(PlayerEffectsController), (writer) => {
- /// writer.WriteUInt64(1ul); // DirtyObjectsBit
- /// writer.WriteUInt32(1); // DirtyIndexCount
+ /// MirrorExtensions.SendFakeSyncObject(player, player.NetworkIdentity, typeof(PlayerEffectsController), (writer) => {
+ /// writer.WriteULong(1ul); // DirtyObjectsBit
+ /// writer.WriteUInt(1); // DirtyIndexCount
/// writer.WriteByte((byte)SyncList<byte>.Operation.OP_SET); // Operations
- /// writer.WriteUInt32(17); // EditIndex
- /// writer.WriteByte(1); // Value
+ /// writer.WriteUInt(17); // EditIndex
/// });
///
///
diff --git a/EXILED/Exiled.API/Extensions/StringExtensions.cs b/EXILED/Exiled.API/Extensions/StringExtensions.cs
index 69b184bc6..37d1a6777 100644
--- a/EXILED/Exiled.API/Extensions/StringExtensions.cs
+++ b/EXILED/Exiled.API/Extensions/StringExtensions.cs
@@ -161,7 +161,11 @@ public static string GetBefore(this string input, char symbol)
///
/// The user id.
/// Returns the raw user id.
- public static string GetRawUserId(this string userId) => userId.Substring(0, userId.LastIndexOf('@'));
+ public static string GetRawUserId(this string userId)
+ {
+ int index = userId.IndexOf('@');
+ return index == -1 ? userId : userId.Substring(0, index);
+ }
///
/// Gets a SHA256 hash of a player's user id without the authentication.
diff --git a/EXILED/Exiled.API/Features/Camera.cs b/EXILED/Exiled.API/Features/Camera.cs
index 67f5d7418..56e7e7f24 100644
--- a/EXILED/Exiled.API/Features/Camera.cs
+++ b/EXILED/Exiled.API/Features/Camera.cs
@@ -28,7 +28,7 @@ public class Camera : IWrapper, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary Camera079ToCamera = new(250);
+ internal static readonly Dictionary Camera079ToCamera = new(250, new ComponentsEqualityComparer());
private static readonly Dictionary NameToCameraType = new()
{
diff --git a/EXILED/Exiled.API/Features/Doors/AirlockController.cs b/EXILED/Exiled.API/Features/Doors/AirlockController.cs
index 62645a392..eceda5f31 100644
--- a/EXILED/Exiled.API/Features/Doors/AirlockController.cs
+++ b/EXILED/Exiled.API/Features/Doors/AirlockController.cs
@@ -20,7 +20,7 @@ public class AirlockController
///
/// A containing all known 's and their corresponding .
///
- internal static readonly Dictionary BaseToExiledControllers = new();
+ internal static readonly Dictionary BaseToExiledControllers = new(new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Doors/Door.cs b/EXILED/Exiled.API/Features/Doors/Door.cs
index 338ee1900..c06a13b9b 100644
--- a/EXILED/Exiled.API/Features/Doors/Door.cs
+++ b/EXILED/Exiled.API/Features/Doors/Door.cs
@@ -40,7 +40,7 @@ public class Door : TypeCastObject, IWrapper, IWorldSpace
///
/// A containing all known 's and their corresponding .
///
- internal static readonly Dictionary DoorVariantToDoor = new();
+ internal static readonly Dictionary DoorVariantToDoor = new(new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Generator.cs b/EXILED/Exiled.API/Features/Generator.cs
index b3f5bfb69..315f9af0f 100644
--- a/EXILED/Exiled.API/Features/Generator.cs
+++ b/EXILED/Exiled.API/Features/Generator.cs
@@ -26,7 +26,7 @@ public class Generator : IWrapper, IWorldSpace
///
/// A of on the map.
///
- internal static readonly Dictionary Scp079GeneratorToGenerator = new();
+ internal static readonly Dictionary Scp079GeneratorToGenerator = new(new ComponentsEqualityComparer());
private Room room;
///
diff --git a/EXILED/Exiled.API/Features/Hazards/Hazard.cs b/EXILED/Exiled.API/Features/Hazards/Hazard.cs
index ce5942161..253825530 100644
--- a/EXILED/Exiled.API/Features/Hazards/Hazard.cs
+++ b/EXILED/Exiled.API/Features/Hazards/Hazard.cs
@@ -25,7 +25,7 @@ public class Hazard : TypeCastObject, IWrapper
///
/// with to it's .
///
- internal static readonly Dictionary EnvironmentalHazardToHazard = new();
+ internal static readonly Dictionary EnvironmentalHazardToHazard = new(new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Items/Ammo.cs b/EXILED/Exiled.API/Features/Items/Ammo.cs
index 4c88a7f84..64c598ad7 100644
--- a/EXILED/Exiled.API/Features/Items/Ammo.cs
+++ b/EXILED/Exiled.API/Features/Items/Ammo.cs
@@ -20,7 +20,7 @@ public class Ammo : Item, IWrapper
///
/// Gets the absolute maximum amount of ammo that may be held at one time, if ammo is forcefully given to the player (regardless of worn armor or server configuration).
///
- /// For accessing the maximum amount of ammo that may be held based on worn armor and server settings, see .
+ /// For accessing the maximum amount of ammo that may be held based on worn armor and server settings, see .
///
///
public const ushort AmmoLimit = ushort.MaxValue;
diff --git a/EXILED/Exiled.API/Features/Items/Item.cs b/EXILED/Exiled.API/Features/Items/Item.cs
index cef0b5017..96d02257f 100644
--- a/EXILED/Exiled.API/Features/Items/Item.cs
+++ b/EXILED/Exiled.API/Features/Items/Item.cs
@@ -41,7 +41,7 @@ public class Item : TypeCastObject- , IWrapper
///
/// A dictionary of all 's that have been converted into .
///
- internal static readonly Dictionary BaseToItem = new();
+ internal static readonly Dictionary BaseToItem = new(new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Lift.cs b/EXILED/Exiled.API/Features/Lift.cs
index 6a939bcdd..9a3b183c2 100644
--- a/EXILED/Exiled.API/Features/Lift.cs
+++ b/EXILED/Exiled.API/Features/Lift.cs
@@ -33,7 +33,7 @@ public class Lift : IWrapper, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary ElevatorChamberToLift = new(8);
+ internal static readonly Dictionary ElevatorChamberToLift = new(8, new ComponentsEqualityComparer());
///
/// Internal list that contains all ElevatorDoor for current group.
diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs
index a3693d4f6..cb9e84fa3 100644
--- a/EXILED/Exiled.API/Features/Player.cs
+++ b/EXILED/Exiled.API/Features/Player.cs
@@ -80,6 +80,16 @@ public class Player : TypeCastObject, IEntity, IWorldSpace
/// A list of the player's items.
///
internal readonly List
- ItemsValue = new(8);
+
+ ///
+ /// A dictionary of custom item category limits.
+ ///
+ internal Dictionary CustomCategoryLimits = new();
+
+ ///
+ /// A dictionary of custom ammo limits.
+ ///
+ internal Dictionary CustomAmmoLimits = new();
#pragma warning restore SA1401
private readonly HashSet componentsInChildren = new();
@@ -282,6 +292,7 @@ public AuthenticationType AuthenticationType
"northwood" => AuthenticationType.Northwood,
"localhost" => AuthenticationType.LocalHost,
"ID_Dedicated" => AuthenticationType.DedicatedServer,
+ "offline" => AuthenticationType.Offline,
_ => AuthenticationType.Unknown,
};
}
@@ -1300,7 +1311,7 @@ public static Player Get(string args)
if (int.TryParse(args, out int id))
return Get(id);
- if (args.EndsWith("@steam") || args.EndsWith("@discord") || args.EndsWith("@northwood"))
+ if (args.EndsWith("@steam") || args.EndsWith("@discord") || args.EndsWith("@northwood") || args.EndsWith("@offline"))
{
foreach (Player player in Dictionary.Values)
{
@@ -2382,21 +2393,170 @@ public bool DropAmmo(AmmoType ammoType, ushort amount, bool checkMinimals = fals
///
/// Gets the maximum amount of ammo the player can hold, given the ammo .
- /// This method factors in the armor the player is wearing, as well as server configuration.
- /// For the maximum amount of ammo that can be given regardless of worn armor and server configuration, see .
///
/// The of the ammo to check.
- /// The maximum amount of ammo this player can carry. Guaranteed to be between 0 and .
- public int GetAmmoLimit(AmmoType type) =>
- InventorySystem.Configs.InventoryLimits.GetAmmoLimit(type.GetItemType(), referenceHub);
+ /// If the method should ignore the armor the player is wearing.
+ /// The maximum amount of ammo this player can carry.
+ public ushort GetAmmoLimit(AmmoType type, bool ignoreArmor = false)
+ {
+ if (ignoreArmor)
+ {
+ if (CustomAmmoLimits.TryGetValue(type, out ushort limit))
+ return limit;
+
+ ItemType itemType = type.GetItemType();
+ return ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FirstOrDefault(x => x.AmmoType == itemType).Limit;
+ }
+
+ return InventorySystem.Configs.InventoryLimits.GetAmmoLimit(type.GetItemType(), referenceHub);
+ }
+
+ ///
+ /// Gets the maximum amount of ammo the player can hold, given the ammo .
+ /// This limit will scale with the armor the player is wearing.
+ /// For armor ammo limits, see .
+ ///
+ /// The of the ammo to check.
+ /// The number that will define the new limit.
+ public void SetAmmoLimit(AmmoType ammoType, ushort limit)
+ {
+ CustomAmmoLimits[ammoType] = limit;
+
+ ItemType itemType = ammoType.GetItemType();
+ int index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == itemType);
+ MirrorExtensions.SendFakeSyncObject(this, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer =>
+ {
+ writer.WriteULong(2ul);
+ writer.WriteUInt(1);
+ writer.WriteByte((byte)SyncList.Operation.OP_SET);
+ writer.WriteInt(index);
+ writer.WriteAmmoLimit(new() { Limit = limit, AmmoType = itemType, });
+ });
+ }
+
+ ///
+ /// Reset a custom limit.
+ ///
+ /// The of the ammo to reset.
+ public void ResetAmmoLimit(AmmoType ammoType)
+ {
+ if (!HasCustomAmmoLimit(ammoType))
+ {
+ Log.Error($"{nameof(Player)}.{nameof(ResetAmmoLimit)}(AmmoType): AmmoType.{ammoType} does not have a custom limit.");
+ return;
+ }
+
+ CustomAmmoLimits.Remove(ammoType);
+
+ ItemType itemType = ammoType.GetItemType();
+ int index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == itemType);
+ MirrorExtensions.SendFakeSyncObject(this, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer =>
+ {
+ writer.WriteULong(2ul);
+ writer.WriteUInt(1);
+ writer.WriteByte((byte)SyncList.Operation.OP_SET);
+ writer.WriteInt(index);
+ writer.WriteAmmoLimit(ServerConfigSynchronizer.Singleton.AmmoLimitsSync[index]);
+ });
+ }
+
+ ///
+ /// Check if the player has a custom limit for a specific .
+ ///
+ /// The to check.
+ /// If the player has a custom limit for the specific .
+ public bool HasCustomAmmoLimit(AmmoType ammoType) => CustomAmmoLimits.ContainsKey(ammoType);
///
/// Gets the maximum amount of an the player can hold, based on the armor the player is wearing, as well as server configuration.
///
/// The to check.
+ /// If the method should ignore the armor the player is wearing.
/// The maximum amount of items in the category that the player can hold.
- public int GetCategoryLimit(ItemCategory category) =>
- InventorySystem.Configs.InventoryLimits.GetCategoryLimit(category, referenceHub);
+ public sbyte GetCategoryLimit(ItemCategory category, bool ignoreArmor = false)
+ {
+ int index = InventorySystem.Configs.InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList().FindIndex(x => x.Key == category);
+
+ if (ignoreArmor && index != -1)
+ {
+ if (CustomCategoryLimits.TryGetValue(category, out sbyte customLimit))
+ return customLimit;
+
+ return ServerConfigSynchronizer.Singleton.CategoryLimits[index];
+ }
+
+ sbyte limit = InventorySystem.Configs.InventoryLimits.GetCategoryLimit(category, referenceHub);
+
+ return limit == -1 ? (sbyte)1 : limit;
+ }
+
+ ///
+ /// Set the maximum amount of an the player can hold. Only works with , , , and .
+ /// This limit will scale with the armor the player is wearing.
+ /// For armor category limits, see .
+ ///
+ /// The to check.
+ /// The number that will define the new limit.
+ public void SetCategoryLimit(ItemCategory category, sbyte limit)
+ {
+ int index = InventorySystem.Configs.InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList().FindIndex(x => x.Key == category);
+
+ if (index == -1)
+ {
+ Log.Error($"{nameof(Player)}.{nameof(SetCategoryLimit)}(ItemCategory, sbyte): Cannot set category limit for ItemCategory.{category}.");
+ return;
+ }
+
+ CustomCategoryLimits[category] = limit;
+
+ MirrorExtensions.SendFakeSyncObject(this, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer =>
+ {
+ writer.WriteULong(1ul);
+ writer.WriteUInt(1);
+ writer.WriteByte((byte)SyncList.Operation.OP_SET);
+ writer.WriteInt(index);
+ writer.WriteSByte(limit);
+ });
+ }
+
+ ///
+ /// Reset a custom limit. Only works with , , , and .
+ ///
+ /// The of the category to reset.
+ public void ResetCategoryLimit(ItemCategory category)
+ {
+ int index = InventorySystem.Configs.InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList().FindIndex(x => x.Key == category);
+
+ if (index == -1)
+ {
+ Log.Error($"{nameof(Player)}.{nameof(ResetCategoryLimit)}(ItemCategory, sbyte): Cannot reset category limit for ItemCategory.{category}.");
+ return;
+ }
+
+ if (!HasCustomCategoryLimit(category))
+ {
+ Log.Error($"{nameof(Player)}.{nameof(ResetCategoryLimit)}(ItemCategory): ItemCategory.{category} does not have a custom limit.");
+ return;
+ }
+
+ CustomCategoryLimits.Remove(category);
+
+ MirrorExtensions.SendFakeSyncObject(this, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer =>
+ {
+ writer.WriteULong(1ul);
+ writer.WriteUInt(1);
+ writer.WriteByte((byte)SyncList.Operation.OP_SET);
+ writer.WriteInt(index);
+ writer.WriteSByte(ServerConfigSynchronizer.Singleton.CategoryLimits[index]);
+ });
+ }
+
+ ///
+ /// Check if the player has a custom limit for a specific .
+ ///
+ /// The to check.
+ /// If the player has a custom limit for the specific .
+ public bool HasCustomCategoryLimit(ItemCategory category) => CustomCategoryLimits.ContainsKey(category);
///
/// Adds an item of the specified type with default durability(ammo/charge) and no mods to the player's inventory.
diff --git a/EXILED/Exiled.API/Features/Ragdoll.cs b/EXILED/Exiled.API/Features/Ragdoll.cs
index 9d73104fe..e1aefa56a 100644
--- a/EXILED/Exiled.API/Features/Ragdoll.cs
+++ b/EXILED/Exiled.API/Features/Ragdoll.cs
@@ -40,7 +40,7 @@ public class Ragdoll : IWrapper, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary BasicRagdollToRagdoll = new(250);
+ internal static readonly Dictionary BasicRagdollToRagdoll = new(250, new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Recontainer.cs b/EXILED/Exiled.API/Features/Recontainer.cs
index bdbbfcdf7..a63f8ada5 100644
--- a/EXILED/Exiled.API/Features/Recontainer.cs
+++ b/EXILED/Exiled.API/Features/Recontainer.cs
@@ -35,6 +35,11 @@ public static class Recontainer
///
public static bool IsCassieBusy => Base.CassieBusy;
+ ///
+ /// Gets a value about how many generator have been activated.
+ ///
+ public static int EngagedGeneratorCount => Base._prevEngaged;
+
///
/// Gets or sets a value indicating whether the containment zone is open.
///
diff --git a/EXILED/Exiled.API/Features/Roles/FpcRole.cs b/EXILED/Exiled.API/Features/Roles/FpcRole.cs
index 27abeded8..dcf4e27cb 100644
--- a/EXILED/Exiled.API/Features/Roles/FpcRole.cs
+++ b/EXILED/Exiled.API/Features/Roles/FpcRole.cs
@@ -8,9 +8,11 @@
namespace Exiled.API.Features.Roles
{
using System.Collections.Generic;
+ using System.Reflection;
using Exiled.API.Features.Pools;
+ using HarmonyLib;
using PlayerRoles;
using PlayerRoles.FirstPersonControl;
@@ -24,6 +26,7 @@ namespace Exiled.API.Features.Roles
///
public abstract class FpcRole : Role
{
+ private static FieldInfo enableFallDamageField;
private bool isUsingStamina = true;
///
@@ -55,6 +58,19 @@ public RelativePosition RelativePosition
set => FirstPersonController.FpcModule.Motor.ReceivedPosition = value;
}
+ ///
+ /// Gets or sets a value indicating whether if the player should get damage.
+ ///
+ public bool IsFallDamageEnable
+ {
+ get => FirstPersonController.FpcModule.Motor._enableFallDamage;
+ set
+ {
+ enableFallDamageField ??= AccessTools.Field(typeof(FpcMotor), nameof(FpcMotor._enableFallDamage));
+ enableFallDamageField.SetValue(FirstPersonController.FpcModule.Motor, value);
+ }
+ }
+
///
/// Gets or sets a value indicating whether if a rotation is detected on the player.
///
diff --git a/EXILED/Exiled.API/Features/Roles/Scp049Role.cs b/EXILED/Exiled.API/Features/Roles/Scp049Role.cs
index aecc872d4..eff90bc44 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp049Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp049Role.cs
@@ -25,7 +25,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-049.
///
- public class Scp049Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp049Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp079Role.cs b/EXILED/Exiled.API/Features/Roles/Scp079Role.cs
index 8782137a7..a3de5059c 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp079Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp079Role.cs
@@ -16,6 +16,7 @@ namespace Exiled.API.Features.Roles
using MapGeneration;
using Mirror;
using PlayerRoles;
+ using PlayerRoles.PlayableScps;
using PlayerRoles.PlayableScps.Scp079;
using PlayerRoles.PlayableScps.Scp079.Cameras;
using PlayerRoles.PlayableScps.Scp079.Pinging;
@@ -31,7 +32,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-079.
///
- public class Scp079Role : Role, ISubroutinedScpRole
+ public class Scp079Role : Role, ISubroutinedScpRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp096Role.cs b/EXILED/Exiled.API/Features/Roles/Scp096Role.cs
index 6d2662ad3..f30342b17 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp096Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp096Role.cs
@@ -11,6 +11,7 @@ namespace Exiled.API.Features.Roles
using System.Linq;
using PlayerRoles;
+ using PlayerRoles.PlayableScps;
using PlayerRoles.PlayableScps.HumeShield;
using PlayerRoles.PlayableScps.Scp096;
using PlayerRoles.Subroutines;
@@ -20,7 +21,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-096.
///
- public class Scp096Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp096Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp106Role.cs b/EXILED/Exiled.API/Features/Roles/Scp106Role.cs
index 826e756a5..9480a37bc 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp106Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp106Role.cs
@@ -11,6 +11,7 @@ namespace Exiled.API.Features.Roles
using Exiled.API.Enums;
using PlayerRoles;
+ using PlayerRoles.PlayableScps;
using PlayerRoles.PlayableScps.HumeShield;
using PlayerRoles.PlayableScps.Scp049;
using PlayerRoles.PlayableScps.Scp106;
@@ -24,7 +25,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-106.
///
- public class Scp106Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp106Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp173Role.cs b/EXILED/Exiled.API/Features/Roles/Scp173Role.cs
index 23536f696..3a5f625ca 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp173Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp173Role.cs
@@ -13,6 +13,7 @@ namespace Exiled.API.Features.Roles
using Exiled.API.Features.Hazards;
using Mirror;
using PlayerRoles;
+ using PlayerRoles.PlayableScps;
using PlayerRoles.PlayableScps.HumeShield;
using PlayerRoles.PlayableScps.Scp173;
using PlayerRoles.Subroutines;
@@ -23,7 +24,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-173.
///
- public class Scp173Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp173Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp3114Role.cs b/EXILED/Exiled.API/Features/Roles/Scp3114Role.cs
index 3f7f6fae1..0d23e48ba 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp3114Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp3114Role.cs
@@ -23,7 +23,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-3114.
///
- public class Scp3114Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp3114Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Roles/Scp939Role.cs b/EXILED/Exiled.API/Features/Roles/Scp939Role.cs
index b6dca72b8..ec404f349 100644
--- a/EXILED/Exiled.API/Features/Roles/Scp939Role.cs
+++ b/EXILED/Exiled.API/Features/Roles/Scp939Role.cs
@@ -13,6 +13,7 @@ namespace Exiled.API.Features.Roles
using Exiled.API.Features.Pools;
using PlayerRoles;
+ using PlayerRoles.PlayableScps;
using PlayerRoles.PlayableScps.HumeShield;
using PlayerRoles.PlayableScps.Scp939;
using PlayerRoles.PlayableScps.Scp939.Mimicry;
@@ -28,7 +29,7 @@ namespace Exiled.API.Features.Roles
///
/// Defines a role that represents SCP-939.
///
- public class Scp939Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole
+ public class Scp939Role : FpcRole, ISubroutinedScpRole, IHumeShieldRole, ISpawnableScp
{
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Room.cs b/EXILED/Exiled.API/Features/Room.cs
index 8d0b70fe3..4ca8bc4d3 100644
--- a/EXILED/Exiled.API/Features/Room.cs
+++ b/EXILED/Exiled.API/Features/Room.cs
@@ -32,7 +32,7 @@ public class Room : MonoBehaviour, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary RoomIdentifierToRoom = new(250);
+ internal static readonly Dictionary RoomIdentifierToRoom = new(250, new ComponentsEqualityComparer());
///
/// Gets a of which contains all the instances.
diff --git a/EXILED/Exiled.API/Features/TeslaGate.cs b/EXILED/Exiled.API/Features/TeslaGate.cs
index 023d7a1b1..4d7909592 100644
--- a/EXILED/Exiled.API/Features/TeslaGate.cs
+++ b/EXILED/Exiled.API/Features/TeslaGate.cs
@@ -27,7 +27,7 @@ public class TeslaGate : IWrapper, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary BaseTeslaGateToTeslaGate = new(10);
+ internal static readonly Dictionary BaseTeslaGateToTeslaGate = new(10, new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.API/Features/Window.cs b/EXILED/Exiled.API/Features/Window.cs
index 961fbab88..63ca362fd 100644
--- a/EXILED/Exiled.API/Features/Window.cs
+++ b/EXILED/Exiled.API/Features/Window.cs
@@ -25,7 +25,7 @@ public class Window : IWrapper, IWorldSpace
///
/// A containing all known s and their corresponding .
///
- internal static readonly Dictionary BreakableWindowToWindow = new();
+ internal static readonly Dictionary BreakableWindowToWindow = new(new ComponentsEqualityComparer());
///
/// Initializes a new instance of the class.
diff --git a/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs b/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs
index 51e4ac008..61f88ec2c 100644
--- a/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs
+++ b/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs
@@ -537,6 +537,16 @@ public virtual void AddRole(Player player)
Log.Debug($"{Name}: Adding {itemName} to inventory.");
TryAddItem(player, itemName);
}
+
+ if (Ammo.Count > 0)
+ {
+ Log.Debug($"{Name}: Adding Ammo to {player.Nickname} inventory.");
+ foreach (AmmoType type in EnumUtils.Values)
+ {
+ if (type != AmmoType.None)
+ player.SetAmmo(type, Ammo.ContainsKey(type) ? Ammo[type] == ushort.MaxValue ? InventoryLimits.GetAmmoLimit(type.GetItemType(), player.ReferenceHub) : Ammo[type] : (ushort)0);
+ }
+ }
});
Log.Debug($"{Name}: Setting health values.");
@@ -910,25 +920,6 @@ private void OnInternalChangingRole(ChangingRoleEventArgs ev)
{
RemoveRole(ev.Player);
}
- else if (Check(ev.Player))
- {
- Log.Debug($"{Name}: Checking ammo stuff {Ammo.Count}");
- if (Ammo.Count > 0)
- {
- Log.Debug($"{Name}: Clearing ammo");
- ev.Ammo.Clear();
- Timing.CallDelayed(
- 0.5f,
- () =>
- {
- foreach (AmmoType type in Enum.GetValues(typeof(AmmoType)))
- {
- if (type != AmmoType.None)
- ev.Player.SetAmmo(type, Ammo.ContainsKey(type) ? Ammo[type] == ushort.MaxValue ? InventoryLimits.GetAmmoLimit(type.GetItemType(), ev.Player.ReferenceHub) : Ammo[type] : (ushort)0);
- }
- });
- }
- }
}
private void OnSpawningRagdoll(SpawningRagdollEventArgs ev)
diff --git a/EXILED/Exiled.Events/EventArgs/Interfaces/IHazardEvent.cs b/EXILED/Exiled.Events/EventArgs/Interfaces/IHazardEvent.cs
index 54f6a3f74..d10d34631 100644
--- a/EXILED/Exiled.Events/EventArgs/Interfaces/IHazardEvent.cs
+++ b/EXILED/Exiled.Events/EventArgs/Interfaces/IHazardEvent.cs
@@ -10,7 +10,7 @@ namespace Exiled.Events.EventArgs.Interfaces
using Exiled.API.Features.Hazards;
///
- /// Event args for all related events.
+ /// Event args for all related events.
///
public interface IHazardEvent : IExiledEvent
{
diff --git a/EXILED/Exiled.Events/EventArgs/Interfaces/IScp330Event.cs b/EXILED/Exiled.Events/EventArgs/Interfaces/IScp330Event.cs
new file mode 100644
index 000000000..45a3072e7
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Interfaces/IScp330Event.cs
@@ -0,0 +1,22 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.EventArgs.Interfaces
+{
+ using Exiled.API.Features.Items;
+
+ ///
+ /// Event args used for all related events.
+ ///
+ public interface IScp330Event : IItemEvent
+ {
+ ///
+ /// Gets the triggering the event.
+ ///
+ public Scp330 Scp330 { get; }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/EventArgs/Interfaces/IUsableEvent.cs b/EXILED/Exiled.Events/EventArgs/Interfaces/IUsableEvent.cs
index 6c34c16e8..67b28f5b6 100644
--- a/EXILED/Exiled.Events/EventArgs/Interfaces/IUsableEvent.cs
+++ b/EXILED/Exiled.Events/EventArgs/Interfaces/IUsableEvent.cs
@@ -10,12 +10,12 @@ namespace Exiled.Events.EventArgs.Interfaces
using Exiled.API.Features.Items;
///
- /// Event args used for all related events.
+ /// Event args used for all related events.
///
public interface IUsableEvent : IItemEvent
{
///
- /// Gets the triggering the event.
+ /// Gets the triggering the event.
///
public Usable Usable { get; }
}
diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs
index 403f5f705..2265309e4 100644
--- a/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs
@@ -46,7 +46,7 @@ public Item Item
get => newItem;
set
{
- if (!Player.Inventory.UserInventory.Items.TryGetValue(value.Serial, out _))
+ if (value != null && !Player.Inventory.UserInventory.Items.TryGetValue(value.Serial, out _))
throw new InvalidOperationException("ev.NewItem cannot be set to an item they do not have.");
newItem = value;
diff --git a/EXILED/Exiled.Events/EventArgs/Scp079/RecontainedEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp079/RecontainedEventArgs.cs
index 84a2e93ad..49b5dbebd 100644
--- a/EXILED/Exiled.Events/EventArgs/Scp079/RecontainedEventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Scp079/RecontainedEventArgs.cs
@@ -22,10 +22,16 @@ public class RecontainedEventArgs : IScp079Event
///
///
///
- public RecontainedEventArgs(Player player)
+ ///
+ ///
+ ///
+ public RecontainedEventArgs(Player player, PlayerRoles.PlayableScps.Scp079.Scp079Recontainer scp079Recontainer)
{
Player = player;
Scp079 = player.Role.As();
+ Recontainer = scp079Recontainer;
+ Attacker = Player.Get(scp079Recontainer._activatorGlass.LastAttacker);
+ IsAutomatic = scp079Recontainer._activatorGlass.LastAttacker.IsSet;
}
///
@@ -35,5 +41,20 @@ public RecontainedEventArgs(Player player)
///
public Scp079Role Scp079 { get; }
+
+ ///
+ /// Gets the instance that handle SCP-079 recontained proccess.
+ ///
+ public PlayerRoles.PlayableScps.Scp079.Scp079Recontainer Recontainer { get; }
+
+ ///
+ /// Gets the player who recontained SCP-079.
+ ///
+ public Player Attacker { get; }
+
+ ///
+ /// Gets a value indicating whether the recontainment has been made automatically or by triggering the process.
+ ///
+ public bool IsAutomatic { get; }
}
-}
\ No newline at end of file
+}
diff --git a/EXILED/Exiled.Events/EventArgs/Scp330/DroppingScp330EventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp330/DroppingScp330EventArgs.cs
index 509550074..985e90337 100644
--- a/EXILED/Exiled.Events/EventArgs/Scp330/DroppingScp330EventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Scp330/DroppingScp330EventArgs.cs
@@ -17,7 +17,7 @@ namespace Exiled.Events.EventArgs.Scp330
///
/// Contains all information before a player drops a SCP-330 candy.
///
- public class DroppingScp330EventArgs : IPlayerEvent, IDeniableEvent
+ public class DroppingScp330EventArgs : IPlayerEvent, IScp330Event, IDeniableEvent
{
///
/// Initializes a new instance of the class.
@@ -39,9 +39,12 @@ public DroppingScp330EventArgs(Player player, Scp330Bag scp330, CandyKindID cand
}
///
- /// Gets or sets a value representing the being picked up.
+ /// Gets or sets a value representing the being picked up.
///
- public Scp330 Scp330 { get; set; }
+ public Scp330 Scp330 { get; set; } // Todo Remove set
+
+ ///
+ public Item Item => Scp330;
///
/// Gets or sets a value indicating whether or not the type of candy drop.
diff --git a/EXILED/Exiled.Events/EventArgs/Scp330/EatenScp330EventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp330/EatenScp330EventArgs.cs
index 3fa09415b..29beac357 100644
--- a/EXILED/Exiled.Events/EventArgs/Scp330/EatenScp330EventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Scp330/EatenScp330EventArgs.cs
@@ -8,7 +8,7 @@
namespace Exiled.Events.EventArgs.Scp330
{
using API.Features;
-
+ using Exiled.API.Features.Items;
using Interfaces;
using InventorySystem.Items.Usables.Scp330;
@@ -16,16 +16,18 @@ namespace Exiled.Events.EventArgs.Scp330
///
/// Contains all information after a player has eaten SCP-330.
///
- public class EatenScp330EventArgs : IPlayerEvent
+ public class EatenScp330EventArgs : IPlayerEvent, IScp330Event
{
///
/// Initializes a new instance of the class.
///
/// .
+ /// .
/// .
- public EatenScp330EventArgs(Player player, ICandy candy)
+ public EatenScp330EventArgs(Player player, Scp330Bag scp330, ICandy candy)
{
Player = player;
+ Scp330 = (Scp330)Item.Get(scp330);
Candy = candy;
}
@@ -38,5 +40,11 @@ public EatenScp330EventArgs(Player player, ICandy candy)
/// Gets the player who has eaten SCP-330.
///
public Player Player { get; }
+
+ ///
+ public Scp330 Scp330 { get; }
+
+ ///
+ public Item Item => Scp330;
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/EventArgs/Scp330/EatingScp330EventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp330/EatingScp330EventArgs.cs
index d24a57ad4..825f8424b 100644
--- a/EXILED/Exiled.Events/EventArgs/Scp330/EatingScp330EventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Scp330/EatingScp330EventArgs.cs
@@ -8,7 +8,7 @@
namespace Exiled.Events.EventArgs.Scp330
{
using API.Features;
-
+ using Exiled.API.Features.Items;
using Interfaces;
using InventorySystem.Items.Usables.Scp330;
@@ -16,17 +16,19 @@ namespace Exiled.Events.EventArgs.Scp330
///
/// Contains all information before a player eats SCP-330.
///
- public class EatingScp330EventArgs : IPlayerEvent, IDeniableEvent
+ public class EatingScp330EventArgs : IPlayerEvent, IScp330Event, IDeniableEvent
{
///
/// Initializes a new instance of the class.
///
/// .
- /// .
+ /// .
+ /// .
/// .
- public EatingScp330EventArgs(Player player, ICandy candy, bool isAllowed = true)
+ public EatingScp330EventArgs(Player player, Scp330Bag scp330, ICandy candy, bool isAllowed = true)
{
Player = player;
+ Scp330 = (Scp330)Item.Get(scp330);
Candy = candy;
IsAllowed = isAllowed;
}
@@ -45,5 +47,11 @@ public EatingScp330EventArgs(Player player, ICandy candy, bool isAllowed = true)
/// Gets the player who's eating SCP-330.
///
public Player Player { get; }
+
+ ///
+ public Scp330 Scp330 { get; }
+
+ ///
+ public Item Item => Scp330;
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/EventArgs/Scp330/InteractingScp330EventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp330/InteractingScp330EventArgs.cs
index 30cd0a1ac..b87238842 100644
--- a/EXILED/Exiled.Events/EventArgs/Scp330/InteractingScp330EventArgs.cs
+++ b/EXILED/Exiled.Events/EventArgs/Scp330/InteractingScp330EventArgs.cs
@@ -8,15 +8,16 @@
namespace Exiled.Events.EventArgs.Scp330
{
using API.Features;
-
+ using Exiled.API.Features.Items;
using Interfaces;
using InventorySystem.Items.Usables.Scp330;
+ using YamlDotNet.Core.Tokens;
///
/// Contains all information before a player interacts with SCP-330.
///
- public class InteractingScp330EventArgs : IPlayerEvent, IDeniableEvent
+ public class InteractingScp330EventArgs : IPlayerEvent, IScp330Event, IDeniableEvent
{
///
/// Initializes a new instance of the class.
@@ -30,6 +31,7 @@ public class InteractingScp330EventArgs : IPlayerEvent, IDeniableEvent
public InteractingScp330EventArgs(Player player, int usage)
{
Player = player;
+ Scp330 = Scp330Bag.TryGetBag(player.ReferenceHub, out Scp330Bag scp330Bag) ? (Scp330)Item.Get(scp330Bag) : null;
Candy = Scp330Candies.GetRandom();
UsageCount = usage;
ShouldSever = usage >= 2;
@@ -60,5 +62,13 @@ public InteractingScp330EventArgs(Player player, int usage)
/// Gets the triggering the event.
///
public Player Player { get; }
+
+ ///
+ /// This value can be null.
+ public Scp330 Scp330 { get; }
+
+ ///
+ /// This value can be null.
+ public Item Item => Scp330;
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs
new file mode 100644
index 000000000..107a7c869
--- /dev/null
+++ b/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs
@@ -0,0 +1,51 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.EventArgs.Scp939
+{
+ using API.Features;
+ using API.Features.Hazards;
+ using Interfaces;
+ using PlayerRoles.PlayableScps.Scp939;
+
+ using Scp939Role = API.Features.Roles.Scp939Role;
+
+ ///
+ /// Contains all information after SCP-939 used its amnestic cloud ability.
+ ///
+ public class PlacedAmnesticCloudEventArgs : IScp939Event
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PlacedAmnesticCloudEventArgs(ReferenceHub hub, Scp939AmnesticCloudInstance cloud)
+ {
+ Player = Player.Get(hub);
+ AmnesticCloud = new AmnesticCloudHazard(cloud);
+ Scp939 = Player.Role.As();
+ }
+
+ ///
+ /// Gets the player who's controlling SCP-939.
+ ///
+ public Player Player { get; }
+
+ ///
+ /// Gets the instance.
+ ///
+ public AmnesticCloudHazard AmnesticCloud { get; }
+
+ ///
+ public Scp939Role Scp939 { get; }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Handlers/Scp939.cs b/EXILED/Exiled.Events/Handlers/Scp939.cs
index 14d1245c6..e82e94a80 100644
--- a/EXILED/Exiled.Events/Handlers/Scp939.cs
+++ b/EXILED/Exiled.Events/Handlers/Scp939.cs
@@ -32,6 +32,11 @@ public static class Scp939
///
public static Event PlacingAmnesticCloud { get; set; } = new();
+ ///
+ /// Invoked after SCP-939 used its amnestic cloud ability.
+ ///
+ public static Event PlacedAmnesticCloud { get; set; } = new();
+
///
/// Invoked before SCP-939 plays a stolen voice.
///
@@ -81,6 +86,12 @@ public static class Scp939
/// The instance.
public static void OnPlacingAmnesticCloud(PlacingAmnesticCloudEventArgs ev) => PlacingAmnesticCloud.InvokeSafely(ev);
+ ///
+ /// Called after SCP-939 used its amnestic cloud ability.
+ ///
+ /// The instance.
+ public static void OnPlacedAmnesticCloud(PlacedAmnesticCloudEventArgs ev) => PlacedAmnesticCloud.InvokeSafely(ev);
+
///
/// Called before SCP-939 plays a stolen voice.
///
diff --git a/EXILED/Exiled.Events/Patches/Events/Map/SpawningItem.cs b/EXILED/Exiled.Events/Patches/Events/Map/SpawningItem.cs
index ff3cd5762..28fd021f0 100644
--- a/EXILED/Exiled.Events/Patches/Events/Map/SpawningItem.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Map/SpawningItem.cs
@@ -67,7 +67,7 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable instruction.IsLdarg(0));
newInstructions[lastIndex].labels.Add(doorSpawn);
+ // Replace
+ // "base.RegisterUnspawnedObject(doorNametagExtension.TargetDoor, itemPickupBase.gameObject);"
+ // with "base.RegisterUnspawnedObject(ev.Door.Base, itemPickupBase.gameObject);"
offset = -1;
index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldfld) + offset;
@@ -122,7 +120,7 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable newInstructions = ListPool.Pool.Get(instructions);
- int e = 0;
+ LocalBuilder fpcRole = generator.DeclareLocal(typeof(FpcStandardRoleBase));
+
+ // replace HumanRole to FpcStandardRoleBase
+ newInstructions.Find(x => x.opcode == OpCodes.Isinst).operand = typeof(FpcStandardRoleBase);
+
+ // after this index all invalid exit are considered Custom
+ int customExit = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Ldarg_0);
for (int i = 0; i < newInstructions.Count; i++)
{
- CodeInstruction codeInstruction = newInstructions[i];
- if (codeInstruction.opcode == OpCodes.Ldc_I4_0)
- {
- e++;
- if (e > 3)
- {
- newInstructions[i].opcode = OpCodes.Ldc_I4_5;
- }
- }
+ OpCode opcode = newInstructions[i].opcode;
+ if (opcode == OpCodes.Stloc_0)
+ newInstructions[i] = new(OpCodes.Stloc_S, fpcRole.LocalIndex);
+ else if (opcode == OpCodes.Ldloc_0)
+ newInstructions[i] = new(OpCodes.Ldloc_S, fpcRole.LocalIndex);
+ else if (opcode == OpCodes.Ldc_I4_0 && i > customExit)
+ newInstructions[i].opcode = OpCodes.Ldc_I4_5;
}
for (int z = 0; z < newInstructions.Count; z++)
diff --git a/EXILED/Exiled.Events/Patches/Events/Player/Healing.cs b/EXILED/Exiled.Events/Patches/Events/Player/Healing.cs
index 6f17fad14..5acbce087 100644
--- a/EXILED/Exiled.Events/Patches/Events/Player/Healing.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Player/Healing.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (c) Exiled Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
@@ -34,6 +34,8 @@ private static IEnumerable Transpiler(IEnumerable newInstructions = ListPool.Pool.Get(instructions);
Label continueLabel = generator.DefineLabel();
+ Label skipHealing = generator.DefineLabel();
+ Label skipHealed = generator.DefineLabel();
LocalBuilder ev = generator.DeclareLocal(typeof(HealingEventArgs));
LocalBuilder player = generator.DeclareLocal(typeof(Player));
@@ -48,10 +50,14 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable
/// Patches .
@@ -25,12 +29,16 @@ namespace Exiled.Events.Patches.Events.Player
[HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.FinalizeAuthentication))]
internal static class Verified
{
- private static void Postfix(PlayerAuthenticationManager __instance)
+ ///
+ /// Called after the player has been verified.
+ ///
+ /// The player's hub.
+ internal static void PlayerVerified(ReferenceHub hub)
{
- if (!Player.UnverifiedPlayers.TryGetValue(__instance._hub.gameObject, out Player player))
- Joined.CallEvent(__instance._hub, out player);
+ if (!Player.UnverifiedPlayers.TryGetValue(hub.gameObject, out Player player))
+ Joined.CallEvent(hub, out player);
- Player.Dictionary.Add(__instance._hub.gameObject, player);
+ Player.Dictionary.Add(hub.gameObject, player);
player.IsVerified = true;
player.RawUserId = player.UserId.GetRawUserId();
@@ -39,5 +47,41 @@ private static void Postfix(PlayerAuthenticationManager __instance)
Handlers.Player.OnVerified(new VerifiedEventArgs(player));
}
+
+ private static void Postfix(PlayerAuthenticationManager __instance)
+ {
+ PlayerVerified(__instance._hub);
+ }
+ }
+
+ ///
+ /// Patches .
+ /// Adds the event during offline mode.
+ ///
+ [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))]
+ internal static class VerifiedOfflineMode
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ const int offset = 1;
+ int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt && x.OperandIs(Method(typeof(CharacterClassManager), nameof(CharacterClassManager.SyncServerCmdBinding)))) + offset;
+
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ // Verified.PlayerVerified(this._hub);
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new CodeInstruction(OpCodes.Ldfld, Field(typeof(NicknameSync), nameof(NicknameSync._hub))),
+ new CodeInstruction(OpCodes.Call, Method(typeof(Verified), nameof(Verified.PlayerVerified))),
+ });
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Events/Scp079/Recontain.cs b/EXILED/Exiled.Events/Patches/Events/Scp079/Recontain.cs
index 991ffc47e..3338c84ab 100644
--- a/EXILED/Exiled.Events/Patches/Events/Scp079/Recontain.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Scp079/Recontain.cs
@@ -37,9 +37,10 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable instruction.Calls(Method(typeof(Scp330Bag), nameof(Scp330Bag.ServerProcessPickup)))) + removeServerProcessOffset;
-
- newInstructions.RemoveRange(removeServerProcessIndex, 3);
-
- // Replace NW server process logic.
- newInstructions.InsertRange(
- removeServerProcessIndex,
- new[]
- {
- // ldarg.1 is already in the stack
-
- // ev.Candy
- new CodeInstruction(OpCodes.Ldloc, ev),
- new CodeInstruction(OpCodes.Callvirt, PropertyGetter(typeof(InteractingScp330EventArgs), nameof(InteractingScp330EventArgs.Candy))),
-
- // bag
- new CodeInstruction(OpCodes.Ldloca_S, 3),
-
- // ServerProcessPickup(ReferenceHub, CandyKindID, Scp330Bag)
- new CodeInstruction(OpCodes.Call, Method(typeof(InteractingScp330), nameof(ServerProcessPickup), new[] { typeof(ReferenceHub), typeof(CandyKindID), typeof(Scp330Bag).MakeByRefType() })),
- });
-
// This is to find the location of RpcMakeSound to remove the original code and add a new sever logic structure (Start point)
int addShouldSeverOffset = 1;
int addShouldSeverIndex = newInstructions.FindLastIndex(
instruction => instruction.Calls(Method(typeof(Scp330Interobject), nameof(Scp330Interobject.RpcMakeSound)))) + addShouldSeverOffset;
- // This is to find the location of the next return (End point)
- int includeSameLine = 1;
- int nextReturn = newInstructions.FindIndex(addShouldSeverIndex, instruction => instruction.opcode == OpCodes.Ret) + includeSameLine;
- Label originalLabel = newInstructions[addShouldSeverIndex].ExtractLabels()[0];
-
- // Remove original code from after RpcMakeSound to next return and then fully replace it.
- newInstructions.RemoveRange(addShouldSeverIndex, nextReturn - addShouldSeverIndex);
-
- addShouldSeverIndex = newInstructions.FindLastIndex(
- instruction => instruction.Calls(Method(typeof(Scp330Interobject), nameof(Scp330Interobject.RpcMakeSound)))) + addShouldSeverOffset;
+ int serverEffectLocationStart = -1;
+ int enableEffect = newInstructions.FindLastIndex(
+ instruction => instruction.LoadsField(Field(typeof(PlayerEffectsController), nameof(ReferenceHub.playerEffectsController)))) + serverEffectLocationStart;
newInstructions.InsertRange(
addShouldSeverIndex,
- new CodeInstruction[]
+ new[]
{
// if (!ev.ShouldSever)
// goto shouldNotSever;
- new CodeInstruction(OpCodes.Ldloc, ev.LocalIndex).WithLabels(originalLabel),
+ new CodeInstruction(OpCodes.Ldloc, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(InteractingScp330EventArgs), nameof(InteractingScp330EventArgs.ShouldSever))),
new(OpCodes.Brfalse, shouldNotSever),
-
- // ev.Player.EnableEffect("SevereHands", 1, 0f, false)
- new(OpCodes.Ldloc, ev.LocalIndex),
- new(OpCodes.Callvirt, PropertyGetter(typeof(InteractingScp330EventArgs), nameof(InteractingScp330EventArgs.Player))),
- new(OpCodes.Ldstr, nameof(SeveredHands)),
- new(OpCodes.Ldc_I4_1),
- new(OpCodes.Ldc_R4, 0f),
- new(OpCodes.Ldc_I4_0),
- new(OpCodes.Callvirt, Method(typeof(Player), nameof(Player.EnableEffect), new[] { typeof(string), typeof(byte), typeof(float), typeof(bool) })),
- new(OpCodes.Pop),
-
- // return;
- new(OpCodes.Ret),
+ new(OpCodes.Br, enableEffect),
});
// This will let us jump to the taken candies code and lock until ldarg_0, meaning we allow base game logic handle candy adding.
@@ -157,28 +107,5 @@ private static IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions);
}
-
- private static bool ServerProcessPickup(ReferenceHub player, CandyKindID candy, out Scp330Bag bag)
- {
- if (!Scp330Bag.TryGetBag(player, out bag))
- {
- player.inventory.ServerAddItem(ItemType.SCP330);
-
- if (!Scp330Bag.TryGetBag(player, out bag))
- return false;
-
- bag.Candies = new List { candy };
- bag.ServerRefreshBag();
-
- return true;
- }
-
- bool result = bag.TryAddSpecific(candy);
-
- if (bag.AcquisitionAlreadyReceived)
- bag.ServerRefreshBag();
-
- return result;
- }
}
}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Events/Scp939/PlacedAmnesticCloud.cs b/EXILED/Exiled.Events/Patches/Events/Scp939/PlacedAmnesticCloud.cs
new file mode 100644
index 000000000..ac224990f
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Events/Scp939/PlacedAmnesticCloud.cs
@@ -0,0 +1,73 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Events.Scp939
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Features.Pools;
+ using Exiled.Events.Attributes;
+ using Exiled.Events.EventArgs.Scp939;
+ using Exiled.Events.Handlers;
+ using HarmonyLib;
+ using PlayerRoles.PlayableScps.Scp939;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches
+ /// to add the event.
+ ///
+ [EventPatch(typeof(Scp939), nameof(Scp939.PlacedAmnesticCloud))]
+ [HarmonyPatch(typeof(Scp939AmnesticCloudAbility), nameof(Scp939AmnesticCloudAbility.OnStateEnabled))]
+ internal static class PlacedAmnesticCloud
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ LocalBuilder cloud = generator.DeclareLocal(typeof(Scp939AmnesticCloudInstance));
+
+ const int offset = -2;
+ int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt && x.OperandIs(Method(typeof(Scp939AmnesticCloudInstance), nameof(Scp939AmnesticCloudInstance.ServerSetup)))) + offset;
+
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ // Scp939AmnesticCloudInstance cloud = Object.Instantiate(this._instancePrefab)
+ new CodeInstruction(OpCodes.Dup),
+ new CodeInstruction(OpCodes.Stloc_S, cloud),
+ });
+
+ index = newInstructions.Count - 1;
+
+ // Scp939.OnPlacedAmnesticCloud(new PlacedAmnesticCloudEventArgs(this.Owner, cloud));
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ // this.Owner
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(Scp939AmnesticCloudAbility), nameof(Scp939AmnesticCloudAbility.Owner))),
+
+ // cloud
+ new CodeInstruction(OpCodes.Ldloc_S, cloud),
+
+ // Scp939.OnPlacedAmnesticCloud(new PlacedAmnesticCloudEventArgs(this.Owner, cloud));
+ new(OpCodes.Newobj, GetDeclaredConstructors(typeof(PlacedAmnesticCloudEventArgs))[0]),
+ new(OpCodes.Call, Method(typeof(Scp939), nameof(Scp939.OnPlacedAmnesticCloud))),
+ });
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Events/Server/Reporting.cs b/EXILED/Exiled.Events/Patches/Events/Server/Reporting.cs
index bba54c92e..5d22e96c5 100644
--- a/EXILED/Exiled.Events/Patches/Events/Server/Reporting.cs
+++ b/EXILED/Exiled.Events/Patches/Events/Server/Reporting.cs
@@ -40,8 +40,8 @@ private static IEnumerable Transpiler(IEnumerable instruction.opcode == OpCodes.Newarr) + offset;
+ int offset = 2;
+ int index = newInstructions.FindLastIndex(instruction => instruction.opcode == OpCodes.Ldarg_S && instruction.operand == (object)4) + offset;
Label ret = generator.DefineLabel();
diff --git a/EXILED/Exiled.Events/Patches/Fixes/ArmorDropPatch.cs b/EXILED/Exiled.Events/Patches/Fixes/ArmorDropPatch.cs
new file mode 100644
index 000000000..a924f6808
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Fixes/ArmorDropPatch.cs
@@ -0,0 +1,55 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Fixes
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using Exiled.API.Features.Pools;
+ using HarmonyLib;
+ using InventorySystem;
+ using InventorySystem.Items;
+ using InventorySystem.Items.Armor;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches to fix https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/230 bug.
+ ///
+ [HarmonyPatch(typeof(BodyArmorUtils), nameof(BodyArmorUtils.RemoveEverythingExceedingLimits))]
+ internal static class ArmorDropPatch
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label continueLabel = generator.DefineLabel();
+ int continueIndex = newInstructions.FindIndex(x => x.Is(OpCodes.Call, Method(typeof(Dictionary.Enumerator), nameof(Dictionary.Enumerator.MoveNext)))) - 1;
+ newInstructions[continueIndex].WithLabels(continueLabel);
+
+ // before: if (keyValuePair.Value.Category != ItemCategory.Armor)
+ // after: if (keyValuePair.Value.Category != ItemCategory.Armor && keyValuePair.Value.Category != ItemCategory.None)
+ int index = newInstructions.FindIndex(x => x.Is(OpCodes.Ldc_I4_S, 9));
+
+ newInstructions.InsertRange(index, new CodeInstruction[]
+ {
+ // && keyValuePair.Value.Category != ItemCategory.None)
+ new(OpCodes.Ldloca_S, 4),
+ new(OpCodes.Call, PropertyGetter(typeof(KeyValuePair), nameof(KeyValuePair.Value))),
+ new(OpCodes.Ldfld, Field(typeof(ItemBase), nameof(ItemBase.Category))),
+ new(OpCodes.Ldc_I4_0),
+ new(OpCodes.Beq_S, continueLabel),
+ });
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Fixes/Scp173FirstKillPatch.cs b/EXILED/Exiled.Events/Patches/Fixes/Scp173FirstKillPatch.cs
new file mode 100644
index 000000000..f66d35058
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Fixes/Scp173FirstKillPatch.cs
@@ -0,0 +1,55 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Fixes
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using API.Features.Pools;
+ using CustomPlayerEffects;
+ using HarmonyLib;
+ using PlayerRoles.PlayableScps.Scp173;
+ using UnityEngine;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches to fix https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/143 bug.
+ ///
+ [HarmonyPatch(typeof(Scp173SnapAbility), nameof(Scp173SnapAbility.TryHitTarget))]
+ internal static class Scp173FirstKillPatch
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label continueLabel = generator.DefineLabel();
+
+ int index = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Ldc_I4_0);
+ newInstructions[index].WithLabels(continueLabel);
+
+ newInstructions.InsertRange(index, new CodeInstruction[]
+ {
+ // if (hitboxIdentity.TargetHub.playerEffectController.GetEffect().IsEnabled) return false;
+ new(OpCodes.Ldloc_2),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(HitboxIdentity), nameof(HitboxIdentity.TargetHub))),
+ new(OpCodes.Ldfld, Field(typeof(ReferenceHub), nameof(ReferenceHub.playerEffectsController))),
+ new(OpCodes.Callvirt, Method(typeof(PlayerEffectsController), nameof(PlayerEffectsController.GetEffect), generics: new[] { typeof(SpawnProtected) })),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(StatusEffectBase), nameof(StatusEffectBase.IsEnabled))),
+ new(OpCodes.Brfalse_S, continueLabel),
+ new(OpCodes.Ldc_I4_0),
+ new(OpCodes.Ret),
+ });
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}
diff --git a/EXILED/Exiled.Events/Patches/Fixes/Scp173SecondKillPatch.cs b/EXILED/Exiled.Events/Patches/Fixes/Scp173SecondKillPatch.cs
new file mode 100644
index 000000000..2b6dcb028
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Fixes/Scp173SecondKillPatch.cs
@@ -0,0 +1,54 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Fixes
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using API.Features.Pools;
+ using CustomPlayerEffects;
+ using HarmonyLib;
+ using Mirror;
+ using PlayerRoles.PlayableScps.Scp173;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches to fix https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/143 bug.
+ ///
+ [HarmonyPatch(typeof(Scp173TeleportAbility), nameof(Scp173TeleportAbility.ServerProcessCmd))]
+ internal static class Scp173SecondKillPatch
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label returnLabel = generator.DefineLabel();
+
+ newInstructions[newInstructions.Count - 1].WithLabels(returnLabel);
+
+ int offset = -5;
+ int index = newInstructions.FindIndex(x => x.Is(OpCodes.Callvirt, Method(typeof(MovementTracer), nameof(MovementTracer.GenerateBounds)))) + offset;
+
+ newInstructions.InsertRange(index, new[]
+ {
+ // if (hub.playerEffectController.GetEffect().IsEnabled) return;
+ new CodeInstruction(OpCodes.Ldloc_S, 5).MoveLabelsFrom(newInstructions[index]),
+ new(OpCodes.Ldfld, Field(typeof(ReferenceHub), nameof(ReferenceHub.playerEffectsController))),
+ new(OpCodes.Callvirt, Method(typeof(PlayerEffectsController), nameof(PlayerEffectsController.GetEffect), generics: new[] { typeof(SpawnProtected) })),
+ new(OpCodes.Callvirt, PropertyGetter(typeof(StatusEffectBase), nameof(StatusEffectBase.IsEnabled))),
+ new(OpCodes.Brtrue_S, returnLabel),
+ });
+
+ for (int z = 0; z < newInstructions.Count; z++)
+ yield return newInstructions[z];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+}
diff --git a/EXILED/Exiled.Events/Patches/Fixes/SlownessFix.cs b/EXILED/Exiled.Events/Patches/Fixes/SlownessFix.cs
index e2e69b756..5a05c31a0 100644
--- a/EXILED/Exiled.Events/Patches/Fixes/SlownessFix.cs
+++ b/EXILED/Exiled.Events/Patches/Fixes/SlownessFix.cs
@@ -62,4 +62,4 @@ private static IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions);
}
}
-}
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Generic/GetCustomAmmoLimit.cs b/EXILED/Exiled.Events/Patches/Generic/GetCustomAmmoLimit.cs
new file mode 100644
index 000000000..eb533862e
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Generic/GetCustomAmmoLimit.cs
@@ -0,0 +1,35 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Generic
+{
+ using System;
+
+ using Exiled.API.Extensions;
+ using Exiled.API.Features;
+ using HarmonyLib;
+ using InventorySystem.Configs;
+ using UnityEngine;
+
+ ///
+ /// Patches the delegate.
+ /// Sync .
+ /// Changes to .
+ ///
+ [HarmonyPatch(typeof(InventoryLimits), nameof(InventoryLimits.GetAmmoLimit), new Type[] { typeof(ItemType), typeof(ReferenceHub) })]
+ internal static class GetCustomAmmoLimit
+ {
+#pragma warning disable SA1313
+ private static void Postfix(ItemType ammoType, ReferenceHub player, ref ushort __result)
+ {
+ if (!Player.TryGet(player, out Player ply) || !ply.CustomAmmoLimits.TryGetValue(ammoType.GetAmmoType(), out ushort limit))
+ return;
+
+ __result = (ushort)Mathf.Clamp(limit + __result - InventoryLimits.GetAmmoLimit(null, ammoType), ushort.MinValue, ushort.MaxValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Events/Patches/Generic/GetCustomCategoryLimit.cs b/EXILED/Exiled.Events/Patches/Generic/GetCustomCategoryLimit.cs
new file mode 100644
index 000000000..8ceff7538
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Generic/GetCustomCategoryLimit.cs
@@ -0,0 +1,34 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Generic
+{
+ using System;
+
+ using Exiled.API.Features;
+ using HarmonyLib;
+ using InventorySystem.Configs;
+ using UnityEngine;
+
+ ///
+ /// Patches the delegate.
+ /// Sync , .
+ /// Changes to .
+ ///
+ [HarmonyPatch(typeof(InventoryLimits), nameof(InventoryLimits.GetCategoryLimit), new Type[] { typeof(ItemCategory), typeof(ReferenceHub), })]
+ internal static class GetCustomCategoryLimit
+ {
+#pragma warning disable SA1313
+ private static void Postfix(ItemCategory category, ReferenceHub player, ref sbyte __result)
+ {
+ if (!Player.TryGet(player, out Player ply) || !ply.CustomCategoryLimits.TryGetValue(category, out sbyte limit))
+ return;
+
+ __result = (sbyte)Mathf.Clamp(limit + __result - InventoryLimits.GetCategoryLimit(null, category), sbyte.MinValue, sbyte.MaxValue);
+ }
+ }
+}
diff --git a/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs b/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs
new file mode 100644
index 000000000..46c412dc3
--- /dev/null
+++ b/EXILED/Exiled.Events/Patches/Generic/OfflineModeIds.cs
@@ -0,0 +1,164 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) Exiled Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Events.Patches.Generic
+{
+#pragma warning disable SA1402 // File may only contain a single type
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using API.Features.Pools;
+ using CentralAuth;
+ using HarmonyLib;
+ using PluginAPI.Core.Interfaces;
+ using PluginAPI.Events;
+
+ using static HarmonyLib.AccessTools;
+
+ ///
+ /// Patches to add an @offline suffix to UserIds in Offline Mode.
+ ///
+ [HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))]
+ internal static class OfflineModeIds
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ const int offset = -1;
+ int index = newInstructions.FindLastIndex(instruction => instruction.opcode == OpCodes.Call && instruction.OperandIs(PropertySetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId)))) + offset;
+
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ new CodeInstruction(OpCodes.Call, Method(typeof(OfflineModeIds), nameof(BuildUserId))),
+ });
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+
+ private static string BuildUserId(string userId) => $"{userId}@offline";
+ }
+
+ ///
+ /// Patches to add the player's UserId to the dictionary.
+ ///
+ [HarmonyPatch(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.Start))]
+ internal static class OfflineModePlayerIds
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ Label skipLabel = generator.DefineLabel();
+
+ const int offset = 1;
+ int index = newInstructions.FindLastIndex(instruction => instruction.opcode == OpCodes.Call && instruction.OperandIs(PropertySetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId)))) + offset;
+
+ // if (!Player.PlayersUserIds.ContainsKey(this.UserId))
+ // Player.PlayersUserIds.Add(this.UserId, this._hub);
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ // if (Player.PlayersUserIds.ContainsKey(this.UserId)) goto skip;
+ new(OpCodes.Ldsfld, Field(typeof(PluginAPI.Core.Player), nameof(PluginAPI.Core.Player.PlayersUserIds))),
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Call, PropertyGetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId))),
+ new(OpCodes.Callvirt, Method(typeof(Dictionary), nameof(Dictionary.ContainsKey))),
+ new(OpCodes.Brtrue_S, skipLabel),
+
+ // Player.PlayersUserIds.Add(this.UserId, this._hub);
+ new(OpCodes.Ldsfld, Field(typeof(PluginAPI.Core.Player), nameof(PluginAPI.Core.Player.PlayersUserIds))),
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Call, PropertyGetter(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager.UserId))),
+ new(OpCodes.Ldarg_0),
+ new(OpCodes.Ldfld, Field(typeof(PlayerAuthenticationManager), nameof(PlayerAuthenticationManager._hub))),
+ new(OpCodes.Callvirt, Method(typeof(Dictionary), nameof(Dictionary.Add))),
+
+ // skip:
+ new CodeInstruction(OpCodes.Nop).WithLabels(skipLabel),
+ });
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+
+ ///
+ /// Patches to prevent it from executing the event when the server is in offline mode.
+ ///
+ [HarmonyPatch(typeof(ReferenceHub), nameof(ReferenceHub.Start))]
+ internal static class OfflineModeReferenceHub
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ const int offset = 1;
+ int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt) + offset;
+
+ Label returnLabel = generator.DefineLabel();
+
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ new CodeInstruction(OpCodes.Br_S, returnLabel),
+ });
+
+ newInstructions[newInstructions.Count - 1].WithLabels(returnLabel);
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+ }
+
+ ///
+ /// Patches to execute the event when the server is in offline mode.
+ ///
+ [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.UserCode_CmdSetNick__String))]
+ internal static class OfflineModeJoin
+ {
+ private static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ List newInstructions = ListPool.Pool.Get(instructions);
+
+ const int offset = 1;
+ int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Callvirt && x.OperandIs(Method(typeof(CharacterClassManager), nameof(CharacterClassManager.SyncServerCmdBinding)))) + offset;
+
+ // EventManager.ExecuteEvent(new PlayerJoinedEvent(this._hub));
+ newInstructions.InsertRange(
+ index,
+ new[]
+ {
+ // EventManager.ExecuteEvent(new PlayerJoinedEvent(this._hub));
+ new CodeInstruction(OpCodes.Ldarg_0),
+ new CodeInstruction(OpCodes.Ldfld, Field(typeof(NicknameSync), nameof(NicknameSync._hub))),
+ new CodeInstruction(OpCodes.Call, Method(typeof(OfflineModeJoin), nameof(ExecuteNwEvent))),
+ });
+
+ for (int i = 0; i < newInstructions.Count; i++)
+ yield return newInstructions[i];
+
+ ListPool.Pool.Return(newInstructions);
+ }
+
+ private static void ExecuteNwEvent(ReferenceHub hub)
+ {
+ EventManager.ExecuteEvent(new PlayerJoinedEvent(hub));
+ }
+ }
+}
\ No newline at end of file