diff --git a/EXILED/Exiled.API/Features/Player.cs b/EXILED/Exiled.API/Features/Player.cs index 6874edb59d..24d0760efc 100644 --- a/EXILED/Exiled.API/Features/Player.cs +++ b/EXILED/Exiled.API/Features/Player.cs @@ -127,6 +127,7 @@ public Player(GameObject gameObject) DictionaryPool.Pool.Return(SessionVariables); DictionaryPool.Pool.Return(FriendlyFireMultiplier); DictionaryPool>.Pool.Return(CustomRoleFriendlyFireMultiplier); + ListPool>.Pool.Return(FakeRoleGenerator); } /// @@ -407,6 +408,11 @@ public float InfoViewRange /// public Dictionary SessionVariables { get; } = DictionaryPool.Pool.Get(); + /// + /// Gets a dictionary that contains from this players POV, a dictionary containing other players and their faked roles with custom data. + /// + public Dictionary FakeRoles { get; } = new(); + /// /// Gets a value indicating whether the player has Do Not Track (DNT) enabled. If this value is , data about the player unrelated to server security shouldn't be stored. /// @@ -607,6 +613,12 @@ internal set } } + /// + /// Gets a of generating a to fake this players role whenever this player changes role. + /// + /// See for usage. + public List> FakeRoleGenerator { get; } = ListPool>.Pool.Get(); + /// /// Gets the role that player had before changing role. /// @@ -1839,6 +1851,50 @@ public void TrySetCustomRoleFriendlyFire(string roleTypeId, Dictionary Whether the item was able to be added. public bool TryRemoveCustomeRoleFriendlyFire(string role) => CustomRoleFriendlyFireMultiplier.Remove(role); + /// + /// Adds a from a to a that is used every time this players role changes. + /// + /// The function that determines if this players role will be faked (to a viewer) after their role changes. + /// The first Func in that returns a RoleData that is not will be used for faking appearance. + /// An example use case would be to make a scientist appear as a Class-D to all other Class-D, that Func would look like: + /// + /// player => player.Role.Team is Team.ClassD ? new RoleData(RoleTypeId.ClassD) : RoleData.None + /// + /// This method can be further optimized by only using static RoleData instances in your Funcs. + /// + /// + public void SetAppearance(Func generator) + { + FakeRoleGenerator.Add(generator); + } + + /// + /// Fakes this players role to other viewers. + /// + /// The players to affect. + /// The fake role. + /// How to handle edge cases. + /// The Unit ID of the player, if is an NTF role. + public void SetAppearance(IEnumerable viewers, RoleTypeId fakeRole, RoleData.Authority authority = RoleData.Authority.None, byte unitId = 0) + { + foreach (Player player in viewers) + { + player.SetAppearance(this, fakeRole, authority, unitId); + } + } + + /// + /// Fakes another players role to this player. + /// + /// The target. + /// The fake role. + /// How to handle edge cases. + /// The Unit ID of the player, if is an NTF role. + public void SetAppearance(Player player, RoleTypeId fakeRole, RoleData.Authority authority = RoleData.Authority.None, byte unitId = 0) + { + FakeRoles[player] = new RoleData(fakeRole, authority, unitId); + } + /// /// Forces the player's client to play the weapon reload animation, bypassing server-side checks. /// diff --git a/EXILED/Exiled.API/Structs/RoleData.cs b/EXILED/Exiled.API/Structs/RoleData.cs new file mode 100644 index 0000000000..ae65f305bc --- /dev/null +++ b/EXILED/Exiled.API/Structs/RoleData.cs @@ -0,0 +1,117 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.API.Structs +{ + using System; + + using Mirror; + using PlayerRoles; + + /// + /// A struct representing all data regarding a fake role. + /// + public struct RoleData : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The fake role. + /// The authority of the role data. + /// The fake UnitID, if is an NTF role. + public RoleData(RoleTypeId role = RoleTypeId.None, Authority authority = Authority.None, byte unitId = 0) + { + Role = role; + DataAuthority = authority; + UnitId = unitId; + } + + /// + /// Represents flags for how Exiled should handle edge cases. + /// + [Flags] + public enum Authority + { + /// + /// Indicates Exiled should only fake the role of the target of this in ideal conditions. + /// + None = 0, + + /// + /// Indicates that Exiled should attempt to override other plugins fake role attempts if they exist. + /// + /// This is not guaranteed to always work. + Override = 1, + + /// + /// Indicates that the fake role should always be sent without checking if the player is dead, etc... + /// + Always = 2, + + /// + /// Indicates that Exiled should not reset the fake role if the target of this dies. + /// + Persist = 4, + + /// + /// Indicates that this can make a player view themselves as a different role. + /// + AffectSelf = 8, + } + + /// + /// Gets the static representing no data. + /// + public static RoleData None { get; } = new(RoleTypeId.None); + + /// + /// Gets or sets the fake role. + /// + public RoleTypeId Role { get; set; } + + /// + /// Gets or sets the UnitID of the fake role, if is an NTF role. + /// + public byte UnitId { get; set; } + + /// + /// Gets or sets the authority of this instance. see for details. + /// + public Authority DataAuthority { get; set; } = Authority.None; + + /// + /// Gets or sets custom data written to network writers when fake data is generated. + /// + /// Leave this value as null unless you are writing custom role-specific data. + public Action CustomData { get; set; } + + /// + /// Checks if 2 are equal. + /// + /// A . + /// The other . + /// Whether the parameters are equal. + public static bool operator ==(RoleData left, RoleData right) => left.Equals(right); + + /// + /// Checks if 2 are not equal. + /// + /// A . + /// The other . + /// Whether the parameters are not equal. + public static bool operator !=(RoleData left, RoleData right) => !left.Equals(right); + + /// + public bool Equals(RoleData other) => Role == other.Role && DataAuthority == other.DataAuthority && UnitId == other.UnitId; + + /// + public override bool Equals(object obj) => obj is RoleData other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine((int)Role, UnitId, (int)DataAuthority, CustomData); + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Events.cs b/EXILED/Exiled.Events/Events.cs index c941abd7ca..876ccf1f37 100644 --- a/EXILED/Exiled.Events/Events.cs +++ b/EXILED/Exiled.Events/Events.cs @@ -18,6 +18,7 @@ namespace Exiled.Events using HarmonyLib; using InventorySystem.Items.Pickups; using InventorySystem.Items.Usables; + using PlayerRoles.FirstPersonControl.NetworkMessages; using PlayerRoles.Ragdolls; using PlayerRoles.RoleAssign; @@ -68,6 +69,8 @@ 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.Player.Dying.Subscribe(Handlers.Internal.Round.OnDying, -100); Handlers.Player.SpawningRagdoll += Handlers.Internal.Round.OnSpawningRagdoll; Handlers.Scp049.ActivatingSense += Handlers.Internal.Round.OnActivatingSense; Handlers.Player.Verified += Handlers.Internal.Round.OnVerified; @@ -93,6 +96,8 @@ public override void OnEnabled() LabApi.Events.Handlers.PlayerEvents.ReloadingWeapon += Handlers.Player.OnReloadingWeapon; LabApi.Events.Handlers.PlayerEvents.UnloadingWeapon += Handlers.Player.OnUnloadingWeapon; + FpcServerPositionDistributor.RoleSyncEvent += Handlers.Internal.Round.OnRoleSyncEvent; + LabApi.Events.Handlers.Scp127Events.Talking += Handlers.Scp127.OnTalking; LabApi.Events.Handlers.Scp127Events.Talked += Handlers.Scp127.OnTalked; LabApi.Events.Handlers.Scp127Events.GainingExperience += Handlers.Scp127.OnGainingExperience; @@ -116,6 +121,8 @@ 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.Player.Dying -= Handlers.Internal.Round.OnDying; Handlers.Player.SpawningRagdoll -= Handlers.Internal.Round.OnSpawningRagdoll; Handlers.Scp049.ActivatingSense -= Handlers.Internal.Round.OnActivatingSense; Handlers.Player.Verified -= Handlers.Internal.Round.OnVerified; @@ -136,6 +143,8 @@ public override void OnDisabled() LabApi.Events.Handlers.PlayerEvents.ReloadingWeapon -= Handlers.Player.OnReloadingWeapon; LabApi.Events.Handlers.PlayerEvents.UnloadingWeapon -= Handlers.Player.OnUnloadingWeapon; + FpcServerPositionDistributor.RoleSyncEvent -= Handlers.Internal.Round.OnRoleSyncEvent; + LabApi.Events.Handlers.Scp127Events.Talking -= Handlers.Scp127.OnTalking; LabApi.Events.Handlers.Scp127Events.Talked -= Handlers.Scp127.OnTalked; LabApi.Events.Handlers.Scp127Events.GainingExperience -= Handlers.Scp127.OnGainingExperience; diff --git a/EXILED/Exiled.Events/Handlers/Internal/Round.cs b/EXILED/Exiled.Events/Handlers/Internal/Round.cs index 70798015e2..2f4e76d15a 100644 --- a/EXILED/Exiled.Events/Handlers/Internal/Round.cs +++ b/EXILED/Exiled.Events/Handlers/Internal/Round.cs @@ -7,6 +7,7 @@ namespace Exiled.Events.Handlers.Internal { + using System; using System.Collections.Generic; using System.Linq; @@ -29,9 +30,12 @@ namespace Exiled.Events.Handlers.Internal using InventorySystem.Items.Usables; using InventorySystem.Items.Usables.Scp244.Hypothermia; using InventorySystem.Items.Usables.Scp330; + using Mirror; using PlayerRoles; using PlayerRoles.FirstPersonControl; using PlayerRoles.RoleAssign; + using RelativePositioning; + using Respawning.NamingRules; using UnityEngine; using Utils.Networking; using Utils.NonAllocLINQ; @@ -86,6 +90,39 @@ public static void OnChangingRole(ChangingRoleEventArgs ev) ev.Player.Inventory.ServerDropEverything(); } + /// + public static void OnSpawned(SpawnedEventArgs ev) + { + foreach (Player viewer in Player.Enumerable) + { + foreach (Func generator in ev.Player.FakeRoleGenerator) + { + RoleData data = generator(viewer); + + if (data.Role == RoleTypeId.None) + continue; + + if (viewer != ev.Player || (data.DataAuthority & RoleData.Authority.AffectSelf) == RoleData.Authority.AffectSelf) + { + viewer.FakeRoles[ev.Player] = data; + } + } + } + } + + /// + public static void OnDying(DyingEventArgs ev) + { + if (!ev.IsAllowed) + return; + + foreach (Player viewer in Player.Enumerable) + { + if (viewer.FakeRoles.TryGetValue(ev.Player, out RoleData data) && (data.DataAuthority & RoleData.Authority.Persist) == RoleData.Authority.None) + viewer.FakeRoles.Remove(ev.Player); + } + } + /// public static void OnSpawningRagdoll(SpawningRagdollEventArgs ev) { @@ -125,9 +162,94 @@ public static void OnVerified(VerifiedEventArgs ev) foreach (Player player in ReferenceHub.AllHubs.Select(Player.Get)) { player.SetFakeScale(player.Scale, new List() { ev.Player }); + + foreach (Func generator in player.FakeRoleGenerator) + { + RoleData data = generator(ev.Player); + + if (data.Role == RoleTypeId.None) + continue; + + if (player != ev.Player || (data.DataAuthority & RoleData.Authority.AffectSelf) == RoleData.Authority.AffectSelf) + { + ev.Player.FakeRoles[player] = data; + } + } } } + /// + /// Makes fake role API work. + /// + /// The of the player. + /// The of the viewer. + /// The actual . + /// The pooled . + /// A role, fake if needed. + public static RoleTypeId OnRoleSyncEvent(ReferenceHub ownerHub, ReferenceHub viewerHub, RoleTypeId actualRole, NetworkWriter writer) + { + Player owner = Player.Get(ownerHub); + Player viewer = Player.Get(viewerHub); + + if (viewer.FakeRoles.TryGetValue(owner, out RoleData data)) + { + if (data.Role == actualRole) + return actualRole; + + if (ownerHub.roleManager.PreviouslySentRole.TryGetValue(viewerHub.netId, out RoleTypeId previousRole) && previousRole == data.Role) + return data.Role; + + // if another plugin has written data, we can't reliably modify and expect non-breaking behavior. + // if we send faulty data we can accidentally soft-dc the entire server which is much worse than a plugin not working. + if (writer.Position != 0 && (data.DataAuthority & RoleData.Authority.Override) == RoleData.Authority.None) + return actualRole; + + writer.Position = 0; + + // I doubt most devs want people who are dead to have fake roles. + if (actualRole.IsDead() && (data.DataAuthority & RoleData.Authority.Always) == RoleData.Authority.None) + return actualRole; + + if (data.CustomData != null) + { + data.CustomData(writer); + } + else + { + if (data.Role.GetRoleBase() is PlayerRoles.HumanRole { UsesUnitNames: true }) + { + if (data.UnitId != 0) + { + writer.WriteByte(data.UnitId); + } + else + { + if (!NamingRulesManager.GeneratedNames.TryGetValue(Team.FoundationForces, out List list)) + return actualRole; + + writer.WriteByte((byte)list.Count); + } + } + + if (data.Role.GetRoleBase() is PlayerRoles.PlayableScps.Scp1507.Scp1507Role flamingo) + writer.WriteByte((byte)flamingo.ServerSpawnReason); + + if (data.Role == RoleTypeId.Scp0492) + { + writer.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(owner.MaxHealth), 0, ushort.MaxValue)); + writer.WriteBool(false); + } + + writer.WriteRelativePosition(new RelativePosition(owner.Position)); + writer.WriteUShort((ushort)Mathf.RoundToInt(Mathf.InverseLerp(0.0f, 360f, owner.Rotation.eulerAngles.y) * ushort.MaxValue)); + } + + return data.Role; + } + + return actualRole; + } + /// public static void OnWarheadDetonated() {