Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Exiled::API] Rework MirrorExtension #2731

Merged
merged 15 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading