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 { /// /// System that manages all the players currently in ClientSim. /// /// /// Sends Events: /// - ClientSimOnNewMasterEvent /// - ClientSimOnPlayerJoinedEvent /// - ClientSimOnPlayerLeftEvent /// public class ClientSimPlayerManager : IClientSimPlayerManager, IDisposable { private int _masterID = -1; private int _localPlayerID = -1; private int _nextPlayerID = 1; private readonly Dictionary _playerIDs = new Dictionary(); private readonly Dictionary _players = new Dictionary(); // List of players that have joined before ClientSim has finished initializing. private readonly List _waitingPlayers = new List(); 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 players = new List(_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(); 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(); 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(); foreach (IClientSimSyncable sync in syncs) { sync.SetOwner(player.playerId); } IClientSimSyncableHandler[] syncHandlers = obj.GetComponents(); 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(); if (player != null) { return player.Player; } return null; } public static List GetAllPlayersWithinRange(Vector3 pos, float radius, int limit = -1) { List resultList = new List(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 GetPlayersWithTag(string tagName, string tagValue) { List players = new List(); 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 } }