Skip to content
Open
56 changes: 56 additions & 0 deletions EXILED/Exiled.API/Features/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public Player(GameObject gameObject)
DictionaryPool<string, object>.Pool.Return(SessionVariables);
DictionaryPool<RoleTypeId, float>.Pool.Return(FriendlyFireMultiplier);
DictionaryPool<string, Dictionary<RoleTypeId, float>>.Pool.Return(CustomRoleFriendlyFireMultiplier);
ListPool<Func<Player, RoleData>>.Pool.Return(FakeRoleGenerator);
}

/// <summary>
Expand Down Expand Up @@ -407,6 +408,11 @@ public float InfoViewRange
/// </summary>
public Dictionary<string, object> SessionVariables { get; } = DictionaryPool<string, object>.Pool.Get();

/// <summary>
/// Gets a dictionary that contains from this players POV, a dictionary containing other players and their faked roles with custom data.
/// </summary>
public Dictionary<Player, RoleData> FakeRoles { get; } = new();

/// <summary>
/// Gets a value indicating whether the player has Do Not Track (DNT) enabled. If this value is <see langword="true"/>, data about the player unrelated to server security shouldn't be stored.
/// </summary>
Expand Down Expand Up @@ -607,6 +613,12 @@ internal set
}
}

/// <summary>
/// Gets a <see cref="List{T}"/> of <see cref="Func{T1, T2}"/> generating a <see cref="RoleData"/> to fake this players role whenever this player changes role.
/// </summary>
/// <remarks>See <see cref="SetAppearance(Func{Player,RoleData})"/> for usage.</remarks>
public List<Func<Player, RoleData>> FakeRoleGenerator { get; } = ListPool<Func<Player, RoleData>>.Pool.Get();

