diff --git a/EXILED/Exiled.API/Enums/BloodType.cs b/EXILED/Exiled.API/Enums/BloodType.cs deleted file mode 100644 index 369793e2a..000000000 --- a/EXILED/Exiled.API/Enums/BloodType.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.API.Enums -{ - using System; - - /// - /// Unique identifier for the different types of blood decals. - /// - /// - /// - [Obsolete("This blood decal are outdated now used DecalPoolType.Blood", true)] - public enum BloodType - { - /// - /// The default blood decal. - /// - Default, - - /// - /// The blood decal placed after Scp106 sends someone to the pocket dimension. - /// - Scp106, - - /// - /// The spreaded blood decal. - /// - Spreaded, - - /// - /// The faded blood decal. - /// - Faded, - } -} \ No newline at end of file diff --git a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs index bbd325878..66452d24e 100644 --- a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs +++ b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs @@ -22,7 +22,10 @@ namespace Exiled.API.Extensions using CustomPlayerEffects; + using Decals; + using Exiled.API.Enums; + using Exiled.API.Features.Items; using Exiled.API.Features.Items.Keycards; using Exiled.API.Features.Pickups.Keycards; @@ -56,6 +59,8 @@ namespace Exiled.API.Extensions using Utils.Networking; + using Firearm = Features.Items.Firearm; + /// /// A set of extensions for Networking. /// @@ -211,58 +216,245 @@ public static ReadOnlyDictionary RpcFullNames /// /// Target to play. /// Position to play on. - /// 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.")] - public static void PlayGunSound(this Player player, Vector3 position, ItemType itemType, byte volume, byte audioClipId = 0) - => PlayGunSound(player, position, itemType.GetFirearmType(), volume, audioClipId); + /// Weapon's sound to play. + /// Speed of sound. + /// Index of clip. + [Obsolete("This method is deprecated, use PlayGunSound(this Player, FirearmType, int, Vector3, MixerChannel, float, float) instead.")] + public static void PlayGunSound(this Player player, Vector3 position, FirearmType firearmType, float pitch = 1, int clipIndex = 0) => player.PlayGunSound(firearmType, clipIndex, position, pitch: pitch); /// /// Plays a gun sound that only the can hear. /// /// Target to play. - /// Position to play on. /// 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) + /// Position to play on. + /// Audio's mixer channel. + /// Max range of sound. + /// Speed of sound. + public static void PlayGunSound(this Player player, FirearmType firearmType, int clipIndex, Vector3 position, MixerChannel mixerChannel = MixerChannel.Weapons, float? range = null, float? pitch = null) { - if (firearmType is FirearmType.ParticleDisruptor or FirearmType.None) + if (firearmType is FirearmType.None) + { + Log.Error($"Failed to play gun sound for player {player.Nickname} because firearm type was None."); return; + } - Features.Items.Firearm firearm = Features.Items.Firearm.ItemTypeToFirearmInstance[firearmType]; + if (!InventoryItemLoader.TryGetItem(firearmType.GetItemType(), out ItemBase itemBase)) + { + Log.Error($"Failed to get ItemBase for firearm type {firearmType} when trying to play gun sound for player {player.Nickname}"); + return; + } + Firearm firearm = Item.Get(itemBase); if (firearm == null) + { + Log.Error($"Failed to get Firearm for firearm type {firearmType} when trying to play gun sound."); return; + } using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { writer.WriteUShort(NetworkMessageId.Id); - new RoleSyncInfo(Server.Host.ReferenceHub, RoleTypeId.ClassD, player.ReferenceHub, null).Write(writer); + new RoleSyncInfo(Server.Host.ReferenceHub, RoleTypeId.Tutorial, player.ReferenceHub, null).Write(writer); writer.WriteRelativePosition(new RelativePosition(0, 0, 0, 0, false)); writer.WriteUShort(0); player.Connection.Send(writer); } - firearm.BarrelAmmo = 1; - firearm.BarrelMagazine.IsCocked = true; player.SendFakeSyncVar(Server.Host.Inventory.netIdentity, typeof(Inventory), nameof(Inventory.NetworkCurItem), firearm.Identifier); - if (!firearm.Base.TryGetModule(out AudioModule audioModule)) - return; - - Timing.CallDelayed(0.1f, () => // due to selecting item we need to delay shot a bit + Timing.CallDelayed(0.1f, () => { - audioModule.SendRpc(player.ReferenceHub, writer => - audioModule.ServerSend(writer, clipIndex, pitch, MixerChannel.Weapons, 12f, position, false)); + if (!player.IsConnected) + return; + firearm.PlaySound(player, clipIndex, mixerChannel, position, shooterVisible: false, range, pitch); player.SendFakeSyncVar(Server.Host.Inventory.netIdentity, typeof(Inventory), nameof(Inventory.NetworkCurItem), ItemIdentifier.None); - player.Connection.Send(new RoleSyncInfo(Server.Host.ReferenceHub, Server.Host.Role, player.ReferenceHub, null)); }); } + /// + /// Plays a gun sound to the specified player. + /// + /// The firearm whose to use. + /// The player to send the sound to. + /// The index of the audio clip to play. + /// The to play the sound on. + /// The world position the sound originates from. + /// Whether the shooter is visible to the target. If , the sound will be played at instead of on the firearm's transform. + /// The range of the sound. + /// The pitch of the sound. + /// if the sound was played successfully; if is . + public static bool PlaySound(this Firearm firearm, Player target, int index, MixerChannel channel, Vector3 position, bool shooterVisible, float? range = null, float? pitch = null) + { + AudioModule audioModule = firearm.AudioModule; + if (audioModule == null) + { + Log.Error($"Firearm {firearm} doesn't have an audio module."); + return false; + } + + if (target == null) + { + Log.Error("Target player is null."); + return false; + } + + if (!range.HasValue) + range = audioModule.FinalGunshotRange; + + if (!pitch.HasValue) + pitch = audioModule.RandomPitch; + + audioModule.SendRpc(target.ReferenceHub, writer => audioModule.ServerSend(writer, index, pitch.Value, channel, range.Value, position, shooterVisible)); + return true; + } + + /// + /// Plays a gun sound to the specified players. + /// + /// The firearm whose to use. + /// The players to send the sound to. + /// The index of the audio clip to play. + /// The to play the sound on. + /// The world position the sound originates from. + /// Whether the shooter is visible to the target. If , the sound will be played at instead of on the firearm's transform. + /// The range of the sound. + /// The pitch of the sound. + /// if the sound was played successfully; if is . + public static bool PlaySound(this Firearm firearm, IEnumerable targets, int index, MixerChannel channel, Vector3 position, bool shooterVisible, float? range = null, float? pitch = null) + { + AudioModule audioModule = firearm.AudioModule; + if (audioModule == null) + { + Log.Error($"Firearm {firearm} doesn't have an audio module."); + return false; + } + + if (targets == null) + { + Log.Error("Failed to play sound, targets is null."); + return false; + } + + if (!range.HasValue) + range = audioModule.FinalGunshotRange; + + if (!pitch.HasValue) + pitch = audioModule.RandomPitch; + + HashSet targetHubs = targets.Select(p => p.ReferenceHub).ToHashSet(); + + audioModule.SendRpc(targetHubs.Contains, writer => audioModule.ServerSend(writer, index, pitch.Value, channel, range.Value, position, shooterVisible)); + return true; + } + + /// + /// Spawns a blood decal for this player. + /// + /// Target to spawn blood decal for. + /// The position of the blood decal. + /// The raycast origin used to determine the decal's orientation. + /// if the blood decal was successfully spawned; otherwise, . + public static bool SpawnBlood(this Player player, Vector3 position, Vector3 sourcePosition) => SpawnDecal(player, position, sourcePosition, DecalPoolType.Blood); + + /// + /// Spawns a blood decal for the specified players. + /// + /// The players for which to spawn the blood decal. + /// The position of the blood decal. + /// The raycast origin used to determine the decal's orientation. + /// if the blood decal was successfully spawned; otherwise, . + public static bool SpawnBlood(this IEnumerable players, Vector3 position, Vector3 sourcePosition) => SpawnDecal(players, position, sourcePosition, DecalPoolType.Blood, FirearmType.Com15); + + /// + /// Spawns a decal for this player. + /// + /// Target to spawn decal for. + /// The position of the decal. + /// The raycast origin used to determine the decal's orientation. + /// The . + /// The to use. + /// if the decal was successfully spawned; otherwise, . + public static bool SpawnDecal(this Player player, Vector3 position, Vector3 sourcePosition, DecalPoolType decalType, FirearmType firearmType = FirearmType.Com15) + { + if (!InventoryItemLoader.TryGetItem(firearmType.GetItemType(), out ItemBase itemBase)) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + Firearm firearm = Item.Get(itemBase); + if (firearm == null) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + ImpactEffectsModule impactEffectsModule = firearm.ImpactEffectsModule; + if (impactEffectsModule == null) + { + Log.Error($"Failed to spawn decal: Could not find an ImpactEffectsModule for {firearmType}."); + return false; + } + + impactEffectsModule.SendRpc(player.ReferenceHub, writer => + { + writer.WriteSubheader(ImpactEffectsModule.RpcType.ImpactDecal); + writer.WriteByte((byte)decalType); + writer.WriteRelativePosition(new RelativePosition(position)); + writer.WriteRelativePosition(new RelativePosition(sourcePosition)); + }); + + return true; + } + + /// + /// Spawns a decal for the specified targets. + /// + /// The targets for which to spawn the decal. + /// The position of the decal. + /// The raycast origin used to determine the decal's orientation. + /// The . + /// The to use. + /// if the decal was successfully spawned; otherwise, . + public static bool SpawnDecal(this IEnumerable targets, Vector3 position, Vector3 sourcePosition, DecalPoolType decalType, FirearmType firearmType = FirearmType.Com15) + { + if (!InventoryItemLoader.TryGetItem(firearmType.GetItemType(), out ItemBase itemBase)) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + Firearm firearm = Item.Get(itemBase); + if (firearm == null) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + ImpactEffectsModule impactEffectsModule = firearm.ImpactEffectsModule; + if (impactEffectsModule == null) + { + Log.Error($"Failed to spawn decal: Could not find an ImpactEffectsModule for {firearmType}."); + return false; + } + + HashSet targetHubs = targets.Select(p => p.ReferenceHub).ToHashSet(); + + impactEffectsModule.SendRpc(targetHubs.Contains, writer => + { + writer.WriteSubheader(ImpactEffectsModule.RpcType.ImpactDecal); + writer.WriteByte((byte)decalType); + writer.WriteRelativePosition(new RelativePosition(position)); + writer.WriteRelativePosition(new RelativePosition(sourcePosition)); + }); + + return true; + } + /// /// Place blood that only the can see. /// @@ -271,6 +463,7 @@ public static void PlayGunSound(this Player player, Vector3 position, FirearmTyp /// The direction of the blood decal. /// The RoleTypeId from who blood come from. /// The sound than player get when getting shot. + [Obsolete("Use Player::SpawnBlood(Vector3, Vector3) instead.")] #pragma warning disable IDE0060 // TODO: Deleted the unused param public static void PlaceBlood(this Player player, Vector3 position, Vector3 origin, RoleTypeId roleTypeId, int gettingShotSoundIndex) #pragma warning restore IDE0060 diff --git a/EXILED/Exiled.API/Features/Items/Firearm.cs b/EXILED/Exiled.API/Features/Items/Firearm.cs index 00630fad3..4aea76359 100644 --- a/EXILED/Exiled.API/Features/Items/Firearm.cs +++ b/EXILED/Exiled.API/Features/Items/Firearm.cs @@ -10,6 +10,8 @@ namespace Exiled.API.Features.Items using System.Collections.Generic; using System.Linq; + using AudioPooling; + using CameraShaking; using Enums; @@ -28,7 +30,10 @@ namespace Exiled.API.Features.Items using InventorySystem.Items.Firearms.Attachments.Components; using InventorySystem.Items.Firearms.Modules; + using UnityEngine; + using static InventorySystem.Items.Firearms.Modules.AnimatorReloaderModuleBase; + using static InventorySystem.Items.Firearms.Modules.AutomaticActionModule; using BaseFirearm = InventorySystem.Items.Firearms.Firearm; using FirearmPickup = Pickups.FirearmPickup; @@ -67,6 +72,8 @@ public Firearm(BaseFirearm itemBase) case IAmmoContainerModule ammoModule: BarrelMagazine ??= (BarrelMagazine)Magazine.Get(ammoModule); + if (module is AutomaticActionModule automaticActionModule) + AutomaticActionModule = automaticActionModule; break; case HitscanHitregModuleBase hitregModule: @@ -77,6 +84,14 @@ public Firearm(BaseFirearm itemBase) AnimatorReloaderModule = animatorReloaderModule; break; + case ImpactEffectsModule impactEffectsModule: + ImpactEffectsModule = impactEffectsModule; + break; + + case AudioModule audioModule: + AudioModule = audioModule; + break; + default: break; } @@ -152,6 +167,21 @@ public static IReadOnlyDictionary public AnimatorReloaderModuleBase AnimatorReloaderModule { get; } + /// + /// Gets an impact effects module for the current firearm. + /// + public ImpactEffectsModule ImpactEffectsModule { get; } + + /// + /// Gets an automatic action module for the current firearm. + /// + public AutomaticActionModule AutomaticActionModule { get; } + + /// + /// Gets an audio module for the current firearm. + /// + public AudioModule AudioModule { get; } + /// /// Gets or sets the amount of ammo in the firearm magazine. /// @@ -757,6 +787,64 @@ public void Unload() AnimatorReloaderModule.SendRpcHeaderWithRandomByte(ReloaderMessageHeader.Unload); } + /// + /// Plays a firearm sound to nearby players. + /// + /// The index of the audio clip to play. + /// The to play the sound on. + /// The range within which nearby players can hear the sound. + /// The pitch of the sound. + /// if the sound was played successfully; if is . + public bool PlaySound(int index, MixerChannel channel, float? range = null, float? pitch = null) + { + if (AudioModule == null) + { + Log.Error($"Failed to play sound, firearm {Type} does not have an AudioModule."); + return false; + } + + if (!range.HasValue) + range = AudioModule.FinalGunshotRange; + + if (!pitch.HasValue) + pitch = AudioModule.RandomPitch; + + AudioModule.ServerSendToNearbyPlayers(index, channel, range.Value, pitch.Value); + return true; + } + + /// + /// Simulates a fire. + /// + /// Rpc header for fire type like fire or dry fire. + /// The number of chambers fired. + /// The index of the sound to play. 0 is DryFire, 1 is default gunshot. + public void FakeFire(MessageHeader rpcHeader = MessageHeader.RpcFire, byte chambersFired = 1, byte soundIndex = 1) + { + PlaySound(soundIndex, MixerChannel.Weapons, AudioModule.FinalGunshotRange, AudioModule.RandomPitch); + + if (AutomaticActionModule?.gameObject != null) + { + AutomaticActionModule.SendRpc( + writer => + { + writer.WriteSubheader(rpcHeader); + if (rpcHeader == MessageHeader.RpcFire) + writer.WriteByte(chambersFired); + }, + true); + } + + if (ImpactEffectsModule != null) + { + Transform camera = Owner.CameraTransform; + float maxDist = HitscanHitregModule.FullDamageDistance + HitscanHitregModule.DamageFalloffDistance; + + if (Physics.Raycast(camera.position, camera.forward, out RaycastHit hit, maxDist, HitscanHitregModuleBase.HitregMask)) + ImpactEffectsModule.ServerProcessHit(hit, camera.position, true); + } + } + /// /// Clones current object. /// diff --git a/EXILED/Exiled.API/Features/Map.cs b/EXILED/Exiled.API/Features/Map.cs index 5f3205755..a7c59a1ee 100644 --- a/EXILED/Exiled.API/Features/Map.cs +++ b/EXILED/Exiled.API/Features/Map.cs @@ -27,6 +27,9 @@ namespace Exiled.API.Features using Interactables.Interobjects; using InventorySystem; + using InventorySystem.Items; + using InventorySystem.Items.Autosync; + using InventorySystem.Items.Firearms.Modules; using InventorySystem.Items.Pickups; using InventorySystem.Items.ThrowableProjectiles; @@ -36,8 +39,12 @@ namespace Exiled.API.Features using MapGeneration; + using Mirror; + using PlayerRoles.Ragdolls; + using RelativePositioning; + using RemoteAdmin; using UnityEngine; @@ -436,12 +443,64 @@ public static void CleanAllRagdolls(IEnumerable ragDolls) public static void Clean(DecalPoolType decalType) => Clean(decalType, int.MaxValue); /// - /// Places a blood decal. + /// Places a blood decal using a raycast. + /// + /// The origin position of the raycast. + /// The direction in which the raycast is fired to detect a surface. + public static void PlaceBlood(Vector3 position, Vector3 direction) + { + if (Physics.Raycast(position, direction, out RaycastHit hitInfo, ImpactEffectsModule.ReceivingLayers)) + SpawnBlood(hitInfo.point + (hitInfo.normal * Decal.SurfaceDistance), -hitInfo.normal); + } + + /// + /// Spawns a blood decal. /// /// The position of the blood decal. - /// The direction of the blood decal. - [Obsolete("Use PlaceBlood(this Player, Vector3, Vector3, RoleTypeId, int) instead.")] - public static void PlaceBlood(Vector3 position, Vector3 direction) => _ = 0; + /// The raycast origin used to determine the decal's orientation. + /// if the blood decal was successfully spawned; otherwise, . + public static bool SpawnBlood(Vector3 position, Vector3 sourcePosition) => SpawnDecal(position, sourcePosition, DecalPoolType.Blood, FirearmType.Com15); + + /// + /// Spawns a decal. + /// + /// The position of the decal. + /// The raycast origin used to determine the decal's orientation. + /// The . + /// The to use. + /// if the decal was successfully spawned; otherwise, . + public static bool SpawnDecal(Vector3 position, Vector3 sourcePosition, DecalPoolType decalType, FirearmType firearmType = FirearmType.Com15) + { + if (!InventoryItemLoader.TryGetItem(firearmType.GetItemType(), out ItemBase itemBase)) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + Firearm firearm = Item.Get(itemBase); + if (firearm == null) + { + Log.Error($"Failed to spawn decal: Could not find a Firearm for {firearmType}."); + return false; + } + + ImpactEffectsModule impactEffectsModule = firearm.ImpactEffectsModule; + if (impactEffectsModule == null) + { + Log.Error($"Failed to spawn decal: Could not find an ImpactEffectsModule for {firearmType}."); + return false; + } + + impactEffectsModule.SendRpc(writer => + { + writer.WriteSubheader(ImpactEffectsModule.RpcType.ImpactDecal); + writer.WriteByte((byte)decalType); + writer.WriteRelativePosition(new RelativePosition(position)); + writer.WriteRelativePosition(new RelativePosition(sourcePosition)); + }); + + return true; + } /// /// Gets all the near cameras. diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs index 936a82c4c..ef43a01eb 100644 --- a/EXILED/Exiled.API/Features/Player.cs +++ b/EXILED/Exiled.API/Features/Player.cs @@ -14,6 +14,8 @@ namespace Exiled.API.Features using System.Reflection; using System.Runtime.CompilerServices; + using AudioPooling; + using Core; using CustomPlayerEffects; @@ -49,6 +51,7 @@ namespace Exiled.API.Features using InventorySystem.Items; using InventorySystem.Items.Armor; using InventorySystem.Items.Firearms.Attachments; + using InventorySystem.Items.Firearms.BasicMessages; using InventorySystem.Items.Firearms.Modules; using InventorySystem.Items.Firearms.ShotEvents; using InventorySystem.Items.Usables; @@ -3855,19 +3858,28 @@ 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); + /// + [Obsolete("Use Player::PlayGunSound(FirearmType, int, Vector3, MixerChannel, float?, float?) instead of this.")] + public void PlayGunSound(FirearmType itemType, float pitch = 1, int clipIndex = 0) => this.PlayGunSound(itemType, clipIndex, Position, pitch: pitch); /// - [Obsolete("Use PlaceBlood(this Player, Vector3, Vector3, RoleTypeId, int) instead.")] public void PlaceBlood(Vector3 direction) => Map.PlaceBlood(Position, direction); + /// + /// Sends a damage indicator to player. + /// + /// The amount of damage dealt. Controls the size of the damage indicator. + /// The world position the damage originated from. + /// If true, spectators watching this player will also see the indicator. + public void SendHitEffect(float dmgDealt, Vector3 position, bool sendSpectatorsToo = true) + { + DamageIndicatorMessage damageIndicatorMessage = new(dmgDealt, position); + if (!sendSpectatorsToo) + Connection.Send(damageIndicatorMessage); + else + damageIndicatorMessage.SendToSpectatorsOf(ReferenceHub, true); + } + /// public IEnumerable GetNearCameras(float toleration = 15f) => Map.GetNearCameras(Position, toleration); @@ -3881,8 +3893,7 @@ public void PlayGunSound(FirearmType itemType, float pitch = 1, int clipIndex = /// Teleports the player to the given object, with no offset. /// /// The object to teleport to. - public void Teleport(object obj) - => Teleport(obj, Vector3.zero); + public void Teleport(object obj) => Teleport(obj, Vector3.zero); /// /// Teleports the player to the given object, offset by the defined offset value.