Skip to content

Commit

Permalink
[Exiled::API] Rework MirrorExtension (#2731)
Browse files Browse the repository at this point in the history
* Added UsersWhitelisted in Server.cs

* MirrorExtensions Rework

* Removed wrong import

* Rework

* Fixed issue and renamed the functions

* Fixed Stylecop and issue with building

* Fixed Name

* Changed Name from UsersWhitelisted to WhitelistedPlayers

---------

Co-authored-by: xNexusACS <83370388+xNexusACS@users.noreply.github.com>
  • Loading branch information
NotZer0Two and xNexusACS authored Jul 19, 2024
1 parent a07c88c commit 244d90e
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 203 deletions.
3 changes: 3 additions & 0 deletions Exiled.API/Extensions/ItemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ namespace Exiled.API.Extensions
using Features.Items;
using InventorySystem;
using InventorySystem.Items;
using InventorySystem.Items.Firearms;
using InventorySystem.Items.Firearms.Attachments;
using InventorySystem.Items.Pickups;
using InventorySystem.Items.ThrowableProjectiles;
using Structs;

using Firearm = Features.Items.Firearm;

/// <summary>
/// A set of extensions for <see cref="ItemType"/>.
/// </summary>
Expand Down
229 changes: 31 additions & 198 deletions Exiled.API/Extensions/MirrorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,204 +144,6 @@ public static ReadOnlyDictionary<string, string> RpcFullNames
/// </summary>
public static MethodInfo SendSpawnMessageMethodInfo => sendSpawnMessageMethodInfoValue ??= typeof(NetworkServer).GetMethod("SendSpawnMessage", BindingFlags.NonPublic | BindingFlags.Static);

/// <summary>
/// Plays a beep sound that only the target <paramref name="player"/> can hear.
/// </summary>
/// <param name="player">Target to play sound to.</param>
public static void PlayBeepSound(this Player player) => SendFakeTargetRpc(player, ReferenceHub.HostHub.networkIdentity, typeof(AmbientSoundPlayer), nameof(AmbientSoundPlayer.RpcPlaySound), 7);

/// <summary>
/// Set <see cref="Player.CustomInfo"/> on the <paramref name="target"/> player that only the <paramref name="player"/> can see.
/// </summary>
/// <param name="player">Only this player can see info.</param>
/// <param name="target">Target to set info.</param>
/// <param name="info">Setting info.</param>
public static void SetPlayerInfoForTargetOnly(this Player player, Player target, string info) => player.SendFakeSyncVar(target.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_customPlayerInfoString), info);

/// <summary>
/// Plays a gun sound that only the <paramref name="player"/> can hear.
/// </summary>
/// <param name="player">Target to play.</param>
/// <param name="position">Position to play on.</param>
/// <param name="itemType">Weapon' sound to play.</param>
/// <param name="volume">Sound's volume to set.</param>
/// <param name="audioClipId">GunAudioMessage's audioClipId to set (default = 0).</param>
public static void PlayGunSound(this Player player, Vector3 position, ItemType itemType, byte volume, byte audioClipId = 0)
{
GunAudioMessage message = new()
{
Weapon = itemType,
AudioClipId = audioClipId,
MaxDistance = volume,
ShooterHub = player.ReferenceHub,
ShooterPosition = new RelativePosition(position),
};

player.Connection.Send(message);
}

/// <summary>
/// Sets <see cref="Room.Color"/> of a <paramref name="room"/> that only the <paramref name="target"/> player can see.
/// </summary>
/// <param name="room">Room to modify.</param>
/// <param name="target">Only this player can see room color.</param>
/// <param name="color">Color to set.</param>
public static void SetRoomColorForTargetOnly(this Room room, Player target, Color color) => target.SendFakeSyncVar(room.RoomLightControllerNetIdentity, typeof(RoomLightController), nameof(RoomLightController.NetworkOverrideColor), color);

