diff --git a/EXILED/.editorconfig b/EXILED/.editorconfig index b64d58c275..3124f1d41f 100644 --- a/EXILED/.editorconfig +++ b/EXILED/.editorconfig @@ -19,7 +19,6 @@ ij_wrap_on_typing = false csharp_style_var_for_built_in_types = false:error csharp_style_var_when_type_is_apparent = false:error csharp_style_var_elsewhere = false:error -dotnet_diagnostic.IDE0305.severity = none # ReSharper properties resharper_csharp_max_line_length = 400 diff --git a/EXILED/EXILED.props b/EXILED/EXILED.props index f67ef6f8da..be76c1db26 100644 --- a/EXILED/EXILED.props +++ b/EXILED/EXILED.props @@ -7,7 +7,7 @@ net48 - 13.0 + 9.0 x64 false $(MSBuildThisFileDirectory)\bin\$(Configuration)\ @@ -15,7 +15,7 @@ - 9.10.1 + 9.10.0 false @@ -25,8 +25,8 @@ Copyright © $(Authors) 2020 - $([System.DateTime]::Now.ToString("yyyy")) Git - https://github.com/Exmod-Team/EXILED - https://github.com/Exmod-Team/EXILED + https://github.com/ExSLMod-Team/EXILED + https://github.com/ExSLMod-Team/EXILED CC-BY-SA-3.0 $(DefineConstants);PUBLIC_BETA diff --git a/EXILED/EXILED.sln b/EXILED/EXILED.sln index 60b7f5f72c..4993bbe373 100644 --- a/EXILED/EXILED.sln +++ b/EXILED/EXILED.sln @@ -21,7 +21,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exiled.CreditTags", "Exiled EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exiled.CustomRoles", "Exiled.CustomRoles\Exiled.CustomRoles.csproj", "{417C3309-8B93-4218-A1D1-D4BB7B09BE0F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "docs", "docs\docs.csproj", "{BE130A80-6819-44C6-AA1B-BF068DEA67FF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Exiled.Starter", "Exiled.Starter\Exiled.Starter.csproj", "{290794B1-EABF-4416-A36E-57F341B68372}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -45,7 +45,6 @@ Global {10A8BFEC-B9E2-4119-BB21-2CF1EA820D19}.Installer|Any CPU.ActiveCfg = Installer|Any CPU {10A8BFEC-B9E2-4119-BB21-2CF1EA820D19}.Installer|Any CPU.Build.0 = Installer|Any CPU {10A8BFEC-B9E2-4119-BB21-2CF1EA820D19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10A8BFEC-B9E2-4119-BB21-2CF1EA820D19}.Release|Any CPU.Build.0 = Release|Any CPU {4FFB9CEB-2956-4F62-88B3-6416DB8A8ED7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FFB9CEB-2956-4F62-88B3-6416DB8A8ED7}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FFB9CEB-2956-4F62-88B3-6416DB8A8ED7}.Installer|Any CPU.ActiveCfg = Installer|Any CPU @@ -60,7 +59,6 @@ Global {4F183633-0A36-408C-A42E-6FBA48751054}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F183633-0A36-408C-A42E-6FBA48751054}.Installer|Any CPU.ActiveCfg = Installer|Any CPU {4F183633-0A36-408C-A42E-6FBA48751054}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4F183633-0A36-408C-A42E-6FBA48751054}.Release|Any CPU.Build.0 = Release|Any CPU {B7FBA3C1-6182-4E96-A33B-053EDDCC4F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B7FBA3C1-6182-4E96-A33B-053EDDCC4F65}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7FBA3C1-6182-4E96-A33B-053EDDCC4F65}.Installer|Any CPU.ActiveCfg = Installer|Any CPU @@ -70,18 +68,17 @@ Global {9FEBCAEA-EB51-46D0-BC04-F74789A40079}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FEBCAEA-EB51-46D0-BC04-F74789A40079}.Installer|Any CPU.ActiveCfg = Installer|Any CPU {9FEBCAEA-EB51-46D0-BC04-F74789A40079}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9FEBCAEA-EB51-46D0-BC04-F74789A40079}.Release|Any CPU.Build.0 = Release|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Installer|Any CPU.ActiveCfg = Installer|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU {417C3309-8B93-4218-A1D1-D4BB7B09BE0F}.Release|Any CPU.Build.0 = Release|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Installer|Any CPU.ActiveCfg = Installer|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Installer|Any CPU.Build.0 = Installer|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE130A80-6819-44C6-AA1B-BF068DEA67FF}.Release|Any CPU.Build.0 = Release|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Installer|Any CPU.ActiveCfg = Debug|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Installer|Any CPU.Build.0 = Debug|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {290794B1-EABF-4416-A36E-57F341B68372}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EXILED/Exiled.API/Enums/AdminToyType.cs b/EXILED/Exiled.API/Enums/AdminToyType.cs index 381e592f27..bcb0d9668d 100644 --- a/EXILED/Exiled.API/Enums/AdminToyType.cs +++ b/EXILED/Exiled.API/Enums/AdminToyType.cs @@ -54,7 +54,7 @@ public enum AdminToyType TextToy, /// - /// Waypoint toy. + /// Waypoint Toy. /// WaypointToy, } diff --git a/EXILED/Exiled.API/Enums/BoneType.cs b/EXILED/Exiled.API/Enums/BoneType.cs new file mode 100644 index 0000000000..d2b213e5d2 --- /dev/null +++ b/EXILED/Exiled.API/Enums/BoneType.cs @@ -0,0 +1,50 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Enums +{ + /// + /// Более подробная чем точка попадания в игрока. + /// + public enum BoneType + { + /// + /// Попадание в голову. + /// + Head = 0, + + /// + /// Попадание в тело. + /// + Body = 1, + + /// + /// Попадание в левую руку. + /// + LeftHand = 2, + + /// + /// Попадание в правую руку. + /// + RightHand = 3, + + /// + /// Попадание в левую ногу. + /// + LeftLeg = 4, + + /// + /// Попадание в правую ногу. + /// + RightLeg = 5, + + /// + /// Зону попадания не удалось определить. + /// + Unknown = 6, + } +} diff --git a/EXILED/Exiled.API/Enums/KeycardPermissions.cs b/EXILED/Exiled.API/Enums/KeycardPermissions.cs index 044e967eb5..a70564c0cf 100644 --- a/EXILED/Exiled.API/Enums/KeycardPermissions.cs +++ b/EXILED/Exiled.API/Enums/KeycardPermissions.cs @@ -31,11 +31,6 @@ public enum KeycardPermissions /// Checkpoints = 1, - /// - /// Opens Gate A and Gate B. - /// - ExitGates = 2, - /// /// Opens the Intercom door. /// @@ -80,5 +75,10 @@ public enum KeycardPermissions /// . /// ScpOverride = 1024, // 0x0400 + + /// + /// Opens Gate A and Gate B. + /// + ExitGates = 2050, // 0x0800 } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Enums/LockerType.cs b/EXILED/Exiled.API/Enums/LockerType.cs index 14c2f365df..705faf2ee2 100644 --- a/EXILED/Exiled.API/Enums/LockerType.cs +++ b/EXILED/Exiled.API/Enums/LockerType.cs @@ -15,10 +15,9 @@ namespace Exiled.API.Enums public enum LockerType { /// - /// The pedestal used by SCP items. + /// Unknown type of locker. /// - [Obsolete("This value is not used.")] - Pedestal, + Unknown, /// /// Large weapon locker. @@ -45,17 +44,6 @@ public enum LockerType /// Adrenaline, - /// - /// Unknown type of locker. - /// - Unknown, - - /// - /// Unknown type of locker. - /// - [Obsolete("Use LockerType.Unknown", true)] - Unknow = Unknown, - /// /// MircoHid pedestal. /// @@ -120,5 +108,10 @@ public enum LockerType /// SCP-127 pedestal. /// Scp127Pedestal, + + /// + /// SCP pedestal. + /// + ScpPedestal, } } diff --git a/EXILED/Exiled.API/Enums/RoomType.cs b/EXILED/Exiled.API/Enums/RoomType.cs index 0b7737c193..5133407188 100644 --- a/EXILED/Exiled.API/Enums/RoomType.cs +++ b/EXILED/Exiled.API/Enums/RoomType.cs @@ -335,7 +335,7 @@ public enum RoomType EzSmallrooms, /// - /// Heavy Containment Zone's SCP-127 room. + /// Heavy Containment Zone's SCP-330 room. /// Hcz127, diff --git a/EXILED/Exiled.API/Enums/SpawnLocationType.cs b/EXILED/Exiled.API/Enums/SpawnLocationType.cs index 2fc9d2a780..792b706c0a 100644 --- a/EXILED/Exiled.API/Enums/SpawnLocationType.cs +++ b/EXILED/Exiled.API/Enums/SpawnLocationType.cs @@ -79,10 +79,14 @@ public enum SpawnLocationType InsideLczCafe, /// - /// Inside the Nuke armory. + /// Inside the Hid Lab. /// - [Obsolete("This Location has been removed from the game.")] - InsideNukeArmory, + InsideHidLab, + + /// + /// Inside the SCP-127 Lab. + /// + Inside127Lab, /// /// Inside the surface nuke room. @@ -129,18 +133,6 @@ public enum SpawnLocationType /// InsideHidChamber, - /// - /// Inside the lower door that leads to the stairs in Micro-HID room. - /// - [Obsolete("This location has been removed from the game.")] - InsideHidLower, - - /// - /// Inside the upper door that leads into the Micro-HID room just after the stairs. - /// - [Obsolete("This location has been removed from the game. Use InsideHidLab instead.")] - InsideHidUpper, - /// /// Just inside the LCZ WC door. /// @@ -170,15 +162,5 @@ public enum SpawnLocationType /// Inside SCP-079's Armory /// Inside079Armory, - - /// - /// Inside SCP-127's Lab - /// - Inside127Lab, - - /// - /// Inside the upper door that leads into the Micro-HID Lab room. - /// - InsideHidLab, } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Exiled.API.csproj b/EXILED/Exiled.API/Exiled.API.csproj index 31fe36e002..529e6578fe 100644 --- a/EXILED/Exiled.API/Exiled.API.csproj +++ b/EXILED/Exiled.API/Exiled.API.csproj @@ -22,11 +22,15 @@ - - $(EXILED_REFERENCES)\Assembly-CSharp-Publicized.dll - + + + $(EXILED_REFERENCES)\Exiled.CustomRoles.dll + + + $(EXILED_REFERENCES)\FLXLib.dll + @@ -34,22 +38,26 @@ + + $(EXILED_REFERENCES)\UnityEngine.AudioModule.dll + - - - if not "$(EXILED_DEV_PLUGINAPI_REFERENCE)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_PLUGINAPI_REFERENCE)\dependencies\" && if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\dependencies\" + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\dependencies\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/dependencies/"; fi - if [[ ! -z "$EXILED_DEV_PLUGINAPI_REFERENCE" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_PLUGINAPI_REFERENCE/dependencies/"; fi diff --git a/EXILED/Exiled.API/Extensions/BoneTypeExtensions.cs b/EXILED/Exiled.API/Extensions/BoneTypeExtensions.cs new file mode 100644 index 0000000000..aa8f5e1ca9 --- /dev/null +++ b/EXILED/Exiled.API/Extensions/BoneTypeExtensions.cs @@ -0,0 +1,58 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Extensions +{ + using System; + + using Exiled.API.Enums; + using Exiled.API.Features; + using UnityEngine; + + /// + /// Содержит методы для определения точки попадания в игрока. + /// + public static class BoneTypeExtensions + { + /// + /// Определяет точку попадания в игрока по конкретному хитбоксу. + /// + /// Игрок, попадание в которого рассчитывется. + /// Хитбокс, попадание в которого. + /// Точная зона попадания в игрока. + public static BoneType GetByMassCenter(this Player target, HitboxIdentity hitboxIdentity) + { + BoneType boneType; + Vector3 boneLocalMassCenter = target.Transform.InverseTransformPoint(hitboxIdentity.CenterOfMass); + switch (hitboxIdentity._dmgMultiplier) + { + case HitboxType.Headshot: + boneType = BoneType.Head; + break; + case HitboxType.Body: + if (IsArms(boneLocalMassCenter)) + boneType = IsRight(boneLocalMassCenter) ? BoneType.RightHand : BoneType.LeftHand; + else + boneType = BoneType.Body; + break; + case HitboxType.Limb: + boneType = IsRight(boneLocalMassCenter) ? BoneType.RightLeg : BoneType.LeftLeg; + break; + default: + boneType = BoneType.Unknown; + break; + } + + Log.Debug($"[{nameof(GetByMassCenter)}] [{hitboxIdentity._dmgMultiplier}] at {boneLocalMassCenter}: {boneType}"); + return boneType; + } + + private static bool IsArms(Vector3 point) => point.z <= -0.2 || Math.Abs(point.x) > 0.1; + + private static bool IsRight(Vector3 point) => point.x > 0; + } +} diff --git a/EXILED/Exiled.API/Extensions/CommonExtensions.cs b/EXILED/Exiled.API/Extensions/CommonExtensions.cs index 6b175fc281..70b556c415 100644 --- a/EXILED/Exiled.API/Extensions/CommonExtensions.cs +++ b/EXILED/Exiled.API/Extensions/CommonExtensions.cs @@ -7,11 +7,17 @@ namespace Exiled.API.Extensions { + using System; using System.Collections.Generic; using System.Linq; + using Exiled.API.Features; + using PlayerRoles; + using UnityEngine; + using Random = UnityEngine.Random; + /// /// A set of extensions for common things. /// @@ -79,5 +85,35 @@ public static AnimationCurve Add(this AnimationCurve curve, float amount) curve.keys = keys; return curve; } + + /// + /// Adds an action for OnCollisionEnter event of specified gameObject. + /// + /// GameObject to attach action. + /// Action on collision. + /// GameObject that will be ignored in collision. + /// Delay before collision may be proceeded. + public static void AttachActionOnCollision(this GameObject gameObject, Action action, Player owner = null, float fuseDelay = 0.15f) + { + gameObject.AddComponent().Init((owner ?? Server.Host).GameObject, action, fuseDelay); + } + + /// + /// Заменяет вспомогательные теги в тексте (в основном для кесси). + /// + /// Искомый текст. + /// Новый текст. + public static string ReplaceVars(this string text) => text + .Replace("{classd}", Player.List.Count(x => x.Role.Team == Team.ClassD).ToString()) + .Replace("{scps}", Player.List.Count(x => x.Role.Team == Team.SCPs).ToString()) + .Replace("{scpsno079}", Player.List.Count(x => x.Role.Team == Team.SCPs && x.Role.Type != RoleTypeId.Scp079).ToString()) + .Replace("{scientists}", Player.List.Count(x => x.Role.Team == Team.Scientists).ToString()) + + .Replace("{mtf}", Player.List.Count(x => x.Role.Team == Team.FoundationForces && x.Role.Type != RoleTypeId.FacilityGuard).ToString()) + .Replace("{guards}", Player.List.Count(x => x.Role.Type == RoleTypeId.FacilityGuard).ToString()) + .Replace("{foundationforces}", Player.List.Count(x => x.Role.Team == Team.FoundationForces).ToString()) + + .Replace("{ci}", Player.List.Count(x => x.Role.Team == Team.ChaosInsurgency).ToString()) + .Replace("{human}", Player.List.Count(x => x.IsHuman).ToString()); } } diff --git a/EXILED/Exiled.API/Extensions/EffectTypeExtension.cs b/EXILED/Exiled.API/Extensions/EffectTypeExtension.cs index c44a32362d..f7120b229f 100644 --- a/EXILED/Exiled.API/Extensions/EffectTypeExtension.cs +++ b/EXILED/Exiled.API/Extensions/EffectTypeExtension.cs @@ -15,7 +15,6 @@ namespace Exiled.API.Extensions using CustomPlayerEffects; using CustomRendering; using Enums; - using Exiled.API.Features; using InventorySystem.Items.MarshmallowMan; using InventorySystem.Items.Usables.Scp244.Hypothermia; using PlayerRoles.FirstPersonControl; @@ -132,15 +131,7 @@ public static bool TryGetType(this EffectType effect, out Type type) /// The enum. /// The . public static EffectType GetEffectType(this StatusEffectBase statusEffectBase) - { - if (!TypeToEffectType.TryGetValue(statusEffectBase.GetType(), out EffectType type)) - { - Log.Warn($"Missing EffectType for Type {statusEffectBase.GetType()}!!! This issue likely originates from a new update or a CustomEffect on your Server"); - return EffectType.None; - } - - return type; - } + => TypeToEffectType.TryGetValue(statusEffectBase.GetType(), out EffectType effect) ? effect : throw new InvalidOperationException($"Invalid effect status base provided {statusEffectBase.GetType().Name}"); /// /// Gets the of the specified . diff --git a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs index 605e6466c0..ec216ac53e 100644 --- a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs +++ b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs @@ -10,6 +10,7 @@ namespace Exiled.API.Extensions using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Diagnostics; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -186,24 +187,25 @@ public static ReadOnlyDictionary RpcFullNames /// Weapon' sound to play. /// Sound's volume to set. /// GunAudioMessage's audioClipId to set (default = 0). - [Obsolete("This method is not working. Use PlayGunSound(Player, Vector3, FirearmType, float, int, bool) overload instead.")] + [Obsolete("This method is not working. Use PlayGunSound(Player, Vector3, ItemType, float, int, bool) overload instead.")] public static void PlayGunSound(this Player player, Vector3 position, ItemType itemType, byte volume, byte audioClipId = 0) - => PlayGunSound(player, position, itemType.GetFirearmType(), volume, audioClipId); + { + } /// /// Plays a gun sound that only the can hear. /// /// Target to play. /// Position to play on. - /// Weapon's sound to play. + /// Weapon's sound to play. /// Speed of sound. /// Index of clip. - public static void PlayGunSound(this Player player, Vector3 position, FirearmType firearmType, float pitch = 1, int clipIndex = 0) + public static void PlayGunSound(this Player player, Vector3 position, FirearmType itemType, float pitch = 1, int clipIndex = 0) { - if (firearmType is FirearmType.ParticleDisruptor or FirearmType.None) + if (itemType is FirearmType.ParticleDisruptor or FirearmType.None) return; - Features.Items.Firearm firearm = Features.Items.Firearm.ItemTypeToFirearmInstance[firearmType]; + Features.Items.Firearm firearm = Features.Items.Firearm.ItemTypeToFirearmInstance[itemType]; if (firearm == null) return; @@ -350,62 +352,23 @@ public static void SetName(this Player target, Player player, string name) /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is NTF). public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable playersToAffect, bool skipJump = false, byte unitId = 0) { - if (!player.IsConnected || !RoleExtensions.TryGetRoleBase(type, out PlayerRoleBase roleBase)) + if (!player.IsConnected) return; - bool isRisky = type.GetTeam() is Team.Dead || player.IsDead; + Log.Error($"{nameof(ChangeAppearance)} Вызывал {new StackTrace()} [IRacle] проверь можно ли переписать на свои штуки"); - NetworkWriterPooled writer = NetworkWriterPool.Get(); - writer.WriteUShort(38952); - writer.WriteUInt(player.NetId); - writer.WriteRoleType(type); - - if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) + if (!player.Role.CheckAppearanceCompatibility(type)) { - if (player.Role.Base is not HumanRole) - isRisky = true; - writer.WriteByte(unitId); - } - - if (roleBase is ZombieRole) - { - if (player.Role.Base is not ZombieRole) - isRisky = true; - - writer.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(player.MaxHealth), ushort.MinValue, ushort.MaxValue)); - writer.WriteBool(true); - } - - if (roleBase is Scp1507Role) - { - if (player.Role.Base is not Scp1507Role) - isRisky = true; - - writer.WriteByte((byte)player.Role.SpawnReason); - } - - if (roleBase is FpcStandardRoleBase fpc) - { - if (player.Role.Base is not FpcStandardRoleBase playerfpc) - isRisky = true; - else - fpc = playerfpc; - - ushort value = 0; - fpc?.FpcModule.MouseLook.GetSyncValues(0, out value, out ushort _); - writer.WriteRelativePosition(player.RelativePosition); - writer.WriteUShort(value); + Log.Error($"[IRacle] Кринжанули братки {player.Role.Type} {type}"); + return; } foreach (Player target in playersToAffect) { - if (target != player || !isRisky) - target.Connection.Send(writer.ToArraySegment()); - else - Log.Error($"Prevent Seld-Desync of {player.Nickname} with {type}"); + player.Role.TrySetIndividualAppearance(target, type, false); } - NetworkWriterPool.Return(writer); + player.Role.UpdateAppearance(); // To counter a bug that makes the player invisible until they move after changing their appearance, we will teleport them upwards slightly to force a new position update for all clients. if (!skipJump) @@ -516,7 +479,7 @@ public static void PlayCassieAnnouncement(this Player player, string words, bool { if (controller != null) { - SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), words, makeHold, makeNoise, isSubtitles); + SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), words.ReplaceVars(), makeHold, makeNoise, isSubtitles); } } } @@ -527,16 +490,15 @@ public static void PlayCassieAnnouncement(this Player player, string words, bool /// Target to send. /// The message to be reproduced. /// The translation should be show in the subtitles. - /// The custom subtitles to show. /// Same on 's isHeld. /// Same on 's isNoisy. /// Same on 's isSubtitles. - public static void MessageTranslated(this Player player, string words, string translation, string customSubtitles, bool makeHold = false, bool makeNoise = true, bool isSubtitles = true) + public static void MessageTranslated(this Player player, string words, string translation, bool makeHold = false, bool makeNoise = true, bool isSubtitles = true) { StringBuilder announcement = StringBuilderPool.Pool.Get(); - string[] cassies = words.Split('\n'); - string[] translations = translation.Split('\n'); + string[] cassies = words.ReplaceVars().Split('\n'); + string[] translations = translation.ReplaceVars().Split('\n'); for (int i = 0; i < cassies.Length; i++) announcement.Append($"{translations[i].Replace(' ', ' ')} {cassies[i]} "); @@ -545,10 +507,10 @@ public static void MessageTranslated(this Player player, string words, string tr foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers) { - if (controller != null) - { - SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), message, makeHold, makeNoise, isSubtitles, customSubtitles); - } + if (!controller) + continue; + + SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), message, makeHold, makeNoise, isSubtitles); } } @@ -560,9 +522,6 @@ public static void MessageTranslated(this Player player, string words, string tr /// The position to change. public static void MoveNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 pos) { - if (identity == null) - return; - identity.gameObject.transform.position = pos; ObjectDestroyMessage objectDestroyMessage = new() { @@ -610,9 +569,6 @@ public static void ChangeSceneToAllClients(ScenesType scene) /// The scale the object needs to be set to. public static void ScaleNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 scale) { - if (identity == null) - return; - identity.gameObject.transform.localScale = scale; ObjectDestroyMessage objectDestroyMessage = new() { @@ -630,9 +586,6 @@ public static void ScaleNetworkIdentityObject(this Player player, NetworkIdentit /// The position to change. public static void MoveNetworkIdentityObject(this NetworkIdentity identity, Vector3 pos) { - if (identity == null) - return; - identity.gameObject.transform.position = pos; ObjectDestroyMessage objectDestroyMessage = new() { @@ -653,9 +606,6 @@ public static void MoveNetworkIdentityObject(this NetworkIdentity identity, Vect /// The scale the object needs to be set to. public static void ScaleNetworkIdentityObject(this NetworkIdentity identity, Vector3 scale) { - if (identity == null) - return; - identity.gameObject.transform.localScale = scale; ObjectDestroyMessage objectDestroyMessage = new() { @@ -680,7 +630,7 @@ public static void ScaleNetworkIdentityObject(this NetworkIdentity identity, Vec /// Value of send to target. public static void SendFakeSyncVar(this Player target, NetworkIdentity behaviorOwner, Type targetType, string propertyName, T value) { - if (!target.IsConnected || behaviorOwner == null) + if (!target.IsConnected) return; NetworkWriterPooled writer = NetworkWriterPool.Get(); @@ -724,12 +674,7 @@ void CustomSyncVarGenerator(NetworkWriter targetWriter) /// of object that owns . /// 's type. /// Property name starting with Network. - public static void ResyncSyncVar(NetworkIdentity behaviorOwner, Type targetType, string propertyName) - { - if (behaviorOwner == null) - return; - SetDirtyBitsMethodInfo.Invoke(behaviorOwner.gameObject.GetComponent(targetType), new object[] { SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"] }); - } + public static void ResyncSyncVar(NetworkIdentity behaviorOwner, Type targetType, string propertyName) => SetDirtyBitsMethodInfo.Invoke(behaviorOwner.gameObject.GetComponent(targetType), new object[] { SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"] }); /// /// Send fake values to client's . @@ -741,7 +686,7 @@ public static void ResyncSyncVar(NetworkIdentity behaviorOwner, Type targetType, /// Values of send to target. public static void SendFakeTargetRpc(Player target, NetworkIdentity behaviorOwner, Type targetType, string rpcName, params object[] values) { - if (!target.IsConnected || behaviorOwner == null) + if (!target.IsConnected) return; NetworkWriterPooled writer = NetworkWriterPool.Get(); diff --git a/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs b/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs index 21b1d61043..3467d7725b 100644 --- a/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs +++ b/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs @@ -63,7 +63,10 @@ public static void CopyProperties(this object target, object source) throw new InvalidTypeException("Target and source type mismatch!"); foreach (PropertyInfo sourceProperty in type.GetProperties()) - type.GetProperty(sourceProperty.Name)?.SetValue(target, sourceProperty.GetValue(source, null), null); + { + if (sourceProperty.SetMethod != null && sourceProperty.GetMethod != null) + sourceProperty.SetValue(target, sourceProperty.GetValue(source, null), null); + } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Extensions/RoleExtensions.cs b/EXILED/Exiled.API/Extensions/RoleExtensions.cs index 369910654a..07a998804e 100644 --- a/EXILED/Exiled.API/Extensions/RoleExtensions.cs +++ b/EXILED/Exiled.API/Extensions/RoleExtensions.cs @@ -12,7 +12,10 @@ namespace Exiled.API.Extensions using System.Linq; using Enums; - using Features.Spawn; + using Exiled.API.Features; + using Exiled.API.Features.Roles; + using Exiled.API.Features.Spawn; + using FLXLib.Extensions; using Footprinting; using InventorySystem; using InventorySystem.Configs; @@ -229,6 +232,37 @@ public static Dictionary GetStartingAmmo(this RoleTypeId roleT return info.Ammo.ToDictionary(kvp => kvp.Key.GetAmmoType(), kvp => kvp.Value); } + /// + /// Gets a custom appearance for target , using , and . + /// + /// The player >, whose appearance we want to get. + /// Target . + /// A valid , what target will see. + public static RoleTypeId GetAppearanceForPlayer(this Role role, Player player) + { + RoleTypeId appearance = role.GlobalAppearance; + + if (player == null) + return appearance; + + if (role.IndividualAppearances.TryGetValue(player, out appearance)) + { + return appearance; + } + + if (role.RoleAppearances.TryGetValue(player.GetCustomOrBasicRole(), out appearance)) + { + return appearance; + } + + if (role.TeamAppearances.TryGetValue(player.Role.Team, out appearance)) + { + return appearance; + } + + return role.GlobalAppearance; + } + /// /// Gets the of a . /// diff --git a/EXILED/Exiled.API/Extensions/SpawnExtensions.cs b/EXILED/Exiled.API/Extensions/SpawnExtensions.cs index 5e21f52eaa..4f72ef545a 100644 --- a/EXILED/Exiled.API/Extensions/SpawnExtensions.cs +++ b/EXILED/Exiled.API/Extensions/SpawnExtensions.cs @@ -27,6 +27,7 @@ public static class SpawnExtensions SpawnLocationType.Inside079First, SpawnLocationType.InsideHidLab, SpawnLocationType.Inside173Gate, + SpawnLocationType.Inside173Armory, SpawnLocationType.InsideGateA, SpawnLocationType.InsideGateB, SpawnLocationType.InsideLczWc, diff --git a/EXILED/Exiled.API/Extensions/StringExtensions.cs b/EXILED/Exiled.API/Extensions/StringExtensions.cs index 1cc8dc7c32..520ca36b14 100644 --- a/EXILED/Exiled.API/Extensions/StringExtensions.cs +++ b/EXILED/Exiled.API/Extensions/StringExtensions.cs @@ -178,5 +178,92 @@ public static string GetHashedUserId(this string userId) byte[] hash = Sha256.ComputeHash(textData); return BitConverter.ToString(hash).Replace("-", string.Empty); } + + /// + /// Checks if custom info string valid for NW Client. + /// + /// Custominfo string to check. + /// Info about denial reason. + /// Is Custominfo valid. + public static bool IsCustomInfoValid(this string customInfo, out string denialReason) + { + denialReason = string.Empty; + if (string.IsNullOrEmpty(customInfo)) + return true; + + bool flag1 = customInfo.Contains("<"); + bool flag2 = customInfo.Contains("\\u003c"); + if (!flag1 && !flag2) + return true; + + List stringList = new(); + if (flag1) + stringList.AddRange(customInfo.Split(new[] { '<' }, StringSplitOptions.None)); + + if (flag2) + stringList.AddRange(customInfo.Split(new[] { "\\u003c" }, StringSplitOptions.None)); + + bool flag3 = true; + foreach (string str in stringList) + { + if (!str.StartsWith("/", StringComparison.Ordinal) && !str.StartsWith("b>", StringComparison.Ordinal) && !str.StartsWith("i>", StringComparison.Ordinal) && !str.StartsWith("size=", StringComparison.Ordinal) && str.Length != 0) + { + if (str.StartsWith("color=", StringComparison.Ordinal)) + { + if (str.Length < 14) + { + denialReason = "Указанный тег цвета не соответствует требованиям - Некорректный цвет"; + flag3 = false; + break; + } + + if (str[13] != '>') + { + denialReason = "Указанный тег цвета не соответствует требованиям - незакрытый тег цвета (отсутствует '>')"; + flag3 = false; + break; + } + + if (!Misc.AcceptedColours.Contains(str.Substring(7, 6))) + { + denialReason = "Указанный тег цвета не соответствует требованиям - Данный цвет не входит в список разрешенных"; + flag3 = false; + break; + } + } + else if (str.StartsWith("#", StringComparison.Ordinal)) + { + if (str.Length < 8) + { + denialReason = "Указанный тег цвета не соответствует требованиям - Некорректный цвет"; + flag3 = false; + break; + } + + if (str[7] != '>') + { + denialReason = "Указанный тег цвета не соответствует требованиям - незакрытый тег цвета (отсутствует '>')"; + flag3 = false; + break; + } + + if (!Misc.AcceptedColours.Contains(str.Substring(1, 6))) + { + denialReason = "Указанный тег цвета не соответствует требованиям - Данный цвет не входит в список разрешенных"; + flag3 = false; + break; + } + } + else + { + denialReason = "Указанный текст содержит тег форматирования, который не разрешен"; + flag3 = false; + break; + } + } + } + + return flag3; + } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Badge.cs b/EXILED/Exiled.API/Features/Badge.cs index cb6fb88820..b4e4110694 100644 --- a/EXILED/Exiled.API/Features/Badge.cs +++ b/EXILED/Exiled.API/Features/Badge.cs @@ -77,4 +77,4 @@ public static bool IsValidColor(string hex, out Misc.PlayerInfoColorTypes? color /// A string containing Badge-related data. public override string ToString() => $"{Text} ({Color}) [{IsGlobal}]"; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Cassie.cs b/EXILED/Exiled.API/Features/Cassie.cs index 0730f9a5a9..45814dde6e 100644 --- a/EXILED/Exiled.API/Features/Cassie.cs +++ b/EXILED/Exiled.API/Features/Cassie.cs @@ -11,14 +11,11 @@ namespace Exiled.API.Features using System.Linq; using System.Text; + using Exiled.API.Enums; using Exiled.API.Features.Pools; - using MEC; - using PlayerRoles; - using PlayerStatsSystem; - using Respawning; using CustomFirearmHandler = DamageHandlers.FirearmDamageHandler; @@ -144,24 +141,69 @@ public static void ScpTermination(Player scp, DamageHandlerBase info) /// /// SCP Name. Note that for larger numbers, C.A.S.S.I.E will pronounce the place (eg. "457" -> "four hundred fifty seven"). Spaces can be used to prevent this behavior. /// Hit Information. - public static void CustomScpTermination(string scpName, CustomHandlerBase info) + /// Should apply translations or not. + public static void CustomScpTermination(string scpName, CustomHandlerBase info, bool isTranslated = false) { - string result = scpName; - if (info.Is(out MicroHidDamageHandler _)) - result += " SUCCESSFULLY TERMINATED BY AUTOMATIC SECURITY SYSTEM"; - else if (info.Is(out WarheadDamageHandler _)) - result += " SUCCESSFULLY TERMINATED BY ALPHA WARHEAD"; - else if (info.Is(out UniversalDamageHandler _)) - result += " LOST IN DECONTAMINATION SEQUENCE"; - else if (info.BaseIs(out CustomFirearmHandler firearmDamageHandler) && firearmDamageHandler.Attacker is Player attacker) - result += " CONTAINEDSUCCESSFULLY " + ConvertTeam(attacker.Role.Team, attacker.UnitName); - - // result += "To be changed"; + string message = $"SCP {scpName} "; + string translation = $"SCP-{scpName.Replace(" ", string.Empty)} "; + if (info.Type == DamageType.Tesla) + { + message += "SUCCESSFULLY TERMINATED BY AUTOMATIC SECURITY SYSTEM"; + translation += "успешно уничтожен Автоматической Системой Охраны."; + } + else if (info.Type == DamageType.Warhead) + { + message += "SUCCESSFULLY TERMINATED BY ALPHA WARHEAD"; + translation += "успешно уничтожен боеголовкой Альфа."; + } + else if (info.Type == DamageType.Decontamination) + { + message += "LOST IN DECONTAMINATION SEQUENCE"; + translation += " утерян в процессе обеззараживания."; + } + else if (info.BaseIs(out DamageHandlers.AttackerDamageHandler attackerDamageHandler) && attackerDamageHandler.Attacker is Player attacker) + { + message += "CONTAINEDSUCCESSFULLY " + ConvertTeam(attacker.Role.Team, attacker.UnitName); + switch (attacker.Role.Team) + { + case Team.Scientists: + translation += "успешно сдержан научным персоналом."; + break; + case Team.ChaosInsurgency: + translation += "успешно сдержан Повстанцами Хаоса."; + break; + case Team.FoundationForces: + translation += "успешно сдержан отрядом " + attacker.UnitName + "."; + break; + case Team.ClassD: + translation += "успешно сдержан персоналом класса-Д."; + break; + case Team.OtherAlive: + translation += "успешно сдержан неизвестным человеком."; + break; + case Team.Dead: + translation += "успешно сдержан."; + break; + case Team.SCPs: + translation += "успешно сдержан " + attacker.Role.Name + "."; + break; + } + } else - result += " SUCCESSFULLY TERMINATED . TERMINATION CAUSE UNSPECIFIED"; - - float num = AlphaWarheadController.TimeUntilDetonation <= 0f ? 3.5f : 1f; - GlitchyMessage(result, UnityEngine.Random.Range(0.1f, 0.14f) * num, UnityEngine.Random.Range(0.07f, 0.08f) * num); + { + message += "SUCCESSFULLY TERMINATED . TERMINATION CAUSE UNSPECIFIED"; + translation += "успешно уничтожен. Причина не указана."; + } + + if (isTranslated) + { + MessageTranslated(message, translation); + } + else + { + float num = AlphaWarheadController.TimeUntilDetonation <= 0f ? 3.5f : 1f; + GlitchyMessage(message, UnityEngine.Random.Range(0.1f, 0.14f) * num, UnityEngine.Random.Range(0.07f, 0.08f) * num); + } } /// diff --git a/EXILED/Exiled.API/Features/CassieMessage.cs b/EXILED/Exiled.API/Features/CassieMessage.cs new file mode 100644 index 0000000000..79d024253c --- /dev/null +++ b/EXILED/Exiled.API/Features/CassieMessage.cs @@ -0,0 +1,82 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + /// + /// Useful class to save cassie message configs in a cleaner way. + /// + public class CassieMessage + { + /// + /// Initializes a new instance of the class. + /// + public CassieMessage() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The cassie message. + /// The cassie subtitles. + /// Indicates whether C.A.S.S.I.E has to hold the message. + /// Indicates whether C.A.S.S.I.E has to make noises or not during the message. + /// Indicates whether C.A.S.S.I.E has to make subtitles. + public CassieMessage(string message, string subtitles, bool isHeld = false, bool isNoisy = true, bool isSubtitles = true) + { + Message = message; + Subtitles = subtitles; + IsHeld = isHeld; + IsNoisy = isNoisy; + IsSubtitles = isSubtitles; + } + + /// + /// Gets or sets the cassie message. + /// + [Description("The cassie message")] + public string Message { get; set; } + + /// + /// Gets or sets the cassie subtitles. + /// + [Description("The cassie subtitles")] + public string Subtitles { get; set; } + + /// + /// Gets or sets a value indicating whether C.A.S.S.I.E has to hold the message. + /// + [Description("Indicates whether C.A.S.S.I.E has to hold the message")] + public bool IsHeld { get; set; } = false; + + /// + /// Gets or sets a value indicating whether C.A.S.S.I.E has to make noises or not during the message. + /// + [Description("Indicates whether C.A.S.S.I.E has to make noises or not during the message")] + public bool IsNoisy { get; set; } = true; + + /// + /// Gets or sets a value indicating whether C.A.S.S.I.E has to make subtitles. + /// + [Description("Indicates whether C.A.S.S.I.E has to make subtitles")] + public bool IsSubtitles { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the cassie should be sended or not. + /// + [Description("Indicates whether the cassie should be sended or not")] + public bool Show { get; set; } + } +} diff --git a/EXILED/Exiled.API/Features/Components/CollisionHandler.cs b/EXILED/Exiled.API/Features/Components/CollisionHandler.cs index cf09ae1457..459e439a04 100644 --- a/EXILED/Exiled.API/Features/Components/CollisionHandler.cs +++ b/EXILED/Exiled.API/Features/Components/CollisionHandler.cs @@ -10,9 +10,8 @@ namespace Exiled.API.Features.Components using System; using Features; - using InventorySystem.Items.ThrowableProjectiles; - + using Mirror; using UnityEngine; /// @@ -21,27 +20,26 @@ namespace Exiled.API.Features.Components public class CollisionHandler : MonoBehaviour { private bool initialized; + private float activableTime; + private Action onCollisionAction; /// /// Gets the thrower of the grenade. /// public GameObject Owner { get; private set; } - /// - /// Gets the grenade itself. - /// - public EffectGrenade Grenade { get; private set; } - /// /// Inits the object. /// /// The grenade owner. - /// The grenade component. - public void Init(GameObject owner, ThrownProjectile grenade) + /// Action on collision. + /// Delay before onCollisionAction may be executed by collision. + public void Init(GameObject owner, Action onCollisionAction, float fuseDelay = 0.15f) { Owner = owner; - Grenade = (EffectGrenade)grenade; initialized = true; + this.onCollisionAction = onCollisionAction; + activableTime = (float)NetworkTime.time + fuseDelay; } private void OnCollisionEnter(Collision collision) @@ -50,14 +48,17 @@ private void OnCollisionEnter(Collision collision) { if (!initialized) return; - if (Owner == null || Grenade == null) - return; - if (collision?.collider?.gameObject == Owner) + if (activableTime > NetworkTime.time) return; - if (collision.collider.gameObject.TryGetComponent(out _)) + + if (Owner == null) + Log.Error($"Owner is null!"); + if (collision.gameObject == null) + Log.Error("pepehm"); + if (collision.collider.gameObject == Owner || collision.collider.gameObject.TryGetComponent(out _)) return; - Grenade.TargetTime = 0.1f; + onCollisionAction.Invoke(); } catch (Exception exception) { @@ -66,4 +67,4 @@ private void OnCollisionEnter(Collision collision) } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Core/Interfaces/ISettingHandler.cs b/EXILED/Exiled.API/Features/Core/Interfaces/ISettingHandler.cs new file mode 100644 index 0000000000..68e0d86f3a --- /dev/null +++ b/EXILED/Exiled.API/Features/Core/Interfaces/ISettingHandler.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features.Core.Interfaces +{ + using UserSettings; + + /// + /// Represents a config of ServerSpecific keybinds. + /// + public interface ISettingHandler + { + /// + /// Creating a SettingBase Instanse. + /// + /// Player. + /// Setting. + void Handle(Player player, SettingBase setting); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs index 46938d13b4..c84b6eda43 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/ButtonSetting.cs @@ -97,5 +97,80 @@ public override string ToString() { return base.ToString() + $" ={Text}= -{HoldTime}- /{LastPress}/"; } + + /// + /// Represents a config for ButtonSetting. + /// + public class ButtonConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + public ButtonConfig(string label, string buttonText, string headerName = null, float holdTime = 0.0f, string hintDescription = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + ButtonText = buttonText; + HoldTime = holdTime; + HintDescription = hintDescription; + HeaderName = headerName; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public ButtonConfig() + { + } + + /// + /// Gets or sets label of a ButtonConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets ButtonText of a ButtonConfig. + /// + public string ButtonText { get; set; } + + /// + /// Gets or sets HoldTime of a ButtonConfig. + /// + public float HoldTime { get; set; } + + /// + /// Gets or sets HintDescription of a ButtonConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a ButtonConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a ButtonConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a ButtonSetting instanse. + /// + /// ButtonSetting. + public override ButtonSetting Create() => new(++IdIncrementor, Label, ButtonText, HoldTime, HintDescription, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs index ada19ec63a..0065b08ac0 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/DropdownSetting.cs @@ -19,32 +19,6 @@ namespace Exiled.API.Features.Core.UserSettings /// public class DropdownSetting : SettingBase, IWrapper { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Will be removed in Exiled 10 in favour of ctor with more params")] - public DropdownSetting( - int id, - string label, - IEnumerable options, - int defaultOptionIndex, - SSDropdownSetting.DropdownEntryType dropdownEntryType, - string hintDescription, - HeaderSetting header, - Action onChanged) - : base(new SSDropdownSetting(id, label, options.ToArray(), defaultOptionIndex, dropdownEntryType, hintDescription), header, onChanged) - { - Base = (SSDropdownSetting)base.Base; - } - /// /// Initializes a new instance of the class. /// @@ -178,5 +152,95 @@ public override string ToString() { return base.ToString() + $" ={DefaultOptionIndex}= -{SelectedIndex}- /{string.Join(";", Options)}/"; } + + /// + /// Represents a config for DropdownSetting. + /// + public class DropdownConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public DropdownConfig(string label, IEnumerable options, int defaultOptionIndex, bool isServerOnly, SSDropdownSetting.DropdownEntryType dropdownEntryType = SSDropdownSetting.DropdownEntryType.Regular, string hintDescription = null, string headerName = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + Options = options; + DefaultOptionIndex = defaultOptionIndex; + DropdownEntryType = dropdownEntryType; + HintDescription = hintDescription; + IsServerOnly = isServerOnly; + HeaderName = headerName; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public DropdownConfig() + { + } + + /// + /// Gets or sets label of a DropdownConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets Options of a DropdownConfig. + /// + public IEnumerable Options { get; set; } + + /// + /// Gets or sets DefaultOptionIndex of a DropdownConfig. + /// + public int DefaultOptionIndex { get; set; } + + /// + /// Gets or sets DropdownEntryType of a DropdownConfig. + /// + public SSDropdownSetting.DropdownEntryType DropdownEntryType { get; set; } + + /// + /// Gets or sets a value indicating whether updates come from client. + /// + public bool IsServerOnly { get; set; } + + /// + /// Gets or sets HintDescription of a DropdownConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a DropdownConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a DropdownConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a DropdownSetting instanse. + /// + /// DropdownSetting. + public override DropdownSetting Create() => new(++IdIncrementor, Label, Options, DefaultOptionIndex, DropdownEntryType, HintDescription, 255, IsServerOnly, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/HeaderSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/HeaderSetting.cs index 4b13238b65..b68d02d171 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/HeaderSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/HeaderSetting.cs @@ -22,29 +22,11 @@ public class HeaderSetting : SettingBase, IWrapper /// /// /// - /// - [Obsolete("Use constructor with Id, old headers will use random number based on headers name")] - public HeaderSetting(string name, string hintDescription, bool paddling) - : this(new SSGroupHeader(name, paddling, hintDescription)) - { - Base = (SSGroupHeader)base.Base; - - Base.SetId(null, name); - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// /// - public HeaderSetting(int id, string name, string hintDescription = "", bool padding = false) - : this(new SSGroupHeader(id, name, padding, hintDescription)) + public HeaderSetting(string name, string hintDescription = "", bool padding = false) + : this(new SSGroupHeader(0, name, padding, hintDescription)) { Base = (SSGroupHeader)base.Base; - - Base.SetId(id, name); } /// @@ -55,7 +37,7 @@ internal HeaderSetting(SSGroupHeader settingBase) : base(settingBase) { Base = settingBase; - Base.SetId(null, settingBase.Label); + Base.SetId(0, settingBase.Label); } /// @@ -79,5 +61,52 @@ public override string ToString() { return base.ToString() + $" /{ReducedPaddling}/"; } + + /// + /// Represents a config for KeybindSetting. + /// + public class HeaderConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public HeaderConfig(string name = null, string description = null, bool paddling = false) + { + Name = name; + Description = description; + Paddling = paddling; + } + + /// + /// Initializes a new instance of the class. + /// + public HeaderConfig() + { + } + + /// + /// Gets or sets HeaderName of a HeaderConfig. + /// + public string Name { get; set; } + + /// + /// Gets or sets HeaderDescription of a HeaderConfig. + /// + public string Description { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool Paddling { get; set; } + + /// + /// Creates a HeaderSetting instanse. + /// + /// HeaderSetting. + public override HeaderSetting Create() => new(Name, Description, Paddling); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs index e3b40e8f0c..25011e1c30 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/KeybindSetting.cs @@ -18,41 +18,6 @@ namespace Exiled.API.Features.Core.UserSettings /// public class KeybindSetting : SettingBase, IWrapper { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete("This method will be removed next major version because of a new feature. Use the constructor with \"CollectionId\" instead.")] - public KeybindSetting(int id, string label, KeyCode suggested, bool preventInteractionOnGUI, string hintDescription, HeaderSetting header, Action onChanged) - : base(new SSKeybindSetting(id, label, suggested, preventInteractionOnGUI, false, hintDescription), header, onChanged) - { - Base = (SSKeybindSetting)base.Base; - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Will be removed in Exiled 10 in favour of ctor with more params.")] - public KeybindSetting(int id, string label, KeyCode suggested, bool preventInteractionOnGUI, bool allowSpectatorTrigger, string hintDescription, HeaderSetting header, Action onChanged) - : base(new SSKeybindSetting(id, label, suggested, preventInteractionOnGUI, allowSpectatorTrigger, hintDescription), header, onChanged) - { - Base = (SSKeybindSetting)base.Base; - } - /// /// Initializes a new instance of the class. /// @@ -124,5 +89,87 @@ public override string ToString() { return base.ToString() + $" /{IsPressed}/ *{KeyCode}* +{PreventInteractionOnGUI}+"; } + + /// + /// Represents a config for KeybindSetting. + /// + public class KeybindConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public KeybindConfig(string label, KeyCode keyCode, string hintDescription = null, bool preventInteractionOnGui = false, bool allowSpectatorTrigger = true, string headerName = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + KeyCode = keyCode; + HintDescription = hintDescription; + PreventInteractionOnGUI = preventInteractionOnGui; + AllowSpectatorTrigger = allowSpectatorTrigger; + HeaderName = headerName; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public KeybindConfig() + { + } + + /// + /// Gets or sets label of a KeybindConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets ButtonText of a KeybindConfig. + /// + public KeyCode KeyCode { get; set; } + + /// + /// Gets or sets a value indicating whether interaction on GUI would be prevented. + /// + public bool PreventInteractionOnGUI { get; set; } + + /// + /// Gets or sets a value indicating whether interaction on GUI would be prevented. + /// + public bool AllowSpectatorTrigger { get; set; } + + /// + /// Gets or sets HintDescription of a KeybindConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a KeybindConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a KeybindConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a KeybindSetting instanse. + /// + /// KeybindSetting. + public override KeybindSetting Create() => new(++IdIncrementor, Label, KeyCode, PreventInteractionOnGUI, AllowSpectatorTrigger, HintDescription, 255, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs b/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs index 38b5132ec7..ffc0f8b49f 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/SettingBase.cs @@ -31,6 +31,8 @@ public class SettingBase : TypeCastObject, IWrapper internal static readonly List Settings = new(); + private static readonly Dictionary WasPressed = new(); + /// /// Initializes a new instance of the class. /// @@ -73,11 +75,6 @@ public static IReadOnlyDictionary> Synce /// public static IReadOnlyCollection List => Settings; - /// - /// Gets or sets the predicate for syncing this setting when a player joins. - /// - public static Predicate SyncOnJoin { get; set; } - /// public ServerSpecificSettingBase Base { get; } @@ -157,6 +154,11 @@ public byte CollectionId /// public Action OnChanged { get; set; } + /// + /// Gets or sets incrementor to avoid useless id setting. + /// + protected static int IdIncrementor { get; set; } = 0; + /// /// Tries to get the setting with the specified id. /// @@ -262,21 +264,48 @@ public static void SendToPlayer(Player player, IEnumerable settings /// This method is used to sync new settings with players. public static IEnumerable Register(IEnumerable settings, Func predicate = null) { - IEnumerable> grouped = settings.Where(s => s != null).GroupBy(s => s.Header); + SettingBase[] settingBases = settings as SettingBase[] ?? settings.ToArray(); - List result = new(); + List fullList = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()) + .Select(Create) + .Concat(settingBases) + .ToList(); + + Dictionary headersDict = new(); + Dictionary> settingsByLabel = new(); + List settingsWithoutHeaders = new(); - // Group settings by headers - foreach (IGrouping grouping in grouped) + foreach (SettingBase setting in fullList) { - if (grouping.Key != null) - result.Add(grouping.Key); + if (setting is HeaderSetting) + continue; - result.AddRange(grouping); + if (setting.Header == null) + { + settingsWithoutHeaders.Add(setting); + continue; + } + + if (!headersDict.ContainsKey(setting.Header.Label)) + headersDict[setting.Header.Label] = setting.Header; + + if (!settingsByLabel.ContainsKey(setting.Header.Label)) + settingsByLabel[setting.Header.Label] = new List(); + + settingsByLabel[setting.Header.Label].Add(setting); } - ServerSpecificSettingsSync.DefinedSettings = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()).Concat(result.Select(s => s.Base)).ToArray(); - Settings.AddRange(result); + List result = new(); + foreach (HeaderSetting header in headersDict.Values.OrderBy(h => h.Label)) + { + result.Add(header); + result.AddRange(settingsByLabel[header.Label]); + } + + result.AddRange(settingsWithoutHeaders); + + ServerSpecificSettingsSync.DefinedSettings = result.Select(x => x.Base).ToArray(); + Settings.AddRange(settingBases); if (predicate == null) SendToAll(); @@ -295,22 +324,48 @@ public static IEnumerable Register(IEnumerable setting /// This method is used to sync new settings with players. public static IEnumerable Register(Player player, IEnumerable settings) { - IEnumerable> grouped = settings.Where(s => s != null).GroupBy(s => s.Header); + SettingBase[] settingBases = settings as SettingBase[] ?? settings.ToArray(); - List result = new(); + List fullList = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()) + .Select(Create) + .Concat(settingBases) + .ToList(); - // Group settings by headers - foreach (IGrouping grouping in grouped) + Dictionary headersDict = new(); + Dictionary> settingsByLabel = new(); + List settingsWithoutHeaders = new(); + + foreach (SettingBase setting in fullList) { - if (grouping.Key != null) - result.Add(grouping.Key); + if (setting is HeaderSetting) + continue; + + if (setting.Header == null) + { + settingsWithoutHeaders.Add(setting); + continue; + } + + if (!headersDict.ContainsKey(setting.Header.Label)) + headersDict[setting.Header.Label] = setting.Header; - result.AddRange(grouping); + if (!settingsByLabel.ContainsKey(setting.Header.Label)) + settingsByLabel[setting.Header.Label] = new List(); + + settingsByLabel[setting.Header.Label].Add(setting); } - ServerSpecificSettingsSync.DefinedSettings = (ServerSpecificSettingsSync.DefinedSettings ?? Array.Empty()).Concat(result.Select(s => s.Base)).ToArray(); - Settings.AddRange(result); + List result = new(); + foreach (HeaderSetting header in headersDict.Values.OrderBy(h => h.Label)) + { + result.Add(header); + result.AddRange(settingsByLabel[header.Label]); + } + result.AddRange(settingsWithoutHeaders); + + ServerSpecificSettingsSync.DefinedSettings = result.Select(x => x.Base).ToArray(); + Settings.AddRange(settingBases); SendToPlayer(player); return result; @@ -426,7 +481,32 @@ internal static void OnSettingUpdated(ReferenceHub hub, ServerSpecificSettingBas Settings.Add(Create(settingBase.OriginalDefinition)); } + if (setting is KeybindSetting keybindSetting) + { + if (!WasPressed.TryGetValue(player, out bool wasPressedPreviously)) + wasPressedPreviously = false; + + if (wasPressedPreviously == keybindSetting.IsPressed) + return; + + WasPressed[player] = keybindSetting.IsPressed; + } + setting.OriginalDefinition?.OnChanged?.Invoke(player, setting); } + + /// + /// A base class for all Server Specific Settings configs. + /// + /// Type of Server Specific Setting. + public abstract class SettingConfig + where TSetting : SettingBase + { + /// + /// Creates a SettingBase instanse. + /// + /// TextInputSetting. + public abstract TSetting Create(); + } } } diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/SliderSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/SliderSetting.cs index abb522fd76..9bf9c49618 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/SliderSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/SliderSetting.cs @@ -17,25 +17,6 @@ namespace Exiled.API.Features.Core.UserSettings /// public class SliderSetting : SettingBase, IWrapper { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Will be removed in Exiled 10 in favour of ctor with more params.")] - public SliderSetting(int id, string label, float minValue, float maxValue, float defaultValue, bool isInteger, string stringFormat, string displayFormat, string hintDescription) - : this(new SSSliderSetting(id, label, minValue, maxValue, defaultValue, isInteger, stringFormat, displayFormat, hintDescription)) - { - Base = (SSSliderSetting)base.Base; - } - /// /// Initializes a new instance of the class. /// @@ -171,5 +152,114 @@ public override string ToString() { return base.ToString() + $" /{MinimumValue}/ *{MaximumValue}* +{DefaultValue}+ '{SliderValue}'"; } + + /// + /// Represents a config for SliderSetting. + /// + public class SliderConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SliderConfig(string label, float minValue, float maxValue, float defaultValue, bool isInteger = false, string stringFormat = "0.##", string displayFormat = "{0}", bool isServerOnly = false, string hintDescription = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + MinimumValue = minValue; + MaximumValue = maxValue; + DefaultValue = defaultValue; + IsInteger = isInteger; + DisplayFormat = displayFormat; + StringFormat = stringFormat; + IsServerOnly = isServerOnly; + HintDescription = hintDescription; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public SliderConfig() + { + } + + /// + /// Gets or sets label of a ButtonConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets MinimumValue of a ButtonConfig. + /// + public float MinimumValue { get; set; } + + /// + /// Gets or sets MaximumValue of a ButtonConfig. + /// + public float MaximumValue { get; set; } + + /// + /// Gets or sets DefaultValue of a ButtonConfig. + /// + public float DefaultValue { get; set; } + + /// + /// Gets or sets a value indicating whether value is integer. + /// + public bool IsInteger { get; set; } + + /// + /// Gets or sets string format of Slider Setting. + /// + public string StringFormat { get; set; } + + /// + /// Gets or sets display format of Slider Setting. + /// + public string DisplayFormat { get; set; } + + /// + /// Gets or sets a value indicating whether updates come from client. + /// + public bool IsServerOnly { get; set; } + + /// + /// Gets or sets HeaderName of a ButtonConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a ButtonConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderDescription of a ButtonConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a ButtonSetting instanse. + /// + /// ButtonSetting. + public override SliderSetting Create() => new(++IdIncrementor, Label, MinimumValue, MaximumValue, DefaultValue, IsInteger, StringFormat, DisplayFormat, + HintDescription, 255, IsServerOnly, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs index 69c7875ad1..0395d02362 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/TextInputSetting.cs @@ -89,5 +89,80 @@ public override string ToString() { return base.ToString() + $" /{FoldoutMode}/ *{Alignment}*"; } + + /// + /// Represents a config for TextInputSetting. + /// + public class TextInputConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + public TextInputConfig(string label, SSTextArea.FoldoutMode foldoutMode, TextAlignmentOptions textAlignmentOptions, string hintDescription = null, string headerName = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + HintDescription = hintDescription; + FoldoutMode = foldoutMode; + TextAlignmentOptions = textAlignmentOptions; + HeaderName = headerName; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public TextInputConfig() + { + } + + /// + /// Gets or sets label of a TextInputConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets FoldoutMode of a TextInputConfig. + /// + public SSTextArea.FoldoutMode FoldoutMode { get; set; } + + /// + /// Gets or sets TextAlignmentOptions for TextInputConfig. + /// + public TextAlignmentOptions TextAlignmentOptions { get; set; } + + /// + /// Gets or sets HintDescription of a TextInputConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a TextInputConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a TextInputConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a TextInputSetting instanse. + /// + /// TextInputSetting. + public override TextInputSetting Create() => new(++IdIncrementor, Label, FoldoutMode, TextAlignmentOptions, HintDescription, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs index 18233e6ffc..e558720b91 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/TwoButtonsSetting.cs @@ -17,24 +17,6 @@ namespace Exiled.API.Features.Core.UserSettings /// public class TwoButtonsSetting : SettingBase, IWrapper { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Will be removed in Exiled 10 in favour of ctor with more params.")] - public TwoButtonsSetting(int id, string label, string firstOption, string secondOption, bool defaultIsSecond, string hintDescription, HeaderSetting header, Action onChanged) - : base(new SSTwoButtonsSetting(id, label, firstOption, secondOption, defaultIsSecond, hintDescription), header, onChanged) - { - Base = (SSTwoButtonsSetting)base.Base; - } - /// /// Initializes a new instance of the class. /// @@ -151,5 +133,94 @@ public override string ToString() { return base.ToString() + $" /{FirstOption}/ *{SecondOption}* +{IsSecondDefault}+ '{IsFirst}'"; } + + /// + /// Represents a config for TextInputSetting. + /// + public class TwoButtonsConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TwoButtonsConfig(string label, string firstOption, string secondOption, bool isServerOnly, bool defaultIsSecond = false, string hintDescription = null, string headerName = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + FirstOption = firstOption; + IsServerOnly = isServerOnly; + SecondOption = secondOption; + DefaultIsSecond = defaultIsSecond; + HintDescription = hintDescription; + HeaderName = headerName; + HeaderPaddling = headerPaddling; + HeaderDescription = headerDescription; + } + + /// + /// Initializes a new instance of the class. + /// + public TwoButtonsConfig() + { + } + + /// + /// Gets or sets label of a TwoButtonsConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets label of a TwoButtonsConfig. + /// + public string FirstOption { get; set; } + + /// + /// Gets or sets label of a TwoButtonsConfig. + /// + public string SecondOption { get; set; } + + /// + /// Gets or sets a value indicating whether default is second. + /// + public bool DefaultIsSecond { get; set; } + + /// + /// Gets or sets a value indicating whether updates come from client. + /// + public bool IsServerOnly { get; set; } + + /// + /// Gets or sets HintDescription of a TwoButtonsConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a TwoButtonsConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a TwoButtonsConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a TwoButtonsSetting instanse. + /// + /// TwoButtonsSetting. + public override TwoButtonsSetting Create() => new(++IdIncrementor, Label, FirstOption, SecondOption, DefaultIsSecond, HintDescription, 255, IsServerOnly, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Core/UserSettings/UserTextInputSetting.cs b/EXILED/Exiled.API/Features/Core/UserSettings/UserTextInputSetting.cs index 99c163581e..d1d497b796 100644 --- a/EXILED/Exiled.API/Features/Core/UserSettings/UserTextInputSetting.cs +++ b/EXILED/Exiled.API/Features/Core/UserSettings/UserTextInputSetting.cs @@ -18,22 +18,6 @@ namespace Exiled.API.Features.Core.UserSettings /// public class UserTextInputSetting : SettingBase, IWrapper { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Will be removed in Exiled 10 in favour of ctor with more params.")] - public UserTextInputSetting(int id, string label, string placeHolder, int characterLimit, TMP_InputField.ContentType contentType, string hintDescription) - : this(new SSPlaintextSetting(id, label, placeHolder, characterLimit, contentType, hintDescription)) - { - Base = (SSPlaintextSetting)base.Base; - } - /// /// Initializes a new instance of the class. /// @@ -67,7 +51,7 @@ internal UserTextInputSetting(SSPlaintextSetting settingBase) public new SSPlaintextSetting Base { get; } /// - /// Gets the value of the text entered by a client. + /// Gets the value of the text entered by a client. /// public string Text => Base.SyncInputText; @@ -142,5 +126,94 @@ public override string ToString() { return base.ToString() + $" /{Text}/ *{ContentType}* +{CharacterLimit}+"; } + + /// + /// Represents a config for UserTextInputSetting. + /// + public class TextInputConfig : SettingConfig + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TextInputConfig(string label, bool isServerOnly, string placeHolder = "", int characterLimit = 64, TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, string hintDescription = null, string headerName = null, string headerDescription = null, bool headerPaddling = false) + { + Label = label; + PlaceHolder = placeHolder; + CharacterLimit = characterLimit; + IsServerOnly = isServerOnly; + ContentType = contentType; + HintDescription = hintDescription; + HeaderName = headerName; + HeaderDescription = headerDescription; + HeaderPaddling = headerPaddling; + } + + /// + /// Initializes a new instance of the class. + /// + public TextInputConfig() + { + } + + /// + /// Gets or sets label of a TextInputConfig. + /// + public string Label { get; set; } + + /// + /// Gets or sets FoldoutMode of a TextInputConfig. + /// + public string PlaceHolder { get; set; } + + /// + /// Gets or sets TextAlignmentOptions for TextInputConfig. + /// + public int CharacterLimit { get; set; } + + /// + /// Gets or sets TextAlignmentOptions for TextInputConfig. + /// + public TMP_InputField.ContentType ContentType { get; set; } + + /// + /// Gets or sets a value indicating whether updates come from client. + /// + public bool IsServerOnly { get; set; } + + /// + /// Gets or sets HintDescription of a TextInputConfig. + /// + public string HintDescription { get; set; } + + /// + /// Gets or sets HeaderName of a TextInputConfig. + /// + public string HeaderName { get; set; } + + /// + /// Gets or sets HeaderDescription of a TextInputConfig. + /// + public string HeaderDescription { get; set; } + + /// + /// Gets or sets a value indicating whether HeaderPaddling is needed. + /// + public bool HeaderPaddling { get; set; } + + /// + /// Creates a TextInputSetting instanse. + /// + /// TextInputSetting. + public override UserTextInputSetting Create() => new(++IdIncrementor, Label, PlaceHolder, CharacterLimit, ContentType, HintDescription, 255, IsServerOnly, HeaderName == null ? null : new HeaderSetting(HeaderName, HeaderDescription, HeaderPaddling)); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/DamageHandlers/ScpDamageHandler.cs b/EXILED/Exiled.API/Features/DamageHandlers/ScpDamageHandler.cs index 6d44b06d08..09b847c3d3 100644 --- a/EXILED/Exiled.API/Features/DamageHandlers/ScpDamageHandler.cs +++ b/EXILED/Exiled.API/Features/DamageHandlers/ScpDamageHandler.cs @@ -60,4 +60,4 @@ public override DamageType Type } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Doors/Door.cs b/EXILED/Exiled.API/Features/Doors/Door.cs index 19718900a7..cacbe980e3 100644 --- a/EXILED/Exiled.API/Features/Doors/Door.cs +++ b/EXILED/Exiled.API/Features/Doors/Door.cs @@ -168,7 +168,19 @@ public bool IsOpen /// /// This value is if is equal to . /// - public bool IsKeycardDoor => KeycardPermissions is not KeycardPermissions.None; + public bool IsKeycardDoor => Permissions is not Enums.KeycardPermissions.None; + + /// + /// Gets or sets the required permissions to interact with the generator. + /// + /// + /// Setting this value to will allow this door to be opened without a keycard. + /// + public KeycardPermissions Permissions + { + get => (KeycardPermissions)RequiredPermissions; + set => RequiredPermissions = (DoorPermissionFlags)value; + } /// /// Gets or sets the keycard permissions required to open the door. diff --git a/EXILED/Exiled.API/Features/Doors/Gate.cs b/EXILED/Exiled.API/Features/Doors/Gate.cs index 89cbc3ced0..0d45a6e69d 100644 --- a/EXILED/Exiled.API/Features/Doors/Gate.cs +++ b/EXILED/Exiled.API/Features/Doors/Gate.cs @@ -78,7 +78,7 @@ public DoorLockType BlockingPryingMask /// /// if the door was able to be pried open. /// to perform pry gate. - public bool TryPry(Player player = null) => Base.TryPryGate(player?.ReferenceHub); + public bool TryPry(Player player = null) => Base.TryPryGate((player ?? Server.Host).ReferenceHub); /// /// Returns the Door in a human-readable format. diff --git a/EXILED/Exiled.API/Features/Effect.cs b/EXILED/Exiled.API/Features/Effect.cs index f901966917..2df7c63535 100644 --- a/EXILED/Exiled.API/Features/Effect.cs +++ b/EXILED/Exiled.API/Features/Effect.cs @@ -93,4 +93,4 @@ public Effect(EffectType type, float duration, byte intensity = 1, bool addDurat /// A string containing effect-related data. public override string ToString() => $"({Type}) {Duration} {AddDurationIfActive}"; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Generator.cs b/EXILED/Exiled.API/Features/Generator.cs index b19369208d..039554e645 100644 --- a/EXILED/Exiled.API/Features/Generator.cs +++ b/EXILED/Exiled.API/Features/Generator.cs @@ -21,7 +21,7 @@ namespace Exiled.API.Features /// /// Wrapper class for . /// - public class Generator : IWrapper, IWorldSpace + public class Generator : IWrapper, IWorldSpace, IPermission { /// /// A of on the map. @@ -213,6 +213,16 @@ public Player LastActivator /// /// Gets or sets the required permissions to interact with the generator. /// + public KeycardPermissions Permissions + { + get => (KeycardPermissions)Base._requiredPermission; + set => Base._requiredPermission = (Interactables.Interobjects.DoorUtils.DoorPermissionFlags)value; + } + + /// + /// Gets or sets the required permissions to interact with the generator. + /// + [Obsolete] public KeycardPermissions KeycardPermissions { get => (KeycardPermissions)Base._requiredPermission; @@ -317,6 +327,6 @@ public void SetPermissionFlag(KeycardPermissions flag, bool isEnabled) /// Returns the Generator in a human-readable format. /// /// A string containing Generator-related data. - public override string ToString() => $"{State} ({KeycardPermissions})"; + public override string ToString() => $"{State} ({Permissions})"; } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Hazards/TantrumHazard.cs b/EXILED/Exiled.API/Features/Hazards/TantrumHazard.cs index 2e36627d8b..995402ee40 100644 --- a/EXILED/Exiled.API/Features/Hazards/TantrumHazard.cs +++ b/EXILED/Exiled.API/Features/Hazards/TantrumHazard.cs @@ -102,4 +102,4 @@ public static TantrumHazard PlaceTantrum(Vector3 position, bool isActive = true) return Get(tantrum); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Items/Armor.cs b/EXILED/Exiled.API/Features/Items/Armor.cs index 344b7a0118..6393b9ab98 100644 --- a/EXILED/Exiled.API/Features/Items/Armor.cs +++ b/EXILED/Exiled.API/Features/Items/Armor.cs @@ -158,6 +158,14 @@ public IEnumerable CategoryLimits HelmetEfficacy = HelmetEfficacy, }; + /// + internal override void ChangeOwner(Player oldOwner, Player newOwner) + { + Base.Owner = newOwner.ReferenceHub; + + Base.OnAdded(null); + } + /// internal override void ReadPickupInfoBefore(Pickup pickup) { @@ -173,4 +181,4 @@ internal override void ReadPickupInfoBefore(Pickup pickup) } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Items/Consumable.cs b/EXILED/Exiled.API/Features/Items/Consumable.cs index 6b4ec6c5ce..f9630ea4b0 100644 --- a/EXILED/Exiled.API/Features/Items/Consumable.cs +++ b/EXILED/Exiled.API/Features/Items/Consumable.cs @@ -7,7 +7,9 @@ namespace Exiled.API.Features.Items { + using Exiled.API.Extensions; using Exiled.API.Interfaces; + using InventorySystem.Items.Usables; using BaseConsumable = InventorySystem.Items.Usables.Consumable; @@ -40,6 +42,23 @@ internal Consumable(ItemType type) /// public new BaseConsumable Base { get; } + /// + public override void Use(Player owner = null) + { + Player oldOwner = Owner; + owner ??= Owner; + + if (owner is null) + throw new System.InvalidOperationException("The Owner of the item cannot be null."); + + Base.Owner = owner.ReferenceHub; + Base.ActivateEffects(); + + typeof(UsableItemsController).InvokeStaticEvent(nameof(UsableItemsController.ServerOnUsingCompleted), new object[] { owner.ReferenceHub, Base }); + + Base.Owner = oldOwner.ReferenceHub; + } + /// internal override void ChangeOwner(Player oldOwner, Player newOwner) { diff --git a/EXILED/Exiled.API/Features/Items/ExplosiveGrenade.cs b/EXILED/Exiled.API/Features/Items/ExplosiveGrenade.cs index b550c13456..2e8fc7dde6 100644 --- a/EXILED/Exiled.API/Features/Items/ExplosiveGrenade.cs +++ b/EXILED/Exiled.API/Features/Items/ExplosiveGrenade.cs @@ -12,13 +12,9 @@ namespace Exiled.API.Features.Items using Exiled.API.Features.Pickups; using Exiled.API.Features.Pickups.Projectiles; - using InventorySystem.Items; - using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using UnityEngine; - using Object = UnityEngine.Object; - /// /// A wrapper class for . /// @@ -31,7 +27,6 @@ public class ExplosiveGrenade : Throwable public ExplosiveGrenade(ThrowableItem itemBase) : base(itemBase) { - Projectile = (ExplosionGrenadeProjectile)((Throwable)this).Projectile; } /// @@ -45,64 +40,30 @@ internal ExplosiveGrenade(ItemType type, Player player = null) { } - /// - /// Gets a to change grenade properties. - /// - public new ExplosionGrenadeProjectile Projectile { get; } - /// /// Gets or sets the maximum radius of the grenade. /// - public float MaxRadius - { - get => Projectile.MaxRadius; - set => Projectile.MaxRadius = value; - } + public float MaxRadius { get; set; } /// /// Gets or sets the multiplier for damage against players. /// - public float ScpDamageMultiplier - { - get => Projectile.ScpDamageMultiplier; - set => Projectile.ScpDamageMultiplier = value; - } + public float ScpDamageMultiplier { get; set; } /// /// Gets or sets how long the effect will last. /// - public float BurnDuration - { - get => Projectile.BurnDuration; - set => Projectile.BurnDuration = value; - } + public float BurnDuration { get; set; } /// /// Gets or sets how long the effect will last. /// - public float DeafenDuration - { - get => Projectile.DeafenDuration; - set => Projectile.DeafenDuration = value; - } + public float DeafenDuration { get; set; } /// /// Gets or sets how long the effect will last. /// - public float ConcussDuration - { - get => Projectile.ConcussDuration; - set => Projectile.ConcussDuration = value; - } - - /// - /// Gets or sets how long the fuse will last. - /// - public float FuseTime - { - get => Projectile.FuseTime; - set => Projectile.FuseTime = value; - } + public float ConcussDuration { get; set; } /// /// Spawns an active grenade on the map at the specified location. @@ -115,28 +76,14 @@ public ExplosionGrenadeProjectile SpawnActive(Vector3 position, Player owner = n #if DEBUG Log.Debug($"Spawning active grenade: {FuseTime}"); #endif - ItemPickupBase ipb = Object.Instantiate(Projectile.Base, position, Quaternion.identity); - - ipb.Info = new PickupSyncInfo(Type, Weight, ItemSerialGenerator.GenerateNext()); - - ExplosionGrenadeProjectile grenade = Pickup.Get(ipb); - - grenade.Base.gameObject.SetActive(true); - - grenade.MaxRadius = MaxRadius; - grenade.ScpDamageMultiplier = ScpDamageMultiplier; - grenade.BurnDuration = BurnDuration; - grenade.DeafenDuration = DeafenDuration; - grenade.ConcussDuration = ConcussDuration; - grenade.FuseTime = FuseTime; - grenade.PreviousOwner = owner ?? Server.Host; + Projectile projectile = CreateProjectile(position, Quaternion.identity); - grenade.Spawn(); + projectile.PreviousOwner = owner; - grenade.Base.ServerActivate(); + projectile.Activate(); - return grenade; + return (ExplosionGrenadeProjectile)projectile; } /// @@ -175,5 +122,20 @@ internal override void ReadPickupInfoBefore(Pickup pickup) FuseTime = explosiveGrenadePickup.FuseTime; } } + + /// + protected override void InitializeProperties(ThrowableItem throwable) + { + base.InitializeProperties(throwable); + + if (throwable.Projectile is ExplosionGrenade grenade) + { + MaxRadius = grenade.MaxRadius; + ScpDamageMultiplier = grenade.ScpDamageMultiplier; + BurnDuration = grenade._burnedDuration; + DeafenDuration = grenade._deafenedDuration; + ConcussDuration = grenade._concussedDuration; + } + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Items/Firearm.cs b/EXILED/Exiled.API/Features/Items/Firearm.cs index 56215591af..f91ce7ed48 100644 --- a/EXILED/Exiled.API/Features/Items/Firearm.cs +++ b/EXILED/Exiled.API/Features/Items/Firearm.cs @@ -812,7 +812,7 @@ internal override void ReadPickupInfoBefore(Pickup pickup) if (pickup is FirearmPickup firearmPickup) { - PrimaryMagazine.MaxAmmo = firearmPickup.MaxAmmo; + PrimaryMagazine.ConstantMaxAmmo = firearmPickup.MaxAmmo; AmmoDrain = firearmPickup.AmmoDrain; } } diff --git a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/CylinderMagazine.cs b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/CylinderMagazine.cs index 2beedd7bb7..66112cc486 100644 --- a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/CylinderMagazine.cs +++ b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/CylinderMagazine.cs @@ -15,6 +15,7 @@ namespace Exiled.API.Features.Items.FirearmModules.Primary using Exiled.API.Enums; using Exiled.API.Extensions; + using InventorySystem.Items.Firearms.Attachments; using InventorySystem.Items.Firearms.Modules; /// @@ -45,13 +46,17 @@ public override int MaxAmmo { set { - CylinderModule._defaultCapacity = value; + CylinderModule._defaultCapacity = value - (int)CylinderModule.Firearm.AttachmentsValue(AttachmentParam.MagazineCapacityModifier); Resync(); } } /// - public override int ConstantMaxAmmo => CylinderModule._defaultCapacity; + public override int ConstantMaxAmmo + { + get => CylinderModule._defaultCapacity; + set => CylinderModule._defaultCapacity = value; + } /// /// Gets or sets an used for this magazine. diff --git a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/NormalMagazine.cs b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/NormalMagazine.cs index 6c4bb19c28..c1929bedd5 100644 --- a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/NormalMagazine.cs +++ b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/NormalMagazine.cs @@ -12,6 +12,7 @@ namespace Exiled.API.Features.Items.FirearmModules.Primary using Exiled.API.Enums; using Exiled.API.Extensions; + using InventorySystem.Items.Firearms.Attachments; using InventorySystem.Items.Firearms.Modules; /// @@ -40,7 +41,7 @@ public NormalMagazine(MagazineModule magazine) /// public override int MaxAmmo { - set => MagazineModule._defaultCapacity = value; + set => MagazineModule._defaultCapacity = value - (int)MagazineModule.Firearm.AttachmentsValue(AttachmentParam.MagazineCapacityModifier); } /// @@ -54,7 +55,11 @@ public override int Ammo } /// - public override int ConstantMaxAmmo => MagazineModule._defaultCapacity; + public override int ConstantMaxAmmo + { + get => MagazineModule._defaultCapacity; + set => MagazineModule._defaultCapacity = value; + } /// public override AmmoType AmmoType diff --git a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/PrimaryMagazine.cs b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/PrimaryMagazine.cs index 39ff428556..2945fd116d 100644 --- a/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/PrimaryMagazine.cs +++ b/EXILED/Exiled.API/Features/Items/FirearmModules/Primary/PrimaryMagazine.cs @@ -38,9 +38,9 @@ public PrimaryMagazine(IPrimaryAmmoContainerModule magazine) public override int MaxAmmo => Magazine.AmmoMax; /// - /// Gets a max avaible ammo count in magazine without attachments. + /// Gets or sets a max avaible ammo count in magazine without attachments. /// - public abstract int ConstantMaxAmmo { get; } + public abstract int ConstantMaxAmmo { get; set; } /// public override int Ammo @@ -51,7 +51,6 @@ public override int Ammo { int modifyCount = Math.Max(0, value) - Ammo; Magazine.ServerModifyAmmo(modifyCount); - Resync(); } } diff --git a/EXILED/Exiled.API/Features/Items/FlashGrenade.cs b/EXILED/Exiled.API/Features/Items/FlashGrenade.cs index 867d11b3ad..a4f861f95b 100644 --- a/EXILED/Exiled.API/Features/Items/FlashGrenade.cs +++ b/EXILED/Exiled.API/Features/Items/FlashGrenade.cs @@ -11,14 +11,10 @@ namespace Exiled.API.Features.Items using Exiled.API.Features.Pickups; using Exiled.API.Features.Pickups.Projectiles; - using InventorySystem.Items; - using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using UnityEngine; - using Object = UnityEngine.Object; - /// /// A wrapper class for . /// @@ -31,7 +27,6 @@ public class FlashGrenade : Throwable public FlashGrenade(ThrowableItem itemBase) : base(itemBase) { - Projectile = (FlashbangProjectile)((Throwable)this).Projectile; } /// @@ -44,46 +39,20 @@ internal FlashGrenade(Player player = null) { } - /// - /// Gets a to change grenade properties. - /// - public new FlashbangProjectile Projectile { get; } - /// /// Gets or sets the minimum duration of player can take the effect. /// - public float MinimalDurationEffect - { - get => Projectile.MinimalDurationEffect; - set => Projectile.MinimalDurationEffect = value; - } + public float MinimalDurationEffect { get; set; } /// /// Gets or sets the additional duration of the effect. /// - public float AdditionalBlindedEffect - { - get => Projectile.AdditionalBlindedEffect; - set => Projectile.AdditionalBlindedEffect = value; - } + public float AdditionalBlindedEffect { get; set; } /// /// Gets or sets the how mush the flash grenade going to be intensified when explode at . /// - public float SurfaceDistanceIntensifier - { - get => Projectile.SurfaceDistanceIntensifier; - set => Projectile.SurfaceDistanceIntensifier = value; - } - - /// - /// Gets or sets how long the fuse will last. - /// - public float FuseTime - { - get => Projectile.FuseTime; - set => Projectile.FuseTime = value; - } + public float SurfaceDistanceIntensifier { get; set; } /// /// Spawns an active grenade on the map at the specified location. @@ -96,26 +65,14 @@ public FlashbangProjectile SpawnActive(Vector3 position, Player owner = null) #if DEBUG Log.Debug($"Spawning active grenade: {FuseTime}"); #endif - ItemPickupBase ipb = Object.Instantiate(Projectile.Base, position, Quaternion.identity); - - ipb.Info = new PickupSyncInfo(Type, Weight, ItemSerialGenerator.GenerateNext()); - - FlashbangProjectile grenade = Pickup.Get(ipb); - grenade.Base.gameObject.SetActive(true); + Projectile projectile = CreateProjectile(position, Quaternion.identity); - grenade.MinimalDurationEffect = MinimalDurationEffect; - grenade.AdditionalBlindedEffect = AdditionalBlindedEffect; - grenade.SurfaceDistanceIntensifier = SurfaceDistanceIntensifier; - grenade.FuseTime = FuseTime; + projectile.PreviousOwner = owner; - grenade.PreviousOwner = owner ?? Server.Host; + projectile.Activate(); - grenade.Spawn(); - - grenade.Base.ServerActivate(); - - return grenade; + return (FlashbangProjectile)projectile; } /// @@ -150,5 +107,18 @@ internal override void ReadPickupInfoBefore(Pickup pickup) FuseTime = flashGrenadePickup.FuseTime; } } + + /// + protected override void InitializeProperties(ThrowableItem throwable) + { + base.InitializeProperties(throwable); + + if (throwable.Projectile is FlashbangGrenade grenade) + { + MinimalDurationEffect = grenade._minimalEffectDuration; + AdditionalBlindedEffect = grenade._additionalBlurDuration; + SurfaceDistanceIntensifier = grenade._surfaceZoneDistanceIntensifier; + } + } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Items/Item.cs b/EXILED/Exiled.API/Features/Items/Item.cs index f9d8a74a0d..cbafb9e060 100644 --- a/EXILED/Exiled.API/Features/Items/Item.cs +++ b/EXILED/Exiled.API/Features/Items/Item.cs @@ -10,7 +10,6 @@ namespace Exiled.API.Features.Items using System.Collections.Generic; using System.Linq; - using Exiled.API.Extensions; using Exiled.API.Features.Core; using Exiled.API.Features.Items.Keycards; using Exiled.API.Features.Pickups; @@ -412,11 +411,11 @@ public static T Create(ItemType type, Player owner = null) /// The rotation of the item. /// Whether the should be initially spawned. /// The created . - public virtual Pickup CreatePickup(Vector3 position, Quaternion? rotation = null, bool spawn = true) + public virtual Pickup CreatePickup(Vector3 position, Quaternion rotation = default, bool spawn = true) { PickupSyncInfo info = new(Type, Weight, Serial); - ItemPickupBase ipb = InventoryExtensions.ServerCreatePickup(Base, info, position, rotation ?? Quaternion.identity); + ItemPickupBase ipb = InventoryExtensions.ServerCreatePickup(Base, info, position, rotation); Base.OnRemoved(ipb); @@ -467,8 +466,6 @@ internal virtual void ChangeOwner(Player oldOwner, Player newOwner) Base.OnAdded(null); } - // TODO: remove use of GetWorldScale after NW fix WaypointToy. - /// /// Helper method for saving data between items and pickups. /// @@ -482,7 +479,7 @@ internal virtual void ReadPickupInfoBefore(Pickup pickup) { if (pickup is not null) { - Scale = pickup.GameObject.GetWorldScale(); + Scale = pickup.Scale; } } diff --git a/EXILED/Exiled.API/Features/Items/Radio.cs b/EXILED/Exiled.API/Features/Items/Radio.cs index d54abae2d9..3ea7a35ab4 100644 --- a/EXILED/Exiled.API/Features/Items/Radio.cs +++ b/EXILED/Exiled.API/Features/Items/Radio.cs @@ -129,4 +129,4 @@ public void SetRangeSettings(RadioRange range, RadioRangeSettings settings) /// new owner. internal override void ChangeOwner(Player oldOwner, Player newOwner) => Base.Owner = newOwner.ReferenceHub; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Items/Scp018.cs b/EXILED/Exiled.API/Features/Items/Scp018.cs index 37be245a83..c131733cd1 100644 --- a/EXILED/Exiled.API/Features/Items/Scp018.cs +++ b/EXILED/Exiled.API/Features/Items/Scp018.cs @@ -7,19 +7,14 @@ namespace Exiled.API.Features.Items { - using Exiled.API.Features.Pickups; using Exiled.API.Features.Pickups.Projectiles; - using InventorySystem.Items; - using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using UnityEngine; using BaseScp018Projectile = InventorySystem.Items.ThrowableProjectiles.Scp018Projectile; - using Object = UnityEngine.Object; - using Scp018Projectile = Pickups.Projectiles.Scp018Projectile; /// @@ -34,7 +29,6 @@ public class Scp018 : Throwable public Scp018(ThrowableItem itemBase) : base(itemBase) { - Projectile = (Scp018Projectile)((Throwable)this).Projectile; } /// @@ -48,28 +42,10 @@ internal Scp018(ItemType type, Player player = null) { } - /// - /// Gets a to change grenade properties. - /// - public new Scp018Projectile Projectile { get; } - /// /// Gets or sets the time for SCP-018 not to ignore the friendly fire. /// - public float FriendlyFireTime - { - get => Projectile.FriendlyFireTime; - set => Projectile.FriendlyFireTime = value; - } - - /// - /// Gets or sets how long the fuse will last. - /// - public float FuseTime - { - get => Projectile.FuseTime; - set => Projectile.FuseTime = value; - } + public float FriendlyFireTime { get; set; } /// /// Spawns an active grenade on the map at the specified location. @@ -82,24 +58,14 @@ public Scp018Projectile SpawnActive(Vector3 position, Player owner = null) #if DEBUG Log.Debug($"Spawning active grenade: {FuseTime}"); #endif - ItemPickupBase ipb = Object.Instantiate(Projectile.Base, position, Quaternion.identity); - - ipb.Info = new PickupSyncInfo(Type, Weight, ItemSerialGenerator.GenerateNext()); - - Scp018Projectile grenade = Pickup.Get(ipb); - - grenade.Base.gameObject.SetActive(true); - grenade.FriendlyFireTime = FriendlyFireTime; - grenade.FuseTime = FuseTime; + Projectile projectile = CreateProjectile(position, Quaternion.identity); - grenade.PreviousOwner = owner ?? Server.Host; + projectile.PreviousOwner = owner; - grenade.Spawn(); + projectile.Activate(); - grenade.Base.ServerActivate(); - - return grenade; + return (Scp018Projectile)projectile; } /// @@ -119,5 +85,15 @@ public Scp018Projectile SpawnActive(Vector3 position, Player owner = null) PinPullTime = PinPullTime, Repickable = Repickable, }; + + /// + protected override void InitializeProperties(ThrowableItem throwable) + { + base.InitializeProperties(throwable); + if (throwable.Projectile is BaseScp018Projectile grenade) + { + FriendlyFireTime = grenade._friendlyFireTime; + } + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Items/Scp1509.cs b/EXILED/Exiled.API/Features/Items/Scp1509.cs index b6c51626e2..87c7837fb7 100644 --- a/EXILED/Exiled.API/Features/Items/Scp1509.cs +++ b/EXILED/Exiled.API/Features/Items/Scp1509.cs @@ -7,13 +7,8 @@ namespace Exiled.API.Features.Items { - using System.Collections.Generic; - using System.Linq; - - using Exiled.API.Enums; using Exiled.API.Interfaces; using InventorySystem.Items.Scp1509; - using PlayerRoles; /// /// A wrapper class for . @@ -43,11 +38,6 @@ internal Scp1509() /// public new Scp1509Item Base { get; } - /// - /// Gets the instance. - /// - public Scp1509RespawnEligibility RespawnEligibility => Base._respawnEligibility; - /// /// Gets or sets the shield regeneration rate. /// @@ -84,82 +74,6 @@ public float UnequipDecayDelay set => Base.UnequipDecayDelay = value; } - /// - /// Gets or sets the time when resurrection ability will be available again. - /// - public double NextResurrectTime - { - get => Base._nextResurrectTime; - set => Base._nextResurrectTime = value; - } - - /// - /// Gets or sets the cooldown for a melee attack. - /// - public float MeleeCooldown - { - get => Base._meleeCooldown; - set => Base._meleeCooldown = value; // TODO not syned with clients, tests required - } - - /// - /// Gets or sets the amount of AHP bonus that all revived players are receiving. - /// - public float RevivedAhpBonus - { - get => Base._revivedPlayerAOEBonusAHP; - set => Base._revivedPlayerAOEBonusAHP = value; - } - - /// - /// Gets or sets the distance in which all revived players will receive AHP bonus. - /// - public float RevivedAhpBonusDistance - { - get => Base._revivedPlayerAOEBonusAHPDistance; - set => Base._revivedPlayerAOEBonusAHPDistance = value; - } - - /// - /// Gets or sets the max amount of HumeShield that can owner receive. - /// - public float MaxHs - { - get => Base._equippedHS; - set => Base._equippedHS = value; - } - - /// - /// Gets or sets the duration for a revived player. - /// - public float RevivedBlurTime - { - get => Base._revivedPlayerBlurTime; - set => Base._revivedPlayerBlurTime = value; - } - - /// - /// Gets or sets all revived players. - /// - public IEnumerable RevivedPlayers - { - get => Base._revivedPlayers.Select(Player.Get); - set => Base._revivedPlayers = value.Select(x => x.ReferenceHub).ToList(); - } - - /// - /// Gets a player that is eligible for respawn as a . - /// - /// Role to respawn. - /// Found player or null. - public Player GetEligibleSpectator(RoleTypeId roleTypeId) => Player.Get(RespawnEligibility.GetEligibleSpectator(roleTypeId)); - - /// - /// Checks if there is any eligible spectator for spawn. - /// - /// true if any spectator is found. Otherwise, false. - public bool IsAnyEligibleSpectators() => RespawnEligibility.IsAnyEligibleSpectators(); - /// /// Clones current object. /// diff --git a/EXILED/Exiled.API/Features/Items/Scp2176.cs b/EXILED/Exiled.API/Features/Items/Scp2176.cs index 7264f778cf..751f7658ce 100644 --- a/EXILED/Exiled.API/Features/Items/Scp2176.cs +++ b/EXILED/Exiled.API/Features/Items/Scp2176.cs @@ -7,10 +7,8 @@ namespace Exiled.API.Features.Items { - using Exiled.API.Features.Pickups; + using Exiled.API.Features.Pickups.Projectiles; - using InventorySystem.Items; - using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using UnityEngine; @@ -18,7 +16,7 @@ namespace Exiled.API.Features.Items using Scp2176Projectile = Pickups.Projectiles.Scp2176Projectile; /// - /// A wrapper class for . + /// A wrapper class for . /// public class Scp2176 : Throwable { @@ -29,7 +27,6 @@ public class Scp2176 : Throwable public Scp2176(ThrowableItem itemBase) : base(itemBase) { - Projectile = (Scp2176Projectile)((Throwable)this).Projectile; } /// @@ -43,18 +40,9 @@ internal Scp2176(Player player = null) } /// - /// Gets a to change grenade properties. + /// Gets or sets a value indicating whether SCP-2176's next collision will make the dropped sound effect. /// - public new Scp2176Projectile Projectile { get; } - - /// - /// Gets or sets how long the fuse will last. - /// - public float FuseTime - { - get => Projectile.FuseTime; - set => Projectile.FuseTime = value; - } + public bool DropSound { get; set; } /// /// Spawns an active grenade on the map at the specified location. @@ -67,23 +55,14 @@ public Scp2176Projectile SpawnActive(Vector3 position, Player owner = null) #if DEBUG Log.Debug($"Spawning active grenade: {FuseTime}"); #endif - ItemPickupBase ipb = Object.Instantiate(Projectile.Base, position, Quaternion.identity); - - ipb.Info = new PickupSyncInfo(Type, Weight, ItemSerialGenerator.GenerateNext()); - Scp2176Projectile grenade = Pickup.Get(ipb); + Projectile projectile = CreateProjectile(position, Quaternion.identity); - grenade.Base.gameObject.SetActive(true); + projectile.PreviousOwner = owner; - grenade.FuseTime = FuseTime; + projectile.Activate(); - grenade.PreviousOwner = owner ?? Server.Host; - - grenade.Spawn(); - - grenade.Base.ServerActivate(); - - return grenade; + return (Scp2176Projectile)projectile; } /// @@ -102,5 +81,13 @@ public Scp2176Projectile SpawnActive(Vector3 position, Player owner = null) /// /// A string containing ExplosiveGrenade-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{FuseTime}|"; + + /// + protected override void InitializeProperties(ThrowableItem throwable) + { + base.InitializeProperties(throwable); + if (throwable.Projectile is InventorySystem.Items.ThrowableProjectiles.Scp2176Projectile projectile) + DropSound = projectile._playedDropSound; + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Items/Scp244.cs b/EXILED/Exiled.API/Features/Items/Scp244.cs index 9f9c73530d..0fdc312c5f 100644 --- a/EXILED/Exiled.API/Features/Items/Scp244.cs +++ b/EXILED/Exiled.API/Features/Items/Scp244.cs @@ -82,11 +82,11 @@ public bool Primed /// The rotation of the item. /// Whether the should be initially spawned. /// The created . - public override Pickup CreatePickup(Vector3 position, Quaternion? rotation = null, bool spawn = true) + public override Pickup CreatePickup(Vector3 position, Quaternion rotation = default, bool spawn = true) { PickupSyncInfo info = new(Type, Weight, Serial); - Scp244DeployablePickup ipb = (Scp244DeployablePickup)InventoryExtensions.ServerCreatePickup(Base, info, position, rotation ?? Quaternion.identity); + Scp244DeployablePickup ipb = (Scp244DeployablePickup)InventoryExtensions.ServerCreatePickup(Base, info, position, rotation); Base.OnRemoved(ipb); diff --git a/EXILED/Exiled.API/Features/Items/Scp330.cs b/EXILED/Exiled.API/Features/Items/Scp330.cs index 38c9ded765..f31bfcf25a 100644 --- a/EXILED/Exiled.API/Features/Items/Scp330.cs +++ b/EXILED/Exiled.API/Features/Items/Scp330.cs @@ -240,11 +240,11 @@ public IEnumerable DropCandy(CandyKindID type, bool dropAll = fals /// The rotation to give the item. /// Whether the should be initially spawned. /// The created . - public override Pickup CreatePickup(Vector3 position, Quaternion? rotation = null, bool spawn = true) + public override Pickup CreatePickup(Vector3 position, Quaternion rotation = default, bool spawn = true) { PickupSyncInfo info = new(Type, Weight, Serial); - InventorySystem.Items.Usables.Scp330.Scp330Pickup ipb = (InventorySystem.Items.Usables.Scp330.Scp330Pickup)InventoryExtensions.ServerCreatePickup(Base, info, position, rotation ?? Quaternion.identity); + InventorySystem.Items.Usables.Scp330.Scp330Pickup ipb = (InventorySystem.Items.Usables.Scp330.Scp330Pickup)InventoryExtensions.ServerCreatePickup(Base, info, position, rotation); Base.OnRemoved(ipb); diff --git a/EXILED/Exiled.API/Features/Items/Throwable.cs b/EXILED/Exiled.API/Features/Items/Throwable.cs index 721167a3b5..b4f34ab86c 100644 --- a/EXILED/Exiled.API/Features/Items/Throwable.cs +++ b/EXILED/Exiled.API/Features/Items/Throwable.cs @@ -11,6 +11,7 @@ namespace Exiled.API.Features.Items using Exiled.API.Features.Pickups.Projectiles; using Exiled.API.Interfaces; + using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using UnityEngine; @@ -28,10 +29,7 @@ public Throwable(ThrowableItem itemBase) : base(itemBase) { Base = itemBase; - Base.Projectile.gameObject.SetActive(false); - Projectile = Pickup.Get(Object.Instantiate(Base.Projectile)); - Base.Projectile.gameObject.SetActive(true); - Projectile.Serial = Serial; + InitializeProperties(itemBase); } /// @@ -45,22 +43,15 @@ internal Throwable(ItemType type, Player player = null) { } - /// - public override Vector3 Scale - { - get => base.Scale; - set => Projectile.Scale = base.Scale = value; - } - /// /// Gets the base for this item. /// public new ThrowableItem Base { get; } /// - /// Gets a to change grenade properties. + /// Gets or sets how long the fuse will last. /// - public Projectile Projectile { get; } + public float FuseTime { get; set; } /// /// Gets or sets the amount of time it takes to pull the pin. @@ -92,6 +83,30 @@ public void Throw(bool fullForce = true) Base.ServerThrow(settings.StartVelocity, settings.UpwardsFactor, settings.StartTorque, ThrowableNetworkHandler.GetLimitedVelocity(Owner?.Velocity ?? Vector3.one)); } + /// + /// Creates the that based on this . + /// + /// The location to spawn the item. + /// The rotation of the item. + /// Whether the should be initially spawned. + /// The created . + /// wont be activated, use to activate spawned . + public virtual Projectile CreateProjectile(Vector3 position, Quaternion rotation = default, bool spawn = true) + { + ThrownProjectile ipb = Object.Instantiate(Base.Projectile, position, rotation); + + ipb.Info = new PickupSyncInfo(Type, Weight, Serial); + + Projectile projectile = Pickup.Get(ipb); + + projectile.ReadThrowableItemInfo(this); + + if (spawn) + projectile.Spawn(); + + return projectile; + } + /// /// Cancel the the throws of the item. /// @@ -112,5 +127,17 @@ public void Throw(bool fullForce = true) /// /// A string containing Throwable-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{PinPullTime}|"; + + /// + /// initialize throwable item properties. + /// + /// target throwable. + protected virtual void InitializeProperties(ThrowableItem throwable) + { + if (throwable.Projectile is TimeGrenade timeGrenade) + { + FuseTime = timeGrenade._fuseTime; + } + } } } diff --git a/EXILED/Exiled.API/Features/Items/Usable.cs b/EXILED/Exiled.API/Features/Items/Usable.cs index b58f6b8dd9..688882d46e 100644 --- a/EXILED/Exiled.API/Features/Items/Usable.cs +++ b/EXILED/Exiled.API/Features/Items/Usable.cs @@ -110,11 +110,11 @@ public float RemainingCooldown /// The rotation of the item. /// Whether the should be initially spawned. /// The created . - public override Pickup CreatePickup(Vector3 position, Quaternion? rotation = null, bool spawn = true) + public override Pickup CreatePickup(Vector3 position, Quaternion rotation = default, bool spawn = true) { PickupSyncInfo info = new(Type, Weight, Serial); - ItemPickupBase ipb = InventoryExtensions.ServerCreatePickup(Base, info, position, rotation ?? Quaternion.identity); + ItemPickupBase ipb = InventoryExtensions.ServerCreatePickup(Base, info, position, rotation); Pickup pickup = Pickup.Get(ipb); @@ -124,20 +124,19 @@ public override Pickup CreatePickup(Vector3 position, Quaternion? rotation = nul return pickup; } - /// - /// Uses the item. - /// - public virtual void Use() => Use(Owner); - /// /// Uses the item. /// /// Target to use an . + /// The of the item cannot be . public virtual void Use(Player owner = null) { Player oldOwner = Owner; owner ??= Owner; + if (owner is null) + throw new System.InvalidOperationException("The Owner of the item cannot be null."); + Base.Owner = owner.ReferenceHub; Base.ServerOnUsingCompleted(); @@ -157,4 +156,4 @@ internal override void ReadPickupInfoBefore(Pickup pickup) } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Lift.cs b/EXILED/Exiled.API/Features/Lift.cs index 469a33bd22..de7dcfabb7 100644 --- a/EXILED/Exiled.API/Features/Lift.cs +++ b/EXILED/Exiled.API/Features/Lift.cs @@ -19,7 +19,6 @@ namespace Exiled.API.Features using Interactables.Interobjects; using Interactables.Interobjects.DoorUtils; using UnityEngine; - using Utils; using static Interactables.Interobjects.ElevatorChamber; @@ -81,7 +80,7 @@ internal Lift(ElevatorChamber elevator) /// /// Gets a of in the . /// - public IEnumerable Players => Player.List.Where(x => RelativeBounds.Contains(x.Position)); + public IEnumerable Players => Player.List.Where(x => Bounds.Contains(x.Position)); /// /// Gets the lift's name. @@ -128,13 +127,7 @@ public ElevatorSequence Status /// /// Gets the representing the space inside the lift. /// - [Obsolete("It's now necessary to use RelativeBounds instead", true)] - public Bounds Bounds => Base.WorldspaceBounds.Bounds; - - /// - /// Gets the representing the space inside the lift. - /// - public RelativeBounds RelativeBounds => Base.WorldspaceBounds; + public Bounds Bounds => Base.WorldspaceBounds; /// /// Gets the lift's . @@ -257,7 +250,7 @@ public float AnimationTime /// /// The . /// A or if not found. - public static Lift Get(Vector3 position) => Get(lift => lift.RelativeBounds.Contains(position)).FirstOrDefault(); + public static Lift Get(Vector3 position) => Get(lift => lift.Bounds.Contains(position)).FirstOrDefault(); /// /// Gets a of filtered based on a predicate. @@ -317,7 +310,7 @@ public void ChangeLock(DoorLockReason lockReason) /// /// The position. /// if the point is inside the elevator. Otherwise, . - public bool IsInElevator(Vector3 point) => RelativeBounds.Contains(point); + public bool IsInElevator(Vector3 point) => Bounds.Contains(point); /// /// Returns the Lift in a human-readable format. diff --git a/EXILED/Exiled.API/Features/Lockers/Chamber.cs b/EXILED/Exiled.API/Features/Lockers/Chamber.cs index e98281ed5c..a029a90847 100644 --- a/EXILED/Exiled.API/Features/Lockers/Chamber.cs +++ b/EXILED/Exiled.API/Features/Lockers/Chamber.cs @@ -23,7 +23,7 @@ namespace Exiled.API.Features.Lockers /// /// A wrapper for . /// - public class Chamber : IWrapper, IWorldSpace + public class Chamber : IWrapper, IWorldSpace, IPermission { /// /// with and . @@ -112,6 +112,16 @@ public IEnumerable AcceptableTypes /// /// Gets or sets required permissions to open this chamber. /// + public KeycardPermissions Permissions + { + get => (KeycardPermissions)Base.RequiredPermissions; + set => Base.RequiredPermissions = (Interactables.Interobjects.DoorUtils.DoorPermissionFlags)value; + } + + /// + /// Gets or sets required permissions to open this chamber. + /// + [Obsolete] public KeycardPermissions RequiredPermissions { get => (KeycardPermissions)Base.RequiredPermissions; diff --git a/EXILED/Exiled.API/Features/Log.cs b/EXILED/Exiled.API/Features/Log.cs index 48203577c6..39e9ef1829 100644 --- a/EXILED/Exiled.API/Features/Log.cs +++ b/EXILED/Exiled.API/Features/Log.cs @@ -175,4 +175,4 @@ public static void Assert(bool condition, object message) throw new Exception(message.ToString()); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Map.cs b/EXILED/Exiled.API/Features/Map.cs index 8eb89e0226..7a7c32c05d 100644 --- a/EXILED/Exiled.API/Features/Map.cs +++ b/EXILED/Exiled.API/Features/Map.cs @@ -21,6 +21,7 @@ namespace Exiled.API.Features using Exiled.API.Features.Hazards; using Exiled.API.Features.Items.Keycards; using Exiled.API.Features.Pickups; + using Exiled.API.Features.Pickups.Projectiles; using Exiled.API.Features.Toys; using InventorySystem; using InventorySystem.Items.Pickups; @@ -348,24 +349,13 @@ public static IEnumerable GetNearCameras(Vector3 position, float tolerat /// The player who create the explosion. public static void Explode(Vector3 position, ProjectileType projectileType, Player attacker = null) { - ItemType item; - if ((item = projectileType.GetItemType()) is ItemType.None) - return; - attacker ??= Server.Host; - if (!InventoryItemLoader.TryGetItem(item, out ThrowableItem throwableItem)) + if (projectileType is ProjectileType.None) return; - if (Object.Instantiate(throwableItem.Projectile) is not TimeGrenade timedGrenadePickup) - return; - - if (timedGrenadePickup is Scp018Projectile scp018Projectile) - scp018Projectile.SetupModule(); - else - ExplodeEffect(position, projectileType); + attacker ??= Server.Host; - timedGrenadePickup.Position = position; - timedGrenadePickup.PreviousOwner = (attacker ?? Server.Host).Footprint; - timedGrenadePickup.ServerFuseEnd(); + TimeGrenadeProjectile projectile = Projectile.CreateAndSpawn(projectileType, position, Quaternion.identity, false, attacker); + projectile.Explode(); } /// diff --git a/EXILED/Exiled.API/Features/Npc.cs b/EXILED/Exiled.API/Features/Npc.cs index 7791218042..07a3986b4b 100644 --- a/EXILED/Exiled.API/Features/Npc.cs +++ b/EXILED/Exiled.API/Features/Npc.cs @@ -12,12 +12,14 @@ namespace Exiled.API.Features using System.Collections.Generic; using System.Linq; + using CentralAuth; using CommandSystem; using CommandSystem.Commands.RemoteAdmin.Dummies; using Exiled.API.Enums; using Exiled.API.Features.CustomStats; using Exiled.API.Features.Roles; using Footprinting; + using GameCore; using MEC; using Mirror; using NetworkManagerUtils.Dummies; @@ -48,9 +50,14 @@ public Npc(GameObject gameObject) } /// - /// Gets a list of Npcs. + /// Gets a containing all 's on the server. /// - public static new IReadOnlyCollection List => Dictionary.Values.OfType().ToList(); + public static new Dictionary Dictionary { get; } = new(Server.MaxPlayerCount, new ReferenceHub.GameObjectComparer()); + + /// + /// Gets a list of all 's on the server. + /// + public static new IReadOnlyCollection List => Dictionary.Values; /// /// Gets or sets the player's position. @@ -102,7 +109,7 @@ public float? MaxDistance set { - if(!value.HasValue) + if (!value.HasValue) return; if (!GameObject.TryGetComponent(out PlayerFollower follower)) @@ -131,7 +138,7 @@ public float? MinDistance set { - if(!value.HasValue) + if (!value.HasValue) return; if (!GameObject.TryGetComponent(out PlayerFollower follower)) @@ -160,7 +167,7 @@ public float? Speed set { - if(!value.HasValue) + if (!value.HasValue) return; if (!GameObject.TryGetComponent(out PlayerFollower follower)) @@ -343,17 +350,5 @@ public void Destroy() Log.Error($"Error while destroying a NPC: {e.Message}"); } } - - /// - /// Schedules the destruction of the NPC after a delay. - /// - /// The delay in seconds before the NPC is destroyed. - public void LateDestroy(float time) - { - Timing.CallDelayed(time, () => - { - this?.Destroy(); - }); - } } } diff --git a/EXILED/Exiled.API/Features/ParentCommand.cs b/EXILED/Exiled.API/Features/ParentCommand.cs new file mode 100644 index 0000000000..488f9197e2 --- /dev/null +++ b/EXILED/Exiled.API/Features/ParentCommand.cs @@ -0,0 +1,86 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Features +{ + using System; + using System.Collections.Generic; + using System.Text; + + using CommandSystem; + using NorthwoodLib.Pools; + + /// + /// Представляет удобный класс для создания родительских команд. + /// + public abstract class ParentCommand : CommandHandler, ICommand + { + /// + /// Initializes a new instance of the class. + /// + protected ParentCommand() + { + LoadGeneratedCommands(); + } + + /// + public abstract string Command { get; } + + /// + public abstract string[] Aliases { get; set; } + + /// + public abstract string Description { get; set; } + + /// + public override sealed void LoadGeneratedCommands() + { + foreach (Type commandType in CommandsToRegister()) + { + if (commandType.GetInterface(nameof(ICommand)) != typeof(ICommand)) + { + Log.Error($"Invalid command type provided for parent command {Command}: {commandType.FullName}"); + continue; + } + + ICommand command = (ICommand)Activator.CreateInstance(commandType); + RegisterCommand(command); + } + } + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + return arguments.Count != 0 && TryGetCommand(arguments.Array![arguments.Offset], out ICommand command) + ? command.Execute(new ArraySegment(arguments.Array, arguments.Offset + 1, arguments.Count - 1), sender, out response) + : ExecuteParent(arguments, sender, out response); + } + + /// + /// Gets HashSet of subcommands to register. + /// + /// IEnumerable of commands to register. + protected abstract IEnumerable CommandsToRegister(); + + /// + /// Executes parent comand without subcommands. + /// + /// Passed args. + /// Command sender. + /// Response for user. + /// Was command executed succesfully or not. + protected virtual bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + StringBuilder message = StringBuilderPool.Shared.Rent($"{Command}:\n"); + foreach (ICommand command in AllCommands) + message.AppendFormat("- {0}\n{1}\n\n", command.Command, command.Description); + + response = StringBuilderPool.Shared.ToStringReturn(message); + return false; + } + } +} diff --git a/EXILED/Exiled.API/Features/Paths.cs b/EXILED/Exiled.API/Features/Paths.cs index 8d99b1a996..c83cb656bb 100644 --- a/EXILED/Exiled.API/Features/Paths.cs +++ b/EXILED/Exiled.API/Features/Paths.cs @@ -80,6 +80,11 @@ public static class Paths /// public static string Translations { get; set; } + /// + /// Gets or sets translations path. + /// + public static string CommandTranslations { get; set; } + /// /// Gets or sets individual translations directory path. /// @@ -113,6 +118,7 @@ public static void Reload(string rootDirectory = null) BackupConfig = Path.Combine(Configs, $"{Server.Port}-config.yml.old"); IndividualTranslations = Path.Combine(Configs, "Translations"); Translations = Path.Combine(Configs, $"{Server.Port}-translations.yml"); + CommandTranslations = Path.Combine(Configs, $"{Server.Port}-command-translations.yml"); BackupTranslations = Path.Combine(Configs, $"{Server.Port}-translations.yml.old"); Log = Path.Combine(Exiled, $"{Server.Port}-RemoteAdminLog.txt"); } @@ -131,4 +137,4 @@ public static void Reload(string rootDirectory = null) /// The translation path of the plugin. public static string GetTranslationPath(string pluginPrefix) => Path.Combine(IndividualTranslations, pluginPrefix, $"{Server.Port}.yml"); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Pickups/ExplosiveGrenadePickup.cs b/EXILED/Exiled.API/Features/Pickups/ExplosiveGrenadePickup.cs index 62484bcceb..191632a607 100644 --- a/EXILED/Exiled.API/Features/Pickups/ExplosiveGrenadePickup.cs +++ b/EXILED/Exiled.API/Features/Pickups/ExplosiveGrenadePickup.cs @@ -76,20 +76,6 @@ internal override void ReadItemInfo(Item item) } } - /// - internal override void WriteProjectileInfo(Projectile projectile) - { - if (projectile is ExplosionGrenadeProjectile explosionGrenadeProjectile) - { - explosionGrenadeProjectile.MaxRadius = MaxRadius; - explosionGrenadeProjectile.ScpDamageMultiplier = ScpDamageMultiplier; - explosionGrenadeProjectile.BurnDuration = BurnDuration; - explosionGrenadeProjectile.DeafenDuration = DeafenDuration; - explosionGrenadeProjectile.ConcussDuration = ConcussDuration; - explosionGrenadeProjectile.FuseTime = FuseTime; - } - } - /// protected override void InitializeProperties(ItemBase itemBase) { @@ -104,4 +90,4 @@ protected override void InitializeProperties(ItemBase itemBase) } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Pickups/FlashGrenadePickup.cs b/EXILED/Exiled.API/Features/Pickups/FlashGrenadePickup.cs index 3e4c979b50..96e11e305a 100644 --- a/EXILED/Exiled.API/Features/Pickups/FlashGrenadePickup.cs +++ b/EXILED/Exiled.API/Features/Pickups/FlashGrenadePickup.cs @@ -64,18 +64,6 @@ internal override void ReadItemInfo(Item item) } } - /// - internal override void WriteProjectileInfo(Projectile projectile) - { - if (projectile is FlashbangProjectile flashbangProjectile) - { - flashbangProjectile.MinimalDurationEffect = MinimalDurationEffect; - flashbangProjectile.AdditionalBlindedEffect = AdditionalBlindedEffect; - flashbangProjectile.SurfaceDistanceIntensifier = SurfaceDistanceIntensifier; - flashbangProjectile.FuseTime = FuseTime; - } - } - /// protected override void InitializeProperties(ItemBase itemBase) { diff --git a/EXILED/Exiled.API/Features/Pickups/GrenadePickup.cs b/EXILED/Exiled.API/Features/Pickups/GrenadePickup.cs index 3d62c428fc..bfb66d1eab 100644 --- a/EXILED/Exiled.API/Features/Pickups/GrenadePickup.cs +++ b/EXILED/Exiled.API/Features/Pickups/GrenadePickup.cs @@ -9,7 +9,6 @@ namespace Exiled.API.Features.Pickups { using Exiled.API.Enums; using Exiled.API.Extensions; - using Exiled.API.Features.Pickups.Projectiles; using Exiled.API.Interfaces; using Footprinting; @@ -72,18 +71,6 @@ public void Explode(Footprint attacker) Base._attacker = attacker; } - /// - /// Helper method for saving data between projectiles and pickups. - /// - /// -related data to write to. - internal virtual void WriteProjectileInfo(Projectile projectile) - { - if (projectile is TimeGrenadeProjectile timeGrenadeProjectile) - { - timeGrenadeProjectile.FuseTime = FuseTime; - } - } - /// protected override void InitializeProperties(ItemBase itemBase) { diff --git a/EXILED/Exiled.API/Features/Pickups/Keycards/ManagementKeycardPickup.cs b/EXILED/Exiled.API/Features/Pickups/Keycards/ManagementKeycardPickup.cs index 9666c24b21..49fa5e7281 100644 --- a/EXILED/Exiled.API/Features/Pickups/Keycards/ManagementKeycardPickup.cs +++ b/EXILED/Exiled.API/Features/Pickups/Keycards/ManagementKeycardPickup.cs @@ -60,7 +60,10 @@ public Color LabelColor } /// - /// Creates an UnSpawned , to spawn the pickup, call on the returned instance. + /// Creates an UnSpawned , to spawn the pickup, call + /// Pickup.Spawn(Vector3, Quaternion?, Player) + /// + /// on the returned instance. /// /// The permissions of the keycard. /// The color of the permissions of the keycard. diff --git a/EXILED/Exiled.API/Features/Pickups/Pickup.cs b/EXILED/Exiled.API/Features/Pickups/Pickup.cs index 5dcceb6135..8b9c72f9cc 100644 --- a/EXILED/Exiled.API/Features/Pickups/Pickup.cs +++ b/EXILED/Exiled.API/Features/Pickups/Pickup.cs @@ -13,16 +13,15 @@ namespace Exiled.API.Features.Pickups using Exiled.API.Extensions; using Exiled.API.Features.Core; + using Exiled.API.Features.Items; using Exiled.API.Features.Pickups.Keycards; using Exiled.API.Features.Pickups.Projectiles; using Exiled.API.Interfaces; - using InventorySystem; using InventorySystem.Items; using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; using InventorySystem.Items.Usables.Scp244; - using Mirror; using RelativePositioning; using UnityEngine; @@ -39,7 +38,6 @@ namespace Exiled.API.Features.Pickups using BaseScp1576Pickup = InventorySystem.Items.Usables.Scp1576.Scp1576Pickup; using BaseScp2176Projectile = InventorySystem.Items.ThrowableProjectiles.Scp2176Projectile; using BaseScp330Pickup = InventorySystem.Items.Usables.Scp330.Scp330Pickup; - using Object = UnityEngine.Object; /// @@ -147,6 +145,19 @@ public ushort Serial } } + /// + /// Gets or sets a value indicating whether Pickup is kinematic. + /// + public bool IsKinematic + { + get => Rigidbody.isKinematic; + set + { + Rigidbody.isKinematic = value; + PhysicsModule.ServerSendRpc(PhysicsModule.ServerWriteRigidbody); + } + } + /// /// Gets or sets the pickup's scale value. /// @@ -239,7 +250,7 @@ public PickupSyncInfo Info /// /// Gets or sets the previous owner of this item. /// - /// + /// public Player PreviousOwner { get => Player.Get(Base.PreviousOwner.Hub); @@ -262,7 +273,7 @@ public bool InUse /// /// Gets or sets the pickup position. /// - /// + /// public Vector3 Position { get => Base.Position; @@ -281,7 +292,7 @@ public RelativePosition RelativePosition /// /// Gets or sets the pickup rotation. /// - /// + /// public Quaternion Rotation { get => Base.Rotation; @@ -565,8 +576,8 @@ public static T Create(ItemType type) /// The rotation to spawn the . /// An optional previous owner of the item. /// The . See documentation of for more information on casting. - /// - public static Pickup CreateAndSpawn(ItemType type, Vector3 position, Quaternion? rotation = null, Player previousOwner = null) => Create(type).Spawn(position, rotation, previousOwner); + /// + public static Pickup CreateAndSpawn(ItemType type, Vector3 position, Quaternion rotation, Player previousOwner = null) => Create(type).Spawn(position, rotation, previousOwner); /// /// Creates and spawns a . @@ -577,8 +588,8 @@ public static T Create(ItemType type) /// An optional previous owner of the item. /// The specified type. /// The . See documentation of for more information on casting. - /// - public static T CreateAndSpawn(ItemType type, Vector3 position, Quaternion? rotation = null, Player previousOwner = null) + /// + public static Pickup CreateAndSpawn(ItemType type, Vector3 position, Quaternion rotation, Player previousOwner = null) where T : Pickup => CreateAndSpawn(type, position, rotation, previousOwner) as T; /// @@ -621,11 +632,11 @@ public virtual void Spawn() /// The rotation to spawn the . /// An optional previous owner of the item. /// The spawned . - /// - public Pickup Spawn(Vector3 position, Quaternion? rotation = null, Player previousOwner = null) + /// + public Pickup Spawn(Vector3 position, Quaternion rotation, Player previousOwner = null) { Position = position; - Rotation = rotation ?? Quaternion.identity; + Rotation = rotation; PreviousOwner = previousOwner; Spawn(); @@ -668,6 +679,21 @@ public void UnSpawn() /// A string containing Pickup-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{Position}| -{IsLocked}- ={InUse}="; + /// + /// Creates the that based on this . + /// + /// The created . + public virtual Item CreateItem() + { + Item item = Item.Create(Type); + item.Serial = Serial; + item.ReadPickupInfoBefore(this); + item.Base.OnAdded(Base); + item.ReadPickupInfoAfter(this); + + return item; + } + /// /// Helper method for saving data between items and pickups. /// diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/ExplosionGrenadeProjectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/ExplosionGrenadeProjectile.cs index 07cdbebefb..6352af8460 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/ExplosionGrenadeProjectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/ExplosionGrenadeProjectile.cs @@ -103,5 +103,19 @@ public float ScpDamageMultiplier /// /// A string containing ExplosionGrenadePickup-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{Position}| -{IsLocked}- ={InUse}="; + + /// + internal override void ReadGrenadePickupInfo(GrenadePickup pickup) + { + base.ReadGrenadePickupInfo(pickup); + if (pickup is ExplosiveGrenadePickup grenade) + { + MaxRadius = grenade.MaxRadius; + ScpDamageMultiplier = grenade.ScpDamageMultiplier; + BurnDuration = grenade.BurnDuration; + DeafenDuration = grenade.DeafenDuration; + ConcussDuration = grenade.ConcussDuration; + } + } } } diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/FlashbangProjectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/FlashbangProjectile.cs index 8c289c2840..ba68c8f855 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/FlashbangProjectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/FlashbangProjectile.cs @@ -73,5 +73,17 @@ public float SurfaceDistanceIntensifier /// /// A string containing FlashbangPickup-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{Position}| -{IsLocked}- ={InUse}="; + + /// + internal override void ReadGrenadePickupInfo(GrenadePickup pickup) + { + base.ReadGrenadePickupInfo(pickup); + if (pickup is FlashGrenadePickup grenade) + { + MinimalDurationEffect = grenade.MinimalDurationEffect; + AdditionalBlindedEffect = grenade.AdditionalBlindedEffect; + SurfaceDistanceIntensifier = grenade.SurfaceDistanceIntensifier; + } + } } } diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/Projectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/Projectile.cs index f9d5b4f10d..e70ba64e13 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/Projectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/Projectile.cs @@ -11,11 +11,14 @@ namespace Exiled.API.Features.Pickups.Projectiles using Exiled.API.Enums; using Exiled.API.Extensions; + using Exiled.API.Features.Items; using Exiled.API.Interfaces; + using InventorySystem; using InventorySystem.Items; using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; + using UnityEngine; using Object = UnityEngine.Object; @@ -123,7 +126,7 @@ public static T Create(ProjectileType projectiletype) /// Whether the should be in active state after spawn. /// An optional previous owner of the item. /// The . See documentation of for more information on casting. - public static Projectile CreateAndSpawn(ProjectileType type, Vector3 position, Quaternion? rotation = null, bool shouldBeActive = true, Player previousOwner = null) => Create(type).Spawn(position, rotation, shouldBeActive, previousOwner); + public static Projectile CreateAndSpawn(ProjectileType type, Vector3 position, Quaternion rotation, bool shouldBeActive = true, Player previousOwner = null) => Create(type).Spawn(position, rotation, shouldBeActive, previousOwner); /// /// Creates and spawns a . @@ -135,13 +138,13 @@ public static T Create(ProjectileType projectiletype) /// An optional previous owner of the item. /// The specified type. /// The . See documentation of for more information on casting. - public static T CreateAndSpawn(ProjectileType type, Vector3 position, Quaternion? rotation = null, bool shouldBeActive = true, Player previousOwner = null) + public static T CreateAndSpawn(ProjectileType type, Vector3 position, Quaternion rotation, bool shouldBeActive = true, Player previousOwner = null) where T : Projectile => CreateAndSpawn(type, position, rotation, shouldBeActive, previousOwner) as T; /// /// Activates the current . /// - public void Activate() => Base.ServerActivate(); + public virtual void Activate() => Base.ServerActivate(); /// /// Spawns a . @@ -151,10 +154,10 @@ public static T CreateAndSpawn(ProjectileType type, Vector3 position, Quatern /// Whether the should be in active state after spawn. /// An optional previous owner of the item. /// The spawned . - public Projectile Spawn(Vector3 position, Quaternion? rotation = null, bool shouldBeActive = true, Player previousOwner = null) + public Projectile Spawn(Vector3 position, Quaternion rotation, bool shouldBeActive = true, Player previousOwner = null) { Position = position; - Rotation = rotation ?? Quaternion.identity; + Rotation = rotation; PreviousOwner = previousOwner; Spawn(); @@ -169,5 +172,14 @@ public Projectile Spawn(Vector3 position, Quaternion? rotation = null, bool shou /// /// A string containing ProjectilePickup-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{Position}| -{IsLocked}- ={InUse}="; + + /// + /// Helper method for saving data between 's and 's. + /// + /// -related data to give to the . + internal virtual void ReadThrowableItemInfo(Throwable throwable) + { + Scale = throwable.Scale; + } } } diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp018Projectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp018Projectile.cs index f33d935a97..99b13cd061 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp018Projectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp018Projectile.cs @@ -10,6 +10,7 @@ namespace Exiled.API.Features.Pickups.Projectiles using System; using System.Reflection; + using Exiled.API.Features.Items; using Exiled.API.Interfaces; using HarmonyLib; @@ -25,6 +26,9 @@ public class Scp018Projectile : TimeGrenadeProjectile, IWrapper /// Initializes a new instance of the class. /// @@ -54,16 +58,26 @@ internal Scp018Projectile() /// public new Scp018Physics PhysicsModule => Base.PhysicsModule as Scp018Physics; + /// + /// Gets a value indicating whether current Scp018 instance is projectile-typed, or normal pickup. + /// + public bool IsProjectile => Base.PhysicsModule is Scp018Physics; + /// /// Gets or sets the pickup's max velocity. /// public float MaxVelocity { - get => PhysicsModule._maxVel; + get => IsProjectile ? PhysicsModule._maxVel : 0.0f; set { - maxVelocityField ??= AccessTools.Field(typeof(Scp018Physics), nameof(Scp018Physics._maxVel)); - maxVelocityField.SetValue(PhysicsModule, value); + if (IsProjectile) + { + maxVelocityField ??= AccessTools.Field(typeof(Scp018Physics), nameof(Scp018Physics._maxVel)); + maxVelocityField.SetValue(PhysicsModule, value); + } + + maxVelocity = value; } } @@ -72,11 +86,17 @@ public float MaxVelocity /// public float VelocityPerBounce { - get => PhysicsModule._maxVel; + get => IsProjectile ? PhysicsModule._velPerBounce : 0.0f; set { - velocityPerBounceField ??= AccessTools.Field(typeof(Scp018Physics), nameof(Scp018Physics._velPerBounce)); - velocityPerBounceField.SetValue(PhysicsModule, value); + if (IsProjectile) + { + velocityPerBounceField ??= AccessTools.Field(typeof(Scp018Physics), nameof(Scp018Physics._velPerBounce)); + velocityPerBounceField.SetValue(PhysicsModule, value); + return; + } + + velocityPerBounce = value; } } @@ -104,5 +124,42 @@ public float FriendlyFireTime /// /// A string containing Scp018Pickup-related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{Position}| -{Damage}- ={IgnoreFriendlyFire}="; + + /// + public override void Explode() + { + if (!IsProjectile) + Base.SetupModule(); + base.Explode(); + } + + /// + public override void Activate() + { + if (!IsProjectile) + Base.SetupModule(); + base.Activate(); + } + + /// + internal override void ReadThrowableItemInfo(Throwable throwable) + { + base.ReadThrowableItemInfo(throwable); + if (throwable is Scp018 scp018) + { + FriendlyFireTime = scp018.FriendlyFireTime; + } + } + + /// + /// Setups scp018 projectile. + /// + internal void SetupProjectile() + { + if (velocityPerBounce.HasValue) + VelocityPerBounce = velocityPerBounce.Value; + if (maxVelocity.HasValue) + MaxVelocity = maxVelocity.Value; + } } } diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp2176Projectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp2176Projectile.cs index 0c5715a9b9..dc204158f4 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp2176Projectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/Scp2176Projectile.cs @@ -7,6 +7,7 @@ namespace Exiled.API.Features.Pickups.Projectiles { + using Exiled.API.Features.Items; using Exiled.API.Interfaces; using InventorySystem.Items.ThrowableProjectiles; @@ -61,5 +62,15 @@ public bool DropSound /// /// A string containing Scp2176Pickup related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{FuseTime}| ={IsAlreadyDetonated}="; + + /// + internal override void ReadThrowableItemInfo(Throwable throwable) + { + base.ReadThrowableItemInfo(throwable); + if (throwable is Scp2176 scp2176) + { + DropSound = scp2176.DropSound; + } + } } } diff --git a/EXILED/Exiled.API/Features/Pickups/Projectiles/TimeGrenadeProjectile.cs b/EXILED/Exiled.API/Features/Pickups/Projectiles/TimeGrenadeProjectile.cs index cafd9be8cb..89c087b8d6 100644 --- a/EXILED/Exiled.API/Features/Pickups/Projectiles/TimeGrenadeProjectile.cs +++ b/EXILED/Exiled.API/Features/Pickups/Projectiles/TimeGrenadeProjectile.cs @@ -7,6 +7,7 @@ namespace Exiled.API.Features.Pickups.Projectiles { + using Exiled.API.Features.Items; using Exiled.API.Interfaces; using InventorySystem.Items.ThrowableProjectiles; @@ -80,7 +81,7 @@ public bool IsActive /// /// Immediately exploding the . /// - public void Explode() + public virtual void Explode() { Base.ServerFuseEnd(); Base._alreadyDetonated = true; @@ -91,5 +92,22 @@ public void Explode() /// /// A string containing TimeGrenadePickup related data. public override string ToString() => $"{Type} ({Serial}) [{Weight}] *{Scale}* |{FuseTime}| ={IsAlreadyDetonated}="; + + /// + /// Helper method for saving data between 's and 's. + /// + /// -related data to give to the . + internal virtual void ReadGrenadePickupInfo(GrenadePickup pickup) + { + FuseTime = pickup.FuseTime; + Scale = pickup.Scale; + } + + /// + internal override void ReadThrowableItemInfo(Throwable throwable) + { + base.ReadThrowableItemInfo(throwable); + FuseTime = throwable.FuseTime; + } } } diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs index e9e0f5a3e6..2ce483fa1a 100644 --- a/EXILED/Exiled.API/Features/Player.cs +++ b/EXILED/Exiled.API/Features/Player.cs @@ -377,11 +377,6 @@ public string CustomInfo get => ReferenceHub.nicknameSync.Network_customPlayerInfoString; set { - if (!NicknameSync.ValidateCustomInfo(value, out string rejectionText)) - { - Log.Warn($"Could not set CustomInfo for {Nickname}. Reason: {rejectionText}"); - } - InfoArea = string.IsNullOrEmpty(value) ? InfoArea & ~PlayerInfoArea.CustomInfo : InfoArea |= PlayerInfoArea.CustomInfo; ReferenceHub.nicknameSync.Network_customPlayerInfoString = value; } @@ -564,11 +559,7 @@ public Quaternion Rotation public PlayerPermissions RemoteAdminPermissions { get => (PlayerPermissions)ReferenceHub.serverRoles.Permissions; - set - { - ReferenceHub.serverRoles.Permissions = (ulong)value; - ReferenceHub.serverRoles.FinalizeSetGroup(); - } + set => ReferenceHub.serverRoles.Permissions = (ulong)value; } /// @@ -636,7 +627,7 @@ public ScpSpawnPreferences.SpawnPreferences ScpPreferences /// /// Gets a value indicating whether the player is reloading a weapon. /// - public bool IsReloading => CurrentItem is Firearm firearm && !firearm.IsReloading; + public bool IsReloading => CurrentItem is Firearm firearm && firearm.IsReloading; /// /// Gets a value indicating whether the player is aiming with a weapon. @@ -1349,6 +1340,9 @@ public static Player Get(GameObject gameObject) if (Dictionary.TryGetValue(gameObject, out Player player)) return player; + if (Npc.Dictionary.TryGetValue(gameObject, out Npc npc)) + return npc; + if (UnverifiedPlayers.TryGetValue(gameObject, out player)) return player; @@ -1919,7 +1913,7 @@ public bool TryGetItem(ushort serial, out Item item) } /// - /// Receives an existing rank(group) or, if it doesn't exist, creates a new one and assigns it to this player. + /// Sets the player's rank. /// /// The rank name to be set. /// The group to be set. @@ -1927,11 +1921,17 @@ public void SetRank(string name, UserGroup group) { if (ServerStatic.PermissionsHandler.Groups.TryGetValue(name, out UserGroup userGroup)) { + userGroup.BadgeColor = group.BadgeColor; + userGroup.BadgeText = name; + userGroup.HiddenByDefault = !group.Cover; + userGroup.Cover = group.Cover; + ReferenceHub.serverRoles.SetGroup(userGroup, false, false); } else { ServerStatic.PermissionsHandler.Groups.Add(name, group); + ReferenceHub.serverRoles.SetGroup(group, false, false); } @@ -2593,6 +2593,7 @@ public void ResetAmmoLimit(AmmoType ammoType) { if (!HasCustomAmmoLimit(ammoType)) { + Log.Error($"{nameof(Player)}.{nameof(ResetAmmoLimit)}(AmmoType): AmmoType.{ammoType} does not have a custom limit."); return; } @@ -2685,6 +2686,7 @@ public void ResetCategoryLimit(ItemCategory category) if (!HasCustomCategoryLimit(category)) { + Log.Error($"{nameof(Player)}.{nameof(ResetCategoryLimit)}(ItemCategory): ItemCategory.{category} does not have a custom limit."); return; } @@ -3538,6 +3540,19 @@ public void ChangeEffectIntensity(byte intensity, float duration = 0) } } + /// + /// Changes the intensity of a . + /// + /// The to change. + /// The new intensity to use. + public void ChangeEffectIntensity(EffectType type, byte intensity) + { + if (TryGetEffect(type, out StatusEffectBase statusEffect)) + { + statusEffect.Intensity = intensity; + } + } + /// /// Changes the intensity of a . /// @@ -3646,11 +3661,6 @@ public void Reconnect(ushort newPort = 0, float delay = 5, bool reconnect = true Connection.Send(new RoundRestartMessage(roundRestartType, delay, newPort, reconnect, false)); } - /// - [Obsolete("Use PlayGunSound(Player, Vector3, FirearmType, byte, byte) instead.")] - public void PlayGunSound(ItemType type, byte volume, byte audioClipId = 0) - => PlayGunSound(type.GetFirearmType(), volume, audioClipId); - /// public void PlayGunSound(FirearmType itemType, float pitch = 1, int clipIndex = 0) => this.PlayGunSound(Position, itemType, pitch, clipIndex); diff --git a/EXILED/Exiled.API/Features/Plugin.cs b/EXILED/Exiled.API/Features/Plugin.cs index cebeab971f..d43821a952 100644 --- a/EXILED/Exiled.API/Features/Plugin.cs +++ b/EXILED/Exiled.API/Features/Plugin.cs @@ -7,14 +7,13 @@ namespace Exiled.API.Features { - using System.Linq; - #pragma warning disable SA1402 using System; using System.Collections.Generic; using System.Reflection; using CommandSystem; + using Discord; using Enums; using Extensions; using Interfaces; @@ -64,12 +63,7 @@ public Plugin() public virtual bool IgnoreRequiredVersionCheck { get; } = false; /// - public Dictionary> Commands { get; } = new() - { - { typeof(RemoteAdminCommandHandler), new Dictionary() }, - { typeof(GameConsoleCommandHandler), new Dictionary() }, - { typeof(ClientCommandHandler), new Dictionary() }, - }; + public Dictionary)> Commands { get; } = new(); /// public TConfig Config { get; } = new(); @@ -99,67 +93,23 @@ public virtual void OnEnabled() /// public virtual void OnRegisteringCommands() { - Dictionary> toRegister = new(); - foreach (Type type in Assembly.GetTypes()) { - if (type.GetInterface("ICommand") != typeof(ICommand)) + if (type.GetInterface(nameof(ICommand)) != typeof(ICommand)) continue; if (!Attribute.IsDefined(type, typeof(CommandHandlerAttribute))) continue; - foreach (CustomAttributeData customAttributeData in type.GetCustomAttributesData()) + foreach (CustomAttributeData customAttributeData in type.CustomAttributes) { try { if (customAttributeData.AttributeType != typeof(CommandHandlerAttribute)) continue; - Type commandHandlerType = (Type)customAttributeData.ConstructorArguments[0].Value; - - ICommand command = GetCommand(type) ?? (ICommand)Activator.CreateInstance(type); - - if (typeof(ParentCommand).IsAssignableFrom(commandHandlerType)) - { - ParentCommand parentCommand = GetCommand(commandHandlerType) as ParentCommand; - - if (parentCommand == null) - { - if (!toRegister.TryGetValue(commandHandlerType, out List list)) - toRegister.Add(commandHandlerType, new() { command }); - else - list.Add(command); - - continue; - } - - parentCommand.RegisterCommand(command); - continue; - } - - try - { - if (commandHandlerType == typeof(RemoteAdminCommandHandler)) - CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(command); - else if (commandHandlerType == typeof(GameConsoleCommandHandler)) - GameCore.Console.ConsoleCommandHandler.RegisterCommand(command); - else if (commandHandlerType == typeof(ClientCommandHandler)) - QueryProcessor.DotCommandHandler.RegisterCommand(command); - } - catch (ArgumentException e) - { - if (e.Message.StartsWith("An")) - { - Log.Error($"Command with same name has already registered! Command: {command.Command}"); - } - else - { - Log.Error($"An error has occurred while registering a command: {e}"); - } - } - - Commands[commandHandlerType][type] = command; + Type handlerType = (Type)customAttributeData.ConstructorArguments[0].Value; + RegisterCommand(handlerType, (ICommand)Activator.CreateInstance(type)); } catch (Exception exception) { @@ -167,60 +117,79 @@ public virtual void OnRegisteringCommands() } } } - - foreach (KeyValuePair> kvp in toRegister) - { - ParentCommand parentCommand = GetCommand(kvp.Key) as ParentCommand; - - foreach (ICommand command in kvp.Value) - parentCommand.RegisterCommand(command); - } } /// - /// Gets a command by it's type. + /// Удобно регистрирует команду с заносом в важные словарики. Не регистрируйте команды вручную, используйте это. /// - /// 's type. - /// 's type. Defines in which command handler command is registered. - /// A . May be if command is not registered. - public ICommand GetCommand(Type type, Type commandHandler = null) + /// В какой тип консоли будет зарегистрирована команда. + /// Команда для регистрации. + public void RegisterCommand(Type commandHandlerType, ICommand command) { - if (type.GetInterface("ICommand") != typeof(ICommand)) - return null; - - if (commandHandler != null) - { - if (!Commands.TryGetValue(commandHandler, out Dictionary commands)) - return null; + Type commandTypeToRegister = command.GetType(); + if (!Commands.TryGetValue(commandTypeToRegister, out (ICommand, HashSet) commandData)) + commandData = (command, new HashSet()); + if (commandData.Item2.Contains(commandHandlerType)) + return; - if (!commands.TryGetValue(type, out ICommand command)) - return null; - - return command; - } - - return Commands.Keys.Select(commandHandlerType => GetCommand(type, commandHandlerType)).FirstOrDefault(command => command != null); + RegisterCommand(commandHandlerType, commandData); } /// public virtual void OnUnregisteringCommands() { - foreach (KeyValuePair> types in Commands) + foreach ((ICommand, HashSet) command in Commands.Values) { - foreach (ICommand command in types.Value.Values) - { - if (types.Key == typeof(RemoteAdminCommandHandler)) - CommandProcessor.RemoteAdminCommandHandler.UnregisterCommand(command); - else if (types.Key == typeof(GameConsoleCommandHandler)) - GameCore.Console.ConsoleCommandHandler.UnregisterCommand(command); - else if (types.Key == typeof(ClientCommandHandler)) - QueryProcessor.DotCommandHandler.UnregisterCommand(command); - } + if (command.Item2.Contains(typeof(RemoteAdminCommandHandler))) + CommandProcessor.RemoteAdminCommandHandler.UnregisterCommand(command.Item1); + else if (command.Item2.Contains(typeof(GameConsoleCommandHandler))) + GameCore.Console.ConsoleCommandHandler.UnregisterCommand(command.Item1); + else if (command.Item2.Contains(typeof(ClientCommandHandler))) + QueryProcessor.DotCommandHandler.UnregisterCommand(command.Item1); } } /// public int CompareTo(IPlugin other) => -Priority.CompareTo(other.Priority); + + private void RegisterCommand(Type commandHandlerType, (ICommand, HashSet) commandData) + { + ICommand command = commandData.Item1; + Type commandTypeToRegister = command.GetType(); + try + { + if (commandHandlerType == typeof(RemoteAdminCommandHandler)) + { + CommandProcessor.RemoteAdminCommandHandler.RegisterCommand(command); + } + else if (commandHandlerType == typeof(GameConsoleCommandHandler)) + { + GameCore.Console.ConsoleCommandHandler.RegisterCommand(command); + } + else if (commandHandlerType == typeof(ClientCommandHandler)) + { + QueryProcessor.DotCommandHandler.RegisterCommand(command); + } + else + { + Log.Error($"Invalid command handler type provided for command {command.Command} in {Name}: {commandHandlerType}"); + return; + } + } + catch (ArgumentException e) + { + Log.Error($"An error has occurred while registering a command: {e}"); + return; + } + + commandData.Item2.Add(commandHandlerType); + Commands[commandTypeToRegister] = commandData; + if (command is global::ParentCommand) + Log.Send($"[{Name}.{nameof(RegisterCommand)}] Command '{command.Command}' uses obsolete ParentCommand class. Use {typeof(ParentCommand).FullName} instead of {typeof(global::ParentCommand).FullName}.", LogLevel.Debug, ConsoleColor.DarkGray); + + if (!commandTypeToRegister.GetProperty(nameof(ICommand.Description))?.CanWrite ?? true) + Log.Send($"[{Name}.{nameof(RegisterCommand)}] Command '{command.Command}' has description without setter, making translation impossible. Consider fixing this.", LogLevel.Debug, ConsoleColor.DarkGray); + } } /// @@ -246,4 +215,4 @@ public Plugin() /// public TTranslation Translation => (TTranslation)InternalTranslation; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/PrefabHelper.cs b/EXILED/Exiled.API/Features/PrefabHelper.cs index cb17160b50..c52d26ee55 100644 --- a/EXILED/Exiled.API/Features/PrefabHelper.cs +++ b/EXILED/Exiled.API/Features/PrefabHelper.cs @@ -95,12 +95,12 @@ public static T GetPrefab(PrefabType prefabType) /// The position where the will spawn. /// The rotation of the . /// Returns the instantied. - public static GameObject Spawn(PrefabType prefabType, Vector3 position = default, Quaternion? rotation = null) + public static GameObject Spawn(PrefabType prefabType, Vector3 position = default, Quaternion rotation = default) { if (!TryGetPrefab(prefabType, out GameObject gameObject)) return null; - GameObject newGameObject = UnityEngine.Object.Instantiate(gameObject, position, rotation ?? Quaternion.identity); + GameObject newGameObject = UnityEngine.Object.Instantiate(gameObject, position, rotation); NetworkServer.Spawn(newGameObject); return newGameObject; } @@ -113,7 +113,7 @@ public static GameObject Spawn(PrefabType prefabType, Vector3 position = default /// The rotation of the . /// The type. /// Returns the of the . - public static T Spawn(PrefabType prefabType, Vector3 position = default, Quaternion? rotation = null) + public static T Spawn(PrefabType prefabType, Vector3 position = default, Quaternion rotation = default) where T : Component { GameObject gameObject = Spawn(prefabType, position, rotation); diff --git a/EXILED/Exiled.API/Features/Respawn.cs b/EXILED/Exiled.API/Features/Respawn.cs index 0f34495520..e86d6bc3bf 100644 --- a/EXILED/Exiled.API/Features/Respawn.cs +++ b/EXILED/Exiled.API/Features/Respawn.cs @@ -114,6 +114,19 @@ public static bool ProtectedCanShoot /// public static List ProtectedTeams => SpawnProtected.ProtectedTeams; + /// + /// Gets the time untill next wave spawn. + /// + /// Returns TimeSpan.MaxValue when there are no avaible waves. + public static TimeSpan TimeUntillNextWave + { + get + { + Waves.WaveTimer timer = Waves.WaveTimer.GetWaveTimers().Where(wave => !wave.IsPaused).OrderBy(wave => wave.TimeLeft.TotalSeconds).FirstOrDefault(); + return timer == null ? TimeSpan.MaxValue : timer.TimeLeft; + } + } + /// /// Tries to get a . /// @@ -209,24 +222,26 @@ public static void PlayEffect(SpawnableWaveBase wave) } /// - /// Summons the NTF chopper. + /// Play the spawn effect of a target . /// - public static void SummonNtfChopper() + /// The whose effect should be played. + public static void PlayEffect(SpawnableFaction spawnableFaction) { - if (TryGetWaveBase(SpawnableFaction.NtfWave, out SpawnableWaveBase wave)) + if (TryGetWaveBase(spawnableFaction, out SpawnableWaveBase wave)) PlayEffect(wave); } + /// + /// Summons the NTF chopper. + /// + public static void SummonNtfChopper() => PlayEffect(SpawnableFaction.NtfWave); + /// /// Summons the Chaos Insurgency van. /// /// This will also trigger Music effect. /// - public static void SummonChaosInsurgencyVan() - { - if (TryGetWaveBase(SpawnableFaction.ChaosWave, out SpawnableWaveBase wave)) - PlayEffect(wave); - } + public static void SummonChaosInsurgencyVan() => PlayEffect(SpawnableFaction.ChaosWave); /// /// Grants tokens to a given 's s. @@ -291,6 +306,21 @@ public static bool ModifyTokens(Faction faction, int amount) return false; } + /// + /// Gets the tokens of a given 's . + /// + /// The from whose to get the tokens. + /// Target tokens of a given 's . + public static int GetTokens(SpawnableFaction spawnableFaction) + { + if (TryGetWaveBase(spawnableFaction, out SpawnableWaveBase waveBase) && waveBase is ILimitedWave limitedWave) + { + return limitedWave.RespawnTokens; + } + + return 0; + } + /// /// Tries to get the tokens of a given 's . /// diff --git a/EXILED/Exiled.API/Features/Roles/FpcRole.cs b/EXILED/Exiled.API/Features/Roles/FpcRole.cs index 2b0d75cf00..dae9bdb1af 100644 --- a/EXILED/Exiled.API/Features/Roles/FpcRole.cs +++ b/EXILED/Exiled.API/Features/Roles/FpcRole.cs @@ -10,7 +10,12 @@ namespace Exiled.API.Features.Roles using System; using System.Collections.Generic; + using Exiled.API.Extensions; using Exiled.API.Features.Pools; + using HarmonyLib; + + using Mirror; + using PlayerRoles; using PlayerRoles.FirstPersonControl; using PlayerRoles.FirstPersonControl.Thirdperson; @@ -42,7 +47,10 @@ protected FpcRole(FpcStandardRoleBase baseRole) /// /// Finalizes an instance of the class. /// - ~FpcRole() => HashSetPool.Pool.Return(IsInvisibleFor); + ~FpcRole() + { + HashSetPool.Pool.Return(IsInvisibleFor); + } /// /// Gets the . @@ -324,5 +332,23 @@ public void Jump(float? jumpStrength = null) float strength = jumpStrength ?? FirstPersonController.FpcModule.JumpSpeed; FirstPersonController.FpcModule.Motor.JumpController.ForceJump(strength); } + + /// + internal override bool CheckAppearanceCompatibility(RoleTypeId newAppearance) + { + if (!RoleExtensions.TryGetRoleBase(newAppearance, out PlayerRoleBase roleBase)) + return false; + + return roleBase is FpcStandardRoleBase; + } + + /// + internal override void SendAppearanceSpawnMessage(NetworkWriter writer, PlayerRoleBase basicRole) + { + FpcStandardRoleBase fpcRole = (FpcStandardRoleBase)basicRole; + fpcRole.FpcModule.MouseLook.GetSyncValues(0, out ushort syncH, out ushort _); + writer.WriteRelativePosition(new RelativePosition(fpcRole._hubTransform.position)); + writer.WriteUShort(syncH); + } } } diff --git a/EXILED/Exiled.API/Features/Roles/HumanRole.cs b/EXILED/Exiled.API/Features/Roles/HumanRole.cs index 767a010d45..a1ee23aeee 100644 --- a/EXILED/Exiled.API/Features/Roles/HumanRole.cs +++ b/EXILED/Exiled.API/Features/Roles/HumanRole.cs @@ -7,6 +7,8 @@ namespace Exiled.API.Features.Roles { + using Mirror; + using PlayerRoles; using PlayerRoles.PlayableScps.HumeShield; using Respawning; @@ -66,5 +68,14 @@ public byte UnitNameId /// The . /// The armor efficacy. public int GetArmorEfficacy(HitboxType hitbox) => Base.GetArmorEfficacy(hitbox); + + /// + internal override void SendAppearanceSpawnMessage(NetworkWriter writer, PlayerRoleBase basicRole) + { + if (UsesUnitNames) + writer.WriteByte(basicRole is HumanGameRole humanRole && humanRole.UsesUnitNames ? humanRole.UnitNameId : (byte)0); + + base.SendAppearanceSpawnMessage(writer, basicRole); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Roles/Role.cs b/EXILED/Exiled.API/Features/Roles/Role.cs index 31f53ac040..35ffbbc5ae 100644 --- a/EXILED/Exiled.API/Features/Roles/Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Role.cs @@ -8,15 +8,22 @@ namespace Exiled.API.Features.Roles { using System; + using System.Collections.Generic; - using Enums; + using Exiled.API.Enums; + using Exiled.API.Extensions; using Exiled.API.Features.Core; + using Exiled.API.Features.Pools; using Exiled.API.Features.Spawn; using Exiled.API.Interfaces; - using Extensions; + using Mirror; + using PlayerRoles; + using PlayerRoles.FirstPersonControl; using PlayerRoles.PlayableScps.Scp049.Zombies; + using UnityEngine; + using YamlDotNet.Core.Tokens; using DestroyedGameRole = PlayerRoles.DestroyedRole; using FilmmakerGameRole = PlayerRoles.Filmmaker.FilmmakerRole; @@ -38,6 +45,11 @@ namespace Exiled.API.Features.Roles /// public abstract class Role : TypeCastObject, IWrapper { + private RoleTypeId fakeAppearance; + private Dictionary individualAppearances = DictionaryPool.Pool.Get(); + private Dictionary teamAppearances = DictionaryPool.Pool.Get(); + private Dictionary roleAppearances = DictionaryPool.Pool.Get(); + /// /// Initializes a new instance of the class. /// @@ -48,6 +60,16 @@ protected Role(PlayerRoleBase baseRole) Owner = Player.Get(hub); Base = baseRole; + fakeAppearance = baseRole.RoleTypeId; + } + + /// + /// Finalizes an instance of the class. + /// + ~Role() + { + DictionaryPool.Pool.Return(individualAppearances); + DictionaryPool.Pool.Return(teamAppearances); } /// @@ -120,6 +142,26 @@ protected Role(PlayerRoleBase baseRole) /// public int LifeIdentifier => Base.UniqueLifeIdentifier; + /// + /// Gets an overriden global appearance. + /// + public RoleTypeId GlobalAppearance => fakeAppearance; + + /// + /// Gets an overriden appearance for specific 's. + /// + public IReadOnlyDictionary TeamAppearances => teamAppearances; + + /// + /// Gets an overriden appearance for specific 's. + /// + public IReadOnlyDictionary RoleAppearances => roleAppearances; + + /// + /// Gets an overriden appearance for specific 's. + /// + public IReadOnlyDictionary IndividualAppearances => individualAppearances; + /// /// Gets a random spawn position of this role. /// @@ -215,6 +257,204 @@ protected Role(PlayerRoleBase baseRole) public virtual void Set(RoleTypeId newRole, SpawnReason reason, RoleSpawnFlags spawnFlags) => Owner.RoleManager.ServerSetRole(newRole, (RoleChangeReason)reason, spawnFlags); + /// + /// Try-set a new global appearance for current . + /// + /// New global appearance. + /// Whether or not the change-role requect should sent imidiately. + /// A boolean indicating whether or not a target will be used as new appearance. + public bool TrySetGlobalAppearance(RoleTypeId newAppearance, bool update = true) + { + if (!CheckAppearanceCompatibility(newAppearance)) + { + Log.Error($"Prevent Seld-Desync of {Owner.Nickname} ({Type}) with {newAppearance}"); + return false; + } + + fakeAppearance = newAppearance; + + if (update) + { + UpdateAppearance(); + } + + return true; + } + + /// + /// Try-set a new team appearance for current . + /// + /// Target . + /// New team specific appearance. + /// Whether or not the change-role requect should sent imidiately. + /// A boolean indicating whether or not a target will be used as new appearance. + public bool TrySetTeamAppearance(Team team, RoleTypeId newAppearance, bool update = true) + { + if (!CheckAppearanceCompatibility(newAppearance)) + { + Log.Error($"Prevent Seld-Desync of {Owner.Nickname} ({Type}) with {newAppearance}"); + return false; + } + + teamAppearances[team] = newAppearance; + + if (update) + { + UpdateAppearance(); + } + + return true; + } + + /// + /// Try-set a new team appearance for current . + /// + /// Target . + /// New team specific appearance. + /// Whether or not the change-role requect should sent imidiately. + /// A boolean indicating whether or not a target will be used as new appearance. + public bool TrySetRoleAppearance(string role, RoleTypeId newAppearance, bool update = true) + { + if (!CheckAppearanceCompatibility(newAppearance)) + { + Log.Error($"Prevent Seld-Desync of {Owner.Nickname} ({Type}) with {newAppearance}"); + return false; + } + + roleAppearances[role] = newAppearance; + + if (update) + { + UpdateAppearance(); + } + + return true; + } + + /// + /// Try-set a new individual appearance for current . + /// + /// Target . + /// New individual appearance. + /// Whether or not the change-role requect should sent imidiately. + /// A boolean indicating whether or not a target will be used as new appearance. + public bool TrySetIndividualAppearance(Player player, RoleTypeId newAppearance, bool update = true) + { + if (!CheckAppearanceCompatibility(newAppearance)) + { + Log.Error($"Prevent Seld-Desync of {Owner.Nickname} ({Type}) with {newAppearance}"); + return false; + } + + individualAppearances[player] = newAppearance; + + if (update) + { + UpdateAppearanceFor(player); + } + + return true; + } + + /// + /// resets to current . + /// + /// Whether or not the change-role requect should sent imidiately. + public void ClearGlobalAppearance(bool update = true) + { + fakeAppearance = Type; + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Clears all custom . + /// + /// Whether or not the change-role requect should sent imidiately. + public void ClearTeamAppearances(bool update = true) + { + teamAppearances.Clear(); + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Clears all custom . + /// + /// Whether or not the change-role requect should sent imidiately. + public void ClearRoleAppearances(bool update = true) + { + roleAppearances.Clear(); + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Clears all custom . + /// + /// Whether or not the change-role requect should sent imidiately. + public void ClearIndividualAppearances(bool update = true) + { + individualAppearances.Clear(); + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Resets current appearance to a real player . + /// + /// Whether or not the change-role requect should sent imidiately. + /// Clears , and . + public void ResetAppearance(bool update = true) + { + ClearGlobalAppearance(false); + ClearTeamAppearances(false); + ClearIndividualAppearances(false); + ClearRoleAppearances(false); + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Updates current player appearance. + /// + public void UpdateAppearance() + { + if (Owner != null) + Owner.RoleManager.SendNewRoleInfo(); + } + + /// + /// Updates current player visibility, for target . + /// + /// Target . + public void UpdateAppearanceFor(Player player) + { + RoleTypeId roleTypeId = Type; + if (Base is IObfuscatedRole obfuscatedRole) + { + roleTypeId = obfuscatedRole.GetRoleForUser(player.ReferenceHub); + } + + player.Connection.Send(new RoleSyncInfo(Owner.ReferenceHub, roleTypeId, player.ReferenceHub, null)); + Owner.RoleManager.PreviouslySentRole[player.NetId] = roleTypeId; + } + /// /// Creates a role from and . /// @@ -236,10 +476,32 @@ public virtual void Set(RoleTypeId newRole, SpawnReason reason, RoleSpawnFlags s FilmmakerGameRole filmmakerRole => new FilmMakerRole(filmmakerRole), NoneGameRole noneRole => new NoneRole(noneRole), DestroyedGameRole destroyedRole => new DestroyedRole(destroyedRole), -#pragma warning disable CS0618 - Scp1507GameRole scp1507 => new Scp1507Role(scp1507), -#pragma warning restore CS0618 + + // Scp1507GameRole scp1507 => new Scp1507Role(scp1507), _ => throw new Exception($"Missing role found in Exiled.API.Features.Roles.Role::Create ({role?.RoleTypeId}). Please contact an Exiled developer."), }; + + /// + /// Overrides change role sever message, to implement fake appearance, using basic . + /// + /// to write message. + /// Original (not fake) . + /// Not for public usage. Called on fake class, not on real class. + internal virtual void SendAppearanceSpawnMessage(NetworkWriter writer, PlayerRoleBase basicRole) + { + } + + /// + /// Checks compatibility for target appearance using . + /// + /// New . + /// A boolean indicating whether or not a target can be used as new appearance. + internal virtual bool CheckAppearanceCompatibility(RoleTypeId newAppearance) + { + if (!RoleExtensions.TryGetRoleBase(newAppearance, out PlayerRoleBase roleBase)) + return false; + + return roleBase is not(FpcStandardRoleBase or NoneGameRole); + } } } diff --git a/EXILED/Exiled.API/Features/Roles/Scp0492Role.cs b/EXILED/Exiled.API/Features/Roles/Scp0492Role.cs index 7250aa4662..9a1cd0d55c 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp0492Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp0492Role.cs @@ -7,6 +7,8 @@ namespace Exiled.API.Features.Roles { + using Mirror; + using PlayerRoles; using PlayerRoles.PlayableScps.HumeShield; using PlayerRoles.PlayableScps.Scp049; @@ -127,5 +129,23 @@ public float SimulatedStare /// The ragdoll to check. /// if close enough to consume the body; otherwise, . public bool IsInConsumeRange(Ragdoll ragdoll) => ragdoll is not null && IsInConsumeRange(ragdoll.Base); + + /// + internal override void SendAppearanceSpawnMessage(NetworkWriter writer, PlayerRoleBase basicRole) + { + if (basicRole is ZombieRole basicZombieRole) + { + writer.WriteUShort(basicZombieRole._syncMaxHealth); + writer.WriteBool(basicZombieRole._showConfirmationBox); + } + else + { + // Doesn't really affect anything + writer.WriteUShort(400); + writer.WriteBool(false); + } + + base.SendAppearanceSpawnMessage(writer, basicRole); + } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Roles/Scp079Role.cs b/EXILED/Exiled.API/Features/Roles/Scp079Role.cs index 9d7e308659..99bd7cccb1 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp079Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp079Role.cs @@ -280,7 +280,7 @@ public int Experience public int Level { get => TierManager.AccessTierLevel; - set => Experience = value <= 1 ? 0 : TierManager.AbsoluteThresholds[Mathf.Clamp(value - 2, 0, TierManager.AbsoluteThresholds.Length - 1)]; + set => Experience = value <= 1 ? 0 : TierManager.AbsoluteThresholds[Mathf.Clamp(value - 1, 0, TierManager.AbsoluteThresholds.Length - 1)]; } /// diff --git a/EXILED/Exiled.API/Features/Roles/Scp096Role.cs b/EXILED/Exiled.API/Features/Roles/Scp096Role.cs index a59e699ac9..7399de8ee7 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp096Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp096Role.cs @@ -306,4 +306,4 @@ public void Charge(float cooldown = 1f) /// The Spawn Chance. public float GetSpawnChance(List alreadySpawned) => Base.GetSpawnChance(alreadySpawned); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Roles/Scp106Role.cs b/EXILED/Exiled.API/Features/Roles/Scp106Role.cs index 8c5fda149a..92fb1c612a 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp106Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp106Role.cs @@ -10,6 +10,9 @@ namespace Exiled.API.Features.Roles using System.Collections.Generic; using Exiled.API.Enums; + + using MEC; + using PlayerRoles; using PlayerRoles.PlayableScps; using PlayerRoles.PlayableScps.HumeShield; @@ -255,6 +258,12 @@ public bool UsePortal(Vector3 position, float cost = 0f) HuntersAtlasAbility._estimatedCost = cost; HuntersAtlasAbility._syncSubmerged = true; + Timing.CallDelayed(2f, () => + { + if (IsValid) + Owner.Position = position; + }); + return true; } @@ -289,4 +298,4 @@ public bool CapturePlayer(Player player) /// The Spawn Chance. public float GetSpawnChance(List alreadySpawned) => Base.GetSpawnChance(alreadySpawned); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs b/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs index a4c9ba78cf..952000bc46 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs @@ -10,10 +10,18 @@ namespace Exiled.API.Features.Roles using System; using System.Collections.Generic; + using Exiled.API.Enums; + using Exiled.API.Features.Pools; + using Mirror; using PlayerRoles; using PlayerRoles.PlayableScps; using PlayerRoles.PlayableScps.HumeShield; + using PlayerRoles.PlayableScps.Scp939; + using PlayerRoles.PlayableScps.Scp939.Mimicry; + using PlayerRoles.PlayableScps.Scp939.Ripples; using PlayerRoles.Subroutines; + using RelativePositioning; + using UnityEngine; using Scp1507GameRole = PlayerRoles.PlayableScps.Scp1507.Scp1507Role; @@ -65,5 +73,21 @@ internal Scp1507Role(Scp1507GameRole baseRole) /// The List of Roles already spawned. /// The Spawn Chance. public float GetSpawnChance(List alreadySpawned) => Base is ISpawnableScp spawnableScp ? spawnableScp.GetSpawnChance(alreadySpawned) : 0; + + /// + internal override void SendAppearanceSpawnMessage(NetworkWriter writer, PlayerRoleBase basicRole) + { + base.SendAppearanceSpawnMessage(writer, basicRole); + + if (basicRole is Scp1507GameRole baseRole) + { + writer.WriteByte((byte)baseRole.ServerSpawnReason); + } + else + { + // Doesn't really affect anything + writer.WriteByte((byte)RoleChangeReason.ItemUsage); + } + } } } diff --git a/EXILED/Exiled.API/Features/Roles/Scp173Role.cs b/EXILED/Exiled.API/Features/Roles/Scp173Role.cs index f7f603eece..1e601efdcf 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp173Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp173Role.cs @@ -275,4 +275,4 @@ public TantrumHazard PlaceTantrum(bool failIfObserved = false, float cooldown = /// The Spawn Chance. public float GetSpawnChance(List alreadySpawned) => Base.GetSpawnChance(alreadySpawned); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Roles/SpectatorRole.cs b/EXILED/Exiled.API/Features/Roles/SpectatorRole.cs index d7fbcc7920..3e5edfe156 100644 --- a/EXILED/Exiled.API/Features/Roles/SpectatorRole.cs +++ b/EXILED/Exiled.API/Features/Roles/SpectatorRole.cs @@ -8,6 +8,11 @@ namespace Exiled.API.Features.Roles { using System; + using System.Collections.Generic; + + using Exiled.API.Extensions; + using Exiled.API.Features.Pools; + using Mirror; using PlayerRoles; using PlayerRoles.Voice; diff --git a/EXILED/Exiled.API/Features/Round.cs b/EXILED/Exiled.API/Features/Round.cs index 38bb4407ed..9e6faae199 100644 --- a/EXILED/Exiled.API/Features/Round.cs +++ b/EXILED/Exiled.API/Features/Round.cs @@ -59,7 +59,7 @@ public static class Round /// /// Gets a value indicating whether the round is lobby. /// - public static bool IsLobby => !(IsEnded || IsStarted); + public static bool IsLobby => ReferenceHub.TryGetLocalHub(out ReferenceHub localHub) && !(IsEnded || IsStarted); /// /// Gets a value indicating the last ClassList. diff --git a/EXILED/Exiled.API/Features/Server.cs b/EXILED/Exiled.API/Features/Server.cs index 96b8311fdf..5717a9fc86 100644 --- a/EXILED/Exiled.API/Features/Server.cs +++ b/EXILED/Exiled.API/Features/Server.cs @@ -11,8 +11,6 @@ namespace Exiled.API.Features using System.Collections.Generic; using System.Reflection; - using Exiled.API.Enums; - using GameCore; using Interfaces; diff --git a/EXILED/Exiled.API/Features/Spawn/DynamicSpawnPoint.cs b/EXILED/Exiled.API/Features/Spawn/DynamicSpawnPoint.cs index 3a6c7146ba..97b3914016 100644 --- a/EXILED/Exiled.API/Features/Spawn/DynamicSpawnPoint.cs +++ b/EXILED/Exiled.API/Features/Spawn/DynamicSpawnPoint.cs @@ -46,4 +46,4 @@ public override Vector3 Position set => throw new InvalidOperationException("The spawn vector of a dynamic spawn location cannot be changed."); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Spawn/RoomSpawnPoint.cs b/EXILED/Exiled.API/Features/Spawn/RoomSpawnPoint.cs index 97041a2c92..9f1d99c83b 100644 --- a/EXILED/Exiled.API/Features/Spawn/RoomSpawnPoint.cs +++ b/EXILED/Exiled.API/Features/Spawn/RoomSpawnPoint.cs @@ -48,9 +48,6 @@ public override Vector3 Position { Room roomInstance = Features.Room.Get(Room) ?? throw new InvalidOperationException("The room instance could not be found."); - if (roomInstance.Type == RoomType.Surface) - return Offset != Vector3.zero ? Offset : roomInstance.Position; - return Offset != Vector3.zero ? roomInstance.transform.TransformPoint(Offset) : roomInstance.Position; } set => throw new InvalidOperationException("The position of this type of SpawnPoint cannot be changed."); diff --git a/EXILED/Exiled.API/Features/Toys/AdminToy.cs b/EXILED/Exiled.API/Features/Toys/AdminToy.cs index c93cee110e..4a82030c8c 100644 --- a/EXILED/Exiled.API/Features/Toys/AdminToy.cs +++ b/EXILED/Exiled.API/Features/Toys/AdminToy.cs @@ -8,12 +8,14 @@ namespace Exiled.API.Features.Toys { using System.Collections.Generic; + using System.Linq; using AdminToys; using Enums; using Exiled.API.Interfaces; using Footprinting; + using InventorySystem.Items; using Mirror; using UnityEngine; @@ -168,8 +170,7 @@ public static AdminToy Get(AdminToyBase adminToyBase) Scp079CameraToy scp079CameraToy => new CameraToy(scp079CameraToy), InvisibleInteractableToy invisibleInteractableToy => new InteractableToy(invisibleInteractableToy), TextToy textToy => new Text(textToy), - WaypointToy waypointToy => new Waypoint(waypointToy), - _ => throw new System.NotImplementedException(), + _ => throw new System.NotImplementedException() }; } diff --git a/EXILED/Exiled.API/Features/Toys/Light.cs b/EXILED/Exiled.API/Features/Toys/Light.cs index 9e167d1bb5..e08a930537 100644 --- a/EXILED/Exiled.API/Features/Toys/Light.cs +++ b/EXILED/Exiled.API/Features/Toys/Light.cs @@ -172,4 +172,4 @@ public static Light Get(LightSourceToy lightSourceToy) return adminToy is not null ? adminToy as Light : new(lightSourceToy); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.API/Features/Toys/Speaker.cs b/EXILED/Exiled.API/Features/Toys/Speaker.cs index 8b0af6bd79..a7b177e6b4 100644 --- a/EXILED/Exiled.API/Features/Toys/Speaker.cs +++ b/EXILED/Exiled.API/Features/Toys/Speaker.cs @@ -43,7 +43,7 @@ internal Speaker(SpeakerToy speakerToy) /// /// /// A representing the volume level of the audio source, - /// where 0.0 is silent and 1.0 is full volume if it's more it's will amplify it. + /// where 0.0 is silent and 1.0 is full volume. /// public float Volume { diff --git a/EXILED/Exiled.API/Features/Workstation.cs b/EXILED/Exiled.API/Features/Workstation.cs index 7e7978a7ff..40de899812 100644 --- a/EXILED/Exiled.API/Features/Workstation.cs +++ b/EXILED/Exiled.API/Features/Workstation.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) ExMod Team. All rights reserved. // Licensed under the CC BY-SA 3.0 license. diff --git a/EXILED/Exiled.API/Interfaces/IAbstractResolvable.cs b/EXILED/Exiled.API/Interfaces/IAbstractResolvable.cs new file mode 100644 index 0000000000..73b6e36393 --- /dev/null +++ b/EXILED/Exiled.API/Interfaces/IAbstractResolvable.cs @@ -0,0 +1,16 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Interfaces +{ + /// + /// An interface for abstract classes what can be used in configs. + /// + public interface IAbstractResolvable + { + } +} diff --git a/EXILED/Exiled.API/Interfaces/IPermission.cs b/EXILED/Exiled.API/Interfaces/IPermission.cs new file mode 100644 index 0000000000..e0f5f52c4d --- /dev/null +++ b/EXILED/Exiled.API/Interfaces/IPermission.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Interfaces +{ + using Enums; + + /// + /// Represents interface of all objects with required permissions. + /// + public interface IPermission + { + /// + /// Gets or sets a value indicating whether gets or sets required permissions for object. + /// + public abstract KeycardPermissions Permissions { get; set; } + + /// + /// Gets or sets a value indicating whether IPermission object is opened or closed. + /// + public bool IsOpen { get; set; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.API/Interfaces/IPlugin.cs b/EXILED/Exiled.API/Interfaces/IPlugin.cs index ef44c71524..9ceadb97e4 100644 --- a/EXILED/Exiled.API/Interfaces/IPlugin.cs +++ b/EXILED/Exiled.API/Interfaces/IPlugin.cs @@ -46,8 +46,9 @@ public interface IPlugin : IComparable> /// /// Gets the plugin commands. + /// TypeOfCommand, (CommandInstance, ListOfHandlers). /// - Dictionary> Commands { get; } + Dictionary)> Commands { get; } /// /// Gets the plugin priority. diff --git a/EXILED/Exiled.API/Structs/PrimitiveSettings.cs b/EXILED/Exiled.API/Structs/PrimitiveSettings.cs index 24fcc04393..4a1682823c 100644 --- a/EXILED/Exiled.API/Structs/PrimitiveSettings.cs +++ b/EXILED/Exiled.API/Structs/PrimitiveSettings.cs @@ -120,4 +120,4 @@ public PrimitiveSettings(PrimitiveType primitiveType, PrimitiveFlags primitiveFl /// public bool Spawn { get; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CreditTags/Commands/ShowCreditTag.cs b/EXILED/Exiled.CreditTags/Commands/ShowCreditTag.cs index fa2e8305ef..e370336526 100644 --- a/EXILED/Exiled.CreditTags/Commands/ShowCreditTag.cs +++ b/EXILED/Exiled.CreditTags/Commands/ShowCreditTag.cs @@ -26,7 +26,7 @@ public class ShowCreditTag : ICommand public string[] Aliases { get; } = new[] { "crtag", "et", "ct" }; /// - public string Description { get; } = "Shows your EXILED Credits tag, if available."; + public string Description { get; set; } = "Shows your EXILED Credits tag, if available."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.CreditTags/Exiled.CreditTags.csproj b/EXILED/Exiled.CreditTags/Exiled.CreditTags.csproj index a169d69155..4358cf34fb 100644 --- a/EXILED/Exiled.CreditTags/Exiled.CreditTags.csproj +++ b/EXILED/Exiled.CreditTags/Exiled.CreditTags.csproj @@ -38,7 +38,9 @@ - if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/"; fi diff --git a/EXILED/Exiled.CreditTags/Features/DatabaseHandler.cs b/EXILED/Exiled.CreditTags/Features/DatabaseHandler.cs index ac3cc91781..c2ec515742 100644 --- a/EXILED/Exiled.CreditTags/Features/DatabaseHandler.cs +++ b/EXILED/Exiled.CreditTags/Features/DatabaseHandler.cs @@ -17,7 +17,7 @@ namespace Exiled.CreditTags.Features public static class DatabaseHandler { - private const string Url = "https://raw.githubusercontent.com/Exmod-Team/CreditTags/main/data.yml"; + private const string Url = "https://raw.githubusercontent.com/ExSLMod-Team/CreditTags/main/data.yml"; private const string ETagCacheFileName = "etag_cache.txt"; private const string DatabaseCacheFileName = "data.yml"; private const int CacheTimeInMinutes = 5; diff --git a/EXILED/Exiled.CustomItems/API/Extensions.cs b/EXILED/Exiled.CustomItems/API/Extensions.cs index b527a2191a..5187b09b0a 100644 --- a/EXILED/Exiled.CustomItems/API/Extensions.cs +++ b/EXILED/Exiled.CustomItems/API/Extensions.cs @@ -55,7 +55,7 @@ public static void ResetInventory(this Player player, IEnumerable newIte public static void Register(this IEnumerable customItems) { if (customItems is null) - throw new ArgumentNullException("customItems"); + throw new ArgumentNullException(nameof(customItems)); foreach (CustomItem customItem in customItems) customItem.TryRegister(); diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomArmor.cs b/EXILED/Exiled.CustomItems/API/Features/CustomArmor.cs index e6f1932905..38067a7a0d 100644 --- a/EXILED/Exiled.CustomItems/API/Features/CustomArmor.cs +++ b/EXILED/Exiled.CustomItems/API/Features/CustomArmor.cs @@ -58,73 +58,18 @@ public override ItemType Type [Description("The value must be above 0 and below 100")] public virtual int VestEfficacy { get; set; } = 80; - /// - /// Gets or sets the Ammunition limit the player have. - /// - public virtual List AmmoLimits { get; set; } = new(); - - /// - /// Gets or sets the Item Category limit the player have. - /// - public virtual List CategoryLimits { get; set; } = new(); - - /// - public override void Give(Player player, bool displayMessage = true) + /// + public override Item CreateItem() { - Armor armor = (Armor)Item.Create(Type); + Armor armor = (Armor)base.CreateItem(); - armor.Weight = Weight; + armor.Weight = Weight < 0 ? armor.Weight : Weight; armor.StaminaUseMultiplier = StaminaUseMultiplier; armor.VestEfficacy = VestEfficacy; armor.HelmetEfficacy = HelmetEfficacy; - if (AmmoLimits.Count != 0) - armor.AmmoLimits = AmmoLimits; - - if (AmmoLimits.Count != 0) - armor.CategoryLimits = CategoryLimits; - - player.AddItem(armor); - - TrackedSerials.Add(armor.Serial); - - Timing.CallDelayed(0.05f, () => OnAcquired(player, armor, displayMessage)); - - if (displayMessage) - ShowPickedUpMessage(player); - } - - /// - protected override void SubscribeEvents() - { - Exiled.Events.Handlers.Player.PickingUpItem += OnInternalPickingUpItem; - base.SubscribeEvents(); - } - - /// - protected override void UnsubscribeEvents() - { - Exiled.Events.Handlers.Player.PickingUpItem -= OnInternalPickingUpItem; - base.UnsubscribeEvents(); - } - - private void OnInternalPickingUpItem(PickingUpItemEventArgs ev) - { - if (!Check(ev.Pickup) || ev.Player.Items.Count >= 8 || ev.Pickup is Exiled.API.Features.Pickups.BodyArmorPickup) - return; - - OnPickingUp(ev); - - if (!ev.IsAllowed) - return; - - ev.IsAllowed = false; - - TrackedSerials.Remove(ev.Pickup.Serial); - ev.Pickup.Destroy(); - - Give(ev.Player); + return armor; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomGrenade.cs b/EXILED/Exiled.CustomItems/API/Features/CustomGrenade.cs index 48d725ce7e..f943573e70 100644 --- a/EXILED/Exiled.CustomItems/API/Features/CustomGrenade.cs +++ b/EXILED/Exiled.CustomItems/API/Features/CustomGrenade.cs @@ -4,7 +4,6 @@ // Licensed under the CC BY-SA 3.0 license. // // ----------------------------------------------------------------------- - namespace Exiled.CustomItems.API.Features { using System; @@ -17,7 +16,6 @@ namespace Exiled.CustomItems.API.Features using Exiled.API.Features.Roles; using Exiled.Events.EventArgs.Map; using Exiled.Events.EventArgs.Player; - using Footprinting; using InventorySystem.Items; using InventorySystem.Items.Pickups; @@ -25,16 +23,16 @@ namespace Exiled.CustomItems.API.Features using Mirror; using UnityEngine; + using Map = Exiled.Events.Handlers.Map; using Object = UnityEngine.Object; - using Server = Exiled.API.Features.Server; /// - /// The Custom Grenade base class. + /// The Custom Grenade base class. /// public abstract class CustomGrenade : CustomItem { /// - /// Gets or sets the to use for this item. + /// Gets or sets the to use for this item. /// public override ItemType Type { @@ -49,114 +47,105 @@ public override ItemType Type } /// - /// Gets or sets a value indicating whether gets or sets a value that determines if the grenade should explode immediately when contacting any surface. + /// Gets or sets a value indicating whether or not the grenade should explode immediately when contacting any surface. /// public abstract bool ExplodeOnCollision { get; set; } /// - /// Gets or sets a value indicating how long the grenade's fuse time should be. + /// Gets or sets a value indicating how long grenate will not detonate when contacting any surface is you use + /// . /// - public abstract float FuseTime { get; set; } + public virtual float ExplodeOnCollisionFuseTime { get; set; } = 0.15f; /// - /// Throw the CustomGrenade object. + /// Gets or sets a value indicating how long the grenade's fuse time should be. /// - /// The position to throw at. - /// The amount of force to throw with. - /// The Weight of the Grenade. - /// The FuseTime of the grenade. - /// The of the grenade to spawn. - /// The to count as the thrower of the grenade. - /// The spawned. - public virtual Pickup Throw(Vector3 position, float force, float weight, float fuseTime = 3f, ItemType grenadeType = ItemType.GrenadeHE, Player? player = null) - { - if (player is null) - player = Server.Host; + public abstract float FuseTime { get; set; } - player.Role.Is(out FpcRole fpcRole); - Vector3 velocity = fpcRole.FirstPersonController.FpcModule.Motor.Velocity; + /// + public override Item CreateItem() + { + Item item = base.CreateItem(); - Throwable throwable = (Throwable)Item.Create(grenadeType, player); + if (item is Throwable throwable) + { + throwable.FuseTime = FuseTime; + } - ThrownProjectile thrownProjectile = Object.Instantiate(throwable.Base.Projectile, position, throwable.Owner.CameraTransform.rotation); + return item; + } - PickupSyncInfo newInfo = new() - { - ItemId = throwable.Type, - Locked = !throwable.Base._repickupable, - Serial = ItemSerialGenerator.GenerateNext(), - WeightKg = weight, - }; - if (thrownProjectile is TimeGrenade time) - time._fuseTime = fuseTime; - thrownProjectile.NetworkInfo = newInfo; - thrownProjectile.PreviousOwner = new Footprint(throwable.Owner.ReferenceHub); - NetworkServer.Spawn(thrownProjectile.gameObject); - thrownProjectile.InfoReceivedHook(default, newInfo); - if (thrownProjectile.TryGetComponent(out Rigidbody component)) - throwable.Base.PropelBody(component, throwable.Base.FullThrowSettings.StartTorque, ThrowableNetworkHandler.GetLimitedVelocity(velocity)); - thrownProjectile.ServerActivate(); - - return Pickup.Get(thrownProjectile); + /// + /// Spawns activated object. + /// + /// The position to spawn at. + /// The to count as the thrower of the grenade. + /// The spawned. + public virtual Projectile Throw(Vector3 position, Player? player = null) + { + Projectile projectile = ((Throwable)CreateItem()).CreateProjectile(position); + projectile.PreviousOwner = player; + projectile.Activate(); + return projectile; } /// - /// Checks to see if the grenade is a custom grenade. + /// Checks to see if the grenade is a custom grenade. /// /// The grenade to check. /// True if it is a custom grenade. - public virtual bool Check(Projectile grenade) => grenade is not null && TrackedSerials.Contains(grenade.Serial); + public virtual bool Check(Projectile grenade) => grenade != null && TrackedSerials.Contains(grenade.Serial); - /// + /// protected override void SubscribeEvents() { Exiled.Events.Handlers.Player.ThrowingRequest += OnInternalThrowingRequest; Exiled.Events.Handlers.Player.ThrownProjectile += OnInternalThrownProjectile; - Exiled.Events.Handlers.Map.ExplodingGrenade += OnInternalExplodingGrenade; - Exiled.Events.Handlers.Map.ChangedIntoGrenade += OnInternalChangedIntoGrenade; + Map.ExplodingGrenade += OnInternalExplodingGrenade; + Map.ChangedIntoGrenade += OnInternalChangedIntoGrenade; base.SubscribeEvents(); } - /// + /// protected override void UnsubscribeEvents() { Exiled.Events.Handlers.Player.ThrowingRequest -= OnInternalThrowingRequest; Exiled.Events.Handlers.Player.ThrownProjectile -= OnInternalThrownProjectile; - Exiled.Events.Handlers.Map.ExplodingGrenade -= OnInternalExplodingGrenade; - Exiled.Events.Handlers.Map.ChangedIntoGrenade -= OnInternalChangedIntoGrenade; + Map.ExplodingGrenade -= OnInternalExplodingGrenade; + Map.ChangedIntoGrenade -= OnInternalChangedIntoGrenade; base.UnsubscribeEvents(); } /// - /// Handles tracking thrown requests by custom grenades. + /// Handles tracking thrown requests by custom grenades. /// - /// . + /// . protected virtual void OnThrowingRequest(ThrowingRequestEventArgs ev) { } /// - /// Handles tracking thrown custom grenades. + /// Handles tracking thrown custom grenades. /// - /// . + /// . protected virtual void OnThrownProjectile(ThrownProjectileEventArgs ev) { } /// - /// Handles tracking exploded custom grenades. + /// Handles tracking exploded custom grenades. /// - /// . + /// . protected virtual void OnExploding(ExplodingGrenadeEventArgs ev) { } /// - /// Handles the tracking of custom grenade pickups that are changed into live grenades by a frag grenade explosion. + /// Handles the tracking of custom grenade pickups that are changed into live grenades by a frag grenade explosion. /// - /// . + /// . protected virtual void OnChangedIntoGrenade(ChangedIntoGrenadeEventArgs ev) { } @@ -166,10 +155,9 @@ private void OnInternalThrowingRequest(ThrowingRequestEventArgs ev) if (!Check(ev.Player.CurrentItem)) return; - Log.Debug($"{ev.Player.Nickname} has thrown a {Name}!"); + Log.Debug($"{ev.Player.Nickname} is requesting throw of {Name}!"); OnThrowingRequest(ev); - return; } private void OnInternalThrownProjectile(ThrownProjectileEventArgs ev) @@ -178,20 +166,36 @@ private void OnInternalThrownProjectile(ThrownProjectileEventArgs ev) return; OnThrownProjectile(ev); - - if (ev.Projectile is TimeGrenadeProjectile timeGrenade) - timeGrenade.FuseTime = FuseTime; + if (ev.Player == null) + { + Log.Error($"CustomGrenade::OnInternalThrownProjectile player is null {ev.Projectile}"); + } + else + { + Log.Debug($"{ev.Player.Nickname} has thrown a {Name} ({FuseTime}) {ev.Player.Items.ToString(true)}!"); + } if (ExplodeOnCollision) - ev.Projectile.GameObject.AddComponent().Init((ev.Player ?? Server.Host).GameObject, ev.Projectile.Base); + { + ev.Projectile.GameObject.AttachActionOnCollision( + () => + { + if (ev.Projectile is TimeGrenadeProjectile grenadeProjectile) + { + grenadeProjectile.FuseTime = 0.1f; + } + }, + ev.Projectile.PreviousOwner ?? Server.Host, + ExplodeOnCollisionFuseTime); + } } private void OnInternalExplodingGrenade(ExplodingGrenadeEventArgs ev) { if (Check(ev.Projectile)) { - Log.Debug($"A {Name} is exploding!!"); OnExploding(ev); + Log.Debug($"A {Name} is exploding! IsAllowed: {ev.IsAllowed}"); } } @@ -200,13 +204,22 @@ private void OnInternalChangedIntoGrenade(ChangedIntoGrenadeEventArgs ev) if (!Check(ev.Pickup)) return; - if (ev.Projectile is TimeGrenadeProjectile timedGrenade) - timedGrenade.FuseTime = FuseTime; - OnChangedIntoGrenade(ev); + Log.Debug($"A {Name} ChangedIntoGrenade"); if (ExplodeOnCollision) - ev.Projectile.GameObject.AddComponent().Init((ev.Pickup.PreviousOwner ?? Server.Host).GameObject, ev.Projectile.Base); + { + ev.Projectile.GameObject.AttachActionOnCollision( + () => + { + if (ev.Projectile is TimeGrenadeProjectile grenadeProjectile) + { + grenadeProjectile.FuseTime = 0.1f; + } + }, + ev.Projectile.PreviousOwner ?? Server.Host, + ExplodeOnCollisionFuseTime); + } } } -} +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs b/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs index fa39569e0a..2f552c19b8 100644 --- a/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs +++ b/EXILED/Exiled.CustomItems/API/Features/CustomItem.cs @@ -10,6 +10,7 @@ namespace Exiled.CustomItems.API.Features using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Reflection; @@ -23,12 +24,12 @@ namespace Exiled.CustomItems.API.Features using Exiled.API.Features.Spawn; using Exiled.API.Interfaces; using Exiled.CustomItems.API.EventArgs; + using Exiled.Events.EventArgs.Map; using Exiled.Events.EventArgs.Player; using Exiled.Events.EventArgs.Scp914; using Exiled.Loader; using InventorySystem.Items.Pickups; using MEC; - using PlayerRoles; using UnityEngine; using YamlDotNet.Serialization; @@ -46,7 +47,6 @@ namespace Exiled.CustomItems.API.Features /// public abstract class CustomItem { - private static Dictionary stringLookupTable = new(); private static Dictionary idLookupTable = new(); private ItemType type = ItemType.None; @@ -76,6 +76,16 @@ public abstract class CustomItem /// public abstract float Weight { get; set; } + /// + /// Gets or sets the duration of Description hint when picked up. Default duration if smaller than zero. + /// + public virtual float PickedUpHintDuration { get; set; } = -1; + + /// + /// Gets or sets the duration of Description hint when selected. Default duration if smaller than zero. + /// + public virtual float SelectedHintDuration { get; set; } = -1; + /// /// Gets or sets the list of spawn locations and chances for each one. /// @@ -108,21 +118,37 @@ public virtual ItemType Type public HashSet TrackedSerials { get; } = new(); /// - /// Gets a value indicating whether this item causes things to happen that may be considered hacks, and thus be shown to global moderators as being present in a player's inventory when they gban them. + /// Gets a with a specific ID. /// - [YamlIgnore] - public virtual bool ShouldMessageOnGban { get; } = false; + /// The ID. + /// The matching the search, if not registered. + public static CustomItem? Get(uint id) + { + if (!idLookupTable.TryGetValue(id, out CustomItem? ci)) + { + ci = Registered.FirstOrDefault(i => i.Id == id); + idLookupTable.Add(id, ci); + } + + return ci; + } /// /// Gets a with a specific ID. /// + /// The type to cast the customitem to. /// The ID. /// The matching the search, if not registered. - public static CustomItem? Get(uint id) + public static T? Get(uint id) + where T : CustomItem { - if (!idLookupTable.ContainsKey(id)) - idLookupTable.Add(id, Registered.FirstOrDefault(i => i.Id == id)); - return idLookupTable[id]; + if (!idLookupTable.TryGetValue(id, out CustomItem? ci)) + { + ci = GetMany().FirstOrDefault(i => i.Id == id); + idLookupTable.Add(id, ci); + } + + return ci as T; } /// @@ -130,29 +156,91 @@ public virtual ItemType Type /// /// The name. /// The matching the search, if not registered. - public static CustomItem? Get(string name) + public static CustomItem? Get(string name) => Registered.FirstOrDefault(i => i.Name == name); + + /// + /// Gets a with a specific name. + /// + /// The type to cast the customitem to. + /// The name. + /// The matching the search, if not registered. + public static T? Get(string name) + where T : CustomItem => GetMany().FirstOrDefault(i => i.Name == name); + + /// + /// Gets a for target . + /// + /// The to check. + /// The matching the search, if not registered. + public static CustomItem? Get(Item item) => Registered.FirstOrDefault(i => i.Check(item)); + + /// + /// Gets a for target . + /// + /// The type to cast the customitem to. + /// The to check. + /// The matching the search, if not registered. + public static T? Get(Item item) + where T : CustomItem => GetMany().FirstOrDefault(i => i.Check(item)); + + /// + /// Gets a with a specific type. + /// + /// The type. + /// The matching the search, if not registered. + [Obsolete("Для получения типов кастомИтема - используй GetMany😡😡😡", true)] + public static CustomItem? Get(Type t) { - if (!stringLookupTable.ContainsKey(name)) - stringLookupTable.Add(name, Registered.FirstOrDefault(i => i.Name == name)); - return stringLookupTable[name]; + return Registered.FirstOrDefault(i => i.GetType() == t); } /// - /// Retrieves a collection of instances that match a specified type. + /// Gets a with a specific type. /// - /// The type. - /// An containing all registered of the specified type. - public static IEnumerable Get(Type t) => Registered.Where(i => i.GetType() == t); + /// The type to cast the customitem to. + /// The matching the search, if not registered. + [Obsolete("Для получения типов кастомИтема - используй GetMany😡😡😡", true)] + public static T? Get() + where T : CustomItem + { + return Registered.OfType().FirstOrDefault(); + } + + /// + /// Gets a of 's with a specific type. + /// + /// The type to cast the customitem to. + /// The of matching the search. + public static IEnumerable GetMany() + where T : CustomItem + { + return Registered.OfType(); + } /// /// Tries to get a with a specific ID. /// /// The ID to look for. /// The found , if not registered. - /// Returns a value indicating whether the was found. - public static bool TryGet(uint id, out CustomItem? customItem) + /// Returns a value indicating whether the was found or not. + public static bool TryGet(uint id, out CustomItem customItem) { - customItem = Get(id); + customItem = Get(id) !; + + return customItem is not null; + } + + /// + /// Tries to get a with a specific ID. + /// + /// The type to cast the customitem to. + /// The ID to look for. + /// The found , if not registered. + /// Returns a value indicating whether the was found or not. + public static bool TryGet(uint id, out T customItem) + where T : CustomItem + { + customItem = Get(id) !; return customItem is not null; } @@ -162,29 +250,64 @@ public static bool TryGet(uint id, out CustomItem? customItem) /// /// The name to look for. /// The found , if not registered. - /// Returns a value indicating whether the was found. - public static bool TryGet(string name, out CustomItem? customItem) + /// Returns a value indicating whether the was found or not. + public static bool TryGet(string name, out CustomItem customItem) { - customItem = null; + customItem = null!; if (string.IsNullOrEmpty(name)) return false; - customItem = uint.TryParse(name, out uint id) ? Get(id) : Get(name); + customItem = (uint.TryParse(name, out uint id) ? Get(id) : Get(name)) !; return customItem is not null; } /// - /// Attempts to retrieve a collection of instances of a specified type. + /// Tries to get a with a specific name. + /// + /// The type to cast the customitem to. + /// The name to look for. + /// The found , if not registered. + /// Returns a value indicating whether the was found or not. + public static bool TryGet(string name, out T customItem) + where T : CustomItem + { + customItem = null!; + if (string.IsNullOrEmpty(name)) + return false; + + customItem = (uint.TryParse(name, out uint id) ? Get(id) : Get(name)) !; + + return customItem is not null; + } + + /// + /// Tries to get a with a specific type. /// /// The of the item to look for. - /// An output parameter that will contain the found instances, or an empty collection if none are registered. - /// A boolean value indicating whether any instances of the specified type were found. - public static bool TryGet(Type t, out IEnumerable customItems) + /// The found , if not registered. + /// Returns a value indicating whether the was found or not. + [Obsolete("Для получения типов кастомИтема - используй TryGetMany😡😡😡", true)] + public static bool TryGet(Type t, out CustomItem? customItem) { - customItems = Get(t); + customItem = Get(t); - return customItems.Any(); + return customItem is not null; + } + + /// + /// Tries to get a with a specific type. + /// + /// The type to cast the customitem to. + /// The found , if not registered. + /// Returns a value indicating whether the was found or not. + [Obsolete("Для получения типов кастомИтема - используй TryGetMany😡😡😡", true)] + public static bool TryGet(out T? customItem) + where T : CustomItem + { + customItem = Get(); + + return customItem is not null; } /// @@ -193,24 +316,75 @@ public static bool TryGet(Type t, out IEnumerable customItems) /// The to check. /// The in their hand. /// Returns a value indicating whether the has a in their hand or not. - public static bool TryGet(Player player, out CustomItem? customItem) + public static bool TryGet(Player player, out CustomItem customItem) { - customItem = null; + customItem = null!; if (player is null) return false; - customItem = Registered?.FirstOrDefault(tempCustomItem => tempCustomItem.Check(player.CurrentItem)); + customItem = Registered.FirstOrDefault(tempCustomItem => tempCustomItem.Check(player.CurrentItem)); return customItem is not null; } + /// + /// Tries to get the player's current in their hand. + /// + /// The type to cast the customitem to. + /// The to check. + /// The founded . + /// Returns a value indicating whether the has a . + public static bool TryGet(Player player, out T customItem) + where T : CustomItem + { + customItem = null!; + if (player is null) + return false; + + customItem = GetMany().FirstOrDefault(tempCustomItem => player.Items.Any(item => tempCustomItem.Check(item))); + + return customItem is not null; + } + + /// + /// Tries to get the player's current , with a specific type. + /// + /// The type to cast the customitem to. + /// The to check. + /// The founded . + /// The founded . + /// Returns a value indicating whether the has a . + public static bool TryGet(Player player, out Item item, out T customItem) + where T : CustomItem + { + item = null!; + customItem = null!; + if (player is null) + return false; + + foreach (Item plyItems in player.Items) + { + foreach (T ci in GetMany()) + { + if (ci.Check(plyItems)) + { + item = plyItems; + customItem = ci; + return true; + } + } + } + + return false; + } + /// /// Tries to get the player's of . /// /// The to check. /// The player's of . /// Returns a value indicating whether the has a in their hand or not. - public static bool TryGet(Player player, out IEnumerable? customItems) + public static bool TryGet(Player player, out IEnumerable customItems) { customItems = Enumerable.Empty(); if (player is null) @@ -221,15 +395,49 @@ public static bool TryGet(Player player, out IEnumerable? customItem return customItems.Any(); } + /// + /// Tries to get the player's of . + /// + /// The type to cast the customitem to. + /// The to check. + /// The player's of . + /// Returns a value indicating whether the has a in their hand or not. + public static bool TryGet(Player player, out IEnumerable customItems) + where T : CustomItem + { + customItems = Enumerable.Empty(); + if (player is null) + return false; + + customItems = GetMany().Where(tempCustomItem => player.Items.Any(tempCustomItem.Check)); + + return customItems.Any(); + } + + /// + /// Checks to see if this item is a custom item. + /// + /// The to check. + /// The this item is. + /// True if the item is a custom item. + public static bool TryGet(Item item, out CustomItem customItem) + { + customItem = (item == null ? null : Get(item)) !; + + return customItem is not null; + } + /// /// Checks to see if this item is a custom item. /// + /// The type to cast the customitem to. /// The to check. /// The this item is. /// True if the item is a custom item. - public static bool TryGet(Item item, out CustomItem? customItem) + public static bool TryGet(Item item, out T customItem) + where T : CustomItem { - customItem = item == null ? null : Registered?.FirstOrDefault(tempCustomItem => tempCustomItem.TrackedSerials.Contains(item.Serial)); + customItem = (item == null ? null : Get(item)) !; return customItem is not null; } @@ -240,9 +448,24 @@ public static bool TryGet(Item item, out CustomItem? customItem) /// The to check. /// The this pickup is. /// True if the pickup is a custom item. - public static bool TryGet(Pickup pickup, out CustomItem? customItem) + public static bool TryGet(Pickup pickup, out CustomItem customItem) { - customItem = Registered?.FirstOrDefault(tempCustomItem => tempCustomItem.TrackedSerials.Contains(pickup.Serial)); + customItem = Registered.FirstOrDefault(tempCustomItem => tempCustomItem.Check(pickup)); + + return customItem is not null; + } + + /// + /// Checks if this pickup is a custom item. + /// + /// The type to cast the customitem to. + /// The to check. + /// The this pickup is. + /// True if the pickup is a custom item. + public static bool TryGet(Pickup pickup, out T customItem) + where T : CustomItem + { + customItem = GetMany().FirstOrDefault(tempCustomItem => tempCustomItem.Check(pickup)); return customItem is not null; } @@ -253,7 +476,7 @@ public static bool TryGet(Pickup pickup, out CustomItem? customItem) /// The ID of the to spawn. /// The location to spawn the item. /// The instance of the . - /// Returns a value indicating whether the was spawned. + /// Returns a value indicating whether the was spawned or not. public static bool TrySpawn(uint id, Vector3 position, out Pickup? pickup) { pickup = default; @@ -272,7 +495,7 @@ public static bool TrySpawn(uint id, Vector3 position, out Pickup? pickup) /// The name of the to spawn. /// The location to spawn the item. /// The instance of the . - /// Returns a value indicating whether the was spawned. + /// Returns a value indicating whether the was spawned or not. public static bool TrySpawn(string name, Vector3 position, out Pickup? pickup) { pickup = default; @@ -322,7 +545,7 @@ public static bool TryGive(Player player, uint id, bool displayMessage = true) /// /// Registers all the 's present in the current assembly. /// - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). + /// Whether or not reflection is skipped (more efficient if you are not using your custom item classes as config objects). /// The class to search properties for, if different from the plugin's config class. /// A of which contains all registered 's. public static IEnumerable RegisterItems(bool skipReflection = false, object? overrideClass = null) @@ -401,7 +624,7 @@ public static IEnumerable RegisterItems(bool skipReflection = false, /// /// The of containing the target types. /// A value indicating whether the target types should be ignored. - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). + /// Whether or not reflection is skipped (more efficient if you are not using your custom item classes as config objects). /// The class to search properties for, if different from the plugin's config class. /// A of which contains all registered 's. public static IEnumerable RegisterItems(IEnumerable targetTypes, bool isIgnored = false, bool skipReflection = false, object? overrideClass = null) @@ -493,23 +716,25 @@ public static IEnumerable UnregisterItems(IEnumerable targetTy public static IEnumerable UnregisterItems(IEnumerable targetItems, bool isIgnored = false) => UnregisterItems(targetItems.Select(x => x.GetType()), isIgnored); /// - /// Spawns the in a specific location. + /// Creates a new current instance. /// - /// The x coordinate. - /// The y coordinate. - /// The z coordinate. - /// The wrapper of the spawned . - public virtual Pickup? Spawn(float x, float y, float z) => Spawn(new Vector3(x, y, z)); + /// A created . + public virtual Item CreateItem() + { + Item item = Item.Create(Type); + item.Scale = Scale; + TrackedSerials.Add(item.Serial); + return item; + } /// - /// Spawns a as a in a specific location. + /// Spawns the in a specific location. /// /// The x coordinate. /// The y coordinate. /// The z coordinate. - /// The to be spawned as a . /// The wrapper of the spawned . - public virtual Pickup? Spawn(float x, float y, float z, Item item) => Spawn(new Vector3(x, y, z), item); + public virtual Pickup? Spawn(float x, float y, float z) => Spawn(new Vector3(x, y, z)); /// /// Spawns the where a specific is, and optionally sets the previous owner. @@ -519,41 +744,20 @@ public static IEnumerable UnregisterItems(IEnumerable targetTy /// The of the spawned . public virtual Pickup? Spawn(Player player, Player? previousOwner = null) => Spawn(player.Position, previousOwner); - /// - /// Spawns a as a where a specific is, and optionally sets the previous owner. - /// - /// The position where the will be spawned. - /// The to be spawned as a . - /// The previous owner of the pickup, can be null. - /// The of the spawned . - public virtual Pickup? Spawn(Player player, Item item, Player? previousOwner = null) => Spawn(player.Position, item, previousOwner); - /// /// Spawns the in a specific position. /// /// The where the will be spawned. /// The of the item. Can be null. /// The of the spawned . - public virtual Pickup? Spawn(Vector3 position, Player? previousOwner = null) => Spawn(position, Item.Create(Type), previousOwner); - - /// - /// Spawns the in a specific position. - /// - /// The where the will be spawned. - /// The to be spawned as a . - /// The of the item. Can be null. - /// The of the spawned . - public virtual Pickup? Spawn(Vector3 position, Item item, Player? previousOwner = null) + public virtual Pickup? Spawn(Vector3 position, Player? previousOwner = null) { + Item item = CreateItem(); Pickup? pickup = item.CreatePickup(position); - pickup.Scale = Scale; - pickup.Weight = Weight; if (previousOwner is not null) pickup.PreviousOwner = previousOwner; - TrackedSerials.Add(pickup.Serial); - return pickup; } @@ -574,35 +778,18 @@ public virtual uint Spawn(IEnumerable spawnPoints, uint limit) if (Loader.Random.NextDouble() * 100 >= spawnPoint.Chance || (limit > 0 && spawned >= limit)) continue; - Pickup? pickup; - if (spawnPoint is LockerSpawnPoint { UseChamber: true } lockerSpawnPoint) + spawned++; + + if (spawnPoint is RoleSpawnPoint roleSpawnPoint) { - try - { - lockerSpawnPoint.GetSpawningInfo(out _, out Chamber? chamber, out Vector3 position); - pickup = Spawn(position); - chamber?.AddItem(pickup); - } - catch (Exception e) - { - Log.Error($"CustomItem {Name}({Id} failed to spawn: {e.Message})"); - continue; - } + Spawn(roleSpawnPoint.Role.GetRandomSpawnLocation().Position, null); } else { - pickup = Spawn(spawnPoint.Position); - } - - if (pickup == null) - continue; + Pickup? pickup = Spawn(spawnPoint.Position, null); - spawned++; - - /*if (pickup.Is(out FirearmPickup firearmPickup) && this is CustomWeapon customWeapon) - { - // TODO: Set MaxAmmo (if synced) - }*/ + Log.Debug($"Spawned {Name} at {spawnPoint.Position} ({spawnPoint.Name})"); + } } return spawned; @@ -616,8 +803,8 @@ public virtual void SpawnAll() if (SpawnProperties is null) return; - // This will go over each spawn property type (static, dynamic, role-based, room-based, and locker-based) to try and spawn the item. - // It will attempt to spawn in role-based locations, then dynamic ones, followed by room-based, locker-based, and finally static. + // This will go over each spawn property type (static, dynamic and role) to try and spawn the item. + // It will attempt to spawn in role-based locations, and then dynamic ones, and finally static. // Math.Min is used here to ensure that our recursive Spawn() calls do not result in exceeding the spawn limit config. // This is the same as: // int spawned = 0; @@ -625,12 +812,8 @@ public virtual void SpawnAll() // if (spawned < SpawnProperties.Limit) // spawned += Spawn(SpawnProperties.DynamicSpawnPoints, SpawnProperties.Limit - spawned); // if (spawned < SpawnProperties.Limit) - // spawned += Spawn(SpawnProperties.RoomSpawnPoints, SpawnProperties.Limit - spawned); - // if (spawned < SpawnProperties.Limit) - // spawned += Spawn(SpawnProperties.LockerSpawnPoints, SpawnProperties.Limit - spawned); - // if (spawned < SpawnProperties.Limit) // Spawn(SpawnProperties.StaticSpawnPoints, SpawnProperties.Limit - spawned); - Spawn(SpawnProperties.StaticSpawnPoints, Math.Min(SpawnProperties.Limit, SpawnProperties.Limit - Math.Min(Spawn(SpawnProperties.DynamicSpawnPoints, SpawnProperties.Limit), SpawnProperties.Limit - Math.Min(Spawn(SpawnProperties.RoleSpawnPoints, SpawnProperties.Limit), SpawnProperties.Limit - Math.Min(Spawn(SpawnProperties.RoomSpawnPoints, SpawnProperties.Limit), SpawnProperties.Limit - Spawn(SpawnProperties.LockerSpawnPoints, SpawnProperties.Limit)))))); + Spawn(SpawnProperties.StaticSpawnPoints, Math.Min(0, SpawnProperties.Limit - Math.Min(0, Spawn(SpawnProperties.DynamicSpawnPoints, SpawnProperties.Limit) - Spawn(SpawnProperties.RoleSpawnPoints, SpawnProperties.Limit)))); } /// @@ -638,19 +821,15 @@ public virtual void SpawnAll() /// /// The who will receive the item. /// The to be given. - /// Indicates whether will be called when the player receives the item. + /// Indicates whether or not will be called when the player receives the item. public virtual void Give(Player player, Item item, bool displayMessage = true) { try { - Log.Debug($"{Name}.{nameof(Give)}: Item Serial: {item.Serial} Ammo: {(item is Firearm firearm ? firearm.MagazineAmmo : -1)}"); + Log.Debug($"{Name}.{nameof(Give)}: Item Serial: {item.Serial}"); player.AddItem(item); - Log.Debug($"{nameof(Give)}: Adding {item.Serial} to tracker."); - if (!TrackedSerials.Contains(item.Serial)) - TrackedSerials.Add(item.Serial); - Timing.CallDelayed(0.05f, () => OnAcquired(player, item, displayMessage)); } catch (Exception e) @@ -659,27 +838,18 @@ public virtual void Give(Player player, Item item, bool displayMessage = true) } } - /// - /// Gives a as a to a . - /// - /// The who will receive the item. - /// The to be given. - /// Indicates whether will be called when the player receives the item. - public virtual void Give(Player player, Pickup pickup, bool displayMessage = true) => Give(player, player.AddItem(pickup), displayMessage); - /// /// Gives the to a player. /// /// The who will receive the item. - /// Indicates whether will be called when the player receives the item. - public virtual void Give(Player player, bool displayMessage = true) => Give(player, Item.Create(Type), displayMessage); + /// Indicates whether or not will be called when the player receives the item. + public virtual void Give(Player player, bool displayMessage = true) => Give(player, CreateItem(), displayMessage); /// /// Called when the item is registered. /// public virtual void Init() { - stringLookupTable.Add(Name, this); idLookupTable.Add(Id, this); SubscribeEvents(); @@ -692,7 +862,6 @@ public virtual void Destroy() { UnsubscribeEvents(); - stringLookupTable.Remove(Name); idLookupTable.Remove(Id); } @@ -723,7 +892,7 @@ public virtual void Destroy() /// /// Registers a . /// - /// Returns a value indicating whether the was registered. + /// Returns a value indicating whether the was registered or not. internal bool TryRegister() { if (!Instance?.Config.IsEnabled ?? false) @@ -735,7 +904,7 @@ internal bool TryRegister() Log.Debug("Registered items doesn't contain this item yet.."); if (Registered.Any(customItem => customItem.Id == Id)) { - Log.Warn($"{Name} has tried to register with the same custom item ID as another item: {Id}. It will not be registered."); + Log.Error($"{Name} has tried to register with the same custom item ID as another item: {Id}. It will not be registered."); return false; } @@ -750,7 +919,7 @@ internal bool TryRegister() return true; } - Log.Warn($"Couldn't register {Name} ({Id}) [{Type}] as it already exists."); + Log.Error($"Couldn't register {Name} ({Id}) [{Type}] as it already exists."); return false; } @@ -758,7 +927,7 @@ internal bool TryRegister() /// /// Tries to unregister a . /// - /// Returns a value indicating whether the was unregistered. + /// Returns a value indicating whether the was unregistered or not. internal bool TryUnregister() { Destroy(); @@ -779,8 +948,7 @@ internal bool TryUnregister() protected virtual void SubscribeEvents() { Exiled.Events.Handlers.Player.Dying += OnInternalOwnerDying; - Exiled.Events.Handlers.Player.DroppingItem += OnInternalDroppingItem; - Exiled.Events.Handlers.Player.DroppingAmmo += OnInternalDroppingAmmo; + Exiled.Events.Handlers.Player.DroppingItem += OnInternalDropping; Exiled.Events.Handlers.Player.ChangingItem += OnInternalChanging; Exiled.Events.Handlers.Player.Escaping += OnInternalOwnerEscaping; Exiled.Events.Handlers.Player.PickingUpItem += OnInternalPickingUp; @@ -790,6 +958,7 @@ protected virtual void SubscribeEvents() Exiled.Events.Handlers.Player.Handcuffing += OnInternalOwnerHandcuffing; Exiled.Events.Handlers.Player.ChangingRole += OnInternalOwnerChangingRole; Exiled.Events.Handlers.Scp914.UpgradingInventoryItem += OnInternalUpgradingInventoryItem; + Exiled.Events.Handlers.Map.PickupAdded += OnInternalPickupAdded; } /// @@ -798,8 +967,7 @@ protected virtual void SubscribeEvents() protected virtual void UnsubscribeEvents() { Exiled.Events.Handlers.Player.Dying -= OnInternalOwnerDying; - Exiled.Events.Handlers.Player.DroppingItem -= OnInternalDroppingItem; - Exiled.Events.Handlers.Player.DroppingAmmo -= OnInternalDroppingAmmo; + Exiled.Events.Handlers.Player.DroppingItem -= OnInternalDropping; Exiled.Events.Handlers.Player.ChangingItem -= OnInternalChanging; Exiled.Events.Handlers.Player.Escaping -= OnInternalOwnerEscaping; Exiled.Events.Handlers.Player.PickingUpItem -= OnInternalPickingUp; @@ -809,6 +977,7 @@ protected virtual void UnsubscribeEvents() Exiled.Events.Handlers.Player.Handcuffing -= OnInternalOwnerHandcuffing; Exiled.Events.Handlers.Player.ChangingRole -= OnInternalOwnerChangingRole; Exiled.Events.Handlers.Scp914.UpgradingInventoryItem -= OnInternalUpgradingInventoryItem; + Exiled.Events.Handlers.Map.PickupAdded -= OnInternalPickupAdded; } /// @@ -847,27 +1016,10 @@ protected virtual void OnOwnerHandcuffing(OwnerHandcuffingEventArgs ev) /// Handles tracking items when they are dropped by a player. /// /// . - protected virtual void OnDroppingItem(DroppingItemEventArgs ev) - { - } - - /// - /// Handles tracking items when they are dropped by a player. - /// - /// . - [Obsolete("Use OnDroppingItem instead.", false)] protected virtual void OnDropping(DroppingItemEventArgs ev) { } - /// - /// Handles tracking when player requests drop of item which equals to the specified by . - /// - /// . - protected virtual void OnDroppingAmmo(DroppingAmmoEventArgs ev) - { - } - /// /// Handles tracking items when they are picked up by a player. /// @@ -900,7 +1052,7 @@ protected virtual void OnUpgrading(UpgradingItemEventArgs ev) /// /// The acquiring the item. /// The being acquired. - /// Whether the Pickup hint should be displayed. + /// Whether or not the Pickup hint should be displayed. protected virtual void OnAcquired(Player player, Item item, bool displayMessage) { if (displayMessage) @@ -922,7 +1074,7 @@ protected virtual void OnWaitingForPlayers() protected virtual void ShowPickedUpMessage(Player player) { if (Instance!.Config.PickedUpHint.Show) - player.ShowHint(string.Format(Instance.Config.PickedUpHint.Content, Name, Description), Instance.Config.PickedUpHint.Duration); + player.ShowHint(string.Format(Instance.Config.PickedUpHint.Content, Name, Description), PickedUpHintDuration < 0 ? Instance.Config.PickedUpHint.Duration : PickedUpHintDuration); } /// @@ -932,7 +1084,7 @@ protected virtual void ShowPickedUpMessage(Player player) protected virtual void ShowSelectedMessage(Player player) { if (Instance!.Config.SelectedHint.Show) - player.ShowHint(string.Format(Instance.Config.SelectedHint.Content, Name, Description), Instance.Config.SelectedHint.Duration); + player.ShowHint(string.Format(Instance.Config.SelectedHint.Content, Name, Description), SelectedHintDuration < 0 ? Instance.Config.SelectedHint.Duration : SelectedHintDuration); } private void OnInternalOwnerChangingRole(ChangingRoleEventArgs ev) @@ -946,15 +1098,7 @@ private void OnInternalOwnerChangingRole(ChangingRoleEventArgs ev) continue; OnOwnerChangingRole(new OwnerChangingRoleEventArgs(item.Base, ev)); - - TrackedSerials.Remove(item.Serial); - - ev.Player.RemoveItem(item); - - Spawn(ev.Player, item, ev.Player); } - - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_myNickSync)); } private void OnInternalOwnerDying(DyingEventArgs ev) @@ -965,20 +1109,7 @@ private void OnInternalOwnerDying(DyingEventArgs ev) continue; OnOwnerDying(new OwnerDyingEventArgs(item, ev)); - - if (!ev.IsAllowed) - continue; - - ev.Player.RemoveItem(item); - - TrackedSerials.Remove(item.Serial); - - Spawn(ev.Player, item, ev.Player); - - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_myNickSync)); } - - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_myNickSync)); } private void OnInternalOwnerEscaping(EscapingEventArgs ev) @@ -989,20 +1120,7 @@ private void OnInternalOwnerEscaping(EscapingEventArgs ev) continue; OnOwnerEscaping(new OwnerEscapingEventArgs(item, ev)); - - if (!ev.IsAllowed) - continue; - - ev.Player.RemoveItem(item); - - TrackedSerials.Remove(item.Serial); - - Timing.CallDelayed(1.5f, () => Spawn(ev.Player.Position, item, null)); - - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_myNickSync)); } - - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_myNickSync)); } private void OnInternalOwnerHandcuffing(HandcuffingEventArgs ev) @@ -1013,37 +1131,15 @@ private void OnInternalOwnerHandcuffing(HandcuffingEventArgs ev) continue; OnOwnerHandcuffing(new OwnerHandcuffingEventArgs(item, ev)); - - if (!ev.IsAllowed) - continue; - - ev.Target.RemoveItem(item); - - TrackedSerials.Remove(item.Serial); - - Spawn(ev.Target, item, ev.Target); } } - private void OnInternalDroppingItem(DroppingItemEventArgs ev) + private void OnInternalDropping(DroppingItemEventArgs ev) { if (!Check(ev.Item)) return; - OnDroppingItem(ev); - - // TODO: Don't forget to remove this with next update -#pragma warning disable CS0618 OnDropping(ev); -#pragma warning restore CS0618 - } - - private void OnInternalDroppingAmmo(DroppingAmmoEventArgs ev) - { - if (Type != ev.ItemType) - return; - - OnDroppingAmmo(ev); } private void OnInternalPickingUp(PickingUpItemEventArgs ev) @@ -1052,9 +1148,6 @@ private void OnInternalPickingUp(PickingUpItemEventArgs ev) return; OnPickingUp(ev); - - if (!ev.IsAllowed) - return; } private void OnInternalItemAdded(ItemAddedEventArgs ev) @@ -1069,16 +1162,9 @@ private void OnInternalChanging(ChangingItemEventArgs ev) { if (!Check(ev.Item)) { - MirrorExtensions.ResyncSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_displayName)); return; } - if (ShouldMessageOnGban) - { - foreach (Player player in Player.Get(RoleTypeId.Spectator)) - Timing.CallDelayed(0.5f, () => player.SendFakeSyncVar(ev.Player.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_displayName), $"{ev.Player.Nickname} (CustomItem: {Name})")); - } - OnChanging(ev); } @@ -1099,11 +1185,15 @@ private void OnInternalUpgradingPickup(UpgradingPickupEventArgs ev) ev.IsAllowed = false; - Timing.CallDelayed(3.5f, () => - { - ev.Pickup.Position = ev.OutputPosition; - OnUpgrading(new UpgradingEventArgs(ev.Pickup.Base, ev.OutputPosition, ev.KnobSetting)); - }); + OnUpgrading(new UpgradingEventArgs(ev.Pickup.Base, ev.OutputPosition, ev.KnobSetting)); + } + + private void OnInternalPickupAdded(PickupAddedEventArgs ev) + { + if (!Check(ev.Pickup)) + return; + + ev.Pickup.Weight = Weight < 0 ? ev.Pickup.Weight : Weight; } } } diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomKeycard.cs b/EXILED/Exiled.CustomItems/API/Features/CustomKeycard.cs deleted file mode 100644 index 1777953129..0000000000 --- a/EXILED/Exiled.CustomItems/API/Features/CustomKeycard.cs +++ /dev/null @@ -1,237 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomItems.API.Features -{ - using System; - using System.Linq; - - using Exiled.API.Enums; - using Exiled.API.Extensions; - using Exiled.API.Features; - using Exiled.API.Features.Doors; - using Exiled.API.Features.Items; - using Exiled.API.Features.Items.Keycards; - using Exiled.API.Features.Lockers; - using Exiled.API.Features.Pickups; - using Exiled.API.Interfaces.Keycards; - using Exiled.Events.EventArgs.Item; - using Exiled.Events.EventArgs.Player; - using InventorySystem.Items.Keycards; - using UnityEngine; - - /// - /// The Custom keycard base class. - /// - public abstract class CustomKeycard : CustomItem - { - /// - /// Throws if specified is not Keycard. - public override ItemType Type - { - get => base.Type; - set - { - if (!value.IsKeycard()) - throw new ArgumentOutOfRangeException(nameof(Type), value, "Invalid keycard type."); - - base.Type = value; - } - } - - /// - /// Gets or sets name of keycard holder. - /// - public virtual string KeycardName { get; set; } = string.Empty; - - /// - /// Gets or sets a label for keycard. - /// - public virtual string KeycardLabel { get; set; } = string.Empty; - - /// - /// Gets or sets a color of keycard label. - /// - public virtual Color32? KeycardLabelColor { get; set; } - - /// - /// Gets or sets a tint color. - /// - public virtual Color32? TintColor { get; set; } - - /// - /// Gets or sets the permissions for custom keycard. - /// - public virtual KeycardPermissions Permissions { get; set; } = KeycardPermissions.None; - - /// - /// Gets or sets a color of keycard permissions. - /// - public virtual Color32? KeycardPermissionsColor { get; set; } - - /// - /// Gets or sets the wear of a keycard. - /// - /// - /// Only works on CustomSite02 keycards with values 0-4 and CustomMetalCase keycards with values 0-5. - /// - public virtual byte Wear { get; set; } = byte.MaxValue; - - /// - /// Gets or sets the serial number of a keycard. - /// - /// - /// Only works on CustomMetalCase and CustomTaskForce keycards. Max length is 12. Non-numerical characters will be replaced with '-'. - /// - public virtual string SerialNumber { get; set; } = string.Empty; - - /// - /// Gets or sets the rank of a keycard. - /// - /// - /// Only works on CustomTaskForce keycards with values 0-3. Value runs in reverse (3 is rank 1, 2 -> 2, 1 -> 3, 0 -> 0). - /// - public virtual byte Rank { get; set; } = byte.MaxValue; - - /// - public override void Give(Player player, Item item, bool displayMessage = true) - { - if (item.Is(out Keycard card)) - SetupKeycard(card); - base.Give(player, item, displayMessage); - } - - /// - public override Pickup? Spawn(Vector3 position, Item item, Player? previousOwner = null) - { - if (item.Is(out Keycard card)) - SetupKeycard(card); - - return base.Spawn(position, item, previousOwner); - } - - /// - /// Setups keycard according to this class. - /// - /// Item instance. - protected virtual void SetupKeycard(Keycard keycard) - { - if (keycard is CustomKeycardItem customKeycard) - { - customKeycard.Permissions = Permissions; - - if (KeycardPermissionsColor.HasValue) - customKeycard.PermissionsColor = KeycardPermissionsColor.Value; - - if (TintColor.HasValue) - customKeycard.Color = TintColor.Value; - - if (!string.IsNullOrEmpty(Name)) - customKeycard.ItemName = Name; - - if (!string.IsNullOrEmpty(KeycardName) && customKeycard is INameTagKeycard nametag) - nametag.NameTag = KeycardName; - - if (customKeycard is ILabelKeycard label) - { - if (!string.IsNullOrEmpty(KeycardLabel)) - label.Label = KeycardLabel; - if (KeycardLabelColor.HasValue) - label.LabelColor = KeycardLabelColor.Value; - } - - if (customKeycard is IWearKeycard wear) - wear.Wear = Wear; - - if (customKeycard is ISerialNumberKeycard serialNumber) - serialNumber.SerialNumber = SerialNumber; - - if (customKeycard is IRankKeycard rank) - rank.Rank = Rank; - } - else if (keycard.Base.Customizable) - { - // Some keycards have customizable name tags but nothing else. This should handle those. - DetailBase[] details = keycard.Base.Details; - - NametagDetail? nametag = details.OfType().FirstOrDefault(); - - if (nametag != null) - { - NametagDetail._customNametag = KeycardName; - - if (KeycardDetailSynchronizer.Database.Remove(keycard.Serial)) - { - KeycardDetailSynchronizer.ServerProcessItem(keycard.Base); - } - } - } - } - - /// - /// Called when custom keycard interacts with a door. - /// - /// Owner of Custom keycard. - /// Door with which interacting. - protected virtual void OnInteractingDoor(Player player, Door door) - { - } - - /// - /// Called when custom keycard interacts with a locker. - /// - /// Owner of Custom keycard. - /// Chamber with which interacting. - protected virtual void OnInteractingLocker(Player player, Chamber chamber) - { - } - - /// - protected override void SubscribeEvents() - { - base.SubscribeEvents(); - - Exiled.Events.Handlers.Player.InteractingDoor += OnInternalInteractingDoor; - Exiled.Events.Handlers.Player.InteractingLocker += OnInternalInteractingLocker; - Exiled.Events.Handlers.Item.KeycardInteracting += OnInternalKeycardInteracting; - } - - /// - protected override void UnsubscribeEvents() - { - base.UnsubscribeEvents(); - - Exiled.Events.Handlers.Player.InteractingDoor -= OnInternalInteractingDoor; - Exiled.Events.Handlers.Player.InteractingLocker -= OnInternalInteractingLocker; - Exiled.Events.Handlers.Item.KeycardInteracting -= OnInternalKeycardInteracting; - } - - private void OnInternalKeycardInteracting(KeycardInteractingEventArgs ev) - { - if (!Check(ev.Pickup)) - return; - - OnInteractingDoor(ev.Player, ev.Door); - } - - private void OnInternalInteractingDoor(InteractingDoorEventArgs ev) - { - if (!Check(ev.Player.CurrentItem)) - return; - - OnInteractingDoor(ev.Player, ev.Door); - } - - private void OnInternalInteractingLocker(InteractingLockerEventArgs ev) - { - if (!Check(ev.Player.CurrentItem)) - return; - - OnInteractingLocker(ev.Player, ev.InteractingChamber); - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/API/Features/CustomWeapon.cs b/EXILED/Exiled.CustomItems/API/Features/CustomWeapon.cs index 9053cf2d66..fc6b5a8c69 100644 --- a/EXILED/Exiled.CustomItems/API/Features/CustomWeapon.cs +++ b/EXILED/Exiled.CustomItems/API/Features/CustomWeapon.cs @@ -8,35 +8,35 @@ namespace Exiled.CustomItems.API.Features { using System; + using System.Collections.Generic; + using System.ComponentModel; using System.Linq; + using CustomPlayerEffects; using Exiled.API.Enums; using Exiled.API.Extensions; using Exiled.API.Features; using Exiled.API.Features.DamageHandlers; using Exiled.API.Features.Items; using Exiled.API.Features.Pickups; + using Exiled.API.Structs; using Exiled.Events.EventArgs.Item; using Exiled.Events.EventArgs.Player; using InventorySystem.Items.Firearms.Attachments; using InventorySystem.Items.Firearms.Attachments.Components; - using InventorySystem.Items.Firearms.Modules; + using InventorySystem.Items.Firearms.BasicMessages; + using MEC; + using PlayerRoles; using UnityEngine; - using Firearm = Exiled.API.Features.Items.Firearm; - using Player = Exiled.API.Features.Player; - /// - /// The Custom Weapon base class. + /// The Custom Weapon base class. /// public abstract class CustomWeapon : CustomItem { - /// - /// Gets or sets value indicating what s the weapon will have. - /// - public virtual AttachmentName[] Attachments { get; set; } = { }; + private readonly HashSet cooldownedPlayers = new(); - /// + /// public override ItemType Type { get => base.Type; @@ -50,129 +50,147 @@ public override ItemType Type } /// - /// Gets or sets the weapon damage. + /// Gets or sets value indicating what s the weapon will have. /// - public virtual float Damage { get; set; } = -1; + public virtual AttachmentName[] Attachments { get; set; } = { }; /// - /// Gets or sets a value indicating how big of a clip the weapon will have. + /// Gets or sets value indicating what s the weapon wont have. /// - /// Warning for and . - /// They are not fully compatible with this features. - public virtual byte ClipSize { get; set; } + public virtual AttachmentName[] BannedAttachments { get; set; } = { }; /// - /// Gets or sets a value indicating whether to allow friendly fire with this weapon on FF-enabled servers. + /// Gets or sets the weapon damage. /// - public virtual bool FriendlyFire { get; set; } - - /// - public override Pickup? Spawn(Vector3 position, Player? previousOwner = null) - { - if (Item.Create(Type) is not Firearm firearm) - { - Log.Debug($"{nameof(Spawn)}: Item is not Firearm."); - return null; - } + public abstract float Damage { get; set; } - if (!Attachments.IsEmpty()) - firearm.AddAttachment(Attachments); + /// + /// Gets or sets a value indicating how big of a clip the weapon will have. + /// + public virtual byte ClipSize { get; set; } - Pickup? pickup = firearm.CreatePickup(position); + /// + /// Gets or sets a value indicating how many ammo will be spent per shot. + /// + public virtual byte AmmoUsage { get; set; } = 1; - if (pickup is null) - { - Log.Debug($"{nameof(Spawn)}: Pickup is null."); - return null; - } + /// + /// Gets or sets a value indicating whether firearm can be unloaded. + /// + public virtual bool CanUnload { get; set; } = true; - if (ClipSize > 0) - firearm.MagazineAmmo = ClipSize; + /// + /// Gets or sets a value indicating whether firearm's attachments can be modified. + /// + public bool AllowAttachmentsChange { get; set; } = true; - pickup.Weight = Weight; - pickup.Scale = Scale; - if (previousOwner is not null) - pickup.PreviousOwner = previousOwner; + /// + /// Gets or sets a value indicating shot cooldown. + /// + [Description("Кулдаун на выстрелы. Работает только при ClipSize > 1 и FireCooldown > 0. -1 для отключения.")] + public float FireCooldown { get; set; } = -1; - TrackedSerials.Add(pickup.Serial); - return pickup; - } + /// + /// Gets or sets a value indicating message, displayed to players, trying to reload cooldowned weapon. + /// + [Description("Сообщение при попытке перезарядить оружие под кулдауном. {0} - кулдаун из конфига")] + public string WeaponNotReady { get; set; } = "Оружие ещё не готово к выстрелу! Оно может стрелять только раз в {0} секунд."; - /// - public override Pickup? Spawn(Vector3 position, Item item, Player? previousOwner = null) + /// + /// Gets or sets a value indicating damage multipliers by ArmorType and HitboxType. + /// + [Description("Множители урона в зависимости от брони и точки попадания. Словарь ТипБрони: (ЗонаПопадания: МножительУрона)")] + public Dictionary> ArmorAndZoneDamageMultipliers { get; set; } = new() { - if (item is Firearm firearm) + [ItemType.None] = new Dictionary { - if (!Attachments.IsEmpty()) - firearm.AddAttachment(Attachments); - - if (ClipSize > 0) - firearm.MagazineAmmo = ClipSize; - int ammo = firearm.MagazineAmmo; - Log.Debug($"{nameof(Name)}.{nameof(Spawn)}: Spawning weapon with {ammo} ammo."); - Pickup? pickup = firearm.CreatePickup(position); - pickup.Scale = Scale; + [HitboxType.Headshot] = 1, + [HitboxType.Limb] = 1, + [HitboxType.Body] = 1, + }, + [ItemType.ArmorLight] = new Dictionary + { + [HitboxType.Headshot] = 1, + }, + [ItemType.ArmorCombat] = new Dictionary + { + [HitboxType.Headshot] = 1, + }, + [ItemType.ArmorHeavy] = new Dictionary + { + [HitboxType.Headshot] = 1, + }, + }; - if (previousOwner is not null) - pickup.PreviousOwner = previousOwner; + /// + /// Gets or sets a value indicating damage multipliers by target RoleTypeId. + /// + [Description("Множители урона для ролей. Словарь RoleType: МножительУрона")] + public Dictionary RoleDamageMultipliers { get; set; } = new() + { + { RoleTypeId.Scp096, 1 }, + { RoleTypeId.Scp173, 1 }, + }; - TrackedSerials.Add(pickup.Serial); - return pickup; - } + /// + /// Gets or sets a value indicating whether firearm's will be reset after shot. + /// + [Description("Будет ли оружие убрано-возвращено в руки после выстрела")] + public bool ForceResetWeaponOnShot { get; set; } = false; - return base.Spawn(position, item, previousOwner); - } + /// + /// Gets or sets a value indicating whether firearm's will be double shot. + /// + [Description("При использовании КД выстрелов, разрешать ли двойной")] + public bool AllowDoubleShot { get; set; } = false; - /// - public override void Give(Player player, bool displayMessage = true) + /// + public override Item CreateItem() { - Item item = player.AddItem(Type); + Item item = base.CreateItem(); if (item is Firearm firearm) { if (!Attachments.IsEmpty()) firearm.AddAttachment(Attachments); - if (ClipSize > 0) - firearm.MagazineAmmo = ClipSize; + firearm.MagazineAmmo = firearm.MaxMagazineAmmo = ClipSize; + firearm.AmmoDrain = AmmoUsage; } - Log.Debug($"{nameof(Give)}: Adding {item.Serial} to tracker."); - TrackedSerials.Add(item.Serial); - - OnAcquired(player, item, displayMessage); + return item; } - /// + /// protected override void SubscribeEvents() { Exiled.Events.Handlers.Player.ReloadingWeapon += OnInternalReloading; - Exiled.Events.Handlers.Player.ReloadedWeapon += OnInternalReloaded; Exiled.Events.Handlers.Player.Shooting += OnInternalShooting; Exiled.Events.Handlers.Player.Shot += OnInternalShot; Exiled.Events.Handlers.Player.Hurting += OnInternalHurting; - Exiled.Events.Handlers.Item.ChangingAttachments += OnInternalChangingAttachment; + Exiled.Events.Handlers.Player.UnloadingWeapon += OnInternalUnloading; + Exiled.Events.Handlers.Item.ChangingAttachments += OnInternalChangingAttachments; base.SubscribeEvents(); } - /// + /// protected override void UnsubscribeEvents() { Exiled.Events.Handlers.Player.ReloadingWeapon -= OnInternalReloading; - Exiled.Events.Handlers.Player.ReloadedWeapon -= OnInternalReloaded; Exiled.Events.Handlers.Player.Shooting -= OnInternalShooting; Exiled.Events.Handlers.Player.Shot -= OnInternalShot; Exiled.Events.Handlers.Player.Hurting -= OnInternalHurting; - Exiled.Events.Handlers.Item.ChangingAttachments -= OnInternalChangingAttachment; + Exiled.Events.Handlers.Player.UnloadingWeapon -= OnInternalUnloading; + Exiled.Events.Handlers.Item.ChangingAttachments -= OnInternalChangingAttachments; base.UnsubscribeEvents(); } /// - /// Handles reloading for custom weapons. + /// Handles reloading for custom weapons. /// - /// . + /// . protected virtual void OnReloading(ReloadingWeaponEventArgs ev) { } @@ -188,128 +206,112 @@ protected virtual void OnReloaded(ReloadedWeaponEventArgs ev) /// /// Handles shooting for custom weapons. /// - /// . + /// . protected virtual void OnShooting(ShootingEventArgs ev) { } /// - /// Handles shot for custom weapons. + /// Handles shot for custom weapons. /// - /// . + /// . protected virtual void OnShot(ShotEventArgs ev) { } /// - /// Handles hurting for custom weapons. + /// Handles hurting for custom weapons. /// - /// . + /// . protected virtual void OnHurting(HurtingEventArgs ev) { - if (ev.IsAllowed && Damage >= 0) - ev.Amount = Damage; } /// - /// Handles attachment changing for custom weapons. + /// Handles unloading for custom weapons. /// - /// . - protected virtual void OnChangingAttachment(ChangingAttachmentsEventArgs ev) + /// . + protected virtual void OnUnloading(UnloadingWeaponEventArgs ev) + { + } + + private void OnInternalChangingAttachments(ChangingAttachmentsEventArgs ev) { + if (!Check(ev.Player.CurrentItem)) + return; + + IEnumerable newAttachments = ev.NewAttachmentIdentifiers.Except(ev.CurrentAttachmentIdentifiers); + if (!AllowAttachmentsChange || newAttachments.Any(x => BannedAttachments.Contains(x.Name))) + ev.IsAllowed = false; } private void OnInternalReloading(ReloadingWeaponEventArgs ev) { - if (!Check(ev.Item)) + if (!Check(ev.Player.CurrentItem)) return; - if (ClipSize > 0 && ev.Firearm.TotalAmmo >= ClipSize) + if (cooldownedPlayers.Contains(ev.Player)) { ev.IsAllowed = false; + ev.Player.ShowHint(string.Format(WeaponNotReady, FireCooldown)); return; } + Log.Debug($"{nameof(Name)}.{nameof(OnInternalReloading)}: Reloading weapon. Calling external reload event.."); OnReloading(ev); + + Log.Debug($"{nameof(Name)}.{nameof(OnInternalReloading)}: External event ended. {ev.IsAllowed}"); } - private void OnInternalReloaded(ReloadedWeaponEventArgs ev) + private void OnInternalShooting(ShootingEventArgs ev) { - if (!Check(ev.Item)) + if (!Check(ev.Player)) return; - if (ClipSize > 0) + if (cooldownedPlayers.Contains(ev.Player)) { - int ammoChambered = ((AutomaticActionModule?)ev.Firearm.Base.Modules.FirstOrDefault(x => x is AutomaticActionModule))?.SyncAmmoChambered ?? 0; - int ammoToGive = ClipSize - ammoChambered; - - AmmoType ammoType = ev.Firearm.AmmoType; - int firearmAmmo = ev.Firearm.MagazineAmmo; - int ammoDrop = -(ClipSize - firearmAmmo - ammoChambered); - - int ammoInInventory = ev.Player.GetAmmo(ammoType) + firearmAmmo; - if (ammoToGive < ammoInInventory) - { - ev.Firearm.MagazineAmmo = ammoToGive; - int newAmmo = ev.Player.GetAmmo(ammoType) + ammoDrop; - ev.Player.SetAmmo(ammoType, (ushort)newAmmo); - } - else - { - ev.Firearm.MagazineAmmo = ammoInInventory; - ev.Player.SetAmmo(ammoType, 0); - } + ev.IsAllowed = false; + ev.Player.ShowHint(string.Format(WeaponNotReady, FireCooldown)); + Log.Debug($"Disallowed shot from cooldowned on {Name} player {ev.Player.Nickname}"); + return; } - OnReloaded(ev); - } + if (!AllowDoubleShot) + cooldownedPlayers.Add(ev.Player); - private void OnInternalShooting(ShootingEventArgs ev) - { - if (!Check(ev.Item)) - return; + Timing.CallDelayed(FireCooldown, () => + { + cooldownedPlayers.Remove(ev.Player); + Log.Debug($"Cooldown of {Name} removed from player {ev.Player.Nickname}"); + }); OnShooting(ev); } private void OnInternalShot(ShotEventArgs ev) { - if (!Check(ev.Item)) + Item curItem = ev.Player.CurrentItem; + if (!Check(curItem)) return; OnShot(ev); + if (ForceResetWeaponOnShot) + Timing.RunCoroutine(ResetWeapon(ev.Player)); } - private void OnInternalHurting(HurtingEventArgs ev) + private IEnumerator ResetWeapon(Player player) { - if (ev.Attacker is null) - { - return; - } - - if (ev.Player is null) - { - Log.Debug($"{Name}: {nameof(OnInternalHurting)}: target null"); - return; - } - - if (!Check(ev.Attacker.CurrentItem)) - { - Log.Debug($"{Name}: {nameof(OnInternalHurting)}: !Check()"); - return; - } - - if (ev.Attacker == ev.Player) - { - Log.Debug($"{Name}: {nameof(OnInternalHurting)}: attacker == target"); - return; - } + Item curItem = player.CurrentItem; + yield return Timing.WaitForSeconds(0.01f); + player.CurrentItem = null; + yield return Timing.WaitForSeconds(0.08f); + player.CurrentItem = curItem; + } - if (ev.DamageHandler is null) - { - Log.Debug($"{Name}: {nameof(OnInternalHurting)}: Handler null"); + private void OnInternalHurting(HurtingEventArgs ev) + { + if (ev.Attacker is null || ev.Player is null || ev.Attacker == ev.Player || !Check(ev.Attacker.CurrentItem) || ev.DamageHandler == null) return; - } if (!ev.DamageHandler.CustomBase.BaseIs(out FirearmDamageHandler firearmDamageHandler)) { @@ -323,21 +325,31 @@ private void OnInternalHurting(HurtingEventArgs ev) return; } - if (!FriendlyFire && (ev.Attacker.Role.Team == ev.Player.Role.Team)) + ev.Amount = Damage; + if (ev.Player.IsHuman && ArmorAndZoneDamageMultipliers.TryGetValue(ev.Player.CurrentArmor?.Type ?? ItemType.None, out Dictionary dic) && + dic.TryGetValue(firearmDamageHandler.Hitbox, out float multiplier)) { - Log.Debug($"{Name}: {nameof(OnInternalHurting)}: FF is disabled for this weapon!"); - return; + Log.Debug($"{Name}: {nameof(OnInternalHurting)}: Found damage muptiplier for armor/hitbox {multiplier}"); + ev.Amount *= multiplier; + } + + if (RoleDamageMultipliers.TryGetValue(ev.Player.Role.Type, out multiplier)) + { + Log.Debug($"{Name}: {nameof(OnInternalHurting)}: Found damage muptiplier for target role: {multiplier}"); + ev.Amount *= multiplier; } OnHurting(ev); } - private void OnInternalChangingAttachment(ChangingAttachmentsEventArgs ev) + private void OnInternalUnloading(UnloadingWeaponEventArgs ev) { - if (!Check(ev.Player.CurrentItem)) + if (!Check(ev.Firearm)) return; - OnChangingAttachment(ev); + ev.IsAllowed = CanUnload; + + OnUnloading(ev); } } } diff --git a/EXILED/Exiled.CustomItems/Commands/Give.cs b/EXILED/Exiled.CustomItems/Commands/Give.cs index cc63ed2228..e51a2dd661 100644 --- a/EXILED/Exiled.CustomItems/Commands/Give.cs +++ b/EXILED/Exiled.CustomItems/Commands/Give.cs @@ -12,29 +12,16 @@ namespace Exiled.CustomItems.Commands using System.Linq; using CommandSystem; - using Exiled.API.Features; using Exiled.CustomItems.API.Features; using Exiled.Permissions.Extensions; - using RemoteAdmin; - using UnityStandardAssets.Effects; - using Utils; /// /// The command to give a player an item. /// internal sealed class Give : ICommand { - private Give() - { - } - - /// - /// Gets the instance. - /// - public static Give Instance { get; } = new(); - /// public string Command { get; } = "give"; @@ -42,26 +29,83 @@ private Give() public string[] Aliases { get; } = { "g" }; /// - public string Description { get; } = "Gives a custom item."; + public string Description { get; set; } = "Дает кастомный предмет."; + + /// + /// Gets or sets message displayed when the user does not have the required permission. + /// + public string PermissionRequiredMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets message displayed when the user uses the command incorrectly. + /// + public string UsageMessage { get; set; } = "give <Название/ID кастомного предмета> [Никнейм/ID/SteamID игрока или */all для выдачи всем]"; + + /// + /// Gets or sets message displayed when the specified item is not found. + /// + /// + /// The {0} placeholder will be replaced with the item name. + /// + public string ItemNotFoundMessage { get; set; } = "Кастомный предмет {0} не найден!"; + + /// + /// Gets or sets message displayed when the specified player is not found. + /// + public string PlayerNotFoundMessage { get; set; } = "Игрок не найден."; + + /// + /// Gets or sets message displayed when the player is not eligible to receive the item. + /// + public string NotEligibleMessage { get; set; } = "Вы не можете получить кастомный предмет!"; + + /// + /// Gets or sets message displayed when the item is successfully given to a player. + /// + /// + /// The {0} placeholder will be replaced with the item name, {1} with the player's nickname, and {2} with the player's ID. + /// + public string ItemGivenMessage { get; set; } = "{0} дан игроку {1} ({2})"; + + /// + /// Gets or sets message displayed when the item is successfully given to all players. + /// + /// + /// The {0} placeholder will be replaced with the item name, and {1} with the number of players. + /// + public string ItemGivenToAllMessage { get; set; } = "Кастомный предмет {0} дан ({1} игрокам)"; + + /// + /// Gets or sets message displayed when a player is not eligible to receive the item. + /// + public string PlayerNotEligibleMessage { get; set; } = "Игрок не можете получить кастомный предмет!"; + + /// + /// Gets or sets message displayed when a player cannot be found by their identifier. + /// + /// + /// The {0} placeholder will be replaced with the identifier. + /// + public string PlayerNotFoundByIdentifierMessage { get; set; } = "Невозможно найти игрока: {0}."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission("customitems.give")) { - response = "Permission Denied, required: customitems.give"; + response = PermissionRequiredMessage; return false; } if (arguments.Count == 0) { - response = "give [Nickname/PlayerID/UserID/all/*]"; + response = UsageMessage; return false; } if (!CustomItem.TryGet(arguments.At(0), out CustomItem? item)) { - response = $"Custom item {arguments.At(0)} not found!"; + response = string.Format(ItemNotFoundMessage, arguments.At(0)); return false; } @@ -73,16 +117,16 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s if (!CheckEligible(player)) { - response = "You cannot receive custom items!"; + response = NotEligibleMessage; return false; } item?.Give(player); - response = $"{item?.Name} given to {player.Nickname} ({player.UserId})"; + response = string.Format(ItemGivenMessage, item?.Name, player.Nickname, player.UserId); return true; } - response = "Failed to provide a valid player, please follow the syntax."; + response = PlayerNotFoundMessage; return false; } @@ -96,30 +140,25 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s foreach (Player ply in eligiblePlayers) item?.Give(ply); - response = $"Custom item {item?.Name} given to all players who can receive them ({eligiblePlayers.Count} players)"; + response = string.Format(ItemGivenToAllMessage, item?.Name, eligiblePlayers.Count); return true; default: - break; - } - - IEnumerable list = Player.GetProcessedData(arguments, 1); + if (Player.Get(identifier) is not { } player) + { + response = string.Format(PlayerNotFoundByIdentifierMessage, identifier); + return false; + } - if (list.IsEmpty()) - { - response = "Cannot find player! Try using the player ID!"; - return false; - } + if (!CheckEligible(player)) + { + response = PlayerNotEligibleMessage; + return false; + } - foreach (Player player in list) - { - if (CheckEligible(player)) - { item?.Give(player); - } + response = string.Format(ItemGivenMessage, item?.Name, player.Nickname, player.UserId); + return true; } - - response = $"{item?.Name} given to {list.Count()} players!"; - return true; } /// @@ -127,4 +166,4 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s /// private bool CheckEligible(Player player) => player.IsAlive && !player.IsCuffed && (player.Items.Count < 8); } -} +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/Commands/Info.cs b/EXILED/Exiled.CustomItems/Commands/Info.cs index bc48698b34..5cf5aacecc 100644 --- a/EXILED/Exiled.CustomItems/Commands/Info.cs +++ b/EXILED/Exiled.CustomItems/Commands/Info.cs @@ -11,7 +11,6 @@ namespace Exiled.CustomItems.Commands using System.Text; using CommandSystem; - using Exiled.API.Features.Pools; using Exiled.API.Features.Spawn; using Exiled.CustomItems.API.Features; @@ -22,68 +21,103 @@ namespace Exiled.CustomItems.Commands /// internal sealed class Info : ICommand { - private Info() - { - } + /// + /// Gets or sets the permission required to execute the info command. + /// + public string Permission { get; set; } = "customitems.info"; + + /// + /// Gets or sets the message displayed when the user does not have the required permission. + /// + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets the usage message displayed when the user provides invalid arguments. + /// + public string UsageMessage { get; set; } = "info [Название/ID кастомного предмета]"; + + /// + /// Gets or sets the message displayed when the specified item is not found. + /// + /// The {0} placeholder will be replaced with the item name or ID. + public string NotFoundMessage { get; set; } = "{0} не найден."; + + /// + /// Gets or sets the first color used in the item information display. + /// + public string Color1 { get; set; } = "#E6AC00"; + + /// + /// Gets or sets the second color used in the item information display. + /// + public string Color2 { get; set; } = "#00D639"; + + /// + /// Gets or sets the third color used in the item information display. + /// + public string Color3 { get; set; } = "#05C4EB"; + + /// + /// Gets or sets the label used to display the spawn limit. + /// + public string SpawnLimitLabel { get; set; } = "- Лимит спавна: "; + + /// + /// Gets or sets the label used to display the spawn points. + /// + /// The {0} placeholder will be replaced with the number of spawn points. + public string SpawnPointsLabel { get; set; } = "[Локации ({0})]"; /// - /// Gets the instance. + /// Gets or sets the format used to display individual spawn points. /// - public static Info Instance { get; } = new(); + /// The {0} placeholder will be replaced with the spawn point name, {1} with the position, and {2} with the chance. + public string SpawnPointFormat { get; set; } = "{0} {1} Шанс: {2}%"; /// - public string Command { get; } = "info"; + public string Command { get; set; } = "info"; /// - public string[] Aliases { get; } = { "i" }; + public string[] Aliases { get; set; } = { "i" }; /// - public string Description { get; } = "Gets more information about the specified custom item."; + public string Description { get; set; } = "Дает информацию о кастомном предмете."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { - if (!sender.CheckPermission("customitems.info")) + if (!sender.CheckPermission(Permission)) { - response = "Permission Denied, required: customitems.info"; + response = NoPermissionMessage; return false; } if (arguments.Count < 1) { - response = "info [Custom item name/Custom item ID]"; + response = UsageMessage; return false; } if (!(uint.TryParse(arguments.At(0), out uint id) && CustomItem.TryGet(id, out CustomItem? item)) && !CustomItem.TryGet(arguments.At(0), out item)) { - response = $"{arguments.At(0)} is not a valid custom item."; + response = string.Format(NotFoundMessage, arguments.At(0)); return false; } StringBuilder message = StringBuilderPool.Pool.Get().AppendLine(); - message.Append("- ").Append(item?.Name).Append(" (").Append(item?.Id).AppendLine(")") + message.Append("- ").Append(item?.Name).Append(" (").Append(item?.Id).AppendLine(")") .Append("- ").AppendLine(item?.Description) .AppendLine(item?.Type.ToString()) - .Append("- Spawn Limit: ").AppendLine(item?.SpawnProperties?.Limit.ToString()).AppendLine() - .Append("[Spawn Locations (").Append(item?.SpawnProperties?.Count()).AppendLine(")]"); + .Append(SpawnLimitLabel).AppendLine(item?.SpawnProperties?.Limit.ToString()).AppendLine() + .Append(string.Format(SpawnPointsLabel, item?.SpawnProperties?.DynamicSpawnPoints.Count + item?.SpawnProperties?.StaticSpawnPoints.Count)); foreach (DynamicSpawnPoint spawnPoint in item?.SpawnProperties?.DynamicSpawnPoints!) - message.Append(spawnPoint.Name).Append(' ').Append(spawnPoint.Position).Append(" Chance: ").Append(spawnPoint.Chance).AppendLine("%"); + message.Append(string.Format(SpawnPointFormat, spawnPoint.Name, spawnPoint.Position, spawnPoint.Chance)).AppendLine("%"); foreach (StaticSpawnPoint spawnPoint in item.SpawnProperties.StaticSpawnPoints) - message.Append(spawnPoint.Name).Append(' ').Append(spawnPoint.Position).Append(" Chance: ").Append(spawnPoint.Chance).AppendLine("%"); - - foreach (RoleSpawnPoint spawnPoint in item.SpawnProperties.RoleSpawnPoints) - message.Append(spawnPoint.Name).Append(' ').Append(spawnPoint.Position).Append(" Chance: ").Append(spawnPoint.Chance).AppendLine("%"); - - foreach (LockerSpawnPoint spawnPoint in item.SpawnProperties.LockerSpawnPoints) - message.Append(spawnPoint.Name).Append(' ').Append(spawnPoint.Position).Append(" Chance: ").Append(spawnPoint.Chance).AppendLine("%"); - - foreach (RoomSpawnPoint spawnPoint in item.SpawnProperties.RoomSpawnPoints) - message.Append(spawnPoint.Name).Append(' ').Append(spawnPoint.Position).Append(" Chance: ").Append(spawnPoint.Chance).AppendLine("%"); + message.Append(string.Format(SpawnPointFormat, spawnPoint.Name, spawnPoint.Position, spawnPoint.Chance)).AppendLine("%"); response = StringBuilderPool.Pool.ToStringReturn(message); return true; diff --git a/EXILED/Exiled.CustomItems/Commands/List/List.cs b/EXILED/Exiled.CustomItems/Commands/List/List.cs index d564d96499..01f03bd015 100644 --- a/EXILED/Exiled.CustomItems/Commands/List/List.cs +++ b/EXILED/Exiled.CustomItems/Commands/List/List.cs @@ -8,52 +8,29 @@ namespace Exiled.CustomItems.Commands.List { using System; + using System.Collections.Generic; - using CommandSystem; + using Exiled.API.Features; /// /// The command to list all installed items. /// internal sealed class List : ParentCommand { - private List() - { - LoadGeneratedCommands(); - } - - /// - /// Gets the instance. - /// - public static List Instance { get; } = new(); - /// public override string Command { get; } = "list"; /// - public override string[] Aliases { get; } = { "s", "l", "show", "sh" }; + public override string[] Aliases { get; set; } = { "s", "l", "show", "sh" }; /// - public override string Description { get; } = "Gets a list of all currently registered custom items."; + public override string Description { get; set; } = "Gets a list of all currently registered custom items."; /// - public override void LoadGeneratedCommands() + protected override IEnumerable CommandsToRegister() { - RegisterCommand(Registered.Instance); - RegisterCommand(Tracked.Instance); - } - - /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) - { - if (arguments.IsEmpty() && TryGetCommand(Registered.Instance.Command, out ICommand command)) - { - command.Execute(arguments, sender, out response); - response += $"\nTo view custom items in players' inventories, use the command: {string.Join(" ", arguments.Array)} insideinventories"; - return true; - } - - response = "Invalid subcommand! Available: registered, insideinventories"; - return false; + yield return typeof(Registered); + yield return typeof(Tracked); } } } \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/Commands/List/Registered.cs b/EXILED/Exiled.CustomItems/Commands/List/Registered.cs index 70ffe5e266..b0054f5b28 100644 --- a/EXILED/Exiled.CustomItems/Commands/List/Registered.cs +++ b/EXILED/Exiled.CustomItems/Commands/List/Registered.cs @@ -20,15 +20,6 @@ namespace Exiled.CustomItems.Commands.List /// internal sealed class Registered : ICommand { - private Registered() - { - } - - /// - /// Gets the command instance. - /// - public static Registered Instance { get; } = new(); - /// public string Command { get; } = "registered"; @@ -36,35 +27,49 @@ private Registered() public string[] Aliases { get; } = { "r", "reg" }; /// - public string Description { get; } = "Gets a list of registered custom items."; + public string Description { get; set; } = "Получает все зарегистрированные кастомные предметы."; + + /// + /// Gets or sets the message displayed when a user does not have the required permission. + /// + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets the message displayed when there are no custom items on the server. + /// + public string NoCustomItemsMessage { get; set; } = "На сервере нет кастомных предметов."; + + /// + /// Gets or sets the header format for displaying custom items, where {0} is the number of custom items. + /// + public string CustomItemsHeader { get; set; } = "[Кастомные предметы ({0})]"; + + /// + /// Gets or sets the format for displaying a single custom item, where {0} is the item ID, {1} is the item name, and {2} is the item type. + /// + public string CustomItemFormat { get; set; } = "[{0}. {1} ({2})]"; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission("customitems.list.registered")) { - response = "Permission Denied, required: customitems.list.registered"; - return false; - } - - if (arguments.Count != 0) - { - response = "list registered"; + response = NoPermissionMessage; return false; } if (CustomItem.Registered.Count == 0) { - response = "There are no custom items currently on this server."; + response = NoCustomItemsMessage; return false; } StringBuilder message = StringBuilderPool.Pool.Get().AppendLine(); - message.Append("[Registered custom items (").Append(CustomItem.Registered.Count).AppendLine(")]"); + message.Append(string.Format(CustomItemsHeader, CustomItem.Registered.Count)); foreach (CustomItem customItem in CustomItem.Registered.OrderBy(item => item.Id)) - message.Append('[').Append(customItem.Id).Append(". ").Append(customItem.Name).Append(" (").Append(customItem.Type).Append(')').AppendLine("]"); + message.Append(string.Format(CustomItemFormat, customItem.Id, customItem.Name, customItem.Type)).AppendLine(); response = StringBuilderPool.Pool.ToStringReturn(message); return true; diff --git a/EXILED/Exiled.CustomItems/Commands/List/Tracked.cs b/EXILED/Exiled.CustomItems/Commands/List/Tracked.cs index c04e50fe64..72a554c3d8 100644 --- a/EXILED/Exiled.CustomItems/Commands/List/Tracked.cs +++ b/EXILED/Exiled.CustomItems/Commands/List/Tracked.cs @@ -12,47 +12,73 @@ namespace Exiled.CustomItems.Commands.List using System.Text; using CommandSystem; - using Exiled.API.Features; using Exiled.API.Features.Pools; using Exiled.CustomItems.API.Features; using Exiled.Permissions.Extensions; - using RemoteAdmin; /// internal sealed class Tracked : ICommand { - private Tracked() - { - } + /// + public string Command { get; set; } = "insideinventories"; + + /// + public string[] Aliases { get; set; } = { "ii", "inside", "inv", "inventories" }; + + /// + public string Description { get; set; } = "Получает все предметы которые лежат в инвенторях игроков."; /// - /// Gets the command instance. + /// Gets or sets the message displayed when a user does not have the required permission. /// - public static Tracked Instance { get; } = new(); + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; - /// - public string Command { get; } = "insideinventories"; + /// + /// Gets or sets the message displayed when no custom items are found. + /// + public string NoCustomItemsMessage { get; set; } = "Кастомные предметы не найдены."; - /// - public string[] Aliases { get; } = { "ii", "inside", "inv", "inventories" }; + /// + /// Gets or sets the format for the title of the custom items list, where {0} is the count of items. + /// + public string TitleFormat { get; set; } = "[Custom items inside inventories ({0})]"; - /// - public string Description { get; } = "Gets a list of custom items actually inside of players' inventories."; + /// + /// Gets or sets the format for a single custom item in the list, where: + /// {0} is the item ID, + /// {1} is the item name, + /// {2} is the item type, + /// {3} is the count of items. + /// + public string ItemFormat { get; set; } = "[{0}. {1} ({2}) {{ {3} }}]"; + + /// + /// Gets or sets the format for a serial number in the list, where {0} is the serial number. + /// + public string SerialFormat { get; set; } = "{0}. "; + + /// + /// Gets or sets the message displayed when an item has no owner. + /// + public string NoOwnerMessage { get; set; } = "Никто"; + + /// + /// Gets or sets the format for an item owner, where: + /// {0} is the owner's nickname, + /// {1} is the owner's user ID, + /// {2} is the owner's ID, + /// {3} is the owner's role. + /// + public string OwnerFormat { get; set; } = "{0} ({1}) ({2}) [{3}]"; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission("customitems.list.insideinventories") && sender is PlayerCommandSender playerSender && !playerSender.FullPermissions) { - response = "Permission Denied, required: customitems.list.insideinventories"; - return false; - } - - if (arguments.Count != 0) - { - response = "list insideinventories"; + response = NoPermissionMessage; return false; } @@ -66,8 +92,8 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s continue; message.AppendLine() - .Append('[').Append(customItem.Id).Append(". ").Append(customItem.Name).Append(" (").Append(customItem.Type).Append(')') - .Append(" {").Append(customItem.TrackedSerials.Count).AppendLine("}]").AppendLine(); + .AppendFormat(ItemFormat, customItem.Id, customItem.Name, customItem.Type, customItem.TrackedSerials.Count) + .AppendLine(); count += customItem.TrackedSerials.Count; @@ -75,19 +101,19 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { Player owner = Player.List.FirstOrDefault(player => player.Inventory.UserInventory.Items.Any(item => item.Key == insideInventory)); - message.Append(insideInventory).Append(". "); + message.AppendFormat(SerialFormat, insideInventory); if (owner is null) - message.AppendLine("Nobody"); + message.AppendLine(NoOwnerMessage); else - message.Append(owner.Nickname).Append(" (").Append(owner.UserId).Append(") (").Append(owner.Id).Append(") [").Append(owner.Role).AppendLine("]"); + message.AppendFormat(OwnerFormat, owner.Nickname, owner.UserId, owner.Id, owner.Role).AppendLine(); } } if (message.Length == 0) - message.Append("There are no custom items inside inventories."); + message.Append(NoCustomItemsMessage); else - message.Insert(0, Environment.NewLine + "[Custom items inside inventories (" + count + ")]" + Environment.NewLine); + message.Insert(0, Environment.NewLine + string.Format(TitleFormat, count) + Environment.NewLine); response = StringBuilderPool.Pool.ToStringReturn(message); return true; diff --git a/EXILED/Exiled.CustomItems/Commands/Main.cs b/EXILED/Exiled.CustomItems/Commands/Main.cs index 7aba0b0366..8a4f5fff5e 100644 --- a/EXILED/Exiled.CustomItems/Commands/Main.cs +++ b/EXILED/Exiled.CustomItems/Commands/Main.cs @@ -8,8 +8,10 @@ namespace Exiled.CustomItems.Commands { using System; + using System.Collections.Generic; using CommandSystem; + using Exiled.API.Features; /// /// The main command. @@ -18,37 +20,22 @@ namespace Exiled.CustomItems.Commands [CommandHandler(typeof(GameConsoleCommandHandler))] internal sealed class Main : ParentCommand { - /// - /// Initializes a new instance of the class. - /// - public Main() - { - LoadGeneratedCommands(); - } - /// public override string Command { get; } = "customitems"; /// - public override string[] Aliases { get; } = { "ci", "cis" }; + public override string[] Aliases { get; set; } = { "ci", "cis" }; /// - public override string Description { get; } = string.Empty; - - /// - public override void LoadGeneratedCommands() - { - RegisterCommand(Give.Instance); - RegisterCommand(Spawn.Instance); - RegisterCommand(Info.Instance); - RegisterCommand(List.List.Instance); - } + public override string Description { get; set; } = string.Empty; /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + protected override IEnumerable CommandsToRegister() { - response = "Invalid subcommand! Available: give, spawn, info, list"; - return false; + yield return typeof(Give); + yield return typeof(Spawn); + yield return typeof(Info); + yield return typeof(List.List); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomItems/Commands/Spawn.cs b/EXILED/Exiled.CustomItems/Commands/Spawn.cs index 2ba955dc76..f792729a67 100644 --- a/EXILED/Exiled.CustomItems/Commands/Spawn.cs +++ b/EXILED/Exiled.CustomItems/Commands/Spawn.cs @@ -10,13 +10,9 @@ namespace Exiled.CustomItems.Commands using System; using CommandSystem; - - using Exiled.API.Enums; - using Exiled.API.Extensions; using Exiled.API.Features; using Exiled.CustomItems.API.Features; using Exiled.Permissions.Extensions; - using UnityEngine; /// @@ -24,80 +20,103 @@ namespace Exiled.CustomItems.Commands /// internal sealed class Spawn : ICommand { - private Spawn() - { - } + /// + /// Gets or sets message displayed when the user has insufficient permissions to execute the command. + /// + public string InsufficientPermissionsMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets message displayed when the user provides invalid arguments for the command. + /// + public string InvalidArgumentsMessage { get; set; } = "spawn [Название/ID кастомного предмета] [Никнейм/SteamID игока]\nspawn [Название/ID кастомного предмета] [X] [Y] [Z]"; + + /// + /// Gets or sets message displayed when the user tries to spawn an invalid custom item. + /// + public string InvalidCustomItemMessage { get; set; } = " {0} is not a valid custom item."; + + /// + /// Gets or sets message displayed when the target player is dead. + /// + public string PlayerIsDeadMessage { get; set; } = "Игрок мертв!"; + + /// + /// Gets or sets message displayed when the user provides invalid coordinates for the spawn location. + /// + public string InvalidCoordinatesMessage { get; set; } = "Невозможно получить координату (попробуй писать через , а не .)"; + + /// + /// Gets or sets message displayed when the system is unable to find a valid spawn location. + /// + public string UnableToFindLocationMessage { get; set; } = "Невозможно найти локацию для спавна."; /// - /// Gets the instance. + /// Gets or sets message displayed when the spawn is successful. /// - public static Spawn Instance { get; } = new(); + public string SpawnSuccessMessage { get; set; } = "{0} ({1}) заспавнился на позиции {2}."; /// - public string Command { get; } = "spawn"; + public string Command { get; set; } = "spawn"; /// - public string[] Aliases { get; } = { "sp" }; + public string[] Aliases { get; set; } = { "sp" }; /// - public string Description { get; } = "Spawn an item at the specified Spawn Location, coordinates, or at the designated player's feet."; + public string Description { get; set; } = "Спавнит кастомный предмет."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) { if (!sender.CheckPermission("customitems.spawn")) { - response = "Permission Denied, required: customitems.spawn"; + response = InsufficientPermissionsMessage; return false; } if (arguments.Count < 2) { - response = "spawn [Custom item name] [Location name]\nspawn [Custom item name] [Nickname/PlayerID/UserID]\nspawn [Custom item name] [X] [Y] [Z]"; + response = InvalidArgumentsMessage; return false; } if (!CustomItem.TryGet(arguments.At(0), out CustomItem? item)) { - response = $" {arguments.At(0)} is not a valid custom item."; + response = string.Format(InvalidCustomItemMessage, arguments.At(0)); return false; } Vector3 position; - if (arguments.Count > 3) + + if (Player.Get(arguments.At(1)) is Player player) { - if (!float.TryParse(arguments.At(1), out float x) || !float.TryParse(arguments.At(2), out float y) || !float.TryParse(arguments.At(3), out float z)) + if (player.IsDead) { - response = "Invalid coordinates selected."; + response = PlayerIsDeadMessage; return false; } - position = new Vector3(x, y, z); + position = player.Position; } - else if (Player.Get(arguments.At(1)) is Player player) + else if (arguments.Count > 3) { - if (player.IsDead) + if (!float.TryParse(arguments.At(1), out float x) || !float.TryParse(arguments.At(2), out float y) || !float.TryParse(arguments.At(3), out float z)) { - response = $"Cannot spawn custom items under dead players!"; + response = InvalidCoordinatesMessage; return false; } - position = player.Position; - } - else if (Enum.TryParse(arguments.At(1), out SpawnLocationType location)) - { - position = location.GetPosition(); + position = new Vector3(x, y, z); } else { - response = $"Unable to find spawn location: {arguments.At(1)}."; + response = UnableToFindLocationMessage; return false; } item?.Spawn(position); - response = $"{item?.Name} ({item?.Type}) has been spawned at {position}."; + response = string.Format(SpawnSuccessMessage, item?.Name, item?.Type, position); return true; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomItems/CustomItems.cs b/EXILED/Exiled.CustomItems/CustomItems.cs index adac9294ae..10dd31d57a 100644 --- a/EXILED/Exiled.CustomItems/CustomItems.cs +++ b/EXILED/Exiled.CustomItems/CustomItems.cs @@ -19,9 +19,8 @@ namespace Exiled.CustomItems /// public class CustomItems : Plugin { - private MapHandler? roundHandler; - private PlayerHandler? playerHandler; - private Harmony? harmony; + private RoundHandler roundHandler = null!; + private Harmony harmony = null!; /// /// Gets the static reference to this class. @@ -32,13 +31,11 @@ public class CustomItems : Plugin public override void OnEnabled() { Instance = this; - roundHandler = new MapHandler(); - playerHandler = new PlayerHandler(); + roundHandler = new RoundHandler(); + Exiled.Events.Handlers.Server.RoundStarted += roundHandler.OnRoundStarted; Exiled.Events.Handlers.Server.WaitingForPlayers += roundHandler.OnWaitingForPlayers; - Exiled.Events.Handlers.Player.ChangingItem += playerHandler.OnChangingItem; - harmony = new Harmony($"com.{nameof(CustomItems)}.ExiledTeam-{DateTime.Now.Ticks}"); GlobalPatchProcessor.PatchAll(harmony, out int failedPatch); if (failedPatch != 0) @@ -50,13 +47,12 @@ public override void OnEnabled() /// public override void OnDisabled() { - Exiled.Events.Handlers.Server.WaitingForPlayers -= roundHandler!.OnWaitingForPlayers; - - Exiled.Events.Handlers.Player.ChangingItem -= playerHandler!.OnChangingItem; + Exiled.Events.Handlers.Server.RoundStarted -= roundHandler.OnRoundStarted; + Exiled.Events.Handlers.Server.WaitingForPlayers -= roundHandler.OnWaitingForPlayers; harmony?.UnpatchAll(); base.OnDisabled(); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomItems/Events/MapHandler.cs b/EXILED/Exiled.CustomItems/Events/MapHandler.cs deleted file mode 100644 index ed1ae61970..0000000000 --- a/EXILED/Exiled.CustomItems/Events/MapHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomItems.Events -{ - using Exiled.API.Features; - using Exiled.CustomItems.API.Features; - using MEC; - - /// - /// Event Handlers for the CustomItem API. - /// - internal sealed class MapHandler - { - /// - public void OnWaitingForPlayers() - { - Timing.CallDelayed(2, () => // The delay is necessary because the generation of the lockers takes time, due to the way they are made in the base game. - { - foreach (CustomItem customItem in CustomItem.Registered) - { - try - { - customItem?.SpawnAll(); - } - catch (System.Exception e) - { - Log.Error($"There was an error while spawning the custom item '{customItem?.Name}' ({customItem?.Id}) | {e.Message}"); - } - } - }); - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/Events/PlayerHandler.cs b/EXILED/Exiled.CustomItems/Events/PlayerHandler.cs deleted file mode 100644 index d8ded30782..0000000000 --- a/EXILED/Exiled.CustomItems/Events/PlayerHandler.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomItems.Events -{ - using Exiled.API.Extensions; - using Exiled.API.Features; - using Exiled.CustomItems.API.Features; - using Exiled.Events.EventArgs.Player; - - /// - /// Handles Player events for the CustomItem API. - /// - internal sealed class PlayerHandler - { - /// - public void OnChangingItem(ChangingItemEventArgs ev) - { - if (!ev.IsAllowed) - return; - if (CustomItem.TryGet(ev.Item, out CustomItem? newItem) && (newItem?.ShouldMessageOnGban ?? false)) - { - SpectatorCustomNickname(ev.Player, $"{ev.Player.CustomName} (CustomItem: {newItem.Name})"); - } - else if (ev.Player != null && CustomItem.TryGet(ev.Player.CurrentItem, out _)) - { - SpectatorCustomNickname(ev.Player, ev.Player.HasCustomName ? ev.Player.CustomName : string.Empty); - } - } - - private void SpectatorCustomNickname(Player player, string itemName) - { - foreach (Player spectator in Player.List) - spectator.SendFakeSyncVar(player.NetworkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_displayName), itemName); - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/Events/RoundHandler.cs b/EXILED/Exiled.CustomItems/Events/RoundHandler.cs new file mode 100644 index 0000000000..615dcde3e0 --- /dev/null +++ b/EXILED/Exiled.CustomItems/Events/RoundHandler.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomItems.Events +{ + using Exiled.CustomItems.API.Features; + using Exiled.Events.Handlers; + + /// + /// Event Handlers for the CustomItem API. + /// + internal sealed class RoundHandler + { + /// + public void OnRoundStarted() + { + foreach (CustomItem customItem in CustomItem.Registered) + customItem?.SpawnAll(); + } + + /// + public void OnWaitingForPlayers() + { + foreach (CustomItem? customItem in CustomItem.Registered) + customItem.TrackedSerials.Clear(); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomItems/Exiled.CustomItems.csproj b/EXILED/Exiled.CustomItems/Exiled.CustomItems.csproj index 6d45708c17..4f774538b6 100644 --- a/EXILED/Exiled.CustomItems/Exiled.CustomItems.csproj +++ b/EXILED/Exiled.CustomItems/Exiled.CustomItems.csproj @@ -20,8 +20,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -31,6 +34,7 @@ + @@ -40,7 +44,13 @@ - if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/"; fi diff --git a/EXILED/Exiled.CustomRoles/API/Extensions.cs b/EXILED/Exiled.CustomRoles/API/Extensions.cs index 8a0821216d..f9fe086339 100644 --- a/EXILED/Exiled.CustomRoles/API/Extensions.cs +++ b/EXILED/Exiled.CustomRoles/API/Extensions.cs @@ -10,32 +10,49 @@ namespace Exiled.CustomRoles.API using System; using System.Collections.Generic; using System.Collections.ObjectModel; - using System.Linq; using Exiled.API.Features; - using Exiled.CustomRoles.API.Features; - using Exiled.CustomRoles.API.Features.Enums; - using Utils.NonAllocLINQ; + using Features; /// - /// A collection of API methods. + /// A collection of API methods. /// public static class Extensions { /// - /// Gets a of the player's current custom roles. + /// Gets a of all players that have custom role. /// - /// The to check for roles. - /// A of all current custom roles. + public static IReadOnlyDictionary PlayerToCustomRoles => InternalPlayerToCustomRoles; + + /// + /// Gets a of all players that have custom role. + /// + internal static Dictionary InternalPlayerToCustomRoles { get; } = new(); + + /// + /// Gets a containing all temp cache-roles for setting position. + /// + internal static Dictionary ToChangeRolePlayers { get; } = new(); + + /// + /// Gets a containing all players that should receive customrole inventory. + /// + internal static HashSet AssignInventoryPlayers { get; } = new(); + + /// + /// Gets a of the player's current custom roles. + /// + /// The to check for roles. + /// A of all current custom roles. + [Obsolete("Для ФЛХ используйте GetCustomRole!!!", true)] public static ReadOnlyCollection GetCustomRoles(this Player player) { List roles = new(); - foreach (CustomRole customRole in CustomRole.Registered) + if (TryGetCustomRole(player, out CustomRole role)) { - if (customRole.Check(player)) - roles.Add(customRole); + roles.Add(role); } return roles.AsReadOnly(); @@ -63,65 +80,112 @@ public static bool HasAnyCustomRole(this Player player) /// /// Registers an of s. /// - /// s to be registered. - public static void Register(this IEnumerable customRoles) + /// The to check for role. + /// A target (can be null). + public static CustomRole? GetCustomRole(this Player player) { - if (customRoles is null) - throw new ArgumentNullException(nameof(customRoles)); + if (player != null && PlayerToCustomRoles.TryGetValue(player, out CustomRole? role)) + return role; + return null; + } - foreach (CustomRole customRole in customRoles) - customRole.TryRegister(); + /// + /// Gets a of the player. + /// + /// The to check for role. + /// A target . + /// A boolean indicating whether or not a custom role was found. + public static bool TryGetCustomRole(this Player player, out CustomRole customRole) + { + return (customRole = GetCustomRole(player) !) is not null; } /// - /// Registers a . + /// Gets a value indicating whether or not player has any . /// - /// to be registered. - public static void Register(this CustomRole role) => role.TryRegister(); + /// The to check for role. + /// A boolean indicating whether or not player has . + public static bool HasCustomRole(this Player player) + { + return player.GetCustomRole() is not null; + } + + /// + /// Gets a specific by type of the player. + /// + /// The to check for role. + /// The specified type. + /// A target (can be null). + public static T? GetCustomRole(this Player player) + where T : CustomRole + { + if (player != null && PlayerToCustomRoles.TryGetValue(player, out CustomRole? role) && role is T typedRole) + return typedRole; + + return null; + } /// - /// Registers a . + /// Gets a specific by type of the player. /// - /// The to be registered. - public static void Register(this CustomAbility ability) => ability.TryRegister(); + /// The to check for role. + /// A target . + /// The specified type. + /// A boolean indicating whether or not a custom role was found. + public static bool TryGetCustomRole(this Player player, out T customRole) + where T : CustomRole + { + return (customRole = GetCustomRole(player) !) is not null; + } /// - /// Unregisters an of s. + /// Gets a value indicating whether or not player has a specific by type. /// - /// s to be unregistered. - public static void Unregister(this IEnumerable customRoles) + /// The to check for role. + /// The specified type. + /// A boolean indicating whether or not player has specific . + public static bool HasCustomRole(this Player player) + where T : CustomRole + { + return player.GetCustomRole() is not null; + } + + /// + /// Registers an of s. + /// + /// s to be registered. + public static void Register(this IEnumerable customRoles) { if (customRoles is null) throw new ArgumentNullException(nameof(customRoles)); foreach (CustomRole customRole in customRoles) - customRole.TryUnregister(); + customRole.TryRegister(); } /// - /// Unregisters a . + /// Registers a . /// - /// to be unregistered. - public static void Unregister(this CustomRole role) => role.TryUnregister(); + /// to be registered. + public static void Register(this CustomRole role) => role.TryRegister(); /// - /// Unregisters a . + /// Unregisters an of s. /// - /// The to be unregistered. - public static void Unregister(this CustomAbility ability) => ability.TryUnregister(); + /// s to be unregistered. + public static void Unregister(this IEnumerable customRoles) + { + if (customRoles is null) + throw new ArgumentNullException(nameof(customRoles)); - /// - /// Gets all s a specific is able to use. - /// - /// The to get abilities for. - /// A of their active abilities, or if none. - public static IEnumerable? GetActiveAbilities(this Player player) => !ActiveAbility.AllActiveAbilities.TryGetValue(player, out HashSet abilities) ? null : abilities; + foreach (CustomRole customRole in customRoles) + customRole.TryUnregister(); + } /// - /// Gets the 's selected ability. + /// Unregisters a . /// - /// The to check. - /// The the has selected, or . - public static ActiveAbility? GetSelectedAbility(this Player player) => !ActiveAbility.AllActiveAbilities.TryGetValue(player, out HashSet abilities) ? null : abilities.FirstOrDefault(a => a.Check(player, CheckType.Selected)); + /// to be unregistered. + public static void Unregister(this CustomRole role) => role.TryUnregister(); } } diff --git a/EXILED/Exiled.CustomRoles/API/Features/ActiveAbility.cs b/EXILED/Exiled.CustomRoles/API/Features/ActiveAbility.cs index cadc7c63f0..ed61eadb01 100644 --- a/EXILED/Exiled.CustomRoles/API/Features/ActiveAbility.cs +++ b/EXILED/Exiled.CustomRoles/API/Features/ActiveAbility.cs @@ -11,236 +11,142 @@ namespace Exiled.CustomRoles.API.Features using System.Collections.Generic; using Exiled.API.Features; - using Exiled.CustomRoles.API.Features.Enums; using MEC; using YamlDotNet.Serialization; /// - /// The base class for active (on-use) abilities. + /// The base class for active (on-use) abilities. /// public abstract class ActiveAbility : CustomAbility { /// - /// Gets a containing all players with active abilities, and the abilities they have access to. - /// - public static Dictionary> AllActiveAbilities { get; } = new(); - - /// - /// Gets or sets how long the ability lasts. + /// Gets or sets how long the ability lasts. /// public abstract float Duration { get; set; } /// - /// Gets or sets how long must go between ability uses. + /// Gets or sets how long must go between ability uses. /// public abstract float Cooldown { get; set; } /// - /// Gets or sets an action to override the behavior of . - /// - [YamlIgnore] - public virtual Func? CanUseOverride { get; set; } - - /// - /// Gets the last time this ability was used. + /// Gets the last time this ability was used. /// [YamlIgnore] public Dictionary LastUsed { get; } = new(); /// - /// Gets all players actively using this ability. + /// Gets all players actively using this ability. /// [YamlIgnore] public HashSet ActivePlayers { get; } = new(); /// - /// Gets all players who have this ability selected. + /// Uses the ability. /// - [YamlIgnore] - public HashSet SelectedPlayers { get; } = new(); - - /// - /// Uses the ability. - /// - /// The using the ability. - public void UseAbility(Player player) + /// The using the ability. + public virtual void UseAbility(Player player) { ActivePlayers.Add(player); LastUsed[player] = DateTime.Now; + ShowMessage(player); AbilityUsed(player); - Timing.CallDelayed(Duration, () => EndAbility(player)); + Timing.CallDelayed(Cooldown, () => RemindAbility(player)); + if (Duration > 0) + Timing.CallDelayed(Duration, () => EndAbility(player)); } /// - /// Ends the ability. + /// Reminds if ability is ready. /// - /// The the ability is ended for. - public void EndAbility(Player player) + /// The the ability is ready for. + public void RemindAbility(Player player) { - if (!ActivePlayers.Contains(player)) + if (!base.Check(player) || !player.IsConnected || !LastUsed.TryGetValue(player, out DateTime dateTime) || Math.Abs((DateTime.Now - dateTime).TotalSeconds - Cooldown) > 1f || !CustomRoles.Instance!.Config.AbilityReadyHint.Show) return; - ActivePlayers.Remove(player); - AbilityEnded(player); + player.ShowHint(string.Format(CustomRoles.Instance!.Config.AbilityReadyHint.Content, Name, Description), CustomRoles.Instance.Config.AbilityReadyHint.Duration); } /// - /// Selects the ability. + /// Ends the ability. /// - /// The to select the ability. - public void SelectAbility(Player player) + /// The the ability is ended for. + public void EndAbility(Player player) { - if (!SelectedPlayers.Contains(player)) - { - SelectedPlayers.Add(player); - Selected(player); - } - } + if (!ActivePlayers.Contains(player)) + return; - /// - /// Un-Selects the ability. - /// - /// The to un-select the ability. - public void UnSelectAbility(Player player) - { - if (SelectedPlayers.Contains(player)) - { - SelectedPlayers.Remove(player); - if (Check(player, CheckType.Active)) - EndAbility(player); - Unselected(player); - } + ActivePlayers.Remove(player); + AbilityEnded(player); } /// - /// Checks if the specified player is using the ability. + /// Checks if the specified player is using the ability. /// - /// The to check. + /// The to check. /// True if the player is actively using the ability. - public override bool Check(Player player) => Check(player, CheckType.Active); + public override bool Check(Player player) => player is not null && ActivePlayers.Contains(player); /// - /// Checks if the specified meets certain check criteria. - /// - /// The to check. - /// The type of check to preform. - /// The results of the check. - /// : Checks if the ability is currently active for the player. - /// : Checks if the player has the ability selected. - /// : Checks if the player has the ability. - /// - /// This should never happen unless Joker fucks up. - public virtual bool Check(Player player, CheckType type) - { - if (player is null) - return false; - bool result = type switch - { - CheckType.Active => ActivePlayers.Contains(player), - CheckType.Selected => SelectedPlayers.Contains(player), - CheckType.Available => Players.Contains(player), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - - return result; - } - - /// - /// Checks to see if the ability is usable by the player. + /// Checks to see if the ability is usable by the player. /// /// The player to check. /// The response to send to the player. - /// Whether to disallow usage if the ability is not selected. /// True if the ability is usable. - public virtual bool CanUseAbility(Player player, out string response, bool selectedOnly = false) + public virtual bool CanUseAbility(Player player, out string response) { - if (CanUseOverride is not null) - { - response = string.Empty; - return CanUseOverride.Invoke(); - } - - if (selectedOnly && !SelectedPlayers.Contains(player)) - { - response = $"{Name} not selected."; - return false; - } - - if (!LastUsed.ContainsKey(player)) + if (!LastUsed.TryGetValue(player, out DateTime lastUsed)) { response = string.Empty; return true; } - DateTime usableTime = LastUsed[player] + TimeSpan.FromSeconds(Cooldown); + DateTime usableTime = lastUsed + TimeSpan.FromSeconds(Cooldown); if (DateTime.Now > usableTime) { response = string.Empty; - return true; } - response = - $"You must wait another {Math.Round((usableTime - DateTime.Now).TotalSeconds, 2)} seconds to use {Name}"; + Hint hint = CustomRoles.Instance!.Config.AbilityOnCooldownHint; + response = string.Format(hint.Content, Math.Round((usableTime - DateTime.Now).TotalSeconds, 2), Name); + if (hint.Show) + player.ShowHint(response, hint.Duration); return false; } - /// - protected override void AbilityAdded(Player player) - { - if (!AllActiveAbilities.ContainsKey(player)) - AllActiveAbilities.Add(player, new()); - - if (!AllActiveAbilities[player].Contains(this)) - AllActiveAbilities[player].Add(this); - base.AbilityAdded(player); - } - - /// + /// protected override void AbilityRemoved(Player player) { - if (!AllActiveAbilities.ContainsKey(player)) - return; - - SelectedPlayers.Remove(player); - - AllActiveAbilities[player].Remove(this); + LastUsed.Remove(player); base.AbilityRemoved(player); } /// - /// Called when the ability is used. + /// Called when the ability is used. /// - /// The using the ability. + /// The using the ability. protected virtual void AbilityUsed(Player player) { } /// - /// Called when the abilities duration has ended. + /// Called when the abilities duration has ended. /// - /// The the ability has ended for. + /// The the ability has ended for. protected virtual void AbilityEnded(Player player) { } /// - /// Called when the ability is selected. + /// Called when the ability is successfully used. /// - /// The selecting the ability. - protected virtual void Selected(Player player) - { - } - - /// - /// Called when the ability is un-selected. - /// - /// The un-selecting the ability. - protected virtual void Unselected(Player player) - { - } + /// The using the ability. + protected virtual void ShowMessage(Player player) => + player.ShowHint(string.Format(CustomRoles.Instance!.Config.UsedAbilityHint.Content, Name, Description), CustomRoles.Instance.Config.UsedAbilityHint.Duration); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomRoles/API/Features/CustomAbility.cs b/EXILED/Exiled.CustomRoles/API/Features/CustomAbility.cs index 1528d08fa3..674765dc71 100644 --- a/EXILED/Exiled.CustomRoles/API/Features/CustomAbility.cs +++ b/EXILED/Exiled.CustomRoles/API/Features/CustomAbility.cs @@ -9,72 +9,59 @@ namespace Exiled.CustomRoles.API.Features { using System; using System.Collections.Generic; - using System.ComponentModel; using System.Linq; - using System.Reflection; using Exiled.API.Features; - using Exiled.API.Features.Attributes; using Exiled.API.Interfaces; - using YamlDotNet.Serialization; /// - /// The custom ability base class. + /// The custom ability base class. /// - public abstract class CustomAbility + public abstract class CustomAbility : IAbstractResolvable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public CustomAbility() - { - AbilityType = GetType().Name; - } + public CustomAbility() => Init(); /// - /// Gets a list of all registered custom abilities. + /// Gets a list of all registered custom abilities. /// public static HashSet Registered { get; } = new(); /// - /// Gets or sets the name of the ability. + /// Gets all players who have this ability. /// - public abstract string Name { get; set; } - - /// - /// Gets or sets the description of the ability. - /// - public abstract string Description { get; set; } + [YamlIgnore] + public HashSet Players { get; } = new(); /// - /// Gets all players who have this ability. + /// Gets or sets the name of the ability. /// - [YamlIgnore] - public HashSet Players { get; } = new(); + public abstract string Name { get; set; } /// - /// Gets the for this ability. + /// Gets or sets the description of the ability. /// - [Description("Changing this will likely break your config.")] - public string AbilityType { get; } + public abstract string Description { get; set; } /// - /// Gets a by name. + /// Gets a by name. /// /// The name of the ability to get. - /// The ability, or if it doesn't exist. + /// The ability, or if it doesn't exist. public static CustomAbility? Get(string name) => Registered?.FirstOrDefault(r => r.Name == name); /// - /// Gets a by type. + /// Gets a by type. /// /// The type of the ability to get. - /// The type, or if it doesn't exist. + /// The type, or if it doesn't exist. public static CustomAbility? Get(Type type) => Registered?.FirstOrDefault(r => r.GetType() == type); /// - /// Tries to get a by type. + /// Tries to get a by type. /// /// The type of the ability to get. /// The custom ability. @@ -87,12 +74,12 @@ public static bool TryGet(Type type, out CustomAbility? customAbility) } /// - /// Tries to get a by name. + /// Tries to get a by name. /// /// The name of the ability to get. /// The custom ability. /// True if the ability exists. - /// If the name is or an empty string. + /// If the name is or an empty string. public static bool TryGet(string name, out CustomAbility? customAbility) { if (string.IsNullOrEmpty(name)) @@ -104,179 +91,16 @@ public static bool TryGet(string name, out CustomAbility? customAbility) } /// - /// Registers all the 's present in the current assembly. - /// - /// Whether to register by attribute. - /// A of which contains all registered 's. - /// - /// This is just a dumbed down version of for QoL, if you actually use , do not use this overload. - /// - public static IEnumerable RegisterAbilities(bool byAttribute = false) - { - if (byAttribute) - { - return RegisterAbilities(false, null); - } - - List abilities = new(); - - foreach (Type type in Assembly.GetCallingAssembly().GetTypes()) - { - if (type.IsAbstract || !type.IsSubclassOf(typeof(CustomAbility))) - continue; - - CustomAbility ability = (CustomAbility)Activator.CreateInstance(type); - - if (ability.TryRegister()) - abilities.Add(ability); - } - - return abilities; - } - - /// - /// Registers all the 's present in the current assembly. - /// - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). - /// The class to search properties for, if different from the plugin's config class. - /// A of which contains all registered 's. - public static IEnumerable RegisterAbilities(bool skipReflection = false, object? overrideClass = null) - { - List abilities = new(); - Assembly assembly = Assembly.GetCallingAssembly(); - foreach (Type type in assembly.GetTypes()) - { - if (type.BaseType != typeof(CustomAbility) || type.GetCustomAttribute(typeof(CustomAbilityAttribute)) is null) - continue; - - CustomAbility? customAbility = null; - - if (!skipReflection && Server.PluginAssemblies.ContainsKey(assembly)) - { - IPlugin plugin = Server.PluginAssemblies[assembly]; - - foreach (PropertyInfo property in overrideClass?.GetType().GetProperties() ?? - plugin.Config.GetType().GetProperties()) - { - if (property.PropertyType != type) - continue; - - customAbility = property.GetValue(overrideClass ?? plugin.Config) as CustomAbility; - break; - } - } - - if (customAbility is null) - customAbility = (CustomAbility)Activator.CreateInstance(type); - - if (customAbility.TryRegister()) - abilities.Add(customAbility); - } - - return abilities; - } - - /// - /// Registers all the 's present in the current assembly. - /// - /// The of containing the target types. - /// A value indicating whether the target types should be ignored. - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). - /// The class to search properties for, if different from the plugin's config class. - /// A of which contains all registered 's. - public static IEnumerable RegisterAbilities(IEnumerable targetTypes, bool isIgnored = false, bool skipReflection = false, object? overrideClass = null) - { - List abilities = new(); - Assembly assembly = Assembly.GetCallingAssembly(); - foreach (Type type in assembly.GetTypes()) - { - if (((type.BaseType != typeof(CustomAbility)) && !type.IsSubclassOf(typeof(CustomAbility))) || type.GetCustomAttribute(typeof(CustomAbilityAttribute)) is null || - (isIgnored && targetTypes.Contains(type)) || (!isIgnored && !targetTypes.Contains(type))) - continue; - - CustomAbility? customAbility = null; - - if (!skipReflection && Server.PluginAssemblies.ContainsKey(assembly)) - { - IPlugin plugin = Server.PluginAssemblies[assembly]; - - foreach (PropertyInfo property in overrideClass?.GetType().GetProperties() ?? plugin.Config.GetType().GetProperties()) - { - if (property.PropertyType != type) - continue; - - customAbility = property.GetValue(overrideClass ?? plugin.Config) as CustomAbility; - } - } - - if (customAbility is null) - customAbility = (CustomAbility)Activator.CreateInstance(type); - - if (customAbility.TryRegister()) - abilities.Add(customAbility); - } - - return abilities; - } - - /// - /// Unregisters all the 's present in the current assembly. - /// - /// A of which contains all unregistered 's. - public static IEnumerable UnregisterAbilities() - { - List unregisteredAbilities = new(); - - foreach (CustomAbility customAbility in Registered) - { - customAbility.TryUnregister(); - unregisteredAbilities.Add(customAbility); - } - - return unregisteredAbilities; - } - - /// - /// Unregisters all the 's present in the current assembly. - /// - /// The of containing the target types. - /// A value indicating whether the target types should be ignored. - /// A of which contains all unregistered 's. - public static IEnumerable UnregisterAbilities(IEnumerable targetTypes, bool isIgnored = false) - { - List unregisteredAbilities = new(); - - foreach (CustomAbility customAbility in Registered) - { - if ((targetTypes.Contains(customAbility.GetType()) && isIgnored) || (!targetTypes.Contains(customAbility.GetType()) && !isIgnored)) - continue; - - customAbility.TryUnregister(); - unregisteredAbilities.Add(customAbility); - } - - return unregisteredAbilities; - } - - /// - /// Unregisters all the 's present in the current assembly. + /// Checks to see if the specified player has this ability. /// - /// The of containing the target roles. - /// A value indicating whether the target abilities should be ignored. - /// A of which contains all unregistered 's. - public static IEnumerable UnregisterAbilities(IEnumerable targetAbilities, bool isIgnored = false) => UnregisterAbilities(targetAbilities.Select(x => x.GetType()), isIgnored); - - /// - /// Checks to see if the specified player has this ability. - /// - /// The to check. + /// The to check. /// True if the player has this ability. public virtual bool Check(Player player) => player is not null && Players.Contains(player); /// - /// Adds this ability to the player. + /// Adds this ability to the player. /// - /// The to give the ability to. + /// The to give the ability to. public void AddAbility(Player player) { Log.Debug($"Added {Name} to {player.Nickname}"); @@ -285,9 +109,9 @@ public void AddAbility(Player player) } /// - /// Removes this ability from the player. + /// Removes this ability from the player. /// - /// The to remove this ability from. + /// The to remove this ability from. public void RemoveAbility(Player player) { Log.Debug($"Removed {Name} from {player.Nickname}"); @@ -296,83 +120,41 @@ public void RemoveAbility(Player player) } /// - /// Initializes this ability. + /// Initializes this ability. /// public void Init() => SubscribeEvents(); /// - /// Destroys this ability. + /// Destroys this ability. /// public void Destroy() => UnsubscribeEvents(); /// - /// Tries to register this ability. - /// - /// True if the ability registered properly. - internal bool TryRegister() - { - if (!CustomRoles.Instance!.Config.IsEnabled) - return false; - - if (!Registered.Contains(this)) - { - Registered.Add(this); - Init(); - - Log.Debug($"{Name} has been successfully registered."); - - return true; - } - - Log.Warn($"Couldn't register {Name} as it already exists."); - - return false; - } - - /// - /// Tries to unregister this ability. - /// - /// True if the ability is unregistered properly. - internal bool TryUnregister() - { - Destroy(); - - if (!Registered.Remove(this)) - { - Log.Warn($"Cannot unregister {Name}, it hasn't been registered yet."); - - return false; - } - - return true; - } - - /// - /// Loads the internal event handlers for the ability. + /// Loads the internal event handlers for the ability. /// protected virtual void SubscribeEvents() { } /// - /// Unloads the internal event handlers for the ability. + /// Unloads the internal event handlers for the ability. /// protected virtual void UnsubscribeEvents() { } /// - /// Called when the ability is first added to the player. + /// Called when the ability is first added to the player. /// - /// The using the ability. + /// The using the ability. protected virtual void AbilityAdded(Player player) { } /// - /// Called when the ability is being removed. + /// Called when the ability is being removed. /// - /// The using the ability. + /// The using the ability. protected virtual void AbilityRemoved(Player player) { } diff --git a/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs b/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs index f29e2d525c..295c97828c 100644 --- a/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs +++ b/EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs @@ -9,154 +9,127 @@ namespace Exiled.CustomRoles.API.Features { using System; using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; using System.Linq; using System.Reflection; - using System.Text; + using CustomItems.API.Features; using Exiled.API.Enums; using Exiled.API.Extensions; using Exiled.API.Features; using Exiled.API.Features.Attributes; - using Exiled.API.Features.Items; using Exiled.API.Features.Pools; using Exiled.API.Features.Roles; - using Exiled.API.Features.Spawn; using Exiled.API.Interfaces; - using Exiled.CustomItems.API.Features; using Exiled.Events.EventArgs.Player; - using Exiled.Loader; - using InventorySystem.Configs; + using FLXLib.Spawns; using MEC; + using Mirror; using PlayerRoles; - using UnityEngine; - using YamlDotNet.Serialization; + using Extensions = Extensions; + /// - /// The custom role base class. + /// The custom role base class. /// public abstract class CustomRole { - private const float AddRoleDelay = 0.25f; - - // used in AddRole and InternalChangingRole - private static bool skipChangingCheck; - - private static Dictionary typeLookupTable = new(); - - private static Dictionary stringLookupTable = new(); + /// + /// This var makes player skip base role replace by for one rolechange. + /// + public const string SkipBaseRoleReplaceKey = "skipRoleReplace"; - private static Dictionary idLookupTable = new(); + private static readonly Dictionary IdLookupTable = new(); /// - /// Gets a list of all registered custom roles. + /// Gets a list of all registered custom roles. /// public static HashSet Registered { get; } = new(); /// - /// Gets or sets the custom RoleID of the role. + /// Gets or sets the custom RoleID of the role. /// public abstract uint Id { get; set; } /// - /// Gets or sets the max for the role. + /// Gets or sets the max for the role. /// public abstract int MaxHealth { get; set; } /// - /// Gets or sets the name of this role. + /// Gets or sets the name of this role. /// public abstract string Name { get; set; } /// - /// Gets or sets the description of this role. + /// Gets or sets the description of this role. /// public abstract string Description { get; set; } /// - /// Gets or sets the CustomInfo of this role. + /// Gets or sets the CustomInfo of this role. /// public abstract string CustomInfo { get; set; } /// - /// Gets all of the players currently set to this role. + /// Gets or sets the SpectatorText of this role. /// - [YamlIgnore] - public HashSet TrackedPlayers { get; } = new(); - - /// - /// Gets or sets the to spawn this role as. - /// - public virtual RoleTypeId Role { get; set; } + public abstract string SpectatorText { get; set; } /// - /// Gets or sets a list of the roles custom abilities. + /// Gets all of the players currently set to this role. /// - public virtual List? CustomAbilities { get; set; } = new(); + [YamlIgnore] + public HashSet TrackedPlayers { get; } = new(); /// - /// Gets or sets the starting inventory for the role. + /// Gets or sets the to spawn this role as. /// - public virtual List Inventory { get; set; } = new(); + public virtual RoleTypeId Role { get; set; } /// - /// Gets or sets the starting ammo for the role. + /// Gets or sets a value indicating whether the role is always replaced by this CustomRole. /// - public virtual Dictionary Ammo { get; set; } = new(); + public virtual bool ReplacesBaseRole { get; set; } = false; /// - /// Gets or sets the possible spawn locations for this role. + /// Gets or sets a list of the roles custom abilities. /// - public virtual SpawnProperties SpawnProperties { get; set; } = new(); + public virtual List? CustomAbilities { get; set; } = new(); /// - /// Gets or sets a value indicating whether players keep their current position when gaining this role. + /// Gets or sets List {Dictionary {Item, Chance}}. Supports CustomItems by IDs. Chance can't be decimal. /// - public virtual bool KeepPositionOnSpawn { get; set; } + [Description("List {Dictionary {Item, Chance}}. Supports CustomItems by IDs. Chance can't be decimal. Max slots: 8")] + public virtual List> Inventory { get; set; } = + new(); /// - /// Gets or sets a value indicating whether players keep their current inventory when gaining this role. + /// Gets or sets the starting ammo for the role. /// - public virtual bool KeepInventoryOnSpawn { get; set; } + public virtual Dictionary Ammo { get; set; } = new(); /// - /// Gets or sets a value indicating whether players die when this role is removed. + /// Gets or sets the possible spawn locations for this role. /// - public virtual bool RemovalKillsPlayer { get; set; } = true; + public virtual SpawnPositionSettings SpawnProperties { get; set; } = new(); /// - /// Gets or sets a value indicating whether players keep this role when they die. + /// Gets or sets a value indicating whether players keep this role when they die. /// public virtual bool KeepRoleOnDeath { get; set; } /// - /// Gets or sets a value indicating the Spawn Chance of the Role. - /// - public virtual float SpawnChance { get; set; } - - /// - /// Gets or sets a value indicating whether the spawn system is ignored for this role. - /// - public virtual bool IgnoreSpawnSystem { get; set; } - - /// - /// Gets or sets a value indicating whether players keep this Custom Role when they switch roles: Class-D -> Scientist for example. - /// - public virtual bool KeepRoleOnChangingRole { get; set; } - - /// - /// Gets or sets a value indicating broadcast that will be shown to the player. - /// - public virtual Broadcast Broadcast { get; set; } = new Broadcast(); - - /// - /// Gets or sets a value indicating whether players will receive a message for getting a custom item, when gaining it through the inventory config for this role. + /// Gets or sets a value indicating whether players will receive a message for getting a custom item, when gaining it + /// through the inventory config for this role. /// public virtual bool DisplayCustomItemMessages { get; set; } = true; /// - /// Gets or sets a value indicating the 's size. + /// Gets or sets a value indicating the 's size. /// public virtual Vector3 Scale { get; set; } = Vector3.one; @@ -167,63 +140,77 @@ public abstract class CustomRole /// /// Gets or sets a containing cached and their which is cached Role with FF multiplier. + /// Gets or sets a containing cached and their + /// which is cached Role with FF multiplier. /// public virtual Dictionary CustomRoleFFMultiplier { get; set; } = new(); /// - /// Gets or sets a for the console message given to players when they receive a role. + /// Gets a by ID. /// - public virtual string ConsoleMessage { get; set; } = $"You have spawned as a custom role!"; + /// The ID of the role to get. + /// The role, or if it doesn't exist. + public static CustomRole? Get(uint id) + { + if (!IdLookupTable.ContainsKey(id)) + IdLookupTable.Add(id, Registered.FirstOrDefault(r => r.Id == id)); + return IdLookupTable[id]; + } /// - /// Gets or sets a for the ability usage help sent to players in the player console. + /// Gets a by type. /// - public virtual string AbilityUsage { get; set; } = "Enter \".special\" in the console to use your ability. If you have multiple abilities, you can use this command to cycle through them, or specify the one to use with \".special ROLENAME AbilityNum\""; + /// The to get. + /// The role, or if it doesn't exist. + public static CustomRole? Get(Type t) + { + return Registered.FirstOrDefault(r => r.GetType() == t); + } /// - /// Gets or sets the number of players that naturally spawned with this custom role. + /// Gets a of by type. /// - [YamlIgnore] - public int SpawnedPlayers { get; set; } + /// The to get. + /// The of . + public static IEnumerable GetMany(Type t) + { + return Registered.Where(r => r.GetType() == t); + } /// - /// Gets a by ID. + /// Gets a by name. /// - /// The ID of the role to get. - /// The role, or if it doesn't exist. - public static CustomRole? Get(uint id) + /// The name of the role to get. + /// The role, or if it doesn't exist. + public static CustomRole? Get(string name) { - if (!idLookupTable.ContainsKey(id)) - idLookupTable.Add(id, Registered?.FirstOrDefault(r => r.Id == id)); - return idLookupTable[id]; + return Registered.FirstOrDefault(r => r.Name == name); } /// /// Gets a by type. /// - /// The to get. + /// The specified type. /// The role, or if it doesn't exist. - public static CustomRole? Get(Type t) + public static T? Get() + where T : CustomRole { - if (!typeLookupTable.ContainsKey(t)) - typeLookupTable.Add(t, Registered?.FirstOrDefault(r => r.GetType() == t)); - return typeLookupTable[t]; + return Registered.OfType().FirstOrDefault(); } /// - /// Gets a by name. + /// Gets a of by type. /// - /// The name of the role to get. - /// The role, or if it doesn't exist. - public static CustomRole? Get(string name) + /// The specified type. + /// The of . + public static IEnumerable GetMany() + where T : CustomRole { - if (!stringLookupTable.ContainsKey(name)) - stringLookupTable.Add(name, Registered?.FirstOrDefault(r => r.Name == name)); - return stringLookupTable[name]; + return Registered.OfType(); } /// - /// Tries to get a by . + /// Tries to get a by . /// /// The ID of the role to get. /// The custom role. @@ -236,12 +223,12 @@ public static bool TryGet(uint id, out CustomRole? customRole) } /// - /// Tries to get a by name. + /// Tries to get a by name. /// /// The name of the role to get. /// The custom role. /// True if the role exists. - /// If the name is or an empty string. + /// If the name is or an empty string. public static bool TryGet(string name, out CustomRole? customRole) { if (string.IsNullOrEmpty(name)) @@ -253,12 +240,12 @@ public static bool TryGet(string name, out CustomRole? customRole) } /// - /// Tries to get a by name. + /// Tries to get a by name. /// - /// The of the role to get. + /// The of the role to get. /// The custom role. /// True if the role exists. - /// If the name is or an empty string. + /// If the name is or an empty string. public static bool TryGet(Type t, out CustomRole? customRole) { customRole = Get(t); @@ -267,19 +254,20 @@ public static bool TryGet(Type t, out CustomRole? customRole) } /// - /// Tries to get a of the specified 's s. + /// Tries to get a of the specified 's + /// s. /// /// The player to check. /// The custom roles the player has. /// True if the player has custom roles. - /// If the player is . + /// If the player is . public static bool TryGet(Player player, out IReadOnlyCollection customRoles) { if (player is null) throw new ArgumentNullException(nameof(player)); List tempList = ListPool.Pool.Get(); - tempList.AddRange(Registered?.Where(customRole => customRole.Check(player)) ?? Array.Empty()); + tempList.AddRange(Registered.Where(customRole => customRole.Check(player)) ?? Array.Empty()); customRoles = tempList.AsReadOnly(); ListPool.Pool.Return(tempList); @@ -288,52 +276,47 @@ public static bool TryGet(Player player, out IReadOnlyCollection cus } /// - /// Registers all the 's present in the current assembly. + /// Tries to get a by name. /// - /// Whether to register by attribute. - /// A of which contains all registered 's. - /// - /// This is just a dumbed down version of for QoL, if you actually use , do not use this overload. - /// - public static IEnumerable RegisterRoles(bool byAttribute = false) + /// The custom role. + /// The specified type. + /// True if the role exists. + public static bool TryGet(out T? customRole) + where T : CustomRole { - if (byAttribute) - { - return RegisterRoles(false, null); - } - - List roles = new(); - - foreach (Type type in Assembly.GetCallingAssembly().GetTypes()) - { - if (type.IsAbstract || !type.IsSubclassOf(typeof(CustomRole))) - continue; + customRole = Get(); - CustomRole role = (CustomRole)Activator.CreateInstance(type); - - if (role.TryRegister()) - roles.Add(role); - } - - return roles; + return customRole is not null; } /// - /// Registers all the 's present in the current assembly. + /// Registers all the 's present in the current assembly. /// - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). + /// + /// Whether or not reflection is skipped (more efficient if you are not using your custom item + /// classes as config objects). + /// /// The class to search properties for, if different from the plugin's config class. - /// A of which contains all registered 's. + /// + /// A of which contains all registered + /// 's. + /// public static IEnumerable RegisterRoles(bool skipReflection = false, object? overrideClass = null) => RegisterRoles(skipReflection, overrideClass, true, Assembly.GetCallingAssembly()); /// - /// Registers all the 's present in the current assembly. + /// Registers all the 's present in the current assembly. /// - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). + /// + /// Whether or not reflection is skipped (more efficient if you are not using your custom item + /// classes as config objects). + /// /// The class to search properties for, if different from the plugin's config class. - /// Whether inherited attributes should be taken into account for registration. + /// Whether or not inherited attributes should be taken into account for registration. /// Assembly which is calling this method. - /// A of which contains all registered 's. + /// + /// A of which contains all registered + /// 's. + /// public static IEnumerable RegisterRoles(bool skipReflection = false, object? overrideClass = null, bool inheritAttributes = true, Assembly? assembly = null) { List roles = new(); @@ -381,13 +364,19 @@ public static IEnumerable RegisterRoles(bool skipReflection = false, } /// - /// Registers all the 's present in the current assembly. + /// Registers all the 's present in the current assembly. /// - /// The of containing the target types. + /// The of containing the target types. /// A value indicating whether the target types should be ignored. - /// Whether reflection is skipped (more efficient if you are not using your custom item classes as config objects). + /// + /// Whether or not reflection is skipped (more efficient if you are not using your custom item + /// classes as config objects). + /// /// The class to search properties for, if different from the plugin's config class. - /// A of which contains all registered 's. + /// + /// A of which contains all registered + /// 's. + /// public static IEnumerable RegisterRoles(IEnumerable targetTypes, bool isIgnored = false, bool skipReflection = false, object? overrideClass = null) { List roles = new(); @@ -397,8 +386,7 @@ public static IEnumerable RegisterRoles(IEnumerable targetType { if (type.BaseType != typeof(CustomItem) || type.GetCustomAttribute(typeof(CustomRoleAttribute)) is null || - (isIgnored && targetTypes.Contains(type)) || - (!isIgnored && !targetTypes.Contains(type))) + (isIgnored == targetTypes.Contains(type))) { continue; } @@ -435,9 +423,12 @@ public static IEnumerable RegisterRoles(IEnumerable targetType } /// - /// Unregisters all the 's present in the current assembly. + /// Unregisters all the 's present in the current assembly. /// - /// A of which contains all unregistered 's. + /// + /// A of which contains all unregistered + /// 's. + /// public static IEnumerable UnregisterRoles() { List unregisteredRoles = new(); @@ -452,18 +443,21 @@ public static IEnumerable UnregisterRoles() } /// - /// Unregisters all the 's present in the current assembly. + /// Unregisters all the 's present in the current assembly. /// - /// The of containing the target types. + /// The of containing the target types. /// A value indicating whether the target types should be ignored. - /// A of which contains all unregistered 's. + /// + /// A of which contains all unregistered + /// 's. + /// public static IEnumerable UnregisterRoles(IEnumerable targetTypes, bool isIgnored = false) { List unregisteredRoles = new(); foreach (CustomRole customRole in Registered) { - if ((targetTypes.Contains(customRole.GetType()) && isIgnored) || (!targetTypes.Contains(customRole.GetType()) && !isIgnored)) + if (targetTypes.Contains(customRole.GetType()) == isIgnored) continue; customRole.TryUnregister(); @@ -474,19 +468,22 @@ public static IEnumerable UnregisterRoles(IEnumerable targetTy } /// - /// Unregisters all the 's present in the current assembly. + /// Unregisters all the 's present in the current assembly. /// - /// The of containing the target roles. + /// The of containing the target roles. /// A value indicating whether the target roles should be ignored. - /// A of which contains all unregistered 's. + /// + /// A of which contains all unregistered + /// 's. + /// public static IEnumerable UnregisterRoles(IEnumerable targetRoles, bool isIgnored = false) => UnregisterRoles(targetRoles.Select(x => x.GetType()), isIgnored); /// - /// ResyncCustomRole Friendly Fire with Player (Append, or Overwrite). + /// ResyncCustomRole Friendly Fire with Player (Append, or Overwrite). /// - /// to sync with player. - /// Player to add custom role to. - /// whether to force sync (Overwriting previous information). + /// to sync with player. + /// Player to add custom role to. + /// whether to force sync (Overwriting previous information). public static void SyncPlayerFriendlyFire(CustomRole roleToSync, Player player, bool overwrite = false) { if (overwrite) @@ -501,187 +498,202 @@ public static void SyncPlayerFriendlyFire(CustomRole roleToSync, Player player, } /// - /// Force sync CustomRole Friendly Fire with Player (Set to). + /// Force sync CustomRole Friendly Fire with Player (Set to). /// - /// to sync with player. - /// Player to add custom role to. + /// to sync with player. + /// Player to add custom role to. public static void ForceSyncSetPlayerFriendlyFire(CustomRole roleToSync, Player player) { player.TrySetCustomRoleFriendlyFire(roleToSync.Name, roleToSync.CustomRoleFFMultiplier); } /// - /// Checks if the given player has this role. + /// Checks if the given player has this role. /// - /// The to check. + /// The to check. /// True if the player has this role. public virtual bool Check(Player? player) => player is not null && TrackedPlayers.Contains(player); /// - /// Initializes this role manager. + /// Initializes this role manager. /// public virtual void Init() { - idLookupTable.Add(Id, this); - typeLookupTable.Add(GetType(), this); - stringLookupTable.Add(Name, this); + IdLookupTable.Add(Id, this); SubscribeEvents(); } /// - /// Destroys this role manager. + /// Destroys this role manager. /// public virtual void Destroy() { - idLookupTable.Remove(Id); - typeLookupTable.Remove(GetType()); - stringLookupTable.Remove(Name); + IdLookupTable.Remove(Id); UnsubscribeEvents(); } /// - /// Handles setup of the role, including spawn location, inventory and registering event handlers and add FF rules. + /// Gives to a target items and ammos preset for current . /// - /// The to add the role to. - public virtual void AddRole(Player player) + /// Target . + public virtual void GivePreset(Player player) { - Log.Debug($"{Name}: Adding role to {player.Nickname}."); - player.UniqueRole = Name; - - if (Role != RoleTypeId.None) + try { - try - { - skipChangingCheck = true; - if (KeepPositionOnSpawn) - { - if (KeepInventoryOnSpawn) - player.Role.Set(Role, SpawnReason.ForceClass, RoleSpawnFlags.None); - else - player.Role.Set(Role, SpawnReason.ForceClass, RoleSpawnFlags.AssignInventory); - } - else - { - if (KeepInventoryOnSpawn && player.IsAlive) - player.Role.Set(Role, SpawnReason.ForceClass, RoleSpawnFlags.UseSpawnpoint); - else - player.Role.Set(Role, SpawnReason.ForceClass, RoleSpawnFlags.All); - } - } - finally - { - skipChangingCheck = false; - } - } - - player.UniqueRole = Name; - TrackedPlayers.Add(player); - - Timing.CallDelayed( - AddRoleDelay, - () => + player.TryGetSessionVariable("buffitems", out short itemsBuff); + foreach (Dictionary slot in Inventory) { - if (!KeepInventoryOnSpawn) + foreach (KeyValuePair item in slot) { - Log.Debug($"{Name}: Clearing {player.Nickname}'s inventory."); - player.ClearInventory(); - } + byte chance = (byte)Mathf.Clamp(item.Value + itemsBuff, 0, 100); + if (UnityEngine.Random.Range(0, 101) > chance) + continue; - foreach (string itemName in Inventory) - { - Log.Debug($"{Name}: Adding {itemName} to inventory."); - if (TryAddItem(player, itemName) && CustomItem.TryGet(itemName, out CustomItem? customItem) && customItem is CustomWeapon customWeapon) + try { - if (player.CurrentItem is Firearm firearm && !customWeapon.Attachments.IsEmpty()) - { - firearm.AddAttachment(customWeapon.Attachments); - Log.Debug($"{Name}: Applied attachments to {itemName}."); - } + if (CustomItem.TryGet(item.Key, out CustomItem? customItem)) + customItem?.Give(player, DisplayCustomItemMessages); + else if (!uint.TryParse(item.Key, out _) && Enum.TryParse(item.Key, out ItemType itemType)) // Чтобы числа могли обозначать только кастомпредметы + player.AddItem(itemType); + else + Log.Error($"Error at adding items to custom role {Name} ({Id}). Wrong item: {item.Key}"); } - } - - if (Ammo.Count > 0) - { - Log.Debug($"{Name}: Adding Ammo to {player.Nickname} inventory."); - foreach (AmmoType type in EnumUtils.Values) + catch (Exception e) { - 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.Error($"Failed to give item {item.Key} to {player}\nCustom role: {Name} ({Id})!\n{e}"); } + + break; } - }); + } - Log.Debug($"{Name}: Setting health values."); + foreach (KeyValuePair ammo in Ammo) + player.SetAmmo(ammo.Key, ammo.Value); + } + catch (Exception e) + { + Log.Error($"Exception in customrole {Name} ({Id}) inventory delayed process for player {player}:\n{e}"); + } + } + + /// + /// Adds all properties to a target . + /// + /// The to add the role to. + /// The to spawn player. + /// Should receive inventory. + public virtual void AddProperties(Player player, SpawnReason spawnReason, bool assignInventory) + { + if (assignInventory) + { + player.ClearInventory(); + GivePreset(player); + } + + Log.Debug($"{Name} ({Id}): Setting health values."); player.Health = MaxHealth; player.MaxHealth = MaxHealth; player.Scale = Scale; if (Gravity.HasValue && player.Role is FpcRole fpcRole) fpcRole.Gravity = Gravity.Value; - if (!KeepPositionOnSpawn) - { - Vector3 position = GetSpawnPosition(); - if (position != Vector3.zero) - { - player.Position = position; - } - } - Log.Debug($"{Name}: Setting player info"); + player.InfoArea &= ~PlayerInfoArea.Role; + if (CustomInfo.ToLowerInvariant() != "none") + player.CustomInfo = CustomInfo; - player.CustomInfo = $"{player.CustomName}\n{CustomInfo}"; - player.InfoArea &= ~(PlayerInfoArea.Role | PlayerInfoArea.Nickname); - - if (CustomAbilities is not null) + if (Extensions.InternalPlayerToCustomRoles.TryGetValue(player, out CustomRole cr)) { - foreach (CustomAbility ability in CustomAbilities) - ability.AddAbility(player); + Log.Error($"player: {player} already has custom role in AddRole: cr is {cr.Name} ({cr.Id})"); + cr.TrackedPlayers.Remove(player); + Extensions.InternalPlayerToCustomRoles.Remove(player); } - if (CustomRoles.Instance!.Config.GotRoleHint.Show) - ShowMessage(player); - - ShowBroadcast(player); - RoleAdded(player); + TrackedPlayers.Add(player); + Extensions.InternalPlayerToCustomRoles.Add(player, this); + ShowMessage(player); + player.UniqueRole = Name; player.TryAddCustomRoleFriendlyFire(Name, CustomRoleFFMultiplier); + } + + /// + /// Handles setup of the role, including spawn location, inventory and registering event handlers and add FF rules. + /// + /// The to add the role to. + /// The to spawn player. + /// The to spawn player. + public virtual void AddRole(Player player, SpawnReason spawnReason, RoleSpawnFlags spawnFlags) => + AddRole(player, spawnReason, spawnFlags, true); - if (!string.IsNullOrEmpty(ConsoleMessage)) + /// + /// Handles setup of the role, including spawn location, inventory and registering event handlers and add FF rules. + /// + /// The to add the role to. + /// The to spawn player. + /// The to spawn player. + /// Whether or not will be forced. + public virtual void AddRole(Player player, SpawnReason spawnReason, RoleSpawnFlags spawnFlags, bool forceRole) + { + Log.Debug($"{Name}: Adding role to {player.Nickname} with flags {spawnFlags}."); + + bool useSpawnpoint = spawnFlags.HasFlag(RoleSpawnFlags.UseSpawnpoint); + bool assignInventory = spawnFlags.HasFlag(RoleSpawnFlags.AssignInventory); + + if (forceRole && Role != RoleTypeId.None) { - StringBuilder builder = StringBuilderPool.Pool.Get(); + RoleSpawnFlags flags = RoleSpawnFlags.None; + if (!SpawnProperties.IsAny && useSpawnpoint) + flags |= RoleSpawnFlags.UseSpawnpoint; - builder.AppendLine(Name); - builder.AppendLine(Description); - builder.AppendLine(); - builder.AppendLine(ConsoleMessage); + if (assignInventory) + Extensions.AssignInventoryPlayers.Add(player); - if (CustomAbilities?.Count > 0) - { - builder.AppendLine(AbilityUsage); - builder.AppendLine("Your custom abilities are:"); - for (int i = 1; i < CustomAbilities.Count + 1; i++) - builder.AppendLine($"{i}. {CustomAbilities[i - 1].Name} - {CustomAbilities[i - 1].Description}"); + Extensions.ToChangeRolePlayers[player] = this; + player.Role.Set(Role, spawnReason, flags); - builder.AppendLine( - "You can keybind the command for this ability by using \"cmdbind .special KEY\", where KEY is any un-used letter on your keyboard. You can also keybind each specific ability for a role in this way. For ex: \"cmdbind .special g\" or \"cmdbind .special bulldozer 1 g\""); - } + Log.Debug($"{Name}: Set basic role (force) to {player.Nickname} with flags: {flags}."); + } + else + { + Log.Debug($"Спавним игрока {player.Nickname} по второму сценарию"); - player.SendConsoleMessage(StringBuilderPool.Pool.ToStringReturn(builder), "green"); + if (SpawnProperties.IsAny && useSpawnpoint && NetworkServer.active && player.IsConnected) + player.Position = SpawnProperties.GetRandomPoint() + (Vector3.up * 1.5f); + + AddProperties(player, spawnReason, assignInventory); + RoleAdded(player); + Log.Debug($"{Name}: Set basic role (nonforce) to {player.Nickname}"); } } /// - /// Removes the role from a specific player and FF rules. + /// Returns player's nickname for spectators. + /// + /// Player to send text. + /// Spectator text of player. + public string GetSpectatorText(Player player) => + string.IsNullOrEmpty(SpectatorText) + ? player.CustomName + : $"{player.CustomName} | {SpectatorText}"; + + /// + /// Removes the role from a specific player and FF rules. /// - /// The to remove the role from. + /// The to remove the role from. public virtual void RemoveRole(Player player) { + Log.Debug($"{Name}: (before) Removing role from {player.Nickname}"); + if (!TrackedPlayers.Contains(player)) return; Log.Debug($"{Name}: Removing role from {player.Nickname}"); + Extensions.InternalPlayerToCustomRoles.Remove(player); TrackedPlayers.Remove(player); - player.CustomInfo = string.Empty; - player.InfoArea |= PlayerInfoArea.Role | PlayerInfoArea.Nickname; + if (CustomInfo.ToLowerInvariant() != "none") + player.CustomInfo = string.Empty; + player.InfoArea |= PlayerInfoArea.Role; player.Scale = Vector3.one; + if (CustomAbilities is not null) { foreach (CustomAbility ability in CustomAbilities) @@ -693,13 +705,10 @@ public virtual void RemoveRole(Player player) RoleRemoved(player); player.UniqueRole = string.Empty; player.TryRemoveCustomeRoleFriendlyFire(Name); - - if (RemovalKillsPlayer) - player.Role.Set(RoleTypeId.Spectator); } /// - /// Tries to add to CustomRole FriendlyFire rules. + /// Tries to add to CustomRole FriendlyFire rules. /// /// Role to add. /// Friendly fire multiplier. @@ -716,7 +725,7 @@ public void SetFriendlyFire(RoleTypeId roleToAdd, float ffMult) } /// - /// Wrapper to call . + /// Wrapper to call . /// /// Role with FF to add even if it exists. public void SetFriendlyFire(KeyValuePair roleFF) @@ -725,7 +734,7 @@ public void SetFriendlyFire(KeyValuePair roleFF) } /// - /// Tries to add to CustomRole FriendlyFire rules. + /// Tries to add to CustomRole FriendlyFire rules. /// /// Role to add. /// Friendly fire multiplier. @@ -742,14 +751,14 @@ public bool TryAddFriendlyFire(RoleTypeId roleToAdd, float ffMult) } /// - /// Tries to add to CustomRole FriendlyFire rules. + /// Tries to add to CustomRole FriendlyFire rules. /// /// Role FF multiplier to add. /// Whether the item was able to be added. public bool TryAddFriendlyFire(KeyValuePair pairedRoleFF) => TryAddFriendlyFire(pairedRoleFF.Key, pairedRoleFF.Value); /// - /// Tries to add to CustomRole FriendlyFire rules. + /// Tries to add to CustomRole FriendlyFire rules. /// /// Roles to add with friendly fire values. /// Whether to overwrite current values if they exist. @@ -791,7 +800,20 @@ public bool TryAddFriendlyFire(Dictionary ffRules, bool overw } /// - /// Tries to register this role. + /// Called after the role has been added to the player. + /// + /// The the role was added to. + public virtual void RoleAdded(Player player) + { + if (CustomAbilities is not null) + { + foreach (CustomAbility ability in CustomAbilities) + ability.AddAbility(player); + } + } + + /// + /// Tries to register this role. /// /// True if the role registered properly. internal bool TryRegister() @@ -803,7 +825,7 @@ internal bool TryRegister() { if (Registered.Any(r => r.Id == Id)) { - Log.Warn($"{Name} has tried to register with the same Role ID as another role: {Id}. It will not be registered!"); + Log.Error($"{Name} has tried to register with the same Role ID as another role: {Id}. It will not be registered!"); return false; } @@ -816,13 +838,13 @@ internal bool TryRegister() return true; } - Log.Warn($"Couldn't register {Name} ({Id}) [{Role}] as it already exists."); + Log.Error($"Couldn't register {Name} ({Id}) [{Role}] as it already exists."); return false; } /// - /// Tries to unregister this role. + /// Tries to unregister this role. /// /// True if the role is unregistered properly. internal bool TryUnregister() @@ -840,16 +862,16 @@ internal bool TryUnregister() } /// - /// Tries to add an item to the player's inventory by name. + /// Tries to add an item to the player's inventory by name. /// - /// The to try giving the item to. + /// The to try giving the item to. /// The name of the item to try adding. - /// Whether the item was able to be added. + /// Whether or not the item was able to be added. protected bool TryAddItem(Player player, string itemName) { if (CustomItem.TryGet(itemName, out CustomItem? customItem)) { - customItem?.Give(player, DisplayCustomItemMessages); + customItem?.Give(player); return true; } @@ -870,93 +892,20 @@ protected bool TryAddItem(Player player, string itemName) } /// - /// Gets a random from . - /// - /// The chosen spawn location. - protected Vector3 GetSpawnPosition() - { - if (SpawnProperties is null || SpawnProperties.Count() == 0) - { - return Vector3.zero; - } - - float totalchance = 0f; - List<(float chance, Vector3 pos)> spawnPointPool = new(4); - - void Add(Vector3 pos, float chance) - { - if (chance <= 0f) - return; - - spawnPointPool.Add((chance, pos)); - totalchance += chance; - } - - if (!SpawnProperties.StaticSpawnPoints.IsEmpty()) - { - foreach (StaticSpawnPoint sp in SpawnProperties.StaticSpawnPoints) - { - Add(sp.Position, sp.Chance); - } - } - - if (!SpawnProperties.DynamicSpawnPoints.IsEmpty()) - { - foreach (DynamicSpawnPoint sp in SpawnProperties.DynamicSpawnPoints) - { - Add(sp.Position, sp.Chance); - } - } - - if (!SpawnProperties.RoleSpawnPoints.IsEmpty()) - { - foreach (RoleSpawnPoint sp in SpawnProperties.RoleSpawnPoints) - { - Add(sp.Position, sp.Chance); - } - } - - if (!SpawnProperties.RoomSpawnPoints.IsEmpty()) - { - foreach (RoomSpawnPoint sp in SpawnProperties.RoomSpawnPoints) - { - Add(sp.Position, sp.Chance); - } - } - - if (spawnPointPool.Count == 0 || totalchance <= 0f) - { - return Vector3.zero; - } - - float randomRoll = (float)(Loader.Random.NextDouble() * totalchance); - foreach ((float chance, Vector3 pos) in spawnPointPool) - { - if (randomRoll < chance) - { - return pos; - } - - randomRoll -= chance; - } - - return Vector3.zero; - } - - /// - /// Called when the role is initialized to setup internal events. + /// Called when the role is initialized to setup internal events. /// protected virtual void SubscribeEvents() { Log.Debug($"{Name}: Loading events."); - Exiled.Events.Handlers.Player.ChangingNickname += OnInternalChangingNickname; + Exiled.Events.Handlers.Player.ChangingRole += OnInternalChangingRole; - Exiled.Events.Handlers.Player.SpawningRagdoll += OnSpawningRagdoll; - Exiled.Events.Handlers.Player.Destroying += OnDestroying; + + if (ReplacesBaseRole) + Exiled.Events.Handlers.Player.Spawned += OnInternalSpawned; } /// - /// Called when the role is destroyed to unsubscribe internal event handlers. + /// Called when the role is destroyed to unsubscribe internal event handlers. /// protected virtual void UnsubscribeEvents() { @@ -964,60 +913,48 @@ protected virtual void UnsubscribeEvents() RemoveRole(player); Log.Debug($"{Name}: Unloading events."); - Exiled.Events.Handlers.Player.ChangingNickname -= OnInternalChangingNickname; + Exiled.Events.Handlers.Player.ChangingRole -= OnInternalChangingRole; - Exiled.Events.Handlers.Player.SpawningRagdoll -= OnSpawningRagdoll; - Exiled.Events.Handlers.Player.Destroying -= OnDestroying; + + if (ReplacesBaseRole) + Exiled.Events.Handlers.Player.Spawned -= OnInternalSpawned; } /// - /// Shows the spawn message to the player. + /// Shows the spawn message to the player. /// - /// The to show the message to. + /// The to show the message to. protected virtual void ShowMessage(Player player) => player.ShowHint(string.Format(CustomRoles.Instance!.Config.GotRoleHint.Content, Name, Description), CustomRoles.Instance.Config.GotRoleHint.Duration); /// - /// Shows the spawn broadcast to the player. - /// - /// The to show the message to. - protected virtual void ShowBroadcast(Player player) => player.Broadcast(Broadcast); - - /// - /// Called after the role has been added to the player. - /// - /// The the role was added to. - protected virtual void RoleAdded(Player player) - { - } - - /// - /// Called 1 frame before the role is removed from the player. + /// Called after the role is removed from the player. /// - /// The the role was removed from. + /// The the role was removed from. protected virtual void RoleRemoved(Player player) { } - private void OnInternalChangingNickname(ChangingNicknameEventArgs ev) - { - if (Check(ev.Player)) - ev.Player.CustomInfo = $"{ev.NewName}\n{CustomInfo}"; - } - private void OnInternalChangingRole(ChangingRoleEventArgs ev) { - if (!skipChangingCheck && ev.IsAllowed && ev.Reason != SpawnReason.Destroyed && Check(ev.Player) && ((ev.NewRole == RoleTypeId.Spectator && !KeepRoleOnDeath) || (ev.NewRole != RoleTypeId.Spectator && !KeepRoleOnChangingRole))) + if (Check(ev.Player)) + { RemoveRole(ev.Player); - else - skipChangingCheck = false; + } } - private void OnSpawningRagdoll(SpawningRagdollEventArgs ev) + private void OnInternalSpawned(SpawnedEventArgs ev) { - if (Check(ev.Player) && Role.IsFpcRole()) - ev.Role = Role; + if (ev.Player.Role == Role && !ev.Player.HasCustomRole() && !Extensions.ToChangeRolePlayers.ContainsKey(ev.Player) && !ev.Player.SessionVariables.Remove(SkipBaseRoleReplaceKey)) + { + try + { + AddRole(ev.Player, SpawnReason.ForceClass, RoleSpawnFlags.All, false); + } + catch (Exception e) + { + Log.Error($"[{nameof(CustomRole)}.{nameof(OnInternalChangingRole)}] [{Name}] Failed to add customRole-replacer of basic {Role}:\n{e}"); + } + } } - - private void OnDestroying(DestroyingEventArgs ev) => RemoveRole(ev.Player); } } diff --git a/EXILED/Exiled.CustomRoles/API/Features/Enums/CheckType.cs b/EXILED/Exiled.CustomRoles/API/Features/Enums/CheckType.cs deleted file mode 100644 index fb636665a8..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Enums/CheckType.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Enums -{ - /// - /// The possible types of checks to preform on active abilities. - /// - public enum CheckType - { - /// - /// Check if the ability is available to the player. (DOES NOT CHECK COOLDOWNS) - /// - Available, - - /// - /// Check if the ability is selected, but not active. - /// - Selected, - - /// - /// The ability is currently active. - /// - Active, - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Enums/KeypressActivationType.cs b/EXILED/Exiled.CustomRoles/API/Features/Enums/KeypressActivationType.cs deleted file mode 100644 index d63845ff70..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Enums/KeypressActivationType.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Enums -{ - /// - /// The action type that should be triggered from a keypress trigger. - /// - public enum AbilityKeypressTriggerType - { - /// - /// No action. - /// - None, - - /// - /// Activate ability. - /// - Activate, - - /// - /// Switch to next ability. - /// - SwitchForward, - - /// - /// Switch to previous ability. - /// - SwitchBackward, - - /// - /// Display information about the ability to the user. - /// - DisplayInfo, - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Extensions/ParserExtension.cs b/EXILED/Exiled.CustomRoles/API/Features/Extensions/ParserExtension.cs deleted file mode 100644 index 49463b8e8a..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Extensions/ParserExtension.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Extensions -{ - using System; - - using Exiled.CustomRoles.API.Features.Parsers; - - using YamlDotNet.Core; - using YamlDotNet.Core.Events; - - /// - /// Extensions for . - /// - public static class ParserExtension - { - /// - /// Tries to find a valid mapping entry. - /// - /// The parser. - /// The selector. - /// The key found. - /// The value found. - /// when a valid mapping entry is found; otherwise, . - public static bool TryFindMappingEntry(this ParsingEventBuffer parser, Func selector, out Scalar? key, out ParsingEvent? value) - { - parser.Consume(); - do - { - switch (parser.Current) - { - case Scalar scalar: - bool keyMatched = selector(scalar); - parser.MoveNext(); - if (keyMatched) - { - value = parser.Current; - key = scalar; - return true; - } - - parser.SkipThisAndNestedEvents(); - break; - case MappingStart _: - case SequenceStart _: - parser.SkipThisAndNestedEvents(); - break; - default: - parser.MoveNext(); - break; - } - } - while (parser.Current is not null); - - key = null; - value = null; - return false; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Interfaces/ITypeDiscriminator.cs b/EXILED/Exiled.CustomRoles/API/Features/Interfaces/ITypeDiscriminator.cs deleted file mode 100644 index aa4f415b37..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Interfaces/ITypeDiscriminator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Interfaces -{ - using System; - - using Exiled.CustomRoles.API.Features.Parsers; - - /// - /// A discriminator. - /// - public interface ITypeDiscriminator - { - /// - /// Gets the base . - /// - Type BaseType { get; } - - /// - /// Tries to resolve a mapping into a specific . - /// - /// The parser buffer. - /// The to resolve the mapping key. - /// if resolution is successful; otherwise, . - bool TryResolve(ParsingEventBuffer buffer, out Type? suggestedType); - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/KeypressActivator.cs b/EXILED/Exiled.CustomRoles/API/Features/KeypressActivator.cs deleted file mode 100644 index 4e94c1dba7..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/KeypressActivator.cs +++ /dev/null @@ -1,210 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features -{ - using System.Collections.Generic; - using System.Globalization; - using System.Text; - - using Exiled.API.Features; - using Exiled.API.Features.Pools; - using Exiled.API.Features.Roles; - using Exiled.CustomRoles.API.Features.Enums; - using Exiled.Events.EventArgs.Player; - using Exiled.Events.EventArgs.Server; - - using MEC; - - using PlayerRoles.FirstPersonControl; - - /// - /// Control class for keypress ability actions. - /// - internal class KeypressActivator - { - private readonly Dictionary altTracker = DictionaryPool.Pool.Get(); - private readonly Dictionary coroutineTracker = DictionaryPool.Pool.Get(); - - /// - /// Initializes a new instance of the class. - /// - internal KeypressActivator() - { - Exiled.Events.Handlers.Player.TogglingNoClip += OnTogglingNoClip; - Exiled.Events.Handlers.Server.EndingRound += OnEndingRound; - } - - /// - /// Finalizes an instance of the class. - /// - ~KeypressActivator() - { - Exiled.Events.Handlers.Player.TogglingNoClip -= OnTogglingNoClip; - Exiled.Events.Handlers.Server.EndingRound -= OnEndingRound; - DictionaryPool.Pool.Return(altTracker); - DictionaryPool.Pool.Return(coroutineTracker); - } - - private void OnTogglingNoClip(TogglingNoClipEventArgs ev) - { - if (ev.Player.IsNoclipPermitted) - return; - - if (!ActiveAbility.AllActiveAbilities.ContainsKey(ev.Player)) - return; - - if (!altTracker.ContainsKey(ev.Player)) - altTracker.Add(ev.Player, 0); - - altTracker[ev.Player]++; - - if (!coroutineTracker.ContainsKey(ev.Player)) - coroutineTracker.Add(ev.Player, default); - - if (!coroutineTracker[ev.Player].IsRunning) - coroutineTracker[ev.Player] = Timing.RunCoroutine(ProcessAltKey(ev.Player)); - } - - private void OnEndingRound(EndingRoundEventArgs ev) - { - altTracker.Clear(); - foreach (CoroutineHandle handle in coroutineTracker.Values) - Timing.KillCoroutines(handle); - coroutineTracker.Clear(); - } - - private IEnumerator ProcessAltKey(Player player) - { - yield return Timing.WaitForSeconds(0.25f); - - if (!altTracker.TryGetValue(player, out int pressCount)) - yield break; - - Log.Debug($"{player.Nickname}: {pressCount} {(player.Role is FpcRole fpc ? fpc.MoveState : false)}"); - AbilityKeypressTriggerType type = pressCount switch - { - 1 when player.Role is FpcRole { MoveState: PlayerMovementState.Sneaking } => AbilityKeypressTriggerType.DisplayInfo, - 1 => AbilityKeypressTriggerType.Activate, - 2 when player.Role is FpcRole { MoveState: PlayerMovementState.Sneaking } => AbilityKeypressTriggerType.SwitchBackward, - 2 => AbilityKeypressTriggerType.SwitchForward, - _ => AbilityKeypressTriggerType.None, - }; - - bool preformed = PreformAction(player, type, out string response); - switch (preformed) - { - case true when type == AbilityKeypressTriggerType.Activate: - string[] split = response.Split('|'); - response = string.Format(CustomRoles.Instance.Config.UsedAbilityHint.Content, split); - break; - case true when type is AbilityKeypressTriggerType.SwitchBackward or AbilityKeypressTriggerType.SwitchForward: - response = string.Format(CustomRoles.Instance.Config.SwitchedAbilityHint.Content, response); - break; - case false: - response = string.Format(CustomRoles.Instance.Config.FailedActionHint.Content, response); - break; - } - - float dur = type switch - { - AbilityKeypressTriggerType.Activate when preformed => CustomRoles.Instance.Config.UsedAbilityHint.Duration, - AbilityKeypressTriggerType.SwitchBackward or AbilityKeypressTriggerType.SwitchForward when preformed => CustomRoles.Instance.Config.SwitchedAbilityHint.Duration, - _ => CustomRoles.Instance.Config.FailedActionHint.Duration, - }; - - player.ShowHint(response, dur); - altTracker[player] = 0; - } - - private bool PreformAction(Player player, AbilityKeypressTriggerType type, out string response) - { - ActiveAbility? selected = player.GetSelectedAbility(); - if (type == AbilityKeypressTriggerType.Activate) - { - if (selected is null) - { - response = "No selected abilities."; - return false; - } - - if (!selected.CanUseAbility(player, out response, CustomRoles.Instance.Config.ActivateOnlySelected)) - return false; - response = $"{selected.Name}|{selected.Description}"; - selected.UseAbility(player); - return true; - } - - if (type is AbilityKeypressTriggerType.SwitchForward or AbilityKeypressTriggerType.SwitchBackward) - { - List abilities = ListPool.Pool.Get(player.GetActiveAbilities()); - - if (abilities.Count == 0) - { - response = "No abilities to switch to."; - return false; - } - - if (selected is not null) - { - int index = abilities.IndexOf(selected); - int mod = type == AbilityKeypressTriggerType.SwitchForward ? 1 : -1; - if (index + mod > abilities.Count - 1) - index = 0; - else if (index + mod < 0) - index = abilities.Count - 1; - else - index += mod; - - if (index < 0 || index > abilities.Count - 1) - { - Log.Warn("Joker can't do math."); - response = "Jokey did a fucky wucky wif his maths"; - return false; - } - - if (abilities.Count <= 1) - { - response = "No abilities to switch to."; - return false; - } - - selected.UnSelectAbility(player); - abilities[index].SelectAbility(player); - response = $"{abilities[index].Name}"; - return true; - } - - abilities[0].SelectAbility(player); - response = $"{abilities[0].Name}"; - return true; - } - - if (type == AbilityKeypressTriggerType.DisplayInfo) - { - if (selected is null) - { - response = "No ability selected."; - return false; - } - - StringBuilder builder = StringBuilderPool.Pool.Get(); - builder.AppendLine(selected.Name); - builder.AppendLine(selected.Description); - builder.AppendLine(selected.Duration.ToString(CultureInfo.InvariantCulture)).Append(" (").Append(selected.Cooldown).Append(") ").AppendLine(); - builder.AppendLine($"Usable: ").Append(selected.CanUseAbility(player, out string res)); - if (!string.IsNullOrEmpty(res)) - builder.Append(" [").Append(res).Append("]"); - response = StringBuilderPool.Pool.ToStringReturn(builder); - return true; - } - - response = $"Invalid action: {type}."; - return false; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Parsers/AbstractClassNodeTypeResolver.cs b/EXILED/Exiled.CustomRoles/API/Features/Parsers/AbstractClassNodeTypeResolver.cs deleted file mode 100644 index 2a9ff722d6..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Parsers/AbstractClassNodeTypeResolver.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Parsers -{ - using System; - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using System.Linq; - - using Exiled.CustomRoles.API.Features.Interfaces; - - using YamlDotNet.Core; - using YamlDotNet.Core.Events; - using YamlDotNet.Serialization; - - /// - /// A node resolver for . - /// - public class AbstractClassNodeTypeResolver : INodeDeserializer - { - private readonly INodeDeserializer original; - private readonly ITypeDiscriminator[] typeDiscriminators; - - /// - /// Initializes a new instance of the class. - /// - /// The original deserializer. - /// The array of discriminators. - public AbstractClassNodeTypeResolver(INodeDeserializer original, params ITypeDiscriminator[] discriminators) - { - this.original = original; - typeDiscriminators = discriminators; - } - - /// - public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) - { - if (!reader.Accept(out MappingStart? mapping)) - { - value = null; - return false; - } - - IEnumerable supportedTypes = typeDiscriminators.Where(t => t.BaseType == expectedType).ToArray(); - if (!supportedTypes.Any()) - { - if (original.Deserialize(reader, expectedType, nestedObjectDeserializer, out value)) - { - Validator.ValidateObject(value!, new ValidationContext(value!, null, null), true); - - return true; - } - - return false; - } - - Mark? start = reader.Current?.Start; - Type? actualType; - ParsingEventBuffer buffer; - try - { - buffer = new ParsingEventBuffer(ReadNestedMapping(reader)); - actualType = CheckWithDiscriminators(expectedType, supportedTypes, buffer); - } - catch (Exception exception) - { - throw new YamlException(start ?? new(), reader.Current?.End ?? new(), "Failed when resolving abstract type", exception); - } - - buffer.Reset(); - - if (original.Deserialize(buffer, actualType!, nestedObjectDeserializer, out value)) - { - Validator.ValidateObject(value!, new ValidationContext(value!, null, null), true); - - return true; - } - - return false; - } - - private static Type? CheckWithDiscriminators(Type expectedType, IEnumerable supportedTypes, ParsingEventBuffer buffer) - { - foreach (ITypeDiscriminator discriminator in supportedTypes) - { - buffer.Reset(); - if (!discriminator.TryResolve(buffer, out Type? actualType)) - continue; - - return actualType; - } - - throw new Exception( - $"None of the registered type discriminators could supply a child class for {expectedType}"); - } - - private static LinkedList ReadNestedMapping(IParser reader) - { - LinkedList result = new(); - result.AddLast(reader.Consume()); - int depth = 0; - do - { - ParsingEvent next = reader.Consume(); - depth += next.NestingIncrease; - result.AddLast(next); - } - while (depth >= 0); - - return result; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Parsers/AggregateExpectationTypeResolver.cs b/EXILED/Exiled.CustomRoles/API/Features/Parsers/AggregateExpectationTypeResolver.cs deleted file mode 100644 index 646c34bb50..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Parsers/AggregateExpectationTypeResolver.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Parsers -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - - using Exiled.API.Features; - using Exiled.CustomRoles.API.Features; - using Exiled.CustomRoles.API.Features.Extensions; - using Exiled.CustomRoles.API.Features.Interfaces; - - using YamlDotNet.Core.Events; - using YamlDotNet.Serialization; - - /// - public class AggregateExpectationTypeResolver : ITypeDiscriminator - where T : class - { - private const string TargetKey = nameof(CustomAbility.AbilityType); - private readonly string targetKey; - private readonly Dictionary typeLookup; - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public AggregateExpectationTypeResolver(INamingConvention namingConvention) - { - targetKey = namingConvention.Apply(TargetKey); - typeLookup = new Dictionary(); - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - foreach (Type? t in assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(T)))) - typeLookup.Add(t.Name, t); - } - catch (Exception e) - { - Log.Warn($"Error loading types for {assembly.FullName}. It can be ignored if it's not using Exiled.CustomRoles. (Enabled Debug for more detail.)"); - Log.Debug(e); - } - } - } - - /// - public Type BaseType => typeof(CustomAbility); - - /// - public bool TryResolve(ParsingEventBuffer buffer, out Type? suggestedType) - { - if (buffer.TryFindMappingEntry( - scalar => targetKey == scalar.Value, - out Scalar? key, - out ParsingEvent? value)) - { - if (value is Scalar valueScalar) - { - suggestedType = CheckName(valueScalar.Value); - - return true; - } - - FailEmpty(); - } - - suggestedType = null; - return false; - } - - private void FailEmpty() - { - throw new Exception($"Could not determine expectation type, {targetKey} has an empty value"); - } - - private Type? CheckName(string value) - { - if (typeLookup.TryGetValue(value, out Type? childType)) - return childType; - - string known = string.Join(",", typeLookup.Keys); - throw new Exception( - $"Could not match `{targetKey}: {value}` to a known expectation. Expecting one of: {known}"); - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Parsers/ParsingEventBuffer.cs b/EXILED/Exiled.CustomRoles/API/Features/Parsers/ParsingEventBuffer.cs deleted file mode 100644 index 9b7c9e113d..0000000000 --- a/EXILED/Exiled.CustomRoles/API/Features/Parsers/ParsingEventBuffer.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.API.Features.Parsers -{ - using System.Collections.Generic; - - using YamlDotNet.Core; - using YamlDotNet.Core.Events; - - /// - public class ParsingEventBuffer : IParser - { - private readonly LinkedList buffer; - - private LinkedListNode? current; - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - public ParsingEventBuffer(LinkedList events) - { - buffer = events; - current = events.First; - } - - /// - public ParsingEvent? Current => current?.Value; - - /// - public bool MoveNext() - { - current = current?.Next; - return current is not null; - } - - /// - /// Resets the buffer. - /// - public void Reset() - { - current = buffer.First; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/PassiveAbility.cs b/EXILED/Exiled.CustomRoles/API/Features/PassiveAbility.cs index 8dbe7f5dc6..3d3ecb25ff 100644 --- a/EXILED/Exiled.CustomRoles/API/Features/PassiveAbility.cs +++ b/EXILED/Exiled.CustomRoles/API/Features/PassiveAbility.cs @@ -8,9 +8,13 @@ namespace Exiled.CustomRoles.API.Features { /// - /// The base class for passive (always active) abilities. + /// The base class for passive (always active) abilities. /// public abstract class PassiveAbility : CustomAbility { + /// + /// Gets or sets a value indicating whether ability will be dispayed in .roleinfo or not. + /// + public virtual bool IsHidden { get; set; } = false; } } \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/API/Features/Scp079ActiveAbility.cs b/EXILED/Exiled.CustomRoles/API/Features/Scp079ActiveAbility.cs new file mode 100644 index 0000000000..d33b276b9d --- /dev/null +++ b/EXILED/Exiled.CustomRoles/API/Features/Scp079ActiveAbility.cs @@ -0,0 +1,93 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.API.Features +{ + using System; + using System.ComponentModel; + + using Exiled.API.Features; + using Exiled.API.Features.Roles; + + using PlayerRoles; + + /// + /// The base class for active (on-use) energy-using abilities for SCP-079. + /// + public abstract class Scp079ActiveAbility : ActiveAbility + { + /// + /// Gets or sets minimal SCP-079 level to use this ability. + /// + public abstract byte MinRequiredLevel { get; set; } + + /// + /// Gets or sets maximum SCP-079 level to use this ability. + /// + [Description("Уровень, с которого способность перестанет отображаться и станет недоступна для использования.")] + public virtual byte MaxRequiredLevel { get; set; } = 10; + + /// + /// Gets or sets energy usage for ability. + /// + public abstract float EnergyUsage { get; set; } + + /// + public override bool CanUseAbility(Player player, out string response) + { + if (!player.Role.Is(out Scp079Role scp079Role)) + { + response = "Вы не SCP-079, чтобы использовать эту команду."; + return false; + } + + if (scp079Role.Level < MinRequiredLevel) + { + Hint hint = CustomRoles.Instance!.Config.InsufficientLevelHint; + response = string.Format(hint.Content, scp079Role.Level + 1, MinRequiredLevel + 1); + if (hint.Show) + player.ShowHint(response, hint.Duration); + return false; + } + + if (scp079Role.Level > MaxRequiredLevel) + { + Hint hint = CustomRoles.Instance!.Config.RedundantLevelHint; + response = string.Format(hint.Content, scp079Role.Level + 1, MaxRequiredLevel + 1); + if (hint.Show) + player.ShowHint(response, hint.Duration); + return false; + } + + if (scp079Role.Energy < EnergyUsage) + { + Hint hint = CustomRoles.Instance!.Config.InsufficientEnergyHint; + response = string.Format(hint.Content, scp079Role.Energy, EnergyUsage); + if (hint.Show) + player.ShowHint(response, hint.Duration); + return false; + } + + return base.CanUseAbility(player, out response); + } + + /// + /// Checks if current is avaible for something krisprs mmnt. + /// + /// Target . + /// skibidi ohio rizz. + internal bool IsAvailable(Player player) => + player.Role.Is(out Scp079Role scp079Role) && scp079Role.Level <= MaxRequiredLevel && (!CustomRoles.Instance!.Config.HideUnavailableHighLevelAbilities || scp079Role.Level >= MinRequiredLevel); + + /// + protected override void AbilityUsed(Player player) + { + if (player.Role.Is(out Scp079Role role)) + role.Energy -= EnergyUsage; + } + } +} diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/Give.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/Give.cs new file mode 100644 index 0000000000..97c0339992 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/Give.cs @@ -0,0 +1,158 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using API; + using API.Features; + using CommandSystem; + using Exiled.API.Enums; + using Exiled.API.Features; + using Exiled.API.Features.Pools; + using Permissions.Extensions; + using PlayerRoles; + using RemoteAdmin; + + /// + /// The command to give a role to player(s). + /// + internal sealed class Give : ICommand + { + /// + public string Command { get; set; } = "give"; + + /// + public string[] Aliases { get; set; } = { "g" }; + + /// + public string Description { get; set; } = "Gives the specified custom role to the indicated player(s)."; + + /// + /// Gets or sets the message displayed when the user does not have the required permission. + /// + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets the message displayed when the specified custom role is not found. + /// + /// + /// The message contains a placeholder for the role name, which is replaced with the actual role name when the message is displayed. + /// + public string NoRoleFoundMessage { get; set; } = "Кастомная роль {0} не найдена!"; + + /// + /// Gets or sets the message displayed when the specified player is not found. + /// + public string PlayerNotFoundMessage { get; set; } = "Игрок не найден."; + + /// + /// Gets or sets the message displayed when a custom role is successfully given to a player. + /// + /// + /// The message contains placeholders for the role name and the player's nickname, which are replaced with the actual values when the message is displayed. + /// + public string RoleGivenMessage { get; set; } = "Кастомная роль {0} дана игроку {1}."; + + /// + /// Gets or sets the message displayed when a custom role is successfully given to all players. + /// + /// + /// The message contains a placeholder for the role name, which is replaced with the actual role name when the message is displayed. + /// + public string AllPlayersRoleGivenMessage { get; set; } = "Кастомная роль {0} дана всем игрокам."; + + /// + /// Gets or sets the error message displayed when a player is not found. + /// + /// + /// The message contains a placeholder for the player's nickname or ID, which is replaced with the actual value when the message is displayed. + /// + public string PlayerNotFoundErrorMessage { get; set; } = "Игрок {0} не найден"; + + /// + /// Gets or sets the usage message displayed when the command is used incorrectly. + /// + /// + /// The message contains a placeholder for the command name, which is replaced with the actual command name when the message is displayed. + /// + public string UsageMessage { get; set; } = "{0} <Название/ID кастомной роли> [Никнейм/ID/SteamID игрока или all/*]"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customroles.give")) + { + response = NoPermissionMessage; + return false; + } + + if (arguments.Count == 0) + { + response = string.Format(UsageMessage, Command); + return false; + } + + if (!CustomRole.TryGet(arguments.At(0), out CustomRole? role) || role is null) + { + response = string.Format(NoRoleFoundMessage, arguments.At(0)); + return false; + } + + if (arguments.Count == 1) + { + if (sender is PlayerCommandSender playerCommandSender) + { + Player player = Player.Get(playerCommandSender); + + TryAddRole(player, role); + response = string.Format(RoleGivenMessage, role.Name, player.Nickname); + return true; + } + + response = PlayerNotFoundMessage; + return false; + } + + string identifier = string.Join(" ", arguments.Skip(1)); + + switch (identifier) + { + case "*": + case "all": + List players = ListPool.Pool.Get(Player.List); + + foreach (Player player in players) + TryAddRole(player, role); + + response = string.Format(AllPlayersRoleGivenMessage, role.Name); + ListPool.Pool.Return(players); + return true; + default: + if (Player.Get(identifier) is not { } ply) + { + response = string.Format(PlayerNotFoundErrorMessage, identifier); + return false; + } + + TryAddRole(ply, role); + response = string.Format(RoleGivenMessage, role.Name, ply.Nickname); + return true; + } + } + + private void TryAddRole(Player player, CustomRole customRole) + { + player.GetCustomRole()?.RemoveRole(player); + + customRole.AddRole(player, SpawnReason.ForceClass, RoleSpawnFlags.All); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/Info.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/Info.cs new file mode 100644 index 0000000000..042bf8dac4 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/Info.cs @@ -0,0 +1,108 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin +{ + using System; + using System.Text; + + using API.Features; + + using CommandSystem; + + using Exiled.API.Features.Pools; + + using Permissions.Extensions; + + /// + /// The command to view info about a specific role. + /// + internal sealed class Info : ICommand + { + /// + public string Command { get; set; } = "info"; + + /// + public string[] Aliases { get; set; } = { "i" }; + + /// + public string Description { get; set; } = "Информация про кастомную роль."; + + /// + /// Gets or sets the usage message for the command. + /// + public string Usage { get; set; } = "info [Название/ID кастомной роли]"; + + /// + /// Gets or sets the error message displayed when a role is not found. + /// + public string ErrorNoRole { get; set; } = "{0} не идентификатор кастомной роли."; + + /// + /// Gets or sets the first color used in the role information display. + /// + public string Color1 { get; set; } = "#E6AC00"; + + /// + /// Gets or sets the second color used in the role information display. + /// + public string Color2 { get; set; } = "#00D639"; + + /// + /// Gets or sets the third color used in the role information display. + /// + public string Color3 { get; set; } = "#05C4E8"; + + /// + /// Gets or sets the format string used to display role information. + /// + public string RoleInfoFormat { get; set; } = "- {0} ({1}) - {2}"; + + /// + /// Gets or sets the format string used to display role health information. + /// + public string RoleHealthFormat { get; set; } = "- Здоровье: {0}"; + + /// + /// Gets or sets the error message displayed when the user lacks permissions. + /// + public string NoPermissions { get; set; } = "Не хватает прав!"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customroles.info")) + { + response = NoPermissions; + return false; + } + + if (arguments.Count < 1) + { + response = Usage; + return false; + } + + if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomRole.TryGet(id, out CustomRole? role)) && !CustomRole.TryGet(arguments.At(0), out role)) || role is null) + { + response = string.Format(ErrorNoRole, arguments.At(0)); + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append("- ").Append(role.Name) + .Append(" (").Append(role.Id).Append(")") + .Append(string.Format(RoleInfoFormat, role.Name, role.Id, role.Description)) + .AppendLine(role.Role.ToString()) + .Append(string.Format(RoleHealthFormat, role.MaxHealth.ToString())).AppendLine(); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/List/InGame.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/List/InGame.cs new file mode 100644 index 0000000000..40929ec90b --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/List/InGame.cs @@ -0,0 +1,101 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin.List +{ + using System; + using System.Text; + + using API.Features; + using CommandSystem; + using Exiled.API.Features; + using NorthwoodLib.Pools; + using Permissions.Extensions; + + /// + internal sealed class InGame : ICommand + { + /// + public string Command { get; } = "ingame"; + + /// + public string[] Aliases { get; } = { "ig", "alife" }; + + /// + public string Description { get; set; } = "Получает все кастомные роли которые сейчас учавствуют в раунде."; + + /// + /// Gets or sets the permission required to execute the command. + /// + public string Permission { get; set; } = "customroles.list.ingame"; + + /// + /// Gets or sets the message to display when the user does not have the required permission. + /// + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets the message to display when there are no custom roles found. + /// + public string NoCustomRolesMessage { get; set; } = "Кастомные роли не найдены."; + + /// + /// Gets or sets the format of the header that displays the number of current in-game custom roles. + /// + public string CustomRolesHeaderFormat { get; set; } = "[Текущие живые кастомные роли: ({0})]{1}"; + + /// + /// Gets or sets the format of each custom role displayed in the list. + /// + public string CustomRoleFormat { get; set; } = "[{0}. {1} ({2}) {{ {3} }}]{1}"; + + /// + /// Gets or sets the format of each player displayed in the list. + /// + public string PlayerFormat { get; set; } = "{0} ({1}) ({2}) [{3}]"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission(Permission)) + { + response = NoPermissionMessage; + return false; + } + + StringBuilder message = StringBuilderPool.Shared.Rent(); + + int count = 0; + + foreach (CustomRole customRole in CustomRole.Registered) + { + if (customRole.TrackedPlayers.Count == 0) + continue; + + message.AppendLine() + .AppendFormat(CustomRoleFormat, customRole.Id, customRole.Name, customRole.Role, customRole.TrackedPlayers.Count) + .AppendLine(); + + count += customRole.TrackedPlayers.Count; + + foreach (Player owner in customRole.TrackedPlayers) + { + message.AppendFormat(PlayerFormat, owner.Nickname, owner.UserId, owner.Id, owner.Role.Type) + .AppendLine(); + } + } + + if (message.Length == 0) + message.Append(NoCustomRolesMessage); + else + message.Insert(0, string.Format(CustomRolesHeaderFormat, count, Environment.NewLine)); + + response = StringBuilderPool.Shared.ToStringReturn(message); + return true; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/List/List.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/List/List.cs new file mode 100644 index 0000000000..4ce1218f80 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/List/List.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin.List +{ + using System; + using System.Collections.Generic; + + using Exiled.API.Features; + + /// + /// The command to list all registered roles. + /// + internal sealed class List : ParentCommand + { + /// + public override string Command { get; } = "list"; + + /// + public override string[] Aliases { get; set; } = { "l" }; + + /// + public override string Description { get; set; } = "Списки кастомных ролей."; + + /// + protected override IEnumerable CommandsToRegister() + { + yield return typeof(Registered); + yield return typeof(InGame); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/List/Registered.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/List/Registered.cs new file mode 100644 index 0000000000..b9dfab3471 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/List/Registered.cs @@ -0,0 +1,81 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin.List +{ + using System; + using System.Linq; + using System.Text; + + using API.Features; + using CommandSystem; + using Exiled.API.Features.Pools; + using Permissions.Extensions; + + /// + internal sealed class Registered : ICommand + { + /// + /// Gets or sets the command aliases. + /// + public string[] Aliases { get; set; } = { "r", "reg" }; + + /// + /// Gets or sets the command description. + /// + public string Description { get; set; } = "Список всех кастомных ролей."; + + /// + /// Gets or sets the message to display when the sender lacks permission. + /// + public string NoPermissionMessage { get; set; } = "Не хватает прав!"; + + /// + /// Gets or sets the message to display when there are no custom roles. + /// + public string NoCustomRolesMessage { get; set; } = "На сервере нет кастомных ролей."; + + /// + /// Gets or sets the format for the custom roles list. + /// + public string CustomRolesListFormat { get; set; } = "[Кастомные роли ({0})]"; + + /// + /// Gets or sets the format for a single custom role. + /// + public string CustomRoleFormat { get; set; } = "[{0}. {1} ({2})]"; + + /// + public string Command { get; set; } = "registered"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!sender.CheckPermission("customroles.list.registered")) + { + response = NoPermissionMessage; + return false; + } + + if (CustomRole.Registered.Count == 0) + { + response = NoCustomRolesMessage; + return false; + } + + StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); + + builder.Append(string.Format(CustomRolesListFormat, CustomRole.Registered.Count)); + + foreach (CustomRole role in CustomRole.Registered.OrderBy(r => r.Id)) + builder.Append(string.Format(CustomRoleFormat, role.Id, role.Name, role.Role)).AppendLine(); + + response = StringBuilderPool.Pool.ToStringReturn(builder); + return true; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Admin/Parent.cs b/EXILED/Exiled.CustomRoles/Commands/Admin/Parent.cs new file mode 100644 index 0000000000..f62ec0bebf --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/Admin/Parent.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.Admin +{ + using System; + using System.Collections.Generic; + + using CommandSystem; + using Exiled.API.Features; + + /// + /// The main parent command. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + public class Parent : ParentCommand + { + /// + public override string Command { get; } = "customroles"; + + /// + public override string[] Aliases { get; set; } = { "cr", "crs" }; + + /// + public override string Description { get; set; } = string.Empty; + + /// + protected override IEnumerable CommandsToRegister() + { + yield return typeof(Give); + yield return typeof(Info); + yield return typeof(List.List); + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Give.cs b/EXILED/Exiled.CustomRoles/Commands/Give.cs index 2f74b46498..150f325b66 100644 --- a/EXILED/Exiled.CustomRoles/Commands/Give.cs +++ b/EXILED/Exiled.CustomRoles/Commands/Give.cs @@ -12,12 +12,12 @@ namespace Exiled.CustomRoles.Commands using System.Linq; using CommandSystem; - + using Exiled.API.Enums; using Exiled.API.Features; using Exiled.API.Features.Pools; using Exiled.CustomRoles.API.Features; using Exiled.Permissions.Extensions; - + using PlayerRoles; using RemoteAdmin; /// @@ -72,7 +72,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s { Player player = Player.Get(playerCommandSender); - role.AddRole(player); + role.AddRole(player, SpawnReason.None, RoleSpawnFlags.None); response = $"{role.Name} given to {player.Nickname}."; return true; } @@ -90,7 +90,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s List players = ListPool.Pool.Get(Player.List); foreach (Player player in players) - role.AddRole(player); + role.AddRole(player, SpawnReason.None, RoleSpawnFlags.None); response = $"Custom role {role.Name} given to all players."; ListPool.Pool.Return(players); @@ -107,7 +107,7 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s } foreach (Player player in list) - role.AddRole(player); + role.AddRole(player, SpawnReason.None, RoleSpawnFlags.None); response = $"Customrole {role.Name} given to {list.Count()} players!"; @@ -121,4 +121,4 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.CustomRoles/Commands/Info.cs b/EXILED/Exiled.CustomRoles/Commands/Info.cs deleted file mode 100644 index a4fc7938c1..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/Info.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands -{ - using System; - using System.Text; - - using CommandSystem; - - using Exiled.API.Features.Pools; - using Exiled.CustomRoles.API.Features; - using Exiled.Permissions.Extensions; - - /// - /// The command to view info about a specific role. - /// - internal sealed class Info : ICommand - { - private Info() - { - } - - /// - /// Gets the instance. - /// - public static Info Instance { get; } = new(); - - /// - public string Command { get; } = "info"; - - /// - public string[] Aliases { get; } = { "i" }; - - /// - public string Description { get; } = "Gets more information about the specified custom role."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (!sender.CheckPermission("customroles.info")) - { - response = "Permission Denied, required: customroles.info"; - return false; - } - - if (arguments.Count < 1) - { - response = "info [Custom role name/Custom role ID]"; - return false; - } - - if ((!(uint.TryParse(arguments.At(0), out uint id) && CustomRole.TryGet(id, out CustomRole? role)) && !CustomRole.TryGet(arguments.At(0), out role)) || role is null) - { - response = $"{arguments.At(0)} is not a valid custom role."; - return false; - } - - StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); - - builder.Append("- ").Append(role.Name) - .Append(" (").Append(role.Id).Append(")") - .Append("- ").AppendLine(role.Description) - .AppendLine(role.Role.ToString()) - .Append("- Health: ").AppendLine(role.MaxHealth.ToString()).AppendLine(); - - response = StringBuilderPool.Pool.ToStringReturn(builder); - return true; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/List/Abilities.cs b/EXILED/Exiled.CustomRoles/Commands/List/Abilities.cs deleted file mode 100644 index 8fd1e9cd6d..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/List/Abilities.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands.List -{ - using System; - using System.Linq; - using System.Text; - - using CommandSystem; - - using Exiled.API.Features.Pools; - using Exiled.CustomRoles.API.Features; - using Exiled.Permissions.Extensions; - - /// - internal sealed class Abilities : ICommand - { - /// - public string Command => "abilities"; - - /// - public string[] Aliases => new[] { "a" }; - - /// - public string Description => "Lists all abilities on the server."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (!sender.CheckPermission("customroles.list.abilities")) - { - response = "Permission Denied, required: customroles.list.abilities"; - return false; - } - - if (CustomAbility.Registered.Count == 0) - { - response = "There are no custom abilities currently on this server."; - return false; - } - - StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); - - builder.Append("[Registered custom roles (").Append(CustomRole.Registered.Count).AppendLine(")]"); - - foreach (CustomAbility ability in CustomAbility.Registered.OrderBy(r => r.Name)) - builder.Append('[').Append(ability.Name).Append(" (").Append(ability.Description).Append(')').AppendLine("]"); - - response = StringBuilderPool.Pool.ToStringReturn(builder); - return true; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/List/List.cs b/EXILED/Exiled.CustomRoles/Commands/List/List.cs deleted file mode 100644 index 5e706b53f5..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/List/List.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands.List -{ - using System; - - using CommandSystem; - - /// - /// The command to list all registered roles. - /// - internal sealed class List : ParentCommand - { - private List() - { - LoadGeneratedCommands(); - } - - /// - /// Gets the command instance. - /// - public static List Instance { get; } = new(); - - /// - public override string Command { get; } = "list"; - - /// - public override string[] Aliases { get; } = { "l" }; - - /// - public override string Description { get; } = "Gets a list of all currently registered custom roles."; - - /// - public override void LoadGeneratedCommands() - { - RegisterCommand(Registered.Instance); - RegisterCommand(new Abilities()); - } - - /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) - { - if (arguments.IsEmpty() && TryGetCommand(Registered.Instance.Command, out ICommand command)) - { - command.Execute(arguments, sender, out response); - response += $"\nTo view all abilities registered use command: {string.Join(" ", arguments.Array)} abilities"; - return true; - } - - response = "Invalid subcommand! Available: registered, abilities"; - return false; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/List/Registered.cs b/EXILED/Exiled.CustomRoles/Commands/List/Registered.cs deleted file mode 100644 index 98ba945a62..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/List/Registered.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands.List -{ - using System; - using System.Linq; - using System.Text; - - using CommandSystem; - - using Exiled.API.Features.Pools; - using Exiled.CustomRoles.API.Features; - using Exiled.Permissions.Extensions; - - /// - internal sealed class Registered : ICommand - { - private Registered() - { - } - - /// - /// Gets the command instance. - /// - public static Registered Instance { get; } = new(); - - /// - public string Command { get; } = "registered"; - - /// - public string[] Aliases { get; } = { "r" }; - - /// - public string Description { get; } = "Gets a list of registered custom roles."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (!sender.CheckPermission("customroles.list.registered")) - { - response = "Permission Denied, required: customroles.list.registered"; - return false; - } - - if (CustomRole.Registered.Count == 0) - { - response = "There are no custom roles currently on this server."; - return false; - } - - StringBuilder builder = StringBuilderPool.Pool.Get().AppendLine(); - - builder.Append("[Registered custom roles (").Append(CustomRole.Registered.Count).AppendLine(")]"); - - foreach (CustomRole role in CustomRole.Registered.OrderBy(r => r.Id)) - builder.Append('[').Append(role.Id).Append(". ").Append(role.Name).Append(" (").Append(role.Role).Append(')').AppendLine("]"); - - response = StringBuilderPool.Pool.ToStringReturn(builder); - return true; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/Parent.cs b/EXILED/Exiled.CustomRoles/Commands/Parent.cs deleted file mode 100644 index f64facabec..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/Parent.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands -{ - using System; - - using CommandSystem; - - /// - /// The main parent command. - /// - [CommandHandler(typeof(RemoteAdminCommandHandler))] - [CommandHandler(typeof(GameConsoleCommandHandler))] - public class Parent : ParentCommand - { - /// - /// Initializes a new instance of the class. - /// - public Parent() - { - LoadGeneratedCommands(); - } - - /// - public override string Command { get; } = "customroles"; - - /// - public override string[] Aliases { get; } = { "cr", "crs" }; - - /// - public override string Description { get; } = string.Empty; - - /// - public override void LoadGeneratedCommands() - { - RegisterCommand(Give.Instance); - RegisterCommand(Info.Instance); - RegisterCommand(List.List.Instance); - } - - /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) - { - response = "Invalid subcommand! Available: give, info, list"; - return false; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/UseAbility.cs b/EXILED/Exiled.CustomRoles/Commands/UseAbility.cs deleted file mode 100644 index 10accb2871..0000000000 --- a/EXILED/Exiled.CustomRoles/Commands/UseAbility.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Commands -{ - using System; - using System.Linq; - - using CommandSystem; - - using Exiled.API.Features; - using Exiled.CustomRoles.API; - using Exiled.CustomRoles.API.Features; - - /// - /// Handles the using of custom role abilities. - /// - [CommandHandler(typeof(ClientCommandHandler))] - public class UseAbility : ICommand - { - /// - public string Command { get; } = "useability"; - - /// - public string[] Aliases { get; } = { "special" }; - - /// - public string Description { get; } = "Use your custom roles special ability, if available."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - Player player = Player.Get((CommandSender)sender); - string abilityName = string.Empty; - ActiveAbility? ability; - - if (arguments.Count > 0) - { - foreach (string s in arguments.Skip(1)) - abilityName += s; - - if (!CustomAbility.TryGet(abilityName, out CustomAbility? customAbility) || customAbility is null) - { - response = $"Ability {abilityName} does not exist."; - return false; - } - - if (customAbility is not ActiveAbility activeAbility) - { - response = $"{abilityName} is not an active ability."; - return false; - } - - ability = activeAbility; - } - else - { - ability = player.GetSelectedAbility(); - } - - if (ability is null) - { - response = "No selected ability."; - return false; - } - - if (!ability.CanUseAbility(player, out response, CustomRoles.Instance.Config.ActivateOnlySelected)) - return false; - response = $"{ability.Name} has been used."; - ability.UseAbility(player); - return true; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/User/RoleInfo.cs b/EXILED/Exiled.CustomRoles/Commands/User/RoleInfo.cs new file mode 100644 index 0000000000..3eb19eb674 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/User/RoleInfo.cs @@ -0,0 +1,145 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Commands.User +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + + using API; + using API.Features; + using CommandSystem; + using Exiled.API.Features; + using NorthwoodLib.Pools; + + /// + /// Handles the displaing of custom role info. + /// + [CommandHandler(typeof(ClientCommandHandler))] + public class RoleInfo : ICommand + { + /// + /// Gets or sets the header text for passive abilities. + /// + /// The header text for passive abilities. + public static string PassiveAbilitiesHeaderText { get; set; } = "Пассивные способности:"; + + /// + /// Gets or sets the header text for active abilities. + /// + /// The header text for active abilities. + public static string ActiveAbilitiesHeaderText { get; set; } = "Активные способности:"; + + /// + /// Gets or sets the text to display for instant duration abilities. + /// + /// The text to display for instant duration abilities. + public static string InstantDurationText { get; set; } = "Мгновенно"; + + /// + /// Gets or sets the response message to display when a player is not found. + /// + /// The response message to display when a player is not found. + public string PlayerNotFoundResponse { get; set; } = "Попробуйте позже"; + + /// + /// Gets or sets the response message to display when a player has no custom role. + /// + /// The response message to display when a player has no custom role. + public string NoCustomRoleResponse { get; set; } = "У вас нет особых ролей!"; + + /// + public string Command { get; set; } = "roleinfo"; + + /// + public string[] Aliases { get; set; } = { "rinfo" }; + + /// + public string Description { get; set; } = "Даёт справку по вашей текущей особой роли и её способностях."; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + Player player = Player.Get(sender); + if (player == null) + { + response = PlayerNotFoundResponse; + return false; + } + + if (!player.TryGetCustomRole(out CustomRole? customRole)) + { + response = NoCustomRoleResponse; + return false; + } + + response = string.Format(CustomRoles.Instance!.Config.RoleInfoResponse, customRole.Name, customRole.Id, customRole.Description, GetAbilitiesInfo(player, customRole)); + return true; + } + + /// + /// Get abilities information. + /// + /// Target . + /// Target . + /// . + /// represents abilities info. + internal static string GetAbilitiesInfo(Player player, CustomRole customRole, bool includePassive = true) + { + List passiveAbilities = new(); + List activeAbilities = new(); + List scp079ActiveAbilities = new(); + StringBuilder? stringBuilder = StringBuilderPool.Shared.Rent(); + foreach (CustomAbility? ability in customRole.CustomAbilities!) + { + switch (ability) + { + case Scp079ActiveAbility scp079ActiveAbility: + if (!scp079ActiveAbility.IsAvailable(player)) + continue; + + scp079ActiveAbilities.Add(scp079ActiveAbility); + break; + case ActiveAbility activeAbility: + activeAbilities.Add(activeAbility); + break; + case PassiveAbility { IsHidden: false } passiveAbility when includePassive: + passiveAbilities.Add(passiveAbility); + break; + } + } + + if (includePassive && passiveAbilities.Any()) + { + stringBuilder.AppendFormat(CustomRoles.Instance!.Config.AbilityBlockFormat + '\n', PassiveAbilitiesHeaderText); + for (int i = 0; i < passiveAbilities.Count; i++) + stringBuilder.AppendFormat(CustomRoles.Instance!.Config.PassiveAbilityLineFormat + '\n', i + 1, passiveAbilities[i].Name, passiveAbilities[i].Description); + + stringBuilder.AppendLine(); + } + + if (activeAbilities.Any() || scp079ActiveAbilities.Any()) + { + stringBuilder.AppendFormat(CustomRoles.Instance!.Config.AbilityBlockFormat + '\n', ActiveAbilitiesHeaderText); + for (int i = 0; i < activeAbilities.Count; i++) + stringBuilder.AppendFormat(CustomRoles.Instance!.Config.ActiveAbilityLineFormat + '\n', i + 1, activeAbilities[i].Name, activeAbilities[i].Description, GetAbilityDuration(activeAbilities[i]), activeAbilities[i].Cooldown); + + for (int i = 0; i < scp079ActiveAbilities.Count; i++) + { + stringBuilder.AppendFormat(CustomRoles.Instance!.Config.Active079AbilityLineFormat + '\n', i + 1, scp079ActiveAbilities[i].Name, scp079ActiveAbilities[i].Description, GetAbilityDuration(scp079ActiveAbilities[i]), scp079ActiveAbilities[i].Cooldown, scp079ActiveAbilities[i].MinRequiredLevel, scp079ActiveAbilities[i].EnergyUsage); + } + } + + return StringBuilderPool.Shared.ToStringReturn(stringBuilder); + } + + private static string GetAbilityDuration(ActiveAbility activeAbility) => activeAbility.Duration <= 0 ? InstantDurationText : activeAbility.Duration.ToString(CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Commands/User/UseAbility.cs b/EXILED/Exiled.CustomRoles/Commands/User/UseAbility.cs new file mode 100644 index 0000000000..56e011fe23 --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Commands/User/UseAbility.cs @@ -0,0 +1,131 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- +namespace Exiled.CustomRoles.Commands.User +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using API; + using API.Features; + using CommandSystem; + using Exiled.API.Features; + + /// + /// Handles the using of custom role abilities. + /// + [CommandHandler(typeof(ClientCommandHandler))] + public class UseAbility : ICommand + { + /// + public string Command { get; set; } = "ability"; + + /// + public string[] Aliases { get; set; } = { "a" }; + + /// + public string Description { get; set; } = "Использует спецспособность"; + + /// + /// Gets or sets the response when no custom role is found. + /// + public string NoCustomRoleResponse { get; set; } = "У вас нет спецролей со спецспособностями"; + + /// + /// Gets or sets the response when no abilities detected. + /// + public string NoAbilitiesResponse { get; set; } = "У вашей спецроли нет спецспособностей!"; + + /// + /// Gets or sets the response when an invalid ability number is detected. + /// + public string InvalidAbilityNumberResponse { get; set; } = "Такая способность не существует!"; + + /// + /// Gets or sets the response when invalid arguments detected. + /// + public string InvalidArgumentResponse { get; set; } = "{0} не является числом!"; + + /// + /// Gets or sets the response when an ability is already used. + /// + public string AbilityUsedResponse { get; set; } = "Способность {0} успешно использована!"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + Player player = Player.Get((CommandSender)sender); + + if (!player.TryGetCustomRole(out CustomRole role)) + { + response = NoCustomRoleResponse; + return false; + } + + if (arguments.Count == 0) + { + response = string.Format(CustomRoles.Instance!.Config.UseAbilityResponse, RoleInfo.GetAbilitiesInfo(player, role)); + return false; + } + + if (role.CustomAbilities == null) + { + response = NoAbilitiesResponse; + return false; + } + + List activeAbilities = new(); + foreach (CustomAbility? ability in role.CustomAbilities) + { + if (ability is Scp079ActiveAbility scp079ActiveAbility) + { + if (!scp079ActiveAbility.IsAvailable(player)) + continue; + + activeAbilities.Add(scp079ActiveAbility); + } + else if (ability is ActiveAbility activeAbility) + { + activeAbilities.Add(activeAbility); + } + } + + if (activeAbilities.IsEmpty()) + { + response = NoAbilitiesResponse; + return false; + } + + int abilityNumber = 0; + if (arguments.Count > 0) + { + if (!int.TryParse(arguments.At(0), out abilityNumber)) + { + response = string.Format(InvalidArgumentResponse, arguments.At(0)); + return false; + } + } + + if (activeAbilities.Count < abilityNumber) + { + response = InvalidAbilityNumberResponse; + return false; + } + + if (!activeAbilities[abilityNumber - 1].CanUseAbility(player, out string res)) + { + response = res; + player.ShowHint(response, 5); + return false; + } + + activeAbilities[abilityNumber - 1].UseAbility(player); + response = string.Format(AbilityUsedResponse, activeAbilities[abilityNumber - 1].Name); + return false; + } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Config.cs b/EXILED/Exiled.CustomRoles/Config.cs index 3ce80a2c88..038e5334c8 100644 --- a/EXILED/Exiled.CustomRoles/Config.cs +++ b/EXILED/Exiled.CustomRoles/Config.cs @@ -9,60 +9,129 @@ namespace Exiled.CustomRoles { using System.ComponentModel; + using API.Features; + + using Commands.User; + using Exiled.API.Features; using Exiled.API.Interfaces; - using Exiled.CustomRoles.API.Features; /// - /// The plugin's config. + /// The plugin's config. /// public class Config : IConfig { - /// - [Description("Whether the plugin is enabled.")] + /// + [Description("Whether or not the plugin is enabled.")] public bool IsEnabled { get; set; } = true; /// - /// Gets or sets a value indicating whether debug messages should be printed to the console. + /// Gets or sets a value indicating whether debug messages should be printed to the console. /// - /// . - [Description("Whether debug messages should be shown.")] + /// . + [Description("Whether or not debug messages should be shown.")] public bool Debug { get; set; } = false; /// - /// Gets the hint that is shown when someone gets a . + /// Gets or sets the hint that is shown when someone gets a . /// [Description("The hint that is shown when someone gets a custom role.")] - public Broadcast GotRoleHint { get; private set; } = new("You have spawned as a {0}\n{1}", 6); + public Hint GotRoleHint { get; set; } = new("You have spawned as a {0}\n{1}", 6); /// - /// Gets the hint that is shown when someone used an . + /// Gets or sets the hint that is shown when someone used a . /// [Description("The hint that is shown when someone used a custom ability.")] - public Broadcast UsedAbilityHint { get; private set; } = new("Ability {0} has been activated.\n{1}", 5); + public Hint UsedAbilityHint { get; set; } = new("Ability {0} has been activated.\n{1}", 5); + + /// + /// Gets or sets the hint that is shown when someone used a . + /// + [Description("The hint that is shown when someone's custom ability is ready.")] + public Hint AbilityReadyHint { get; set; } = new("Ability {0} is ready.\n{1}", 5); + + /// + /// Gets or sets the hint that is shown when someone used a cooldowned . + /// + [Description("The hint that is shown when someone tries to use cooldowned ability. Also used in console respond. {0} - remaining cooldown, {1} - ability name")] + public Hint AbilityOnCooldownHint { get; set; } = new("Способность на перезарядке!\nПодождите ещё {0} секунд перед использованием.", 5); + + /// + /// Gets or sets the hint that is shown when someone tries to use without required energy. + /// + [Description("The hint that is shown when someone tries to use ability without required energy. Also used in console respond. {0} - current energy, {1} - required energy")] + public Hint InsufficientEnergyHint { get; set; } = new("Недостаточно энергии!\nУ вас {0}/{1}", 5); + + /// + /// Gets or sets the hint that is shown when someone tries to use without required level. + /// + [Description("The hint that is shown when someone tries to use ability without required level. Also used in console respond. {0} - current level, {1} - required level")] + public Hint InsufficientLevelHint { get; set; } = new("Недостаточный уровень!\nУ вас {0}/{1}", 5); + + /// + /// Gets or sets the hint that is shown when someone tries to use with too high level. + /// + [Description("The hint that is shown when someone tries to use ability with too high level. Also used in console respond. {0} - current level, {1} - required level")] + public Hint RedundantLevelHint { get; set; } = new("Избыточный уровень!\nУ вас {0}/{1}", 5); + + /// + /// Gets or sets response of . + /// + [Description("Формат ответа команды RoleInfo. 0 - Название, 1 - айди, 2 - описание, 3 - способности")] + public string RoleInfoResponse { get; set; } = "\nВаша особая роль: {0}\n" + + "ID: {1}\n" + + "{2}\n" + + "{3}\n" + // Теги будут влиять на заголовки разделов + "Для активации способности напишите команду .ability\n\n" + + "Вы можете сделать активацию способности по клавише!\n" + + "Для этого напишите команду cmdbind КЛАВИША .ability НОМЕР_СПОСОБНОСТИ"; + + /// + /// Gets or sets response of . + /// + [Description("Формат ответа команды RoleInfo. 0 - способности")] + public string UseAbilityResponse { get; set; } = "\nУкажите номер способности.\n" + + "Список:\n" + + "\n{0}\n" + + "Вы можете сделать активацию способности по клавише!\n" + + "Для этого напишите команду cmdbind КЛАВИША .ability НОМЕР_СПОСОБНОСТИ"; + + /// + /// Gets or sets format for displaing in command. + /// + [Description("Формат отображения строки способности. 0 - номер в разделе, 1 - название, 2 - описание. Следует помнить, что эта строка вставляется в раздел 3 выше")] + public string PassiveAbilityLineFormat { get; set; } = " - #{0} {1}\n{2}"; + + /// + /// Gets or sets format for displaing in and + /// commands. + /// + [Description("Формат отображения строки способности. 0 - номер в разделе, 1 - название, 2 - описание, 3 - длительность (заменяется на 'Мгновенно', если равно нулю), 4 - КД. Следует помнить, что эта строка вставляется в раздел 3 выше")] + public string ActiveAbilityLineFormat { get; set; } = " - #{0} {1}\n{2}"; /// - /// Gets or sets a value indicating whether keypress ability activations can be used on the server. + /// Gets or sets format for displaing in and + /// commands. /// - [Description("Whether Keypress ability activations will work on the server.")] - public bool UseKeypressActivation { get; set; } = true; + [Description("Формат отображения строки способности. 0 - номер в разделе, 1 - название, 2 - описание, 3 - длительность (заменяется на 'Мгновенно', если равно нулю), 4 - КД, 5 - требуемый уровень, 6 - требуемая энергия. Следует помнить, что эта строка вставляется в раздел 3 выше")] + public string Active079AbilityLineFormat { get; set; } = " - #{0} {1}\n{2}"; /// - /// Gets or sets a value indicating whether abilities that are not the keypress primary can still be activated. + /// Gets or sets ability line format. /// - [Description("Whether abilities that are not selected as the current keypress ability can still be activated.")] - public bool ActivateOnlySelected { get; set; } = true; + [Description("Формат названия раздела способностей. {0} - 'Пассивные способности:'/'Активные способности:'")] + public string AbilityBlockFormat { get; set; } = "{0}"; /// - /// Gets or sets the hint that is shown when someone fails to use a . + /// Gets or sets a value indicating whether fuck these docs. /// - [Description("The hint showed to players when they fail to preform an action.")] - public Broadcast FailedActionHint { get; set; } = new("Failed to preform action: {0}", 5); + [Description("Формат названия раздела способностей. {0} - 'Пассивные способности:'/'Активные способности:'")] + public bool HideUnavailableHighLevelAbilities { get; set; } = false; /// - /// Gets or sets the hint that is shown when someone switches their selected . + /// Gets or sets customroles nickname display to spectators. /// - [Description("The hint that is shown to players when they switch abilities.")] - public Broadcast SwitchedAbilityHint { get; set; } = new("Selected ability {0}", 5); + [Description("Задержка до синхронизации имён кастомных ролей и наблюдателей.")] + public float CustomRolesSpectatorDisplayDelay { get; set; } = 2; } } \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/CustomRoles.cs b/EXILED/Exiled.CustomRoles/CustomRoles.cs index a66457b3bd..3fc1495514 100644 --- a/EXILED/Exiled.CustomRoles/CustomRoles.cs +++ b/EXILED/Exiled.CustomRoles/CustomRoles.cs @@ -7,79 +7,61 @@ namespace Exiled.CustomRoles { - using System.Collections.Generic; - + using Events; using Exiled.API.Features; - using Exiled.CustomRoles.API.Features; - using Exiled.CustomRoles.API.Features.Parsers; - using Exiled.CustomRoles.Events; - using Exiled.Loader; - using Exiled.Loader.Features.Configs.CustomConverters; - using YamlDotNet.Serialization; - using YamlDotNet.Serialization.NamingConventions; - using YamlDotNet.Serialization.NodeDeserializers; + using Player = Exiled.Events.Handlers.Player; + using Server = Exiled.Events.Handlers.Server; /// - /// Handles all custom role API functions. + /// Handles all custom role API functions. /// public class CustomRoles : Plugin { - private PlayerHandlers? playerHandlers; - private KeypressActivator? keypressActivator; + private PlayerHandler playerHandler = null!; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CustomRoles() { - Loader.Deserializer = new DeserializerBuilder() - .WithTypeConverter(new VectorsConverter()) - .WithTypeConverter(new ColorConverter()) - .WithTypeConverter(new AttachmentIdentifiersConverter()) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithNodeDeserializer(inner => new AbstractClassNodeTypeResolver(inner, new AggregateExpectationTypeResolver(UnderscoredNamingConvention.Instance)), s => s.InsteadOf()) - .IgnoreFields() - .IgnoreUnmatchedProperties() - .Build(); } /// - /// Gets a static reference to the plugin's instance. - /// - public static CustomRoles Instance { get; private set; } = null!; - - /// - /// Gets a list of players to stop spawning ragdolls for. + /// Gets a static reference to the plugin's instance. /// - internal List StopRagdollPlayers { get; } = new(); + public static CustomRoles? Instance { get; private set; } - /// + /// public override void OnEnabled() { Instance = this; - playerHandlers = new PlayerHandlers(this); + playerHandler = new PlayerHandler(); + + Server.WaitingForPlayers += playerHandler.OnWaitingForPlayers; - if (Config.UseKeypressActivation) - keypressActivator = new(); + Player.Spawned += playerHandler.OnSpawned; - Exiled.Events.Handlers.Player.Spawned += playerHandlers.OnSpawned; - Exiled.Events.Handlers.Player.SpawningRagdoll += playerHandlers.OnSpawningRagdoll; + Player.ChangingRole += playerHandler.OnChangingRole; + Player.SendingRole += playerHandler.OnSendingRole; + Player.ChangedNickname += playerHandler.OnChangedNickname; - Exiled.Events.Handlers.Server.WaitingForPlayers += playerHandlers.OnWaitingForPlayers; base.OnEnabled(); } - /// + /// public override void OnDisabled() { - Exiled.Events.Handlers.Player.Spawned -= playerHandlers!.OnSpawned; - Exiled.Events.Handlers.Player.SpawningRagdoll -= playerHandlers!.OnSpawningRagdoll; + Server.WaitingForPlayers -= playerHandler.OnWaitingForPlayers; + + Player.Spawned -= playerHandler.OnSpawned; - Exiled.Events.Handlers.Server.WaitingForPlayers -= playerHandlers!.OnWaitingForPlayers; + Player.ChangingRole -= playerHandler.OnChangingRole; + Player.SendingRole -= playerHandler.OnSendingRole; + Player.ChangedNickname -= playerHandler.OnChangedNickname; - keypressActivator = null; + Instance = null; base.OnDisabled(); } } -} +} \ No newline at end of file diff --git a/EXILED/Exiled.CustomRoles/Events/PlayerHandler.cs b/EXILED/Exiled.CustomRoles/Events/PlayerHandler.cs new file mode 100644 index 0000000000..081a2bbdab --- /dev/null +++ b/EXILED/Exiled.CustomRoles/Events/PlayerHandler.cs @@ -0,0 +1,119 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.CustomRoles.Events +{ + using System.Linq; + + using API; + using API.Features; + + using Exiled.API.Enums; + using Exiled.API.Features; + using Exiled.API.Features.Roles; + using Exiled.API.Features.Spawn; + using Exiled.Events.EventArgs.Player; + + using FLXLib.Extensions; + + using MEC; + + using PlayerRoles; + + using UnityEngine; + + /// + /// Event Handlers for the CustomRole API. + /// + public class PlayerHandler + { + /// + /// SessionVariable key. + /// + public const string LastCustomRoleKey = "LastCustomRole"; + + /// + public void OnWaitingForPlayers() + { + Extensions.InternalPlayerToCustomRoles.Clear(); + Extensions.ToChangeRolePlayers.Clear(); + Extensions.AssignInventoryPlayers.Clear(); + } + + /// + public void OnChangingRole(ChangingRoleEventArgs ev) + { + if (ev.Reason is SpawnReason.Destroyed or SpawnReason.None) + return; + + if (ev.Player.TryGetCustomRole(out CustomRole customRole)) + ev.Player.SessionVariables[LastCustomRoleKey] = customRole; + else + ev.Player.SessionVariables.Remove(LastCustomRoleKey); + } + + /// + public void OnSpawned(SpawnedEventArgs ev) + { + Log.Debug($"Spawned {ev.Player.Nickname}"); + if (Extensions.ToChangeRolePlayers.TryGetValue(ev.Player, out CustomRole cr)) + { + Log.Debug($"Player with role {ev.Player.Role.Type} going to custom role {cr.Name} ({cr.Id})"); + if (cr.SpawnProperties.IsAny && !ev.SpawnFlags.HasFlag(RoleSpawnFlags.UseSpawnpoint)) + ev.Player.Position = cr.SpawnProperties.GetRandomPoint() + (Vector3.up * 1.5f); + + cr.AddProperties(ev.Player, ev.Reason, Extensions.AssignInventoryPlayers.Remove(ev.Player)); + cr.RoleAdded(ev.Player); + + Extensions.ToChangeRolePlayers.Remove(ev.Player); + } + } + + /// + public void OnSendingRole(SendingRoleEventArgs ev) + { + if (ev.Target == null) + return; + + if (ev.Player.IsDead && ev.Target.TryGetCustomRole(out CustomRole role)) + { + ev.Player.SetDispayNicknameForTargetOnly(ev.Target, role.GetSpectatorText(ev.Target)); + Log.Debug($"[Name sync] Sent name of {ev.Target.Nickname} to {ev.Player.Nickname}"); + } + else if (ev.Target.IsDead && ev.Player.TryGetCustomRole(out role)) + { + ev.Target.SetDispayNicknameForTargetOnly(ev.Player, role.GetSpectatorText(ev.Player)); + Log.Debug($"[Name sync] Sent name of {ev.Player.Nickname} to {ev.Target.Nickname}"); + } + else + { + Log.Debug($"[Name sync] Name reset for {ev.Player.Nickname} of {ev.Target.Nickname}."); + ev.Target.SetDispayNicknameForTargetOnly(ev.Player, ev.Player.CustomName); + ev.Player.SetDispayNicknameForTargetOnly(ev.Target, ev.Target.CustomName); + } + } + + /// + public void OnChangedNickname(ChangedNicknameEventArgs ev) + { + Timing.CallDelayed(0.1f, () => + { + if (ev.Player is { IsConnected: true } && ev.Player.TryGetCustomRole(out CustomRole role)) + { + foreach (Player player in Player.List) + { + if (!player.IsDead) + continue; + + player.SetDispayNicknameForTargetOnly(ev.Player, role.GetSpectatorText(ev.Player)); + Log.Debug($"[Name sync] [{nameof(OnChangedNickname)}] Sent name of {ev.Player.Nickname} to {player.Nickname}"); + } + } + }); + } + } +} diff --git a/EXILED/Exiled.CustomRoles/Events/PlayerHandlers.cs b/EXILED/Exiled.CustomRoles/Events/PlayerHandlers.cs deleted file mode 100644 index 1a10e38dcf..0000000000 --- a/EXILED/Exiled.CustomRoles/Events/PlayerHandlers.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.CustomRoles.Events -{ - using System; - using System.Collections.Generic; - using System.Threading; - - using Exiled.API.Enums; - using Exiled.API.Features; - using Exiled.CustomRoles.API; - using Exiled.CustomRoles.API.Features; - using Exiled.Events.EventArgs.Player; - - /// - /// Handles general events for players. - /// - public class PlayerHandlers - { - private static readonly HashSet ValidSpawnReasons = new() - { - SpawnReason.RoundStart, - SpawnReason.Respawn, - SpawnReason.LateJoin, - SpawnReason.Revived, - SpawnReason.Escaped, - SpawnReason.ItemUsage, - }; - - private readonly CustomRoles plugin; - - /// - /// Initializes a new instance of the class. - /// - /// The plugin instance. - public PlayerHandlers(CustomRoles plugin) - { - this.plugin = plugin; - } - - /// - internal void OnWaitingForPlayers() - { - foreach (CustomRole role in CustomRole.Registered) - { - role.SpawnedPlayers = 0; - } - } - - /// - internal void OnSpawningRagdoll(SpawningRagdollEventArgs ev) - { - if (plugin.StopRagdollPlayers.Contains(ev.Player)) - { - ev.IsAllowed = false; - plugin.StopRagdollPlayers.Remove(ev.Player); - } - } - - /// - internal void OnSpawned(SpawnedEventArgs ev) - { - if (!ValidSpawnReasons.Contains(ev.Reason) || ev.Player.HasAnyCustomRole()) - { - return; - } - - float totalChance = 0f; - List eligibleRoles = new(8); - - foreach (CustomRole role in CustomRole.Registered) - { - if (role.Role == ev.Player.Role.Type && !role.IgnoreSpawnSystem && role.SpawnChance > 0 && !role.Check(ev.Player) && (role.SpawnProperties is null || role.SpawnedPlayers < role.SpawnProperties.Limit)) - { - eligibleRoles.Add(role); - totalChance += role.SpawnChance; - } - } - - if (eligibleRoles.Count == 0) - { - return; - } - - float lotterySize = Math.Max(100f, totalChance); - float randomRoll = (float)Loader.Loader.Random.NextDouble() * lotterySize; - - if (randomRoll >= totalChance) - { - return; - } - - foreach (CustomRole candidateRole in eligibleRoles) - { - if (randomRoll >= candidateRole.SpawnChance) - { - randomRoll -= candidateRole.SpawnChance; - continue; - } - - if (candidateRole.SpawnProperties is null) - { - candidateRole.AddRole(ev.Player); - break; - } - - int newSpawnCount = candidateRole.SpawnedPlayers++; - if (newSpawnCount <= candidateRole.SpawnProperties.Limit) - { - candidateRole.AddRole(ev.Player); - break; - } - else - { - candidateRole.SpawnedPlayers--; - randomRoll -= candidateRole.SpawnChance; - } - } - } - } -} diff --git a/EXILED/Exiled.CustomRoles/Exiled.CustomRoles.csproj b/EXILED/Exiled.CustomRoles/Exiled.CustomRoles.csproj index 78b2030580..2fddb90049 100644 --- a/EXILED/Exiled.CustomRoles/Exiled.CustomRoles.csproj +++ b/EXILED/Exiled.CustomRoles/Exiled.CustomRoles.csproj @@ -30,6 +30,7 @@ + @@ -41,9 +42,14 @@ - - if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/"; fi diff --git a/EXILED/Exiled.Events/Commands/Config/EConfig.cs b/EXILED/Exiled.Events/Commands/Config/EConfig.cs deleted file mode 100644 index 6d4882873a..0000000000 --- a/EXILED/Exiled.Events/Commands/Config/EConfig.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Commands.Config -{ - using System; - - using CommandSystem; - - /// - /// The config command. - /// - [CommandHandler(typeof(GameConsoleCommandHandler))] - public class EConfig : ParentCommand - { - /// - public override string Command { get; } = "econfig"; - - /// - public override string[] Aliases { get; } = new[] { "ecfg" }; - - /// - public override string Description { get; } = "Changes from one config distribution to another one."; - - /// - public override void LoadGeneratedCommands() - { - } - - /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) - { - response = "Please, specify a valid subcommand! Available ones: merge, split"; - return false; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Commands/Config/Merge.cs b/EXILED/Exiled.Events/Commands/Config/Merge.cs deleted file mode 100644 index c6cf79042d..0000000000 --- a/EXILED/Exiled.Events/Commands/Config/Merge.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Commands.Config -{ - using System; - using System.Collections.Generic; - using System.Linq; - - using API.Enums; - using API.Features; - using API.Interfaces; - using CommandSystem; - using Loader; - - /// - /// The config merge command. - /// - [CommandHandler(typeof(EConfig))] - public class Merge : ICommand - { - /// - /// Gets static instance of the command. - /// - public static Merge Instance { get; } = new(); - - /// - public string Command { get; } = "merge"; - - /// - public string[] Aliases { get; } = Array.Empty(); - - /// - public string Description { get; } = "Merges your configs into the default config distribution."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (LoaderPlugin.Config.ConfigType == ConfigType.Default) - { - response = "Configs are already merged."; - return false; - } - - SortedDictionary configs = ConfigManager.LoadSorted(ConfigManager.Read()); - LoaderPlugin.Config.ConfigType = ConfigType.Default; - bool haveBeenSaved = ConfigManager.Save(configs); - - LoaderPlugin.Instance.SaveConfig(); - response = $"Configs have been merged successfully! Feel free to remove the directory in the following path:\n\"{Paths.IndividualConfigs}\""; - return haveBeenSaved; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Commands/Config/Split.cs b/EXILED/Exiled.Events/Commands/Config/Split.cs deleted file mode 100644 index 34816af579..0000000000 --- a/EXILED/Exiled.Events/Commands/Config/Split.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Commands.Config -{ - using System; - using System.Collections.Generic; - using System.Linq; - - using API.Enums; - using API.Features; - using API.Interfaces; - using CommandSystem; - using Loader; - - /// - /// The config split command. - /// - [CommandHandler(typeof(EConfig))] - public class Split : ICommand - { - /// - /// Gets static instance of the command. - /// - public static Split Instance { get; } = new(); - - /// - public string Command { get; } = "split"; - - /// - public string[] Aliases { get; } = Array.Empty(); - - /// - public string Description { get; } = "Splits your configs into the separated config distribution."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - if (LoaderPlugin.Config.ConfigType == ConfigType.Separated) - { - response = "Configs are already separated."; - return false; - } - - SortedDictionary configs = ConfigManager.LoadSorted(ConfigManager.Read()); - LoaderPlugin.Config.ConfigType = ConfigType.Separated; - bool haveBeenSaved = ConfigManager.Save(configs); - - LoaderPlugin.Instance.SaveConfig(); - response = $"Configs have been merged successfully! Feel free to remove the file in the following path:\n\"{Paths.Config}\""; - return haveBeenSaved; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Commands/PluginManager/Disable.cs b/EXILED/Exiled.Events/Commands/PluginManager/Disable.cs index 4169b84cd7..3575551077 100644 --- a/EXILED/Exiled.Events/Commands/PluginManager/Disable.cs +++ b/EXILED/Exiled.Events/Commands/PluginManager/Disable.cs @@ -17,14 +17,8 @@ namespace Exiled.Events.Commands.PluginManager /// /// The command to disable a plugin. /// - [CommandHandler(typeof(PluginManager))] public sealed class Disable : ICommand { - /// - /// Gets static instance of the command. - /// - public static Disable Instance { get; } = new(); - /// public string Command { get; } = "disable"; @@ -32,7 +26,7 @@ public sealed class Disable : ICommand public string[] Aliases { get; } = { "ds", "dis" }; /// - public string Description { get; } = "Disable a plugin."; + public string Description { get; set; } = "Disable a plugin."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) @@ -66,4 +60,4 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s return true; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Commands/PluginManager/Enable.cs b/EXILED/Exiled.Events/Commands/PluginManager/Enable.cs index 36b6aa1ecd..9512501452 100644 --- a/EXILED/Exiled.Events/Commands/PluginManager/Enable.cs +++ b/EXILED/Exiled.Events/Commands/PluginManager/Enable.cs @@ -21,14 +21,8 @@ namespace Exiled.Events.Commands.PluginManager /// /// The command to enable a plugin. /// - [CommandHandler(typeof(PluginManager))] public sealed class Enable : ICommand { - /// - /// Gets static instance of the command. - /// - public static Enable Instance { get; } = new(); - /// public string Command { get; } = "enable"; @@ -36,7 +30,7 @@ public sealed class Enable : ICommand public string[] Aliases { get; } = { "e", "en" }; /// - public string Description { get; } = "Enable a plugin"; + public string Description { get; set; } = "Enable a plugin"; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/PluginManager/Patches.cs b/EXILED/Exiled.Events/Commands/PluginManager/Patches.cs index 6d5651e30a..ffd9da0609 100644 --- a/EXILED/Exiled.Events/Commands/PluginManager/Patches.cs +++ b/EXILED/Exiled.Events/Commands/PluginManager/Patches.cs @@ -23,14 +23,8 @@ namespace Exiled.Events.Commands.PluginManager /// /// The command to show all the patches done by plugins. /// - [CommandHandler(typeof(PluginManager))] public sealed class Patches : ICommand { - /// - /// Gets static instance of the command. - /// - public static Patches Instance { get; } = new(); - /// public string Command { get; } = "patches"; @@ -38,7 +32,7 @@ public sealed class Patches : ICommand public string[] Aliases { get; } = { "patched" }; /// - public string Description { get; } = "Returns information about all patches (whether they are patched)"; + public string Description { get; set; } = "Returns information about all patches (whether they are patched or not)"; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) @@ -72,4 +66,4 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s return true; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Commands/PluginManager/PluginManager.cs b/EXILED/Exiled.Events/Commands/PluginManager/PluginManager.cs index 1f9a62c13d..b656116166 100644 --- a/EXILED/Exiled.Events/Commands/PluginManager/PluginManager.cs +++ b/EXILED/Exiled.Events/Commands/PluginManager/PluginManager.cs @@ -8,7 +8,9 @@ namespace Exiled.Events.Commands.PluginManager { using System; + using System.Collections.Generic; + using API.Features; using CommandSystem; /// @@ -22,21 +24,18 @@ public class PluginManager : ParentCommand public override string Command { get; } = "pluginmanager"; /// - public override string[] Aliases { get; } = new[] { "plymanager", "plmanager", "pmanager", "plym" }; + public override string[] Aliases { get; set; } = new[] { "plymanager", "plmanager", "pmanager", "plym" }; /// - public override string Description { get; } = "Manage plugin. Enable, disable and show plugins."; + public override string Description { get; set; } = "Manage plugin. Enable, disable and show plugins."; /// - public override void LoadGeneratedCommands() + protected override IEnumerable CommandsToRegister() { - } - - /// - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) - { - response = "Please, specify a valid subcommand! Available ones: enable, disable, show, patches"; - return false; + yield return typeof(Show); + yield return typeof(Enable); + yield return typeof(Disable); + yield return typeof(Patches); } } } diff --git a/EXILED/Exiled.Events/Commands/PluginManager/Show.cs b/EXILED/Exiled.Events/Commands/PluginManager/Show.cs index 0b9f4d5459..27906f12f6 100644 --- a/EXILED/Exiled.Events/Commands/PluginManager/Show.cs +++ b/EXILED/Exiled.Events/Commands/PluginManager/Show.cs @@ -24,14 +24,8 @@ namespace Exiled.Events.Commands.PluginManager /// /// The command to show all plugins. /// - [CommandHandler(typeof(PluginManager))] public sealed class Show : ICommand { - /// - /// Gets static instance of the command. - /// - public static Show Instance { get; } = new(); - /// public string Command { get; } = "show"; @@ -39,7 +33,7 @@ public sealed class Show : ICommand public string[] Aliases { get; } = { "shw", "sh" }; /// - public string Description { get; } = "Get all plugins, names, authors and versions"; + public string Description { get; set; } = "Get all plugins, names, authors and versions"; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/Reload/All.cs b/EXILED/Exiled.Events/Commands/Reload/All.cs deleted file mode 100644 index 55c791c0c8..0000000000 --- a/EXILED/Exiled.Events/Commands/Reload/All.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Commands.Reload -{ - using System; - - using CommandSystem; - - /// - /// The reload all command. - /// - [CommandHandler(typeof(Reload))] - public class All : ICommand - { - /// - /// Gets static instance of the command. - /// - public static All Instance { get; } = new(); - - /// - public string Command { get; } = "all"; - - /// - public string[] Aliases { get; } = new[] { "a" }; - - /// - public string Description { get; } = "Reload all configs and plugins."; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - bool success = true; - - if (!Configs.Instance.Execute(arguments, sender, out string responsetemp)) - success = false; - sender.Respond(responsetemp); - - if (!Translations.Instance.Execute(arguments, sender, out responsetemp)) - success = false; - sender.Respond(responsetemp); - - if (!GamePlay.Instance.Execute(arguments, sender, out responsetemp)) - success = false; - sender.Respond(responsetemp); - - if (!RemoteAdmin.Instance.Execute(arguments, sender, out responsetemp)) - success = false; - sender.Respond(responsetemp); - - if (!Plugins.Instance.Execute(arguments, sender, out responsetemp)) - success = false; - sender.Respond(responsetemp); - - if (!Permissions.Instance.Execute(arguments, sender, out responsetemp)) - success = false; - sender.Respond(responsetemp); - - response = success ? "Reloaded all configs and plugins successfully!" : "Failed to reload all configs and plugins. Read above."; - - return success; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Commands/Reload/Configs.cs b/EXILED/Exiled.Events/Commands/Reload/Configs.cs index 05f75a07ed..9b1b6570a3 100644 --- a/EXILED/Exiled.Events/Commands/Reload/Configs.cs +++ b/EXILED/Exiled.Events/Commands/Reload/Configs.cs @@ -20,14 +20,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload configs command. /// - [CommandHandler(typeof(Reload))] public class Configs : ICommand { - /// - /// Gets static instance of the command. - /// - public static Configs Instance { get; } = new(); - /// public string Command { get; } = "configs"; @@ -35,7 +29,7 @@ public class Configs : ICommand public string[] Aliases { get; } = new[] { "cfgs" }; /// - public string Description { get; } = "Reload plugin configs."; + public string Description { get; set; } = "Reload plugin configs."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) @@ -53,8 +47,6 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s foreach (IPlugin plugin in Loader.Plugins) { - plugin.OnUnregisteringCommands(); - plugin.OnRegisteringCommands(); if (plugin.Config.Debug) API.Features.Log.DebugEnabled.Add(plugin.Assembly); } diff --git a/EXILED/Exiled.Events/Commands/Reload/GamePlay.cs b/EXILED/Exiled.Events/Commands/Reload/GamePlay.cs index 780689bc7e..a34a254d39 100644 --- a/EXILED/Exiled.Events/Commands/Reload/GamePlay.cs +++ b/EXILED/Exiled.Events/Commands/Reload/GamePlay.cs @@ -18,14 +18,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload gameplay command. /// - [CommandHandler(typeof(Reload))] public class GamePlay : ICommand { - /// - /// Gets static instance of the command. - /// - public static GamePlay Instance { get; } = new(); - /// public string Command { get; } = "gameplay"; @@ -33,7 +27,7 @@ public class GamePlay : ICommand public string[] Aliases { get; } = new[] { "gm" }; /// - public string Description { get; } = "Reloads gameplay configs."; + public string Description { get; set; } = "Reloads gameplay configs."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/Reload/Permissions.cs b/EXILED/Exiled.Events/Commands/Reload/Permissions.cs index ae8bd4c162..382ccfc937 100644 --- a/EXILED/Exiled.Events/Commands/Reload/Permissions.cs +++ b/EXILED/Exiled.Events/Commands/Reload/Permissions.cs @@ -17,14 +17,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload permissions command. /// - [CommandHandler(typeof(Reload))] public class Permissions : ICommand { - /// - /// Gets static instance of the command. - /// - public static Permissions Instance { get; } = new(); - /// public string Command { get; } = "permissions"; @@ -32,7 +26,7 @@ public class Permissions : ICommand public string[] Aliases { get; } = new[] { "perms" }; /// - public string Description { get; } = "Reload permissions."; + public string Description { get; set; } = "Reload permissions."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/Reload/Plugins.cs b/EXILED/Exiled.Events/Commands/Reload/Plugins.cs index b10e46de79..fbe18da0e2 100644 --- a/EXILED/Exiled.Events/Commands/Reload/Plugins.cs +++ b/EXILED/Exiled.Events/Commands/Reload/Plugins.cs @@ -19,14 +19,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload plugins command. /// - [CommandHandler(typeof(Reload))] public class Plugins : ICommand { - /// - /// Gets static instance of the command. - /// - public static Plugins Instance { get; } = new(); - /// public string Command { get; } = "plugins"; @@ -34,7 +28,7 @@ public class Plugins : ICommand public string[] Aliases { get; } = new[] { "pl" }; /// - public string Description { get; } = "Reloads all plugins."; + public string Description { get; set; } = "Reloads all plugins."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/Reload/Reload.cs b/EXILED/Exiled.Events/Commands/Reload/Reload.cs index 7fa7fd22fa..dfde3c2b39 100644 --- a/EXILED/Exiled.Events/Commands/Reload/Reload.cs +++ b/EXILED/Exiled.Events/Commands/Reload/Reload.cs @@ -8,7 +8,9 @@ namespace Exiled.Events.Commands.Reload { using System; + using System.Collections.Generic; + using API.Features; using CommandSystem; /// @@ -22,20 +24,35 @@ public class Reload : ParentCommand public override string Command { get; } = "reload"; /// - public override string[] Aliases { get; } = new[] { "rld" }; + public override string[] Aliases { get; set; } = new[] { "rld" }; /// - public override string Description { get; } = "Reload plugins, configs, gameplay configs, remote admin configs, translations, permissions or all of them."; + public override string Description { get; set; } = "Reload plugins, configs, gameplay configs, remote admin configs, translations, permissions or all of them."; /// - public override void LoadGeneratedCommands() + protected override IEnumerable CommandsToRegister() { + yield return typeof(Configs); + yield return typeof(Translations); + yield return typeof(Plugins); + yield return typeof(GamePlay); + yield return typeof(RemoteAdmin); + yield return typeof(Permissions); } /// protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { - response = "Please, specify a valid subcommand! Available ones: all, plugins, gameplay, configs, remoteadmin, translations, permissions"; + if (arguments.At(0).ToLower() != "all") + return base.ExecuteParent(arguments, sender, out response); + + foreach (ICommand child in Commands.Values) + { + bool done = child.Execute(arguments, sender, out string localResponse); + sender.Respond(localResponse, done); + } + + response = "Executed all reloads."; return false; } } diff --git a/EXILED/Exiled.Events/Commands/Reload/RemoteAdmin.cs b/EXILED/Exiled.Events/Commands/Reload/RemoteAdmin.cs index 888288e948..5f52af1e34 100644 --- a/EXILED/Exiled.Events/Commands/Reload/RemoteAdmin.cs +++ b/EXILED/Exiled.Events/Commands/Reload/RemoteAdmin.cs @@ -18,14 +18,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload remoteadmin command. /// - [CommandHandler(typeof(Reload))] public class RemoteAdmin : ICommand { - /// - /// Gets static instance of the command. - /// - public static RemoteAdmin Instance { get; } = new(); - /// public string Command { get; } = "remoteadmin"; @@ -33,7 +27,7 @@ public class RemoteAdmin : ICommand public string[] Aliases { get; } = new[] { "ra" }; /// - public string Description { get; } = "Reloads remote admin configs."; + public string Description { get; set; } = "Reloads remote admin configs."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) diff --git a/EXILED/Exiled.Events/Commands/Reload/Translations.cs b/EXILED/Exiled.Events/Commands/Reload/Translations.cs index 99459b3f19..0e09941b16 100644 --- a/EXILED/Exiled.Events/Commands/Reload/Translations.cs +++ b/EXILED/Exiled.Events/Commands/Reload/Translations.cs @@ -9,8 +9,6 @@ namespace Exiled.Events.Commands.Reload { using System; - using API.Interfaces; - using CommandSystem; using Exiled.Permissions.Extensions; @@ -20,14 +18,8 @@ namespace Exiled.Events.Commands.Reload /// /// The reload translations command. /// - [CommandHandler(typeof(Reload))] public class Translations : ICommand { - /// - /// Gets static instance of the command. - /// - public static Translations Instance { get; } = new(); - /// public string Command { get; } = "translations"; @@ -35,7 +27,7 @@ public class Translations : ICommand public string[] Aliases { get; } = Array.Empty(); /// - public string Description { get; } = "Reload plugin translations."; + public string Description { get; set; } = "Reload plugin translations."; /// public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) @@ -50,14 +42,8 @@ public bool Execute(ArraySegment arguments, ICommandSender sender, out s Handlers.Server.OnReloadedTranslations(); - foreach (IPlugin plugin in Loader.Plugins) - { - plugin.OnUnregisteringCommands(); - plugin.OnRegisteringCommands(); - } - response = "Plugin translations have been reloaded successfully!"; return haveBeenReloaded; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Commands/TpsCommand.cs b/EXILED/Exiled.Events/Commands/TpsCommand.cs deleted file mode 100644 index 722de3b61e..0000000000 --- a/EXILED/Exiled.Events/Commands/TpsCommand.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Commands -{ - using System; - - using CommandSystem; - using Exiled.API.Features; - - /// - /// Command for showing current server TPS. - /// - [CommandHandler(typeof(RemoteAdminCommandHandler))] - [CommandHandler(typeof(GameConsoleCommandHandler))] - public class TpsCommand : ICommand - { - /// - public string Command { get; } = "tps"; - - /// - public string[] Aliases { get; } = Array.Empty(); - - /// - public string Description { get; } = "Shows the current TPS of the server"; - - /// - public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) - { - double diff = Server.Tps / Server.MaxTps; - string color = diff switch - { - > 0.9 => "green", - > 0.5 => "yellow", - _ => "red" - }; - - response = $"{Server.Tps}/{Server.MaxTps}"; - return true; - } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Config.cs b/EXILED/Exiled.Events/Config.cs index fd244f699b..9172de448e 100644 --- a/EXILED/Exiled.Events/Config.cs +++ b/EXILED/Exiled.Events/Config.cs @@ -110,5 +110,17 @@ public sealed class Config : IConfig /// [Description("Whether to log RA commands.")] public bool LogRaCommands { get; set; } = true; + + /// + /// Gets or sets a default value for reseting . + /// + [Description("Default PlayerInfoArea for player, resets on spawn.")] + public PlayerInfoArea DefaultPlayerInfoArea { get; set; } = PlayerInfoArea.Nickname | PlayerInfoArea.Badge | PlayerInfoArea.CustomInfo | PlayerInfoArea.Role | PlayerInfoArea.UnitName | PlayerInfoArea.PowerStatus; + + /// + /// Gets or sets a value indicating whether custom info will reload on spawning. + /// + [Description("Whether or not custom info will resets on spawn.")] + public bool ResetCustomInfo { get; set; } = true; } } diff --git a/EXILED/Exiled.Events/EventArgs/Interfaces/IProjectileEvent.cs b/EXILED/Exiled.Events/EventArgs/Interfaces/IProjectileEvent.cs new file mode 100644 index 0000000000..bc39863ac8 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Interfaces/IProjectileEvent.cs @@ -0,0 +1,22 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Interfaces +{ + using Exiled.API.Features.Pickups.Projectiles; + + /// + /// Event args used for all related events. + /// + public interface IProjectileEvent : IPickupEvent + { + /// + /// Gets the triggering the event. + /// + public Projectile Projectile { get; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Item/ChangingMicroHIDPickupStateEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Item/ChangingMicroHIDPickupStateEventArgs.cs index a70c37fde7..b6bfc9fbd0 100644 --- a/EXILED/Exiled.Events/EventArgs/Item/ChangingMicroHIDPickupStateEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Item/ChangingMicroHIDPickupStateEventArgs.cs @@ -55,4 +55,4 @@ public ChangingMicroHIDPickupStateEventArgs(ItemPickupBase microHID, MicroHidPha /// public Pickup Pickup => MicroHID; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Map/ChangedIntoGrenadeEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/ChangedIntoGrenadeEventArgs.cs index 197c741949..98fa9ad67e 100644 --- a/EXILED/Exiled.Events/EventArgs/Map/ChangedIntoGrenadeEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Map/ChangedIntoGrenadeEventArgs.cs @@ -25,7 +25,7 @@ public class ChangedIntoGrenadeEventArgs : IExiledEvent, IPickupEvent public ChangedIntoGrenadeEventArgs(TimedGrenadePickup pickup, ThrownProjectile projectile) { Pickup = API.Features.Pickups.Pickup.Get(pickup); - Projectile = API.Features.Pickups.Pickup.Get(projectile); + Projectile = (Projectile)API.Features.Pickups.Pickup.Get(projectile); } /// @@ -36,6 +36,7 @@ public ChangedIntoGrenadeEventArgs(TimedGrenadePickup pickup, ThrownProjectile p /// /// Gets a value indicating the projectile that spawned. /// + // TODO: Make that TimedGrenadeProjectile public Projectile Projectile { get; } /// diff --git a/EXILED/Exiled.Events/EventArgs/Map/ChangingIntoGrenadeEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Map/ChangingIntoGrenadeEventArgs.cs index 262a686f74..09caab7096 100644 --- a/EXILED/Exiled.Events/EventArgs/Map/ChangingIntoGrenadeEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Map/ChangingIntoGrenadeEventArgs.cs @@ -34,6 +34,7 @@ public ChangingIntoGrenadeEventArgs(TimedGrenadePickup pickup) /// /// Gets a value indicating the pickup being changed into a grenade. /// + // TODO: Make that GrenadePickup public Pickup Pickup { get; } /// diff --git a/EXILED/Exiled.Events/EventArgs/Player/BanningEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/BanningEventArgs.cs index 86d15bf8d8..9462eecf49 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/BanningEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/BanningEventArgs.cs @@ -53,4 +53,4 @@ public long Duration } } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangedNicknameEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangedNicknameEventArgs.cs new file mode 100644 index 0000000000..a0f7d0f55d --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangedNicknameEventArgs.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Player +{ + using Exiled.API.Features; + using Exiled.Events.EventArgs.Interfaces; + + /// + /// Contains all information after changed a player's in-game nickname. + /// + public class ChangedNicknameEventArgs : IPlayerEvent + { + /// + /// Initializes a new instance of the class. + /// + /// The who's name is changed. + /// The old name. + public ChangedNicknameEventArgs(Player player, string oldName) + { + Player = player; + OldName = oldName; + NewName = player.CustomName; + } + + /// + /// Gets the 's old name. + /// + public string OldName { get; } + + /// + /// Gets or sets the 's new name. + /// + public string NewName { get; set; } + + /// + /// Gets the who's name is changed. + /// + public Player Player { get; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs index 6978172b54..41f90d3a02 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingItemEventArgs.cs @@ -63,4 +63,4 @@ public Item Item /// public Player Player { get; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingMicroHIDStateEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingMicroHIDStateEventArgs.cs index 3e3c1eee9c..dc6ac7dff5 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ChangingMicroHIDStateEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingMicroHIDStateEventArgs.cs @@ -12,7 +12,6 @@ namespace Exiled.Events.EventArgs.Player using API.Features; using API.Features.Items; using Interfaces; - using InventorySystem.Items; using InventorySystem.Items.MicroHID; using InventorySystem.Items.MicroHID.Modules; @@ -62,4 +61,4 @@ public ChangingMicroHIDStateEventArgs(ItemBase microHID, MicroHidPhase newPhase, /// public Player Player => MicroHID.Owner; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingMoveStateEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingMoveStateEventArgs.cs index 36d5240158..f42f478007 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ChangingMoveStateEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingMoveStateEventArgs.cs @@ -52,4 +52,4 @@ public ChangingMoveStateEventArgs(Player player, PlayerMovementState oldState, P /// public PlayerMovementState NewState { get; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ChangingRoleEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ChangingRoleEventArgs.cs index 898ab9fcd6..d9e5d32220 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ChangingRoleEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ChangingRoleEventArgs.cs @@ -8,6 +8,7 @@ namespace Exiled.Events.EventArgs.Player { using System.Collections.Generic; + using System.Diagnostics; using API.Enums; using API.Features; @@ -23,6 +24,7 @@ namespace Exiled.Events.EventArgs.Player public class ChangingRoleEventArgs : IPlayerEvent, IDeniableEvent { private RoleTypeId newRole; + private SpawnReason reason; /// /// Initializes a new instance of the class. @@ -43,7 +45,7 @@ public ChangingRoleEventArgs(Player player, RoleTypeId newRole, RoleChangeReason { Player = player; NewRole = newRole; - Reason = (SpawnReason)reason; + this.reason = (SpawnReason)reason; SpawnFlags = spawnFlags; } @@ -69,6 +71,12 @@ public RoleTypeId NewRole get => newRole; set { + if (reason == SpawnReason.Destroyed) + { + Log.Error($"Tried to change NewRole for Destroyed!\n{new StackTrace()}"); + return; + } + InventoryRoleInfo inventory = value.GetInventory(); Items.Clear(); @@ -84,6 +92,11 @@ public RoleTypeId NewRole } } + /// + /// Gets a value indicating whether the current event is safe to do some actions with player. + /// + public bool IsSafe => NewRole != RoleTypeId.Destroyed && Reason != SpawnReason.Destroyed; + /// /// Gets base items that the player will receive. /// @@ -106,7 +119,24 @@ public bool ShouldPreserveInventory /// /// Gets or sets the reason for their class change. /// - public SpawnReason Reason { get; set; } + public SpawnReason Reason + { + get + { + return reason; + } + + set + { + if (reason == SpawnReason.Destroyed) + { + Log.Error("Tried to change Destroyed reason!"); + return; + } + + reason = value; + } + } /// /// Gets or sets the spawn flags for their class change. diff --git a/EXILED/Exiled.Events/EventArgs/Player/HurtingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/HurtingEventArgs.cs index f328bddd20..5c77206bac 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/HurtingEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/HurtingEventArgs.cs @@ -33,14 +33,17 @@ public HurtingEventArgs(Player target, DamageHandlerBase damageHandler) { DamageHandler = new CustomDamageHandler(target, damageHandler); + Attacker = DamageHandler.BaseIs(out CustomAttackerHandler attackerDamageHandler) ? attackerDamageHandler.Attacker : null; Player = target; - if (DamageHandler.BaseIs(out CustomAttackerHandler attackerDamageHandler)) + if (DamageHandler.BaseIs(out attackerDamageHandler)) Attacker = attackerDamageHandler.Attacker; else if (damageHandler is GenericDamageHandler genericDamageHandler) Attacker = Player.Get(genericDamageHandler.Attacker); else Attacker = null; + + Log.Assert(target != null, "HurtingEventArgs - target is null!"); } /// diff --git a/EXILED/Exiled.Events/EventArgs/Player/ItemAddedEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ItemAddedEventArgs.cs index 694188c1c4..b84d49e35b 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ItemAddedEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ItemAddedEventArgs.cs @@ -7,6 +7,9 @@ namespace Exiled.Events.EventArgs.Player { + using System.Diagnostics; + + using API.Features; using Exiled.API.Features.Items; using Exiled.API.Features.Pickups; using Exiled.Events.EventArgs.Interfaces; @@ -27,15 +30,17 @@ public class ItemAddedEventArgs : IItemEvent, IPickupEvent /// The the originated from, or if the item was not picked up. public ItemAddedEventArgs(ReferenceHub referenceHub, ItemBase itemBase, ItemPickupBase pickupBase) { - Player = API.Features.Player.Get(referenceHub); + Player = Player.Get(referenceHub); Item = Item.Get(itemBase); Pickup = Pickup.Get(pickupBase); + Log.Assert(Item != null, $"ItemAddedEventArgs ctor: Item is null! Base: '{itemBase}'"); + Log.Assert(Player != null, $"ItemAddedEventArgs ctor: Player is null! Base: '{referenceHub} {new StackTrace()}'"); } /// /// Gets the player that had the item added. /// - public API.Features.Player Player { get; } + public Player Player { get; } /// /// Gets the item that was added. diff --git a/EXILED/Exiled.Events/EventArgs/Player/KickingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/KickingEventArgs.cs index cfa2864b85..1bd808864c 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/KickingEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/KickingEventArgs.cs @@ -14,35 +14,34 @@ namespace Exiled.Events.EventArgs.Player using Interfaces; /// - /// Contains all information before kicking a player from the server. + /// Contains all information before kicking a player from the server. /// public class KickingEventArgs : IPlayerEvent, IDeniableEvent { - private readonly string startkickmessage; private bool isAllowed; - private Player issuer; private Player target; + private ICommandSender sender; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// + /// /// /// - /// + /// /// /// /// /// /// - /// + /// /// /// - /// + /// /// /// - /// + /// /// public KickingEventArgs(Player target, Player issuer, ICommandSender commandSender, string reason, string fullMessage, bool isAllowed = true) { @@ -50,12 +49,12 @@ public KickingEventArgs(Player target, Player issuer, ICommandSender commandSend Player = issuer ?? Server.Host; CommandSender = commandSender; Reason = reason; - startkickmessage = fullMessage; + FullMessage = fullMessage; IsAllowed = isAllowed; } /// - /// Gets or sets the ban target. + /// Gets or sets the ban target. /// public Player Target { @@ -73,14 +72,14 @@ public Player Target } /// - /// Gets or sets the kick reason. + /// Gets or sets the kick reason. /// public string Reason { get; set; } /// - /// Gets the full kick message. + /// Gets or sets the full kick message. /// - public string FullMessage => startkickmessage + Reason; + public string FullMessage { get; set; } /// /// Gets or sets a value indicating whether action is taken against the target. @@ -101,20 +100,25 @@ public bool IsAllowed } /// - /// Gets or sets the ban issuer. + /// Gets the ban issuer. /// - public Player Player + public Player Player { get; } + + /// + /// Gets or sets the ban issuer. + /// + public ICommandSender Sender { - get => issuer; + get => sender; set { - if (value is null || issuer == value) + if (value is null || sender == value) return; - if (Events.Instance.Config.ShouldLogBans && issuer is not null) - LogBanChange(Assembly.GetCallingAssembly().GetName().Name, $" changed the ban issuer from user {issuer.Nickname} ({issuer.UserId}) to {value.Nickname} ({value.UserId})"); + if (Events.Instance.Config.ShouldLogBans && sender is not null) + LogBanChange(Assembly.GetCallingAssembly().GetName().Name, $" changed the ban sender from user {sender.LogName} to {value.LogName}"); - issuer = value; + sender = value; } } @@ -141,4 +145,4 @@ protected void LogBanChange(string assemblyName, string message) ServerLogs._state = ServerLogs.LoggingState.Write; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/RotatingRevolverEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/RotatingRevolverEventArgs.cs index 515d8c7b04..fede46653b 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/RotatingRevolverEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/RotatingRevolverEventArgs.cs @@ -61,4 +61,4 @@ public RotatingRevolverEventArgs(FirearmBase firearm, int rotations) /// public Player Player { get; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/SearchingPickupEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/SearchingPickupEventArgs.cs index fe17eb979a..15f34b06c7 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/SearchingPickupEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/SearchingPickupEventArgs.cs @@ -9,7 +9,7 @@ namespace Exiled.Events.EventArgs.Player { using Exiled.API.Features; using Exiled.API.Features.Pickups; - using Exiled.Events.EventArgs.Interfaces; + using Interfaces; using InventorySystem.Items.Pickups; using InventorySystem.Searching; diff --git a/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs new file mode 100644 index 0000000000..f4afb06dcf --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs @@ -0,0 +1,73 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Player +{ + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Features.Roles; + using Exiled.Events.EventArgs.Interfaces; + + using PlayerRoles; + + /// + /// Contains all information before a 's role is sent to a client. + /// + public class SendingRoleEventArgs : IPlayerEvent + { + private RoleTypeId roleTypeId; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public SendingRoleEventArgs(Player player, uint target, RoleTypeId roleType) + { + Player = player; + Target = Player.Get(target); + roleTypeId = roleType; + } + + /// + /// Gets the on whose behalf the role change request is sent. + /// + public Player Player { get; } + + /// + /// gets the to whom the request is sent. + /// + public Player Target { get; } + + /// + /// Gets or sets the that is sent to the . + /// + /// Checks value by player . + public RoleTypeId RoleType + { + get + { + return roleTypeId; + } + + set + { + if (Player.Role.CheckAppearanceCompatibility(value)) + { + roleTypeId = value; + } + } + } + } +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ShootingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ShootingEventArgs.cs index 3c2f9d12b6..0bb0dc7800 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ShootingEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ShootingEventArgs.cs @@ -76,4 +76,4 @@ public Vector3 Direction /// public bool IsAllowed { get; set; } = true; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs index 18d2303353..cf3963a4e9 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs @@ -7,6 +7,8 @@ namespace Exiled.Events.EventArgs.Player { + using API.Enums; + using API.Extensions; using API.Features; using Exiled.API.Features.Items; using Interfaces; @@ -14,7 +16,7 @@ namespace Exiled.Events.EventArgs.Player using UnityEngine; /// - /// Contains all information after a player has fired a weapon. + /// Contains all information after a player hits a hitbox with a weapon. /// public class ShotEventArgs : IPlayerEvent, IFirearmEvent { @@ -40,6 +42,8 @@ public ShotEventArgs(HitscanHitregModuleBase hitregModule, RaycastHit hitInfo, I { Hitbox = hitboxIdentity; Target = Player.Get(Hitbox.TargetHub); + if (Target != null) + BoneType = Target.GetByMassCenter(Hitbox); } } @@ -86,6 +90,11 @@ public ShotEventArgs(HitscanHitregModuleBase hitregModule, RaycastHit hitInfo, I /// public Player Target { get; } + /// + /// Gets the damaged part of human body. Can be if is null. + /// + public BoneType? BoneType { get; } + /// /// Gets the component of the target player that was hit. Can be null. /// diff --git a/EXILED/Exiled.Events/EventArgs/Player/ThrowingProjectileEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ThrowingProjectileEventArgs.cs new file mode 100644 index 0000000000..3f829690f5 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Player/ThrowingProjectileEventArgs.cs @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Player +{ + using Exiled.API.Features; + using Exiled.API.Features.Items; + using Exiled.API.Features.Pickups; + using Exiled.API.Features.Pickups.Projectiles; + using Exiled.Events.EventArgs.Interfaces; + + using InventorySystem.Items.ThrowableProjectiles; + + /// + /// Contains all information before player throws a grenade. + /// + public class ThrowingProjectileEventArgs : IPlayerEvent, IItemEvent, IProjectileEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public ThrowingProjectileEventArgs(ThrownProjectile projectile, Player player, ThrowableItem item) + { + Player = player; + Throwable = Item.Get(item); + Projectile = Pickup.Get(projectile); + } + + /// + /// Gets the player who's thrown the grenade. + /// + public Player Player { get; } + + /// + /// Gets the item being thrown. + /// + public Throwable Throwable { get; } + + /// + public Item Item => Throwable; + + /// + public Projectile Projectile { get; } + + /// + public Pickup Pickup => Projectile; + } +} diff --git a/EXILED/Exiled.Events/EventArgs/Player/ThrownProjectileEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/ThrownProjectileEventArgs.cs index 14ab42de1e..d41f2118df 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/ThrownProjectileEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/ThrownProjectileEventArgs.cs @@ -18,7 +18,7 @@ namespace Exiled.Events.EventArgs.Player /// /// Contains all information after a player throws a grenade. /// - public class ThrownProjectileEventArgs : IItemEvent, IPickupEvent + public class ThrownProjectileEventArgs : IPlayerEvent, IItemEvent, IProjectileEvent { /// /// Initializes a new instance of the class. @@ -46,9 +46,7 @@ public ThrownProjectileEventArgs(ThrownProjectile projectile, Player player, Thr /// public Item Item => Throwable; - /// - /// Gets the thrown grenade. - /// + /// public Projectile Projectile { get; } /// diff --git a/EXILED/Exiled.Events/EventArgs/Player/TogglingFlashlightEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/TogglingFlashlightEventArgs.cs index 183476b48a..118517634a 100644 --- a/EXILED/Exiled.Events/EventArgs/Player/TogglingFlashlightEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Player/TogglingFlashlightEventArgs.cs @@ -67,4 +67,4 @@ public bool IsAllowed /// public Player Player { get; } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Scp1509/ResurrectingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp1509/ResurrectingEventArgs.cs deleted file mode 100644 index 4484f3b730..0000000000 --- a/EXILED/Exiled.Events/EventArgs/Scp1509/ResurrectingEventArgs.cs +++ /dev/null @@ -1,68 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.EventArgs.Scp1509 -{ - using Exiled.API.Features; - using Exiled.API.Features.Items; - using Exiled.Events.EventArgs.Interfaces; - using InventorySystem.Items.Scp1509; - using PlayerRoles; - - /// - /// Contains all information before player is resurrected. - /// - public class ResurrectingEventArgs : IItemEvent, IDeniableEvent - { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - /// - public ResurrectingEventArgs(Player target, Player victim, RoleTypeId newRole, Scp1509Item scp1509, bool isAllowed = true) - { - Target = target; - Victim = victim; - NewRole = newRole; - Scp1509 = Item.Get(scp1509); - Player = Scp1509.Owner; - IsAllowed = isAllowed; - } - - /// - /// Gets or sets the role which will be set to the after resurrection. - /// - public RoleTypeId NewRole { get; set; } - - /// - /// Gets the target of resurrection. - /// - public Player Target { get; } - - /// - public Player Player { get; } - - /// - /// Gets the victim of this kill. - /// - public Player Victim { get; } - - /// - /// Gets the SCP-1509 instance. - /// - public Scp1509 Scp1509 { get; } - - /// - public Item Item => Scp1509; - - /// - public bool IsAllowed { get; set; } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Scp1509/TriggeringAttackEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp1509/TriggeringAttackEventArgs.cs deleted file mode 100644 index cbc9b54106..0000000000 --- a/EXILED/Exiled.Events/EventArgs/Scp1509/TriggeringAttackEventArgs.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.EventArgs.Scp1509 -{ - using Exiled.API.Features; - using Exiled.API.Features.Items; - using Exiled.Events.EventArgs.Interfaces; - using InventorySystem.Items.Scp1509; - - /// - /// Contains all information before SCP-1509 melee attack is triggered. - /// - public class TriggeringAttackEventArgs : IItemEvent, IDeniableEvent - { - /// - /// Initializes a new instance of the class. - /// - /// - /// - public TriggeringAttackEventArgs(Scp1509Item scp1509Item, bool isAllowed = true) - { - Scp1509 = Item.Get(scp1509Item); - Player = Scp1509.Owner; - IsAllowed = isAllowed; - } - - /// - public Player Player { get; } - - /// - public Item Item => Scp1509; - - /// - /// Gets the SCP-1509 instance. - /// - public Scp1509 Scp1509 { get; } - - /// - public bool IsAllowed { get; set; } - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Scp3114/SlappingEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp3114/SlappingEventArgs.cs new file mode 100644 index 0000000000..5e0826fe0e --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Scp3114/SlappingEventArgs.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Scp3114 +{ + using API.Features; + using Exiled.API.Features.Roles; + using Interfaces; + + /// + /// Contains all information before SCP-3114 slaps. + /// + public class SlappingEventArgs : IScp3114Event, IDeniableEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public SlappingEventArgs(Player player) + { + Player = player; + Scp3114 = Player.Role.As(); + } + + /// + public bool IsAllowed { get; set; } = true; + + /// + public Player Player { get; } + + /// + public Scp3114Role Scp3114 { get; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Scp939/CreatedAmnesticCloudEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp939/CreatedAmnesticCloudEventArgs.cs new file mode 100644 index 0000000000..5332bae483 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Scp939/CreatedAmnesticCloudEventArgs.cs @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Scp939 +{ + using API.Features; + + using Exiled.API.Features.Hazards; + using Exiled.API.Features.Roles; + using Interfaces; + + using PlayerRoles.PlayableScps.Scp939; + + using Scp939Role = API.Features.Roles.Scp939Role; + + /// + /// Contains all information after SCP-939 fully created target . + /// + public class CreatedAmnesticCloudEventArgs : IScp939Event, IHazardEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + public CreatedAmnesticCloudEventArgs(ReferenceHub hub, Scp939AmnesticCloudInstance cloud) + { + Player = Player.Get(hub); + AmnesticCloud = (AmnesticCloudHazard)Hazard.Get(cloud); + Scp939 = Player.Role.As(); + } + + /// + public Player Player { get; } + + /// + /// Gets the instance. + /// + public AmnesticCloudHazard AmnesticCloud { get; } + + /// + public Scp939Role Scp939 { get; } + + /// + public Hazard Hazard => AmnesticCloud; + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs index d2545aad3d..992561fbd7 100644 --- a/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Scp939/PlacedAmnesticCloudEventArgs.cs @@ -51,4 +51,4 @@ public PlacedAmnesticCloudEventArgs(ReferenceHub hub, Scp939AmnesticCloudInstanc /// public Hazard Hazard => AmnesticCloud; } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/EventArgs/Server/RespawningTeamEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Server/RespawningTeamEventArgs.cs index df999d90dd..e65bdbf03c 100644 --- a/EXILED/Exiled.Events/EventArgs/Server/RespawningTeamEventArgs.cs +++ b/EXILED/Exiled.Events/EventArgs/Server/RespawningTeamEventArgs.cs @@ -7,16 +7,13 @@ namespace Exiled.Events.EventArgs.Server { - using System; using System.Collections.Generic; using Exiled.API.Features; using Exiled.API.Features.Waves; using Exiled.Events.EventArgs.Interfaces; using PlayerRoles; - using PlayerRoles.Spectating; using Respawning; - using Respawning.Objectives; using Respawning.Waves; /// @@ -41,22 +38,10 @@ public class RespawningTeamEventArgs : IDeniableEvent public RespawningTeamEventArgs(List players, int maxRespawn, SpawnableWaveBase wave) { Players = players; - if (Players.Remove(null)) - { - string debug = string.Empty; - foreach (ReferenceHub hub in ReferenceHub.AllHubs) - { - if (WaveSpawner.CanBeSpawned(hub) && hub.roleManager.CurrentRole is SpectatorRole spectatorRole) - debug += $"({Player.Get(hub)}) {hub.GetNickname()} (ActiveTime: {spectatorRole.ActiveTime}) [{hub.authManager.InstanceMode}]\n"; - } - - Log.Error("(RespawningTeamEventArgs) preventing a null player to spawn:\n" + debug); - } - MaximumRespawnAmount = maxRespawn; - SpawnQueue = new(); + SpawnQueue = WaveSpawner.SpawnQueue; Wave = new TimedWave((TimeBasedWave)wave); - Wave.PopulateQueue(SpawnQueue, MaximumRespawnAmount); + IsAllowed = true; } /// @@ -95,7 +80,7 @@ public int MaximumRespawnAmount /// /// Gets or sets a value indicating whether the spawn can occur. /// - public bool IsAllowed { get; set; } = true; + public bool IsAllowed { get; set; } /// /// Gets or sets the RoleTypeId spawn queue. diff --git a/EXILED/Exiled.Events/Events.cs b/EXILED/Exiled.Events/Events.cs index 1645fe20a1..59de2fd0a5 100644 --- a/EXILED/Exiled.Events/Events.cs +++ b/EXILED/Exiled.Events/Events.cs @@ -68,7 +68,7 @@ public override void OnEnabled() Handlers.Server.RestartingRound += Handlers.Internal.Round.OnRestartingRound; Handlers.Server.RoundStarted += Handlers.Internal.Round.OnRoundStarted; Handlers.Player.ChangingRole += Handlers.Internal.Round.OnChangingRole; - Handlers.Player.SpawningRagdoll += Handlers.Internal.Round.OnSpawningRagdoll; + Handlers.Player.Spawned += Handlers.Internal.Round.OnSpawned; Handlers.Scp049.ActivatingSense += Handlers.Internal.Round.OnActivatingSense; Handlers.Player.Verified += Handlers.Internal.Round.OnVerified; Handlers.Map.ChangedIntoGrenade += Handlers.Internal.ExplodingGrenade.OnChangedIntoGrenade; @@ -87,8 +87,6 @@ public override void OnEnabled() AdminToys.AdminToyBase.OnAdded += Handlers.Internal.AdminToyList.OnAddedAdminToys; AdminToys.AdminToyBase.OnRemoved += Handlers.Internal.AdminToyList.OnRemovedAdminToys; - ServerSpecificSettingsSync.ServerOnSettingValueReceived += SettingBase.OnSettingUpdated; - LabApi.Events.Handlers.PlayerEvents.ReloadingWeapon += Handlers.Player.OnReloadingWeapon; LabApi.Events.Handlers.PlayerEvents.UnloadingWeapon += Handlers.Player.OnUnloadingWeapon; @@ -110,7 +108,7 @@ public override void OnDisabled() Handlers.Server.RestartingRound -= Handlers.Internal.Round.OnRestartingRound; Handlers.Server.RoundStarted -= Handlers.Internal.Round.OnRoundStarted; Handlers.Player.ChangingRole -= Handlers.Internal.Round.OnChangingRole; - Handlers.Player.SpawningRagdoll -= Handlers.Internal.Round.OnSpawningRagdoll; + Handlers.Player.Spawned -= Handlers.Internal.Round.OnSpawned; Handlers.Scp049.ActivatingSense -= Handlers.Internal.Round.OnActivatingSense; Handlers.Player.Verified -= Handlers.Internal.Round.OnVerified; Handlers.Map.ChangedIntoGrenade -= Handlers.Internal.ExplodingGrenade.OnChangedIntoGrenade; diff --git a/EXILED/Exiled.Events/Exiled.Events.csproj b/EXILED/Exiled.Events/Exiled.Events.csproj index c8d9aa1cd1..a66c687e2e 100644 --- a/EXILED/Exiled.Events/Exiled.Events.csproj +++ b/EXILED/Exiled.Events/Exiled.Events.csproj @@ -37,12 +37,14 @@ - - - if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + + if not "$(EXILED_DEV_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_DEV_REFERENCES)\Plugins\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).dll" "$(EXILED_REFERENCES)\" + if not "$(EXILED_REFERENCES)"=="" copy /y "$(OutputPath)$(AssemblyName).xml" "$(EXILED_REFERENCES)\" + if [[ ! -z "$EXILED_DEV_REFERENCES" ]]; then cp "$(OutputPath)$(AssemblyName).dll" "$EXILED_DEV_REFERENCES/Plugins/"; fi diff --git a/EXILED/Exiled.Events/Features/Event{T}.cs b/EXILED/Exiled.Events/Features/Event{T}.cs index 2d6252b91e..800c6201e4 100644 --- a/EXILED/Exiled.Events/Features/Event{T}.cs +++ b/EXILED/Exiled.Events/Features/Event{T}.cs @@ -10,9 +10,12 @@ namespace Exiled.Events.Features using System; using System.Collections.Generic; using System.Linq; + using System.Reflection; using Exiled.API.Features; using Exiled.Events.EventArgs.Interfaces; + using Exiled.Events.EventArgs.Player; + using MEC; /// @@ -274,7 +277,7 @@ internal void InvokeNormal(T arg) } catch (Exception ex) { - Log.Error($"Method \"{registration.handler.Method.Name}\" of the class \"{registration.handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{GetType().FullName}\"\n{ex}"); + Log.Error($"Method \"{registration.handler.Method.Name}\" of the class \"{registration.handler.Method.ReflectedType.FullName}\" caused an exception when handling the event \"{typeof(T).FullName}\"\n{ex}"); } } } diff --git a/EXILED/Exiled.Events/Handlers/Internal/ExplodingGrenade.cs b/EXILED/Exiled.Events/Handlers/Internal/ExplodingGrenade.cs index c7f0a36698..9e6dae7473 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/ExplodingGrenade.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/ExplodingGrenade.cs @@ -7,7 +7,7 @@ namespace Exiled.Events.Handlers.Internal { - using Exiled.API.Features.Pickups; + using Exiled.API.Features.Pickups.Projectiles; using Exiled.Events.EventArgs.Map; /// @@ -18,7 +18,7 @@ internal static class ExplodingGrenade /// public static void OnChangedIntoGrenade(ChangedIntoGrenadeEventArgs ev) { - ev.Pickup.WriteProjectileInfo(ev.Projectile); + ((TimeGrenadeProjectile)ev.Projectile).ReadGrenadePickupInfo(ev.Pickup); } } } diff --git a/EXILED/Exiled.Events/Handlers/Internal/MapGenerated.cs b/EXILED/Exiled.Events/Handlers/Internal/MapGenerated.cs index 60ec679791..1938dcc7fd 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/MapGenerated.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/MapGenerated.cs @@ -46,4 +46,4 @@ public static void OnMapGenerated() Timing.CallDelayed(1, Handlers.Map.OnGenerated); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs index 3ce8660bbe..7974291499 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs @@ -29,10 +29,8 @@ namespace Exiled.Events.Handlers.Internal using InventorySystem.Items.Usables; using InventorySystem.Items.Usables.Scp244.Hypothermia; using PlayerRoles; - using PlayerRoles.FirstPersonControl; using PlayerRoles.RoleAssign; - using UnityEngine; - using Utils.Networking; + using UserSettings.ServerSpecific; using Utils.NonAllocLINQ; /// @@ -55,6 +53,7 @@ public static void OnWaitingForPlayers() if (Events.Instance.Config.ShouldReloadTranslationsAtRoundRestart) TranslationManager.Reload(); + ServerSpecificSettingsSync.ServerOnSettingValueReceived += SettingBase.OnSettingUpdated; RoundSummary.RoundLock = false; } @@ -81,15 +80,31 @@ public static void OnRestartingRound() /// public static void OnChangingRole(ChangingRoleEventArgs ev) { + if (Events.Instance.Config.ResetCustomInfo) + ev.Player.CustomInfo = null; + + ev.Player.InfoArea = Events.Instance.Config.DefaultPlayerInfoArea; + if (!ev.Player.IsHost && ev.NewRole == RoleTypeId.Spectator && ev.Reason != API.Enums.SpawnReason.Destroyed && Events.Instance.Config.ShouldDropInventory) ev.Player.Inventory.ServerDropEverything(); } - /// - public static void OnSpawningRagdoll(SpawningRagdollEventArgs ev) + /// + public static void OnSpawned(SpawnedEventArgs ev) { - if (ev.Role.IsDead() || !ev.Role.IsFpcRole()) - ev.IsAllowed = false; + if (ev.Reason is SpawnReason.Destroyed or SpawnReason.None) + return; + + foreach (Player player in Player.List) + { + if (player == ev.Player) + continue; + + if (player.Role.TeamAppearances.ContainsKey(ev.Player.Role.Team) || player.Role.TeamAppearances.ContainsKey(ev.OldRole.Team)) + { + player.Role.UpdateAppearanceFor(ev.Player); + } + } } /// @@ -107,8 +122,7 @@ public static void OnVerified(VerifiedEventArgs ev) { RoleAssigner.CheckLateJoin(ev.Player.ReferenceHub, ClientInstanceMode.ReadyClient); - if (SettingBase.SyncOnJoin != null && SettingBase.SyncOnJoin(ev.Player)) - SettingBase.SendToPlayer(ev.Player); + SettingBase.SendToPlayer(ev.Player); // TODO: Remove if this has been fixed for https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/52 foreach (Room room in Room.List.Where(current => current.AreLightsOff)) @@ -117,10 +131,10 @@ public static void OnVerified(VerifiedEventArgs ev) ev.Player.SendFakeSyncVar(room.RoomLightControllerNetIdentity, typeof(RoomLightController), nameof(RoomLightController.NetworkLightsEnabled), false); } - // Fix bug that player that Join do not receive information about other players Scale - foreach (Player player in ReferenceHub.AllHubs.Select(Player.Get)) + // TODO: Remove if this has been fixed for https://git.scpslgame.com/northwood-qa/scpsl-bug-reporting/-/issues/947 + if (ev.Player.TryGetEffect(out Hypothermia hypothermia)) { - player.SetFakeScale(player.Scale, new List() { ev.Player }); + hypothermia.SubEffects = hypothermia.SubEffects.Where(x => x.GetType() != typeof(PostProcessSubEffect)).ToArray(); } } diff --git a/EXILED/Exiled.Events/Handlers/Internal/SceneUnloaded.cs b/EXILED/Exiled.Events/Handlers/Internal/SceneUnloaded.cs index 55b9d0738b..564015422a 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/SceneUnloaded.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/SceneUnloaded.cs @@ -35,6 +35,8 @@ public static void OnSceneUnloaded(Scene _) { Player.UserIdsCache.Clear(); Player.Dictionary.Clear(); + Npc.Dictionary.Clear(); + AdminToy.BaseToAdminToy.Clear(); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Handlers/Player.cs b/EXILED/Exiled.Events/Handlers/Player.cs index 97e6137003..216cf40acc 100644 --- a/EXILED/Exiled.Events/Handlers/Player.cs +++ b/EXILED/Exiled.Events/Handlers/Player.cs @@ -200,6 +200,11 @@ public class Player /// If is set to when Escape is , tickets will still be given to the escapee's team even though they will 'fail' to escape. Use to block escapes instead. public static Event ChangingRole { get; set; } = new(); + /// + /// Invoked before throwing an . + /// + public static Event ThrowingProjectile { get; set; } = new(); + /// /// Invoked afer throwing an . /// @@ -583,18 +588,28 @@ public class Player /// /// Invoked before a player's emotion changed. /// - public static Event ChangingEmotion { get; set; } = new(); + public static Event ChangedNickname { get; set; } = new(); /// - /// Invoked after a player's emotion changed. + /// Invoked before a 's role is sent to a client. /// - public static Event ChangedEmotion { get; set; } = new(); + public static Event SendingRole { get; set; } = new(); /// /// Invoked before a 's rotates the revolver. /// public static Event RotatingRevolver { get; set; } = new(); + /// + /// Invoked before a player's emotion changed. + /// + public static Event ChangingEmotion { get; set; } = new(); + + /// + /// Invoked after a player's emotion changed. + /// + public static Event ChangedEmotion { get; set; } = new(); + /// /// Invoked before disruptor's mode is changed. /// @@ -717,6 +732,12 @@ public class Player /// The instance. public static void OnInteracted(InteractedEventArgs ev) => Interacted.InvokeSafely(ev); + /// + /// Called before sending role to a . + /// + /// The instance. + public static void OnSendingRole(SendingRoleEventArgs ev) => SendingRole.InvokeSafely(ev); + /// /// Called before spawning a ragdoll. /// @@ -769,6 +790,12 @@ public class Player /// /// Called before throwing a grenade. /// + /// The instance. + public static void OnThrowingProjectile(ThrowingProjectileEventArgs ev) => ThrowingProjectile.InvokeSafely(ev); + + /// + /// Called after throwing a grenade. + /// /// The instance. public static void OnThrownProjectile(ThrownProjectileEventArgs ev) => ThrownProjectile.InvokeSafely(ev); diff --git a/EXILED/Exiled.Events/Handlers/Scp1509.cs b/EXILED/Exiled.Events/Handlers/Scp1509.cs deleted file mode 100644 index 9a8d09d8da..0000000000 --- a/EXILED/Exiled.Events/Handlers/Scp1509.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) ExMod Team. All rights reserved. -// Licensed under the CC BY-SA 3.0 license. -// -// ----------------------------------------------------------------------- - -namespace Exiled.Events.Handlers -{ - using Exiled.Events.EventArgs.Scp1509; - using Exiled.Events.Features; -#pragma warning disable SA1623 - - /// - /// SCP-1509 related events. - /// - public static class Scp1509 - { - /// - /// Invoked before player is resurrected. - /// - public static Event Resurrecting { get; set; } = new(); - - /// - /// Invoked before SCP-1509 melee attack is triggered. - /// - public static Event TriggeringAttack { get; set; } = new(); - - /// - /// Called before SCP-1509 melee attack is triggered. - /// - /// The instance. - public static void OnTriggeringAttack(TriggeringAttackEventArgs ev) => TriggeringAttack.InvokeSafely(ev); - - /// - /// Called before player is resurrected. - /// - /// The instance. - public static void OnResurrecting(ResurrectingEventArgs ev) => Resurrecting.InvokeSafely(ev); - } -} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Handlers/Scp3114.cs b/EXILED/Exiled.Events/Handlers/Scp3114.cs index 2e76d90d5e..d2964ab38c 100644 --- a/EXILED/Exiled.Events/Handlers/Scp3114.cs +++ b/EXILED/Exiled.Events/Handlers/Scp3114.cs @@ -37,6 +37,11 @@ public static class Scp3114 /// public static Event Revealed { get; set; } = new(); + /// + /// Invoked before SCP-3114 slaps a player. + /// + public static Event Slapping { get; set; } = new(); + /// /// Invoked after SCP-3114 slaps a player. /// @@ -86,6 +91,12 @@ public static class Scp3114 /// The instance. public static void OnRevealed(RevealedEventArgs ev) => Revealed.InvokeSafely(ev); + /// + /// Called before SCP-3114 slaps. + /// + /// The instance. + public static void OnSlapping(SlappingEventArgs ev) => Slapping.InvokeSafely(ev); + /// /// Called after SCP-3114 slaps a player. /// @@ -116,4 +127,4 @@ public static class Scp3114 /// The instance. public static void OnStrangling(StranglingEventArgs ev) => Strangling.InvokeSafely(ev); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Handlers/Scp939.cs b/EXILED/Exiled.Events/Handlers/Scp939.cs index d5d5da378e..ee36389492 100644 --- a/EXILED/Exiled.Events/Handlers/Scp939.cs +++ b/EXILED/Exiled.Events/Handlers/Scp939.cs @@ -151,4 +151,4 @@ public static class Scp939 /// The instance. public static void OnPlacingMimicPoint(PlacingMimicPointEventArgs ev) => PlacingMimicPoint.InvokeSafely(ev); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Map/ExplodingFlashGrenade.cs b/EXILED/Exiled.Events/Patches/Events/Map/ExplodingFlashGrenade.cs index 88549bc13a..cf8c24b69e 100644 --- a/EXILED/Exiled.Events/Patches/Events/Map/ExplodingFlashGrenade.cs +++ b/EXILED/Exiled.Events/Patches/Events/Map/ExplodingFlashGrenade.cs @@ -7,6 +7,7 @@ namespace Exiled.Events.Patches.Events.Map { + using System; using System.Collections.Generic; using System.Linq; using System.Reflection.Emit; @@ -66,6 +67,9 @@ private static void ProcessEvent(FlashbangGrenade instance, float distance) HashSet targetToAffect = HashSetPool.Pool.Get(); foreach (Player player in ReferenceHub.AllHubs.Select(Player.Get)) { + if (player == null || !player.IsConnected) + continue; + if ((instance.transform.position - player.Position).sqrMagnitude > distance) continue; @@ -91,7 +95,7 @@ private static void ProcessEvent(FlashbangGrenade instance, float distance) return; foreach (Player player in explodingGrenadeEvent.TargetsToAffect) - instance.ProcessPlayer(player.ReferenceHub); + instance.ProcessPlayer(player?.ReferenceHub); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Player/Banning.cs b/EXILED/Exiled.Events/Patches/Events/Player/Banning.cs index 573c2bd820..6ad545f59c 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/Banning.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/Banning.cs @@ -52,7 +52,6 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ChangingMoveState.cs b/EXILED/Exiled.Events/Patches/Events/Player/ChangingMoveState.cs index 5c9879f7ca..a43c0afb70 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/ChangingMoveState.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/ChangingMoveState.cs @@ -86,4 +86,4 @@ private static IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ChangingNickname.cs b/EXILED/Exiled.Events/Patches/Events/Player/ChangingNickname.cs index f903e459ba..1e4aea59de 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/ChangingNickname.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/ChangingNickname.cs @@ -22,13 +22,17 @@ namespace Exiled.Events.Patches.Events.Player /// /// Patches to add the event. /// - [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ChangingNickname))] - [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_displayName), MethodType.Setter)] + // TODO: Не работает, вновь, срёт. + // [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ChangingNickname))] + // [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_displayName), MethodType.Setter)] internal static class ChangingNickname { private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) { List newInstructions = ListPool.Pool.Get(instructions); + + LocalBuilder player = generator.DeclareLocal(typeof(Player)); + LocalBuilder oldName = generator.DeclareLocal(typeof(string)); Label continueLabel = generator.DefineLabel(); const int offset = 1; @@ -40,11 +44,19 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable instruction.Calls(Method(typeof(GameObjectPools.PoolObject), nameof(GameObjectPools.PoolObject.SetupPoolObject)))) + offset; + newInstructions[index].WithLabels(continueLabel1); + newInstructions.InsertRange( index, new[] { + // if (player == null) + // continue + new CodeInstruction(OpCodes.Ldloc_S, player.LocalIndex), + new(OpCodes.Brfalse_S, continueLabel1), + // player.Role = Role.Create(roleBase); new CodeInstruction(OpCodes.Ldloc_S, player.LocalIndex), new(OpCodes.Ldloc_2), @@ -142,10 +156,22 @@ private static IEnumerable Transpiler(IEnumerable i.Calls(Method(typeof(PlayerRoleManager.RoleChanged), nameof(PlayerRoleManager.RoleChanged.Invoke)))) + offset; + newInstructions[index].labels.Add(continueLabel2); + newInstructions.InsertRange( index, - new CodeInstruction[] + new[] { + // if (player == null) + // continue + new CodeInstruction(OpCodes.Ldloc_S, player.LocalIndex), + new(OpCodes.Brfalse_S, continueLabel2), + + // if (changingRoleEventArgs == null) + // continue + new CodeInstruction(OpCodes.Ldloc_S, changingRoleEventArgs.LocalIndex), + new(OpCodes.Brfalse_S, continueLabel2), + // changingRoleEventArgs new(OpCodes.Ldloc_S, changingRoleEventArgs.LocalIndex), @@ -154,7 +180,7 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ReservedSlotPatch.cs b/EXILED/Exiled.Events/Patches/Events/Player/ReservedSlotPatch.cs index 68c3c315bc..c5b819f1bf 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/ReservedSlotPatch.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/ReservedSlotPatch.cs @@ -11,52 +11,40 @@ namespace Exiled.Events.Patches.Events.Player using System.Reflection.Emit; using API.Features.Pools; + using Exiled.API.Extensions; + using Exiled.API.Features; using Exiled.Events.Attributes; using Exiled.Events.EventArgs.Player; - using Handlers; - using HarmonyLib; using static HarmonyLib.AccessTools; /// /// Patches . - /// Adds the event. + /// Adds the event. /// - [EventPatch(typeof(Player), nameof(Player.ReservedSlot))] + [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ReservedSlot))] [HarmonyPatch(typeof(ReservedSlot), nameof(ReservedSlot.HasReservedSlot))] internal static class ReservedSlotPatch { private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) { List newInstructions = ListPool.Pool.Get(instructions); - - Label callExiledEvent = generator.DefineLabel(); - - for (int i = 0; i < newInstructions.Count; i++) - { - if (newInstructions[i].opcode == OpCodes.Ret) - { - newInstructions[i] = new(OpCodes.Br, callExiledEvent); - } - } - newInstructions.InsertRange(newInstructions.Count - 1, new[] { // flag is already loaded - new CodeInstruction(OpCodes.Ldarg_0).WithLabels(callExiledEvent), + new CodeInstruction(OpCodes.Ldarg_0), // ReservedSlotCheckEventArgs ev = new(flag, userid); new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ReservedSlotsCheckEventArgs))[0]), new(OpCodes.Dup), // Handlers.Player.OnReservedSlot(ev); - new(OpCodes.Call, Method(typeof(Player), nameof(Player.OnReservedSlot))), + new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnReservedSlot))), // return ev.IsAllowed new(OpCodes.Callvirt, PropertyGetter(typeof(ReservedSlotsCheckEventArgs), nameof(ReservedSlotsCheckEventArgs.IsAllowed))), - new(OpCodes.Ret), }); for (int z = 0; z < newInstructions.Count; z++) @@ -65,4 +53,4 @@ private static IEnumerable Transpiler(IEnumerable.Pool.Return(newInstructions); } } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Player/Spawning.cs b/EXILED/Exiled.Events/Patches/Events/Player/Spawning.cs index af8b3a6c20..c5de7a127e 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/Spawning.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/Spawning.cs @@ -16,7 +16,10 @@ namespace Exiled.Events.Patches.Events.Player using Exiled.Events.Attributes; using Exiled.Events.EventArgs.Player; using HarmonyLib; + using Mirror; + using PlayerRoles; + using PlayerRoles.FirstPersonControl; using PlayerRoles.FirstPersonControl.Spawnpoints; using UnityEngine; @@ -31,60 +34,46 @@ namespace Exiled.Events.Patches.Events.Player [HarmonyPatch(typeof(RoleSpawnpointManager), nameof(RoleSpawnpointManager.SetPosition))] internal static class Spawning { - private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + private static bool Prefix(ReferenceHub hub, PlayerRoleBase newRole) { - List newInstructions = ListPool.Pool.Get(instructions); - - int offset = -1; - - // Locate the call to `Transform.position` setter to determine where to insert new instructions. - int index = newInstructions.FindIndex(instr => instr.Calls(PropertySetter(typeof(Transform), nameof(Transform.position)))) + offset; - - // Declare the `SpawningEventArgs` local variable. - LocalBuilder ev = generator.DeclareLocal(typeof(SpawningEventArgs)); - - newInstructions.InsertRange( - index, - new[] + if (newRole is not IFpcRole fpcRole) { - // Load `ReferenceHub` (argument 0) and get `Player`. - new CodeInstruction(OpCodes.Ldarg_0), // Load `hub` (first argument passed to the method). - new CodeInstruction(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), // Call Player.Get(hub) to get the Player instance. - - // Load `position` (local variable 2). - new CodeInstruction(OpCodes.Ldloc_2), - - // Load `rotation` (local variable 3). - new CodeInstruction(OpCodes.Ldloc_3), + CallEventForNonFpc(hub, newRole); + return false; + } - // Load `newRole` (argument 1). - new CodeInstruction(OpCodes.Ldarg_1), // Load `newRole` from argument 1. + ISpawnpointHandler spawnpointHandler = fpcRole.SpawnpointHandler; - // Create a new instance of `SpawningEventArgs`. - new CodeInstruction(OpCodes.Newobj, GetDeclaredConstructors(typeof(SpawningEventArgs))[0]), - - // Duplicate the object to store it and pass it around. - new CodeInstruction(OpCodes.Dup), // Duplicate the `SpawningEventArgs` object. - new CodeInstruction(OpCodes.Stloc, ev.LocalIndex), // Store the duplicated object in a local variable. + if (spawnpointHandler == null || !spawnpointHandler.TryGetSpawnpoint(out Vector3 position, out float horizontalRot) || !newRole.ServerSpawnFlags.HasFlag(RoleSpawnFlags.UseSpawnpoint)) + { + position = hub.transform.position; + horizontalRot = fpcRole.FpcModule.MouseLook?.CurrentHorizontal ?? 0.0f; + } - // Call `Handlers.Player.OnSpawning`. - new CodeInstruction(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnSpawning))), + Player player = Player.Get(hub); - // Modify `position` from `ev.Position`. - new CodeInstruction(OpCodes.Ldloc, ev.LocalIndex), // Load the `SpawningEventArgs` object stored in the local variable. - new CodeInstruction(OpCodes.Call, PropertyGetter(typeof(SpawningEventArgs), nameof(SpawningEventArgs.Position))), // Get the `Position` property from `SpawningEventArgs`. - new CodeInstruction(OpCodes.Stloc_2), // Store the position value back in the local variable 2 (`position`). + SpawningEventArgs ev = new SpawningEventArgs(player, position, horizontalRot, newRole); + Handlers.Player.OnSpawning(ev); - // Modify `rotation` from `ev.HorizontalRotation`. - new CodeInstruction(OpCodes.Ldloc, ev.LocalIndex), // Load the `SpawningEventArgs` object again. - new CodeInstruction(OpCodes.Call, PropertyGetter(typeof(SpawningEventArgs), nameof(SpawningEventArgs.HorizontalRotation))), // Get the `HorizontalRotation` property from `SpawningEventArgs`. - new CodeInstruction(OpCodes.Stloc_3), // Store the rotation value back in the local variable 3 (`rotation`). - }); + hub.transform.position = ev.Position; + if (fpcRole.FpcModule.MouseLook != null) + { + fpcRole.FpcModule.MouseLook.CurrentHorizontal = ev.HorizontalRotation; + } - for (int z = 0; z < newInstructions.Count; z++) - yield return newInstructions[z]; + return false; + } - ListPool.Pool.Return(newInstructions); + private static void CallEventForNonFpc(ReferenceHub hub, PlayerRoleBase newRole) + { + Player player = Player.Get(hub); + if (player == null) + return; + if (player.IsVerified || player.IsNPC) + { + SpawningEventArgs ev = new SpawningEventArgs(player, player.Position, 0.0f, newRole); + Handlers.Player.OnSpawning(ev); + } } } } diff --git a/EXILED/Exiled.Events/Patches/Events/Player/ThrownProjectile.cs b/EXILED/Exiled.Events/Patches/Events/Player/ThrownProjectile.cs index d4ecc59a65..db65cdb145 100644 --- a/EXILED/Exiled.Events/Patches/Events/Player/ThrownProjectile.cs +++ b/EXILED/Exiled.Events/Patches/Events/Player/ThrownProjectile.cs @@ -11,6 +11,9 @@ namespace Exiled.Events.Patches.Events.Player using System.Reflection.Emit; using API.Features.Pools; + + using Exiled.API.Extensions; + using Exiled.API.Features; using Exiled.Events.Attributes; using Exiled.Events.EventArgs.Player; @@ -26,9 +29,10 @@ namespace Exiled.Events.Patches.Events.Player /// /// Patches . - /// Adds the event. + /// Adds the and events. /// [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ThrownProjectile))] + [EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.ThrowingProjectile))] [HarmonyPatch(typeof(ThrowableItem), nameof(ThrowableItem.ServerThrow))] internal static class ThrownProjectile { @@ -44,17 +48,47 @@ private static IEnumerable Transpiler(IEnumerable i.Calls(Method(typeof(InventorySystem.Items.ThrowableProjectiles.ThrownProjectile), nameof(InventorySystem.Items.ThrowableProjectiles.ThrownProjectile.ServerActivate)))) + offset; + + // remove original ServerActivate + List