diff --git a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs index cf0a97872..09969e637 100644 --- a/EXILED/Exiled.API/Extensions/MirrorExtensions.cs +++ b/EXILED/Exiled.API/Extensions/MirrorExtensions.cs @@ -10,12 +10,15 @@ 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; using System.Text; using Exiled.API.Enums; + using Exiled.API.Features.Roles; + using Features; using Features.Pools; @@ -24,7 +27,6 @@ namespace Exiled.API.Extensions using PlayerRoles; using PlayerRoles.FirstPersonControl; using PlayerRoles.PlayableScps.Scp049.Zombies; - using PlayerRoles.PlayableScps.Scp1507; using PlayerRoles.Voice; using RelativePositioning; @@ -229,9 +231,7 @@ public static void SetName(this Player target, Player player, string name) /// /// Player to change. /// Model type. - /// Whether to skip the little jump that works around an invisibility issue. - /// 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, bool skipJump = false, byte unitId = 0) => ChangeAppearance(player, type, Player.List.Where(x => x != player), skipJump, unitId); + public static void ChangeAppearance(this Player player, RoleTypeId type) => ChangeAppearance(player, type, Player.List.Where(x => x != player)); /// /// Change character model for appearance. @@ -240,70 +240,23 @@ public static void SetName(this Player target, Player player, string name) /// Player to change. /// Model type. /// The players who should see the changed appearance. - /// Whether to skip the little jump that works around an invisibility issue. - /// 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) + public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable playersToAffect) { - if (!player.IsConnected || !RoleExtensions.TryGetRoleBase(type, out PlayerRoleBase roleBase)) + if (!player.IsConnected) return; - bool isRisky = type.GetTeam() is Team.Dead || player.IsDead; - - NetworkWriterPooled writer = NetworkWriterPool.Get(); - writer.WriteUShort(38952); - writer.WriteUInt(player.NetId); - writer.WriteRoleType(type); - - if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) - { - 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.CheckAppearanceCompatibility(type)) { - 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($"Prevent Seld-Desync of {player.Nickname} ({player.Role.Type}) with {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); - - // 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) - player.Position += Vector3.up * 0.25f; + player.Role.UpdateAppearance(); } /// diff --git a/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs b/EXILED/Exiled.API/Extensions/ReflectionExtensions.cs index 21b1d6104..98fd44e6a 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 369910654..f4e25edc0 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 Footprinting; using InventorySystem; using InventorySystem.Configs; @@ -229,6 +232,32 @@ 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.TeamAppearances.TryGetValue(player.Role.Team, out appearance)) + { + return appearance; + } + + return role.GlobalAppearance; + } + /// /// Gets the of a . /// diff --git a/EXILED/Exiled.API/Features/Roles/FpcRole.cs b/EXILED/Exiled.API/Features/Roles/FpcRole.cs index ecc946c9b..20998d57d 100644 --- a/EXILED/Exiled.API/Features/Roles/FpcRole.cs +++ b/EXILED/Exiled.API/Features/Roles/FpcRole.cs @@ -10,8 +10,12 @@ namespace Exiled.API.Features.Roles using System.Collections.Generic; using System.Reflection; + using Exiled.API.Extensions; using Exiled.API.Features.Pools; using HarmonyLib; + + using Mirror; + using PlayerRoles; using PlayerRoles.FirstPersonControl; using PlayerRoles.Ragdolls; @@ -282,5 +286,23 @@ public void ResetStamina(bool multipliers = false) StaminaUsageMultiplier = 1f; StaminaRegenMultiplier = 1f; } + + /// + 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); + } } } \ No newline at end of file diff --git a/EXILED/Exiled.API/Features/Roles/HumanRole.cs b/EXILED/Exiled.API/Features/Roles/HumanRole.cs index 8a238a3e7..4ed3398c2 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 Respawning; @@ -62,5 +64,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 31f53ac04..b1c7f34ef 100644 --- a/EXILED/Exiled.API/Features/Roles/Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Role.cs @@ -8,13 +8,18 @@ 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; @@ -38,6 +43,10 @@ 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(); + /// /// Initializes a new instance of the class. /// @@ -48,6 +57,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 +139,21 @@ 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 IndividualAppearances => individualAppearances; + /// /// Gets a random spawn position of this role. /// @@ -215,6 +249,164 @@ 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 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 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); + + if (update) + { + UpdateAppearance(); + } + } + + /// + /// Updates current player appearance. + /// + public void UpdateAppearance() + { + if (Owner != null) + Owner.RoleManager._sendNextFrame = true; + } + + /// + /// 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)); + Owner.RoleManager.PreviouslySentRole[player.NetId] = roleTypeId; + } + /// /// Creates a role from and . /// @@ -241,5 +433,28 @@ public virtual void Set(RoleTypeId newRole, SpawnReason reason, RoleSpawnFlags s #pragma warning restore CS0618 _ => 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 7250aa466..9a1cd0d55 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/Scp1507Role.cs b/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs index 559fdd662..5b29c990d 100644 --- a/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs +++ b/EXILED/Exiled.API/Features/Roles/Scp1507Role.cs @@ -10,6 +10,7 @@ namespace Exiled.API.Features.Roles using System; using System.Collections.Generic; + using Mirror; using PlayerRoles; using PlayerRoles.PlayableScps; using PlayerRoles.PlayableScps.HumeShield; @@ -55,5 +56,13 @@ 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) + { + writer.WriteByte((byte)basicRole.ServerSpawnReason); + + base.SendAppearanceSpawnMessage(writer, basicRole); + } } } diff --git a/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs new file mode 100644 index 000000000..aee4312e2 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Player/SendingRoleEventArgs.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------- +// +// 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 => roleTypeId; + + set + { + if (Player.Role.CheckAppearanceCompatibility(value)) + { + roleTypeId = value; + } + } + } + } +} diff --git a/EXILED/Exiled.Events/Events.cs b/EXILED/Exiled.Events/Events.cs index ac37f5528..1572f7694 100644 --- a/EXILED/Exiled.Events/Events.cs +++ b/EXILED/Exiled.Events/Events.cs @@ -68,6 +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.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; @@ -103,6 +104,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.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; @@ -161,4 +163,4 @@ public void Unpatch() Log.Debug("All events have been unpatched complete. Goodbye!"); } } -} \ 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 87d5d2a2c..6cbeadfc0 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs @@ -81,6 +81,24 @@ public static void OnChangingRole(ChangingRoleEventArgs ev) ev.Player.Inventory.ServerDropEverything(); } + /// + public static void OnSpawned(SpawnedEventArgs ev) + { + 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); + } + } + } + /// public static void OnActivatingSense(ActivatingSenseEventArgs ev) { diff --git a/EXILED/Exiled.Events/Handlers/Player.cs b/EXILED/Exiled.Events/Handlers/Player.cs index ac58bb308..0e2770f7e 100644 --- a/EXILED/Exiled.Events/Handlers/Player.cs +++ b/EXILED/Exiled.Events/Handlers/Player.cs @@ -553,6 +553,11 @@ public class Player /// public static Event ChangingNickname { get; set; } = new(); + /// + /// Invoked before a 's role is sent to a client. + /// + public static Event SendingRole { get; set; } = new(); + /// /// Invoked before a sends valid command. /// @@ -1236,6 +1241,12 @@ public static void OnItemRemoved(ReferenceHub referenceHub, InventorySystem.Item /// The instance. public static void OnChangingNickname(ChangingNicknameEventArgs ev) => ChangingNickname.InvokeSafely(ev); + /// + /// Called before a 's role is sent to a client. + /// + /// The instance. + public static void OnSendingRole(SendingRoleEventArgs ev) => SendingRole.InvokeSafely(ev); + /// /// Called before a sends valid command. /// diff --git a/EXILED/Exiled.Events/Patches/Generic/RoleAppearance.cs b/EXILED/Exiled.Events/Patches/Generic/RoleAppearance.cs new file mode 100644 index 000000000..4e1a4cef0 --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Generic/RoleAppearance.cs @@ -0,0 +1,154 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Generic +{ + using System.Collections.Generic; + using System.Reflection.Emit; + + using API.Features; + + using Exiled.API.Extensions; + using Exiled.API.Features.Pools; + using Exiled.API.Features.Roles; + using Exiled.Events.EventArgs.Player; + + using HarmonyLib; + + using Mirror; + + using PlayerRoles; + using PlayerRoles.SpawnData; + + using static HarmonyLib.AccessTools; + + /// + /// Patches to implement , and . + /// + [HarmonyPatch(typeof(RoleSyncInfo), nameof(RoleSyncInfo.Write))] + internal class RoleAppearance + { + private static IEnumerable Transpiler(IEnumerable codeInstructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(codeInstructions); + + LocalBuilder player = generator.DeclareLocal(typeof(Player)); + LocalBuilder role = generator.DeclareLocal(typeof(Role)); + + Label skipEvent = generator.DefineLabel(); + Label skip = generator.DefineLabel(); + Label skip2 = generator.DefineLabel(); + + int offset = -2; + int index = newInstructions.FindIndex(i => i.LoadsField(Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetRole)))) + offset; + + newInstructions.InsertRange( + index, + new[] + { + // Player player = Player.Get(_targetNetId); + // if (player == null) + // skip; + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetNetId))), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(uint) })), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, player.LocalIndex), + new(OpCodes.Brfalse_S, skipEvent), + + // if (_targetNetId == _receiverNetId) + // skip; + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetNetId))), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._receiverNetId))), + new(OpCodes.Beq_S, skipEvent), + + // role = player.Role; + // if (role == null) + // skip; + new(OpCodes.Ldloc_S, player.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(Player), nameof(Player.Role))), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, role.LocalIndex), + new(OpCodes.Brfalse_S, skip), + + // _targetRole = role.GetAppearanceForPlayer(Player.Get(this._receiverHub)); + new(OpCodes.Ldarg_0), + + new(OpCodes.Ldloc_S, role.LocalIndex), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._receiverNetId))), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(uint) })), + new(OpCodes.Call, Method(typeof(RoleExtensions), nameof(RoleExtensions.GetAppearanceForPlayer))), + new(OpCodes.Stfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetRole))), + + new CodeInstruction(OpCodes.Nop).WithLabels(skip), + + // SendingRoleEventArgs ev = new(player, _receiverNetId, _targetRole); + // Player.OnSendingRole(ev); + // roleType = ev.RoleType; + new(OpCodes.Ldarg_0), + + new(OpCodes.Ldloc_S, player.LocalIndex), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._receiverNetId))), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetRole))), + + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(SendingRoleEventArgs))[0]), + new(OpCodes.Dup), + new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnSendingRole))), + + new(OpCodes.Callvirt, PropertyGetter(typeof(SendingRoleEventArgs), nameof(SendingRoleEventArgs.RoleType))), + new(OpCodes.Stfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetRole))), + + new CodeInstruction(OpCodes.Nop).WithLabels(skipEvent), + }); + + offset = -2; + index = newInstructions.FindIndex(i => i.Calls(Method(typeof(IPublicSpawnDataWriter), nameof(IPublicSpawnDataWriter.WritePublicSpawnData)))) + offset; + + Label cnt = (Label)newInstructions[index - 1].operand; + + newInstructions.InsertRange( + index, + new[] + { + // if (role == null) + // skip; + new CodeInstruction(OpCodes.Ldloc_S, role.LocalIndex), + new(OpCodes.Brfalse_S, skip2), + + // SendMessage(player.Role, writer, _targetRole) + // skip original nw code; + new(OpCodes.Ldloc_S, player.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(Player), nameof(Player.Role))), + new(OpCodes.Ldarg_1), + new(OpCodes.Ldarg_0), + new(OpCodes.Ldfld, Field(typeof(RoleSyncInfo), nameof(RoleSyncInfo._targetRole))), + + new(OpCodes.Call, Method(typeof(RoleAppearance), nameof(RoleAppearance.SendMessage))), + new(OpCodes.Br_S, cnt), + + new CodeInstruction(OpCodes.Nop).WithLabels(skip2), + }); + + for (int z = 0; z < newInstructions.Count; z++) + yield return newInstructions[z]; + + ListPool.Pool.Return(newInstructions); + } + + private static void SendMessage(Role role, NetworkWriter writer, RoleTypeId appearance) + { + Role appearancedRole = role.Type == appearance ? role : Role.Create(appearance.GetRoleBase()); + + appearancedRole.SendAppearanceSpawnMessage(writer, role.Base); + } + } +} \ No newline at end of file