Files
2026-06-07 16:58:24 +01:00

787 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDKBase;
using VRC.Udon;
using Object = UnityEngine.Object;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System that manages all the players currently in ClientSim.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimOnNewMasterEvent
/// - ClientSimOnPlayerJoinedEvent
/// - ClientSimOnPlayerLeftEvent
/// </remarks>
public class ClientSimPlayerManager : IClientSimPlayerManager, IDisposable
{
private int _masterID = -1;
private int _localPlayerID = -1;
private int _nextPlayerID = 1;
private readonly Dictionary<VRCPlayerApi, int> _playerIDs = new Dictionary<VRCPlayerApi, int>();
private readonly Dictionary<int, VRCPlayerApi> _players = new Dictionary<int, VRCPlayerApi>();
// List of players that have joined before ClientSim has finished initializing.
private readonly List<VRCPlayerApi> _waitingPlayers = new List<VRCPlayerApi>();
private bool _networkReady = false;
private VRCPlayerObject[] _playerObjectList;
private VRCPlayerApi _localPlayer;
private IClientSimEventDispatcher _eventDispatcher;
private static IClientSimPlayerHeightManager _heightManager;
public ClientSimPlayerManager(IClientSimEventDispatcher eventDispatcher, IClientSimPlayerHeightManager heightManager)
{
_eventDispatcher = eventDispatcher;
_heightManager = heightManager;
// Ensure no other players have been added to the list.
VRCPlayerApi.AllPlayers.Clear();
}
~ClientSimPlayerManager()
{
Dispose();
}
public void Dispose()
{
// Prevent sending new events as players are cleared.
_eventDispatcher = null;
// Dispose of all players added by this manager.
List<VRCPlayerApi> players = new List<VRCPlayerApi>(_playerIDs.Keys);
foreach (VRCPlayerApi player in players)
{
RemovePlayer(player);
}
_heightManager?.Dispose();
}
private int GetNextPlayerId()
{
int id = _nextPlayerID;
++_nextPlayerID;
return id;
}
private void InitializePlayer(ClientSimPlayer clientSimPlayer, VRCPlayerApi player, int playerId)
{
if (_players.ContainsKey(playerId))
{
throw new ClientSimException($"PlayerId already assigned to player! {playerId}");
}
this.Log($"Assigning player id {playerId}");
_playerIDs[player] = playerId;
_players[playerId] = player;
// Adding player to the list makes them valid. This should happen before network ready has been sent.
player.AddToList();
if (_masterID == -1)
{
this.Log($"Player {playerId} is now master");
_masterID = playerId;
_eventDispatcher?.SendEvent(new ClientSimOnNewMasterEvent
{
oldMasterPlayer = null,
newMasterPlayer = player
});
}
#if VRC_ENABLE_PLAYER_PERSISTENCE
if (ClientSimMain.TryGetInstance(out var instance))
clientSimPlayer.SetupPlayerPersistence(
instance.GetEventDispatcher(),
instance.GetUdonEventSender(),
instance.GetBlacklistManager(),
instance.GetUdonManager(),
instance.GetSyncedObjectManager(),
instance.GetPlayerManager()
);
#endif
if (_networkReady)
{
DispatchPlayerJoinedEvent(player);
}
else
{
_waitingPlayers.Add(player);
}
}
public void OnClientSimReady()
{
_networkReady = true;
foreach (var player in _waitingPlayers)
{
DispatchPlayerJoinedEvent(player);
}
_waitingPlayers.Clear();
}
private void DispatchPlayerJoinedEvent(VRCPlayerApi player)
{
_eventDispatcher?.SendEvent(new ClientSimOnPlayerJoinedEvent { player = player });
}
public VRCPlayerApi CreateNewPlayer(bool local, ClientSimPlayer player, string name = "")
{
int playerID = GetNextPlayerId();
string objectName = $"[{playerID}] {(local ? "Local" : "Remote")} Player";
player.gameObject.name = objectName;
VRCPlayerApi playerApi = new VRCPlayerApi
{
gameObject = player.gameObject,
displayName = string.IsNullOrEmpty(name) ? objectName : name,
isLocal = local
};
player.SetPlayer(playerApi);
InitializePlayer(player, playerApi, playerID);
if (local)
{
_localPlayerID = playerID;
_localPlayer = playerApi;
}
Debug.Assert(playerApi.isLocal == local, "ClientSimPlayerManager:CreateNewPlayer New player does not match local settings!");
return playerApi;
}
public void RemovePlayer(VRCPlayerApi player)
{
// Master is leaving, pick a new master.
if (_masterID == player.playerId)
{
_masterID = -1;
VRCPlayerApi newMaster = null;
Debug.Assert(VRCPlayerApi.AllPlayers[0] == player, "ClientSimPlayerManager:RemovePlayer Removing master player who was not first in the list!");
// First player is the current master who is leaving.
// If there is another player, select them as the new master.
if (VRCPlayerApi.AllPlayers.Count > 1)
{
newMaster = VRCPlayerApi.AllPlayers[1];
_masterID = newMaster.playerId;
}
_eventDispatcher?.SendEvent(new ClientSimOnNewMasterEvent
{
oldMasterPlayer = player,
newMasterPlayer = newMaster
});
}
_eventDispatcher?.SendEvent(new ClientSimOnPlayerLeftEvent { player = player });
_playerIDs.Remove(player);
_players.Remove(player.playerId);
player.RemoveFromList();
if (player.isLocal)
{
_localPlayer = null;
_localPlayerID = -1;
}
Object.Destroy(player.gameObject);
}
public int GetMasterID()
{
return _masterID;
}
public VRCPlayerApi GetMaster()
{
return GetPlayerByID(_masterID);
}
public VRCPlayerApi GetInstanceOwner()
{
foreach (var player in _players.Values)
{
if (player.GetClientSimPlayer().isInstanceOwner)
{
return player;
}
}
return null;
}
public VRCPlayerApi LocalPlayer()
{
return _localPlayer;
}
public VRCPlayerApi GetPlayerByID(int playerID)
{
_players.TryGetValue(playerID, out VRCPlayerApi player);
return player;
}
public int GetPlayerID(VRCPlayerApi player)
{
if (player == null)
{
return -1;
}
_playerIDs.TryGetValue(player, out int playerId);
return playerId;
}
public bool IsMaster(VRCPlayerApi player)
{
return GetPlayerID(player) == _masterID;
}
public bool IsInstanceOwner(VRCPlayerApi player)
{
return player.GetClientSimPlayer().isInstanceOwner;
}
public bool IsInstanceOwner()
{
return IsInstanceOwner(_localPlayer);
}
public bool IsLocalPlayerMaster()
{
return _localPlayerID == _masterID;
}
public bool IsSuspended(VRCPlayerApi player)
{
return player.GetClientSimPlayer().isSuspended;
}
public bool IsVRCPlus(VRCPlayerApi player)
{
return player.GetClientSimPlayer().isVRCPlus;
}
public VRCPlayerApi GetOwner(GameObject obj)
{
// TODO consider SyncMode.None
IClientSimSyncable sync = obj.GetComponent<IClientSimSyncable>();
int playerID = sync != null ? sync.GetOwner() : _masterID;
if (!_players.TryGetValue(playerID, out VRCPlayerApi player))
{
return null;
}
return player;
}
public bool IsOwner(VRCPlayerApi player, GameObject obj)
{
IClientSimSyncable sync = obj.GetComponent<IClientSimSyncable>();
int owner = sync == null ? _masterID : sync.GetOwner();
return owner == player.playerId;
}
public static void SetOwner(VRCPlayerApi player, GameObject obj)
{
if (Networking.GetOwner(obj) == player)
{
return;
}
IClientSimSyncable[] syncs = obj.GetComponents<IClientSimSyncable>();
foreach (IClientSimSyncable sync in syncs)
{
sync.SetOwner(player.playerId);
}
IClientSimSyncableHandler[] syncHandlers = obj.GetComponents<IClientSimSyncableHandler>();
foreach (IClientSimSyncableHandler syncHandler in syncHandlers)
{
syncHandler.OnOwnershipTransferred(player.playerId);
}
}
public static bool IsUserInVR(VRCPlayerApi player)
{
return player.GetClientSimPlayer().IsUserVR;
}
public static void EnablePickups(VRCPlayerApi player, bool enabled)
{
if (!player.isLocal)
{
player.LogWarning($"[VRCPlayerAPI.EnablePickups] EnablePickups for remote players will do nothing. PlayerId: {player.playerId}");
return;
}
player.GetClientSimPlayer().pickupData.SetPickupsEnabled(enabled);
}
public static void Immobilize(VRCPlayerApi player, bool immobilized)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.Immobilize] You cannot set remote players Immobilized");
}
player.GetClientSimPlayer().locomotionData.SetImmobilized(immobilized);
}
public static void TeleportToOrientationLerp(VRCPlayerApi player, Vector3 position, Quaternion rotation, VRC_SceneDescriptor.SpawnOrientation orientation, bool lerp)
{
if (!player.isLocal)
{
player.LogWarning($"[VRCPlayerAPI.TeleportTo] Teleporting remote players will do nothing. PlayerId: {player.playerId}");
return;
}
// Ignore lerp since there is no networking here
player.GetPlayerController().Teleport(position, rotation, orientation == VRC_SceneDescriptor.SpawnOrientation.AlignRoomWithSpawnPoint);
}
public static void TeleportToOrientation(VRCPlayerApi player, Vector3 position, Quaternion rotation, VRC_SceneDescriptor.SpawnOrientation orientation)
{
TeleportToOrientationLerp(player, position, rotation, orientation, false);
}
public static void TeleportTo(VRCPlayerApi player, Vector3 position, Quaternion rotation)
{
TeleportToOrientation(player, position, rotation, VRC_SceneDescriptor.SpawnOrientation.Default);
}
public static void Respawn(VRCPlayerApi playerApi)
{
if (!playerApi.isLocal)
{
playerApi.LogWarning($"[VRCPlayerApi.Respawn] Respawn for remote players will do nothing.");
return;
}
playerApi.GetPlayerController().Respawn();
}
public static void RespawnWithIndex(VRCPlayerApi playerApi, int index)
{
if (!playerApi.isLocal)
{
playerApi.LogWarning($"[VRCPlayerApi.Respawn] Respawn for remote players will do nothing.");
return;
}
playerApi.GetPlayerController().Respawn(index);
}
public static void PlayHapticEventInHand(VRCPlayerApi player, VRC_Pickup.PickupHand hand, float duration, float amplitude, float frequency)
{
if (!player.isLocal)
{
player.LogWarning($"[VRCPlayerAPI.PlayHapticEventInHand] PlayHapticEventInHand for remote players will do nothing. PlayerId: {player.playerId}");
return;
}
// TODO
player.Log($"[VRCPlayerAPI.PlayHapticEventInHand] Playing haptics for player. PlayerId: {player.playerId}, Hand: {hand}, Duration: {duration}, Amplitude: {amplitude}, Frequency: {frequency}");
}
public static VRCPlayerApi GetPlayerByGameObject(GameObject obj)
{
ClientSimPlayer player = obj.GetComponentInParent<ClientSimPlayer>();
if (player != null)
{
return player.Player;
}
return null;
}
public static List<VRCPlayerApi> GetAllPlayersWithinRange(Vector3 pos, float radius, int limit = -1)
{
List<VRCPlayerApi> resultList = new List<VRCPlayerApi>(VRCPlayerApi.AllPlayers.Count);
if (limit != 0)
{
float radiusSqr = radius * radius;
foreach (VRCPlayerApi player in VRCPlayerApi.AllPlayers)
{
Vector3 playerPosition = player.GetPosition();
Vector3 delta = pos - playerPosition;
if (delta.sqrMagnitude <= radiusSqr)
{
resultList.Add(player);
if (limit > 0 && limit <= resultList.Count)
{
break;
}
}
}
resultList.Sort(ComparePlayersByDistance);
}
return resultList;
int ComparePlayersByDistance(VRCPlayerApi a, VRCPlayerApi b)
{
float aDistSqr = (pos - a.GetPosition()).sqrMagnitude;
float bDistSqr = (pos - b.GetPosition()).sqrMagnitude;
return aDistSqr.CompareTo(bDistSqr);
}
}
public static VRC_Pickup GetPickupInHand(VRCPlayerApi player, VRC_Pickup.PickupHand hand)
{
return player.GetClientSimPlayer().pickupData.GetPickupInHand(hand);
}
public static VRCPlayerApi.TrackingData GetTrackingData(VRCPlayerApi player, VRCPlayerApi.TrackingDataType trackingDataType)
{
// Remote players do not have tracking data, so get respective bone
if (!player.isLocal)
{
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
switch (trackingDataType)
{
case VRCPlayerApi.TrackingDataType.Head:
position = GetBonePosition(player, HumanBodyBones.Head);
rotation = GetBoneRotation(player, HumanBodyBones.Head);
break;
case VRCPlayerApi.TrackingDataType.LeftHand:
position = GetBonePosition(player, HumanBodyBones.LeftHand);
rotation = GetBoneRotation(player, HumanBodyBones.LeftHand);
break;
case VRCPlayerApi.TrackingDataType.RightHand:
position = GetBonePosition(player, HumanBodyBones.RightHand);
rotation = GetBoneRotation(player, HumanBodyBones.RightHand);
break;
case VRCPlayerApi.TrackingDataType.Origin:
position = GetPosition(player);
rotation = GetRotation(player);
break;
}
return new VRCPlayerApi.TrackingData(position, rotation);
}
return player.GetClientSimPlayer().GetTrackingProvider().GetTrackingData(trackingDataType);
}
#region Player Locomotion
public static float GetRunSpeed(VRCPlayerApi player)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.GetRunSpeed] You cannot get run speed for remote clients!");
}
return player.GetClientSimPlayer().locomotionData.GetRunSpeed();
}
public static void SetRunSpeed(VRCPlayerApi player, float speed)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.SetRunSpeed] You cannot set run speed for remote clients!");
}
player.GetClientSimPlayer().locomotionData.SetRunSpeed(speed);
}
public static float GetStrafeSpeed(VRCPlayerApi player)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.GetStrafeSpeed] You cannot get strafe speed for remote clients!");
}
return player.GetClientSimPlayer().locomotionData.GetStrafeSpeed();
}
public static void SetStrafeSpeed(VRCPlayerApi player, float speed)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.SetStrafeSpeed] You cannot set strafe speed for remote clients!");
}
player.GetClientSimPlayer().locomotionData.SetStrafeSpeed(speed);
}
public static float GetWalkSpeed(VRCPlayerApi player)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.GetWalkSpeed] You cannot get walk speed for remote clients!");
}
return player.GetClientSimPlayer().locomotionData.GetWalkSpeed();
}
public static void SetWalkSpeed(VRCPlayerApi player, float speed)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.SetWalkSpeed] You cannot set walk speed for remote clients!");
}
player.GetClientSimPlayer().locomotionData.SetWalkSpeed(speed);
}
public static float GetJumpImpulse(VRCPlayerApi player)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.GetJumpImpulse] You cannot get jump impulse for remote clients!");
}
return player.GetClientSimPlayer().locomotionData.GetJump();
}
public static void SetJumpImpulse(VRCPlayerApi player, float jump)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.SetJumpImpulse] You cannot set jump impulse for remote clients!");
}
player.GetClientSimPlayer().locomotionData.SetJump(jump);
}
public static float GetGravityStrength(VRCPlayerApi player)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.GetGravityStrength] You cannot get gravity strength for remote clients!");
}
return player.GetClientSimPlayer().locomotionData.GetGravityStrength();
}
public static void SetGravityStrength(VRCPlayerApi player, float gravity)
{
if (!player.isLocal)
{
// VRChatBug: Throw an exception to crash the udon program similar to VRChat Client.
throw new ClientSimException("[VRCPlayerAPI.SetGravityStrength] You cannot set gravity strength for remote clients!");
}
player.GetClientSimPlayer().locomotionData.SetGravityStrength(gravity);
}
public static void UseLegacyLocomotion(VRCPlayerApi player)
{
if (!player.isLocal)
{
return;
}
player.GetClientSimPlayer().locomotionData.SetUseLegacyLocomotion(true);
}
public static Vector3 GetVelocity(VRCPlayerApi player)
{
if (!player.isLocal)
{
return Vector3.zero;
}
return player.GetPlayerController().GetVelocity();
}
public static void SetVelocity(VRCPlayerApi player, Vector3 velocity)
{
if (!player.isLocal)
{
return;
}
player.GetPlayerController().SetVelocity(velocity);
}
public static Vector3 GetPosition(VRCPlayerApi player)
{
return player.GetClientSimPlayer().GetPosition();
}
public static Quaternion GetRotation(VRCPlayerApi player)
{
return player.GetClientSimPlayer().GetRotation();
}
public static bool IsGrounded(VRCPlayerApi player)
{
if (!player.isLocal)
{
// TODO verify remote player values when not grounded.
return false;
}
return player.GetPlayerController().IsGrounded();
}
#endregion
public static Quaternion GetBoneRotation(VRCPlayerApi player, HumanBodyBones bone)
{
return player.GetClientSimPlayer().GetAvatarDataProvider().GetBoneRotation(bone);
}
public static Vector3 GetBonePosition(VRCPlayerApi player, HumanBodyBones bone)
{
return player.GetClientSimPlayer().GetAvatarDataProvider().GetBonePosition(bone);
}
#region Player Tags
public static List<int> GetPlayersWithTag(string tagName, string tagValue)
{
List<int> players = new List<int>();
foreach (var player in VRCPlayerApi.AllPlayers)
{
if (player.GetClientSimPlayer().tagData.HasPlayerTag(tagName, tagValue))
{
players.Add(player.playerId);
}
}
return players;
}
public static void ClearPlayerTags(VRCPlayerApi player)
{
player.LogError("Clearing all player tags. VRCPlayerApi.ClearPlayerTags is a dangerous call, as it will clear all the tags and this might break prefabs that rely on them.");
player.GetClientSimPlayer().tagData.ClearPlayerTags();
}
public static void SetPlayerTag(VRCPlayerApi player, string tagName, string tagValue)
{
player.GetClientSimPlayer().tagData.SetPlayerTag(tagName, tagValue);
}
public static string GetPlayerTag(VRCPlayerApi player, string tagName)
{
return player.GetClientSimPlayer().tagData.GetPlayerTag(tagName);
}
public static void ClearSilence(VRCPlayerApi player)
{
// TODO?
}
public static void SetSilencedToUntagged(VRCPlayerApi player, int number, string tagName, string tagValue)
{
// TODO?
}
public static void SetSilencedToTagged(VRCPlayerApi player, int number, string tagName, string tagValue)
{
// TODO?
}
#endregion
#region Player Audio
public static void SetAvatarAudioVolumetricRadius(VRCPlayerApi player, float value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioVolumetricRadius(value);
}
public static void SetAvatarAudioNearRadius(VRCPlayerApi player, float value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioNearRadius(value);
}
public static void SetAvatarAudioFarRadius(VRCPlayerApi player, float value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioFarRadius(value);
}
public static void SetAvatarAudioGain(VRCPlayerApi player, float value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioGain(value);
}
public static void SetAvatarAudioForceSpatial(VRCPlayerApi player, bool value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioForceSpatial(value);
}
public static void SetAvatarAudioCustomCurve(VRCPlayerApi player, bool value)
{
player.GetClientSimPlayer().audioData.SetAvatarAudioCustomCurve(value);
}
public static void SetVoiceGain(VRCPlayerApi player, float value) =>
player.GetClientSimPlayer().audioData.SetVoiceGain(value);
public static void SetVoiceDistanceNear(VRCPlayerApi player, float value) =>
player.GetClientSimPlayer().audioData.SetVoiceDistanceNear(value);
public static void SetVoiceDistanceFar(VRCPlayerApi player, float value) =>
player.GetClientSimPlayer().audioData.SetVoiceDistanceFar(value);
public static void SetVoiceVolumetricRadius(VRCPlayerApi player, float value) =>
player.GetClientSimPlayer().audioData.SetVoiceVolumetricRadius(value);
public static void SetVoiceLowpass(VRCPlayerApi player, bool value) =>
player.GetClientSimPlayer().audioData.SetVoiceLowpass(value);
public static float GetVoiceGain(VRCPlayerApi player) =>
player.GetClientSimPlayer().audioData.GetVoiceGain();
public static float GetVoiceDistanceNear(VRCPlayerApi player) =>
player.GetClientSimPlayer().audioData.GetVoiceDistanceNear();
public static float GetVoiceDistanceFar(VRCPlayerApi player) =>
player.GetClientSimPlayer().audioData.GetVoiceDistanceFar();
public static float GetVoiceVolumetricRadius(VRCPlayerApi player) =>
player.GetClientSimPlayer().audioData.GetVoiceVolumetricRadius();
public static bool GetVoiceLowpass(VRCPlayerApi player) =>
player.GetClientSimPlayer().audioData.GetVoiceLowpass();
public static string GetCurrentLanguage()
{
return ClientSimSettings.Instance.currentLanguage;
}
public static string[] GetAvailableLanguages()
{
return ClientSimSettings.Instance.availableLanguages;
}
#endregion
#region Player Scaling
public static bool GetManualAvatarScalingAllowed(VRCPlayerApi _) => _heightManager.GetManualAvatarScalingAllowed();
public static void SetManualAvatarScalingAllowed(VRCPlayerApi _, bool value) => _heightManager.SetManualAvatarScalingAllowed(value);
public static float GetAvatarEyeHeightMinimumAsMeters(VRCPlayerApi _) => _heightManager.GetAvatarEyeHeightMinimumAsMeters();
public static float GetAvatarEyeHeightMaximumAsMeters(VRCPlayerApi _) => _heightManager.GetAvatarEyeHeightMaximumAsMeters();
public static float GetAvatarEyeHeightAsMeters(VRCPlayerApi _) => _heightManager.GetAvatarEyeHeightAsMeters();
public static void SetAvatarEyeHeightMinimumByMeters(VRCPlayerApi _, float value) => _heightManager.SetAvatarEyeHeightMinimumByMeters(value);
public static void SetAvatarEyeHeightMaximumByMeters(VRCPlayerApi _, float value) => _heightManager.SetAvatarEyeHeightMaximumByMeters(value);
public static void SetAvatarEyeHeightByMeters(VRCPlayerApi _, float value) => _heightManager.SetAvatarEyeHeightByMeters(value);
public static void SetAvatarEyeHeightByMultiplier(VRCPlayerApi _, float value) => _heightManager.SetAvatarEyeHeightByMultiplier(value);
#endregion
}
}