/// <summary>
/// Sets the lights of a <paramref name="room"/> to be either on or off, visible only to the <paramref name="target"/> player.
/// </summary>
/// <param name="room">The room to modify the lights of.</param>
/// <param name="target">The player who will see the lights state change.</param>
/// <param name="value">The state to set the lights to. True for on, false for off.</param>
public static void SetRoomLightsForTargetOnly(this Room room, Player target, bool value) => target.SendFakeSyncVar(room.RoomLightControllerNetIdentity, typeof(RoomLightController), nameof(RoomLightController.NetworkLightsEnabled), value);

/// <summary>
/// Sets <see cref="EIntercom.DisplayText"/> that only the <paramref name="target"/> player can see.
/// </summary>
/// <param name="target">Only this player can see Display Text.</param>
/// <param name="text">Text displayed to the player.</param>
public static void SetIntercomDisplayTextForTargetOnly(this Player target, string text) => target.SendFakeSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText), text);

/// <summary>
/// Resync <see cref="EIntercom.DisplayText"/>.
/// </summary>
public static void ResetIntercomDisplayText() => ResyncSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText));

/// <summary>
/// Sets <see cref="Player.DisplayNickname"/> of a <paramref name="player"/> that only the <paramref name="target"/> player can see.
/// </summary>
/// <param name="target">Only this player can see the name changed.</param>
/// <param name="player">Player that will desync the CustomName.</param>
/// <param name="name">Nickname to set.</param>
public static void SetName(this Player target, Player player, string name)
{
target.SendFakeSyncVar(player.NetworkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_displayName), name);
}

/// <summary>
/// Change <see cref="Player"/> character model for appearance.
/// It will continue until <see cref="Player"/>'s <see cref="RoleTypeId"/> changes.
/// </summary>
/// <param name="player">Player to change.</param>
/// <param name="type">Model type.</param>
/// <param name="skipJump">Whether or not to skip the little jump that works around an invisibility issue.</param>
/// <param name="unitId">The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is NTF).</param>
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);

/// <summary>
/// Change <see cref="Player"/> character model for appearance.
/// It will continue until <see cref="Player"/>'s <see cref="RoleTypeId"/> changes.
/// </summary>
/// <param name="player">Player to change.</param>
/// <param name="type">Model type.</param>
/// <param name="playersToAffect">The players who should see the changed appearance.</param>
/// <param name="skipJump">Whether or not to skip the little jump that works around an invisibility issue.</param>
/// <param name="unitId">The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is NTF).</param>
public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable<Player> playersToAffect, bool skipJump = false, byte unitId = 0)
{
if (!player.IsConnected || !RoleExtensions.TryGetRoleBase(type, out PlayerRoleBase roleBase))
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 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);
}

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));
}

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}");
}

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

/// <summary>
/// Send CASSIE announcement that only <see cref="Player"/> can hear.
/// </summary>
/// <param name="player">Target to send.</param>
/// <param name="words">Announcement words.</param>
/// <param name="makeHold">Same on <see cref="Cassie.Message(string, bool, bool, bool)"/>'s isHeld.</param>
/// <param name="makeNoise">Same on <see cref="Cassie.Message(string, bool, bool, bool)"/>'s isNoisy.</param>
/// <param name="isSubtitles">Same on <see cref="Cassie.Message(string, bool, bool, bool)"/>'s isSubtitles.</param>
public static void PlayCassieAnnouncement(this Player player, string words, bool makeHold = false, bool makeNoise = true, bool isSubtitles = false)
{
foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers)
{
if (controller != null)
{
SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), words, makeHold, makeNoise, isSubtitles);
}
}
}

