diff --git a/EXILED/Exiled.API/Enums/WearableElementType.cs b/EXILED/Exiled.API/Enums/WearableElementType.cs new file mode 100644 index 000000000..0704e954f --- /dev/null +++ b/EXILED/Exiled.API/Enums/WearableElementType.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Enums +{ + using System; + + /// + /// An enum containing all types of Wearable elements. + /// + [Flags] + public enum WearableElementType + { + /// + /// No wearable elements. + /// + None = 0, + + /// + /// SCP-268 wearable element. + /// + Scp268Hat = 1, + + /// + /// SCP-1344 wearable element. + /// + Scp1344Goggles = 2, + + /// + /// Armor wearable element. + /// if armor is not specified it's will choose the one from Inventory + /// + ArmorDefault = 4, + + /// + /// Force the Light armor wearable element. + /// + ArmorLight = ArmorDefault | 8, + + /// + /// Force the Combat armor wearable element. + /// + ArmorCombat = ArmorDefault | 16, + + /// + /// Force the Heavy armor wearable element. + /// + ArmorHeavy = ArmorDefault | 32, + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Extensions/ItemExtensions.cs b/EXILED/Exiled.API/Extensions/ItemExtensions.cs index 3a4bb791c..d07f9c3a6 100644 --- a/EXILED/Exiled.API/Extensions/ItemExtensions.cs +++ b/EXILED/Exiled.API/Extensions/ItemExtensions.cs @@ -195,6 +195,21 @@ public static int GetMaxAmmo(this FirearmType item) _ => FirearmType.None, }; + /// + /// Converts a valid firearm into a . + /// + /// The to convert. + /// The firearm type of the given item. + public static WearableElementType GetWearableElementType(this ItemType type) => type switch + { + ItemType.SCP268 => WearableElementType.Scp268Hat, + ItemType.ArmorLight => WearableElementType.ArmorLight, + ItemType.ArmorCombat => WearableElementType.ArmorCombat, + ItemType.ArmorHeavy => WearableElementType.ArmorHeavy, + ItemType.SCP1344 => WearableElementType.Scp1344Goggles, + _ => WearableElementType.None, + }; + /// /// Converts an into it's corresponding . /// diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs index a377c0a48..c4da78a8c 100644 --- a/EXILED/Exiled.API/Features/Player.cs +++ b/EXILED/Exiled.API/Features/Player.cs @@ -63,7 +63,9 @@ namespace Exiled.API.Features using PlayerRoles; using PlayerRoles.FirstPersonControl; + using PlayerRoles.FirstPersonControl.Thirdperson; using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers; + using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers.Wearables; using PlayerRoles.RoleAssign; using PlayerRoles.Spectating; using PlayerRoles.Voice; @@ -76,6 +78,8 @@ namespace Exiled.API.Features using RoundRestarting; + using Unity.Collections.LowLevel.Unsafe; + using UnityEngine; using Utils; @@ -855,6 +859,67 @@ public VoiceChatChannel VoiceChannel } } + /// + /// Gets or sets the player's wearable elements. + /// + /// + public WearableElementType Wearables + { + get + { + if (!WearableSync.TryGetData(ReferenceHub, out WearableSyncMessage data)) + return WearableElementType.None; + + WearableElements flags = data.Flags; + WearableElementType exiledFlags = WearableElementType.None; + + if (flags.HasFlagFast(WearableElements.Armor) && data.Payload.Length is 1) + { + ItemType armor = (ItemType)UnsafeUtility.As(ref data.Payload[0]); + + exiledFlags = armor.GetWearableElementType(); + } + + return (WearableElementType)flags | exiledFlags; + } + + set + { + if (value is WearableElementType.None) + { + Log.Info("None"); + + WearableSyncMessage wearableSyncMessage = new(ReferenceHub); + WearableSync.UpdateDatabaseEntry(wearableSyncMessage); + NetworkServer.SendToAll(wearableSyncMessage, 0, false); + return; + } + + WearableSync.PayloadWriter.Reset(); + Log.Info("newWearables" + value); + + if (value.HasFlagFast(WearableElementType.ArmorDefault)) + { + ItemType displayedArmor = value.HasFlagFast(WearableElementType.ArmorLight) ? ItemType.ArmorLight : + value.HasFlagFast(WearableElementType.ArmorCombat) ? ItemType.ArmorCombat : + value.HasFlagFast(WearableElementType.ArmorHeavy) ? ItemType.ArmorHeavy : + CurrentArmor?.Type ?? ItemType.None; + + if (displayedArmor is not ItemType.None) + WearableSync.PayloadWriter.WriteSByte((sbyte)displayedArmor); + else + value &= ~WearableElementType.ArmorDefault; + + value &= ~WearableElementType.ArmorLight | WearableElementType.ArmorCombat | WearableElementType.ArmorHeavy; + Log.Info("DiplayedArmor" + displayedArmor); + } + + WearableSyncMessage wearableSyncMessage2 = new(ReferenceHub, (WearableElements)value, WearableSync.PayloadWriter); + WearableSync.UpdateDatabaseEntry(wearableSyncMessage2); + NetworkServer.SendToAll(wearableSyncMessage2, 0, false); + } + } + /// /// Gets a value indicating whether the player is transmitting on a Radio. /// @@ -2528,6 +2593,20 @@ public void Broadcast(ushort duration, string message, global::Broadcast.Broadca /// public void ClearBroadcasts() => Server.Broadcast.TargetClearElements(Connection); + /// + /// Enables the specified on the player. + /// + /// The flags to enable. + /// + public void EnableWearables(WearableElementType wearableElements) => Wearables |= wearableElements; + + /// + /// Disables the specified on the player. + /// + /// The flags to disable. + /// + public void DisableWearables(WearableElementType wearableElements) => Wearables &= ~wearableElements; + /// /// Adds the amount of a specified ammo type to the player's inventory. /// diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingWearablesEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingWearablesEventArgs.cs new file mode 100644 index 000000000..6f10ff202 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingWearablesEventArgs.cs @@ -0,0 +1,57 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Player +{ + using Exiled.API.Enums; + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.Events.EventArgs.Interfaces; + + using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers.Wearables; + + using Unity.Collections.LowLevel.Unsafe; + + /// + /// Contains all information before new information about wearables is sent to clients. + /// + public class ChangingWearablesEventArgs : IPlayerEvent, IDeniableEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public ChangingWearablesEventArgs(Player player, WearableElements newWearables, bool isAllowed = true) + { + Player = player; + IsAllowed = isAllowed; + + WearableElementType exiledFlags = WearableElementType.None; + + if (newWearables.HasFlagFast(WearableElements.Armor) && WearableSync.PayloadWriter.buffer.Length is 1) + { + ItemType armor = (ItemType)UnsafeUtility.As(ref WearableSync.PayloadWriter.buffer[0]); + exiledFlags = armor.GetWearableElementType(); + } + + NewWearables = (WearableElementType)newWearables | exiledFlags; + } + + /// + public Player Player { get; } + + /// + /// Gets or sets new wearables that'll be displayed on . + /// + public WearableElementType NewWearables { get; set; } + + /// + public bool IsAllowed { get; set; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Handlers/Player.cs b/EXILED/Exiled.Events/Handlers/Player.cs index c2ffca618..a8062aa82 100644 --- a/EXILED/Exiled.Events/Handlers/Player.cs +++ b/EXILED/Exiled.Events/Handlers/Player.cs @@ -655,6 +655,11 @@ public class Player /// public static Event Scp1576TransmissionEnded { get; set; } = new(); + /// + /// Invoked before new information about wearables is sent to clients. + /// + public static Event ChangingWearables { get; set; } = new(); + /// /// Called before a player's emotion changed. /// @@ -1440,9 +1445,15 @@ public static void OnItemRemoved(ReferenceHub referenceHub, InventorySystem.Item public static void OnInteractingEmergencyButton(InteractingEmergencyButtonEventArgs ev) => InteractingEmergencyButton.InvokeSafely(ev); /// - /// Called after a 1576 transmisiion has ended. + /// Called after a 1576 transmission has ended. /// /// The instance. public static void OnScp1576TransmissionEnded(Scp1576TransmissionEndedEventArgs ev) => Scp1576TransmissionEnded.InvokeSafely(ev); + + /// + /// Called before new information about wearables is sent to clients. + /// + /// The instance. + public static void OnChangingWearables(ChangingWearablesEventArgs ev) => ChangingWearables.InvokeSafely(ev); } } \ No newline at end of file diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ChangingWearables.cs b/EXILED/Exiled.Events/Patches/Events/Player/ChangingWearables.cs new file mode 100644 index 000000000..881b8ea90 --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Events/Player/ChangingWearables.cs @@ -0,0 +1,120 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Events.Player +{ + using System.Collections.Generic; + using System.Reflection.Emit; + + using Exiled.API.Enums; + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Pools; + using Exiled.Events.Attributes; + using Exiled.Events.EventArgs.Player; + + using HarmonyLib; + + using Mirror; + + using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers.Wearables; + + using static HarmonyLib.AccessTools; + + /// + /// Patches + /// to add event. + /// + [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ChangingWearables))] + [HarmonyPatch(typeof(WearableSync), nameof(WearableSync.OverrideWearables))] + internal static class ChangingWearables + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Ldarg_1); + + LocalBuilder ev = generator.DeclareLocal(typeof(ChangingWearablesEventArgs)); + + Label returnLabel = generator.DefineLabel(); + newInstructions[^1].labels.Add(returnLabel); + + newInstructions.RemoveAt(index); + + newInstructions.InsertRange(index, new CodeInstruction[] + { + // Player.Get(hub) + new(OpCodes.Ldarg_0), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), + + // newWearables + new(OpCodes.Ldarg_1), + + // true + new(OpCodes.Ldc_I4_1), + + // ChangingWearablesEventArgs ev = new(Player.Get(hub), newWearables, true); + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ChangingWearablesEventArgs))[0]), + new(OpCodes.Dup), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, ev.LocalIndex), + + // Handlers.Player.OnChangingWearables(ev); + new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnChangingWearables))), + + // if (!ev.IsAllowed) + // return; + new(OpCodes.Callvirt, PropertyGetter(typeof(ChangingWearablesEventArgs), nameof(ChangingWearablesEventArgs.IsAllowed))), + new(OpCodes.Brfalse_S, returnLabel), + + // push ev.NewWearables to stack for check + new(OpCodes.Ldloc_S, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(ChangingWearablesEventArgs), nameof(ChangingWearablesEventArgs.NewWearables))), + }); + + int offset = -4; + index = newInstructions.FindLastIndex(x => x.opcode == OpCodes.Ldarg_0) + offset; + + newInstructions.RemoveRange(index, 4); + newInstructions.InsertRange(index, new CodeInstruction[] + { + // newWearables = ChangingWearables.WriteArmor(ev); + new(OpCodes.Ldloc_S, ev.LocalIndex), + new(OpCodes.Call, Method(typeof(ChangingWearables), nameof(WriteArmor))), + new(OpCodes.Starg_S, 1), + }); + + for (int z = 0; z < newInstructions.Count; z++) + yield return newInstructions[z]; + + ListPool.Pool.Return(newInstructions); + } + + private static WearableElements WriteArmor(ChangingWearablesEventArgs ev) + { + WearableElementType value = ev.NewWearables; + + if (value is WearableElementType.None) + return WearableElements.None; + + if (value.HasFlagFast(WearableElementType.ArmorDefault)) + { + ItemType displayedArmor = value.HasFlagFast(WearableElementType.ArmorLight) ? ItemType.ArmorLight : + value.HasFlagFast(WearableElementType.ArmorCombat) ? ItemType.ArmorCombat : + value.HasFlagFast(WearableElementType.ArmorHeavy) ? ItemType.ArmorHeavy : + ev.Player.CurrentArmor?.Type ?? ItemType.None; + + WearableSync.PayloadWriter.WriteSByte((sbyte)displayedArmor); + + value &= ~WearableElementType.ArmorLight | WearableElementType.ArmorCombat | WearableElementType.ArmorHeavy; + } + + return (WearableElements)value; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Patches/Generic/WearableArmorPatch.cs b/EXILED/Exiled.Events/Patches/Generic/WearableArmorPatch.cs new file mode 100644 index 000000000..998e9635d --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Generic/WearableArmorPatch.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Generic +{ + using System.Collections.Generic; + + using HarmonyLib; + + using PlayerRoles.FirstPersonControl.Thirdperson.Subcontrollers.Wearables; + + /// + /// Patches . + /// + [HarmonyPatch(typeof(WearableSync), nameof(WearableSync.OnHubAdded))] + internal static class WearableArmorPatch + { + private static bool Prefix(ref ReferenceHub hub) + { + foreach (KeyValuePair item in WearableSync.Database) + { + hub.connectionToClient.Send(item.Value); + } + + return false; + } + } +} \ No newline at end of file