Skip to content
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 @@ -1839,6 +1851,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
Loading
Loading