/// <summary>
/// Send CASSIE announcement with custom subtitles for translation that only <see cref="Player"/> can hear and see it.
/// </summary>
/// <param name="player">Target to send.</param>
/// <param name="words">The message to be reproduced.</param>
/// <param name="translation">The translation should be show in the subtitles.</param>
/// <param name="makeHold">Same on <see cref="Cassie.MessageTranslated(string, string, bool, bool, bool)"/>'s isHeld.</param>
/// <param name="makeNoise">Same on <see cref="Cassie.MessageTranslated(string, string, bool, bool, bool)"/>'s isNoisy.</param>
/// <param name="isSubtitles">Same on <see cref="Cassie.MessageTranslated(string, string, bool, bool, bool)"/>'s isSubtitles.</param>
public static void MessageTranslated(this Player player, string words, string translation, bool makeHold = false, bool makeNoise = true, bool isSubtitles = true)
{
StringBuilder announcement = StringBuilderPool.Pool.Get();

string[] cassies = words.Split('\n');
string[] translations = translation.Split('\n');

for (int i = 0; i < cassies.Length; i++)
announcement.Append($"{translations[i].Replace(' ', ' ')}<size=0> {cassies[i]} </size><split>");

string message = StringBuilderPool.Pool.ToStringReturn(announcement);

foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers)
{
if (controller != null)
{
SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), message, makeHold, makeNoise, isSubtitles);
}
}
}

/// <summary>
/// Send fake values to client's <see cref="SyncVarAttribute"/>.
/// </summary>
Expand Down Expand Up @@ -412,6 +214,37 @@ public static void SendFakeTargetRpc(Player target, NetworkIdentity behaviorOwne
NetworkWriterPool.Return(writer);
}

/// <summary>
/// Send fake values to client's <see cref="ClientRpcAttribute"/>.
/// </summary>
/// <param name="target">Target to send.</param>
/// <param name="behaviorOwner"><see cref="NetworkIdentity"/> of object that owns <see cref="NetworkBehaviour"/>.</param>
/// <param name="targetType"><see cref="NetworkBehaviour"/>'s type.</param>
/// <param name="rpcName">Property name starting with Rpc.</param>
/// <param name="values">Values of send to target.</param>
public static void SendFakeTargetRpc(ReferenceHub target, NetworkIdentity behaviorOwner, Type targetType, string rpcName, params object[] values)
{
if (target.gameObject == null)
return;

NetworkWriterPooled writer = NetworkWriterPool.Get();

foreach (object value in values)
WriterExtensions[value.GetType()].Invoke(null, new[] { writer, value });

RpcMessage msg = new()
{
netId = behaviorOwner.netId,
componentIndex = (byte)GetComponentIndex(behaviorOwner, targetType),
functionHash = (ushort)RpcFullNames[$"{targetType.Name}.{rpcName}"].GetStableHashCode(),
payload = writer.ToArraySegment(),
};

target.connectionToClient.Send(msg);

NetworkWriterPool.Return(writer);
}

/// <summary>
/// Send fake values to client's <see cref="SyncObject"/>.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion Exiled.API/Extensions/RoomExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
namespace Exiled.API.Extensions
{
using Exiled.API.Enums;

using Exiled.API.Features;
using MapGeneration;
using UnityEngine;

/// <summary>
/// A set of extensions for <see cref="RoomType"/> and <see cref="ZoneType"/>.
Expand Down
13 changes: 13 additions & 0 deletions Exiled.API/Features/Intercom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Exiled.API.Features
{
using Exiled.API.Extensions;
using Mirror;

using PlayerRoles.Voice;
Expand Down Expand Up @@ -97,5 +98,17 @@ public static float SpeechRemainingTime
/// Times out the intercom.
/// </summary>
public static void Timeout() => State = IntercomState.Cooldown;

/// <summary>
/// Sets <see cref="DisplayText"/> that only the <paramref name="target"/> player can see.
/// </summary>
/// <param name="target">Only this player can see Display Text.</param>
/// <param name="text">Text displayed to the player.</param>
public static void SetIntercomDisplayTextForTargetOnly(this Player target, string text) => target.SendFakeSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText), text);

/// <summary>
/// Resync <see cref="DisplayText"/>.
/// </summary>
public static void ResetIntercomDisplayText() => MirrorExtensions.ResyncSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText));
}
}
Loading

0 comments on commit 244d90e

Please sign in to comment.