/// <summary>
/// Gets the role that player had before changing role.
/// </summary>
Expand Down Expand Up @@ -1844,6 +1856,50 @@ public void TrySetCustomRoleFriendlyFire(string roleTypeId, Dictionary<RoleTypeI
/// <returns> Whether the item was able to be added. </returns>
public bool TryRemoveCustomeRoleFriendlyFire(string role) => CustomRoleFriendlyFireMultiplier.Remove(role);

/// <summary>
/// Adds a <see cref="Func{Player, RoleData}"/> from a <see cref="Player"/> to a <see cref="RoleTypeId"/> that is used every time this players role changes.
/// </summary>
/// <param name="generator">The function that determines if this players role will be faked (to a viewer) after their role changes.</param>
/// <remarks>The first Func in <see cref="FakeRoleGenerator"/> that returns a RoleData that is not <see cref="RoleData.None"/> will be used for faking appearance.
/// <para>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:
/// <code>
/// player => player.Role.Team is Team.ClassD ? new RoleData(RoleTypeId.ClassD) : RoleData.None
/// </code>
/// This method can be further optimized by only using static RoleData instances in your Funcs.
/// </para>
/// </remarks>
public void SetAppearance(Func<Player, RoleData> generator)
{
FakeRoleGenerator.Add(generator);
}

/// <summary>
/// Fakes this players role to other viewers.
/// </summary>
/// <param name="viewers">The players to affect.</param>
/// <param name="fakeRole">The fake role.</param>
/// <param name="authority">How to handle edge cases.</param>
/// <param name="unitId">The Unit ID of the player, if <paramref name="fakeRole"/> is an NTF role.</param>
public void SetAppearance(IEnumerable<Player> viewers, RoleTypeId fakeRole, RoleData.Authority authority = RoleData.Authority.None, byte unitId = 0)
{
foreach (Player player in viewers)
{
player.SetAppearance(this, fakeRole, authority, unitId);
}
}

/// <summary>
/// Fakes another players role to this player.
/// </summary>
/// <param name="player">The target.</param>
/// <param name="fakeRole">The fake role.</param>
/// <param name="authority">How to handle edge cases.</param>
/// <param name="unitId">The Unit ID of the player, if <paramref name="fakeRole"/> is an NTF role.</param>
public void SetAppearance(Player player, RoleTypeId fakeRole, RoleData.Authority authority = RoleData.Authority.None, byte unitId = 0)
{
FakeRoles[player] = new RoleData(fakeRole, authority, unitId);
}

/// <summary>
/// Forces the player's client to play the weapon reload animation, bypassing server-side checks.
/// </summary>
Expand Down
117 changes: 117 additions & 0 deletions EXILED/Exiled.API/Structs/RoleData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// -----------------------------------------------------------------------
// <copyright file="RoleData.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Structs
{
using System;

using Mirror;
using PlayerRoles;

/// <summary>
/// A struct representing all data regarding a fake role.
/// </summary>
public struct RoleData : IEquatable<RoleData>
{
/// <summary>
/// Initializes a new instance of the <see cref="RoleData"/> struct.
/// </summary>
/// <param name="role">The fake role.</param>
/// <param name="authority">The authority of the role data.</param>
/// <param name="unitId">The fake UnitID, if <paramref name="role"/> is an NTF role.</param>
public RoleData(RoleTypeId role = RoleTypeId.None, Authority authority = Authority.None, byte unitId = 0)
{
Role = role;
DataAuthority = authority;
UnitId = unitId;
}

/// <summary>
/// Represents flags for how Exiled should handle edge cases.
/// </summary>
[Flags]
public enum Authority
{
/// <summary>
/// Indicates Exiled should only fake the role of the target of this <see cref="RoleData"/> in ideal conditions.
/// </summary>
None = 0,

/// <summary>
/// Indicates that Exiled should attempt to override other plugins fake role attempts if they exist.
/// </summary>
/// <remarks>This is not guaranteed to always work.</remarks>
Override = 1,

/// <summary>
/// Indicates that the fake role should always be sent without checking if the player is dead, etc...
/// </summary>
Always = 2,

/// <summary>
/// Indicates that Exiled should not reset the fake role if the target of this <see cref="RoleData"/> dies.
/// </summary>
Persist = 4,

/// <summary>
/// Indicates that this <see cref="RoleData"/> can make a player view themselves as a different role.
/// </summary>
AffectSelf = 8,
}

/// <summary>
/// Gets the static <see cref="RoleData"/> representing no data.
/// </summary>
public static RoleData None { get; } = new(RoleTypeId.None);

/// <summary>
/// Gets or sets the fake role.
/// </summary>
public RoleTypeId Role { get; set; }

/// <summary>
/// Gets or sets the UnitID of the fake role, if <see cref="Role"/> is an NTF role.
/// </summary>
public byte UnitId { get; set; }

/// <summary>
/// Gets or sets the authority of this <see cref="RoleData"/> instance. see <see cref="Authority"/> for details.
/// </summary>
public Authority DataAuthority { get; set; } = Authority.None;

/// <summary>
/// Gets or sets custom data written to network writers when fake data is generated.
/// </summary>
/// <remarks>Leave this value as null unless you are writing custom role-specific data.</remarks>
public Action<NetworkWriter> CustomData { get; set; }

/// <summary>
/// Checks if 2 <see cref="RoleData"/> are equal.
/// </summary>
/// <param name="left">A <see cref="RoleData"/>.</param>
/// <param name="right">The other <see cref="RoleData"/>.</param>
/// <returns>Whether the parameters are equal.</returns>
public static bool operator ==(RoleData left, RoleData right) => left.Equals(right);

/// <summary>
/// Checks if 2 <see cref="RoleData"/> are not equal.
/// </summary>
/// <param name="left">A <see cref="RoleData"/>.</param>
/// <param name="right">The other <see cref="RoleData"/>.</param>
/// <returns>Whether the parameters are not equal.</returns>
public static bool operator !=(RoleData left, RoleData right) => !left.Equals(right);

/// <inheritdoc/>
public bool Equals(RoleData other) => Role == other.Role && DataAuthority == other.DataAuthority && UnitId == other.UnitId;

/// <inheritdoc/>
public override bool Equals(object obj) => obj is RoleData other && Equals(other);

/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine((int)Role, UnitId, (int)DataAuthority, CustomData);
}
}
9 changes: 9 additions & 0 deletions EXILED/Exiled.Events/Events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
117 changes: 117 additions & 0 deletions EXILED/Exiled.Events/Handlers/Internal/Round.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Exiled.Events.Handlers.Internal
{
using System;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -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;
Expand Down Expand Up @@ -89,6 +93,39 @@ public static void OnChangingRole(ChangingRoleEventArgs ev)
ev.Player.Inventory.ServerDropEverything();
}

/// <inheritdoc cref="Handlers.Player.OnSpawned(SpawnedEventArgs)" />
public static void OnSpawned(SpawnedEventArgs ev)
{
foreach (Player viewer in Player.Enumerable)
{
foreach (Func<Player, RoleData> 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;
}
}
}
}

/// <inheritdoc cref="Handlers.Player.OnDying(DyingEventArgs)" />
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);
}
}

/// <inheritdoc cref="Handlers.Player.OnSpawningRagdoll(SpawningRagdollEventArgs)" />
public static void OnSpawningRagdoll(SpawningRagdollEventArgs ev)
{
Expand Down Expand Up @@ -125,9 +162,89 @@ public static void OnVerified(VerifiedEventArgs ev)
foreach (Player player in ReferenceHub.AllHubs.Select(Player.Get))
{
player.SetFakeScale(player.Scale, new List<Player>() { ev.Player });

foreach (Func<Player, RoleData> 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;
}
}
}
}

/// <summary>
/// Makes fake role API work.
/// </summary>
/// <param name="ownerHub">The <see cref="ReferenceHub"/> of the player.</param>
/// <param name="viewerHub">The <see cref="ReferenceHub"/> of the viewer.</param>
/// <param name="actualRole">The actual <see cref="RoleTypeId"/>.</param>
/// <param name="writer">The pooled <see cref="NetworkWriter"/>.</param>
/// <returns>A role, fake if needed.</returns>
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) || data.Role == actualRole)
return actualRole;

if (ownerHub.roleManager.PreviouslySentRole.TryGetValue(viewerHub.netId, out RoleTypeId previousRole) && previousRole == data.Role)
return previousRole;

// 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<string> 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;
}

/// <inheritdoc cref="Handlers.Warhead.OnDetonated()"/>
public static void OnWarheadDetonated()
{
Expand Down
Loading