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